Файловый менеджер - Редактировать - /home/bean7936/perfect-community.com/442aa3/src.tar
Назад
Iri.php 0000644 00000071666 15174671662 0006037 0 ustar 00 <?php /** * IRI parser/serialiser/normaliser * * @package Requests\Utilities */ namespace WpOrg\Requests; use WpOrg\Requests\Exception; use WpOrg\Requests\Exception\InvalidArgument; use WpOrg\Requests\Ipv6; use WpOrg\Requests\Port; use WpOrg\Requests\Utility\InputValidator; /** * IRI parser/serialiser/normaliser * * Copyright (c) 2007-2010, Geoffrey Sneddon and Steve Minutillo. * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * * Neither the name of the SimplePie Team nor the names of its contributors * may be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. * * @package Requests\Utilities * @author Geoffrey Sneddon * @author Steve Minutillo * @copyright 2007-2009 Geoffrey Sneddon and Steve Minutillo * @license https://opensource.org/licenses/bsd-license.php * @link http://hg.gsnedders.com/iri/ * * @property string $iri IRI we're working with * @property-read string $uri IRI in URI form, {@see \WpOrg\Requests\Iri::to_uri()} * @property string $scheme Scheme part of the IRI * @property string $authority Authority part, formatted for a URI (userinfo + host + port) * @property string $iauthority Authority part of the IRI (userinfo + host + port) * @property string $userinfo Userinfo part, formatted for a URI (after '://' and before '@') * @property string $iuserinfo Userinfo part of the IRI (after '://' and before '@') * @property string $host Host part, formatted for a URI * @property string $ihost Host part of the IRI * @property string $port Port part of the IRI (after ':') * @property string $path Path part, formatted for a URI (after first '/') * @property string $ipath Path part of the IRI (after first '/') * @property string $query Query part, formatted for a URI (after '?') * @property string $iquery Query part of the IRI (after '?') * @property string $fragment Fragment, formatted for a URI (after '#') * @property string $ifragment Fragment part of the IRI (after '#') */ class Iri { /** * Scheme * * @var string|null */ protected $scheme = null; /** * User Information * * @var string|null */ protected $iuserinfo = null; /** * ihost * * @var string|null */ protected $ihost = null; /** * Port * * @var string|null */ protected $port = null; /** * ipath * * @var string */ protected $ipath = ''; /** * iquery * * @var string|null */ protected $iquery = null; /** * ifragment|null * * @var string */ protected $ifragment = null; /** * Normalization database * * Each key is the scheme, each value is an array with each key as the IRI * part and value as the default value for that part. * * @var array */ protected $normalization = array( 'acap' => array( 'port' => Port::ACAP, ), 'dict' => array( 'port' => Port::DICT, ), 'file' => array( 'ihost' => 'localhost', ), 'http' => array( 'port' => Port::HTTP, ), 'https' => array( 'port' => Port::HTTPS, ), ); /** * Return the entire IRI when you try and read the object as a string * * @return string */ public function __toString() { return $this->get_iri(); } /** * Overload __set() to provide access via properties * * @param string $name Property name * @param mixed $value Property value */ public function __set($name, $value) { if (method_exists($this, 'set_' . $name)) { call_user_func(array($this, 'set_' . $name), $value); } elseif ( $name === 'iauthority' || $name === 'iuserinfo' || $name === 'ihost' || $name === 'ipath' || $name === 'iquery' || $name === 'ifragment' ) { call_user_func(array($this, 'set_' . substr($name, 1)), $value); } } /** * Overload __get() to provide access via properties * * @param string $name Property name * @return mixed */ public function __get($name) { // isset() returns false for null, we don't want to do that // Also why we use array_key_exists below instead of isset() $props = get_object_vars($this); if ( $name === 'iri' || $name === 'uri' || $name === 'iauthority' || $name === 'authority' ) { $method = 'get_' . $name; $return = $this->$method(); } elseif (array_key_exists($name, $props)) { $return = $this->$name; } // host -> ihost elseif (($prop = 'i' . $name) && array_key_exists($prop, $props)) { $name = $prop; $return = $this->$prop; } // ischeme -> scheme elseif (($prop = substr($name, 1)) && array_key_exists($prop, $props)) { $name = $prop; $return = $this->$prop; } else { trigger_error('Undefined property: ' . get_class($this) . '::' . $name, E_USER_NOTICE); $return = null; } if ($return === null && isset($this->normalization[$this->scheme][$name])) { return $this->normalization[$this->scheme][$name]; } else { return $return; } } /** * Overload __isset() to provide access via properties * * @param string $name Property name * @return bool */ public function __isset($name) { return (method_exists($this, 'get_' . $name) || isset($this->$name)); } /** * Overload __unset() to provide access via properties * * @param string $name Property name */ public function __unset($name) { if (method_exists($this, 'set_' . $name)) { call_user_func(array($this, 'set_' . $name), ''); } } /** * Create a new IRI object, from a specified string * * @param string|Stringable|null $iri * * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $iri argument is not a string, Stringable or null. */ public function __construct($iri = null) { if ($iri !== null && InputValidator::is_string_or_stringable($iri) === false) { throw InvalidArgument::create(1, '$iri', 'string|Stringable|null', gettype($iri)); } $this->set_iri($iri); } /** * Create a new IRI object by resolving a relative IRI * * Returns false if $base is not absolute, otherwise an IRI. * * @param \WpOrg\Requests\Iri|string $base (Absolute) Base IRI * @param \WpOrg\Requests\Iri|string $relative Relative IRI * @return \WpOrg\Requests\Iri|false */ public static function absolutize($base, $relative) { if (!($relative instanceof self)) { $relative = new self($relative); } if (!$relative->is_valid()) { return false; } elseif ($relative->scheme !== null) { return clone $relative; } if (!($base instanceof self)) { $base = new self($base); } if ($base->scheme === null || !$base->is_valid()) { return false; } if ($relative->get_iri() !== '') { if ($relative->iuserinfo !== null || $relative->ihost !== null || $relative->port !== null) { $target = clone $relative; $target->scheme = $base->scheme; } else { $target = new self; $target->scheme = $base->scheme; $target->iuserinfo = $base->iuserinfo; $target->ihost = $base->ihost; $target->port = $base->port; if ($relative->ipath !== '') { if ($relative->ipath[0] === '/') { $target->ipath = $relative->ipath; } elseif (($base->iuserinfo !== null || $base->ihost !== null || $base->port !== null) && $base->ipath === '') { $target->ipath = '/' . $relative->ipath; } elseif (($last_segment = strrpos($base->ipath, '/')) !== false) { $target->ipath = substr($base->ipath, 0, $last_segment + 1) . $relative->ipath; } else { $target->ipath = $relative->ipath; } $target->ipath = $target->remove_dot_segments($target->ipath); $target->iquery = $relative->iquery; } else { $target->ipath = $base->ipath; if ($relative->iquery !== null) { $target->iquery = $relative->iquery; } elseif ($base->iquery !== null) { $target->iquery = $base->iquery; } } $target->ifragment = $relative->ifragment; } } else { $target = clone $base; $target->ifragment = null; } $target->scheme_normalization(); return $target; } /** * Parse an IRI into scheme/authority/path/query/fragment segments * * @param string $iri * @return array */ protected function parse_iri($iri) { $iri = trim($iri, "\x20\x09\x0A\x0C\x0D"); $has_match = preg_match('/^((?P<scheme>[^:\/?#]+):)?(\/\/(?P<authority>[^\/?#]*))?(?P<path>[^?#]*)(\?(?P<query>[^#]*))?(#(?P<fragment>.*))?$/', $iri, $match); if (!$has_match) { throw new Exception('Cannot parse supplied IRI', 'iri.cannot_parse', $iri); } if ($match[1] === '') { $match['scheme'] = null; } if (!isset($match[3]) || $match[3] === '') { $match['authority'] = null; } if (!isset($match[5])) { $match['path'] = ''; } if (!isset($match[6]) || $match[6] === '') { $match['query'] = null; } if (!isset($match[8]) || $match[8] === '') { $match['fragment'] = null; } return $match; } /** * Remove dot segments from a path * * @param string $input * @return string */ protected function remove_dot_segments($input) { $output = ''; while (strpos($input, './') !== false || strpos($input, '/.') !== false || $input === '.' || $input === '..') { // A: If the input buffer begins with a prefix of "../" or "./", // then remove that prefix from the input buffer; otherwise, if (strpos($input, '../') === 0) { $input = substr($input, 3); } elseif (strpos($input, './') === 0) { $input = substr($input, 2); } // B: if the input buffer begins with a prefix of "/./" or "/.", // where "." is a complete path segment, then replace that prefix // with "/" in the input buffer; otherwise, elseif (strpos($input, '/./') === 0) { $input = substr($input, 2); } elseif ($input === '/.') { $input = '/'; } // C: if the input buffer begins with a prefix of "/../" or "/..", // where ".." is a complete path segment, then replace that prefix // with "/" in the input buffer and remove the last segment and its // preceding "/" (if any) from the output buffer; otherwise, elseif (strpos($input, '/../') === 0) { $input = substr($input, 3); $output = substr_replace($output, '', (strrpos($output, '/') ?: 0)); } elseif ($input === '/..') { $input = '/'; $output = substr_replace($output, '', (strrpos($output, '/') ?: 0)); } // D: if the input buffer consists only of "." or "..", then remove // that from the input buffer; otherwise, elseif ($input === '.' || $input === '..') { $input = ''; } // E: move the first path segment in the input buffer to the end of // the output buffer, including the initial "/" character (if any) // and any subsequent characters up to, but not including, the next // "/" character or the end of the input buffer elseif (($pos = strpos($input, '/', 1)) !== false) { $output .= substr($input, 0, $pos); $input = substr_replace($input, '', 0, $pos); } else { $output .= $input; $input = ''; } } return $output . $input; } /** * Replace invalid character with percent encoding * * @param string $text Input string * @param string $extra_chars Valid characters not in iunreserved or * iprivate (this is ASCII-only) * @param bool $iprivate Allow iprivate * @return string */ protected function replace_invalid_with_pct_encoding($text, $extra_chars, $iprivate = false) { // Normalize as many pct-encoded sections as possible $text = preg_replace_callback('/(?:%[A-Fa-f0-9]{2})+/', array($this, 'remove_iunreserved_percent_encoded'), $text); // Replace invalid percent characters $text = preg_replace('/%(?![A-Fa-f0-9]{2})/', '%25', $text); // Add unreserved and % to $extra_chars (the latter is safe because all // pct-encoded sections are now valid). $extra_chars .= 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~%'; // Now replace any bytes that aren't allowed with their pct-encoded versions $position = 0; $strlen = strlen($text); while (($position += strspn($text, $extra_chars, $position)) < $strlen) { $value = ord($text[$position]); // Start position $start = $position; // By default we are valid $valid = true; // No one byte sequences are valid due to the while. // Two byte sequence: if (($value & 0xE0) === 0xC0) { $character = ($value & 0x1F) << 6; $length = 2; $remaining = 1; } // Three byte sequence: elseif (($value & 0xF0) === 0xE0) { $character = ($value & 0x0F) << 12; $length = 3; $remaining = 2; } // Four byte sequence: elseif (($value & 0xF8) === 0xF0) { $character = ($value & 0x07) << 18; $length = 4; $remaining = 3; } // Invalid byte: else { $valid = false; $length = 1; $remaining = 0; } if ($remaining) { if ($position + $length <= $strlen) { for ($position++; $remaining; $position++) { $value = ord($text[$position]); // Check that the byte is valid, then add it to the character: if (($value & 0xC0) === 0x80) { $character |= ($value & 0x3F) << (--$remaining * 6); } // If it is invalid, count the sequence as invalid and reprocess the current byte: else { $valid = false; $position--; break; } } } else { $position = $strlen - 1; $valid = false; } } // Percent encode anything invalid or not in ucschar if ( // Invalid sequences !$valid // Non-shortest form sequences are invalid || $length > 1 && $character <= 0x7F || $length > 2 && $character <= 0x7FF || $length > 3 && $character <= 0xFFFF // Outside of range of ucschar codepoints // Noncharacters || ($character & 0xFFFE) === 0xFFFE || $character >= 0xFDD0 && $character <= 0xFDEF || ( // Everything else not in ucschar $character > 0xD7FF && $character < 0xF900 || $character < 0xA0 || $character > 0xEFFFD ) && ( // Everything not in iprivate, if it applies !$iprivate || $character < 0xE000 || $character > 0x10FFFD ) ) { // If we were a character, pretend we weren't, but rather an error. if ($valid) { $position--; } for ($j = $start; $j <= $position; $j++) { $text = substr_replace($text, sprintf('%%%02X', ord($text[$j])), $j, 1); $j += 2; $position += 2; $strlen += 2; } } } return $text; } /** * Callback function for preg_replace_callback. * * Removes sequences of percent encoded bytes that represent UTF-8 * encoded characters in iunreserved * * @param array $regex_match PCRE match * @return string Replacement */ protected function remove_iunreserved_percent_encoded($regex_match) { // As we just have valid percent encoded sequences we can just explode // and ignore the first member of the returned array (an empty string). $bytes = explode('%', $regex_match[0]); // Initialize the new string (this is what will be returned) and that // there are no bytes remaining in the current sequence (unsurprising // at the first byte!). $string = ''; $remaining = 0; // Loop over each and every byte, and set $value to its value for ($i = 1, $len = count($bytes); $i < $len; $i++) { $value = hexdec($bytes[$i]); // If we're the first byte of sequence: if (!$remaining) { // Start position $start = $i; // By default we are valid $valid = true; // One byte sequence: if ($value <= 0x7F) { $character = $value; $length = 1; } // Two byte sequence: elseif (($value & 0xE0) === 0xC0) { $character = ($value & 0x1F) << 6; $length = 2; $remaining = 1; } // Three byte sequence: elseif (($value & 0xF0) === 0xE0) { $character = ($value & 0x0F) << 12; $length = 3; $remaining = 2; } // Four byte sequence: elseif (($value & 0xF8) === 0xF0) { $character = ($value & 0x07) << 18; $length = 4; $remaining = 3; } // Invalid byte: else { $valid = false; $remaining = 0; } } // Continuation byte: else { // Check that the byte is valid, then add it to the character: if (($value & 0xC0) === 0x80) { $remaining--; $character |= ($value & 0x3F) << ($remaining * 6); } // If it is invalid, count the sequence as invalid and reprocess the current byte as the start of a sequence: else { $valid = false; $remaining = 0; $i--; } } // If we've reached the end of the current byte sequence, append it to Unicode::$data if (!$remaining) { // Percent encode anything invalid or not in iunreserved if ( // Invalid sequences !$valid // Non-shortest form sequences are invalid || $length > 1 && $character <= 0x7F || $length > 2 && $character <= 0x7FF || $length > 3 && $character <= 0xFFFF // Outside of range of iunreserved codepoints || $character < 0x2D || $character > 0xEFFFD // Noncharacters || ($character & 0xFFFE) === 0xFFFE || $character >= 0xFDD0 && $character <= 0xFDEF // Everything else not in iunreserved (this is all BMP) || $character === 0x2F || $character > 0x39 && $character < 0x41 || $character > 0x5A && $character < 0x61 || $character > 0x7A && $character < 0x7E || $character > 0x7E && $character < 0xA0 || $character > 0xD7FF && $character < 0xF900 ) { for ($j = $start; $j <= $i; $j++) { $string .= '%' . strtoupper($bytes[$j]); } } else { for ($j = $start; $j <= $i; $j++) { $string .= chr(hexdec($bytes[$j])); } } } } // If we have any bytes left over they are invalid (i.e., we are // mid-way through a multi-byte sequence) if ($remaining) { for ($j = $start; $j < $len; $j++) { $string .= '%' . strtoupper($bytes[$j]); } } return $string; } protected function scheme_normalization() { if (isset($this->normalization[$this->scheme]['iuserinfo']) && $this->iuserinfo === $this->normalization[$this->scheme]['iuserinfo']) { $this->iuserinfo = null; } if (isset($this->normalization[$this->scheme]['ihost']) && $this->ihost === $this->normalization[$this->scheme]['ihost']) { $this->ihost = null; } if (isset($this->normalization[$this->scheme]['port']) && $this->port === $this->normalization[$this->scheme]['port']) { $this->port = null; } if (isset($this->normalization[$this->scheme]['ipath']) && $this->ipath === $this->normalization[$this->scheme]['ipath']) { $this->ipath = ''; } if (isset($this->ihost) && empty($this->ipath)) { $this->ipath = '/'; } if (isset($this->normalization[$this->scheme]['iquery']) && $this->iquery === $this->normalization[$this->scheme]['iquery']) { $this->iquery = null; } if (isset($this->normalization[$this->scheme]['ifragment']) && $this->ifragment === $this->normalization[$this->scheme]['ifragment']) { $this->ifragment = null; } } /** * Check if the object represents a valid IRI. This needs to be done on each * call as some things change depending on another part of the IRI. * * @return bool */ public function is_valid() { $isauthority = $this->iuserinfo !== null || $this->ihost !== null || $this->port !== null; if ($this->ipath !== '' && ( $isauthority && $this->ipath[0] !== '/' || ( $this->scheme === null && !$isauthority && strpos($this->ipath, ':') !== false && (strpos($this->ipath, '/') === false ? true : strpos($this->ipath, ':') < strpos($this->ipath, '/')) ) ) ) { return false; } return true; } public function __wakeup() { $class_props = get_class_vars( __CLASS__ ); $string_props = array( 'scheme', 'iuserinfo', 'ihost', 'port', 'ipath', 'iquery', 'ifragment' ); $array_props = array( 'normalization' ); foreach ( $class_props as $prop => $default_value ) { if ( in_array( $prop, $string_props, true ) && ! is_string( $this->$prop ) ) { throw new UnexpectedValueException(); } elseif ( in_array( $prop, $array_props, true ) && ! is_array( $this->$prop ) ) { throw new UnexpectedValueException(); } $this->$prop = null; } } /** * Set the entire IRI. Returns true on success, false on failure (if there * are any invalid characters). * * @param string $iri * @return bool */ protected function set_iri($iri) { static $cache; if (!$cache) { $cache = array(); } if ($iri === null) { return true; } $iri = (string) $iri; if (isset($cache[$iri])) { list($this->scheme, $this->iuserinfo, $this->ihost, $this->port, $this->ipath, $this->iquery, $this->ifragment, $return) = $cache[$iri]; return $return; } $parsed = $this->parse_iri($iri); $return = $this->set_scheme($parsed['scheme']) && $this->set_authority($parsed['authority']) && $this->set_path($parsed['path']) && $this->set_query($parsed['query']) && $this->set_fragment($parsed['fragment']); $cache[$iri] = array($this->scheme, $this->iuserinfo, $this->ihost, $this->port, $this->ipath, $this->iquery, $this->ifragment, $return); return $return; } /** * Set the scheme. Returns true on success, false on failure (if there are * any invalid characters). * * @param string $scheme * @return bool */ protected function set_scheme($scheme) { if ($scheme === null) { $this->scheme = null; } elseif (!preg_match('/^[A-Za-z][0-9A-Za-z+\-.]*$/', $scheme)) { $this->scheme = null; return false; } else { $this->scheme = strtolower($scheme); } return true; } /** * Set the authority. Returns true on success, false on failure (if there are * any invalid characters). * * @param string $authority * @return bool */ protected function set_authority($authority) { static $cache; if (!$cache) { $cache = array(); } if ($authority === null) { $this->iuserinfo = null; $this->ihost = null; $this->port = null; return true; } if (isset($cache[$authority])) { list($this->iuserinfo, $this->ihost, $this->port, $return) = $cache[$authority]; return $return; } $remaining = $authority; if (($iuserinfo_end = strrpos($remaining, '@')) !== false) { $iuserinfo = substr($remaining, 0, $iuserinfo_end); $remaining = substr($remaining, $iuserinfo_end + 1); } else { $iuserinfo = null; } if (($port_start = strpos($remaining, ':', (strpos($remaining, ']') ?: 0))) !== false) { $port = substr($remaining, $port_start + 1); if ($port === false || $port === '') { $port = null; } $remaining = substr($remaining, 0, $port_start); } else { $port = null; } $return = $this->set_userinfo($iuserinfo) && $this->set_host($remaining) && $this->set_port($port); $cache[$authority] = array($this->iuserinfo, $this->ihost, $this->port, $return); return $return; } /** * Set the iuserinfo. * * @param string $iuserinfo * @return bool */ protected function set_userinfo($iuserinfo) { if ($iuserinfo === null) { $this->iuserinfo = null; } else { $this->iuserinfo = $this->replace_invalid_with_pct_encoding($iuserinfo, '!$&\'()*+,;=:'); $this->scheme_normalization(); } return true; } /** * Set the ihost. Returns true on success, false on failure (if there are * any invalid characters). * * @param string $ihost * @return bool */ protected function set_host($ihost) { if ($ihost === null) { $this->ihost = null; return true; } if (substr($ihost, 0, 1) === '[' && substr($ihost, -1) === ']') { if (Ipv6::check_ipv6(substr($ihost, 1, -1))) { $this->ihost = '[' . Ipv6::compress(substr($ihost, 1, -1)) . ']'; } else { $this->ihost = null; return false; } } else { $ihost = $this->replace_invalid_with_pct_encoding($ihost, '!$&\'()*+,;='); // Lowercase, but ignore pct-encoded sections (as they should // remain uppercase). This must be done after the previous step // as that can add unescaped characters. $position = 0; $strlen = strlen($ihost); while (($position += strcspn($ihost, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ%', $position)) < $strlen) { if ($ihost[$position] === '%') { $position += 3; } else { $ihost[$position] = strtolower($ihost[$position]); $position++; } } $this->ihost = $ihost; } $this->scheme_normalization(); return true; } /** * Set the port. Returns true on success, false on failure (if there are * any invalid characters). * * @param string $port * @return bool */ protected function set_port($port) { if ($port === null) { $this->port = null; return true; } if (strspn($port, '0123456789') === strlen($port)) { $this->port = (int) $port; $this->scheme_normalization(); return true; } $this->port = null; return false; } /** * Set the ipath. * * @param string $ipath * @return bool */ protected function set_path($ipath) { static $cache; if (!$cache) { $cache = array(); } $ipath = (string) $ipath; if (isset($cache[$ipath])) { $this->ipath = $cache[$ipath][(int) ($this->scheme !== null)]; } else { $valid = $this->replace_invalid_with_pct_encoding($ipath, '!$&\'()*+,;=@:/'); $removed = $this->remove_dot_segments($valid); $cache[$ipath] = array($valid, $removed); $this->ipath = ($this->scheme !== null) ? $removed : $valid; } $this->scheme_normalization(); return true; } /** * Set the iquery. * * @param string $iquery * @return bool */ protected function set_query($iquery) { if ($iquery === null) { $this->iquery = null; } else { $this->iquery = $this->replace_invalid_with_pct_encoding($iquery, '!$&\'()*+,;=:@/?', true); $this->scheme_normalization(); } return true; } /** * Set the ifragment. * * @param string $ifragment * @return bool */ protected function set_fragment($ifragment) { if ($ifragment === null) { $this->ifragment = null; } else { $this->ifragment = $this->replace_invalid_with_pct_encoding($ifragment, '!$&\'()*+,;=:@/?'); $this->scheme_normalization(); } return true; } /** * Convert an IRI to a URI (or parts thereof) * * @param string|bool $iri IRI to convert (or false from {@see \WpOrg\Requests\Iri::get_iri()}) * @return string|false URI if IRI is valid, false otherwise. */ protected function to_uri($iri) { if (!is_string($iri)) { return false; } static $non_ascii; if (!$non_ascii) { $non_ascii = implode('', range("\x80", "\xFF")); } $position = 0; $strlen = strlen($iri); while (($position += strcspn($iri, $non_ascii, $position)) < $strlen) { $iri = substr_replace($iri, sprintf('%%%02X', ord($iri[$position])), $position, 1); $position += 3; $strlen += 2; } return $iri; } /** * Get the complete IRI * * @return string|false */ protected function get_iri() { if (!$this->is_valid()) { return false; } $iri = ''; if ($this->scheme !== null) { $iri .= $this->scheme . ':'; } if (($iauthority = $this->get_iauthority()) !== null) { $iri .= '//' . $iauthority; } $iri .= $this->ipath; if ($this->iquery !== null) { $iri .= '?' . $this->iquery; } if ($this->ifragment !== null) { $iri .= '#' . $this->ifragment; } return $iri; } /** * Get the complete URI * * @return string */ protected function get_uri() { return $this->to_uri($this->get_iri()); } /** * Get the complete iauthority * * @return string|null */ protected function get_iauthority() { if ($this->iuserinfo === null && $this->ihost === null && $this->port === null) { return null; } $iauthority = ''; if ($this->iuserinfo !== null) { $iauthority .= $this->iuserinfo . '@'; } if ($this->ihost !== null) { $iauthority .= $this->ihost; } if ($this->port !== null) { $iauthority .= ':' . $this->port; } return $iauthority; } /** * Get the complete authority * * @return string */ protected function get_authority() { $iauthority = $this->get_iauthority(); if (is_string($iauthority)) { return $this->to_uri($iauthority); } else { return $iauthority; } } } Session.php 0000644 00000021623 15174671662 0006723 0 ustar 00 <?php /** * Session handler for persistent requests and default parameters * * @package Requests\SessionHandler */ namespace WpOrg\Requests; use WpOrg\Requests\Cookie\Jar; use WpOrg\Requests\Exception\InvalidArgument; use WpOrg\Requests\Iri; use WpOrg\Requests\Requests; use WpOrg\Requests\Utility\InputValidator; /** * Session handler for persistent requests and default parameters * * Allows various options to be set as default values, and merges both the * options and URL properties together. A base URL can be set for all requests, * with all subrequests resolved from this. Base options can be set (including * a shared cookie jar), then overridden for individual requests. * * @package Requests\SessionHandler */ class Session { /** * Base URL for requests * * URLs will be made absolute using this as the base * * @var string|null */ public $url = null; /** * Base headers for requests * * @var array */ public $headers = []; /** * Base data for requests * * If both the base data and the per-request data are arrays, the data will * be merged before sending the request. * * @var array */ public $data = []; /** * Base options for requests * * The base options are merged with the per-request data for each request. * The only default option is a shared cookie jar between requests. * * Values here can also be set directly via properties on the Session * object, e.g. `$session->useragent = 'X';` * * @var array */ public $options = []; /** * Create a new session * * @param string|Stringable|null $url Base URL for requests * @param array $headers Default headers for requests * @param array $data Default data for requests * @param array $options Default options for requests * * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $url argument is not a string, Stringable or null. * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $headers argument is not an array. * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $data argument is not an array. * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $options argument is not an array. */ public function __construct($url = null, $headers = [], $data = [], $options = []) { if ($url !== null && InputValidator::is_string_or_stringable($url) === false) { throw InvalidArgument::create(1, '$url', 'string|Stringable|null', gettype($url)); } if (is_array($headers) === false) { throw InvalidArgument::create(2, '$headers', 'array', gettype($headers)); } if (is_array($data) === false) { throw InvalidArgument::create(3, '$data', 'array', gettype($data)); } if (is_array($options) === false) { throw InvalidArgument::create(4, '$options', 'array', gettype($options)); } $this->url = $url; $this->headers = $headers; $this->data = $data; $this->options = $options; if (empty($this->options['cookies'])) { $this->options['cookies'] = new Jar(); } } /** * Get a property's value * * @param string $name Property name. * @return mixed|null Property value, null if none found */ public function __get($name) { if (isset($this->options[$name])) { return $this->options[$name]; } return null; } /** * Set a property's value * * @param string $name Property name. * @param mixed $value Property value */ public function __set($name, $value) { $this->options[$name] = $value; } /** * Remove a property's value * * @param string $name Property name. */ public function __isset($name) { return isset($this->options[$name]); } /** * Remove a property's value * * @param string $name Property name. */ public function __unset($name) { unset($this->options[$name]); } /**#@+ * @see \WpOrg\Requests\Session::request() * @param string $url * @param array $headers * @param array $options * @return \WpOrg\Requests\Response */ /** * Send a GET request */ public function get($url, $headers = [], $options = []) { return $this->request($url, $headers, null, Requests::GET, $options); } /** * Send a HEAD request */ public function head($url, $headers = [], $options = []) { return $this->request($url, $headers, null, Requests::HEAD, $options); } /** * Send a DELETE request */ public function delete($url, $headers = [], $options = []) { return $this->request($url, $headers, null, Requests::DELETE, $options); } /**#@-*/ /**#@+ * @see \WpOrg\Requests\Session::request() * @param string $url * @param array $headers * @param array $data * @param array $options * @return \WpOrg\Requests\Response */ /** * Send a POST request */ public function post($url, $headers = [], $data = [], $options = []) { return $this->request($url, $headers, $data, Requests::POST, $options); } /** * Send a PUT request */ public function put($url, $headers = [], $data = [], $options = []) { return $this->request($url, $headers, $data, Requests::PUT, $options); } /** * Send a PATCH request * * Note: Unlike {@see \WpOrg\Requests\Session::post()} and {@see \WpOrg\Requests\Session::put()}, * `$headers` is required, as the specification recommends that should send an ETag * * @link https://tools.ietf.org/html/rfc5789 */ public function patch($url, $headers, $data = [], $options = []) { return $this->request($url, $headers, $data, Requests::PATCH, $options); } /**#@-*/ /** * Main interface for HTTP requests * * This method initiates a request and sends it via a transport before * parsing. * * @see \WpOrg\Requests\Requests::request() * * @param string $url URL to request * @param array $headers Extra headers to send with the request * @param array|null $data Data to send either as a query string for GET/HEAD requests, or in the body for POST requests * @param string $type HTTP request type (use \WpOrg\Requests\Requests constants) * @param array $options Options for the request (see {@see \WpOrg\Requests\Requests::request()}) * @return \WpOrg\Requests\Response * * @throws \WpOrg\Requests\Exception On invalid URLs (`nonhttp`) */ public function request($url, $headers = [], $data = [], $type = Requests::GET, $options = []) { $request = $this->merge_request(compact('url', 'headers', 'data', 'options')); return Requests::request($request['url'], $request['headers'], $request['data'], $type, $request['options']); } /** * Send multiple HTTP requests simultaneously * * @see \WpOrg\Requests\Requests::request_multiple() * * @param array $requests Requests data (see {@see \WpOrg\Requests\Requests::request_multiple()}) * @param array $options Global and default options (see {@see \WpOrg\Requests\Requests::request()}) * @return array Responses (either \WpOrg\Requests\Response or a \WpOrg\Requests\Exception object) * * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $requests argument is not an array or iterable object with array access. * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $options argument is not an array. */ public function request_multiple($requests, $options = []) { if (InputValidator::has_array_access($requests) === false || InputValidator::is_iterable($requests) === false) { throw InvalidArgument::create(1, '$requests', 'array|ArrayAccess&Traversable', gettype($requests)); } if (is_array($options) === false) { throw InvalidArgument::create(2, '$options', 'array', gettype($options)); } foreach ($requests as $key => $request) { $requests[$key] = $this->merge_request($request, false); } $options = array_merge($this->options, $options); // Disallow forcing the type, as that's a per request setting unset($options['type']); return Requests::request_multiple($requests, $options); } public function __wakeup() { throw new \LogicException( __CLASS__ . ' should never be unserialized' ); } /** * Merge a request's data with the default data * * @param array $request Request data (same form as {@see \WpOrg\Requests\Session::request_multiple()}) * @param boolean $merge_options Should we merge options as well? * @return array Request data */ protected function merge_request($request, $merge_options = true) { if ($this->url !== null) { $request['url'] = Iri::absolutize($this->url, $request['url']); $request['url'] = $request['url']->uri; } if (empty($request['headers'])) { $request['headers'] = []; } $request['headers'] = array_merge($this->headers, $request['headers']); if (empty($request['data'])) { if (is_array($this->data)) { $request['data'] = $this->data; } } elseif (is_array($request['data']) && is_array($this->data)) { $request['data'] = array_merge($this->data, $request['data']); } if ($merge_options === true) { $request['options'] = array_merge($this->options, $request['options']); // Disallow forcing the type, as that's a per request setting unset($request['options']['type']); } return $request; } } Hooks.php 0000644 00000005730 15174671662 0006364 0 ustar 00 <?php /** * Handles adding and dispatching events * * @package Requests\EventDispatcher */ namespace WpOrg\Requests; use WpOrg\Requests\Exception\InvalidArgument; use WpOrg\Requests\HookManager; use WpOrg\Requests\Utility\InputValidator; /** * Handles adding and dispatching events * * @package Requests\EventDispatcher */ class Hooks implements HookManager { /** * Registered callbacks for each hook * * @var array */ protected $hooks = []; /** * Register a callback for a hook * * @param string $hook Hook name * @param callable $callback Function/method to call on event * @param int $priority Priority number. <0 is executed earlier, >0 is executed later * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $hook argument is not a string. * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $callback argument is not callable. * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $priority argument is not an integer. */ public function register($hook, $callback, $priority = 0) { if (is_string($hook) === false) { throw InvalidArgument::create(1, '$hook', 'string', gettype($hook)); } if (is_callable($callback) === false) { throw InvalidArgument::create(2, '$callback', 'callable', gettype($callback)); } if (InputValidator::is_numeric_array_key($priority) === false) { throw InvalidArgument::create(3, '$priority', 'integer', gettype($priority)); } if (!isset($this->hooks[$hook])) { $this->hooks[$hook] = [ $priority => [], ]; } elseif (!isset($this->hooks[$hook][$priority])) { $this->hooks[$hook][$priority] = []; } $this->hooks[$hook][$priority][] = $callback; } /** * Dispatch a message * * @param string $hook Hook name * @param array $parameters Parameters to pass to callbacks * @return boolean Successfulness * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $hook argument is not a string. * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $parameters argument is not an array. */ public function dispatch($hook, $parameters = []) { if (is_string($hook) === false) { throw InvalidArgument::create(1, '$hook', 'string', gettype($hook)); } // Check strictly against array, as Array* objects don't work in combination with `call_user_func_array()`. if (is_array($parameters) === false) { throw InvalidArgument::create(2, '$parameters', 'array', gettype($parameters)); } if (empty($this->hooks[$hook])) { return false; } if (!empty($parameters)) { // Strip potential keys from the array to prevent them being interpreted as parameter names in PHP 8.0. $parameters = array_values($parameters); } ksort($this->hooks[$hook]); foreach ($this->hooks[$hook] as $priority => $hooked) { foreach ($hooked as $callback) { $callback(...$parameters); } } return true; } public function __wakeup() { throw new \LogicException( __CLASS__ . ' should never be unserialized' ); } } Proxy.php 0000644 00000001543 15174671662 0006420 0 ustar 00 <?php /** * Proxy connection interface * * @package Requests\Proxy * @since 1.6 */ namespace WpOrg\Requests; use WpOrg\Requests\Hooks; /** * Proxy connection interface * * Implement this interface to handle proxy settings and authentication * * Parameters should be passed via the constructor where possible, as this * makes it much easier for users to use your provider. * * @see \WpOrg\Requests\Hooks * * @package Requests\Proxy * @since 1.6 */ interface Proxy { /** * Register hooks as needed * * This method is called in {@see \WpOrg\Requests\Requests::request()} when the user * has set an instance as the 'auth' option. Use this callback to register all the * hooks you'll need. * * @see \WpOrg\Requests\Hooks::register() * @param \WpOrg\Requests\Hooks $hooks Hook system */ public function register(Hooks $hooks); } Exception/Transport.php 0000644 00000000364 15174671662 0011231 0 ustar 00 <?php /** * Transport Exception * * @package Requests\Exceptions */ namespace WpOrg\Requests\Exception; use WpOrg\Requests\Exception; /** * Transport Exception * * @package Requests\Exceptions */ class Transport extends Exception {} Exception/Http.php 0000644 00000003006 15174671662 0010150 0 ustar 00 <?php /** * Exception based on HTTP response * * @package Requests\Exceptions */ namespace WpOrg\Requests\Exception; use WpOrg\Requests\Exception; use WpOrg\Requests\Exception\Http\StatusUnknown; /** * Exception based on HTTP response * * @package Requests\Exceptions */ class Http extends Exception { /** * HTTP status code * * @var integer */ protected $code = 0; /** * Reason phrase * * @var string */ protected $reason = 'Unknown'; /** * Create a new exception * * There is no mechanism to pass in the status code, as this is set by the * subclass used. Reason phrases can vary, however. * * @param string|null $reason Reason phrase * @param mixed $data Associated data */ public function __construct($reason = null, $data = null) { if ($reason !== null) { $this->reason = $reason; } $message = sprintf('%d %s', $this->code, $this->reason); parent::__construct($message, 'httpresponse', $data, $this->code); } /** * Get the status message. * * @return string */ public function getReason() { return $this->reason; } /** * Get the correct exception class for a given error code * * @param int|bool $code HTTP status code, or false if unavailable * @return string Exception class name to use */ public static function get_class($code) { if (!$code) { return StatusUnknown::class; } $class = sprintf('\WpOrg\Requests\Exception\Http\Status%d', $code); if (class_exists($class)) { return $class; } return StatusUnknown::class; } } Exception/ArgumentCount.php 0000644 00000002664 15174671662 0012035 0 ustar 00 <?php namespace WpOrg\Requests\Exception; use WpOrg\Requests\Exception; /** * Exception for when an incorrect number of arguments are passed to a method. * * Typically, this exception is used when all arguments for a method are optional, * but certain arguments need to be passed together, i.e. a method which can be called * with no arguments or with two arguments, but not with one argument. * * Along the same lines, this exception is also used if a method expects an array * with a certain number of elements and the provided number of elements does not comply. * * @package Requests\Exceptions * @since 2.0.0 */ final class ArgumentCount extends Exception { /** * Create a new argument count exception with a standardized text. * * @param string $expected The argument count expected as a phrase. * For example: `at least 2 arguments` or `exactly 1 argument`. * @param int $received The actual argument count received. * @param string $type Exception type. * * @return \WpOrg\Requests\Exception\ArgumentCount */ public static function create($expected, $received, $type) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_debug_backtrace $stack = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2); return new self( sprintf( '%s::%s() expects %s, %d given', $stack[1]['class'], $stack[1]['function'], $expected, $received ), $type ); } } Exception/Http/StatusUnknown.php 0000644 00000001712 15174671662 0013015 0 ustar 00 <?php /** * Exception for unknown status responses * * @package Requests\Exceptions */ namespace WpOrg\Requests\Exception\Http; use WpOrg\Requests\Exception\Http; use WpOrg\Requests\Response; /** * Exception for unknown status responses * * @package Requests\Exceptions */ final class StatusUnknown extends Http { /** * HTTP status code * * @var integer|bool Code if available, false if an error occurred */ protected $code = 0; /** * Reason phrase * * @var string */ protected $reason = 'Unknown'; /** * Create a new exception * * If `$data` is an instance of {@see \WpOrg\Requests\Response}, uses the status * code from it. Otherwise, sets as 0 * * @param string|null $reason Reason phrase * @param mixed $data Associated data */ public function __construct($reason = null, $data = null) { if ($data instanceof Response) { $this->code = (int) $data->status_code; } parent::__construct($reason, $data); } } Exception/Http/Status502.php 0000644 00000000711 15174671662 0011662 0 ustar 00 <?php /** * Exception for 502 Bad Gateway responses * * @package Requests\Exceptions */ namespace WpOrg\Requests\Exception\Http; use WpOrg\Requests\Exception\Http; /** * Exception for 502 Bad Gateway responses * * @package Requests\Exceptions */ final class Status502 extends Http { /** * HTTP status code * * @var integer */ protected $code = 502; /** * Reason phrase * * @var string */ protected $reason = 'Bad Gateway'; } Exception/Http/Status504.php 0000644 00000000725 15174671662 0011671 0 ustar 00 <?php /** * Exception for 504 Gateway Timeout responses * * @package Requests\Exceptions */ namespace WpOrg\Requests\Exception\Http; use WpOrg\Requests\Exception\Http; /** * Exception for 504 Gateway Timeout responses * * @package Requests\Exceptions */ final class Status504 extends Http { /** * HTTP status code * * @var integer */ protected $code = 504; /** * Reason phrase * * @var string */ protected $reason = 'Gateway Timeout'; } Exception/Http/Status418.php 0000644 00000001054 15174671662 0011671 0 ustar 00 <?php /** * Exception for 418 I'm A Teapot responses * * @link https://tools.ietf.org/html/rfc2324 * * @package Requests\Exceptions */ namespace WpOrg\Requests\Exception\Http; use WpOrg\Requests\Exception\Http; /** * Exception for 418 I'm A Teapot responses * * @link https://tools.ietf.org/html/rfc2324 * * @package Requests\Exceptions */ final class Status418 extends Http { /** * HTTP status code * * @var integer */ protected $code = 418; /** * Reason phrase * * @var string */ protected $reason = "I'm A Teapot"; } Exception/Http/Status503.php 0000644 00000000741 15174671662 0011666 0 ustar 00 <?php /** * Exception for 503 Service Unavailable responses * * @package Requests\Exceptions */ namespace WpOrg\Requests\Exception\Http; use WpOrg\Requests\Exception\Http; /** * Exception for 503 Service Unavailable responses * * @package Requests\Exceptions */ final class Status503 extends Http { /** * HTTP status code * * @var integer */ protected $code = 503; /** * Reason phrase * * @var string */ protected $reason = 'Service Unavailable'; } Exception/Http/Status408.php 0000644 00000000725 15174671662 0011674 0 ustar 00 <?php /** * Exception for 408 Request Timeout responses * * @package Requests\Exceptions */ namespace WpOrg\Requests\Exception\Http; use WpOrg\Requests\Exception\Http; /** * Exception for 408 Request Timeout responses * * @package Requests\Exceptions */ final class Status408 extends Http { /** * HTTP status code * * @var integer */ protected $code = 408; /** * Reason phrase * * @var string */ protected $reason = 'Request Timeout'; } Exception/Http/Status403.php 0000644 00000000703 15174671662 0011663 0 ustar 00 <?php /** * Exception for 403 Forbidden responses * * @package Requests\Exceptions */ namespace WpOrg\Requests\Exception\Http; use WpOrg\Requests\Exception\Http; /** * Exception for 403 Forbidden responses * * @package Requests\Exceptions */ final class Status403 extends Http { /** * HTTP status code * * @var integer */ protected $code = 403; /** * Reason phrase * * @var string */ protected $reason = 'Forbidden'; } Exception/Http/Status416.php 0000644 00000001005 15174671662 0011663 0 ustar 00 <?php /** * Exception for 416 Requested Range Not Satisfiable responses * * @package Requests\Exceptions */ namespace WpOrg\Requests\Exception\Http; use WpOrg\Requests\Exception\Http; /** * Exception for 416 Requested Range Not Satisfiable responses * * @package Requests\Exceptions */ final class Status416 extends Http { /** * HTTP status code * * @var integer */ protected $code = 416; /** * Reason phrase * * @var string */ protected $reason = 'Requested Range Not Satisfiable'; } Exception/Http/Status406.php 0000644 00000000722 15174671662 0011667 0 ustar 00 <?php /** * Exception for 406 Not Acceptable responses * * @package Requests\Exceptions */ namespace WpOrg\Requests\Exception\Http; use WpOrg\Requests\Exception\Http; /** * Exception for 406 Not Acceptable responses * * @package Requests\Exceptions */ final class Status406 extends Http { /** * HTTP status code * * @var integer */ protected $code = 406; /** * Reason phrase * * @var string */ protected $reason = 'Not Acceptable'; } Exception/Http/Status501.php 0000644 00000000725 15174671662 0011666 0 ustar 00 <?php /** * Exception for 501 Not Implemented responses * * @package Requests\Exceptions */ namespace WpOrg\Requests\Exception\Http; use WpOrg\Requests\Exception\Http; /** * Exception for 501 Not Implemented responses * * @package Requests\Exceptions */ final class Status501 extends Http { /** * HTTP status code * * @var integer */ protected $code = 501; /** * Reason phrase * * @var string */ protected $reason = 'Not Implemented'; } Exception/Http/Status304.php 0000644 00000000714 15174671662 0011665 0 ustar 00 <?php /** * Exception for 304 Not Modified responses * * @package Requests\Exceptions */ namespace WpOrg\Requests\Exception\Http; use WpOrg\Requests\Exception\Http; /** * Exception for 304 Not Modified responses * * @package Requests\Exceptions */ final class Status304 extends Http { /** * HTTP status code * * @var integer */ protected $code = 304; /** * Reason phrase * * @var string */ protected $reason = 'Not Modified'; } Exception/Http/Status412.php 0000644 00000000741 15174671662 0011665 0 ustar 00 <?php /** * Exception for 412 Precondition Failed responses * * @package Requests\Exceptions */ namespace WpOrg\Requests\Exception\Http; use WpOrg\Requests\Exception\Http; /** * Exception for 412 Precondition Failed responses * * @package Requests\Exceptions */ final class Status412 extends Http { /** * HTTP status code * * @var integer */ protected $code = 412; /** * Reason phrase * * @var string */ protected $reason = 'Precondition Failed'; } Exception/Http/Status429.php 0000644 00000001163 15174671662 0011674 0 ustar 00 <?php /** * Exception for 429 Too Many Requests responses * * @link https://tools.ietf.org/html/draft-nottingham-http-new-status-04 * * @package Requests\Exceptions */ namespace WpOrg\Requests\Exception\Http; use WpOrg\Requests\Exception\Http; /** * Exception for 429 Too Many Requests responses * * @link https://tools.ietf.org/html/draft-nottingham-http-new-status-04 * * @package Requests\Exceptions */ final class Status429 extends Http { /** * HTTP status code * * @var integer */ protected $code = 429; /** * Reason phrase * * @var string */ protected $reason = 'Too Many Requests'; } Exception/Http/Status405.php 0000644 00000000736 15174671662 0011673 0 ustar 00 <?php /** * Exception for 405 Method Not Allowed responses * * @package Requests\Exceptions */ namespace WpOrg\Requests\Exception\Http; use WpOrg\Requests\Exception\Http; /** * Exception for 405 Method Not Allowed responses * * @package Requests\Exceptions */ final class Status405 extends Http { /** * HTTP status code * * @var integer */ protected $code = 405; /** * Reason phrase * * @var string */ protected $reason = 'Method Not Allowed'; } Exception/Http/Status407.php 0000644 00000000777 15174671662 0011702 0 ustar 00 <?php /** * Exception for 407 Proxy Authentication Required responses * * @package Requests\Exceptions */ namespace WpOrg\Requests\Exception\Http; use WpOrg\Requests\Exception\Http; /** * Exception for 407 Proxy Authentication Required responses * * @package Requests\Exceptions */ final class Status407 extends Http { /** * HTTP status code * * @var integer */ protected $code = 407; /** * Reason phrase * * @var string */ protected $reason = 'Proxy Authentication Required'; } Exception/Http/Status414.php 0000644 00000000747 15174671662 0011675 0 ustar 00 <?php /** * Exception for 414 Request-URI Too Large responses * * @package Requests\Exceptions */ namespace WpOrg\Requests\Exception\Http; use WpOrg\Requests\Exception\Http; /** * Exception for 414 Request-URI Too Large responses * * @package Requests\Exceptions */ final class Status414 extends Http { /** * HTTP status code * * @var integer */ protected $code = 414; /** * Reason phrase * * @var string */ protected $reason = 'Request-URI Too Large'; } Exception/Http/Status404.php 0000644 00000000703 15174671662 0011664 0 ustar 00 <?php /** * Exception for 404 Not Found responses * * @package Requests\Exceptions */ namespace WpOrg\Requests\Exception\Http; use WpOrg\Requests\Exception\Http; /** * Exception for 404 Not Found responses * * @package Requests\Exceptions */ final class Status404 extends Http { /** * HTTP status code * * @var integer */ protected $code = 404; /** * Reason phrase * * @var string */ protected $reason = 'Not Found'; } Exception/Http/Status409.php 0000644 00000000700 15174671662 0011666 0 ustar 00 <?php /** * Exception for 409 Conflict responses * * @package Requests\Exceptions */ namespace WpOrg\Requests\Exception\Http; use WpOrg\Requests\Exception\Http; /** * Exception for 409 Conflict responses * * @package Requests\Exceptions */ final class Status409 extends Http { /** * HTTP status code * * @var integer */ protected $code = 409; /** * Reason phrase * * @var string */ protected $reason = 'Conflict'; } Exception/Http/Status411.php 0000644 00000000725 15174671662 0011666 0 ustar 00 <?php /** * Exception for 411 Length Required responses * * @package Requests\Exceptions */ namespace WpOrg\Requests\Exception\Http; use WpOrg\Requests\Exception\Http; /** * Exception for 411 Length Required responses * * @package Requests\Exceptions */ final class Status411 extends Http { /** * HTTP status code * * @var integer */ protected $code = 411; /** * Reason phrase * * @var string */ protected $reason = 'Length Required'; } Exception/Http/Status413.php 0000644 00000000760 15174671662 0011667 0 ustar 00 <?php /** * Exception for 413 Request Entity Too Large responses * * @package Requests\Exceptions */ namespace WpOrg\Requests\Exception\Http; use WpOrg\Requests\Exception\Http; /** * Exception for 413 Request Entity Too Large responses * * @package Requests\Exceptions */ final class Status413 extends Http { /** * HTTP status code * * @var integer */ protected $code = 413; /** * Reason phrase * * @var string */ protected $reason = 'Request Entity Too Large'; } Exception/Http/Status511.php 0000644 00000001145 15174671662 0011664 0 ustar 00 <?php /** * Exception for 511 Network Authentication Required responses * * @link https://tools.ietf.org/html/rfc6585 * * @package Requests\Exceptions */ namespace WpOrg\Requests\Exception\Http; use WpOrg\Requests\Exception\Http; /** * Exception for 511 Network Authentication Required responses * * @link https://tools.ietf.org/html/rfc6585 * * @package Requests\Exceptions */ final class Status511 extends Http { /** * HTTP status code * * @var integer */ protected $code = 511; /** * Reason phrase * * @var string */ protected $reason = 'Network Authentication Required'; } Exception/Http/Status402.php 0000644 00000000730 15174671662 0011662 0 ustar 00 <?php /** * Exception for 402 Payment Required responses * * @package Requests\Exceptions */ namespace WpOrg\Requests\Exception\Http; use WpOrg\Requests\Exception\Http; /** * Exception for 402 Payment Required responses * * @package Requests\Exceptions */ final class Status402 extends Http { /** * HTTP status code * * @var integer */ protected $code = 402; /** * Reason phrase * * @var string */ protected $reason = 'Payment Required'; } Exception/Http/Status400.php 0000644 00000000711 15174671662 0011657 0 ustar 00 <?php /** * Exception for 400 Bad Request responses * * @package Requests\Exceptions */ namespace WpOrg\Requests\Exception\Http; use WpOrg\Requests\Exception\Http; /** * Exception for 400 Bad Request responses * * @package Requests\Exceptions */ final class Status400 extends Http { /** * HTTP status code * * @var integer */ protected $code = 400; /** * Reason phrase * * @var string */ protected $reason = 'Bad Request'; } Exception/Http/Status417.php 0000644 00000000736 15174671662 0011676 0 ustar 00 <?php /** * Exception for 417 Expectation Failed responses * * @package Requests\Exceptions */ namespace WpOrg\Requests\Exception\Http; use WpOrg\Requests\Exception\Http; /** * Exception for 417 Expectation Failed responses * * @package Requests\Exceptions */ final class Status417 extends Http { /** * HTTP status code * * @var integer */ protected $code = 417; /** * Reason phrase * * @var string */ protected $reason = 'Expectation Failed'; } Exception/Http/Status306.php 0000644 00000000714 15174671662 0011667 0 ustar 00 <?php /** * Exception for 306 Switch Proxy responses * * @package Requests\Exceptions */ namespace WpOrg\Requests\Exception\Http; use WpOrg\Requests\Exception\Http; /** * Exception for 306 Switch Proxy responses * * @package Requests\Exceptions */ final class Status306 extends Http { /** * HTTP status code * * @var integer */ protected $code = 306; /** * Reason phrase * * @var string */ protected $reason = 'Switch Proxy'; } Exception/Http/Status305.php 0000644 00000000703 15174671662 0011664 0 ustar 00 <?php /** * Exception for 305 Use Proxy responses * * @package Requests\Exceptions */ namespace WpOrg\Requests\Exception\Http; use WpOrg\Requests\Exception\Http; /** * Exception for 305 Use Proxy responses * * @package Requests\Exceptions */ final class Status305 extends Http { /** * HTTP status code * * @var integer */ protected $code = 305; /** * Reason phrase * * @var string */ protected $reason = 'Use Proxy'; } Exception/Http/Status401.php 0000644 00000000714 15174671662 0011663 0 ustar 00 <?php /** * Exception for 401 Unauthorized responses * * @package Requests\Exceptions */ namespace WpOrg\Requests\Exception\Http; use WpOrg\Requests\Exception\Http; /** * Exception for 401 Unauthorized responses * * @package Requests\Exceptions */ final class Status401 extends Http { /** * HTTP status code * * @var integer */ protected $code = 401; /** * Reason phrase * * @var string */ protected $reason = 'Unauthorized'; } Exception/Http/Status431.php 0000644 00000001145 15174671662 0011665 0 ustar 00 <?php /** * Exception for 431 Request Header Fields Too Large responses * * @link https://tools.ietf.org/html/rfc6585 * * @package Requests\Exceptions */ namespace WpOrg\Requests\Exception\Http; use WpOrg\Requests\Exception\Http; /** * Exception for 431 Request Header Fields Too Large responses * * @link https://tools.ietf.org/html/rfc6585 * * @package Requests\Exceptions */ final class Status431 extends Http { /** * HTTP status code * * @var integer */ protected $code = 431; /** * Reason phrase * * @var string */ protected $reason = 'Request Header Fields Too Large'; } Exception/Http/Status415.php 0000644 00000000752 15174671662 0011672 0 ustar 00 <?php /** * Exception for 415 Unsupported Media Type responses * * @package Requests\Exceptions */ namespace WpOrg\Requests\Exception\Http; use WpOrg\Requests\Exception\Http; /** * Exception for 415 Unsupported Media Type responses * * @package Requests\Exceptions */ final class Status415 extends Http { /** * HTTP status code * * @var integer */ protected $code = 415; /** * Reason phrase * * @var string */ protected $reason = 'Unsupported Media Type'; } Exception/Http/Status505.php 0000644 00000000766 15174671662 0011677 0 ustar 00 <?php /** * Exception for 505 HTTP Version Not Supported responses * * @package Requests\Exceptions */ namespace WpOrg\Requests\Exception\Http; use WpOrg\Requests\Exception\Http; /** * Exception for 505 HTTP Version Not Supported responses * * @package Requests\Exceptions */ final class Status505 extends Http { /** * HTTP status code * * @var integer */ protected $code = 505; /** * Reason phrase * * @var string */ protected $reason = 'HTTP Version Not Supported'; } Exception/Http/Status500.php 0000644 00000000747 15174671662 0011671 0 ustar 00 <?php /** * Exception for 500 Internal Server Error responses * * @package Requests\Exceptions */ namespace WpOrg\Requests\Exception\Http; use WpOrg\Requests\Exception\Http; /** * Exception for 500 Internal Server Error responses * * @package Requests\Exceptions */ final class Status500 extends Http { /** * HTTP status code * * @var integer */ protected $code = 500; /** * Reason phrase * * @var string */ protected $reason = 'Internal Server Error'; } Exception/Http/Status410.php 0000644 00000000664 15174671662 0011667 0 ustar 00 <?php /** * Exception for 410 Gone responses * * @package Requests\Exceptions */ namespace WpOrg\Requests\Exception\Http; use WpOrg\Requests\Exception\Http; /** * Exception for 410 Gone responses * * @package Requests\Exceptions */ final class Status410 extends Http { /** * HTTP status code * * @var integer */ protected $code = 410; /** * Reason phrase * * @var string */ protected $reason = 'Gone'; } Exception/Http/Status428.php 0000644 00000001107 15174671662 0011671 0 ustar 00 <?php /** * Exception for 428 Precondition Required responses * * @link https://tools.ietf.org/html/rfc6585 * * @package Requests\Exceptions */ namespace WpOrg\Requests\Exception\Http; use WpOrg\Requests\Exception\Http; /** * Exception for 428 Precondition Required responses * * @link https://tools.ietf.org/html/rfc6585 * * @package Requests\Exceptions */ final class Status428 extends Http { /** * HTTP status code * * @var integer */ protected $code = 428; /** * Reason phrase * * @var string */ protected $reason = 'Precondition Required'; } Exception/InvalidArgument.php 0000644 00000002122 15174671662 0012320 0 ustar 00 <?php namespace WpOrg\Requests\Exception; use InvalidArgumentException; /** * Exception for an invalid argument passed. * * @package Requests\Exceptions * @since 2.0.0 */ final class InvalidArgument extends InvalidArgumentException { /** * Create a new invalid argument exception with a standardized text. * * @param int $position The argument position in the function signature. 1-based. * @param string $name The argument name in the function signature. * @param string $expected The argument type expected as a string. * @param string $received The actual argument type received. * * @return \WpOrg\Requests\Exception\InvalidArgument */ public static function create($position, $name, $expected, $received) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_debug_backtrace $stack = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2); return new self( sprintf( '%s::%s(): Argument #%d (%s) must be of type %s, %s given', $stack[1]['class'], $stack[1]['function'], $position, $name, $expected, $received ) ); } } Exception/Transport/Curl.php 0000644 00000002565 15174671662 0012143 0 ustar 00 <?php /** * CURL Transport Exception. * * @package Requests\Exceptions */ namespace WpOrg\Requests\Exception\Transport; use WpOrg\Requests\Exception\Transport; /** * CURL Transport Exception. * * @package Requests\Exceptions */ final class Curl extends Transport { const EASY = 'cURLEasy'; const MULTI = 'cURLMulti'; const SHARE = 'cURLShare'; /** * cURL error code * * @var integer */ protected $code = -1; /** * Which type of cURL error * * EASY|MULTI|SHARE * * @var string */ protected $type = 'Unknown'; /** * Clear text error message * * @var string */ protected $reason = 'Unknown'; /** * Create a new exception. * * @param string $message Exception message. * @param string $type Exception type. * @param mixed $data Associated data, if applicable. * @param int $code Exception numerical code, if applicable. */ public function __construct($message, $type, $data = null, $code = 0) { if ($type !== null) { $this->type = $type; } if ($code !== null) { $this->code = (int) $code; } if ($message !== null) { $this->reason = $message; } $message = sprintf('%d %s', $this->code, $this->reason); parent::__construct($message, $this->type, $data, $this->code); } /** * Get the error message. * * @return string */ public function getReason() { return $this->reason; } } Auth/Basic.php 0000644 00000004755 15174671662 0007231 0 ustar 00 <?php /** * Basic Authentication provider * * @package Requests\Authentication */ namespace WpOrg\Requests\Auth; use WpOrg\Requests\Auth; use WpOrg\Requests\Exception\ArgumentCount; use WpOrg\Requests\Exception\InvalidArgument; use WpOrg\Requests\Hooks; /** * Basic Authentication provider * * Provides a handler for Basic HTTP authentication via the Authorization * header. * * @package Requests\Authentication */ class Basic implements Auth { /** * Username * * @var string */ public $user; /** * Password * * @var string */ public $pass; /** * Constructor * * @since 2.0 Throws an `InvalidArgument` exception. * @since 2.0 Throws an `ArgumentCount` exception instead of the Requests base `Exception. * * @param array|null $args Array of user and password. Must have exactly two elements * * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed argument is not an array or null. * @throws \WpOrg\Requests\Exception\ArgumentCount On incorrect number of array elements (`authbasicbadargs`). */ public function __construct($args = null) { if (is_array($args)) { if (count($args) !== 2) { throw ArgumentCount::create('an array with exactly two elements', count($args), 'authbasicbadargs'); } list($this->user, $this->pass) = $args; return; } if ($args !== null) { throw InvalidArgument::create(1, '$args', 'array|null', gettype($args)); } } /** * Register the necessary callbacks * * @see \WpOrg\Requests\Auth\Basic::curl_before_send() * @see \WpOrg\Requests\Auth\Basic::fsockopen_header() * @param \WpOrg\Requests\Hooks $hooks Hook system */ public function register(Hooks $hooks) { $hooks->register('curl.before_send', [$this, 'curl_before_send']); $hooks->register('fsockopen.after_headers', [$this, 'fsockopen_header']); } /** * Set cURL parameters before the data is sent * * @param resource|\CurlHandle $handle cURL handle */ public function curl_before_send(&$handle) { curl_setopt($handle, CURLOPT_HTTPAUTH, CURLAUTH_BASIC); curl_setopt($handle, CURLOPT_USERPWD, $this->getAuthString()); } /** * Add extra headers to the request before sending * * @param string $out HTTP header string */ public function fsockopen_header(&$out) { $out .= sprintf("Authorization: Basic %s\r\n", base64_encode($this->getAuthString())); } /** * Get the authentication string (user:pass) * * @return string */ public function getAuthString() { return $this->user . ':' . $this->pass; } } Transport.php 0000644 00000003010 15174671662 0007262 0 ustar 00 <?php /** * Base HTTP transport * * @package Requests\Transport */ namespace WpOrg\Requests; /** * Base HTTP transport * * @package Requests\Transport */ interface Transport { /** * Perform a request * * @param string $url URL to request * @param array $headers Associative array of request headers * @param string|array $data Data to send either as the POST body, or as parameters in the URL for a GET/HEAD * @param array $options Request options, see {@see \WpOrg\Requests\Requests::response()} for documentation * @return string Raw HTTP result */ public function request($url, $headers = [], $data = [], $options = []); /** * Send multiple requests simultaneously * * @param array $requests Request data (array of 'url', 'headers', 'data', 'options') as per {@see \WpOrg\Requests\Transport::request()} * @param array $options Global options, see {@see \WpOrg\Requests\Requests::response()} for documentation * @return array Array of \WpOrg\Requests\Response objects (may contain \WpOrg\Requests\Exception or string responses as well) */ public function request_multiple($requests, $options); /** * Self-test whether the transport can be used. * * The available capabilities to test for can be found in {@see \WpOrg\Requests\Capability}. * * @param array<string, bool> $capabilities Optional. Associative array of capabilities to test against, i.e. `['<capability>' => true]`. * @return bool Whether the transport can be used. */ public static function test($capabilities = []); } Auth.php 0000644 00000001534 15174671662 0006200 0 ustar 00 <?php /** * Authentication provider interface * * @package Requests\Authentication */ namespace WpOrg\Requests; use WpOrg\Requests\Hooks; /** * Authentication provider interface * * Implement this interface to act as an authentication provider. * * Parameters should be passed via the constructor where possible, as this * makes it much easier for users to use your provider. * * @see \WpOrg\Requests\Hooks * * @package Requests\Authentication */ interface Auth { /** * Register hooks as needed * * This method is called in {@see \WpOrg\Requests\Requests::request()} when the user * has set an instance as the 'auth' option. Use this callback to register all the * hooks you'll need. * * @see \WpOrg\Requests\Hooks::register() * @param \WpOrg\Requests\Hooks $hooks Hook system */ public function register(Hooks $hooks); } Response.php 0000644 00000010271 15174671662 0007073 0 ustar 00 <?php /** * HTTP response class * * Contains a response from \WpOrg\Requests\Requests::request() * * @package Requests */ namespace WpOrg\Requests; use WpOrg\Requests\Cookie\Jar; use WpOrg\Requests\Exception; use WpOrg\Requests\Exception\Http; use WpOrg\Requests\Response\Headers; /** * HTTP response class * * Contains a response from \WpOrg\Requests\Requests::request() * * @package Requests */ class Response { /** * Response body * * @var string */ public $body = ''; /** * Raw HTTP data from the transport * * @var string */ public $raw = ''; /** * Headers, as an associative array * * @var \WpOrg\Requests\Response\Headers Array-like object representing headers */ public $headers = []; /** * Status code, false if non-blocking * * @var integer|boolean */ public $status_code = false; /** * Protocol version, false if non-blocking * * @var float|boolean */ public $protocol_version = false; /** * Whether the request succeeded or not * * @var boolean */ public $success = false; /** * Number of redirects the request used * * @var integer */ public $redirects = 0; /** * URL requested * * @var string */ public $url = ''; /** * Previous requests (from redirects) * * @var array Array of \WpOrg\Requests\Response objects */ public $history = []; /** * Cookies from the request * * @var \WpOrg\Requests\Cookie\Jar Array-like object representing a cookie jar */ public $cookies = []; /** * Constructor */ public function __construct() { $this->headers = new Headers(); $this->cookies = new Jar(); } /** * Is the response a redirect? * * @return boolean True if redirect (3xx status), false if not. */ public function is_redirect() { $code = $this->status_code; return in_array($code, [300, 301, 302, 303, 307], true) || $code > 307 && $code < 400; } /** * Throws an exception if the request was not successful * * @param boolean $allow_redirects Set to false to throw on a 3xx as well * * @throws \WpOrg\Requests\Exception If `$allow_redirects` is false, and code is 3xx (`response.no_redirects`) * @throws \WpOrg\Requests\Exception\Http On non-successful status code. Exception class corresponds to "Status" + code (e.g. {@see \WpOrg\Requests\Exception\Http\Status404}) */ public function throw_for_status($allow_redirects = true) { if ($this->is_redirect()) { if ($allow_redirects !== true) { throw new Exception('Redirection not allowed', 'response.no_redirects', $this); } } elseif (!$this->success) { $exception = Http::get_class($this->status_code); throw new $exception(null, $this); } } /** * JSON decode the response body. * * The method parameters are the same as those for the PHP native `json_decode()` function. * * @link https://php.net/json-decode * * @param bool|null $associative Optional. When `true`, JSON objects will be returned as associative arrays; * When `false`, JSON objects will be returned as objects. * When `null`, JSON objects will be returned as associative arrays * or objects depending on whether `JSON_OBJECT_AS_ARRAY` is set in the flags. * Defaults to `true` (in contrast to the PHP native default of `null`). * @param int $depth Optional. Maximum nesting depth of the structure being decoded. * Defaults to `512`. * @param int $options Optional. Bitmask of JSON_BIGINT_AS_STRING, JSON_INVALID_UTF8_IGNORE, * JSON_INVALID_UTF8_SUBSTITUTE, JSON_OBJECT_AS_ARRAY, JSON_THROW_ON_ERROR. * Defaults to `0` (no options set). * * @return array * * @throws \WpOrg\Requests\Exception If `$this->body` is not valid json. */ public function decode_body($associative = true, $depth = 512, $options = 0) { $data = json_decode($this->body, $associative, $depth, $options); if (json_last_error() !== JSON_ERROR_NONE) { $last_error = json_last_error_msg(); throw new Exception('Unable to parse JSON data: ' . $last_error, 'response.invalid', $this); } return $data; } } Ipv6.php 0000644 00000013007 15174671662 0006121 0 ustar 00 <?php /** * Class to validate and to work with IPv6 addresses * * @package Requests\Utilities */ namespace WpOrg\Requests; use WpOrg\Requests\Exception\InvalidArgument; use WpOrg\Requests\Utility\InputValidator; /** * Class to validate and to work with IPv6 addresses * * This was originally based on the PEAR class of the same name, but has been * entirely rewritten. * * @package Requests\Utilities */ final class Ipv6 { /** * Uncompresses an IPv6 address * * RFC 4291 allows you to compress consecutive zero pieces in an address to * '::'. This method expects a valid IPv6 address and expands the '::' to * the required number of zero pieces. * * Example: FF01::101 -> FF01:0:0:0:0:0:0:101 * ::1 -> 0:0:0:0:0:0:0:1 * * @author Alexander Merz <alexander.merz@web.de> * @author elfrink at introweb dot nl * @author Josh Peck <jmp at joshpeck dot org> * @copyright 2003-2005 The PHP Group * @license https://opensource.org/licenses/bsd-license.php * * @param string|Stringable $ip An IPv6 address * @return string The uncompressed IPv6 address * * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed argument is not a string or a stringable object. */ public static function uncompress($ip) { if (InputValidator::is_string_or_stringable($ip) === false) { throw InvalidArgument::create(1, '$ip', 'string|Stringable', gettype($ip)); } $ip = (string) $ip; if (substr_count($ip, '::') !== 1) { return $ip; } list($ip1, $ip2) = explode('::', $ip); $c1 = ($ip1 === '') ? -1 : substr_count($ip1, ':'); $c2 = ($ip2 === '') ? -1 : substr_count($ip2, ':'); if (strpos($ip2, '.') !== false) { $c2++; } if ($c1 === -1 && $c2 === -1) { // :: $ip = '0:0:0:0:0:0:0:0'; } elseif ($c1 === -1) { // ::xxx $fill = str_repeat('0:', 7 - $c2); $ip = str_replace('::', $fill, $ip); } elseif ($c2 === -1) { // xxx:: $fill = str_repeat(':0', 7 - $c1); $ip = str_replace('::', $fill, $ip); } else { // xxx::xxx $fill = ':' . str_repeat('0:', 6 - $c2 - $c1); $ip = str_replace('::', $fill, $ip); } return $ip; } /** * Compresses an IPv6 address * * RFC 4291 allows you to compress consecutive zero pieces in an address to * '::'. This method expects a valid IPv6 address and compresses consecutive * zero pieces to '::'. * * Example: FF01:0:0:0:0:0:0:101 -> FF01::101 * 0:0:0:0:0:0:0:1 -> ::1 * * @see \WpOrg\Requests\Ipv6::uncompress() * * @param string $ip An IPv6 address * @return string The compressed IPv6 address */ public static function compress($ip) { // Prepare the IP to be compressed. // Note: Input validation is handled in the `uncompress()` method, which is the first call made in this method. $ip = self::uncompress($ip); $ip_parts = self::split_v6_v4($ip); // Replace all leading zeros $ip_parts[0] = preg_replace('/(^|:)0+([0-9])/', '\1\2', $ip_parts[0]); // Find bunches of zeros if (preg_match_all('/(?:^|:)(?:0(?::|$))+/', $ip_parts[0], $matches, PREG_OFFSET_CAPTURE)) { $max = 0; $pos = null; foreach ($matches[0] as $match) { if (strlen($match[0]) > $max) { $max = strlen($match[0]); $pos = $match[1]; } } $ip_parts[0] = substr_replace($ip_parts[0], '::', $pos, $max); } if ($ip_parts[1] !== '') { return implode(':', $ip_parts); } else { return $ip_parts[0]; } } /** * Splits an IPv6 address into the IPv6 and IPv4 representation parts * * RFC 4291 allows you to represent the last two parts of an IPv6 address * using the standard IPv4 representation * * Example: 0:0:0:0:0:0:13.1.68.3 * 0:0:0:0:0:FFFF:129.144.52.38 * * @param string $ip An IPv6 address * @return string[] [0] contains the IPv6 represented part, and [1] the IPv4 represented part */ private static function split_v6_v4($ip) { if (strpos($ip, '.') !== false) { $pos = strrpos($ip, ':'); $ipv6_part = substr($ip, 0, $pos); $ipv4_part = substr($ip, $pos + 1); return [$ipv6_part, $ipv4_part]; } else { return [$ip, '']; } } /** * Checks an IPv6 address * * Checks if the given IP is a valid IPv6 address * * @param string $ip An IPv6 address * @return bool true if $ip is a valid IPv6 address */ public static function check_ipv6($ip) { // Note: Input validation is handled in the `uncompress()` method, which is the first call made in this method. $ip = self::uncompress($ip); list($ipv6, $ipv4) = self::split_v6_v4($ip); $ipv6 = explode(':', $ipv6); $ipv4 = explode('.', $ipv4); if (count($ipv6) === 8 && count($ipv4) === 1 || count($ipv6) === 6 && count($ipv4) === 4) { foreach ($ipv6 as $ipv6_part) { // The section can't be empty if ($ipv6_part === '') { return false; } // Nor can it be over four characters if (strlen($ipv6_part) > 4) { return false; } // Remove leading zeros (this is safe because of the above) $ipv6_part = ltrim($ipv6_part, '0'); if ($ipv6_part === '') { $ipv6_part = '0'; } // Check the value is valid $value = hexdec($ipv6_part); if (dechex($value) !== strtolower($ipv6_part) || $value < 0 || $value > 0xFFFF) { return false; } } if (count($ipv4) === 4) { foreach ($ipv4 as $ipv4_part) { $value = (int) $ipv4_part; if ((string) $value !== $ipv4_part || $value < 0 || $value > 0xFF) { return false; } } } return true; } else { return false; } } } Port.php 0000644 00000002741 15174671662 0006224 0 ustar 00 <?php /** * Port utilities for Requests * * @package Requests\Utilities * @since 2.0.0 */ namespace WpOrg\Requests; use WpOrg\Requests\Exception; use WpOrg\Requests\Exception\InvalidArgument; /** * Find the correct port depending on the Request type. * * @package Requests\Utilities * @since 2.0.0 */ final class Port { /** * Port to use with Acap requests. * * @var int */ const ACAP = 674; /** * Port to use with Dictionary requests. * * @var int */ const DICT = 2628; /** * Port to use with HTTP requests. * * @var int */ const HTTP = 80; /** * Port to use with HTTP over SSL requests. * * @var int */ const HTTPS = 443; /** * Retrieve the port number to use. * * @param string $type Request type. * The following requests types are supported: * 'acap', 'dict', 'http' and 'https'. * * @return int * * @throws \WpOrg\Requests\Exception\InvalidArgument When a non-string input has been passed. * @throws \WpOrg\Requests\Exception When a non-supported port is requested ('portnotsupported'). */ public static function get($type) { if (!is_string($type)) { throw InvalidArgument::create(1, '$type', 'string', gettype($type)); } $type = strtoupper($type); if (!defined("self::{$type}")) { $message = sprintf('Invalid port type (%s) passed', $type); throw new Exception($message, 'portnotsupported'); } return constant("self::{$type}"); } } Ssl.php 0000644 00000012461 15174671662 0006041 0 ustar 00 <?php /** * SSL utilities for Requests * * @package Requests\Utilities */ namespace WpOrg\Requests; use WpOrg\Requests\Exception\InvalidArgument; use WpOrg\Requests\Utility\InputValidator; /** * SSL utilities for Requests * * Collection of utilities for working with and verifying SSL certificates. * * @package Requests\Utilities */ final class Ssl { /** * Verify the certificate against common name and subject alternative names * * Unfortunately, PHP doesn't check the certificate against the alternative * names, leading things like 'https://www.github.com/' to be invalid. * * @link https://tools.ietf.org/html/rfc2818#section-3.1 RFC2818, Section 3.1 * * @param string|Stringable $host Host name to verify against * @param array $cert Certificate data from openssl_x509_parse() * @return bool * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $host argument is not a string or a stringable object. * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $cert argument is not an array or array accessible. */ public static function verify_certificate($host, $cert) { if (InputValidator::is_string_or_stringable($host) === false) { throw InvalidArgument::create(1, '$host', 'string|Stringable', gettype($host)); } if (InputValidator::has_array_access($cert) === false) { throw InvalidArgument::create(2, '$cert', 'array|ArrayAccess', gettype($cert)); } $has_dns_alt = false; // Check the subjectAltName if (!empty($cert['extensions']['subjectAltName'])) { $altnames = explode(',', $cert['extensions']['subjectAltName']); foreach ($altnames as $altname) { $altname = trim($altname); if (strpos($altname, 'DNS:') !== 0) { continue; } $has_dns_alt = true; // Strip the 'DNS:' prefix and trim whitespace $altname = trim(substr($altname, 4)); // Check for a match if (self::match_domain($host, $altname) === true) { return true; } } if ($has_dns_alt === true) { return false; } } // Fall back to checking the common name if we didn't get any dNSName // alt names, as per RFC2818 if (!empty($cert['subject']['CN'])) { // Check for a match return (self::match_domain($host, $cert['subject']['CN']) === true); } return false; } /** * Verify that a reference name is valid * * Verifies a dNSName for HTTPS usage, (almost) as per Firefox's rules: * - Wildcards can only occur in a name with more than 3 components * - Wildcards can only occur as the last character in the first * component * - Wildcards may be preceded by additional characters * * We modify these rules to be a bit stricter and only allow the wildcard * character to be the full first component; that is, with the exclusion of * the third rule. * * @param string|Stringable $reference Reference dNSName * @return boolean Is the name valid? * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed argument is not a string or a stringable object. */ public static function verify_reference_name($reference) { if (InputValidator::is_string_or_stringable($reference) === false) { throw InvalidArgument::create(1, '$reference', 'string|Stringable', gettype($reference)); } if ($reference === '') { return false; } if (preg_match('`\s`', $reference) > 0) { // Whitespace detected. This can never be a dNSName. return false; } $parts = explode('.', $reference); if ($parts !== array_filter($parts)) { // DNSName cannot contain two dots next to each other. return false; } // Check the first part of the name $first = array_shift($parts); if (strpos($first, '*') !== false) { // Check that the wildcard is the full part if ($first !== '*') { return false; } // Check that we have at least 3 components (including first) if (count($parts) < 2) { return false; } } // Check the remaining parts foreach ($parts as $part) { if (strpos($part, '*') !== false) { return false; } } // Nothing found, verified! return true; } /** * Match a hostname against a dNSName reference * * @param string|Stringable $host Requested host * @param string|Stringable $reference dNSName to match against * @return boolean Does the domain match? * @throws \WpOrg\Requests\Exception\InvalidArgument When either of the passed arguments is not a string or a stringable object. */ public static function match_domain($host, $reference) { if (InputValidator::is_string_or_stringable($host) === false) { throw InvalidArgument::create(1, '$host', 'string|Stringable', gettype($host)); } // Check if the reference is blocklisted first if (self::verify_reference_name($reference) !== true) { return false; } // Check for a direct match if ((string) $host === (string) $reference) { return true; } // Calculate the valid wildcard match if the host is not an IP address // Also validates that the host has 3 parts or more, as per Firefox's ruleset, // as a wildcard reference is only allowed with 3 parts or more, so the // comparison will never match if host doesn't contain 3 parts or more as well. if (ip2long($host) === false) { $parts = explode('.', $host); $parts[0] = '*'; $wildcard = implode('.', $parts); if ($wildcard === (string) $reference) { return true; } } return false; } } Response/Headers.php 0000644 00000006035 15174671662 0010451 0 ustar 00 <?php /** * Case-insensitive dictionary, suitable for HTTP headers * * @package Requests */ namespace WpOrg\Requests\Response; use WpOrg\Requests\Exception; use WpOrg\Requests\Exception\InvalidArgument; use WpOrg\Requests\Utility\CaseInsensitiveDictionary; use WpOrg\Requests\Utility\FilteredIterator; /** * Case-insensitive dictionary, suitable for HTTP headers * * @package Requests */ class Headers extends CaseInsensitiveDictionary { /** * Get the given header * * Unlike {@see \WpOrg\Requests\Response\Headers::getValues()}, this returns a string. If there are * multiple values, it concatenates them with a comma as per RFC2616. * * Avoid using this where commas may be used unquoted in values, such as * Set-Cookie headers. * * @param string $offset Name of the header to retrieve. * @return string|null Header value */ public function offsetGet($offset) { if (is_string($offset)) { $offset = strtolower($offset); } if (!isset($this->data[$offset])) { return null; } return $this->flatten($this->data[$offset]); } /** * Set the given item * * @param string $offset Item name * @param string $value Item value * * @throws \WpOrg\Requests\Exception On attempting to use dictionary as list (`invalidset`) */ public function offsetSet($offset, $value) { if ($offset === null) { throw new Exception('Object is a dictionary, not a list', 'invalidset'); } if (is_string($offset)) { $offset = strtolower($offset); } if (!isset($this->data[$offset])) { $this->data[$offset] = []; } $this->data[$offset][] = $value; } /** * Get all values for a given header * * @param string $offset Name of the header to retrieve. * @return array|null Header values * * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed argument is not valid as an array key. */ public function getValues($offset) { if (!is_string($offset) && !is_int($offset)) { throw InvalidArgument::create(1, '$offset', 'string|int', gettype($offset)); } if (is_string($offset)) { $offset = strtolower($offset); } if (!isset($this->data[$offset])) { return null; } return $this->data[$offset]; } /** * Flattens a value into a string * * Converts an array into a string by imploding values with a comma, as per * RFC2616's rules for folding headers. * * @param string|array $value Value to flatten * @return string Flattened value * * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed argument is not a string or an array. */ public function flatten($value) { if (is_string($value)) { return $value; } if (is_array($value)) { return implode(',', $value); } throw InvalidArgument::create(1, '$value', 'string|array', gettype($value)); } /** * Get an iterator for the data * * Converts the internally stored values to a comma-separated string if there is more * than one value for a key. * * @return \ArrayIterator */ public function getIterator() { return new FilteredIterator($this->data, [$this, 'flatten']); } } Cookie/Jar.php 0000644 00000010413 15174671662 0007220 0 ustar 00 <?php /** * Cookie holder object * * @package Requests\Cookies */ namespace WpOrg\Requests\Cookie; use ArrayAccess; use ArrayIterator; use IteratorAggregate; use ReturnTypeWillChange; use WpOrg\Requests\Cookie; use WpOrg\Requests\Exception; use WpOrg\Requests\Exception\InvalidArgument; use WpOrg\Requests\HookManager; use WpOrg\Requests\Iri; use WpOrg\Requests\Response; /** * Cookie holder object * * @package Requests\Cookies */ class Jar implements ArrayAccess, IteratorAggregate { /** * Actual item data * * @var array */ protected $cookies = []; /** * Create a new jar * * @param array $cookies Existing cookie values * * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed argument is not an array. */ public function __construct($cookies = []) { if (is_array($cookies) === false) { throw InvalidArgument::create(1, '$cookies', 'array', gettype($cookies)); } $this->cookies = $cookies; } /** * Normalise cookie data into a \WpOrg\Requests\Cookie * * @param string|\WpOrg\Requests\Cookie $cookie Cookie header value, possibly pre-parsed (object). * @param string $key Optional. The name for this cookie. * @return \WpOrg\Requests\Cookie */ public function normalize_cookie($cookie, $key = '') { if ($cookie instanceof Cookie) { return $cookie; } return Cookie::parse($cookie, $key); } /** * Check if the given item exists * * @param string $offset Item key * @return boolean Does the item exist? */ #[ReturnTypeWillChange] public function offsetExists($offset) { return isset($this->cookies[$offset]); } /** * Get the value for the item * * @param string $offset Item key * @return string|null Item value (null if offsetExists is false) */ #[ReturnTypeWillChange] public function offsetGet($offset) { if (!isset($this->cookies[$offset])) { return null; } return $this->cookies[$offset]; } /** * Set the given item * * @param string $offset Item name * @param string $value Item value * * @throws \WpOrg\Requests\Exception On attempting to use dictionary as list (`invalidset`) */ #[ReturnTypeWillChange] public function offsetSet($offset, $value) { if ($offset === null) { throw new Exception('Object is a dictionary, not a list', 'invalidset'); } $this->cookies[$offset] = $value; } /** * Unset the given header * * @param string $offset The key for the item to unset. */ #[ReturnTypeWillChange] public function offsetUnset($offset) { unset($this->cookies[$offset]); } /** * Get an iterator for the data * * @return \ArrayIterator */ #[ReturnTypeWillChange] public function getIterator() { return new ArrayIterator($this->cookies); } /** * Register the cookie handler with the request's hooking system * * @param \WpOrg\Requests\HookManager $hooks Hooking system */ public function register(HookManager $hooks) { $hooks->register('requests.before_request', [$this, 'before_request']); $hooks->register('requests.before_redirect_check', [$this, 'before_redirect_check']); } /** * Add Cookie header to a request if we have any * * As per RFC 6265, cookies are separated by '; ' * * @param string $url * @param array $headers * @param array $data * @param string $type * @param array $options */ public function before_request($url, &$headers, &$data, &$type, &$options) { if (!$url instanceof Iri) { $url = new Iri($url); } if (!empty($this->cookies)) { $cookies = []; foreach ($this->cookies as $key => $cookie) { $cookie = $this->normalize_cookie($cookie, $key); // Skip expired cookies if ($cookie->is_expired()) { continue; } if ($cookie->domain_matches($url->host)) { $cookies[] = $cookie->format_for_header(); } } $headers['Cookie'] = implode('; ', $cookies); } } /** * Parse all cookies from a response and attach them to the response * * @param \WpOrg\Requests\Response $response Response as received. */ public function before_redirect_check(Response $response) { $url = $response->url; if (!$url instanceof Iri) { $url = new Iri($url); } $cookies = Cookie::parse_from_headers($response->headers, $url); $this->cookies = array_merge($this->cookies, $cookies); $response->cookies = $this; } } Requests.php 0000644 00000102321 15174671662 0007106 0 ustar 00 <?php /** * Requests for PHP * * Inspired by Requests for Python. * * Based on concepts from SimplePie_File, RequestCore and WP_Http. * * @package Requests */ namespace WpOrg\Requests; use WpOrg\Requests\Auth\Basic; use WpOrg\Requests\Capability; use WpOrg\Requests\Cookie\Jar; use WpOrg\Requests\Exception; use WpOrg\Requests\Exception\InvalidArgument; use WpOrg\Requests\Hooks; use WpOrg\Requests\IdnaEncoder; use WpOrg\Requests\Iri; use WpOrg\Requests\Proxy\Http; use WpOrg\Requests\Response; use WpOrg\Requests\Transport\Curl; use WpOrg\Requests\Transport\Fsockopen; use WpOrg\Requests\Utility\InputValidator; /** * Requests for PHP * * Inspired by Requests for Python. * * Based on concepts from SimplePie_File, RequestCore and WP_Http. * * @package Requests */ class Requests { /** * POST method * * @var string */ const POST = 'POST'; /** * PUT method * * @var string */ const PUT = 'PUT'; /** * GET method * * @var string */ const GET = 'GET'; /** * HEAD method * * @var string */ const HEAD = 'HEAD'; /** * DELETE method * * @var string */ const DELETE = 'DELETE'; /** * OPTIONS method * * @var string */ const OPTIONS = 'OPTIONS'; /** * TRACE method * * @var string */ const TRACE = 'TRACE'; /** * PATCH method * * @link https://tools.ietf.org/html/rfc5789 * @var string */ const PATCH = 'PATCH'; /** * Default size of buffer size to read streams * * @var integer */ const BUFFER_SIZE = 1160; /** * Option defaults. * * @see \WpOrg\Requests\Requests::get_default_options() * @see \WpOrg\Requests\Requests::request() for values returned by this method * * @since 2.0.0 * * @var array */ const OPTION_DEFAULTS = [ 'timeout' => 10, 'connect_timeout' => 10, 'useragent' => 'php-requests/' . self::VERSION, 'protocol_version' => 1.1, 'redirected' => 0, 'redirects' => 10, 'follow_redirects' => true, 'blocking' => true, 'type' => self::GET, 'filename' => false, 'auth' => false, 'proxy' => false, 'cookies' => false, 'max_bytes' => false, 'idn' => true, 'hooks' => null, 'transport' => null, 'verify' => null, 'verifyname' => true, ]; /** * Default supported Transport classes. * * @since 2.0.0 * * @var array */ const DEFAULT_TRANSPORTS = [ Curl::class => Curl::class, Fsockopen::class => Fsockopen::class, ]; /** * Current version of Requests * * @var string */ const VERSION = '2.0.11'; /** * Selected transport name * * Use {@see \WpOrg\Requests\Requests::get_transport()} instead * * @var array */ public static $transport = []; /** * Registered transport classes * * @var array */ protected static $transports = []; /** * Default certificate path. * * @see \WpOrg\Requests\Requests::get_certificate_path() * @see \WpOrg\Requests\Requests::set_certificate_path() * * @var string */ protected static $certificate_path = __DIR__ . '/../certificates/cacert.pem'; /** * All (known) valid deflate, gzip header magic markers. * * These markers relate to different compression levels. * * @link https://stackoverflow.com/a/43170354/482864 Marker source. * * @since 2.0.0 * * @var array */ private static $magic_compression_headers = [ "\x1f\x8b" => true, // Gzip marker. "\x78\x01" => true, // Zlib marker - level 1. "\x78\x5e" => true, // Zlib marker - level 2 to 5. "\x78\x9c" => true, // Zlib marker - level 6. "\x78\xda" => true, // Zlib marker - level 7 to 9. ]; /** * This is a static class, do not instantiate it * * @codeCoverageIgnore */ private function __construct() {} /** * Register a transport * * @param string $transport Transport class to add, must support the \WpOrg\Requests\Transport interface */ public static function add_transport($transport) { if (empty(self::$transports)) { self::$transports = self::DEFAULT_TRANSPORTS; } self::$transports[$transport] = $transport; } /** * Get the fully qualified class name (FQCN) for a working transport. * * @param array<string, bool> $capabilities Optional. Associative array of capabilities to test against, i.e. `['<capability>' => true]`. * @return string FQCN of the transport to use, or an empty string if no transport was * found which provided the requested capabilities. */ protected static function get_transport_class(array $capabilities = []) { // Caching code, don't bother testing coverage. // @codeCoverageIgnoreStart // Array of capabilities as a string to be used as an array key. ksort($capabilities); $cap_string = serialize($capabilities); // Don't search for a transport if it's already been done for these $capabilities. if (isset(self::$transport[$cap_string])) { return self::$transport[$cap_string]; } // Ensure we will not run this same check again later on. self::$transport[$cap_string] = ''; // @codeCoverageIgnoreEnd if (empty(self::$transports)) { self::$transports = self::DEFAULT_TRANSPORTS; } // Find us a working transport. foreach (self::$transports as $class) { if (!class_exists($class)) { continue; } $result = $class::test($capabilities); if ($result === true) { self::$transport[$cap_string] = $class; break; } } return self::$transport[$cap_string]; } /** * Get a working transport. * * @param array<string, bool> $capabilities Optional. Associative array of capabilities to test against, i.e. `['<capability>' => true]`. * @return \WpOrg\Requests\Transport * @throws \WpOrg\Requests\Exception If no valid transport is found (`notransport`). */ protected static function get_transport(array $capabilities = []) { $class = self::get_transport_class($capabilities); if ($class === '') { throw new Exception('No working transports found', 'notransport', self::$transports); } return new $class(); } /** * Checks to see if we have a transport for the capabilities requested. * * Supported capabilities can be found in the {@see \WpOrg\Requests\Capability} * interface as constants. * * Example usage: * `Requests::has_capabilities([Capability::SSL => true])`. * * @param array<string, bool> $capabilities Optional. Associative array of capabilities to test against, i.e. `['<capability>' => true]`. * @return bool Whether the transport has the requested capabilities. */ public static function has_capabilities(array $capabilities = []) { return self::get_transport_class($capabilities) !== ''; } /**#@+ * @see \WpOrg\Requests\Requests::request() * @param string $url * @param array $headers * @param array $options * @return \WpOrg\Requests\Response */ /** * Send a GET request */ public static function get($url, $headers = [], $options = []) { return self::request($url, $headers, null, self::GET, $options); } /** * Send a HEAD request */ public static function head($url, $headers = [], $options = []) { return self::request($url, $headers, null, self::HEAD, $options); } /** * Send a DELETE request */ public static function delete($url, $headers = [], $options = []) { return self::request($url, $headers, null, self::DELETE, $options); } /** * Send a TRACE request */ public static function trace($url, $headers = [], $options = []) { return self::request($url, $headers, null, self::TRACE, $options); } /**#@-*/ /**#@+ * @see \WpOrg\Requests\Requests::request() * @param string $url * @param array $headers * @param array $data * @param array $options * @return \WpOrg\Requests\Response */ /** * Send a POST request */ public static function post($url, $headers = [], $data = [], $options = []) { return self::request($url, $headers, $data, self::POST, $options); } /** * Send a PUT request */ public static function put($url, $headers = [], $data = [], $options = []) { return self::request($url, $headers, $data, self::PUT, $options); } /** * Send an OPTIONS request */ public static function options($url, $headers = [], $data = [], $options = []) { return self::request($url, $headers, $data, self::OPTIONS, $options); } /** * Send a PATCH request * * Note: Unlike {@see \WpOrg\Requests\Requests::post()} and {@see \WpOrg\Requests\Requests::put()}, * `$headers` is required, as the specification recommends that should send an ETag * * @link https://tools.ietf.org/html/rfc5789 */ public static function patch($url, $headers, $data = [], $options = []) { return self::request($url, $headers, $data, self::PATCH, $options); } /**#@-*/ /** * Main interface for HTTP requests * * This method initiates a request and sends it via a transport before * parsing. * * The `$options` parameter takes an associative array with the following * options: * * - `timeout`: How long should we wait for a response? * Note: for cURL, a minimum of 1 second applies, as DNS resolution * operates at second-resolution only. * (float, seconds with a millisecond precision, default: 10, example: 0.01) * - `connect_timeout`: How long should we wait while trying to connect? * (float, seconds with a millisecond precision, default: 10, example: 0.01) * - `useragent`: Useragent to send to the server * (string, default: php-requests/$version) * - `follow_redirects`: Should we follow 3xx redirects? * (boolean, default: true) * - `redirects`: How many times should we redirect before erroring? * (integer, default: 10) * - `blocking`: Should we block processing on this request? * (boolean, default: true) * - `filename`: File to stream the body to instead. * (string|boolean, default: false) * - `auth`: Authentication handler or array of user/password details to use * for Basic authentication * (\WpOrg\Requests\Auth|array|boolean, default: false) * - `proxy`: Proxy details to use for proxy by-passing and authentication * (\WpOrg\Requests\Proxy|array|string|boolean, default: false) * - `max_bytes`: Limit for the response body size. * (integer|boolean, default: false) * - `idn`: Enable IDN parsing * (boolean, default: true) * - `transport`: Custom transport. Either a class name, or a * transport object. Defaults to the first working transport from * {@see \WpOrg\Requests\Requests::getTransport()} * (string|\WpOrg\Requests\Transport, default: {@see \WpOrg\Requests\Requests::getTransport()}) * - `hooks`: Hooks handler. * (\WpOrg\Requests\HookManager, default: new WpOrg\Requests\Hooks()) * - `verify`: Should we verify SSL certificates? Allows passing in a custom * certificate file as a string. (Using true uses the system-wide root * certificate store instead, but this may have different behaviour * across transports.) * (string|boolean, default: certificates/cacert.pem) * - `verifyname`: Should we verify the common name in the SSL certificate? * (boolean, default: true) * - `data_format`: How should we send the `$data` parameter? * (string, one of 'query' or 'body', default: 'query' for * HEAD/GET/DELETE, 'body' for POST/PUT/OPTIONS/PATCH) * * @param string|Stringable $url URL to request * @param array $headers Extra headers to send with the request * @param array|null $data Data to send either as a query string for GET/HEAD requests, or in the body for POST requests * @param string $type HTTP request type (use Requests constants) * @param array $options Options for the request (see description for more information) * @return \WpOrg\Requests\Response * * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $url argument is not a string or Stringable. * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $type argument is not a string. * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $options argument is not an array. * @throws \WpOrg\Requests\Exception On invalid URLs (`nonhttp`) */ public static function request($url, $headers = [], $data = [], $type = self::GET, $options = []) { if (InputValidator::is_string_or_stringable($url) === false) { throw InvalidArgument::create(1, '$url', 'string|Stringable', gettype($url)); } if (is_string($type) === false) { throw InvalidArgument::create(4, '$type', 'string', gettype($type)); } if (is_array($options) === false) { throw InvalidArgument::create(5, '$options', 'array', gettype($options)); } if (empty($options['type'])) { $options['type'] = $type; } $options = array_merge(self::get_default_options(), $options); self::set_defaults($url, $headers, $data, $type, $options); $options['hooks']->dispatch('requests.before_request', [&$url, &$headers, &$data, &$type, &$options]); if (!empty($options['transport'])) { $transport = $options['transport']; if (is_string($options['transport'])) { $transport = new $transport(); } } else { $need_ssl = (stripos($url, 'https://') === 0); $capabilities = [Capability::SSL => $need_ssl]; $transport = self::get_transport($capabilities); } $response = $transport->request($url, $headers, $data, $options); $options['hooks']->dispatch('requests.before_parse', [&$response, $url, $headers, $data, $type, $options]); return self::parse_response($response, $url, $headers, $data, $options); } /** * Send multiple HTTP requests simultaneously * * The `$requests` parameter takes an associative or indexed array of * request fields. The key of each request can be used to match up the * request with the returned data, or with the request passed into your * `multiple.request.complete` callback. * * The request fields value is an associative array with the following keys: * * - `url`: Request URL Same as the `$url` parameter to * {@see \WpOrg\Requests\Requests::request()} * (string, required) * - `headers`: Associative array of header fields. Same as the `$headers` * parameter to {@see \WpOrg\Requests\Requests::request()} * (array, default: `array()`) * - `data`: Associative array of data fields or a string. Same as the * `$data` parameter to {@see \WpOrg\Requests\Requests::request()} * (array|string, default: `array()`) * - `type`: HTTP request type (use \WpOrg\Requests\Requests constants). Same as the `$type` * parameter to {@see \WpOrg\Requests\Requests::request()} * (string, default: `\WpOrg\Requests\Requests::GET`) * - `cookies`: Associative array of cookie name to value, or cookie jar. * (array|\WpOrg\Requests\Cookie\Jar) * * If the `$options` parameter is specified, individual requests will * inherit options from it. This can be used to use a single hooking system, * or set all the types to `\WpOrg\Requests\Requests::POST`, for example. * * In addition, the `$options` parameter takes the following global options: * * - `complete`: A callback for when a request is complete. Takes two * parameters, a \WpOrg\Requests\Response/\WpOrg\Requests\Exception reference, and the * ID from the request array (Note: this can also be overridden on a * per-request basis, although that's a little silly) * (callback) * * @param array $requests Requests data (see description for more information) * @param array $options Global and default options (see {@see \WpOrg\Requests\Requests::request()}) * @return array Responses (either \WpOrg\Requests\Response or a \WpOrg\Requests\Exception object) * * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $requests argument is not an array or iterable object with array access. * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $options argument is not an array. */ public static function request_multiple($requests, $options = []) { if (InputValidator::has_array_access($requests) === false || InputValidator::is_iterable($requests) === false) { throw InvalidArgument::create(1, '$requests', 'array|ArrayAccess&Traversable', gettype($requests)); } if (is_array($options) === false) { throw InvalidArgument::create(2, '$options', 'array', gettype($options)); } $options = array_merge(self::get_default_options(true), $options); if (!empty($options['hooks'])) { $options['hooks']->register('transport.internal.parse_response', [static::class, 'parse_multiple']); if (!empty($options['complete'])) { $options['hooks']->register('multiple.request.complete', $options['complete']); } } foreach ($requests as $id => &$request) { if (!isset($request['headers'])) { $request['headers'] = []; } if (!isset($request['data'])) { $request['data'] = []; } if (!isset($request['type'])) { $request['type'] = self::GET; } if (!isset($request['options'])) { $request['options'] = $options; $request['options']['type'] = $request['type']; } else { if (empty($request['options']['type'])) { $request['options']['type'] = $request['type']; } $request['options'] = array_merge($options, $request['options']); } self::set_defaults($request['url'], $request['headers'], $request['data'], $request['type'], $request['options']); // Ensure we only hook in once if ($request['options']['hooks'] !== $options['hooks']) { $request['options']['hooks']->register('transport.internal.parse_response', [static::class, 'parse_multiple']); if (!empty($request['options']['complete'])) { $request['options']['hooks']->register('multiple.request.complete', $request['options']['complete']); } } } unset($request); if (!empty($options['transport'])) { $transport = $options['transport']; if (is_string($options['transport'])) { $transport = new $transport(); } } else { $transport = self::get_transport(); } $responses = $transport->request_multiple($requests, $options); foreach ($responses as $id => &$response) { // If our hook got messed with somehow, ensure we end up with the // correct response if (is_string($response)) { $request = $requests[$id]; self::parse_multiple($response, $request); $request['options']['hooks']->dispatch('multiple.request.complete', [&$response, $id]); } } return $responses; } /** * Get the default options * * @see \WpOrg\Requests\Requests::request() for values returned by this method * @param boolean $multirequest Is this a multirequest? * @return array Default option values */ protected static function get_default_options($multirequest = false) { $defaults = static::OPTION_DEFAULTS; $defaults['verify'] = self::$certificate_path; if ($multirequest !== false) { $defaults['complete'] = null; } return $defaults; } /** * Get default certificate path. * * @return string Default certificate path. */ public static function get_certificate_path() { return self::$certificate_path; } /** * Set default certificate path. * * @param string|Stringable|bool $path Certificate path, pointing to a PEM file. * * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $url argument is not a string, Stringable or boolean. */ public static function set_certificate_path($path) { if (InputValidator::is_string_or_stringable($path) === false && is_bool($path) === false) { throw InvalidArgument::create(1, '$path', 'string|Stringable|bool', gettype($path)); } self::$certificate_path = $path; } /** * Set the default values * * The $options parameter is updated with the results. * * @param string $url URL to request * @param array $headers Extra headers to send with the request * @param array|null $data Data to send either as a query string for GET/HEAD requests, or in the body for POST requests * @param string $type HTTP request type * @param array $options Options for the request * @return void * * @throws \WpOrg\Requests\Exception When the $url is not an http(s) URL. */ protected static function set_defaults(&$url, &$headers, &$data, &$type, &$options) { if (!preg_match('/^http(s)?:\/\//i', $url, $matches)) { throw new Exception('Only HTTP(S) requests are handled.', 'nonhttp', $url); } if (empty($options['hooks'])) { $options['hooks'] = new Hooks(); } if (is_array($options['auth'])) { $options['auth'] = new Basic($options['auth']); } if ($options['auth'] !== false) { $options['auth']->register($options['hooks']); } if (is_string($options['proxy']) || is_array($options['proxy'])) { $options['proxy'] = new Http($options['proxy']); } if ($options['proxy'] !== false) { $options['proxy']->register($options['hooks']); } if (is_array($options['cookies'])) { $options['cookies'] = new Jar($options['cookies']); } elseif (empty($options['cookies'])) { $options['cookies'] = new Jar(); } if ($options['cookies'] !== false) { $options['cookies']->register($options['hooks']); } if ($options['idn'] !== false) { $iri = new Iri($url); $iri->host = IdnaEncoder::encode($iri->ihost); $url = $iri->uri; } // Massage the type to ensure we support it. $type = strtoupper($type); if (!isset($options['data_format'])) { if (in_array($type, [self::HEAD, self::GET, self::DELETE], true)) { $options['data_format'] = 'query'; } else { $options['data_format'] = 'body'; } } } /** * HTTP response parser * * @param string $headers Full response text including headers and body * @param string $url Original request URL * @param array $req_headers Original $headers array passed to {@link request()}, in case we need to follow redirects * @param array $req_data Original $data array passed to {@link request()}, in case we need to follow redirects * @param array $options Original $options array passed to {@link request()}, in case we need to follow redirects * @return \WpOrg\Requests\Response * * @throws \WpOrg\Requests\Exception On missing head/body separator (`requests.no_crlf_separator`) * @throws \WpOrg\Requests\Exception On missing head/body separator (`noversion`) * @throws \WpOrg\Requests\Exception On missing head/body separator (`toomanyredirects`) */ protected static function parse_response($headers, $url, $req_headers, $req_data, $options) { $return = new Response(); if (!$options['blocking']) { return $return; } $return->raw = $headers; $return->url = (string) $url; $return->body = ''; if (!$options['filename']) { $pos = strpos($headers, "\r\n\r\n"); if ($pos === false) { // Crap! throw new Exception('Missing header/body separator', 'requests.no_crlf_separator'); } $headers = substr($return->raw, 0, $pos); // Headers will always be separated from the body by two new lines - `\n\r\n\r`. $body = substr($return->raw, $pos + 4); if (!empty($body)) { $return->body = $body; } } // Pretend CRLF = LF for compatibility (RFC 2616, section 19.3) $headers = str_replace("\r\n", "\n", $headers); // Unfold headers (replace [CRLF] 1*( SP | HT ) with SP) as per RFC 2616 (section 2.2) $headers = preg_replace('/\n[ \t]/', ' ', $headers); $headers = explode("\n", $headers); preg_match('#^HTTP/(1\.\d)[ \t]+(\d+)#i', array_shift($headers), $matches); if (empty($matches)) { throw new Exception('Response could not be parsed', 'noversion', $headers); } $return->protocol_version = (float) $matches[1]; $return->status_code = (int) $matches[2]; if ($return->status_code >= 200 && $return->status_code < 300) { $return->success = true; } foreach ($headers as $header) { list($key, $value) = explode(':', $header, 2); $value = trim($value); preg_replace('#(\s+)#i', ' ', $value); $return->headers[$key] = $value; } if (isset($return->headers['transfer-encoding'])) { $return->body = self::decode_chunked($return->body); unset($return->headers['transfer-encoding']); } if (isset($return->headers['content-encoding'])) { $return->body = self::decompress($return->body); } //fsockopen and cURL compatibility if (isset($return->headers['connection'])) { unset($return->headers['connection']); } $options['hooks']->dispatch('requests.before_redirect_check', [&$return, $req_headers, $req_data, $options]); if ($return->is_redirect() && $options['follow_redirects'] === true) { if (isset($return->headers['location']) && $options['redirected'] < $options['redirects']) { if ($return->status_code === 303) { $options['type'] = self::GET; } $options['redirected']++; $location = $return->headers['location']; if (strpos($location, 'http://') !== 0 && strpos($location, 'https://') !== 0) { // relative redirect, for compatibility make it absolute $location = Iri::absolutize($url, $location); $location = $location->uri; } $hook_args = [ &$location, &$req_headers, &$req_data, &$options, $return, ]; $options['hooks']->dispatch('requests.before_redirect', $hook_args); $redirected = self::request($location, $req_headers, $req_data, $options['type'], $options); $redirected->history[] = $return; return $redirected; } elseif ($options['redirected'] >= $options['redirects']) { throw new Exception('Too many redirects', 'toomanyredirects', $return); } } $return->redirects = $options['redirected']; $options['hooks']->dispatch('requests.after_request', [&$return, $req_headers, $req_data, $options]); return $return; } /** * Callback for `transport.internal.parse_response` * * Internal use only. Converts a raw HTTP response to a \WpOrg\Requests\Response * while still executing a multiple request. * * `$response` is either set to a \WpOrg\Requests\Response instance, or a \WpOrg\Requests\Exception object * * @param string $response Full response text including headers and body (will be overwritten with Response instance) * @param array $request Request data as passed into {@see \WpOrg\Requests\Requests::request_multiple()} * @return void */ public static function parse_multiple(&$response, $request) { try { $url = $request['url']; $headers = $request['headers']; $data = $request['data']; $options = $request['options']; $response = self::parse_response($response, $url, $headers, $data, $options); } catch (Exception $e) { $response = $e; } } /** * Decoded a chunked body as per RFC 2616 * * @link https://tools.ietf.org/html/rfc2616#section-3.6.1 * @param string $data Chunked body * @return string Decoded body */ protected static function decode_chunked($data) { if (!preg_match('/^([0-9a-f]+)(?:;(?:[\w-]*)(?:=(?:(?:[\w-]*)*|"(?:[^\r\n])*"))?)*\r\n/i', trim($data))) { return $data; } $decoded = ''; $encoded = $data; while (true) { $is_chunked = (bool) preg_match('/^([0-9a-f]+)(?:;(?:[\w-]*)(?:=(?:(?:[\w-]*)*|"(?:[^\r\n])*"))?)*\r\n/i', $encoded, $matches); if (!$is_chunked) { // Looks like it's not chunked after all return $data; } $length = hexdec(trim($matches[1])); if ($length === 0) { // Ignore trailer headers return $decoded; } $chunk_length = strlen($matches[0]); $decoded .= substr($encoded, $chunk_length, $length); $encoded = substr($encoded, $chunk_length + $length + 2); if (trim($encoded) === '0' || empty($encoded)) { return $decoded; } } // We'll never actually get down here // @codeCoverageIgnoreStart } // @codeCoverageIgnoreEnd /** * Convert a key => value array to a 'key: value' array for headers * * @param iterable $dictionary Dictionary of header values * @return array List of headers * * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed argument is not iterable. */ public static function flatten($dictionary) { if (InputValidator::is_iterable($dictionary) === false) { throw InvalidArgument::create(1, '$dictionary', 'iterable', gettype($dictionary)); } $return = []; foreach ($dictionary as $key => $value) { $return[] = sprintf('%s: %s', $key, $value); } return $return; } /** * Decompress an encoded body * * Implements gzip, compress and deflate. Guesses which it is by attempting * to decode. * * @param string $data Compressed data in one of the above formats * @return string Decompressed string * * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed argument is not a string. */ public static function decompress($data) { if (is_string($data) === false) { throw InvalidArgument::create(1, '$data', 'string', gettype($data)); } if (trim($data) === '') { // Empty body does not need further processing. return $data; } $marker = substr($data, 0, 2); if (!isset(self::$magic_compression_headers[$marker])) { // Not actually compressed. Probably cURL ruining this for us. return $data; } if (function_exists('gzdecode')) { $decoded = @gzdecode($data); if ($decoded !== false) { return $decoded; } } if (function_exists('gzinflate')) { $decoded = @gzinflate($data); if ($decoded !== false) { return $decoded; } } $decoded = self::compatible_gzinflate($data); if ($decoded !== false) { return $decoded; } if (function_exists('gzuncompress')) { $decoded = @gzuncompress($data); if ($decoded !== false) { return $decoded; } } return $data; } /** * Decompression of deflated string while staying compatible with the majority of servers. * * Certain Servers will return deflated data with headers which PHP's gzinflate() * function cannot handle out of the box. The following function has been created from * various snippets on the gzinflate() PHP documentation. * * Warning: Magic numbers within. Due to the potential different formats that the compressed * data may be returned in, some "magic offsets" are needed to ensure proper decompression * takes place. For a simple progmatic way to determine the magic offset in use, see: * https://core.trac.wordpress.org/ticket/18273 * * @since 1.6.0 * @link https://core.trac.wordpress.org/ticket/18273 * @link https://www.php.net/gzinflate#70875 * @link https://www.php.net/gzinflate#77336 * * @param string $gz_data String to decompress. * @return string|bool False on failure. * * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed argument is not a string. */ public static function compatible_gzinflate($gz_data) { if (is_string($gz_data) === false) { throw InvalidArgument::create(1, '$gz_data', 'string', gettype($gz_data)); } if (trim($gz_data) === '') { return false; } // Compressed data might contain a full zlib header, if so strip it for // gzinflate() if (substr($gz_data, 0, 3) === "\x1f\x8b\x08") { $i = 10; $flg = ord(substr($gz_data, 3, 1)); if ($flg > 0) { if ($flg & 4) { list($xlen) = unpack('v', substr($gz_data, $i, 2)); $i += 2 + $xlen; } if ($flg & 8) { $i = strpos($gz_data, "\0", $i) + 1; } if ($flg & 16) { $i = strpos($gz_data, "\0", $i) + 1; } if ($flg & 2) { $i += 2; } } $decompressed = self::compatible_gzinflate(substr($gz_data, $i)); if ($decompressed !== false) { return $decompressed; } } // If the data is Huffman Encoded, we must first strip the leading 2 // byte Huffman marker for gzinflate() // The response is Huffman coded by many compressors such as // java.util.zip.Deflater, Ruby's Zlib::Deflate, and .NET's // System.IO.Compression.DeflateStream. // // See https://decompres.blogspot.com/ for a quick explanation of this // data type $huffman_encoded = false; // low nibble of first byte should be 0x08 list(, $first_nibble) = unpack('h', $gz_data); // First 2 bytes should be divisible by 0x1F list(, $first_two_bytes) = unpack('n', $gz_data); if ($first_nibble === 0x08 && ($first_two_bytes % 0x1F) === 0) { $huffman_encoded = true; } if ($huffman_encoded) { $decompressed = @gzinflate(substr($gz_data, 2)); if ($decompressed !== false) { return $decompressed; } } if (substr($gz_data, 0, 4) === "\x50\x4b\x03\x04") { // ZIP file format header // Offset 6: 2 bytes, General-purpose field // Offset 26: 2 bytes, filename length // Offset 28: 2 bytes, optional field length // Offset 30: Filename field, followed by optional field, followed // immediately by data list(, $general_purpose_flag) = unpack('v', substr($gz_data, 6, 2)); // If the file has been compressed on the fly, 0x08 bit is set of // the general purpose field. We can use this to differentiate // between a compressed document, and a ZIP file $zip_compressed_on_the_fly = ((0x08 & $general_purpose_flag) === 0x08); if (!$zip_compressed_on_the_fly) { // Don't attempt to decode a compressed zip file return $gz_data; } // Determine the first byte of data, based on the above ZIP header // offsets: $first_file_start = array_sum(unpack('v2', substr($gz_data, 26, 4))); $decompressed = @gzinflate(substr($gz_data, 30 + $first_file_start)); if ($decompressed !== false) { return $decompressed; } return false; } // Finally fall back to straight gzinflate $decompressed = @gzinflate($gz_data); if ($decompressed !== false) { return $decompressed; } // Fallback for all above failing, not expected, but included for // debugging and preventing regressions and to track stats $decompressed = @gzinflate(substr($gz_data, 2)); if ($decompressed !== false) { return $decompressed; } return false; } } Capability.php 0000644 00000001214 15174671662 0007353 0 ustar 00 <?php /** * Capability interface declaring the known capabilities. * * @package Requests\Utilities */ namespace WpOrg\Requests; /** * Capability interface declaring the known capabilities. * * This is used as the authoritative source for which capabilities can be queried. * * @package Requests\Utilities */ interface Capability { /** * Support for SSL. * * @var string */ const SSL = 'ssl'; /** * Collection of all capabilities supported in Requests. * * Note: this does not automatically mean that the capability will be supported for your chosen transport! * * @var string[] */ const ALL = [ self::SSL, ]; } Autoload.php 0000644 00000022167 15174671662 0007054 0 ustar 00 <?php /** * Autoloader for Requests for PHP. * * Include this file if you'd like to avoid having to create your own autoloader. * * @package Requests * @since 2.0.0 * * @codeCoverageIgnore */ namespace WpOrg\Requests; /* * Ensure the autoloader is only declared once. * This safeguard is in place as this is the typical entry point for this library * and this file being required unconditionally could easily cause * fatal "Class already declared" errors. */ if (class_exists('WpOrg\Requests\Autoload') === false) { /** * Autoloader for Requests for PHP. * * This autoloader supports the PSR-4 based Requests 2.0.0 classes in a case-sensitive manner * as the most common server OS-es are case-sensitive and the file names are in mixed case. * * For the PSR-0 Requests 1.x BC-layer, requested classes will be treated case-insensitively. * * @package Requests */ final class Autoload { /** * List of the old PSR-0 class names in lowercase as keys with their PSR-4 case-sensitive name as a value. * * @var array */ private static $deprecated_classes = [ // Interfaces. 'requests_auth' => '\WpOrg\Requests\Auth', 'requests_hooker' => '\WpOrg\Requests\HookManager', 'requests_proxy' => '\WpOrg\Requests\Proxy', 'requests_transport' => '\WpOrg\Requests\Transport', // Classes. 'requests_cookie' => '\WpOrg\Requests\Cookie', 'requests_exception' => '\WpOrg\Requests\Exception', 'requests_hooks' => '\WpOrg\Requests\Hooks', 'requests_idnaencoder' => '\WpOrg\Requests\IdnaEncoder', 'requests_ipv6' => '\WpOrg\Requests\Ipv6', 'requests_iri' => '\WpOrg\Requests\Iri', 'requests_response' => '\WpOrg\Requests\Response', 'requests_session' => '\WpOrg\Requests\Session', 'requests_ssl' => '\WpOrg\Requests\Ssl', 'requests_auth_basic' => '\WpOrg\Requests\Auth\Basic', 'requests_cookie_jar' => '\WpOrg\Requests\Cookie\Jar', 'requests_proxy_http' => '\WpOrg\Requests\Proxy\Http', 'requests_response_headers' => '\WpOrg\Requests\Response\Headers', 'requests_transport_curl' => '\WpOrg\Requests\Transport\Curl', 'requests_transport_fsockopen' => '\WpOrg\Requests\Transport\Fsockopen', 'requests_utility_caseinsensitivedictionary' => '\WpOrg\Requests\Utility\CaseInsensitiveDictionary', 'requests_utility_filterediterator' => '\WpOrg\Requests\Utility\FilteredIterator', 'requests_exception_http' => '\WpOrg\Requests\Exception\Http', 'requests_exception_transport' => '\WpOrg\Requests\Exception\Transport', 'requests_exception_transport_curl' => '\WpOrg\Requests\Exception\Transport\Curl', 'requests_exception_http_304' => '\WpOrg\Requests\Exception\Http\Status304', 'requests_exception_http_305' => '\WpOrg\Requests\Exception\Http\Status305', 'requests_exception_http_306' => '\WpOrg\Requests\Exception\Http\Status306', 'requests_exception_http_400' => '\WpOrg\Requests\Exception\Http\Status400', 'requests_exception_http_401' => '\WpOrg\Requests\Exception\Http\Status401', 'requests_exception_http_402' => '\WpOrg\Requests\Exception\Http\Status402', 'requests_exception_http_403' => '\WpOrg\Requests\Exception\Http\Status403', 'requests_exception_http_404' => '\WpOrg\Requests\Exception\Http\Status404', 'requests_exception_http_405' => '\WpOrg\Requests\Exception\Http\Status405', 'requests_exception_http_406' => '\WpOrg\Requests\Exception\Http\Status406', 'requests_exception_http_407' => '\WpOrg\Requests\Exception\Http\Status407', 'requests_exception_http_408' => '\WpOrg\Requests\Exception\Http\Status408', 'requests_exception_http_409' => '\WpOrg\Requests\Exception\Http\Status409', 'requests_exception_http_410' => '\WpOrg\Requests\Exception\Http\Status410', 'requests_exception_http_411' => '\WpOrg\Requests\Exception\Http\Status411', 'requests_exception_http_412' => '\WpOrg\Requests\Exception\Http\Status412', 'requests_exception_http_413' => '\WpOrg\Requests\Exception\Http\Status413', 'requests_exception_http_414' => '\WpOrg\Requests\Exception\Http\Status414', 'requests_exception_http_415' => '\WpOrg\Requests\Exception\Http\Status415', 'requests_exception_http_416' => '\WpOrg\Requests\Exception\Http\Status416', 'requests_exception_http_417' => '\WpOrg\Requests\Exception\Http\Status417', 'requests_exception_http_418' => '\WpOrg\Requests\Exception\Http\Status418', 'requests_exception_http_428' => '\WpOrg\Requests\Exception\Http\Status428', 'requests_exception_http_429' => '\WpOrg\Requests\Exception\Http\Status429', 'requests_exception_http_431' => '\WpOrg\Requests\Exception\Http\Status431', 'requests_exception_http_500' => '\WpOrg\Requests\Exception\Http\Status500', 'requests_exception_http_501' => '\WpOrg\Requests\Exception\Http\Status501', 'requests_exception_http_502' => '\WpOrg\Requests\Exception\Http\Status502', 'requests_exception_http_503' => '\WpOrg\Requests\Exception\Http\Status503', 'requests_exception_http_504' => '\WpOrg\Requests\Exception\Http\Status504', 'requests_exception_http_505' => '\WpOrg\Requests\Exception\Http\Status505', 'requests_exception_http_511' => '\WpOrg\Requests\Exception\Http\Status511', 'requests_exception_http_unknown' => '\WpOrg\Requests\Exception\Http\StatusUnknown', ]; /** * Register the autoloader. * * Note: the autoloader is *prepended* in the autoload queue. * This is done to ensure that the Requests 2.0 autoloader takes precedence * over a potentially (dependency-registered) Requests 1.x autoloader. * * @internal This method contains a safeguard against the autoloader being * registered multiple times. This safeguard uses a global constant to * (hopefully/in most cases) still function correctly, even if the * class would be renamed. * * @return void */ public static function register() { if (defined('REQUESTS_AUTOLOAD_REGISTERED') === false) { spl_autoload_register([self::class, 'load'], true); define('REQUESTS_AUTOLOAD_REGISTERED', true); } } /** * Autoloader. * * @param string $class_name Name of the class name to load. * * @return bool Whether a class was loaded or not. */ public static function load($class_name) { // Check that the class starts with "Requests" (PSR-0) or "WpOrg\Requests" (PSR-4). $psr_4_prefix_pos = strpos($class_name, 'WpOrg\\Requests\\'); if (stripos($class_name, 'Requests') !== 0 && $psr_4_prefix_pos !== 0) { return false; } $class_lower = strtolower($class_name); if ($class_lower === 'requests') { // Reference to the original PSR-0 Requests class. $file = dirname(__DIR__) . '/library/Requests.php'; } elseif ($psr_4_prefix_pos === 0) { // PSR-4 classname. $file = __DIR__ . '/' . strtr(substr($class_name, 15), '\\', '/') . '.php'; } if (isset($file) && file_exists($file)) { include $file; return true; } /* * Okay, so the class starts with "Requests", but we couldn't find the file. * If this is one of the deprecated/renamed PSR-0 classes being requested, * let's alias it to the new name and throw a deprecation notice. */ if (isset(self::$deprecated_classes[$class_lower])) { /* * Integrators who cannot yet upgrade to the PSR-4 class names can silence deprecations * by defining a `REQUESTS_SILENCE_PSR0_DEPRECATIONS` constant and setting it to `true`. * The constant needs to be defined before the first deprecated class is requested * via this autoloader. */ if (!defined('REQUESTS_SILENCE_PSR0_DEPRECATIONS') || REQUESTS_SILENCE_PSR0_DEPRECATIONS !== true) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error trigger_error( 'The PSR-0 `Requests_...` class names in the Requests library are deprecated.' . ' Switch to the PSR-4 `WpOrg\Requests\...` class names at your earliest convenience.', E_USER_DEPRECATED ); // Prevent the deprecation notice from being thrown twice. if (!defined('REQUESTS_SILENCE_PSR0_DEPRECATIONS')) { define('REQUESTS_SILENCE_PSR0_DEPRECATIONS', true); } } // Create an alias and let the autoloader recursively kick in to load the PSR-4 class. return class_alias(self::$deprecated_classes[$class_lower], $class_name, true); } return false; } } } Cookie.php 0000644 00000036035 15174671662 0006514 0 ustar 00 <?php /** * Cookie storage object * * @package Requests\Cookies */ namespace WpOrg\Requests; use WpOrg\Requests\Exception\InvalidArgument; use WpOrg\Requests\Iri; use WpOrg\Requests\Response\Headers; use WpOrg\Requests\Utility\CaseInsensitiveDictionary; use WpOrg\Requests\Utility\InputValidator; /** * Cookie storage object * * @package Requests\Cookies */ class Cookie { /** * Cookie name. * * @var string */ public $name; /** * Cookie value. * * @var string */ public $value; /** * Cookie attributes * * Valid keys are `'path'`, `'domain'`, `'expires'`, `'max-age'`, `'secure'` and * `'httponly'`. * * @var \WpOrg\Requests\Utility\CaseInsensitiveDictionary|array Array-like object */ public $attributes = []; /** * Cookie flags * * Valid keys are `'creation'`, `'last-access'`, `'persistent'` and `'host-only'`. * * @var array */ public $flags = []; /** * Reference time for relative calculations * * This is used in place of `time()` when calculating Max-Age expiration and * checking time validity. * * @var int */ public $reference_time = 0; /** * Create a new cookie object * * @param string $name The name of the cookie. * @param string $value The value for the cookie. * @param array|\WpOrg\Requests\Utility\CaseInsensitiveDictionary $attributes Associative array of attribute data * @param array $flags The flags for the cookie. * Valid keys are `'creation'`, `'last-access'`, * `'persistent'` and `'host-only'`. * @param int|null $reference_time Reference time for relative calculations. * * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $name argument is not a string. * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $value argument is not a string. * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $attributes argument is not an array or iterable object with array access. * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $flags argument is not an array. * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $reference_time argument is not an integer or null. */ public function __construct($name, $value, $attributes = [], $flags = [], $reference_time = null) { if (is_string($name) === false) { throw InvalidArgument::create(1, '$name', 'string', gettype($name)); } if (is_string($value) === false) { throw InvalidArgument::create(2, '$value', 'string', gettype($value)); } if (InputValidator::has_array_access($attributes) === false || InputValidator::is_iterable($attributes) === false) { throw InvalidArgument::create(3, '$attributes', 'array|ArrayAccess&Traversable', gettype($attributes)); } if (is_array($flags) === false) { throw InvalidArgument::create(4, '$flags', 'array', gettype($flags)); } if ($reference_time !== null && is_int($reference_time) === false) { throw InvalidArgument::create(5, '$reference_time', 'integer|null', gettype($reference_time)); } $this->name = $name; $this->value = $value; $this->attributes = $attributes; $default_flags = [ 'creation' => time(), 'last-access' => time(), 'persistent' => false, 'host-only' => true, ]; $this->flags = array_merge($default_flags, $flags); $this->reference_time = time(); if ($reference_time !== null) { $this->reference_time = $reference_time; } $this->normalize(); } /** * Get the cookie value * * Attributes and other data can be accessed via methods. */ public function __toString() { return $this->value; } /** * Check if a cookie is expired. * * Checks the age against $this->reference_time to determine if the cookie * is expired. * * @return boolean True if expired, false if time is valid. */ public function is_expired() { // RFC6265, s. 4.1.2.2: // If a cookie has both the Max-Age and the Expires attribute, the Max- // Age attribute has precedence and controls the expiration date of the // cookie. if (isset($this->attributes['max-age'])) { $max_age = $this->attributes['max-age']; return $max_age < $this->reference_time; } if (isset($this->attributes['expires'])) { $expires = $this->attributes['expires']; return $expires < $this->reference_time; } return false; } /** * Check if a cookie is valid for a given URI * * @param \WpOrg\Requests\Iri $uri URI to check * @return boolean Whether the cookie is valid for the given URI */ public function uri_matches(Iri $uri) { if (!$this->domain_matches($uri->host)) { return false; } if (!$this->path_matches($uri->path)) { return false; } return empty($this->attributes['secure']) || $uri->scheme === 'https'; } /** * Check if a cookie is valid for a given domain * * @param string $domain Domain to check * @return boolean Whether the cookie is valid for the given domain */ public function domain_matches($domain) { if (is_string($domain) === false) { return false; } if (!isset($this->attributes['domain'])) { // Cookies created manually; cookies created by Requests will set // the domain to the requested domain return true; } $cookie_domain = $this->attributes['domain']; if ($cookie_domain === $domain) { // The cookie domain and the passed domain are identical. return true; } // If the cookie is marked as host-only and we don't have an exact // match, reject the cookie if ($this->flags['host-only'] === true) { return false; } if (strlen($domain) <= strlen($cookie_domain)) { // For obvious reasons, the cookie domain cannot be a suffix if the passed domain // is shorter than the cookie domain return false; } if (substr($domain, -1 * strlen($cookie_domain)) !== $cookie_domain) { // The cookie domain should be a suffix of the passed domain. return false; } $prefix = substr($domain, 0, strlen($domain) - strlen($cookie_domain)); if (substr($prefix, -1) !== '.') { // The last character of the passed domain that is not included in the // domain string should be a %x2E (".") character. return false; } // The passed domain should be a host name (i.e., not an IP address). return !preg_match('#^(.+\.)\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$#', $domain); } /** * Check if a cookie is valid for a given path * * From the path-match check in RFC 6265 section 5.1.4 * * @param string $request_path Path to check * @return boolean Whether the cookie is valid for the given path */ public function path_matches($request_path) { if (empty($request_path)) { // Normalize empty path to root $request_path = '/'; } if (!isset($this->attributes['path'])) { // Cookies created manually; cookies created by Requests will set // the path to the requested path return true; } if (is_scalar($request_path) === false) { return false; } $cookie_path = $this->attributes['path']; if ($cookie_path === $request_path) { // The cookie-path and the request-path are identical. return true; } if (strlen($request_path) > strlen($cookie_path) && substr($request_path, 0, strlen($cookie_path)) === $cookie_path) { if (substr($cookie_path, -1) === '/') { // The cookie-path is a prefix of the request-path, and the last // character of the cookie-path is %x2F ("/"). return true; } if (substr($request_path, strlen($cookie_path), 1) === '/') { // The cookie-path is a prefix of the request-path, and the // first character of the request-path that is not included in // the cookie-path is a %x2F ("/") character. return true; } } return false; } /** * Normalize cookie and attributes * * @return boolean Whether the cookie was successfully normalized */ public function normalize() { foreach ($this->attributes as $key => $value) { $orig_value = $value; if (is_string($key)) { $value = $this->normalize_attribute($key, $value); } if ($value === null) { unset($this->attributes[$key]); continue; } if ($value !== $orig_value) { $this->attributes[$key] = $value; } } return true; } /** * Parse an individual cookie attribute * * Handles parsing individual attributes from the cookie values. * * @param string $name Attribute name * @param string|int|bool $value Attribute value (string/integer value, or true if empty/flag) * @return mixed Value if available, or null if the attribute value is invalid (and should be skipped) */ protected function normalize_attribute($name, $value) { switch (strtolower($name)) { case 'expires': // Expiration parsing, as per RFC 6265 section 5.2.1 if (is_int($value)) { return $value; } $expiry_time = strtotime($value); if ($expiry_time === false) { return null; } return $expiry_time; case 'max-age': // Expiration parsing, as per RFC 6265 section 5.2.2 if (is_int($value)) { return $value; } // Check that we have a valid age if (!preg_match('/^-?\d+$/', $value)) { return null; } $delta_seconds = (int) $value; if ($delta_seconds <= 0) { $expiry_time = 0; } else { $expiry_time = $this->reference_time + $delta_seconds; } return $expiry_time; case 'domain': // Domains are not required as per RFC 6265 section 5.2.3 if (empty($value)) { return null; } // Domain normalization, as per RFC 6265 section 5.2.3 if ($value[0] === '.') { $value = substr($value, 1); } return $value; default: return $value; } } /** * Format a cookie for a Cookie header * * This is used when sending cookies to a server. * * @return string Cookie formatted for Cookie header */ public function format_for_header() { return sprintf('%s=%s', $this->name, $this->value); } /** * Format a cookie for a Set-Cookie header * * This is used when sending cookies to clients. This isn't really * applicable to client-side usage, but might be handy for debugging. * * @return string Cookie formatted for Set-Cookie header */ public function format_for_set_cookie() { $header_value = $this->format_for_header(); if (!empty($this->attributes)) { $parts = []; foreach ($this->attributes as $key => $value) { // Ignore non-associative attributes if (is_numeric($key)) { $parts[] = $value; } else { $parts[] = sprintf('%s=%s', $key, $value); } } $header_value .= '; ' . implode('; ', $parts); } return $header_value; } /** * Parse a cookie string into a cookie object * * Based on Mozilla's parsing code in Firefox and related projects, which * is an intentional deviation from RFC 2109 and RFC 2616. RFC 6265 * specifies some of this handling, but not in a thorough manner. * * @param string $cookie_header Cookie header value (from a Set-Cookie header) * @param string $name * @param int|null $reference_time * @return \WpOrg\Requests\Cookie Parsed cookie object * * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $cookie_header argument is not a string. * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $name argument is not a string. */ public static function parse($cookie_header, $name = '', $reference_time = null) { if (is_string($cookie_header) === false) { throw InvalidArgument::create(1, '$cookie_header', 'string', gettype($cookie_header)); } if (is_string($name) === false) { throw InvalidArgument::create(2, '$name', 'string', gettype($name)); } $parts = explode(';', $cookie_header); $kvparts = array_shift($parts); if (!empty($name)) { $value = $cookie_header; } elseif (strpos($kvparts, '=') === false) { // Some sites might only have a value without the equals separator. // Deviate from RFC 6265 and pretend it was actually a blank name // (`=foo`) // // https://bugzilla.mozilla.org/show_bug.cgi?id=169091 $name = ''; $value = $kvparts; } else { list($name, $value) = explode('=', $kvparts, 2); } $name = trim($name); $value = trim($value); // Attribute keys are handled case-insensitively $attributes = new CaseInsensitiveDictionary(); if (!empty($parts)) { foreach ($parts as $part) { if (strpos($part, '=') === false) { $part_key = $part; $part_value = true; } else { list($part_key, $part_value) = explode('=', $part, 2); $part_value = trim($part_value); } $part_key = trim($part_key); $attributes[$part_key] = $part_value; } } return new static($name, $value, $attributes, [], $reference_time); } /** * Parse all Set-Cookie headers from request headers * * @param \WpOrg\Requests\Response\Headers $headers Headers to parse from * @param \WpOrg\Requests\Iri|null $origin URI for comparing cookie origins * @param int|null $time Reference time for expiration calculation * @return array * * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $origin argument is not null or an instance of the Iri class. */ public static function parse_from_headers(Headers $headers, $origin = null, $time = null) { $cookie_headers = $headers->getValues('Set-Cookie'); if (empty($cookie_headers)) { return []; } if ($origin !== null && !($origin instanceof Iri)) { throw InvalidArgument::create(2, '$origin', Iri::class . ' or null', gettype($origin)); } $cookies = []; foreach ($cookie_headers as $header) { $parsed = self::parse($header, '', $time); // Default domain/path attributes if (empty($parsed->attributes['domain']) && !empty($origin)) { $parsed->attributes['domain'] = $origin->host; $parsed->flags['host-only'] = true; } else { $parsed->flags['host-only'] = false; } $path_is_valid = (!empty($parsed->attributes['path']) && $parsed->attributes['path'][0] === '/'); if (!$path_is_valid && !empty($origin)) { $path = $origin->path; // Default path normalization as per RFC 6265 section 5.1.4 if (substr($path, 0, 1) !== '/') { // If the uri-path is empty or if the first character of // the uri-path is not a %x2F ("/") character, output // %x2F ("/") and skip the remaining steps. $path = '/'; } elseif (substr_count($path, '/') === 1) { // If the uri-path contains no more than one %x2F ("/") // character, output %x2F ("/") and skip the remaining // step. $path = '/'; } else { // Output the characters of the uri-path from the first // character up to, but not including, the right-most // %x2F ("/"). $path = substr($path, 0, strrpos($path, '/')); } $parsed->attributes['path'] = $path; } // Reject invalid cookie domains if (!empty($origin) && !$parsed->domain_matches($origin->host)) { continue; } $cookies[$parsed->name] = $parsed; } return $cookies; } } HookManager.php 0000644 00000001305 15174671662 0007466 0 ustar 00 <?php /** * Event dispatcher * * @package Requests\EventDispatcher */ namespace WpOrg\Requests; /** * Event dispatcher * * @package Requests\EventDispatcher */ interface HookManager { /** * Register a callback for a hook * * @param string $hook Hook name * @param callable $callback Function/method to call on event * @param int $priority Priority number. <0 is executed earlier, >0 is executed later */ public function register($hook, $callback, $priority = 0); /** * Dispatch a message * * @param string $hook Hook name * @param array $parameters Parameters to pass to callbacks * @return boolean Successfulness */ public function dispatch($hook, $parameters = []); } Proxy/Http.php 0000644 00000010171 15174671662 0007334 0 ustar 00 <?php /** * HTTP Proxy connection interface * * @package Requests\Proxy * @since 1.6 */ namespace WpOrg\Requests\Proxy; use WpOrg\Requests\Exception\ArgumentCount; use WpOrg\Requests\Exception\InvalidArgument; use WpOrg\Requests\Hooks; use WpOrg\Requests\Proxy; /** * HTTP Proxy connection interface * * Provides a handler for connection via an HTTP proxy * * @package Requests\Proxy * @since 1.6 */ final class Http implements Proxy { /** * Proxy host and port * * Notation: "host:port" (eg 127.0.0.1:8080 or someproxy.com:3128) * * @var string */ public $proxy; /** * Username * * @var string */ public $user; /** * Password * * @var string */ public $pass; /** * Do we need to authenticate? (ie username & password have been provided) * * @var boolean */ public $use_authentication; /** * Constructor * * @since 1.6 * * @param array|string|null $args Proxy as a string or an array of proxy, user and password. * When passed as an array, must have exactly one (proxy) * or three elements (proxy, user, password). * * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed argument is not an array, a string or null. * @throws \WpOrg\Requests\Exception\ArgumentCount On incorrect number of arguments (`proxyhttpbadargs`) */ public function __construct($args = null) { if (is_string($args)) { $this->proxy = $args; } elseif (is_array($args)) { if (count($args) === 1) { list($this->proxy) = $args; } elseif (count($args) === 3) { list($this->proxy, $this->user, $this->pass) = $args; $this->use_authentication = true; } else { throw ArgumentCount::create( 'an array with exactly one element or exactly three elements', count($args), 'proxyhttpbadargs' ); } } elseif ($args !== null) { throw InvalidArgument::create(1, '$args', 'array|string|null', gettype($args)); } } /** * Register the necessary callbacks * * @since 1.6 * @see \WpOrg\Requests\Proxy\Http::curl_before_send() * @see \WpOrg\Requests\Proxy\Http::fsockopen_remote_socket() * @see \WpOrg\Requests\Proxy\Http::fsockopen_remote_host_path() * @see \WpOrg\Requests\Proxy\Http::fsockopen_header() * @param \WpOrg\Requests\Hooks $hooks Hook system */ public function register(Hooks $hooks) { $hooks->register('curl.before_send', [$this, 'curl_before_send']); $hooks->register('fsockopen.remote_socket', [$this, 'fsockopen_remote_socket']); $hooks->register('fsockopen.remote_host_path', [$this, 'fsockopen_remote_host_path']); if ($this->use_authentication) { $hooks->register('fsockopen.after_headers', [$this, 'fsockopen_header']); } } /** * Set cURL parameters before the data is sent * * @since 1.6 * @param resource|\CurlHandle $handle cURL handle */ public function curl_before_send(&$handle) { curl_setopt($handle, CURLOPT_PROXYTYPE, CURLPROXY_HTTP); curl_setopt($handle, CURLOPT_PROXY, $this->proxy); if ($this->use_authentication) { curl_setopt($handle, CURLOPT_PROXYAUTH, CURLAUTH_ANY); curl_setopt($handle, CURLOPT_PROXYUSERPWD, $this->get_auth_string()); } } /** * Alter remote socket information before opening socket connection * * @since 1.6 * @param string $remote_socket Socket connection string */ public function fsockopen_remote_socket(&$remote_socket) { $remote_socket = $this->proxy; } /** * Alter remote path before getting stream data * * @since 1.6 * @param string $path Path to send in HTTP request string ("GET ...") * @param string $url Full URL we're requesting */ public function fsockopen_remote_host_path(&$path, $url) { $path = $url; } /** * Add extra headers to the request before sending * * @since 1.6 * @param string $out HTTP header string */ public function fsockopen_header(&$out) { $out .= sprintf("Proxy-Authorization: Basic %s\r\n", base64_encode($this->get_auth_string())); } /** * Get the authentication string (user:pass) * * @since 1.6 * @return string */ public function get_auth_string() { return $this->user . ':' . $this->pass; } } Exception.php 0000644 00000002132 15174671662 0007230 0 ustar 00 <?php /** * Exception for HTTP requests * * @package Requests\Exceptions */ namespace WpOrg\Requests; use Exception as PHPException; /** * Exception for HTTP requests * * @package Requests\Exceptions */ class Exception extends PHPException { /** * Type of exception * * @var string */ protected $type; /** * Data associated with the exception * * @var mixed */ protected $data; /** * Create a new exception * * @param string $message Exception message * @param string $type Exception type * @param mixed $data Associated data * @param integer $code Exception numerical code, if applicable */ public function __construct($message, $type, $data = null, $code = 0) { parent::__construct($message, $code); $this->type = $type; $this->data = $data; } /** * Like {@see \Exception::getCode()}, but a string code. * * @codeCoverageIgnore * @return string */ public function getType() { return $this->type; } /** * Gives any relevant data * * @codeCoverageIgnore * @return mixed */ public function getData() { return $this->data; } } Utility/CaseInsensitiveDictionary.php 0000644 00000004713 15174671662 0014066 0 ustar 00 <?php /** * Case-insensitive dictionary, suitable for HTTP headers * * @package Requests\Utilities */ namespace WpOrg\Requests\Utility; use ArrayAccess; use ArrayIterator; use IteratorAggregate; use ReturnTypeWillChange; use WpOrg\Requests\Exception; /** * Case-insensitive dictionary, suitable for HTTP headers * * @package Requests\Utilities */ class CaseInsensitiveDictionary implements ArrayAccess, IteratorAggregate { /** * Actual item data * * @var array */ protected $data = []; /** * Creates a case insensitive dictionary. * * @param array $data Dictionary/map to convert to case-insensitive */ public function __construct(array $data = []) { foreach ($data as $offset => $value) { $this->offsetSet($offset, $value); } } /** * Check if the given item exists * * @param string $offset Item key * @return boolean Does the item exist? */ #[ReturnTypeWillChange] public function offsetExists($offset) { if (is_string($offset)) { $offset = strtolower($offset); } return isset($this->data[$offset]); } /** * Get the value for the item * * @param string $offset Item key * @return string|null Item value (null if the item key doesn't exist) */ #[ReturnTypeWillChange] public function offsetGet($offset) { if (is_string($offset)) { $offset = strtolower($offset); } if (!isset($this->data[$offset])) { return null; } return $this->data[$offset]; } /** * Set the given item * * @param string $offset Item name * @param string $value Item value * * @throws \WpOrg\Requests\Exception On attempting to use dictionary as list (`invalidset`) */ #[ReturnTypeWillChange] public function offsetSet($offset, $value) { if ($offset === null) { throw new Exception('Object is a dictionary, not a list', 'invalidset'); } if (is_string($offset)) { $offset = strtolower($offset); } $this->data[$offset] = $value; } /** * Unset the given header * * @param string $offset The key for the item to unset. */ #[ReturnTypeWillChange] public function offsetUnset($offset) { if (is_string($offset)) { $offset = strtolower($offset); } unset($this->data[$offset]); } /** * Get an iterator for the data * * @return \ArrayIterator */ #[ReturnTypeWillChange] public function getIterator() { return new ArrayIterator($this->data); } /** * Get the headers as an array * * @return array Header data */ public function getAll() { return $this->data; } } Utility/FilteredIterator.php 0000644 00000004155 15174671662 0012214 0 ustar 00 <?php /** * Iterator for arrays requiring filtered values * * @package Requests\Utilities */ namespace WpOrg\Requests\Utility; use ArrayIterator; use ReturnTypeWillChange; use WpOrg\Requests\Exception\InvalidArgument; use WpOrg\Requests\Utility\InputValidator; /** * Iterator for arrays requiring filtered values * * @package Requests\Utilities */ final class FilteredIterator extends ArrayIterator { /** * Callback to run as a filter * * @var callable */ private $callback; /** * Create a new iterator * * @param array $data The array or object to be iterated on. * @param callable $callback Callback to be called on each value * * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $data argument is not iterable. */ public function __construct($data, $callback) { if (InputValidator::is_iterable($data) === false) { throw InvalidArgument::create(1, '$data', 'iterable', gettype($data)); } parent::__construct($data); if (is_callable($callback)) { $this->callback = $callback; } } /** * Prevent unserialization of the object for security reasons. * * @phpcs:disable PHPCompatibility.FunctionNameRestrictions.NewMagicMethods.__unserializeFound * * @param array $data Restored array of data originally serialized. * * @return void */ #[ReturnTypeWillChange] public function __unserialize($data) {} // phpcs:enable /** * Perform reinitialization tasks. * * Prevents a callback from being injected during unserialization of an object. * * @return void */ public function __wakeup() { unset($this->callback); } /** * Get the current item's value after filtering * * @return string */ #[ReturnTypeWillChange] public function current() { $value = parent::current(); if (is_callable($this->callback)) { $value = call_user_func($this->callback, $value); } return $value; } /** * Prevent creating a PHP value from a stored representation of the object for security reasons. * * @param string $data The serialized string. * * @return void */ #[ReturnTypeWillChange] public function unserialize($data) {} } Utility/InputValidator.php 0000644 00000004720 15174671662 0011707 0 ustar 00 <?php /** * Input validation utilities. * * @package Requests\Utilities */ namespace WpOrg\Requests\Utility; use ArrayAccess; use CurlHandle; use Traversable; /** * Input validation utilities. * * @package Requests\Utilities */ final class InputValidator { /** * Verify that a received input parameter is of type string or is "stringable". * * @param mixed $input Input parameter to verify. * * @return bool */ public static function is_string_or_stringable($input) { return is_string($input) || self::is_stringable_object($input); } /** * Verify whether a received input parameter is usable as an integer array key. * * @param mixed $input Input parameter to verify. * * @return bool */ public static function is_numeric_array_key($input) { if (is_int($input)) { return true; } if (!is_string($input)) { return false; } return (bool) preg_match('`^-?[0-9]+$`', $input); } /** * Verify whether a received input parameter is "stringable". * * @param mixed $input Input parameter to verify. * * @return bool */ public static function is_stringable_object($input) { return is_object($input) && method_exists($input, '__toString'); } /** * Verify whether a received input parameter is _accessible as if it were an array_. * * @param mixed $input Input parameter to verify. * * @return bool */ public static function has_array_access($input) { return is_array($input) || $input instanceof ArrayAccess; } /** * Verify whether a received input parameter is "iterable". * * @internal The PHP native `is_iterable()` function was only introduced in PHP 7.1 * and this library still supports PHP 5.6. * * @param mixed $input Input parameter to verify. * * @return bool */ public static function is_iterable($input) { return is_array($input) || $input instanceof Traversable; } /** * Verify whether a received input parameter is a Curl handle. * * The PHP Curl extension worked with resources prior to PHP 8.0 and with * an instance of the `CurlHandle` class since PHP 8.0. * {@link https://www.php.net/manual/en/migration80.incompatible.php#migration80.incompatible.resource2object} * * @param mixed $input Input parameter to verify. * * @return bool */ public static function is_curl_handle($input) { if (is_resource($input)) { return get_resource_type($input) === 'curl'; } if (is_object($input)) { return $input instanceof CurlHandle; } return false; } } IdnaEncoder.php 0000644 00000030223 15174671662 0007447 0 ustar 00 <?php namespace WpOrg\Requests; use WpOrg\Requests\Exception; use WpOrg\Requests\Exception\InvalidArgument; use WpOrg\Requests\Utility\InputValidator; /** * IDNA URL encoder * * Note: Not fully compliant, as nameprep does nothing yet. * * @package Requests\Utilities * * @link https://tools.ietf.org/html/rfc3490 IDNA specification * @link https://tools.ietf.org/html/rfc3492 Punycode/Bootstrap specification */ class IdnaEncoder { /** * ACE prefix used for IDNA * * @link https://tools.ietf.org/html/rfc3490#section-5 * @var string */ const ACE_PREFIX = 'xn--'; /** * Maximum length of a IDNA URL in ASCII. * * @see \WpOrg\Requests\IdnaEncoder::to_ascii() * * @since 2.0.0 * * @var int */ const MAX_LENGTH = 64; /**#@+ * Bootstrap constant for Punycode * * @link https://tools.ietf.org/html/rfc3492#section-5 * @var int */ const BOOTSTRAP_BASE = 36; const BOOTSTRAP_TMIN = 1; const BOOTSTRAP_TMAX = 26; const BOOTSTRAP_SKEW = 38; const BOOTSTRAP_DAMP = 700; const BOOTSTRAP_INITIAL_BIAS = 72; const BOOTSTRAP_INITIAL_N = 128; /**#@-*/ /** * Encode a hostname using Punycode * * @param string|Stringable $hostname Hostname * @return string Punycode-encoded hostname * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed argument is not a string or a stringable object. */ public static function encode($hostname) { if (InputValidator::is_string_or_stringable($hostname) === false) { throw InvalidArgument::create(1, '$hostname', 'string|Stringable', gettype($hostname)); } $parts = explode('.', $hostname); foreach ($parts as &$part) { $part = self::to_ascii($part); } return implode('.', $parts); } /** * Convert a UTF-8 text string to an ASCII string using Punycode * * @param string $text ASCII or UTF-8 string (max length 64 characters) * @return string ASCII string * * @throws \WpOrg\Requests\Exception Provided string longer than 64 ASCII characters (`idna.provided_too_long`) * @throws \WpOrg\Requests\Exception Prepared string longer than 64 ASCII characters (`idna.prepared_too_long`) * @throws \WpOrg\Requests\Exception Provided string already begins with xn-- (`idna.provided_is_prefixed`) * @throws \WpOrg\Requests\Exception Encoded string longer than 64 ASCII characters (`idna.encoded_too_long`) */ public static function to_ascii($text) { // Step 1: Check if the text is already ASCII if (self::is_ascii($text)) { // Skip to step 7 if (strlen($text) < self::MAX_LENGTH) { return $text; } throw new Exception('Provided string is too long', 'idna.provided_too_long', $text); } // Step 2: nameprep $text = self::nameprep($text); // Step 3: UseSTD3ASCIIRules is false, continue // Step 4: Check if it's ASCII now if (self::is_ascii($text)) { // Skip to step 7 /* * As the `nameprep()` method returns the original string, this code will never be reached until * that method is properly implemented. */ // @codeCoverageIgnoreStart if (strlen($text) < self::MAX_LENGTH) { return $text; } throw new Exception('Prepared string is too long', 'idna.prepared_too_long', $text); // @codeCoverageIgnoreEnd } // Step 5: Check ACE prefix if (strpos($text, self::ACE_PREFIX) === 0) { throw new Exception('Provided string begins with ACE prefix', 'idna.provided_is_prefixed', $text); } // Step 6: Encode with Punycode $text = self::punycode_encode($text); // Step 7: Prepend ACE prefix $text = self::ACE_PREFIX . $text; // Step 8: Check size if (strlen($text) < self::MAX_LENGTH) { return $text; } throw new Exception('Encoded string is too long', 'idna.encoded_too_long', $text); } /** * Check whether a given text string contains only ASCII characters * * @internal (Testing found regex was the fastest implementation) * * @param string $text Text to examine. * @return bool Is the text string ASCII-only? */ protected static function is_ascii($text) { return (preg_match('/(?:[^\x00-\x7F])/', $text) !== 1); } /** * Prepare a text string for use as an IDNA name * * @todo Implement this based on RFC 3491 and the newer 5891 * @param string $text Text to prepare. * @return string Prepared string */ protected static function nameprep($text) { return $text; } /** * Convert a UTF-8 string to a UCS-4 codepoint array * * Based on \WpOrg\Requests\Iri::replace_invalid_with_pct_encoding() * * @param string $input Text to convert. * @return array Unicode code points * * @throws \WpOrg\Requests\Exception Invalid UTF-8 codepoint (`idna.invalidcodepoint`) */ protected static function utf8_to_codepoints($input) { $codepoints = []; // Get number of bytes $strlen = strlen($input); // phpcs:ignore Generic.CodeAnalysis.JumbledIncrementer -- This is a deliberate choice. for ($position = 0; $position < $strlen; $position++) { $value = ord($input[$position]); if ((~$value & 0x80) === 0x80) { // One byte sequence: $character = $value; $length = 1; $remaining = 0; } elseif (($value & 0xE0) === 0xC0) { // Two byte sequence: $character = ($value & 0x1F) << 6; $length = 2; $remaining = 1; } elseif (($value & 0xF0) === 0xE0) { // Three byte sequence: $character = ($value & 0x0F) << 12; $length = 3; $remaining = 2; } elseif (($value & 0xF8) === 0xF0) { // Four byte sequence: $character = ($value & 0x07) << 18; $length = 4; $remaining = 3; } else { // Invalid byte: throw new Exception('Invalid Unicode codepoint', 'idna.invalidcodepoint', $value); } if ($remaining > 0) { if ($position + $length > $strlen) { throw new Exception('Invalid Unicode codepoint', 'idna.invalidcodepoint', $character); } for ($position++; $remaining > 0; $position++) { $value = ord($input[$position]); // If it is invalid, count the sequence as invalid and reprocess the current byte: if (($value & 0xC0) !== 0x80) { throw new Exception('Invalid Unicode codepoint', 'idna.invalidcodepoint', $character); } --$remaining; $character |= ($value & 0x3F) << ($remaining * 6); } $position--; } if (// Non-shortest form sequences are invalid $length > 1 && $character <= 0x7F || $length > 2 && $character <= 0x7FF || $length > 3 && $character <= 0xFFFF // Outside of range of ucschar codepoints // Noncharacters || ($character & 0xFFFE) === 0xFFFE || $character >= 0xFDD0 && $character <= 0xFDEF || ( // Everything else not in ucschar $character > 0xD7FF && $character < 0xF900 || $character < 0x20 || $character > 0x7E && $character < 0xA0 || $character > 0xEFFFD ) ) { throw new Exception('Invalid Unicode codepoint', 'idna.invalidcodepoint', $character); } $codepoints[] = $character; } return $codepoints; } /** * RFC3492-compliant encoder * * @internal Pseudo-code from Section 6.3 is commented with "#" next to relevant code * * @param string $input UTF-8 encoded string to encode * @return string Punycode-encoded string * * @throws \WpOrg\Requests\Exception On character outside of the domain (never happens with Punycode) (`idna.character_outside_domain`) */ public static function punycode_encode($input) { $output = ''; // let n = initial_n $n = self::BOOTSTRAP_INITIAL_N; // let delta = 0 $delta = 0; // let bias = initial_bias $bias = self::BOOTSTRAP_INITIAL_BIAS; // let h = b = the number of basic code points in the input $h = 0; $b = 0; // see loop // copy them to the output in order $codepoints = self::utf8_to_codepoints($input); $extended = []; foreach ($codepoints as $char) { if ($char < 128) { // Character is valid ASCII // TODO: this should also check if it's valid for a URL $output .= chr($char); $h++; // Check if the character is non-ASCII, but below initial n // This never occurs for Punycode, so ignore in coverage // @codeCoverageIgnoreStart } elseif ($char < $n) { throw new Exception('Invalid character', 'idna.character_outside_domain', $char); // @codeCoverageIgnoreEnd } else { $extended[$char] = true; } } $extended = array_keys($extended); sort($extended); $b = $h; // [copy them] followed by a delimiter if b > 0 if (strlen($output) > 0) { $output .= '-'; } // {if the input contains a non-basic code point < n then fail} // while h < length(input) do begin $codepointcount = count($codepoints); while ($h < $codepointcount) { // let m = the minimum code point >= n in the input $m = array_shift($extended); //printf('next code point to insert is %s' . PHP_EOL, dechex($m)); // let delta = delta + (m - n) * (h + 1), fail on overflow $delta += ($m - $n) * ($h + 1); // let n = m $n = $m; // for each code point c in the input (in order) do begin for ($num = 0; $num < $codepointcount; $num++) { $c = $codepoints[$num]; // if c < n then increment delta, fail on overflow if ($c < $n) { $delta++; } elseif ($c === $n) { // if c == n then begin // let q = delta $q = $delta; // for k = base to infinity in steps of base do begin for ($k = self::BOOTSTRAP_BASE; ; $k += self::BOOTSTRAP_BASE) { // let t = tmin if k <= bias {+ tmin}, or // tmax if k >= bias + tmax, or k - bias otherwise if ($k <= ($bias + self::BOOTSTRAP_TMIN)) { $t = self::BOOTSTRAP_TMIN; } elseif ($k >= ($bias + self::BOOTSTRAP_TMAX)) { $t = self::BOOTSTRAP_TMAX; } else { $t = $k - $bias; } // if q < t then break if ($q < $t) { break; } // output the code point for digit t + ((q - t) mod (base - t)) $digit = (int) ($t + (($q - $t) % (self::BOOTSTRAP_BASE - $t))); $output .= self::digit_to_char($digit); // let q = (q - t) div (base - t) $q = (int) floor(($q - $t) / (self::BOOTSTRAP_BASE - $t)); } // end // output the code point for digit q $output .= self::digit_to_char($q); // let bias = adapt(delta, h + 1, test h equals b?) $bias = self::adapt($delta, $h + 1, $h === $b); // let delta = 0 $delta = 0; // increment h $h++; } // end } // end // increment delta and n $delta++; $n++; } // end return $output; } /** * Convert a digit to its respective character * * @link https://tools.ietf.org/html/rfc3492#section-5 * * @param int $digit Digit in the range 0-35 * @return string Single character corresponding to digit * * @throws \WpOrg\Requests\Exception On invalid digit (`idna.invalid_digit`) */ protected static function digit_to_char($digit) { // @codeCoverageIgnoreStart // As far as I know, this never happens, but still good to be sure. if ($digit < 0 || $digit > 35) { throw new Exception(sprintf('Invalid digit %d', $digit), 'idna.invalid_digit', $digit); } // @codeCoverageIgnoreEnd $digits = 'abcdefghijklmnopqrstuvwxyz0123456789'; return substr($digits, $digit, 1); } /** * Adapt the bias * * @link https://tools.ietf.org/html/rfc3492#section-6.1 * @param int $delta * @param int $numpoints * @param bool $firsttime * @return int|float New bias * * function adapt(delta,numpoints,firsttime): */ protected static function adapt($delta, $numpoints, $firsttime) { // if firsttime then let delta = delta div damp if ($firsttime) { $delta = floor($delta / self::BOOTSTRAP_DAMP); } else { // else let delta = delta div 2 $delta = floor($delta / 2); } // let delta = delta + (delta div numpoints) $delta += floor($delta / $numpoints); // let k = 0 $k = 0; // while delta > ((base - tmin) * tmax) div 2 do begin $max = floor(((self::BOOTSTRAP_BASE - self::BOOTSTRAP_TMIN) * self::BOOTSTRAP_TMAX) / 2); while ($delta > $max) { // let delta = delta div (base - tmin) $delta = floor($delta / (self::BOOTSTRAP_BASE - self::BOOTSTRAP_TMIN)); // let k = k + base $k += self::BOOTSTRAP_BASE; } // end // return k + (((base - tmin + 1) * delta) div (delta + skew)) return $k + floor(((self::BOOTSTRAP_BASE - self::BOOTSTRAP_TMIN + 1) * $delta) / ($delta + self::BOOTSTRAP_SKEW)); } } Transport/Curl.php 0000644 00000046163 15174671662 0010207 0 ustar 00 <?php /** * cURL HTTP transport * * @package Requests\Transport */ namespace WpOrg\Requests\Transport; use RecursiveArrayIterator; use RecursiveIteratorIterator; use WpOrg\Requests\Capability; use WpOrg\Requests\Exception; use WpOrg\Requests\Exception\InvalidArgument; use WpOrg\Requests\Exception\Transport\Curl as CurlException; use WpOrg\Requests\Requests; use WpOrg\Requests\Transport; use WpOrg\Requests\Utility\InputValidator; /** * cURL HTTP transport * * @package Requests\Transport */ final class Curl implements Transport { const CURL_7_10_5 = 0x070A05; const CURL_7_16_2 = 0x071002; /** * Raw HTTP data * * @var string */ public $headers = ''; /** * Raw body data * * @var string */ public $response_data = ''; /** * Information on the current request * * @var array cURL information array, see {@link https://www.php.net/curl_getinfo} */ public $info; /** * cURL version number * * @var int */ public $version; /** * cURL handle * * @var resource|\CurlHandle Resource in PHP < 8.0, Instance of CurlHandle in PHP >= 8.0. */ private $handle; /** * Hook dispatcher instance * * @var \WpOrg\Requests\Hooks */ private $hooks; /** * Have we finished the headers yet? * * @var boolean */ private $done_headers = false; /** * If streaming to a file, keep the file pointer * * @var resource */ private $stream_handle; /** * How many bytes are in the response body? * * @var int */ private $response_bytes; /** * What's the maximum number of bytes we should keep? * * @var int|bool Byte count, or false if no limit. */ private $response_byte_limit; /** * Constructor */ public function __construct() { $curl = curl_version(); $this->version = $curl['version_number']; $this->handle = curl_init(); curl_setopt($this->handle, CURLOPT_HEADER, false); curl_setopt($this->handle, CURLOPT_RETURNTRANSFER, 1); if ($this->version >= self::CURL_7_10_5) { curl_setopt($this->handle, CURLOPT_ENCODING, ''); } if (defined('CURLOPT_PROTOCOLS')) { // phpcs:ignore PHPCompatibility.Constants.NewConstants.curlopt_protocolsFound curl_setopt($this->handle, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS); } if (defined('CURLOPT_REDIR_PROTOCOLS')) { // phpcs:ignore PHPCompatibility.Constants.NewConstants.curlopt_redir_protocolsFound curl_setopt($this->handle, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS); } } /** * Destructor */ public function __destruct() { if (is_resource($this->handle)) { curl_close($this->handle); } } /** * Perform a request * * @param string|Stringable $url URL to request * @param array $headers Associative array of request headers * @param string|array $data Data to send either as the POST body, or as parameters in the URL for a GET/HEAD * @param array $options Request options, see {@see \WpOrg\Requests\Requests::response()} for documentation * @return string Raw HTTP result * * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $url argument is not a string or Stringable. * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $headers argument is not an array. * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $data parameter is not an array or string. * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $options argument is not an array. * @throws \WpOrg\Requests\Exception On a cURL error (`curlerror`) */ public function request($url, $headers = [], $data = [], $options = []) { if (InputValidator::is_string_or_stringable($url) === false) { throw InvalidArgument::create(1, '$url', 'string|Stringable', gettype($url)); } if (is_array($headers) === false) { throw InvalidArgument::create(2, '$headers', 'array', gettype($headers)); } if (!is_array($data) && !is_string($data)) { if ($data === null) { $data = ''; } else { throw InvalidArgument::create(3, '$data', 'array|string', gettype($data)); } } if (is_array($options) === false) { throw InvalidArgument::create(4, '$options', 'array', gettype($options)); } $this->hooks = $options['hooks']; $this->setup_handle($url, $headers, $data, $options); $options['hooks']->dispatch('curl.before_send', [&$this->handle]); if ($options['filename'] !== false) { // phpcs:ignore WordPress.PHP.NoSilencedErrors -- Silenced the PHP native warning in favour of throwing an exception. $this->stream_handle = @fopen($options['filename'], 'wb'); if ($this->stream_handle === false) { $error = error_get_last(); throw new Exception($error['message'], 'fopen'); } } $this->response_data = ''; $this->response_bytes = 0; $this->response_byte_limit = false; if ($options['max_bytes'] !== false) { $this->response_byte_limit = $options['max_bytes']; } if (isset($options['verify'])) { if ($options['verify'] === false) { curl_setopt($this->handle, CURLOPT_SSL_VERIFYHOST, 0); curl_setopt($this->handle, CURLOPT_SSL_VERIFYPEER, 0); } elseif (is_string($options['verify'])) { curl_setopt($this->handle, CURLOPT_CAINFO, $options['verify']); } } if (isset($options['verifyname']) && $options['verifyname'] === false) { curl_setopt($this->handle, CURLOPT_SSL_VERIFYHOST, 0); } curl_exec($this->handle); $response = $this->response_data; $options['hooks']->dispatch('curl.after_send', []); if (curl_errno($this->handle) === CURLE_WRITE_ERROR || curl_errno($this->handle) === CURLE_BAD_CONTENT_ENCODING) { // Reset encoding and try again curl_setopt($this->handle, CURLOPT_ENCODING, 'none'); $this->response_data = ''; $this->response_bytes = 0; curl_exec($this->handle); $response = $this->response_data; } $this->process_response($response, $options); // Need to remove the $this reference from the curl handle. // Otherwise \WpOrg\Requests\Transport\Curl won't be garbage collected and the curl_close() will never be called. curl_setopt($this->handle, CURLOPT_HEADERFUNCTION, null); curl_setopt($this->handle, CURLOPT_WRITEFUNCTION, null); return $this->headers; } /** * Send multiple requests simultaneously * * @param array $requests Request data * @param array $options Global options * @return array Array of \WpOrg\Requests\Response objects (may contain \WpOrg\Requests\Exception or string responses as well) * * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $requests argument is not an array or iterable object with array access. * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $options argument is not an array. */ public function request_multiple($requests, $options) { // If you're not requesting, we can't get any responses ¯\_(ツ)_/¯ if (empty($requests)) { return []; } if (InputValidator::has_array_access($requests) === false || InputValidator::is_iterable($requests) === false) { throw InvalidArgument::create(1, '$requests', 'array|ArrayAccess&Traversable', gettype($requests)); } if (is_array($options) === false) { throw InvalidArgument::create(2, '$options', 'array', gettype($options)); } $multihandle = curl_multi_init(); $subrequests = []; $subhandles = []; $class = get_class($this); foreach ($requests as $id => $request) { $subrequests[$id] = new $class(); $subhandles[$id] = $subrequests[$id]->get_subrequest_handle($request['url'], $request['headers'], $request['data'], $request['options']); $request['options']['hooks']->dispatch('curl.before_multi_add', [&$subhandles[$id]]); curl_multi_add_handle($multihandle, $subhandles[$id]); } $completed = 0; $responses = []; $subrequestcount = count($subrequests); $request['options']['hooks']->dispatch('curl.before_multi_exec', [&$multihandle]); do { $active = 0; do { $status = curl_multi_exec($multihandle, $active); } while ($status === CURLM_CALL_MULTI_PERFORM); $to_process = []; // Read the information as needed while ($done = curl_multi_info_read($multihandle)) { $key = array_search($done['handle'], $subhandles, true); if (!isset($to_process[$key])) { $to_process[$key] = $done; } } // Parse the finished requests before we start getting the new ones foreach ($to_process as $key => $done) { $options = $requests[$key]['options']; if ($done['result'] !== CURLE_OK) { //get error string for handle. $reason = curl_error($done['handle']); $exception = new CurlException( $reason, CurlException::EASY, $done['handle'], $done['result'] ); $responses[$key] = $exception; $options['hooks']->dispatch('transport.internal.parse_error', [&$responses[$key], $requests[$key]]); } else { $responses[$key] = $subrequests[$key]->process_response($subrequests[$key]->response_data, $options); $options['hooks']->dispatch('transport.internal.parse_response', [&$responses[$key], $requests[$key]]); } curl_multi_remove_handle($multihandle, $done['handle']); curl_close($done['handle']); if (!is_string($responses[$key])) { $options['hooks']->dispatch('multiple.request.complete', [&$responses[$key], $key]); } $completed++; } } while ($active || $completed < $subrequestcount); $request['options']['hooks']->dispatch('curl.after_multi_exec', [&$multihandle]); curl_multi_close($multihandle); return $responses; } /** * Get the cURL handle for use in a multi-request * * @param string $url URL to request * @param array $headers Associative array of request headers * @param string|array $data Data to send either as the POST body, or as parameters in the URL for a GET/HEAD * @param array $options Request options, see {@see \WpOrg\Requests\Requests::response()} for documentation * @return resource|\CurlHandle Subrequest's cURL handle */ public function &get_subrequest_handle($url, $headers, $data, $options) { $this->setup_handle($url, $headers, $data, $options); if ($options['filename'] !== false) { $this->stream_handle = fopen($options['filename'], 'wb'); } $this->response_data = ''; $this->response_bytes = 0; $this->response_byte_limit = false; if ($options['max_bytes'] !== false) { $this->response_byte_limit = $options['max_bytes']; } $this->hooks = $options['hooks']; return $this->handle; } /** * Setup the cURL handle for the given data * * @param string $url URL to request * @param array $headers Associative array of request headers * @param string|array $data Data to send either as the POST body, or as parameters in the URL for a GET/HEAD * @param array $options Request options, see {@see \WpOrg\Requests\Requests::response()} for documentation */ private function setup_handle($url, $headers, $data, $options) { $options['hooks']->dispatch('curl.before_request', [&$this->handle]); // Force closing the connection for old versions of cURL (<7.22). if (!isset($headers['Connection'])) { $headers['Connection'] = 'close'; } /** * Add "Expect" header. * * By default, cURL adds a "Expect: 100-Continue" to most requests. This header can * add as much as a second to the time it takes for cURL to perform a request. To * prevent this, we need to set an empty "Expect" header. To match the behaviour of * Guzzle, we'll add the empty header to requests that are smaller than 1 MB and use * HTTP/1.1. * * https://curl.se/mail/lib-2017-07/0013.html */ if (!isset($headers['Expect']) && $options['protocol_version'] === 1.1) { $headers['Expect'] = $this->get_expect_header($data); } $headers = Requests::flatten($headers); if (!empty($data)) { $data_format = $options['data_format']; if ($data_format === 'query') { $url = self::format_get($url, $data); $data = ''; } elseif (!is_string($data)) { $data = http_build_query($data, '', '&'); } } switch ($options['type']) { case Requests::POST: curl_setopt($this->handle, CURLOPT_POST, true); curl_setopt($this->handle, CURLOPT_POSTFIELDS, $data); break; case Requests::HEAD: curl_setopt($this->handle, CURLOPT_CUSTOMREQUEST, $options['type']); curl_setopt($this->handle, CURLOPT_NOBODY, true); break; case Requests::TRACE: curl_setopt($this->handle, CURLOPT_CUSTOMREQUEST, $options['type']); break; case Requests::PATCH: case Requests::PUT: case Requests::DELETE: case Requests::OPTIONS: default: curl_setopt($this->handle, CURLOPT_CUSTOMREQUEST, $options['type']); if (!empty($data)) { curl_setopt($this->handle, CURLOPT_POSTFIELDS, $data); } } // cURL requires a minimum timeout of 1 second when using the system // DNS resolver, as it uses `alarm()`, which is second resolution only. // There's no way to detect which DNS resolver is being used from our // end, so we need to round up regardless of the supplied timeout. // // https://github.com/curl/curl/blob/4f45240bc84a9aa648c8f7243be7b79e9f9323a5/lib/hostip.c#L606-L609 $timeout = max($options['timeout'], 1); if (is_int($timeout) || $this->version < self::CURL_7_16_2) { curl_setopt($this->handle, CURLOPT_TIMEOUT, ceil($timeout)); } else { // phpcs:ignore PHPCompatibility.Constants.NewConstants.curlopt_timeout_msFound curl_setopt($this->handle, CURLOPT_TIMEOUT_MS, round($timeout * 1000)); } if (is_int($options['connect_timeout']) || $this->version < self::CURL_7_16_2) { curl_setopt($this->handle, CURLOPT_CONNECTTIMEOUT, ceil($options['connect_timeout'])); } else { // phpcs:ignore PHPCompatibility.Constants.NewConstants.curlopt_connecttimeout_msFound curl_setopt($this->handle, CURLOPT_CONNECTTIMEOUT_MS, round($options['connect_timeout'] * 1000)); } curl_setopt($this->handle, CURLOPT_URL, $url); curl_setopt($this->handle, CURLOPT_USERAGENT, $options['useragent']); if (!empty($headers)) { curl_setopt($this->handle, CURLOPT_HTTPHEADER, $headers); } if ($options['protocol_version'] === 1.1) { curl_setopt($this->handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); } else { curl_setopt($this->handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0); } if ($options['blocking'] === true) { curl_setopt($this->handle, CURLOPT_HEADERFUNCTION, [$this, 'stream_headers']); curl_setopt($this->handle, CURLOPT_WRITEFUNCTION, [$this, 'stream_body']); curl_setopt($this->handle, CURLOPT_BUFFERSIZE, Requests::BUFFER_SIZE); } } /** * Process a response * * @param string $response Response data from the body * @param array $options Request options * @return string|false HTTP response data including headers. False if non-blocking. * @throws \WpOrg\Requests\Exception If the request resulted in a cURL error. */ public function process_response($response, $options) { if ($options['blocking'] === false) { $fake_headers = ''; $options['hooks']->dispatch('curl.after_request', [&$fake_headers]); return false; } if ($options['filename'] !== false && $this->stream_handle) { fclose($this->stream_handle); $this->headers = trim($this->headers); } else { $this->headers .= $response; } if (curl_errno($this->handle)) { $error = sprintf( 'cURL error %s: %s', curl_errno($this->handle), curl_error($this->handle) ); throw new Exception($error, 'curlerror', $this->handle); } $this->info = curl_getinfo($this->handle); $options['hooks']->dispatch('curl.after_request', [&$this->headers, &$this->info]); return $this->headers; } /** * Collect the headers as they are received * * @param resource|\CurlHandle $handle cURL handle * @param string $headers Header string * @return integer Length of provided header */ public function stream_headers($handle, $headers) { // Why do we do this? cURL will send both the final response and any // interim responses, such as a 100 Continue. We don't need that. // (We may want to keep this somewhere just in case) if ($this->done_headers) { $this->headers = ''; $this->done_headers = false; } $this->headers .= $headers; if ($headers === "\r\n") { $this->done_headers = true; } return strlen($headers); } /** * Collect data as it's received * * @since 1.6.1 * * @param resource|\CurlHandle $handle cURL handle * @param string $data Body data * @return integer Length of provided data */ public function stream_body($handle, $data) { $this->hooks->dispatch('request.progress', [$data, $this->response_bytes, $this->response_byte_limit]); $data_length = strlen($data); // Are we limiting the response size? if ($this->response_byte_limit) { if ($this->response_bytes === $this->response_byte_limit) { // Already at maximum, move on return $data_length; } if (($this->response_bytes + $data_length) > $this->response_byte_limit) { // Limit the length $limited_length = ($this->response_byte_limit - $this->response_bytes); $data = substr($data, 0, $limited_length); } } if ($this->stream_handle) { fwrite($this->stream_handle, $data); } else { $this->response_data .= $data; } $this->response_bytes += strlen($data); return $data_length; } /** * Format a URL given GET data * * @param string $url Original URL. * @param array|object $data Data to build query using, see {@link https://www.php.net/http_build_query} * @return string URL with data */ private static function format_get($url, $data) { if (!empty($data)) { $query = ''; $url_parts = parse_url($url); if (empty($url_parts['query'])) { $url_parts['query'] = ''; } else { $query = $url_parts['query']; } $query .= '&' . http_build_query($data, '', '&'); $query = trim($query, '&'); if (empty($url_parts['query'])) { $url .= '?' . $query; } else { $url = str_replace($url_parts['query'], $query, $url); } } return $url; } /** * Self-test whether the transport can be used. * * The available capabilities to test for can be found in {@see \WpOrg\Requests\Capability}. * * @codeCoverageIgnore * @param array<string, bool> $capabilities Optional. Associative array of capabilities to test against, i.e. `['<capability>' => true]`. * @return bool Whether the transport can be used. */ public static function test($capabilities = []) { if (!function_exists('curl_init') || !function_exists('curl_exec')) { return false; } // If needed, check that our installed curl version supports SSL if (isset($capabilities[Capability::SSL]) && $capabilities[Capability::SSL]) { $curl_version = curl_version(); if (!(CURL_VERSION_SSL & $curl_version['features'])) { return false; } } return true; } /** * Get the correct "Expect" header for the given request data. * * @param string|array $data Data to send either as the POST body, or as parameters in the URL for a GET/HEAD. * @return string The "Expect" header. */ private function get_expect_header($data) { if (!is_array($data)) { return strlen((string) $data) >= 1048576 ? '100-Continue' : ''; } $bytesize = 0; $iterator = new RecursiveIteratorIterator(new RecursiveArrayIterator($data)); foreach ($iterator as $datum) { $bytesize += strlen((string) $datum); if ($bytesize >= 1048576) { return '100-Continue'; } } return ''; } } Transport/Fsockopen.php 0000644 00000037033 15174671662 0011225 0 ustar 00 <?php /** * fsockopen HTTP transport * * @package Requests\Transport */ namespace WpOrg\Requests\Transport; use WpOrg\Requests\Capability; use WpOrg\Requests\Exception; use WpOrg\Requests\Exception\InvalidArgument; use WpOrg\Requests\Port; use WpOrg\Requests\Requests; use WpOrg\Requests\Ssl; use WpOrg\Requests\Transport; use WpOrg\Requests\Utility\CaseInsensitiveDictionary; use WpOrg\Requests\Utility\InputValidator; /** * fsockopen HTTP transport * * @package Requests\Transport */ final class Fsockopen implements Transport { /** * Second to microsecond conversion * * @var integer */ const SECOND_IN_MICROSECONDS = 1000000; /** * Raw HTTP data * * @var string */ public $headers = ''; /** * Stream metadata * * @var array Associative array of properties, see {@link https://www.php.net/stream_get_meta_data} */ public $info; /** * What's the maximum number of bytes we should keep? * * @var int|bool Byte count, or false if no limit. */ private $max_bytes = false; /** * Cache for received connection errors. * * @var string */ private $connect_error = ''; /** * Perform a request * * @param string|Stringable $url URL to request * @param array $headers Associative array of request headers * @param string|array $data Data to send either as the POST body, or as parameters in the URL for a GET/HEAD * @param array $options Request options, see {@see \WpOrg\Requests\Requests::response()} for documentation * @return string Raw HTTP result * * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $url argument is not a string or Stringable. * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $headers argument is not an array. * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $data parameter is not an array or string. * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $options argument is not an array. * @throws \WpOrg\Requests\Exception On failure to connect to socket (`fsockopenerror`) * @throws \WpOrg\Requests\Exception On socket timeout (`timeout`) */ public function request($url, $headers = [], $data = [], $options = []) { if (InputValidator::is_string_or_stringable($url) === false) { throw InvalidArgument::create(1, '$url', 'string|Stringable', gettype($url)); } if (is_array($headers) === false) { throw InvalidArgument::create(2, '$headers', 'array', gettype($headers)); } if (!is_array($data) && !is_string($data)) { if ($data === null) { $data = ''; } else { throw InvalidArgument::create(3, '$data', 'array|string', gettype($data)); } } if (is_array($options) === false) { throw InvalidArgument::create(4, '$options', 'array', gettype($options)); } $options['hooks']->dispatch('fsockopen.before_request'); $url_parts = parse_url($url); if (empty($url_parts)) { throw new Exception('Invalid URL.', 'invalidurl', $url); } $host = $url_parts['host']; $context = stream_context_create(); $verifyname = false; $case_insensitive_headers = new CaseInsensitiveDictionary($headers); // HTTPS support if (isset($url_parts['scheme']) && strtolower($url_parts['scheme']) === 'https') { $remote_socket = 'ssl://' . $host; if (!isset($url_parts['port'])) { $url_parts['port'] = Port::HTTPS; } $context_options = [ 'verify_peer' => true, 'capture_peer_cert' => true, ]; $verifyname = true; // SNI, if enabled (OpenSSL >=0.9.8j) // phpcs:ignore PHPCompatibility.Constants.NewConstants.openssl_tlsext_server_nameFound if (defined('OPENSSL_TLSEXT_SERVER_NAME') && OPENSSL_TLSEXT_SERVER_NAME) { $context_options['SNI_enabled'] = true; if (isset($options['verifyname']) && $options['verifyname'] === false) { $context_options['SNI_enabled'] = false; } } if (isset($options['verify'])) { if ($options['verify'] === false) { $context_options['verify_peer'] = false; $context_options['verify_peer_name'] = false; $verifyname = false; } elseif (is_string($options['verify'])) { $context_options['cafile'] = $options['verify']; } } if (isset($options['verifyname']) && $options['verifyname'] === false) { $context_options['verify_peer_name'] = false; $verifyname = false; } // Handle the PHP 8.4 deprecation (PHP 9.0 removal) of the function signature we use for stream_context_set_option(). // Ref: https://wiki.php.net/rfc/deprecate_functions_with_overloaded_signatures#stream_context_set_option if (function_exists('stream_context_set_options')) { // PHP 8.3+. stream_context_set_options($context, ['ssl' => $context_options]); } else { // PHP < 8.3. stream_context_set_option($context, ['ssl' => $context_options]); } } else { $remote_socket = 'tcp://' . $host; } $this->max_bytes = $options['max_bytes']; if (!isset($url_parts['port'])) { $url_parts['port'] = Port::HTTP; } $remote_socket .= ':' . $url_parts['port']; // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_set_error_handler set_error_handler([$this, 'connect_error_handler'], E_WARNING | E_NOTICE); $options['hooks']->dispatch('fsockopen.remote_socket', [&$remote_socket]); $socket = stream_socket_client($remote_socket, $errno, $errstr, ceil($options['connect_timeout']), STREAM_CLIENT_CONNECT, $context); restore_error_handler(); if ($verifyname && !$this->verify_certificate_from_context($host, $context)) { throw new Exception('SSL certificate did not match the requested domain name', 'ssl.no_match'); } if (!$socket) { if ($errno === 0) { // Connection issue throw new Exception(rtrim($this->connect_error), 'fsockopen.connect_error'); } throw new Exception($errstr, 'fsockopenerror', null, $errno); } $data_format = $options['data_format']; if ($data_format === 'query') { $path = self::format_get($url_parts, $data); $data = ''; } else { $path = self::format_get($url_parts, []); } $options['hooks']->dispatch('fsockopen.remote_host_path', [&$path, $url]); $request_body = ''; $out = sprintf("%s %s HTTP/%.1F\r\n", $options['type'], $path, $options['protocol_version']); if ($options['type'] !== Requests::TRACE) { if (is_array($data)) { $request_body = http_build_query($data, '', '&'); } else { $request_body = $data; } // Always include Content-length on POST requests to prevent // 411 errors from some servers when the body is empty. if (!empty($data) || $options['type'] === Requests::POST) { if (!isset($case_insensitive_headers['Content-Length'])) { $headers['Content-Length'] = strlen($request_body); } if (!isset($case_insensitive_headers['Content-Type'])) { $headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8'; } } } if (!isset($case_insensitive_headers['Host'])) { $out .= sprintf('Host: %s', $url_parts['host']); $scheme_lower = strtolower($url_parts['scheme']); if (($scheme_lower === 'http' && $url_parts['port'] !== Port::HTTP) || ($scheme_lower === 'https' && $url_parts['port'] !== Port::HTTPS)) { $out .= ':' . $url_parts['port']; } $out .= "\r\n"; } if (!isset($case_insensitive_headers['User-Agent'])) { $out .= sprintf("User-Agent: %s\r\n", $options['useragent']); } $accept_encoding = $this->accept_encoding(); if (!isset($case_insensitive_headers['Accept-Encoding']) && !empty($accept_encoding)) { $out .= sprintf("Accept-Encoding: %s\r\n", $accept_encoding); } $headers = Requests::flatten($headers); if (!empty($headers)) { $out .= implode("\r\n", $headers) . "\r\n"; } $options['hooks']->dispatch('fsockopen.after_headers', [&$out]); if (substr($out, -2) !== "\r\n") { $out .= "\r\n"; } if (!isset($case_insensitive_headers['Connection'])) { $out .= "Connection: Close\r\n"; } $out .= "\r\n" . $request_body; $options['hooks']->dispatch('fsockopen.before_send', [&$out]); fwrite($socket, $out); $options['hooks']->dispatch('fsockopen.after_send', [$out]); if (!$options['blocking']) { fclose($socket); $fake_headers = ''; $options['hooks']->dispatch('fsockopen.after_request', [&$fake_headers]); return ''; } $timeout_sec = (int) floor($options['timeout']); if ($timeout_sec === $options['timeout']) { $timeout_msec = 0; } else { $timeout_msec = self::SECOND_IN_MICROSECONDS * $options['timeout'] % self::SECOND_IN_MICROSECONDS; } stream_set_timeout($socket, $timeout_sec, $timeout_msec); $response = ''; $body = ''; $headers = ''; $this->info = stream_get_meta_data($socket); $size = 0; $doingbody = false; $download = false; if ($options['filename']) { // phpcs:ignore WordPress.PHP.NoSilencedErrors -- Silenced the PHP native warning in favour of throwing an exception. $download = @fopen($options['filename'], 'wb'); if ($download === false) { $error = error_get_last(); throw new Exception($error['message'], 'fopen'); } } while (!feof($socket)) { $this->info = stream_get_meta_data($socket); if ($this->info['timed_out']) { throw new Exception('fsocket timed out', 'timeout'); } $block = fread($socket, Requests::BUFFER_SIZE); if (!$doingbody) { $response .= $block; if (strpos($response, "\r\n\r\n")) { list($headers, $block) = explode("\r\n\r\n", $response, 2); $doingbody = true; } } // Are we in body mode now? if ($doingbody) { $options['hooks']->dispatch('request.progress', [$block, $size, $this->max_bytes]); $data_length = strlen($block); if ($this->max_bytes) { // Have we already hit a limit? if ($size === $this->max_bytes) { continue; } if (($size + $data_length) > $this->max_bytes) { // Limit the length $limited_length = ($this->max_bytes - $size); $block = substr($block, 0, $limited_length); } } $size += strlen($block); if ($download) { fwrite($download, $block); } else { $body .= $block; } } } $this->headers = $headers; if ($download) { fclose($download); } else { $this->headers .= "\r\n\r\n" . $body; } fclose($socket); $options['hooks']->dispatch('fsockopen.after_request', [&$this->headers, &$this->info]); return $this->headers; } /** * Send multiple requests simultaneously * * @param array $requests Request data (array of 'url', 'headers', 'data', 'options') as per {@see \WpOrg\Requests\Transport::request()} * @param array $options Global options, see {@see \WpOrg\Requests\Requests::response()} for documentation * @return array Array of \WpOrg\Requests\Response objects (may contain \WpOrg\Requests\Exception or string responses as well) * * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $requests argument is not an array or iterable object with array access. * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $options argument is not an array. */ public function request_multiple($requests, $options) { // If you're not requesting, we can't get any responses ¯\_(ツ)_/¯ if (empty($requests)) { return []; } if (InputValidator::has_array_access($requests) === false || InputValidator::is_iterable($requests) === false) { throw InvalidArgument::create(1, '$requests', 'array|ArrayAccess&Traversable', gettype($requests)); } if (is_array($options) === false) { throw InvalidArgument::create(2, '$options', 'array', gettype($options)); } $responses = []; $class = get_class($this); foreach ($requests as $id => $request) { try { $handler = new $class(); $responses[$id] = $handler->request($request['url'], $request['headers'], $request['data'], $request['options']); $request['options']['hooks']->dispatch('transport.internal.parse_response', [&$responses[$id], $request]); } catch (Exception $e) { $responses[$id] = $e; } if (!is_string($responses[$id])) { $request['options']['hooks']->dispatch('multiple.request.complete', [&$responses[$id], $id]); } } return $responses; } /** * Retrieve the encodings we can accept * * @return string Accept-Encoding header value */ private static function accept_encoding() { $type = []; if (function_exists('gzinflate')) { $type[] = 'deflate;q=1.0'; } if (function_exists('gzuncompress')) { $type[] = 'compress;q=0.5'; } $type[] = 'gzip;q=0.5'; return implode(', ', $type); } /** * Format a URL given GET data * * @param array $url_parts Array of URL parts as received from {@link https://www.php.net/parse_url} * @param array|object $data Data to build query using, see {@link https://www.php.net/http_build_query} * @return string URL with data */ private static function format_get($url_parts, $data) { if (!empty($data)) { if (empty($url_parts['query'])) { $url_parts['query'] = ''; } $url_parts['query'] .= '&' . http_build_query($data, '', '&'); $url_parts['query'] = trim($url_parts['query'], '&'); } if (isset($url_parts['path'])) { if (isset($url_parts['query'])) { $get = $url_parts['path'] . '?' . $url_parts['query']; } else { $get = $url_parts['path']; } } else { $get = '/'; } return $get; } /** * Error handler for stream_socket_client() * * @param int $errno Error number (e.g. E_WARNING) * @param string $errstr Error message */ public function connect_error_handler($errno, $errstr) { // Double-check we can handle it if (($errno & E_WARNING) === 0 && ($errno & E_NOTICE) === 0) { // Return false to indicate the default error handler should engage return false; } $this->connect_error .= $errstr . "\n"; return true; } /** * Verify the certificate against common name and subject alternative names * * Unfortunately, PHP doesn't check the certificate against the alternative * names, leading things like 'https://www.github.com/' to be invalid. * Instead * * @link https://tools.ietf.org/html/rfc2818#section-3.1 RFC2818, Section 3.1 * * @param string $host Host name to verify against * @param resource $context Stream context * @return bool * * @throws \WpOrg\Requests\Exception On failure to connect via TLS (`fsockopen.ssl.connect_error`) * @throws \WpOrg\Requests\Exception On not obtaining a match for the host (`fsockopen.ssl.no_match`) */ public function verify_certificate_from_context($host, $context) { $meta = stream_context_get_options($context); // If we don't have SSL options, then we couldn't make the connection at // all if (empty($meta) || empty($meta['ssl']) || empty($meta['ssl']['peer_certificate'])) { throw new Exception(rtrim($this->connect_error), 'ssl.connect_error'); } $cert = openssl_x509_parse($meta['ssl']['peer_certificate']); return Ssl::verify_certificate($host, $cert); } /** * Self-test whether the transport can be used. * * The available capabilities to test for can be found in {@see \WpOrg\Requests\Capability}. * * @codeCoverageIgnore * @param array<string, bool> $capabilities Optional. Associative array of capabilities to test against, i.e. `['<capability>' => true]`. * @return bool Whether the transport can be used. */ public static function test($capabilities = []) { if (!function_exists('fsockopen')) { return false; } // If needed, check that streams support SSL if (isset($capabilities[Capability::SSL]) && $capabilities[Capability::SSL]) { if (!extension_loaded('openssl') || !function_exists('openssl_x509_parse')) { return false; } } return true; } } Tasks/Meta.php 0000644 00000012705 15174710275 0007246 0 ustar 00 <?php namespace WPForms\Tasks; use WPForms_DB; /** * Class Meta helps to manage the tasks meta information * between Action Scheduler and WPForms hooks arguments. * We can't pass arguments longer than >191 chars in JSON to AS, * so we need to store them somewhere (and clean from time to time). * * @since 1.5.9 */ class Meta extends WPForms_DB { /** * Primary key (unique field) for the database table. * * @since 1.5.9 * * @var string */ public $primary_key = 'id'; /** * Database type identifier. * * @since 1.5.9 * * @var string */ public $type = 'tasks_meta'; /** * Primary class constructor. * * @since 1.5.9 */ public function __construct() { parent::__construct(); $this->table_name = self::get_table_name(); } /** * Get the DB table name. * * @since 1.5.9 * * @return string */ public static function get_table_name() { global $wpdb; return $wpdb->prefix . 'wpforms_tasks_meta'; } /** * Get table columns. * * @since 1.5.9 */ public function get_columns() { return [ 'id' => '%d', 'action' => '%s', 'data' => '%s', 'date' => '%s', ]; } /** * Default column values. * * @since 1.5.9 * * @return array */ public function get_column_defaults() { return [ 'action' => '', 'data' => '', 'date' => gmdate( 'Y-m-d H:i:s' ), ]; } /** * Create custom entry meta database table. * Used in migration and on plugin activation. * * @since 1.5.9 * * @noinspection UnusedFunctionResultInspection */ public function create_table() { global $wpdb; require_once ABSPATH . 'wp-admin/includes/upgrade.php'; $charset_collate = $wpdb->get_charset_collate(); $sql = "CREATE TABLE $this->table_name ( id bigint(20) NOT NULL AUTO_INCREMENT, action varchar(255) NOT NULL, data longtext NOT NULL, date datetime NOT NULL, PRIMARY KEY (id) ) $charset_collate;"; dbDelta( $sql ); } /** * Remove queue records for a defined period of time in the past. * Calling this method will remove queue records that are older than $period seconds. * * @since 1.5.9 * * @param string $action Action that should be cleaned up. * @param int $interval Number of seconds from now. * * @return int Number of removed tasks meta records. */ public function clean_by( $action, $interval ) { global $wpdb; if ( empty( $action ) || empty( $interval ) ) { return 0; } $table = self::get_table_name(); $action = sanitize_key( $action ); $date = gmdate( 'Y-m-d H:i:s', time() - (int) $interval ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching return (int) $wpdb->query( $wpdb->prepare( "DELETE FROM $table WHERE action = %s AND date < %s", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared $action, $date ) ); } /** * Inserts a new record into the database. * * @since 1.5.9 * * @param array $data Column data. * @param string $type Optional. Data type context. * * @return int ID for the newly inserted record. Zero otherwise. */ public function add( $data, $type = '' ) { if ( empty( $data['action'] ) || ! is_string( $data['action'] ) ) { return 0; } $data['action'] = sanitize_key( $data['action'] ); if ( isset( $data['data'] ) ) { $data['data'] = $this->prepare_data( $data['data'] ); } if ( empty( $type ) ) { $type = $this->type; } return parent::add( $data, $type ); } /** * Prepare data. * * @since 1.7.0 * * @param array $data Meta data. * * @return string */ private function prepare_data( $data ) { $string = wp_json_encode( $data ); if ( $string === false ) { $string = ''; } /* * We are encoding the string representation of all the data to make sure that nothing can harm the database. * This is not an encryption, and we need this data later "as is", * so we are using one of the fastest ways to do that. * This data is removed from DB daily. */ // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode return base64_encode( $string ); } /** * Retrieve a row from the database based on a given row ID. * * @since 1.5.9 * * @param int $meta_id Meta ID. * * @return null|object * @noinspection PhpParameterNameChangedDuringInheritanceInspection */ public function get( $meta_id ) { $meta = parent::get( $meta_id ); if ( empty( $meta ) || empty( $meta->data ) ) { return $meta; } // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode $decoded = base64_decode( $meta->data ); if ( $decoded === false || ! is_string( $decoded ) ) { $meta->data = ''; } else { $meta->data = json_decode( $decoded, true ); } return $meta; } /** * Get meta ID by action name and params. * * @since 1.7.0 * * @param string $action Action name. * @param array $params Action params. * * @return int */ public function get_meta_id( $action, $params ) { global $wpdb; $table = self::get_table_name(); $action = sanitize_key( $action ); $data = $this->prepare_data( array_values( $params ) ); return absint( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $wpdb->get_var( $wpdb->prepare( "SELECT id FROM $table WHERE action = %s AND data = %s LIMIT 1", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared $action, $data ) ) ); } } Tasks/Actions/IconChoicesFontAwesomeUpgradeTask.php 0000644 00000006107 15174710275 0016450 0 ustar 00 <?php namespace WPForms\Tasks\Actions; use WPForms\Tasks\Task; /** * Class Font Awesome Upgrade task. * * @since 1.8.3 */ class IconChoicesFontAwesomeUpgradeTask extends Task { /** * Action name for this task. * * @since 1.8.3 */ const ACTION = 'wpforms_process_font_awesome_upgrade'; /** * Status option name. * * @since 1.8.3 */ const STATUS = 'wpforms_process_font_awesome_upgrade_status'; /** * Start status. * * @since 1.8.3 */ const START = 'start'; /** * In progress status. * * @since 1.8.3 */ const IN_PROGRESS = 'in_progress'; /** * Completed status. * * @since 1.8.3 */ const COMPLETED = 'completed'; /** * Log title. * * @since 1.9.1 * * @var string */ protected $log_title = 'Migration'; /** * Constructor. * * @since 1.8.3 */ public function __construct() { parent::__construct( self::ACTION ); } /** * Process the task. * * @since 1.8.3 */ public function init() { // Bail out if migration is not started or completed. $status = get_option( self::STATUS ); // This task is run in \WPForms\Pro\Migrations\Upgrade183::run(), // and started in \WPForms\Migrations\UpgradeBase::run_async(). // Bail out if a task is not started or completed. if ( ! $status || $status === self::COMPLETED ) { return; } // Mark that migration is in progress. update_option( self::STATUS, self::IN_PROGRESS ); $this->hooks(); $tasks = wpforms()->obj( 'tasks' ); // Add new if none exists. if ( $tasks->is_scheduled( self::ACTION ) !== false ) { return; } $tasks->create( self::ACTION )->async()->register(); } /** * Hooks. * * @since 1.8.3 */ private function hooks() { add_action( self::ACTION, [ $this, 'upgrade' ] ); } /** * Upgrade. * * @since 1.8.3 */ public function upgrade() { $upload_dir = wpforms_upload_dir(); $tmp_base_path = $upload_dir['path'] . '/icon-choices-tmp'; $cache_base_path = $upload_dir['path'] . '/icon-choices'; $icons_data_file = $cache_base_path . '/icons.json'; if ( ! file_exists( $icons_data_file ) ) { $this->log( 'Font Awesome Upgrade: Font Awesome Upgrade: Library is not present, nothing to upgrade.' ); update_option( self::STATUS, self::COMPLETED ); return; } require_once ABSPATH . 'wp-admin/includes/file.php'; WP_Filesystem(); global $wp_filesystem; $wp_filesystem->rmdir( $tmp_base_path, true ); wpforms()->obj( 'icon_choices' )->run_install( $tmp_base_path ); if ( is_dir( $tmp_base_path ) ) { // Remove old cache. $this->log( 'Font Awesome Upgrade: Removing existing instance of the library.' ); $wp_filesystem->rmdir( $cache_base_path, true ); // Rename temporary directory. $this->log( 'Font Awesome Upgrade: Renaming temporary directory.' ); $wp_filesystem->move( $tmp_base_path, $cache_base_path ); // Mark that migration is finished. $this->log( 'Font Awesome Upgrade: Finished upgrading.' ); update_option( self::STATUS, self::COMPLETED ); return; } $this->log( 'Font Awesome Upgrade: Something went wrong, library was not upgraded.' ); } } Tasks/Actions/EntryEmailsTask.php 0000644 00000002532 15174710275 0013034 0 ustar 00 <?php namespace WPForms\Tasks\Actions; use WPForms\Tasks\Task; use WPForms\Tasks\Meta; /** * Class EntryEmailsTask is responsible for defining how to send emails, * when the form was submitted. * * @since 1.5.9 */ class EntryEmailsTask extends Task { /** * Action name for this task. * * @since 1.5.9 */ const ACTION = 'wpforms_process_entry_emails'; /** * Class constructor. * * @since 1.5.9 */ public function __construct() { parent::__construct( self::ACTION ); $this->async(); } /** * Get the data from Tasks meta table, check/unpack it and * send the email straight away. * * @since 1.5.9 * @since 1.5.9.3 Send immediately instead of calling \WPForms_Process::entry_email() method. * * @param int $meta_id ID for meta information for a task. */ public static function process( $meta_id ) { $task_meta = new Meta(); $meta = $task_meta->get( (int) $meta_id ); // We should actually receive something. if ( empty( $meta ) || empty( $meta->data ) ) { return; } // We expect a certain number of params. if ( count( $meta->data ) !== 5 ) { return; } // We expect a certain meta data structure for this task. list( $to, $subject, $message, $headers, $attachments ) = $meta->data; // Let's do this NOW, finally. wp_mail( $to, $subject, $message, $headers, $attachments ); } } Tasks/Actions/DomainAutoRegistrationTask.php 0000644 00000004601 15174710275 0015232 0 ustar 00 <?php namespace WPForms\Tasks\Actions; use WPForms\Tasks\Task; use WPForms\Integrations\Stripe\Api\DomainManager; use WPForms\Integrations\Stripe\Helpers; /** * Class DomainAutoRegistrationTask. * * @since 1.8.6 */ class DomainAutoRegistrationTask extends Task { /** * Action name. * * @since 1.8.6 */ const ACTION = 'wpforms_process_domain_auto_registration'; /** * Status option name. * * @since 1.8.6 */ const STATUS = 'wpforms_process_domain_auto_registration_status'; /** * Start status. * * @since 1.8.6 */ const START = 'start'; /** * In progress status. * * @since 1.8.6 */ const IN_PROGRESS = 'in_progress'; /** * Completed status. * * @since 1.8.6 */ const COMPLETED = 'completed'; /** * Domain manager. * * @since 1.8.6 * * @var DomainManager */ private $domain_manager; /** * Log title. * * @since 1.9.1 * * @var string */ protected $log_title = 'Migration'; /** * Constructor. * * @since 1.8.6 */ public function __construct() { parent::__construct( self::ACTION ); $this->domain_manager = new DomainManager(); } /** * Process the task. * * @since 1.8.6 */ public function init() { // Get a task status. $status = get_option( self::STATUS ); // This task is run in \WPForms\Migrations\Upgrade186::run(), // and started in \WPForms\Migrations\UpgradeBase::run_async(). // Bail out if a task is not started or completed. if ( ! $status || $status === self::COMPLETED ) { return; } // Mark that the task is in progress. update_option( self::STATUS, self::IN_PROGRESS ); // Register hooks. $this->hooks(); $tasks = wpforms()->obj( 'tasks' ); // Add new if none exists. if ( $tasks->is_scheduled( self::ACTION ) !== false ) { return; } $tasks->create( self::ACTION )->async()->register(); } /** * Register hooks. * * @since 1.8.6 */ private function hooks() { add_action( self::ACTION, [ $this, 'process' ] ); } /** * Process the task. * * @since 1.8.6 */ public function process() { // If the Stripe account is connected, then try to register domain. if ( Helpers::has_stripe_keys() && $this->domain_manager->validate() ) { $this->log( 'Stripe Payments: Stripe domain auto registration during migration to WPForms 1.8.6.' ); } // Mark that the task is completed. update_option( self::STATUS, self::COMPLETED ); } } Tasks/Actions/WebhooksAutoConfigurationTask.php 0000644 00000004621 15174710275 0015743 0 ustar 00 <?php namespace WPForms\Tasks\Actions; use WPForms\Tasks\Task; use WPForms\Integrations\Stripe\Api\WebhooksManager; use WPForms\Integrations\Stripe\Helpers; /** * Class WebhooksAutoConfigurationTask. * * @since 1.8.4 */ class WebhooksAutoConfigurationTask extends Task { /** * Action name. * * @since 1.8.4 */ const ACTION = 'wpforms_process_webhooks_auto_configuration'; /** * Status option name. * * @since 1.8.4 */ const STATUS = 'wpforms_process_webhooks_auto_configuration_status'; /** * Start status. * * @since 1.8.4 */ const START = 'start'; /** * In progress status. * * @since 1.8.4 */ const IN_PROGRESS = 'in_progress'; /** * Completed status. * * @since 1.8.4 */ const COMPLETED = 'completed'; /** * Webhooks manager. * * @since 1.8.4 * * @var WebhooksManager */ private $webhooks_manager; /** * Log title. * * @since 1.9.1 * * @var string */ protected $log_title = 'Migration'; /** * Constructor. * * @since 1.8.4 */ public function __construct() { parent::__construct( self::ACTION ); $this->webhooks_manager = new WebhooksManager(); } /** * Process the task. * * @since 1.8.4 */ public function init() { // Get a task status. $status = get_option( self::STATUS ); // This task is run in \WPForms\Migrations\Upgrade184::run(), // and started in \WPForms\Migrations\UpgradeBase::run_async(). // Bail out if a task is not started or completed. if ( ! $status || $status === self::COMPLETED ) { return; } // Mark that the task is in progress. update_option( self::STATUS, self::IN_PROGRESS ); // Register hooks. $this->hooks(); $tasks = wpforms()->obj( 'tasks' ); // Add new if none exists. if ( $tasks->is_scheduled( self::ACTION ) !== false ) { return; } $tasks->create( self::ACTION )->async()->register(); } /** * Register hooks. * * @since 1.8.4 */ private function hooks() { add_action( self::ACTION, [ $this, 'process' ] ); } /** * Process the task. * * @since 1.8.4 */ public function process() { // If the Stripe account is connected, then try to configure webhooks. if ( Helpers::has_stripe_keys() && $this->webhooks_manager->connect() ) { $this->log( 'Stripe Payments: Webhooks configured during migration to WPForms 1.8.4.' ); } // Mark that the task is completed. update_option( self::STATUS, self::COMPLETED ); } } Tasks/Actions/EntryEmailsMetaCleanupTask.php 0000644 00000004164 15174710275 0015156 0 ustar 00 <?php namespace WPForms\Tasks\Actions; use WPForms\Tasks\Task; use WPForms\Tasks\Meta; /** * Class EntryEmailsMetaCleanupTask. * * @since 1.5.9 */ class EntryEmailsMetaCleanupTask extends Task { /** * Action name for this task. * * @since 1.5.9 */ const ACTION = 'wpforms_process_entry_emails_meta_cleanup'; /** * Class constructor. * * @since 1.5.9 */ public function __construct() { parent::__construct( self::ACTION ); $this->init(); } /** * Initialize the task with all the proper checks. * * @since 1.5.9 */ public function init() { // Register the action handler. $this->hooks(); $tasks = wpforms()->obj( 'tasks' ); $email_async = wpforms_setting( 'email-async' ); // Add new if none exists. if ( $tasks->is_scheduled( self::ACTION ) !== false ) { // Cancel scheduled action if email async option is not set. if ( ! $email_async ) { $this->cancel(); } return; } // Do not schedule action if email async option is not set. if ( ! $email_async ) { return; } // phpcs:disable WPForms.PHP.ValidateHooks.InvalidHookName /** * Filters the email cleanup task interval. * * @since 1.5.9 * * @param int $interval Interval in seconds. */ $interval = (int) apply_filters( 'wpforms_tasks_entry_emails_meta_cleanup_interval', DAY_IN_SECONDS ); // phpcs:enable WPForms.PHP.ValidateHooks.InvalidHookName $this->recurring( strtotime( 'tomorrow' ), $interval ) ->params( $interval ) ->register(); } /** * Add hooks. * * @since 1.7.3 */ private function hooks() { add_action( self::ACTION, [ $this, 'process' ] ); } /** * Perform the cleanup action: remove outdated meta for entry emails task. * * @since 1.5.9 * * @param int $meta_id ID for meta information for a task. */ public function process( $meta_id ) { $task_meta = new Meta(); $meta = $task_meta->get( (int) $meta_id ); // We should actually receive something. if ( empty( $meta ) || empty( $meta->data ) ) { return; } list( $interval ) = $meta->data; $task_meta->clean_by( EntryEmailsTask::ACTION, (int) $interval ); } } Tasks/Actions/SquareSubscriptionTransactionIDTask.php 0000644 00000006056 15174710275 0017075 0 ustar 00 <?php namespace WPForms\Tasks\Actions; use WPForms\Integrations\Square\Api\Api; use WPForms\Integrations\Square\Connection; use WPForms\Tasks\Task; use WPForms\Tasks\Meta; /** * Class SquareSubscriptionTransactionIDTask. * * @since 1.9.5 */ class SquareSubscriptionTransactionIDTask extends Task { /** * Action name. * * @since 1.9.5 */ private const ACTION = 'wpforms_process_square_subscription_transaction_id'; /** * Constructor. * * @since 1.9.5 */ public function __construct() { parent::__construct( self::ACTION ); $this->init(); } /** * Initialize. * * @since 1.9.5 */ private function init() { $this->hooks(); } /** * Register hooks. * * @since 1.9.5 */ private function hooks() { add_action( 'wpforms_process_payment_saved', [ $this, 'add_task' ], 999, 3 ); add_action( self::ACTION, [ $this, 'process' ] ); } /** * Add task to the queue. * * @since 1.9.5 * * @param string $payment_id Payment ID. * @param array $fields Final/sanitized submitted field data. * @param array $form_data Form data and settings. */ public function add_task( $payment_id, array $fields, array $form_data ) { $payment_obj = wpforms()->obj( 'payment' ); if ( ! $payment_obj ) { return; } $payment = $payment_obj->get( (int) $payment_id ); if ( ! $payment ) { return; } // Bail early if not Square subscription. if ( $payment->gateway !== 'square' || $payment->type !== 'subscription' ) { return; } // Bail early if transaction_id is already set via webhooks. if ( ! empty( $payment->transaction_id ) ) { return; } // Add task to the queue. wpforms()->obj( 'tasks' ) ->create( self::ACTION ) ->once( time() + MINUTE_IN_SECONDS ) ->params( (int) $payment_id ) ->register(); } /** * Process the task. * * @since 1.9.5 * * @param int $meta_id Meta ID. */ public function process( $meta_id ) { $task_meta = new Meta(); $meta = $task_meta->get( (int) $meta_id ); if ( empty( $meta ) || empty( $meta->data ) ) { return; } [ $payment_id ] = $meta->data; $payment = wpforms()->obj( 'payment' )->get( (int) $payment_id ); // Bail early if transaction_id is already set via webhooks. if ( ! empty( $payment->transaction_id ) ) { return; } if ( ! Connection::get() ) { return; } $api = new Api( Connection::get() ); $subscription = $api->retrieve_subscription( $payment->subscription_id ); if ( $subscription === null ) { return; } $invoice = $api->get_latest_subscription_invoice( $subscription ); if ( $invoice === null ) { return; } $transaction_id = $api->get_latest_invoice_transaction_id( $invoice ); // Set transaction_id for the subscription in case it not received earlier. wpforms()->obj( 'payment' )->update( $payment_id, [ 'transaction_id' => $transaction_id ], '', '', [ 'cap' => false ] ); // Log. wpforms()->obj( 'payment_meta' )->add_log( $payment_id, sprintf( 'Square subscription was created. (Invoice ID: %s)', $invoice->getId() ) ); } } Tasks/Actions/Migration175Task.php 0000644 00000027505 15174710275 0012775 0 ustar 00 <?php namespace WPForms\Tasks\Actions; use WPForms\Tasks\Task; use WPForms\Tasks\Tasks; use WPForms_Entry_Handler; use WPForms_Entry_Meta_Handler; /** * Class Migration175Task. * * @since 1.7.5 */ class Migration175Task extends Task { /** * Action name for this task. * * @since 1.7.5 */ const ACTION = 'wpforms_process_migration_175'; /** * Status option name. * * @since 1.7.5 */ const STATUS = 'wpforms_process_migration_175_status'; /** * Start status. * * @since 1.7.5 */ const START = 'start'; /** * In progress status. * * @since 1.7.5 */ const IN_PROGRESS = 'in progress'; /** * Completed status. * * @since 1.7.5 */ const COMPLETED = 'completed'; /** * Chunk size to use. * Specifies how many entries to convert in one db request. * * @since 1.7.5 */ const CHUNK_SIZE = 5000; /** * Chunk size of the migration task. * Specifies how many entry ids to load at once for further conversion. * * @since 1.7.5 */ const TASK_CHUNK_SIZE = self::CHUNK_SIZE * 10; /** * Entry handler. * * @since 1.7.5 * * @var WPForms_Entry_Handler */ private $entry_handler; /** * Entry meta handler. * * @since 1.7.5 * * @var WPForms_Entry_Meta_Handler */ private $entry_meta_handler; /** * Temporary table name. * * @since 1.7.5 * * @var string */ private $temp_table_name; /** * Class constructor. * * @since 1.7.5 */ public function __construct() { parent::__construct( self::ACTION ); } /** * Initialize the task with all the proper checks. * * @since 1.7.5 */ public function init() { global $wpdb; $this->entry_handler = wpforms()->obj( 'entry' ); $this->entry_meta_handler = wpforms()->obj( 'entry_meta' ); $this->temp_table_name = "{$wpdb->prefix}wpforms_temp_entry_ids"; if ( ! $this->entry_handler || ! $this->entry_meta_handler ) { return; } // Bail out if migration is not started or completed. $status = get_option( self::STATUS ); if ( ! $status || $status === self::COMPLETED ) { return; } $this->hooks(); if ( $status === self::START ) { // Mark that migration is in progress. update_option( self::STATUS, self::IN_PROGRESS ); // Alter entry meta table. $this->alter_entry_meta_table(); // Init migration. $this->init_migration(); } } /** * Modify field in the entry meta table. * * @since 1.7.5 */ private function alter_entry_meta_table() { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $wpdb->query( "ALTER TABLE {$this->entry_meta_handler->table_name} MODIFY type VARCHAR(255)" ); } /** * Add index to a table. * * @since 1.7.5 * * @param string $table_name Table. * @param string $index_name Index name. * @param string $key_part Key part. * * @return void */ private function add_index( $table_name, $index_name, $key_part ) { global $wpdb; // Check id index already exists. // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.NoCaching $result = $wpdb->get_var( "SELECT COUNT(1) IndexIsThere FROM INFORMATION_SCHEMA.STATISTICS WHERE table_schema = DATABASE() AND table_name = '$table_name' AND index_name = '$index_name'" ); if ( $result === '1' ) { return; } // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.NoCaching // Change the column length for the wp_wpforms_entry_meta.type column to 255 and add an index. // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.NoCaching $wpdb->query( "CREATE INDEX $index_name ON $table_name ( $key_part )" ); // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.NoCaching } /** * Add hooks. * * @since 1.7.5 */ private function hooks() { // Register the migrate action. add_action( self::ACTION, [ $this, 'migrate' ] ); // Register after process queue action. add_action( 'action_scheduler_after_process_queue', [ $this, 'after_process_queue' ] ); } /** * Migrate an entry. * * @param int $action_index Action index. * * @since 1.7.5 */ public function migrate( $action_index ) { global $wpdb; $db_indexes = [ - 3 => [ 'table_name' => $this->entry_meta_handler->table_name, 'index_name' => 'form_id', 'key_part' => 'form_id', ], - 2 => [ 'table_name' => $this->entry_meta_handler->table_name, 'index_name' => 'type', 'key_part' => 'type', ], - 1 => [ 'table_name' => $this->entry_meta_handler->table_name, 'index_name' => 'data', 'key_part' => 'data(32)', ], ]; // We create indexes in the background as it could take significant time on a big database. if ( array_key_exists( $action_index, $db_indexes ) ) { $this->add_index( $db_indexes[ $action_index ]['table_name'], $db_indexes[ $action_index ]['index_name'], $db_indexes[ $action_index ]['key_part'] ); return; } // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.NoCaching // The query length in migrate_payment_data() is about 500 chars for 1 entry (7 metas). // The length of the query is defined by MAX_ALLOWED_PACKET variable, which defaults to 4 MB on MySQL 5.7. // We increase MAX_ALLOWED_PACKET variable to fit the number of entries specified in self::CHUNK_SIZE. $new_max_allowed_packet = 500 * self::CHUNK_SIZE; $max_allowed_packet = (int) $wpdb->get_var( "SHOW VARIABLES LIKE 'MAX_ALLOWED_PACKET'", 1 ); if ( $new_max_allowed_packet > $max_allowed_packet ) { $wpdb->query( "SET MAX_ALLOWED_PACKET = $new_max_allowed_packet" ); } // Using OFFSET makes a way longer request, as MySQL has to access all rows before OFFSET. // We follow very fast way with indexed column (id > $action_index). $entry_ids = $wpdb->get_col( $wpdb->prepare( "SELECT entry_id FROM $this->temp_table_name WHERE id > %d LIMIT %d", $action_index, self::TASK_CHUNK_SIZE ) ); $i = 0; $entry_ids_count = count( $entry_ids ); // This cycle is twice less memory consuming than array_chunk( $entry_ids ). while ( $i < $entry_ids_count ) { $entry_ids_chunk = array_slice( $entry_ids, $i, self::CHUNK_SIZE ); $this->migrate_payment_data( implode( ',', $entry_ids_chunk ) ); $i += self::CHUNK_SIZE; } if ( $new_max_allowed_packet > $max_allowed_packet ) { $wpdb->query( "SET MAX_ALLOWED_PACKET = $max_allowed_packet" ); } // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.NoCaching } /** * After process queue action. * Set status as completed. * * @since 1.7.5 */ public function after_process_queue() { $tasks = wpforms()->obj( 'tasks' ); if ( ! $tasks || $tasks->is_scheduled( self::ACTION ) ) { return; } $this->drop_temp_table(); // Mark that migration is finished. update_option( self::STATUS, self::COMPLETED ); } /** * Init migration. * * @since 1.7.5 * @noinspection PhpUndefinedFunctionInspection */ private function init_migration() { // Get all payment entries. $count = $this->get_unprocessed_payment_entry_ids(); if ( ! $count ) { $this->drop_temp_table(); } // We need 3 preliminary steps to create indexes. $index = - 3; while ( $index < $count ) { // We do not use Task class here as we do not need meta. So, we reduce the number of DB requests. as_enqueue_async_action( self::ACTION, [ $index ], Tasks::GROUP ); $index = $index < 0 ? $index + 1 : $index + self::CHUNK_SIZE; } } /** * Migrate payment data to the correct table. * * @param string $entry_ids_list List of entry ids. * * @since 1.7.5 */ private function migrate_payment_data( $entry_ids_list ) { global $wpdb; // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.NoCaching $wpdb->query( "SELECT entry_id, form_id, user_id, status, meta, date FROM {$this->entry_handler->table_name} WHERE entry_id IN ( $entry_ids_list )" ); // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.NoCaching $values = []; foreach ( $wpdb->last_result as $entry ) { $meta = json_decode( $entry->meta, true ); if ( ! is_array( $meta ) ) { continue; } foreach ( $meta as $meta_key => $meta_value ) { // If meta_key doesn't begin with `payment_`, prefix it. $meta_key = strpos( $meta_key, 'payment_' ) === 0 ? $meta_key : "payment_$meta_key"; // We do not use $wpdb->prepare here, as it is 5 times slower. // Prepare takes 1.3 sec to prepare 1000 entries (6000 meta records). // It is incomparable with the two queries here. // With sprintf, the total processing time of this method is 0.15 sec for 1000 entries. $values[] = sprintf( "( %d, %d, %d, '%s', '%s', '%s', '%s' )", $entry->entry_id, $entry->form_id, $entry->user_id, $entry->status, $meta_key, $meta_value, $entry->date ); } } // Bail out if there is no found payment meta. if ( empty( $values ) ) { return; } $values = implode( ', ', $values ); // The following query length is about 500 chars for 1 entry (7 metas). // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.NoCaching $wpdb->query( "INSERT INTO {$this->entry_meta_handler->table_name} ( entry_id, form_id, user_id, status, type, data, date ) VALUES $values" ); // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.NoCaching } /** * Get entry ids which do not have relevant entry field records. * Store them in a temporary table. * * @since 1.7.5 * * @return int */ private function get_unprocessed_payment_entry_ids() { global $wpdb; $this->drop_temp_table(); // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $wpdb->query( "CREATE TABLE $this->temp_table_name ( id BIGINT AUTO_INCREMENT PRIMARY KEY, entry_id BIGINT NOT NULL )" ); $wpdb->query( "INSERT INTO $this->temp_table_name (entry_id) SELECT entry_id FROM {$this->entry_handler->table_name} WHERE type = 'payment' AND entry_id NOT IN (SELECT entry_id FROM {$this->entry_meta_handler->table_name} WHERE type LIKE 'payment_%')" ); // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.InterpolatedNotPrepared return $wpdb->rows_affected; } /** * Drop a temporary table. * * @since 1.7.5 */ private function drop_temp_table() { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.SchemaChange $wpdb->query( "DROP TABLE IF EXISTS $this->temp_table_name" ); } } Tasks/Actions/StripeLinkSubscriptionsTask.php 0000644 00000015471 15174710275 0015462 0 ustar 00 <?php namespace WPForms\Tasks\Actions; use WPForms\Integrations\Stripe\Api\PaymentIntents; use WPForms\Tasks\Task; use WPForms\Integrations\Stripe\Helpers; /** * Class StripeLinkSubscriptionsTask. * * @since 1.8.7 */ class StripeLinkSubscriptionsTask extends Task { /** * Action name for this task. * * @since 1.8.7 */ const ACTION = 'wpforms_process_stripe_link_subscriptions'; /** * Status option name. * * @since 1.8.7 */ const STATUS = 'wpforms_process_stripe_link_subscriptions_status'; /** * Start status. * * @since 1.8.7 */ const START = 'start'; /** * In progress status. * * @since 1.8.7 */ const IN_PROGRESS = 'in_progress'; /** * Completed status. * * @since 1.8.7 */ const COMPLETED = 'completed'; /** * Latest processed payment id. * * @since 1.8.7 */ const LATEST_PROCESSED_OPTION = 'wpforms_stripe_link_subscriptions_latest_processed'; /** * Stripe PaymentIntents API. * * @since 1.8.7 * * @var PaymentIntents */ private $api; /** * Log title. * * @since 1.9.1 * * @var string */ protected $log_title = 'Migration'; /** * Class constructor. * * @since 1.8.7 */ public function __construct() { parent::__construct( self::ACTION ); } /** * Initialize the task. * * @since 1.8.7 */ public function init() { // Get a task status. $status = get_option( self::STATUS ); // This task is run in \WPForms\Migrations\Upgrade187::run(), // and started in \WPForms\Migrations\UpgradeBase::run_async(). // Bail out if a task is not started or completed. if ( ! $status || $status === self::COMPLETED ) { return; } // Mark that the task is in progress. if ( $status === self::START ) { update_option( self::STATUS, self::IN_PROGRESS ); } // Register hooks. $this->hooks(); $tasks = wpforms()->obj( 'tasks' ); // Add new if none exists. if ( $tasks->is_scheduled( self::ACTION ) !== false ) { return; } // Add a new task if none exists. $tasks->create( self::ACTION ) ->async() ->register(); } /** * Register hooks. * * @since 1.8.7 */ private function hooks() { // Register the migrate action. add_action( self::ACTION, [ $this, 'run' ] ); } /** * Run a process task. * * @since 1.8.7 */ public function run() { // Bail if no Stripe account is connected. if ( ! Helpers::has_stripe_keys() ) { $this->complete(); return; } $link_subscriptions = $this->get_link_subscriptions(); // Bail if all subscription were processed. if ( empty( $link_subscriptions ) ) { $this->complete(); return; } $this->api = new PaymentIntents(); $this->process( $link_subscriptions ); } /** * Process subscriptions. * * @since 1.8.7 * * @param array $subscriptions Array of subscriptions. */ private function process( array $subscriptions ) { foreach ( $subscriptions as $subscription ) { $this->update_latest_processed( $subscription->id ); // Use subscription mode to cover all cases (e.g. mode might be switched to test while upgrading). $payment = $this->api->retrieve_payment_intent( $subscription->transaction_id, [ 'mode' => $subscription->mode ] ); // Bail if original payment was unsuccessful. if ( is_null( $payment ) || empty( $payment->status ) || $payment->status !== 'succeeded' ) { continue; } $setup_intent_data = $this->prepare_setup_intent_data( $payment, $subscription ); // Bail if subscription has already had correct mandate. if ( ! $setup_intent_data ) { continue; } $intent = $this->api->create_setup_intent( $setup_intent_data, [ 'mode' => $subscription->mode ] ); // Log failed subscription payment id. if ( empty( $intent ) ) { $this->log( 'Stripe Link Subscriptions: Failed ' . $subscription->id ); } } } /** * Update latest processed id. * * @since 1.8.7 * * @param int $id Subscription ID. */ private function update_latest_processed( int $id ) { update_option( self::LATEST_PROCESSED_OPTION, $id ); } /** * Get all Stripe subscriptions charged through Link. * * @since 1.8.7 * * @return array */ private function get_link_subscriptions(): array { global $wpdb; $latest_payment = (int) get_option( self::LATEST_PROCESSED_OPTION, 0 ); $payments_table = wpforms()->obj( 'payment' )->table_name; $paymentmeta_table = wpforms()->obj( 'payment_meta' )->table_name; $query[] = "SELECT p.* FROM {$payments_table} as p"; $query[] = "INNER JOIN {$paymentmeta_table} as pm ON p.id = pm.payment_id"; $query[] = "WHERE p.id > %d AND p.gateway = 'stripe' AND p.type = 'subscription' AND pm.meta_key = 'method_type' AND pm.meta_value = 'link'"; // Stripe API allows up to 100 read operations per second and 100 write operations per second in live mode, // and 25 operations per second for each in test mode. $query[] = 'ORDER BY p.id LIMIT 20'; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare return $wpdb->get_results( $wpdb->prepare( implode( ' ', $query ), $latest_payment ), OBJECT_K ); } /** * Prepare Setup Intent data. * * @since 1.8.7 * * @param object $payment Stripe payment object. * @param object $subscription Subscription object. * * @return array */ private function prepare_setup_intent_data( $payment, $subscription ): array { if ( ! empty( $payment->mandate ) ) { $mandate = $this->api->retrieve_mandate( $payment->mandate, [ 'mode' => $subscription->mode ] ); } $data = [ 'payment_method_types' => [ 'link' ], 'customer' => $payment->customer, 'payment_method' => $payment->payment_method, 'usage' => 'off_session', 'confirm' => true, ]; // Prepare default data in case mandate is not available. if ( empty( $mandate ) ) { $subscription_meta = wpforms()->obj( 'payment_meta' )->get_all( $subscription->id ); $data['mandate_data'] = [ 'customer_acceptance' => [ 'type' => 'online', 'online' => [ 'ip_address' => $subscription_meta['ip_address']->value, 'user_agent' => $subscription_meta['user_agent']->value, ], ], ]; return $data; } // Mandate is correct so no actions needed. if ( $mandate->type !== 'single_use' ) { return []; } $data['mandate_data'] = [ 'customer_acceptance' => [ 'type' => 'online', 'online' => [ 'ip_address' => $mandate->customer_acceptance->online->ip_address, 'user_agent' => $mandate->customer_acceptance->online->user_agent, ], ], ]; return $data; } /** * Mark that the task is completed. * * @since 1.8.7 */ public function complete() { $this->log( 'Stripe Link Subscriptions: Completed' ); update_option( self::STATUS, self::COMPLETED ); } } Tasks/Actions/FormsLocatorScanTask.php 0000644 00000031353 15174710275 0014022 0 ustar 00 <?php // phpcs:disable Generic.Commenting.DocComment.MissingShort /** @noinspection PhpUnnecessaryCurlyVarSyntaxInspection */ /** @noinspection SqlResolve */ // phpcs:enable Generic.Commenting.DocComment.MissingShort namespace WPForms\Tasks\Actions; use WP_Post; use WP_Query; use WP_Screen; use WPForms\Forms\Locator; use WPForms\Tasks\Meta; use WPForms\Tasks\Task; use WPForms\Tasks\Tasks; /** * Class FormLocatorScanTask. * * @since 1.7.4 */ class FormsLocatorScanTask extends Task { /** * Scan action name for this task. * * @since 1.7.4 */ const SCAN_ACTION = 'wpforms_process_forms_locator_scan'; /** * Re-scan action name for this task. * * @since 1.7.4 */ const RESCAN_ACTION = 'wpforms_process_forms_locator_rescan'; /** * Save action name for this task. * * @since 1.7.4 */ const SAVE_ACTION = 'wpforms_process_forms_locator_save'; /** * Delete action name for this task. * * @since 1.7.4 */ const DELETE_ACTION = 'wpforms_process_forms_locator_delete'; /** * Scan status option name. * * @since 1.7.4 */ const SCAN_STATUS = 'wpforms_process_forms_locator_status'; /** * Scan status "In Progress". * * @since 1.7.4 */ const SCAN_STATUS_IN_PROGRESS = 'in progress'; /** * Scan status "Completed". * * @since 1.7.4 */ const SCAN_STATUS_COMPLETED = 'completed'; /** * Locations query arg. * * @since 1.7.4 */ const LOCATIONS_QUERY_ARG = 'locations'; /** * Chunk size to use in get_form_locations(). * Specifies how many posts to load for scanning in one db request. * Affects memory usage. * * @since 1.7.4 */ const CHUNK_SIZE = 50; /** * Locator class instance. * * @since 1.7.4 * * @var Locator */ private $locator; /** * Tasks class instance. * * @since 1.7.4 * * @var Tasks */ private $tasks; /** * Task recurring interval in seconds. * * @since 1.7.4 * * @var int */ private $interval; /** * Log title. * * @since 1.9.1 * * @var string */ protected $log_title = 'Forms Locator'; /** * Class constructor. * * @since 1.7.4 */ public function __construct() { parent::__construct( self::SCAN_ACTION ); $this->init(); } /** * Initialize the task with all the proper checks. * * @since 1.7.4 */ public function init() { $this->locator = wpforms()->obj( 'locator' ); /** * Allow developers to modify the task interval. * * @since 1.7.4 * * @param int $interval The task recurring interval in seconds. If <= 0, the task will be cancelled. */ $this->interval = (int) apply_filters( 'wpforms_tasks_actions_forms_locator_scan_task_interval', DAY_IN_SECONDS ); $this->hooks(); $this->tasks = wpforms()->obj( 'tasks' ); // Do not add a new one if scheduled. if ( $this->tasks->is_scheduled( self::SCAN_ACTION ) !== false ) { if ( $this->interval <= 0 ) { $this->cancel(); } return; } $this->add_scan_task(); } /** * Add scan task. * * @since 1.7.4 */ private function add_scan_task() { if ( $this->interval <= 0 ) { return; } // Add a new task if none exists. $this->recurring( time(), $this->interval ) ->params() ->register(); } /** * Add hooks. * * @since 1.7.4 */ private function hooks() { // Register hidden action for testing and support. add_action( 'current_screen', [ $this, 'maybe_run_actions_in_admin' ] ); // Register Action Scheduler actions. add_action( self::SCAN_ACTION, [ $this, 'scan' ] ); add_action( self::RESCAN_ACTION, [ $this, 'rescan' ] ); add_action( self::SAVE_ACTION, [ $this, 'save' ] ); add_action( self::DELETE_ACTION, [ $this, 'delete' ] ); add_action( 'action_scheduler_after_process_queue', [ $this, 'after_process_queue' ] ); } /** * Maybe rescan or delete locations. * Hidden undocumented actions for tests and support. * * @since 1.7.4 * * @param WP_Screen $current_screen Current WP_Screen object. */ public function maybe_run_actions_in_admin( $current_screen ) { // phpcs:disable WordPress.Security.NonceVerification.Recommended if ( ! $current_screen || $current_screen->id !== 'toplevel_page_wpforms-overview' || ! isset( $_GET[ self::LOCATIONS_QUERY_ARG ] ) || ! wpforms_debug() ) { return; } if ( $_GET[ self::LOCATIONS_QUERY_ARG ] === 'delete' ) { $this->delete(); } if ( $_GET[ self::LOCATIONS_QUERY_ARG ] === 'scan' ) { $this->rescan(); } // phpcs:enable WordPress.Security.NonceVerification.Recommended wp_safe_redirect( remove_query_arg( [ self::LOCATIONS_QUERY_ARG ] ) ); exit; } /** * Run scan task. * * @since 1.7.4 */ public function scan() { if ( ! $this->tasks ) { return; } // Bail out if the scan is already in progress. if ( self::SCAN_STATUS_IN_PROGRESS === (string) get_option( self::SCAN_STATUS ) ) { return; } // Mark that scan is in progress. update_option( self::SCAN_STATUS, self::SCAN_STATUS_IN_PROGRESS ); $this->log( 'Forms Locator scan action started.' ); // This part of the scan shouldn't take more than 1 second even on big sites. $post_ids = $this->search_in_posts(); $post_locations = $this->get_form_locations( $post_ids ); $widget_locations = $this->locator->search_in_widgets(); $standalone_locations = $this->search_in_standalone_forms(); $locations = array_merge( $post_locations, $widget_locations, $standalone_locations ); $form_location_metas = $this->get_form_location_metas( $locations ); /** * This part of the scan can take a while. * Saving hundreds of metas with a potentially very high number of locations could be time and memory consuming. * That is why we perform save via Action Scheduler. */ $meta_chunks = array_chunk( $form_location_metas, self::CHUNK_SIZE, true ); $count = count( $meta_chunks ); foreach ( $meta_chunks as $index => $meta_chunk ) { $this->tasks->create( self::SAVE_ACTION )->async()->params( $meta_chunk, $index, $count )->register(); } $this->log( 'Save tasks created.' ); } /** * Run immediate scan. * * @since 1.7.4 */ public function rescan() { $this->cancel(); $this->add_scan_task(); } /** * Save form locations. * * @since 1.7.4 * * @param int $meta_id Action meta id. */ public function save( $meta_id ) { $params = ( new Meta() )->get( $meta_id ); if ( ! $params ) { return; } list( $meta_chunk, $index, $count ) = $params->data; foreach ( $meta_chunk as $form_id => $meta ) { update_post_meta( $form_id, Locator::LOCATIONS_META, $meta ); } $this->log( sprintf( 'Forms Locator save action %1$d/%2$d completed.', $index + 1, $count ) ); } /** * Delete form locations. * * @since 1.7.4 */ public function delete() { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $wpdb->query( $wpdb->prepare( "DELETE FROM $wpdb->postmeta WHERE meta_key = %s", Locator::LOCATIONS_META ) ); delete_option( self::SCAN_STATUS ); wp_cache_flush(); } /** * After process queue action. * Delete transient to indicate that scanning is completed. * * @since 1.7.4 */ public function after_process_queue() { if ( $this->tasks->is_scheduled( self::SAVE_ACTION ) ) { return; } // Mark that scan is finished. if ( (string) get_option( self::SCAN_STATUS ) === self::SCAN_STATUS_IN_PROGRESS ) { update_option( self::SCAN_STATUS, self::SCAN_STATUS_COMPLETED ); $this->log( 'Forms Locator scan action completed.' ); } } /** * Search form in posts. * * @since 1.7.4 * * @return int[] */ private function search_in_posts() { global $wpdb; $post_statuses = wpforms_wpdb_prepare_in( $this->locator->get_post_statuses() ); $post_types = wpforms_wpdb_prepare_in( $this->locator->get_post_types() ); // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $ids = $wpdb->get_col( "SELECT p.ID FROM (SELECT ID FROM $wpdb->posts WHERE post_status IN ( $post_statuses ) AND post_type IN ( $post_types ) ) AS ids INNER JOIN $wpdb->posts as p ON ids.ID = p.ID WHERE p.post_content REGEXP '\\\[wpforms|wpforms/form-selector'" ); // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared return array_map( 'intval', $ids ); } /** * Filters the SELECT clause of the query. * Get a minimal set of fields from the post record. * * @since 1.7.4 * * @param string $fields The SELECT clause of the query. * @param WP_Query $query The WP_Query instance (passed by reference). * * @return string * * @noinspection PhpUnusedParameterInspection */ public function posts_fields_filter( $fields, $query ) { global $wpdb; $fields_arr = [ 'ID', 'post_title', 'post_status', 'post_type', 'post_content', 'post_name' ]; $fields_arr = array_map( static function ( $field ) use ( $wpdb ) { return "$wpdb->posts." . $field; }, $fields_arr ); return implode( ', ', $fields_arr ); } /** * Get form locations. * * @since 1.7.4 * * @param int[] $post_ids Post IDs. * * @return array */ private function get_form_locations( $post_ids ) { // phpcs:ignore WPForms.PHP.HooksMethod.InvalidPlaceForAddingHooks /** * Block caching here, as caching produces unneeded db requests in * update_object_term_cache() and update_postmeta_cache(). */ $query_args = [ 'post_type' => $this->locator->get_post_types(), 'post_status' => $this->locator->get_post_statuses(), 'post__in' => $post_ids, 'no_found_rows' => true, 'posts_per_page' => - 1, 'cache_results' => false, ]; // Get form locations by chunks to prevent out of memory issue. $post_id_chunks = array_chunk( $post_ids, self::CHUNK_SIZE ); $locations = []; add_filter( 'posts_fields', [ $this, 'posts_fields_filter' ], 10, 2 ); foreach ( $post_id_chunks as $post_id_chunk ) { $query_args['post__in'] = $post_id_chunk; $query = new WP_Query( $query_args ); $locations = $this->get_form_locations_from_posts( $query->posts, $locations ); } remove_filter( 'posts_fields', [ $this, 'posts_fields_filter' ] ); return $locations; } /** * Get locations from posts. * * @since 1.7.4 * * @param WP_Post[] $posts Posts. * @param array $locations Locations. * * @return array */ private function get_form_locations_from_posts( $posts, $locations = [] ) { $home_url = home_url(); foreach ( $posts as $post ) { $form_ids = $this->locator->get_form_ids( $post->post_content ); if ( ! $form_ids ) { continue; } $url = get_permalink( $post ); $url = ( $url === false || is_wp_error( $url ) ) ? '' : $url; $url = str_replace( $home_url, '', $url ); foreach ( $form_ids as $form_id ) { $locations[] = [ 'type' => $post->post_type, 'title' => $post->post_title, 'form_id' => $form_id, 'id' => $post->ID, 'status' => $post->post_status, 'url' => $url, ]; } } return $locations; } /** * Search in standalone forms. * * @since 1.8.7 * * @return array */ private function search_in_standalone_forms(): array { global $wpdb; $location_types = []; foreach ( Locator::STANDALONE_LOCATION_TYPES as $location_type ) { $location_types[] = '"' . $location_type . '_enable":"1"'; } $regexp = implode( '|', $location_types ); $post_statuses = wpforms_wpdb_prepare_in( $this->locator->get_post_statuses() ); // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $standalone_forms = $wpdb->get_results( "SELECT ID, post_content, post_status FROM $wpdb->posts WHERE post_status IN ( $post_statuses ) AND post_type = 'wpforms' AND post_content REGEXP '$regexp';" ); // phpcs:enable WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $locations = []; foreach ( $standalone_forms as $standalone_form ) { $form_data = json_decode( $standalone_form->post_content, true ); $locations[] = $this->locator->build_standalone_location( (int) $standalone_form->ID, $form_data, $standalone_form->post_status ); } return $locations; } /** * Get form location metas. * * @param array $locations Locations. * * @since 1.7.4 * * @return array */ private function get_form_location_metas( $locations ) { $metas = []; foreach ( $locations as $location ) { if ( empty( $location['form_id'] ) ) { continue; } $metas[ $location['form_id'] ][] = $location; } return $metas; } } Tasks/Actions/PurgeSpamTask.php 0000644 00000003636 15174710275 0012511 0 ustar 00 <?php namespace WPForms\Tasks\Actions; use WPForms\Tasks\Task; /** * Class PurgeSpamTask. * * @since 1.9.1 */ class PurgeSpamTask extends Task { /** * Action name for this task. * * @since 1.9.1 */ const ACTION = 'wpforms_process_purge_spam'; /** * Interval in seconds. * * @since 1.9.1 * * @var int */ private $interval; /** * Tasks class instance. * * @since 1.9.1 * * @var Tasks */ private $tasks; /** * Log title. * * @since 1.9.1 * * @var string */ protected $log_title = 'Purge Spam'; /** * Class constructor. * * @since 1.9.1 */ public function __construct() { parent::__construct( self::ACTION ); $this->init(); $this->hooks(); } /** * Init. * * @since 1.9.1 */ public function init() { /** * Filter the interval for the purge spam task, in seconds. * * @since 1.9.1 * * @param int $interval Interval in seconds. * * @return int */ $this->interval = (int) apply_filters( 'wpforms_tasks_actions_purge_spam_task_interval', DAY_IN_SECONDS ); $this->tasks = wpforms()->obj( 'tasks' ); // Do not add a new one if scheduled. if ( $this->tasks->is_scheduled( self::ACTION ) !== false ) { if ( $this->interval <= 0 ) { $this->cancel(); } return; } $this->add_scan_task(); } /** * Add hooks. * * @since 1.9.1 */ public function hooks() { add_action( self::ACTION, [ $this, 'process' ] ); } /** * Add a new task. * * @since 1.9.1 */ private function add_scan_task() { if ( $this->interval <= 0 ) { return; } $this->tasks->create( self::ACTION ) ->recurring( time(), $this->interval ) ->params() ->register(); } /** * Purge spam action. * * @since 1.9.1 */ public function process() { $entry_obj = wpforms()->obj( 'entry' ); if ( ! $entry_obj ) { return; } $entry_obj->purge_spam(); $this->log( 'Purge spam completed.' ); } } Tasks/Actions/Migration173Task.php 0000644 00000012460 15174710275 0012765 0 ustar 00 <?php namespace WPForms\Tasks\Actions; use WPForms\Tasks\Meta; use WPForms\Tasks\Task; use WPForms\Tasks\Tasks; use WPForms_Entry_Fields_Handler; use WPForms_Entry_Handler; /** * Class Migration173Task. * * @since 1.7.3 */ class Migration173Task extends Task { /** * Action name for this task. * * @since 1.7.3 */ const ACTION = 'wpforms_process_migration_173'; /** * Status option name. * * @since 1.7.3 */ const STATUS = 'wpforms_process_migration_173_status'; /** * Start status. * * @since 1.7.3 */ const START = 'start'; /** * In progress status. * * @since 1.7.3 */ const IN_PROGRESS = 'in progress'; /** * Completed status. * * @since 1.7.3 */ const COMPLETED = 'completed'; /** * Chunk size to use. * Specifies how many entries to load for scanning in one db request. * Affects memory usage. * * @since 1.7.3 */ const CHUNK_SIZE = 50; /** * Entry handler. * * @since 1.7.3 * * @var WPForms_Entry_Handler */ private $entry_handler; /** * Entry fields handler. * * @since 1.7.3 * * @var WPForms_Entry_Fields_Handler */ private $entry_fields_handler; /** * Class constructor. * * @since 1.7.3 */ public function __construct() { parent::__construct( self::ACTION ); } /** * Initialize the task with all the proper checks. * * @since 1.7.3 */ public function init() { $this->entry_handler = wpforms()->obj( 'entry' ); $this->entry_fields_handler = wpforms()->obj( 'entry_fields' ); if ( ! $this->entry_handler || ! $this->entry_fields_handler ) { return; } // Bail out if migration is not started or completed. $status = get_option( self::STATUS ); if ( ! $status || $status === self::COMPLETED ) { return; } // Mark that migration is in progress. update_option( self::STATUS, self::IN_PROGRESS ); $this->hooks(); $tasks = wpforms()->obj( 'tasks' ); // Add new if none exists. if ( $tasks->is_scheduled( self::ACTION ) !== false ) { return; } // Init migration. $this->init_migration( $tasks ); } /** * Add hooks. * * @since 1.7.3 */ private function hooks() { // Register the migrate action. add_action( self::ACTION, [ $this, 'migrate' ] ); // Register after process queue action. add_action( 'action_scheduler_after_process_queue', [ $this, 'after_process_queue' ] ); } /** * Migrate an entry. * * @since 1.7.3 * * @param int $meta_id Action meta id. */ public function migrate( $meta_id ) { $params = ( new Meta() )->get( $meta_id ); if ( ! $params ) { return; } list( $entry_id_chunk ) = $params->data; foreach ( $entry_id_chunk as $entry_id ) { $this->save_entry( $entry_id ); } } /** * After process queue action. * Set status as completed. * * @since 1.7.3 */ public function after_process_queue() { if ( as_has_scheduled_action( self::ACTION ) ) { return; } // Mark that migration is finished. update_option( self::STATUS, self::COMPLETED ); } /** * Init migration. * * @since 1.7.3 * * @param Tasks $tasks Tasks class instance. */ private function init_migration( $tasks ) { // This part of the migration shouldn't take more than 1 second even on big sites. $entry_ids = $this->get_legacy_entry_ids(); if ( ! $entry_ids ) { // Mark that migration is completed. update_option( self::STATUS, self::COMPLETED ); return; } /** * This part of the migration can take a while. * Saving hundreds of entries with a potentially very high number of entry fields could be time and memory consuming. * That is why we perform save via Action Scheduler. */ $entry_id_chunks = array_chunk( $entry_ids, self::CHUNK_SIZE, true ); foreach ( $entry_id_chunks as $entry_id_chunk ) { $tasks->create( self::ACTION )->async()->params( $entry_id_chunk )->register(); } } /** * Get entry ids which do not have relevant entry field records. * * @since 1.7.3 * * @return int[] */ private function get_legacy_entry_ids() { global $wpdb; // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.NoCaching $entries = $wpdb->get_results( " SELECT e.entry_id FROM {$this->entry_handler->table_name} e LEFT JOIN {$this->entry_fields_handler->table_name} ef ON e.entry_id=ef.entry_id WHERE e.status IN( 'partial', 'abandoned' ) AND ef.entry_id IS NULL" ); // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.NoCaching if ( ! $entries || ! is_array( $entries ) ) { return []; } return array_map( 'intval', wp_list_pluck( $entries, 'entry_id' ) ); } /** * Save entry properly. * * @since 1.7.3 * * @param int $entry_id Entry id. */ private function save_entry( $entry_id ) { $entry = $this->entry_handler->get( $entry_id ); if ( ! $entry || ! isset( $entry->form_id, $entry->fields, $entry->date_modified ) ) { return; } $fields = json_decode( $entry->fields, true ); if ( ! is_array( $fields ) ) { return; } $form_data = [ 'id' => (int) $entry->form_id, 'date' => $entry->date_modified, ]; $this->entry_fields_handler->save( $fields, $form_data, $entry_id, true ); } } Tasks/Actions/AsyncRequestTask.php 0000644 00000002135 15174710275 0013225 0 ustar 00 <?php namespace WPForms\Tasks\Actions; use WPForms\Tasks\Task; use WPForms\Tasks\Meta; /** * Class AsyncRequestTask is responsible to send information in the background. * * @since 1.7.5 */ class AsyncRequestTask extends Task { /** * Action name for this task. * * @since 1.7.5 */ const ACTION = 'wpforms_process_async_request'; /** * Class constructor. * * @since 1.7.5 */ public function __construct() { // Task functionality is needed on cron request only. if ( ! ( defined( 'DOING_CRON' ) && DOING_CRON ) ) { return; } parent::__construct( self::ACTION ); $this->hooks(); } /** * Add hooks. * * @since 1.7.5 */ private function hooks() { // Register the migrate action. add_action( self::ACTION, [ $this, 'process' ] ); } /** * Send usage tracking to the server. * * @since 1.7.5 * * @param int $meta_id Action meta id. */ public static function process( $meta_id ) { $params = ( new Meta() )->get( $meta_id ); if ( ! $params ) { return; } list( $url, $args ) = $params->data; wp_safe_remote_get( $url, $args ); } } Tasks/Task.php 0000644 00000015077 15174710275 0007267 0 ustar 00 <?php namespace WPForms\Tasks; use InvalidArgumentException; use UnexpectedValueException; /** * Class Task. * * @since 1.5.9 */ class Task { /** * This task is async (runs asap). * * @since 1.5.9 */ const TYPE_ASYNC = 'async'; /** * This task is a recurring. * * @since 1.5.9 */ const TYPE_RECURRING = 'scheduled'; /** * This task is run once. * * @since 1.5.9 */ const TYPE_ONCE = 'once'; /** * Type of the task. * * @since 1.5.9 * * @var string */ private $type; /** * Action that will be used as a hook. * * @since 1.5.9 * * @var string */ private $action; /** * Task meta ID. * * @since 1.5.9 * * @var int */ private $meta_id; /** * All the params that should be passed to the hook. * * @since 1.5.9 * * @var array */ private $params; /** * When the first instance of the job will run. * Used for ONCE ane RECURRING tasks. * * @since 1.5.9 * * @var int */ private $timestamp; /** * How long to wait between runs. * Used for RECURRING tasks. * * @since 1.5.9 * * @var int */ private $interval; /** * Task meta. * * @since 1.7.0 * * @var Meta */ private $meta; /** * Log title. * * @since 1.9.1 * * @var string */ protected $log_title = 'Task'; /** * Task constructor. * * @since 1.5.9 * * @param string $action Action of the task. * * @throws InvalidArgumentException When action is not a string. * @throws UnexpectedValueException When action is empty. */ public function __construct( $action ) { if ( ! is_string( $action ) ) { throw new InvalidArgumentException( 'Task action should be a string.' ); } $this->action = sanitize_key( $action ); $this->meta = new Meta(); if ( empty( $this->action ) ) { throw new UnexpectedValueException( 'Task action cannot be empty.' ); } } /** * Define the type of the task as async. * * @since 1.5.9 * * @return Task */ public function async() { $this->type = self::TYPE_ASYNC; return $this; } /** * Define the type of the task as recurring. * * @since 1.5.9 * * @param int $timestamp When the first instance of the job will run. * @param int $interval How long to wait between runs. * * @return Task */ public function recurring( $timestamp, $interval ) { $this->type = self::TYPE_RECURRING; $this->timestamp = (int) $timestamp; $this->interval = (int) $interval; return $this; } /** * Define the type of the task as one-time. * * @since 1.5.9 * * @param int $timestamp When the first instance of the job will run. * * @return Task */ public function once( $timestamp ) { $this->type = self::TYPE_ONCE; $this->timestamp = (int) $timestamp; return $this; } /** * Pass any number of params that should be saved to Meta table. * * @since 1.5.9 * * @return Task */ public function params() { $this->params = func_get_args(); return $this; } /** * Register the action. * Should be the final call in a chain. * * @since 1.5.9 * * @return null|string Action ID. */ public function register() { $action_id = null; // No processing if ActionScheduler is not usable. if ( ! wpforms()->obj( 'tasks' )->is_usable() ) { return $action_id; } // Save data to tasks meta table. if ( $this->params !== null ) { $this->meta_id = $this->meta->add( [ 'action' => $this->action, 'data' => $this->params, ] ); if ( empty( $this->meta_id ) ) { return $action_id; } } // Prevent 500 errors when Action Scheduler tables don't exist. try { switch ( $this->type ) { case self::TYPE_ASYNC: $action_id = $this->register_async(); break; case self::TYPE_RECURRING: $action_id = $this->register_recurring(); break; case self::TYPE_ONCE: $action_id = $this->register_once(); break; } } catch ( \RuntimeException $exception ) { $action_id = null; } return $action_id; } /** * Register the async task. * * @since 1.5.9 * * @return null|string Action ID. * @noinspection PhpUndefinedFunctionInspection */ protected function register_async() { if ( ! function_exists( 'as_enqueue_async_action' ) ) { return null; } return as_enqueue_async_action( $this->action, /** * Filter arguments passed to the async task. * * @since 1.9.4 * * @param array $args Arguments passed to the async task. * * @return array */ (array) apply_filters( 'wpforms_tasks_task_register_async_args', [ 'tasks_meta_id' => $this->meta_id, ] ), Tasks::GROUP ); } /** * Register the recurring task. * * @since 1.5.9 * * @return null|string Action ID. * @noinspection PhpUndefinedFunctionInspection */ protected function register_recurring() { if ( ! function_exists( 'as_schedule_recurring_action' ) ) { return null; } return as_schedule_recurring_action( $this->timestamp, $this->interval, $this->action, [ 'tasks_meta_id' => $this->meta_id ], Tasks::GROUP ); } /** * Register the one-time task. * * @since 1.5.9 * * @return null|string Action ID. * @noinspection PhpUndefinedFunctionInspection */ protected function register_once() { if ( ! function_exists( 'as_schedule_single_action' ) ) { return null; } return as_schedule_single_action( $this->timestamp, $this->action, [ 'tasks_meta_id' => $this->meta_id ], Tasks::GROUP ); } /** * Cancel all occurrences of this task. * * @since 1.6.1 * * @return null|bool|string Null if no matching action found, * false if AS library is missing, * true if scheduled task has no params, * string of the scheduled action ID if a scheduled action was found and unscheduled. * @noinspection PhpUndefinedFunctionInspection */ public function cancel() { if ( ! function_exists( 'as_unschedule_all_actions' ) ) { return false; } if ( $this->params === null ) { as_unschedule_all_actions( $this->action ); return true; } $this->meta_id = $this->meta->get_meta_id( $this->action, $this->params ); if ( $this->meta_id === null ) { return null; } return as_unschedule_action( $this->action, [ 'tasks_meta_id' => $this->meta_id ], Tasks::GROUP ); } /** * Log message to WPForms logger and standard debug.log file. * * @since 1.9.1 * * @param string $message The error message that should be logged. */ protected function log( $message ) { wpforms_log( $this->log_title, $message, [ 'type' => 'log' ] ); } } Tasks/Tasks.php 0000644 00000026032 15174710275 0007443 0 ustar 00 <?php // phpcs:ignore Generic.Commenting.DocComment.MissingShort /** @noinspection PhpUndefinedClassInspection */ namespace WPForms\Tasks; use ActionScheduler; use ActionScheduler_Action; use ActionScheduler_DataController; use ActionScheduler_DBStore; use WPForms\Helpers\Transient; use WPForms\Tasks\Actions\EntryEmailsMetaCleanupTask; use WPForms\Tasks\Actions\EntryEmailsTask; use WPForms\Tasks\Actions\FormsLocatorScanTask; use WPForms\Tasks\Actions\AsyncRequestTask; use WPForms\Tasks\Actions\PurgeSpamTask; /** * Class Tasks manages the tasks queue and provides API to work with it. * * @since 1.5.9 */ class Tasks { /** * Group that will be assigned to all actions. * * @since 1.5.9 */ const GROUP = 'wpforms'; /** * Actions setting name. * * @since 1.7.3 */ const ACTIONS = 'actions'; /** * WPForms pending or in-progress actions. * * @since 1.7.3 * * @var array */ private $active_actions; /** * Determine if WPForms task is executing. * * @since 1.9.4 * * @var bool */ private static $task_executing = false; /** * Perform certain things on class init. * * @since 1.5.9 */ public function init() { // Get WPForms pending or in-progress actions. $this->active_actions = $this->get_active_actions(); // Register WPForms tasks. foreach ( $this->get_tasks() as $task ) { if ( ! is_subclass_of( $task, Task::class ) ) { continue; } new $task(); } $this->hooks(); } /** * Hooks. * * @since 1.7.5 */ public function hooks() { add_action( 'delete_expired_transients', [ Transient::class, 'delete_all_expired' ], 11 ); add_action( 'admin_menu', [ $this, 'admin_hide_as_menu' ], PHP_INT_MAX ); /* * By default we send emails in the same process as the form submission is done. * That means that when many emails are set in form Notifications - * the form submission can take a while because of all those emails that are sending in the background. * Since WPForms 1.6.0 users can enable a new option in Settings > Emails, * called "Optimize Email Sending", to send email in async way. * This feature was enabled for WPForms 1.5.9, but some users were not happy. */ if ( ! (bool) wpforms_setting( 'email-async', false ) ) { add_filter( 'wpforms_tasks_entry_emails_trigger_send_same_process', '__return_true' ); } add_action( EntryEmailsTask::ACTION, [ EntryEmailsTask::class, 'process' ] ); add_action( 'action_scheduler_after_execute', [ $this, 'clear_action_meta' ], PHP_INT_MAX, 2 ); add_action( 'action_scheduler_begin_execute', [ $this, 'start_executing' ], 1 ); add_action( 'action_scheduler_after_execute', [ $this, 'stop_executing' ], 1, 2 ); } /** * Public interface to check if WPForms task is executing. * * @since 1.9.4 * * @return bool */ public static function is_executing(): bool { return self::$task_executing; } /** * Set a flag to indicate that WPForms task is executing. * * @since 1.9.4 * * @param int $action_id The action ID to process. */ public function start_executing( $action_id ) { $action_id = (int) $action_id; if ( ! class_exists( 'ActionScheduler' ) ) { return; } $store = ActionScheduler::store(); if ( ! $store ) { return; } $action = $store->fetch_action( $action_id ); if ( ! $action || $action->get_group() !== self::GROUP ) { return; } self::$task_executing = true; /** * Fires before WPForms task is executing. * * @since 1.9.4 * * @param int $action_id The action ID to process. * @param ActionScheduler_Action $action Action Scheduler action object. */ do_action( 'wpforms_tasks_start_executing', $action_id, $action ); } /** * Set a flag to indicate that WPForms task is executing. * * @since 1.9.4 * * @param int $action_id The action ID to process. * @param ActionScheduler_Action $action Action Scheduler action object. */ public function stop_executing( $action_id, $action ) { if ( ! $action || ! method_exists( $action, 'get_group' ) || $action->get_group() !== self::GROUP ) { return; } self::$task_executing = false; /** * Fires after WPForms task is executed. * * @since 1.9.4 * * @param int $action_id The action ID to process. * @param ActionScheduler_Action $action Action Scheduler action object. */ do_action( 'wpforms_tasks_stop_executing', $action_id, $action ); } /** * Get the list of WPForms default scheduled tasks. * Tasks, that are fired under certain specific circumstances * (like sending form submission email notifications) * are not listed here. * * @since 1.5.9 * * @return Task[] List of tasks classes. */ public function get_tasks() { if ( ! $this->is_usable() ) { return []; } $tasks = [ EntryEmailsMetaCleanupTask::class, FormsLocatorScanTask::class, AsyncRequestTask::class, PurgeSpamTask::class, ]; /** * Filters the task class list to initialize. * * @since 1.5.9 * * @param array $tasks Task class list. */ return apply_filters( 'wpforms_tasks_get_tasks', $tasks ); } /** * Hide Action Scheduler admin area when not in debug mode. * * @since 1.5.9 * @since 1.9.4 Does not hide the menu when some popular plugins are active. */ public function admin_hide_as_menu(): void { $plugin_exceptions = [ 'action-scheduler/action-scheduler.php', 'woocommerce/woocommerce.php', 'wp-rocket/wp-rocket.php', ]; /** * Filters the list of plugins for which * the Action Scheduler Tools -> Scheduled Actions menu item * should remain visible. * * @since 1.9.4 * * @param array $plugin_exceptions List of plugin exceptions. */ $plugin_exceptions = apply_filters( 'wpforms_tasks_action_scheduler_tools_plugin_exceptions', $plugin_exceptions ); $show_as_menu = ( defined( 'WPFORMS_SHOW_ACTION_SCHEDULER_MENU' ) && constant( 'WPFORMS_SHOW_ACTION_SCHEDULER_MENU' ) ) || wpforms_debug() || ! empty( array_filter( $plugin_exceptions, 'is_plugin_active' ) ); $hide_as_menu = ! $show_as_menu; /** * Filter to redefine that WPForms hides Tools > Action Scheduler menu item. * * @since 1.5.9 * * @param bool $hide_as_menu Hide Tools > Action Scheduler menu item. */ if ( apply_filters( 'wpforms_tasks_admin_hide_as_menu', $hide_as_menu ) ) { remove_submenu_page( 'tools.php', 'action-scheduler' ); } } /** * Create a new task. * Used for "inline" tasks, that require additional information * from the plugin runtime before they can be scheduled. * * Example: * wpforms()->obj( 'tasks' ) * ->create( 'i_am_the_dude' ) * ->async() * ->params( 'The Big Lebowski', 1998 ) * ->register(); * * This `i_am_the_dude` action will be later processed as: * add_action( 'i_am_the_dude', 'thats_what_you_call_me' ); * * Function `thats_what_you_call_me()` will receive `$meta_id` param, * and you will be able to receive all params from the action like this: * $params = ( new Meta() )->get( (int) $meta_id ); * list( $name, $year ) = $params->data; * * @since 1.5.9 * * @param string $action Action that will be used as a hook. * * @return Task */ public function create( $action ) { return new Task( $action ); } /** * Cancel all the AS actions for a group. * * @since 1.5.9 * * @param string $group Group to cancel all actions for. */ public function cancel_all( $group = '' ) { if ( empty( $group ) ) { $group = self::GROUP; } else { $group = sanitize_key( $group ); } if ( class_exists( 'ActionScheduler_DBStore' ) ) { ActionScheduler_DBStore::instance()->cancel_actions_by_group( $group ); $this->active_actions = $this->get_active_actions(); } } /** * Whether ActionScheduler thinks that it has migrated or not. * * @since 1.5.9.3 * * @return bool */ public function is_usable() { // No tasks if ActionScheduler wasn't loaded. if ( ! class_exists( 'ActionScheduler_DataController' ) ) { return false; } return ActionScheduler_DataController::is_migration_complete(); } /** * Whether task has been scheduled and is pending or in-progress. * * @since 1.6.0 * * @param string $hook Hook to check for. * * @return bool|null * @noinspection PhpUndefinedFunctionInspection */ public function is_scheduled( $hook ) { if ( ! function_exists( 'as_has_scheduled_action' ) ) { return null; } if ( in_array( $hook, $this->active_actions, true ) ) { return true; } // Action is not in the array, so it is not scheduled or belongs to another group. return as_has_scheduled_action( $hook ); } /** * Get all WPForms pending or in-progress actions. * * @since 1.7.3 */ private function get_active_actions() { global $wpdb; $group = self::GROUP; $sql = "SELECT a.hook FROM {$wpdb->prefix}actionscheduler_actions a JOIN {$wpdb->prefix}actionscheduler_groups g ON g.group_id = a.group_id WHERE g.slug = '$group' AND a.status IN ( 'in-progress', 'pending' )"; // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared $results = $wpdb->get_results( $sql, 'ARRAY_N' ); // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared return $results ? array_merge( ...$results ) : []; } /** * Delete a task by its ID. * * @since 1.9.6.1 * * @param int $action_id Action ID. */ public function delete_action( $action_id ): void { global $wpdb; $sql = "DELETE FROM {$wpdb->prefix}actionscheduler_actions WHERE action_id = %d"; // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared $wpdb->query( $wpdb->prepare( $sql, (int) $action_id ) ); // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared } /** * Fetch action by ID. * * @since 1.9.6.1 * * @param int $action_id Action ID. * * @return null|ActionScheduler_Action */ public function fetch_action( $action_id ): ?ActionScheduler_Action { if ( ! class_exists( 'ActionScheduler' ) ) { return null; } return ActionScheduler::store()->fetch_action( $action_id ); } /** * Clear the meta after action complete. * Fired before an action is marked as completed. * * @since 1.7.5 * * @param integer $action_id Action ID. * @param ActionScheduler_Action $action Action name. */ public function clear_action_meta( $action_id, $action ) { $action_schedule = $action->get_schedule(); if ( $action_schedule === null || $action_schedule->is_recurring() ) { return; } $hook_name = $action->get_hook(); if ( ! $this->is_scheduled( $hook_name ) ) { return; } $hook_args = $action->get_args(); if ( ! isset( $hook_args['tasks_meta_id'] ) ) { return; } $meta = new Meta(); $meta->delete( $hook_args['tasks_meta_id'] ); } } Lite/Emails/Summaries.php 0000644 00000014167 15174710275 0011353 0 ustar 00 <?php namespace WPForms\Lite\Emails; use WPForms\Lite\Reports\EntriesCount; use WPForms\Emails\Summaries as BaseSummaries; /** * Email Summaries. * * @since 1.8.8 */ class Summaries extends BaseSummaries { /** * Whether counting entries is allowed for Lite users. * * @since 1.8.8 * * @var bool */ private $allow_entries_count_lite; /** * Constructor for the class. * Initializes the object and registers the Lite weekly entries count cron schedule. * * @since 1.8.8 */ public function __construct() { // phpcs:disable WPForms.PHP.ValidateHooks.InvalidHookName // Disabling this filter will prevent entries submission count from being updated. /** This filter is documented in /lite/wpforms-lite.php */ $this->allow_entries_count_lite = apply_filters( 'wpforms_dash_widget_allow_entries_count_lite', true ); // phpcs:enable WPForms.PHP.ValidateHooks.InvalidHookName parent::__construct(); // Register the Lite weekly entries count cron schedule. $this->register_entries_count_schedule(); } /** * Hooks. * * @since 1.8.8 */ public function hooks() { parent::hooks(); // The following schedule is essential for the Lite version. // Regardless of the "Weekly Summaries" feature being disabled or enabled, // it ensures that entries numbers are consistently updated. if ( ! $this->allow_entries_count_lite ) { return; } add_action( 'wpforms_weekly_entries_count_cron', [ $this, 'entries_count_cron' ] ); } /** * Adjusts the Lite weekly entries count cron schedule. * * This function modifies the Lite weekly entries count cron schedule by reducing the interval by 5 seconds. * * @since 1.8.8 * @deprecated 1.9.1 * * @param array $schedules WP cron schedules. * * @return array */ public function weekly_entries_count( $schedules ) { _deprecated_function( __METHOD__, '1.9.1 of the WPForms plugin' ); $schedules['wpforms_weekly_entries_count'] = [ 'interval' => $this->get_next_launch_time() - time(), 'display' => esc_html__( 'Calculate WPForms Lite Weekly Entries Count', 'wpforms-lite' ), ]; return $schedules; } /** * Run the cron job to update entries count for Lite users. * * This function retrieves the current entries count for Lite users, calculates the count for the * previous week, and updates the necessary post meta data for trend calculations. * * @since 1.8.8 */ public function entries_count_cron() { // Get entries count for Lite users. $entries = ( new EntriesCount() )->get_by_form(); // Exit if there are no form entries to update. if ( empty( $entries ) ) { return; } foreach ( $entries as $form_id => &$form ) { // Set total entries count to the current count. $form['total'] = $form['count']; // Retrieve the previous week's count data from post meta. $previous_week_count = get_post_meta( $form_id, 'wpforms_entries_count_previous_week', true ); // Continue to the next form if the count data is not valid. if ( ! is_array( $previous_week_count ) || count( $previous_week_count ) !== 3 ) { $prev_count_previous_week = $form['total']; // Set the previous week's count zero "0" if the form was published less than or equal to 7 days ago. if ( $this->is_form_created_in_7days( $form_id ) ) { $prev_count_previous_week = 0; } update_post_meta( $form_id, 'wpforms_entries_count_previous_week', [ $form['total'], $form['total'], $prev_count_previous_week ] ); continue; } list( $total_previous_week, $count_previous_week ) = $previous_week_count; // Calculate count, count_previous_week, and trends. $form['count'] = $form['total'] - $total_previous_week; if ( count( array_unique( $previous_week_count ) ) === 1 ) { // If the previous week's count is the same as the current count, skip trends calculation. update_post_meta( $form_id, 'wpforms_entries_count_previous_week_skip_trends', true ); } else { // If the previous week's count is different from the current count, calculate trends. delete_post_meta( $form_id, 'wpforms_entries_count_previous_week_skip_trends' ); } // Update post meta data for trend calculations. update_post_meta( $form_id, 'wpforms_entries_count_previous_week', [ $form['total'], $form['count'], $count_previous_week ] ); } } /** * Get form entries. * * @since 1.8.8 * * @return array */ protected function get_entries(): array { return ( new EntriesCount() )->get_form_trends(); } /** * Register entries count schedule. * * @since 1.8.8 */ private function register_entries_count_schedule() { if ( ! $this->allow_entries_count_lite && wp_next_scheduled( 'wpforms_weekly_entries_count_cron' ) ) { wp_clear_scheduled_hook( 'wpforms_weekly_entries_count_cron' ); return; } if ( $this->allow_entries_count_lite && ! wp_next_scheduled( 'wpforms_weekly_entries_count_cron' ) ) { // Since v1.9.1 we use a single event and manually reoccur it // because a recurring event cannot guarantee // its firing at the same time during WP_CLI execution. wp_schedule_single_event( $this->get_next_launch_time(), 'wpforms_weekly_entries_count_cron' ); } } /** * Get next Monday midnight with WordPress offset. * * @since 1.9.1 * * @return int */ protected function get_next_launch_time(): int { $datetime = date_create( 'next monday', wp_timezone() ); if ( ! $datetime ) { return time() + WEEK_IN_SECONDS; } return absint( $datetime->getTimestamp() ); } /** * Check if the given form_id was published less than or equal to 7 days ago. * * @since 1.8.8 * * @param int $form_id The ID of the form (post). * * @return bool */ private function is_form_created_in_7days( int $form_id ): bool { // Get the form (post) publish date. $date_created = get_post_field( 'post_date', $form_id, 'raw' ); // If the form date is not available, return false. if ( empty( $date_created ) ) { return false; } // Calculate the time difference between the post date and the current date. $time_difference = time() - strtotime( $date_created ); // Compare the time difference with 7 days in seconds. return $time_difference <= 7 * DAY_IN_SECONDS; } } Lite/Reports/EntriesCount.php 0000644 00000012473 15174710275 0012252 0 ustar 00 <?php namespace WPForms\Lite\Reports; /** * Generate form submissions reports. * * @since 1.5.4 */ class EntriesCount { /** * Constructor. * * @since 1.5.4 */ public function __construct() {} /** * Get entries count grouped by form. * Main point of entry to fetch form entry count data from DB. * Cache the result. * * @since 1.5.4 * * @return array */ public function get_by_form() { // Get form IDs. $forms = wpforms()->obj( 'form' )->get( '', [ 'fields' => 'ids' ] ); // Return early if no forms found. if ( empty( $forms ) || ! is_array( $forms ) ) { return []; } $results = []; // Iterate through form IDs. foreach ( $forms as $form_id ) { // Get entries count for the form. $count = absint( get_post_meta( $form_id, 'wpforms_entries_count', true ) ); // Skip if the count is empty. if ( empty( $count ) ) { continue; } // Add form details to the result. $results[ $form_id ] = [ 'form_id' => $form_id, 'count' => $count, 'title' => get_the_title( $form_id ), ]; } // Sort forms by entries count (desc). if ( ! empty( $results ) ) { uasort( $results, function ( $a, $b ) { return ( $a['count'] > $b['count'] ) ? -1 : 1; } ); } return $results; } /** * Retrieve and calculate form trends data for Lite users. * * This function calculates and returns trends data for Lite users based on the total number * of entries submitted per week compared to the previous week's total entries. Optionally * updates the database with the calculated data. * * @since 1.8.8 * * @return array */ public function get_form_trends() { // Get form IDs. $results = $this->get_by_form(); // Collection of form IDs that don't have valid previous week's count data. $maybe_unset_form_ids = []; foreach ( $results as $form_id => &$form ) { // Retrieve the previous week's count data from post meta. $previous_week_count = get_post_meta( $form_id, 'wpforms_entries_count_previous_week', true ); // Continue to the next form if the count data is not valid. if ( ! is_array( $previous_week_count ) || count( $previous_week_count ) !== 3 ) { $maybe_unset_form_ids[] = $form_id; continue; } // Continue to the next form if the previous week's count data is not valid. if ( count( array_unique( $previous_week_count ) ) === 1 ) { continue; } list( $total_previous_week, $count_previous_week, $prev_count_previous_week ) = $previous_week_count; // Calculate the form's trends data. $form['total'] = $total_previous_week + $count_previous_week; $form['count'] = $form['total'] - $total_previous_week; $form['count_previous_week'] = $prev_count_previous_week; // If both the current week's count and the previous week's count are zero, set trends to zero. if ( $form['count_previous_week'] === 0 && $form['count'] === 0 ) { $form['trends'] = 0; continue; } // If trends are set to be skipped, set trends to zero, and set the previous week's count to zero. // Thies's been needed since at this stage we don't know the number of entries submitted in the previous week. if ( (bool) get_post_meta( $form_id, 'wpforms_entries_count_previous_week_skip_trends', true ) ) { $form['trends'] = 0; $form['count_previous_week'] = 0; continue; } $form['trends'] = $this->get_calculated_trends( $form['count'], $form['count_previous_week'] ); } // Unset forms that don't have valid previous week's count data. return $this->maybe_unset_form_ids( $results, $maybe_unset_form_ids ); } /** * Unsets forms from the results array that lack valid previous week's count data. * * This function checks for the presence of valid previous week's count data for each form in the * provided results array. If all forms in the array lack valid data, the original results array is * returned without any changes. Otherwise, forms without valid data are unset from the array. * * @since 1.8.8 * * @param array $results The original array of form results. * @param array $maybe_unset_form_ids The form IDs that may need to be unset. * * @return array */ private function maybe_unset_form_ids( $results, $maybe_unset_form_ids ) { if ( empty( $maybe_unset_form_ids ) ) { return $results; } // If all forms don't have valid previous week's count data, return early. if ( count( $maybe_unset_form_ids ) === count( $results ) ) { return $results; } // Unset forms that don't have valid previous week's count data. foreach ( $maybe_unset_form_ids as $form_id ) { unset( $results[ $form_id ] ); } return $results; } /** * Get the calculated trends based on the count and count from the previous week. * * This function calculates and returns the trends based on the current count * and the count from the previous week. * * @since 1.8.8 * * @param int $count The current count. * @param int $count_previous_week The count from the previous week. * * @return int */ private function get_calculated_trends( $count, $count_previous_week ) { // If count from the previous week is zero, set trends to 100 to avoid division by zero. return ( $count_previous_week === 0 ) ? 100 : round( ( $count - $count_previous_week ) / $count_previous_week * 100 ); } } Lite/Admin/ConnectSkin.php 0000644 00000001754 15174710275 0011440 0 ustar 00 <?php namespace WPForms\Lite\Admin; use WP_Ajax_Upgrader_Skin; use WP_Error; // phpcs:ignore WPForms.PHP.UseStatement.UnusedUseStatement /** * WPForms Connect Skin. * * WPForms Connect is our service that makes it easy for non-techy users to * upgrade to WPForms Pro without having to manually install WPForms Pro plugin. * * @since 1.5.5 * @since 1.5.6.1 Extend PluginSilentUpgraderSkin and clean up the class. * @since 1.9.5 Extend WP_Ajax_Upgrader_Skin class. */ class ConnectSkin extends WP_Ajax_Upgrader_Skin { /** * Instead of outputting HTML for errors, json_encode the errors and send them * back to the Ajax script for processing. * * @since 1.5.5 * * @param string|WP_Error $errors Errors. * @param mixed ...$args Optional text replacements. */ public function error( $errors, ...$args ) { if ( ! empty( $errors ) ) { wp_send_json_error( esc_html__( 'There was an error installing WPForms Pro. Please try again.', 'wpforms-lite' ) ); } } } Lite/Admin/Pages/Addons.php 0000644 00000004705 15174710275 0011470 0 ustar 00 <?php namespace WPForms\Lite\Admin\Pages; /** * Addons page for Lite. * * @since 1.6.7 */ class Addons { /** * Page slug. * * @since 1.6.7 * * @type string */ const SLUG = 'addons'; /** * Determine if current class is allowed to load. * * @since 1.6.7 * * @return bool */ public function allow_load() { return wpforms_is_admin_page( self::SLUG ); } /** * Init. * * @since 1.6.7 */ public function init() { if ( ! $this->allow_load() ) { return; } // Define hooks. $this->hooks(); } /** * Hooks. * * @since 1.6.7 */ public function hooks() { add_action( 'admin_enqueue_scripts', [ $this, 'enqueues' ] ); add_action( 'admin_notices', [ $this, 'notices' ] ); add_action( 'wpforms_admin_page', [ $this, 'output' ] ); } /** * Add appropriate scripts to the Addons page. * * @since 1.6.7 */ public function enqueues() { // JavaScript. wp_enqueue_script( 'listjs', WPFORMS_PLUGIN_URL . 'assets/lib/list.min.js', [ 'jquery' ], '1.5.0', false ); } /** * Notices. * * @since 1.6.7.1 */ public function notices() { $notice = sprintf( '<p class="notice-title"><strong>%1$s</strong></p> <p>%2$s</p> <p class="notice-buttons"> <a href="%3$s" class="wpforms-btn wpforms-btn-orange wpforms-btn-md" target="_blank" rel="noopener noreferrer"> %4$s </a> </p>', esc_html__( 'Upgrade to Unlock WPForms Addons', 'wpforms-lite' ), esc_html__( 'Access powerful marketing and payment integrations, advanced form fields, and more when you purchase our Plus, Pro, or Elite plans.', 'wpforms-lite' ), esc_url( wpforms_admin_upgrade_link( 'addons', 'All Addons' ) ), esc_html__( 'Upgrade Now', 'wpforms-lite' ) ); \WPForms\Admin\Notice::info( $notice, [ 'autop' => false ] ); } /** * Render the Addons page. * * @since 1.6.7 */ public function output() { $addons = wpforms()->obj( 'addons' )->get_all(); if ( empty( $addons ) ) { return; } // WPForms 1.8.7 core includes Custom Captcha. // The Custom Captcha addon will only work on WPForms 1.8.6 and earlier versions. unset( $addons['wpforms-captcha'] ); echo wpforms_render( // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped 'admin/addons', [ 'upgrade_link_base' => wpforms_admin_upgrade_link( 'addons' ), 'addons' => $addons, ], true ); } } Lite/Admin/Education/Builder/Confirmations.php 0000644 00000003633 15174710275 0015334 0 ustar 00 <?php namespace WPForms\Lite\Admin\Education\Builder; use WPForms_Builder_Panel_Settings; use WPForms\Admin\Education\EducationInterface; /** * Confirmations Education feature. * * @since 1.6.9 */ class Confirmations implements EducationInterface { /** * Indicate if current Education feature is allowed to load. * * @since 1.6.9 * * @return bool */ public function allow_load() { return wpforms_is_admin_page( 'builder' ); } /** * Init. * * @since 1.6.9 */ public function init() { if ( ! $this->allow_load() ) { return; } $this->hooks(); } /** * Load hooks. * * @since 1.6.9 */ private function hooks() { add_action( 'wpforms_lite_form_settings_confirmations_single_after', [ $this, 'entry_preview_settings' ], 10, 2 ); } /** * Add education settings located in confirmation inside the message block. * * @since 1.6.9 * * @param WPForms_Builder_Panel_Settings $settings Builder panel settings. * @param int $field_id Field ID. */ public function entry_preview_settings( $settings, $field_id ) { wpforms_panel_field( 'toggle', 'confirmations', 'message_entry_preview', $settings->form_data, esc_html__( 'Show entry preview after confirmation', 'wpforms-lite' ), [ 'input_id' => 'wpforms-panel-field-confirmations-message_entry_preview-' . wpforms_validate_field_id( $field_id ), 'input_class' => 'wpforms-panel-field-confirmations-message_entry_preview education-modal', 'parent' => 'settings', 'subsection' => wpforms_validate_field_id( $field_id ), 'pro_badge' => true, 'data' => [ 'action' => 'upgrade', 'name' => esc_html__( 'Show Entry Preview', 'wpforms-lite' ), 'utm-content' => 'Show Entry Preview', 'licence' => 'pro', ], 'attrs' => [ 'disabled' => 'disabled', ], 'value' => false, ] ); } } Lite/Admin/Education/Builder/Notifications.php 0000644 00000006317 15174710275 0015334 0 ustar 00 <?php namespace WPForms\Lite\Admin\Education\Builder; use WPForms\Admin\Education\EducationInterface; use WPForms_Builder_Panel_Settings; /** * Notifications Education feature. * * @since 1.7.7 */ class Notifications implements EducationInterface { /** * Init. * * @since 1.7.7 */ public function init() { if ( ! $this->allow_load() ) { return; } $this->hooks(); } /** * Indicate if current Education feature is allowed to load. * * @since 1.7.7 * * @return bool */ public function allow_load() { return wpforms_is_admin_page( 'builder' ); } /** * Load hooks. * * @since 1.7.7 */ private function hooks() { add_action( 'wpforms_lite_form_settings_notifications_block_content_after', [ $this, 'advanced_section' ], 10, 2 ); } /** * Output Notification Advanced section. * * @since 1.7.7 * * @param WPForms_Builder_Panel_Settings $settings Builder panel settings. * @param int $id Notification id. */ public function advanced_section( $settings, $id ) { /** * Filter the "Advanced" content. * * @since 1.8.5 * * @param string $content The content. * @param WPForms_Builder_Panel_Settings $settings Builder panel settings. * @param int $id Notification id. */ $content = apply_filters( 'wpforms_lite_admin_education_builder_notifications_advanced_settings_content', '', $settings, $id ); $content .= wpforms_panel_field( 'toggle', 'notifications', 'file_upload_attachment_enable', $settings->form_data, esc_html__( 'Enable File Upload Attachments', 'wpforms-lite' ), [ 'input_class' => 'notifications_enable_file_upload_attachment_toggle education-modal', 'parent' => 'settings', 'subsection' => $id, 'pro_badge' => true, 'data' => [ 'action' => 'upgrade', 'name' => esc_html__( 'File Upload Attachments', 'wpforms-lite' ), 'utm-content' => 'File Upload Attachments', 'licence' => 'pro', ], 'attrs' => [ 'disabled' => 'disabled', ], 'value' => false, ], false ); $content .= wpforms_panel_field( 'toggle', 'notifications', 'entry_csv_attachment_enable', $settings->form_data, esc_html__( 'Enable Entry CSV Attachment', 'wpforms-lite' ), [ 'input_class' => 'notifications_enable_entry_csv_attachment_toggle education-modal', 'parent' => 'settings', 'subsection' => $id, 'pro_badge' => true, 'data' => [ 'action' => 'upgrade', 'name' => esc_html__( 'Entry CSV Attachment', 'wpforms-lite' ), 'utm-content' => 'Entry CSV Attachment', 'licence' => 'pro', ], 'attrs' => [ 'disabled' => 'disabled', ], 'value' => false, ], false ); // Wrap advanced settings to the unfoldable group. wpforms_panel_fields_group( $content, [ 'borders' => [ 'top' ], 'class' => 'wpforms-builder-notifications-advanced opened', 'default' => 'opened', 'group' => 'settings_notifications_advanced', 'title' => esc_html__( 'Advanced', 'wpforms-lite' ), 'unfoldable' => true, ] ); } } Lite/Admin/Education/Builder/Fields.php 0000644 00000013011 15174710275 0013716 0 ustar 00 <?php namespace WPForms\Lite\Admin\Education\Builder; use WPForms\Admin\Education; use WPForms\Helpers\Form; /** * Builder/Fields Education for Lite. * * @since 1.6.6 */ class Fields extends Education\Builder\Fields { /** * Hooks. * * @since 1.6.6 */ public function hooks() { add_filter( 'wpforms_builder_fields_buttons', [ $this, 'add_fields' ], 500 ); add_filter( 'wpforms_builder_field_button_attributes', [ $this, 'fields_attributes' ], 100, 2 ); add_action( 'wpforms_field_options_after_advanced-options', [ $this, 'field_conditional_logic' ] ); add_action( 'wpforms_builder_panel_fields_panel_content_title_after', [ $this, 'form_preview_notice' ] ); } /** * Add fields. * * @since 1.6.6 * * @param array|mixed $fields Form fields. * * @return array */ public function add_fields( $fields ) { $fields = (array) $fields; foreach ( $fields as $group => $group_data ) { $edu_fields = $this->fields->get_by_group( $group ); $edu_fields = $this->fields->set_values( $edu_fields, 'class', 'education-modal', 'empty' ); foreach ( $edu_fields as $edu_field ) { // Skip if in the current group already exist field of this type. if ( ! empty( wp_list_filter( $group_data, [ 'type' => $edu_field['type'] ] ) ) ) { continue; } $addon = ! empty( $edu_field['addon'] ) ? $this->addons->get_addon( $edu_field['addon'] ) : []; if ( ! empty( $addon ) ) { $edu_field['license'] = $addon['license_level'] ?? ''; } $fields[ $group ]['fields'][] = $edu_field; } } return $fields; } /** * Display a conditional logic settings section for fields inside the form builder. * * @since 1.6.6 * * @param array $field Field data. */ public function field_conditional_logic( array $field ): void { // Certain fields don't support conditional logic. if ( in_array( $field['type'], [ 'pagebreak', 'divider', 'hidden' ], true ) ) { return; } ?> <div class="wpforms-field-option-group wpforms-field-option-group-conditionals"> <a href="#" class="wpforms-field-option-group-toggle education-modal" data-name="<?php esc_attr_e( 'Smart Conditional Logic', 'wpforms-lite' ); ?>" data-utm-content="Smart Conditional Logic"> <?php esc_html_e( 'Smart Logic', 'wpforms-lite' ); ?> </a> </div> <?php } /** * Adjust attributes on field buttons. * * @since 1.6.6 * * @param array|mixed $atts Button attributes. * @param array $field Button properties. * * @return array Attributes array. */ public function fields_attributes( $atts, $field ) { $atts = (array) $atts; $atts['data']['utm-content'] = ! empty( $field['name_en'] ) ? $field['name_en'] : ''; if ( ! empty( $field['class'] ) && $field['class'] === 'education-modal' ) { $atts['class'][] = 'wpforms-not-available'; } if ( empty( $field['addon'] ) ) { return $atts; } $addon = $this->addons->get_addon( $field['addon'] ); if ( empty( $addon ) ) { return $atts; } if ( ! empty( $addon['video'] ) ) { $atts['data']['video'] = $addon['video']; } if ( ! empty( $field['license'] ) ) { $atts['data']['license'] = $field['license']; } return $atts; } /** * The form preview Pro fields notice. * * @since 1.9.4 * * @param array $form_data Form data. * * @noinspection HtmlUnknownTarget */ public function form_preview_notice( array $form_data ): void { $dismissed = get_user_meta( get_current_user_id(), 'wpforms_dismissed', true ); $pro_fields = Form::get_form_pro_fields( $form_data ); $is_quiz_enabled = ! empty( $form_data['settings']['quiz']['enabled'] ); // Check whether the notice is dismissed OR the form doesn't contain Pro fields. if ( ! empty( $dismissed['edu-pro-fields-form-preview-notice'] ) || ( empty( $pro_fields ) && ! $is_quiz_enabled ) ) { return; } if ( $is_quiz_enabled ) { $this->print_quiz_notice(); return; } $content = sprintf( wp_kses( /* translators: %s - WPForms.com announcement page URL. */ __( 'They will not be present in the published form. <a href="%1$s" target="_blank" rel="noopener noreferrer">Upgrade now</a> to unlock these features.', 'wpforms-lite' ), [ 'a' => [ 'href' => [], 'target' => [], 'rel' => [], ], ] ), wpforms_admin_upgrade_link( 'Builder - Settings', 'AI Form - Pro Fields in Lite notice' ) ); $this->print_form_preview_notice( [ 'class' => 'wpforms-alert-warning', 'title' => esc_html__( 'Your Form Contains Pro Fields', 'wpforms-lite' ), 'content' => $content, 'dismiss_section' => 'pro-fields-form-preview-notice', ] ); } /** * Print the Quiz addon notice. * * @since 1.9.9 * * @noinspection HtmlUnknownTarget */ private function print_quiz_notice(): void { $content = sprintf( wp_kses( /* translators: %s - Upgrade license page URL. */ __( 'Quiz functionality will not be present in the published form. <a href="%1$s" target="_blank" rel="noopener noreferrer">Upgrade now</a> to unlock the Quiz Addon.', 'wpforms-lite' ), [ 'a' => [ 'href' => [], 'target' => [], 'rel' => [], ], ] ), wpforms_admin_upgrade_link( 'Builder - Settings', 'AI Form - Quiz addon in Lite notice' ) ); $this->print_form_preview_notice( [ 'class' => 'wpforms-alert-warning', 'title' => esc_html__( 'Your Form Uses the Quiz Addon', 'wpforms-lite' ), 'content' => $content, 'dismiss_section' => 'quiz-form-preview-notice', ] ); } } Lite/Admin/Education/Builder/DidYouKnow.php 0000644 00000004164 15174710275 0014555 0 ustar 00 <?php namespace WPForms\Lite\Admin\Education\Builder; use \WPForms\Admin\Education\EducationInterface; /** * Builder/DidYouKnow Education feature. * * @since 1.6.6 */ class DidYouKnow implements EducationInterface { /** * Indicate if current Education feature is allowed to load. * * @since 1.6.6 * * @return bool */ public function allow_load() { return wpforms_is_admin_page( 'builder' ); } /** * Init. * * @since 1.6.6 */ public function init() { if ( ! $this->allow_load() ) { return; } // Define hooks. $this->hooks(); } /** * Hooks. * * @since 1.6.6 */ public function hooks() { add_action( 'wpforms_builder_settings_notifications_after', [ $this, 'notifications' ] ); add_action( 'wpforms_builder_settings_confirmations_after', [ $this, 'confirmations' ] ); } /** * Display on the Notifications panel. * * @since 1.6.6 */ public function notifications() { $this->display( 'notifications', [ 'desc' => esc_html__( 'You can have multiple notifications with conditional logic.', 'wpforms-lite' ) ] ); } /** * Display on the Confirmations panel. * * @since 1.6.6 */ public function confirmations() { $this->display( 'confirmations', [ 'desc' => esc_html__( 'You can have multiple confirmations with conditional logic.', 'wpforms-lite' ) ] ); } /** * Display message. * * @since 1.6.6 * * @param string $section Form builder section/area (slug). * @param array $settings Notice settings array. */ private function display( $section, $settings ) { $dismissed = get_user_meta( get_current_user_id(), 'wpforms_dismissed', true ); // Check if not dismissed. if ( ! empty( $dismissed[ 'edu-builder-did-you-know-' . $section ] ) ) { return; } echo wpforms_render( // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped 'education/builder/did-you-know', [ 'desc' => $settings['desc'], 'more' => ! empty( $settings['more'] ) ? $settings['more'] : '', 'link' => wpforms_admin_upgrade_link( 'Form Builder DYK', ucfirst( $section ) ), 'section' => $section, ], true ); } } Lite/Admin/Education/Core.php 0000644 00000001630 15174710275 0012016 0 ustar 00 <?php namespace WPForms\Lite\Admin\Education; /** * Education core for Lite. * * @since 1.6.6 */ class Core extends \WPForms\Admin\Education\Core { /** * Hooks. * * @since 1.6.6 */ protected function hooks() { parent::hooks(); add_action( 'admin_enqueue_scripts', [ $this, 'enqueues' ] ); } /** * Load enqueues. * * @since 1.6.6 */ public function enqueues() { parent::enqueues(); $min = wpforms_get_min_suffix(); wp_enqueue_script( 'wpforms-lite-admin-education-core', WPFORMS_PLUGIN_URL . "assets/lite/js/admin/education/core{$min}.js", [ 'wpforms-admin-education-core' ], WPFORMS_VERSION, false ); // Builder Education styles. if ( wpforms_is_admin_page( 'builder' ) ) { wp_enqueue_style( 'wpforms-lite-admin-education-builder', WPFORMS_PLUGIN_URL . "assets/lite/css/builder-education{$min}.css", [], WPFORMS_VERSION ); } } } Lite/Admin/Education/LiteConnect.php 0000644 00000027137 15174710275 0013347 0 ustar 00 <?php namespace WPForms\Lite\Admin\Education; use WPForms\Admin\Education; use WPForms\Integrations\LiteConnect\API; use WPForms\Lite\Integrations\LiteConnect\LiteConnect as LiteConnectClass; use WPForms\Lite\Integrations\LiteConnect\Integration as LiteConnectIntegration; /** * Admin/Settings/LiteConnect Education feature for Lite. * * @since 1.7.4 */ class LiteConnect implements Education\EducationInterface { /** * Indicate if Lite Connect entry backup is enabled. * * @since 1.7.4 * * @var int */ private $is_enabled; /** * Indicate if current Education feature is allowed to load. * * @since 1.7.4 * * @return bool */ public function allow_load() { // Do not load if Lite Connect integration is not allowed. if ( ! LiteConnectClass::is_allowed() ) { return false; } // Do not load if user doesn't have permissions to update settings. if ( ! wpforms_current_user_can( wpforms_get_capability_manage_options() ) ) { return false; } // Load only in certain cases. return wp_doing_ajax() || wpforms_is_admin_page( 'builder' ) || wpforms_is_admin_page( 'settings' ) || wpforms_is_admin_page( 'overview' ) || wpforms_is_admin_page( 'entries' ) || $this->is_dashboard() || $this->is_embed_page(); } /** * Init. * * @since 1.7.4 */ public function init() { if ( ! $this->allow_load() ) { return; } $this->is_enabled = LiteConnectClass::is_enabled() ? 1 : 0; // Define hooks. $this->hooks(); } /** * Hooks. * * @since 1.7.4 */ private function hooks() { add_action( 'admin_footer', [ $this, 'modal_template' ], 10, 2 ); add_action( 'admin_enqueue_scripts', [ $this, 'enqueues' ] ); // Ajax action. add_action( 'wp_ajax_wpforms_update_lite_connect_enabled_setting', [ $this, 'ajax_update_lite_connect_enabled_setting' ] ); add_action( 'wp_ajax_wpforms_lite_connect_finalize', [ $this, 'ajax_lite_connect_finalize' ] ); // Content filters. add_filter( 'wpforms_lite_admin_dashboard_widget_content_html_chart_block_before', [ $this, 'dashboard_widget_before_content' ] ); add_filter( 'wpforms_builder_output_before_toolbar', [ $this, 'top_bar_content' ] ); add_filter( 'wpforms_admin_challenge_embed_template_congrats_popup_footer', [ $this, 'challenge_popup_footer_content' ] ); } /** * Check whether it is the Dashboard admin page. * * @since 1.7.4 * * @return bool */ private function is_dashboard() { global $pagenow; return $pagenow === 'index.php'; } /** * Check whether it is the form embedding admin page (Edit Post or Edit Page). * * @since 1.7.4 * * @return bool */ private function is_embed_page() { if ( function_exists( 'get_current_screen' ) ) { return wpforms()->obj( 'challenge' )->is_form_embed_page(); } global $pagenow; return in_array( $pagenow, [ 'edit.php', 'post.php', 'post-new.php' ], true ); } /** * Load enqueues. * * @since 1.7.4 */ public function enqueues() { $min = wpforms_get_min_suffix(); // On the Dashboard and form embedding pages we should load additional scripts and styles. if ( $this->is_dashboard() || $this->is_embed_page() ) { $this->dashboard_enqueues(); } wp_enqueue_script( 'wpforms-lite-admin-education-lite-connect', WPFORMS_PLUGIN_URL . "assets/lite/js/admin/education/lite-connect{$min}.js", [ 'jquery', 'wp-util' ], WPFORMS_VERSION, true ); wp_localize_script( 'wpforms-lite-admin-education-lite-connect', 'wpforms_education_lite_connect', $this->get_js_strings() ); } /** * Dashboard enqueues. * * @since 1.7.4 */ private function dashboard_enqueues() { $min = wpforms_get_min_suffix(); // jQuery.Confirm Reloaded. wp_enqueue_script( 'jquery-confirm', WPFORMS_PLUGIN_URL . 'assets/lib/jquery.confirm/jquery-confirm.min.js', [ 'jquery' ], '1.0.0', true ); // jQuery.Confirm Reloaded. wp_enqueue_style( 'jquery-confirm', WPFORMS_PLUGIN_URL . 'assets/lib/jquery.confirm/jquery-confirm.min.css', [], '1.0.0' ); // FontAwesome. wp_enqueue_style( 'wpforms-font-awesome', WPFORMS_PLUGIN_URL . 'assets/lib/font-awesome/css/all.min.css', null, '7.0.1' ); // FontAwesome v4 compatibility shims. wp_enqueue_style( 'wpforms-font-awesome-v4-shim', WPFORMS_PLUGIN_URL . 'assets/lib/font-awesome/css/v4-shims.min.css', null, '4.7.0' ); // Dashboard Education styles. wp_enqueue_style( 'wpforms-lite-admin-education-lite-connect', WPFORMS_PLUGIN_URL . "assets/lite/css/dashboard-education{$min}.css", [], WPFORMS_VERSION ); } /** * Confirmation modal template. * * @since 1.7.4 */ public function modal_template() { // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo wpforms_render( 'education/lite-connect-modal' ); if ( wpforms_is_admin_page( 'builder' ) ) { // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo wpforms_render( 'education/builder/lite-connect/ai-modal' ); } } /** * Get localize strings. * * @since 1.7.4 * * @return array */ private function get_js_strings() { return [ 'ajax_url' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'wpforms-lite-connect-toggle' ), 'is_enabled' => $this->is_enabled, 'enable_modal' => [ 'confirm' => esc_html__( 'Enable Entry Backups', 'wpforms-lite' ), 'cancel' => esc_html__( 'No Thanks', 'wpforms-lite' ), ], 'enable_ai' => [ 'confirm' => esc_html__( 'Enable AI Features', 'wpforms-lite' ), 'enabled_title' => esc_html__( 'AI Features Enabled', 'wpforms-lite' ), ], 'disable_modal' => [ 'title' => esc_html__( 'Are you sure?', 'wpforms-lite' ), 'content' => esc_html__( 'If you disable Lite Connect, you will no longer be able to restore your entries when you upgrade to WPForms Pro.', 'wpforms-lite' ), 'confirm' => esc_html__( 'Disable Entry Backups', 'wpforms-lite' ), 'cancel' => esc_html__( 'Cancel', 'wpforms-lite' ), ], 'update_result' => [ 'enabled_title' => esc_html__( 'Entry Backups Enabled', 'wpforms-lite' ), 'enabled' => esc_html__( 'Awesome! If you decide to upgrade to WPForms Pro, you can restore your entries and will have instant access to reports.', 'wpforms-lite' ), 'disabled_title' => esc_html__( 'Entry Backups Disabled', 'wpforms-lite' ), 'disabled' => esc_html__( 'Form Entry Backups were successfully disabled.', 'wpforms-lite' ), 'error_title' => esc_html__( 'Error', 'wpforms-lite' ), 'error' => esc_html__( 'Unfortunately, the error occurs while updating Form Entry Backups setting. Please try again later.', 'wpforms-lite' ), 'close' => esc_html__( 'Close', 'wpforms-lite' ), ], ]; } /** * Generate Lite Connect entries information. * * @since 1.7.4 * * @return string */ private function get_lite_connect_entries_since_info() { $entries_count = LiteConnectIntegration::get_new_entries_count(); $enabled_since = LiteConnectIntegration::get_enabled_since(); $string = sprintf( esc_html( /* translators: %d - backed up entries count. */ _n( '%d entry backed up', '%d entries backed up', $entries_count, 'wpforms-lite' ) ), absint( $entries_count ) ); if ( ! empty( $enabled_since ) ) { $string .= ' ' . sprintf( /* translators: %s - time when Lite Connect was enabled. */ esc_html__( 'since %s', 'wpforms-lite' ), esc_html( wpforms_date_format( $enabled_since, '', true ) ) ); } return $string; } /** * Add content before the Chart block in the Dashboard Widget. * * @since 1.7.4 * * @param string $content Content. * * @return string */ public function dashboard_widget_before_content( $content ) { $toggle = wpforms_panel_field_toggle_control( [ 'control-class' => 'wpforms-setting-lite-connect-auto-save-toggle', ], 'wpforms-setting-lite-connect-enabled', '', esc_html__( 'Enable Form Entry Backups', 'wpforms-lite' ), $this->is_enabled, 'disabled' ); return wpforms_render( 'education/admin/lite-connect/dashboard-widget-before', [ 'toggle' => $toggle, 'is_enabled' => $this->is_enabled, 'entries_since_info' => $this->get_lite_connect_entries_since_info(), ], true ); } /** * Add top bar before the toolbar in the Form Builder. * * @since 1.7.4 * * @param string $content Content before the toolbar. Defaults to empty string. * * @return string */ public function top_bar_content( $content ) { if ( $this->is_enabled ) { return $content; } $dismissed = get_user_meta( get_current_user_id(), 'wpforms_dismissed', true ); // Skip when top bar is dismissed. if ( ! empty( $dismissed['edu-builder-lite-connect-top-bar'] ) ) { return $content; } $toggle = wpforms_panel_field_toggle_control( [ 'control-class' => 'wpforms-setting-lite-connect-auto-save-toggle', ], 'wpforms-setting-lite-connect-enabled', '', esc_html__( 'Enable Form Entry Backups for Free', 'wpforms-lite' ), $this->is_enabled, 'disabled' ); return wpforms_render( 'education/builder/lite-connect/top-bar', [ 'toggle' => $toggle, 'is_enabled' => $this->is_enabled, ], true ) . $content; } /** * Challenge Congrats popup footer. * * @since 1.7.4 * * @param string $content Footer content. * * @return string */ public function challenge_popup_footer_content( $content ) { if ( $this->is_enabled ) { return $content; } $toggle = wpforms_panel_field_toggle_control( [ 'control-class' => 'wpforms-setting-lite-connect-auto-save-toggle', ], 'wpforms-setting-lite-connect-enabled', '', esc_html__( 'Enable Form Entry Backups for Free', 'wpforms-lite' ), $this->is_enabled, 'disabled' ); return wpforms_render( 'education/admin/lite-connect/challenge-popup-footer', [ 'toggle' => $toggle, 'is_enabled' => $this->is_enabled, ], true ); } /** * AJAX checks. * * @since 1.9.1 */ private function ajax_checks() { // Run a security check. check_ajax_referer( 'wpforms-lite-connect-toggle', 'nonce' ); // Check for permissions. if ( ! wpforms_current_user_can( wpforms_get_capability_manage_options() ) ) { wp_send_json_error( esc_html__( 'You do not have permission.', 'wpforms-lite' ) ); } } /** * AJAX action: update Lite Connect Enabled setting. * * @since 1.7.4 */ public function ajax_update_lite_connect_enabled_setting() { $this->ajax_checks(); $slug = LiteConnectClass::SETTINGS_SLUG; $settings = get_option( 'wpforms_settings', [] ); $settings[ $slug ] = ! empty( $_POST['value'] ); // phpcs:ignore WordPress.Security.NonceVerification.Missing wpforms_update_settings( $settings ); if ( ! $settings[ $slug ] ) { wp_send_json_success( '' ); } // Reset generate key attempts counter. update_option( API::GENERATE_KEY_ATTEMPT_COUNTER_OPTION, 0 ); // We have to start requesting site keys in ajax, turning on the LC functionality. // First, the request to the API server will be sent. // Second, the server will respond to our callback URL /wpforms/auth/key/nonce, and the site key will be stored in the DB. // Third, we have to get access via a separate HTTP request. ( new LiteConnectIntegration() )->update_keys(); // First request here. wp_send_json_success( $this->get_lite_connect_entries_since_info() ); } /** * AJAX action: Finalize Lite Connect setup. * * @since 1.9.1 */ public function ajax_lite_connect_finalize() { $this->ajax_checks(); if ( $this->is_enabled ) { ( new LiteConnectIntegration() )->update_keys(); // Simulate third request. } wp_send_json_success(); } } Lite/Admin/Education/Admin/NoticeBar.php 0000644 00000002253 15174710275 0014026 0 ustar 00 <?php namespace WPForms\Lite\Admin\Education\Admin; use \WPForms\Admin\Education; /** * Admin/NoticeBar Education feature for Lite. * * @since 1.6.6 */ class NoticeBar implements Education\EducationInterface { /** * Indicate if current Education feature is allowed to load. * * @since 1.6.6 * * @return bool */ public function allow_load() { return wpforms_is_admin_page(); } /** * Init. * * @since 1.6.6 */ public function init() { if ( ! $this->allow_load() ) { return; } // Define hooks. $this->hooks(); } /** * Hooks. * * @since 1.6.6 */ public function hooks() { add_action( 'wpforms_admin_header_before', [ $this, 'display' ] ); } /** * Notice bar display message. * * @since 1.6.6 */ public function display() { $dismissed = get_user_meta( get_current_user_id(), 'wpforms_dismissed', true ); if ( ! empty( $dismissed['edu-admin-notice-bar'] ) ) { return; } echo wpforms_render( // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped 'education/admin/notice-bar', [ 'upgrade_link' => wpforms_admin_upgrade_link( 'notice-bar', 'Upgrade to WPForms Pro' ), ], true ); } } Lite/Admin/Education/Admin/DidYouKnow.php 0000644 00000013542 15174710275 0014217 0 ustar 00 <?php namespace WPForms\Lite\Admin\Education\Admin; use WP_List_Table; use WPForms\Admin\Education\EducationInterface; use WPForms\Lite\Integrations\LiteConnect\LiteConnect; use WPForms\Lite\Integrations\LiteConnect\Integration as LiteConnectIntegration; /** * Admin/DidYouKnow Education feature. * * @since 1.7.4 */ class DidYouKnow implements EducationInterface { /** * Indicate if current Education feature is allowed to load. * * @since 1.7.4 * * @return bool */ public function allow_load() { // Load only on the `All Forms` admin page. return wpforms_is_admin_page( 'overview' ); } /** * Init. * * @since 1.7.4 */ public function init() { if ( ! $this->allow_load() ) { return; } // Define hooks. $this->hooks(); } /** * Hooks. * * @since 1.7.4 */ private function hooks() { add_action( 'wpforms_admin_overview_after_rows', [ $this, 'display' ] ); } /** * Messages. * * @since 1.7.4 * * @return array */ private function messages() { return [ [ 'slug' => 'lite-connect', 'is_allowed' => LiteConnect::is_allowed(), 'cont_class' => LiteConnect::is_enabled() ? 'wpforms-education-lite-connect-setting wpforms-hidden' : 'wpforms-education-lite-connect-setting', 'title' => esc_html__( 'Entries are not stored in WPForms Lite', 'wpforms-lite' ), 'desc' => esc_html__( 'Entries are available through email notifications. If you enable Entry Backups, you can restore them once you upgrade to WPForms Pro.', 'wpforms-lite' ), 'more_title' => esc_html__( 'Enable Entry Backups', 'wpforms-lite' ), 'more_link' => admin_url( 'admin.php?page=wpforms-settings' ), 'icon' => '<svg fill="#fff" viewBox="0 0 20 14"><path d="M16.78 6.1a3 3 0 0 0-4.47-3.56A4.97 4.97 0 0 0 3 5v.28A4.48 4.48 0 0 0 4.5 14H16a4 4 0 0 0 4-4c0-1.9-1.38-3.53-3.22-3.9Zm-4.37 2-.35.34A.75.75 0 0 1 11 8.4l-1-1.07v3.91c0 .44-.34.75-.75.75h-.5a.72.72 0 0 1-.75-.75v-3.9L6.97 8.4a.75.75 0 0 1-1.06.03l-.35-.35c-.31-.3-.31-.78 0-1.06l2.9-2.9a.74.74 0 0 1 1.04 0l2.9 2.9c.32.28.32.75 0 1.06Z"/></svg>', 'item' => 1, 'enabled' => [ 'cont_class' => LiteConnect::is_enabled() ? 'wpforms-education-lite-connect-enabled-info' : 'wpforms-education-lite-connect-enabled-info wpforms-hidden', 'title' => esc_html__( 'Entries Backups Are Enabled', 'wpforms-lite' ), 'more_title' => esc_html__( 'Restore Form Entries', 'wpforms-lite' ), 'more_link' => wpforms_admin_upgrade_link( 'forms-overview', 'restore-entries' ), 'more_class' => 'wpforms-is-enabled', 'desc' => $this->get_lite_connect_entries_since_info(), ], ], ]; } /** * Random message. * * @since 1.7.4 */ private function message_rnd() { $messages = $this->messages(); return $messages[ array_rand( $messages ) ]; } /** * Display message. * * @since 1.7.4 * * @param WP_List_Table $wp_list_table Instance of WP_List_Table. */ public function display( $wp_list_table ) { $dismissed = get_user_meta( get_current_user_id(), 'wpforms_dismissed', true ); // Do not display the message if it was dismissed. if ( ! empty( $dismissed['edu-admin-did-you-know-overview'] ) ) { return; } $message = $this->message_rnd(); // Display the message only if it is allowed. if ( isset( $message['is_allowed'] ) && empty( $message['is_allowed'] ) ) { return; } echo wpforms_render( // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped 'education/admin/did-you-know', [ 'slug' => ! empty( $message['slug'] ) ? $message['slug'] : '', 'cols' => $wp_list_table->get_column_count(), 'icon' => ! empty( $message['icon'] ) ? $message['icon'] : '', 'title' => ! empty( $message['title'] ) ? $message['title'] : esc_html__( 'Did You Know?', 'wpforms-lite' ), 'desc' => ! empty( $message['desc'] ) ? $message['desc'] : '', 'more_title' => ! empty( $message['more_title'] ) ? $message['more_title'] : esc_html__( 'Learn More', 'wpforms-lite' ), 'more_link' => ! empty( $message['more_link'] ) ? $message['more_link'] : '', 'more_class' => ! empty( $message['more_class'] ) ? $message['more_class'] : '', 'cont_class' => ! empty( $message['cont_class'] ) ? $message['cont_class'] : '', 'enabled_title' => ! empty( $message['enabled']['title'] ) ? $message['enabled']['title'] : esc_html__( 'Did You Know?', 'wpforms-lite' ), 'enabled_desc' => ! empty( $message['enabled']['desc'] ) ? $message['enabled']['desc'] : '', 'enabled_more_title' => ! empty( $message['enabled']['more_title'] ) ? $message['enabled']['more_title'] : esc_html__( 'Learn More', 'wpforms-lite' ), 'enabled_more_link' => ! empty( $message['enabled']['more_link'] ) ? $message['enabled']['more_link'] : '', 'enabled_more_class' => ! empty( $message['enabled']['more_class'] ) ? $message['enabled']['more_class'] : '', 'enabled_cont_class' => ! empty( $message['enabled']['cont_class'] ) ? $message['enabled']['cont_class'] : '', ], true ); } /** * Generate Lite Connect entries information. * * @since 1.7.4 * * @return string */ private function get_lite_connect_entries_since_info() { $entries_count = LiteConnectIntegration::get_new_entries_count(); $enabled_since = LiteConnectIntegration::get_enabled_since(); $string = sprintf( esc_html( /* translators: %d - backed up entries count. */ _n( '%d entry backed up', '%d entries backed up', $entries_count, 'wpforms-lite' ) ), absint( $entries_count ) ); if ( ! empty( $enabled_since ) ) { $string .= ' '; $string .= esc_html( sprintf( /* translators: %1$s - time when Lite Connect was enabled. */ __( 'since %1$s', 'wpforms-lite' ), wpforms_date_format( $enabled_since, '', true ) ) ); } return $string; } } Lite/Admin/Connect.php 0000644 00000020047 15174710275 0010607 0 ustar 00 <?php namespace WPForms\Lite\Admin; use WP_Error; use WPForms\Helpers\PluginSilentUpgrader; /** * WPForms Connect. * * WPForms Connect is our service that makes it easy for non-techy users to * upgrade to WPForms Pro without having to manually install WPForms Pro plugin. * * @since 1.5.5 */ class Connect { /** * WPForms Pro plugin basename. * * @since 1.8.4 * * @var string */ const PRO_PLUGIN = 'wpforms/wpforms.php'; /** * Constructor. * * @since 1.5.5 */ public function __construct() { $this->hooks(); } /** * Hooks. * * @since 1.5.5 */ public function hooks() { add_action( 'wpforms_settings_enqueue', [ $this, 'settings_enqueues' ] ); add_action( 'wp_ajax_wpforms_connect_url', [ $this, 'generate_url' ] ); add_action( 'wp_ajax_nopriv_wpforms_connect_process', [ $this, 'process' ] ); } /** * Settings page enqueues. * * @since 1.5.5 */ public function settings_enqueues() { $min = wpforms_get_min_suffix(); wp_enqueue_script( 'wpforms-connect', WPFORMS_PLUGIN_URL . "assets/lite/js/admin/connect{$min}.js", [ 'jquery' ], WPFORMS_VERSION, true ); } /** * Generate and return WPForms Connect URL. * * @since 1.5.5 */ public function generate_url() { // Run a security check. check_ajax_referer( 'wpforms-admin', 'nonce' ); // Check for permissions. if ( ! current_user_can( 'install_plugins' ) ) { wp_send_json_error( [ 'message' => esc_html__( 'You are not allowed to install plugins.', 'wpforms-lite' ) ] ); } $current_plugin = plugin_basename( WPFORMS_PLUGIN_FILE ); $is_pro = wpforms()->is_pro(); // Local development environment. if ( $current_plugin === self::PRO_PLUGIN && ! $is_pro ) { wp_send_json_error( [ 'message' => esc_html__( 'There must be a non-developer Lite version installed to upgrade.', 'wpforms-lite' ) ] ); } $key = ! empty( $_POST['key'] ) ? sanitize_text_field( wp_unslash( $_POST['key'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification // Empty license key. if ( empty( $key ) ) { wp_send_json_error( [ 'message' => esc_html__( 'Please enter your license key to connect.', 'wpforms-lite' ) ] ); } // Whether it is the pro version. if ( $is_pro ) { wp_send_json_error( [ 'message' => esc_html__( 'Only the Lite version can be upgraded.', 'wpforms-lite' ) ] ); } // Verify pro version is not installed. $active = activate_plugin( self::PRO_PLUGIN, false, false, true ); if ( ! is_wp_error( $active ) ) { // Deactivate Lite. deactivate_plugins( $current_plugin ); // phpcs:ignore WPForms.Comments.PHPDocHooks.RequiredHookDocumentation, WPForms.PHP.ValidateHooks.InvalidHookName do_action( 'wpforms_plugin_deactivated', $current_plugin ); wp_send_json_success( [ 'message' => esc_html__( 'WPForms Pro is installed but not activated.', 'wpforms-lite' ), 'reload' => true, ] ); } // Generate URL. $oth = hash( 'sha512', wp_rand() ); $hashed_oth = hash_hmac( 'sha512', $oth, wp_salt() ); update_option( 'wpforms_connect_token', $oth ); update_option( 'wpforms_connect', $key ); $version = WPFORMS_VERSION; $endpoint = admin_url( 'admin-ajax.php' ); $redirect = admin_url( 'admin.php?page=wpforms-settings' ); $url = add_query_arg( [ 'key' => $key, 'oth' => $hashed_oth, 'endpoint' => $endpoint, 'version' => $version, 'siteurl' => admin_url(), 'homeurl' => site_url(), 'redirect' => rawurldecode( base64_encode( $redirect ) ), // phpcs:ignore 'v' => 2, ], 'https://upgrade.wpforms.com' ); wp_send_json_success( [ 'url' => $url, 'back_url' => add_query_arg( [ 'action' => 'wpforms_connect', 'oth' => $hashed_oth, ], $endpoint ), ] ); } /** * Process WPForms Connect. * * @since 1.5.5 */ public function process() { // phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh, WPForms.PHP.HooksMethod.InvalidPlaceForAddingHooks $error = esc_html__( 'There was an error while installing an upgrade. Please download the plugin from wpforms.com and install it manually.', 'wpforms-lite' ); // Verify params present (oth & download link). $post_oth = ! empty( $_REQUEST['oth'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['oth'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification $post_url = ! empty( $_REQUEST['file'] ) ? esc_url_raw( wp_unslash( $_REQUEST['file'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification if ( empty( $post_oth ) || empty( $post_url ) ) { wp_send_json_error( $error ); } // Verify oth. $oth = get_option( 'wpforms_connect_token' ); if ( empty( $oth ) ) { wp_send_json_error( $error ); } if ( hash_hmac( 'sha512', $oth, wp_salt() ) !== $post_oth ) { wp_send_json_error( $error ); } // Delete so cannot replay. delete_option( 'wpforms_connect_token' ); // Set the current screen to avoid undefined notices. set_current_screen( 'wpforms_page_wpforms-settings' ); // Prepare variables. $url = esc_url_raw( add_query_arg( [ 'page' => 'wpforms-settings' ], admin_url( 'admin.php' ) ) ); // Verify pro not activated. if ( wpforms()->is_pro() ) { wp_send_json_success( esc_html__( 'Plugin installed & activated.', 'wpforms-lite' ) ); } // Verify pro not installed. $active = activate_plugin( self::PRO_PLUGIN, $url, false, true ); if ( ! is_wp_error( $active ) ) { $plugin = plugin_basename( WPFORMS_PLUGIN_FILE ); deactivate_plugins( $plugin ); // phpcs:ignore WPForms.Comments.PHPDocHooks.RequiredHookDocumentation, WPForms.PHP.ValidateHooks.InvalidHookName do_action( 'wpforms_plugin_deactivated', $plugin ); wp_send_json_success( esc_html__( 'Plugin installed & activated.', 'wpforms-lite' ) ); } $creds = request_filesystem_credentials( $url, '', false, false ); // Check for file system permissions. if ( $creds === false || ! WP_Filesystem( $creds ) ) { wp_send_json_error( esc_html__( 'There was an error while installing an upgrade. Please check file system permissions and try again. Also, you can download the plugin from wpforms.com and install it manually.', 'wpforms-lite' ) ); } /* * We do not need any extra credentials if we have gotten this far, so let's install the plugin. */ // Do not allow WordPress to search/download translations, as this will break JS output. remove_action( 'upgrader_process_complete', [ 'Language_Pack_Upgrader', 'async_upgrade' ], 20 ); // Create the plugin upgrader with our custom skin. $installer = new PluginSilentUpgrader( new ConnectSkin() ); // Error check. if ( ! method_exists( $installer, 'install' ) ) { wp_send_json_error( $error ); } // Check license key. $key = get_option( 'wpforms_connect', false ); if ( empty( $key ) ) { wp_send_json_error( new WP_Error( '403', esc_html__( 'No key provided.', 'wpforms-lite' ) ) ); } $installer->install( $post_url ); // phpcs:ignore // Flush the cache and return the newly installed plugin basename. wp_cache_flush(); $plugin_basename = $installer->plugin_info(); if ( $plugin_basename ) { // Deactivate the lite version first. $plugin = plugin_basename( WPFORMS_PLUGIN_FILE ); deactivate_plugins( $plugin ); // phpcs:ignore WPForms.Comments.PHPDocHooks.RequiredHookDocumentation, WPForms.PHP.ValidateHooks.InvalidHookName do_action( 'wpforms_plugin_deactivated', $plugin ); // Activate the plugin silently. $activated = activate_plugin( $plugin_basename, '', false, true ); if ( ! is_wp_error( $activated ) ) { add_option( 'wpforms_install', 1 ); wp_send_json_success( esc_html__( 'Plugin installed & activated.', 'wpforms-lite' ) ); } else { // Reactivate the lite plugin if pro activation failed. activate_plugin( plugin_basename( WPFORMS_PLUGIN_FILE ), '', false, true ); wp_send_json_error( esc_html__( 'Pro version installed but needs to be activated on the Plugins page inside your WordPress admin.', 'wpforms-lite' ) ); } } wp_send_json_error( $error ); } } Lite/Admin/Settings/Access.php 0000644 00000007726 15174710275 0012230 0 ustar 00 <?php namespace WPForms\Lite\Admin\Settings; use WPForms\Admin\Education\Helpers; /** * Settings Access tab. * * @since 1.5.8 */ class Access { /** * View slug. * * @since 1.5.8 * * @var string */ const SLUG = 'access'; /** * Constructor. * * @since 1.5.8 */ public function __construct() { $this->hooks(); } /** * Hooks. * * @since 1.5.8 */ public function hooks() { add_action( 'admin_enqueue_scripts', [ $this, 'enqueues' ] ); add_filter( 'wpforms_settings_tabs', [ $this, 'add_tab' ] ); add_filter( 'wpforms_settings_defaults', [ $this, 'add_section' ] ); } /** * Enqueues. * * @since 1.5.8 */ public function enqueues() { if ( ! wpforms_is_admin_page( 'settings', self::SLUG ) ) { return; } // Lity. wp_enqueue_style( 'wpforms-lity', WPFORMS_PLUGIN_URL . 'assets/lib/lity/lity.min.css', null, '3.0.0' ); wp_enqueue_script( 'wpforms-lity', WPFORMS_PLUGIN_URL . 'assets/lib/lity/lity.min.js', [ 'jquery' ], '3.0.0', true ); } /** * Add Access tab. * * @since 1.5.8 * * @param array $tabs Array of tabs. * * @return array Array of tabs. */ public function add_tab( $tabs ) { $tab = [ self::SLUG => [ 'name' => esc_html__( 'Access', 'wpforms-lite' ), 'form' => false, 'submit' => false, ], ]; return wpforms_list_insert_after( $tabs, 'geolocation', $tab ); } /** * Add Access settings section. * * @since 1.5.8 * * @param array $settings Settings sections. * * @return array */ public function add_section( $settings ) { $settings[ self::SLUG ][ self::SLUG . '-page' ] = [ 'id' => self::SLUG . '-page', 'content' => wpforms_render( 'education/admin/page', $this->template_data(),true ), 'type' => 'content', 'no_label' => true, ]; return $settings; } /** * Get the template data. * * @since 1.8.6 * * @return array */ private function template_data(): array { $images_url = WPFORMS_PLUGIN_URL . 'assets/images/lite-settings-access/'; return [ 'features' => [ __( 'Create Forms', 'wpforms-lite' ), __( 'Delete Forms', 'wpforms-lite' ), __( 'Edit Forms Entries', 'wpforms-lite' ), __( 'Edit Forms', 'wpforms-lite' ), __( 'Delete Others Forms', 'wpforms-lite' ), __( 'Edit Others Forms Entries', 'wpforms-lite' ), __( 'Edit Others Forms', 'wpforms-lite' ), __( 'View Forms Entries', 'wpforms-lite' ), __( 'Delete Forms Entries', 'wpforms-lite' ), __( 'View Forms', 'wpforms-lite' ), __( 'View Others Forms Entries', 'wpforms-lite' ), __( 'Delete Others Forms Entries', 'wpforms-lite' ), __( 'View Others Forms', 'wpforms-lite' ), ], 'images' => [ [ 'url' => $images_url . 'screenshot-access-controls.png', 'url2x' => $images_url . 'screenshot-access-controls@2x.png', 'title' => __( 'Simple Built-in Controls', 'wpforms-lite' ), ], [ 'url' => $images_url . 'screenshot-members.png', 'url2x' => $images_url . 'screenshot-members@2x.png', 'title' => __( 'Members Integration', 'wpforms-lite' ), ], [ 'url' => $images_url . 'screenshot-user-role-editor.png', 'url2x' => $images_url . 'screenshot-user-role-editor@2x.png', 'title' => __( 'User Role Editor Integration', 'wpforms-lite' ), ], ], 'utm_medium' => 'Settings - Access', 'utm_content' => 'Access Controls', 'heading_title' => __( 'Access Controls', 'wpforms-lite' ), 'heading_description' => sprintf( '<p>%1$s</p>', __( 'Access controls allows you to manage and customize access to WPForms functionality. You can easily grant or restrict access using the simple built-in controls, or use our official integrations with Members and User Role Editor plugins.', 'wpforms-lite' ) ), 'badge' => __( 'Pro', 'wpforms-lite' ), 'features_description' => __( 'Custom access to the following capabilities…', 'wpforms-lite' ), ]; } } Lite/Admin/DashboardWidget.php 0000644 00000041174 15174710275 0012255 0 ustar 00 <?php namespace WPForms\Lite\Admin; use WPForms\Admin\Blocks\Links; use WPForms\Admin\Dashboard\Widget; /** * Dashboard Widget shows a chart and the form entries stats in WP Dashboard. * * @since 1.5.0 */ class DashboardWidget extends Widget { /** * Widget settings. * * @since 1.5.0 * * @var array */ public $settings; /** * Init class. * * @since 1.5.5 */ public function init() { // phpcs:ignore WPForms.PHP.HooksMethod.InvalidPlaceForAddingHooks // phpcs:disable WPForms.PHP.ValidateHooks.InvalidHookName /** * Allow disabling the widget. * * @since 1.5.1 * * @param bool $load Should the widget be loaded? */ if ( ! apply_filters( 'wpforms_admin_dashboardwidget', true ) ) { return; } // phpcs:enable WPForms.PHP.ValidateHooks.InvalidHookName add_action( 'wpforms_process_complete', [ static::class, 'clear_widget_cache' ] ); add_action( 'admin_init', [ $this, 'admin_init' ] ); } /** * Admin init class. * * @since 1.8.3 */ public function admin_init() { // phpcs:ignore WPForms.PHP.HooksMethod.InvalidPlaceForAddingHooks // This widget should be displayed for certain high-level users only. if ( ! wpforms_current_user_can( 'view_forms' ) ) { return; } add_action( 'wpforms_create_form', [ static::class, 'clear_widget_cache' ] ); add_action( 'wpforms_save_form', [ static::class, 'clear_widget_cache' ] ); add_action( 'wpforms_delete_form', [ static::class, 'clear_widget_cache' ] ); /** * Clear cache after Lite plugin deactivation. * * Also triggered when the user upgrades the plugin to the Pro version. * After activation of the Pro version, the cache will be cleared. */ add_action( 'deactivate_wpforms-lite/wpforms.php', [ static::class, 'clear_widget_cache' ] ); if ( ! $this->is_dashboard_page() && ! $this->is_dashboard_widget_ajax_request() ) { return; } $this->settings(); $this->hooks(); } /** * Filterable widget settings. * * @since 1.5.0 */ public function settings() { // phpcs:disable WPForms.Comments.PHPDocHooks.RequiredHookDocumentation, WPForms.PHP.ValidateHooks.InvalidHookName $this->settings = [ // Number of forms to display in the forms' list before the "Show More" button appears. 'forms_list_number_to_display' => apply_filters( 'wpforms_dash_widget_forms_list_number_to_display', 5 ), // Allow results caching to reduce a DB load. 'allow_data_caching' => apply_filters( 'wpforms_dash_widget_allow_data_caching', true ), // Transient lifetime in seconds. Defaults to the end of a current day. 'transient_lifetime' => apply_filters( 'wpforms_dash_widget_transient_lifetime', strtotime( 'tomorrow' ) - time() ), // Determine if the forms with no entries should appear in a forms' list. // Once switched, the effect applies after cache expiration. 'display_forms_list_empty_entries' => apply_filters( 'wpforms_dash_widget_display_forms_list_empty_entries', true ), ]; // phpcs:enable WPForms.Comments.PHPDocHooks.RequiredHookDocumentation, WPForms.PHP.ValidateHooks.InvalidHookName } /** * Widget hooks. * * @since 1.5.0 */ public function hooks() { $widget_slug = static::SLUG; add_action( 'admin_enqueue_scripts', [ $this, 'widget_scripts' ] ); add_action( 'wp_dashboard_setup', [ $this, 'widget_register' ] ); add_action( 'admin_init', [ $this, 'hide_widget' ] ); add_action( "wp_ajax_wpforms_{$widget_slug}_save_widget_meta", [ $this, 'save_widget_meta_ajax' ] ); } /** * Load widget-specific scripts. * * @since 1.5.0 * * @param string $hook_suffix The current admin page. */ public function widget_scripts( $hook_suffix ) { if ( $hook_suffix !== 'index.php' ) { return; } $min = wpforms_get_min_suffix(); wp_enqueue_style( 'wpforms-dashboard-widget', WPFORMS_PLUGIN_URL . "assets/css/dashboard-widget{$min}.css", [], WPFORMS_VERSION ); wp_enqueue_script( 'wpforms-chart', WPFORMS_PLUGIN_URL . 'assets/lib/chart.min.js', [ 'moment' ], '4.5.1', true ); wp_enqueue_script( 'wpforms-dashboard-widget', WPFORMS_PLUGIN_URL . "assets/lite/js/admin/dashboard-widget{$min}.js", [ 'jquery', 'wpforms-chart' ], WPFORMS_VERSION, true ); wp_localize_script( 'wpforms-dashboard-widget', 'wpforms_dashboard_widget', [ 'nonce' => wp_create_nonce( 'wpforms_' . static::SLUG . '_nonce' ), 'slug' => static::SLUG, 'show_more_html' => esc_html__( 'Show More', 'wpforms-lite' ) . '<span class="dashicons dashicons-arrow-down"></span>', 'show_less_html' => esc_html__( 'Show Less', 'wpforms-lite' ) . '<span class="dashicons dashicons-arrow-up"></span>', 'i18n' => [ 'entries' => esc_html__( 'Entries', 'wpforms-lite' ), ], // Adapter for Chart.js to use Moment.js for date formatting. 'adapter_path' => WPFORMS_PLUGIN_URL . 'assets/lib/chartjs-adapter-moment.min.js?ver=1.0.1', ] ); } /** * Register the widget. * * @since 1.5.0 */ public function widget_register() { global $wp_meta_boxes; $widget_key = 'wpforms_reports_widget_lite'; wp_add_dashboard_widget( $widget_key, esc_html__( 'WPForms', 'wpforms-lite' ), [ $this, 'widget_content' ] ); // Attempt to place the widget at the top. $normal_dashboard = $wp_meta_boxes['dashboard']['normal']['core']; $widget_instance = [ $widget_key => $normal_dashboard[ $widget_key ] ]; unset( $normal_dashboard[ $widget_key ] ); $sorted_dashboard = array_merge( $widget_instance, $normal_dashboard ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited $wp_meta_boxes['dashboard']['normal']['core'] = $sorted_dashboard; } /** * Load widget content. * * @since 1.5.0 */ public function widget_content() { $forms = wpforms()->obj( 'form' )->get( '', [ 'fields' => 'ids' ] ); $hide_graph = (bool) $this->widget_meta( 'get', 'hide_graph' ); $no_graph_class = $hide_graph ? 'wpforms-dash-widget-no-graph' : ''; echo '<div class="wpforms-dash-widget wpforms-lite ' . esc_attr( $no_graph_class ) . '">'; if ( empty( $forms ) ) { $this->widget_content_no_forms_html(); } else { $this->widget_content_html( $hide_graph ); } Links::render( [ 'docs' => [ 'medium' => 'dashboard-widget', 'content' => 'docs', ], ] ); $plugin = $this->get_recommended_plugin(); $hide_recommended = $this->widget_meta( 'get', 'hide_recommended_block' ); if ( ! empty( $plugin ) && ! empty( $forms ) && ! $hide_recommended ) { $this->recommended_plugin_block_html( $plugin ); } echo '</div><!-- .wpforms-dash-widget -->'; } /** * Widget content HTML if a user has no forms. * * @since 1.5.0 */ public function widget_content_no_forms_html() { $create_form_url = add_query_arg( 'page', 'wpforms-builder', admin_url( 'admin.php' ) ); $learn_more_url = 'https://wpforms.com/docs/creating-first-form/?utm_source=WordPress&utm_medium=link&utm_campaign=liteplugin&utm_content=dashboardwidget'; ?> <div class="wpforms-dash-widget-block wpforms-dash-widget-block-no-forms"> <img class="wpforms-dash-widget-block-sullie-logo" src="<?php echo esc_url( WPFORMS_PLUGIN_URL . 'assets/images/sullie.png' ); ?>" alt="<?php esc_attr_e( 'Sullie the WPForms mascot', 'wpforms-lite' ); ?>"> <h2><?php esc_html_e( 'Create Your First Form to Start Collecting Leads', 'wpforms-lite' ); ?></h2> <p><?php esc_html_e( 'You can use WPForms to build contact forms, surveys, payment forms, and more with just a few clicks.', 'wpforms-lite' ); ?></p> <?php if ( wpforms_current_user_can( 'create_forms' ) ) : ?> <a href="<?php echo esc_url( $create_form_url ); ?>" class="button button-primary"> <?php esc_html_e( 'Create Your Form', 'wpforms-lite' ); ?> </a> <?php endif; ?> <a href="<?php echo esc_url( $learn_more_url ); ?>" class="button" target="_blank" rel="noopener noreferrer"> <?php esc_html_e( 'Learn More', 'wpforms-lite' ); ?> </a> </div> <?php } /** * Widget content HTML. * * @since 1.5.0 * @since 1.7.4 Added hide graph parameter. * * @param bool $hide_graph Whether the graph is hidden. */ public function widget_content_html( $hide_graph = false ) { // phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped /** * Filters the content before the Dashboard Widget Chart block container (for Lite). * * @since 1.7.4 * * @param string $chart_block_before Chart block before markup. */ echo apply_filters( 'wpforms_lite_admin_dashboard_widget_content_html_chart_block_before', '' ); // phpcs:enable WordPress.Security.EscapeOutput.OutputNotEscaped if ( ! $hide_graph ) : ?> <div class="wpforms-dash-widget-chart-block-container"> <div class="wpforms-dash-widget-block wpforms-dash-widget-chart-block"> <canvas id="wpforms-dash-widget-chart" width="400" height="300"></canvas> </div> <div class="wpforms-dash-widget-block-upgrade"> <div class="wpforms-dash-widget-modal"> <a href="#" class="wpforms-dash-widget-dismiss-chart-upgrade"> <span class="dashicons dashicons-no-alt"></span> </a> <h2><?php esc_html_e( 'View all Form Entries inside the WordPress Dashboard', 'wpforms-lite' ); ?></h2> <p><?php esc_html_e( 'Form entries reports are not available.', 'wpforms-lite' ); ?> <?php esc_html_e( 'Form entries are not stored in Lite.', 'wpforms-lite' ); ?> <?php esc_html_e( 'Upgrade to Pro and get access to the reports.', 'wpforms-lite' ); ?></p> <p> <a href="<?php echo esc_url( wpforms_admin_upgrade_link( 'dashboard-widget', 'upgrade-to-pro' ) ); ?>" class="wpforms-dash-widget-upgrade-btn" target="_blank" rel="noopener noreferrer"> <?php esc_html_e( 'Upgrade to WPForms Pro', 'wpforms-lite' ); ?> </a> </p> </div> </div> </div> <?php endif; ?> <div class="wpforms-dash-widget-block wpforms-dash-widget-block-title"> <h3><?php esc_html_e( 'Total Entries by Form', 'wpforms-lite' ); ?></h3> <div class="wpforms-dash-widget-settings"> <?php $this->timespan_select_html( 0, false ); $this->widget_settings_html( false ); ?> </div> </div> <div id="wpforms-dash-widget-forms-list-block" class="wpforms-dash-widget-block wpforms-dash-widget-forms-list-block"> <?php $this->forms_list_block(); ?> </div> <?php } /** * Forms list block. * * @since 1.5.0 */ public function forms_list_block() { $forms = $this->get_entries_count_by_form(); if ( empty( $forms ) ) { $this->forms_list_block_empty_html(); } else { $this->forms_list_block_html( $forms ); } } /** * Empty forms list block HTML. * * @since 1.5.0 */ public function forms_list_block_empty_html() { ?> <p class="wpforms-error wpforms-error-no-data-forms-list"> <?php esc_html_e( 'No entries were submitted yet.', 'wpforms-lite' ); ?> </p> <?php } /** * Forms list block HTML. * * @since 1.5.0 * * @param array $forms Forms to display in the list. */ public function forms_list_block_html( $forms ) { // Number of forms to display in the forms' list before the "Show More" button appears. $show_forms = $this->settings['forms_list_number_to_display']; ?> <table id="wpforms-dash-widget-forms-list-table" cellspacing="0"> <?php foreach ( array_values( $forms ) as $key => $form ) : ?> <tr <?php echo $key >= $show_forms ? 'class="wpforms-dash-widget-forms-list-hidden-el"' : ''; ?> data-form-id="<?php echo absint( $form['form_id'] ); ?>"> <td><span class="wpforms-dash-widget-form-title"><?php echo esc_html( $form['title'] ); ?></span></td> <td><?php echo absint( $form['count'] ); ?></td> </tr> <?php endforeach; ?> </table> <?php if ( count( $forms ) > $show_forms ) : ?> <button type="button" id="wpforms-dash-widget-forms-more" class="wpforms-dash-widget-forms-more" title="<?php esc_html_e( 'Show all forms', 'wpforms-lite' ); ?>"> <?php esc_html_e( 'Show More', 'wpforms-lite' ); ?> <span class="dashicons dashicons-arrow-down"></span> </button> <?php endif; ?> <?php } /** * Recommended plugin block HTML. * * @since 1.5.0 * @since 1.7.3 Added plugin parameter. * * @param array $plugin Plugin data. */ public function recommended_plugin_block_html( $plugin = [] ) { if ( ! $plugin ) { return; } $install_url = wp_nonce_url( self_admin_url( 'update.php?action=install-plugin&plugin=' . rawurlencode( $plugin['slug'] ) ), 'install-plugin_' . $plugin['slug'] ); ?> <div class="wpforms-dash-widget-block wpforms-dash-widget-recommended-plugin-block"> <span class="wpforms-dash-widget-recommended-plugin"> <span class="recommended"><?php esc_html_e( 'Recommended Plugin:', 'wpforms-lite' ); ?></span> <strong><?php echo esc_html( $plugin['name'] ); ?></strong> <span class="sep">-</span> <span class="action-links"> <?php if ( wpforms_can_install( 'plugin' ) ) { ?> <a href="<?php echo esc_url( $install_url ); ?>"><?php esc_html_e( 'Install', 'wpforms-lite' ); ?></a> <span class="sep sep-vertical">|</span> <?php } ?> <a href="<?php echo esc_url( $plugin['more'] ); ?>?utm_source=wpformsplugin&utm_medium=link&utm_campaign=wpformsdashboardwidget"><?php esc_html_e( 'Learn More', 'wpforms-lite' ); ?></a> </span> </span> <button type="button" class="wpforms-dash-widget-dismiss-icon" title="<?php esc_html_e( 'Dismiss', 'wpforms-lite' ); ?>" data-field="hide_recommended_block"> <span class="dashicons dashicons-no-alt"></span> </button> </div> <?php } /** * The welcome block HTML. * * @since 1.8.7 * @deprecated 1.9.7 */ public function welcome_block_html() { return ''; } /** * Get entries count grouped by form. * Main point of entry to fetch form entry count data from DB. * Cache the result. * * @since 1.5.0 * * @return array */ public function get_entries_count_by_form(): array { // Allow results caching to reduce a DB load. $allow_caching = $this->settings['allow_data_caching']; $transient_name = 'wpforms_dash_widget_lite_entries_by_form'; if ( $allow_caching ) { $cache = get_transient( $transient_name ); /** * Filters the cache to clear or alter its data. * * @since 1.5.0 * * @param mixed $cache The cache content. */ $cache = apply_filters( 'wpforms_dash_widget_lite_cached_data', $cache ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName } // is_array() detects cached empty searches. if ( $allow_caching && is_array( $cache ) ) { return $cache; } $forms = wpforms()->obj( 'form' )->get( '', [ 'fields' => 'ids' ] ); if ( empty( $forms ) || ! is_array( $forms ) ) { return []; } $result = []; foreach ( $forms as $form_id ) { $count = absint( get_post_meta( $form_id, 'wpforms_entries_count', true ) ); if ( empty( $count ) && empty( $this->settings['display_forms_list_empty_entries'] ) ) { continue; } $result[ $form_id ] = [ 'form_id' => $form_id, 'count' => $count, 'title' => get_the_title( $form_id ), ]; } if ( ! empty( $result ) ) { // Sort forms by entries count (desc). uasort( $result, static function ( $a, $b ) { return ( $a['count'] > $b['count'] ) ? -1 : 1; } ); } if ( $allow_caching ) { // Transient lifetime in seconds. Defaults to the end of a current day. $transient_lifetime = $this->settings['transient_lifetime']; set_transient( $transient_name, $result, $transient_lifetime ); } return $result; } /** * Hide dashboard widget. * Use dashboard screen options to make it visible again. * * @since 1.5.0 */ public function hide_widget() { if ( ! is_admin() || ! is_user_logged_in() ) { return; } if ( ! isset( $_GET['wpforms-nonce'] ) || ! wp_verify_nonce( sanitize_key( wp_unslash( $_GET['wpforms-nonce'] ) ), 'wpforms_hide_dash_widget' ) ) { return; } if ( ! isset( $_GET['wpforms-widget'] ) || $_GET['wpforms-widget'] !== 'hide' ) { return; } $user_id = get_current_user_id(); $metaboxhidden = get_user_meta( $user_id, 'metaboxhidden_dashboard', true ); if ( ! is_array( $metaboxhidden ) ) { update_user_meta( $user_id, 'metaboxhidden_dashboard', [ 'wpforms_reports_widget_lite' ] ); } if ( is_array( $metaboxhidden ) && ! in_array( 'wpforms_reports_widget_lite', $metaboxhidden, true ) ) { $metaboxhidden[] = 'wpforms_reports_widget_lite'; update_user_meta( $user_id, 'metaboxhidden_dashboard', $metaboxhidden ); } $redirect_url = remove_query_arg( [ 'wpforms-widget', 'wpforms-nonce' ] ); wp_safe_redirect( $redirect_url ); exit(); } /** * Clear dashboard widget cached data. * * @since 1.5.2 */ public static function clear_widget_cache() { delete_transient( 'wpforms_dash_widget_lite_entries_by_form' ); } } Lite/Integrations/Elementor/ThemesData.php 0000644 00000000727 15174710275 0014610 0 ustar 00 <?php namespace WPForms\Lite\Integrations\Elementor; use WPForms\Integrations\Elementor\ThemesData as ThemesDataBase; /** * Themes data for Gutenberg block for Lite. * * @since 1.9.6 */ class ThemesData extends ThemesDataBase { /** * WPForms themes JSON file path. * * Relative to the WPForms plugin directory. * * @since 1.9.6 * * @var string */ protected const THEMES_WPFORMS_JSON_PATH = 'assets/lite/js/integrations/elementor/themes.json'; } Lite/Integrations/Abilities/Abilities.php 0000644 00000005676 15174710275 0014461 0 ustar 00 <?php namespace WPForms\Lite\Integrations\Abilities; use WP_Error; use WPForms\Integrations\Abilities\Abilities as AbilitiesBase; /** * WordPress Abilities API Integration for WPForms Lite. * * @since 1.9.9 */ class Abilities extends AbilitiesBase { /** * Register WPForms abilities for Lite version. * * @since 1.9.9 */ public function register_abilities(): void { // Register common abilities (list_forms, get_form). $this->register_common_abilities(); // Lite-specific: Register form stats ability (basic). $this->register_form_stats_ability(); } /** * Register the form_stats ability (Lite version - basic stats with upsell). * * @since 1.9.9 */ protected function register_form_stats_ability(): void { wp_register_ability( self::ABILITY_NAMESPACE . '/get-form-stats', [ 'label' => __( 'Get Form Stats', 'wpforms-lite' ), 'description' => __( 'Get basic statistics for a WPForms form. Upgrade to Pro for detailed entry data.', 'wpforms-lite' ), 'category' => self::CATEGORY_SLUG, 'execute_callback' => [ $this, 'ability_get_form_stats' ], 'permission_callback' => [ $this, 'check_view_single_form_permission' ], 'input_schema' => [ 'type' => 'object', 'properties' => [ 'form_id' => [ 'description' => __( 'The ID of the form to get stats for.', 'wpforms-lite' ), 'type' => 'integer', 'required' => true, 'minimum' => 1, ], ], 'required' => [ 'form_id' ], ], 'output_schema' => [ 'type' => 'object', 'properties' => [ 'form_id' => [ 'type' => 'integer' ], 'entries_available' => [ 'type' => 'boolean' ], 'message' => [ 'type' => 'string' ], ], ], 'meta' => [ 'annotations' => [ 'readonly' => true, 'destructive' => false, 'idempotent' => true, ], 'show_in_rest' => true, 'mcp' => [ 'public' => true, ], ], ] ); } /** * Ability callback: Get form stats (Lite version). * * @since 1.9.9 * * @param mixed $input Input data. * * @return array|WP_Error */ public function ability_get_form_stats( $input = null ) { $args = $this->normalize_input( $input ); $form_id = absint( $args['form_id'] ?? 0 ); $form_handler = $this->get_form_handler(); if ( is_wp_error( $form_handler ) ) { return $form_handler; } $form = $form_handler->get( $form_id ); if ( empty( $form ) ) { return new WP_Error( 'wpforms_form_not_found', __( 'Form not found.', 'wpforms-lite' ), [ 'status' => 404 ] ); } // Lite version returns limited stats with the upsell message. return [ 'form_id' => $form_id, 'entries_available' => false, 'message' => __( 'Entry statistics require WPForms Pro. Upgrade to access detailed form submission data.', 'wpforms-lite' ), ]; } } Lite/Integrations/Gutenberg/ThemesData.php 0000644 00000000711 15174710275 0014571 0 ustar 00 <?php namespace WPForms\Lite\Integrations\Gutenberg; use WPForms\Integrations\Gutenberg\ThemesData as ThemesDataBase; /** * Themes data for Gutenberg block for Lite. * * @since 1.8.8 */ class ThemesData extends ThemesDataBase { /** * WPForms themes JSON file path. * * Relative to WPForms plugin directory. * * @since 1.8.8 * * @var string */ const THEMES_WPFORMS_JSON_PATH = 'assets/lite/js/integrations/gutenberg/themes.json'; } Lite/Integrations/Gutenberg/FormSelector.php 0000644 00000004175 15174710275 0015166 0 ustar 00 <?php namespace WPForms\Lite\Integrations\Gutenberg; use WPForms\Integrations\Gutenberg\FormSelector as FormSelectorBase; use WPForms\Integrations\Gutenberg\RestApi; /** * Gutenberg block for Lite. * * @since 1.8.8 */ class FormSelector extends FormSelectorBase { /** * Load an integration. * * @since 1.8.8 */ public function load() { $this->themes_data_obj = new ThemesData(); parent::load(); } /** * Integration hooks. * * @since 1.8.8 */ protected function hooks() { add_action( 'rest_api_init', [ $this, 'init_rest' ] ); parent::hooks(); } /** * Initialize rest API. * * @since 1.8.8 */ public function init_rest() { if ( ! $this->rest_api_obj ) { $this->rest_api_obj = new RestApi( $this, $this->themes_data_obj ); } } /** * Register WPForms Gutenberg block styles. * * @since 1.8.8 */ protected function register_styles() { if ( ! is_admin() ) { return; } parent::register_styles(); // FontAwesome. wp_enqueue_style( 'wpforms-font-awesome', WPFORMS_PLUGIN_URL . 'assets/lib/font-awesome/css/all.min.css', null, '7.0.1' ); // FontAwesome v4 compatibility shims. wp_enqueue_style( 'wpforms-font-awesome-v4-shim', WPFORMS_PLUGIN_URL . 'assets/lib/font-awesome/css/v4-shims.min.css', null, '4.7.0' ); } /** * Load WPForms Gutenberg block scripts. * * @since 1.8.8 */ public function enqueue_block_editor_assets() { parent::enqueue_block_editor_assets(); $min = wpforms_get_min_suffix(); wp_enqueue_script( 'wpforms-generic-utils', WPFORMS_PLUGIN_URL . "assets/js/share/utils{$min}.js", [ 'jquery' ], WPFORMS_VERSION, true ); if ( ! $this->is_legacy_block() ) { wp_enqueue_script( 'wpforms-gutenberg-form-selector', WPFORMS_PLUGIN_URL . "assets/lite/js/integrations/gutenberg/formselector.es5{$min}.js", [ 'wp-blocks', 'wp-i18n', 'wp-element', 'jquery', 'wpforms-admin-education-core', 'wpforms-generic-utils' ], WPFORMS_VERSION, true ); } wp_localize_script( 'wpforms-gutenberg-form-selector', 'wpforms_gutenberg_form_selector', $this->get_localize_data() ); } } Lite/Integrations/LiteConnect/SendEntryTask.php 0000644 00000005643 15174710275 0015606 0 ustar 00 <?php namespace WPForms\Lite\Integrations\LiteConnect; use WPForms\Integrations\LiteConnect\API; use WPForms\Tasks\Meta; /** * Class SendEntryTask. * * @since 1.7.4 */ class SendEntryTask extends Integration { /** * Task name. * * @since 1.7.4 * * @var string */ const LITE_CONNECT_TASK = 'wpforms_lite_connect_send_entry'; /** * SendEntryTask constructor. * * @since 1.7.4 */ public function __construct() { parent::__construct(); $this->hooks(); } /** * Initialize the hooks. * * @since 1.7.4 */ private function hooks() { // Process the tasks as needed. add_action( self::LITE_CONNECT_TASK, [ $this, 'process' ] ); } /** * Creates a task to submit the lite entry to the Lite Connect API via * Action Scheduler. * * @since 1.7.4 * * @param int $form_id The form ID. * @param string $entry_data The entry data. */ public function create( $form_id, $entry_data ) { $action_id = wpforms()->obj( 'tasks' ) ->create( self::LITE_CONNECT_TASK ) ->params( $form_id, $entry_data ) ->once( time() + wp_rand( 10, 60 ) * MINUTE_IN_SECONDS ) ->register(); if ( $action_id === null ) { wpforms_log( 'Lite Connect: error creating the AS task', [ 'task' => self::LITE_CONNECT_TASK, ], [ 'type' => [ 'error' ] ] ); } } /** * Process the task to submit the entry to the Lite Connect API via * Action Scheduler. * * @since 1.7.4 * * @param int $meta_id The meta ID. */ public function process( $meta_id ) { // Load task data. $params = ( new Meta() )->get( (int) $meta_id ); list( $form_id, $entry_data ) = $params->data; // Grab current access token. If site key or access token is not available, then it recreates the task to run later. $access_token = $this->get_access_token( $this->get_site_key() ); if ( ! $access_token ) { $this->create( $form_id, $entry_data ); return; } // Submit entry to the Lite Connect API. $response = ( new API() )->add_form_entry( $access_token, $form_id, $entry_data ); if ( $response ) { $response = json_decode( $response, true ); } if ( isset( $response['error'] ) && $response['error'] === 'Access token is invalid or expired.' ) { // Force to re-generate access token in case it is invalid. $this->get_access_token( $this->get_site_key(), true ); } if ( ! empty( $response['error'] ) ) { wpforms_log( 'Lite Connect: error submitting form entry (AS task)', [ 'response' => $response, 'entry_data' => $entry_data, ], [ 'type' => [ 'error' ], 'form_id' => $form_id, ] ); } // Recreate the task if the request to the API fail for any reasons. if ( ! isset( $response['status'] ) || $response['status'] !== 'success' ) { $this->create( $form_id, $entry_data ); return; } // Increase the entries count if the entry has been added successfully. $this->increase_entries_count( $form_id ); } } Lite/Integrations/LiteConnect/LiteConnect.php 0000644 00000011731 15174710275 0015252 0 ustar 00 <?php namespace WPForms\Lite\Integrations\LiteConnect; /** * Class LiteConnect for WPForms Lite. * * @since 1.7.4 */ class LiteConnect extends \WPForms\Integrations\LiteConnect\LiteConnect { /** * The Integration object. * * @since 1.7.4 * * @var Integration */ private $integration; /** * Send Entry Task object. * * @since 1.7.4 * * @var SendEntryTask */ private $send_entry_task; /** * Whether Lite Connect is enabled. * * @since 1.7.4 * * @return bool */ public static function is_enabled() { // phpcs:disable WPForms.PHP.ValidateHooks.InvalidHookName /** * Determine whether LiteConnect is enabled on the WPForms > Settings admin page. * * @since 1.7.4 * * @param bool $is_enabled Is LiteConnect enabled on WPForms > Settings page? */ return (bool) apply_filters( 'wpforms_lite_integrations_lite_connect_is_enabled', wpforms_setting( self::SETTINGS_SLUG ) ); // phpcs:enable WPForms.PHP.ValidateHooks.InvalidHookName } /** * Load the integration. * * @since 1.7.4 */ public function load() { parent::load(); // Do not load if user doesn't have permissions to update settings. if ( ! wpforms_current_user_can( wpforms_get_capability_manage_options() ) ) { return; } // Hooks. $this->hooks(); // Process any pending submissions to the API, even if the Lite Connect integration is disabled. $this->send_entry_task = new SendEntryTask(); // It won't load if the Lite Connect integration is not enabled. if ( ! self::is_enabled() ) { return; } // We always need to instance the Integration class as part of the load process for the Lite Connect integration. $this->integration = new Integration(); } /** * Hooks. * * @since 1.7.4 */ private function hooks() { // Add Lite Connect option to settings. add_filter( 'wpforms_settings_defaults', [ $this, 'settings_option' ] ); // Automatically save the timestamp when Lite Connect was enabled first time. add_filter( 'wpforms_update_settings', [ $this, 'update_enabled_settings' ] ); } /** * Add "Lite Connect: Enable Entry Backups" to the WPForms Lite settings. * * @since 1.7.4 * * @param array $settings WPForms settings. * * @return array */ public function settings_option( $settings ) { $setting = [ self::SETTINGS_SLUG => [ 'id' => self::SETTINGS_SLUG, 'name' => esc_html__( 'Lite Connect', 'wpforms-lite' ), 'label' => esc_html__( 'Enable Entry Backups', 'wpforms-lite' ), 'type' => 'toggle', 'is-important' => true, 'control-class' => 'wpforms-setting-lite-connect-auto-save-toggle', 'input-attr' => 'disabled', 'desc-on' => sprintf( wp_kses( /* translators: %s - upgrade to WPForms Pro landing page URL. */ __( '<strong>Your form entries are not being stored locally, but are backed up remotely.</strong> If you <a href="%s" target="_blank" rel="noopener noreferrer" class="wpforms-upgrade-modal">upgrade to WPForms PRO</a>, you can restore your entries and they’ll be available in the WordPress dashboard.', 'wpforms-lite' ), [ 'a' => [ 'href' => [], 'class' => [], 'target' => [], 'rel' => [], ], 'strong' => [], ] ), esc_url( wpforms_admin_upgrade_link( 'settings-lite-connect-enabled' ) ) ), 'desc-off' => sprintf( wp_kses( /* translators: %s - upgrade to WPForms Pro landing page URL. */ __( '<strong>Your form entries are not being stored in WordPress, and your entry backups are not active.</strong> If there\'s a problem with deliverability, you\'ll lose form entries. We recommend that you enable Entry Backups, especially if you\'re considering <a href="%s" target="_blank" rel="noopener noreferrer" class="wpforms-upgrade-modal">upgrading to WPForms PRO</a>.', 'wpforms-lite' ), [ 'a' => [ 'href' => [], 'class' => [], 'target' => [], 'rel' => [], ], 'strong' => [], ] ), esc_url( wpforms_admin_upgrade_link( 'settings-lite-connect-disabled', 'Upgrade to WPForms Pro text Link' ) ) ), ], ]; $settings['general'] = wpforms_list_insert_after( $settings['general'], 'license-key', $setting ); return $settings; } /** * Automatically save the additional info when Lite Connect was enabled first time. * * @since 1.7.4 * * @param array $settings WPForms settings. * * @return array */ public function update_enabled_settings( $settings ) { if ( empty( $settings[ self::SETTINGS_SLUG ] ) ) { return $settings; } $since = self::SETTINGS_SLUG . '-since'; $email = self::SETTINGS_SLUG . '-email'; if ( empty( $settings[ $since ] ) ) { $settings[ $since ] = time(); } if ( empty( $settings[ $email ] ) ) { $user = wp_get_current_user(); $settings[ $email ] = $user && ! empty( $user->user_email ) ? $user->user_email : get_option( 'admin_email' ); } return $settings; } } Lite/Integrations/LiteConnect/Integration.php 0000644 00000006247 15174710275 0015334 0 ustar 00 <?php namespace WPForms\Lite\Integrations\LiteConnect; use WPForms\Helpers\Crypto; /** * Integration between Lite Connect API and WPForms Lite. * * @since 1.7.4 */ class Integration extends \WPForms\Integrations\LiteConnect\Integration { /** * Encrypt the form entry and submit it to the Lite Connect API. * * If the regular wp_remote_post() request fail for any reasons, then an * Action Scheduler task will be created to retry a couple of minutes later. * * @since 1.7.4 * * @param array $entry_args The entry data. * @param array $form_data The form data. * * @return false|string */ public function submit( $entry_args, $form_data ) { if ( ! is_array( $entry_args ) ) { return false; } $entry_args['form_data'] = $form_data; // Encrypt entry using the WPForms Crypto class. $entry_data = Crypto::encrypt( wp_json_encode( $entry_args ) ); // We have to start requesting site keys in ajax, turning on the LC functionality. // First, the request to the API server will be sent. // Second, the server will respond to our callback URL /wpforms/auth/key/nonce, and the site key will be stored in the DB. // Third, we have to get access via a separate HTTP request. $this->update_keys(); // Third request here. // Submit entry to the Lite Connect API. $response = $this->add_form_entry( $this->auth['access_token'], $entry_args['form_id'], $entry_data ); // Confirm if entry has been added successfully to the Lite Connect API. if ( $response ) { $response = json_decode( $response, true ); } if ( isset( $response['error'] ) && $response['error'] === 'Access token is invalid or expired.' ) { // Force to re-generate access token in case it is invalid. $this->get_access_token( $this->get_site_key(), true ); } if ( ! isset( $response['status'] ) || $response['status'] !== 'success' ) { /** * If Lite Connect API is not available in the add_form_entry() * request above, then a task is created to run it later via Action * Scheduler. */ ( new SendEntryTask() )->create( $entry_args['form_id'], $entry_data ); } // Increase the entries count if the entry has been added successfully. if ( isset( $response['status'] ) && $response['status'] === 'success' ) { $this->increase_entries_count( $entry_args['form_id'] ); } if ( ! empty( $response['error'] ) ) { wpforms_log( 'Lite Connect: error submitting form entry', [ 'response' => $response, 'entry_args' => $entry_args, ], [ 'type' => [ 'error' ], 'form_id' => $entry_args['form_id'], ] ); } return $response; } /** * Increases the Lite Connect entries count. * * @since 1.7.4 * * @param int|false $form_id The form ID. */ public function increase_entries_count( $form_id = false ) { self::maybe_set_entries_count(); update_option( self::LITE_CONNECT_ENTRIES_COUNT_OPTION, self::get_entries_count() + 1 ); // Increase the form entries count. // It allows counting entries on per form level. if ( ! empty( $form_id ) ) { $count = self::get_form_entries_count( (int) $form_id ); update_post_meta( $form_id, self::LITE_CONNECT_FORM_ENTRIES_COUNT_META, ++$count ); } } } Emails/Tasks/FetchInfoBlocksTask.php 0000644 00000004156 15174710275 0013421 0 ustar 00 <?php namespace WPForms\Emails\Tasks; use WPForms\Emails\InfoBlocks; use WPForms\Tasks\Task; /** * Action Scheduler task to fetch and cache Email Summaries Info Blocks. * * @since 1.6.4 */ class FetchInfoBlocksTask extends Task { /** * Action name for this task. * * @since 1.6.4 */ const ACTION = 'wpforms_email_summaries_fetch_info_blocks'; /** * Option name to store the timestamp of the last run. * * @since 1.6.4 */ const LAST_RUN = 'wpforms_email_summaries_fetch_info_blocks_last_run'; /** * Class constructor. * * @since 1.6.4 */ public function __construct() { parent::__construct( self::ACTION ); $this->init(); } /** * Initialize the task with all the proper checks. * * @since 1.6.4 */ public function init() { $this->hooks(); $tasks = wpforms()->obj( 'tasks' ); // Add new if none exists. if ( $tasks->is_scheduled( self::ACTION ) !== false ) { return; } $this->recurring( $this->generate_start_date(), WEEK_IN_SECONDS )->register(); } /** * Add hooks. * * @since 1.7.3 */ private function hooks() { // Register the action handler. add_action( self::ACTION, [ $this, 'process' ] ); } /** * Randomly pick a timestamp which is not more than 1 week in the future * starting before Email Summaries dispatch happens. * * @since 1.6.4 * * @return int */ private function generate_start_date() { $tracking = []; $tracking['days'] = wp_rand( 0, 6 ) * DAY_IN_SECONDS; $tracking['hours'] = wp_rand( 0, 23 ) * HOUR_IN_SECONDS; $tracking['minutes'] = wp_rand( 0, 59 ) * MINUTE_IN_SECONDS; $tracking['seconds'] = wp_rand( 0, 59 ); return strtotime( 'previous monday 1pm' ) + array_sum( $tracking ); } /** * Process the task. * * @since 1.6.4 */ public function process() { $last_run = get_option( self::LAST_RUN ); // Make sure we do not run it more than once a day. if ( $last_run !== false && ( time() - $last_run ) < DAY_IN_SECONDS ) { return; } ( new InfoBlocks() )->cache_all(); // Update the last run option to the current timestamp. update_option( self::LAST_RUN, time() ); } } Emails/Templates/Classic.php 0000644 00000000450 15174710275 0012016 0 ustar 00 <?php namespace WPForms\Emails\Templates; /** * Class Classic. * This is an updated version of our standard email template. * * @since 1.8.5 */ class Classic extends Notifications { /** * Template slug. * * @since 1.8.5 * * @var string */ const TEMPLATE_SLUG = 'classic'; } Emails/Templates/General.php 0000644 00000021450 15174710275 0012015 0 ustar 00 <?php namespace WPForms\Emails\Templates; use WPForms\Emails\Helpers; use WPForms\Emails\Styler; use WPForms\Helpers\Templates; /** * Base email template class. * * @since 1.5.4 */ class General { /** * Template slug. * * @since 1.5.4 * * @var string */ const TEMPLATE_SLUG = 'general'; /** * Email message. * * @since 1.5.4 * * @var string */ protected $message; /** * Content is plain text type. * * @since 1.5.4 * * @var bool */ protected $plain_text; /** * Dynamic {{tags}}. * * @since 1.5.4 * * @var array */ protected $tags; /** * Header/footer/body arguments. * * @since 1.5.4 * * @var array */ protected $args; /** * Final email content. * * @since 1.5.4 * * @var string */ protected $content; /** * Constructor. * * @since 1.5.4 * * @param string $message Email message. */ public function __construct( $message = '' ) { $this->set_message( $message ); $this->plain_text = Helpers::is_plain_text_template(); $this->set_initial_args(); } /** * Set initial arguments to use in a template. * * @since 1.5.4 */ public function set_initial_args() { $header_args = [ 'title' => \esc_html__( 'WPForms', 'wpforms-lite' ), ]; if ( ! $this->plain_text ) { $header_args['header_image'] = $this->get_header_image(); } $args = [ 'header' => $header_args, 'body' => [ 'message' => $this->get_message() ], 'footer' => [], 'style' => [], ]; $args = \apply_filters( 'wpforms_emails_templates_general_set_initial_args', $args, $this ); $this->set_args( $args ); } /** * Get the template slug. * * @since 1.5.4 * * @return string */ public function get_slug() { return static::TEMPLATE_SLUG; } /** * Get the template parent slug. * * @since 1.5.4 * * @return string */ public function get_parent_slug() { return self::TEMPLATE_SLUG; } /** * Get the message. * * @since 1.5.4 * * @return string */ public function get_message() { return \apply_filters( 'wpforms_emails_templates_general_get_message', $this->message, $this ); } /** * Get the dynamic tags. * * @since 1.5.4 * * @return array */ public function get_tags() { return \apply_filters( 'wpforms_emails_templates_general_get_tags', $this->tags, $this ); } /** * Get header/footer/body arguments * * @since 1.5.4 * * @param string $type Header/footer/body. * * @return array */ public function get_args( $type ) { if ( ! empty( $type ) ) { return isset( $this->args[ $type ] ) ? apply_filters( 'wpforms_emails_templates_general_get_args_' . $type, $this->args[ $type ], $this ) : []; } return apply_filters( 'wpforms_emails_templates_general_get_args', $this->args, $this ); } /** * Set email message. * * @since 1.5.4 * * @param string $message Email message. * * @return General */ public function set_message( $message ) { $message = \apply_filters( 'wpforms_emails_templates_general_set_message', $message, $this ); if ( ! \is_string( $message ) ) { return $this; } $this->message = $message; return $this; } /** * Set the dynamic tags. * * @since 1.5.4 * * @param array $tags Tags to set. * * @return General */ public function set_tags( $tags ) { $tags = \apply_filters( 'wpforms_emails_templates_general_set_tags', $tags, $this ); if ( ! \is_array( $tags ) ) { return $this; } $this->tags = $tags; return $this; } /** * Set header/footer/body/style arguments to use in a template. * * @since 1.5.4 * * @param array $args Arguments to set. * @param bool $merge Merge the arguments with existing once or replace. * * @return General */ public function set_args( $args, $merge = true ) { $args = \apply_filters( 'wpforms_emails_templates_general_set_args', $args, $this ); if ( empty( $args ) || ! \is_array( $args ) ) { return $this; } foreach ( $args as $type => $value ) { if ( ! \is_array( $value ) ) { continue; } if ( ! isset( $this->args[ $type ] ) || ! \is_array( $this->args[ $type ] ) ) { $this->args[ $type ] = []; } $this->args[ $type ] = $merge ? \array_merge( $this->args[ $type ], $value ) : $value; } return $this; } /** * Process and replace any dynamic tags. * * @since 1.5.4 * * @param string $content Content to make replacements in. * * @return string */ public function process_tags( $content ) { $tags = $this->get_tags(); if ( empty( $tags ) ) { return $content; } foreach ( $tags as $tag => $value ) { $content = \str_replace( $tag, $value, $content ); } return $content; } /** * Conditionally modify email template name. * * @since 1.5.4 * * @param string $name Base template name. * * @return string */ protected function get_full_template_name( $name ) { $name = \sanitize_file_name( $name ); if ( $this->plain_text ) { $name .= '-plain'; } $template = 'emails/' . $this->get_slug() . '-' . $name; if ( ! Templates::locate( $template . '.php' ) ) { $template = 'emails/' . $this->get_parent_slug() . '-' . $name; } return \apply_filters( 'wpforms_emails_templates_general_get_full_template_name', $template, $this ); } /** * Get header image URL from settings. * * @since 1.5.4 * * @return array */ protected function get_header_image() { /** * Additional 'width' key with an integer value can be added to $img array to control image's width in pixels. * This setting helps to scale an image in some versions of MS Outlook and old email clients. * Percentage 'width' values have no effect in MS Outlook and will be sanitized as integer by an email template.. * * Example: * * $img = [ * 'url' => \wpforms_setting( 'email-header-image' ), * 'width' => 150, * ]; * * * To set percentage values for the modern email clients, use $this->set_args() method: * * $this->set_args( * [ * 'style' => [ * 'header_image_max_width' => '45%', * ], * ] *); * * Both pixel and percentage approaches work well with 'wpforms_emails_templates_general_get_header_image' filter or this class extension. */ $img = [ 'url' => wpforms_setting( 'email-header-image' ), 'size' => wpforms_setting( 'email-header-image-size', 'medium' ), ]; return \apply_filters( 'wpforms_emails_templates_general_get_header_image', $img, $this ); } /** * Get content part HTML. * * @since 1.5.4 * * @param string $name Name of the content part. * * @return string */ protected function get_content_part( $name ) { if ( ! \is_string( $name ) ) { return ''; } $html = Templates::get_html( $this->get_full_template_name( $name ), $this->get_args( $name ), true ); return \apply_filters( 'wpforms_emails_templates_general_get_content_part', $html, $name, $this ); } /** * Assemble all content parts in an array. * * @since 1.5.4 * * @return array */ protected function get_content_parts() { $parts = [ 'header' => $this->get_content_part( 'header' ), 'body' => $this->get_content_part( 'body' ), 'footer' => $this->get_content_part( 'footer' ), ]; return \apply_filters( 'wpforms_emails_templates_general_get_content_parts', $parts, $this ); } /** * Apply inline styling and save email content. * * @since 1.5.4 * * @param string $content Content with no styling applied. */ protected function save_styled( $content ) { if ( empty( $content ) ) { $this->content = ''; return; } if ( $this->plain_text ) { $this->content = \wp_strip_all_tags( $content ); return; } $style_templates = [ 'style' => $this->get_full_template_name( 'style' ), 'queries' => $this->get_full_template_name( 'queries' ), ]; $styler = new Styler( $content, $style_templates, $this->get_args( 'style' ) ); $this->content = \apply_filters( 'wpforms_emails_templates_general_save_styled_content', $styler->get(), $this ); } /** * Build an email including styling. * * @since 1.5.4 * * @param bool $force Rebuild the content if it was already built and saved. */ protected function build( $force = false ) { if ( $this->content && ! $force ) { return; } $content = \implode( $this->get_content_parts() ); if ( empty( $content ) ) { return; } $content = $this->process_tags( $content ); if ( ! $this->plain_text ) { $content = \make_clickable( $content ); } $content = \apply_filters( 'wpforms_emails_templates_general_build_content', $content, $this ); $this->save_styled( $content ); } /** * Return final email. * * @since 1.5.4 * * @param bool $force Rebuild the content if it was already built and saved. * * @return string */ public function get( $force = false ) { $this->build( $force ); return $this->content; } } Emails/Templates/Notifications.php 0000644 00000005041 15174710275 0013247 0 ustar 00 <?php namespace WPForms\Emails\Templates; use WPForms\Emails\Helpers; /** * Class Notifications. * * This is a wrapper for the General template to extend it. * This is the default template for all notifications. * * @since 1.8.5 */ class Notifications extends General { /** * Whether is preview or not. * * @since 1.8.5 * * @var bool */ protected $is_preview = false; /** * Initialize class. * In case the class instance meant for preview, we need to set the plain text property to false. * * @since 1.8.5 * @since 1.8.5.2 New param was added, $current_template * * @param string $message Optional. Message. * @param bool $is_preview Optional. Whether is preview or not. Default false. * @param string $current_template Optional. The name of the email template to evaluate. */ public function __construct( $message = '', $is_preview = false, $current_template = '' ) { parent::__construct( $message ); $this->is_preview = $is_preview; $this->plain_text = ! $is_preview && Helpers::is_plain_text_template( $current_template ); // Call the parent method after to set the correct header properties. $this->set_initial_args(); } /** * Set template message. * * @since 1.8.5 * * @param string $message Message. */ public function set_field( $message ) { // Leave if not a string. if ( ! is_string( $message ) ) { return; } // Set the template message. $this->set_args( [ 'body' => [ 'message' => $message, ], ] ); } /** * Get field template. * * @since 1.8.5 * * @return string */ public function get_field_template() { return $this->get_content_part( 'field' ); } /** * Get header image URL from settings. * This method has been overridden to add support for filtering the returned image. * * @since 1.8.6 * * @return array */ protected function get_header_image() { // Retrieve header image URL and size from WPForms settings. $img = [ 'url_light' => wpforms_setting( 'email-header-image' ), 'size_light' => wpforms_setting( 'email-header-image-size', 'medium' ), 'url_dark' => wpforms_setting( 'email-header-image-dark' ), 'size_dark' => wpforms_setting( 'email-header-image-size-dark', 'medium' ), ]; /** * Filter the email header image. * * @since 1.8.6 * * @param array $img Email header image. * @param Notifications $this Current instance of the class. */ return (array) apply_filters( 'wpforms_emails_templates_notifications_get_header_image', $img, $this ); } } Emails/Templates/Plain.php 0000644 00000003015 15174710275 0011500 0 ustar 00 <?php namespace WPForms\Emails\Templates; /** * Class Plain. * This template is used for the plain text email notifications. * * @since 1.8.5 */ class Plain extends Notifications { /** * Template slug. * * @since 1.8.5 * * @var string */ const TEMPLATE_SLUG = 'plain'; /** * Initialize class. * * @since 1.8.5 * * @param mixed ...$args Variable number of parameters to be passed to the parent class. */ public function __construct( ...$args ) { // Ensure preparation for initialization by calling the parent class constructor with all passed arguments. parent::__construct( ...$args ); // We already know that this is a plain text template. No need for further evaluation. $this->plain_text = true; // Call the parent method after to set the correct header properties. $this->set_initial_args(); } /** * Maybe prepare the content for the preview. * * @since 1.8.5 * * @param string $content Content with no styling applied. */ protected function save_styled( $content ) { // Leave early if we are not in preview mode. if ( ! $this->is_preview ) { // Call the parent method to handle the proper styling. parent::save_styled( $content ); return; } // Leave if content is empty. if ( empty( $content ) ) { $this->content = ''; return; } // Stop here as we don't need to apply any styling for the preview. // The only exception here is to keep the break tags to maintain the readability. $this->content = wp_kses( $content, [ 'br' => [] ] ); } } Emails/Templates/Compact.php 0000644 00000000463 15174710275 0012027 0 ustar 00 <?php namespace WPForms\Emails\Templates; /** * Class Compact. * As the name suggests, it's a compact variant of the Classic template. * * @since 1.8.5 */ class Compact extends Notifications { /** * Template slug. * * @since 1.8.5 * * @var string */ const TEMPLATE_SLUG = 'compact'; } Emails/Templates/Summary.php 0000644 00000005305 15174710275 0012076 0 ustar 00 <?php namespace WPForms\Emails\Templates; /** * Email Summaries email template class. * * @since 1.5.4 */ class Summary extends General { /** * Template slug. * * @since 1.5.4 * * @var string */ const TEMPLATE_SLUG = 'summary'; /** * Initialize class. * * @since 1.8.8 * * @param string $message Optional. Message. */ public function __construct( $message = '' ) { parent::__construct( $message ); // Maybe revert (override) the default background color value. $this->set_args( $this->maybe_revert_background_color() ); } /** * Get header image URL from settings. * * @since 1.5.4 * * @return array */ protected function get_header_image() { $legacy_header_image = $this->maybe_revert_header_image(); // Bail early, if legacy behavior is enabled. if ( ! empty( $legacy_header_image ) ) { return $legacy_header_image; } // Set specific WPForms logo width in pixels for MS Outlook and old email clients. return [ 'url_light' => WPFORMS_PLUGIN_URL . 'assets/images/logo.png', 'url_dark' => WPFORMS_PLUGIN_URL . 'assets/images/logo-negative.png', ]; } /** * Checks if legacy header image overrides should be applied. * * @since 1.8.8 * * @return array */ private function maybe_revert_header_image() { /** * This filter is designed to restore the legacy behavior, reverting the WPForms logo and template background color * to values defined in the WPForms → Settings → Email tab. * * @since 1.8.8 * * @param bool $revert_legacy_style_overrides Whether to apply legacy style overrides. */ if ( ! (bool) apply_filters( 'wpforms_emails_templates_summary_revert_legacy_style_overrides', false ) ) { return []; } $header_image = wpforms_setting( 'email-header-image' ); // Bail early, if no custom header image if set. if ( empty( $header_image ) ) { return []; } return [ 'url_light' => esc_url( $header_image ) ]; } /** * Checks if legacy background color overrides should be applied. * * @since 1.8.8 * * @return array */ private function maybe_revert_background_color() { /** * This filter is designed to restore the legacy behavior, reverting the WPForms logo and template background color * to values defined in the WPForms → Settings → Email tab. * * @since 1.8.8 * * @param bool $revert_legacy_style_overrides Whether to apply legacy style overrides. */ if ( ! (bool) apply_filters( 'wpforms_emails_templates_summary_revert_legacy_style_overrides', false ) ) { return [ 'style' => [ 'email_background_color' => '' ] ]; } return [ 'style' => [ 'email_background_color' => wpforms_setting( 'email-background-color', '#e9eaec' ), ], ]; } } Emails/Helpers.php 0000644 00000031145 15174710275 0010106 0 ustar 00 <?php namespace WPForms\Emails; /** * Helper class for the email templates. * * @since 1.8.5 */ class Helpers { /** * Get Email template choices. * * @since 1.8.5 * * @param bool $include_legacy Whether to include a Legacy template into the list. * * @return array */ public static function get_email_template_choices( $include_legacy = true ) { $choices = []; $templates = Notifications::get_all_templates(); // If there are no templates, return empty choices. if ( empty( $templates ) || ! is_array( $templates ) ) { return $choices; } // Add legacy template to the choices as the first option. if ( $include_legacy && self::is_legacy_html_template() ) { $choices['default'] = [ 'name' => esc_html__( 'Legacy', 'wpforms-lite' ), ]; } // Iterate through templates and build $choices array. foreach ( $templates as $template_key => $template ) { // Skip if the template name is empty. if ( empty( $template['name'] ) ) { continue; } $choices[ $template_key ] = $template; } return $choices; } /** * Retrieves the current email template name. * If the current template is not found, the default template will be returned. * * This method respects backward compatibility and will return the old "Legacy" template if it is set. * If a template name is provided, the function will attempt to validate and return it. If validation fails, * it will default to the email template name "Classic." * * @since 1.8.5 * * @param string $template_name Optional. The name of the email template to evaluate. * * @return string */ public static function get_current_template_name( $template_name = '' ) { // If a template name is provided, sanitize it. Otherwise, use the default template name from settings. $settings_template = wpforms_setting( 'email-template', Notifications::DEFAULT_TEMPLATE ); $template = ! empty( $template_name ) ? trim( sanitize_text_field( $template_name ) ) : $settings_template; // If the user has set the legacy template, return it. if ( $template === Notifications::LEGACY_TEMPLATE && self::is_legacy_html_template() ) { return Notifications::LEGACY_TEMPLATE; } // In case the user has changed the general settings template, // but the form submitted still uses the “Legacy” template, // we need to revert to the general settings template. if ( $template === Notifications::LEGACY_TEMPLATE && ! self::is_legacy_html_template() ) { $template = wpforms_setting( 'email-template', Notifications::DEFAULT_TEMPLATE ); } // Check if the given template name is valid by looking into available templates. $current_template = Notifications::get_available_templates( $template ); // If the current template is not found or its corresponding class does not exist, return the default template. if ( ! isset( $current_template['path'] ) || ! class_exists( $current_template['path'] ) ) { // Last resort, check if the template defined in the settings can be used. // This would be helpful when user downgrades from Pro to Lite version and the template is not available anymore. if ( isset( $current_template[ $settings_template ] ) ) { return $settings_template; } return Notifications::DEFAULT_TEMPLATE; } // The provided template is valid, so return it. return $template; } /** * Get the current email template class path. * * @since 1.8.5 * * @param string $template_name Optional. The name of the email template to evaluate. * @param string $fallback_class Optional. The class to use if the template is not found. * This argument most likely will be used for backward compatibility and supporting the "Legacy" template. * * @return string */ public static function get_current_template_class( $template_name = '', $fallback_class = '' ) { $template_name = self::get_current_template_name( $template_name ); // If the user has set the legacy template, return the "General" template. if ( $template_name === Notifications::LEGACY_TEMPLATE ) { return ! empty( $fallback_class ) && class_exists( $fallback_class ) ? $fallback_class : __NAMESPACE__ . '\Templates\General'; } // Check if the given template name is valid by looking into available templates. $current_template = Notifications::get_available_templates( $template_name ); // If the current template is not found or its corresponding class does not exist, return the "Classic" template. if ( ! isset( $current_template['path'] ) || ! class_exists( $current_template['path'] ) ) { return Notifications::get_available_templates( Notifications::DEFAULT_TEMPLATE )['path']; } // The provided template is valid, so return it. return $current_template['path']; } /** * Get the style overrides for the current email template. * * This function retrieves the style overrides for the email template, including background color, * body color, text color, link color, and typography. It provides default values and handles * different settings for both the free and Pro versions of the plugin. * * @since 1.8.5 * * @return array */ public static function get_current_template_style_overrides() { // Get the header image size for the current template. list( $header_image_size, $header_image_size_dark ) = self::get_template_header_image_size(); // Get the typography for the current template. list( $email_typography, $email_typography_dark ) = self::get_template_typography(); // Default style overrides. $defaults = [ 'email_background_color' => '#e9eaec', 'email_body_color' => '#ffffff', 'email_text_color' => '#333333', 'email_links_color' => '#e27730', 'email_background_color_dark' => '#2d2f31', 'email_body_color_dark' => '#1f1f1f', 'email_text_color_dark' => '#dddddd', 'email_links_color_dark' => '#e27730', 'email_typography' => $email_typography, 'email_typography_dark' => $email_typography_dark, 'header_image_max_width' => $header_image_size['width'], 'header_image_max_height' => $header_image_size['height'], 'header_image_max_width_dark' => $header_image_size_dark['width'], 'header_image_max_height_dark' => $header_image_size_dark['height'], ]; // Retrieve old background colors setting from the Lite version. $lite_background_color = wpforms_setting( 'email-background-color', $defaults['email_background_color'] ); $lite_background_color_dark = wpforms_setting( 'email-background-color-dark', $defaults['email_background_color_dark'] ); // Leave early if the user has the Lite version. if ( ! wpforms()->is_pro() ) { // Override the background colors with the old setting. $defaults['email_background_color'] = $lite_background_color; $defaults['email_background_color_dark'] = $lite_background_color_dark; /** * Filter the style overrides for the current email template. * * @since 1.8.6 * * @param array $overrides The current email template style overrides. */ return (array) apply_filters( 'wpforms_emails_helpers_style_overrides_args', $defaults ); } // Get the color scheme from the settings. $color_scheme = wpforms_setting( 'email-color-scheme', [] ); // If the user has the Pro version, but the light mode background color is the old setting, override it. if ( empty( $color_scheme['email_background_color'] ) && ! empty( $lite_background_color ) ) { $color_scheme['email_background_color'] = $lite_background_color; } // Get the dark mode color scheme from the settings. $color_scheme_dark = wpforms_setting( 'email-color-scheme-dark', [] ); // If the user has the Pro version, but the dark mode background color is the old setting, override it. if ( empty( $color_scheme_dark['email_background_color_dark'] ) && ! empty( $lite_background_color_dark ) ) { $color_scheme_dark['email_background_color_dark'] = $lite_background_color_dark; } // Merge the color schemes with the defaults. $overrides = wp_parse_args( $color_scheme + $color_scheme_dark, $defaults ); /** * Filter the style overrides for the current email template. * * @since 1.8.6 * * @param array $overrides The current email template style overrides. */ return (array) apply_filters( 'wpforms_emails_helpers_style_overrides_args', $overrides ); } /** * Check if the current email template is plain text. * * @since 1.8.5 * * @param string $template_name Optional. The name of the email template to compare. * * @return bool */ public static function is_plain_text_template( $template_name = '' ) { // Leave early in case the given template name is not empty, and we can resolve it early. if ( ! empty( $template_name ) ) { return $template_name === Notifications::PLAIN_TEMPLATE; } return wpforms_setting( 'email-template', Notifications::DEFAULT_TEMPLATE ) === Notifications::PLAIN_TEMPLATE; } /** * Check if the current template is legacy. * Legacy template is the one that its value is 'default'. * * @since 1.8.5 * * @return bool */ public static function is_legacy_html_template() { return wpforms_setting( 'email-template', Notifications::DEFAULT_TEMPLATE ) === Notifications::LEGACY_TEMPLATE; } /** * Get the current template's typography. * * This function retrieves the typography setting for email templates and returns the corresponding font family. * * If the user has the Pro version, the font-family is determined based on the current template. * For free users, the font-family defaults to "Sans Serif" because the available templates * ("Classic" and "Compact") use this font-family in their design. * * @since 1.8.5 * @since 1.8.6 Added $typography argument. * * @param string $typography Optional. The typography setting to evaluate. * * @return array|string */ public static function get_template_typography( $typography = '' ) { // Predefined font families for light and dark modes. $font_families = [ 'sans_serif' => '-apple-system, BlinkMacSystemFont, avenir next, avenir, segoe ui, helvetica neue, helvetica, Cantarell, Ubuntu, roboto, noto, arial, sans-serif', 'serif' => 'Iowan Old Style, Apple Garamond, Baskerville, Times New Roman, Droid Serif, Times, Source Serif Pro, serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol', ]; // If the user is not using the Pro version, return "Sans Serif" font-family. if ( ! wpforms()->is_pro() ) { return [ $font_families['sans_serif'], $font_families['sans_serif'] ]; } // Leave early if a specific typography is requested. if ( ! empty( $typography ) ) { // Validate the input and return the corresponding font family. return $font_families[ $typography ] ?? $font_families['sans_serif']; } // Get typography settings from email settings. $setting_typography = [ // Light mode. wpforms_setting( 'email-typography', 'sans-serif' ), // Dark mode. wpforms_setting( 'email-typography-dark', 'sans-serif' ), ]; // Map setting values to predefined font families, default to 'sans_serif' if not found. return array_map( static function ( $item ) use ( $font_families ) { return $font_families[ $item ] ?? $font_families['sans_serif']; }, $setting_typography ); } /** * Get the header image size based on the specified size or 'medium' by default. * * Note that when given a size input, this function will only validate the input and return the corresponding size. * Otherwise, it will return the header image size for the current template in both light and dark modes. * * @since 1.8.5 * @since 1.8.6 Added $size argument. * * @param string $size Optional. The desired image size ('small', 'medium', or 'large'). * * @return array */ public static function get_template_header_image_size( $size = '' ) { // Predefined image sizes. $sizes = [ 'small' => [ 'width' => '240', 'height' => '120', ], 'medium' => [ 'width' => '350', 'height' => '180', ], 'large' => [ 'width' => '500', 'height' => '240', ], ]; // Leave early if a specific size is requested. if ( ! empty( $size ) ) { // Validate the input and return the corresponding size. return $sizes[ $size ] ?? $sizes['medium']; } // Get header image sizes from settings. $setting_size = [ // Light mode. wpforms_setting( 'email-header-image-size', 'medium' ), // Dark mode. wpforms_setting( 'email-header-image-size-dark', 'medium' ), ]; // Map setting values to predefined sizes, default to 'medium' if not found. return array_map( static function ( $item ) use ( $sizes ) { return $sizes[ $item ] ?? $sizes['medium']; }, $setting_size ); } } Emails/Mailer.php 0000644 00000031250 15174710275 0007712 0 ustar 00 <?php namespace WPForms\Emails; use WPForms\Emails\Templates\General; /** * Mailer class to wrap wp_mail(). * * @since 1.5.4 */ class Mailer { /** * Array or comma-separated list of email addresses to send a message. * * @since 1.5.4 * * @var string|string[] */ private $to_email; /** * CC addresses (comma delimited). * * @since 1.5.4 * * @var string */ private $cc; /** * From address. * * @since 1.5.4 * * @var string */ private $from_address = ''; /** * From name. * * @since 1.5.4 * * @var string */ private $from_name; /** * Reply to address. * * @since 1.5.4 * * @var string */ private $reply_to; /** * Email headers. * * @since 1.5.4 * * @var string */ private $headers; /** * Email content type. * * @since 1.5.4 * * @var string */ private $content_type; /** * Email attachments. * * @since 1.5.4 * * @var string|string[] */ private $attachments; /** * Email subject. * * @since 1.5.4 * * @var string */ private $subject; /** * Email message. * * @since 1.5.4 * * @var string */ private $message; /** * Email template. * * @since 1.5.4 * * @var General */ private $template; /** * Set a property. * * @since 1.5.4 * * @param string $key Property name. * @param string|array $value Property value. */ public function __set( string $key, $value ) { $this->$key = $value; } /** * Get a property. * * @since 1.5.4 * * @param string $key Property name. * * @return string */ public function __get( $key ) { return $this->$key; } /** * Check if a property exists. * * @since 1.5.4 * * @param string $key Property name. * * @return bool */ public function __isset( $key ) { return isset( $this->key ); } /** * Unset a property. * * @since 1.5.4 * * @param string $key Property name. */ public function __unset( $key ) { unset( $this->key ); } /** * Email kill switch if needed. * * @since 1.5.4 * * @return bool */ public function is_email_disabled() { // phpcs:ignore WPForms.Comments.PHPDocHooks.RequiredHookDocumentation return (bool) apply_filters( 'wpforms_emails_mailer_is_email_disabled', false, $this ); } /** * Sanitize the string. * * @since 1.5.4 * @since 1.6.0 Deprecated param: $linebreaks. This is handled by wpforms_decode_string(). * * @param string $input String that may contain tags. * @param string $context Context of the string. * * @return string * @uses wpforms_decode_string() */ public function sanitize( $input = '', $context = '' ): string { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed return wpforms_decode_string( $input ); } /** * Get the email from the name. * * @since 1.5.4 * * @return string */ public function get_from_name() { $this->from_name = $this->from_name ? $this->sanitize( $this->from_name ) : get_bloginfo( 'name' ); // phpcs:ignore WPForms.Comments.PHPDocHooks.RequiredHookDocumentation return apply_filters( 'wpforms_emails_mailer_get_from_name', $this->from_name, $this ); } /** * Get the email from the address. * * @since 1.5.4 * * @return string */ public function get_from_address() { $from_address = $this->sanitize( $this->from_address, 'notification-from' ); $from_address = $from_address ? $from_address : get_option( 'admin_email' ); // phpcs:ignore WPForms.Comments.PHPDocHooks.RequiredHookDocumentation return apply_filters( 'wpforms_emails_mailer_get_from_address', $from_address, $this ); } /** * Get the email reply to the address. * * @since 1.5.4 * * @return string */ public function get_reply_to_address() { if ( empty( $this->reply_to ) || ! is_email( $this->reply_to ) ) { $this->reply_to = $this->from_address; } $this->reply_to = $this->sanitize( $this->reply_to, 'notification-reply-to' ); if ( empty( $this->reply_to ) || ! is_email( $this->reply_to ) ) { $this->reply_to = get_option( 'admin_email' ); } // phpcs:ignore WPForms.Comments.PHPDocHooks.RequiredHookDocumentation return apply_filters( 'wpforms_emails_mailer_get_reply_to_address', $this->reply_to, $this ); } /** * Get the email carbon copy addresses. * * @since 1.5.4 * @since 1.8.9 Allow using CC field as an array. * * @return string The email carbon copy addresses. */ public function get_cc_address() { if ( is_array( $this->cc ) ) { $this->cc = implode( ',', $this->cc ); } if ( empty( $this->cc ) ) { /** * Filters the email carbon copy addresses. * * @since 1.5.4 * * @param string $cc Carbon copy addresses. * @param Mailer $this Mailer instance. */ return apply_filters( 'wpforms_emails_mailer_get_cc_address', $this->cc, $this ); } $this->cc = $this->sanitize( $this->cc ); $addresses = array_filter( array_map( 'sanitize_email', explode( ',', $this->cc ) ) ); $this->cc = implode( ',', $addresses ); /** This filter is documented in src/Emails/Mailer.php. */ return apply_filters( 'wpforms_emails_mailer_get_cc_address', $this->cc, $this ); } /** * Get the email content type. * * @since 1.5.4 * * @return string The email content type. */ public function get_content_type() { $is_html = ! Helpers::is_plain_text_template(); if ( ! $this->content_type && $is_html ) { // phpcs:ignore WPForms.Comments.PHPDocHooks.RequiredHookDocumentation $this->content_type = apply_filters( 'wpforms_emails_mailer_get_content_type_default', 'text/html', $this ); } elseif ( ! $is_html ) { $this->content_type = 'text/plain'; } // phpcs:ignore WPForms.Comments.PHPDocHooks.RequiredHookDocumentation return apply_filters( 'wpforms_emails_mailer_get_content_type', $this->content_type, $this ); } /** * Get the email subject. * * @since 1.8.9 * * @return string The email subject. */ private function get_subject() { if ( empty( $this->subject ) ) { $this->subject = __( 'New Email Submit', 'wpforms-lite' ); } /** * Filters the email subject. * * @since 1.8.9 * * @param string $subject Email subject. * @param Mailer $this Mailer instance. */ return apply_filters( 'wpforms_emails_mailer_get_subject', $this->subject, $this ); } /** * Get the email message. * * @since 1.5.4 * * @return string The email message. */ public function get_message() { if ( empty( $this->message ) && ! empty( $this->template ) ) { $this->message = $this->template->get(); } /** * Filters the email message. * * @since 1.5.4 * * @param string $message Email message. * @param Mailer $this Mailer instance. */ return apply_filters( 'wpforms_emails_mailer_get_message', $this->message, $this ); } /** * Get the email headers. * * @since 1.5.4 * * @return string The email headers. */ public function get_headers() { $this->headers = "From: {$this->get_from_name()} <{$this->get_from_address()}>\r\n"; if ( $this->get_reply_to_address() ) { $this->headers .= "Reply-To: {$this->get_reply_to_address()}\r\n"; } $cc = $this->get_cc_address(); if ( $cc ) { $this->headers .= "Cc: {$cc}\r\n"; } $this->headers .= "Content-Type: {$this->get_content_type()}; charset=utf-8\r\n"; /** * Filters the email headers. * * @since 1.5.4 * * @param string $headers Email headers. * @param Mailer $this Mailer instance. */ return apply_filters( 'wpforms_emails_mailer_get_headers', $this->headers, $this ); } /** * Get the email attachments. * * @since 1.5.4 * * @return string|string[] */ public function get_attachments() { if ( $this->attachments === null ) { $this->attachments = []; } /** * Filters the email attachments. * * @since 1.5.4 * * @param string|string[] $attachments Array or string with attachment paths. * @param Mailer $this Mailer instance. */ return apply_filters( 'wpforms_emails_mailer_get_attachments', $this->attachments, $this ); } /** * Set an email address to send to. * * @since 1.5.4 * * @param string|string[] $email Array or comma-separated list of email addresses to send a message. * * @return Mailer */ public function to_email( $email ) { if ( is_string( $email ) ) { $email = explode( ',', $email ); } // phpcs:ignore WPForms.Comments.PHPDocHooks.RequiredHookDocumentation $this->to_email = apply_filters( 'wpforms_emails_mailer_to_email', $email, $this ); return $this; } /** * Set an email subject. * * @since 1.5.4 * * @param string $subject Email subject. * * @return Mailer */ public function subject( $subject ) { $subject = $this->sanitize( $subject ); // phpcs:ignore WPForms.Comments.PHPDocHooks.RequiredHookDocumentation $this->subject = apply_filters( 'wpforms_emails_mailer_subject', $subject, $this ); return $this; } /** * Set an email message (body). * * @since 1.5.4 * * @param string $message Email message. * * @return Mailer */ public function message( $message ) { // phpcs:ignore WPForms.Comments.PHPDocHooks.RequiredHookDocumentation $this->message = apply_filters( 'wpforms_emails_mailer_message', $message, $this ); return $this; } /** * Set email template. * * @since 1.5.4 * * @param General $template Email template. * * @return Mailer */ public function template( General $template ) { // phpcs:ignore WPForms.Comments.PHPDocHooks.RequiredHookDocumentation $this->template = apply_filters( 'wpforms_emails_mailer_template', $template, $this ); return $this; } /** * Get email errors. * * @since 1.5.4 * * @return array */ protected function get_errors() { $errors = []; foreach ( (array) $this->to_email as $email ) { if ( ! is_email( $email ) ) { $errors[] = sprintf( /* translators: %1$s - namespaced class name, %2$s - invalid email. */ esc_html__( '%1$s Invalid email address %2$s.', 'wpforms-lite' ), '[WPForms\Emails\Mailer]', $email ); } } if ( empty( $this->get_subject() ) ) { $errors[] = sprintf( /* translators: %s - namespaced class name. */ esc_html__( '%s Empty subject line.', 'wpforms-lite' ), '[WPForms\Emails\Mailer]' ); } if ( empty( $this->get_message() ) ) { $errors[] = sprintf( /* translators: %s - namespaced class name. */ esc_html__( '%s Empty message.', 'wpforms-lite' ), '[WPForms\Emails\Mailer]' ); } return $errors; } /** * Log given email errors. * * @since 1.5.4 * * @param array $errors Errors to log. */ protected function log_errors( $errors ): void { if ( empty( $errors ) || ! is_array( $errors ) ) { return; } foreach ( $errors as $error ) { wpforms_log( $error, [ 'to_email' => $this->to_email, 'subject' => $this->subject, 'message' => wp_trim_words( $this->get_message() ), ], [ 'type' => 'error', ] ); } } /** * Send the email. * * @since 1.5.4 * * @return bool */ public function send() { if ( ! did_action( 'init' ) && ! did_action( 'admin_init' ) ) { _doing_it_wrong( __FUNCTION__, esc_html__( 'You cannot send emails with WPForms\Emails\Mailer until init/admin_init has been reached.', 'wpforms-lite' ), null ); return false; } // Don't send anything if emails have been disabled. if ( $this->is_email_disabled() ) { return false; } $errors = $this->get_errors(); if ( $errors ) { $this->log_errors( $errors ); return false; } $this->send_before(); $sent = wp_mail( $this->to_email, $this->get_subject(), $this->get_message(), $this->get_headers(), $this->get_attachments() ); $this->send_after(); return $sent; } /** * Add filters / actions before the email is sent. * * @since 1.5.4 */ public function send_before(): void { // phpcs:ignore WPForms.PHP.HooksMethod.InvalidPlaceForAddingHooks // phpcs:ignore WPForms.Comments.PHPDocHooks.RequiredHookDocumentation do_action( 'wpforms_emails_mailer_send_before', $this ); add_filter( 'wp_mail_from', [ $this, 'get_from_address' ] ); add_filter( 'wp_mail_from_name', [ $this, 'get_from_name' ] ); add_filter( 'wp_mail_content_type', [ $this, 'get_content_type' ] ); } /** * Remove filters / actions after the email is sent. * * @since 1.5.4 */ public function send_after(): void { // phpcs:ignore WPForms.PHP.HooksMethod.InvalidPlaceForAddingHooks // phpcs:ignore WPForms.Comments.PHPDocHooks.RequiredHookDocumentation do_action( 'wpforms_emails_mailer_send_after', $this ); remove_filter( 'wp_mail_from', [ $this, 'get_from_address' ] ); remove_filter( 'wp_mail_from_name', [ $this, 'get_from_name' ] ); remove_filter( 'wp_mail_content_type', [ $this, 'get_content_type' ] ); } } Emails/Notifications.php 0000644 00000117733 15174710275 0011325 0 ustar 00 <?php // phpcs:ignore Generic.Commenting.DocComment.MissingShort /** @noinspection PhpDeprecationInspection */ namespace WPForms\Emails; use DOMDocument; use WPForms\SmartTags\SmartTag\SmartTag; use WPForms_WP_Emails; use WPForms\Tasks\Actions\EntryEmailsTask; use WPForms\Emails\Templates\General; // phpcs:ignore WPForms.PHP.UseStatement.UnusedUseStatement use WPForms\Pro\Emails\Templates\Modern; use WPForms\Pro\Emails\Templates\Elegant; use WPForms\Pro\Emails\Templates\Tech; /** * Class Notifications. * Used to send email notifications. * * @since 1.8.5 */ class Notifications extends Mailer { /** * List of submitted fields. * * @since 1.8.5 * * @var array */ public $fields = []; /** * Form data. * * @since 1.8.5 * * @var array */ public $form_data = []; /** * Entry id. * * @since 1.8.5 * * @var int */ public $entry_id; /** * Notification ID that is currently being processed. * * @since 1.8.5 * * @var int */ public $notification_id = ''; /** * Current email template. * * @since 1.8.5 * * @var string */ private $current_template; /** * Field template. * * @since 1.8.5 * * @var string */ protected $field_template = ''; /** * Default email template name. * * @since 1.8.5 * * @var string */ public const DEFAULT_TEMPLATE = 'classic'; /** * Plain/Text email template name. * * @since 1.8.5 * * @var string */ public const PLAIN_TEMPLATE = 'none'; /** * Legacy email template name. * * @since 1.8.5 * * @var string */ public const LEGACY_TEMPLATE = 'default'; /** * Whether the email is being sent to a PDF. * * @since 1.9.7.3 * * @var string */ public $rendering_context; /** * Get the instance of a class. * * @since 1.8.9 */ public static function get_instance() { static $instance; if ( ! $instance ) { $instance = new self(); } return $instance; } /** * This method will initialize the class. * * Maybe use the old class for backward compatibility. * The old class might be removed in the future. * * @since 1.8.5 * * @param string $template Email template name. * @param string $rendering_context Where the email is being rendered, 'mail' or 'pdf'. * * @return $this|WPForms_WP_Emails * @noinspection PhpDeprecationInspection */ public function init( string $template = '', string $rendering_context = 'mail' ) { $this->rendering_context = $rendering_context; // Add hooks. $this->hooks(); // Assign the current template. $this->current_template = Helpers::get_current_template_name( $template ); // If the old class doesn't exist, return the current class. // The old class might be removed in the future. if ( ! class_exists( 'WPForms_WP_Emails' ) ) { return $this; } // In case the user is still using the old "Legacy" default template, use the old class. // Use the old class if the current template is "Legacy". if ( $this->current_template === self::LEGACY_TEMPLATE ) { return new WPForms_WP_Emails(); } // Plain text and other HTML templates will use the current class. return $this; } /** * Add hooks. * * @since 1.9.0 */ private function hooks(): void { add_filter( 'wpforms_smart_tags_formatted_field_value', [ $this, 'get_multi_field_formatted_value' ], 10, 4 ); add_filter( 'wpforms_smarttags_process_value', [ self::class, 'filter_smarttags_process_value' ], PHP_INT_MAX, 6 ); } /** * Maybe send an email right away or schedule it. * * @since 1.8.5 * * @return bool Whether the email was sent successfully. */ public function send() { // Leave the method if the arguments are empty. // We will be looking for 3 arguments: $to, $subject, $message. // The primary reason for this method not to take any direct arguments is to make it compatible with the parent class. if ( empty( func_get_args() ) || count( func_get_args() ) < 3 ) { return false; } // Don't send anything if emails have been disabled. if ( $this->is_email_disabled() ) { return false; } // Set the arguments. [ $to, $subject, $message ] = func_get_args(); // Don't send it if the email address is invalid. if ( ! is_email( $to ) ) { return false; } /** * Fires before the email is sent. * * The filter has been ported from "class-emails.php" to maintain backward compatibility * and avoid unintended breaking changes where these hooks may have been used. * * @since 1.8.5.2 * * @param Notifications $this An instance of the "Notifications" class. */ do_action( 'wpforms_email_send_before', $this ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName // Set the attachments to an empty array. // We will set the attachments later in the filter. $attachments = []; /** * Preliminary set the unfiltered recipient email address. * It will be used in get_headers() while resolving smart tags. */ $this->to_email( $to ); /** * Filter the email data before sending. * * The filter has been ported from "class-emails.php" to maintain backward compatibility * and avoid unintended breaking changes where these hooks may have been used. * * @since 1.8.5 * * @param array $data Email data. * @param Notifications $this An instance of the "Notifications" class. */ $data = (array) apply_filters( // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName 'wpforms_emails_send_email_data', [ 'to' => $to, 'subject' => $subject, 'message' => $message, 'headers' => $this->get_headers(), 'attachments' => $attachments, ], $this ); // Set the recipient email address. $this->to_email( $data['to'] ); // Set the email subject. $this->subject( $this->process_subject( $data['subject'] ) ); // Process the email template. $this->process_email_template( $data['message'] ); // Set the attachments to the email. $this->__set( 'attachments', $data['attachments'] ); $entry_obj = wpforms()->obj( 'entry' ); /** * Filter whether to send the email in the same process. * * The filter has been ported from "class-emails.php" to maintain backward compatibility * and avoid unintended breaking changes where these hooks may have been used. * * @since 1.8.5 * * @param bool $send_same_process Whether to send the email in the same process. * @param array $fields List of submitted fields. * @param array $entry Entry data. * @param array $form_data Form data. * @param int $entry_id Entry ID. * @param string $type Email type. */ $send_same_process = (bool) apply_filters( // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName, WPForms.Comments.ParamTagHooks.InvalidParamTagsQuantity 'wpforms_tasks_entry_emails_trigger_send_same_process', false, $this->fields, $entry_obj ? $entry_obj->get( $this->entry_id ) : [], $this->form_data, $this->entry_id, 'entry' ); // Send the email immediately. if ( $send_same_process || ! empty( $this->form_data['settings']['disable_entries'] ) ) { $results = parent::send(); } else { $results = (bool) ( new EntryEmailsTask() ) ->params( $this->__get( 'to_email' ), $this->__get( 'subject' ), $this->get_message(), $this->get_headers(), $this->get_attachments() ) ->register(); } /** * Fires after the email has been sent. * * The filter has been ported from "class-emails.php" to maintain backward compatibility * and avoid unintended breaking changes where these hooks may have been used. * * @since 1.8.5.2 * * @param Notifications $this An instance of the "Notifications" class. */ do_action( 'wpforms_email_send_after', $this ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName return $results; } /** * Process the email template. * * @since 1.8.5 * * @param string $message Email message. */ public function process_email_template( string $message ): void { $template = self::get_available_templates( $this->current_template ); // Return if the template is not set. // This can happen if the template is not found or if the template class doesn't exist. if ( ! isset( $template['path'] ) || ! class_exists( $template['path'] ) ) { return; } // Set the email template, i.e., WPForms\Emails\Templates\Classic. $this->template( new $template['path']( '', false, $this->current_template ) ); /** * Email template. * * @var General $email_template */ $email_template = $this->__get( 'template' ); if ( ! method_exists( $email_template, 'get_field_template' ) || ! method_exists( $email_template, 'set_field' ) ) { return; } // Set the field template. $this->field_template = $email_template->get_field_template(); // Set the email template fields. $email_template->set_field( $this->process_message( $message ) ); $content = $email_template->get(); // Return if the template is empty. if ( ! $content ) { return; } $this->message( $content ); } /** * Format and process the email subject. * * @since 1.8.5 * * @param string $subject Email subject. * * @return string */ private function process_subject( $subject ) { $subject = $this->process_tag( $subject ); $subject = trim( str_replace( [ "\r\n", "\r", "\n" ], ' ', $subject ) ); return wpforms_decode_string( $subject ); } /** * Process the email message. * * @since 1.8.5 * * @param string $message Email message. * * @return string */ private function process_message( $message ) { $message = $this->process_tag( $message ); if ( strpos( $message, '{all_fields}' ) !== false ) { $message = str_replace( '{all_fields}', $this->process_field_values(), $message ); } /** * Filter and modify the email message content before sending. * This filter allows customizing the email message content for notifications. * * @since 1.8.5 * * @param string $message The email message to be sent out. * @param string $template The email template name. * @param Notifications $this The instance of the "Notifications" class. */ $message = (string) apply_filters( 'wpforms_emails_notifications_message', $message, $this->current_template, $this ); $message = $this->fix_table_body_markup( $message ); // Leave early if the template is set to plain text. if ( Helpers::is_plain_text_template( $this->current_template ) ) { return $message; } /** * Filter and modify the processed email message content before sending. * This filter allows customizing the processed email message content for notifications. * * @since 1.9.9 * * @param string $processed_message The processed email message to be sent out. * @param string $message The email message before processing. * @param Notifications $this The instance of the "Notifications" class. * * @return string The processed email message to be sent out. */ return (string) apply_filters( 'wpforms_emails_notifications_processed_message', make_clickable( str_replace( "\r\n", '<br/>', $message ) ), // TODO: Replacing line breaks may not work as expected. Needs further investigation. $message, $this ); } /** * Process the field values. * * @since 1.8.5 * * @return string * @noinspection PhpUnusedLocalVariableInspection */ private function process_field_values() { // If fields are empty, return an empty message. if ( empty( $this->fields ) ) { return ''; } // If no message was generated, create an empty message. $default_message = esc_html__( 'An empty form was submitted.', 'wpforms-lite' ); /** * Filter whether to display empty fields in the email. * * @since 1.8.5 * @deprecated 1.8.5.2 * * @param bool $show_empty_fields Whether to display empty fields in the email. */ $show_empty_fields = apply_filters_deprecated( // phpcs:disable WPForms.Comments.ParamTagHooks.InvalidParamTagsQuantity 'wpforms_emails_notifications_display_empty_fields', [ false ], '1.8.5.2 of the WPForms plugin', 'wpforms_email_display_empty_fields' ); /** This filter is documented in /includes/emails/class-emails.php */ $show_empty_fields = apply_filters( // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName 'wpforms_email_display_empty_fields', false ); // Process either plain text or HTML message based on the template type. if ( Helpers::is_plain_text_template( $this->current_template ) ) { $message = $this->process_plain_message( $show_empty_fields ); } else { $message = $this->process_html_message( $show_empty_fields ); } /** * Filter the email message content before sending. * * @since 1.9.7.3 * * @param string $message The email message to be sent out. * @param string $template The email template name. * @param Mailer $this The instance of the "Notifications" class. */ return empty( $message ) ? $default_message : apply_filters( 'wpforms_emails_notifications_process_field_values_message', $message, $this->current_template, $this ); } /** * Get processed field values. * * @since 1.9.7.3 * * @return string */ public function get_processed_field_values(): string { $template = self::get_available_templates( $this->current_template ); // Return if the template is not set. // This can happen if the template is not found or if the template class doesn't exist. if ( ! isset( $template['path'] ) || ! class_exists( $template['path'] ) ) { return ''; } // Set the email template, i.e., WPForms\Emails\Templates\Classic. $this->template( new $template['path']( '', false, $this->current_template ) ); $email_template = $this->__get( 'template' ); if ( ! method_exists( $email_template, 'get_field_template' ) || ! method_exists( $email_template, 'set_field' ) ) { return ''; } $this->field_template = $email_template->get_field_template(); $field_values = trim( $this->process_field_values() ); return make_clickable( $field_values ); } /** * Process the plain text email message. * * @since 1.8.5 * * @param bool $show_empty_fields Whether to display empty fields in the email. * * @return string */ private function process_plain_message( bool $show_empty_fields = false ): string { /** * Filter the form data before it is used to generate the email message. * * @since 1.8.9 * * @param array $form_data Form data. * @param array $fields List of submitted fields. */ $this->form_data = apply_filters( 'wpforms_emails_notifications_form_data', $this->form_data, $this->fields ); $message = ''; foreach ( $this->form_data['fields'] as $field ) { /** * Filter whether to ignore the field in the email. * * @since 1.9.0 * * @param bool $ignore Whether to ignore the field in the email. * @param array $field Field data. * @param array $form_data Form data. */ if ( apply_filters( 'wpforms_emails_notifications_field_ignored', false, $field, $this->form_data ) ) { continue; } $field_message = $this->get_field_plain( $field, $show_empty_fields ); /** * Filter the field message before it is added to the email message. * * @since 1.8.9 * @since 1.8.9.3 The $notifications parameter was added. * * @param string $field_message Field message. * @param array $field Field data. * @param bool $show_empty_fields Whether to display empty fields in the email. * @param array $form_data Form data. * @param array $fields List of submitted fields. * @param Notifications $notifications Notifications instance. */ $message .= apply_filters( 'wpforms_emails_notifications_field_message_plain', $field_message, $field, $show_empty_fields, $this->form_data, $this->fields, $this ); } // Trim the message and return. return rtrim( $message, "\r\n" ); } /** * Get a single field plain text markup. * * @since 1.8.9 * * @param array $field Field data. * @param bool $show_empty_fields Whether to display empty fields in the email. * * @return string */ public function get_field_plain( array $field, bool $show_empty_fields ): string { // phpcs:ignore Generic.Metrics.CyclomaticComplexity $field_id = $field['id'] ?? ''; $field = $this->fields[ $field_id ] ?? $field; $message = ''; if ( ! $show_empty_fields && ( ! isset( $field['value'] ) || (string) $field['value'] === '' ) ) { return $message; } if ( $this->is_calculated_field_hidden( $field_id ) ) { return $message; } $field_name = $field['name'] ?? ''; $field_val = empty( $field['value'] ) && ! is_numeric( $field['value'] ) ? esc_html__( '(empty)', 'wpforms-lite' ) : $field['value']; // Add quantity for the field. if ( wpforms_payment_has_quantity( $field, $this->form_data ) ) { $field_val = wpforms_payment_format_quantity( $field ); } // Set a default field name if empty. if ( empty( $field_name ) && $field_name !== null ) { $field_name = $this->get_default_field_name( $field['id'] ); } $message .= '--- ' . $field_name . " ---\r\n\r\n"; $field_value = wpforms_decode_string( $field_val ) . "\r\n\r\n"; /** * Filter the field value before it is added to the email message. * * @since 1.8.5 * @deprecated 1.8.7 * * @param string $field_value Field value. * @param array $field Field data. * @param array $form_data Form data. */ $field_value = apply_filters_deprecated( // phpcs:disable WPForms.Comments.ParamTagHooks.InvalidParamTagsQuantity 'wpforms_emails_notifications_plaintext_field_value', [ $field_value, $field, $this->form_data ], '1.8.7 of the WPForms plugin', 'wpforms_plaintext_field_value' ); /** This filter is documented in /includes/emails/class-emails.php */ $field_value = apply_filters( // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName 'wpforms_plaintext_field_value', $field_value, $field, $this->form_data ); // Append the filtered field value to the message. $message .= $field_value; return $message; } /** * Process the HTML email message. * * @since 1.8.5 * * @param bool $show_empty_fields Whether to display empty fields in the email. * * @return string * @noinspection PhpUnusedLocalVariableInspection */ private function process_html_message( $show_empty_fields = false ) { // phpcs:ignore Generic.Metrics.CyclomaticComplexity $message = ''; /** * Filter the list of field types to display in the email. * * @since 1.8.5 * @deprecated 1.8.5.2 * * @param array $other_fields List of field types. * @param array $form_data Form data. */ $other_fields = apply_filters_deprecated( // phpcs:disable WPForms.Comments.ParamTagHooks.InvalidParamTagsQuantity 'wpforms_emails_notifications_display_other_fields', [ [], $this->form_data ], '1.8.5.2 of the WPForms plugin', 'wpforms_email_display_other_fields' ); /** This filter is documented in /includes/emails/class-emails.php */ $other_fields = (array) apply_filters( // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName 'wpforms_email_display_other_fields', [], $this ); /** * Filter the form data before it is used to generate the email message. * * @since 1.8.8 * @since 1.8.9 The $fields parameter was added. * * @param array $form_data Form data. * @param array $fields List of submitted fields. */ $this->form_data = apply_filters( 'wpforms_emails_notifications_form_data', $this->form_data, $this->fields ); foreach ( $this->form_data['fields'] as $field ) { /** * Filter whether to ignore the field in the email. * * @since 1.9.0 * * @param bool $ignore Whether to ignore the field in the email. * @param array $field Field data. * @param array $form_data Form data. */ if ( apply_filters( 'wpforms_emails_notifications_field_ignored', false, $field, $this->form_data ) ) { continue; } $field_message = $this->get_field_html( $field, $show_empty_fields, $other_fields ); /** * Filter the field message before it is added to the email message. * * @since 1.8.9 * @since 1.8.9.3 The $notifications parameter was added. * * @param string $field_message Field message. * @param array $field Field data. * @param bool $show_empty_fields Whether to display empty fields in the email. * @param array $other_fields List of field types. * @param array $form_data Form data. * @param array $fields List of submitted fields. * @param Notifications $notifications Notifications instance. */ $field_message = (string) apply_filters( 'wpforms_emails_notifications_field_message_html', $field_message, $field, $show_empty_fields, $other_fields, $this->form_data, $this->fields, $this ); $message .= trim( $field_message ); } return $message; } /** * Get a single field HTML markup. * * @since 1.8.9 * * @param array $field Field data. * @param bool $show_empty_fields Whether to display empty fields in the email. * @param array $other_fields List of field types. * * @return string */ public function get_field_html( array $field, bool $show_empty_fields, array $other_fields ): string { // phpcs:ignore Generic.Metrics.CyclomaticComplexity $field_type = ! empty( $field['type'] ) ? $field['type'] : ''; $field_id = $field['id'] ?? ''; // Check if the field is empty in $this->fields. if ( empty( $this->fields[ $field_id ] ) ) { // Check if the field type is in $other_fields, otherwise skip. // Skip if the field is conditionally hidden. if ( empty( $other_fields ) || ! in_array( $field_type, $other_fields, true ) || ( wpforms()->is_pro() && wpforms_conditional_logic_fields()->field_is_hidden( $this->form_data, $field_id ) ) ) { return ''; } // Handle specific field types. [ $field_name, $field_val ] = $this->process_special_field_values( $field ); } else { // Handle fields that are not empty in $this->fields. if ( ! $show_empty_fields && ( ! isset( $this->fields[ $field_id ]['value'] ) || (string) $this->fields[ $field_id ]['value'] === '' ) ) { return ''; } if ( $this->is_calculated_field_hidden( $field_id ) ) { return ''; } $field_name = $this->fields[ $field_id ]['name'] ?? ''; $field_val = empty( $this->fields[ $field_id ]['value'] ) && ! is_numeric( $this->fields[ $field_id ]['value'] ) ? '<em>' . esc_html__( '(empty)', 'wpforms-lite' ) . '</em>' : $this->fields[ $field_id ]['value']; } // Set a default field name if empty. if ( empty( $field_name ) && $field_name !== null ) { $field_name = $this->get_default_field_name( $field_id ); } /** * Filter the field name before it is added to the email message. * * @since 1.9.1 * * @param string $field_name Field name. * @param array $field Field data. * @param array $form_data Form data. * @param string $context Context of the field name. */ $field_name = (string) apply_filters( // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName 'wpforms_html_field_name', $field_name, $this->fields[ $field_id ] ?? $field, $this->form_data, 'email-html' ); /** This filter is documented in src/SmartTags/SmartTag/FieldHtmlId.php.*/ $field_val = (string) apply_filters( // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName 'wpforms_html_field_value', $field_val, $this->fields[ $field_id ] ?? $field, $this->form_data, 'email-html' ); $field_val = str_replace( [ "\r\n", "\r", "\n" ], '<br/>', $field_val ); // Replace the payment total value if an order summary is enabled. // Ideally, it could be done through the `wpforms_html_field_value` filter, // but necessary data is missed there, e.g., entry data ($this->fields). if ( $field_type === 'payment-total' && ! empty( $field['summary'] ) ) { $field_val = $this->get_payment_total_value( $field_val ); } // Append the field item to the message. return str_replace( [ '{field_type}', '{field_name}', '{field_value}' ], [ $field_type, $field_name, $field_val ], $this->field_template ); } /** * Get payment total value. * * @since 1.9.3 * * @param string $value Field value. * * @return string */ private function get_payment_total_value( string $value ): string { return $this->process_tag( '{order_summary}' ) . '<span class="wpforms-payment-total">' . $value . '</span>'; } /** * Check if a calculated field is hidden. * * @since 1.8.9.5 * * @param int $field_id Field ID. * * @return bool */ private function is_calculated_field_hidden( $field_id ): bool { return ! empty( $this->form_data['fields'][ $field_id ]['calculation_is_enabled'] ) && ! empty( $this->form_data['fields'][ $field_id ]['calculation_code_php'] ) && isset( $this->fields[ $field_id ]['visible'] ) && ! $this->fields[ $field_id ]['visible']; } /** * Process a smart tag. * * @since 1.8.5 * * @param string $input Smart tag. * @param string $context Context of the smart tag. * * @return string */ private function process_tag( $input = '', $context = 'notification' ): string { $context_data = []; /** * Email(s). * * @var string|string[] $to_email */ $to_email = array_filter( (array) ( $this->__get( 'to_email' ) ?? '' ) ); $context_data['to_email'] = $to_email; return wpforms_process_smart_tags( $input, $this->form_data, $this->fields, (string) $this->entry_id, $context, $context_data ); } /** * Filter the smart tag value for the mailer email addresses. * * @since 1.9.5 * * @param string|mixed $value Smart Tag value. * @param string $tag_name Smart tag name. * @param array $form_data Form data. * @param array $fields List of fields. * @param int $entry_id Entry ID. * @param SmartTag $smart_tag_object The smart tag object or the Generic object for those cases when class * unregistered. * * @return string|null * @noinspection PhpMissingParamTypeInspection * @noinspection PhpUnusedParameterInspection */ public static function filter_smarttags_process_value( $value, $tag_name, $form_data, $fields, $entry_id, $smart_tag_object ): ?string { $tag_name = (string) $tag_name; $fields = (array) $fields; // Smart tag isn't registered and can be replaced via filters. if ( $value === null ) { return null; } $value = (string) $value; $context = $smart_tag_object->context ?? ''; $allowed_tags = [ 'admin_email', 'user_email', ]; // In these contexts, we need to check if the smart tag is allowed. $address_context = [ 'notification-from', ]; // Check if the smart tag is allowed AND if the context is allowed. if ( in_array( $tag_name, $allowed_tags, true ) || ! in_array( $context, $address_context, true ) ) { return $value; } return self::validate_notification_email_smart_tags( $value, $tag_name, $fields, $smart_tag_object ); } /** * Validate notification email fields. * * @since 1.9.5 * * @param string|mixed $value Smart Tag value. * @param string $tag_name Smart tag name. * @param array $fields List of fields. * @param SmartTag $smart_tag_object The smart tag object or the Generic object for those cases when class unregistered. * * @return string */ private static function validate_notification_email_smart_tags( string $value, string $tag_name, array $fields, SmartTag $smart_tag_object ): string { $field_id = self::get_smart_tag_field_id( $tag_name, $smart_tag_object ); // Empty value for all non-field smart tags. if ( $field_id === null || $field_id === '' || ! isset( $fields[ $field_id ]['type'] ) ) { return ''; } $field_type = $fields[ $field_id ]['type']; // If the field type is Email, return the value. if ( $field_type === 'email' ) { return $value; } // Allow the Name field value in the Reply To setting. if ( $field_type === 'name' && $smart_tag_object->context === 'notification-reply-to' ) { return $value; } // Otherwise, return an empty string if the value is not an email. return wpforms_is_email( $value ) ? $value : ''; } /** * Get smart tag field ID. * * @since 1.9.5 * * @param string $tag_name Smart tag name. * @param SmartTag $smart_tag_object The smart tag object or the Generic object for those cases when class unregistered. * * @return mixed|string|null */ private static function get_smart_tag_field_id( string $tag_name, SmartTag $smart_tag_object ) { if ( $tag_name === 'field_value_id' ) { return $smart_tag_object->get_attributes()[ $tag_name ] ?? null; } if ( $tag_name !== 'field_id' ) { return null; } $field_id_parts = explode( '|', $smart_tag_object->get_attributes()['field_id'] ?? '' ); return $field_id_parts[0] ?? null; } /** * Process special field types. * This is used for fields such as Page Break, HTML, Content, etc. * * @since 1.8.5 * * @param array $field Field data. * * @return array */ private function process_special_field_values( $field ) { // phpcs:ignore Generic.Metrics.CyclomaticComplexity $field_name = null; $field_val = null; // Use a switch-case statement to handle specific field types. switch ( $field['type'] ) { case 'divider': $field_name = ! empty( $field['label'] ) ? str_repeat( '—', 3 ) . ' ' . $field['label'] . ' ' . str_repeat( '—', 3 ) : null; $field_val = ! empty( $field['description'] ) ? $field['description'] : ''; break; case 'pagebreak': // Skip if the position is 'bottom'. if ( ! empty( $field['position'] ) && $field['position'] === 'bottom' ) { break; } $title = ! empty( $field['title'] ) ? $field['title'] : esc_html__( 'Page Break', 'wpforms-lite' ); $field_name = str_repeat( '—', 6 ) . ' ' . $title . ' ' . str_repeat( '—', 6 ); break; case 'html': $field_name = ! empty( $field['name'] ) ? $field['name'] : esc_html__( 'HTML / Code Block', 'wpforms-lite' ); $field_val = $field['code']; break; case 'content': $field_name = esc_html__( 'Content', 'wpforms-lite' ); $field_val = wpforms_esc_richtext_field( $field['content'] ); break; default: $field_name = ''; $field_val = ''; break; } return [ $field_name, $field_val ]; } /** * Get the email reply to the address. * This method has been overridden to add support for the Reply-to Name. * * @since 1.8.5 * * @return string */ public function get_reply_to_address() { $reply_to = $this->__get( 'reply_to' ); $reply_to_name = false; if ( ! empty( $reply_to ) ) { // Optional custom format with a Reply-to Name specified: John Doe <john@doe.com> // - starts with anything, // - followed by space, // - ends with <anything> (expected to be an email, validated later). $regex = '/^(.+) (<.+>)$/'; $matches = []; if ( preg_match( $regex, $reply_to, $matches ) ) { $reply_to_name = $this->sanitize( $matches[1] ); $reply_to = trim( $matches[2], '<> ' ); } $reply_to = $this->process_tag( $reply_to, 'notification-reply-to' ); if ( ! is_email( $reply_to ) ) { $reply_to = false; $reply_to_name = false; } } if ( $reply_to_name ) { $reply_to = "$reply_to_name <{$reply_to}>"; } /** * Filter the email reply-to address. * * @since 1.8.5 * * @param string $reply_to Email reply-to address. * @param object $this Instance of the Notifications class. */ return apply_filters( 'wpforms_emails_notifications_get_reply_to_address', $reply_to, $this ); } /** * Sanitize the string. * This method has been overridden to add support for processing smart tags. * * @since 1.8.5 * * @param string $input String to sanitize and process for smart tags. * @param string $context Context of the smart tag. * * @return string */ public function sanitize( $input = '', $context = 'notification' ): string { return wpforms_decode_string( $this->process_tag( $input, $context ) ); } /** * Get the email content type. * This method has been overridden to better declare the email template assigned to each notification. * * @since 1.8.5.2 * * @return string */ public function get_content_type() { $content_type = 'text/html'; if ( Helpers::is_plain_text_template( $this->current_template ) ) { $content_type = 'text/plain'; } /** * Filter the email content type. * * @since 1.8.5.2 * * @param string $content_type The email content type. * @param Notifications $this An instance of the "Notifications" class. */ $content_type = apply_filters( 'wpforms_emails_notifications_get_content_type', $content_type, $this ); // Set the content type. $this->__set( 'content_type', $content_type ); // Return the content type. return $content_type; } /** * Check if all emails are disabled. * * @since 1.8.5 * * @return bool */ public function is_email_disabled() { /** * Filter to control email disabling. * * The "Notifications" class is designed to mirror the properties and methods * provided by the "WPForms_WP_Emails" class for backward compatibility. * * @since 1.8.5 * * @param bool $is_disabled Whether to disable all emails. * @param Notifications $this An instance of the "Notifications" class. */ return (bool) apply_filters( // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName 'wpforms_disable_all_emails', false, $this ); } /** * Get the default field name as a fallback. * * @since 1.8.5 * * @param int $field_id Field ID. * * @return string */ private function get_default_field_name( $field_id ) { return sprintf( /* translators: %1$d - field ID. */ esc_html__( 'Field ID #%1$s', 'wpforms-lite' ), wpforms_validate_field_id( $field_id ) ); } /** * Wrap content in the 'tr' tag on the first level depth. * * @since 1.9.6 * * @param string $content Processed smart tag content. * * @return string */ private function fix_table_body_markup( string $content ): string { $content = trim( $content ); libxml_use_internal_errors( true ); $dom = new DOMDocument( '1.0', 'UTF-8' ); // We should encode `<` and `>` symbols to prevent unexpected HTML tags. $html = '<!DOCTYPE html><html><head><meta charset="UTF-8"></head><body>' . wp_pre_kses_less_than( $content ) . '</body></html>'; $dom->loadHTML( $html, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD | LIBXML_NOERROR ); libxml_clear_errors(); $body = $dom->getElementsByTagName( 'body' )->item( 0 ); if ( ! $body ) { return $this->wrap_content_with_row( $content ); } $modified_content = ''; $content_to_wrap = ''; // phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase foreach ( $body->childNodes as $node ) { $node_text = $node->nodeType === XML_TEXT_NODE ? $node->nodeValue : $dom->saveHTML( $node ); if ( ! property_exists( $node, 'tagName' ) || $node->tagName !== 'tr' ) { $content_to_wrap .= $node_text; continue; } // Wrap content before the `tr` tag. $modified_content .= $this->wrap_content_with_row( $content_to_wrap ); // Save the `tr` tag without wrapping. $modified_content .= $node_text; $content_to_wrap = ''; } // phpcs:enable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase if ( ! wpforms_is_empty_string( $content_to_wrap ) ) { $modified_content .= $this->wrap_content_with_row( $content_to_wrap ); } return $modified_content; } /** * Wrap content to the `tr` tag. * * @since 1.9.6 * * @param string $content Content. * * @return string */ private function wrap_content_with_row( string $content ): string { $content = trim( $content ); if ( wpforms_is_empty_string( $content ) ) { return ''; } return sprintf( '<tr class="smart-tag"><td class="field-name field-value" colspan="2">%s</td></tr>', $content ); } /** * Get the list of available email templates. * * Given a template name, this method will return the template data. * If no template name is provided, all available templates will be returned. * * Templates will go through a conditional check to make sure they are available for the current plugin edition. * * @since 1.8.5 * * @param string $template Template name. If empty, all available templates will be returned. * * @return array */ public static function get_available_templates( $template = '' ) { $templates = self::get_all_templates(); // Filter the list of available email templates based on the edition of WPForms. if ( ! wpforms()->is_pro() ) { $templates = array_filter( $templates, static function ( $instance ) { return ! $instance['is_pro']; } ); } return $templates[ $template ] ?? $templates; } /** * Get the list of all email templates. * * Given the name of a template, this method will return the template data. * If the template is not found, all available templates will be returned. * * @since 1.8.5 * * @param string $template Template name. If empty, all templates will be returned. * * @return array */ public static function get_all_templates( $template = '' ) { $templates = [ 'classic' => [ 'name' => esc_html__( 'Classic', 'wpforms-lite' ), 'path' => Templates\Classic::class, 'is_pro' => false, ], 'compact' => [ 'name' => esc_html__( 'Compact', 'wpforms-lite' ), 'path' => Templates\Compact::class, 'is_pro' => false, ], 'modern' => [ 'name' => esc_html__( 'Modern', 'wpforms-lite' ), 'path' => Modern::class, 'is_pro' => true, ], 'elegant' => [ 'name' => esc_html__( 'Elegant', 'wpforms-lite' ), 'path' => Elegant::class, 'is_pro' => true, ], 'tech' => [ 'name' => esc_html__( 'Tech', 'wpforms-lite' ), 'path' => Tech::class, 'is_pro' => true, ], 'none' => [ 'name' => esc_html__( 'Plain Text', 'wpforms-lite' ), 'path' => Templates\Plain::class, 'is_pro' => false, ], ]; // Make sure the current user can preview templates. if ( wpforms_current_user_can() ) { // Add a preview key to each template. foreach ( $templates as $key => &$tmpl ) { $tmpl['preview'] = wp_nonce_url( add_query_arg( [ 'wpforms_email_preview' => '1', 'wpforms_email_template' => $key, ], admin_url() ), Preview::PREVIEW_NONCE_NAME ); } // Make sure to unset the reference to avoid unintended changes later. unset( $tmpl ); } return $templates[ $template ] ?? $templates; } /** * Get multiple field formatted value. * * @since 1.9.0 * * @param string $value Field value. * @param int $field_id Field ID. * @param array $fields List of fields. * @param string $field_key Field key to get value from. * * @return string * * @noinspection PhpUnusedParameterInspection */ public function get_multi_field_formatted_value( string $value, int $field_id, array $fields, string $field_key ): string { $field_type = $fields[ $field_id ]['type'] ?? ''; // Leave early if the field type is not a multi-field. if ( ! in_array( $field_type, wpforms_get_multi_fields(), true ) ) { return $value; } // Leave early if the template is set to plain text. if ( Helpers::is_plain_text_template( $this->current_template ) ) { // Replace <br/> tags with line breaks. return str_replace( '<br/>', "\r\n", $value ); } return str_replace( [ "\r\n", "\r", "\n" ], '<br/>', $value ); } /** * Get the current template name. * * @since 1.9.3 * * @return string */ public function get_current_template(): string { return $this->current_template; } /** * Get the current field template markup. * * @since 1.9.4 * * @return string */ public function get_current_field_template(): string { return $this->field_template; } } Emails/NotificationBlocks.php 0000644 00000011202 15174710275 0012260 0 ustar 00 <?php namespace WPForms\Emails; use WPForms\Admin\Notifications\Notifications; /** * Notification class. * This class is responsible for displaying the notification block in the email summaries. * * @since 1.8.8 */ class NotificationBlocks { /** * Notifications class instance. * * @since 1.8.8 * * @var Notifications */ private $notifications; /** * Class constructor. * Initializes the Notifications class instance. * * @since 1.8.8 */ public function __construct() { // Store the instance of the "Notifications" class. $this->notifications = wpforms()->obj( 'notifications' ); } /** * Retrieves the notification block from the feed, considering shown notifications and license type. * * @since 1.8.8 * * @return array */ public function get_block(): array { // Check if the user has access to notifications. // If the user has disabled announcements, return an empty array. if ( ! $this->notifications || ! $this->notifications->has_access() ) { return []; } // Get the response array from the notifications. $notifications = $this->notifications->get_option(); // Check if 'feed' key is present and non-empty. if ( empty( $notifications['feed'] ) || ! is_array( $notifications['feed'] ) ) { return []; } // Remove items from $feed where their id index is in `shown_notifications` option value. $feed = $this->filter_feed( $notifications['feed'] ); // Sort the array of items using usort and the custom comparison function. $feed = $this->sort_feed( $feed ); // Get the very first item from the $feed. $block = reset( $feed ); // Check if $block is empty. if ( empty( $block ) ) { return []; } // Return the notification block. return $this->prepare_and_sanitize_content( $block ); } /** * Save the shown notification block if it's not empty. * * @since 1.8.8 * * @param array $notification The notification to be saved. */ public function maybe_remember_shown_block( array $notification ) { // Check if the notification or its ID is empty. if ( empty( $notification ) || empty( $notification['id'] ) ) { // If the notification or its ID is empty, return early. return; } // Get shown notifications from options. $shown_notifications = (array) get_option( 'wpforms_email_summaries_shown_notifications', [] ); // Add the notification id to the $shown_notifications array. $shown_notifications[] = (int) $notification['id']; // Update the shown notifications in the options. // Avoid autoloading the option, as it's not needed. update_option( 'wpforms_email_summaries_shown_notifications', $shown_notifications, false ); } /** * Filter the feed to remove shown notifications. * * @since 1.8.8 * * @param array $feed The feed to filter. * * @return array */ private function filter_feed( array $feed ): array { $shown_notifications = (array) get_option( 'wpforms_email_summaries_shown_notifications', [] ); return array_filter( $feed, static function ( $item ) use ( $shown_notifications ) { return ! in_array( $item['id'], $shown_notifications, true ); } ); } /** * Sort the feed in descending order by start date. * * @since 1.8.8 * * @param array $feed The feed to sort. * * @return array */ private function sort_feed( array $feed ): array { usort( $feed, static function ( $a, $b ) { return strtotime( $b['start'] ) - strtotime( $a['start'] ); } ); return $feed; } /** * Prepare and sanitize content for display. * * @since 1.8.8 * * @param string|array $content The content to be prepared and sanitized. * * @return string|array */ private function prepare_and_sanitize_content( $content ) { // If the content is empty, return as is. if ( empty( $content ) ) { return $content; } // If the content is already a string, sanitize and return it. if ( is_string( $content ) ) { // Define allowed HTML tags and attributes. $content_allowed_tags = $this->notifications->get_allowed_tags(); // For design consistency, remove the 'p' tag from the allowed tags. unset( $content_allowed_tags['p'] ); // Apply wp_kses() for sanitization. return wp_kses( $content, $content_allowed_tags ); } // If the content is an array with the 'content' index, modify and sanitize it. if ( is_array( $content ) && isset( $content['content'] ) ) { // Sanitize the content of the array. $content['content'] = $this->prepare_and_sanitize_content( $content['content'] ); // Return the modified array. return $content; } // If the content is not a string or an array with 'content' index, return the content as is. return $content; } } Emails/Preview.php 0000644 00000031712 15174710275 0010125 0 ustar 00 <?php namespace WPForms\Emails; /** * Class Preview. * Handles previewing email templates. * * @since 1.8.5 */ class Preview { /** * List of preview fields. * * @since 1.8.5 * * @var array */ private $fields = []; /** * Current email template. * * @since 1.8.5 * * @var string */ private $current_template; /** * Field template. * * @since 1.8.5 * * @var string */ private $field_template; /** * Content is plain text type. * * @since 1.8.5 * * @var bool */ private $plain_text; /** * Preview nonce name. * * @since 1.8.5 * * @var string */ const PREVIEW_NONCE_NAME = 'wpforms_email_preview'; /** * XOR key. * * The encryption key is a critical element in encryption algorithms, * playing a crucial role in XOR encryption as employed in the WPFormsXOR plugin class. * This key serves to govern the transformation of data during both encryption and decryption processes. * * The default and placeholder value for the key, as defined in the plugin class, is set to 42. * If you wish to employ a different key (any numerical value is acceptable), you must provide * that specific number to the plugin instance. It's essential to use the exact same key for * both encrypting and decrypting data in the PHP environment as well. * * @since 1.8.6 * * @var int */ const XOR_KEY = 42; /** * Initialize class. * * @since 1.8.5 */ public function init() { // Leave if user can't access. if ( ! wpforms_current_user_can() ) { return; } // Leave early if nonce verification failed. if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( sanitize_key( $_GET['_wpnonce'] ), self::PREVIEW_NONCE_NAME ) ) { return; } // Leave early if preview is not requested. if ( ! isset( $_GET['wpforms_email_preview'], $_GET['wpforms_email_template'] ) ) { return; } $this->current_template = sanitize_key( $_GET['wpforms_email_template'] ); $this->plain_text = $this->current_template === 'none'; $this->hooks(); $this->preview(); } /** * Hooks. * * @since 1.8.6 */ private function hooks() { add_filter( 'wpforms_emails_templates_notifications_get_header_image', [ $this, 'edit_current_template_header_image' ] ); add_filter( 'wpforms_emails_helpers_style_overrides_args', [ $this, 'edit_current_template_style_overrides' ] ); } /** * This filter is used to override the current email template header image. * * This is needed to make sure the preview link is able to reflect the * changes made in the email template style settings without saving the settings page. * * @since 1.8.6 * * @param array $header_image The current email template header image. * * @return array */ public function edit_current_template_header_image( $header_image ) { // Get style overrides. $overrides = $this->get_style_overrides(); // Leave early if no overrides are passed for the preview. if ( empty( $header_image ) || empty( $overrides ) ) { return $header_image; } // Check for the presence of light mode header image in the query string. if ( isset( $overrides['email_header_image'] ) ) { $header_image['url_light'] = esc_url_raw( $overrides['email_header_image'] ); // Check for the presence of light mode header image size in the query string. if ( ! empty( $overrides['email_header_image_size'] ) ) { $header_image['size_light'] = sanitize_text_field( $overrides['email_header_image_size'] ); } } // Check for the presence of dark mode header image in the query string. if ( isset( $overrides['email_header_image_dark'] ) ) { $header_image['url_dark'] = esc_url_raw( $overrides['email_header_image_dark'] ); if ( ! empty( $overrides['email_header_image_size_dark'] ) ) { $header_image['size_dark'] = sanitize_text_field( $overrides['email_header_image_size_dark'] ); } } return $header_image; } /** * This filter is used to override the current email template style overrides. * * This is needed to make sure the preview link is able to reflect the * changes made in the email template style settings without saving the settings page. * * @since 1.8.6 * * @param array $styles The current email template styles. * * @return array */ public function edit_current_template_style_overrides( $styles ) { // Get style overrides. $overrides = $this->get_style_overrides(); // Leave early if no overrides are passed for the preview. if ( empty( $overrides ) ) { return $styles; } // Check for the presence of light mode background color in the query string. if ( ! empty( $overrides['email_background_color'] ) ) { $styles['email_background_color'] = sanitize_hex_color( $overrides['email_background_color'] ); } // Check for the presence of dark mode background color in the query string. if ( ! empty( $overrides['email_background_color_dark'] ) ) { $styles['email_background_color_dark'] = sanitize_hex_color( $overrides['email_background_color_dark'] ); } // Leave early if the user has the Lite version. if ( ! wpforms()->is_pro() ) { // The only allowed override for the Lite version is the header image size. // This is needed to make sure the preview link is able to reflect the // changes made in the email template style settings without saving the settings page. if ( empty( $overrides['email_header_image_size'] ) ) { // Return the styles if no header image size override is passed for the preview. return $styles; } // Override and process the header image size. $overrides = [ 'email_header_image_size' => $overrides['email_header_image_size'] ]; return $this->process_allowed_overrides( $styles, $overrides ); } // Process allowed overrides using a separate function. return $this->process_allowed_overrides( $styles, $overrides ); } /** * Get style overrides. * * @since 1.8.6 * * @return array */ private function get_style_overrides() { // phpcs:disable WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized // Check if the 'wpforms_email_style_overrides' parameter is empty. if ( empty( $_GET['wpforms_email_style_overrides'] ) ) { return []; } // Retrieve and unslash the encoded style overrides from the query string. $style_overrides = wp_unslash( $_GET['wpforms_email_style_overrides'] ); // phpcs:enable WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $overrides = ''; $overrides_len = strlen( $style_overrides ); // Decode the overrides. // This is needed because the overrides are encoded before being passed in the query string. for ( $i = 0; $i < $overrides_len; $i++ ) { $overrides .= chr( ord( $style_overrides[ $i ] ) ^ self::XOR_KEY ); } // Return the decoded overrides as an associative array. return json_decode( $overrides, true ); } /** * Process allowed style overrides. * * @since 1.8.6 * * @param array $styles Current styles. * @param array $overrides Style overrides. * * @return array Updated styles. */ private function process_allowed_overrides( $styles, $overrides ) { // Leave early if no overrides are passed for the preview. if ( empty( $overrides ) ) { return $styles; } // Define an array of allowed query parameters. $allowed_overrides = [ 'email_body_color', 'email_text_color', 'email_links_color', 'email_typography', 'email_header_image_size', 'email_body_color_dark', 'email_text_color_dark', 'email_links_color_dark', 'email_typography_dark', 'email_header_image_size_dark', ]; // Loop through allowed parameters and update $overrides if present in the query string. foreach ( $allowed_overrides as $param ) { // Leave early if the parameter is not present in the query string. if ( empty( $overrides[ $param ] ) ) { continue; } $styles = $this->process_override( $param, $styles, $overrides ); } return $styles; } /** * Process a specific style override. * * @since 1.8.6 * * @param string $param Style parameter. * @param array $styles Current styles. * @param array $overrides Style overrides. * * @return array Updated styles. */ private function process_override( $param, $styles, $overrides ) { // phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh // Use a switch to handle specific cases. switch ( $param ) { case 'email_body_color': case 'email_text_color': case 'email_links_color': case 'email_body_color_dark': case 'email_text_color_dark': case 'email_links_color_dark': $styles[ $param ] = sanitize_hex_color( $overrides[ $param ] ); break; case 'email_typography': case 'email_typography_dark': $styles[ $param ] = Helpers::get_template_typography( sanitize_text_field( $overrides[ $param ] ) ); break; case 'email_header_image_size': $header_image_size = Helpers::get_template_header_image_size( sanitize_text_field( $overrides[ $param ] ) ); $styles['header_image_max_width'] = $header_image_size['width']; $styles['header_image_max_height'] = $header_image_size['height']; break; case 'email_header_image_size_dark': $header_image_size_dark = Helpers::get_template_header_image_size( sanitize_text_field( $overrides[ $param ] ) ); $styles['header_image_max_width_dark'] = $header_image_size_dark['width']; $styles['header_image_max_height_dark'] = $header_image_size_dark['height']; break; } return $styles; } /** * Preview email template. * * @since 1.8.5 */ private function preview() { $template = Notifications::get_available_templates( $this->current_template ); /** * Filter the email template to be previewed. * * @since 1.8.5 * * @param array $template Email template. */ $template = (array) apply_filters( 'wpforms_emails_preview_template', $template ); // Redirect to the email settings page if the template is not set. if ( ! isset( $template['path'] ) || ! class_exists( $template['path'] ) ) { wp_safe_redirect( add_query_arg( [ 'page' => 'wpforms-settings', 'view' => 'email', ], admin_url( 'admin.php' ) ) ); exit; } // Set the email template, i.e. WPForms\Emails\Templates\Classic. $template = new $template['path']( '', true ); // Set the field template. // This is used to replace the placeholders in the email template. $this->field_template = $template->get_field_template(); // Set the email template fields. $template->set_field( $this->get_placeholder_message() ); // Get the email template content. $content = $template->get(); // Return if the template is empty. if ( ! $content ) { return; } // Echo the email template content. echo $content; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped exit; // No need to continue. WordPress will die() after this. } /** * Get preview content. * * @since 1.8.5 * * @return string Placeholder message. */ private function get_placeholder_message() { $this->fields = [ [ 'type' => 'name', 'name' => __( 'Name', 'wpforms-lite' ), 'value' => 'Sullie Eloso', ], [ 'type' => 'email', 'name' => __( 'Email', 'wpforms-lite' ), 'value' => 'sullie@wpforms.com', ], [ 'type' => 'textarea', 'name' => __( 'Comment or Message', 'wpforms-lite' ), 'value' => "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Odio ut sem nulla pharetra diam sit amet. Sed risus pretium quam vulputate dignissim suspendisse in est ante. Risus ultricies tristique nulla aliquet enim tortor at auctor. Nisl tincidunt eget nullam non nisi est sit amet facilisis. Duis at tellus at urna condimentum mattis pellentesque id nibh. Curabitur vitae nunc sed velit dignissim.\r\n\r\nLeo urna molestie at elementum eu facilisis sed odio. Scelerisque mauris pellentesque pulvinar pellentesque habitant morbi. Volutpat maecenas volutpat blandit aliquam. Libero id faucibus nisl tincidunt. Et malesuada fames ac turpis egestas.", ], ]; // Early return if the template is plain text. if ( $this->plain_text ) { return $this->process_plain_message(); } return $this->process_html_message(); } /** * Process the HTML email message. * * @since 1.8.5 * * @return string */ private function process_html_message() { $message = ''; foreach ( $this->fields as $field ) { $message .= str_replace( [ '{field_type}', '{field_name}', '{field_value}', "\r\n" ], [ $field['type'], $field['name'], $field['value'], '<br>' ], $this->field_template ); } return $message; } /** * Process the plain text email message. * * @since 1.8.5 * * @return string */ private function process_plain_message() { $message = ''; foreach ( $this->fields as $field ) { $message .= '--- ' . $field['name'] . " ---\r\n\r\n" . str_replace( [ "\n", "\r" ], '', $field['value'] ) . "\r\n\r\n"; } return nl2br( $message ); } } Emails/Summaries.php 0000644 00000034176 15174710275 0010460 0 ustar 00 <?php namespace WPForms\Emails; use Exception; use WPForms\Emails\Tasks\FetchInfoBlocksTask; /** * Email Summaries main class. * * @since 1.5.4 */ class Summaries { /** * Constructor. * * @since 1.5.4 */ public function __construct() { $this->hooks(); $summaries_disabled = $this->is_disabled(); if ( $summaries_disabled && wp_next_scheduled( 'wpforms_email_summaries_cron' ) ) { wp_clear_scheduled_hook( 'wpforms_email_summaries_cron' ); } if ( ! $summaries_disabled && ! wp_next_scheduled( 'wpforms_email_summaries_cron' ) ) { // Since v1.9.1 we use a single event and manually reoccur it // because a recurring event cannot guarantee // its firing at the same time during WP_CLI execution. wp_schedule_single_event( $this->get_next_launch_time(), 'wpforms_email_summaries_cron' ); } } /** * Get the instance of a class and store it in itself. * * @since 1.5.4 */ public static function get_instance() { static $instance; if ( ! $instance ) { $instance = new self(); } return $instance; } /** * Email Summaries hooks. * * @since 1.5.4 */ public function hooks() { add_filter( 'wpforms_settings_defaults', [ $this, 'disable_summaries_setting' ] ); add_action( 'wpforms_settings_updated', [ $this, 'deregister_fetch_info_blocks_task' ] ); // Leave early if Email Summaries are disabled in settings. if ( $this->is_disabled() ) { return; } add_action( 'init', [ $this, 'preview' ] ); add_action( 'wpforms_email_summaries_cron', [ $this, 'cron' ] ); add_filter( 'wpforms_tasks_get_tasks', [ $this, 'register_fetch_info_blocks_task' ] ); } /** * Check if Email Summaries are disabled in settings. * * @since 1.5.4 * * @return bool */ protected function is_disabled(): bool { /** * Allows to modify whether Email Summaries are disabled in settings. * * @since 1.5.4 * * @param bool $is_disabled True if Email Summaries are disabled in settings. False by default. */ return (bool) apply_filters( 'wpforms_emails_summaries_is_disabled', (bool) wpforms_setting( 'email-summaries-disable', false ) ); } /** * Add "Disable Email Summaries" to WPForms settings. * * @since 1.5.4 * * @param array $settings WPForms settings. * * @return mixed */ public function disable_summaries_setting( $settings ) { /** This filter is documented in wpforms/src/Emails/Summaries.php */ if ( (bool) apply_filters( 'wpforms_emails_summaries_is_disabled', false ) ) { return $settings; } $url = wp_nonce_url( add_query_arg( [ 'wpforms_email_template' => 'summary', 'wpforms_email_preview' => '1', ], admin_url() ), Preview::PREVIEW_NONCE_NAME ); $desc = esc_html__( 'Disable Email Summaries weekly delivery.', 'wpforms-lite' ); if ( ! $this->is_disabled() ) { $desc .= ' <a href="' . $url . '" target="_blank">' . esc_html__( 'View Email Summary Example', 'wpforms-lite' ) . '</a>.'; } // Get the uninstall data setting. $uninstall_data = $settings['misc']['uninstall-data']; // Remove the uninstall data setting. unset( $settings['misc']['uninstall-data'] ); // Add the email summaries setting. $settings['misc']['email-summaries-disable'] = [ 'id' => 'email-summaries-disable', 'name' => esc_html__( 'Disable Email Summaries', 'wpforms-lite' ), 'desc' => $desc, 'type' => 'toggle', 'status' => true, ]; // Add the uninstall data setting to the end. $settings['misc']['uninstall-data'] = $uninstall_data; return $settings; } /** * Preview Email Summary. * * @since 1.5.4 */ public function preview() { // Leave early if the current request is not a preview for the summaries email template. if ( ! $this->is_preview() ) { return; } // Get form entries. $entries = $this->get_entries(); $args = [ 'body' => [ 'overview' => $this->get_calculation_overview( $entries ), 'entries' => $this->format_trends_for_display( $entries ), 'has_trends' => $this->entries_has_trends( $entries ), 'notification_block' => ( new NotificationBlocks() )->get_block(), 'info_block' => ( new InfoBlocks() )->get_next(), 'icons' => $this->get_icons_url(), ], ]; $template = ( new Templates\Summary() )->set_args( $args ); /** * Filters the summaries email template. * * @since 1.5.4 * * @param Templates\Summary $template Default summaries email template. */ $template = apply_filters( 'wpforms_emails_summaries_template', $template ); $content = $template->get(); if ( Helpers::is_plain_text_template() ) { $content = wpautop( $content ); } // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo $content; exit; } /** * Get next cron occurrence date. * * @since 1.5.4 * @deprecated 1.9.1 * * @return int */ protected function get_first_cron_date_gmt(): int { _deprecated_function( __METHOD__, '1.9.1 of the WPForms plugin', __CLASS__ . '::get_next_launch_time()' ); return $this->get_next_launch_time(); } /** * Get next Monday 2p.m with WordPress offset. * * @since 1.9.1 * * @return int */ protected function get_next_launch_time(): int { $datetime = date_create( 'now', wp_timezone() ); $now_plus_week = time() + constant( 'WEEK_IN_SECONDS' ); if ( ! $datetime ) { return $now_plus_week; } $hours = 14; // If today is Monday and the current time is less than 2 p.m., // we can launch the cron for today. if ( (int) $datetime->format( 'N' ) !== 1 || (int) $datetime->format( 'H' ) >= $hours ) { try { $datetime->modify( 'next monday' ); } catch ( Exception $e ) { return $now_plus_week; } } $datetime->setTime( $hours, 0 ); $timestamp = $datetime->getTimestamp(); return $timestamp > 0 ? $timestamp : $now_plus_week; } /** * Add custom Email Summaries cron schedule. * * @since 1.5.4 * @deprecated 1.9.1 * * @param array $schedules WP cron schedules. * * @return array */ public function add_weekly_cron_schedule( $schedules ) { _deprecated_function( __METHOD__, '1.9.1 of the WPForms plugin' ); $schedules['wpforms_email_summaries_weekly'] = [ 'interval' => $this->get_next_launch_time() - time(), 'display' => esc_html__( 'Weekly WPForms Email Summaries', 'wpforms-lite' ), ]; return $schedules; } /** * Email Summaries cron callback. * * @since 1.5.4 */ public function cron() { $entries = $this->get_entries(); // Email won't be sent if there are no form entries. if ( empty( $entries ) ) { return; } $notification = new NotificationBlocks(); $notification_block = $notification->get_block(); $info_blocks = new InfoBlocks(); $next_block = $info_blocks->get_next(); $args = [ 'body' => [ 'overview' => $this->get_calculation_overview( $entries ), 'entries' => $this->format_trends_for_display( $entries ), 'has_trends' => $this->entries_has_trends( $entries ), 'notification_block' => $notification_block, 'info_block' => $next_block, 'icons' => $this->get_icons_url(), ], ]; $template = ( new Templates\Summary() )->set_args( $args ); /** This filter is documented in preview() method above. */ $template = apply_filters( 'wpforms_emails_summaries_template', $template ); $content = $template->get(); if ( ! $content ) { return; } $parsed_home_url = wp_parse_url( home_url() ); $site_domain = $parsed_home_url['host']; if ( is_multisite() && isset( $parsed_home_url['path'] ) ) { $site_domain .= $parsed_home_url['path']; } $subject = sprintf( /* translators: %s - site domain. */ esc_html__( 'Your Weekly WPForms Summary for %s', 'wpforms-lite' ), $site_domain ); /** * Filters the summaries email subject. * * @since 1.5.4 * * @param string $subject Default summaries email subject. */ $subject = apply_filters( 'wpforms_emails_summaries_cron_subject', $subject ); /** * Filters the summaries recipient email address. * * @since 1.5.4 * * @param string $option Default summaries recipient email address. */ $to_email = apply_filters( 'wpforms_emails_summaries_cron_to_email', get_option( 'admin_email' ) ); $sent = ( new Mailer() ) ->template( $template ) ->subject( $subject ) ->to_email( $to_email ) ->send(); if ( $sent === true ) { $info_blocks->register_sent( $next_block ); // Cache the notification block shown to avoid showing it again in the future. $notification->maybe_remember_shown_block( $notification_block ); } } /** * Get form entries. * * @since 1.5.4 * * @return array */ protected function get_entries(): array { // The return value is intentionally left empty, as each email summary // depending on the plugin edition Lite/Pro will have different implementation. return []; } /** * Get calculation overview. * * @since 1.8.8 * * @param array $entries Form entries. * * @return array */ private function get_calculation_overview( $entries ): array { // Check if the entries array is empty. if ( empty( $entries ) ) { return []; } // Get the sum of 'count' index in all entries. $sum_current = array_sum( array_column( $entries, 'count' ) ); // Choose a specific 'form_id' to check if 'count_previous_week' index exists. $sample_form_id = key( $entries ); // Check if 'count_previous_week' index doesn't exist and return early. if ( ! isset( $entries[ $sample_form_id ]['count_previous_week'] ) ) { return []; } // Get the sum of 'count_previous_week' index in all entries. $sum_previous_week = array_sum( array_column( $entries, 'count_previous_week' ) ); // Check if the sum of counts from the previous week is 0. // If so, return the sum of counts from the current week and trends as "+100%". if ( $sum_previous_week === 0 ) { return [ 'total' => $sum_current, 'trends' => $this->format_trends_for_display( $sum_current === 0 ? 0 : 100 ), ]; } // Calculate trends based on the sum of counts from the current week and the previous week. $trends = round( ( $sum_current - $sum_previous_week ) / $sum_previous_week * 100 ); // Return an array with the total and trends. return [ 'total' => $sum_current, 'trends' => $this->format_trends_for_display( $trends ), ]; } /** * Register Action Scheduler task to fetch and cache Info Blocks. * * @since 1.6.4 * * @param \WPForms\Tasks\Task[] $tasks List of task classes. * * @return array */ public static function register_fetch_info_blocks_task( $tasks ): array { $tasks[] = FetchInfoBlocksTask::class; return $tasks; } /** * Deregister Action Scheduler task to fetch and cache Info Blocks. * * @since 1.6.4 */ public function deregister_fetch_info_blocks_task() { if ( ! $this->is_disabled() ) { return; } // Deregister the task. ( new FetchInfoBlocksTask() )->cancel(); // Delete last run time record. delete_option( FetchInfoBlocksTask::LAST_RUN ); // Remove the cache file if it exists. $file_name = ( new InfoBlocks() )->get_cache_file_path(); if ( file_exists( $file_name ) ) { // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged, WordPress.WP.AlternativeFunctions.unlink_unlink @unlink( $file_name ); } } /** * Check if the current request is a preview for the summaries email template. * * @since 1.8.8 * * @return bool */ private function is_preview(): bool { // Leave if the current user can't access. if ( ! wpforms_current_user_can() ) { return false; } // phpcs:disable WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized // Leave early if nonce verification failed. if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( sanitize_key( $_GET['_wpnonce'] ), Preview::PREVIEW_NONCE_NAME ) ) { return false; } // Leave early if preview is not requested. if ( ! isset( $_GET['wpforms_email_preview'], $_GET['wpforms_email_template'] ) ) { return false; } // Leave early if preview is not requested for the summaries template. if ( $_GET['wpforms_email_template'] !== 'summary' ) { return false; } // phpcs:enable WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized return true; } /** * Format entries trends for display. * * This function takes an array of entries and formats the 'trends' value for display. * * @since 1.8.8 * * @param array|int $input Input data to format. * * @return array|string */ private function format_trends_for_display( $input ) { // If input is a numeric value, format and return it. if ( is_numeric( $input ) ) { return sprintf( '%s%s%%', $input >= 0 ? '+' : '', $input ); } // Loop through entries and format 'trends' values. foreach ( $input as &$form ) { // Leave early if 'trends' index doesn't exist. if ( ! isset( $form['trends'] ) ) { continue; } // Add percent sign to trends and + sign if value greater than zero. $form['trends'] = sprintf( '%s%s%%', $form['trends'] >= 0 ? '+' : '', $form['trends'] ); } return $input; } /** * Check if trends can be displayed for the given entries. * * @since 1.8.8 * * @param array $entries The entries data. * * @return bool */ private function entries_has_trends( array $entries ): bool { // Return false if entries array is empty. if ( empty( $entries ) ) { return false; } // Check if at least one array item has the 'trends' key. foreach ( $entries as $entry ) { if ( isset( $entry['trends'] ) ) { return true; } } return false; } /** * Get icons URL. * Primarily used in the HTML version of the email template. * * @since 1.8.8 * * @return array */ private function get_icons_url(): array { $base_url = WPFORMS_PLUGIN_URL . 'assets/images/email/'; return [ 'overview' => $base_url . 'icon-overview.png', 'upward' => $base_url . 'icon-upward.png', 'downward' => $base_url . 'icon-downward.png', 'notification_block' => $base_url . 'notification-block-icon.png', 'info_block' => $base_url . 'info-block-icon.png', ]; } } Emails/InfoBlocks.php 0000644 00000012563 15174710275 0010540 0 ustar 00 <?php namespace WPForms\Emails; /** * Fetching and formatting Info Blocks for Email Summaries class. * * @since 1.5.4 */ class InfoBlocks { /** * Source of info blocks content. * * @since 1.5.4 */ const SOURCE_URL = 'https://wpformsapi.com/feeds/v1/email-summaries/%s'; /** * Get info blocks info from the cache file or remote. * * @since 1.6.4 * * @return array */ public function get_all() { $cache_file = $this->get_cache_file_path(); if ( empty( $cache_file ) || ! is_readable( $cache_file ) ) { return $this->fetch_all(); } // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents $contents = file_get_contents( $cache_file ); $contents = json_decode( $contents, true ); return $this->verify_fetched( $contents ); } /** * Fetch info blocks info from remote. * * @since 1.5.4 * * @return array */ public function fetch_all() { $info = []; $res = wp_remote_get( $this->get_remote_source(), [ 'timeout' => 10, 'user-agent' => wpforms_get_default_user_agent(), ] ); if ( is_wp_error( $res ) ) { return $info; } $body = wp_remote_retrieve_body( $res ); if ( empty( $body ) ) { return $info; } $body = json_decode( $body, true ); return $this->verify_fetched( $body ); } /** * Verify fetched blocks data. * * @since 1.5.4 * * @param array $fetched Fetched blocks data. * * @return array */ protected function verify_fetched( $fetched ) { $info = []; if ( ! \is_array( $fetched ) ) { return $info; } foreach ( $fetched as $item ) { if ( empty( $item['id'] ) ) { continue; } $id = \absint( $item['id'] ); if ( empty( $id ) ) { continue; } $info[ $id ] = $item; } return $info; } /** * Get info blocks relevant to customer's licence. * * @since 1.5.4 * * @return array */ protected function get_by_license() { $data = $this->get_all(); $filtered = []; if ( empty( $data ) || ! \is_array( $data ) ) { return $filtered; } // When there is no license, we assume it's a Lite version. // This is needed to show blocks for Lite users, as they don't have a license type. $license_type = wpforms_setting( 'type', 'lite', 'wpforms_license' ); foreach ( $data as $key => $item ) { if ( ! isset( $item['type'] ) || ! \is_array( $item['type'] ) ) { continue; } if ( ! \in_array( $license_type, $item['type'], true ) ) { continue; } $filtered[ $key ] = $item; } return $filtered; } /** * Get the first block with a valid id. * Needed to ignore blocks with invalid/missing ids. * * @since 1.5.4 * * @param array $data Blocks array. * * @return array */ protected function get_first_with_id( $data ) { if ( empty( $data ) || ! \is_array( $data ) ) { return []; } foreach ( $data as $item ) { $item_id = \absint( $item['id'] ); if ( ! empty( $item_id ) ) { return $item; } } return []; } /** * Get next info block that wasn't sent yet. * * @since 1.5.4 * * @return array */ public function get_next() { $data = $this->get_by_license(); $block = []; if ( empty( $data ) || ! \is_array( $data ) ) { return $block; } $blocks_sent = \get_option( 'wpforms_emails_infoblocks_sent' ); if ( empty( $blocks_sent ) || ! \is_array( $blocks_sent ) ) { $block = $this->get_first_with_id( $data ); } if ( empty( $block ) ) { $data = \array_diff_key( $data, \array_flip( $blocks_sent ) ); $block = $this->get_first_with_id( $data ); } return $block; } /** * Register a block as sent. * * @since 1.5.4 * * @param array $info_block Info block. */ public function register_sent( $info_block ) { $block_id = isset( $info_block['id'] ) ? absint( $info_block['id'] ) : false; if ( empty( $block_id ) ) { return; } $option_name = 'wpforms_email_summaries_info_blocks_sent'; $blocks = get_option( $option_name ); if ( empty( $blocks ) || ! is_array( $blocks ) ) { update_option( $option_name, [ $block_id ] ); return; } if ( in_array( $block_id, $blocks, true ) ) { return; } $blocks[] = $block_id; update_option( $option_name, $blocks ); } /** * Get a path of the blocks cache file. * * @since 1.6.4 * * @return string */ public function get_cache_file_path() { $upload_dir = wpforms_upload_dir(); if ( ! isset( $upload_dir['path'] ) ) { return ''; } $cache_dir = trailingslashit( $upload_dir['path'] ) . 'cache'; return wp_normalize_path( trailingslashit( $cache_dir ) . 'email-summaries.json' ); } /** * Fetch and cache blocks in a file. * * @since 1.6.4 */ public function cache_all() { $file_path = $this->get_cache_file_path(); if ( empty( $file_path ) ) { return; } $dir = dirname( $file_path ); if ( ! wp_mkdir_p( $dir ) ) { return; } wpforms_create_index_html_file( $dir ); wpforms_create_upload_dir_htaccess_file(); $info_blocks = $this->fetch_all(); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents file_put_contents( $file_path, wp_json_encode( $info_blocks ) ); } /** * Retrieve the source URL. * * @since 1.9.5 * * @return string */ protected function get_remote_source(): string { $license_type = wpforms()->is_pro() ? wpforms_get_license_type() : 'lite'; return sprintf( self::SOURCE_URL, $license_type ); } } Emails/Styler.php 0000644 00000005043 15174710275 0007764 0 ustar 00 <?php namespace WPForms\Emails; use WPForms\Vendor\TijsVerkoyen\CssToInlineStyles\CssToInlineStyles; use WPForms\Helpers\Templates; /** * Styler class inline style email templates. * * @since 1.5.4 */ class Styler { /** * Email message with no styles. * * @since 1.5.4 * * @var string */ protected $email; /** * Email style templates names. * * @since 1.5.4 * * @var array */ protected $style_templates; /** * Email style overrides. * * @since 1.5.4 * * @var array */ protected $style_overrides; /** * Email message with inline styles. * * @since 1.5.4 * * @var string */ protected $styled_email; /** * Constructor. * * @since 1.5.4 * * @param string $email Email with no styles. * @param array $style_templates Email style templates. * @param array $style_overrides Email style overrides. */ public function __construct( $email, $style_templates, $style_overrides ) { $this->email = $email; $this->style_templates = is_array( $style_templates ) ? $style_templates : []; $this->style_overrides = is_array( $style_overrides ) ? $style_overrides : []; } /** * Template style overrides. * * @since 1.5.4 * * @return array */ protected function get_style_overrides() { $defaults = Helpers::get_current_template_style_overrides(); $overrides = \wp_parse_args( $this->style_overrides, $defaults ); return \apply_filters( 'wpforms_emails_mailer_get_style_overrides', $overrides, $this ); } /** * Locate template name matching styles. * * @since 1.5.4 * * @param string $name Template file name part. * * @return string */ protected function get_styles( $name = 'style' ) { if ( ! \array_key_exists( $name, $this->style_templates ) ) { return ''; } return Templates::get_html( $this->style_templates[ $name ], $this->get_style_overrides(), true ); } /** * Final processing of the template markup. * * @since 1.5.4 */ public function process_markup() { $this->styled_email = ( new CssToInlineStyles() )->convert( $this->email, $this->get_styles() ); $queries = '<style type="text/css">' . $this->get_styles( 'queries' ) . "</style>\n</head>"; // Inject media queries, CssToInlineStyles strips them. $this->styled_email = \str_replace( '</head>', $queries, $this->styled_email ); } /** * Get an email with inline styles. * * @since 1.5.4 * * @return string */ public function get() { if ( empty( $this->styled_email ) ) { $this->process_markup(); } return $this->styled_email; } } SmartTags/SmartTag/UrlLostPassword.php 0000644 00000000722 15174710275 0014045 0 ustar 00 <?php namespace WPForms\SmartTags\SmartTag; /** * Class UrlRegister. * * @since 1.6.7 */ class UrlLostPassword extends SmartTag { /** * Get smart tag value. * * @since 1.6.7 * * @param array $form_data Form data. * @param array $fields List of fields. * @param string $entry_id Entry ID. * * @return string */ public function get_value( $form_data, $fields = [], $entry_id = '' ) { return esc_url( wp_lostpassword_url() ); } } SmartTags/SmartTag/OrderSummary.php 0000644 00000021737 15174710275 0013360 0 ustar 00 <?php namespace WPForms\SmartTags\SmartTag; /** * Class Order Summary. * * @since 1.8.7 */ class OrderSummary extends SmartTag { /** * Get smart tag value. * * @since 1.8.7 * * @param array $form_data Form data. * @param array $fields List of fields. * @param string $entry_id Entry ID. * * @return string */ public function get_value( $form_data, $fields = [], $entry_id = '' ): string { if ( empty( $fields ) && ! $entry_id ) { return ''; } if ( empty( $fields ) ) { $entry = wpforms()->obj( 'entry' )->get( $entry_id ); $fields = isset( $entry->fields ) ? (array) wpforms_decode( $entry->fields ) : []; } $fields = $this->prepare_fields( $fields, $form_data ); [ $items, $foot, $total_width ] = $this->prepare_payment_fields_data( $fields ); $preview = wpforms_render( 'fields/total/summary-preview', [ 'items' => $this->filter_items( $items ), 'foot' => $foot, 'total_width' => $total_width, 'context' => 'smart_tag', ], true ); if ( $this->context === 'email' ) { // Remove new lines for the legacy Notification template to prevent HTML markup breaks. // We remove only new lines before closing HTML tag symbol to keep new lines inside the table content. return preg_replace( '/(>$\n)/m', '>', $preview ); } return $preview; } /** * Filter items. * * @since 1.9.3 * * @param array $items Items data. * * @return array */ private function filter_items( array $items ): array { // Bail early if not in notification context. if ( $this->context !== 'notification' ) { return $items; } return array_filter( $items, function ( $item ) { // Return items that are not hidden. return empty( $item['is_hidden'] ); } ); } /** * Prepare fields data for summary preview. * Add label_hide property to fields if needed. * * @since 1.9.2 * * @param array $fields Fields data. * @param array $form_data Form data and settings. * * @return array */ private function prepare_fields( array $fields, array $form_data ): array { return array_map( function ( $field ) use ( $form_data ) { return $this->prepare_field( $field, $form_data ); }, $fields ); } /** * Prepare field data for summary preview. * * @since 1.9.3 * * @param array $field Field data. * @param array $form_data Form data and settings. * * @return array */ private function prepare_field( array $field, array $form_data ): array { $form_data_fields = $form_data['fields'] ?? []; $field_data = $form_data_fields[ $field['id'] ] ?? []; if ( isset( $field_data['label_hide'] ) ) { $field['label_hide'] = true; } if ( isset( $field_data['format'] ) && $field_data['format'] === 'hidden' ) { $field['is_hidden'] = true; } return $field; } /** * Prepare payment fields data for summary preview. * * @since 1.8.7 * * @param array $fields Fields data. * * @return array */ private function prepare_payment_fields_data( array $fields ): array { $payment_fields = wpforms_payment_fields(); $items = []; $coupon = []; $foot = []; $total = 0; $total_width = 0; foreach ( $fields as $field ) { if ( empty( $field['value'] ) || ! in_array( $field['type'], $payment_fields, true ) ) { continue; } if ( $field['type'] === 'payment-coupon' ) { $coupon = $field; continue; } $this->prepare_single_item( $field, $items, $total ); $this->prepare_multiple_item( $field, $items, $total ); } $this->prepare_coupon_item( $coupon, $foot, $total, $total_width ); $total = wpforms_format_amount( $total, true ); $foot[] = [ 'label' => __( 'Total', 'wpforms-lite' ), 'quantity' => '', 'amount' => $total, 'class' => 'wpforms-order-summary-preview-total', ]; // Add two extra characters units to accommodate symbols that can be wider than one character (e.g. “€”), // and to normalize the ch-unit width discrepancy between Windows and Unix-based operating systems. $total_width = max( $total_width, mb_strlen( html_entity_decode( $total, ENT_COMPAT, 'UTF-8' ) ) + 2 ); return [ $items, $foot, $total_width ]; } /** * Prepare single item for summary preview. * * @since 1.8.7 * * @param array $field Field data. * @param array $items Summary items. * @param string $total Form total. */ private function prepare_single_item( array $field, array &$items, string &$total ) { // Single value. if ( ! in_array( $field['type'], [ 'payment-single', 'payment-multiple', 'payment-select' ], true ) ) { return; } $quantity = $this->get_payment_field_quantity( $field ); if ( ! $quantity ) { return; } $value_raw = $field['value_raw'] ?? ''; /* translators: %s - item number. */ $value_choice = ! empty( $field['value_choice'] ) ? $field['value_choice'] : sprintf( esc_html__( 'Item %s', 'wpforms-lite' ), $value_raw ); $label = ! empty( $value_raw ) ? $field['name'] . ' - ' . $value_choice : $field['name']; $amount = $field['amount_raw'] * $quantity; $items[] = [ 'label' => ! empty( $field['label_hide'] ) ? $value_choice : $label, 'quantity' => $quantity, 'amount' => wpforms_format_amount( $amount, true ), 'is_hidden' => ! empty( $field['is_hidden'] ), ]; $total += $amount; } /** * Prepare multiple item for summary preview. * * @since 1.8.7 * * @param array $field Field data. * @param array $items Summary items. * @param string $total Form total. */ private function prepare_multiple_item( array $field, array &$items, string &$total ) { if ( $field['type'] !== 'payment-checkbox' ) { return; } $quantity = $this->get_payment_field_quantity( $field ); if ( ! $quantity ) { return; } // Multiple values. $value_choices = explode( "\n", $field['value'] ); foreach ( $value_choices as $key => $value_choice ) { $choice_data = explode( ' - ', $value_choice ); $labels = $this->get_multiple_item_labels( $choice_data, $field, $key ); $items[] = [ 'label' => ! empty( $field['label_hide'] ) ? implode( ' - ', $labels ) : $field['name'] . ' - ' . implode( ' - ', $labels ), 'quantity' => $quantity, 'amount' => end( $choice_data ), ]; } $total += $field['amount_raw']; } /** * Get multiple item labels. * * @since 1.9.3 * * @param array $choice_data Choice data. * @param array $field Field data. * @param int $key Choice key. * * @return array */ private function get_multiple_item_labels( array $choice_data, array $field, int $key ): array { $labels = array_slice( $choice_data, 0, -1 ); if ( ! empty( $labels ) ) { return $labels; } $raw_values = explode( ',', $field['value_raw'] ); /* translators: %s - item number. */ return [ sprintf( esc_html__( 'Item %s', 'wpforms-lite' ), $raw_values[ $key ] ?? '' ) ]; } /** * Prepare coupon item for summary preview. * * @since 1.8.7 * * @param array $coupon Coupon data. * @param array $foot Summary footer. * @param string $total Form total. * @param string $total_width Total width. */ private function prepare_coupon_item( array $coupon, array &$foot, string &$total, string &$total_width ) { if ( empty( $coupon ) ) { return; } $foot[] = [ 'label' => __( 'Subtotal', 'wpforms-lite' ), 'quantity' => '', 'amount' => wpforms_format_amount( $total, true ), 'class' => 'wpforms-order-summary-preview-subtotal', ]; $coupon_label = sprintf( /* translators: %s - Coupon value. */ __( 'Coupon (%s)', 'wpforms-lite' ), $coupon['value'] ); $coupon_amount = $this->get_coupon_amount( $coupon ); $foot[] = [ 'label' => $coupon_label, 'quantity' => '', 'amount' => $coupon_amount, 'class' => 'wpforms-order-summary-preview-coupon-total', ]; // Coupon value saved as negative. $total += $coupon['amount_raw']; $total_width = strlen( html_entity_decode( $coupon_amount, ENT_COMPAT, 'UTF-8' ) ); } /** * Get coupon amount. * * @since 1.8.7 * * @param array $coupon Coupon data. * * @return string Formatted coupon amount. */ private function get_coupon_amount( array $coupon ): string { // Coupon amount saved as negative, so we need to format it nicely. $coupon_amount = '- ' . wpforms_format_amount( abs( $coupon['amount_raw'] ), true ); /** * Allow to filter order summary coupon amount. * * @since 1.8.7 * * @param string $coupon_amount Coupon amount. * @param array $coupon Coupon data. */ return apply_filters( 'wpforms_smart_tags_smart_tag_order_summary_coupon_amount', $coupon_amount, $coupon ); } /** * Get payment field quantity. * * @since 1.8.7 * * @param array $field Field data. * * @return int */ private function get_payment_field_quantity( array $field ): int { // phpcs:ignore WPForms.Formatting.EmptyLineBeforeReturn.RemoveEmptyLineBeforeReturnStatement return isset( $field['quantity'] ) ? (int) $field['quantity'] : 1; } } SmartTags/SmartTag/UrlReferer.php 0000644 00000001736 15174710275 0013001 0 ustar 00 <?php namespace WPForms\SmartTags\SmartTag; /** * Class UrlReferer. * * @since 1.6.7 */ class UrlReferer extends SmartTag { /** * Get smart tag value. * * @since 1.6.7 * * @param array $form_data Form data. * @param array $fields List of fields. * @param string $entry_id Entry ID. * * @return string */ public function get_value( $form_data, $fields = [], $entry_id = '' ): string { $referer = $this->get_meta( $entry_id, 'url_referer' ); if ( ! empty( $referer ) ) { return $this->context === 'confirmation_redirect' ? urldecode( $referer ) : esc_url( urldecode( $referer ) ); } $process = wpforms()->obj( 'process' ); if ( $process && ! empty( $process->form_data['entry_meta']['url_referer'] ) ) { return esc_url( urldecode( $process->form_data['entry_meta']['url_referer'] ) ); } if ( wp_doing_ajax() ) { return ''; } $referer = urldecode( (string) wp_get_raw_referer() ); return esc_url( $referer ); } } SmartTags/SmartTag/FormId.php 0000644 00000000735 15174710275 0012102 0 ustar 00 <?php namespace WPForms\SmartTags\SmartTag; /** * Class FormId. * * @since 1.6.7 */ class FormId extends SmartTag { /** * Get smart tag value. * * @since 1.6.7 * * @param array $form_data Form data. * @param array $fields List of fields. * @param string $entry_id Entry ID. * * @return int */ public function get_value( $form_data, $fields = [], $entry_id = '' ) { return ! empty( $form_data['id'] ) ? absint( $form_data['id'] ) : 0; } } SmartTags/SmartTag/UserFullName.php 0000644 00000001262 15174710275 0013260 0 ustar 00 <?php namespace WPForms\SmartTags\SmartTag; use WP_User; /** * Class UserFullName. * * @since 1.6.7 */ class UserFullName extends SmartTag { /** * Get smart tag value. * * @since 1.6.7 * * @param array $form_data Form data. * @param array $fields List of fields. * @param string $entry_id Entry ID. * * @return string */ public function get_value( $form_data, $fields = [], $entry_id = '' ) { $current_user = $this->get_user( $entry_id ); if ( ! $current_user instanceof WP_User ) { return ''; } return $current_user->exists() ? esc_html( wp_strip_all_tags( $current_user->user_firstname . ' ' . $current_user->user_lastname ) ) : ''; } } SmartTags/SmartTag/UserIp.php 0000644 00000001443 15174710275 0012126 0 ustar 00 <?php namespace WPForms\SmartTags\SmartTag; /** * Class UserIp. * * @since 1.6.7 */ class UserIp extends SmartTag { /** * Get smart tag value. * * @since 1.6.7 * @since 1.8.2 Return empty string if IP collection is disabled. Return entry IP address if entry ID is provided. * * @param array $form_data Form data. * @param array $fields List of fields. * @param string $entry_id Entry ID. * * @return string */ public function get_value( $form_data, $fields = [], $entry_id = '' ) { if ( ! wpforms_is_collecting_ip_allowed() ) { return ''; } if ( ! $entry_id ) { return esc_html( wpforms_get_ip() ); } $entry_obj = wpforms()->obj( 'entry' ); $entry = $entry_obj ? $entry_obj->get( $entry_id ) : null; return $entry->ip_address ?? ''; } } SmartTags/SmartTag/UniqueValue.php 0000644 00000003622 15174710275 0013163 0 ustar 00 <?php namespace WPForms\SmartTags\SmartTag; /** * Class UniqueValue. * * @since 1.7.5 */ class UniqueValue extends SmartTag { /** * Default length of the unique value to be generated. * * @since 1.7.5 * * @var int */ const DEFAULT_LENGTH = 16; /** * Default format of the unique value to be generated. * * @since 1.7.5 * * @var string */ const DEFAULT_FORMAT = 'alphanumeric'; /** * Get smart tag value. * * @since 1.7.5 * * @param array $form_data Form data. * @param array $fields List of fields. * @param string $entry_id Entry ID. * * @return string */ public function get_value( $form_data, $fields = [], $entry_id = '' ) { $length = self::DEFAULT_LENGTH; $format = self::DEFAULT_FORMAT; $attributes = $this->get_attributes(); if ( array_key_exists( 'length', $attributes ) ) { $length = max( $length, absint( $attributes['length'] ) ); } if ( array_key_exists( 'format', $attributes ) && ! empty( $attributes['format'] ) ) { $format = $attributes['format']; } return $this->generate_string( $length, $format ); } /** * Generates a random string in defined format. * * @since 1.7.5 * * @param int $length Optional. The length of string to generate. * @param string $format The format of string to generate. Accepts 'alphanumeric', * 'numeric', and 'alpha'. Default 'alphanumeric'. * * @return string */ private function generate_string( $length = 16, $format = 'alphanumeric' ) { $alpha = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; $numbers = '0123456789'; switch ( strtolower( $format ) ) { case 'numeric': $chars = $numbers; break; case 'alpha': $chars = $alpha; break; default: $chars = $alpha . $numbers; break; } $chars = str_pad( $chars, $length, $chars ); return substr( str_shuffle( $chars ), 0, $length ); } } SmartTags/SmartTag/AdminEmail.php 0000644 00000000731 15174710275 0012716 0 ustar 00 <?php namespace WPForms\SmartTags\SmartTag; /** * Class AdminEmail. * * @since 1.6.7 */ class AdminEmail extends SmartTag { /** * Get smart tag value. * * @since 1.6.7 * * @param array $form_data Form data. * @param array $fields List of fields. * @param string $entry_id Entry ID. * * @return string */ public function get_value( $form_data, $fields = [], $entry_id = '' ) { return sanitize_email( get_option( 'admin_email' ) ); } } SmartTags/SmartTag/UserEmail.php 0000644 00000001164 15174710275 0012605 0 ustar 00 <?php namespace WPForms\SmartTags\SmartTag; use WP_User; /** * Class UserEmail. * * @since 1.6.7 */ class UserEmail extends SmartTag { /** * Get smart tag value. * * @since 1.6.7 * * @param array $form_data Form data. * @param array $fields List of fields. * @param string $entry_id Entry ID. * * @return string */ public function get_value( $form_data, $fields = [], $entry_id = '' ) { $current_user = $this->get_user( $entry_id ); if ( ! $current_user instanceof WP_User ) { return ''; } return $current_user->exists() ? sanitize_email( $current_user->user_email ) : ''; } } SmartTags/SmartTag/FormName.php 0000644 00000001152 15174710275 0012420 0 ustar 00 <?php namespace WPForms\SmartTags\SmartTag; /** * Class FormName. * * @since 1.6.7 */ class FormName extends SmartTag { /** * Get smart tag value. * * @since 1.6.7 * * @param array $form_data Form data. * @param array $fields List of fields. * @param string $entry_id Entry ID. * * @return string */ public function get_value( $form_data, $fields = [], $entry_id = '' ) { if ( ! isset( $form_data['settings']['form_title'] ) || $form_data['settings']['form_title'] === '' ) { return ''; } return esc_html( wp_strip_all_tags( $form_data['settings']['form_title'] ) ); } } SmartTags/SmartTag/PageUrl.php 0000644 00000001540 15174710275 0012254 0 ustar 00 <?php namespace WPForms\SmartTags\SmartTag; /** * Class PageUrl. * * @since 1.6.7 */ class PageUrl extends SmartTag { /** * Get smart tag value. * * @since 1.6.7 * * @param array $form_data Form data. * @param array $fields List of fields. * @param string $entry_id Entry ID. * * @return string */ public function get_value( $form_data, $fields = [], $entry_id = '' ): string { $page_url = $this->get_meta( $entry_id, 'page_url' ); if ( ! empty( $page_url ) ) { return esc_url( urldecode( $page_url ) ); } // phpcs:disable WordPress.Security.NonceVerification $page_url = ! empty( $_POST['page_url'] ) ? esc_url_raw( wp_unslash( $_POST['page_url'] ) ) : wpforms_current_url(); $page_url = urldecode( $page_url ); // phpcs:enable WordPress.Security.NonceVerification return esc_url( $page_url ); } } SmartTags/SmartTag/UserLastName.php 0000644 00000001214 15174710275 0013256 0 ustar 00 <?php namespace WPForms\SmartTags\SmartTag; use WP_User; /** * Class UserLastName. * * @since 1.6.7 */ class UserLastName extends SmartTag { /** * Get smart tag value. * * @since 1.6.7 * * @param array $form_data Form data. * @param array $fields List of fields. * @param string $entry_id Entry ID. * * @return string */ public function get_value( $form_data, $fields = [], $entry_id = '' ) { $current_user = $this->get_user( $entry_id ); if ( ! $current_user instanceof WP_User ) { return ''; } return $current_user->exists() ? esc_html( wp_strip_all_tags( $current_user->user_lastname ) ) : ''; } } SmartTags/SmartTag/UrlLogin.php 0000644 00000000701 15174710275 0012446 0 ustar 00 <?php namespace WPForms\SmartTags\SmartTag; /** * Class UrlLogin. * * @since 1.6.7 */ class UrlLogin extends SmartTag { /** * Get smart tag value. * * @since 1.6.7 * * @param array $form_data Form data. * @param array $fields List of fields. * @param string $entry_id Entry ID. * * @return string */ public function get_value( $form_data, $fields = [], $entry_id = '' ) { return esc_url( wp_login_url() ); } } SmartTags/SmartTag/FieldHtmlId.php 0000644 00000002705 15174710275 0013046 0 ustar 00 <?php namespace WPForms\SmartTags\SmartTag; /** * Class FieldHtmlId. * * @since 1.6.7 */ class FieldHtmlId extends SmartTag { /** * Get smart tag value. * * @since 1.6.7 * * @param array $form_data Form data. * @param array $fields List of fields. * @param string $entry_id Entry ID. * * @return string */ public function get_value( $form_data, $fields = [], $entry_id = '' ) { $attributes = $this->get_attributes(); if ( ! isset( $attributes['field_html_id'] ) || ! is_numeric( $attributes['field_html_id'] ) || $attributes['field_html_id'] < 0 ) { return ''; } $field_id = absint( $attributes['field_html_id'] ); if ( empty( $fields[ $field_id ] ) ) { return ''; } if ( ! isset( $fields[ $field_id ]['value'] ) || (string) $fields[ $field_id ]['value'] === '' ) { return '<em>' . esc_html__( '(empty)', 'wpforms-lite' ) . '</em>'; } $value = $this->get_formatted_field_value( (int) $field_id, (array) $fields, 'value' ); $value = wp_kses_post( wp_unslash( $value ) ); /** * Modify value for the {field_html_id="123"} tag. * * @since 1.4.0 * * @param string $value Smart tag value. * @param array $field The field. * @param array $form_data Processed form settings/data, prepared to be used later. * @param string $context Context usage. */ return (string) apply_filters( 'wpforms_html_field_value', $value, $fields[ $field_id ], $form_data, 'smart-tag' ); } } SmartTags/SmartTag/UrlRegister.php 0000644 00000000716 15174710275 0013170 0 ustar 00 <?php namespace WPForms\SmartTags\SmartTag; /** * Class UrlRegister. * * @since 1.6.7 */ class UrlRegister extends SmartTag { /** * Get smart tag value. * * @since 1.6.7 * * @param array $form_data Form data. * @param array $fields List of fields. * @param string $entry_id Entry ID. * * @return string */ public function get_value( $form_data, $fields = [], $entry_id = '' ) { return esc_url( wp_registration_url() ); } } SmartTags/SmartTag/QueryVar.php 0000644 00000003457 15174710275 0012504 0 ustar 00 <?php namespace WPForms\SmartTags\SmartTag; /** * Class QueryVar. * * @since 1.6.7 */ class QueryVar extends SmartTag { /** * Get smart tag value. * * @since 1.6.7 * @since 1.7.6 Added support for ajax submissions. * * @param array $form_data Form data. * @param array $fields List of fields. * @param string $entry_id Entry ID. * * @return string */ public function get_value( $form_data, $fields = [], $entry_id = '' ) { $attributes = $this->get_attributes(); if ( empty( $attributes['key'] ) ) { return ''; } // phpcs:disable WordPress.Security.NonceVerification.Recommended if ( ! empty( $_GET[ $attributes['key'] ] ) ) { return esc_html( sanitize_text_field( wp_unslash( $_GET[ $attributes['key'] ] ) ) ); } // phpcs:enable WordPress.Security.NonceVerification.Recommended $page_url = $this->get_page_url( $entry_id ); if ( empty( $page_url ) ) { return ''; } $query = wp_parse_url( esc_url_raw( wp_unslash( $page_url ) ), PHP_URL_QUERY ); // phpcs:enable WordPress.Security.NonceVerification.Missing if ( ! $query ) { return ''; } parse_str( $query, $results ); return ! empty( $results[ $attributes['key'] ] ) ? esc_html( sanitize_text_field( wp_unslash( $results[ $attributes['key'] ] ) ) ) : ''; } /** * Get page URL. * * @since 1.9.4 * * @param string $entry_id Entry ID. * * @return string */ private function get_page_url( $entry_id ): string { // phpcs:disable WordPress.Security.NonceVerification.Missing if ( ! empty( $_POST['page_url'] ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput return (string) $_POST['page_url']; // It sanitized in the get_value method. } // phpcs:enable WordPress.Security.NonceVerification.Missing return $this->get_meta( $entry_id, 'page_url' ); } } SmartTags/SmartTag/SmartTag.php 0000644 00000020341 15174710275 0012437 0 ustar 00 <?php namespace WPForms\SmartTags\SmartTag; use WP_User; /** * Class SmartTag. * * @since 1.6.7 */ abstract class SmartTag { /** * Full smart tag. * For example, {smart_tag attr="1" attr2="true"}. * * @since 1.6.7 * * @var string */ protected $smart_tag; /** * Context. * * @since 1.8.7 * * @var string */ public $context; /** * Context data. * * @since 1.9.9.2 * * @var array */ public $context_data; /** * List of attributes. * * @since 1.6.7 * * @var array */ protected $attributes = []; /** * SmartTag constructor. * * @since 1.6.7 * @since 1.8.7 Added $context parameter. * * @param string $smart_tag Full smart tag. * @param string $context Context. * @param array $context_data Context data. */ public function __construct( $smart_tag, $context = '', array $context_data = [] ) { $this->smart_tag = $smart_tag; $this->context = $context; $this->context_data = $context_data; } /** * Get smart tag value. * * @since 1.6.7 * * @param array $form_data Form data. * @param array $fields List of fields. * @param string $entry_id Entry ID. * * @return string */ abstract public function get_value( $form_data, $fields = [], $entry_id = '' ); /** * Get a list of smart tag attributes. * * @since 1.6.7 * * @return array */ public function get_attributes() { if ( ! empty( $this->attributes ) ) { return $this->attributes; } /** * (\w+) an attribute name and also the first capturing group. Lowercase or uppercase letters, digits, underscore. * = the equal sign. * (["\']) single or double quote, the second capturing group. * (.+?) an attribute value within the quotes, and also the third capturing group. Any number of any characters except the new line. Lazy mode - match as few characters as possible to allow multiple attributes on one line. * \2 - repeat the second capturing group. */ preg_match_all( '/(\w+)=(["\'])(.+?)\2/', $this->smart_tag, $attributes ); $this->attributes = array_combine( $attributes[1], $attributes[3] ); return $this->attributes; } /** * Get current user. * * @since 1.8.7 * * @param string|int $entry_id Entry ID. * * @return WP_User|string */ public function get_user( $entry_id ) { $user = $this->get_entry_user( $entry_id ); if ( ! empty( $user ) ) { return $user; } return ! wpforms_doing_scheduled_action() && is_user_logged_in() ? wp_get_current_user() : ''; } /** * Get user from the entry. * * @since 1.8.8 * * @param string|int $entry_id Entry ID. * * @return WP_User|string */ private function get_entry_user( $entry_id ) { $entry_user_id = $this->get_entry_user_id( $entry_id ); if ( empty( $entry_user_id ) ) { return ''; } $user = get_user_by( 'id', $entry_user_id ); return $user instanceof WP_User ? $user : ''; } /** * Retrieve user ID from entry meta or AS task. * * @since 1.9.4 * * @param int|string $entry_id Entry ID. * * @return int */ private function get_entry_user_id( $entry_id ): int { if ( empty( $entry_id ) ) { return (int) $this->get_meta( 0, 'user_id' ); } $entry = wpforms()->obj( 'entry' ); if ( empty( $entry ) ) { return 0; } $entry_data = $entry->get( $entry_id ); return $entry_data && isset( $entry_data->user_id ) ? (int) $entry_data->user_id : 0; } /** * Get author. * * @since 1.8.7 * * @param int $post_id Submitted post ID. * * @return WP_User|false WP_User object on success, false on failure. */ public function get_author( $post_id ) { $author_id = get_post_field( 'post_author', $post_id ); return get_user_by( 'id', $author_id ); } /** * Get author property. * * @since 1.8.8 * * @param int|string $entry_id Entry ID. * @param string $meta_key User property. * * @return string */ protected function get_author_meta( $entry_id, string $meta_key ): string { $page_id = $this->get_meta( $entry_id, 'page_id' ); if ( empty( $page_id ) ) { return ''; } $author = $this->get_author( $page_id ); if ( ! $author ) { return ''; } return $author->{$meta_key} ?? ''; } /** * Get entry meta. * * @since 1.8.7 * * @param string|int $entry_id Entry ID. * @param string $meta_key Meta key. * * @return string Meta value. */ public function get_meta( $entry_id, string $meta_key ): string { $meta_data = ''; if ( ! empty( $entry_id ) ) { $entry_meta = wpforms()->obj( 'entry_meta' ); if ( $entry_meta ) { $meta = $entry_meta->get_meta( [ 'entry_id' => $entry_id, 'type' => $meta_key, 'number' => 1, ] ); $meta_data = isset( $meta[0]->data ) ? (string) $meta[0]->data : ''; } } /** * Allow modifying the entry meta-value. * * @since 1.9.4 * * @param string $meta_data Meta value. * @param string $meta_key Meta key. * @param string|int $entry_id Entry ID. * @param SmartTag $smart_tag Smart tag object. * * @return string */ return (string) apply_filters( 'wpforms_smart_tags_smart_tag_get_meta_value', $meta_data, $meta_key, $entry_id, $this ); //phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName } /** * Get a formatted field value. * * @since 1.8.9 * * @param int $field_id Field ID. * @param array $fields List of fields. * @param string $field_key Field key to get value from. * @param array $form_data Form data. * * @return string */ protected function get_formatted_field_value( int $field_id, array $fields, string $field_key, array $form_data = [] ): string { $value = $fields[ $field_id ][ $field_key ] ?? ''; /** * Allow modifying the formatted field value. * * @since 1.9.0 * * @param string $value Field value. * @param int $field_id Field ID. * @param array $fields List of fields. * @param string $field_key Field key to get value from. * @param array $form_data Form data. * * @return string */ $value = (string) apply_filters( 'wpforms_smart_tags_formatted_field_value', $value, $field_id, $fields, $field_key, $form_data ); //phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName if ( ! wpforms_is_repeated_field( $field_id, $fields ) ) { return $value; } return $this->get_repeated_field_value( $value, $field_id, $fields, $field_key ); } /** * Get repeated fields value. * * @since 1.8.9 * * @param string $value Field value. * @param int $field_id Field ID. * @param array $fields List of fields. * @param string $field_key Field key to get value from. * * @return string */ private function get_repeated_field_value( string $value, int $field_id, array $fields, string $field_key ): string { $comma_separated_contexts = [ 'notification-send-to-email', 'notification-carboncopy' ]; $prefix = $field_id . '_'; $separator = in_array( $this->context, $comma_separated_contexts, true ) ? ',' : "\n"; foreach ( $fields as $key => $field ) { if ( strpos( $key, $prefix ) !== 0 ) { continue; } if ( ! isset( $field[ $field_key ] ) ) { continue; } $value .= $separator . $field[ $field_key ]; } return $value; } /** * Check if a user has capabilities to get the smart tag value. * * @since 1.9.9.2 * * @return bool */ protected function has_cap(): bool { switch ( $this->context ) { case 'notification': case 'notification-carboncopy': case 'notification-from': case 'notification-reply-to': case 'email': $cap = $this->recipient_has_cap(); break; case 'confirmation': $cap = current_user_can( 'manage_options' ); break; default: $cap = true; } return $cap; } /** * Check if the notification recipient is allowed to view the author email. * * @since 1.9.9.2 * * @return bool */ private function recipient_has_cap(): bool { $emails = $this->context_data['to_email'] ?? []; $emails = array_unique( array_filter( array_map( 'trim', $emails ) ) ); if ( ! $emails ) { return false; } return array_reduce( $emails, static function ( $carry, $email ) { $user = get_user_by( 'email', $email ); return $carry && $user && user_can( $user, 'manage_options' ); }, true ); } } SmartTags/SmartTag/FieldId.php 0000644 00000002534 15174710275 0012221 0 ustar 00 <?php namespace WPForms\SmartTags\SmartTag; /** * Class FieldId. * * @since 1.6.7 */ class FieldId extends SmartTag { /** * Get smart tag value. * * @since 1.6.7 * * @param array $form_data Form data. * @param array $fields List of fields. * @param string $entry_id Entry ID. * * @return string */ public function get_value( $form_data, $fields = [], $entry_id = '' ) { $attributes = $this->get_attributes(); if ( ! isset( $attributes['field_id'] ) || $attributes['field_id'] === '' ) { return ''; } $field_parts = explode( '|', $attributes['field_id'] ); $field_id = $field_parts[0]; if ( ! isset( $fields[ $field_id ] ) || $fields[ $field_id ] === '' ) { return ''; } $field_key = ! empty( $field_parts[1] ) ? sanitize_key( $field_parts[1] ) : 'value'; $value = $this->get_formatted_field_value( (int) $field_id, (array) $fields, $field_key, $form_data ); $value = wp_kses_post( wp_unslash( $value ) ); /** * Modify value for the `field_id` smart tag. * * @since 1.5.3 * @deprecated 1.6.7 * * @see This filter is documented in wp-includes/plugin.php * * @param string $value Smart tag value. */ return (string) apply_filters_deprecated( 'wpforms_field_smart_tag_value', [ $value ], '1.6.7', 'wpforms_smarttags_process_field_id_value' ); } } SmartTags/SmartTag/UserMeta.php 0000644 00000001403 15174710275 0012440 0 ustar 00 <?php namespace WPForms\SmartTags\SmartTag; use WP_User; /** * Class UserMeta. * * @since 1.6.7 */ class UserMeta extends SmartTag { /** * Get smart tag value. * * @since 1.6.7 * * @param array $form_data Form data. * @param array $fields List of fields. * @param string $entry_id Entry ID. * * @return string */ public function get_value( $form_data, $fields = [], $entry_id = '' ) { $attributes = $this->get_attributes(); if ( empty( $attributes['key'] ) ) { return ''; } $current_user = $this->get_user( $entry_id ); if ( ! $current_user instanceof WP_User ) { return ''; } return wp_kses_post( get_user_meta( $current_user->ID, sanitize_text_field( $attributes['key'] ), true ) ); } } SmartTags/SmartTag/Date.php 0000644 00000001274 15174710275 0011576 0 ustar 00 <?php namespace WPForms\SmartTags\SmartTag; /** * Class Date. * * @since 1.6.7 */ class Date extends SmartTag { /** * Get smart tag value. * * @since 1.6.7 * * @param array $form_data Form data. * @param array $fields List of fields. * @param string $entry_id Entry ID. * * @return string */ public function get_value( $form_data, $fields = [], $entry_id = '' ) { $attributes = $this->get_attributes(); if ( empty( $attributes['format'] ) ) { return wpforms_date_format( time(), '', true ); } $format = strtolower( $attributes['format'] ) === 'timestamp' ? 'U' : $attributes['format']; return wpforms_datetime_format( time(), $format, true ); } } SmartTags/SmartTag/UrlLogout.php 0000644 00000000704 15174710275 0012652 0 ustar 00 <?php namespace WPForms\SmartTags\SmartTag; /** * Class UrlLogout. * * @since 1.6.7 */ class UrlLogout extends SmartTag { /** * Get smart tag value. * * @since 1.6.7 * * @param array $form_data Form data. * @param array $fields List of fields. * @param string $entry_id Entry ID. * * @return string */ public function get_value( $form_data, $fields = [], $entry_id = '' ) { return esc_url( wp_logout_url() ); } } SmartTags/SmartTag/UserId.php 0000644 00000001130 15174710275 0012103 0 ustar 00 <?php namespace WPForms\SmartTags\SmartTag; use WP_User; /** * Class UserId. * * @since 1.6.7 */ class UserId extends SmartTag { /** * Get smart tag value. * * @since 1.6.7 * * @param array $form_data Form data. * @param array $fields List of fields. * @param string $entry_id Entry ID. * * @return int|string */ public function get_value( $form_data, $fields = [], $entry_id = '' ) { $current_user = $this->get_user( $entry_id ); if ( ! $current_user instanceof WP_User ) { return ''; } return $current_user->exists() ? $current_user->ID : ''; } } SmartTags/SmartTag/SiteName.php 0000644 00000000746 15174710275 0012431 0 ustar 00 <?php namespace WPForms\SmartTags\SmartTag; /** * Class SiteName. * * @since 1.8.3 */ class SiteName extends SmartTag { /** * Get smart tag value. * * @since 1.8.3 * * @param array $form_data Form data. * @param array $fields List of fields. * @param string $entry_id Entry ID. * * @return string */ public function get_value( $form_data, $fields = [], $entry_id = '' ) { return wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES ); } } SmartTags/SmartTag/FieldValueId.php 0000644 00000002044 15174710275 0013212 0 ustar 00 <?php namespace WPForms\SmartTags\SmartTag; /** * Class FieldValueId. * * @since 1.6.7 */ class FieldValueId extends SmartTag { /** * Get smart tag value. * * @since 1.6.7 * * @param array $form_data Form data. * @param array $fields List of fields. * @param string $entry_id Entry ID. * * @return string */ public function get_value( $form_data, $fields = [], $entry_id = '' ) { $attributes = $this->get_attributes(); if ( ! isset( $attributes['field_value_id'] ) || $attributes['field_value_id'] === '' ) { return ''; } $field_id = $attributes['field_value_id']; if ( ! isset( $fields[ $field_id ] ) || $fields[ $field_id ] === '' ) { return ''; } $field_key = isset( $fields[ $field_id ]['value_raw'] ) && ! is_array( $fields[ $field_id ]['value_raw'] ) && (string) $fields[ $field_id ]['value_raw'] !== '' ? 'value_raw' : 'value'; $value = $this->get_formatted_field_value( (int) $field_id, (array) $fields, $field_key, $form_data ); return wp_kses_post( wp_unslash( $value ) ); } } SmartTags/SmartTag/PageId.php 0000644 00000001612 15174710275 0012046 0 ustar 00 <?php namespace WPForms\SmartTags\SmartTag; /** * Class PageId. * * @since 1.6.7 */ class PageId extends SmartTag { /** * Get smart tag value. * * @since 1.6.7 * * @param array $form_data Form data. * @param array $fields List of fields. * @param string $entry_id Entry ID. * * @return int|string */ public function get_value( $form_data, $fields = [], $entry_id = '' ) { $page_id = $this->get_meta( $entry_id, 'page_id' ); if ( ! empty( $page_id ) ) { return absint( $page_id ); } // phpcs:disable WordPress.Security.NonceVerification.Missing if ( ! empty( $_POST['page_id'] ) ) { return absint( $_POST['page_id'] ); } // phpcs:enable WordPress.Security.NonceVerification.Missing // We should not return any value on pages that don't belong to the page type. return is_singular() || ( is_front_page() && is_page() ) ? get_the_ID() : ''; } } SmartTags/SmartTag/UserFirstName.php 0000644 00000001217 15174710275 0013445 0 ustar 00 <?php namespace WPForms\SmartTags\SmartTag; use WP_User; /** * Class UserFirstName. * * @since 1.6.7 */ class UserFirstName extends SmartTag { /** * Get smart tag value. * * @since 1.6.7 * * @param array $form_data Form data. * @param array $fields List of fields. * @param string $entry_id Entry ID. * * @return string */ public function get_value( $form_data, $fields = [], $entry_id = '' ) { $current_user = $this->get_user( $entry_id ); if ( ! $current_user instanceof WP_User ) { return ''; } return $current_user->exists() ? esc_html( wp_strip_all_tags( $current_user->user_firstname ) ) : ''; } } SmartTags/SmartTag/UserDisplay.php 0000644 00000001211 15174710275 0013154 0 ustar 00 <?php namespace WPForms\SmartTags\SmartTag; use WP_User; /** * Class UserDisplay. * * @since 1.6.7 */ class UserDisplay extends SmartTag { /** * Get smart tag value. * * @since 1.6.7 * * @param array $form_data Form data. * @param array $fields List of fields. * @param string $entry_id Entry ID. * * @return string */ public function get_value( $form_data, $fields = [], $entry_id = '' ) { $current_user = $this->get_user( $entry_id ); if ( ! $current_user instanceof WP_User ) { return ''; } return $current_user->exists() ? esc_html( wp_strip_all_tags( $current_user->display_name ) ) : ''; } } SmartTags/SmartTag/AuthorEmail.php 0000644 00000001670 15174710275 0013133 0 ustar 00 <?php namespace WPForms\SmartTags\SmartTag; /** * Class AuthorEmail. * * @since 1.6.7 */ class AuthorEmail extends SmartTag { /** * Get smart tag value. * * @since 1.6.7 * * @param array $form_data Form data. * @param array $fields List of fields. * @param string $entry_id Entry ID. * * @return string */ public function get_value( $form_data, $fields = [], $entry_id = '' ): string { $author_email = $this->get_author_meta( $entry_id, 'user_email' ); if ( empty( $author_email ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing $page_id = isset( $_POST['page_id'] ) ? absint( $_POST['page_id'] ) : 0; $author_id = $page_id ? (int) get_post_field( 'post_author', $page_id ) : get_current_user_id(); $author_email = get_the_author_meta( 'user_email', $author_id ); } $author_email = $this->has_cap() ? $author_email : ''; return sanitize_email( $author_email ); } } SmartTags/SmartTag/PageTitle.php 0000644 00000003777 15174710275 0012611 0 ustar 00 <?php namespace WPForms\SmartTags\SmartTag; /** * Class PageTitle. * * @since 1.6.7 */ class PageTitle extends SmartTag { /** * Get smart tag value. * * @since 1.6.7 * * @param array $form_data Form data. * @param array $fields List of fields. * @param string $entry_id Entry ID. * * @return string */ public function get_value( $form_data, $fields = [], $entry_id = '' ) { $page_title = $this->get_meta( $entry_id, 'page_title' ); if ( ! empty( $page_title ) ) { return wp_kses_post( $page_title ); } // phpcs:disable WordPress.Security.NonceVerification.Missing if ( ! empty( $_POST['page_title'] ) && ! is_array( $_POST['page_title'] ) ) { return wp_kses_post( wp_unslash( $_POST['page_title'] ) ); } // phpcs:enable WordPress.Security.NonceVerification.Missing if ( is_front_page() ) { return wp_kses_post( is_page() ? get_the_title( get_the_ID() ) : get_bloginfo( 'name' ) ); } return wp_kses_post( $this->get_wp_title() ); } /** * Retrieve a page title based on `wp_title()`. * * @since 1.7.9 * * @return string */ private function get_wp_title() { global $wp_filter; // Back up all callbacks. $callbacks = isset( $wp_filter['wp_title']->callbacks ) ? $wp_filter['wp_title']->callbacks : []; if ( ! empty( $callbacks ) ) { // Unset all callbacks. $wp_filter['wp_title']->callbacks = []; } /* * In most cases `wp_title()` returns the value we're going to use, except: * - on static front page (we can use page title as a fallback), * - on standard front page with the latest post (we can use the site name as a fallback). */ $title = trim( wp_title( '', false ) ); // Run through the default transformations WordPress does on this hook. $title = wptexturize( $title ); $title = convert_chars( $title ); $title = esc_html( $title ); $title = capital_P_dangit( $title ); if ( ! empty( $callbacks ) ) { // Restore all callbacks. $wp_filter['wp_title']->callbacks = $callbacks; } return $title; } } SmartTags/SmartTag/AuthorDisplay.php 0000644 00000002007 15174710275 0013504 0 ustar 00 <?php namespace WPForms\SmartTags\SmartTag; /** * Class AuthorDisplay. * * @since 1.6.7 */ class AuthorDisplay extends SmartTag { /** * Get smart tag value. * * @since 1.6.7 * * @param array $form_data Form data. * @param array $fields List of fields. * @param string $entry_id Entry ID. * * @return string */ public function get_value( $form_data, $fields = [], $entry_id = '' ): string { $author_display_name = $this->get_author_meta( $entry_id, 'display_name' ); if ( empty( $author_display_name ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing $page_id = isset( $_POST['page_id'] ) ? absint( $_POST['page_id'] ) : 0; $author_id = $page_id ? (int) get_post_field( 'post_author', $page_id ) : get_current_user_id(); $author_display_name = get_the_author_meta( 'display_name', $author_id ); } $author_display_name = $this->has_cap() ? $author_display_name : ''; return esc_html( wp_strip_all_tags( $author_display_name ) ); } } SmartTags/SmartTag/Generic.php 0000644 00000000666 15174710275 0012301 0 ustar 00 <?php namespace WPForms\SmartTags\SmartTag; /** * Class Generic. * * @since 1.6.7.1 */ class Generic extends SmartTag { /** * Mock for the get_value method. * * @since 1.6.7.1 * * @param array $form_data Form data. * @param array $fields List of fields. * @param string $entry_id Entry ID. * * @return null */ public function get_value( $form_data, $fields = [], $entry_id = '' ) { return null; } } SmartTags/SmartTag/AuthorId.php 0000644 00000001527 15174710275 0012441 0 ustar 00 <?php namespace WPForms\SmartTags\SmartTag; /** * Class AuthorId. * * @since 1.6.7 */ class AuthorId extends SmartTag { /** * Get smart tag value. * * @since 1.6.7 * * @param array $form_data Form data. * @param array $fields List of fields. * @param string $entry_id Entry ID. * * @return int|string */ public function get_value( $form_data, $fields = [], $entry_id = '' ) { $author_id = $this->get_author_meta( $entry_id, 'ID' ); if ( empty( $author_id ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing $page_id = isset( $_POST['page_id'] ) ? absint( $_POST['page_id'] ) : 0; $author_id = $page_id ? (int) get_post_field( 'post_author', $page_id ) : get_current_user_id(); } $author_id = $this->has_cap() ? $author_id : ''; return $author_id ? absint( $author_id ) : ''; } } SmartTags/SmartTags.php 0000644 00000041271 15174710275 0011105 0 ustar 00 <?php // phpcs:disable Generic.Commenting.DocComment.MissingShort /** @noinspection PhpIllegalPsrClassPathInspection */ /** @noinspection PhpUndefinedClassInspection */ // phpcs:enable Generic.Commenting.DocComment.MissingShort namespace WPForms\SmartTags; use ActionScheduler_Action; use WPForms\SmartTags\SmartTag\Generic; use WPForms\SmartTags\SmartTag\SmartTag; /** * Class SmartTags. * * @since 1.6.7 */ class SmartTags { /** * List of smart tags. * * @since 1.6.7 * * @var array */ protected $smart_tags = []; /** * AS task action arguments. * Temporarily store them to use in the filter. * * @since 1.9.4 * * @var array|null */ private $action_args; /** * Fallback for entry meta. * Temporary store callback to remove it after AS task execution. * * @since 1.9.4 * * @var callable|null */ private $fallback; /** * Hooks. * * @since 1.6.7 */ public function hooks(): void { add_filter( 'wpforms_process_smart_tags', [ $this, 'process' ], 10, 6 ); add_filter( 'wpforms_builder_enqueues_smart_tags', [ $this, 'builder' ] ); add_filter( 'wpforms_builder_strings', [ $this, 'add_builder_strings' ], 10, 2 ); add_action( 'wpforms_process_entry_saved', function () { // Save super globals only after successes processing. add_filter( 'wpforms_tasks_task_register_async_args', [ $this, 'save_smart_tags_tasks_meta' ] ); } ); add_action( 'wpforms_tasks_start_executing', [ $this, 'maybe_add_entry_meta_fallback_value' ], 1, 2 ); add_action( 'wpforms_tasks_stop_executing', [ $this, 'maybe_remove_entry_meta_fallback_value' ], 1 ); } /** * Get the list of smart tags. * * @since 1.6.7 * * @return array */ public function get_smart_tags(): array { if ( ! empty( $this->smart_tags ) ) { return $this->smart_tags; } /** * Modify the smart tags' list. * * @since 1.4.0 * * @param array $tags The list of smart tags. */ $this->smart_tags = (array) apply_filters( // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName 'wpforms_smart_tags', $this->smart_tags_list() ); return $this->smart_tags; } /** * Get the list of registered smart tags. * * @since 1.6.7 * * @return array */ protected function smart_tags_list() { return [ 'admin_email' => esc_html__( 'Site Administrator Email', 'wpforms-lite' ), 'field_id' => esc_html__( 'Field ID', 'wpforms-lite' ), 'field_html_id' => esc_html__( 'Field HTML ID', 'wpforms-lite' ), 'field_value_id' => esc_html__( 'Field Value', 'wpforms-lite' ), 'form_id' => esc_html__( 'Form ID', 'wpforms-lite' ), 'form_name' => esc_html__( 'Form Name', 'wpforms-lite' ), 'page_title' => esc_html__( 'Embedded Post/Page Title', 'wpforms-lite' ), 'page_url' => esc_html__( 'Embedded Post/Page URL', 'wpforms-lite' ), 'page_id' => esc_html__( 'Embedded Post/Page ID', 'wpforms-lite' ), 'date' => esc_html__( 'Date', 'wpforms-lite' ), 'query_var' => esc_html__( 'Query String Variable', 'wpforms-lite' ), 'user_ip' => esc_html__( 'User IP Address', 'wpforms-lite' ), 'user_id' => esc_html__( 'User ID', 'wpforms-lite' ), 'user_display' => esc_html__( 'User Display Name', 'wpforms-lite' ), 'user_full_name' => esc_html__( 'User Full Name', 'wpforms-lite' ), 'user_first_name' => esc_html__( 'User First Name', 'wpforms-lite' ), 'user_last_name' => esc_html__( 'User Last Name', 'wpforms-lite' ), 'user_email' => esc_html__( 'Logged-in User\'s Email', 'wpforms-lite' ), 'user_meta' => esc_html__( 'User Meta', 'wpforms-lite' ), 'author_id' => esc_html__( 'Author ID', 'wpforms-lite' ), 'author_display' => esc_html__( 'Author Name', 'wpforms-lite' ), 'author_email' => esc_html__( 'Author Email', 'wpforms-lite' ), 'url_referer' => esc_html__( 'Referrer URL', 'wpforms-lite' ), 'url_login' => esc_html__( 'Login URL', 'wpforms-lite' ), 'url_logout' => esc_html__( 'Logout URL', 'wpforms-lite' ), 'url_register' => esc_html__( 'Register URL', 'wpforms-lite' ), 'url_lost_password' => esc_html__( 'Lost Password URL', 'wpforms-lite' ), 'unique_value' => esc_html__( 'Unique Value', 'wpforms-lite' ), 'site_name' => esc_html__( 'Site Name', 'wpforms-lite' ), 'order_summary' => esc_html__( 'Order Summary', 'wpforms-lite' ), ]; } /** * Add the Form Builder strings. * * @since 1.9.5 * * @param array $strings Localized strings. * @param WP_Post $form Form object. * * @return array * @noinspection HtmlUnknownTarget * @noinspection PhpMissingParamTypeInspection * @noinspection PhpUnusedParameterInspection */ public function add_builder_strings( $strings, $form ): array { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed $strings = (array) $strings; /** * Smart Tags. * * @since 1.6.7 * * @param array $smart_tags Array of smart tags. */ $smart_tags = (array) apply_filters( 'wpforms_builder_enqueues_smart_tags', $this->get_smart_tags() ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName $st_strings = [ 'smart_tags_dropdown_mce_icon' => WPFORMS_PLUGIN_URL . 'assets/images/icon-tags.svg', 'smart_tags' => $smart_tags, 'smart_tags_disabled_for_fields' => [ 'entry_id' ], 'smart_tags_edit_ok_button' => esc_html__( 'Apply changes', 'wpforms-lite' ), 'smart_tags_delete_button' => esc_html__( 'Delete smart tag', 'wpforms-lite' ), 'smart_tags_edit' => esc_html__( 'edit', 'wpforms-lite' ), 'smart_tags_arg' => esc_html__( 'argument', 'wpforms-lite' ), 'smart_tags_unknown_field' => esc_html__( 'Unknown Field', 'wpforms-lite' ), 'smart_tags_templates' => [ /* translators: %1$s - field ID, %2$s - field label. */ 'field_id' => esc_html__( 'Field %1$s', 'wpforms-lite' ), /* translators: %1$s - field ID, %2$s - field label. */ 'field_value_id' => esc_html__( 'Field value %1$s', 'wpforms-lite' ), /* translators: %1$s - field ID, %2$s - field label. */ 'field_html_id' => esc_html__( 'Field HTML %1$s', 'wpforms-lite' ), /* translators: %1$s - Query String Variable. */ 'query_var' => esc_html__( 'Query String Variable: %1$s', 'wpforms-lite' ), /* translators: %1$s - User meta key. */ 'user_meta' => esc_html__( 'User Meta: %1$s', 'wpforms-lite' ), /* translators: %1$s - Date format. */ 'date' => esc_html__( 'Date: %1$s', 'wpforms-lite' ), /* translators: %1$s - Date format. */ 'entry_date' => esc_html__( 'Entry Date: %1$s', 'wpforms-lite' ), ], /** * Filters the list of Smart Tags that are disabled for confirmations. * * @since 1.9.3 * * @param array $disabled List of disabled Smart Tags. */ 'smart_tags_disabled_for_confirmations' => apply_filters( 'wpforms_builder_smart_tags_disabled_for_confirmations', [] ), // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName ]; $st_strings['smart_tags_button_tooltip'] = sprintf( wp_kses( /* translators: %1$s - link to the WPForms.com doc article. */ __( 'Easily add dynamic information from various sources with <a href="%1$s" target="_blank" rel="noopener noreferrer">Smart Tags</a>.', 'wpforms-lite' ), [ 'a' => [ 'href' => [], 'rel' => [], 'target' => [], ], ] ), esc_url( wpforms_utm_link( 'https://wpforms.com/docs/how-to-use-smart-tags-in-wpforms/', 'Builder Settings', 'Smart Tags Documentation' ) ) ); return array_merge( $strings, $st_strings ); } /** * Get all smart tags in the content. * * @since 1.6.7 * * @param string $content Content. * * @return array */ private function get_all_smart_tags( $content ) { /** * A smart tag should start and end with a curly brace. * ([a-z0-9_]+) a smart tag name and also the first capturing group. * Lowercase letters, digits, and an underscore. * (|[ =][^\n}]*) - second capturing group: * | no characters at all or the following: * [ =][^\n}]* space or equal sign and any number of any characters except new line and closing curly brace. */ preg_match_all( '~{([a-z0-9_]+)(|[ =][^\n}]*)}~', $content, $smart_tags ); return array_combine( $smart_tags[0], $smart_tags[1] ); } /** * Process smart tags. * * @since 1.6.7 * @since 1.8.7 Added `$context` parameter. * * @param string $content Content. * @param array $form_data Form data. * @param array $fields List of fields. * @param string $entry_id Entry ID. * @param string $context Context. * * @return string */ public function process( $content, $form_data, $fields = [], $entry_id = '', $context = '', array $context_data = [] ) { // We shouldn't process smart tags in different WordPress editors // since it produce unexpected results. if ( wpforms_is_editor_page() ) { return $content; } $smart_tags = $this->get_all_smart_tags( $content ); if ( empty( $smart_tags ) ) { return $content; } foreach ( $smart_tags as $smart_tag => $tag_name ) { $class_name = $this->get_smart_tag_class_name( $tag_name ); $smart_tag_object = new $class_name( $smart_tag, $context, $context_data ); $value = $smart_tag_object->get_value( $form_data, $fields, $entry_id ); $field_id = $smart_tag_object->get_attributes()['field_id'] ?? 0; $field_id = (int) explode( '|', $field_id )[0]; if ( $context === 'confirmation_redirect' && $field_id > 0 && in_array( $fields[ $field_id ]['type'], wpforms_get_multi_fields(), true ) ) { // Protect from the case where the user already placed a pipe in the value. $value = str_replace( [ "\r\n", "\r", "\n", '|' ], [ rawurlencode( '|' ), '|', '|', '|' ], $value ); } /** * Modify the smart tag value. * * @since 1.6.7 * @since 1.6.7.1 Added the 5th argument. * @since 1.9.0 Added the 6th argument. * * @param scalar|null $value Smart Tag value. * @param array $form_data Form data. * @param array $fields List of fields. * @param int $entry_id Entry ID. * @param SmartTag $smart_tag_object The smart tag object or the Generic object for those cases when class unregistered. * @param string $context Context. */ $value = apply_filters( // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName "wpforms_smarttags_process_{$tag_name}_value", $value, $form_data, $fields, $entry_id, $smart_tag_object, $context ); /** * Modify a smart tag value. * * @since 1.6.7.1 * @since 1.9.7.3 Added the 7th argument. * * @param scalar|null $value Smart Tag value. * @param string $tag_name Smart tag name. * @param array $form_data Form data. * @param array $fields List of fields. * @param int $entry_id Entry ID. * @param SmartTag $smart_tag_object The smart tag object or the Generic object for those cases when class unregistered. * @param string $context Context. */ $value = apply_filters( // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName 'wpforms_smarttags_process_value', $value, $tag_name, $form_data, $fields, $entry_id, $smart_tag_object, $context ); if ( $value !== null ) { $content = $this->replace( $smart_tag, $value, $content ); } /** * Modify content with smart tags. * * @since 1.4.0 * @since 1.6.7.1 Added 3rd, 4th, 5th, 6th arguments. * * @param string $content Content of the Smart Tag. * @param string $tag_name Tag name of the Smart Tag. * @param array $form_data Form data. * @param string $fields List of fields. * @param int $entry_id Entry ID. * @param SmartTag $smart_tag_object The smart tag object or the Generic object for those cases when class unregistered. */ $content = (string) apply_filters( // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName 'wpforms_smart_tag_process', $content, $tag_name, $form_data, $fields, $entry_id, $smart_tag_object ); } return $content; } /** * Determine if the smart tag is registered. * * @since 1.6.7 * * @param string $smart_tag_name Smart tag name. * * @return bool */ protected function has_smart_tag( $smart_tag_name ) { return array_key_exists( $smart_tag_name, $this->get_smart_tags() ); } /** * Get a smart tag class name. * * @since 1.6.7 * * @param string $smart_tag_name Smart tag name. * * @return string */ protected function get_smart_tag_class_name( $smart_tag_name ) { if ( ! $this->has_smart_tag( $smart_tag_name ) ) { return Generic::class; } $class_name = str_replace( ' ', '', ucwords( str_replace( '_', ' ', $smart_tag_name ) ) ); $full_class_name = '\\WPForms\\SmartTags\\SmartTag\\' . $class_name; if ( class_exists( $full_class_name ) ) { return $full_class_name; } /** * Modify a smart tag class name that describes the smart tag logic. * * @since 1.6.7 * * @param string $class_name The value. * @param string $smart_tag_name Smart tag name. */ $full_class_name = apply_filters( 'wpforms_smarttags_get_smart_tag_class_name', '', $smart_tag_name ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName return class_exists( $full_class_name ) ? $full_class_name : Generic::class; } /** * Retrieve the builder's special tags. * * @since 1.6.7 * * @return array */ protected function get_replacement_builder_tags() { return [ 'date' => 'date format="m/d/Y"', 'query_var' => 'query_var key=""', 'user_meta' => 'user_meta key=""', ]; } /** * Hide smart tags in the builder. * * @since 1.6.7 * * @return array */ protected function get_hidden_builder_tags() { return [ 'field_id', 'field_html_id', 'field_value_id', ]; } /** * Builder tags. * * @since 1.6.7 * * @return array */ public function builder() { $smart_tags = $this->get_smart_tags(); $replacement_tags = $this->get_replacement_builder_tags(); $hidden_tags = $this->get_hidden_builder_tags(); foreach ( $replacement_tags as $tag => $replacement_tag ) { $smart_tags = wpforms_array_insert( $smart_tags, [ $replacement_tag => $smart_tags[ $tag ] ], $tag ); unset( $smart_tags[ $tag ] ); } foreach ( $hidden_tags as $hidden_tag ) { unset( $smart_tags[ $hidden_tag ] ); } return $smart_tags; } /** * Replace a found smart tag with the final value. * * @since 1.6.7 * * @param string $tag The tag. * @param string $value The value. * @param string $content Content. * * @return string */ private function replace( $tag, $value, $content ) { return str_replace( $tag, strip_shortcodes( $value ), $content ); } /** * Filter arguments passed to the async task. * * @since 1.9.4 * * @param array|mixed $args Arguments passed to the async task. */ public function save_smart_tags_tasks_meta( $args ): array { $args = (array) $args; $process = wpforms()->obj( 'process' ); if ( ! $process || empty( $process->form_data['entry_meta'] ) ) { return $args; } $args['entry_meta'] = $process->form_data['entry_meta']; return $args; } /** * Maybe add a fallback for entry meta for WPForms Action Scheduler tasks meta. * * @since 1.9.4 * * @param int|mixed $action_id Action ID. * @param ActionScheduler_Action $action Action Scheduler action object. * * @noinspection PhpUnusedParameterInspection * @noinspection PhpMissingParamTypeInspection */ public function maybe_add_entry_meta_fallback_value( $action_id, $action ): void { // phpcs:ignore WPForms.PHP.HooksMethod.InvalidPlaceForAddingHooks $this->action_args = $action->get_args(); $this->fallback = function ( $value, $var_name ) { if ( ! wpforms_is_empty_string( $value ) ) { return $value; } return $this->action_args['entry_meta'][ $var_name ] ?? $value; }; add_filter( 'wpforms_smart_tags_smart_tag_get_meta_value', $this->fallback, 10, 2 ); } /** * Maybe remove a fallback for entry meta for WPForms Action Scheduler tasks meta. * * @since 1.9.4 */ public function maybe_remove_entry_meta_fallback_value(): void { // phpcs:ignore WPForms.PHP.HooksMethod.InvalidPlaceForAddingHooks if ( ! $this->fallback ) { return; } remove_filter( 'wpforms_smart_tags_smart_tag_get_meta_value', $this->fallback ); } } Providers/Providers.php 0000644 00000003253 15174710275 0011223 0 ustar 00 <?php namespace WPForms\Providers; /** * Class Providers gives ability to track/load all providers. * * @since 1.4.7 * @since 1.7.3 Renamed from `Loader` to `Providers`. */ class Providers { /** * Get the instance of a class and store it in itself. * Later we will be able to use this class as `$providers_loader = \WPForms\Providers\Providers::get_instance();`. * * @since 1.4.7 */ public static function get_instance() { static $instance; if ( ! $instance ) { $instance = new Providers(); } return $instance; } /** * Loader constructor. * * @since 1.4.7 */ public function __construct() { } /** * Register a provider. * * @since 1.4.7 * * @param \WPForms\Providers\Provider\Core $provider The core class of a single provider. */ public function register( Provider\Core $provider ) { add_filter( 'wpforms_providers_available', [ $provider, 'register_provider' ] ); // WPForms > Settings > Integrations page. $integration = $provider->get_page_integrations(); if ( $integration !== null ) { add_action( 'wpforms_settings_providers', [ $integration, 'display' ], $provider::PRIORITY, 2 ); } // Editing Single Form > Form Builder. $form_builder = $provider->get_form_builder(); if ( $form_builder !== null ) { add_action( 'wpforms_providers_panel_sidebar', [ $form_builder, 'display_sidebar' ], $provider::PRIORITY ); add_action( 'wpforms_providers_panel_content', [ $form_builder, 'display_content' ], $provider::PRIORITY ); } // Process entry submission. $process = $provider->get_process(); if ( $process !== null ) { add_action( 'wpforms_process_complete', [ $process, 'process' ], 5, 4 ); } } } Providers/Provider/Status.php 0000644 00000011416 15174710275 0012323 0 ustar 00 <?php namespace WPForms\Providers\Provider; use stdClass; /** * Class Status gives ability to check/work with provider statuses. * Might be used later to track Provider errors on data-delivery. * * @since 1.4.8 */ class Status { /** * Provider identifier, its slug. * * @since 1.4.8 * * @var string */ private $provider; /** * Form data and settings. * * @since 1.4.8 * * @var array */ protected $form_data = []; /** * Status constructor. * * @since 1.4.8 * * @param string $provider Provider slug. */ public function __construct( $provider ) { $this->provider = sanitize_key( (string) $provider ); } /** * Provide an ability to statically init the object. * Useful for inline-invocations. * * @example: Status::init( 'drip' )->is_ready(); * * @since 1.4.8 * @since 1.5.9 Added a check on provider. * * @param string $provider Provider slug. * * @return Status */ public static function init( $provider ) { static $instance; if ( ! $instance || $provider !== $instance->provider ) { $instance = new self( $provider ); } return $instance; } /** * Check whether the defined provider is configured or not. * "Configured" means has an account that might be checked/updated on Settings > Integrations. * * @since 1.4.8 * * @return bool */ public function is_configured() { $options = wpforms_get_providers_options(); /** * Use this filter to change the configuration status of the provider. * We need the filter for BC reasons. * * @since 1.4.8 * * @param bool $is_configured Is the provider configured? */ $is_configured = apply_filters( // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName "wpforms_providers_{$this->provider}_configured", ! empty( $options[ $this->provider ] ) ); /** * Use this filter to change the configuration status of the provider. * * @since 1.4.8 * * @param bool $is_configured Is the provider configured? * @param string $provider Provider slug. */ return apply_filters( 'wpforms_providers_status_is_configured', $is_configured, $this->provider ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName } /** * Check whether the defined provider is connected to some form. * "Connected" means it has a Connection in Form Builder > Providers > Provider tab. * * @since 1.4.8 * * @param int $form_id Form ID to check the status against. * * @return bool */ public function is_connected( $form_id ) { $is_connected = false; $revisions = wpforms()->obj( 'revisions' ); $revision = $revisions ? $revisions->get_revision() : null; if ( $revision ) { $this->form_data = wpforms_decode( $revision->post_content ); } else { $this->form_data = wpforms()->obj( 'form' )->get( (int) $form_id, [ 'content_only' => true ] ); } if ( ! empty( $this->form_data['providers'][ $this->provider ] ) ) { $is_connected = $this->check_valid_connections(); } /** * Use this filter to change the connection status of the provider. * * @since 1.4.8 * * @param bool $is_connected Is the provider connected to the form? * @param string $provider Provider slug. */ return (bool) apply_filters( 'wpforms_providers_status_is_connected', $is_connected, $this->provider ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName } /** * Is the current provider ready to be used? * It means both configured and connected. * * @since 1.4.8 * * @param int $form_id Form ID to check the status against. * * @return bool */ public function is_ready( $form_id ) { return $this->is_configured() && $this->is_connected( $form_id ); } /** * Check if connections belong to an existing account. * * @since 1.8.8 * * @return bool */ private function check_valid_connections(): bool { $account_ids = array_keys( wpforms_get_providers_options( $this->provider ) ); // BC for the Salesforce addon that uses `resource_owner_id` key instead of `account_id` value. if ( $this->provider === 'salesforce' ) { $account_ids = array_column( wpforms_get_providers_options( 'salesforce' ), 'resource_owner_id' ); } // Account id is generated by the `uniqid` function that sometimes returns an integer value. $account_ids = array_map( 'strval', $account_ids ); $connection_accounts_ids = array_column( $this->form_data['providers'][ $this->provider ], 'account_id' ); // BC for the Drip addon that uses `option_id` key for storing a connection provider. if ( $this->provider === 'drip' ) { $connection_accounts_ids = array_column( $this->form_data['providers'][ $this->provider ], 'option_id' ); } foreach ( $connection_accounts_ids as $account ) { if ( in_array( (string) $account, $account_ids, true ) ) { return true; } } return false; } } Providers/Provider/Core.php 0000644 00000006447 15174710275 0011740 0 ustar 00 <?php namespace WPForms\Providers\Provider; /** * Class Core stores the basic information about the provider. * It's also a Container to load single instances of requires classes. * * @since 1.4.7 */ abstract class Core { /** * Unique provider slug. * * @since 1.4.7 * * @var string */ public $slug; /** * Translatable provider name. * * @since 1.4.7 * * @var string */ public $name; /** * Custom provider icon (logo). * * @since 1.4.7 * * @var string */ public $icon; /** * Custom priority for a provider, that will affect loading/placement order. * * @since 1.4.8 * * @var int */ const PRIORITY = 10; /** * Get the instance of the class. * * @since 1.4.7 * @since 1.7.3 Compatibility with PHP 8.1 * * @return Core */ public static function get_instance() { static $instance; $class = static::class; if ( empty( $instance[ $class ] ) ) { // Same as new static(), but allows avoiding "abstract class init" error. $instance[ $class ] = new $class(); } return $instance[ $class ]; } /** * Core constructor. * * @since 1.4.7 * * @param array $params Possible keys: slug*, name*, icon. * are required. * * @throws \UnexpectedValueException Provider class should define provider's "slug"/"name" params. */ public function __construct( array $params ) { // Define required provider properties. if ( ! empty( $params['slug'] ) ) { $this->slug = \sanitize_key( $params['slug'] ); } else { throw new \UnexpectedValueException( 'Provider class should define a provider "slug" param in its constructor.' ); } if ( ! empty( $params['name'] ) ) { $this->name = \sanitize_text_field( $params['name'] ); } else { throw new \UnexpectedValueException( 'Provider class should define a provider "name" param in its constructor.' ); } $this->icon = WPFORMS_PLUGIN_URL . 'assets/images/sullie.png'; if ( ! empty( $params['icon'] ) ) { $this->icon = \esc_url_raw( $params['icon'] ); } } /** * Add to list of registered providers. * * @since 1.4.7 * * @param array $providers Array of all active providers. * * @return array */ public function register_provider( array $providers ) { $providers[ $this->slug ] = $this->name; return $providers; } /** * Provide an instance of the object, that should process the submitted entry. * It will use data from an already saved entry to pass it further to a Provider. * * @since 1.4.7 * * @return null|\WPForms\Providers\Provider\Process */ abstract public function get_process(); /** * Provide an instance of the object, that should display provider settings * on Settings > Integrations page in admin area. * If you don't want to display it (i.e. you don't need it), you can pass null here in your Core provider class. * * @since 1.4.7 * * @return null|\WPForms\Providers\Provider\Settings\PageIntegrations */ abstract public function get_page_integrations(); /** * Provide an instance of the object, that should display provider settings in the Form Builder. * If you don't want to display it (i.e. you don't need it), you can pass null here in your Core provider class. * * @since 1.4.7 * * @return null|\WPForms\Providers\Provider\Settings\FormBuilder */ abstract public function get_form_builder(); } Providers/Provider/Settings/FormBuilderInterface.php 0000644 00000001265 15174710275 0016674 0 ustar 00 <?php namespace WPForms\Providers\Provider\Settings; /** * Interface FormBuilderInterface defines required method for builder to work properly. * * @since 1.4.7 */ interface FormBuilderInterface { /** * Every provider should display a title in a Builder. * * @since 1.4.7 */ public function display_sidebar(); /** * Every provider should display a content of its settings in a Builder. * * @since 1.4.7 */ public function display_content(); /** * Use this method to register own templates for form builder. * Make sure, that you have `tmpl-` in template name in `<script id="tmpl-*">`. * * @since 1.4.7 */ public function builder_custom_templates(); } Providers/Provider/Settings/FormBuilder.php 0000644 00000061370 15174710275 0015056 0 ustar 00 <?php namespace WPForms\Providers\Provider\Settings; use WPForms\Providers\Provider\Core; use WPForms\Providers\Provider\Status; /** * Class FormBuilder handles functionality inside the form builder. * * @since 1.4.7 */ abstract class FormBuilder implements FormBuilderInterface { /** * Get the Core loader class of a provider. * * @since 1.4.7 * * @var Core */ protected $core; /** * Most Marketing providers will have a 'connection' type. * Payment providers may have (or not) something different. * * @since 1.4.7 * * @var string */ protected $type = 'connection'; /** * Form data and settings. * * @since 1.4.7 * * @var array */ protected $form_data = []; /** * Integrations constructor. * * @since 1.4.7 * * @param Core $core Core provider class. */ public function __construct( Core $core ) { $this->core = $core; $form_obj = wpforms()->obj( 'form' ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended $form_id = isset( $_GET['form_id'] ) ? absint( $_GET['form_id'] ) : 0; if ( $form_obj && $form_id ) { $this->form_data = $form_obj->get( $form_id, [ 'content_only' => true ] ); // Form ID isn't defined for newly created forms. if ( empty( $this->form_data['id'] ) && is_array( $this->form_data ) ) { $this->form_data['id'] = $form_id; } } $this->init_hooks(); } /** * Register all hooks (actions and filters) here. * * @since 1.4.7 */ protected function init_hooks() { // phpcs:ignore WPForms.PHP.HooksMethod.InvalidPlaceForAddingHooks // Register builder HTML template(s). add_action( 'wpforms_builder_print_footer_scripts', [ $this, 'builder_templates' ] ); add_action( 'wpforms_builder_print_footer_scripts', [ $this, 'builder_custom_templates' ], 11 ); // Process builder AJAX requests. add_action( "wp_ajax_wpforms_builder_provider_ajax_{$this->core->slug}", [ $this, 'process_ajax' ] ); /* * Enqueue assets. */ if ( ( ! empty( $_GET['page'] ) && $_GET['page'] === 'wpforms-builder' ) && // phpcs:ignore ! empty( $_GET['form_id'] ) && // phpcs:ignore is_admin() ) { add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] ); } add_filter( 'wpforms_save_form_args', [ $this, 'remove_connection_locks' ], 1, 3 ); } /** * Used to register generic templates for all providers inside form builder. * * @since 1.4.7 * @since 1.6.2 Added sub-templates for conditional logic based on provider. */ public function builder_templates(): void { $cl_builder_block = wpforms()->is_pro() ? wpforms_conditional_logic()->builder_block( [ 'form' => $this->form_data, 'type' => 'panel', 'parent' => 'providers', 'panel' => esc_attr( $this->core->slug ), 'subsection' => '%connection_id%', ], false ) : ''; ?> <!-- Single connection block sub-template: FIELDS --> <script type="text/html" id="tmpl-wpforms-providers-builder-content-connection-fields"> <div class="wpforms-builder-provider-connection-block wpforms-builder-provider-connection-fields"> <h4><?php esc_html_e( 'Custom Fields', 'wpforms-lite' ); ?></h4> <table class="wpforms-builder-provider-connection-fields-table wpforms-undo-redo-container"> <thead> <tr> <th><?php esc_html_e( 'Custom Field Name', 'wpforms-lite' ); ?></th> <th colspan="3"><?php esc_html_e( 'Form Field Value', 'wpforms-lite' ); ?></th> </tr> </thead> <tbody> <# if ( ! _.isEmpty( data.connection.fields_meta ) ) { #> <# _.each( data.connection.fields_meta, function( item, meta_id ) { #> <tr class="wpforms-builder-provider-connection-fields-table-row"> <td> <# if ( ! _.isEmpty( data.provider.fields ) ) { #> <select class="wpforms-builder-provider-connection-field-name" name="providers[{{ data.provider.slug }}][{{ data.connection.id }}][fields_meta][{{ meta_id }}][name]"> <option value=""><# if ( ! _.isEmpty( data.provider.placeholder ) ) { #>{{ data.provider.placeholder }}<# } else { #><?php esc_html_e( '--- Select Field ---', 'wpforms-lite' ); ?><# } #></option> <# _.each( data.provider.fields, function( field_name, field_id ) { #> <option value="{{ field_id }}" <# if ( field_id === item.name ) { #>selected="selected"<# } #> > {{ field_name }} </option> <# } ); #> </select> <# } else { #> <input type="text" value="{{ item.name }}" class="wpforms-builder-provider-connection-field-name" name="providers[{{ data.provider.slug }}][{{ data.connection.id }}][fields_meta][{{ meta_id }}][name]" placeholder="<?php esc_attr_e( 'Field Name', 'wpforms-lite' ); ?>" /> <# } #> </td> <td> <select class="wpforms-builder-provider-connection-field-value" data-support-subfields="{{ data.isSupportSubfields }}" name="providers[{{ data.provider.slug }}][{{ data.connection.id }}][fields_meta][{{ meta_id }}][field_id]"> <option value=""><?php esc_html_e( '--- Select Form Field ---', 'wpforms-lite' ); ?></option> <# _.each( data.fields, function( field, key ) { const fieldId = field.id.toString(); const itemId = item.field_id.toString(); isSelected = fieldId === itemId <?php // BC: Previously saved name fields don't have the `.full` suffix in DB. ?> || ( ! itemId.includes('.') && fieldId === itemId + '.full' ); #> <option value="{{ fieldId }}"<# if ( isSelected ) { #> selected="selected"<# } #>> <# if ( ! _.isUndefined( field.label ) && field.label.toString().trim() !== '' ) { #> {{ field.label.toString().trim() }} <# } else { #> {{ wpforms_builder.field + ' #' + key }} <# } #> </option> <# } ); #> </select> </td> <td class="add"> <button class="button-secondary js-wpforms-builder-provider-connection-fields-add" title="<?php esc_attr_e( 'Add Another', 'wpforms-lite' ); ?>"> <i class="fa fa-plus-circle"></i> </button> </td> <td class="delete"> <button class="button js-wpforms-builder-provider-connection-fields-delete <# if ( meta_id === 0 ) { #>hidden<# } #>" title="<?php esc_attr_e( 'Remove', 'wpforms-lite' ); ?>"> <i class="fa fa-minus-circle"></i> </button> </td> </tr> <# } ); #> <# } else { #> <tr class="wpforms-builder-provider-connection-fields-table-row"> <td> <# if ( ! _.isEmpty( data.provider.fields ) ) { #> <select class="wpforms-builder-provider-connection-field-name" name="providers[{{ data.provider.slug }}][{{ data.connection.id }}][fields_meta][0][name]"> <option value=""><# if ( ! _.isEmpty( data.provider.placeholder ) ) { #>{{ data.provider.placeholder }}<# } else { #><?php esc_html_e( '--- Select Field ---', 'wpforms-lite' ); ?><# } #></option> <# _.each( data.provider.fields, function( field_name, field_id ) { #> <option value="{{ field_id }}"> {{ field_name }} </option> <# } ); #> </select> <# } else { #> <input type="text" value="" class="wpforms-builder-provider-connection-field-name" name="providers[{{ data.provider.slug }}][{{ data.connection.id }}][fields_meta][0][name]" placeholder="<?php esc_attr_e( 'Field Name', 'wpforms-lite' ); ?>" /> <# } #> </td> <td> <select class="wpforms-builder-provider-connection-field-value" name="providers[{{ data.provider.slug }}][{{ data.connection.id }}][fields_meta][0][field_id]"> <option value=""><?php esc_html_e( '--- Select Form Field ---', 'wpforms-lite' ); ?></option> <# _.each( data.fields, function( field, key ) { #> <option value="{{ field.id }}"> <# if ( ! _.isUndefined( field.label ) && field.label.toString().trim() !== '' ) { #> {{ field.label.toString().trim() }} <# } else { #> {{ wpforms_builder.field + ' #' + key }} <# } #> </option> <# } ); #> </select> </td> <td class="add"> <button class="button-secondary js-wpforms-builder-provider-connection-fields-add" title="<?php esc_attr_e( 'Add Another', 'wpforms-lite' ); ?>"> <i class="fa fa-plus-circle"></i> </button> </td> <td class="delete"> <button class="button js-wpforms-builder-provider-connection-fields-delete hidden" title="<?php esc_attr_e( 'Delete', 'wpforms-lite' ); ?>"> <i class="fa fa-minus-circle"></i> </button> </td> </tr> <# } #> </tbody> </table><!-- /.wpforms-builder-provider-connection-fields-table --> <p class="description"> <?php esc_html_e( 'Map custom fields (or properties) to form fields values.', 'wpforms-lite' ); ?> </p> </div><!-- /.wpforms-builder-provider-connection-fields --> </script> <!-- Single connection block sub-template: CONDITIONAL LOGIC --> <script type="text/html" id="tmpl-wpforms-<?php echo esc_attr( $this->core->slug ); ?>-builder-content-connection-conditionals"> <?php echo $cl_builder_block; // phpcs:ignore ?> </script> <!-- DEPRECATED: Should be removed when we make changes in our addons. --> <script type="text/html" id="tmpl-wpforms-providers-builder-content-connection-conditionals"> <?php echo $cl_builder_block; // phpcs:ignore ?> </script> <?php $this->builder_error_template(); } /** * Enqueue the JavaScript and CSS files if needed. * When extending - include the `parent::enqueue_assets();` not to break things! * * @since 1.4.7 */ public function enqueue_assets() { $min = wpforms_get_min_suffix(); wp_enqueue_script( 'wpforms-admin-builder-templates', WPFORMS_PLUGIN_URL . "assets/js/admin/builder/templates{$min}.js", [ 'wp-util' ], WPFORMS_VERSION, true ); wp_enqueue_script( 'wpforms-admin-builder-providers', WPFORMS_PLUGIN_URL . "assets/js/admin/builder/providers{$min}.js", [ 'wpforms-utils', 'wpforms-builder', 'wpforms-admin-builder-templates' ], WPFORMS_VERSION, true ); } /** * Process the Builder AJAX requests. * * @since 1.4.7 */ public function process_ajax(): void { // Run a security check. check_ajax_referer( 'wpforms-builder', 'nonce' ); // Check for permissions. if ( ! wpforms_current_user_can( 'edit_forms' ) ) { wp_send_json_error( [ 'error' => esc_html__( 'You do not have permission to perform this action.', 'wpforms-lite' ), ] ); } // Process required values. $error = [ 'error' => esc_html__( 'Something went wrong while performing an AJAX request.', 'wpforms-lite' ) ]; if ( empty( $_POST['id'] ) || empty( $_POST['task'] ) ) { wp_send_json_error( $error ); } $form_id = (int) $_POST['id']; $task = sanitize_key( $_POST['task'] ); $revisions = wpforms()->obj( 'revisions' ); $revision = $revisions ? $revisions->get_revision() : null; if ( $revision ) { // Set up form data based on the revision_id that we got from AJAX request. $this->form_data = wpforms_decode( $revision->post_content ); } else { // Set up form data based on the ID that we got from AJAX request. $form_handler = wpforms()->obj( 'form' ); $this->form_data = $form_handler ? $form_handler->get( $form_id, [ 'content_only' => true ] ) : []; } // Do not allow proceeding further, as form_id may be incorrect. if ( empty( $this->form_data ) ) { wp_send_json_error( $error ); } $data = apply_filters( // phpcs:ignore WPForms.Comments.PHPDocHooks.RequiredHookDocumentation, WPForms.PHP.ValidateHooks.InvalidHookName 'wpforms_providers_settings_builder_ajax_' . $task . '_' . $this->core->slug, null ); if ( ! empty( $data['error_msg'] ) ) { wp_send_json_error( [ 'error_msg' => $data['error_msg'] ] ); } if ( $data !== null ) { wp_send_json_success( $data ); } wp_send_json_error( $error ); } /** * Display content inside the panel sidebar area. * * @since 1.4.7 */ public function display_sidebar() { $configured = ''; if ( ! empty( $this->form_data['id'] ) && Status::init( $this->core->slug )->is_ready( $this->form_data['id'] ) ) { $configured = 'configured'; } $classes = [ 'wpforms-panel-sidebar-section', 'icon', $configured, 'wpforms-panel-sidebar-section-' . $this->core->slug, ]; ?> <a href="#" class="<?php echo esc_attr( implode( ' ', $classes ) ); ?>" data-section="<?php echo esc_attr( $this->core->slug ); ?>"> <img src="<?php echo esc_url( $this->core->icon ); ?>" alt="icon"> <?php echo esc_html( $this->core->name ); ?> <i class="fa fa-angle-right wpforms-toggle-arrow"></i> <?php if ( ! empty( $configured ) ) : ?> <i class="fa fa-check-circle-o"></i> <?php endif; ?> </a> <?php } /** * Wrap the builder section content with the required (for tabs switching) markup. * * @since 1.4.7 */ public function display_content() { ?> <div class="wpforms-panel-content-section wpforms-builder-provider wpforms-panel-content-section-<?php echo esc_attr( $this->core->slug ); ?>" id="<?php echo esc_attr( $this->core->slug ); ?>-provider" data-provider="<?php echo esc_attr( $this->core->slug ); ?>" data-provider-name="<?php echo esc_attr( $this->core->name ); ?>"> <!-- Provider content goes here. --> <?php $this->display_content_header(); $form_id = ! empty( $this->form_data['id'] ) ? $this->form_data['id'] : ''; self::display_content_default_screen( Status::init( $this->core->slug )->is_ready( $form_id ), $this->core->slug, $this->core->name, $this->core->icon ); $this->display_lock_field(); ?> <div class="wpforms-builder-provider-body"> <div class="wpforms-provider-connections-wrap wpforms-clear"> <div class="wpforms-builder-provider-connections"></div> </div> </div> </div> <?php } /** * Display provider default screen. * * @since 1.6.8 * * @param bool $is_connected True if connections are configured. * @param string $slug Provider slug. * @param string $name Provider name. * @param string $icon Provider icon. */ public static function display_content_default_screen( $is_connected, $slug, $name, $icon ): void { // Hide the provider default settings screen when it's already connected. $class = $is_connected ? ' wpforms-hidden' : ''; ?> <div class="wpforms-builder-provider-connections-default<?php echo esc_attr( $class ); ?>"> <img src="<?php echo esc_url( $icon ); ?>" alt=""> <div class="wpforms-builder-provider-settings-default-content"> <?php /* * Allows developers to change the default content of the provider's settings default screen. * * @since 1.6.8 * * @param string $content Content of the provider's settings default screen. */ echo apply_filters( // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped, WPForms.Comments.PHPDocHooks.RequiredHookDocumentation, WPForms.PHP.ValidateHooks.InvalidHookName "wpforms_providers_provider_settings_formbuilder_display_content_default_screen_{$slug}", sprintf( /* translators: %s - provider name. */ '<p>' . esc_html__( 'Get the most out of WPForms — use it with an active %s account.', 'wpforms-lite' ) . '</p>', esc_html( $name ) ) ); ?> </div> </div> <?php } /** * Display the lock field. * * @since 1.8.9 */ protected function display_lock_field(): void { if ( ! $this->is_lock_field_required( $this->core->slug ) ) { return; } ?> <input type="hidden" class="wpforms-builder-provider-connections-save-lock" value="1" name="providers[<?php echo esc_attr( $this->core->slug ); ?>][__lock__]"> <?php } /** * Section content header. * * @since 1.4.7 */ protected function display_content_header() { $provider_status = Status::init( $this->core->slug ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended $form_id = isset( $_GET['form_id'] ) ? absint( $_GET['form_id'] ) : 0; $is_configured = $provider_status->is_configured(); $is_connected = $provider_status->is_ready( $form_id ); ?> <div class="wpforms-builder-provider-title wpforms-panel-content-section-title"> <?php echo esc_html( $this->core->name ); ?> <span class="wpforms-builder-provider-title-spinner <?php echo $is_connected ? '' : 'wpforms-hidden'; ?>"> <i class="wpforms-loading-spinner wpforms-loading-md wpforms-loading-inline"></i> </span> <button class="wpforms-builder-provider-title-add js-wpforms-builder-provider-connection-add <?php echo $is_configured ? '' : 'hidden'; ?>" data-form_id="<?php echo esc_attr( $form_id ); ?>" data-provider="<?php echo esc_attr( $this->core->slug ); ?>"> <?php esc_html_e( 'Add New Connection', 'wpforms-lite' ); ?> </button> <button class="wpforms-builder-provider-title-add js-wpforms-builder-provider-account-add <?php echo ! $is_configured ? '' : 'hidden'; ?>" data-form_id="<?php echo esc_attr( $form_id ); ?>" data-provider="<?php echo esc_attr( $this->core->slug ); ?>"> <?php esc_html_e( 'Add New Account', 'wpforms-lite' ); ?> </button> </div> <?php } /** * Determine whether the lock field is required. * * @WPFormsBackCompat Support Drip v1.7.0 and earlier, support Uncanny Automator. * * @since 1.8.9 * * @param string $provider The provider slug. * * @return bool */ protected function is_lock_field_required( string $provider ): bool { // Compatibility with the legacy Drip addon versions where the lock field was unnecessary. // Uncanny Automator does not have a lock field. if ( in_array( $provider, [ 'uncanny-automator', 'drip' ], true ) ) { return false; } return true; } /** * Temporary fix to remove __lock__ field with value 1 from the form post_content. * In the future, it will be handled in the save_form () method in the core for all providers. * * @since 1.8.9 * * @param array|mixed $form Form array, usable with wp_update_post. * @param array $data Data retrieved from $_POST and processed. * @param array $args Update form arguments. * * @return array * @noinspection PhpMissingParamTypeInspection * @noinspection PhpUnusedParameterInspection */ public function remove_connection_locks( $form, $data, $args ): array { $form = (array) $form; $form_data = json_decode( stripslashes( $form['post_content'] ), true ); if ( empty( $form_data['providers'][ $this->core->slug ] ) ) { return $form; } $provider = $form_data['providers'][ $this->core->slug ]; $lock = '__lock__'; // Remove the lock field if it's the only one and it's locked. if ( isset( $provider[ $lock ] ) && count( $provider ) === 1 && absint( $provider[ $lock ] ) === 1 ) { unset( $form_data['providers'][ $this->core->slug ]['__lock__'] ); $form['post_content'] = wpforms_encode( $form_data ); } return $form; } /** * Received field values for fields with multiple choices, e.g., multi-select. * Connection Data has only the last saved field option. * So, we should receive data from super global $_POST and receive all submitted options instead. * WARNING: Sanitization of these values is required. * * @since 1.9.7 * * @param string $name Field name. * @param array $connection_data Connection data. * * @return array */ protected function get_multiple_option_field( string $name, array $connection_data ): array { // The nonce checked in the `wpforms_save_form` function. // phpcs:disable WordPress.Security.NonceVerification // When we duplicate a form the `$_POST['data']` is empty, // we shouldn't update the field and use copied data. if ( empty( $_POST['data'] ) || empty( $connection_data['id'] ) ) { return isset( $connection_data[ $name ] ) ? (array) $connection_data[ $name ] : []; } $connection_id = $connection_data['id']; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $form_post = json_decode( wp_unslash( $_POST['data'] ), true ) ?? []; $full_name = "providers[{$this->core->slug}][$connection_id][$name][]"; $values = []; // phpcs:enable WordPress.Security.NonceVerification foreach ( $form_post as $post_pair ) { if ( empty( $post_pair['name'] ) || $post_pair['name'] !== $full_name ) { continue; } $values[] = $post_pair['value']; } return $values; } /** * Sanitize custom fields. * * @since 1.9.3 * * @param array $connection Connection data. */ protected function sanitize_connection_fields_meta( array &$connection ): void { if ( ! isset( $connection['fields_meta'] ) ) { return; } if ( ! is_array( $connection['fields_meta'] ) ) { unset( $connection['fields_meta'] ); return; } foreach ( $connection['fields_meta'] as $row_number => $field ) { if ( ! isset( $field['field_id'], $field['name'] ) ) { unset( $connection['fields_meta'][ $row_number ] ); continue; } // Field ID can contain a subfield, e.g. `1.first`. $field_id = sanitize_text_field( $field['field_id'] ); $name = sanitize_text_field( $field['name'] ); if ( wpforms_is_empty_string( $field_id ) || wpforms_is_empty_string( $name ) ) { unset( $connection['fields_meta'][ $row_number ] ); continue; } $connection['fields_meta'][ $row_number ] = [ 'name' => $name, 'field_id' => $field_id, ]; } $connection['fields_meta'] = array_values( $connection['fields_meta'] ); } /** * Sanitize conditional logic connection fields. * * @since 1.9.3 * * @param array $connection Connection data. */ protected function sanitize_connection_conditionals( array &$connection ): void { if ( ! isset( $connection['conditionals'] ) ) { return; } if ( ! is_array( $connection['conditionals'] ) ) { unset( $connection['conditionals'] ); return; } foreach ( $connection['conditionals'] as $group_id => $group ) { foreach ( $group as $rule ) { $this->sanitize_connection_conditional_rule( $rule ); } $group = array_filter( $group ); if ( empty( $group ) ) { unset( $connection['conditionals'][ $group_id ] ); continue; } $connection['conditionals'][ $group_id ] = $group; } } /** * Sanitize conditional logic rule. * * @since 1.9.3 * * @param array $rule Conditional logic rule. */ private function sanitize_connection_conditional_rule( array &$rule ): void { if ( ! isset( $rule['field'], $rule['operator'] ) ) { $rule = []; return; } $sanitized_rule = [ 'field' => sanitize_text_field( $rule['field'] ), 'operator' => sanitize_text_field( $rule['operator'] ), ]; if ( wpforms_is_empty_string( $sanitized_rule['field'] ) || wpforms_is_empty_string( $sanitized_rule['operator'] ) ) { $rule = []; return; } if ( isset( $rule['value'] ) ) { $sanitized_rule['value'] = sanitize_text_field( $rule['value'] ); } $rule = $sanitized_rule; } /** * Builder error template. * This generates an HTML template for displaying an error message * when the connection to the provider fails. The message includes * a link to the connection settings page for troubleshooting. * * @since 1.9.5 * * @noinspection HtmlUnknownTarget */ protected function builder_error_template(): void { ?> <script type="text/html" id="tmpl-wpforms-<?php echo esc_attr( $this->core->slug ); ?>-builder-content-connection-default-error"> <div class="wpforms-builder-provider-connections-error wpforms-hidden" id="wpforms-<?php echo esc_attr( $this->core->slug ); ?>-builder-provider-error" > <span class="wpforms-builder-provider-connections-error-message"> <?php printf( wp_kses( /* translators: %1$s - Documentation URL. */ __( 'Something went wrong, and we can’t connect to the provider. Please check your <a href="%s" target="_blank" rel="noopener noreferrer">connection settings</a>.', 'wpforms-lite' ), [ 'a' => [ 'href' => [], 'target' => [], 'rel' => [], ], ] ), esc_url( $this->get_settings_url() ) ); ?> </span> </div> </script> <?php } /** * Retrieves the settings URL for the specific provider. * * @since 1.9.5 * * @return string The URL to the settings page for the provider. */ private function get_settings_url(): string { return admin_url( sprintf( 'admin.php?page=wpforms-settings&view=integrations#wpforms-integration-%s', $this->core->slug ) ); } } Providers/Provider/Settings/PageIntegrationsInterface.php 0000644 00000001110 15174710275 0017712 0 ustar 00 <?php namespace WPForms\Providers\Provider\Settings; /** * Interface PageIntegrationsInterface defines methods that are common among all Integration page providers content. * * @since 1.4.7 */ interface PageIntegrationsInterface { /** * Display the data for the Integrations tab. * This is a default one that can be easily overwritten inside the child class of a specific provider. * * @since 1.4.7 * * @param array $active Array of activated providers addons. * @param array $settings Providers options. */ public function display( $active, $settings ); } Providers/Provider/Settings/PageIntegrations.php 0000644 00000024147 15174710275 0016110 0 ustar 00 <?php // phpcs:ignore Generic.Commenting.DocComment.MissingShort /** @noinspection PhpUndefinedConstantInspection */ namespace WPForms\Providers\Provider\Settings; use WPForms\Providers\Provider\Core; /** * Class PageIntegrations handles the WPForms -> Settings -> Integrations page. * * @since 1.4.7 */ abstract class PageIntegrations implements PageIntegrationsInterface { /** * Get the Core loader class of a provider. * * @since 1.4.7 * * @var Core */ protected $core; /** * Integrations constructor. * * @since 1.4.7 * * @param Core $core Core provider object. */ public function __construct( Core $core ) { $this->core = $core; $this->ajax(); } /** * Process the default ajax functionality. * * @since 1.4.7 */ protected function ajax() { // phpcs:ignore WPForms.PHP.HooksMethod.InvalidPlaceForAddingHooks // Remove provider from Settings Integrations tab. add_action( "wp_ajax_wpforms_settings_provider_disconnect_{$this->core->slug}", [ $this, 'ajax_disconnect' ] ); // Add new provider from Settings Integrations tab. add_action( "wp_ajax_wpforms_settings_provider_add_{$this->core->slug}", [ $this, 'ajax_connect' ] ); } /** * @inheritdoc */ public function display( $active, $settings ) { $accounts = ! empty( $settings[ $this->core->slug ] ) ? $settings[ $this->core->slug ] : []; $classes = $this->get_provider_classes( $active, $settings ); $arrow = in_array( 'focus-in', $classes, true ) ? 'down' : 'right'; ?> <div id="wpforms-integration-<?php echo esc_attr( $this->core->slug ); ?>" class="wpforms-settings-provider wpforms-clear <?php echo esc_attr( $this->core->slug ); ?> <?php echo wpforms_sanitize_classes( $classes, true ); ?>"> <div class="wpforms-settings-provider-header wpforms-clear" data-provider="<?php echo esc_attr( $this->core->slug ); ?>"> <div class="wpforms-settings-provider-logo"> <i title="<?php esc_attr_e( 'Show Accounts', 'wpforms-lite' ); ?>" class="fa fa-chevron-<?php echo esc_attr( $arrow ); ?>"></i> <img src="<?php echo esc_url( $this->core->icon ); ?>" alt="icon"> </div> <div class="wpforms-settings-provider-info"> <h3><?php echo esc_html( $this->core->name ); ?></h3> <p> <?php printf( /* translators: %s - provider name. */ esc_html__( 'Integrate %s with WPForms', 'wpforms-lite' ), esc_html( $this->core->name ) ); ?> </p> <span class="connected-indicator green"> <i class="fa fa-check-circle-o"></i> <span><?php esc_html_e( 'Connected', 'wpforms-lite' ); ?></span> </span> </div> </div> <div class="wpforms-settings-provider-accounts" id="provider-<?php echo esc_attr( $this->core->slug ); ?>"> <div class="wpforms-settings-provider-accounts-list"> <ul> <?php if ( ! empty( $accounts ) ) { foreach ( $accounts as $account_id => $account ) { if ( empty( $account_id ) ) { continue; } $this->display_connected_account( $account_id, $account ); } } ?> </ul> </div> <?php $this->display_add_new(); ?> </div> </div> <?php } /** * Get provider classes. * * @since 1.8.6 * * @param array $active Array of activated providers addons. * @param array $settings Providers options. */ protected function get_provider_classes( $active, $settings ) { $connected = ! empty( $active[ $this->core->slug ] ); $accounts = ! empty( $settings[ $this->core->slug ] ) ? $settings[ $this->core->slug ] : []; $classes = []; if ( $connected && $accounts ) { $classes[] = 'connected'; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( empty( $_GET['wpforms-integration'] ) ) { return $classes; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended $classes[] = $this->core->slug === $_GET['wpforms-integration'] ? 'focus-in' : 'focus-out'; return $classes; } /** * Display a connected account. * * @since 1.7.5 * * @param string $account_id Account ID. * @param array $account Account data. */ protected function display_connected_account( $account_id, $account ) { $account_connected = ! empty( $account['date'] ) ? wpforms_date_format( $account['date'], '', true ) : esc_html__( 'N/A', 'wpforms-lite' ); echo '<li>'; /** * Allow adding markup before connected account item. * * @since 1.7.5 * * @param string $account_id Account ID. * @param array $account Account data. */ do_action( 'wpforms_providers_provider_settings_page_integrations_display_connected_account_item_before', $account_id, $account ); echo '<span class="label">'; echo ! empty( $account['label'] ) ? esc_html( $account['label'] ) : '<em>' . esc_html__( 'No Label', 'wpforms-lite' ) . '</em>'; echo '</span>'; echo '<span class="date">'; echo esc_html( sprintf( /* translators: %1$s - Connection date. */ __( 'Connected on: %1$s', 'wpforms-lite' ), $account_connected ) ); if ( defined( 'WPFORMS_DEBUG' ) && WPFORMS_DEBUG ) { $this->display_account_id_debug( $account_id ); $this->display_expires_in_debug( $account ); } echo '</span>'; echo( '<span class="remove"><a href="#" data-provider="' . esc_attr( $this->core->slug ) . '" data-key="' . esc_attr( $account_id ) . '">' . esc_html__( 'Disconnect', 'wpforms-lite' ) . '</a></span>' ); /** * Allow adding markup after connected account item. * * @since 1.7.5 * * @param string $account_id Account ID. * @param array $account Account data. */ do_action( 'wpforms_providers_provider_settings_page_integrations_display_connected_account_item_after', $account_id, $account ); echo '</li>'; } /** * Display the account ID for debugging purposes. * * @since 1.9.5 * * @param mixed $account_id Account ID to display. If null, it displays 'no_id'. */ protected function display_account_id_debug( $account_id ): void { echo ' <br />ID: ' . esc_html( $account_id ?? 'no_id' ); } /** * Display the expiration information in debug mode. * * @since 1.9.5 * * @param array $account The account information containing the 'expires_in' timestamp. */ protected function display_expires_in_debug( array $account ): void { if ( empty( $account['expires_in'] ) ) { return; } $valid_until_timestamp = $account['expires_in']; if ( $valid_until_timestamp > time() ) { $format = sprintf( '%s \a\t %s', get_option( 'date_format' ), get_option( 'time_format' ) ); $valid_until = wpforms_datetime_format( $valid_until_timestamp, $format, true ); echo ' <br />Valid until: ' . esc_html( $valid_until ?? 'no_valid_until' ); } } /** * Any new connection should be added. * So display the content of that. * * @since 1.4.7 */ protected function display_add_new() { ?> <p class="wpforms-settings-provider-accounts-toggle"> <a class="wpforms-btn wpforms-btn-md wpforms-btn-light-grey" href="#" data-provider="<?php echo esc_attr( $this->core->slug ); ?>"> <i class="fa fa-plus"></i> <?php esc_html_e( 'Add New Account', 'wpforms-lite' ); ?> </a> </p> <div class="wpforms-settings-provider-accounts-connect"> <form> <p class="wpforms-settings-provider-accounts-connect-general-description"> <?php esc_html_e( 'Please fill out all of the fields below to add your new provider account.', 'wpforms-lite' ); ?> </p> <div class="wpforms-settings-provider-accounts-connect-fields"> <?php $this->display_add_new_connection_fields(); ?> </div> <?php $this->display_add_new_connection_submit_button(); ?> </form> </div> <?php } /** * Some providers may or may not have fields. * * @since 1.4.7 */ protected function display_add_new_connection_fields() { } /** * Some providers may modify the form button and add their form handler. * * @since 1.7.4 */ protected function display_add_new_connection_submit_button() { /* translators: %s - provider name. */ $title = sprintf( __( 'Connect to %s', 'wpforms-lite' ), $this->core->name ); ?> <button type="submit" class="wpforms-btn wpforms-btn-md wpforms-btn-orange wpforms-settings-provider-connect" data-provider="<?php echo esc_attr( $this->core->slug ); ?>" title="<?php echo esc_attr( $title ); ?>"> <?php echo esc_html( $title ); ?> </button> <?php } /** * AJAX to disconnect a provider from the settings integrations tab. * * @since 1.4.7 */ public function ajax_disconnect() { // Run a security check. if ( ! check_ajax_referer( 'wpforms-admin', 'nonce', false ) ) { wp_send_json_error( [ 'error_msg' => esc_html__( 'Your session expired. Please reload the page.', 'wpforms-lite' ), ] ); } // Check for permissions. if ( ! wpforms_current_user_can() ) { wp_send_json_error( [ 'error_msg' => esc_html__( 'You do not have permission.', 'wpforms-lite' ), ] ); } if ( empty( $_POST['provider'] ) || empty( $_POST['key'] ) ) { wp_send_json_error( [ 'error_msg' => esc_html__( 'Missing data.', 'wpforms-lite' ), ] ); } $providers = wpforms_get_providers_options(); if ( ! empty( $providers[ $_POST['provider'] ][ $_POST['key'] ] ) ) { unset( $providers[ $_POST['provider'] ][ $_POST['key'] ] ); update_option( 'wpforms_providers', $providers ); wp_send_json_success(); } else { wp_send_json_error( [ 'error_msg' => esc_html__( 'Connection missing.', 'wpforms-lite' ), ] ); } } /** * AJAX to add a provider from the settings integrations tab. * * @since 1.4.7 */ public function ajax_connect() { // Run a security check. if ( ! check_ajax_referer( 'wpforms-admin', 'nonce', false ) ) { wp_send_json_error( [ 'error_msg' => esc_html__( 'Your session expired. Please reload the page.', 'wpforms-lite' ), ] ); } // Check for permissions. if ( ! wpforms_current_user_can() ) { wp_send_json_error( [ 'error_msg' => esc_html__( 'You do not have permissions.', 'wpforms-lite' ), ] ); } if ( empty( $_POST['data'] ) ) { wp_send_json_error( [ 'error_msg' => esc_html__( 'Missing required data in payload.', 'wpforms-lite' ), ] ); } } } Providers/Provider/Process.php 0000644 00000004441 15174710275 0012456 0 ustar 00 <?php namespace WPForms\Providers\Provider; /** * Class Process handles entries processing using the provider settings and configuration. * * @since 1.4.7 */ abstract class Process { /** * Get the Core loader class of a provider. * * @since 1.4.7 * * @var Core */ protected $core; /** * Array of form fields. * * @since 1.4.7 * * @var array */ protected $fields = []; /** * Submitted form content. * * @since 1.4.7 * * @var array */ protected $entry = []; /** * Form data and settings. * * @since 1.4.7 * * @var array */ protected $form_data = []; /** * ID of a saved entry. * * @since 1.4.7 * * @var int */ protected $entry_id; /** * Process constructor. * * @since 1.4.7 * * @param Core $core Provider core class. */ public function __construct( Core $core ) { $this->core = $core; } /** * Receive all wpforms_process_complete params and do the actual processing. * * @since 1.4.7 * * @param array $fields Array of form fields. * @param array $entry Submitted form content. * @param array $form_data Form data and settings. * @param int $entry_id ID of a saved entry. */ abstract public function process( $fields, $entry, $form_data, $entry_id ); /** * Process conditional logic for a connection. * * @since 1.4.7 * * @param array $fields Array of form fields. * @param array $form_data Form data and settings. * @param array $connection All connection data. * * @return bool */ protected function process_conditionals( $fields, $form_data, $connection ) { if ( empty( $connection['conditional_logic'] ) || empty( $connection['conditionals'] ) || ! function_exists( 'wpforms_conditional_logic' ) ) { return true; } if ( ! wpforms()->is_pro() ) { return true; } $process = wpforms_conditional_logic()->process( $fields, $form_data, $connection['conditionals'] ); if ( ! empty( $connection['conditional_type'] ) && $connection['conditional_type'] === 'stop' ) { $process = ! $process; } return $process; } /** * Get provider options, saved on Settings > Integrations page. * * @since 1.4.7 * * @return array */ protected function get_options() { return wpforms_get_providers_options( $this->core->slug ); } } Loader.php 0000644 00000044555 15174710275 0006511 0 ustar 00 <?php namespace WPForms; /** * WPForms Class Loader. * * @since 1.5.8 */ class Loader { /** * Classes to register. * * @since 1.5.8 * * @var array */ private $classes = []; /** * Loader init. * * @since 1.5.8 */ public function init(): void { $this->populate_classes(); wpforms()->register_bulk( $this->classes ); } /** * Populate the classes to register. * * @since 1.5.8 */ protected function populate_classes(): void { $this->populate_common(); $this->populate_frontend(); $this->populate_admin(); $this->populate_caches(); $this->populate_fields(); $this->populate_forms_overview(); $this->populate_entries(); $this->populate_builder(); $this->populate_db(); $this->populate_migrations(); $this->populate_capabilities(); $this->populate_tasks(); $this->populate_forms(); $this->populate_smart_tags(); $this->populate_logger(); $this->populate_education(); $this->populate_robots(); $this->populate_anti_spam(); } /** * Populate common classes. * * @since 1.8.6 */ private function populate_common(): void { $this->classes[] = [ 'name' => 'API', 'id' => 'api', ]; $this->classes[] = [ 'name' => 'Emails\Summaries', ]; } /** * Populate the Forms related classes. * * @since 1.6.2 */ private function populate_forms(): void { $this->classes[] = [ 'name' => 'Forms\Preview', 'id' => 'preview', ]; $this->classes[] = [ 'name' => 'Forms\Token', 'id' => 'token', ]; $this->classes[] = [ 'name' => 'Forms\Honeypot', 'id' => 'honeypot', ]; $this->classes[] = [ 'name' => 'Forms\Akismet', 'id' => 'akismet', ]; $this->classes[] = [ 'name' => 'Forms\Submission', 'id' => 'submission', 'hook' => false, 'run' => false, ]; $this->classes[] = [ 'name' => 'Forms\Locator', 'id' => 'locator', ]; $this->classes[] = [ 'name' => 'Forms\IconChoices', 'id' => 'icon_choices', ]; $this->classes[] = [ 'name' => 'Forms\AntiSpam', 'id' => 'anti_spam', ]; } /** * Populate Frontend-related classes. * * @since 1.8.1 */ private function populate_frontend(): void { $this->classes[] = [ 'name' => 'Frontend\Address', 'id' => 'address', ]; $this->classes[] = [ 'name' => 'Frontend\Amp', 'id' => 'amp', ]; $this->classes[] = [ 'name' => 'Frontend\Captcha', 'id' => 'captcha', ]; $this->classes[] = [ 'name' => 'Frontend\CSSVars', 'id' => 'css_vars', ]; $this->classes[] = [ 'name' => 'Frontend\Classic', 'id' => 'frontend_classic', ]; $this->classes[] = [ 'name' => 'Frontend\Modern', 'id' => 'frontend_modern', ]; $this->classes[] = [ 'name' => 'Frontend\Frontend', 'id' => 'frontend', ]; } /** * Populate Admin-related classes. * * @since 1.6.0 */ private function populate_admin(): void { array_push( $this->classes, [ 'name' => 'Admin\Notice', 'id' => 'notice', ], [ 'name' => 'Admin\Revisions', 'id' => 'revisions', 'hook' => 'admin_init', ], [ 'name' => 'Admin\Addons\AddonsCache', 'id' => 'addons_cache', ], [ 'name' => 'Admin\CoreInfoCache', 'id' => 'core_info_cache', ], [ 'name' => 'Admin\Addons\Addons', 'id' => 'addons', ], [ 'name' => 'Admin\AdminBarMenu', 'hook' => 'init', ], [ 'name' => 'Admin\Notifications\Notifications', 'id' => 'notifications', ], [ 'name' => 'Admin\Entries\Handler', 'hook' => 'admin_init', ], [ 'name' => 'Admin\Pages\Templates', 'id' => 'templates_page', 'hook' => 'admin_init', ], [ 'name' => 'Admin\Forms\UserTemplates', 'id' => 'user_templates', ], [ 'name' => 'Admin\Forms\Page', 'id' => 'forms_overview', ], [ 'name' => 'Admin\Challenge', 'id' => 'challenge', ], [ 'name' => 'Admin\FormEmbedWizard', 'hook' => 'admin_init', 'id' => 'form_embed_wizard', ], [ 'name' => 'Admin\SiteHealth', 'hook' => 'admin_init', ], [ 'name' => 'Admin\Settings\ModernMarkup', 'hook' => 'admin_init', ], [ 'name' => 'Admin\Settings\Email', 'hook' => 'admin_init', ], [ 'name' => 'Admin\Settings\Captcha\Page', 'hook' => 'admin_init', ], [ 'name' => 'Admin\Settings\Payments', 'hook' => 'admin_init', ], [ 'name' => 'Admin\Tools\Tools', 'hook' => 'current_screen', ], [ 'name' => 'Admin\Payments\Payments', 'hook' => 'init', ], [ 'name' => 'Admin\Payments\Views\Overview\Ajax', 'hook' => 'admin_init', 'run' => 'hooks', 'condition' => wpforms_is_admin_ajax(), ], [ 'name' => 'Admin\Tools\Importers', 'hook' => 'admin_init', 'run' => 'load', 'condition' => wp_doing_ajax(), ], [ 'name' => 'Admin\Pages\Addons', 'id' => 'addons_page', ], [ 'name' => 'Admin\Pages\ConstantContact', 'hook' => 'admin_init', ], [ 'name' => 'Admin\Pages\PrivacyCompliance', 'hook' => 'admin_init', ], [ 'name' => 'Admin\Pages\SugarCalendar', 'hook' => 'admin_init', ], [ 'name' => 'Admin\Pages\Duplicator', 'hook' => 'admin_init', ], [ 'name' => 'Admin\Pages\UncannyAutomator', 'hook' => 'admin_init', ], [ 'name' => 'Forms\Fields\Richtext\EntryViewContent', ], [ 'name' => 'Admin\DashboardWidget', 'hook' => wpforms()->is_pro() ? 'admin_init' : 'init', ], [ 'name' => 'Emails\Preview', 'hook' => 'admin_init', ], [ 'name' => 'Admin\Addons\GoogleSheets', 'hook' => 'admin_init', ], [ 'name' => 'Admin\PluginList', 'id' => 'plugin_list', 'hook' => 'admin_init', ], [ 'name' => 'Admin\Splash\SplashScreen', 'id' => 'splash_screen', 'hook' => 'admin_init', ], [ 'name' => 'Admin\Splash\SplashCache', 'id' => 'splash_cache', 'hook' => 'plugins_loaded', ], [ 'name' => 'Admin\Splash\SplashUpgrader', 'id' => 'splash_upgrader', 'hook' => 'plugins_loaded', ] ); } /** * Populate Caches related classes. * * @since 1.8.7 */ private function populate_caches(): void { array_push( $this->classes, [ 'name' => 'LicenseApi\PluginUpdateCache', 'id' => 'license_api_plugin_update_cache', ], [ 'name' => 'LicenseApi\ValidateKeyCache', 'id' => 'license_api_validate_key_cache', ] ); } /** * Populate Fields related classes. * * @since 1.8.2 * * @noinspection ClassConstantCanBeUsedInspection */ private function populate_fields(): void { // Fancy fields. $this->classes[] = [ 'name' => 'Forms\Fields\Address\Field', 'hook' => 'init', ]; $this->classes[] = [ 'name' => 'Forms\Fields\Content\Field', 'hook' => 'init', ]; $this->classes[] = [ 'name' => 'Forms\Fields\DateTime\Field', 'hook' => 'init', ]; $this->classes[] = [ 'name' => 'Forms\Fields\Divider\Field', 'hook' => 'init', ]; $this->classes[] = [ 'name' => 'Forms\Fields\FileUpload\Field', 'hook' => 'init', ]; $this->classes[] = [ 'name' => 'Forms\Fields\Hidden\Field', 'hook' => 'init', ]; $this->classes[] = [ 'name' => 'Forms\Fields\Html\Field', 'hook' => 'init', ]; $this->classes[] = [ 'name' => 'Forms\Fields\Phone\Field', 'hook' => 'init', ]; $this->classes[] = [ 'name' => 'Forms\Fields\EntryPreview\Field', 'hook' => 'init', ]; $this->classes[] = [ 'name' => 'Forms\Fields\Password\Field', 'hook' => 'init', ]; $this->classes[] = [ 'name' => 'Forms\Fields\CreditCard\Field', 'hook' => 'init', ]; $this->classes[] = [ 'name' => 'Forms\Fields\Rating\Field', 'hook' => 'init', ]; $this->classes[] = [ 'name' => 'Forms\Fields\Url\Field', 'hook' => 'init', ]; $this->classes[] = [ 'name' => 'Forms\Fields\Richtext\Field', 'hook' => 'init', ]; $this->classes[] = [ 'name' => 'Forms\Fields\Pagebreak\Field', 'hook' => 'init', ]; $this->classes[] = [ 'name' => 'Forms\Fields\CustomCaptcha\Field', ]; $this->classes[] = [ 'name' => 'Forms\Fields\Layout\Field', 'hook' => 'init', ]; $this->classes[] = [ 'name' => 'Forms\Fields\Layout\Process', 'hook' => 'init', ]; $this->classes[] = [ 'name' => 'Forms\Fields\Layout\Notifications', 'hook' => 'init', ]; $this->classes[] = [ 'name' => 'Forms\Fields\Repeater\Field', ]; $this->classes[] = [ 'name' => 'Forms\Fields\Camera\Field', 'hook' => 'init', ]; $this->classes[] = [ 'name' => 'Forms\Fields\Repeater\Process', 'id' => 'repeater_process', 'hook' => 'init', ]; $this->classes[] = [ 'name' => 'Forms\Fields\Repeater\Notifications', 'hook' => 'init', ]; // Payment fields. $this->classes[] = [ 'name' => 'Forms\Fields\PaymentCheckbox\Field', 'hook' => 'init', ]; $this->classes[] = [ 'name' => 'Forms\Fields\PaymentMultiple\Field', 'hook' => 'init', ]; $this->classes[] = [ 'name' => 'Forms\Fields\PaymentSelect\Field', 'hook' => 'init', ]; $this->classes[] = [ 'name' => 'Forms\Fields\PaymentSingle\Field', 'hook' => 'init', ]; $this->classes[] = [ 'name' => 'Forms\Fields\PaymentTotal\Field', 'hook' => 'init', ]; // Addon fields in Lite. $this->classes[] = [ 'name' => 'Forms\Fields\Addons\Coupon\Field', 'addon_class' => 'WPFormsCoupons\Field', 'addon_slug' => 'coupons', ]; $this->classes[] = [ 'name' => 'Forms\Fields\Addons\Signature\Field', 'addon_class' => 'WPFormsSignatures\Fields\Signature', 'addon_slug' => 'signatures', ]; $this->classes[] = [ 'name' => 'Forms\Fields\Addons\LikertScale\Field', 'addon_class' => 'WPFormsSurveys\Fields\LikertScale\Field', 'addon_slug' => 'surveys-polls', ]; $this->classes[] = [ 'name' => 'Forms\Fields\Addons\NetPromoterScore\Field', 'addon_class' => 'WPFormsSurveys\Fields\NetPromoterScore\Field', 'addon_slug' => 'surveys-polls', ]; $this->classes[] = [ 'name' => 'Forms\Fields\Addons\Map\Field', 'addon_class' => 'WPFormsGeolocation\Forms\Field', 'addon_slug' => 'geolocation', ]; } /** * Populate Forms Overview admin page related classes. * * @since 1.7.5 */ private function populate_forms_overview(): void { if ( ! wpforms_is_admin_page( 'overview' ) && ! wpforms_is_admin_ajax() ) { return; } array_push( $this->classes, [ 'name' => 'Admin\Forms\Ajax\Columns', 'id' => 'forms_columns_ajax', ], [ 'name' => 'Admin\Forms\Ajax\Tags', 'id' => 'forms_tags_ajax', ], [ 'name' => 'Admin\Forms\Search', 'id' => 'forms_search', ], [ 'name' => 'Admin\Forms\Views', 'id' => 'forms_views', ], [ 'name' => 'Admin\Forms\BulkActions', 'id' => 'forms_bulk_actions', ], [ 'name' => 'Admin\Forms\Tags', 'id' => 'forms_tags', ] ); } /** * Populate Entries related classes. * * @since 1.8.6 */ private function populate_entries(): void { array_push( $this->classes, [ 'name' => 'Admin\Entries\PageOptions', 'id' => 'entries_page_options', ], [ 'name' => 'Admin\Entries\Page', 'id' => 'entries_list_page', 'hook' => 'admin_init', ], [ 'name' => 'Admin\Entries\Overview\Page', 'id' => 'entries_overview', ], [ 'name' => 'Admin\Entries\Overview\Ajax', 'hook' => 'admin_init', 'run' => 'hooks', 'condition' => wpforms_is_admin_ajax(), ], [ 'name' => 'Admin\Entries\Ajax\Columns', 'id' => 'entries_columns_ajax', ], [ 'name' => 'Admin\Entries\Edit', 'id' => 'entries_edit', 'hook' => 'admin_init', ], [ 'name' => 'Admin\Entries\Export\Export', 'id' => 'entries_export', 'hook' => 'init', ], [ 'name' => 'Admin\Entries\DefaultScreen', 'hook' => 'admin_init', ] ); } /** * Populate Form Builder related classes. * * @since 1.6.8 */ private function populate_builder(): void { array_push( $this->classes, [ 'name' => 'Admin\Builder\HelpCache', 'id' => 'builder_help_cache', ], [ 'name' => 'Admin\Builder\Help', 'id' => 'builder_help', ], [ 'name' => 'Admin\Builder\Shortcuts', ], [ 'name' => 'Admin\Builder\TemplatesCache', 'id' => 'builder_templates_cache', ], [ 'name' => 'Admin\Builder\TemplateSingleCache', 'id' => 'builder_template_single', ], [ 'name' => 'Admin\Builder\Templates', 'id' => 'builder_templates', ], [ 'name' => 'Admin\Builder\AntiSpam', 'hook' => 'wpforms_builder_init', ], [ 'name' => 'Admin\Builder\Settings\Themes', 'hook' => 'wpforms_builder_init', ], [ 'name' => 'Admin\Builder\Notifications\Advanced\EmailTemplate', 'hook' => 'wpforms_builder_init', ], [ 'name' => 'Admin\Builder\ContextMenu', 'hook' => 'wpforms_builder_init', 'id' => 'context_menu', ], [ 'name' => 'Admin\Builder\ImageUpload', 'hook' => 'wpforms_builder_init', 'id' => 'image_upload', ], [ 'name' => 'Admin\Builder\Notifications\Advanced\Settings', ], [ 'name' => 'Admin\Builder\Notifications\Advanced\FileUploadAttachment', ], [ 'name' => 'Admin\Builder\Notifications\Advanced\EntryCsvAttachment', ], [ 'name' => 'Admin\Builder\Ajax\PanelLoader', ], [ 'name' => 'Admin\Builder\Addons', ], [ 'name' => 'Admin\Builder\Ajax\SaveForm', 'id' => 'builder_save_form', ], [ 'name' => 'Admin\Builder\Payments', 'hook' => 'wpforms_builder_init', 'id' => 'builder_payments', ] ); } /** * Populate database classes. * * @since 1.8.2 */ private function populate_db(): void { array_push( $this->classes, [ 'name' => 'Db\Payments\Payment', 'id' => 'payment', 'hook' => false, 'run' => false, ], [ 'name' => 'Db\Payments\Meta', 'id' => 'payment_meta', 'hook' => false, 'run' => false, ], [ 'name' => 'Db\Payments\Queries', 'id' => 'payment_queries', 'hook' => false, 'run' => false, ], [ 'name' => 'Db\Files\ProtectedFiles', 'id' => 'protected_files', 'hook' => false, 'run' => false, ], [ 'name' => 'Db\Files\Restrictions', 'id' => 'file_restrictions', 'hook' => false, 'run' => false, ] ); } /** * Populate migration classes. * * @since 1.5.9 */ private function populate_migrations(): void { $this->classes[] = [ 'name' => 'Migrations\Migrations', 'hook' => 'plugins_loaded', ]; } /** * Populate access management (capabilities) classes. * * @since 1.5.8 */ private function populate_capabilities(): void { array_push( $this->classes, [ 'name' => 'Access\Capabilities', 'id' => 'access', 'hook' => 'plugins_loaded', ], [ 'name' => 'Access\Integrations', ], [ 'name' => 'Access\File', 'hook' => 'init', 'condition' => ! is_admin(), ], [ 'name' => 'Admin\Settings\Access', 'condition' => is_admin(), ] ); } /** * Populate tasks related classes. * * @since 1.5.9 */ private function populate_tasks(): void { array_push( $this->classes, [ 'name' => 'Tasks\Tasks', 'id' => 'tasks', 'hook' => 'init', ], [ 'name' => 'Tasks\Meta', 'id' => 'tasks_meta', 'hook' => false, 'run' => false, ] ); } /** * Populate smart tags loaded classes. * * @since 1.6.7 */ private function populate_smart_tags(): void { $this->classes[] = [ 'name' => 'SmartTags\SmartTags', 'id' => 'smart_tags', 'run' => 'hooks', ]; } /** * Populate logger-loaded classes. * * @since 1.6.3 */ private function populate_logger(): void { $this->classes[] = [ 'name' => 'Logger\Log', 'id' => 'log', 'hook' => false, 'run' => 'hooks', ]; } /** * Populate education-related classes. * * @since 1.6.6 */ private function populate_education(): void { // Kill switch. /** * Filters admin education status. * * @since 1.6.6 * * @param bool $status Current admin education status. * * @return bool */ if ( ! apply_filters( 'wpforms_admin_education', true ) ) { // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName return; } // Education core classes. array_push( $this->classes, [ 'name' => 'Admin\Education\Core', 'id' => 'education', ], [ 'name' => 'Admin\Education\Fields', 'id' => 'education_fields', ], [ 'name' => 'Admin\Education\Admin\Settings\SMTP', 'id' => 'education_smtp_notice', ], [ 'name' => 'Admin\Education\Admin\EditPost', 'hook' => 'load-edit.php', ], [ 'name' => 'Admin\Education\Admin\EditPost', 'hook' => 'load-post-new.php', ], [ 'name' => 'Admin\Education\Admin\EditPost', 'hook' => 'load-post.php', ], [ 'name' => 'Admin\Education\Admin\EditPost', 'hook' => 'load-site-editor.php', ], [ 'name' => 'Admin\Education\Pointers\Payment', 'hook' => 'admin_init', 'priority' => 20, ] ); // Education features classes. $features = [ 'LiteConnect', 'Builder\Calculations', 'Builder\Captcha', 'Builder\Fields', 'Builder\Settings', 'Builder\Providers', 'Builder\Payments', 'Builder\DidYouKnow', 'Builder\Geolocation', 'Builder\Quiz', 'Builder\Confirmations', 'Builder\Notifications', 'Builder\PDF', 'Admin\DidYouKnow', 'Admin\Settings\Integrations', 'Admin\Settings\Geolocation', 'Admin\NoticeBar', 'Admin\Entries\Geolocation', 'Admin\Entries\UserJourney', ]; foreach ( $features as $feature ) { $this->classes[] = [ 'name' => 'Admin\Education\\' . $feature, ]; } } /** * Populate robots loaded class. * * @since 1.7.0 */ private function populate_robots(): void { $this->classes[] = [ 'name' => 'Robots', 'run' => 'hooks', ]; } /** * Populate AntiSpam loaded classes. * * @since 1.7.8 */ private function populate_anti_spam(): void { array_push( $this->classes, [ 'name' => 'AntiSpam\CountryFilter', 'id' => 'antispam_country_filter', 'hook' => 'init', ], [ 'name' => 'AntiSpam\KeywordFilter', 'id' => 'antispam_keyword_filter', 'hook' => 'init', ], [ 'name' => 'AntiSpam\SpamEntry', 'id' => 'spam_entry', 'hook' => 'init', ] ); } } Logger/Record.php 0000644 00000010437 15174710275 0007730 0 ustar 00 <?php namespace WPForms\Logger; /** * Class Record. * * @since 1.6.3 */ class Record { /** * Record ID. * * @since 1.6.3 * * @var int */ private $id; /** * Record title. * * @since 1.6.3 * * @var string */ private $title; /** * Record message. * * @since 1.6.3 * * @var string */ private $message; /** * Array, string, or string separated by commas types. * * @since 1.6.3 * * @var array|string */ private $types; /** * Datetime of creating record. * * @since 1.6.3 * * @var string */ private $create_at; /** * Record form ID. * * @since 1.6.3 * * @var int */ private $form_id; /** * Record entry ID. * * @since 1.6.3 * * @var int */ private $entry_id; /** * Record user ID. * * @since 1.6.3 * * @var int */ private $user_id; /** * Record constructor. * * @since 1.6.3 * * @param int $id Record ID. * @param string $title Record title. * @param string $message Record message. * @param array|string $types Array, string, or string separated by commas types. * @param string $create_at Datetime of creating record. * @param int $form_id Record form ID. * @param int $entry_id Record entry ID. * @param int $user_id Record user ID. */ public function __construct( $id, $title, $message, $types, $create_at, $form_id = 0, $entry_id = 0, $user_id = 0 ) { $this->id = $id; $this->title = $title; $this->message = $message; $this->types = $types; $this->create_at = strtotime( $create_at ); $this->form_id = $form_id; $this->entry_id = $entry_id; $this->user_id = $user_id; } /** * Get record ID. * * @since 1.6.3 * * @return int */ public function get_id() { return $this->id; } /** * Get record title. * * @since 1.6.3 * * @return string */ public function get_title() { return $this->title; } /** * Get record message. * * @since 1.6.3 * * @return string */ public function get_message() { return $this->message; } /** * Get record types. * * @since 1.6.3 * * @param string $view Keys or labels. * * @return array */ public function get_types( $view = 'key' ) { $this->types = is_array( $this->types ) ? $this->types : explode( ',', $this->types ); if ( $view === 'label' ) { return array_intersect_key( Log::get_log_types(), array_flip( $this->types ) ); } return $this->types; } /** * Get date of creating record. * * @since 1.6.3 * * @param string $format Date format full|short|default sql format. * * @return string */ public function get_date( $format = 'short' ) { switch ( $format ) { case 'short': $date = wpforms_date_format( $this->create_at, '', true ); break; case 'full': $date = wpforms_datetime_format( $this->create_at, '', true ); break; case 'sql': $date = wpforms_datetime_format( $this->create_at, 'Y-m-d H:i:s' ); break; case 'sql-local': $date = wpforms_datetime_format( $this->create_at, 'Y-m-d H:i:s', true ); break; default: $date = ''; break; } return $date; } /** * Get form ID. * * @since 1.6.3 * * @return int */ public function get_form_id() { return $this->form_id; } /** * Get entry ID. * * @since 1.6.3 * * @return int */ public function get_entry_id() { return $this->entry_id; } /** * Get user ID. * * @since 1.6.3 * * @return int */ public function get_user_id() { return $this->user_id; } /** * Create new record. * * @since 1.6.3 * * @param string $title Record title. * @param string $message Record message. * @param array|string $types Array, string, or string separated by commas types. * @param int $form_id Record form ID. * @param int $entry_id Record entry ID. * @param int $user_id Record user ID. * * @return Record */ public static function create( $title, $message, $types, $form_id = 0, $entry_id = 0, $user_id = 0 ) { return new Record( 0, sanitize_text_field( $title ), wp_kses( $message, [ 'pre' => [] ] ), $types, gmdate( 'Y-m-d H:i:s' ), absint( $form_id ), absint( $entry_id ), absint( $user_id ) ); } } Logger/Records.php 0000644 00000003702 15174710275 0010110 0 ustar 00 <?php namespace WPForms\Logger; use Iterator; use Countable; /** * Class Records. * * @since 1.6.3 */ class Records implements Countable, Iterator { /** * Iterator position. * * @since 1.6.3 * * @var int */ private $iterator_position = 0; /** * List of log records. * * @since 1.6.3 * * @var array */ private $list = []; /** * Return the current element. * * @since 1.6.3 * * @return \WPForms\Logger\Record|null Return null when no items in collection. */ #[\ReturnTypeWillChange] public function current() { return $this->valid() ? $this->list[ $this->iterator_position ] : null; } /** * Move forward to next element. * * @since 1.6.3 */ #[\ReturnTypeWillChange] public function next() { ++ $this->iterator_position; } /** * Return the key of the current element. * * @since 1.6.3 * * @return int */ #[\ReturnTypeWillChange] public function key() { return $this->iterator_position; } /** * Checks if current position is valid. * * @since 1.6.3 * * @return bool */ #[\ReturnTypeWillChange] public function valid() { return isset( $this->list[ $this->iterator_position ] ); } /** * Rewind the Iterator to the first element. * * @since 1.6.3 */ #[\ReturnTypeWillChange] public function rewind() { $this->iterator_position = 0; } /** * Count number of Record in a Queue. * * @since 1.6.3 * * @return int */ #[\ReturnTypeWillChange] public function count() { return count( $this->list ); } /** * Push record to list. * * @since 1.6.3 * * @param \WPForms\Logger\Record $record Record. */ #[\ReturnTypeWillChange] public function push( $record ) { if ( ! is_a( $record, '\WPForms\Logger\Record' ) ) { return; } $this->list[] = $record; } /** * Clear collection. * * @since 1.6.3 */ #[\ReturnTypeWillChange] public function clear() { $this->list = []; $this->iterator_position = 0; } } Logger/Log.php 0000644 00000012432 15174710275 0007230 0 ustar 00 <?php // phpcs:ignore Generic.Commenting.DocComment.MissingShort /** @noinspection PhpIllegalPsrClassPathInspection */ namespace WPForms\Logger; /** * Class Log. * * @since 1.6.3 */ class Log { /** * Repository. * * @since 1.6.3 * * @var Repository */ private $repository; /** * List table. * * @since 1.6.3 * * @var ListTable */ private $list_table; /** * Register log hooks. * * @since 1.6.3 */ public function hooks() { $this->repository = new Repository(); add_action( 'shutdown', [ $this->repository, 'save' ] ); add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_styles' ] ); add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_scripts' ] ); add_action( 'wp_ajax_wpforms_get_log_record', [ $this, 'get_record' ] ); } /** * Enqueue styles. * * @since 1.6.3 */ public function enqueue_styles() { if ( ! $this->is_logger_page() ) { return; } $min = wpforms_get_min_suffix(); wp_enqueue_style( 'wpforms-tools-logger', WPFORMS_PLUGIN_URL . "assets/css/logger{$min}.css", [], WPFORMS_VERSION ); } /** * Enqueue styles. * * @since 1.6.3 */ public function enqueue_scripts() { if ( ! $this->is_logger_page() ) { return; } $min = wpforms_get_min_suffix(); wp_enqueue_script( 'wpforms-tools-logger', WPFORMS_PLUGIN_URL . "assets/js/admin/logger/logger{$min}.js", [ 'jquery', 'jquery-confirm', 'wp-util' ], WPFORMS_VERSION, true ); } /** * Get log types. * * @since 1.6.3 * * @return array */ public static function get_log_types() { return [ 'conditional_logic' => esc_html__( 'Conditional Logic', 'wpforms-lite' ), 'entry' => esc_html__( 'Entries', 'wpforms-lite' ), 'error' => esc_html__( 'Errors', 'wpforms-lite' ), 'log' => esc_html__( 'Log', 'wpforms-lite' ), 'payment' => esc_html__( 'Payment', 'wpforms-lite' ), 'provider' => esc_html__( 'Providers', 'wpforms-lite' ), 'security' => esc_html__( 'Security', 'wpforms-lite' ), 'spam' => esc_html__( 'Spam', 'wpforms-lite' ), 'translation' => esc_html__( 'Translation', 'wpforms-lite' ), ]; } /** * Determine if it is a Logs page. * * @since 1.6.3 * * @return bool */ private function is_logger_page() { return wpforms_is_admin_page( 'tools', 'logs' ); } /** * Create new record. * * @since 1.6.3 * * @param string $title Record title. * @param string $message Record message. * @param array|string $types Array, string, or string separated by comma types. * @param int $form_id Record form ID. * @param int $entry_id Record entry ID. * @param int $user_id Record user ID. */ public function add( $title, $message, $types, $form_id = 0, $entry_id = 0, $user_id = 0 ) { $this->repository->add( $title, $message, $types, $form_id, $entry_id, $user_id ); } /** * Check if the database table exists. * Used in \WPForms_Install::maybe_create_tables() during plugin installation. * * @since 1.8.7 * * @return bool */ public function table_exists(): bool { // phpcs:ignore WPForms.Formatting.EmptyLineBeforeReturn.RemoveEmptyLineBeforeReturnStatement return $this->repository->table_exists(); } /** * Create table for logs. * * @since 1.6.3 */ public function create_table() { if ( $this->table_exists() ) { return; } $this->repository->create_table(); } /** * Get ListView. * * @since 1.6.3 * * @return ListTable */ public function get_list_table() { // phpcs:ignore WPForms.PHP.HooksMethod.InvalidPlaceForAddingHooks if ( ! $this->list_table ) { $this->list_table = new ListTable( $this->repository ); add_action( 'admin_print_scripts', [ $this->list_table, 'popup_template' ] ); } return $this->list_table; } /** * Json config for detail information about log record. * * @since 1.6.3 */ public function get_record() { if ( ! check_ajax_referer( 'wpforms-admin', 'nonce', false ) || ! wpforms_current_user_can() ) { wp_send_json_error( esc_html__( 'You do not have permission.', 'wpforms-lite' ) ); } $id = filter_input( INPUT_GET, 'recordId', FILTER_VALIDATE_INT ); if ( ! $id ) { wp_send_json_error( esc_html__( 'Record ID not found', 'wpforms-lite' ), 404 ); } $item = $this->repository->record( $id ); if ( $item === null ) { wp_send_json_error( esc_html__( 'No such record.', 'wpforms-lite' ), 404 ); } wp_send_json_success( [ 'ID' => absint( $item->get_id() ), 'title' => esc_html( $item->get_title() ), 'message' => wp_kses( $item->get_message(), [ 'pre' => [] ] ), 'types' => esc_html( implode( ', ', $item->get_types( 'label' ) ) ), 'create_at' => esc_html( $item->get_date( 'full' ) ), 'form_id' => absint( $item->get_form_id() ), 'entry_id' => absint( $item->get_entry_id() ), 'user_id' => absint( $item->get_user_id() ), 'form_url' => admin_url( sprintf( 'admin.php?page=wpforms-builder&view=fields&form_id=%d', absint( $item->get_form_id() ) ) ), 'entry_url' => admin_url( sprintf( 'admin.php?page=wpforms-entries&view=details&entry_id=%d', absint( $item->get_entry_id() ) ) ), 'user_url' => esc_url( get_edit_user_link( $item->get_user_id() ) ), ] ); } } Logger/RecordQuery.php 0000644 00000003507 15174710275 0010756 0 ustar 00 <?php // phpcs:ignore Generic.Commenting.DocComment.MissingShort /** @noinspection PhpIllegalPsrClassPathInspection */ namespace WPForms\Logger; /** * Class RecordQuery. * * @since 1.6.3 */ class RecordQuery { /** * Build query. * * @since 1.6.3 * * @param int $limit Query limit of records. * @param int $offset Offset of records. * @param string $search Search. * @param string $type Type of records. * * @return array */ public function get( $limit, $offset = 0, $search = '', $type = '' ) { global $wpdb; //phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared return (array) $wpdb->get_results( $this->build_query( $limit, $offset, $search, $type ) ); //phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared } /** * Build query. * * @since 1.6.3 * * @param int $limit Query limit of records. * @param int $offset Offset of records. * @param string $search Search. * @param string $type Type of records. * * @return string */ private function build_query( $limit, $offset = 0, $search = '', $type = '' ) { global $wpdb; $sql = 'SELECT SQL_CALC_FOUND_ROWS * FROM ' . Repository::get_table_name(); $where = []; if ( ! empty( $search ) ) { $where[] = $wpdb->prepare( '`title` REGEXP %s OR `message` REGEXP %s', $search, $search ); } if ( ! empty( $type ) ) { $where[] = $wpdb->prepare( '`types` REGEXP %s', $type ); } if ( $where ) { $sql .= ' WHERE ' . implode( ' AND ', $where ); } $sql .= ' ORDER BY `create_at` DESC, `id` DESC'; $sql .= $wpdb->prepare( ' LIMIT %d, %d', absint( $offset ), absint( $limit ) ); return $sql; } } Logger/Repository.php 0000644 00000013714 15174710275 0010672 0 ustar 00 <?php // phpcs:ignore Generic.Commenting.DocComment.MissingShort /** @noinspection PhpIllegalPsrClassPathInspection */ namespace WPForms\Logger; use WPForms\Helpers\DB; /** * Class Repository. * * @since 1.6.3 */ class Repository { /** * Cache key name for total logs. * * @since 1.6.3 */ const CACHE_TOTAL_KEY = 'wpforms_logs_total'; /** * Records query. * * @since 1.6.3 * * @var RecordQuery */ private $records_query; /** * Records. * * @since 1.6.3 * * @var Records */ private $records; /** * Get a not-limited total query. * * @since 1.6.4.1 * * @var int */ private $full_total; /** * Log constructor. * * @since 1.6.3 * @since 1.9.0 Removed the argument. */ public function __construct() { $this->full_total = false; $this->records_query = new RecordQuery(); $this->records = new Records(); } /** * Get log table name. * * @since 1.6.3 * * @return string */ public static function get_table_name(): string { global $wpdb; return $wpdb->prefix . 'wpforms_logs'; } /** * Create table in the database. * * @since 1.6.3 */ public function create_table() { global $wpdb; $table = self::get_table_name(); require_once ABSPATH . 'wp-admin/includes/upgrade.php'; $charset_collate = $wpdb->get_charset_collate(); $sql = "CREATE TABLE $table ( id BIGINT(20) NOT NULL AUTO_INCREMENT, title VARCHAR(255) NOT NULL, message LONGTEXT NOT NULL, types VARCHAR(255) NOT NULL, create_at DATETIME NOT NULL, form_id BIGINT(20), entry_id BIGINT(20), user_id BIGINT(20), PRIMARY KEY (id) ) $charset_collate;"; dbDelta( $sql ); } /** * Create new record. * * @since 1.6.3 * * @param string $title Record title. * @param string $message Record message. * @param array|string $types Array, string, or string separated by comma types. * @param int $form_id Record form ID. * @param int $entry_id Record entry ID. * @param int $user_id Record user ID. */ public function add( $title, $message, $types, $form_id, $entry_id, $user_id ) { $this->records->push( Record::create( $title, $message, $types, $form_id, $entry_id, $user_id ) ); } /** * Get records. * * @since 1.6.3 * * @param int $limit Query limit of records. * @param int $offset Offset of records. * @param string $search Search. * @param string $type Type of records. * * @return Records */ public function records( $limit, $offset = 0, $search = '', $type = '' ) { $data = $this->records_query->get( $limit, $offset, $search, $type ); $this->full_total = true; $records = new Records(); // As we got raw data, we need to convert to Record. foreach ( $data as $row ) { $records->push( $this->prepare_record( $row ) ); } return $records; } /** * Get record. * * @since 1.6.3 * * @param int $id Record ID. * * @return Record|null */ public function record( $id ) { global $wpdb; //phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $item = $wpdb->get_row( $wpdb->prepare( 'SELECT * FROM ' . self::get_table_name() . ' WHERE id = %d', //phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared absint( $id ) ) ); if ( $item ) { $item = $this->prepare_record( $item ); } return $item; } /** * Create record from DB row. * * @since 1.6.3 * * @param object $row Row from DB. * * @return Record */ private function prepare_record( $row ) { return new Record( absint( $row->id ), $row->title, $row->message, $row->types, $row->create_at, absint( $row->form_id ), absint( $row->entry_id ), absint( $row->user_id ) ); } /** * Save records to the database. * * @since 1.6.3 */ public function save() { global $wpdb; // We can't use the empty function because it doesn't work with a Countable object. if ( ! count( $this->records ) ) { return; } $sql = 'INSERT INTO ' . self::get_table_name() . ' ( `id`, `title`, `message`, `types`, `create_at`, `form_id`, `entry_id`, `user_id` ) VALUES '; foreach ( $this->records as $record ) { $sql .= $wpdb->prepare( '( NULL, %s, %s, %s, %s, %d, %d, %d ),', $record->get_title(), $record->get_message(), implode( ',', $record->get_types() ), $record->get_date( 'sql' ), $record->get_form_id(), $record->get_entry_id(), $record->get_user_id() ); } $sql = rtrim( $sql, ',' ); //phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared $wpdb->query( $sql ); //phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared wp_cache_delete( self::CACHE_TOTAL_KEY ); } /** * Check if the database table exists. * * @since 1.6.4 * * @return bool */ public function table_exists() { return DB::table_exists( self::get_table_name() ); } /** * Get total count of logs. * * @since 1.6.3 * * @return int */ public function get_total() { global $wpdb; $total = wp_cache_get( self::CACHE_TOTAL_KEY ); if ( ! $total ) { //phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared $total = $this->full_total ? $wpdb->get_var( 'SELECT FOUND_ROWS()' ) : $wpdb->get_var( 'SELECT COUNT( ID ) FROM ' . self::get_table_name() ); //phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared wp_cache_set( self::CACHE_TOTAL_KEY, $total, 'wpforms', DAY_IN_SECONDS ); } return absint( $total ); } /** * Clear all records in the Database. * * @since 1.6.3 */ public function clear_all() { global $wpdb; //phpcs:ignore WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared $wpdb->query( 'TRUNCATE TABLE ' . self::get_table_name() ); } } Logger/ListTable.php 0000644 00000030731 15174710275 0010374 0 ustar 00 <?php namespace WPForms\Logger; if ( ! defined( 'ABSPATH' ) ) { exit; } use WP_List_Table; if ( ! class_exists( 'WP_List_Table' ) ) { require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php'; } /** * Class ListTable. * * @since 1.6.3 */ class ListTable extends WP_List_Table { /** * Record Query. * * @since 1.6.3 * * @var Repository */ private $repository; /** * ListTable constructor. * * @since 1.6.3 * * @param Repository $repository Repository. */ public function __construct( $repository ) { $this->repository = $repository; parent::__construct( [ 'plural' => esc_html__( 'Logs', 'wpforms-lite' ), 'singular' => esc_html__( 'Log', 'wpforms-lite' ), ] ); $this->hooks(); add_screen_option( 'per_page', [ 'default' => $this->get_items_per_page( $this->get_per_page_option_name() ) ] ); set_screen_options(); } /** * Hooks. * * @since 1.7.5 */ private function hooks() { add_filter( 'set_screen_option_' . $this->get_per_page_option_name(), [ $this, 'set_items_per_page_option' ], 10, 3 ); } /** * Handles setting the items_per_page option for this screen. * * @since 1.7.5 * * @param mixed $status Default false (to skip saving the current option). * @param string $option Screen option name. * @param int $value Screen option value. * * @return int * @noinspection PhpUnusedParameterInspection */ public function set_items_per_page_option( $status, $option, $value ) { return $value; } /** * Whether the table has items to display or not. * * @since 1.6.3 * * @return bool */ public function has_items() { // We can't use the empty function because it doesn't work with the Countable object. return (bool) count( $this->items ); } /** * Prepares the list of items for displaying. * * @since 1.6.3 */ public function prepare_items() { $offset = $this->get_items_offset(); $search = $this->get_request_search_query(); $types = $this->get_items_type(); $per_page = $this->get_items_per_page( $this->get_per_page_option_name() ); $this->items = $this->repository->records( $per_page, $offset, $search, $types ); $total_items = $this->get_total(); $this->set_pagination_args( [ 'total_items' => $total_items, 'per_page' => $per_page, 'total_pages' => (int) ceil( $total_items / $per_page ), ] ); } /** * Return the type of records. * * @since 1.6.3 * * @return string */ private function get_items_type() { return filter_input( INPUT_GET, 'log_type', FILTER_SANITIZE_FULL_SPECIAL_CHARS ); } /** * Return the number of items to offset/skip for this current view. * * @since 1.6.3 * * @return int */ private function get_items_offset() { return $this->get_items_per_page( $this->get_per_page_option_name() ) * ( $this->get_pagenum() - 1 ); } /** * Return the search filter for this request, if any. * * @since 1.6.3 * * @return string */ private function get_request_search_query() { return filter_input( INPUT_GET, 's', FILTER_SANITIZE_FULL_SPECIAL_CHARS ); } /** * Column title. * * @since 1.6.3 * * @param Record $item List table item. * * @return string * @noinspection PhpUnused */ public function column_log_title( $item ) { return sprintf( '<a href="#" class="js-single-log-target" data-log-id="%1$d"><strong>%2$s</strong></a>', absint( $item->get_id() ), esc_html( $item->get_title() ) ); } /** * Column message. * * @since 1.6.3 * * @param Record $item List table item. * * @return string * @noinspection PhpUnused */ public function column_message( $item ) { $message = $item->get_message(); if ( preg_match( '/\[body].+{"error":"(.+)"}/i', $message, $m ) ) { $message = $m[1]; } if ( preg_match( '/\[error] => (.+)/i', $message, $m ) ) { $message = $m[1]; } return esc_html( $this->crop_message( $message ) ); } /** * Column form ID. * * @since 1.6.3 * * @param Record $item List table item. * * @return int * @noinspection PhpUnused */ public function column_form_id( $item ) { return absint( $item->get_form_id() ); } /** * Column types. * * @since 1.6.3 * * @param Record $item List table item. * * @return string * @noinspection PhpUnused */ public function column_types( $item ) { return esc_html( implode( ', ', $item->get_types( 'label' ) ) ); } /** * Column date. * * @since 1.6.3 * * @param Record $item List table item. * * @return string * @noinspection PhpUnused */ public function column_date( $item ) { return esc_html( $item->get_date( 'sql-local' ) ); } /** * Crop message for preview on list table. * * @since 1.6.3 * * @param string $message Message. * * @return string */ private function crop_message( $message ) { return wp_html_excerpt( $message, 97, '...' ); } /** * Prepares the _column_headers property which is used by WP_Table_List at rendering. * It merges the columns and the sortable columns. * * @since 1.6.3 */ private function prepare_column_headers() { $this->_column_headers = [ $this->get_columns(), get_hidden_columns( $this->screen ), [], ]; } /** * Return the columns' names for rendering. * * @since 1.6.3 * * @return array */ public function get_columns() { return [ 'log_title' => __( 'Log Title', 'wpforms-lite' ), 'message' => __( 'Message', 'wpforms-lite' ), 'form_id' => __( 'Form ID', 'wpforms-lite' ), 'types' => __( 'Types', 'wpforms-lite' ), 'date' => __( 'Date', 'wpforms-lite' ), ]; } /** * Header before log table. * * @since 1.6.3 */ private function header() { ?> <div class="wpforms-admin-content-header"> <h4 class="wp-heading-inline"><?php esc_html_e( 'View Logs', 'wpforms-lite' ); ?> <?php if ( $this->get_request_search_query() ) { ?> <span class="subtitle"> <?php printf( /* translators: %s - search query. */ esc_html__( 'Search results for "%s"', 'wpforms-lite' ), esc_html( $this->get_request_search_query() ) ); ?> </span> <?php } ?> </h4> <?php $this->hidden_fields(); $this->search_box( esc_html__( 'Search Logs', 'wpforms-lite' ), 'plugin' ); ?> </div> <?php } /** * Generate the table navigation above or below the table. * * @since 1.6.3 * * @param string $which Which position. */ protected function display_tablenav( $which ) { ?> <div class="tablenav <?php echo esc_attr( $which ); ?>"> <?php if ( $which === 'top' ) { $this->extra_tablenav( $which ); } $this->pagination( $which ); ?> <br class="clear" /> </div> <?php } /** * Table list actions. * * @since 1.6.3 * * @param string $which Position of navigation (top or bottom). */ protected function extra_tablenav( $which ) { if ( ! $this->get_total() ) { return; } $this->log_type_select(); $this->clear_all(); } /** * Clear all log records. * * @since 1.6.3 */ private function clear_all() { ?> <button name="clear-all" type="submit" class="button" value="1"><?php esc_html_e( 'Delete All Logs', 'wpforms-lite' ); ?></button> <?php } /** * Update URL when table showing. * _wp_http_referer is used only on bulk actions, we remove it to keep the $_GET shorter. * * @since 1.6.3 */ public function process_admin_ui() { $nonce = isset( $_REQUEST['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['_wpnonce'] ) ) : ''; if ( ! wp_verify_nonce( $nonce, 'wpforms-table-' . $this->_args['plural'] ) ) { return; } if ( empty( $_REQUEST['_wp_http_referer'] ) && empty( $_REQUEST['clear-all'] ) ) { return; } if ( ! empty( $_REQUEST['clear-all'] ) ) { $this->repository->clear_all(); } $uri = isset( $_SERVER['REQUEST_URI'] ) ? esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : ''; wp_safe_redirect( remove_query_arg( [ '_wp_http_referer', '_wpnonce', 'clear-all' ], $uri ) ); exit; } /** * Message to be displayed when there are no items. * * @since 1.6.3 */ public function no_items() { esc_html_e( 'No logs found.', 'wpforms-lite' ); } /** * Print all hidden fields. * * @since 1.6.3 */ private function hidden_fields() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended foreach ( $_GET as $key => $value ) { if ( $key[0] === '_' || $key === 'paged' || $key === 'ID' ) { continue; } echo '<input type="hidden" name="' . esc_attr( $key ) . '" value="' . esc_attr( $value ) . '" />'; } } /** * Select for choose a log type. * * @since 1.6.3 */ private function log_type_select() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $current_type = ! empty( $_GET['log_type'] ) ? sanitize_text_field( wp_unslash( $_GET['log_type'] ) ) : ''; ?> <select name="log_type"> <option value=""><?php esc_html_e( 'All Logs', 'wpforms-lite' ); ?></option> <?php foreach ( Log::get_log_types() as $type_slug => $type ) { ?> <option value="<?php echo esc_attr( $type_slug ); ?>" <?php selected( $type_slug, $current_type ); ?>> <?php echo esc_html( $type ); ?> </option> <?php } ?> </select> <input type="submit" class="button" value="<?php esc_attr_e( 'Apply', 'wpforms-lite' ); ?>"> <?php } /** * Popup view. * * @since 1.6.3 */ public function popup_template() { ?> <script type="text/html" id="tmpl-wpforms-log-record"> <div class="wpforms-log-popup"> <div class="wpforms-log-popup-block"> <div class="wpforms-log-popup-label"><?php esc_html_e( 'Log Title', 'wpforms-lite' ); ?></div> <div class="wpforms-log-popup-title">{{{ data.title }}}</div> </div> <div class="wpforms-log-popup-block"> <div class="wpforms-log-popup-label"><?php esc_html_e( 'Message', 'wpforms-lite' ); ?></div> <div class="wpforms-log-popup-message">{{{ data.message }}}</div> </div> <div class="wpforms-log-popup-flex wpforms-log-popup-flex-column-2"> <div> <div class="wpforms-log-popup-label"><?php esc_html_e( 'Date', 'wpforms-lite' ); ?></div> <div class="wpforms-log-popup-create-at">{{ data.create_at }}</div> </div> <div> <div class="wpforms-log-popup-label"><?php esc_html_e( 'Types', 'wpforms-lite' ); ?></div> <div class="wpforms-log-popup-types">{{ data.types }}</div> </div> </div> <div class="wpforms-log-popup-flex wpforms-log-popup-flex-column-4"> <div> <div class="wpforms-log-popup-label"><?php esc_html_e( 'Log ID', 'wpforms-lite' ); ?></div> <div class="wpforms-log-popup-id">{{ data.ID }}</div> </div> <div> <div class="wpforms-log-popup-label"><?php esc_html_e( 'Form ID', 'wpforms-lite' ); ?></div> <div class="wpforms-log-popup-form-id"> <# if ( data.form_id ) { #> <a href="{{ data.form_url }}"> <# } #> {{ data.form_id }} <# if ( data.form_id ) { #> </a> <# } #> </div> </div> <div> <div class="wpforms-log-popup-label"><?php esc_html_e( 'Entry ID', 'wpforms-lite' ); ?></div> <div class="wpforms-log-popup-entry-id"> <# if ( data.entry_id ) { #> <a href="{{ data.entry_url }}"> <# } #> {{ data.entry_id }} <# if ( data.entry_id ) { #> </a> <# } #> </div> </div> <div> <div class="wpforms-log-popup-label"><?php esc_html_e( 'User ID', 'wpforms-lite' ); ?></div> <div class="wpforms-log-popup-user-id"> <# if ( data.user_id ) { #> <a href="{{ data.user_url }}"> <# } #> {{ data.user_id }} <# if ( data.user_id ) { #> </a> <# } #> </div> </div> </div> </div> </script> <?php } /** * Display list table page. * * @since 1.6.3 */ public function display_page() { $this->prepare_column_headers(); $this->prepare_items(); $slug = $this->_args['plural']; echo '<div class="wpforms-list-table wpforms-list-table--logs">'; echo '<form id="' . esc_attr( $slug ) . '-filter" method="get">'; wp_nonce_field( 'wpforms-table-' . $slug ); $this->header(); $this->display(); echo '</form>'; echo '</div>'; } /** * Get total logs. * * @since 1.6.3 * * @return int */ public function get_total() { return $this->repository->get_total(); } /** * Gets the screen per_page option name. * * @since 1.7.5 * * @return string */ private function get_per_page_option_name() { return str_replace( '-', '_', $this->screen->id ) . '_per_page'; } } ErrorHandler.php 0000644 00000026715 15174710275 0007670 0 ustar 00 <?php /** * The error handler to suppress error messages from vendor directories. */ // phpcs:ignore Generic.Commenting.DocComment.MissingShort /** @noinspection PhpUndefinedClassInspection */ namespace WPForms; use QM_Collectors; /** * Class ErrorHandler. * * @since 1.8.5 */ class ErrorHandler { /** * Directories from where errors should be suppressed. * * @since 1.8.5 * * @var string[] */ protected $dirs; /** * Previous error handler. * * @since 1.8.6 * * @var callable|null */ private $previous_error_handler; /** * Error levels to suppress. * * @since 1.8.6 * * @var int */ protected $levels; /** * Whether the error handler is handling an error. * * @since 1.9.2 * * @var bool */ private $handling = false; /** * Class constructor. * * @since 1.9.3 * * @param array $dirs Directories from where errors should be suppressed. * @param int $levels Error levels to suppress. */ public function __construct( array $dirs = [], int $levels = 0 ) { $this->dirs = $dirs; $this->levels = $levels; } /** * Init class. * * @since 1.8.5 * * @return void * @noinspection PhpUndefinedConstantInspection */ public function init() { if ( defined( 'WPFORMS_DISABLE_ERROR_HANDLER' ) && WPFORMS_DISABLE_ERROR_HANDLER ) { return; } $this->dirs = [ // WPForms. WPFORMS_PLUGIN_DIR . 'vendor/', WPFORMS_PLUGIN_DIR . 'vendor_prefixed/', // Addons. WP_PLUGIN_DIR . '/wpforms-activecampaign/vendor/', WP_PLUGIN_DIR . '/wpforms-authorize-net/vendor/', WP_PLUGIN_DIR . '/wpforms-aweber/deprecated/', WP_PLUGIN_DIR . '/wpforms-aweber/vendor/', WP_PLUGIN_DIR . '/wpforms-calculations/vendor/', WP_PLUGIN_DIR . '/wpforms-campaign-monitor/vendor/', WP_PLUGIN_DIR . '/wpforms-captcha/vendor/', WP_PLUGIN_DIR . '/wpforms-conversational-forms/vendor/', WP_PLUGIN_DIR . '/wpforms-convertkit/vendor/', WP_PLUGIN_DIR . '/wpforms-convertkit/vendor_prefixed/', WP_PLUGIN_DIR . '/wpforms-coupons/vendor/', WP_PLUGIN_DIR . '/wpforms-drip/vendor/', WP_PLUGIN_DIR . '/wpforms-e2e-helpers/vendor/', WP_PLUGIN_DIR . '/wpforms-entry-automation/vendor/', WP_PLUGIN_DIR . '/wpforms-form-abandonment/vendor/', WP_PLUGIN_DIR . '/wpforms-form-locker/vendor/', WP_PLUGIN_DIR . '/wpforms-form-pages/vendor/', WP_PLUGIN_DIR . '/wpforms-geolocation/vendor/', WP_PLUGIN_DIR . '/wpforms-getresponse/vendor/', WP_PLUGIN_DIR . '/wpforms-google-sheets/vendor/', WP_PLUGIN_DIR . '/wpforms-hubspot/vendor/', WP_PLUGIN_DIR . '/wpforms-lead-forms/vendor/', WP_PLUGIN_DIR . '/wpforms-mailchimp/vendor/', WP_PLUGIN_DIR . '/wpforms-mailerlite/vendor/', WP_PLUGIN_DIR . '/wpforms-offline-forms/vendor/', WP_PLUGIN_DIR . '/wpforms-paypal-commerce/vendor/', WP_PLUGIN_DIR . '/wpforms-paypal-standard/vendor/', WP_PLUGIN_DIR . '/wpforms-post-submissions/vendor/', WP_PLUGIN_DIR . '/wpforms-salesforce/vendor/', WP_PLUGIN_DIR . '/wpforms-salesforce/vendor_prefixed/', WP_PLUGIN_DIR . '/wpforms-save-resume/vendor/', WP_PLUGIN_DIR . '/wpforms-sendinblue/vendor/', WP_PLUGIN_DIR . '/wpforms-signatures/vendor/', WP_PLUGIN_DIR . '/wpforms-square/vendor/', // Backward compatibility. WP_PLUGIN_DIR . '/wpforms-stripe/vendor/', WP_PLUGIN_DIR . '/wpforms-surveys-polls/vendor/', WP_PLUGIN_DIR . '/wpforms-user-journey/vendor/', WP_PLUGIN_DIR . '/wpforms-user-registration/vendor/', WP_PLUGIN_DIR . '/wpforms-webhooks/vendor/', WP_PLUGIN_DIR . '/wpforms-zapier/vendor/', ]; /** * Allow modifying the list of dirs to suppress messages from. * * @since 1.8.6 * * @param array $dirs The list of dirs to suppress messages from. */ $this->dirs = (array) apply_filters( 'wpforms_error_handler_dirs', $this->dirs ); $this->normalize_dirs(); /** * Allow modifying the levels of messages to suppress. * * @since 1.8.6 * * @param int $levels Error levels of messages to suppress. */ $this->levels = (int) apply_filters( 'wpforms_error_handler_levels', E_WARNING | E_NOTICE | E_USER_WARNING | E_USER_NOTICE | E_DEPRECATED | E_USER_DEPRECATED ); $this->hooks(); } /** * Add hooks. * * @since 1.9.1 * * @return void */ protected function hooks() { if ( $this->dirs && $this->levels ) { // Set error handler. $this->set_error_handler(); // Some plugins destroy an error handler chain. Set the error handler again upon loading them. add_action( 'plugins_loaded', [ $this, 'plugins_loaded' ], 1000 ); } // Suppress the _load_textdomain_just_in_time() notices related the WPForms for WP 6.7+. if ( version_compare( $GLOBALS['wp_version'], '6.7', '>=' ) ) { add_action( 'doing_it_wrong_run', [ $this,'action_doing_it_wrong_run' ], 0, 3 ); add_action( 'doing_it_wrong_run', [ $this,'action_doing_it_wrong_run' ], 20, 3 ); add_filter( 'doing_it_wrong_trigger_error', [ $this, 'filter_doing_it_wrong_trigger_error' ], 10, 4 ); } } /** * Set error handler and save original. * * @since 1.9.1 */ public function set_error_handler() { // To chain error handlers, we must not specify the second argument and catch all errors in our handler. // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_set_error_handler $this->previous_error_handler = set_error_handler( [ $this, 'error_handler' ] ); } /** * The 'plugins_loaded' hook. * * @since 0.32 * * @return void */ public function plugins_loaded() { // Constants of plugins that destroy an error handler chain. $constants = [ 'QM_VERSION', // Query Monitor. 'AUTOMATOR_PLUGIN_VERSION', // Uncanny Automator. ]; $found = false; foreach ( $constants as $constant ) { if ( defined( $constant ) ) { $found = true; break; } } if ( ! $found ) { return; } // Set this error handler after loading a plugin to chain its error handler. ( new self( $this->dirs, $this->levels ) )->set_error_handler(); } /** * Error handler. * * @since 1.8.5 * * @param int $level Error level. * @param string $message Error message. * @param string $file File produced an error. * @param int $line Line number. * * @return bool * @noinspection PhpTernaryExpressionCanBeReplacedWithConditionInspection */ public function error_handler( int $level, string $message, string $file, int $line ): bool { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed if ( $this->handling ) { $this->handling = false; // Prevent infinite recursion and fallback to standard error handler. return false; } $this->handling = true; if ( ( $level & $this->levels ) === 0 ) { // Not served error level, use fallback error handler. // phpcs:ignore PHPCompatibility.FunctionUse.ArgumentFunctionsReportCurrentValue.NeedsInspection return $this->fallback_error_handler( func_get_args() ); } // Process error. $normalized_file = str_replace( DIRECTORY_SEPARATOR, '/', $file ); foreach ( $this->dirs as $dir ) { if ( strpos( $normalized_file, $dir ) !== false ) { $this->handling = false; // Suppress deprecated errors from this directory. return true; } } // Not served directory, use fallback error handler. // phpcs:ignore PHPCompatibility.FunctionUse.ArgumentFunctionsReportCurrentValue.NeedsInspection return $this->fallback_error_handler( func_get_args() ); } /** * Action for _doing_it_wrong() calls. * * @since 1.9.2.2 * * @param string $function_name The function that was called. * @param string $message A message explaining what has been done incorrectly. * @param string $version The version of WordPress where the message was added. * * @return void * @noinspection PhpMissingParamTypeInspection * @noinspection PhpUnusedParameterInspection */ public function action_doing_it_wrong_run( $function_name, $message, $version ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed, WPForms.PHP.HooksMethod.InvalidPlaceForAddingHooks global $wp_filter; $function_name = (string) $function_name; $message = (string) $message; if ( ! class_exists( 'QM_Collectors' ) || ! $this->is_just_in_time_for_wpforms_domain( $function_name, $message ) ) { return; } $qm_collector_doing_it_wrong = QM_Collectors::get( 'doing_it_wrong' ); $current_priority = $wp_filter['doing_it_wrong_run']->current_priority(); if ( $qm_collector_doing_it_wrong === null || $current_priority === false ) { return; } switch ( $current_priority ) { case 0: remove_action( 'doing_it_wrong_run', [ $qm_collector_doing_it_wrong, 'action_doing_it_wrong_run' ] ); break; case 20: add_action( 'doing_it_wrong_run', [ $qm_collector_doing_it_wrong, 'action_doing_it_wrong_run' ], 10, 3 ); break; default: break; } } /** * Filter for _doing_it_wrong() calls. * * @since 1.9.2.2 * * @param bool|mixed $trigger Whether to trigger the error for _doing_it_wrong() calls. Default true. * @param string $function_name The function that was called. * @param string $message A message explaining what has been done incorrectly. * @param string $version The version of WordPress where the message was added. * * @return bool * @noinspection PhpMissingParamTypeInspection * @noinspection PhpUnusedParameterInspection */ public function filter_doing_it_wrong_trigger_error( $trigger, $function_name, $message, $version ): bool { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed $trigger = (bool) $trigger; $function_name = (string) $function_name; $message = (string) $message; return $this->is_just_in_time_for_wpforms_domain( $function_name, $message ) ? false : $trigger; } /** * Filter for gettext. * * @since 1.9.2.2 * @deprecated 1.9.3 * * @param string|mixed $translation Translated text. * @param string|mixed $text Text to translate. * @param string|mixed $domain Text domain. Unique identifier for retrieving translated strings. * * @return string|mixed * @noinspection PhpUnusedParameterInspection */ public function filter_gettext( $translation, $text, $domain ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed _deprecated_function( __METHOD__, '1.9.3 of the WPForms plugin' ); return $translation; } /** * Fallback error handler. * * @since 1.9.2 * * @param array $args Arguments. * * @return bool * @noinspection PhpTernaryExpressionCanBeReplacedWithConditionInspection */ private function fallback_error_handler( array $args ): bool { $result = $this->previous_error_handler === null ? // Use standard error handler. false : (bool) call_user_func_array( $this->previous_error_handler, $args ); $this->handling = false; return $result; } /** * Normalize dirs. * * @since 1.9.2 * * @return void */ private function normalize_dirs() { $this->dirs = array_filter( array_map( static function ( $dir ) { return str_replace( DIRECTORY_SEPARATOR, '/', trim( $dir ) ); }, $this->dirs ) ); } /** * Whether it is the just_in_time_error for WPForms-related domains. * * @since 1.9.2.2 * * @param string $function_name Function name. * @param string $message Message. * * @return bool */ protected function is_just_in_time_for_wpforms_domain( string $function_name, string $message ): bool { return $function_name === '_load_textdomain_just_in_time' && strpos( $message, '<code>wpforms' ) !== false; } } Requirements/Requirements.php 0000644 00000113713 15174710275 0012442 0 ustar 00 <?php namespace WPForms\Requirements; /** * Requirements management. * * @since 1.8.2.2 */ class Requirements { /** * Whether deactivate addon if requirements not met. * * @since 1.8.2.2 * @since 1.9.2 Keep addons active. */ private const DEACTIVATE_IF_NOT_MET = false; /** * Whether to show PHP version notice. * * @since 1.8.2.2 */ private const SHOW_PHP_NOTICE = true; /** * Whether to show a PHP extension notice. * * @since 1.8.2.2 */ private const SHOW_EXT_NOTICE = true; /** * Whether to show WordPress version notice. * * @since 1.8.2.2 */ private const SHOW_WP_NOTICE = true; /** * Whether to show WPForms version notice. * * @since 1.8.2.2 */ private const SHOW_WPFORMS_NOTICE = true; /** * Whether to show license level notice. * * @since 1.8.2.2 */ private const SHOW_LICENSE_NOTICE = false; /** * Whether to show addon version notice. * * @since 1.8.2.2 */ private const SHOW_ADDON_NOTICE = true; /** * Keys of the requirements' arrays. * * @since 1.8.2.2 */ private const PHP = 'php'; private const EXT = 'ext'; private const WP = 'wp'; private const WPFORMS = 'wpforms'; private const LICENSE = 'license'; private const PRIORITY = 'priority'; private const ADDON = 'addon'; private const ADDON_VERSION_CONSTANT = 'addon_version_constant'; private const VERSION = 'version'; private const COMPARE = 'compare'; private const COMPARE_DEFAULT = '>='; /** * Development version of WPForms. Can be specified in an addon. * * @since 1.8.2.2 */ private const WPFORMS_DEV_VERSION_IN_ADDON = '{WPFORMS_VERSION}'; /** * Basic, Plus, Pro and Top level licenses. * * @since 1.9.8.3 */ public const BASIC_PLUS_PRO_AND_TOP = [ 'basic', 'plus', 'pro', 'elite', 'agency', 'ultimate' ]; /** * Plus, Pro and Top level licenses. * * @since 1.8.2.2 */ private const PLUS_PRO_AND_TOP = [ 'plus', 'pro', 'elite', 'agency', 'ultimate' ]; /** * Pro and Top level licenses. * * @since 1.8.2.2 */ private const PRO_AND_TOP = [ 'pro', 'elite', 'agency', 'ultimate' ]; /** * Top level licenses. * * @since 1.8.2.2 */ private const TOP = [ 'elite', 'agency', 'ultimate' ]; /** * Default minimal addon requirements. * * @since 1.8.2.2 * * @var string[] */ private $defaults = [ self::PHP => '7.2', self::WP => '5.5', self::WPFORMS => self::WPFORMS_DEV_VERSION_IN_ADDON, self::LICENSE => self::PRO_AND_TOP, self::PRIORITY => 10, ]; /** * Some things to do. * * @todo Add custom message for form-templates-pack. */ // phpcs:disable WordPress.Arrays.MultipleStatementAlignment.DoubleArrowNotAligned, WordPress.Arrays.MultipleStatementAlignment.LongIndexSpaceBeforeDoubleArrow /** * Addon requirements. * * Array has the format 'addon basename' => 'addon requirements array'. * * The requirement array can have the following keys: * self::PHP ('php') for the minimal PHP version required, * self::EXT ('ext') for the PHP extensions required, * self::WP ('wp') for the minimal WordPress version required, * self::WPFORMS ('wpforms') for the minimal WPForms version required, * self::LICENSE ('license') for the license level required, * self::ADDON ('addon') for the minimal addon version required, * self::ADDON_VERSION_CONSTANT ('addon_version_constant') for the addon version constant. * self::PRIORITY ('priority') for the priority of the current requirements. * * The requirement array can have the following values: * The 'php' value can be string like '5.6' or an array like 'php' => [ 'version' => '7.2', 'compare' => '=' ]. * The 'ext' value can be a string like 'curl' or an array like 'ext' => [ 'curl', 'mbstring' ]. * The 'wp' value can be string like '5.5' or an array like 'wp' => [ 'version' => '6.4', 'compare' => '=' ]. * The 'wpforms' value can be string like '1.8.2' * or an array like 'wpforms' => [ 'version' => '1.7.5', 'compare' => '=' ]. * When the 'wpforms' value is '{WPFORMS_VERSION}', it is not checked and should be used for development. * The 'license' value can be string like 'elite, agency, ultimate' * or an array like 'license' => [ 'elite', 'agency', 'ultimate' ]. * When the 'license' value is empty like null, false, [], it is not checked. * The 'addon' value can be a string like '2.0.1' * or an array like 'addon' => [ 'version' => '2.0.1', 'compare' => '<=' ]. * The 'addon_version_constant' must be a string like 'WPFORMS_ACTIVECAMPAIGN_VERSION'. * The 'priority' must be an integer like 20. By default, it is 10. * * By default, 'compare' is '>='. * * The default addon version constant is formed from the addon directory name like this: * wpforms-activecampaign -> WPFORMS_ACTIVECAMPAIGN_VERSION. * * Requirements can be specified here or in the addon as a parameter of wpforms_requirements(). * The priorities from lower to higher (if PRIORITY is not set or equal): * 1. Default parameters from $this->defaults. * 2. Current array $this->requirements. * 3. Parameter of wpforms_requirements() call in the addon. * Settings with a higher priority overwrite lower priority settings. * * The minimal-required version of WPForms should be specified in the addons. * The minimal-required version of addons should be specified here, in the `$this->requirements` array. * * We do not plan to restrict the lower addon version so far. * However, if in the future we may need to do so, * we should add to the addon-related requirement array the line like * self::ADDON => '1.x.x' or * self::ADDON => '{WPFORMS_ACTIVECAMPAIGN_VERSION}'. * Here 1.x.x is the specific addon version, and * WPFORMS_ACTIVECAMPAIGN_VERSION is the addon version constant name. * The script will replace the addon version constant name during the addon release. * * @since 1.8.2.2 * * @var array */ private $requirements = [ 'wpforms/wpforms.php' => [ self::EXT => 'curl, dom, json, libxml', self::LICENSE => [], ], 'wpforms-lite/wpforms.php' => [ self::EXT => 'curl, dom, json, libxml', self::LICENSE => [], ], 'wpforms-activecampaign/wpforms-activecampaign.php' => [ self::LICENSE => self::TOP, ], 'wpforms-authorize-net/wpforms-authorize-net.php' => [ self::EXT => 'curl', self::LICENSE => self::TOP, ], 'wpforms-airtable/wpforms-airtable.php' => [ self::LICENSE => self::TOP, ], 'wpforms-aweber/wpforms-aweber.php' => [ self::EXT => 'curl', self::LICENSE => self::PLUS_PRO_AND_TOP, ], 'wpforms-calculations/wpforms-calculations.php' => [ self::ADDON => '1.5.0', ], 'wpforms-campaign-monitor/wpforms-campaign-monitor.php' => [ self::LICENSE => self::PLUS_PRO_AND_TOP, ], 'wpforms-captcha/wpforms-captcha.php' => [ // Deprecated. self::LICENSE => self::BASIC_PLUS_PRO_AND_TOP, self::WPFORMS => [ self::VERSION => [ '1.8.3', '1.8.7' ], self::COMPARE => [ '>=', '<' ], ], self::PRIORITY => 20, ], 'wpforms-conversational-forms/wpforms-conversational-forms.php' => [], 'wpforms-convertkit/wpforms-convertkit.php' => [ self::LICENSE => self::PLUS_PRO_AND_TOP, self::PHP => '7.4', ], 'wpforms-coupons/wpforms-coupons.php' => [ self::ADDON => '1.6.0', ], 'wpforms-drip/wpforms-drip.php' => [ self::EXT => 'curl', self::LICENSE => self::PLUS_PRO_AND_TOP, ], 'wpforms-dropbox/wpforms-dropbox.php' => [ self::ADDON => '1.1.0', ], 'wpforms-entry-automation/wpforms-entry-automation.php' => [ self::LICENSE => self::TOP, ], 'wpforms-form-abandonment/wpforms-form-abandonment.php' => [], 'wpforms-form-locker/wpforms-form-locker.php' => [ self::ADDON => '2.8.0', ], 'wpforms-form-pages/wpforms-form-pages.php' => [], 'wpforms-form-templates-pack/wpforms-form-templates-pack.php' => [ // Deprecated. self::WPFORMS => [ self::VERSION => '1.6.8', self::COMPARE => '<', ], ], 'wpforms-geolocation/wpforms-geolocation.php' => [], 'wpforms-getresponse/wpforms-getresponse.php' => [ self::EXT => 'curl', self::LICENSE => self::PLUS_PRO_AND_TOP, self::PHP => '7.3', ], 'wpforms-google-calendar/wpforms-calendar.php' => [], 'wpforms-google-drive/wpforms-google-drive.php' => [ self::EXT => 'fileinfo', ], 'wpforms-google-sheets/wpforms-google-sheets.php' => [ self::ADDON => '2.2.0', ], 'wpforms-hubspot/wpforms-hubspot.php' => [ self::LICENSE => self::TOP, ], 'wpforms-lead-forms/wpforms-lead-forms.php' => [], 'wpforms-mailchimp/wpforms-mailchimp.php' => [ self::EXT => 'curl', self::LICENSE => self::PLUS_PRO_AND_TOP, ], 'wpforms-mailerlite/wpforms-mailerlite.php' => [ self::LICENSE => self::PLUS_PRO_AND_TOP, ], 'wpforms-mailpoet/wpforms-mailpoet.php' => [ self::LICENSE => self::PLUS_PRO_AND_TOP, ], 'wpforms-make/wpforms-make.php' => [], 'wpforms-n8n/wpforms-n8n.php' => [ self::LICENSE => self::PRO_AND_TOP, ], 'wpforms-notion/wpforms-notion.php' => [ self::LICENSE => self::PLUS_PRO_AND_TOP, ], 'wpforms-offline-forms/wpforms-offline-forms.php' => [], 'wpforms-paypal-commerce/wpforms-paypal-commerce.php' => [], 'wpforms-paypal-standard/wpforms-paypal-standard.php' => [], 'wpforms-pdf/wpforms-pdf.php' => [], 'wpforms-pipedrive/wpforms-pipedrive.php' => [ self::LICENSE => self::TOP, ], 'wpforms-post-submissions/wpforms-post-submissions.php' => [], 'wpforms-salesforce/wpforms-salesforce.php' => [ self::LICENSE => self::TOP, ], 'wpforms-save-resume/wpforms-save-resume.php' => [ self::LICENSE => self::PLUS_PRO_AND_TOP, ], 'wpforms-sendinblue/wpforms-sendinblue.php' => [ self::LICENSE => self::PLUS_PRO_AND_TOP, ], 'wpforms-signatures/wpforms-signatures.php' => [ self::ADDON => '1.12.0', self::EXT => 'gd', ], 'wpforms-slack/wpforms-slack.php' => [ self::LICENSE => self::PLUS_PRO_AND_TOP, ], 'wpforms-square/wpforms-square.php' => [], 'wpforms-stripe/wpforms-stripe.php' => [], 'wpforms-surveys-polls/wpforms-surveys-polls.php' => [ self::ADDON => '1.15.0', ], 'wpforms-twilio/wpforms-twilio.php' => [ self::LICENSE => self::PLUS_PRO_AND_TOP, ], 'wpforms-user-journey/wpforms-user-journey.php' => [], 'wpforms-user-registration/wpforms-user-registration.php' => [], 'wpforms-quiz/wpforms-quiz.php' => [], 'wpforms-webhooks/wpforms-webhooks.php' => [ self::LICENSE => self::TOP, ], 'wpforms-zapier/wpforms-zapier.php' => [], 'wpforms-zoho-crm/wpforms-zoho-crm.php' => [ self::LICENSE => self::TOP, ], 'wpforms-lindris/wpforms-lindris.php' => [ self::LICENSE => [], ], ]; // phpcs:enable WordPress.Arrays.MultipleStatementAlignment.DoubleArrowNotAligned, WordPress.Arrays.MultipleStatementAlignment.LongIndexSpaceBeforeDoubleArrow /** * Addon requirements. * * @since 1.8.2.2 * * @var array */ private $addon_requirements = []; /** * Addon basename. * * @since 1.8.2.2 * * @var string */ private $basename = ''; /** * Validated addons. * * @since 1.8.2.2 * * @var array */ private $validated = []; /** * Not validated addons. * * @since 1.8.2.2 * * @var array */ private $not_validated = []; /** * Get a single instance of the addon. * * @since 1.8.2.2 * * @return Requirements */ public static function get_instance(): Requirements { static $instance; if ( ! $instance ) { $instance = new self(); $instance->init(); } return $instance; } /** * Init class. * * @since 1.8.2.2 */ private function init(): void { foreach ( $this->requirements as $basename => $requirement ) { $this->init_addon_requirements( $basename ); } $this->hooks(); } /** * Add hooks. * * @since 1.8.2.2 */ private function hooks(): void { add_action( 'admin_init', [ $this, 'deactivate' ] ); add_action( 'admin_notices', [ $this, 'show_notices' ] ); add_action( 'network_admin_notices', [ $this, 'show_notices' ] ); } /** * Validate an addon. * * @since 1.8.2.2 * * @param array $addon_requirements Addon requirements. * * @return bool */ public function validate( array $addon_requirements ): bool { $this->addon_requirements = $addon_requirements; $file = $this->addon_requirements['file']; // Requirements' array must contain the addon main filename. if ( ! isset( $file ) ) { return false; } $this->basename = plugin_basename( $file ); // Respect WPF activity. if ( $this->basename === 'wpforms/wpforms.php' && ! wpforms_is_pro() ) { $this->basename = 'wpforms-lite/wpforms.php'; } $this->init_addon_requirements( $this->basename ); $this->addon_requirements = $this->merge_requirements( $this->defaults, $this->requirements[ $this->basename ], $this->addon_requirements ); $php_valid = $this->validate_php(); $ext_valid = $this->validate_ext(); $wp_valid = $this->validate_wp(); $wpforms_valid = $this->validate_wpforms(); $license_valid = $this->validate_license(); $addon_valid = $this->validate_addon(); if ( $php_valid && $ext_valid && $wp_valid && $wpforms_valid && $license_valid && $addon_valid ) { $this->validated[] = $this->basename; } $this->requirements[ $this->basename ] = $this->addon_requirements; return empty( $this->not_validated[ $this->basename ] ); } /** * Determine if addon is validated. * * @since 1.9.2 * * @param string $basename Addon basename. * * @return bool */ public function is_validated( string $basename ): bool { if ( ! file_exists( WP_PLUGIN_DIR . '/' . $basename ) ) { // No more actions if the plugin file does not exist. return false; } if ( ! $this->is_wpforms_addon( $basename ) ) { // No more actions if it is not a wpforms addon. return true; } // We didn't check the addon before. if ( ! isset( $this->not_validated[ $basename ] ) && ! in_array( $basename, $this->validated, true ) ) { $addon_load_function = $this->get_addon_load_function( $basename ); if ( ! is_callable( $addon_load_function ) ) { return false; } // Invoke the addon loading function, which checks requirements. $addon_load_function(); } return in_array( $basename, $this->validated, true ); } /** * Merge requirements by priority. * * @since 1.8.7 * * @param array $defaults Default requirements. * @param array $requirements Requirements. * @param array $addon_requirements Addon requirements. * * @return array */ private function merge_requirements( array $defaults, array $requirements, array $addon_requirements ): array { $chunks = [ $defaults, $requirements, $addon_requirements ]; usort( $chunks, static function ( $chunk1, $chunk2 ) { // phpcs:ignore WPForms.Formatting.EmptyLineBeforeReturn.AddEmptyLineBeforeReturnStatement return ( $chunk1[ self::PRIORITY ] ?? 10 ) <=> ( $chunk2[ self::PRIORITY ] ?? 10 ); } ); return array_merge( ...$chunks ); } /** * Try to deactivate not valid addon. * * @since 1.8.2.2 * * @param string $plugin Path to the plugin file relative to the plugins' directory. * * @return bool True if addon was deactivated. */ public function deactivate_not_valid_addon( string $plugin ): bool { if ( ! self::DEACTIVATE_IF_NOT_MET ) { // No more actions if we not demand deactivation. return false; } if ( ! $this->is_wpforms_addon( $plugin ) ) { // No more actions if it is not a wpforms addon. return false; } // Finalise activation of wpforms addon. $addon_load_function = $this->get_addon_load_function( $plugin ); if ( ! is_callable( $addon_load_function ) ) { return false; } // Invoke the addon loading function, which checks requirements. $addon_load_function(); // Addon may get deactivated after this statement. $this->deactivate(); return ! is_plugin_active( $plugin ); } /** * Check whether a plugin is a wpforms addon. * * @since 1.8.2.2 * * @param string $plugin Path to the plugin file relative to the plugins' directory. * * @return bool */ private function is_wpforms_addon( string $plugin ): bool { if ( strpos( $plugin, 'wpforms-' ) !== 0 ) { // No more actions for the general plugin. return false; } /** * There are some forks of our plugins having the 'wpforms-' prefix. * We have to check the Author name in the plugin header. */ $plugin_data = $this->get_plugin_data( WP_PLUGIN_DIR . '/' . $plugin ); $plugin_author = isset( $plugin_data['Author'] ) ? strtolower( $plugin_data['AuthorName'] ) : ''; // No more actions on forks. return $plugin_author === 'wpforms'; } /** * Wrapper for get_plugin_data. * Check the plugin file for existence to avoid warnings. * * @since 1.9.6 * * @param string $plugin_file Absolute path to the main plugin file. * @param bool $markup Optional. If the returned data should have HTML markup applied. * @param bool $translate Optional. If the returned data should be translated. Default true. * * We set markup and translate to false by default because we need raw values to compare. * * @return array * @noinspection PhpSameParameterValueInspection */ private function get_plugin_data( string $plugin_file, bool $markup = false, bool $translate = false ): array { if ( ! file_exists( $plugin_file ) ) { return []; } if ( ! function_exists( 'get_plugin_data' ) ) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } return get_plugin_data( $plugin_file, $markup, $translate ); } /** * Get the addon function hooked on wpforms_load. * * @since 1.8.2.2 * * @param string $plugin Path to the plugin file relative to the plugins' directory. * * @return string */ private function get_addon_load_function( string $plugin ): string { global $wp_filter; $callbacks = $wp_filter['wpforms_loaded']->callbacks; $prefix = explode( '/', $plugin, 2 )[0]; $prefix = str_replace( '-', '_', $prefix ); $addon_load_function = ''; // Find addon load function. foreach ( $callbacks as $callbacks_at_priority ) { foreach ( $callbacks_at_priority as $key => $callback ) { if ( strpos( $key, $prefix ) === 0 ) { $addon_load_function = $key; break 2; } } } return $addon_load_function; } /** * Normalize version-based requirement. * * @since 1.8.2.2 * * @param string $key Requirements key. * * @return array[] */ private function normalize_version_requirement( string $key ): array { if ( ! isset( $this->addon_requirements[ $key ] ) ) { $this->addon_requirements[ $key ] = []; return []; } $requirement = (array) $this->addon_requirements[ $key ]; $version = isset( $requirement[0] ) ? array_map( 'trim', (array) $requirement[0] ) : [ '' ]; $version = isset( $requirement[ self::VERSION ] ) ? array_map( 'trim', (array) $requirement[ self::VERSION ] ) : $version; $compare = isset( $requirement[ self::COMPARE ] ) ? array_map( 'trim', (array) $requirement[ self::COMPARE ] ) : [ self::COMPARE_DEFAULT ]; $compare = array_pad( $compare, count( $version ), self::COMPARE_DEFAULT ); $requirement = [ self::VERSION => $version, self::COMPARE => $compare, ]; $this->addon_requirements[ $key ] = $requirement; return $requirement; } /** * Normalize array-based requirement. * * @since 1.8.2.2 * * @param string $key Requirements key. * * @return string[] */ private function normalize_array_requirement( string $key ): array { if ( ! isset( $this->addon_requirements[ $key ] ) ) { $this->addon_requirements[ $key ] = []; return []; } $requirement = $this->addon_requirements[ $key ]; if ( is_string( $requirement ) ) { $requirement = explode( ',', $requirement ); } if ( ! is_array( $requirement ) ) { $requirement = []; } $requirement = array_filter( array_map( 'trim', $requirement ) ); $this->addon_requirements[ $key ] = $requirement; return $requirement; } /** * Validate php. * * @since 1.8.2.2 * * @return bool */ private function validate_php(): bool { $php = $this->normalize_version_requirement( self::PHP ); if ( empty( $php ) ) { return true; } if ( $php[ self::VERSION ] && ! $this->version_compare( PHP_VERSION, $php ) ) { $this->not_validated[ $this->basename ][] = self::PHP; return false; } return true; } /** * Validate php extensions. * * @since 1.8.2.2 * * @return bool */ private function validate_ext(): bool { foreach ( $this->normalize_array_requirement( self::EXT ) as $extension ) { if ( ! extension_loaded( $extension ) ) { $this->not_validated[ $this->basename ][] = self::EXT; return false; } } return true; } /** * Validate WP. * * @since 1.8.2.2 * * @return bool */ private function validate_wp(): bool { global $wp_version; $wp = $this->normalize_version_requirement( self::WP ); if ( empty( $wp ) ) { return true; } if ( $wp[ self::VERSION ] && ! $this->version_compare( $wp_version, $wp ) ) { $this->not_validated[ $this->basename ][] = self::WP; return false; } return true; } /** * Validate wpforms. * * @since 1.8.2.2 * * @return bool */ private function validate_wpforms(): bool { $wpforms = $this->normalize_version_requirement( self::WPFORMS ); if ( empty( $wpforms ) ) { return true; } if ( in_array( self::WPFORMS_DEV_VERSION_IN_ADDON, $wpforms[ self::VERSION ], true ) ) { return true; } if ( $wpforms[ self::VERSION ] && ! $this->version_compare( wpforms()->version, $wpforms ) ) { $this->not_validated[ $this->basename ][] = self::WPFORMS; return false; } return true; } /** * Version compare. * * @since 1.8.7 * * @param string $version Version to compare. * @param array $requirement Requirement. * * @return bool */ private function version_compare( string $version, array $requirement ): bool { $compare_arr = $this->get_compare_array( $requirement ); foreach ( $compare_arr as $version2 => $compare ) { $result = version_compare( $version, $version2, $compare ); if ( ! $result ) { return false; } } return true; } /** * Validate license. * * @since 1.8.2.2 * * @return bool */ private function validate_license(): bool { $license = $this->normalize_array_requirement( self::LICENSE ); if ( empty( $license ) ) { return true; } if ( ! in_array( wpforms_get_license_type(), $license, true ) ) { $this->not_validated[ $this->basename ][] = self::LICENSE; return false; } return true; } /** * Validate addon. * * @since 1.8.2.2 * * @return bool */ private function validate_addon(): bool { $addon = $this->normalize_version_requirement( self::ADDON ); $addon_version_constant = trim( $this->addon_requirements[ self::ADDON_VERSION_CONSTANT ] ); if ( empty( $addon ) || empty( $addon_version_constant ) ) { return true; } if ( preg_grep( '/{.+_VERSION}/', $addon[ self::VERSION ] ) ) { return true; } if ( $addon[ self::VERSION ] && ( ! defined( $addon_version_constant ) || ! $this->version_compare( constant( $addon_version_constant ), $addon ) ) ) { $this->not_validated[ $this->basename ][] = self::ADDON; return false; } return true; } /** * Deactivate not validated addons. * * @since 1.8.2.2 */ public function deactivate(): void { if ( ! self::DEACTIVATE_IF_NOT_MET ) { return; } if ( empty( $this->not_validated ) ) { return; } // phpcs:disable WordPress.Security.NonceVerification.Recommended unset( $_GET['activate'] ); if ( empty( $this->validated ) ) { unset( $_GET['activate-multi'] ); } // phpcs:enable WordPress.Security.NonceVerification.Recommended require_once ABSPATH . 'wp-admin/includes/plugin.php'; foreach ( $this->not_validated as $basename => $errors ) { if ( $errors === [ 'license' ] ) { continue; } deactivate_plugins( $basename ); } } /** * Show admin notices. * * @since 1.8.2.2 */ public function show_notices(): void { $notices = $this->get_notices(); if ( ! $notices ) { return; } $this->show_notice( '<p>' . implode( '</p><p>', $notices ) . '</p>' ); } /** * Get admin notices. * * @since 1.8.2.2 * * @return string[] */ public function get_notices(): array { $notices = []; if ( empty( $this->not_validated ) ) { return $notices; } foreach ( $this->not_validated as $basename => $errors ) { $notice = $this->get_notice( $basename ); if ( ! $notice ) { continue; } $notices[] = $notice; } return $notices; } /** * Get an addon compatible message. * * @since 1.9.3 * * @param string $basename Plugin basename. * * @return string * @noinspection HtmlUnknownTarget */ public function get_addon_compatible_message( string $basename ): string { if ( empty( $this->not_validated[ $basename ] ) ) { return ''; } $errors = $this->not_validated[ $basename ]; $message = $this->get_validation_message( $errors, $basename ); if ( ! $message ) { return ''; } $notice = sprintf( /* translators: %1$s - requirements message. */ __( 'It requires %1$s.', 'wpforms-lite' ), $message ); $notice .= $this->get_read_more( $errors ); return $notice; } /** * Get notice. * * @since 1.9.2 * * @param string $basename Plugin basename. * * @return string * @noinspection HtmlUnknownTarget */ public function get_notice( string $basename ): string { if ( empty( $this->not_validated[ $basename ] ) ) { return ''; } $errors = $this->not_validated[ $basename ]; $message = $this->get_validation_message( $errors, $basename ); if ( ! $message ) { return ''; } $is_wpforms_plugin = false !== strpos( $basename, 'wpforms.php' ); if ( $is_wpforms_plugin || in_array( self::ADDON, $errors, true ) ) { $source = __( 'WPForms plugin', 'wpforms-lite' ); } else { $plugin_headers = $this->get_plugin_data( $this->requirements[ $basename ]['file'] ); $source = sprintf( /* translators: %1$s - WPForms addon name. */ __( '%1$s addon', 'wpforms-lite' ), $plugin_headers['Name'] ); } $notice = sprintf( /* translators: %1$s - WPForms plugin or addon name, %2$d - requirements message. */ __( 'The %1$s requires %2$s.', 'wpforms-lite' ), $source, $message ); $notice .= $this->get_read_more( $errors ); /** * Filter the requirements' notice. * * @since 1.8.7 * * @param string $notice Notice. * @param array $errors Validation errors. * @param string $basename Plugin basename. * @param array $requirements Addon requirements. */ return (string) apply_filters( 'wpforms_requirements_notice', $notice, $errors, $basename, $this->requirements[ $basename ] ); } /** * Get read more link. * * @since 1.9.6 * * @param array $errors Errors. * * @return string * @noinspection HtmlUnknownTarget */ private function get_read_more( array $errors ): string { $data = [ self::PHP => [ 'flag' => self::SHOW_PHP_NOTICE, /* translators: %1$s - Read More link. */ 'text' => __( '%1$s for additional information on PHP version.', 'wpforms-lite' ), 'link' => 'https://wpforms.com/docs/supported-php-version/', ], self::EXT => [ 'flag' => self::SHOW_EXT_NOTICE, /* translators: %1$s - Read More link. */ 'text' => __( '%1$s for additional information on PHP extensions.', 'wpforms-lite' ), 'link' => 'https://wpforms.com/docs/required-php-extensions-for-wpforms', ], ]; $read_more = ''; foreach ( $data as $key => $datum ) { if ( ! isset( $datum['flag'], $datum['text'], $datum['link'] ) ) { continue; } if ( ! in_array( $key, $errors, true ) ) { continue; } if ( ! $datum['flag'] ) { continue; } $read_more .= ' ' . sprintf( $datum['text'], sprintf( '<a href="%1$s" target="_blank" rel="noopener noreferrer">%2$s</a>', wpforms_utm_link( $datum['link'], 'all-plugins', 'Addon PHP Notice' ), __( 'Read more', 'wpforms-lite' ) ) ); } return $read_more; } /** * Get a validation message. * * @since 1.8.2.2 * * @param array $errors Validation errors. * @param string $basename Plugin basename. * * @return string */ private function get_validation_message( array $errors, string $basename ): string { $addon_validation_message = $this->get_addon_validation_message( $errors, $basename ); if ( $addon_validation_message ) { // Do not proceed further if addon is required in a higher version. return wpforms_list_array( [ $addon_validation_message ] ); } $messages = []; $messages[] = $this->get_php_validation_message( $errors, $basename ); $messages[] = $this->get_ext_validation_message( $errors, $basename ); $messages[] = $this->get_wp_validation_message( $errors, $basename ); $messages[] = $this->get_wpforms_validation_message( $errors, $basename ); $messages[] = $this->get_license_validation_message( $errors, $basename ); return wpforms_list_array( array_filter( $messages ) ); } /** * Get a PHP validation message. * * @since 1.8.2.2 * * @param array $errors Validation errors. * @param string $basename Plugin basename. * * @return string */ private function get_php_validation_message( array $errors, string $basename ): string { if ( self::SHOW_PHP_NOTICE && in_array( self::PHP, $errors, true ) ) { return $this->list_version_detailed( $this->requirements[ $basename ][ self::PHP ], 'PHP' ); } return ''; } /** * Get an EXT validation message. * * @since 1.8.2.2 * * @param array $errors Validation errors. * @param string $basename Plugin basename. * * @return string */ private function get_ext_validation_message( array $errors, string $basename ): string { if ( self::SHOW_EXT_NOTICE && in_array( self::EXT, $errors, true ) ) { $extensions = array_diff( $this->requirements[ $basename ][ self::EXT ], get_loaded_extensions() ); return sprintf( /* translators: %s - PHP extension name(s). */ _n( '%s PHP extension', '%s PHP extensions', count( $extensions ), 'wpforms-lite' ), wpforms_list_array( $extensions ) ); } return ''; } /** * Get WP validation message. * * @since 1.8.2.2 * * @param array $errors Validation errors. * @param string $basename Plugin basename. * * @return string */ private function get_wp_validation_message( array $errors, string $basename ): string { if ( self::SHOW_WP_NOTICE && in_array( self::WP, $errors, true ) ) { return $this->list_version_detailed( $this->requirements[ $basename ][ self::WP ], 'WordPress' ); } return ''; } /** * Get WPFORMS validation message. * * @since 1.8.2.2 * * @param array $errors Validation errors. * @param string $basename Plugin basename. * * @return string */ private function get_wpforms_validation_message( array $errors, string $basename ): string { if ( self::SHOW_WPFORMS_NOTICE && in_array( self::WPFORMS, $errors, true ) ) { return $this->list_version_detailed( $this->requirements[ $basename ][ self::WPFORMS ], 'WPForms' ); } return ''; } /** * Get LICENSE validation message. * * @since 1.8.2.2 * * @param array $errors Validation errors. * @param string $basename Plugin basename. * * @return string */ private function get_license_validation_message( array $errors, string $basename ): string { if ( self::SHOW_LICENSE_NOTICE && in_array( self::LICENSE, $errors, true ) ) { $license = wpforms_list_array( array_map( 'ucfirst', $this->requirements[ $basename ][ self::LICENSE ] ), false ); return sprintf( /* translators: %s - license name(s). */ __( '%s license', 'wpforms-lite' ), $license ); } return ''; } /** * Get an ADDON validation message. * * @since 1.8.2.2 * * @param array $errors Validation errors. * @param string $basename Plugin basename. * * @return string */ private function get_addon_validation_message( array $errors, string $basename ): string { if ( self::SHOW_ADDON_NOTICE && in_array( self::ADDON, $errors, true ) ) { return $this->list_version_detailed( $this->requirements[ $basename ][ self::ADDON ], $this->get_plugin_data( $this->requirements[ $basename ]['file'] )['Name'] ); } return ''; } /** * Show admin notice. * * @since 1.8.2.2 * * @param string $notice Message. */ private function show_notice( string $notice ): void { echo '<div class="notice notice-error">'; echo wp_kses_post( $notice ); echo '</div>'; } /** * Init addon requirements. * * @since 1.8.2.2 * * @param string $basename Addon basename. */ private function init_addon_requirements( string $basename ): void { if ( ! array_key_exists( $basename, $this->requirements ) ) { $this->requirements[ $basename ] = []; } // Set default addon version constant. if ( array_key_exists( self::ADDON_VERSION_CONSTANT, $this->requirements[ $basename ] ) ) { return; } $const = str_replace( '-', '_', strtoupper( explode( '/', $basename, 2 )[0] ) . '_VERSION' ); $this->requirements[ $basename ][ self::ADDON_VERSION_CONSTANT ] = $const; } /** * Get version from requirements array. * * @since 1.8.2.2 * * @param array $requirement Array containing a requirement. * * @return string */ public function list_version( array $requirement ): string { $compare_arr = $this->get_compare_array( $requirement ); $list = []; foreach ( $compare_arr as $version2 => $compare ) { $list[] = $compare . $version2; } return implode( ', ', $list ); } /** * Get a version from requirements' array in human-readable format. * * @since 1.9.0 * * @param array $requirement Array containing a requirement. * @param string $what What is being checked. * * @return string */ private function list_version_detailed( array $requirement, string $what = '' ): string { $compare_arr = $this->get_compare_array( $requirement ); $list = []; $compare_to_string = [ /* translators: %1$s - What is being checked (PHP, WPForms, etc.), %2$s - required version. This is used as the completion of the sentence "The {addon name} addon requires {here goes this string}". */ '>=' => __( '%1$s %2$s or above', 'wpforms-lite' ), /* translators: %1$s - What is being checked (PHP, WPForms, etc.), %2$s - required version. This is used as the completion of the sentence "The {addon name} addon requires {here goes this string}". */ '<=' => __( '%1$s %2$s or below', 'wpforms-lite' ), '=' => '%1$s %2$s', /* translators: %1$s - What is being checked (PHP, WPForms, etc.), %2$s - required version. This is used as the completion of the sentence "The {addon name} addon requires {here goes this string}". */ '>' => __( 'a newer version of %1$s than %2$s', 'wpforms-lite' ), /* translators: %1$s - What is being checked (PHP, WPForms, etc.), %2$s - required version. This is used as the completion of the sentence "The {addon name} addon requires {here goes this string}". */ '<' => __( 'an older version of %1$s than %2$s', 'wpforms-lite' ), ]; foreach ( $compare_arr as $version2 => $compare ) { if ( isset( $compare_to_string[ $compare ] ) ) { $list[] = sprintf( $compare_to_string[ $compare ], $what, $version2 ); } else { $list[] = $what . ' ' . $compare . ' ' . $version2; } } return implode( ', ', $list ); } /** * Get a compare array in the following format: [ 'version' => 'compare', ... ]. * * @since 1.8.7 * * @param array $requirement Requirement. * * @return array */ public function get_compare_array( array $requirement ): array { $versions = $requirement[ self::VERSION ]; $compares = $requirement[ self::COMPARE ]; return array_combine( $versions, $compares ); } /** * Get requirements. * * @since 1.8.8 * * @return array */ public function get_requirements(): array { return $this->requirements; } /** * Get not validated addons. * * @since 1.9.4 * * @return array */ public function get_not_validated_addons(): array { $all_addons = array_keys( $this->requirements ); return array_values( array_diff( $all_addons, $this->validated ) ); } /** * Get addons by license. * * @since 1.9.8.3 * * @param string|array $license License. * * @return array */ public function get_addons_by_license( $license ): array { if ( is_string( $license ) ) { $license_arr = array_map( 'trim', (array) explode( ',', $license ) ); } else { $license_arr = (array) $license; } $addons_by_license = []; foreach ( $this->requirements as $basename => $this->addon_requirements ) { $this->addon_requirements = $this->merge_requirements( $this->defaults, $this->requirements[ $basename ], $this->addon_requirements ); if ( ! array_intersect( $license_arr, $this->addon_requirements[ self::LICENSE ] ) ) { continue; } $addons_by_license[ $basename ] = $this->addon_requirements; } return $addons_by_license; } } WPForms.php 0000644 00000040623 15174710275 0006630 0 ustar 00 <?php // phpcs:ignore Generic.Commenting.DocComment.MissingShort /** @noinspection PhpIllegalPsrClassPathInspection */ // phpcs:ignore Universal.Namespaces.DisallowCurlyBraceSyntax.Forbidden namespace WPForms { use AllowDynamicProperties; use stdClass; use WPForms\Helpers\DB; use WPForms_Form_Handler; use WPForms_Process; use WPForms_Settings; /** * Main WPForms class. * * @since 1.0.0 */ #[AllowDynamicProperties] final class WPForms { /** * List of screen IDs where heartbeat requests are allowed. * * @since 1.9.3 * * @var string[] */ private const HEARTBEAT_ALLOWED_SCREEN_IDS = [ 'wpforms_page_wpforms-entries', ]; /** * One is the loneliest number that you'll ever do. * * @since 1.0.0 * * @var WPForms */ private static $instance; /** * Plugin version for enqueueing, etc. * The value is got from WPFORMS_VERSION constant. * * @since 1.0.0 * * @var string */ public $version = ''; /** * Classes registry. * * @since 1.5.7 * * @var array */ private $registry = []; /** * List of legacy public properties. * * @since 1.6.8 * * @var string[] */ private $legacy_properties = [ 'form', 'entry', 'entry_fields', 'entry_meta', 'frontend', 'process', 'smart_tags', 'license', ]; /** * Paid returns true, free (Lite) returns false. * * @since 1.3.9 * @since 1.7.3 changed to private. * * @var bool */ private $pro = false; /** * Backward compatibility method for accessing the class registry in an old way, * e.g. 'wpforms()->form' or 'wpforms()->entry'. * * @since 1.5.7 * * @param string $name Name of the object to get. * * @return mixed|null * @noinspection MagicMethodsValidityInspection * @noinspection PhpDeprecationInspection */ public function __get( $name ) { if ( $name === 'smart_tags' ) { _deprecated_argument( 'wpforms()->smart_tags', '1.6.7 of the WPForms plugin', "Please use `wpforms()->obj( 'smart_tags' )` instead." ); } if ( $name === 'pro' ) { _deprecated_argument( 'wpforms()->pro', '1.8.2.2 of the WPForms plugin', 'Please use `wpforms()->is_pro()` instead.' ); return wpforms()->is_pro(); } return $this->get( $name ); } /** * Main WPForms Instance. * * Only one instance of WPForms exists in memory at any one time. * Also, prevent the need to define globals all over the place. * * @since 1.0.0 * * @return WPForms */ public static function instance(): WPForms { if ( self::$instance === null || ! self::$instance instanceof self ) { self::$instance = new self(); self::$instance->init(); } return self::$instance; } /** * Initialize the plugin. * * @since 1.9.3 * * @noinspection UsingInclusionOnceReturnValueInspection */ private function init(): void { if ( self::is_restricted_heartbeat() ) { return; } $this->constants(); $this->includes(); // Load Pro or Lite specific files. if ( $this->is_pro() ) { $this->registry['pro'] = require_once WPFORMS_PLUGIN_DIR . 'pro/wpforms-pro.php'; } else { require_once WPFORMS_PLUGIN_DIR . 'lite/wpforms-lite.php'; } $this->hooks(); } /** * Setup plugin constants. * All the path/URL-related constants are defined in the main plugin file. * * @since 1.0.0 */ private function constants(): void { $this->version = WPFORMS_VERSION; // Plugin Slug - Determine a plugin type and set slug accordingly. // This filter is documented in \WPForms\WPForms::is_pro. if ( apply_filters( 'wpforms_allow_pro_version', file_exists( WPFORMS_PLUGIN_DIR . 'pro/wpforms-pro.php' ) ) ) { $this->pro = true; /** * Pro plugin slug. * * @since 1.5.0 */ define( 'WPFORMS_PLUGIN_SLUG', 'wpforms' ); } else { /** * Lite plugin slug. * * @since 1.5.0 */ define( 'WPFORMS_PLUGIN_SLUG', 'wpforms-lite' ); } } /** * Include files. * * @since 1.0.0 */ private function includes(): void { $this->error_handler(); // Action Scheduler requires a special loading procedure. require_once WPFORMS_PLUGIN_DIR . 'vendor/woocommerce/action-scheduler/action-scheduler.php'; // Autoload Composer packages. require_once WPFORMS_PLUGIN_DIR . 'vendor/autoload.php'; // Base class and functions. require_once WPFORMS_PLUGIN_DIR . 'includes/class-db.php'; require_once WPFORMS_PLUGIN_DIR . 'includes/functions.php'; require_once WPFORMS_PLUGIN_DIR . 'includes/fields/class-base.php'; $this->includes_magic(); // Global includes. require_once WPFORMS_PLUGIN_DIR . 'includes/class-install.php'; require_once WPFORMS_PLUGIN_DIR . 'includes/class-form.php'; require_once WPFORMS_PLUGIN_DIR . 'includes/class-fields.php'; // TODO: class-templates.php should be loaded in admin area only. require_once WPFORMS_PLUGIN_DIR . 'includes/class-templates.php'; // TODO: class-providers.php should be loaded in admin area only. require_once WPFORMS_PLUGIN_DIR . 'includes/class-providers.php'; require_once WPFORMS_PLUGIN_DIR . 'includes/class-process.php'; require_once WPFORMS_PLUGIN_DIR . 'includes/class-widget.php'; require_once WPFORMS_PLUGIN_DIR . 'includes/emails/class-emails.php'; require_once WPFORMS_PLUGIN_DIR . 'includes/integrations.php'; require_once WPFORMS_PLUGIN_DIR . 'includes/deprecated.php'; // Admin/Dashboard only includes, also in ajax. if ( is_admin() ) { require_once WPFORMS_PLUGIN_DIR . 'includes/admin/admin.php'; require_once WPFORMS_PLUGIN_DIR . 'includes/admin/class-notices.php'; require_once WPFORMS_PLUGIN_DIR . 'includes/admin/class-menu.php'; require_once WPFORMS_PLUGIN_DIR . 'includes/admin/builder/class-builder.php'; require_once WPFORMS_PLUGIN_DIR . 'includes/admin/builder/functions.php'; require_once WPFORMS_PLUGIN_DIR . 'includes/admin/class-settings.php'; require_once WPFORMS_PLUGIN_DIR . 'includes/admin/class-welcome.php'; require_once WPFORMS_PLUGIN_DIR . 'includes/admin/class-editor.php'; require_once WPFORMS_PLUGIN_DIR . 'includes/admin/class-review.php'; require_once WPFORMS_PLUGIN_DIR . 'includes/admin/class-about.php'; require_once WPFORMS_PLUGIN_DIR . 'includes/admin/ajax-actions.php'; } } /** * Hooks. * * @since 1.9.0 * @since 1.9.3 No longer static. * * @return void */ private function hooks(): void { add_action( 'plugins_loaded', [ self::$instance, 'objects' ] ); add_action( 'wpforms_settings_init', [ self::$instance, 'reinstall_custom_tables' ] ); } /** * Include the error handler to suppress deprecated messages from vendor folders. * * @since 1.8.5 */ private function error_handler(): void { require_once WPFORMS_PLUGIN_DIR . 'src/ErrorHandler.php'; ( new ErrorHandler() )->init(); } /** * Including the new files with PHP 5.3 style. * * @since 1.4.7 */ private function includes_magic(): void { // phpcs:ignore WPForms.PHP.HooksMethod.InvalidPlaceForAddingHooks // Load the class loader. $this->register( [ 'name' => 'Loader', 'hook' => false, ] ); $this->register( [ 'name' => 'Integrations\SolidCentral\SolidCentral', 'hook' => 'plugins_loaded', 'priority' => 0, 'condition' => ! empty( $_GET['ithemes-sync-request'] ), // phpcs:ignore WordPress.Security.NonceVerification.Recommended ] ); /* * Load admin components. Exclude from the frontend. */ if ( is_admin() ) { add_action( 'wpforms_loaded', [ '\WPForms\Admin\Loader', 'get_instance' ] ); } /* * Properly init the providers' loader that will handle all the related logic and further loading. */ add_action( 'wpforms_loaded', [ '\WPForms\Providers\Providers', 'get_instance' ] ); /* * Properly init the integration loader that will handle all the related logic and further loading. */ add_action( 'wpforms_loaded', [ '\WPForms\Integrations\Loader', 'get_instance' ] ); } /** * Setup objects. * * @since 1.0.0 */ public function objects(): void { // Global objects. $this->registry['form'] = new WPForms_Form_Handler(); $this->registry['process'] = new WPForms_Process(); /** * Executes when all the WPForms stuff was loaded. * * @since 1.4.0 */ do_action( 'wpforms_loaded' ); } /** * Re-create plugin custom tables if they don't exist. * * @since 1.9.0 * * @param WPForms_Settings $wpforms_settings WPForms settings object. */ public function reinstall_custom_tables( WPForms_Settings $wpforms_settings ): void { if ( empty( $wpforms_settings->view ) ) { return; } // Proceed on the Settings plugin admin area page only. if ( $wpforms_settings->view !== 'general' ) { return; } // Install on the current site only. if ( ! DB::custom_tables_exist() ) { DB::create_custom_tables(); } } /** * Register a class. * * @since 1.5.7 * * @param array $class_data Class registration info. * * $class_data array accepts these params: name, id, hook, run, condition. * - name: required -- class name to register. * - id: optional -- class ID to register. * - hook: optional -- hook to register the class on -- default wpforms_loaded. * - run: optional -- method to run on class instantiation -- default init. * - condition: optional -- condition to check before registering the class. * * @noinspection OnlyWritesOnParameterInspection */ public function register( $class_data ): void { // phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh, WPForms.PHP.HooksMethod.InvalidPlaceForAddingHooks if ( empty( $class_data['name'] ) || ! is_string( $class_data['name'] ) ) { return; } if ( isset( $class_data['condition'] ) && empty( $class_data['condition'] ) ) { return; } $full_name = $this->is_pro() ? '\WPForms\Pro\\' . $class_data['name'] : '\WPForms\Lite\\' . $class_data['name']; $full_name = class_exists( $full_name ) ? $full_name : '\WPForms\\' . $class_data['name']; // Register an addon class. if ( ! empty( $class_data['addon_class'] ) && ! empty( $class_data['addon_slug'] ) ) { $is_initialized = wpforms_is_addon_initialized( $class_data['addon_slug'] ) && $this->is_pro(); $full_name = $is_initialized ? $class_data['addon_class'] : $full_name; $full_name = strpos( $full_name, '\\' ) !== 0 ? '\\' . $full_name : $full_name; // The core plugin classes have priority 10. // Addon classes should be initialized after the core. $class_data['priority'] = 100; } // Bail if the class doesn't exist AND it is not an addon class. if ( ! class_exists( $full_name ) && empty( $class_data['addon_class'] ) ) { return; } $id = $class_data['id'] ?? ''; $id = $id ? preg_replace( '/[^a-z_]/', '', (string) $id ) : $id; $hook = isset( $class_data['hook'] ) ? (string) $class_data['hook'] : 'wpforms_loaded'; $run = $class_data['run'] ?? 'init'; $priority = isset( $class_data['priority'] ) && is_int( $class_data['priority'] ) ? $class_data['priority'] : 10; $callback = function () use ( $full_name, $id, $run, $hook ) { if ( ! class_exists( $full_name ) ) { return; } // Instantiate class. $instance = new $full_name(); $this->register_instance( $id, $instance ); if ( $run && method_exists( $instance, $run ) ) { $instance->{$run}(); } }; if ( $hook ) { add_action( $hook, $callback, $priority ); } else { $callback(); } } /** * Register any class instance. * * @since 1.8.6 * * @param string $id Class ID. * @param object $instance Any class instance (object). */ public function register_instance( $id, $instance ): void { if ( $id && is_object( $instance ) && ! array_key_exists( $id, $this->registry ) ) { $this->registry[ $id ] = $instance; } } /** * Register classes in bulk. * * @since 1.5.7 * * @param array $classes Classes to register. */ public function register_bulk( $classes ): void { if ( ! is_array( $classes ) ) { return; } foreach ( $classes as $class ) { $this->register( $class ); } } /** * Get a class instance from a registry. * Use \WPForms\WPForms::obj() instead. * * @since 1.5.7 * @deprecated 1.9.1 * * @param string $name Class name or an alias. * * @return mixed|stdClass|null */ public function get( $name ) { if ( ! empty( $this->registry[ $name ] ) ) { return $this->registry[ $name ]; } // Backward compatibility for old public properties. // Return null to save old condition for these properties. if ( in_array( $name, $this->legacy_properties, true ) ) { return $this->{$name} ?? null; } return new stdClass(); } /** * Get a class instance from a registry. * * @since 1.9.1 * * @param string $name Class name or an alias. * * @return object|null */ public function obj( string $name ): ?object { return $this->registry[ $name ] ?? null; } /** * Get the list of all custom tables starting with `wpforms_*`. * * @since 1.6.3 * * @return array List of table names. */ public function get_existing_custom_tables(): array { // phpcs:ignore WPForms.Formatting.EmptyLineBeforeReturn.RemoveEmptyLineBeforeReturnStatement return DB::get_existing_custom_tables(); } /** * Whether the current instance of the plugin is a paid version, or free. * * @since 1.7.3 * * @return bool */ public function is_pro(): bool { /** * Filters whether the current plugin version is pro. * * @since 1.7.3 * * @param bool $pro Whether the current plugin version is pro. */ return (bool) apply_filters( 'wpforms_allow_pro_version', $this->pro ); } /** * Whether the current request is restricted heartbeat. * * @since 1.9.3 * * @return bool */ public static function is_restricted_heartbeat(): bool { // phpcs:disable WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $action = $_POST['action'] ?? ''; if ( $action !== 'heartbeat' || ! wp_doing_ajax() ) { return false; } $screen_id = sanitize_key( $_POST['screen_id'] ?? '' ); $data = array_map( 'sanitize_text_field', $_POST['data'] ?? [] ); // phpcs:enable WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized /** * Filters the screen ids where the heartbeat is allowed. * * @since 1.9.3 * * @param array $allowed_screen_ids Screen IDs where the heartbeat is allowed. */ $allowed_screen_ids = (array) apply_filters( 'wpforms_heartbeat_allowed_screen_ids', self::HEARTBEAT_ALLOWED_SCREEN_IDS ); // Allow heartbeat requests on specific screens. if ( in_array( $screen_id, $allowed_screen_ids, true ) ) { return false; } /** * Filters whether the current request is restricted heartbeat. * * @since 1.9.3 * * @param bool $is_restricted Whether the current request is restricted heartbeat. * @param string $screen_id Screen ID. * @param array $data Heartbeat request data. */ return (bool) apply_filters( 'wpforms_is_restricted_heartbeat', true, $screen_id, $data ); } } } // phpcs:ignore Universal.Namespaces.DisallowCurlyBraceSyntax.Forbidden, Universal.Namespaces.DisallowDeclarationWithoutName.Forbidden, Universal.Namespaces.OneDeclarationPerFile.MultipleFound namespace { // Define `wpforms()` function only if it's not the restricted heartbeat request. if ( ! WPForms\WPForms::is_restricted_heartbeat() ) { /** * The function which returns the one WPForms instance. * * @since 1.0.0 * * @return WPForms\WPForms */ function wpforms(): WPForms\WPForms { // phpcs:ignore Universal.Files.SeparateFunctionsFromOO.Mixed return WPForms\WPForms::instance(); } /** * Adding an alias for backward-compatibility with plugins * that still use class_exists( 'WPForms' ) * instead of function_exists( 'wpforms' ), which is preferred. * * In 1.5.0 we removed support for PHP 5.2 * and moved the former WPForms class to a namespace: WPForms\WPForms. * * @since 1.5.1 */ class_alias( 'WPForms\WPForms', 'WPForms' ); } } Frontend/Amp.php 0000644 00000021664 15174710275 0007573 0 ustar 00 <?php namespace WPForms\Frontend; /** * AMP class. * * @since 1.8.1 */ class Amp { /** * Whether the current page is in AMP mode or not. * * @since 1.8.1 * * @var bool */ private $is_amp_mode; /** * Constructor. * * @since 1.8.1 */ public function __construct() { $this->hooks(); } /** * Register hooks. * * @since 1.8.1 */ private function hooks() { add_filter( 'amp_skip_post', [ $this, 'skip_post' ] ); add_filter( 'wpforms_frontend_form_atts', [ $this, 'form_atts' ], -PHP_INT_MAX, 2 ); add_action( 'wpforms_frontend_output', [ $this, 'output_state' ], -PHP_INT_MAX, 5 ); } /** * Check whether the current page is in AMP mode or not. * * @since 1.8.1 * * @return bool True if the current page is in AMP mode. */ public function is_amp(): bool { if ( is_null( $this->is_amp_mode ) ) { $this->is_amp_mode = wpforms_is_amp(); } return $this->is_amp_mode; } /** * Stop AMP output. * * @since 1.8.1 * * @param array $form_data Form data and settings. * * @return bool True if we need to stop the output. */ public function stop_output( $form_data ): bool { // We need to stop output processing in case we are on AMP page. // phpcs:disable WPForms.PHP.ValidateHooks.InvalidHookName if ( ! $this->should_stop_output() ) { return false; } $form_id = ! empty( $form_data['id'] ) ? (int) $form_data['id'] : 0; $full_page_url = home_url( add_query_arg( 'nonamp', '1' ) . '#wpforms-' . $form_id ); /** * Allow modifying the text or url for the full page on the AMP pages. * * @since 1.4.1.1 * @since 1.7.1 Added $form_id, $full_page_url, and $form_data arguments. * * @param string $text Text. * @param int $form_id Form id. * @param string $full_page_url Full page url. * @param array $form_data Form data and settings. * * @return string */ $text = (string) apply_filters( 'wpforms_frontend_shortcode_amp_text', sprintf( /* translators: %s - URL to a non-amp version of a page with the form. */ __( '<a href="%s">Go to the full page</a> to view and submit the form.', 'wpforms-lite' ), esc_url( $full_page_url ) ), $form_id, $full_page_url, $form_data ); printf( '<p class="wpforms-shortcode-amp-text">%s</p>', wp_kses_post( $text ) ); return true; // phpcs:enable WPForms.PHP.ValidateHooks.InvalidHookName } /** * Whether output should be stopped. * * @since 1.9.0 * * @return bool */ private function should_stop_output(): bool { if ( ! $this->is_amp() ) { return false; } /** * Filters PRO status of the plugin. * Returning `true` means that AMP stop loading. * * @since 1.5.4.2 * * @param bool $pro Pro status. */ if ( apply_filters( 'wpforms_amp_pro', wpforms()->is_pro() ) ) { // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName return true; } return ( ! defined( 'AMP__VERSION' ) || version_compare( AMP__VERSION, '1.2', '<' ) || ! is_ssl() ); } /** * Disable AMP if query param is detected. * * This allows the full form to be accessible for Pro users or sites * that do not have SSL. * * @since 1.8.1 * * @param bool $skip Skip AMP mode, display full post. * * @return bool */ public function skip_post( $skip ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended return isset( $_GET['nonamp'] ) ? true : $skip; } /** * Form attributes filter. * * @since 1.8.1 * * @param array $form_atts Form attributes. * @param array $form_data Form data. * * @return array */ public function form_atts( $form_atts, $form_data ) { if ( ! $this->is_amp() ) { return $form_atts; } // Set submitting state. if ( ! isset( $form_atts['atts']['on'] ) ) { $form_atts['atts']['on'] = ''; } else { $form_atts['atts']['on'] .= ';'; } $form_id = ! empty( $form_data['id'] ) ? (int) $form_data['id'] : 0; $form_atts['atts']['on'] .= sprintf( 'submit:AMP.setState( %1$s ); submit-success:AMP.setState( %2$s ); submit-error:AMP.setState( %2$s );', wp_json_encode( [ $this->get_form_amp_state_id( $form_id ) => [ 'submitting' => true ], ] ), wp_json_encode( [ $this->get_form_amp_state_id( $form_id ) => [ 'submitting' => false ], ] ) ); // Upgrade the form to be an amp-form to avoid sanitizer conversion. if ( isset( $form_atts['atts']['action'] ) ) { $form_atts['atts']['action-xhr'] = $form_atts['atts']['action']; $form_atts['atts']['verify-xhr'] = $form_atts['atts']['action-xhr']; unset( $form_atts['atts']['action'] ); } return $form_atts; } /** * Get the amp-state ID for a given form. * * @since 1.8.1 * * @param int $form_id Form ID. * * @return string State ID. */ private function get_form_amp_state_id( $form_id ) { return sprintf( 'wpforms_form_state_%d', $form_id ); } /** * Output AMP state. * * @since 1.8.1 * * @param array $form_data Form data and settings. * @param null $deprecated Deprecated. * @param string $title Form title. * @param string $description Form description. * @param array $errors Errors. * * @noinspection PhpUnusedParameterInspection */ public function output_state( $form_data, $deprecated, $title, $description, $errors ) { if ( ! $this->is_amp() ) { return; } $state = [ 'submitting' => false ]; $form_id = ! empty( $form_data['id'] ) ? (int) $form_data['id'] : 0; printf( '<amp-state id="%s"><script type="application/json">%s</script></amp-state>', $this->get_form_amp_state_id( $form_id ), // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped wp_json_encode( $state ) ); } /** * Output submit success template. * * @since 1.8.1 * * @param array $form_data Form data and settings. * * @return bool True if the template was printed. */ public function output_success_template( $form_data ) { if ( ! $this->is_amp() ) { return false; } $frontend = wpforms()->obj( 'frontend' ); if ( ! $frontend ) { return false; } $frontend->assets_confirmation( $form_data ); $class = (int) wpforms_setting( 'disable-css', '1' ) === 1 ? 'wpforms-confirmation-container-full' : 'wpforms-confirmation-container'; printf( '<div submit-success><template type="amp-mustache"><div class="%s {{#redirecting}}wpforms-redirection-message{{/redirecting}}">{{{message}}}</div></template></div>', esc_attr( $class ) ); return true; } /** * Output submit error template. * * @since 1.8.1 * * @return bool True if the template was printed. */ public function output_error_template() { if ( ! $this->is_amp() ) { return false; } echo '<div submit-error><template type="amp-mustache"><div class="wpforms-error-container"><p>{{{message}}}</p></div></template></div>'; return true; } /** * Get text attribute. * * @since 1.8.1 * * @param int $form_id Form ID. * @param array $settings Form settings. * @param string $submit Submit button text. * * @return string */ public function get_text_attr( $form_id, $settings, $submit ) { return sprintf( '%s.submitting ? %s : %s', $this->get_form_amp_state_id( $form_id ), wp_json_encode( $settings['submit_text_processing'], JSON_UNESCAPED_UNICODE ), wp_json_encode( $submit, JSON_UNESCAPED_UNICODE ) ); } /** * Output captcha. * * @since 1.8.1 * * @param bool $is_recaptcha_v3 Whether we use v3. * @param array $captcha_settings Captcha settings. * @param array $form_data Form data. * * @return bool */ public function output_captcha( $is_recaptcha_v3, $captcha_settings, $form_data ) { if ( ! $this->is_amp() ) { return false; } if ( $is_recaptcha_v3 ) { printf( '<amp-recaptcha-input name="wpforms[recaptcha]" data-sitekey="%s" data-action="%s" layout="nodisplay"></amp-recaptcha-input>', esc_attr( $captcha_settings['site_key'] ), esc_attr( 'wpforms_' . $form_data['id'] ) ); return true; } if ( is_super_admin() ) { $captcha_provider = $captcha_settings['provider'] === 'hcaptcha' ? esc_html__( 'hCaptcha', 'wpforms-lite' ) : esc_html__( 'Google reCAPTCHA v2', 'wpforms-lite' ); echo '<div class="wpforms-notice wpforms-warning" style="margin: 20px 0;">'; printf( wp_kses( /* translators: %1$s - CAPTCHA provider name, %2$s - URL to reCAPTCHA documentation. */ __( '%1$s is not supported by AMP and is currently disabled.<br><a href="%2$s" rel="noopener noreferrer" target="_blank">Upgrade to reCAPTCHA v3</a> for full AMP support. <br><em>Please note: this message is only displayed to site administrators.</em>', 'wpforms-lite' ), [ 'a' => [ 'href' => [], 'rel' => [], 'target' => [], ], 'br' => [], 'em' => [], ] ), $captcha_provider, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped 'https://wpforms.com/docs/setup-captcha-wpforms/' ); echo '</div>'; return true; } return false; } } Frontend/Classic.php 0000644 00000022302 15174710275 0010425 0 ustar 00 <?php namespace WPForms\Frontend; /** * Classic render engine class. * * @since 1.8.1 */ class Classic { /** * Current form data. * * @since 1.8.1 * * @var array */ public $form_data; /** * Hooks. * * @since 1.8.1 */ public function hooks() { } /** * Open form container. * * @since 1.8.1 * * @param string|array $classes Form container classes. * @param array $form_data Form data. * * @noinspection PhpUnusedParameterInspection */ public function form_container_open( $classes, $form_data ) { printf( '<div class="wpforms-container %s" id="wpforms-%d">', wpforms_sanitize_classes( $classes, true ), absint( $form_data['id'] ) ); } /** * Close form container. * * @since 1.8.1 */ public function form_container_close() { echo '</div> <!-- .wpforms-container -->'; } /** * The form has no fields. * * @since 1.8.1 */ public function form_is_empty() { echo '<!-- WPForms: no fields, form hidden -->'; } /** * Noscript message. * * @since 1.8.1 * * @param string $msg Noscript message. */ public function noscript( $msg ) { printf( '<noscript class="wpforms-error-noscript">%s</noscript>', esc_html( $msg ) ); } /** * Display form error. * * @since 1.8.1 * * @param string $type Error type. * @param string $error Error text. */ public function form_error( $type, $error ) { switch ( $type ) { case 'header': case 'footer': // phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped echo '<div class="wpforms-error-container">' . wpautop( wpforms_sanitize_error( $error ) ) . '</div>'; // phpcs:enable WordPress.Security.EscapeOutput.OutputNotEscaped break; case 'header_styled': case 'footer_styled': // phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped echo '<div class="wpforms-error-container wpforms-error-styled-container"><div class="wpforms-error">' . wpautop( wpforms_sanitize_error( $error ) ) . '</div></div>'; // phpcs:enable WordPress.Security.EscapeOutput.OutputNotEscaped break; case 'recaptcha': echo '<label id="wpforms-field_recaptcha-error" class="wpforms-error">' . wpforms_sanitize_error( $error ) . '</label>'; break; } } /** * Open fields area container. * * @since 1.8.1 */ public function fields_area_open() { echo '<div class="wpforms-field-container">'; } /** * Close fields area container. * * @since 1.8.1 */ public function fields_area_close() { echo '</div><!-- .wpforms-field-container -->'; } /** * Open container for each field. * * @since 1.8.1 * * @param array $field Field data and settings. * @param array $form_data Form data and settings. * * @noinspection HtmlUnknownAttribute * @noinspection PhpUnusedParameterInspection */ public function field_container_open( $field, $form_data ) { $container = $field['properties']['container']; $container['data']['field-id'] = wpforms_validate_field_id( $field['id'] ); printf( '<div %s>', wpforms_html_attributes( $container['id'], $container['class'], $container['data'], $container['attr'] ) ); } /** * Close container markup for each field. * * @since 1.8.1 * * @param array $field Field data and settings. * @param array $form_data Form data and settings. * * @noinspection PhpUnusedParameterInspection */ public function field_container_close( $field, $form_data ) { echo '</div>'; } /** * Open fieldset. * * @since 1.8.1 * * @param array $field Field data and settings. * @param array $form_data Form data and settings. * * @noinspection PhpUnusedParameterInspection */ public function field_fieldset_open( $field, $form_data ) { } /** * Close fieldset. * * @since 1.8.1 * * @param array $field Field data and settings. * @param array $form_data Form data and settings. * * @noinspection PhpUnusedParameterInspection */ public function field_fieldset_close( $field, $form_data ) { } /** * Field label. * * @since 1.8.1 * * @param array $field Field data and settings. * @param array $form_data Form data and settings. * * @noinspection HtmlUnknownAttribute * @noinspection PhpUnusedParameterInspection */ public function field_label( $field, $form_data ) { if ( empty( $field['properties']['label'] ) ) { return; } $label = $field['properties']['label']; $required = $label['required'] ? wpforms_get_field_required_label() : ''; printf( '<label %s>%s%s</label>', wpforms_html_attributes( $label['id'], $label['class'], $label['data'], $label['attr'] ), esc_html( $label['value'] ), // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped $required ); } /** * Field error. * * @since 1.8.1 * * @param array $field Field data and settings. * @param array $form_data Form data and settings. * * @noinspection HtmlUnknownAttribute * @noinspection PhpUnusedParameterInspection */ public function field_error( $field, $form_data ) { if ( empty( $field['properties']['error'] ) ) { return; } $error = $field['properties']['error']; printf( '<label %s>%s</label>', wpforms_html_attributes( $error['id'], $error['class'], $error['data'], $error['attr'] ), esc_html( $error['value'] ) ); } /** * Field description. * * @since 1.8.1 * * @param array $field Field data and settings. * @param array $form_data Form data and settings. * * @noinspection HtmlUnknownAttribute * @noinspection PhpUnusedParameterInspection */ public function field_description( $field, $form_data ) { if ( empty( $field['properties']['description'] ) ) { return; } $description = $field['properties']['description']; printf( '<div %s>%s</div>', wpforms_html_attributes( $description['id'], $description['class'], $description['data'], $description['attr'] ), do_shortcode( $description['value'] ) ); } /** * Confirmation. * * @since 1.8.1 * * @param string $confirmation_message Confirmation message. * @param string $class CSS class. * @param array $form_data Form data and settings. */ public function confirmation( $confirmation_message, $class, $form_data ) { $form_id = isset( $form_data['id'] ) ? $form_data['id'] : 0; printf( '<div class="%s" id="wpforms-confirmation-%d">%s</div>', wpforms_sanitize_classes( $class ), absint( $form_id ), // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped $confirmation_message ); } /** * Form head container. Form title and description. * * @since 1.8.1 * * @param bool $title Whether to display form title. * @param bool $description Whether to display form description. * @param array $form_data Form data. */ public function form_head_container( $title, $description, $form_data ) { $settings = $form_data['settings']; echo '<div class="wpforms-head-container">'; if ( $title === true && ! empty( $settings['form_title'] ) ) { echo '<div class="wpforms-title">' . esc_html( $settings['form_title'] ) . '</div>'; } if ( $description === true && ! empty( $settings['form_desc'] ) ) { echo '<div class="wpforms-description">'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo wpforms_process_smart_tags( $settings['form_desc'], $form_data, [], '', 'form-description' ); echo '</div>'; } echo '</div>'; } /** * Open submit container. * * @since 1.8.1 * * @param int $pages Information for multi-page forms. * @param array $form_data Form data and settings. * * @noinspection PhpUnusedParameterInspection * @noinspection HtmlUnknownAttribute */ public function submit_container_open( $pages, $form_data ) { printf( '<div class="wpforms-submit-container" %s>', $pages ? 'style="display:none;"' : '' ); } /** * Submit button. * * @since 1.8.1 * * @param int $form_id Form ID. * @param string $submit Submit text. * @param array $classes CSS classes. * @param array $data_attrs Data attributes. * @param array $attrs Other attributes. * @param array $form_data Form data and settings. * * @noinspection PhpUnusedParameterInspection */ public function submit_button( $form_id, $submit, $classes, $data_attrs, $attrs, $form_data ) { printf( '<button type="submit" name="wpforms[submit]" %s>%s</button>', wpforms_html_attributes( sprintf( 'wpforms-submit-%d', absint( $form_id ) ), $classes, $data_attrs, $attrs ), esc_html( $submit ) ); } /** * Submit button. * * @since 1.8.1 * * @param string $src Spinner image src attribute. * @param array $form_data Form data and settings. * * @noinspection PhpUnusedParameterInspection */ public function submit_spinner( $src, $form_data ) { printf( '<img src="%s" class="wpforms-submit-spinner" style="display: none;" width="26" height="26" alt="%s">', esc_url( $src ), esc_attr__( 'Loading', 'wpforms-lite' ) ); } /** * Open submit container. * * @since 1.8.1 * * @param array $form_data Form data and settings. * * @noinspection PhpUnusedParameterInspection */ public function submit_container_close( $form_data ) { echo '</div>'; } } Frontend/CSSVars.php 0000644 00000046145 15174710275 0010343 0 ustar 00 <?php namespace WPForms\Frontend; use WPForms\Integrations\Gutenberg\ThemesData; use WPForms\Lite\Integrations\Gutenberg\ThemesData as LiteThemesData; use WPForms\Pro\Integrations\Gutenberg\ThemesData as ProThemesData; use WPForms\Pro\Integrations\Gutenberg\StockPhotos; /** * CSS variables class. * * @since 1.8.1 */ class CSSVars { /** * Root vars and values. * * @since 1.8.1 * * @var array */ public const ROOT_VARS = [ 'field-border-radius' => '3px', 'field-border-style' => 'solid', 'field-border-size' => '1px', 'field-background-color' => self::WHITE, 'field-border-color' => 'rgba( 0, 0, 0, 0.25 )', 'field-text-color' => 'rgba( 0, 0, 0, 0.7 )', 'field-menu-color' => self::WHITE, 'label-color' => 'rgba( 0, 0, 0, 0.85 )', 'label-sublabel-color' => 'rgba( 0, 0, 0, 0.55 )', 'label-error-color' => '#d63637', 'button-border-radius' => '3px', 'button-border-style' => 'none', 'button-border-size' => '1px', 'button-background-color' => '#066aab', 'button-border-color' => '#066aab', 'button-text-color' => self::WHITE, 'page-break-color' => '#066aab', 'background-image' => 'none', 'background-position' => 'center center', 'background-repeat' => 'no-repeat', 'background-size' => 'cover', 'background-width' => '100px', 'background-height' => '100px', 'background-color' => 'rgba( 0, 0, 0, 0 )', 'background-url' => 'url()', 'container-padding' => '0px', 'container-border-style' => 'none', 'container-border-width' => '1px', 'container-border-color' => '#000000', 'container-border-radius' => '3px', ]; /** * Container shadow vars and values. * * @since 1.8.8 * * @var array */ public const CONTAINER_SHADOW_SIZE = [ 'none' => [ 'box-shadow' => 'none', ], 'small' => [ 'box-shadow' => '0px 3px 5px 0px rgba(0, 0, 0, 0.1)', ], 'medium' => [ 'box-shadow' => '0px 10px 20px 0px rgba(0, 0, 0, 0.1)', ], 'large' => [ 'box-shadow' => '0px 30px 50px -10px rgba(0, 0, 0, 0.15)', ], ]; /** * Field Size vars and values. * * @since 1.8.1 * * @var array */ public const FIELD_SIZE = [ 'small' => [ 'input-height' => '31px', 'input-spacing' => '10px', 'font-size' => '14px', 'line-height' => '17px', 'padding-h' => '9px', 'checkbox-size' => '14px', 'sublabel-spacing' => '5px', 'icon-size' => '0.75', ], 'medium' => [ 'input-height' => '43px', 'input-spacing' => '15px', 'font-size' => '16px', 'line-height' => '19px', 'padding-h' => '14px', 'checkbox-size' => '16px', 'sublabel-spacing' => '5px', 'icon-size' => '1', ], 'large' => [ 'input-height' => '50px', 'input-spacing' => '20px', 'font-size' => '18px', 'line-height' => '21px', 'padding-h' => '14px', 'checkbox-size' => '18px', 'sublabel-spacing' => '10px', 'icon-size' => '1.25', ], ]; /** * Label Size vars and values. * * @since 1.8.1 * * @var array */ public const LABEL_SIZE = [ 'small' => [ 'font-size' => '14px', 'line-height' => '17px', 'sublabel-font-size' => '13px', 'sublabel-line-height' => '16px', ], 'medium' => [ 'font-size' => '16px', 'line-height' => '19px', 'sublabel-font-size' => '14px', 'sublabel-line-height' => '17px', ], 'large' => [ 'font-size' => '18px', 'line-height' => '21px', 'sublabel-font-size' => '16px', 'sublabel-line-height' => '19px', ], ]; /** * Button Size vars and values. * * @since 1.8.1 * * @var array */ public const BUTTON_SIZE = [ 'small' => [ 'font-size' => '14px', 'height' => '37px', 'padding-h' => '15px', 'margin-top' => '5px', ], 'medium' => [ 'font-size' => '17px', 'height' => '41px', 'padding-h' => '15px', 'margin-top' => '10px', ], 'large' => [ 'font-size' => '20px', 'height' => '48px', 'padding-h' => '20px', 'margin-top' => '15px', ], ]; /** * Spare variables. * * @since 1.8.8 * * @var array */ private const SPARE_VARS = [ 'field-border-color' ]; /** * White color. * * @since 1.8.8 * * @var string */ private const WHITE = '#ffffff'; /** * Render engine. * * @since 1.8.1 * * @var string */ private $render_engine; /** * CSS variables. * * @since 1.8.1 * * @var array */ private $css_vars; /** * Flag to check if root CSS vars were output. * * @since 1.8.1 * * @var bool */ private $is_root_vars_displayed; /** * Initialize class. * * @since 1.8.1 */ public function init(): void { $this->init_vars(); } /** * CSS variables data. * * @since 1.8.1 */ private function init_vars(): void { $vars = []; $vars[':root'] = array_merge( self::ROOT_VARS, $this->get_complex_vars( 'field-size', self::FIELD_SIZE['medium'] ), $this->get_complex_vars( 'label-size', self::LABEL_SIZE['medium'] ), $this->get_complex_vars( 'button-size', self::BUTTON_SIZE['medium'] ), $this->get_complex_vars( 'container-shadow-size', self::CONTAINER_SHADOW_SIZE['none'] ) ); /** * Allows developers to modify default CSS variables which output on the frontend. * * @since 1.8.1 * * @param array $vars CSS variables two-dimensional array. * The first level keys is the CSS selector. * Second level keys is the variable name without the `--wpforms-` prefix. */ $this->css_vars = apply_filters( 'wpforms_frontend_css_vars_init_vars', $vars ); } /** * Get complex CSS variables data. * * @since 1.8.1 * * @param string $prefix CSS variable prefix. * @param array $values Values. */ public function get_complex_vars( string $prefix, array $values ): array { $vars = []; foreach ( $values as $key => $value ) { $vars[ "{$prefix}-{$key}" ] = $value; } return $vars; } /** * Get CSS variables data by selector. * * @since 1.8.1 * * @param string $selector Selector. * * @return array */ public function get_vars( string $selector = ':root' ): array { if ( empty( $this->css_vars[ $selector ] ) ) { return []; } return $this->css_vars[ $selector ]; } /** * Output root CSS variables. * * @since 1.8.1 * @since 1.8.1.2 Added $force argument. * @deprecated 1.9.3 * * @param bool $force Force output root variables. * * @noinspection PhpMissingParamTypeInspection */ public function output_root( $force = false ): void { _deprecated_function( __METHOD__, '1.9.3 of the WPForms plugin' ); if ( ! empty( $this->is_root_vars_displayed ) && empty( $force ) ) { return; } $this->output_selector_vars( ':root', $this->css_vars[':root'] ); $this->is_root_vars_displayed = true; } /** * Get root variables CSS. * * @since 1.9.3 * * @return string */ public function get_root_vars_css(): string { return $this->get_selector_vars_css( ':root', $this->css_vars[':root'] ); } /** * Output selector's CSS variables. * * @since 1.8.1 * * @param string $selector Selector. * @param array $vars Variables data. * @param string $style_id Style tag ID attribute. Optional. Default is an empty string. * @param string|int $form_id Form ID. Optional. Default is an empty string. */ public function output_selector_vars( string $selector, array $vars, string $style_id = '', $form_id = '' ): void { if ( empty( $this->render_engine ) ) { $this->render_engine = wpforms_get_render_engine(); } if ( $this->render_engine === 'classic' ) { return; } // If this is not full "Base and Form Theme Styling", skip. if ( (int) wpforms_setting( 'disable-css', '1' ) !== 1 ) { return; } $style_id = empty( $style_id ) ? 'wpforms-css-vars-' . $selector : $style_id; printf( '<style id="%1$s"> %2$s </style>', sanitize_key( $style_id ), $this->get_selector_vars_css( $selector, $vars, $form_id ) // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ); } /** * Output CSS vars for the form added as a shortcode. * * @since 1.9.7 * * @param array $atts Shortcode attributes. */ public function output_css_vars_for_shortcode( array $atts ): void { if ( empty( $atts['id'] ) ) { return; } $form_handler = wpforms()->obj( 'form' ); if ( ! $form_handler ) { return; } $form_id = (int) $atts['id']; $form_data = $form_handler->get( $form_id, [ 'content_only' => true ] ); if ( empty( $form_data ) ) { return; } $attr = isset( $form_data['settings']['themes'] ) ? (array) $form_data['settings']['themes'] : []; $attr = $this->maybe_override_attributes( $attr ); $css_vars = $this->get_customized_css_vars( $attr ); $css_vars = $this->add_css_vars_units( $css_vars ); $selector = "#wpforms-{$form_id}"; $style_id = "wpforms-css-vars-{$form_id}"; $this->output_selector_vars( $selector, $css_vars, $style_id, $form_id ); $this->output_custom_css( $attr, $selector, $style_id ); } /** * Output custom CSS. * * @since 1.9.7 * * @param array $attr Attributes. * @param string $selector Selector. * @param string $style_id Style ID. * * @noinspection PhpMissingParamTypeInspection */ private function output_custom_css( array $attr, string $selector, string $style_id ): void { if ( wpforms_get_render_engine() === 'classic' ) { return; } $custom_css = trim( $attr['customCss'] ?? '' ); if ( empty( $custom_css ) ) { return; } printf( '<style id="%1$s-custom-css"> %2$s { %3$s } </style>', sanitize_key( $style_id ), $selector, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped wp_strip_all_tags( $custom_css ) // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ); } /** * Maybe override attributes with themes.json settings. * * @since 1.9.7 * * @param array $attr Attributes. * * @return array */ private function maybe_override_attributes( array $attr ): array { $theme_slug = (string) ( $attr['wpformsTheme'] ?? '' ); if ( empty( $theme_slug ) ) { return $attr; } $attr = $this->normalize_background_url( $attr ); $theme_data = $this->get_themes_data_object()->get_theme( $theme_slug ); $settings = $theme_data['settings'] ?? []; return array_merge( $attr, $settings ); } /** * Normalize background URL. * * Check if the background URL is not wrapped in url() and add it if needed. * * @since 1.9.7 * * @param array $attr Attributes. */ private function normalize_background_url( array $attr ): array { if ( ! isset( $attr['backgroundUrl'] ) ) { return $attr; } if ( strpos( $attr['backgroundUrl'], 'url(' ) === 0 ) { return $attr; } $attr['backgroundUrl'] = 'url(' . $attr['backgroundUrl'] . ')'; return $attr; } /** * Get themes data object. * * @since 1.9.7 * * @return ThemesData */ private function get_themes_data_object(): ThemesData { if ( wpforms()->is_pro() ) { return new ProThemesData( new StockPhotos() ); } return new LiteThemesData(); } /** * Add CSS vars units. * * Form builder saves values without pixels, we need to add them before outputting as CSS vars. * * @since 1.9.7 * * @param array $css_vars CSS vars. * * @return array */ private function add_css_vars_units( array $css_vars ): array { $has_pixels = [ 'field-border-size', 'field-border-radius', 'button-border-size', 'button-border-radius', 'container-padding', 'container-border-width', 'container-border-radius', ]; foreach ( $has_pixels as $key ) { if ( isset( $css_vars[ $key ] ) && is_numeric( $css_vars[ $key ] ) && $css_vars[ $key ] > 0 ) { $css_vars[ $key ] .= 'px'; } } return $css_vars; } /** * Get selector variables CSS. * * @since 1.9.3 * * @param string $selector Selector. * @param array $vars Variables data. * @param string|int $form_id Form ID. Optional. Default is an empty string. * * @return string */ private function get_selector_vars_css( string $selector, array $vars, $form_id = '' ): string { return sprintf( '%1$s { %2$s }', $selector, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped wp_strip_all_tags( $this->get_vars_css( $vars, $form_id ) ) ); } /** * Pre print vars filter. * * @since 1.8.8 * * @param array $vars Variables data. * @param string|int $form_id Form ID. Optional. Default is an empty string. * * @return array */ private function get_pre_print_vars( array $vars, $form_id = '' ): array { // Normalize the `background-url` variable. if ( isset( $vars['background-url'] ) ) { $vars['background-url'] = $vars['background-url'] === 'url()' ? 'none' : $vars['background-url']; } /** * Filter CSS variables right before printing the CSS. * * @since 1.8.8 * * @param array $vars CSS variables. * @param int $form_id Form ID. Optional. Default is an empty string. */ return (array) apply_filters( 'wpforms_frontend_css_vars_pre_print_filter', $vars, $form_id ); } /** * Generate CSS code from given vars data. * * @since 1.8.1 * * @param array $vars Variables data. * @param string|int $form_id Form ID. Optional. Default is an empty string. */ private function get_vars_css( array $vars, $form_id = '' ): string { $vars = $this->get_pre_print_vars( $vars, $form_id ); $result = ''; foreach ( $vars as $name => $value ) { if ( ! is_string( $value ) ) { continue; } if ( $value === '0' ) { $value = '0px'; } $result .= "--wpforms-{$name}: {$value};\n"; if ( in_array( $name, self::SPARE_VARS, true ) ) { $result .= "--wpforms-{$name}-spare: {$value};\n"; } } return $result; } /** * Get customized CSS vars. * * @since 1.8.3 * * @param array $attr Attributes passed by integration. * * @return array */ public function get_customized_css_vars( array $attr ): array { // phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh $root_css_vars = $this->get_vars(); $css_vars = []; foreach ( $attr as $key => $value ) { $var_name = strtolower( preg_replace( '/[A-Z]/', '-$0', $key ) ); // Skip an attribute that is not the CSS var or has the default value. if ( empty( $root_css_vars[ $var_name ] ) || $root_css_vars[ $var_name ] === $value ) { continue; } $css_vars[ $var_name ] = $value; } // Reset border size in case of border style is `none`. $css_vars = $this->maybe_reset_border( $css_vars, 'field-border' ); $css_vars = $this->maybe_reset_border( $css_vars, 'button-border' ); // Set the button alternative background color and use border color for accent in case of transparent color. $button_bg_color = $css_vars['button-background-color'] ?? $root_css_vars['button-background-color']; if ( $this->is_transparent_color( $button_bg_color ) ) { $css_vars['button-background-color-alt'] = $button_bg_color; $border_color = $css_vars['button-border-color'] ?? $root_css_vars['button-border-color']; $css_vars['button-background-color'] = $this->is_transparent_color( $border_color ) ? $root_css_vars['button-background-color'] : $border_color; $button_bg_color = $css_vars['button-background-color']; } $button_bg_color = strtolower( $button_bg_color ); // Set the button alternative text color in case if the background and text color are identical. $button_text_color = strtolower( $css_vars['button-text-color'] ?? $root_css_vars['button-text-color'] ); if ( $button_bg_color === $button_text_color || $this->is_transparent_color( $button_text_color ) ) { $css_vars['button-text-color-alt'] = $this->get_contrast_color( $button_bg_color ); } $size_css_vars = $this->get_size_css_vars( $attr ); return array_merge( $css_vars, $size_css_vars ); } /** * Reset border size in case of border style is `none`. * * @since 1.9.7 * * @param array $css_vars CSS vars. * @param string $key Key. * * @return array */ private function maybe_reset_border( array $css_vars, string $key ): array { $style_key = $key . '-style'; $size_key = $key . '-size'; if ( isset( $css_vars[ $style_key ] ) && $css_vars[ $style_key ] === 'none' ) { $css_vars[ $size_key ] = '0px'; } return $css_vars; } /** * Checks if the provided color has transparency. * * @since 1.8.8 * * @param string $color The color to check. * * @return bool */ private function is_transparent_color( string $color ): bool { $rgba = $this->get_color_as_rgb_array( $color ); $opacity_threshold = 0.33; $opacity = $rgba[3] ?? 1; return $opacity < $opacity_threshold; } /** * Get contrast color relative to a given color. * * @since 1.8.8 * * @param string|array $color The color. * * @return string */ private function get_contrast_color( $color ): string { $rgba = is_array( $color ) ? $color : $this->get_color_as_rgb_array( $color ); $avg = (int) ( ( ( array_sum( $rgba ) ) / 3 ) * ( $rgba[3] ?? 1 ) ); return $avg < 128 ? '#ffffff' : '#000000'; } /** * Get size CSS vars. * * @since 1.8.3 * @since 1.8.8 Removed $css_vars argument. * * @param array $attr Attributes passed by integration. * * @return array */ private function get_size_css_vars( array $attr ): array { $size_items = [ 'field', 'label', 'button', 'container-shadow' ]; $size_css_vars = []; foreach ( $size_items as $item ) { $item_attr = preg_replace_callback( '/-(\w)/', static function ( $matches ) { return strtoupper( $matches[1] ); }, $item ); $item_attr .= 'Size'; $item_key = $item . '-size'; $item_constant = 'self::' . str_replace( '-', '_', strtoupper( $item ) ) . '_SIZE'; if ( empty( $attr[ $item_attr ] ) ) { continue; } $size_css_vars[] = $this->get_complex_vars( $item_key, constant( $item_constant )[ $attr[ $item_attr ] ] ); } return empty( $size_css_vars ) ? [] : array_merge( ...$size_css_vars ); } /** * Get color as an array of RGB(A) values. * * @since 1.8.8 * * @param string $color Color. * * @return array|bool Color as an array of RGBA values. False on error. */ private function get_color_as_rgb_array( string $color ) { // Remove # from the beginning of the string and remove whitespaces. $color = preg_replace( '/^#/', '', strtolower( trim( $color ) ) ); $color = str_replace( ' ', '', (string) $color ); if ( $color === 'transparent' ) { $color = 'rgba(0,0,0,0)'; } $rgba = $color; $rgb_array = []; // Check if color is in HEX(A) format. $is_hex = preg_match( '/[0-9a-f]{6,8}$/', $rgba ); if ( $is_hex ) { // Search and split HEX(A) color into an array of char couples. preg_match_all( '/\w\w/', $rgba, $rgb_array ); $rgb_array = array_map( static function ( $value ) { return hexdec( '0x' . $value ); }, $rgb_array[0] ?? [] ); $rgb_array[3] = ( $rgb_array[3] ?? 255 ) / 255; } else { $rgba = preg_replace( '/[^\d,.]/', '', $rgba ); $rgb_array = explode( ',', $rgba ); } return $rgb_array; } } Frontend/Modern.php 0000644 00000021346 15174710275 0010277 0 ustar 00 <?php namespace WPForms\Frontend; /** * Modern render engine class. * * @since 1.8.1 */ class Modern extends Classic { /** * Hooks. * * @since 1.8.1 */ public function hooks() { add_filter( 'wpforms_field_properties', [ $this, 'field_properties' ], 10, 3 ); add_filter( 'wpforms_get_field_required_label', [ $this, 'get_field_required_label' ], 10 ); add_filter( 'wpforms_frontend_strings', [ $this, 'frontend_strings' ], 10 ); } /** * Open form container. * * @since 1.8.1 * * @param string $classes Form container classes. * @param array $form_data Form data. */ public function form_container_open( $classes, $form_data ) { $classes[] = 'wpforms-render-modern'; parent::form_container_open( $classes, $form_data ); } /** * Noscript message. * * @since 1.8.1 * * @param string $msg Noscript message. */ public function noscript( $msg ) { printf( '<noscript class="wpforms-error-noscript">%1$s</noscript><div id="wpforms-error-noscript" style="display: none;">%1$s</div>', esc_html( $msg ) ); } /** * Display form error. * * @since 1.8.1 * * @param string $type Error type. * @param string $error Error text. */ public function form_error( $type, $error ) { switch ( $type ) { case 'header': case 'footer': printf( '<div id="wpforms-%1$s-%2$s-error" class="wpforms-error-container" role="alert"> <span class="wpforms-hidden" aria-hidden="false">%3$s</span>%4$s </div>', esc_attr( $this->form_data['id'] ?? 0 ), esc_attr( $type ), esc_html__( 'Form error message', 'wpforms-lite' ), wpautop( wpforms_sanitize_error( $error ) ) // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ); break; case 'header_styled': case 'footer_styled': printf( '<div id="wpforms-%1$s-%2$s-error" class="wpforms-error-container wpforms-error-styled-container" role="alert"> <div class="wpforms-error"><span class="wpforms-hidden" aria-hidden="false">%3$s</span>%4$s</div> </div>', esc_attr( $this->form_data['id'] ), esc_attr( $type ), esc_html__( 'Form error message', 'wpforms-lite' ), wpautop( wpforms_sanitize_error( $error ) ) // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ); break; case 'header_styled': case 'footer_styled': printf( '<div id="wpforms-%1$s-%2$s-error" class="wpforms-error-container wpforms-error-styled-container" role="alert"> <div class="wpforms-error"><span class="wpforms-hidden" aria-hidden="false">%3$s</span>%4$s</div> </div>', esc_attr( $this->form_data['id'] ), esc_attr( $type ), esc_html__( 'Form error message', 'wpforms-lite' ), wpautop( wpforms_sanitize_error( $error ) ) // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ); break; case 'header_styled': case 'footer_styled': printf( '<div id="wpforms-%1$s-%2$s-error" class="wpforms-error-container wpforms-error-styled-container" role="alert"> <div class="wpforms-error"><span class="wpforms-hidden" aria-hidden="false">%3$s</span>%4$s</div> </div>', esc_attr( $this->form_data['id'] ), esc_attr( $type ), esc_html__( 'Form error message', 'wpforms-lite' ), wpautop( wpforms_sanitize_error( $error ) ) // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ); break; case 'recaptcha': printf( '<em id="wpforms-field_recaptcha-error" class="wpforms-error" role="alert"> <span class="wpforms-hidden" aria-hidden="false">%1$s</span>%2$s </em>', esc_attr__( 'Recaptcha error message', 'wpforms-lite' ), wpforms_sanitize_error( $error ) // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ); break; } } /** * Field label markup. * * @since 1.8.1 * * @param array $field Field data and settings. * @param array $form_data Form data and settings. */ public function field_label( $field, $form_data ) { // Do not need to output label if the field requires fieldset. if ( $this->is_field_requires_fieldset( $field ) ) { return; } if ( ! empty( $field['label_hide'] ) ) { $field['properties']['label']['attr']['aria-hidden'] = 'false'; } parent::field_label( $field, $form_data ); } /** * Open fieldset markup. * * @since 1.8.1 * * @param array $field Field data and settings. * @param array $form_data Form data and settings. */ public function field_fieldset_open( $field, $form_data ) { if ( ! $this->is_field_requires_fieldset( $field ) ) { return; } if ( ! empty( $field['label_hide'] ) ) { $field['properties']['label']['attr']['aria-hidden'] = 'false'; } $label = $field['properties']['label']; $required = $label['required'] ? wpforms_get_field_required_label() : ''; unset( $label['attr']['for'] ); printf( '<fieldset><legend %s>%s%s</legend>', wpforms_html_attributes( $label['id'], $label['class'], $label['data'], $label['attr'] ), esc_html( $label['value'] ), // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped $required ); } /** * Close fieldset markup. * * @since 1.8.1 * * @param array $field Field data and settings. * @param array $form_data Form data and settings. */ public function field_fieldset_close( $field, $form_data ) { if ( ! $this->is_field_requires_fieldset( $field ) ) { return; } echo '</fieldset>'; } /** * Whether the field requires fieldset markup. * * @since 1.8.1 * * @param array $field Field data and settings. */ private function is_field_requires_fieldset( $field ) { if ( empty( $field['type'] ) ) { return false; } /** * Determine whether the field is requires fieldset+legend markup on the frontend. * * @since 1.8.1 * * @param bool $requires_fieldset True if requires. Defaults to false. * @param array $field Field data. */ return (bool) apply_filters( "wpforms_frontend_modern_is_field_requires_fieldset_{$field['type']}", false, $field ); } /** * Field error. * * @since 1.8.1 * * @param array $field Field data and settings. * @param array $form_data Form data and settings. * * @noinspection HtmlUnknownAttribute */ public function field_error( $field, $form_data ) { if ( empty( $field['properties']['error'] ) ) { return; } $error = $field['properties']['error']; printf( '<em %1$s>%2$s</em>', wpforms_html_attributes( $error['id'], $error['class'], $error['data'], $error['attr'] ), esc_html( $error['value'] ) ); } /** * Define additional field properties. * * @since 1.8.1 * * @param array $properties Field properties. * @param array $field Field settings. * @param array $form_data Form data and settings. * * @return array */ public function field_properties( $properties, $field, $form_data ) { $field_id = "wpforms-{$form_data['id']}-field_{$field['id']}"; $desc_id = "{$field_id}-description"; // Add `id` to field description. $properties['description']['id'] = $desc_id; // Add attributes to error message. $properties['error']['attr']['role'] = 'alert'; $properties['error']['attr']['aria-label'] = esc_html__( 'Error message', 'wpforms-lite' ); $properties['error']['attr']['id'] = $properties['error']['attr']['for'] . '-error'; $properties['error']['attr']['for'] = ''; foreach ( $properties['inputs'] as $input => $input_data ) { // Add `aria-errormessage` to inputs (except hidden according to W3C requirements). if ( ! empty( $input_data['id'] ) && $field['type'] !== 'hidden' ) { $properties['inputs'][ $input ]['attr']['aria-errormessage'] = "{$input_data['id']}-error"; } // Add `aria-describedby` to inputs. if ( ! empty( $field['description'] ) ) { $properties['inputs'][ $input ]['attr']['aria-describedby'] = $desc_id; } } return $properties; } /** * Required label (asterisk) markup. * * @since 1.8.1 * * @param string $label_html Required label markup. * * @return string */ public function get_field_required_label( $label_html ) { return ' <span class="wpforms-required-label" aria-hidden="true">*</span>'; } /** * Modify javascript `wpforms_settings` properties on the front end. * * @since 1.8.1 * * @param array $strings Array `wpforms_settings` properties. * * @return array */ public function frontend_strings( $strings ) { $strings['isModernMarkupEnabled'] = wpforms_get_render_engine() === 'modern'; $strings['formErrorMessagePrefix'] = esc_html__( 'Form error message', 'wpforms-lite' ); $strings['errorMessagePrefix'] = esc_html__( 'Error message', 'wpforms-lite' ); $strings['submitBtnDisabled'] = esc_html__( 'Submit button is disabled during form submission.', 'wpforms-lite' ); return $strings; } } Frontend/Captcha.php 0000644 00000044611 15174710275 0010416 0 ustar 00 <?php namespace WPForms\Frontend; /** * Captcha class. * * @since 1.8.1 */ class Captcha { /** * Initialize class. * * @since 1.8.1 */ public function init() { $this->hooks(); } /** * Register hooks. * * @since 1.8.1 */ private function hooks() { // Filters. add_filter( 'script_loader_tag', [ $this, 'set_defer_attribute' ], 10, 3 ); // Actions. add_action( 'send_headers', [ $this, 'send_headers' ] ); add_action( 'wpforms_frontend_output', [ $this, 'recaptcha' ], 20, 5 ); add_action( 'wp_enqueue_scripts', [ $this, 'recaptcha_noconflict' ], 9999 ); add_action( 'wp_footer', [ $this, 'recaptcha_noconflict' ], 19 ); add_action( 'wpforms_wp_footer', [ $this, 'assets_recaptcha' ] ); } /** * Send HTTP headers to prevent warning in the browser console. * * @since 1.9.8.3 */ public function send_headers(): void { if ( headers_sent() ) { return; } $urls = '"https://www.google.com" "https://www.gstatic.com" "https://recaptcha.net" "https://challenges.cloudflare.com" "https://hcaptcha.com"'; header( 'Permissions-Policy: ' . "private-state-token-redemption=(self $urls), " . "private-state-token-issuance=(self $urls)", false ); } /** * CAPTCHA output if configured. * * @since 1.8.1 * * @param array $form_data Form data and settings. * @param null $deprecated Deprecated in v1.3.7, previously was $form object. * @param bool $title Whether to display form title. * @param bool $description Whether to display form description. * @param array $errors List of all errors filled in WPForms_Process::process(). * * @noinspection HtmlUnknownAttribute * @noinspection PhpUnusedParameterInspection */ public function recaptcha( $form_data, $deprecated, $title, $description, $errors ) { // Check that CAPTCHA is configured in the settings. $captcha_settings = $this->get_form_captcha_settings( $form_data ); if ( ! $captcha_settings ) { return; } $frontend = wpforms()->obj( 'frontend' ); $container_classes = [ 'wpforms-recaptcha-container', 'wpforms-is-' . $captcha_settings['provider'] ]; if ( $captcha_settings['provider'] === 'recaptcha' ) { $container_classes[] = 'wpforms-is-recaptcha-type-' . $captcha_settings['recaptcha_type']; } printf( '<div class="%1$s" %2$s>', wpforms_sanitize_classes( $container_classes, true ), $frontend->pages ? 'style="display:none;"' : '' ); $this->print_recaptcha_fields( $captcha_settings, $form_data ); if ( ! empty( $errors['recaptcha'] ) ) { $frontend->form_error( 'recaptcha', $errors['recaptcha'] ); } echo '</div>'; } /** * Get a provider-specific captcha class. * * @since 1.9.8.3 * * @param string $provider Captcha provider. * * @return string */ private function get_captcha_class( string $provider ): string { $classes = [ 'recaptcha' => 'g-recaptcha', 'hcaptcha' => 'h-captcha', 'turnstile' => 'wpforms-turnstile', ]; return $classes[ $provider ] ?? 'g-recaptcha'; } /** * Get recaptcha data. * * @since 1.8.6 * * @param array $captcha_settings Captcha settings. * @param array $form_data Form data and settings. * * @return array */ private function get_recaptcha_data( array $captcha_settings, array $form_data ): array { /** * Filters captcha sitekey. * * @since 1.7.1 * * @param array $sitekey Sitekey. * @param array $form_data Form data and settings. */ $data = apply_filters( // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName 'wpforms_frontend_recaptcha', [ 'sitekey' => $captcha_settings['site_key'] ], $form_data ); $is_recaptcha = $captcha_settings['provider'] === 'recaptcha'; $is_turnstile = $captcha_settings['provider'] === 'turnstile'; if ( $is_recaptcha && $captcha_settings['recaptcha_type'] === 'invisible' ) { $data['size'] = 'invisible'; } if ( ! $is_turnstile ) { return $data; } /** * Filter Turnstile action value. * * @since 1.8.1 * * @param string $action Action value. Can only contain up to 32 alphanumeric characters including _ and -. * @param array $form_data Form data and settings. */ $data['action'] = apply_filters( // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName 'wpforms_frontend_recaptcha_turnstile_action', sprintf( 'FormID-%d', $form_data['id'] ), $form_data ); return $data; } /** * Print recaptcha fields. * * @since 1.8.6 * * @param array $captcha_settings Captcha settings. * @param array $form_data Form data and settings. */ private function print_recaptcha_fields( array $captcha_settings, array $form_data ) { $data = $this->get_recaptcha_data( $captcha_settings, $form_data ); $is_recaptcha = $captcha_settings['provider'] === 'recaptcha'; $is_recaptcha_v3 = $is_recaptcha && $captcha_settings['recaptcha_type'] === 'v3'; if ( $is_recaptcha_v3 ) { // The value adds via JS code. echo '<input type="hidden" name="wpforms[recaptcha]" value="">'; return; } $captcha_class = $this->get_captcha_class( $captcha_settings['provider'] ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo '<div ' . wpforms_html_attributes( '', [ $captcha_class ], $data ) . '></div>'; if ( $is_recaptcha && $captcha_settings['recaptcha_type'] === 'invisible' ) { return; } printf( '<input type="text" name="g-recaptcha-hidden" class="wpforms-recaptcha-hidden" style="position:absolute!important;clip:rect(0,0,0,0)!important;height:1px!important;width:1px!important;border:0!important;overflow:hidden!important;padding:0!important;margin:0!important;" data-rule-%1$s="1">', esc_attr( $captcha_settings['provider'] ) ); } /** * Get captcha settings for form output. * Return null if captcha is disabled. * * @since 1.8.1 * * @param array $form_data Form data and settings. * * @return array|null * @noinspection NullPointerExceptionInspection */ private function get_form_captcha_settings( $form_data ) { $captcha_settings = wpforms_get_captcha_settings(); if ( empty( $captcha_settings['provider'] ) || $captcha_settings['provider'] === 'none' || empty( $captcha_settings['site_key'] ) || empty( $captcha_settings['secret_key'] ) ) { return null; } // Check that the CAPTCHA is configured for the specific form. if ( ! isset( $form_data['settings']['recaptcha'] ) || $form_data['settings']['recaptcha'] !== '1' ) { return null; } $is_recaptcha_v3 = $captcha_settings['provider'] === 'recaptcha' && $captcha_settings['recaptcha_type'] === 'v3'; if ( wpforms()->obj( 'amp' )->output_captcha( $is_recaptcha_v3, $captcha_settings, $form_data ) ) { return null; } return $captcha_settings; } /** * Google reCAPTCHA no-conflict mode. * * When enabled in the WPForms settings, forcefully remove all other * reCAPTCHA enqueues to prevent conflicts. Filter can be used to target * specific pages, etc. * * @since 1.4.5 * @since 1.6.4 Added hCaptcha support. */ public function recaptcha_noconflict() { $captcha_settings = wpforms_get_captcha_settings(); if ( empty( $captcha_settings['provider'] ) || $captcha_settings['provider'] === 'none' || empty( wpforms_setting( 'recaptcha-noconflict' ) ) || /** * Filters recaptcha no conflict flag. * * @since 1.6.4 * * @param bool $recaptcha_no_conflict No conflict flag. */ ! apply_filters( 'wpforms_frontend_recaptcha_noconflict', true ) // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName ) { return; } $scripts = wp_scripts(); $urls = [ 'google.com/recaptcha', 'gstatic.com/recaptcha', 'hcaptcha.com/1' ]; foreach ( $scripts->queue as $handle ) { // Skip the WPForms javascript-assets. if ( ! isset( $scripts->registered[ $handle ] ) || false !== strpos( $scripts->registered[ $handle ]->handle, 'wpforms' ) ) { return; } foreach ( $urls as $url ) { if ( false !== strpos( $scripts->registered[ $handle ]->src, $url ) ) { wp_dequeue_script( $handle ); wp_deregister_script( $handle ); break; } } } } /** * Load the assets needed for the CAPTCHA. * * @since 1.6.2 * @since 1.6.4 Added hCaptcha support. * * @param array $forms Forms being displayed. */ public function assets_recaptcha( $forms ) { $captcha_settings = $this->get_assets_captcha_settings( $forms ); if ( ! $captcha_settings ) { return; } $is_recaptcha_v3 = $captcha_settings['provider'] === 'recaptcha' && $captcha_settings['recaptcha_type'] === 'v3'; $recaptcha_url = $is_recaptcha_v3 ? 'https://www.google.com/recaptcha/api.js?render=' . $captcha_settings['site_key'] : /** * For backward compatibility reason we have to filter only the v2 reCAPTCHA. * * @since 1.4.0 * * @param string $url The reCaptcha v2 URL. */ apply_filters( 'wpforms_frontend_recaptcha_url', 'https://www.google.com/recaptcha/api.js?onload=wpformsRecaptchaLoad&render=explicit' ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName $captcha_api_array = [ 'hcaptcha' => 'https://hcaptcha.com/1/api.js?onload=wpformsRecaptchaLoad&render=explicit&recaptchacompat=off', 'recaptcha' => $recaptcha_url, 'turnstile' => 'https://challenges.cloudflare.com/turnstile/v0/api.js?onload=wpformsRecaptchaLoad&render=explicit', ]; /** * Filter the CAPTCHA API URL. * * @since 1.6.4 * * @param string $captcha_api The CAPTCHA API URL. */ $captcha_api = apply_filters( 'wpforms_frontend_captcha_api', $captcha_api_array[ $captcha_settings['provider'] ] ); $in_footer = ! wpforms_is_frontend_js_header_force_load(); wp_enqueue_script( 'wpforms-recaptcha', $captcha_api, $is_recaptcha_v3 ? [] : [ 'jquery' ], null, // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion $in_footer ); /** * Filter the string containing the CAPTCHA JavaScript to be added. * * @since 1.6.4 * * @param string $captcha_inline The CAPTCHA JavaScript. */ $captcha_inline = apply_filters( 'wpforms_frontend_captcha_inline_script', $this->get_captcha_inline_script( $captcha_settings ) ); wp_add_inline_script( 'wpforms-recaptcha', $captcha_inline ); } /** * Get captcha settings for assets output. * Return null if captcha is disabled. * * @since 1.8.1 * * @param array $forms Forms being displayed. * * @return array|null * @noinspection NullPointerExceptionInspection */ private function get_assets_captcha_settings( $forms ) { /** * Filters disable captcha switch. * * @since 1.6.2 * * @param bool $is_captcha_disabled Whether captcha is disabled. */ if ( apply_filters( 'wpforms_frontend_recaptcha_disable', false ) ) { // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName return null; } // Load CAPTCHA support if form supports it. $captcha_settings = wpforms_get_captcha_settings(); if ( empty( $captcha_settings['provider'] ) || $captcha_settings['provider'] === 'none' || empty( $captcha_settings['site_key'] ) || empty( $captcha_settings['secret_key'] ) ) { return null; } // Whether at least 1 form on a page has CAPTCHA enabled. $captcha = false; foreach ( $forms as $form ) { if ( ! empty( $form['settings']['recaptcha'] ) ) { $captcha = true; break; } } // Return early. if ( ! $captcha && ! wpforms()->obj( 'frontend' )->assets_global() ) { return null; } return $captcha_settings; } /** * Retrieve the string containing the CAPTCHA inline javascript. * * @since 1.6.4 * * @param array $captcha_settings The CAPTCHA settings. * * @return string * @noinspection JSUnusedLocalSymbols * @noinspection UnnecessaryLocalVariableJS * @noinspection JSUnresolvedVariable * @noinspection JSDeprecatedSymbols * @noinspection JSUnresolvedFunction */ protected function get_captcha_inline_script( $captcha_settings ) { // IE11 polyfills for native `matches()` and `closest()` methods. $polyfills = /** @lang JavaScript */ 'if (!Element.prototype.matches) { Element.prototype.matches = Element.prototype.msMatchesSelector || Element.prototype.webkitMatchesSelector; } if (!Element.prototype.closest) { Element.prototype.closest = function (s) { var el = this; do { if (Element.prototype.matches.call(el, s)) { return el; } el = el.parentElement || el.parentNode; } while (el !== null && el.nodeType === 1); return null; }; } '; // Native equivalent for jQuery's `trigger()` method. $dispatch = /** @lang JavaScript */ 'var wpformsDispatchEvent = function (el, ev, custom) { var e = document.createEvent(custom ? "CustomEvent" : "HTMLEvents"); custom ? e.initCustomEvent(ev, true, true, false) : e.initEvent(ev, true, true); el.dispatchEvent(e); }; '; // Update container class after changing Turnstile type. $turnstile_update_class = /** @lang JavaScript */ 'var turnstileUpdateContainer = function (el) { let form = el.closest( "form" ), iframeWrapperHeight = el.offsetHeight; parseInt(iframeWrapperHeight) === 0 ? form.querySelector(".wpforms-is-turnstile").classList.add( "wpforms-is-turnstile-invisible" ) : form.querySelector(".wpforms-is-turnstile").classList.remove( "wpforms-is-turnstile-invisible" ); }; '; // Captcha callback, used by hCaptcha and checkbox reCaptcha v2. $callback = /** @lang JavaScript */ 'var wpformsRecaptchaCallback = function (el) { var hdn = el.parentNode.querySelector(".wpforms-recaptcha-hidden"); var err = el.parentNode.querySelector("#g-recaptcha-hidden-error"); hdn.value = "1"; wpformsDispatchEvent(hdn, "change", false); hdn.classList.remove("wpforms-error"); err && hdn.parentNode.removeChild(err); }; '; $sync = /** @lang JavaScript */ 'const wpformsRecaptchaSync = ( func ) => { return function() { const context = this; const args = arguments; // Sync with jQuery ready event. jQuery( document ).ready( function() { func.apply( context, args ); } ); } }; '; if ( $captcha_settings['provider'] === 'hcaptcha' ) { $data = $dispatch; $data .= $callback; $data .= /** @lang JavaScript */ 'var wpformsRecaptchaLoad = function () { Array.prototype.forEach.call(document.querySelectorAll(".h-captcha"), function (el) { var captchaID = hcaptcha.render(el, { callback: function () { wpformsRecaptchaCallback(el); } }); el.setAttribute("data-recaptcha-id", captchaID); }); wpformsDispatchEvent(document, "wpformsRecaptchaLoaded", true); }; '; return $data; } if ( $captcha_settings['provider'] === 'turnstile' ) { $data = $dispatch; $data .= $callback; $data .= $turnstile_update_class; $data .= /** @lang JavaScript */ 'var wpformsRecaptchaLoad = function () { Array.prototype.forEach.call(document.querySelectorAll(".wpforms-turnstile"), function (el) { let form = el.closest( "form" ), formId = form.dataset.formid, captchaID = turnstile.render(el, { theme: "' . $captcha_settings['theme'] . '", callback: function () { turnstileUpdateContainer(el); wpformsRecaptchaCallback(el); }, "timeout-callback": function() { turnstileUpdateContainer(el); } }); el.setAttribute("data-recaptcha-id", captchaID); }); wpformsDispatchEvent( document, "wpformsRecaptchaLoaded", true ); }; '; return $data; } if ( $captcha_settings['recaptcha_type'] === 'v3' ) { $data = $dispatch; $data .= /** @lang JavaScript */ 'var wpformsRecaptchaV3Execute = function ( callback ) { grecaptcha.execute( "' . $captcha_settings['site_key'] . '", { action: "wpforms" } ).then( function ( token ) { Array.prototype.forEach.call( document.getElementsByName( "wpforms[recaptcha]" ), function ( el ) { el.value = token; } ); if ( typeof callback === "function" ) { return callback(); } } ); } grecaptcha.ready( function () { wpformsDispatchEvent( document, "wpformsRecaptchaLoaded", true ); } ); '; } elseif ( $captcha_settings['recaptcha_type'] === 'invisible' ) { $data = $polyfills; $data .= $dispatch; $data .= $sync; $data .= /** @lang JavaScript */ 'var wpformsRecaptchaLoad = wpformsRecaptchaSync( function () { Array.prototype.forEach.call(document.querySelectorAll(".g-recaptcha"), function (el) { try { var recaptchaID = grecaptcha.render(el, { "callback": function () { wpformsRecaptchaCallback(el); }, "error-callback": function () { wpformsRecaptchaErrorCallback(el); } }, true); el.closest("form").querySelector("button[type=submit]").recaptchaID = recaptchaID; } catch (error) {} }); wpformsDispatchEvent(document, "wpformsRecaptchaLoaded", true); } ); var wpformsRecaptchaCallback = function (el) { var $form = el.closest("form"); if (typeof wpforms.formSubmit === "function") { wpforms.formSubmit($form); } else { $form.querySelector("button[type=submit]").recaptchaID = false; $form.submit(); } }; var wpformsRecaptchaErrorCallback = function (el) { var $form = el.closest("form"); $form.querySelector("button[type=submit]").dataset.captchaInvalid = true; }; '; } else { $data = $dispatch; $data .= $callback; $data .= /** @lang JavaScript */ 'var wpformsRecaptchaLoad = function () { Array.prototype.forEach.call(document.querySelectorAll(".g-recaptcha"), function (el) { try { var recaptchaID = grecaptcha.render(el, { callback: function () { wpformsRecaptchaCallback(el); } }); el.setAttribute("data-recaptcha-id", recaptchaID); } catch (error) {} }); wpformsDispatchEvent(document, "wpformsRecaptchaLoaded", true); }; '; } return $data; } /** * Cloudflare Turnstile captcha requires defer attribute. * * @since 1.8.1 * * @param string $tag HTML for the script tag. * @param string $handle Handle of script. * @param string $src Src of script. * * @return string */ public function set_defer_attribute( $tag, $handle, $src ) { $captcha_settings = wpforms_get_captcha_settings(); if ( $captcha_settings['provider'] !== 'turnstile' ) { return $tag; } if ( $handle !== 'wpforms-recaptcha' ) { return $tag; } return str_replace( ' src', ' defer src', $tag ); } } Frontend/Frontend.php 0000644 00000202111 15174710275 0010621 0 ustar 00 <?php namespace WPForms\Frontend; use WP_Post; /** * Form front-end rendering. * * @since 1.8.1 */ class Frontend { /** * Field format. * * @since 1.8.9 */ private const FIELD_FORMAT = 'wpforms-%d-field_%s'; /** * Render engine setting value. * * @since 1.8.1 * * @var string */ protected $render_engine; /** * Render engine class instance. * * @since 1.8.1 * * @var Classic|Modern */ private $render_obj; /** * AMP class instance. * * @since 1.8.1 * * @var Amp */ protected $amp_obj; /** * CSS vars class instance. * * @since 1.9.3 * * @var CSSVars */ protected $css_vars_obj; /** * Store form data to be referenced later. * * @since 1.8.1 * * @var array */ public $forms; /** * Store information for multipage forms. * * False for forms that do not contain pages, otherwise an array that contains the number of total pages * and page counter used when displaying pagebreak fields. * * @since 1.8.1 * * @var array|bool */ public $pages = false; /** * If the active form, confirmation should auto-scroll. * * @since 1.8.1 * * @var bool */ public $confirmation_message_scroll = false; /** * Whether ChoiceJS library has already been enqueued on the front end. * This lib is used in different fields that can enqueue it separately, * and we use this property to avoid config duplication. * * @since 1.8.1 * * @var bool */ public $is_choicesjs_enqueued = false; /** * Form action. * * @since 1.8.1 * * @var string */ private $action; /** * Rendered field IDs array. * * @since 1.9.4 * * @var array */ private $rendered_fields; /** * Initialize class. * * @since 1.8.1 */ public function init(): void { $this->forms = []; $this->amp_obj = wpforms()->obj( 'amp' ); $this->css_vars_obj = wpforms()->obj( 'css_vars' ); $this->init_render_engine( wpforms_get_render_engine() ); $this->hooks(); // Register shortcode. add_shortcode( 'wpforms', [ $this, 'shortcode' ] ); } /** * Register hooks. * * @since 1.8.1 */ private function hooks(): void { // Actions. add_action( 'init', [ $this, 'init_style_settings' ] ); add_action( 'wpforms_frontend_output_success', [ $this, 'confirmation' ], 10, 3 ); add_action( 'wpforms_frontend_output', [ $this, 'head' ], 5, 5 ); add_action( 'wpforms_frontend_output', [ $this, 'fields' ], 10, 5 ); add_action( 'wpforms_display_field_before', [ $this, 'field_container_open' ], 5, 2 ); add_action( 'wpforms_display_field_before', [ $this, 'field_fieldset_open' ], 10, 2 ); add_action( 'wpforms_display_field_before', [ $this, 'field_label' ], 15, 2 ); add_action( 'wpforms_display_field_before', [ $this, 'field_description' ], 20, 2 ); add_action( 'wpforms_display_field_after', [ $this, 'field_error' ], 3, 2 ); add_action( 'wpforms_display_field_after', [ $this, 'field_description' ], 5, 2 ); add_action( 'wpforms_display_field_after', [ $this, 'field_fieldset_close' ], 10, 2 ); add_action( 'wpforms_display_field_after', [ $this, 'field_container_close' ], 15, 2 ); add_action( 'wpforms_frontend_output', [ $this, 'foot' ], 25, 5 ); add_action( 'wp_enqueue_scripts', [ $this, 'assets_header' ] ); add_action( 'wp_footer', [ $this, 'assets_footer' ], 15 ); add_action( 'wp_footer', [ $this, 'missing_assets_error_js' ], 20 ); add_action( 'wp_footer', [ $this, 'footer_end' ], 99 ); } /** * Initialize render engine. * * @since 1.8.1 * * @param string $engine Render engine slug, `classic` or `modern`. */ public function init_render_engine( string $engine ): void { $this->render_engine = $engine; $this->render_obj = wpforms()->obj( "frontend_{$this->render_engine}" ); $this->render_obj->hooks(); } /** * Initialize form styling settings. * * @since 1.8.1 */ public function init_style_settings(): void { // Skip if modern markup settings are already set. $modern_markup_is_set = wpforms_setting( 'modern-markup-is-set' ); if ( $modern_markup_is_set ) { return; } $settings = (array) get_option( 'wpforms_settings', [] ); $count_posts = wp_count_posts( 'wpforms' ); // Set the Modern markup checkbox to the checked state for all new users. $settings['modern-markup'] = ( $count_posts->publish + $count_posts->trash ) === 0 ? '1' : '0'; $settings['modern-markup-is-set'] = true; // Hide the Modern markup checkbox for all new users. if ( $settings['modern-markup'] ) { $settings['modern-markup-hide-setting'] = true; } update_option( 'wpforms_settings', $settings ); } /** * Primary function to render a form on the frontend. * * @since 1.8.1 * * @param int $id Form ID. * @param bool $title Whether to display form title. * @param bool $description Whether to display form description. */ public function output( $id, $title = false, $description = false ): void { if ( empty( $id ) ) { return; } // Grab the form data, if not found, then we bail. $form = $this->get_form( $id ); if ( $form === null || empty( $form->post_content ) ) { return; } // We should display only the published form. if ( ! empty( $form->post_status ) && $form->post_status !== 'publish' ) { return; } // Decode the form data. $form_data = wpforms_decode( $form->post_content ); // Skip if the form data is empty. if ( empty( $form_data ) ) { return; } // Basic information. /** * Filter frontend form data. * * @since 1.4.3 * * @param array $form_data Form data. */ $form_data = (array) apply_filters( 'wpforms_frontend_form_data', $form_data ); $form_id = absint( $form->ID ); $this->action = esc_url_raw( remove_query_arg( 'wpforms' ) ); $errors = empty( wpforms()->obj( 'process' )->errors[ $form_id ] ) ? [] : wpforms()->obj( 'process' )->errors[ $form_id ]; $title = filter_var( $title, FILTER_VALIDATE_BOOLEAN ); $description = filter_var( $description, FILTER_VALIDATE_BOOLEAN ); // Pass the current form data to the render object. $this->render_obj->form_data = $form_data; if ( $this->stop_output( $form, $form_data ) ) { return; } // All checks have passed, so calculate multipage details for the form. $this->pages = $this->get_pages( $form_data ); /** * Allow modifying a form action attribute. * * @since 1.1.2 * * @param string $action Action attribute. * @param array $form_data Form data and settings. * @param null $deprecated A deprecated argument. */ $this->action = apply_filters( 'wpforms_frontend_form_action', $this->action, $form_data, null ); $form_classes = [ 'wpforms-validate', 'wpforms-form' ]; if ( ! empty( $form_data['settings']['ajax_submit'] ) && ! $this->amp_obj->is_amp() ) { $form_classes[] = 'wpforms-ajax-form'; } $form_atts = [ 'id' => sprintf( 'wpforms-form-%d', absint( $form_id ) ), 'class' => $form_classes, 'data' => [ 'formid' => absint( $form_id ), ], 'atts' => [ 'method' => 'post', 'enctype' => 'multipart/form-data', 'action' => esc_url( $this->action ), ], ]; /** * Allow modifying form attributes. * * @since 1.4.5 * * @param array $form_atts Form attributes. * @param array $form_data Form data and settings. */ $form_atts = apply_filters( 'wpforms_frontend_form_atts', $form_atts, $form_data ); $this->form_container_open( $form_data, $form ); // Reset rendered fields array. $this->rendered_fields = []; /** * Fires before form output. * * @since 1.5.4.2 * * @param array $form_data Form data. * @param WP_Post $form Form. */ do_action( 'wpforms_frontend_output_form_before', $form_data, $form ); echo '<form ' . wpforms_html_attributes( $form_atts['id'], $form_atts['class'], $form_atts['data'], $form_atts['atts'] ) . '>'; /** * Fires before closing the form. * * @since 1.0.0 * * @param array $form_data Form data. * @param null $deprecated Null. * @param bool $title Whether to display form title. * @param bool $description Whether to display form description. * @param array $errors Form processing errors. */ do_action( 'wpforms_frontend_output', $form_data, null, $title, $description, $errors ); echo '</form>'; /** * Allow adding content after a form. * * @since 1.5.4.2 * * @param array $form_data Form data and settings. * @param WP_Post $form Form post type. */ do_action( 'wpforms_frontend_output_form_after', $form_data, $form ); $this->form_container_close( $form_data, $form ); // Add a form to class property that tracks all forms in a page. $this->forms[ $form_id ] = $form_data; // Optional debug information if WPFORMS_DEBUG is defined. wpforms_debug_data( $_POST ); // phpcs:ignore WordPress.Security.NonceVerification.Missing /** * Fires after frontend output. * * @since 1.0.0 * * @param array $form_data Form data and settings. * @param WP_Post $form Form post type. */ do_action( 'wpforms_frontend_output_after', $form_data, $form ); } /** * Get form. * * @since 1.8.1 * * @param int|string|false $id Form id. * * @return array|WP_Post|null * @noinspection NullPointerExceptionInspection */ private function get_form( $id ) { if ( empty( $id ) ) { return null; } // Grab the form data, if not found, then we bail. $form = wpforms()->obj( 'form' )->get( (int) $id ); if ( empty( $form ) ) { return null; } // We should display only the published form. if ( ! empty( $form->post_status ) && $form->post_status !== 'publish' ) { return null; } return $form; } /** * Check whether we should stop the output. * * @since 1.8.1 * * @param WP_Post $form Form. * @param array $form_data Form data. * * @return bool */ private function stop_output( WP_Post $form, array $form_data ): bool { $form_id = absint( $form->ID ); /** * Is the form empty? * Check before output the form on the frontend. * * @since 1.7.7 * * @param bool $form_is_empty Is the form empty? * @param array $form_data Form data. */ $form_is_empty = apply_filters( 'wpforms_frontend_output_form_is_empty', empty( $form_data['fields'] ), $form_data ); // If the form does not contain any fields - do not proceed. if ( $form_is_empty ) { $this->render_obj->form_is_empty(); return true; } // We need to stop output processing in case we are on the AMP page. if ( $this->amp_obj->stop_output( $form_data ) ) { return true; } // Add url query var wpforms_form_id to track post_max_size overflows. if ( in_array( 'file-upload', wp_list_pluck( $form_data['fields'], 'type' ), true ) ) { $this->action = add_query_arg( 'wpforms_form_id', $form_id, $this->action ); } /** * Fires before form data output. * * @since 1.0.0 * * @param array $form_data Form data. * @param WP_Post $form Form. */ do_action( 'wpforms_frontend_output_before', $form_data, $form ); if ( $this->output_success( $form ) ) { return true; } /** * Allow filter to return early if some condition is not met. * * @since 1.0.0 * * @param bool $load Load frontend flag. * @param array $form_data Form data. * @param null $deprecated Deprecated. */ if ( ! apply_filters( 'wpforms_frontend_load', true, $form_data, null ) ) { $this->form_container_open( $form_data, $form ); /** * Fires when the frontend is not loaded. * * @since 1.4.8 * * @param array $form_data Form data. * @param WP_Post $form Form. */ do_action( 'wpforms_frontend_not_loaded', $form_data, $form ); $this->form_container_close( $form_data, $form ); return true; } return false; } /** * Get pages. * * @since 1.8.1 * * @param array $form_data Form data. * * @return array|false * @noinspection PhpTernaryExpressionCanBeReducedToShortVersionInspection * @noinspection ElvisOperatorCanBeUsedInspection */ private function get_pages( array $form_data ) { $pages = wpforms_get_pagebreak_details( $form_data ); return $pages ? $pages : false; } /** * Check whether the output was successful. * * @since 1.8.1 * * @param WP_Post $form Form. * * @return bool */ private function output_success( WP_Post $form ): bool { $form_id = absint( $form->ID ); $process = wpforms()->obj( 'process' ); if ( ! $process ) { return false; } $form_data = $process->form_data; $errors = empty( $process->errors[ $form_id ] ) ? [] : $process->errors[ $form_id ]; // Check for return hash. if ( // phpcs:ignore WordPress.Security.NonceVerification.Recommended ! empty( $_GET['wpforms_return'] ) && $process->valid_hash && (int) $form_data['id'] === $form_id ) { $this->form_container_open( $form_data, $form ); /** * Fires at successful output. * * @since 1.4.5 * * @param array $form_data Form data. * @param array $fields Form fields. * @param int $entry_id Form ID. */ do_action( 'wpforms_frontend_output_success', $form_data, $process->fields, $process->entry_id ); // phpcs:ignore WordPress.Security.NonceVerification.Missing wpforms_debug_data( $_POST ); $this->form_container_close( $form_data, $form ); return true; } // Check for the error-free completed form. if ( // phpcs:disable WordPress.Security.NonceVerification.Missing empty( $errors ) && ! empty( $form_data ) && ! empty( $_POST['wpforms']['id'] ) && (int) $_POST['wpforms']['id'] === $form_id // phpcs:enable WordPress.Security.NonceVerification.Missing ) { // There is no need for a container wrapper when a form is submitted through AJAX. $this->form_container_open( $form_data, $form ); /** This action is documented in the same method, several lines above. */ do_action( 'wpforms_frontend_output_success', $form_data, $process->fields, $process->entry_id ); $this->form_container_close( $form_data, $form ); // phpcs:ignore WordPress.Security.NonceVerification.Missing wpforms_debug_data( $_POST ); return true; } return false; } /** * Display a form confirmation message. * * @since 1.8.1 * * @param array $form_data Form data and settings. * @param array $fields Sanitized field data. * @param int $entry_id Entry id. */ public function confirmation( $form_data, $fields = [], $entry_id = 0 ): void { $form_data = (array) $form_data; // In AMP, just print template. if ( $this->amp_obj->output_success_template( $form_data ) ) { return; } [ $fields, $entry_id ] = $this->prepare_confirmation_args( $fields, $entry_id ); $process = wpforms()->obj( 'process' ); if ( ! $process ) { return; } $confirmation = $process->get_current_confirmation(); $confirmation_message = $process->get_confirmation_message( $form_data, $fields, $entry_id ); // Only display if a confirmation message has been configured. if ( empty( $confirmation ) || empty( $confirmation_message ) ) { return; } // Load confirmation-specific assets. $this->assets_confirmation( $form_data ); /** * Fires once before the confirmation message. * * @since 1.6.9 * * @param array $confirmation Current confirmation data. * @param array $form_data Form data and settings. * @param array $fields Sanitized field data. * @param int $entry_id Entry id. */ do_action( 'wpforms_frontend_confirmation_message_before', $confirmation, $form_data, $fields, $entry_id ); $class = (int) wpforms_setting( 'disable-css', '1' ) === 1 ? 'wpforms-confirmation-container-full' : 'wpforms-confirmation-container'; $class .= $this->confirmation_message_scroll ? ' wpforms-confirmation-scroll' : ''; $this->render_obj->confirmation( $confirmation_message, $class, $form_data ); /** * Fires once after the confirmation message. * * @since 1.6.9 * * @param array $confirmation Current confirmation data. * @param array $form_data Form data and settings. * @param array $fields Sanitized field data. * @param int $entry_id Entry id. */ do_action( 'wpforms_frontend_confirmation_message_after', $confirmation, $form_data, $fields, $entry_id ); } /** * Prepare confirmation arguments. * * @since 1.8.1 * * @param array $fields Sanitized field data. * @param int $entry_id Entry id. * * @return array */ private function prepare_confirmation_args( $fields = [], $entry_id = 0 ): array { // phpcs:disable WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash if ( empty( $fields ) ) { $fields = ! empty( $_POST['wpforms']['complete'] ) ? $_POST['wpforms']['complete'] : []; } if ( empty( $entry_id ) ) { $entry_id = ! empty( $_POST['wpforms']['entry_id'] ) ? $_POST['wpforms']['entry_id'] : 0; } // phpcs:enable WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash return [ $fields, $entry_id ]; } /** * Form container classes. * * @since 1.7.9 * * @param array $form_data Form data and settings. * * @return array */ private function get_container_classes( $form_data ): array { $classes = (int) wpforms_setting( 'disable-css', '1' ) === 1 ? [ 'wpforms-container-full' ] : []; /** * Allow form container classes to be filtered and user-defined classes. * * @since 1.0.0 * * @param array $classes Classes. * @param array $form_data Form data and settings. */ $classes = (array) apply_filters( 'wpforms_frontend_container_class', $classes, $form_data ); if ( ! empty( $form_data['settings']['form_class'] ) ) { $classes = array_merge( $classes, explode( ' ', $form_data['settings']['form_class'] ) ); } return $classes; } /** * Display the opening container markup for a form. * * @since 1.7.9 * * @param array $form_data Form data and settings. * @param WP_Post $form Form post type. */ private function form_container_open( $form_data, $form ): void { /** * Fires before container open tag. * * @since 1.5.4.2 * * @param array $form_data Form data and settings. * @param WP_Post $form Form post type. */ do_action( 'wpforms_frontend_output_container_before', $form_data, $form ); $classes = $this->get_container_classes( $form_data ); $this->render_obj->form_container_open( $classes, $form_data ); } /** * Display the closing container markup for a form. * * @since 1.7.9 * * @param array $form_data Form data and settings. * @param WP_Post $form Form post type. */ private function form_container_close( $form_data, $form ): void { $this->render_obj->form_container_close(); /** * Fires after container close tag. * * @since 1.5.4.2 * * @param array $form_data Form data and settings. * @param WP_Post $form Form post type. */ do_action( 'wpforms_frontend_output_container_after', $form_data, $form ); } /** * Form head area, for displaying form title and description if enabled. * * @since 1.8.1 * * @param array $form_data Form data and settings. * @param null $deprecated Deprecated in v1.3.7, previously was $form object. * @param bool $title Whether to display form title. * @param bool $description Whether to display form description. * @param array $errors List of all errors filled in WPForms_Process::process(). * * @noinspection PhpUnusedParameterInspection */ public function head( array $form_data, $deprecated, bool $title, bool $description, $errors ): void { // Output title and/or description. if ( $title === true || $description === true ) { $this->render_obj->form_head_container( $title, $description, $form_data ); } /** * Filters <noscript> error message. * * @since 1.5.7 * * @param string $message Message. * @param array $form_data Form data. */ $noscript_msg = apply_filters( 'wpforms_frontend_noscript_error_message', __( 'Please enable JavaScript in your browser to complete this form.', 'wpforms-lite' ), $form_data ); if ( ! empty( $noscript_msg ) && ! empty( $form_data['fields'] ) && ! $this->amp_obj->is_amp() ) { $this->render_obj->noscript( $noscript_msg ); } // Output header errors if they exist. if ( ! empty( $errors['header'] ) ) { $this->form_error( 'header', $errors['header'] ); } } /** * Form field area. * * @since 1.8.1 * * @param array $form_data Form data and settings. * @param null $deprecated Deprecated in v1.3.7, previously was $form object. * @param bool $title Whether to display form title. * @param bool $description Whether to display form description. * @param array $errors List of all errors filled in WPForms_Process::process(). * * @noinspection PhpUnusedParameterInspection */ public function fields( array $form_data, $deprecated, bool $title, bool $description, $errors ): void { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed // We need to have form fields to proceed. if ( empty( $form_data['fields'] ) ) { return; } /** * Filters the base level fields on the frontend. * * @since 1.7.7 * * @param array $fields_data Form fields data. */ $fields = (array) apply_filters( 'wpforms_frontend_fields_base_level', $form_data['fields'] ); // Form fields area. $this->render_obj->fields_area_open(); /** * Core actions on this hook: * Priority / Description * 20 Pagebreak markup (open first page). * * @since 1.3.7 * * @param array $form_data Form data. */ do_action( 'wpforms_display_fields_before', $form_data ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName // Loop through all the fields we have. foreach ( $fields as $field ) { $this->render_field( $form_data, $field ); } /** * Core actions on this hook: * Priority / Description * 5 Pagebreak markup (close last page). * * @since 1.3.7 * * @param array $form_data Form data. */ do_action( 'wpforms_display_fields_after', $form_data ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName $this->render_obj->fields_area_close(); } /** * Return base attributes for a specific field. This is deprecated and * exists for backwards-compatibility purposes. Use field properties instead. * * @since 1.8.1 * * @param array $field Field data and settings. * @param array $form_data Form data and settings. * * @return array */ public function get_field_attributes( array $field, array $form_data ): array { $form_id = absint( $form_data['id'] ); $field_id = wpforms_validate_field_id( $field['id'] ); $attributes = [ 'field_class' => [ 'wpforms-field', 'wpforms-field-' . sanitize_html_class( $field['type'] ) ], 'field_id' => [ sprintf( 'wpforms-%d-field_%s-container', $form_id, $field_id ) ], 'field_style' => '', 'label_class' => [ 'wpforms-field-label' ], 'label_id' => '', 'description_class' => [ 'wpforms-field-description' ], 'description_id' => [], 'input_id' => [ sprintf( self::FIELD_FORMAT, $form_id, $field_id ) ], 'input_class' => [], 'input_data' => [], ]; // Check user field defined classes. if ( ! empty( $field['css'] ) ) { $attributes['field_class'] = array_merge( $attributes['field_class'], wpforms_sanitize_classes( $field['css'], true ) ); } // Check for input column layouts. $attributes = $this->check_input_columns( $field, $attributes ); // Check label visibility. if ( ! empty( $field['label_hide'] ) ) { $attributes['label_class'][] = 'wpforms-label-hide'; } // Check size. if ( ! empty( $field['size'] ) ) { $attributes['input_class'][] = 'wpforms-field-' . sanitize_html_class( $field['size'] ); } // Check if required. if ( ! empty( $field['required'] ) ) { $attributes['input_class'][] = 'wpforms-field-required'; } // Check if there are errors. if ( ! empty( wpforms()->obj( 'process' )->errors[ $form_id ][ $field_id ] ) ) { $attributes['input_class'][] = 'wpforms-error'; } /** * Filters field attributes. * This filter is deprecated, filter the properties (below) instead. * * @since 1.0.0 * * @param array $attributes Field attributes. * @param array $field Field data and settings. * @param array $form_data Form data and settings. */ return (array) apply_filters( 'wpforms_field_atts', $attributes, $field, $form_data ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName } /** * Check input column layouts and set relevant attributes. * * @since 1.8.1 * * @param array $field Field data and settings. * @param array $attributes Attributes. * * @return array */ private function check_input_columns( array $field, array $attributes ): array { if ( ! empty( $field['input_columns'] ) ) { if ( $field['input_columns'] === '2' ) { $attributes['field_class'][] = 'wpforms-list-2-columns'; } elseif ( $field['input_columns'] === '3' ) { $attributes['field_class'][] = 'wpforms-list-3-columns'; } elseif ( $field['input_columns'] === 'inline' ) { $attributes['field_class'][] = 'wpforms-list-inline'; } } return $attributes; } /** * Return base properties for a specific field. * * @since 1.8.1 * * @param array $field Field data and settings. * @param array $form_data Form data and settings. * @param array $attributes List of field attributes. * * @return array */ public function get_field_properties( array $field, array $form_data, array $attributes = [] ): array { [ $field, $attributes, $error ] = $this->prepare_get_field_properties( $field, $form_data, $attributes ); $form_id = absint( $form_data['id'] ); $field_id = wpforms_validate_field_id( $field['id'] ); $properties = [ 'container' => [ 'attr' => [ 'style' => $attributes['field_style'], ], 'class' => $attributes['field_class'], 'data' => [], 'id' => implode( '', array_slice( $attributes['field_id'], 0 ) ), ], 'label' => [ 'attr' => [ 'for' => sprintf( self::FIELD_FORMAT, $form_id, $field_id ), ], 'class' => $attributes['label_class'], 'data' => [], 'disabled' => ! empty( $field['label_disable'] ), 'hidden' => ! empty( $field['label_hide'] ), 'id' => $attributes['label_id'], 'required' => ! empty( $field['required'] ), 'value' => ! empty( $field['label'] ) ? $field['label'] : '', ], 'inputs' => [ 'primary' => [ 'attr' => [ 'name' => "wpforms[fields][{$field_id}]", 'value' => isset( $field['default_value'] ) ? wpforms_process_smart_tags( $field['default_value'], $form_data, [], '', 'field-properties' ) : '', 'placeholder' => $field['placeholder'] ?? '', ], 'class' => $attributes['input_class'], 'data' => $attributes['input_data'], 'id' => implode( array_slice( $attributes['input_id'], 0 ) ), 'required' => ! empty( $field['required'] ) ? 'required' : '', ], ], 'error' => [ 'attr' => [ 'for' => sprintf( self::FIELD_FORMAT, $form_id, $field_id ), ], 'class' => [ 'wpforms-error' ], 'data' => [], 'id' => '', 'value' => $error, ], 'description' => [ 'attr' => [], 'class' => $attributes['description_class'], 'data' => [], 'id' => implode( '', array_slice( $attributes['description_id'], 0 ) ), 'position' => 'after', 'value' => ! empty( $field['description'] ) ? wpforms_process_smart_tags( $field['description'], $form_data, [], '', 'field-properties' ) : '', ], ]; // phpcs:disable WPForms.PHP.ValidateHooks.InvalidHookName /** * Filters field properties. * * @since 1.3.6.2 * * @param array $properties Field properties. * @param array $field Field data and settings. * @param array $form_data Form data and settings. */ $properties = (array) apply_filters( "wpforms_field_properties_{$field['type']}", $properties, $field, $form_data ); /** * Filters properties. * * @since 1.3.6.2 * * @param array $properties Field properties. * @param array $field Field data and settings. * @param array $form_data Form data and settings. */ return (array) apply_filters( 'wpforms_field_properties', $properties, $field, $form_data ); // phpcs:enable WPForms.PHP.ValidateHooks.InvalidHookName } /** * Prepare get_field_properties. * * @since 1.8.1 * * @param array $field Field data and settings. * @param array $form_data Form data and settings. * @param array $attributes List of field attributes. * * @return array */ private function prepare_get_field_properties( array $field, array $form_data, array $attributes ): array { $attributes = empty( $attributes ) ? $this->get_field_attributes( $field, $form_data ) : $attributes; $field = $this->filter_field( $field, $form_data, $attributes ); $form_id = absint( $form_data['id'] ); $field_id = wpforms_validate_field_id( $field['id'] ); $error = ! empty( wpforms()->obj( 'process' )->errors[ $form_id ][ $field_id ] ) ? wpforms()->obj( 'process' )->errors[ $form_id ][ $field_id ] : ''; return [ $field, $attributes, $error ]; } /** * Filter field. * * @since 1.8.1 * * @param array $field Field data and settings. * @param array $form_data Form data and settings. * @param array $attributes Field attributes. * * @return array */ private function filter_field( array $field, array $form_data, array $attributes ): array { // This filter is for backwards compatibility purposes. $types = [ 'text', 'textarea', 'name', 'number', 'email', 'hidden', 'url', 'html', 'divider', 'password', 'phone', 'address', 'select', 'checkbox', 'radio' ]; if ( in_array( $field['type'], $types, true ) ) { // phpcs:disable WPForms.PHP.ValidateHooks.InvalidHookName /** * Filters field. * * @since 1.3.6.2 * * @param array $field Field data and settings. * @param array $attributes Field attributes. * @param array $form_data Form data and settings. */ $filtered_field = apply_filters( "wpforms_{$field['type']}_field_display", $field, $attributes, $form_data ); $field = wpforms_list_intersect_key( (array) $filtered_field, $field ); } elseif ( $field['type'] === 'credit-card' ) { /** * Filters credit card field. * * @since 1.3.6.2 * * @param array $field Field data and settings. * @param array $attributes Field attributes. * @param array $form_data Form data and settings. */ $filtered_field = apply_filters( 'wpforms_creditcard_field_display', $field, $attributes, $form_data ); $field = wpforms_list_intersect_key( (array) $filtered_field, $field ); } elseif ( in_array( $field['type'], [ 'payment-multiple', 'payment-single', 'payment-checkbox' ], true ) ) { $filter_field_type = str_replace( '-', '_', $field['type'] ); /** * Filters payment field. * * @since 1.3.6.2 * * @param array $field Field data and settings. * @param array $attributes Field attributes. * @param array $form_data Form data and settings. */ $filtered_field = apply_filters( 'wpforms_' . $filter_field_type . '_field_display', $field, $attributes, $form_data ); $field = wpforms_list_intersect_key( (array) $filtered_field, $field ); // phpcs:enable WPForms.PHP.ValidateHooks.InvalidHookName } return $field; } /** * Field container open. * * @since 1.8.1 * * @param array $field Field data and settings. * @param array $form_data Form data and settings. */ public function field_container_open( $field, $form_data ): void { $this->render_obj->field_container_open( $field, $form_data ); } /** * Field container close. * * @since 1.8.1 * * @param array $field Field data and settings. * @param array $form_data Form data and settings. */ public function field_container_close( $field, $form_data ): void { $this->render_obj->field_container_close( $field, $form_data ); } /** * Field fieldset open. * * @since 1.8.1 * * @param array $field Field data and settings. * @param array $form_data Form data and settings. */ public function field_fieldset_open( $field, $form_data ): void { $this->render_obj->field_fieldset_open( $field, $form_data ); } /** * Field fieldset close. * * @since 1.8.1 * * @param array $field Field data and settings. * @param array $form_data Form data and settings. */ public function field_fieldset_close( $field, $form_data ): void { $this->render_obj->field_fieldset_close( $field, $form_data ); } /** * Display the label for each field. * * @since 1.8.1 * * @param array $field Field data and settings. * @param array $form_data Form data and settings. */ public function field_label( $field, $form_data ): void { $label = $field['properties']['label']; // If the label is empty or disabled, don't proceed. if ( empty( $label['value'] ) || $label['disabled'] ) { return; } $this->render_obj->field_label( $field, $form_data ); } /** * Display any errors for each field. * * @since 1.8.1 * * @param array $field Field data and settings. * @param array $form_data Form data and settings. */ public function field_error( $field, $form_data ): void { $error = $field['properties']['error']; // If there are no errors, don't proceed. // Advanced fields with multiple inputs (address, name, etc.) errors // will be an array and are handled within the respective field class. if ( empty( $error['value'] ) || is_array( $error['value'] ) ) { return; } $this->render_obj->field_error( $field, $form_data ); } /** * Display the description for each field. * * @since 1.8.1 * * @param array $field Field data and settings. * @param array $form_data Form data and settings. * * @noinspection HtmlUnknownAttribute * @noinspection PhpUnusedParameterInspection */ public function field_description( $field, $form_data ): void { $action = current_action(); $description = $field['properties']['description']; // If the description is empty, don't proceed. if ( empty( $description['value'] ) ) { return; } // Determine positioning. if ( $action === 'wpforms_display_field_before' && $description['position'] !== 'before' ) { return; } if ( $action === 'wpforms_display_field_after' && $description['position'] !== 'after' ) { return; } if ( $description['position'] === 'before' ) { $description['class'][] = 'before'; } $this->render_obj->field_description( $field, $form_data ); } /** * Anti-spam honeypot output if configured. * * @since 1.8.1 * * @param array $form_data Form data and settings. * @param null $deprecated Deprecated in v1.3.7, previously was $form object. * @param bool $title Whether to display form title. * @param bool $description Whether to display form description. * @param array $errors List of all errors filled in WPForms_Process::process(). * * @noinspection PhpUnusedParameterInspection */ public function honeypot( $form_data, $deprecated, $title, $description, $errors ): void { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed if ( empty( $form_data['settings']['honeypot'] ) || $form_data['settings']['honeypot'] !== '1' ) { return; } $names = [ 'Name', 'Phone', 'Comment', 'Message', 'Email', 'Website' ]; echo '<div class="wpforms-field wpforms-field-hp">'; // phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped echo '<label for="wpforms-' . $form_data['id'] . '-field-hp" class="wpforms-field-label">' . $names[ array_rand( $names ) ] . '</label>'; echo '<input type="text" name="wpforms[hp]" id="wpforms-' . $form_data['id'] . '-field-hp" class="wpforms-field-medium">'; // phpcs:enable WordPress.Security.EscapeOutput.OutputNotEscaped echo '</div>'; } /** * Form footer area. * * @since 1.8.1 * * @param array $form_data Form data and settings. * @param null $deprecated Deprecated in v1.3.7, previously was $form object. * @param bool $title Whether to display form title. * @param bool $description Whether to display form description. * @param array $errors List of all errors filled in WPForms_Process::process(). * * @noinspection HtmlUnknownTarget * @noinspection HtmlUnknownAttribute * @noinspection PhpUnusedParameterInspection */ public function foot( $form_data, $deprecated, $title, $description, $errors ): void { // Do not render footer if there are no fields on the front. if ( empty( $this->rendered_fields ) ) { return; } $form_id = absint( $form_data['id'] ); $settings = $form_data['settings']; $submit_text = ! empty( $settings['submit_text'] ) ? $settings['submit_text'] : __( 'Submit', 'wpforms-lite' ); /** * Filter the form submit button text. * * @since 1.0.0 * * @param string $submit_text Submit button text. * @param array $form_data Form data. */ $submit = apply_filters( 'wpforms_field_submit', $submit_text, $form_data ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName $attrs = [ 'aria-live' => 'assertive', 'value' => 'wpforms-submit', ]; $data_attrs = []; /** * Filter the form submit button classes. * * @since 1.7.5.3 * * @param array $classes Button classes. * @param array $form_data Form data. */ $classes = (array) apply_filters( 'wpforms_frontend_foot_submit_classes', [], $form_data ); // A lot of our frontend logic is dependent on this class, so we need to make sure it's present. $classes = array_merge( $classes, [ 'wpforms-submit' ] ); [ $attrs, $data_attrs, $classes ] = $this->check_submit_settings( $settings, $form_id, $submit, $attrs, $data_attrs, $classes ); // AMP submit error template. $this->amp_obj->output_error_template(); // Output footer errors if they exist. if ( ! empty( $errors['footer'] ) ) { $this->form_error( 'footer', $errors['footer'] ); } // Submit button area. $this->render_obj->submit_container_open( $this->pages, $form_data ); echo '<input type="hidden" name="wpforms[id]" value="' . absint( $form_id ) . '">'; if ( is_user_logged_in() ) { ?> <input type="hidden" name="wpforms[nonce]" value="<?php echo esc_attr( wp_create_nonce( "wpforms::form_{$form_id}" ) ); ?>" /> <?php } echo '<input type="hidden" name="page_title" value="' . esc_attr( wpforms_process_smart_tags( '{page_title}', [], [], '', 'frontend-foot-hidden-input' ) ) . '">'; echo '<input type="hidden" name="page_url" value="' . esc_url( wpforms_process_smart_tags( '{page_url}', [], [], '', 'frontend-foot-hidden-input' ) ) . '">'; echo '<input type="hidden" name="url_referer" value="' . esc_url( wpforms_process_smart_tags( '{url_referer}', [], [], '', 'frontend-foot-hidden-input' ) ) . '">'; if ( is_singular() ) { // The field is used for some smart tags determination. echo '<input type="hidden" name="page_id" value="' . absint( get_the_ID() ) . '">'; // The field is used for setting global $post during AJAX submissions. echo '<input type="hidden" name="wpforms[post_id]" value="' . absint( get_the_ID() ) . '">'; } /** * Fires before 'submit' button. * * @since 1.3.6.2 * * @param array $form_data Form data and settings. */ do_action( 'wpforms_display_submit_before', $form_data ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName $this->render_obj->submit_button( $form_id, $submit, $classes, $data_attrs, $attrs, $form_data ); if ( ! empty( $settings['ajax_submit'] ) && ! $this->amp_obj->is_amp() ) { /** * Filter submit spinner image src attribute. * * @since 1.5.4.1 * @deprecated 1.6.7.3 * * @see This filter is documented in wp-includes/plugin.php */ $src = apply_filters_deprecated( 'wpforms_display_sumbit_spinner_src', [ WPFORMS_PLUGIN_URL . 'assets/images/submit-spin.svg', $form_data, ], '1.6.7.3', 'wpforms_display_submit_spinner_src' ); /** * Filter submit spinner image src attribute. * * @since 1.6.7.3 * * @param string $src Spinner image source. * @param array $form_data Form data and settings. */ $src = apply_filters( // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName 'wpforms_display_submit_spinner_src', $src, $form_data ); $this->render_obj->submit_spinner( $src, $form_data ); } /** * Runs right after form Submit button rendering. * * @since 1.5.0 * @since 1.7.5 Added new parameter for detecting button type. * * @param array $form_data Form data. * @param string $button Button type, e.g. `submit`, `next`. */ do_action( 'wpforms_display_submit_after', $form_data, 'submit' ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName $this->render_obj->submit_container_close( $form_data ); // Load the success template in AMP. $this->amp_obj->output_success_template( $form_data ); } /** * Check submit settings and adjust attributes and classes. * * @since 1.8.1 * * @param array $settings Settings. * @param int $form_id Form id. * @param string $submit Submit button text. * @param array $attrs Attributes. * @param array $data_attrs Data attributes. * @param array $classes Classes. * * @return array */ private function check_submit_settings( $settings, $form_id, $submit, $attrs, $data_attrs, $classes ): array { // Check for the 'submit' button alt-text. if ( ! empty( $settings['submit_text_processing'] ) ) { if ( $this->amp_obj->is_amp() ) { $attrs['[text]'] = $this->amp_obj->get_text_attr( $form_id, $settings, $submit ); } else { $data_attrs['alt-text'] = $settings['submit_text_processing']; $data_attrs['submit-text'] = $submit; } } // Check user defined submit button classes. if ( ! empty( $settings['submit_class'] ) ) { $submit_classes = is_array( $settings['submit_class'] ) ? $settings['submit_class'] : array_filter( explode( ' ', $settings['submit_class'] ) ); $classes = array_merge( $classes, $submit_classes ); } return [ $attrs, $data_attrs, $classes ]; } /** * Display form error. * * @since 1.5.3 * @since 1.8.1 Added $form_data optional parameter. * * @param string $type Error type. * @param string $error Error text. * @param array $form_data Form data. Defaults to null. * Added to pass the form data in the case of * the method is called inside the ajax callback. */ public function form_error( string $type, $error, $form_data = null ): void { if ( ! empty( $form_data ) ) { $this->render_obj->form_data = $form_data; } $this->render_obj->form_error( $type, $error ); } /** * Determine if we should load assets globally. * If false, assets will load conditionally (default). * * @since 1.2.4 * * @return bool */ public function assets_global(): bool { // phpcs:ignore WPForms.Formatting.EmptyLineBeforeReturn.RemoveEmptyLineBeforeReturnStatement /** * Filters global assets. * * @since 1.2.4 * * @param bool $are_assets_global Global assets. */ return (bool) apply_filters( 'wpforms_global_assets', wpforms_setting( 'global-assets' ) ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName } /** * Load the necessary assets for single pages/posts earlier if possible. * * If we are viewing a singular page, then we can check the content early * to see if the shortcode was used. If not, we fall back and load the assets * later on during the page (widgets, archives, etc.). * * @since 1.0.0 * @since 1.9.0 Added load JS assets. */ public function assets_header(): void { // Force loading JS assets in the header. if ( ! $this->load_script_in_footer() ) { $this->assets_js(); } /** * Allow loading assets in the header on various pages. * * By default, assets are loaded only on singular pages if WPForms shortcode or editor block is present. * However, if a form is added as a sidebar widget, in a template or somewhere else outside the Loop, * we will discover that too late for assets to be included in the header. * In this case, we will include all required assets in the footer instead. * This may lead to a brief FOUC (Flash Of Unstyled Content). * * Returning `true` from this filter on a particular page that matches your criteria is useful * if you need to load assets in the header on archive pages or any other pages that you know have a form. * It may be as a sidebar widget, dynamically inserted on form preview page, on category pages, etc. * * @since 1.8.1 * * @param bool $force_load Force loading assets in the header, default `false`. */ $force_load_css = (bool) apply_filters( 'wpforms_frontend_assets_header_force_load', false ); if ( $force_load_css ) { $this->assets_css(); return; } if ( ! is_singular() ) { return; } global $post; if ( has_shortcode( $post->post_content, 'wpforms' ) || ( function_exists( 'has_block' ) && has_block( 'wpforms/form-selector' ) ) ) { $this->assets_css(); } } /** * Load the CSS assets for frontend output. * * @since 1.0.0 */ public function assets_css() { /** * Fires before enqueueing frontend CSS. * * @since 1.0.0 * * @param array $forms Array of forms on the page. */ do_action( 'wpforms_frontend_css', $this->forms ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName $min = wpforms_get_min_suffix(); $disable_css = (int) wpforms_setting( 'disable-css', '1' ); if ( $disable_css === 3 ) { wp_enqueue_style( 'wpforms-no-styles', WPFORMS_PLUGIN_URL . "assets/css/frontend/wpforms-no-styles{$min}.css", [], WPFORMS_VERSION ); return; } $style_name = $disable_css === 1 ? 'full' : 'base'; $handle = "wpforms-{$this->render_engine}-{$style_name}"; wp_enqueue_style( $handle, WPFORMS_PLUGIN_URL . "assets/css/frontend/{$this->render_engine}/wpforms-{$style_name}{$min}.css", [], WPFORMS_VERSION ); // Add CSS variables for the Modern Markup mode for full styles. if ( empty( $this->css_vars_obj ) || $this->render_engine !== 'modern' || $style_name !== 'full' ) { return; } wp_add_inline_style( $handle, $this->css_vars_obj->get_root_vars_css() ); } /** * Load the JS assets for frontend output. * * @since 1.0.0 */ public function assets_js() { if ( $this->amp_obj->is_amp() ) { return; } /** * Fire before frontend JS assets are loaded. * * @since 1.0.0 * * @param array $forms Forms on the current page. */ do_action( 'wpforms_frontend_js', $this->forms ); $min = wpforms_get_min_suffix(); $in_footer = $this->load_script_in_footer(); // Load the jQuery validation library - https://jqueryvalidation.org/. wp_enqueue_script( 'wpforms-validation', WPFORMS_PLUGIN_URL . 'assets/lib/jquery.validate.min.js', [ 'jquery' ], '1.21.0', $in_footer ); // Load jQuery input mask library - https://github.com/RobinHerbots/jquery.inputmask. if ( $this->assets_global() || wpforms_has_field_type( [ 'phone', 'address' ], $this->forms, true ) || wpforms_has_field_setting( 'input_mask', $this->forms, true ) ) { wp_enqueue_script( 'wpforms-maskedinput', WPFORMS_PLUGIN_URL . 'assets/lib/jquery.inputmask.min.js', [ 'jquery' ], '5.0.9', $in_footer ); } // Load mailcheck <https://github.com/mailcheck/mailcheck> and punycode libraries. if ( $this->assets_global() || wpforms_has_field_type( [ 'email' ], $this->forms, true ) ) { wp_enqueue_script( 'wpforms-mailcheck', WPFORMS_PLUGIN_URL . 'assets/lib/mailcheck.min.js', [], '1.1.2', $in_footer ); wp_enqueue_script( 'wpforms-punycode', WPFORMS_PLUGIN_URL . 'assets/lib/punycode.min.js', [], '1.0.0', $in_footer ); } wp_enqueue_script( 'wpforms-generic-utils', WPFORMS_PLUGIN_URL . "assets/js/share/utils{$min}.js", [ 'jquery' ], WPFORMS_VERSION, $in_footer ); // Load base JS. wp_enqueue_script( 'wpforms', WPFORMS_PLUGIN_URL . "assets/js/frontend/wpforms{$min}.js", [ 'jquery' ], WPFORMS_VERSION, $in_footer ); // Load JS additions needed in the Modern Markup mode. if ( $this->render_engine === 'modern' ) { wp_enqueue_script( 'wpforms-modern', WPFORMS_PLUGIN_URL . "assets/js/frontend/wpforms-modern{$min}.js", [ 'wpforms' ], WPFORMS_VERSION, $in_footer ); } } /** * Cloudflare Turnstile captcha requires defer attribute. * * @since 1.8.0 * * @param string $tag HTML for the script tag. * @param string $handle Handle of a script. * @param string $src Src of a script. * * @return string * @noinspection PhpMissingParamTypeInspection * @noinspection PhpUnusedParameterInspection */ public function set_defer_attribute( $tag, $handle, $src ): string { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed $captcha_settings = wpforms_get_captcha_settings(); if ( $captcha_settings['provider'] !== 'turnstile' ) { return $tag; } if ( $handle !== 'wpforms-recaptcha' ) { return $tag; } return str_replace( ' src', ' defer src', $tag ); } /** * Load the necessary assets for the confirmation message. * * @since 1.1.2 * @since 1.7.9 Added $form_data argument. * * @param array $form_data Form data and settings. */ public function assets_confirmation( $form_data = [] ): void { $form_data = (array) $form_data; $min = wpforms_get_min_suffix(); $in_footer = $this->load_script_in_footer(); // Base CSS only. if ( (int) wpforms_setting( 'disable-css', '1' ) === 1 ) { wp_enqueue_style( 'wpforms-full', WPFORMS_PLUGIN_URL . "assets/css/frontend/{$this->render_engine}/wpforms-full{$min}.css", [], WPFORMS_VERSION ); } // Special confirmation JS. if ( ! $this->amp_obj->is_amp() ) { wp_enqueue_script( 'wpforms-confirmation', WPFORMS_PLUGIN_URL . "assets/js/frontend/wpforms-confirmation{$min}.js", [ 'jquery' ], WPFORMS_VERSION, $in_footer ); } /** * Fires after enqueueing assets on the confirmation page have been enqueued. * * @since 1.1.2 * @since 1.7.9 Added $form_data argument. * * @param array $form_data Form data and settings. */ do_action( 'wpforms_frontend_confirmation', $form_data ); } /** * Load the assets in the footer if needed (archives, widgets, etc.). * * @since 1.0.0 */ public function assets_footer(): void { if ( empty( $this->forms ) && ! $this->assets_global() ) { return; } $this->assets_css(); $this->assets_js(); /** * Fires after enqueueing footer assets. * * @since 1.0.0 * * @param array $forms Forms being shown. */ do_action( 'wpforms_wp_footer', $this->forms ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName } /** * Get strings to localize. * * @since 1.6.0 * * @return array Array of strings to localize. */ public function get_strings(): array { // Define base strings. $strings = [ 'val_required' => wpforms_setting( 'validation-required', esc_html__( 'This field is required.', 'wpforms-lite' ) ), 'val_email' => wpforms_setting( 'validation-email', esc_html__( 'Please enter a valid email address.', 'wpforms-lite' ) ), 'val_email_suggestion' => wpforms_setting( 'validation-email-suggestion', sprintf( /* translators: %s - suggested email address. */ esc_html__( 'Did you mean %s?', 'wpforms-lite' ), '{suggestion}' ) ), 'val_email_suggestion_title' => esc_attr__( 'Click to accept this suggestion.', 'wpforms-lite' ), 'val_email_restricted' => wpforms_setting( 'validation-email-restricted', esc_html__( 'This email address is not allowed.', 'wpforms-lite' ) ), 'val_number' => wpforms_setting( 'validation-number', esc_html__( 'Please enter a valid number.', 'wpforms-lite' ) ), 'val_number_positive' => wpforms_setting( 'validation-number-positive', esc_html__( 'Please enter a valid positive number.', 'wpforms-lite' ) ), 'val_minimum_price' => wpforms_setting( 'validation-minimum-price', esc_html__( 'Amount entered is less than the required minimum.', 'wpforms-lite' ) ), 'val_confirm' => wpforms_setting( 'validation-confirm', esc_html__( 'Field values do not match.', 'wpforms-lite' ) ), 'val_checklimit' => wpforms_setting( 'validation-check-limit', esc_html__( 'You have exceeded the number of allowed selections: {#}.', 'wpforms-lite' ) ), 'val_limit_characters' => wpforms_setting( 'validation-character-limit', sprintf( /* translators: %1$s - character count, %2$s - character limit. */ esc_html__( '%1$s of %2$s max characters.', 'wpforms-lite' ), '{count}', '{limit}' ) ), 'val_limit_words' => wpforms_setting( 'validation-word-limit', sprintf( /* translators: %1$s - word count, %2$s - word limit. */ esc_html__( '%1$s of %2$s max words.', 'wpforms-lite' ), '{count}', '{limit}' ) ), 'val_min' => wpforms_setting( 'validation-min', esc_html__( 'Please enter a value greater than or equal to {0}.', 'wpforms-lite' ) ), 'val_max' => wpforms_setting( 'validation-max', esc_html__( 'Please enter a value less than or equal to {0}.', 'wpforms-lite' ) ), 'val_recaptcha_fail_msg' => wpforms_setting( 'recaptcha-fail-msg', esc_html__( 'Google reCAPTCHA verification failed, please try again later.', 'wpforms-lite' ) ), 'val_turnstile_fail_msg' => wpforms_setting( 'turnstile-fail-msg', esc_html__( 'Cloudflare Turnstile verification failed, please try again later.', 'wpforms-lite' ) ), 'val_inputmask_incomplete' => wpforms_setting( 'validation-inputmask-incomplete', esc_html__( 'Please fill out the field in required format.', 'wpforms-lite' ) ), 'uuid_cookie' => false, 'locale' => wpforms_get_language_code(), /** * Filters the user's country code. * * Leave empty for most cases, it will be auto-detected. * If set, it will make country recognition in wpforms.js frontend skipped. * Allows testing Phone Smart field with different countries. * * @since 1.9.0 * * @param string|false $country Country code. */ 'country' => apply_filters( 'wpforms_frontend_get_user_country_code', false ), 'country_list_label' => esc_html__( 'Country list', 'wpforms-lite' ), 'wpforms_plugin_url' => WPFORMS_PLUGIN_URL, 'gdpr' => wpforms_setting( 'gdpr' ), 'ajaxurl' => admin_url( 'admin-ajax.php' ), /** * Filters mail check enabled flag. * * @since 1.5.4.2 * * @param bool $flag Enabled flag. */ 'mailcheck_enabled' => (bool) apply_filters( 'wpforms_mailcheck_enabled', true ), // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName /** * Filters mail check domains. * * @since 1.5.4.2 * * @param array $domains Domains to check. */ 'mailcheck_domains' => array_map( 'sanitize_text_field', (array) apply_filters( 'wpforms_mailcheck_domains', [] ) ), // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName /** * Filters toplevel domains for mail check. * * @since 1.5.4.2 * * @param array $toplevel_domains Toplevel domains to check. */ 'mailcheck_toplevel_domains' => array_map( 'sanitize_text_field', (array) apply_filters( 'wpforms_mailcheck_toplevel_domains', [ 'dev' ] ) ), // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName 'is_ssl' => is_ssl(), ]; // Include payment-related strings if needed. $strings = $this->get_payment_strings( $strings ); // Include CSS variables list. $strings = $this->get_css_vars_strings( $strings ); /** * Filters frontend strings. * * @since 1.3.7.3 * * @param array $strings Frontend strings. */ $strings = (array) apply_filters( 'wpforms_frontend_strings', $strings ); foreach ( $strings as $key => $value ) { if ( ! is_scalar( $value ) ) { continue; } $strings[ $key ] = esc_html( html_entity_decode( (string) $value, ENT_QUOTES, 'UTF-8' ) ); } return $strings; } /** * Get payment strings. * * @since 1.8.1 * * @param array $strings Strings. * * @return array */ private function get_payment_strings( array $strings ): array { if ( function_exists( 'wpforms_get_currencies' ) ) { $currency = wpforms_get_currency(); $currencies = wpforms_get_currencies(); $strings['currency_code'] = $currency; $strings['currency_thousands'] = $currencies[ $currency ]['thousands_separator'] ?? ','; $strings['currency_decimals'] = wpforms_get_currency_decimals( $currencies[ $currency ] ); $strings['currency_decimal'] = $currencies[ $currency ]['decimal_separator'] ?? '.'; $strings['currency_symbol'] = $currencies[ $currency ]['symbol'] ?? '$'; $strings['currency_symbol_pos'] = $currencies[ $currency ]['symbol_pos'] ?? 'left'; } $strings['val_requiredpayment'] = wpforms_setting( 'validation-requiredpayment', esc_html__( 'Payment is required.', 'wpforms-lite' ) ); $strings['val_creditcard'] = wpforms_setting( 'validation-creditcard', esc_html__( 'Please enter a valid credit card number.', 'wpforms-lite' ) ); return $strings; } /** * Get CSS variables data. * * @since 1.8.1 * * @param array $strings Strings. * * @return array */ private function get_css_vars_strings( array $strings ): array { if ( wpforms_get_render_engine() !== 'modern' ) { return $strings; } if ( empty( $this->css_vars_obj ) ) { return $strings; } $strings['css_vars'] = array_keys( $this->css_vars_obj->get_vars() ); return $strings; } /** * Hook at fires at a later priority in wp_footer. * * @since 1.0.5 * @since 1.7.0 Load wpforms_settings on the confirmation page for a non-ajax form. */ public function footer_end(): void { if ( ( empty( $this->forms ) && empty( $_POST['wpforms'] ) && ! $this->assets_global() ) || // phpcs:ignore WordPress.Security.NonceVerification.Missing $this->amp_obj->is_amp() ) { return; } $strings = $this->get_strings(); /* * Below we do our own implementation of wp_localize_script in an effort * to be better compatible with caching plugins which were causing * conflicts. */ echo "<script type='text/javascript'>\n"; echo "/* <![CDATA[ */\n"; echo 'var wpforms_settings = ' . wp_json_encode( $strings ) . "\n"; echo "/* ]]> */\n"; echo "</script>\n"; /** * Fires after the end of the footer. * * @since 1.0.6 * * @param array $forms Forms being shown. */ do_action( 'wpforms_wp_footer_end', $this->forms ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName } /** * Shortcode wrapper for the outputting a form. * * @since 1.0.0 * * @param array|mixed $atts Shortcode attributes provided by a user. * * @return string */ public function shortcode( $atts ): string { $atts = (array) $atts; $defaults = [ 'id' => false, 'title' => false, 'description' => false, ]; $atts = shortcode_atts( $defaults, shortcode_atts( $defaults, $atts, 'output' ), 'wpforms' ); ob_start(); $this->css_vars_obj->output_css_vars_for_shortcode( $atts ); $this->output( $atts['id'], $atts['title'], $atts['description'] ); return (string) ob_get_clean(); } /** * Inline a script to check if our main js is loaded and display a warning message otherwise. * * @since 1.6.4.1 */ public function missing_assets_error_js(): void { /** * Disable missing assets error js checking. * * @since 1.6.6 * * @param bool $skip False by default, set to True to disable checking. */ $skip = (bool) apply_filters( 'wpforms_frontend_missing_assets_error_js_disable', false ); if ( $skip || ! wpforms_current_user_can() ) { return; } if ( empty( $this->forms ) && ! $this->assets_global() ) { return; } if ( $this->amp_obj->is_amp() ) { return; } // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped printf( $this->get_missing_assets_error_script(), $this->get_missing_assets_error_message() ); } /** * Get missing assets error script. * * @since 1.6.4.1 * * @return string */ private function get_missing_assets_error_script(): string { return "<script> ( function() { function wpforms_js_error_loading() { if ( typeof window.wpforms !== 'undefined' ) { return; } const forms = document.querySelectorAll( '.wpforms-form' ); if ( ! forms.length ) { return; } const error = document.createElement( 'div' ); error.classList.add( 'wpforms-error-container' ); error.setAttribute( 'role', 'alert' ); error.innerHTML = '%s'; forms.forEach( function( form ) { if ( form.querySelector( '.wpforms-error-container' ) ) { return; } const formError = error.cloneNode( true ), formErrorId = form.id + '-error'; formError.setAttribute( 'id', formErrorId ); form.insertBefore( formError, form.firstChild ); form.setAttribute( 'aria-invalid', 'true' ); form.setAttribute( 'aria-errormessage', formErrorId ); } ); } if ( document.readyState === 'loading' ) { document.addEventListener( 'DOMContentLoaded', wpforms_js_error_loading ); } else { wpforms_js_error_loading(); } }() ); </script>"; } /** * Get a missing assets error message. * * @since 1.6.4.1 * * @return string * @noinspection HtmlUnknownTarget */ private function get_missing_assets_error_message(): string { $message = sprintf( wp_kses( /* translators: %s - URL to the troubleshooting guide. */ __( 'Heads up! WPForms has detected an issue with JavaScript on this page. JavaScript is required for this form to work properly, so this form may not work as expected. See our <a href="%s" target="_blank" rel="noopener noreferrer">troubleshooting guide</a> to learn more or contact support.', 'wpforms-lite' ), [ 'a' => [ 'href' => [], 'target' => [], 'rel' => [], ], ] ), 'https://wpforms.com/docs/getting-support-wpforms/' ); $message .= '<p>'; $message .= esc_html__( 'This message is only displayed to site administrators.', 'wpforms-lite' ); $message .= '</p>'; return $message; } /** * Render the single field. * * @since 1.7.7 * * @param array $form_data Form data. * @param array $field Field data. */ public function render_field( array $form_data, array $field ): void { if ( ! has_action( "wpforms_display_field_{$field['type']}" ) ) { return; } /** * Modify Field before render. * * @since 1.4.0 * * @param array $field Current field. * @param array $form_data Form data and settings. */ $field = (array) apply_filters( 'wpforms_field_data', $field, $form_data ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName if ( empty( $field ) ) { return; } $this->rendered_fields[] = $field['id']; // Get field attributes. Deprecated; Customizations should use // field properties instead. $attributes = $this->get_field_attributes( $field, $form_data ); // Add properties to the field, so it's available everywhere. $field['properties'] = $this->get_field_properties( $field, $form_data, $attributes ); /** * Core actions on this hook: * Priority / Description * 5 Field opening container markup. * 15 Field label. * 20 Field description (depending on position). * * @since 1.3.7 * * @param array $field Field. * @param array $form_data Form data. */ do_action( 'wpforms_display_field_before', $field, $form_data ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName /** * Individual field classes use this hook to display the actual * field form elements. * See `field_display` methods in /includes/fields. * * @since 1.3.7 * * @param array $field Field. * @param array $attributes Field attributes. * @param array $form_data Form data. */ do_action( "wpforms_display_field_{$field['type']}", $field, $attributes, $form_data ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName /** * Core actions on this hook: * Priority / Description * 3 Field error messages. * 5 Field description (depending on position). * 15 Field closing container markup. * 20 Pagebreak markups (close previous page, open next). * * @since 1.3.7 * * @param array $field Field. * @param array $form_data Form data. */ do_action( 'wpforms_display_field_after', $field, $form_data ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName } /** * Whether to print the script in the footer. * * @since 1.9.0 * * @return bool */ protected function load_script_in_footer(): bool { return ! wpforms_is_frontend_js_header_force_load(); } } Migrations/Tasks/UpgradeBaseTask.php 0000644 00000012222 15174710275 0013473 0 ustar 00 <?php namespace WPForms\Migrations\Tasks; use RuntimeException; use WPForms\Tasks\Task; use WPForms\Tasks\Meta; /** * Upgrade task base class. * * @since 1.9.5 */ abstract class UpgradeBaseTask extends Task { /** * Start status. * * @since 1.9.5 */ private const START = 'start'; /** * In progress status. * * @since 1.9.5 */ private const IN_PROGRESS = 'in progress'; /** * Completed status. * * @since 1.9.5 */ private const COMPLETED = 'completed'; /** * Task action name. * * @since 1.9.5 * * @var string */ private $action; /** * Option name to store the task status. * * @since 1.9.5 * * @var string */ private $status_option; /** * Class constructor. * * @since 1.9.5 * * @throws RuntimeException If class name doesn't contain a version. */ public function __construct() { $class_parts = explode( '\\', static::class ); $short_class_name = end( $class_parts ); $short_class_name = strtolower( preg_replace( '/(?<!^)[A-Z]/', '_$0', $short_class_name ) ); $this->action = 'wpforms_process_migration_' . $short_class_name; $this->status_option = $this->action . '_status'; parent::__construct( $this->action ); } /** * Get current task status. * * @since 1.9.5 * * @return string */ private function get_status(): string { return (string) get_option( $this->status_option ); } /** * Update task status. * Use the constants self::START, self::IN_PROGRESS, self::COMPLETED. * * @since 1.9.5 * * @param string $status New status. * * @return void */ private function update_status( string $status ): void { update_option( $this->status_option, $status ); } /** * Initialize the task with all the proper checks. * * @since 1.9.5 */ public function init(): void { $status = $this->get_status(); if ( ! $status || $status === self::COMPLETED ) { return; } $this->set_task_properties(); $this->hooks(); if ( $status !== self::START ) { return; } $this->update_status( self::IN_PROGRESS ); $this->init_migration(); } /** * Create a task. * * @param array $args Task arguments. * * @since 1.9.5 * * @return void */ protected function create_task( array $args = [] ): void { $tasks = wpforms()->obj( 'tasks' ); if ( ! $tasks ) { wpforms_log( 'Migration error', [ 'error' => "Object is not available: `null` returned by `wpforms()->obj( 'tasks' )`", 'class' => static::class, 'method' => __METHOD__, ], [ 'type' => 'error', 'force' => true, ] ); return; } $tasks ->create( $this->action ) ->async() ->params( ...$args ) ->register(); } /** * Set task properties. * * @since 1.9.5 * * @return void */ abstract protected function set_task_properties(): void; /** * Add hooks. * * @since 1.9.5 */ protected function hooks(): void { add_action( $this->action, [ $this, 'migrate' ] ); add_action( 'action_scheduler_after_process_queue', [ $this, 'after_process_queue' ] ); } /** * Migrate an entry. * * @since 1.9.5 * * @param int $meta_id Action meta id. * * @noinspection PhpMissingParamTypeInspection */ public function migrate( $meta_id ): void { $params = ( new Meta() )->get( $meta_id ); if ( ! $params || ! isset( $params->data ) ) { return; } $this->process_migration( (array) $params->data ); } /** * Execute an async migration task. * * @since 1.9.5 * * @param array $data Migration data. * * @return void */ abstract protected function process_migration( array $data ): void; /** * Set the status as completed after processing all queue action. * * @since 1.9.5 * * @return void */ public function after_process_queue(): void { $tasks = wpforms()->obj( 'tasks' ); if ( ! $tasks ) { wpforms_log( 'Migration error', [ 'error' => "Object is not available: `null` returned by `wpforms()->obj( 'tasks' )`", 'class' => static::class, 'method' => __METHOD__, ], [ 'type' => 'error', 'force' => true, ] ); return; } if ( $tasks->is_scheduled( $this->action ) ) { return; } $this->finish_migration(); } /** * Finish migration. * * @since 1.9.5 * * @return void */ protected function finish_migration(): void { $this->update_status( self::COMPLETED ); } /** * Create migration tasks using the `create_task` method * or `finish_migration` method to complete it. * * @since 1.9.5 * * @return void */ abstract protected function init_migration(): void; /** * Determine if the task is completed. * Remove the status option to allow running the task again. * * @since 1.9.5 * * @return bool True if a task is completed. */ public function is_completed(): bool { $status = $this->get_status(); $is_completed = $status === self::COMPLETED; if ( $is_completed ) { delete_option( $this->status_option ); } return $is_completed; } /** * Maybe start the task. * * @since 1.9.5 */ public function maybe_start(): void { $status = $this->get_status(); if ( ! $status ) { $this->update_status( self::START ); } } } Migrations/Migrations.php 0000644 00000001740 15174710275 0011520 0 ustar 00 <?php // phpcs:disable Generic.Commenting.DocComment.MissingShort /** @noinspection PhpIllegalPsrClassPathInspection */ /** @noinspection AutoloadingIssuesInspection */ // phpcs:enable Generic.Commenting.DocComment.MissingShort namespace WPForms\Migrations; /** * Class Migrations handles Lite plugin upgrade routines. * * @since 1.7.5 */ class Migrations extends Base { /** * WP option name to store the migration version. * * @since 1.5.9 */ public const MIGRATED_OPTION_NAME = 'wpforms_versions_lite'; /** * Name of the core plugin used in log messages. * * @since 1.7.5 */ protected const PLUGIN_NAME = 'WPForms'; /** * Upgrade classes. * * @since 1.7.5 */ public const UPGRADE_CLASSES = [ 'Upgrade159', 'Upgrade1672', 'Upgrade168', 'Upgrade175', 'Upgrade1751', 'Upgrade177', 'Upgrade182', 'Upgrade183', 'Upgrade184', 'Upgrade186', 'Upgrade187', 'Upgrade1_9_1', 'Upgrade1_9_2', 'Upgrade1_9_7', 'Upgrade1_9_8_6', ]; } Migrations/Upgrade1_9_1.php 0000644 00000002576 15174710275 0011534 0 ustar 00 <?php namespace WPForms\Migrations; /** * Class upgrade for 1.9.1 release. * * @since 1.9.1 */ class Upgrade1_9_1 extends UpgradeBase { /** * Delete existed notifications for the customer. * * @since 1.9.1 * * @return bool|null Upgrade result: * true - the upgrade completed successfully, * false - in the case of failure, * null - upgrade started but not yet finished (background task). */ public function run() { $this->clean_summaries_cron_event(); $notifications_option_key = 'wpforms_notifications'; $notifications = get_option( $notifications_option_key, [] ); if ( empty( $notifications['events'] ) ) { return true; } $notifications['events'] = []; update_option( 'wpforms_notifications', $notifications ); return true; } /** * Clean summaries and entries count cron events, * Since the 1.9.1 release these cron events recurrences have been changed to single event. * The events will be recreated on the next page load. * * @since 1.9.1 */ private function clean_summaries_cron_event() { if ( wp_next_scheduled( 'wpforms_weekly_entries_count_cron' ) ) { wp_clear_scheduled_hook( 'wpforms_weekly_entries_count_cron' ); } if ( wp_next_scheduled( 'wpforms_email_summaries_cron' ) ) { wp_clear_scheduled_hook( 'wpforms_email_summaries_cron' ); } } } Migrations/Upgrade182.php 0000644 00000006150 15174710275 0011226 0 ustar 00 <?php // phpcs:ignore Generic.Commenting.DocComment.MissingShort /** @noinspection PhpUnused */ namespace WPForms\Migrations; use WPForms\Helpers\Transient; /** * Class v1.8.2 upgrade. * * @since 1.8.2 */ class Upgrade182 extends UpgradeBase { /** * Run upgrade. * * @since 1.8.2 * * @return bool|null Upgrade result: * true - the upgrade completed successfully, * false - in the case of failure, * null - upgrade started but not yet finished (background task). */ public function run() { $cache_dir = $this->get_cache_dir(); $templates_cache_dir = $cache_dir . 'templates/'; $this->set_cache_time( $cache_dir, 'addons.json', 'wpforms_admin_addons_cache_ttl' ); $this->set_cache_time( $cache_dir, 'docs.json', 'wpforms_admin_builder_help_cache_ttl' ); $this->set_cache_time( $cache_dir, 'templates.json', 'wpforms_admin_builder_templates_cache_ttl' ); $files = glob( $templates_cache_dir . '*.json' ); foreach ( $files as $filename ) { $this->set_cache_time( $templates_cache_dir, basename( $filename ), 'wpforms_admin_builder_templates_cache_ttl' ); } return true; } /** * Set cache time to transient. * * @since 1.8.2 * * @param string $cache_dir Cache directory. * @param string $cache_file Cache filename. * @param string $filter Filter name. * * @return void */ private function set_cache_time( $cache_dir, $cache_file, $filter ) { // phpcs:ignore WPForms.Comments.PHPDocHooks.RequiredHookDocumentation, WPForms.PHP.ValidateHooks.InvalidHookName, WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound $cache_ttl = (int) apply_filters( $filter, WEEK_IN_SECONDS ); $cache_file_path = $cache_dir . $cache_file; $cache_modified_time = 0; $transient = $cache_file; $time = time(); if ( is_file( $cache_file_path ) && is_readable( $cache_file_path ) ) { clearstatcache( true, $cache_file_path ); // On WPVIP and similar filesystems, filemtime() could return false. $cache_modified_time = (int) filemtime( $cache_file_path ); } if ( $cache_modified_time === 0 || $cache_modified_time + $cache_ttl <= $time ) { // Do not set transient for non-existing or expired cache. return; } $expiration = $cache_modified_time + $cache_ttl - $time; Transient::set( $transient, $cache_modified_time, $expiration ); } /** * Get cache directory path. * Copy of the CacheBase method. * * @since 1.8.2 */ private function get_cache_dir() { static $cache_dir; if ( $cache_dir ) { /** * Since wpforms_upload_dir() relies on hooks, and hooks can be added unpredictably, * we need to cache the result of this method. * Otherwise, it is the risk to save cache file to one dir and try to get from another. */ return $cache_dir; } $upload_dir = wpforms_upload_dir(); $upload_path = ! empty( $upload_dir['path'] ) ? trailingslashit( wp_normalize_path( $upload_dir['path'] ) ) : trailingslashit( WP_CONTENT_DIR ) . 'uploads/wpforms/'; $cache_dir = $upload_path . 'cache/'; return $cache_dir; } } Migrations/Upgrade1_9_7.php 0000644 00000000515 15174710275 0011531 0 ustar 00 <?php namespace WPForms\Migrations; /** * Class upgrade for 1.9.7 release. * * @since 1.9.7 */ class Upgrade1_9_7 extends UpgradeBase { /** * Run upgrade. * * @since 1.9.7 */ public function run(): bool { // Force update splash data cache. wpforms()->obj( 'splash_cache' )->update( true ); return true; } } Migrations/Upgrade175.php 0000644 00000005261 15174710275 0011232 0 ustar 00 <?php namespace WPForms\Migrations; use WPForms\Tasks\Meta; use WPForms\Tasks\Tasks; /** * Class v1.7.5 upgrade. * * @since 1.7.5 * * @noinspection PhpUnused */ class Upgrade175 extends UpgradeBase { /** * Delete all task meta of not active tasks. * * @since 1.7.5 * * @noinspection ElvisOperatorCanBeUsedInspection * * @return bool|null Upgrade result: * true - the upgrade completed successfully, * false - in the case of failure, * null - upgrade started but not yet finished (background task). */ public function run() { global $wpdb; if ( ! $this->as_tables_exist() ) { return true; } $group = Tasks::GROUP; $sql = "SELECT DISTINCT a.args FROM {$wpdb->prefix}actionscheduler_actions a JOIN {$wpdb->prefix}actionscheduler_groups g ON g.group_id = a.group_id WHERE g.slug = '$group' AND a.status IN ( 'pending', 'in-progress' )"; // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared $results = $wpdb->get_results( $sql, 'ARRAY_A' ); // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared $results = $results ? $results : []; $meta_ids = []; foreach ( $results as $result ) { $args = isset( $result['args'] ) ? json_decode( $result['args'], true ) : null; if ( $args && ! empty( $args['tasks_meta_id'] ) ) { $meta_ids[] = $args['tasks_meta_id']; } } $table_name = Meta::get_table_name(); $not_in = $meta_ids ? wpforms_wpdb_prepare_in( $meta_ids ) : '0'; // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.NoCaching $wpdb->query( "DELETE FROM $table_name WHERE id NOT IN ( $not_in )" ); // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.NoCaching return true; } /** * Check whether AS tables exist. * * @since 1.7.6 * * @return bool */ private function as_tables_exist() { global $wpdb; $required_tables = [ $wpdb->prefix . 'actionscheduler_actions', $wpdb->prefix . 'actionscheduler_groups', ]; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $tables = $wpdb->get_col( "SHOW TABLES LIKE '{$wpdb->prefix}actionscheduler%'" ); $intersect = array_values( array_intersect( $tables, $required_tables ) ); sort( $intersect ); sort( $required_tables ); return $intersect === $required_tables; } } Migrations/Upgrade1672.php 0000644 00000001537 15174710275 0011317 0 ustar 00 <?php namespace WPForms\Migrations; /** * Class v1.6.7.2 upgrade. * * @since 1.7.5 * * @noinspection PhpUnused */ class Upgrade1672 extends UpgradeBase { /** * Run upgrade. * * @since 1.7.5 * * @return bool|null Upgrade result: * true - the upgrade completed successfully, * false - in the case of failure, * null - upgrade started but not yet finished (background task). */ public function run() { $review = get_option( 'wpforms_review' ); if ( empty( $review ) ) { return true; } $notices = get_option( 'wpforms_admin_notices', [] ); if ( isset( $notices['review_request'] ) ) { return true; } $notices['review_request'] = $review; update_option( 'wpforms_admin_notices', $notices, true ); delete_option( 'wpforms_review' ); return true; } } Migrations/Upgrade184.php 0000644 00000001615 15174710275 0011231 0 ustar 00 <?php namespace WPForms\Migrations; use WPForms\Integrations\Stripe\Helpers; use WPForms\Tasks\Actions\WebhooksAutoConfigurationTask; /** * Class upgrade for 1.8.4 release. * * @since 1.8.4 * * @noinspection PhpUnused */ class Upgrade184 extends UpgradeBase { /** * Run upgrade. * * @since 1.8.4 * * @return bool|null */ public function run() { $this->set_webhooks_settings(); return $this->run_async( WebhooksAutoConfigurationTask::class ); } /** * Set Stripe webhooks settings. * * @since 1.8.4 */ private function set_webhooks_settings() { $settings = (array) get_option( 'wpforms_settings', [] ); // Enable Stripe webhooks by default if account is connected. if ( ! isset( $settings['stripe-webhooks-enabled'] ) && Helpers::has_stripe_keys() ) { $settings['stripe-webhooks-enabled'] = true; update_option( 'wpforms_settings', $settings ); } } } Migrations/Upgrade187.php 0000644 00000002553 15174710275 0011236 0 ustar 00 <?php namespace WPForms\Migrations; use WPForms\Admin\Builder\TemplatesCache; use WPForms\Tasks\Actions\StripeLinkSubscriptionsTask; /** * Class upgrade for 1.8.7 release. * * @since 1.8.7 * * @noinspection PhpUnused */ class Upgrade187 extends UpgradeBase { /** * Run upgrade. * * @since 1.8.7 * * @return bool|null */ public function run() { $sync_result = $this->update_templates_cache() && $this->maybe_create_logs_table(); $async_result = $this->run_async( StripeLinkSubscriptionsTask::class ); return $async_result === null ? null : $sync_result && $async_result; } /** * Update templates' cache. * * @since 1.8.7 * * @return bool */ private function update_templates_cache(): bool { $templates_cache = new TemplatesCache(); $templates_cache->init(); $templates_cache->update(); return true; } /** * Maybe create logs' table. * Previously, logs' table was created dynamically on the first access to the Tools->Logs admin page. * As from 1.8.7, we create it only once during the activation of the plugin. * So, the table may not exist, and we must maybe create it during migration to 1.8.7. * * @since 1.8.7 * * @return bool */ private function maybe_create_logs_table(): bool { $log = wpforms()->obj( 'log' ); if ( ! $log ) { return false; } $log->create_table(); return true; } } Migrations/Upgrade1751.php 0000644 00000001062 15174710275 0011306 0 ustar 00 <?php namespace WPForms\Migrations; /** * Class v1.7.5.1 upgrade. * * @since 1.7.5.1 * * @noinspection PhpUnused */ class Upgrade1751 extends UpgradeBase { /** * Repeat 1.7.5 migration. * * @since 1.7.5.1 * * @return bool|null Upgrade result: * true - the upgrade completed successfully, * false - in the case of failure, * null - upgrade started but not yet finished (background task). */ public function run() { return ( new Upgrade175( $this->migrations ) )->run(); } } Migrations/Upgrade1_9_8_6.php 0000644 00000005362 15174710275 0011764 0 ustar 00 <?php namespace WPForms\Migrations; /** * Class upgrade for 1.9.8.6 release. * * @since 1.9.8.6 */ class Upgrade1_9_8_6 extends UpgradeBase { /** * Run upgrade. * * @since 1.9.8.6 */ public function run(): bool { $activated_plugins = []; $this->check_wpconsent_activation( $activated_plugins ); $this->check_sugar_calendar_activation( $activated_plugins ); $this->check_duplicator_activation( $activated_plugins ); $this->check_uncanny_automator_activation( $activated_plugins ); add_option( 'wpforms_rotation_activated_plugins', $activated_plugins ); return true; } /** * Check WPConsent plugin activation time. * * @since 1.9.8.6 * * @param array $activated_plugins Reference to activated plugins array. */ private function check_wpconsent_activation( array &$activated_plugins ): void { $wpconsent = get_option( 'wpconsent_activated' ); $wpconsent_time = $wpconsent['wpconsent'] ?? null; if ( empty( $wpconsent_time ) ) { $wpconsent_time = $wpconsent['wpconsent_pro'] ?? null; } if ( ! empty( $wpconsent_time ) ) { $activated_plugins['wpconsent'] = $wpconsent_time; } } /** * Check Sugar Calendar plugin activation time. * * @since 1.9.8.6 * * @param array $activated_plugins Reference to activated plugins array. */ private function check_sugar_calendar_activation( array &$activated_plugins ): void { $sugar_calendar_activated_time = get_option( 'sugar_calendar_activated_time' ); if ( ! empty( $sugar_calendar_activated_time ) ) { $activated_plugins['sugar-calendar'] = (int) $sugar_calendar_activated_time; } } /** * Check Duplicator plugin activation time. * * @since 1.9.8.6 * * @param array $activated_plugins Reference to activated plugins array. */ private function check_duplicator_activation( array &$activated_plugins ): void { $duplicator_install_info = get_option( 'duplicator_install_info' ); $duplicator_time = $duplicator_install_info['time'] ?? null; if ( empty( $duplicator_time ) ) { $duplicator_pro_install_info = get_option( 'duplicator_pro_install_info' ); $duplicator_time = $duplicator_pro_install_info['time'] ?? null; } if ( ! empty( $duplicator_time ) ) { $activated_plugins['duplicator'] = $duplicator_time; } } /** * Check Uncanny Automator plugin activation time. * * @since 1.9.8.6 * * @param array $activated_plugins Reference to activated plugins array. */ private function check_uncanny_automator_activation( array &$activated_plugins ): void { $uncanny_automator_v6_options_migrated = get_option( 'uncanny_automator_v6_options_migrated' ); if ( ! empty( $uncanny_automator_v6_options_migrated ) ) { $activated_plugins['uncanny-automator'] = (int) $uncanny_automator_v6_options_migrated; } } } Migrations/Upgrade159.php 0000644 00000001303 15174710275 0011225 0 ustar 00 <?php namespace WPForms\Migrations; /** * Class v1.5.9 upgrade. * * @since 1.7.5 * * @noinspection PhpUnused */ class Upgrade159 extends UpgradeBase { /** * Create tasks_meta table. * * @since 1.7.5 * * @return bool|null Upgrade result: * true - the upgrade completed successfully, * false - in the case of failure, * null - upgrade started but not yet finished (background task). */ public function run() { $meta = wpforms()->obj( 'tasks_meta' ); if ( ! $meta ) { return false; } // Create the table if it doesn't exist. if ( ! $meta->table_exists() ) { $meta->create_table(); } return true; } } Migrations/Upgrade186.php 0000644 00000000613 15174710275 0011230 0 ustar 00 <?php namespace WPForms\Migrations; use WPForms\Tasks\Actions\DomainAutoRegistrationTask; /** * Class upgrade for 1.8.6 release. * * @since 1.8.6 * * @noinspection PhpUnused */ class Upgrade186 extends UpgradeBase { /** * Run upgrade. * * @since 1.8.6 * * @return bool|null */ public function run() { return $this->run_async( DomainAutoRegistrationTask::class ); } } Migrations/Upgrade1_9_2.php 0000644 00000001617 15174710275 0011530 0 ustar 00 <?php namespace WPForms\Migrations; use WPForms\Integrations\Stripe\Helpers; use WPForms\Tasks\Actions\WebhooksAutoConfigurationTask; /** * Class upgrade for 1.9.2 release. * * @since 1.9.2 * * @noinspection PhpUnused */ class Upgrade1_9_2 extends UpgradeBase { /** * Run upgrade. * * @since 1.9.2 * * @return bool|null */ public function run() { $this->set_webhooks_settings(); return $this->run_async( WebhooksAutoConfigurationTask::class ); } /** * Set Stripe webhooks settings. * * @since 1.9.2 */ private function set_webhooks_settings() { $settings = (array) get_option( 'wpforms_settings', [] ); // Enable Stripe webhooks by default if account is connected. if ( ! isset( $settings['stripe-webhooks-enabled'] ) && Helpers::has_stripe_keys() ) { $settings['stripe-webhooks-enabled'] = true; update_option( 'wpforms_settings', $settings ); } } } Migrations/UpgradeBase.php 0000644 00000006670 15174710275 0011575 0 ustar 00 <?php // phpcs:disable Generic.Commenting.DocComment.MissingShort /** @noinspection PhpExpressionResultUnusedInspection */ /** @noinspection PhpPropertyOnlyWrittenInspection */ /** @noinspection UnusedConstructorDependenciesInspection */ /** @noinspection PhpUnusedAliasInspection */ // phpcs:enable Generic.Commenting.DocComment.MissingShort namespace WPForms\Migrations; // phpcs:disable WPForms.PHP.UseStatement.UnusedUseStatement use WPForms\Migrations\Migrations; use WPForms\Migrations\Tasks\UpgradeBaseTask; use WPForms\Pro\Migrations\Migrations as MigrationsPro; // phpcs:enable WPForms.PHP.UseStatement.UnusedUseStatement /** * Class UpgradeBase contains both Lite and Pro plugin upgrade methods. * * @since 1.7.5 */ abstract class UpgradeBase { /** * Migration class instance. * * @since 1.7.5 * * @var Migrations|MigrationsPro */ protected $migrations; /** * Primary class constructor. * * @since 1.7.5 * * @param Migrations|MigrationsPro $migrations Instance of Migrations class. */ public function __construct( $migrations ) { $this->migrations = $migrations; } /** * Run upgrade. * * @since 1.7.5 * * @return bool|null Upgrade result: * true - the upgrade completed successfully, * false - in the case of failure, * null - upgrade started but not yet finished (background task). */ abstract public function run(); /** * Run the async upgrade via an Action Scheduler (AS) task. * The AS task has to support STATUS option with START, IN_PROGRESS, and COMPLETED values. * Also, the AS task must have the init() method. * * @since 1.7.5 * * @param string $classname Classname of an async AS task. * * @return bool|null Upgrade result: * true - the upgrade completed successfully, * false - in the case of failure, * null - upgrade started but not yet finished (background task). */ protected function run_async( string $classname ) { // phpcs:ignore WPForms.PHP.HooksMethod.InvalidPlaceForAddingHooks $instance = new $classname(); if ( $this->is_completed( $instance ) ) { return true; } $this->maybe_start( $instance ); // Class Tasks does not exist at this point, so we have to add an action on init. add_action( 'init', static function () use ( $instance ) { $instance->init(); }, PHP_INT_MAX ); return null; } /** * Determine if the async AS task is completed. * * @since 1.9.5 * * @param object $instance Instance of an async AS task. * * @use UpgradeBaseTask::is_completed() * * @return bool */ private function is_completed( $instance ): bool { if ( method_exists( $instance, 'is_completed' ) ) { return $instance->is_completed(); } // Legacy tasks. $status = get_option( $instance::STATUS ); if ( $status === $instance::COMPLETED ) { delete_option( $instance::STATUS ); return true; } return false; } /** * Start the async AS task if it wasn't started yet. * * @since 1.9.5 * * @param object $instance Instance of an async AS task. * * @use UpgradeBaseTask::maybe_start() */ private function maybe_start( $instance ): void { if ( method_exists( $instance, 'maybe_start' ) ) { $instance->maybe_start(); return; } // Legacy tasks. $status = get_option( $instance::STATUS ); if ( ! $status ) { update_option( $instance::STATUS, $instance::START ); } } } Migrations/Upgrade168.php 0000644 00000002364 15174710275 0011235 0 ustar 00 <?php namespace WPForms\Migrations; /** * Class v1.6.8 upgrade. * * @since 1.7.5 * * @noinspection PhpUnused */ class Upgrade168 extends UpgradeBase { /** * Run upgrade. * * @since 1.7.5 * * @return bool|null Upgrade result: * true - the upgrade completed successfully, * false - in the case of failure, * null - upgrade started but not yet finished (background task). */ public function run() { $current_opened_date = get_option( 'wpforms_builder_opened_date', null ); // Do not run migration twice as 0 is a default value for all old users. if ( $current_opened_date === '0' ) { return true; } // We don't want users to report to us if they already previously used the builder by creating a form. $form_handler = wpforms()->obj( 'form' ); if ( ! $form_handler ) { return false; } $forms = $form_handler->get( '', [ 'posts_per_page' => 1, 'nopaging' => false, 'fields' => 'ids', 'update_post_meta_cache' => false, ] ); // At least 1 form exists - set the default value. if ( ! empty( $forms ) ) { add_option( 'wpforms_builder_opened_date', 0, '', 'no' ); } return true; } } Migrations/Upgrade183.php 0000644 00000001401 15174710275 0011221 0 ustar 00 <?php namespace WPForms\Migrations; use WPForms\Tasks\Actions\IconChoicesFontAwesomeUpgradeTask; /** * Class upgrade for Lite. * * @since 1.8.3 * * @noinspection PhpUnused */ class Upgrade183 extends UpgradeBase { /** * Run upgrade. * * We run migration as Action Scheduler task. * Class Tasks does not exist at this point, so here we can only check task completion status. * * @since 1.8.3 * * @return bool|null Upgrade result: * true - the upgrade completed successfully, * false - in the case of failure, * null - upgrade started but not yet finished (background task). */ public function run() { return $this->run_async( IconChoicesFontAwesomeUpgradeTask::class ); } } Migrations/Upgrade177.php 0000644 00000002330 15174710275 0011226 0 ustar 00 <?php namespace WPForms\Migrations; /** * Class v1.7.7 upgrade. * * @since 1.7.7 */ class Upgrade177 extends UpgradeBase { /** * Run upgrade. * * @since 1.7.7 * * @return bool|null Upgrade result: * true - the upgrade completed successfully, * false - in the case of failure, * null - upgrade started but not yet finished (background task). */ public function run() { $settings = (array) get_option( 'wpforms_settings', [] ); $new_inputmask_key = 'validation-inputmask-incomplete'; $old_inputmask_key = 'validation-input-mask-incomplete'; $is_updated = false; if ( isset( $settings[ $new_inputmask_key ] ) && in_array( $settings[ $new_inputmask_key ], [ 'Please fill out all blanks.', esc_html__( 'Please fill out all blanks.', 'wpforms-lite' ) ], true ) ) { unset( $settings[ $new_inputmask_key ] ); $is_updated = true; } if ( empty( $settings[ $new_inputmask_key ] ) && ! empty( $settings[ $old_inputmask_key ] ) ) { $settings[ $new_inputmask_key ] = $settings[ $old_inputmask_key ]; $is_updated = true; } if ( $is_updated ) { update_option( 'wpforms_settings', $settings ); } return true; } } Migrations/Base.php 0000644 00000030026 15174710275 0010255 0 ustar 00 <?php // phpcs:disable Generic.Commenting.DocComment.MissingShort /** @noinspection PhpIllegalPsrClassPathInspection */ /** @noinspection AutoloadingIssuesInspection */ // phpcs:enable Generic.Commenting.DocComment.MissingShort namespace WPForms\Migrations; use ReflectionClass; use WPForms\Helpers\DB; /** * Class Migrations handles both Lite and Pro plugin upgrade routines. * * @since 1.7.5 */ abstract class Base { /** * WP option name to store the migration versions. * Must have 'versions' in the name defined in extending classes, * like 'wpforms_versions', 'wpforms_versions_lite, 'wpforms_stripe_versions', etc. * * @since 1.7.5 */ protected const MIGRATED_OPTION_NAME = ''; /** * Current plugin version. * * @since 1.7.5 */ private const CURRENT_VERSION = WPFORMS_VERSION; /** * WP option name to store the upgraded from version number. * * @since 1.8.8 * @deprecated 1.9.8 * * @todo Delete this option later. There is no sense to creating a separate migration for it. * @noinspection PhpUnusedPrivateFieldInspection */ private const UPGRADED_FROM_OPTION_NAME = 'wpforms_version_upgraded_from'; /** * WP option name to store the previous plugin version. * * @since 1.8.8 */ public const PREVIOUS_CORE_VERSION_OPTION_NAME = 'wpforms_version_previous'; /** * Name of the core plugin used in log messages. * * @since 1.7.5 */ protected const PLUGIN_NAME = ''; /** * Upgrade classes. * * @since 1.7.5 */ protected const UPGRADE_CLASSES = []; /** * Migration started status. * * @since 1.7.5 */ private const STARTED = - 1; /** * Migration failed status. * * @since 1.7.5 */ private const FAILED = - 2; /** * Initial fake version for comparisons. * * @since 1.7.5 */ private const INITIAL_FAKE_VERSION = '0.0.1'; /** * Reflection class instance. * * @since 1.7.5 * * @var ReflectionClass */ protected $reflector; /** * Migrated versions. * * @since 1.7.5 * * @var string[] */ protected $migrated = []; /** * Whether tables' check was done. * * @since 1.8.7 * * @var bool */ private $tables_check_done; /** * Primary class constructor. * * @since 1.7.5 */ public function __construct() { $this->reflector = new ReflectionClass( $this ); } /** * Class init. * * @since 1.7.5 */ public function init(): void { if ( ! $this->is_allowed() ) { return; } $this->maybe_convert_migration_option(); $this->hooks(); } /** * General hooks. * * @since 1.7.5 */ protected function hooks(): void { $priority = $this->is_core_plugin() ? - 9999 : 100; add_action( 'wpforms_loaded', [ $this, 'migrate' ], $priority ); add_action( 'wpforms_loaded', [ $this, 'update_versions' ], $priority + 1 ); } /** * Run the migrations of the core plugin for a specific version. * * @since 1.7.5 * * @noinspection NotOptimalIfConditionsInspection */ public function migrate(): void { // phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh $classes = $this->get_upgrade_classes(); $namespace = $this->reflector->getNamespaceName() . '\\'; foreach ( $classes as $class ) { $upgrade_version = $this->get_upgrade_version( $class ); $plugin_name = $this->get_plugin_name( $class ); $class = $namespace . $class; if ( ( isset( $this->migrated[ $upgrade_version ] ) && $this->migrated[ $upgrade_version ] >= 0 ) || version_compare( $upgrade_version, self::CURRENT_VERSION, '>' ) || ! class_exists( $class ) ) { continue; } $this->maybe_create_tables(); if ( ! isset( $this->migrated[ $upgrade_version ] ) ) { $this->migrated[ $upgrade_version ] = self::STARTED; $this->log( sprintf( 'Migration of %1$s to %2$s started.', $plugin_name, $upgrade_version ) ); } // Run upgrade. $migrated = ( new $class( $this ) )->run(); // Some migration methods can be called several times to support AS action, // so do not log their completion here. if ( $migrated === null ) { continue; } $this->migrated[ $upgrade_version ] = $migrated ? time() : self::FAILED; $this->log_migration_message( $migrated, $plugin_name, $upgrade_version ); } } /** * If an upgrade has occurred, update a version option in the database. * * @since 1.7.5 */ public function update_versions(): void { $this->update_previous_core_version(); // Retrieve the last migrated versions. $last_migrated = get_option( static::MIGRATED_OPTION_NAME, [] ); $migrated = array_merge( $last_migrated, $this->migrated ); /** * Store the current version upgrade timestamp even if there were no migrations to it. * We need it in wpforms_get_upgraded_timestamp() for further usage in Event Driven Plugin Notifications. */ $migrated[ self::CURRENT_VERSION ] = $migrated[ self::CURRENT_VERSION ] ?? time(); uksort( $last_migrated, 'version_compare' ); uksort( $migrated, 'version_compare' ); if ( $migrated === $last_migrated ) { return; } update_option( static::MIGRATED_OPTION_NAME, $migrated ); $fully_completed = array_reduce( $migrated, static function ( $carry, $status ) { return $carry && ( $status >= 0 ); }, true ); if ( ! $fully_completed ) { return; } $this->log( sprintf( 'Migration of %1$s to %2$s is fully completed.', static::PLUGIN_NAME, self::CURRENT_VERSION ) ); } /** * Update previous core version. * * @since 1.9.8 * * @return void */ private function update_previous_core_version(): void { if ( ! $this->is_core_plugin() ) { return; } // Retrieve the last migrated versions. $last_migrated = get_option( static::MIGRATED_OPTION_NAME, [] ); $previous_core_version = $this->get_max_version( $last_migrated ); if ( $previous_core_version === self::INITIAL_FAKE_VERSION || version_compare( $previous_core_version, self::CURRENT_VERSION, '>=' ) ) { return; } // Store the previous core version in the option. update_option( self::PREVIOUS_CORE_VERSION_OPTION_NAME, $previous_core_version ); /** * Fires after the core plugin has been upgraded. * Please note: some of the migrations that run via Active Scheduler can be not completed yet. * * @since 1.8.8 * * @param string $previous_core_version The core version from which the plugin was upgraded. * @param Base $migration_obj The migration class instance. */ do_action( 'wpforms_migrations_base_core_upgraded', $previous_core_version, $this ); } /** * Get upgrade classes. * * @since 1.7.5 * * @return string[] */ protected function get_upgrade_classes(): array { $classes = static::UPGRADE_CLASSES; sort( $classes ); return $classes; } /** * Get an upgrade version from the class name. * * @since 1.7.5 * * @param string $class_name Class name. * * @return string */ public function get_upgrade_version( string $class_name ): string { // Find only the digits and underscores to get the version number. if ( ! preg_match( '/(\d_?)+/', $class_name, $matches ) ) { return ''; } $raw_version = $matches[0]; if ( strpos( $raw_version, '_' ) ) { // Modern notation: 1_10_0_3 means 1.10.0.3 version. return str_replace( '_', '.', $raw_version ); } // Legacy notation, with 1-digit subversion numbers: 1751 means 1.7.5.1 version. return implode( '.', str_split( $raw_version ) ); } /** * Get a plugin /addon name. * * @since 1.7.5 * * @param string $class_name Upgrade class name. * * @return string * @noinspection PhpUnusedParameterInspection */ protected function get_plugin_name( string $class_name ): string { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found return static::PLUGIN_NAME; } /** * Force log message to WPForms logger. * * @since 1.7.5 * * @param string $message The error message that should be logged. */ protected function log( string $message ): void { wpforms_log( 'Migration', $message, [ 'type' => 'log', 'force' => true, ] ); } /** * Determine if migration is allowed. * * @since 1.7.5 */ private function is_allowed(): bool { // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( isset( $_GET['service-worker'] ) ) { return false; } return wp_doing_cron() || is_admin() || wpforms_doing_wp_cli(); } /** * Maybe create custom plugin tables. * * @since 1.7.6 */ public function maybe_create_tables(): void { if ( $this->tables_check_done ) { /** * We should do table check only once - when the first migration has been started. * The DB::get_existing_custom_tables() without caching causes performance issue * on huge multisite with thousands of tables. */ return; } DB::create_custom_tables( true ); $this->tables_check_done = true; } /** * Maybe convert the migration option format. * * @since 1.7.5 */ private function maybe_convert_migration_option(): void { /** * Retrieve the migration option and check its format. * Old format: a string 'x.y.z' containing the last migrated version. * New format: [ 'x.y.z' => {status}, 'x1.y1.z1' => {status}... ], * where {status} is a migration status. * Negative means some status (-1 for 'started' etc.), * zero means completed earlier at an unknown time, * positive means completion timestamp. */ $this->migrated = get_option( static::MIGRATED_OPTION_NAME ); // If the option is an array, it means that it is already converted to the new format. if ( is_array( $this->migrated ) ) { return; } /** * Convert the option to the new format. * * Old option names contained 'version', * like 'wpforms_version', 'wpforms_version_lite', 'wpforms_stripe_version', etc. * We preserve old options for downgrade cases. * New option names should contain 'versions' and be like 'wpforms_versions', etc. */ $this->migrated = get_option( str_replace( 'versions', 'version', static::MIGRATED_OPTION_NAME ) ); $version = $this->migrated === false ? self::INITIAL_FAKE_VERSION : (string) $this->migrated; $timestamp = $version === self::CURRENT_VERSION ? time() : 0; $this->migrated = [ $version => $timestamp ]; $max_version = $this->get_max_version( $this->migrated ); foreach ( $this->get_upgrade_classes() as $upgrade_class ) { $upgrade_version = $this->get_upgrade_version( $upgrade_class ); if ( ! isset( $this->migrated[ $upgrade_version ] ) && version_compare( $upgrade_version, $max_version, '<' ) ) { $this->migrated[ $upgrade_version ] = 0; } } unset( $this->migrated[ self::INITIAL_FAKE_VERSION ] ); ksort( $this->migrated ); update_option( static::MIGRATED_OPTION_NAME, $this->migrated ); } /** * Get the max version. * * @since 1.7.5 * * @param array $versions Versions. * * @return string */ private function get_max_version( array $versions ): string { // phpcs:ignore WPForms.Formatting.EmptyLineBeforeReturn.RemoveEmptyLineBeforeReturnStatement return array_reduce( array_keys( $versions ), static function ( $carry, $version ) { return version_compare( $version, $carry, '>' ) ? $version : $carry; }, self::INITIAL_FAKE_VERSION ); } /** * Determine if it is the core plugin (Lite or Pro). * * @since 1.7.5 * * @return bool True if it is the core plugin. */ protected function is_core_plugin(): bool { // phpcs:ignore WPForms.Formatting.EmptyLineBeforeReturn.RemoveEmptyLineBeforeReturnStatement return strpos( static::MIGRATED_OPTION_NAME, 'wpforms_versions' ) === 0; } /** * Log migration message. * * @since 1.8.2.3 * * @param bool $migrated Migration status. * @param string $plugin_name Plugin name. * @param string $upgrade_version Upgrade version. * * @return void */ private function log_migration_message( bool $migrated, string $plugin_name, string $upgrade_version ): void { $message = $migrated ? sprintf( 'Migration of %1$s to %2$s completed.', $plugin_name, $upgrade_version ) : sprintf( 'Migration of %1$s to %2$s failed.', $plugin_name, $upgrade_version ); $this->log( $message ); } } Forms/Akismet.php 0000644 00000022474 15174710275 0007762 0 ustar 00 <?php namespace WPForms\Forms; use Akismet as AkismetPlugin; /** * Class Akismet. * * @since 1.7.6 */ class Akismet { /** * Is the Akismet plugin installed? * * @since 1.7.6 * * @return bool */ public static function is_installed(): bool { return file_exists( WP_PLUGIN_DIR . '/akismet/akismet.php' ); } /** * Is the Akismet plugin activated? * * @since 1.7.6 * * @return bool */ public static function is_activated(): bool { return is_callable( [ 'Akismet', 'get_api_key' ] ) && is_callable( [ 'Akismet', 'http_post' ] ); } /** * Has the Akismet plugin been configured wih a valid API key? * * @since 1.7.6 * * @return bool */ public static function is_configured(): bool { // Akismet will only allow an API key to be saved if it is a valid key. // We can assume that if there is an API key saved, it is valid. return self::is_activated() && ! empty( AkismetPlugin::get_api_key() ); } /** * Get the list of field types that are allowed to be sent to Akismet. * * @since 1.7.6 * * @return array List of field types that are allowed to be sent to Akismet */ private function get_field_type_allowlist(): array { $field_type_allowlist = [ 'text', 'textarea', 'name', 'email', 'phone', 'address', 'url', 'richtext', ]; /** * Filters the field types that are allowed to be sent to Akismet. * * @since 1.7.6 * * @param array $field_type_allowlist Field types allowed to be sent to Akismet. */ return (array) apply_filters( 'wpforms_forms_akismet_get_field_type_allowlist', $field_type_allowlist ); } /** * Get the entry data to be sent to Akismet. * * @since 1.7.6 * * @param array $fields Field data for the current form. * @param array $entry Entry data. * * @return array $entry_data Entry data to be sent to Akismet. */ private function get_entry_data( array $fields, array $entry ): array { $field_type_allowlist = $this->get_field_type_allowlist(); $entry_data = []; $entry_content = []; foreach ( $fields as $field_id => $field ) { $field_type = $field['type']; if ( ! in_array( $field_type, $field_type_allowlist, true ) ) { continue; } $field_content = $this->get_field_content( $field, $entry, $field_id ); if ( ! isset( $entry_data[ $field_type ] ) && in_array( $field_type, [ 'name', 'email', 'url' ], true ) ) { $entry_data[ $field_type ] = $field_content; continue; } $entry_content[] = $field_content; } $entry_data['content'] = implode( ' ', $entry_content ); return $entry_data; } /** * Get field content. * * @since 1.8.5 * @since 1.8.9.3 Changed $field_id type from string to int|string. * * @param array $field Field data. * @param array $entry Entry data. * @param int|string $field_id Field ID. * * @return string */ private function get_field_content( array $field, array $entry, $field_id ): string { if ( ! isset( $entry['fields'][ $field_id ] ) ) { return ''; } if ( ! is_array( $entry['fields'][ $field_id ] ) ) { return (string) $entry['fields'][ $field_id ]; } if ( ! empty( $field['type'] ) && $field['type'] === 'email' && ! empty( $entry['fields'][ $field_id ]['primary'] ) ) { return (string) $entry['fields'][ $field_id ]['primary']; } return implode( ' ', $entry['fields'][ $field_id ] ); } /** * Is the entry marked as spam by Akismet? * * @since 1.7.6 * * @param array $form_data Form data for the current form. * @param array $entry Entry data for the current entry. * * @return bool */ private function entry_is_spam( array $form_data, array $entry ): bool { $request = $this->get_request_args( $form_data, $entry ); // Tell Akismet to not use the submission for training if we're on the Preview page and the user is // an administrator. Checking for both the preview page and the administrator role prevents // abuse by simply adding a GET parameter. This check happens in the ajax request, // where `\WPForms\Forms\Preview::is_preview_page()` does not work, so we // need to check for the GET parameter directly. if ( // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized isset( $_REQUEST['page_url'] ) && strpos( wp_unslash( $_REQUEST['page_url'] ), 'wpforms_form_preview' ) !== false && current_user_can( 'manage_options' ) ) { $request['is_test'] = true; } $response = $this->http_post( $request, 'comment-check' ); return ! empty( $response ) && isset( $response[1] ) && 'true' === trim( $response[1] ); } /** * Mark the entry as not spam in Akismet. * * @since 1.8.8 * * @param array $form_data Form data for the current form. * @param array $entry Entry data for the current entry. * * @return bool */ public function set_entry_not_spam( array $form_data, array $entry ) { if ( ! self::is_configured() ) { return false; } $request = $this->get_request_args( $form_data, $entry ); $response = $this->http_post( $request, 'submit-ham' ); // Yes, Akismet returns "Thanks for making the web a better place." as the response. return ! empty( $response ) && isset( $response[1] ) && 'Thanks for making the web a better place.' === trim( $response[1] ); } /** * Mark the entry as spam in Akismet. * * @since 1.8.9 * * @param array $form_data Form data for the current form. * @param array $entry Entry data for the current entry. * * @return bool */ public function submit_missed_spam( array $form_data, array $entry ) { if ( ! self::is_configured() ) { return false; } $request = $this->get_request_args( $form_data, $entry ); $response = $this->http_post( $request, 'submit-spam' ); // Yes, Akismet returns "Thanks for making the web a better place." as the response. return ! empty( $response ) && isset( $response[1] ) && 'Thanks for making the web a better place.' === trim( $response[1] ); } /** * Get the request arguments to be sent to Akismet. * * @since 1.8.8 * * @param array $form_data Form data for the current form. * @param array $entry Entry data for the current entry. * * @return array $request_args Request arguments to be sent to Akismet. */ private function get_request_args( $form_data, $entry ) { $entry_data = $this->get_entry_data( $form_data['fields'], $entry ); $entry_id = $entry['entry_id'] ?? null; // We can't use certain real-time functions when the entry is marked as not spam. // In this case, we need to use the smart tag value. if ( ! empty( $entry_id ) ) { $page_url = wpforms_process_smart_tags( '{page_url}', $form_data, [], $entry_id, 'akismet-request-args' ); $url_referer = wpforms_process_smart_tags( '{url_referer}', $form_data, [], $entry_id, 'akismet-request-args' ); $user_id = wpforms_process_smart_tags( '{user_id}', $form_data, [], $entry_id, 'akismet-request-args' ); $user_ip = wpforms_process_smart_tags( '{user_ip}', $form_data, [], $entry_id, 'akismet-request-args' ); $user_agent = ''; } else { $page_url = wpforms_current_url(); $url_referer = wp_get_referer(); $user_id = get_current_user_id(); $user_ip = wpforms_get_ip(); $user_agent = isset( $_SERVER['HTTP_USER_AGENT'] ) ? wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized } return [ 'blog' => get_option( 'home' ), 'blog_lang' => get_locale(), 'blog_charset' => get_bloginfo( 'charset' ), 'permalink' => $page_url, 'user_ip' => wpforms_is_collecting_ip_allowed( $form_data ) ? $user_ip : '', 'user_id' => $user_id, 'user_role' => AkismetPlugin::get_user_roles( $user_id ), 'user_agent' => $user_agent, 'referrer' => $url_referer ? $url_referer : '', 'comment_type' => 'contact-form', 'comment_author' => $entry_data['name'] ?? '', 'comment_author_email' => $entry_data['email'] ?? '', 'comment_author_url' => $entry_data['url'] ?? '', 'comment_content' => $entry_data['content'] ?? '', 'honeypot_field_name' => 'wpforms[hp]', ]; } /** * Send a POST request to the Akismet API. * * @since 1.8.8 * * @param array $request Request arguments to be sent to Akismet. * @param string $path API path. * * @return array */ private function http_post( $request, $path ) { // build_query() does not urlencode the values, but API explicitly requires it. $request = array_map( 'urlencode', $request ); return AkismetPlugin::http_post( build_query( $request ), $path ); } /** * Validate entry. * * @since 1.7.6 * * @param array $form_data Form data for the current form. * @param array $entry Entry data for the current entry. * * @return string|bool */ public function validate( array $form_data, array $entry ) { // If Akismet is turned on in form settings, is activated, is configured and the entry is spam. if ( ! empty( $form_data['settings']['akismet'] ) && self::is_configured() && $this->entry_is_spam( $form_data, $entry ) ) { // This string is being logged not printed, so it does not need to be translatable. return esc_html__( 'Anti-spam verification failed, please try again later.', 'wpforms-lite' ); } return false; } } Forms/IconChoices.php 0000644 00000036545 15174710275 0010557 0 ustar 00 <?php namespace WPForms\Forms; use WPForms\Helpers\PluginSilentUpgrader; use WPForms_Builder; use WP_Ajax_Upgrader_Skin; /** * Icon Choices functionality. * * @since 1.7.9 */ class IconChoices { /** * Remote URL to download the icon library from. * * @since 1.7.9 * * @var string */ const FONT_AWESOME_URL = 'https://wpforms.com/wp-content/icon-choices.zip'; /** * Font Awesome version. * * @since 1.7.9 * * @var string */ const FONT_AWESOME_VERSION = '6.4.0'; /** * Default icon. * * @since 1.7.9 * * @var string */ const DEFAULT_ICON = 'face-smile'; /** * Default icon style. * * @since 1.7.9 * * @var string */ const DEFAULT_ICON_STYLE = 'regular'; /** * Default accent color. * * @since 1.7.9 * * @var string */ const DEFAULT_COLOR = [ 'classic' => '#0399ed', 'modern' => '#066aab', ]; /** * How many icons to display initially and paginate in the Icon Picker. * * @since 1.7.9 * * @var int */ const DEFAULT_ICONS_PER_PAGE = 50; /** * Absolute path to the cache directory. * * @since 1.7.9 * * @var string */ private $cache_base_path; /** * Cache directory URL. * * @since 1.7.9 * * @var string */ private $cache_base_url; /** * Absolute path to the icons data file. * * @since 1.7.9 * * @var string */ private $icons_data_file; /** * Whether icon library is already installed. * * @since 1.7.9 * * @var bool */ private $is_installed; /** * Default list of icon sizes. * * @since 1.7.9 * * @var array */ private $default_icon_sizes; /** * Initialize class. * * @since 1.7.9 */ public function init() { $upload_dir = wpforms_upload_dir(); $this->cache_base_url = $upload_dir['url'] . '/icon-choices'; $this->cache_base_path = $upload_dir['path'] . '/icon-choices'; $this->icons_data_file = $this->cache_base_path . '/icons.json'; $this->default_icon_sizes = [ 'large' => [ 'label' => __( 'Large', 'wpforms-lite' ), 'size' => 64, ], 'medium' => [ 'label' => __( 'Medium', 'wpforms-lite' ), 'size' => 48, ], 'small' => [ 'label' => __( 'Small', 'wpforms-lite' ), 'size' => 32, ], ]; $this->hooks(); } /** * Hook into WordPress lifecycle. * * @since 1.7.9 */ private function hooks() { // Add inline CSS with custom properties on the frontend. add_action( 'wpforms_frontend_css', [ $this, 'css_custom_properties' ] ); // Add inline CSS with custom properties in the form builder. if ( wpforms_is_admin_page( 'builder' ) ) { add_action( 'admin_head', [ $this, 'css_custom_properties' ] ); } // Load Font Awesome assets. add_action( 'wpforms_builder_enqueues', [ $this, 'enqueues' ] ); // Send data to the frontend. add_filter( 'wpforms_builder_strings', [ $this, 'get_strings' ], 10, 2 ); // Download and extract Font Awesome package. add_action( 'wp_ajax_wpforms_icon_choices_install', [ $this, 'install' ] ); } /** * Get Font Awesome library data file. * * @since 1.8.3 * * @return string */ public function get_icons_data_file() { return $this->icons_data_file; } /** * Whether Font Awesome library is already installed or not. * * @since 1.7.9 * * @return bool */ private function is_installed() { if ( $this->is_installed !== null ) { return $this->is_installed; } $this->is_installed = file_exists( $this->icons_data_file ); return $this->is_installed; } /** * Whether Icon Choices mode is active on any of the fields in current form. * * @since 1.7.9 * * @return bool */ private function is_active() { $form_data = WPForms_Builder::instance()->form_data; return wpforms_has_field_setting( 'choices_icons', $form_data, false ); } /** * Install Font Awesome library via Ajax. * * @since 1.7.9 */ public function install() { // Run a security check. check_ajax_referer( 'wpforms-builder', 'nonce' ); // Check for permissions. if ( ! wpforms_current_user_can( 'edit_forms' ) ) { wp_send_json_error(); } $this->run_install( $this->cache_base_path ); $this->is_installed = true; wp_send_json_success(); } /** * Run Install Font Awesome library from our server. * * @since 1.8.3 * * @param string $destination Destination path. */ public function run_install( $destination ) { // phpcs:ignore WPForms.PHP.HooksMethod.InvalidPlaceForAddingHooks // WordPress assumes it's a plugin/theme and tries to get translations. We don't need that, and it breaks JS output. remove_action( 'upgrader_process_complete', [ 'Language_Pack_Upgrader', 'async_upgrade' ], 20 ); if ( ! function_exists( 'request_filesystem_credentials' ) ) { require_once ABSPATH . 'wp-admin/includes/file.php'; } // Create the Upgrader with our custom skin that reports errors as WP JSON. $installer = new PluginSilentUpgrader( new WP_Ajax_Upgrader_Skin() ); // The installer skin reports any errors via wp_send_json_error() with generic error messages. $installer->init(); $installer->run( [ 'package' => self::FONT_AWESOME_URL, 'destination' => $destination, ] ); } /** * Load all necessary Font Awesome assets. * * @since 1.7.9 * * @param string $view Current Form Builder view (panel). */ public function enqueues( $view ) { if ( ! $this->is_installed() ) { return; } wp_enqueue_style( 'wpforms-icon-choices-font-awesome', $this->cache_base_url . '/css/fontawesome.min.css', [], self::FONT_AWESOME_VERSION ); wp_enqueue_style( 'wpforms-icon-choices-font-awesome-brands', $this->cache_base_url . '/css/brands.min.css', [], self::FONT_AWESOME_VERSION ); wp_enqueue_style( 'wpforms-icon-choices-font-awesome-regular', $this->cache_base_url . '/css/regular.min.css', [], self::FONT_AWESOME_VERSION ); wp_enqueue_style( 'wpforms-icon-choices-font-awesome-solid', $this->cache_base_url . '/css/solid.min.css', [], self::FONT_AWESOME_VERSION ); } /** * Define additional field properties specific to Icon Choices feature. * * @since 1.7.9 * * @see WPForms_Field_Checkbox::field_properties() * @see WPForms_Field_Radio::field_properties() * @see WPForms_Field_Payment_Checkbox::field_properties() * @see WPForms_Field_Payment_Multiple::field_properties() * * @param array $properties Field properties. * @param array $field Field settings. * * @return array */ public function field_properties( $properties, $field ) { $properties['input_container']['class'][] = 'wpforms-icon-choices'; $properties['input_container']['class'][] = sanitize_html_class( 'wpforms-icon-choices-' . $field['choices_icons_style'] ); $properties['input_container']['class'][] = sanitize_html_class( 'wpforms-icon-choices-' . $field['choices_icons_size'] ); $icon_color = isset( $field['choices_icons_color'] ) ? wpforms_sanitize_hex_color( $field['choices_icons_color'] ) : ''; $icon_color = empty( $icon_color ) ? self::get_default_color() : $icon_color; $properties['input_container']['attr']['style'] = "--wpforms-icon-choices-color: {$icon_color};"; foreach ( $properties['inputs'] as $key => $inputs ) { $properties['inputs'][ $key ]['container']['class'][] = 'wpforms-icon-choices-item'; if ( in_array( $field['choices_icons_style'], [ 'default', 'modern', 'classic' ], true ) ) { $properties['inputs'][ $key ]['class'][] = 'wpforms-screen-reader-element'; } } return $properties; } /** * Display a single choice on the form front-end. * * @since 1.7.9 * * @see WPForms_Field_Checkbox::field_display() * @see WPForms_Field_Radio::field_display() * @see WPForms_Field_Payment_Checkbox::field_display() * @see WPForms_Field_Payment_Multiple::field_display() * * @param array $field Field settings. * @param array $choice Single choice item settings. * @param string $type Field input type. * @param string|null $label Custom label, used by Payment fields. */ public function field_display( $field, $choice, $type, $label = null ) { // Only Payment fields supply a custom label. if ( ! $label ) { $label = $choice['label']['text']; } if ( is_array( $choice['label']['class'] ) && wpforms_is_empty_string( $label ) ) { $choice['label']['class'][] = 'wpforms-field-label-inline-empty'; } printf( '<label %1$s> <span class="wpforms-icon-choices-icon"> %2$s <span class="wpforms-icon-choices-icon-bg"></span> </span> <input type="%3$s" %4$s %5$s %6$s> <span class="wpforms-icon-choices-label">%7$s</span> </label>', wpforms_html_attributes( $choice['label']['id'], $choice['label']['class'], $choice['label']['data'], $choice['label']['attr'] ), // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped $this->get_icon( $choice['icon'], $choice['icon_style'], $field['choices_icons_size'] ), esc_attr( $type ), wpforms_html_attributes( $choice['id'], $choice['class'], $choice['data'], $choice['attr'] ), esc_attr( $choice['required'] ), checked( '1', $choice['default'], false ), wp_kses_post( $label ) ); } /** * Output inline CSS custom properties (vars). * * @since 1.7.9 * * @param null|array $forms Frontend forms, if available. * * @return void */ public function css_custom_properties( $forms = null ) { $hook = current_action(); // On the frontend, we need these properties only if Icon Choices is in use. if ( $hook === 'wpforms_frontend_css' && ! wpforms_has_field_setting( 'choices_icons', $forms, true ) ) { return; } $selectors = [ 'wpforms_frontend_css' => '.wpforms-container', 'admin_head' => '#wpforms-builder, .wpforms-icon-picker-container', ]; /** * Add CSS custom properties. * * @since 1.7.9 * * @param array $properties CSS custom properties using CSS syntax. */ $custom_properties = (array) apply_filters( 'wpforms_forms_icon_choices_css_custom_properties', [] ); $icon_sizes = $this->get_icon_sizes(); foreach ( $icon_sizes as $slug => $data ) { $custom_properties[ "wpforms-icon-choices-size-{$slug}" ] = $data['size'] . 'px'; } $custom_properties_css = ''; foreach ( $custom_properties as $property => $value ) { $custom_properties_css .= "--{$property}: {$value};"; } printf( '<style id="wpforms-icon-choices-custom-properties">%s { %s }</style>', esc_attr( $selectors[ $hook ] ), esc_html( $custom_properties_css ) ); } /** * Get available icon sizes. * * @since 1.7.9 * * @return array A list of all icon sizes. */ public function get_icon_sizes() { /** * Allow modifying the icon sizes. * * @since 1.7.9 * * @param array $icon_sizes { * Default icon sizes. * * @type string $key The icon slug. * @type array $value { * Individual icon size data. * * @type string $label Translatable label. * @type int $size The size value. * } * } * @param array $default_icon_sizes Default icon sizes for reference. */ $sizes = (array) apply_filters( 'wpforms_forms_icon_choices_get_icon_sizes', [], $this->default_icon_sizes ); return array_merge( $this->default_icon_sizes, $sizes ); } /** * Read icons metadata from disk. * * @since 1.7.9 * * @param array $strings Strings and values sent to the frontend. * @param array $form Current form. * * @return array */ public function get_strings( $strings, $form ) { $strings['continue'] = esc_html__( 'Continue', 'wpforms-lite' ); $strings['done'] = esc_html__( 'Done!', 'wpforms-lite' ); $strings['uh_oh'] = esc_html__( 'Uh oh!', 'wpforms-lite' ); $strings['icon_choices'] = [ 'is_installed' => false, 'is_active' => $this->is_active(), 'default_icon' => self::DEFAULT_ICON, 'default_icon_style' => self::DEFAULT_ICON_STYLE, 'default_color' => self::get_default_color(), 'icons' => [], 'icons_per_page' => self::DEFAULT_ICONS_PER_PAGE, 'strings' => [ 'install_prompt_content' => esc_html__( 'In order to use the Icon Choices feature, an icon library must be downloaded and installed. It\'s quick and easy, and you\'ll only have to do this once.', 'wpforms-lite' ), 'install_title' => esc_html__( 'Installing Icon Library', 'wpforms-lite' ), 'install_content' => esc_html__( 'This should only take a minute. Please don’t close or reload your browser window.', 'wpforms-lite' ), 'install_success_content' => esc_html__( 'The icon library has been installed successfully. We will now save your form and reload the form builder.', 'wpforms-lite' ), 'install_error_content' => wp_kses( sprintf( /* translators: %s - WPForms Support URL. */ __( 'There was an error installing the icon library. Please try again later or <a href="%s" target="_blank" rel="noreferrer noopener">contact support</a> if the issue persists.', 'wpforms-lite' ), esc_url( wpforms_utm_link( 'https://wpforms.com/account/support/', 'builder-modal', 'Icon Library Install Failure' ) ) ), [ 'a' => [ 'href' => true, 'target' => true, 'rel' => true, ], ] ), 'reinstall_prompt_content' => esc_html__( 'The icon library appears to be missing or damaged. It will now be reinstalled.', 'wpforms-lite' ), 'icon_picker_title' => esc_html__( 'Icon Picker', 'wpforms-lite' ), 'icon_picker_description' => esc_html__( 'Browse or search for the perfect icon.', 'wpforms-lite' ), 'icon_picker_search_placeholder' => esc_html__( 'Search 2000+ icons...', 'wpforms-lite' ), 'icon_picker_not_found' => esc_html__( 'Sorry, we didn\'t find any matching icons.', 'wpforms-lite' ), ], ]; if ( ! $this->is_installed() ) { return $strings; } $strings['icon_choices']['is_installed'] = true; $strings['icon_choices']['icons'] = $this->get_icons(); return $strings; } /** * Get an SVG icon code from a file for inline output in HTML. * * Note: the output does not need to escape. * * @since 1.7.9 * * @param string $icon Font Awesome icon name. * @param string $style Font Awesome style (solid, brands). * @param string|int $size Icon display size. * * @return string */ private function get_icon( string $icon, string $style, $size ): string { $size = sanitize_key( (string) $size ); $icon_sizes = $this->get_icon_sizes(); $size = ! empty( $icon_sizes[ $size ]['size'] ) ? (int) $icon_sizes[ $size ]['size'] : (int) $icon_sizes['large']['size']; return wpforms_get_icon_svg( $icon, $style, $size ); } /** * Get all available icons from the metadata file. * * @since 1.7.9 * * @return array */ private function get_icons() { if ( ! is_file( $this->icons_data_file ) || ! is_readable( $this->icons_data_file ) ) { return []; } // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents $icons = file_get_contents( $this->icons_data_file ); if ( ! $icons ) { return []; } return (array) json_decode( $icons, false ); } /** * Get default accent color. * * @since 1.8.1 * * @return string */ public static function get_default_color() { $render_engine = wpforms_get_render_engine(); return array_key_exists( $render_engine, self::DEFAULT_COLOR ) ? self::DEFAULT_COLOR[ $render_engine ] : self::DEFAULT_COLOR['modern']; } } Forms/Submission.php 0000644 00000015620 15174710275 0010513 0 ustar 00 <?php namespace WPForms\Forms; /** * Class Submission. * * @since 1.7.4 */ class Submission { /** * The form fields. * * @since 1.7.4 * * @var array */ protected $fields; /** * The form entry. * * @since 1.7.4 * * @var array */ private $entry; /** * The form ID. * * @since 1.7.4 * * @var int */ private $form_id; /** * The form data. * * @since 1.7.4 * * @var array */ protected $form_data; /** * The date. * * @since 1.7.4 * * @var string */ private $date; /** * Register the submission data. * * @since 1.7.4 * @since 1.8.2 Added a return of instance. * * @param array $fields The form fields. * @param array $entry The form entry. * @param int $form_id The form ID. * @param array $form_data The form data. * * @return Submission */ public function register( array $fields, array $entry, $form_id, array $form_data = [] ) { $this->fields = $fields; $this->entry = $entry; $this->form_id = $form_id; $this->form_data = $form_data; $this->date = gmdate( 'Y-m-d H:i:s' ); return $this; } /** * Prepare the submission data. * * @since 1.7.4 * * @return array|void */ public function prepare_entry_data() { /** * Provide the opportunity to disable entry saving. * * @since 1.0.0 * * @param bool $entry_save Entry save flag. Defaults to true. * @param array $fields Fields data. * @param array $entry Entry data. * @param array $form_data Form data. */ if ( ! apply_filters( 'wpforms_entry_save', true, $this->fields, $this->entry, $this->form_data ) ) { // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName return; } $submitted_fields = $this->get_fields(); $user_info = $this->get_user_info( $submitted_fields ); /** * Information about the entry, that is ready to be saved into the main entries table, * which is used for displaying a list of entries and partially for search. * * @since 1.5.9 * * @param array $entry_data Information about the entry, that will be saved into the DB. * @param array $form_data Form data. */ return (array) apply_filters( // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName 'wpforms_entry_save_args', [ 'form_id' => absint( $this->form_id ), 'user_id' => absint( $user_info['user_id'] ), 'fields' => wp_json_encode( $submitted_fields ), 'ip_address' => sanitize_text_field( $user_info['user_ip'] ), 'user_agent' => sanitize_text_field( $user_info['user_agent'] ), 'date' => $this->date, 'user_uuid' => sanitize_text_field( $user_info['user_uuid'] ), ], $this->form_data ); } /** * Prepare the payment submission data. * * @since 1.8.2 * * @return array */ public function prepare_payment_data() { $submitted_fields = $this->get_fields(); $total_amount = wpforms_get_total_payment( $submitted_fields ); /** * Information about the payment, that is ready to be saved into the main payments table, * which is used for displaying a list of payments and partially for search. * * @since 1.8.2 * * @param array $payment_data Information about the payment, that will be saved into the DB. * @param array $fields Final/sanitized submitted field data. * @param array $form_data Form data and settings. */ $payment_data = (array) apply_filters( 'wpforms_forms_submission_prepare_payment_data', [ 'form_id' => absint( $this->form_id ), 'subtotal_amount' => $total_amount, 'total_amount' => $total_amount, 'currency' => wpforms_get_currency(), 'entry_id' => absint( $this->entry['entry_id'] ), 'date_created_gmt' => $this->date, 'date_updated_gmt' => $this->date, ], $submitted_fields, $this->form_data ); if ( empty( $payment_data['type'] ) ) { $payment_data['type'] = ! empty( $payment_data['subscription_id'] ) ? 'subscription' : 'one-time'; } return $payment_data; } /** * Prepare the payment meta data for each payment. * * @since 1.8.2 * * @return array */ public function prepare_payment_meta() { $submitted_fields = $this->get_fields(); $user_info = $this->get_user_info( $submitted_fields ); /** * Payment meta that is ready to be saved into the payments_meta table. * * @since 1.8.2 * * @param array $payment_meta Payment meta that will be saved into the DB. * @param array $fields Final/sanitized submitted field data. * @param array $form_data Form data and settings. */ return (array) apply_filters( 'wpforms_forms_submission_prepare_payment_meta', [ 'fields' => ! $this->entry['entry_id'] ? wp_json_encode( $submitted_fields ) : '', 'user_id' => absint( $user_info['user_id'] ), 'user_agent' => sanitize_text_field( $user_info['user_agent'] ), 'user_uuid' => sanitize_text_field( $user_info['user_uuid'] ), 'ip_address' => sanitize_text_field( $user_info['user_ip'] ), ], $submitted_fields, $this->form_data ); } /** * Get entry fields. * * @since 1.8.2 * * @return array */ private function get_fields() { /** * Filter the entry data before saving. * * @since 1.0.0 * * @param array $fields Fields data. * @param array $entry Entry data. * @param array $form_data Form data. */ return (array) apply_filters( 'wpforms_entry_save_data', $this->fields, $this->entry, $this->form_data ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName } /** * Get user info. * * @since 1.8.2 * * @param array $fields Fields data. * * @return array */ private function get_user_info( $fields ) { $user_info = [ 'user_ip' => '', 'user_agent' => '', 'user_id' => is_user_logged_in() ? get_current_user_id() : 0, 'user_uuid' => wpforms_is_collecting_cookies_allowed() && ! empty( $_COOKIE['_wpfuuid'] ) ? sanitize_key( $_COOKIE['_wpfuuid'] ) : '', ]; /** * Allow developers disable saving user IP and User Agent within the entry. * * @since 1.5.1 * * @param bool $disable True if you need to disable storing IP and UA within the entry. Defaults to false. * @param array $fields Fields data. * @param array $form_data Form data. */ // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName $is_ip_disabled = apply_filters( 'wpforms_disable_entry_user_ip', '__return_false', $fields, $this->form_data ); // If GDPR enhancements are enabled and user details are disabled // globally or in the form settings, discard the IP and UA. if ( ! $is_ip_disabled || ! wpforms_is_collecting_ip_allowed( $this->form_data ) ) { return $user_info; } $user_info['user_ip'] = wpforms_get_ip(); if ( empty( $_SERVER['HTTP_USER_AGENT'] ) ) { return $user_info; } $user_info['user_agent'] = substr( sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ), 0, 256 ); return $user_info; } } Forms/Token.php 0000644 00000025046 15174710275 0007443 0 ustar 00 <?php namespace WPForms\Forms; /** * Class Token. * * This token class generates tokens that are used in our Anti-Spam checking mechanism. * * @since 1.6.2 */ class Token { /** * Initialise the actions for the Anti-spam. * * @since 1.6.2 */ public function init() { $this->hooks(); } /** * Register hooks. * * @since 1.6.2 */ public function hooks() { add_filter( 'wpforms_frontend_form_atts', [ $this, 'add_token_to_form_atts' ], 10, 2 ); add_filter( 'wpforms_frontend_strings', [ $this, 'add_frontend_strings' ] ); add_action( 'wp_ajax_nopriv_wpforms_get_token', [ $this, 'ajax_get_token' ] ); add_action( 'wp_ajax_wpforms_get_token', [ $this, 'ajax_get_token' ] ); } /** * Return a valid token. * * @since 1.6.2 * @since 1.7.1 Added the $form_data argument. * * @param mixed $current True to use current time, otherwise a timestamp string. * @param array $form_data Form data and settings. * * @return string Token. */ public function get( $current = true, $form_data = [] ) { // If $current was not passed, or it is true, we use the current timestamp. // If $current was passed in as a string, we'll use that passed in timestamp. if ( $current !== true ) { $time = $current; } else { $time = time(); } // Format the timestamp to be less exact, as we want to deal in days. // June 19th, 2020 would get formatted as: 1906202017125. // Day of the month, month number, year, day number of the year, week number of the year. $token_data = gmdate( 'dmYzW', $time ); if ( ! empty( $form_data['id'] ) ) { $token_data .= "::{$form_data['id']}"; } // Combine our token date and our token salt, and md5 it. return md5( $token_data . \WPForms\Helpers\Crypto::get_secret_key() ); } /** * Generate the array of valid tokens to check for. These include two days * before the current date to account for long cache times. * * These two filters are available if a user wants to extend the times. * 'wpforms_form_token_check_before_today' * 'wpforms_form_token_check_after_today' * * @since 1.6.2 * @since 1.7.1 Added the $form_data argument. * * @param array $form_data Form data and settings. * * @return array Array of all valid tokens to check against. */ public function get_valid_tokens( $form_data = [] ) { $current_date = time(); $valid_token_times_before = []; $days_in_5_years = 5 * 365; // Create an array of 5 years worth of days. for ( $i = 1; $i <= $days_in_5_years; $i++ ) { $valid_token_times_before[] = $i * DAY_IN_SECONDS; } // Create our array of times to check before today. A user with a longer // cache time can extend this. A user with a shorter cache time can remove times. $valid_token_times_before = apply_filters( 'wpforms_form_token_check_before_today', $valid_token_times_before ); // Mostly to catch edge cases like the form page loading and submitting on two different days. // This probably won't be filtered by users too much, but they could extend it. $valid_token_times_after = apply_filters( 'wpforms_form_token_check_after_today', [ ( 45 * MINUTE_IN_SECONDS ), // Add in 45 minutes past today to catch some midnight edge cases. ] ); // Built up our valid tokens. $valid_tokens = []; // Add in all the previous times we check. foreach ( $valid_token_times_before as $time ) { $valid_tokens[] = $this->get( $current_date - $time, $form_data ); } // Add in our current date. $valid_tokens[] = $this->get( $current_date, $form_data ); // Add in the times after our check. foreach ( $valid_token_times_after as $time ) { $valid_tokens[] = $this->get( $current_date + $time, $form_data ); } return $valid_tokens; } /** * Check if the given token is valid or not. * * Tokens are valid for some period of time (see wpforms_token_validity_in_hours * and wpforms_token_validity_in_days to extend the validation period). * By default tokens are valid for day. * * @since 1.6.2 * @since 1.7.1 Added the $form_data argument. * * @param string $token Token to validate. * @param array $form_data Form data and settings. * * @return bool Whether the token is valid or not. */ public function verify( string $token, array $form_data = [] ): bool { // Check to see if our token is inside the valid tokens. return in_array( $token, $this->get_valid_tokens( $form_data ), true ); } /** * Add the token to the form attributes. * * @since 1.6.2 * @since 1.7.1 Added the $form_data argument. * * @param array $attrs Form attributes. * @param array $form_data Form data and settings. * * @return array Form attributes. */ public function add_token_to_form_atts( array $attrs, array $form_data ) { $attrs['atts']['data-token'] = $this->get( true, $form_data ); $attrs['atts']['data-token-time'] = time(); return $attrs; } /** * Validate Anti-spam if enabled. * * @since 1.6.2 * * @param array $form_data Form data. * @param array $fields Fields. * @param array $entry Form entry. * * @return bool|string True or a string with the error. */ public function validate( array $form_data, array $fields, array $entry ) { // Bail out if we don't have the antispam setting. if ( empty( $form_data['settings']['antispam'] ) ) { return true; } // Bail out if the antispam setting isn't enabled. if ( $form_data['settings']['antispam'] !== '1' ) { return true; } $is_valid_token = isset( $entry['token'] ) && $this->verify( (string) $entry['token'], $form_data ); if ( $this->process_antispam_filter_wrapper( $is_valid_token, $fields, $entry, $form_data ) ) { return true; } // Prepare the log data. $form_title = $form_data['settings']['form_title'] ?? ''; $form_id = $form_data['id'] ?? 'unknown'; if ( $is_valid_token ) { // Token is OK, but antispam filter is not passed. $log_message = 'Filter is not passed'; $error_message = $this->get_antispam_filter_message(); } else { // Invalid token. $log_message = 'Token is invalid'; $error_message = $this->get_invalid_token_message(); } wpforms_log( 'Antispam: ' . $log_message, [ 'message' => $error_message, 'referer' => esc_url_raw( (string) wp_get_referer() ), 'form' => ! empty( $form_title ) ? $form_title . ' (ID: ' . $form_id . ')' : 'ID: ' . $form_id, 'token' => $entry['token'] ?? '', 'user_ip' => wpforms_get_ip(), 'entry_data' => ! wpforms_setting( 'gdpr' ) ? $entry : 'Not logged', ], [ 'type' => [ 'spam', 'error' ], 'form_id' => $form_data['id'], 'force' => true, ] ); return $error_message; } /** * Helper to run our filter on all the responses for the antispam checks. * * @since 1.6.2 * * @param bool $is_valid_not_spam Is valid entry or not. * @param array $fields Form Fields. * @param array $entry Form entry. * @param array $form_data Form Data. * * @return bool Is valid or not. */ public function process_antispam_filter_wrapper( bool $is_valid_not_spam, array $fields, array $entry, array $form_data ): bool { /** * Allows developers to filter the antispam check result. * * @since 1.6.2 * * @param bool $is_valid_not_spam True if entry valid, false otherwise. * @param array $fields Fields data. * @param array $entry Entry data. * @param array $form_data Form data. */ return (bool) apply_filters( 'wpforms_process_antispam', $is_valid_not_spam, $fields, $entry, $form_data ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName } /** * Helper to get the invalid token message. * * @since 1.6.2.1 * * @return string Invalid token message. */ private function get_invalid_token_message(): string { return $this->get_error_message( esc_html__( 'Antispam token is invalid.', 'wpforms-lite' ) ); } /** * Helper to get the antispam filter error message. * * @since 1.8.9 * * @return string Missing token message. */ private function get_antispam_filter_message(): string { return $this->get_error_message( esc_html__( 'Antispam filter did not allow your data to pass through.', 'wpforms-lite' ) ); } /** * Get error message depends on user. * * @since 1.6.4.1 * * @param string $text Message text. * * @return string */ private function get_error_message( string $text ): string { $text .= ' ' . esc_html__( 'Please reload the page and try submitting the form again.', 'wpforms-lite' ); return wpforms_current_user_can() ? $text . $this->maybe_get_support_text() : $text; } /** * If a user is a super admin, add a support link to the message. * * @since 1.6.2.1 * * @return string Support text if super admin, empty string if not. */ private function maybe_get_support_text(): string { // If a user isn't a super admin, don't return any text. if ( ! is_super_admin() ) { return ''; } // If the user is an admin, return text with a link to support. // We add a space here to separate the sentences, but outside the localized text to avoid it being removed. return ' ' . sprintf( /* translators: placeholders are links. */ esc_html__( 'Please check out our %1$stroubleshooting guide%2$s for details on resolving this issue.', 'wpforms-lite' ), '<a href="https://wpforms.com/docs/getting-support-wpforms/">', '</a>' ); } /** * Add token related strings to the frontend. * * @since 1.8.8 * * @param array|mixed $strings Frontend strings. * * @return array Frontend strings. */ public function add_frontend_strings( $strings ): array { $strings = (array) $strings; $strings['error_updating_token'] = esc_html__( 'Error updating token. Please try again or contact support if the issue persists.', 'wpforms-lite' ); $strings['network_error'] = esc_html__( 'Network error or server is unreachable. Check your connection or try again later.', 'wpforms-lite' ); // Default token lifetime is 24 hours in seconds. $token_lifetime = DAY_IN_SECONDS; /** * Filter token cache lifetime in seconds. * * @since 1.8.8 * * @param integer $token_lifetime Token lifetime in seconds. */ $strings['token_cache_lifetime'] = apply_filters( 'wpforms_forms_token_cache_lifetime', $token_lifetime ); return $strings; } /** * Update token via ajax handler. * * @since 1.8.8 */ public function ajax_get_token() { $form_data = []; $form_data['id'] = filter_input( INPUT_POST, 'formId', FILTER_VALIDATE_INT ); $response = [ 'token' => $this->get( true, $form_data ), ]; wp_send_json_success( $response ); } } Forms/Locator.php 0000644 00000101016 15174710275 0007756 0 ustar 00 <?php // phpcs:disable Generic.Commenting.DocComment.MissingShort /** @noinspection PhpUnnecessaryCurlyVarSyntaxInspection */ // phpcs:enable Generic.Commenting.DocComment.MissingShort namespace WPForms\Forms; use WP_Post; use WPForms\Tasks\Actions\FormsLocatorScanTask; /** * Class Locator. * * @since 1.7.4 */ class Locator { /** * Column name on Forms Overview admin page. * * @since 1.7.4 */ const COLUMN_NAME = 'locations'; /** * Locations meta key. * * @since 1.7.4 */ const LOCATIONS_META = 'wpforms_form_locations'; /** * WPForms widget name. * * @since 1.7.4 */ const WPFORMS_WIDGET_NAME = 'wpforms-widget'; /** * WPForms widget prefix. * * @since 1.7.4 */ const WPFORMS_WIDGET_PREFIX = self::WPFORMS_WIDGET_NAME . '-'; /** * WPForms widgets option name. * * @since 1.7.4 */ const WPFORMS_WIDGET_OPTION = 'widget_' . self::WPFORMS_WIDGET_NAME; /** * Text widget name. * * @since 1.7.4 */ const TEXT_WIDGET_NAME = 'text'; /** * Text widget prefix. * * @since 1.7.4 */ const TEXT_WIDGET_PREFIX = self::TEXT_WIDGET_NAME . '-'; /** * Text widgets option name. * * @since 1.7.4 */ const TEXT_WIDGET_OPTION = 'widget_' . self::TEXT_WIDGET_NAME; /** * Block widget name. * * @since 1.7.4 */ const BLOCK_WIDGET_NAME = 'block'; /** * Block widget prefix. * * @since 1.7.4 */ const BLOCK_WIDGET_PREFIX = self::BLOCK_WIDGET_NAME . '-'; /** * Block widgets' option name. * * @since 1.7.4 */ const BLOCK_WIDGET_OPTION = 'widget_' . self::BLOCK_WIDGET_NAME; /** * Location type for widget. * For a page/post, the location type is the post type. * * @since 1.7.4 */ const WIDGET = 'widget'; /** * WP template post type. * * @since 1.7.4 */ const WP_TEMPLATE = 'wp_template'; /** * WP template post type. * * @since 1.7.4.1 */ const WP_TEMPLATE_PART = 'wp_template_part'; /** * Standalone location types. * * @since 1.8.7 */ const STANDALONE_LOCATION_TYPES = [ 'form_pages', 'conversational_forms' ]; /** * Default title for WPForms widget. * For WPForms widget, we extract title from the widget. If it is empty, we use the default one. * * @since 1.7.4 * * @var string */ private $wpforms_widget_title = ''; /** * Default title for text widget. * For text widget, we extract title from the widget. If it is empty, we use the default one. * * @since 1.7.4 * * @var string */ private $text_widget_title = ''; /** * Fixed title for block widget. * * @since 1.7.4 * * @var string */ private $block_widget_title = ''; /** * Home url. * * @since 1.7.4 * * @var string */ private $home_url; /** * Scan status. * * @since 1.7.4 * * @var string */ private $scan_status; /** * Init class. * * @since 1.7.4 */ public function init() { $this->home_url = home_url(); $this->scan_status = (string) get_option( FormsLocatorScanTask::SCAN_STATUS ); $this->wpforms_widget_title = __( 'WPForms Widget', 'wpforms-lite' ); $this->text_widget_title = __( 'Text Widget', 'wpforms-lite' ); $this->block_widget_title = __( 'Block Widget', 'wpforms-lite' ); $this->hooks(); } /** * Register hooks. * * @since 1.7.4 */ private function hooks() { // View hooks. add_filter( 'wpforms_admin_forms_table_facades_columns_data', [ $this, 'add_column_data' ] ); add_filter( 'wpforms_overview_table_column_value', [ $this, 'column_value' ], 10, 3 ); add_filter( 'wpforms_overview_row_actions', [ $this, 'row_actions_all' ], 10, 2 ); add_action( 'wpforms_overview_enqueue', [ $this, 'localize_overview_script' ] ); // Monitoring hooks. add_action( 'save_post', [ $this, 'save_post' ], 10, 3 ); add_action( 'post_updated', [ $this, 'post_updated' ], 10, 3 ); add_action( 'wp_trash_post', [ $this, 'trash_post' ] ); add_action( 'untrash_post', [ $this, 'untrash_post' ] ); add_action( 'delete_post', [ $this, 'trash_post' ] ); add_action( 'permalink_structure_changed', [ $this, 'permalink_structure_changed' ], 10, 2 ); $wpforms_widget_option = self::WPFORMS_WIDGET_OPTION; $text_widget_option = self::TEXT_WIDGET_OPTION; $block_widget_option = self::BLOCK_WIDGET_OPTION; add_action( "update_option_{$wpforms_widget_option}" , [ $this, 'update_option' ], 10, 3 ); add_action( "update_option_{$text_widget_option}" , [ $this, 'update_option' ], 10, 3 ); add_action( "update_option_{$block_widget_option}", [ $this, 'update_option' ], 10, 3 ); } /** * Add locations' column to the table columns data. * * @since 1.8.6 * * @param array|mixed $columns Columns data. * * @return array */ public function add_column_data( $columns ): array { $columns = (array) $columns; $columns[ self::COLUMN_NAME ] = [ 'label' => esc_html__( 'Locations', 'wpforms-lite' ), 'label_html' => sprintf( '<span class="wpforms-locations-column-title">%1$s</span>' . '<span class="wpforms-locations-column-icon" title="%2$s"></span>', esc_html__( 'Locations', 'wpforms-lite' ), esc_html__( 'Form locations', 'wpforms-lite' ) ), ]; return $columns; } /** * Display column value. * * @since 1.7.4 * * @param mixed $value Column value. * @param WP_Post $form Form. * @param string $column_name Column name. * * @return mixed */ public function column_value( $value, $form, $column_name ) { if ( $column_name !== self::COLUMN_NAME ) { return $value; } $form_locations = get_post_meta( $form->ID, self::LOCATIONS_META, true ); if ( $form_locations === '' ) { $empty_values = [ '' => '—', FormsLocatorScanTask::SCAN_STATUS_IN_PROGRESS => '...', FormsLocatorScanTask::SCAN_STATUS_COMPLETED => '0', ]; return $empty_values[ $this->scan_status ]; } $values = $this->get_location_rows( $form_locations ); if ( ! $values ) { return '0'; } $column_value = sprintf( '<span class="wpforms-locations-count"><a href="#" title="%s">%d</a></span>', esc_attr__( 'View form locations', 'wpforms-lite' ), count( $values ) ); $column_value .= '<p class="locations-list">' . implode( '', $values ) . '</p>'; return $column_value; } /** * Row actions for view "All". * * @since 1.7.4 * * @param array $row_actions Row actions. * @param WP_Post $form Form object. * * @return array */ public function row_actions_all( $row_actions, $form ) { $form_locations = get_post_meta( $form->ID, self::LOCATIONS_META, true ); if ( ! $form_locations ) { return $row_actions; } $locations = [ 'locations' => sprintf( '<a href="#" title="%s">%s</a>', esc_attr__( 'View form locations', 'wpforms-lite' ), esc_html__( 'Locations', 'wpforms-lite' ) ), ]; // Insert Locations action before the first available position in the positions' list or at the end of $row_actions. $positions = [ 'preview_', 'duplicate', 'trash', ]; $keys = array_keys( $row_actions ); foreach ( $positions as $position ) { $pos = array_search( $position, $keys, true ); if ( $pos !== false ) { break; } } $pos = $pos === false ? count( $row_actions ) : $pos; return array_slice( $row_actions, 0, $pos ) + $locations + array_slice( $row_actions, $pos ); } /** * Localize the overview script to pass translation strings. * * @since 1.7.4 */ public function localize_overview_script() { wp_localize_script( 'wpforms-admin-forms-overview', 'wpforms_forms_locator', [ 'paneTitle' => __( 'Form Locations', 'wpforms-lite' ), 'close' => __( 'Close', 'wpforms-lite' ), ] ); } /** * Get id of the sidebar where the widget is positioned. * * @since 1.7.4 * * @param string $widget_id Widget id. * * @return string */ private function get_widget_sidebar_id( $widget_id ) { $sidebars_widgets = wp_get_sidebars_widgets(); foreach ( $sidebars_widgets as $sidebar_id => $sidebar_widgets ) { foreach ( $sidebar_widgets as $sidebar_widget ) { if ( $widget_id === $sidebar_widget ) { return (string) $sidebar_id; } } } return ''; } /** * Get the name of the sidebar where the widget is positioned. * * @since 1.7.4 * * @param string $widget_id Widget id. * * @return string */ private function get_widget_sidebar_name( $widget_id ) { $sidebar_id = $this->get_widget_sidebar_id( $widget_id ); if ( ! $sidebar_id ) { return ''; } $sidebar = $this->get_sidebar( $sidebar_id ); return isset( $sidebar['name'] ) ? (string) $sidebar['name'] : ''; } /** * Retrieves the registered sidebar with the given ID. * * @since 1.7.4 * * @global array $wp_registered_sidebars The registered sidebars. * * @param string $id The sidebar ID. * * @return array|null The discovered sidebar, or null if it is not registered. */ private function get_sidebar( $id ) { if ( function_exists( 'wp_get_sidebar' ) ) { return wp_get_sidebar( $id ); } global $wp_registered_sidebars; if ( ! $wp_registered_sidebars ) { return null; } foreach ( $wp_registered_sidebars as $sidebar ) { if ( $sidebar['id'] === $id ) { return $sidebar; } } if ( $id === 'wp_inactive_widgets' ) { return [ 'id' => 'wp_inactive_widgets', 'name' => __( 'Inactive widgets', 'wpforms-lite' ), ]; } return null; } /** * Get post location title. * * @since 1.7.4 * * @param array $form_location Form location. * * @return string */ private function get_post_location_title( $form_location ) { $title = $form_location['title']; if ( $this->is_wp_template( $form_location['type'] ) ) { return __( 'Site editor template', 'wpforms-lite' ) . ': ' . $title; } return $title; } /** * Whether locations' type is WP Template. * * @since 1.7.4.1 * * @param string $location_type Location type. * * @return bool */ private function is_wp_template( $location_type ) { return in_array( $location_type, [ self::WP_TEMPLATE, self::WP_TEMPLATE_PART ], true ); } /** * Whether a location type is standalone. * * @since 1.8.7 * * @param string $location_type Location type. * * @return bool */ private function is_standalone( string $location_type ): bool { return in_array( $location_type, self::STANDALONE_LOCATION_TYPES, true ); } /** * Get location title. * * @since 1.7.4 * * @param array $form_location Form location. * * @return string */ private function get_location_title( $form_location ) { if ( $form_location['type'] !== self::WIDGET ) { return $this->get_post_location_title( $form_location ); } $sidebar_name = $this->get_widget_sidebar_name( $form_location['id'] ); if ( ! $sidebar_name ) { // The widget is not found. return ''; } $title = $form_location['title']; if ( ! $title ) { if ( strpos( $form_location['id'], self::WPFORMS_WIDGET_PREFIX ) === 0 ) { $title = $this->wpforms_widget_title; } if ( strpos( $form_location['id'], 'text-' ) === 0 ) { $title = $this->text_widget_title; } } return $sidebar_name . ': ' . $title; } /** * Get location url. * * @since 1.7.4 * * @param array $form_location Form location. * * @return string */ private function get_location_url( $form_location ) { // Get widget or wp_template url. if ( $form_location['type'] === self::WIDGET || $this->is_wp_template( $form_location['type'] ) ) { return ''; } // Get standalone url. if ( $this->is_standalone( $form_location['type'] ) ) { return $form_location['url']; } // Get post url. if ( ! $this->is_post_visible( $form_location ) ) { return ''; } return $form_location['url']; } /** * Get location edit url. * * @since 1.7.4 * * @param array $form_location Form location. * * @return string */ private function get_location_edit_url( array $form_location ): string { // Get widget url. if ( $form_location['type'] === self::WIDGET ) { return current_user_can( 'edit_theme_options' ) ? admin_url( 'widgets.php' ) : ''; } // Get standalone url. if ( $this->is_standalone( $form_location['type'] ) ) { return add_query_arg( [ 'page' => 'wpforms-builder', 'view' => 'settings', 'form_id' => $form_location['form_id'], ], admin_url( 'admin.php' ) ); } // Get post url. if ( ! $this->is_post_visible( $form_location ) ) { return ''; } if ( $this->is_wp_template( $form_location['type'] ) ) { return add_query_arg( [ 'postType' => $form_location['type'], 'postId' => get_stylesheet() . '//' . str_replace( '/', '', $form_location['url'] ), ], admin_url( 'site-editor.php' ) ); } return (string) get_edit_post_link( $form_location['id'], '' ); } /** * Get location information to output as a row in the location pane. * * @since 1.7.4 * * @param array $form_location Form location. * * @return string * @noinspection PhpTernaryExpressionCanBeReducedToShortVersionInspection * @noinspection ElvisOperatorCanBeUsedInspection */ private function get_location_row( $form_location ) { $title = $this->get_location_title( $form_location ); $title = $title ? $title : __( '(no title)', 'wpforms-lite' ); $location_url = $this->get_location_url( $form_location ); $location_link = ''; if ( $location_url ) { $location_full_url = $this->home_url . $location_url; // phpcs:ignore Generic.Commenting.DocComment.MissingShort /** @noinspection HtmlUnknownTarget */ $location_link = sprintf( ' <a href="%1$s" target="_blank" class="wpforms-locations-link">%2$s <i class="fa fa-external-link" aria-hidden="true"></i></a>', esc_url( $location_full_url ), esc_url( $location_url ) ); } $location_edit_url = $this->get_location_edit_url( $form_location ); $location_edit_url = $location_edit_url ? $location_edit_url : '#'; // phpcs:ignore Generic.Commenting.DocComment.MissingShort /** @noinspection HtmlUnknownTarget */ $location_edit_link = sprintf( '<a href="%1$s">%2$s</a>', esc_url( $location_edit_url ), esc_html( $title ) ); // Escaped above. return sprintf( '<span class="wpforms-locations-list-item">%s</span>', $location_edit_link . wp_kses_post( urldecode( $location_link ) ) ); } /** * Get location information to output as rows in the location pane. * * @since 1.7.4 * * @param array $form_locations Form locations. * * @return array */ private function get_location_rows( $form_locations ) { $rows = []; foreach ( $form_locations as $form_location ) { $rows[] = $this->get_location_row( $form_location ); } $rows = array_unique( array_filter( $rows ) ); uasort( $rows, static function ( $a, $b ) { $pattern = '/href=".+widgets.php">(.+?)</i'; $widget_title_a = preg_match( $pattern, $a, $ma ) ? $ma[1] : ''; $widget_title_b = preg_match( $pattern, $b, $mb ) ? $mb[1] : ''; return strcmp( $widget_title_a, $widget_title_b ); } ); return $rows; } /** * Update form location on save_post action. * * @since 1.7.4 * * @param int $post_ID Post ID. * @param WP_Post $post Post object. * @param bool $update Whether this is an existing post being updated. * * @noinspection PhpUnusedParameterInspection */ public function save_post( $post_ID, $post, $update ) { if ( $update || ! in_array( $post->post_type, $this->get_post_types(), true ) || ! in_array( $post->post_status, $this->get_post_statuses(), true ) ) { return; } $form_ids = $this->get_form_ids( $post->post_content ); $this->update_form_locations_metas( null, $post, [], $form_ids ); } /** * Update form location on post_updated action. * * @since 1.7.4 * * @param int $post_id Post id. * @param WP_Post $post_after Post after the update. * @param WP_Post $post_before Post before the update. * * @noinspection PhpUnusedParameterInspection */ public function post_updated( $post_id, $post_after, $post_before ) { if ( ! in_array( $post_after->post_type, $this->get_post_types(), true ) || ! in_array( $post_after->post_status, $this->get_post_statuses(), true ) ) { return; } $form_ids_before = $this->get_form_ids( $post_before->post_content ); $form_ids_after = $this->get_form_ids( $post_after->post_content ); $this->update_form_locations_metas( $post_before, $post_after, $form_ids_before, $form_ids_after ); } /** * Update form locations on trash_post action. * * @since 1.7.4 * * @param int $post_id Post id. */ public function trash_post( $post_id ) { $post = get_post( $post_id ); $form_ids_before = $this->get_form_ids( $post->post_content ); $form_ids_after = []; $this->update_form_locations_metas( null, $post, $form_ids_before, $form_ids_after ); } /** * Update form locations on untrash_post action. * * @since 1.7.4 * * @param int $post_id Post id. */ public function untrash_post( $post_id ) { $post = get_post( $post_id ); $form_ids_before = []; $form_ids_after = $this->get_form_ids( $post->post_content ); $this->update_form_locations_metas( null, $post, $form_ids_before, $form_ids_after ); } /** * Prepare widgets for further search. * * @since 1.7.4 * * @param array|null $widgets Widgets. * @param string $type Widget type. * * @return array */ private function prepare_widgets( $widgets, $type ) { $params = [ 'wpforms' => [ 'option' => self::WPFORMS_WIDGET_OPTION, 'content' => 'form_id', ], 'text' => [ 'option' => self::TEXT_WIDGET_OPTION, 'content' => 'text', ], 'block' => [ 'option' => self::BLOCK_WIDGET_OPTION, 'content' => 'content', ], ]; if ( ! array_key_exists( $type, $params ) ) { return []; } $option = $params[ $type ]['option']; $content = $params[ $type ]['content']; $widgets = $widgets ?? (array) get_option( $option, [] ); return array_filter( $widgets, static function ( $widget ) use ( $content ) { return isset( $widget[ $content ] ); } ); } /** * Search forms in WPForms widgets. * * @since 1.7.4 * * @param array $widgets Widgets. * * @return array */ private function search_in_wpforms_widgets( $widgets = null ) { $widgets = $this->prepare_widgets( $widgets, 'wpforms' ); $locations = []; foreach ( $widgets as $id => $widget ) { $locations[] = [ 'type' => self::WIDGET, 'title' => $widget['title'], 'form_id' => $widget['form_id'], 'id' => self::WPFORMS_WIDGET_PREFIX . $id, ]; } return $locations; } /** * Search forms in text widgets. * * @since 1.7.4 * * @param array $widgets Widgets. * * @return array */ private function search_in_text_widgets( $widgets = null ) { $widgets = $this->prepare_widgets( $widgets, 'text' ); $locations = []; foreach ( $widgets as $id => $widget ) { $form_ids = $this->get_form_ids( $widget['text'] ); foreach ( $form_ids as $form_id ) { $locations[] = [ 'type' => self::WIDGET, 'title' => $widget['title'], 'form_id' => $form_id, 'id' => self::TEXT_WIDGET_PREFIX . $id, ]; } } return $locations; } /** * Search forms in block widgets. * * @since 1.7.4 * * @param array $widgets Widgets. * * @return array */ private function search_in_block_widgets( $widgets = null ) { $widgets = $this->prepare_widgets( $widgets, 'block' ); $locations = []; foreach ( $widgets as $id => $widget ) { $form_ids = $this->get_form_ids( $widget['content'] ); foreach ( $form_ids as $form_id ) { $locations[] = [ 'type' => self::WIDGET, 'title' => $this->block_widget_title, 'form_id' => $form_id, 'id' => self::BLOCK_WIDGET_PREFIX . $id, ]; } } return $locations; } /** * Search forms in widgets. * * @since 1.7.4 * * @return array */ public function search_in_widgets() { return array_merge( $this->search_in_wpforms_widgets(), $this->search_in_text_widgets(), $this->search_in_block_widgets() ); } /** * Get the difference of two arrays containing locations. * * @since 1.7.4 * * @param array $locations1 Locations to subtract from. * @param array $locations2 Locations to subtract. * * @return array */ private function array_udiff( $locations1, $locations2 ) { return array_udiff( $locations1, $locations2, static function ( $a, $b ) { return ( $a === $b ) ? 0 : - 1; } ); } /** * Remove locations from metas. * * @since 1.7.4 * * @param array $locations_to_remove Locations to remove. * * @return void */ private function remove_locations( $locations_to_remove ) { foreach ( $locations_to_remove as $location_to_remove ) { $locations = get_post_meta( $location_to_remove['form_id'], self::LOCATIONS_META, true ); if ( ! $locations ) { continue; } foreach ( $locations as $key => $location ) { if ( $location['id'] === $location_to_remove['id'] ) { unset( $locations[ $key ] ); } } update_post_meta( $location_to_remove['form_id'], self::LOCATIONS_META, $locations ); } } /** * Add locations to metas. * * @since 1.7.4 * * @param array $locations_to_add Locations to add. * * @return void */ private function add_locations( $locations_to_add ) { foreach ( $locations_to_add as $location_to_add ) { $locations = get_post_meta( $location_to_add['form_id'], self::LOCATIONS_META, true ); if ( ! $locations ) { $locations = []; } $locations[] = $location_to_add; update_post_meta( $location_to_add['form_id'], self::LOCATIONS_META, $locations ); } } /** * Update form locations on widget update. * * @since 1.7.4 * * @param mixed $old_value The old option value. * @param mixed $value The new option value. * @param string $option Option name. */ public function update_option( $old_value, $value, $option ) { switch ( $option ) { case self::WPFORMS_WIDGET_OPTION: $old_locations = $this->search_in_wpforms_widgets( $old_value ); $new_locations = $this->search_in_wpforms_widgets( $value ); break; case self::TEXT_WIDGET_OPTION: $old_locations = $this->search_in_text_widgets( $old_value ); $new_locations = $this->search_in_text_widgets( $value ); break; case self::BLOCK_WIDGET_OPTION: $old_locations = $this->search_in_block_widgets( $old_value ); $new_locations = $this->search_in_block_widgets( $value ); break; default: // phpcs:ignore WPForms.Formatting.EmptyLineBeforeReturn.AddEmptyLineBeforeReturnStatement return; } $this->remove_locations( $this->array_udiff( $old_locations, $new_locations ) ); $this->add_locations( $this->array_udiff( $new_locations, $old_locations ) ); } /** * Delete locations and schedule new rescan on change of permalink structure. * * @since 1.7.4 * * @param string $old_permalink_structure The previous permalink structure. * @param string $permalink_structure The new permalink structure. * * @noinspection PhpUnusedParameterInspection */ public function permalink_structure_changed( $old_permalink_structure, $permalink_structure ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed /** * Run Forms Locator delete action. * * @since 1.7.4 */ do_action( FormsLocatorScanTask::DELETE_ACTION ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName, WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound /** * Run Forms Locator scan action. * * @since 1.7.4 */ do_action( FormsLocatorScanTask::RESCAN_ACTION ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName, WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound } /** * Update form locations metas. * * @since 1.7.4 * @since 1.8.2.3 Added `$post_before` parameter. * * @param WP_Post|null $post_before The post before the update. * @param WP_Post $post_after The post after the update. * @param array $form_ids_before Form IDs before the update. * @param array $form_ids_after Form IDs after the update. */ private function update_form_locations_metas( $post_before, $post_after, $form_ids_before, $form_ids_after ) { // Determine which locations to remove and which to add. $form_ids_to_remove = array_diff( $form_ids_before, $form_ids_after ); $form_ids_to_add = array_diff( $form_ids_after, $form_ids_before ); // Loop through each form ID to remove the locations' meta. foreach ( $form_ids_to_remove as $form_id ) { update_post_meta( $form_id, self::LOCATIONS_META, $this->get_locations_without_current_post( $form_id, $post_after->ID ) ); } // Determine the titles and slugs. $old_title = $post_before->post_title ?? ''; $old_slug = $post_before->post_name ?? ''; $new_title = $post_after->post_title; $new_slug = $post_after->post_name; // If the title and slug are the same and there are no form IDs to add, bail. if ( empty( $form_ids_to_add ) && $old_title === $new_title && $old_slug === $new_slug ) { return; } // Merge the form IDs and remove duplicates. $form_ids = array_unique( array_merge( $form_ids_to_add, $form_ids_after ) ); $this->save_location_meta( $form_ids, $post_after->ID, $post_after ); } /** * Save the location meta. * * @since 1.8.2.3 * * @param array $form_ids Form IDs. * @param int $post_id Post ID. * @param WP_Post $post_after Post after the update. */ private function save_location_meta( $form_ids, $post_id, $post_after ) { // Build the URL. $url = get_permalink( $post_id ); $url = ( $url === false || is_wp_error( $url ) ) ? '' : $url; $url = str_replace( $this->home_url, '', $url ); // Loop through each Form ID and save the location meta. foreach ( $form_ids as $form_id ) { $locations = $this->get_locations_without_current_post( $form_id, $post_id ); $locations[] = [ 'type' => $post_after->post_type, 'title' => $post_after->post_title, 'form_id' => $form_id, 'id' => $post_id, 'status' => $post_after->post_status, 'url' => $url, ]; update_post_meta( $form_id, self::LOCATIONS_META, $locations ); } } /** * Get post types for search in. * * @since 1.7.4 * * @return string[] */ public function get_post_types() { $args = [ 'public' => true, 'publicly_queryable' => true, ]; $post_types = get_post_types( $args, 'names', 'or' ); unset( $post_types['attachment'] ); $post_types[] = self::WP_TEMPLATE; $post_types[] = self::WP_TEMPLATE_PART; return $post_types; } /** * Get post statuses for search in. * * @since 1.7.4 * * @return string[] */ public function get_post_statuses() { return [ 'publish', 'pending', 'draft', 'future', 'private' ]; } /** * Get form ids from the content. * * @since 1.7.4 * * @param string $content Content. * * @return int[] */ public function get_form_ids( $content ) { $form_ids = []; if ( preg_match_all( /** * Extract id from conventional wpforms shortcode or wpforms block. * Examples: * [wpforms id="32" title="true" description="true"] * <!-- wp:wpforms/form-selector {"clientId":"b5f8e16a-fc28-435d-a43e-7c77719f074c", "formId":"32","displayTitle":true,"displayDesc":true} /--> * In both, we should find 32. */ '#\[\s*wpforms.+id\s*=\s*"(\d+?)".*]|<!-- wp:wpforms/form-selector {.*?"formId":"(\d+?)".*?} /-->#', $content, $matches ) ) { array_shift( $matches ); $form_ids = array_map( 'intval', array_unique( array_filter( array_merge( ...$matches ) ) ) ); } return $form_ids; } /** * Get form locations without a current post. * * @since 1.7.4 * * @param int $form_id Form id. * @param int $post_id Post id. * * @return array */ private function get_locations_without_current_post( $form_id, $post_id ) { $locations = get_post_meta( $form_id, self::LOCATIONS_META, true ); if ( ! is_array( $locations ) ) { $locations = []; } return array_filter( $locations, static function ( $location ) use ( $post_id ) { return $location['id'] !== $post_id; } ); } /** * Determine whether a post is visible. * * @since 1.7.4 * * @param array $location Post location. * * @return bool */ private function is_post_visible( $location ) { $edit_cap = 'edit_post'; $read_cap = 'read_post'; $post_id = $location['id']; if ( ! get_post_type_object( $location['type'] ) ) { // Post type is not registered. return false; } $post_status_obj = get_post_status_object( $location['status'] ); if ( ! $post_status_obj ) { // Post status is not registered, assume it's not public. return current_user_can( $edit_cap, $post_id ); } if ( $post_status_obj->public ) { return true; } if ( ! is_user_logged_in() ) { // User must be logged in to view unpublished posts. return false; } if ( $post_status_obj->protected ) { // User must have edit permissions on the draft to preview. return current_user_can( $edit_cap, $post_id ); } if ( $post_status_obj->private ) { return current_user_can( $read_cap, $post_id ); } return false; } /** * Build a standalone location. * * @since 1.8.7 * * @param int $form_id The form ID. * @param array $form_data Form data. * @param string $status Form status. * * @return array Location. */ public function build_standalone_location( int $form_id, array $form_data, string $status = 'publish' ): array { if ( empty( $form_id ) || empty( $form_data ) ) { return []; } // Form templates should not have any locations. if ( get_post_type( $form_id ) === 'wpforms-template' ) { return []; } foreach ( self::STANDALONE_LOCATION_TYPES as $location_type ) { if ( empty( $form_data['settings'][ "{$location_type}_enable" ] ) ) { continue; } return $this->build_standalone_location_type( $location_type, $form_id, $form_data, $status ); } return []; } /** * Build a standalone location. * * @since 1.8.8 * * @param string $location_type Standalone location type. * @param int $form_id The form ID. * @param array $form_data Form data. * @param string $status Form status. * * @return array Location. */ private function build_standalone_location_type( string $location_type, int $form_id, array $form_data, string $status ): array { $title_key = "{$location_type}_title"; $slug_key = "{$location_type}_page_slug"; $title = $form_data['settings'][ $title_key ] ?? ''; $slug = $form_data['settings'][ $slug_key ] ?? ''; // Return the location array. return [ 'type' => $location_type, 'title' => $title, 'form_id' => (int) $form_data['id'], 'id' => $form_id, 'status' => $status, 'url' => '/' . $slug . '/', ]; } /** * Add standalone form locations to post meta. * * Post meta is used to store all forms' locations, * which is displayed on the WPForms Overview page. * * @since 1.8.7 * * @param int $form_id Form ID. * @param array $data Form data. */ public function add_standalone_location_to_locations_meta( int $form_id, array $data ) { // Build standalone location. $location = $this->build_standalone_location( $form_id, $data ); // No location? Bail. if ( empty( $location ) ) { return; } // Setup data. $new_location[] = $location; $post_meta = get_post_meta( $form_id, self::LOCATIONS_META, true ); // If there is post meta, merge it with the new location. if ( ! empty( $post_meta ) ) { // Remove any previously set standalone locations. $post_meta = $this->remove_standalone_location_from_array( $form_id, $post_meta ); // Merge locations and remove duplicates. $new_location = array_unique( array_merge( $post_meta, $new_location ), SORT_REGULAR ); } // Update post meta. update_post_meta( $form_id, self::LOCATIONS_META, $new_location ); } /** * Remove a form page from an array. * * @since 1.8.7 * * @param int $form_id The form ID. * @param array $post_meta The post meta. * * @return array $post_meta Filtered post meta. */ private function remove_standalone_location_from_array( int $form_id, array $post_meta ): array { // No form ID or post meta? Bail. if ( empty( $form_id ) || empty( $post_meta ) ) { return []; } // Loop over all locations. foreach ( $post_meta as $key => $location ) { // Verify the location keys exist. if ( ! isset( $location['form_id'], $location['type'] ) ) { continue; } // If the form ID and location type match. if ( $location['form_id'] === $form_id && $this->is_standalone( $location['type'] ) ) { // Unset the form page location. unset( $post_meta[ $key ] ); } } return $post_meta; } } Forms/Preview.php 0000644 00000024476 15174710275 0010012 0 ustar 00 <?php namespace WPForms\Forms; /** * Form preview. * * @since 1.5.1 */ class Preview { /** * Form data. * * @since 1.5.1 * * @var array */ public $form_data; /** * Post type. * * @since 1.8.8 * * @var string */ private $post_type; /** * Whether this is a form template. * * @since 1.8.8 * * @var bool */ private $is_form_template; /** * Constructor. * * @since 1.5.1 */ public function __construct() { if ( ! $this->is_preview_page() ) { return; } $this->hooks(); } /** * Check if current page request meets requirements for form preview page. * * @since 1.5.1 * * @return bool */ public function is_preview_page(): bool { // Only proceed for the form preview page. // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( empty( $_GET['wpforms_form_preview'] ) ) { return false; } // Only logged-in users can access the preview page. if ( ! is_user_logged_in() ) { return false; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended $form_id = absint( $_GET['wpforms_form_preview'] ); // Make sure the user is allowed to preview the form. if ( ! wpforms_current_user_can( 'view_form_single', $form_id ) ) { return false; } // Fetch form details. $this->form_data = wpforms()->obj( 'form' )->get( $form_id, [ 'content_only' => true ] ); // Get the post type for preview item. $this->post_type = get_post_type( $form_id ); // Check if this is a form template. $this->is_form_template = $this->post_type === 'wpforms-template'; // Check valid form was found. if ( empty( $this->form_data ) || empty( $this->form_data['id'] ) ) { return false; } return true; } /** * Hooks. * * @since 1.5.1 */ public function hooks() { add_filter( 'wpforms_frontend_assets_header_force_load', '__return_true' ); add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_assets' ] ); add_action( 'pre_get_posts', [ $this, 'pre_get_posts' ] ); add_filter( 'the_title', [ $this, 'the_title' ], 100, 1 ); add_filter( 'the_content', [ $this, 'the_content' ], 999 ); add_filter( 'get_the_excerpt', [ $this, 'the_content' ], 999 ); add_filter( 'home_template_hierarchy', [ $this, 'force_page_template_hierarchy' ] ); add_filter( 'frontpage_template_hierarchy', [ $this, 'force_page_template_hierarchy' ] ); add_filter( 'wpforms_smarttags_process_page_title_value', [ $this, 'smart_tags_process_page_title_value' ], 10, 5 ); add_filter( 'post_thumbnail_html', '__return_empty_string' ); } /** * Enqueue additional form preview styles. * * @since 1.8.8 */ public function enqueue_assets() { $min = wpforms_get_min_suffix(); // Enqueue the form preview styles. wp_enqueue_style( 'wpforms-preview', WPFORMS_PLUGIN_URL . "assets/css/frontend/wpforms-form-preview{$min}.css", [], WPFORMS_VERSION ); } /** * Modify query, limit to one post. * * @since 1.5.1 * @since 1.7.0 Added `page_id`, `post_type` and `post__in` query variables. * * @param \WP_Query $query The WP_Query instance. */ public function pre_get_posts( $query ) { if ( is_admin() || ! $query->is_main_query() ) { return; } $query->set( 'page_id', '' ); $query->set( 'post_type', $this->post_type ?? 'wpforms' ); $query->set( 'post__in', empty( $this->form_data['id'] ) ? [] : [ (int) $this->form_data['id'] ] ); $query->set( 'posts_per_page', 1 ); // The preview page reads as the home page and as an non-singular posts page, neither of which are actually the case. // So we hardcode the correct values for those properties in the query. $query->is_home = false; $query->is_singular = true; $query->is_single = true; } /** * Customize form preview page title. * * @since 1.5.1 * * @param string $title Page title. * * @return string */ public function the_title( $title ) { if ( ! in_the_loop() ) { return $title; } if ( $this->is_form_template ) { return sprintf( /* translators: %s - form name. */ esc_html__( '%s Template Preview', 'wpforms-lite' ), ! empty( $this->form_data['settings']['form_title'] ) ? sanitize_text_field( $this->form_data['settings']['form_title'] ) : esc_html__( 'Form Template', 'wpforms-lite' ) ); } return sprintf( /* translators: %s - form name. */ esc_html__( '%s Preview', 'wpforms-lite' ), ! empty( $this->form_data['settings']['form_title'] ) ? sanitize_text_field( $this->form_data['settings']['form_title'] ) : esc_html__( 'Form', 'wpforms-lite' ) ); } /** * Customize form preview page content. * * @since 1.5.1 * * @return string */ public function the_content() { if ( ! isset( $this->form_data['id'] ) ) { return ''; } if ( ! wpforms_current_user_can( 'view_form_single', $this->form_data['id'] ) ) { return ''; } $admin_url = admin_url( 'admin.php' ); $links = []; if ( wpforms_current_user_can( 'edit_form_single', $this->form_data['id'] ) ) { $links[] = [ 'url' => esc_url( add_query_arg( [ 'page' => 'wpforms-builder', 'view' => 'fields', 'form_id' => absint( $this->form_data['id'] ), ], $admin_url ) ), 'text' => $this->is_form_template ? esc_html__( 'Edit Form Template', 'wpforms-lite' ) : esc_html__( 'Edit Form', 'wpforms-lite' ), ]; } if ( wpforms()->is_pro() && wpforms_current_user_can( 'view_entries_form_single', $this->form_data['id'] ) ) { $links[] = [ 'url' => esc_url( add_query_arg( [ 'page' => 'wpforms-entries', 'view' => 'list', 'form_id' => absint( $this->form_data['id'] ), ], $admin_url ) ), 'text' => esc_html__( 'View Entries', 'wpforms-lite' ), ]; } if ( ! $this->is_form_template && wpforms_current_user_can( wpforms_get_capability_manage_options(), $this->form_data['id'] ) && wpforms()->obj( 'payment' )->get_by( 'form_id', $this->form_data['id'] ) ) { $links[] = [ 'url' => esc_url( add_query_arg( [ 'page' => 'wpforms-payments', 'form_id' => absint( $this->form_data['id'] ), ], $admin_url ) ), 'text' => esc_html__( 'View Payments', 'wpforms-lite' ), ]; } if ( ! empty( $_GET['new_window'] ) ) { // phpcs:ignore $links[] = [ 'url' => 'javascript:window.close();', 'text' => esc_html__( 'Close this window', 'wpforms-lite' ), ]; } $content = ''; $content .= $this->add_preview_notice(); $content .= '<p>'; $content .= $this->is_form_template ? esc_html__( 'This is a preview of the latest saved revision of your form template. If this preview does not match your template, save your changes and then refresh this page. This template preview is not publicly accessible.', 'wpforms-lite' ) : esc_html__( 'This is a preview of the latest saved revision of your form. If this preview does not match your form, save your changes and then refresh this page. This form preview is not publicly accessible.', 'wpforms-lite' ); if ( ! empty( $links ) ) { $content .= '<br>'; $content .= '<span class="wpforms-preview-notice-links">'; foreach ( $links as $key => $link ) { $content .= '<a href="' . $link['url'] . '">' . $link['text'] . '</a>'; $l = array_keys( $links ); if ( end( $l ) !== $key ) { $content .= ' <span style="display:inline-block;margin:0 6px;opacity: 0.5">|</span> '; } } $content .= '</span>'; } $content .= '</p>'; $content .= '<p>'; $content .= sprintf( wp_kses( /* translators: %s - WPForms doc link. */ __( 'For form testing tips, check out our <a href="%s" target="_blank" rel="noopener noreferrer">complete guide!</a>', 'wpforms-lite' ), [ 'a' => [ 'href' => [], 'target' => [], 'rel' => [], ], ] ), esc_url( wpforms_utm_link( 'https://wpforms.com/docs/how-to-properly-test-your-wordpress-forms-before-launching-checklist/', $this->is_form_template ? 'Form Template Preview' : 'Form Preview', 'Form Testing Tips Documentation' ) ) ); $content .= '</p>'; $content .= do_shortcode( '[wpforms id="' . absint( $this->form_data['id'] ) . '"]' ); return $content; } /** * Add preview notice. * * @since 1.8.8 * * @return string HTML content. */ private function add_preview_notice(): string { if ( ! $this->is_form_template ) { return ''; } $content = '<div class="wpforms-preview-notice">'; $content .= sprintf( '<strong>%s</strong> %s', esc_html__( 'Heads up!', 'wpforms-lite' ), esc_html__( 'You\'re viewing a preview of a form template.', 'wpforms-lite' ) ); if ( wpforms()->is_pro() ) { /** This filter is documented in wpforms/src/Pro/Tasks/Actions/PurgeTemplateEntryTask.php */ $delay = (int) apply_filters( 'wpforms_pro_tasks_actions_purge_template_entry_task_delay', DAY_IN_SECONDS ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName $message = sprintf( /* translators: %s - time period, e.g. 24 hours. */ __( 'Entries are automatically deleted after %s.', 'wpforms-lite' ), // The `- 1` hack is to avoid the "1 day" message in favor of "24 hours". human_time_diff( time(), time() + $delay - 1 ) ); $content .= sprintf( '<p>%s</p>', esc_html( $message ) ); } $content .= '</div>'; return wp_kses_post( $content ); } /** * Force page template types. * * @since 1.7.2 * * @param array $templates A list of template candidates, in descending order of priority. * * @return array */ public function force_page_template_hierarchy( $templates ) { return [ 'page.php', 'single.php', 'index.php' ]; } /** * Adjust value of the {page_title} smart tag. * * @since 1.7.7 * * @param string $content Content. * @param array $form_data Form data. * @param array $fields List of fields. * @param string $entry_id Entry ID. * @param object $smart_tag_object The smart tag object or the Generic object for those cases when class unregistered. * * @return string */ public function smart_tags_process_page_title_value( $content, $form_data, $fields, $entry_id, $smart_tag_object ) { return sprintf( /* translators: %s - form name. */ esc_html__( '%s Preview', 'wpforms-lite' ), ! empty( $form_data['settings']['form_title'] ) ? sanitize_text_field( $form_data['settings']['form_title'] ) : esc_html__( 'Form', 'wpforms-lite' ) ); } } Forms/Fields/Phone/Field.php 0000644 00000012107 15174710275 0011657 0 ustar 00 <?php namespace WPForms\Forms\Fields\Phone; use WPForms\Forms\Fields\Traits\ProField as ProFieldTrait; use WPForms_Field; /** * Phone number field. * * @since 1.9.4 */ class Field extends WPForms_Field { use ProFieldTrait; /** * International Telephone Input library CSS. * * @since 1.9.4 */ public const INTL_VERSION = '25.11.3'; /** * Primary class constructor. * * @since 1.9.4 */ public function init() { // Define field type information. $this->name = esc_html__( 'Phone', 'wpforms-lite' ); $this->keywords = esc_html__( 'telephone, mobile, cell', 'wpforms-lite' ); $this->type = 'phone'; $this->icon = 'fa-phone'; $this->order = 50; $this->group = 'fancy'; $this->default_settings = [ 'format' => 'smart', ]; $this->init_pro_field(); $this->hooks(); } /** * Hooks. * * @since 1.9.4 */ protected function hooks() { } /** * Field options panel inside the builder. * * @since 1.9.4 * * @param array $field Field data. */ public function field_options( $field ) { /** * Basic field options. */ // Options open markup. $this->field_option( 'basic-options', $field, [ 'markup' => 'open', 'after_title' => $this->get_field_options_notice(), ] ); // Label. $this->field_option( 'label', $field ); // Format. $lbl = $this->field_element( 'label', $field, [ 'slug' => 'format', 'value' => esc_html__( 'Format', 'wpforms-lite' ), 'tooltip' => esc_html__( 'Select format for the phone form field', 'wpforms-lite' ), ], false ); $fld = $this->field_element( 'select', $field, [ 'slug' => 'format', 'value' => ! empty( $field['format'] ) ? esc_attr( $field['format'] ) : 'smart', 'options' => [ 'smart' => esc_html__( 'Smart', 'wpforms-lite' ), 'us' => esc_html__( 'US', 'wpforms-lite' ), 'international' => esc_html__( 'International', 'wpforms-lite' ), ], ], false ); $args = [ 'slug' => 'format', 'content' => $lbl . $fld, ]; $this->field_element( 'row', $field, $args ); // Description. $this->field_option( 'description', $field ); // Required toggle. $this->field_option( 'required', $field ); // Options close markup. $args = [ 'markup' => 'close', ]; $this->field_option( 'basic-options', $field, $args ); /* * Advanced field options. */ // Options open markup. $args = [ 'markup' => 'open', ]; $this->field_option( 'advanced-options', $field, $args ); // Size. $this->field_option( 'size', $field ); // Placeholder. $this->field_option( 'placeholder', $field ); // Default value. $this->field_option( 'default_value', $field ); // Custom CSS classes. $this->field_option( 'css', $field ); // Hide Label. $this->field_option( 'label_hide', $field ); // Options close markup. $args = [ 'markup' => 'close', ]; $this->field_option( 'advanced-options', $field, $args ); } /** * Field preview inside the builder. * * @since 1.9.4 * * @param array $field Field data. */ public function field_preview( $field ) { // Define data. $placeholder = ! empty( $field['placeholder'] ) ? $field['placeholder'] : ''; $default_value = ! empty( $field['default_value'] ) ? $field['default_value'] : ''; $format = ! empty( $field['format'] ) ? $field['format'] : 'smart'; $size = ! empty( $field['size'] ) ? $field['size'] : 'medium'; // Label. $this->field_preview_option( 'label', $field, [ 'label_badge' => $this->get_field_preview_badge(), ] ); // Primary input inside container for Smart format preview. printf( '<div class="wpforms-field-phone-input-container" data-format="%1$s"> <input type="text" placeholder="%2$s" value="%3$s" class="primary-input wpforms-field-%4$s" readonly> <div class="wpforms-field-phone-country-container"> <div class="wpforms-field-phone-flag"></div> <div class="wpforms-field-phone-arrow"></div> </div> </div>', esc_attr( $format ), esc_attr( $placeholder ), esc_attr( $default_value ), esc_attr( $size ) ); // Description. $this->field_preview_option( 'description', $field ); } /** * Get a preview option. * * @since 1.9.4 * * @param string $option Option name. * @param array $field Field data. * @param array $args Additional arguments. * @param bool $do_echo Echo or return. */ public function field_preview_option( $option, $field, $args = [], $do_echo = true ) { // phpcs:ignore Universal.NamingConventions.NoReservedKeywordParameterNames.echoFound // Skip preview option for the editor. if ( wpforms_is_editor_page() ) { return; } parent::field_preview_option( $option, $field, $args, $do_echo ); } /** * Field display on the form front-end. * * @since 1.9.4 * * @param array $field Field data and settings. * @param array $deprecated Deprecated field attributes. Use field properties. * @param array $form_data Form data and settings. */ public function field_display( $field, $deprecated, $form_data ) { } } Forms/Fields/PaymentCheckbox/Field.php 0000644 00000041410 15174710275 0013671 0 ustar 00 <?php namespace WPForms\Forms\Fields\PaymentCheckbox; use WPForms_Field; /** * Checkbox payment field. * * @since 1.8.2 */ class Field extends WPForms_Field { /** * Primary class constructor. * * @since 1.8.2 */ public function init() { // Define field type information. $this->name = esc_html__( 'Checkbox Items', 'wpforms-lite' ); $this->keywords = esc_html__( 'product, store, ecommerce, pay, payment', 'wpforms-lite' ); $this->type = 'payment-checkbox'; $this->icon = 'fa-check-square-o'; $this->order = 50; $this->group = 'payment'; $this->defaults = [ 1 => [ 'label' => esc_html__( 'First Item', 'wpforms-lite' ), 'value' => '10', 'image' => '', 'icon' => '', 'icon_style' => '', 'default' => '', ], 2 => [ 'label' => esc_html__( 'Second Item', 'wpforms-lite' ), 'value' => '25', 'image' => '', 'icon' => '', 'icon_style' => '', 'default' => '', ], 3 => [ 'label' => esc_html__( 'Third Item', 'wpforms-lite' ), 'value' => '50', 'image' => '', 'icon' => '', 'icon_style' => '', 'default' => '', ], ]; $this->default_settings = [ 'choices' => $this->defaults, ]; $this->hooks(); } /** * Register hooks. * * @since 1.8.1 */ private function hooks() { // Customize HTML field values. add_filter( 'wpforms_html_field_value', [ $this, 'field_html_value' ], 10, 4 ); add_filter( "wpforms_{$this->type}_field_html_value_images", [ $this, 'field_html_value_images' ], 10, 3 ); // Define additional field properties. add_filter( "wpforms_field_properties_{$this->type}", [ $this, 'field_properties' ], 5, 3 ); // This field requires fieldset+legend instead of the field label. add_filter( "wpforms_frontend_modern_is_field_requires_fieldset_{$this->type}", '__return_true', PHP_INT_MAX, 2 ); } /** * Define additional field properties. * * @since 1.8.2 * * @param array $properties Field properties. * @param array $field Field settings. * @param array $form_data Form data and settings. * * @return array */ public function field_properties( $properties, $field, $form_data ) { // phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh // Define data. $form_id = absint( $form_data['id'] ); $field_id = absint( $field['id'] ); $choices = $field['choices']; // Remove primary input, unset for attribute for label. unset( $properties['inputs']['primary'], $properties['label']['attr']['for'] ); // Set input container (ul) properties. $properties['input_container'] = [ 'class' => [], 'data' => [], 'attr' => [], 'id' => "wpforms-{$form_id}-field_{$field_id}", ]; $is_choice_limit_set = ! empty( $field['choice_limit'] ) && (int) $field['choice_limit'] > 0; if ( $is_choice_limit_set ) { $properties['input_container']['data']['choice-limit'] = $field['choice_limit']; } // Set input properties. foreach ( $choices as $key => $choice ) { // Choice labels should not be left blank, but if they are, we provide a basic value. $label = $choice['label']; if ( $label === '' ) { if ( 1 === count( $choices ) ) { $label = esc_html__( 'Checked', 'wpforms-lite' ); } else { /* translators: %s - item number. */ $label = sprintf( esc_html__( 'Item %s', 'wpforms-lite' ), $key ); } } $properties['inputs'][ $key ] = [ 'container' => [ 'attr' => [], 'class' => [ "choice-{$key}" ], 'data' => [], 'id' => '', ], 'label' => [ 'attr' => [ 'for' => "wpforms-{$form_id}-field_{$field_id}_{$key}", ], 'class' => [ 'wpforms-field-label-inline' ], 'data' => [], 'id' => '', 'text' => $label, ], 'attr' => [ 'name' => "wpforms[fields][{$field_id}][]", 'value' => $key, ], 'class' => [ 'wpforms-payment-price' ], 'data' => [ 'amount' => wpforms_format_amount( wpforms_sanitize_amount( $choice['value'] ) ), ], 'id' => "wpforms-{$form_id}-field_{$field_id}_{$key}", 'icon' => $choice['icon'] ?? '', 'icon_style' => $choice['icon_style'] ?? '', 'image' => $choice['image'] ?? '', 'required' => ! empty( $field['required'] ) ? 'required' : '', 'default' => isset( $choice['default'] ), ]; // Rule for validator only if needed. if ( $is_choice_limit_set ) { $properties['inputs'][ $key ]['data']['rule-check-limit'] = 'true'; } } // Required class for pagebreak validation. if ( ! empty( $field['required'] ) ) { $properties['input_container']['class'][] = 'wpforms-field-required'; } // Custom properties if image choices are enabled. if ( ! empty( $field['choices_images'] ) ) { $properties['input_container']['class'][] = 'wpforms-image-choices'; $properties['input_container']['class'][] = 'wpforms-image-choices-' . sanitize_html_class( $field['choices_images_style'] ); foreach ( $properties['inputs'] as $key => $inputs ) { $properties['inputs'][ $key ]['container']['class'][] = 'wpforms-image-choices-item'; if ( in_array( $field['choices_images_style'], [ 'modern', 'classic' ], true ) ) { $properties['inputs'][ $key ]['class'][] = 'wpforms-screen-reader-element'; } } } elseif ( ! empty( $field['choices_icons'] ) ) { $properties = wpforms()->obj( 'icon_choices' )->field_properties( $properties, $field ); } // Add selected class for choices with defaults. foreach ( $properties['inputs'] as $key => $inputs ) { if ( ! empty( $inputs['default'] ) ) { $properties['inputs'][ $key ]['container']['class'][] = 'wpforms-selected'; } } return $properties; } /** * Get field populated single property value. * * @since 1.8.2 * * @param string $raw_value Value from a GET param, always a string. * @param string $input Represent a subfield inside the field. May be empty. * @param array $properties Field properties. * @param array $field Current field specific data. * * @return array Modified field properties. */ protected function get_field_populated_single_property_value( $raw_value, $input, $properties, $field ) { /* * When the form is submitted, we get only choice values from the Fallback. * As payment-checkbox (checkboxes) field doesn't support 'show_values' option - * we should transform that into label to check against using general logic in parent method. */ if ( ! is_string( $raw_value ) || empty( $field['choices'] ) || ! is_array( $field['choices'] ) ) { return $properties; } // The form submits only the sum, so shortcut for Dynamic. if ( ! is_numeric( $raw_value ) ) { return parent::get_field_populated_single_property_value( $raw_value, $input, $properties, $field ); } $get_value = wpforms_format_amount( wpforms_sanitize_amount( $raw_value ) ); foreach ( $field['choices'] as $choice ) { if ( isset( $choice['label'], $choice['value'] ) && wpforms_format_amount( wpforms_sanitize_amount( $choice['value'] ) ) === $get_value ) { $trans_value = $choice['label']; // Stop iterating over choices. break; } } if ( empty( $trans_value ) ) { return $properties; } return parent::get_field_populated_single_property_value( $trans_value, $input, $properties, $field ); } /** * Field options panel inside the builder. * * @since 1.8.2 * * @param array $field Field settings. */ public function field_options( $field ) { /* * Basic field options. */ // Options open markup. $this->field_option( 'basic-options', $field, [ 'markup' => 'open', ] ); // Label. $this->field_option( 'label', $field ); // Choices option. $this->field_option( 'choices_payments', $field ); // Show price after item labels. $fld = $this->field_element( 'toggle', $field, [ 'slug' => 'show_price_after_labels', 'value' => isset( $field['show_price_after_labels'] ) ? '1' : '0', 'desc' => esc_html__( 'Show Price After Item Labels', 'wpforms-lite' ), 'tooltip' => esc_html__( 'Check this option to show price of the item after the label.', 'wpforms-lite' ), ], false ); $args = [ 'slug' => 'show_price_after_labels', 'content' => $fld, ]; $this->field_element( 'row', $field, $args ); // Choices Images. $this->field_option( 'choices_images', $field ); // Hide Choices Images. $this->field_option( 'choices_images_hide', $field ); // Choice Images Style (theme). $this->field_option( 'choices_images_style', $field ); // Choices Icons. $this->field_option( 'choices_icons', $field ); // Choices Icons Color. $this->field_option( 'choices_icons_color', $field ); // Choices Icons Size. $this->field_option( 'choices_icons_size', $field ); // Choices Icons Style. $this->field_option( 'choices_icons_style', $field ); // Description. $this->field_option( 'description', $field ); // Required toggle. $this->field_option( 'required', $field ); // Options close markup. $this->field_option( 'basic-options', $field, [ 'markup' => 'close', ] ); /* * Advanced field options. */ // Options open markup. $this->field_option( 'advanced-options', $field, [ 'markup' => 'open', ] ); // Input columns. $this->field_option( 'input_columns', $field ); // Choice Limit. $this->field_option( 'choice_limit', $field ); // Custom CSS classes. $this->field_option( 'css', $field ); // Hide label. $this->field_option( 'label_hide', $field ); // Options close markup. $this->field_option( 'advanced-options', $field, [ 'markup' => 'close', ] ); } /** * Field preview inside the builder. * * @since 1.8.2 * * @param array $field Field settings. */ public function field_preview( $field ) { // Label. $this->field_preview_option( 'label', $field ); // Choices. $this->field_preview_option( 'choices', $field ); // Description. $this->field_preview_option( 'description', $field ); } /** * Field display on the form front-end. * * @since 1.8.2 * * @param array $field Field settings. * @param array $deprecated Deprecated array. * @param array $form_data Form data and settings. * * @noinspection HtmlUnknownAttribute * @noinspection HtmlUnknownTarget */ public function field_display( $field, $deprecated, $form_data ) { // Define data. $container = $field['properties']['input_container']; $choices = $field['properties']['inputs']; printf( '<ul %s>', wpforms_html_attributes( $container['id'], $container['class'], $container['data'], $container['attr'] ) // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ); foreach ( $choices as $key => $choice ) { $label = $choice['label']['text'] ?? ''; /* translators: %s - item number. */ $label = $label !== '' ? $label : sprintf( esc_html__( 'Item %s', 'wpforms-lite' ), $key ); $label .= ! empty( $field['show_price_after_labels'] ) && isset( $choice['data']['amount'] ) ? $this->get_price_after_label( $choice['data']['amount'] ) : ''; printf( '<li %s>', wpforms_html_attributes( $choice['container']['id'], $choice['container']['class'], $choice['container']['data'], $choice['container']['attr'] ) // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ); if ( empty( $field['dynamic_choices'] ) && ! empty( $field['choices_images'] ) ) { // Image choices. printf( '<label %s>', wpforms_html_attributes( $choice['label']['id'], $choice['label']['class'], $choice['label']['data'], $choice['label']['attr'] ) // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ); echo '<span class="wpforms-image-choices-image">'; if ( ! empty( $choice['image'] ) ) { printf( '<img src="%s" alt="%s"%s>', esc_url( $choice['image'] ), esc_attr( $choice['label']['text'] ), ! empty( $choice['label']['text'] ) ? ' title="' . esc_attr( $choice['label']['text'] ) . '"' : '' ); } echo '</span>'; if ( $field['choices_images_style'] === 'none' ) { echo '<br>'; } printf( '<input type="checkbox" %s %s %s>', wpforms_html_attributes( $choice['id'], $choice['class'], $choice['data'], $choice['attr'] ), // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped esc_attr( $choice['required'] ), checked( '1', $choice['default'], false ) ); echo '<span class="wpforms-image-choices-label">' . wp_kses_post( $label ) . '</span>'; echo '</label>'; } elseif ( empty( $field['dynamic_choices'] ) && ! empty( $field['choices_icons'] ) ) { // Icon Choices. wpforms()->obj( 'icon_choices' )->field_display( $field, $choice, 'checkbox', $label ); } else { // Normal display. printf( '<input type="checkbox" %s %s %s>', wpforms_html_attributes( $choice['id'], $choice['class'], $choice['data'], $choice['attr'] ), // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped esc_attr( $choice['required'] ), checked( '1', $choice['default'], false ) ); printf( '<label %s>%s</label>', wpforms_html_attributes( $choice['label']['id'], $choice['label']['class'], $choice['label']['data'], $choice['label']['attr'] ), // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped wp_kses_post( $label ) ); } echo '</li>'; } echo '</ul>'; } /** * Validate field on submitting the form. * * @since 1.8.2 * * @param int $field_id Field ID. * @param array $field_submit Submitted field value (raw data). * @param array $form_data Form data and settings. */ public function validate( $field_id, $field_submit, $form_data ) { $field_id = (int) $field_id; $error = ''; // Basic required check - If field is marked as required, check for entry data. if ( ! empty( $form_data['fields'][ $field_id ]['required'] ) && empty( $field_submit ) ) { $error = wpforms_get_required_label(); } if ( ! empty( $field_submit ) ) { foreach ( (array) $field_submit as $checked_choice ) { // Validate that the option selected is real. if ( empty( $form_data['fields'][ $field_id ]['choices'][ (int) $checked_choice ] ) ) { $error = esc_html__( 'Invalid payment option.', 'wpforms-lite' ); break; } } } $field_submit = (array) $field_submit; $this->validate_field_choice_limit( $field_id, $field_submit, $form_data ); if ( ! empty( $error ) ) { wpforms()->obj( 'process' )->errors[ $form_data['id'] ][ $field_id ] = $error; } } /** * Format and sanitize field. * * @since 1.8.2 * * @param int $field_id Field ID. * @param array $field_submit Array of selected choice IDs. * @param array $form_data Form data and settings. */ public function format( $field_id, $field_submit, $form_data ) { // phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh, Generic.Metrics.NestingLevel.MaxExceeded $field_submit = array_values( (array) $field_submit ); $field = $form_data['fields'][ $field_id ]; $name = sanitize_text_field( $field['label'] ); $amount = 0; $images = []; $choice_values = []; $choice_labels = []; $choice_keys = []; if ( ! empty( $field_submit ) ) { foreach ( $field_submit as $choice_checked ) { foreach ( $field['choices'] as $choice_id => $choice ) { // Exit early. if ( (int) $choice_checked !== (int) $choice_id ) { continue; } $value = (float) wpforms_sanitize_amount( $choice['value'] ); // Increase the total amount. $amount += $value; $value = wpforms_format_amount( $value, true ); $choice_label = ''; if ( ! empty( $choice['label'] ) ) { $choice_label = sanitize_text_field( $choice['label'] ); $value = $choice_label . ' - ' . $value; } $choice_labels[] = $choice_label; $choice_values[] = $value; $choice_keys[] = $choice_id; } } if ( ! empty( $choice_keys ) && ! empty( $field['choices_images'] ) ) { foreach ( $choice_keys as $choice_key ) { $images[] = ! empty( $field['choices'][ $choice_key ]['image'] ) ? esc_url_raw( $field['choices'][ $choice_key ]['image'] ) : ''; } } } wpforms()->obj( 'process' )->fields[ $field_id ] = [ 'name' => $name, 'value' => implode( "\r\n", $choice_values ), 'value_choice' => implode( "\r\n", $choice_labels ), 'value_raw' => implode( ',', array_map( 'absint', $field_submit ) ), 'amount' => wpforms_format_amount( $amount ), 'amount_raw' => $amount, 'currency' => wpforms_get_currency(), 'images' => $images, 'id' => absint( $field_id ), 'type' => sanitize_key( $this->type ), ]; } } Forms/Fields/DateTime/Field.php 0000644 00000061260 15174710275 0012306 0 ustar 00 <?php namespace WPForms\Forms\Fields\DateTime; use WPForms\Forms\Fields\Traits\ProField as ProFieldTrait; use WPForms_Field; /** * Date / Time field. * * @since 1.9.4 */ class Field extends WPForms_Field { use ProFieldTrait; /** * Field settings defaults. * * @since 1.9.4 */ public const DEFAULTS = [ 'format' => 'date-time', 'date_placeholder' => '', 'date_format' => 'm/d/Y', 'date_type' => 'datepicker', 'time_placeholder' => '', 'time_format' => 'g:i A', 'time_interval' => '30', 'date_limit_days_sun' => '0', 'date_limit_days_mon' => '1', 'date_limit_days_tue' => '1', 'date_limit_days_wed' => '1', 'date_limit_days_thu' => '1', 'date_limit_days_fri' => '1', 'date_limit_days_sat' => '0', 'time_limit_hours_start_hour' => '09', 'time_limit_hours_start_min' => '00', 'time_limit_hours_start_ampm' => 'am', 'time_limit_hours_end_hour' => '06', 'time_limit_hours_end_min' => '00', 'time_limit_hours_end_ampm' => 'pm', ]; /** * Alternative Date Format. * * @since 1.9.4 */ public const ALT_DATE_FORMAT = 'd/m/Y'; /** * Primary class constructor. * * @since 1.9.4 */ public function init() { // Define field type information. $this->name = esc_html__( 'Date / Time', 'wpforms-lite' ); $this->type = 'date-time'; $this->icon = 'fa-calendar-o'; $this->order = 60; $this->group = 'fancy'; $this->default_settings = self::DEFAULTS; $this->init_pro_field(); $this->hooks(); } /** * Hooks. * * @since 1.9.4 */ protected function hooks(): void { // Set custom option wrapper classes. add_filter( 'wpforms_builder_field_option_class', [ $this, 'field_option_class' ], 10, 2 ); } /** * Field options panel inside the builder. * * @since 1.9.4 * * @param array $field Field data and settings. * * @noinspection PackedHashtableOptimizationInspection * @noinspection HtmlUnknownAttribute */ public function field_options( $field ) { // phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh /** * Basic field options */ // Options open markup. $this->field_option( 'basic-options', $field, [ 'markup' => 'open', 'after_title' => $this->get_field_options_notice(), ] ); // Label. $this->field_option( 'label', $field ); // Format option. $format = ! empty( $field['format'] ) ? esc_attr( $field['format'] ) : self::DEFAULTS['format']; $format_label = $this->field_element( 'label', $field, [ 'slug' => 'format', 'value' => esc_html__( 'Format', 'wpforms-lite' ), 'tooltip' => esc_html__( 'Select format for the date field.', 'wpforms-lite' ), ], false ); $format_select = $this->field_element( 'select', $field, [ 'slug' => 'format', 'value' => $format, 'options' => [ 'date-time' => esc_html__( 'Date and Time', 'wpforms-lite' ), 'date' => esc_html__( 'Date', 'wpforms-lite' ), 'time' => esc_html__( 'Time', 'wpforms-lite' ), ], ], false ); $this->field_element( 'row', $field, [ 'slug' => 'format', 'content' => $format_label . $format_select, ] ); // Description. $this->field_option( 'description', $field ); // Required toggle. $this->field_option( 'required', $field ); // Options close markup. $this->field_option( 'basic-options', $field, [ 'markup' => 'close', ] ); /* * Advanced field options */ // Options open markup. $this->field_option( 'advanced-options', $field, [ 'markup' => 'open', ] ); // Size. $this->field_option( 'size', $field ); // Custom options. // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo '<div class="format-selected-' . $format . ' format-selected">'; // Date. $date_placeholder = ! empty( $field['date_placeholder'] ) ? $field['date_placeholder'] : ''; $date_format = ! empty( $field['date_format'] ) ? esc_attr( $field['date_format'] ) : self::DEFAULTS['date_format']; $date_type = ! empty( $field['date_type'] ) ? esc_attr( $field['date_type'] ) : 'datepicker'; // Backwards compatibility with old datepicker format. if ( $date_format === 'mm/dd/yyyy' ) { $date_format = self::DEFAULTS['date_format']; } elseif ( $date_format === 'dd/mm/yyyy' ) { $date_format = self::ALT_DATE_FORMAT; } elseif ( $date_format === 'mmmm d, yyyy' ) { $date_format = 'F j, Y'; } $date_formats = wpforms_date_formats(); printf( '<div class="wpforms-clear wpforms-field-option-row wpforms-field-option-row-date no-gap" id="wpforms-field-option-row-%d-date" data-subfield="date" data-field-id="%d">', esc_attr( $field['id'] ), esc_attr( $field['id'] ) ); $this->field_element( 'label', $field, [ 'slug' => 'date_placeholder', 'value' => esc_html__( 'Date', 'wpforms-lite' ), 'tooltip' => esc_html__( 'Advanced date options.', 'wpforms-lite' ), ] ); echo '<div class="wpforms-field-options-columns-2 wpforms-field-options-columns">'; echo '<div class="type wpforms-field-options-column">'; printf( '<select id="wpforms-field-option-%d-date_type" name="fields[%d][date_type]">', esc_attr( $field['id'] ), esc_attr( $field['id'] ) ); printf( '<option value="datepicker" %s>%s</option>', selected( $date_type, 'datepicker', false ), esc_html__( 'Date Picker', 'wpforms-lite' ) ); printf( '<option value="dropdown" %s>%s</option>', selected( $date_type, 'dropdown', false ), esc_html__( 'Date Dropdown', 'wpforms-lite' ) ); echo '</select>'; printf( '<label for="wpforms-field-option-%d-date_type" class="sub-label">%s</label>', esc_attr( $field['id'] ), esc_html__( 'Type', 'wpforms-lite' ) ); echo '</div>'; echo '<div class="format wpforms-field-options-column">'; printf( '<select id="wpforms-field-option-%d-date_format" name="fields[%d][date_format]">', esc_attr( $field['id'] ), esc_attr( $field['id'] ) ); foreach ( $date_formats as $key => $value ) { if ( in_array( $key, $this->get_regular_date_formats(), true ) ) { printf( '<option value="%s" %s>%s (%s)</option>', esc_attr( $key ), selected( $date_format, $key, false ), esc_html( date( $value ) ), // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date esc_html( $key ) ); } else { printf( '<option value="%s" class="datepicker-only" %s>%s</option>', esc_attr( $key ), selected( $date_format, $key, false ), esc_html( date( $value ) ) // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date ); } } echo '</select>'; printf( '<label for="wpforms-field-option-%d-date_format" class="sub-label">%s</label>', esc_attr( $field['id'] ), esc_html__( 'Format', 'wpforms-lite' ) ); echo '</div>'; echo '</div>'; echo '<div class="placeholder wpforms-field-option-row">'; printf( '<input type="text" class="placeholder" id="wpforms-field-option-%d-date_placeholder" name="fields[%d][date_placeholder]" value="%s">', esc_attr( $field['id'] ), esc_attr( $field['id'] ), esc_attr( $date_placeholder ) ); printf( '<label for="wpforms-field-option-%d-date_placeholder" class="sub-label">%s</label>', esc_attr( $field['id'] ), esc_html__( 'Placeholder', 'wpforms-lite' ) ); echo '</div>'; // Limit Days options. $this->field_options_limit_days( $field ); echo '</div>'; // Time. $time_placeholder = ! empty( $field['time_placeholder'] ) ? $field['time_placeholder'] : ''; $time_format = ! empty( $field['time_format'] ) ? esc_attr( $field['time_format'] ) : self::DEFAULTS['time_format']; $time_formats = wpforms_time_formats(); $time_interval = ! empty( $field['time_interval'] ) ? esc_attr( $field['time_interval'] ) : '30'; /** * Filters the time intervals available for the Time field. * * @since 1.6.0 * * @param array $time_intervals Array of time intervals. */ $time_intervals = apply_filters( // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName 'wpforms_datetime_time_intervals', [ '15' => esc_html__( '15 minutes', 'wpforms-lite' ), '30' => esc_html__( '30 minutes', 'wpforms-lite' ), '60' => esc_html__( '1 hour', 'wpforms-lite' ), ] ); printf( '<div class="wpforms-clear wpforms-field-option-row wpforms-field-option-row-time no-gap" id="wpforms-field-option-row-%d-time" data-subfield="time" data-field-id="%d">', esc_attr( $field['id'] ), esc_attr( $field['id'] ) ); $this->field_element( 'label', $field, [ 'slug' => 'time_placeholder', 'value' => esc_html__( 'Time', 'wpforms-lite' ), 'tooltip' => esc_html__( 'Advanced time options.', 'wpforms-lite' ), ] ); echo '<div class="wpforms-field-options-columns-2 wpforms-field-options-columns">'; echo '<div class="interval wpforms-field-options-column">'; printf( '<select id="wpforms-field-option-%d-time_interval" name="fields[%d][time_interval]">', esc_attr( $field['id'] ), esc_attr( $field['id'] ) ); foreach ( $time_intervals as $key => $value ) { printf( '<option value="%s" %s>%s</option>', esc_attr( $key ), selected( $time_interval, $key, false ), $value // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ); } echo '</select>'; printf( '<label for="wpforms-field-option-%d-time_interval" class="sub-label">%s</label>', esc_attr( $field['id'] ), esc_html__( 'Interval', 'wpforms-lite' ) ); echo '</div>'; echo '<div class="format wpforms-field-options-column">'; printf( '<select id="wpforms-field-option-%d-time_format" name="fields[%d][time_format]">', esc_attr( $field['id'] ), esc_attr( $field['id'] ) ); foreach ( $time_formats as $key => $value ) { printf( '<option value="%s" %s>%s</option>', esc_attr( $key ), selected( $time_format, $key, false ), esc_html( $value ) ); } echo '</select>'; printf( '<label for="wpforms-field-option-%d-time_format" class="sub-label">%s</label>', esc_attr( $field['id'] ), esc_html__( 'Format', 'wpforms-lite' ) ); echo '</div>'; echo '</div>'; echo '<div class="placeholder wpforms-field-option-row">'; printf( '<input type="text" class="placeholder" id="wpforms-field-option-%d-time_placeholder" name="fields[%d][time_placeholder]" value="%s">', esc_attr( $field['id'] ), esc_attr( $field['id'] ), esc_attr( $time_placeholder ) ); printf( '<label for="wpforms-field-option-%d-time_placeholder" class="sub-label">%s</label>', esc_attr( $field['id'] ), esc_html__( 'Placeholder', 'wpforms-lite' ) ); echo '</div>'; // Limit Hours options. $this->field_options_limit_hours( $field ); echo '</div>'; echo '</div>'; // Custom CSS classes. $this->field_option( 'css', $field ); // Hide label. $this->field_option( 'label_hide', $field ); // Hide sublabels. $sublabel_class = isset( $field['format'] ) && $field['format'] !== self::DEFAULTS['format'] ? 'wpforms-hidden' : ''; $this->field_option( 'sublabel_hide', $field, [ 'class' => $sublabel_class ] ); // Options close markup. $this->field_option( 'advanced-options', $field, [ 'markup' => 'close', ] ); } /** * Get regular date formats. * * @since 1.9.8.3 * * @return array */ private function get_regular_date_formats(): array { return [ self::DEFAULTS['date_format'], self::ALT_DATE_FORMAT, 'Y/m/d', 'm.d.Y', 'd.m.Y', 'Y.m.d', ]; } /** * Display limit days options. * * @since 1.9.4 * * @param array $field Field setting. */ private function field_options_limit_days( array $field ): void { echo '<div class="wpforms-clear"></div>'; $output = $this->field_element( 'toggle', $field, [ 'slug' => 'date_limit_days', 'value' => ! empty( $field['date_limit_days'] ) ? '1' : '0', 'desc' => esc_html__( 'Limit Days', 'wpforms-lite' ), 'tooltip' => esc_html__( 'Check this option to adjust which days of the week can be selected.', 'wpforms-lite' ), 'class' => 'wpforms-panel-field-toggle', ], false ); $this->field_element( 'row', $field, [ 'slug' => 'date_limit_days', 'content' => $output, 'class' => 'wpforms-clear', ] ); $week_days = [ 'sun' => esc_html__( 'Sun', 'wpforms-lite' ), 'mon' => esc_html__( 'Mon', 'wpforms-lite' ), 'tue' => esc_html__( 'Tue', 'wpforms-lite' ), 'wed' => esc_html__( 'Wed', 'wpforms-lite' ), 'thu' => esc_html__( 'Thu', 'wpforms-lite' ), 'fri' => esc_html__( 'Fri', 'wpforms-lite' ), 'sat' => esc_html__( 'Sat', 'wpforms-lite' ), ]; // Rearrange days array according to the Start of Week setting. $start_of_week = get_option( 'start_of_week' ); $start_of_week = ! empty( $start_of_week ) ? (int) $start_of_week : 0; if ( $start_of_week > 0 ) { $days_after = $week_days; $days_begin = array_splice( $days_after, 0, $start_of_week ); $days = array_merge( $days_after, $days_begin ); } else { $days = $week_days; } // Limit Days body. $field = $this->field_options_limit_days_body( $days, $field ); // Disable Past Dates. $this->field_options_limit_days_disable_past_dates( $field ); // Disable Today's Date. $output = $this->field_element( 'toggle', $field, [ 'slug' => 'date_disable_todays_date', 'value' => ! empty( $field['date_disable_todays_date'] ) ? '1' : '0', 'desc' => esc_html__( 'Disable Today\'s Date', 'wpforms-lite' ), 'tooltip' => esc_html__( 'Check this option to prevent today\'s date from being selected.', 'wpforms-lite' ), ], false ); $this->field_element( 'row', $field, [ 'slug' => 'date_disable_todays_date', 'content' => $output, 'class' => ! isset( $field['date_disable_past_dates'] ) ? 'wpforms-hide' : '', ] ); } /** * Display limit hours options. * * @since 1.9.4 * * @param array $field Field setting. */ private function field_options_limit_hours( array $field ): void { echo '<div class="wpforms-clear"></div>'; $output = $this->field_element( 'toggle', $field, [ 'slug' => 'time_limit_hours', 'value' => ! empty( $field['time_limit_hours'] ) ? '1' : '0', 'desc' => esc_html__( 'Limit Hours', 'wpforms-lite' ), 'tooltip' => esc_html__( 'Check this option to adjust the range of times that can be selected.', 'wpforms-lite' ), 'class' => 'wpforms-panel-field-toggle', ], false ); $this->field_element( 'row', $field, [ 'slug' => 'time_limit_hours', 'content' => $output, ] ); // Determine a time format type. // If the format contains `g` or `h`, then this is 12-hour format, otherwise 24 hours. $time_format = empty( $field['time_format'] ) || preg_match( '/[gh]/', $field['time_format'] ) ? 12 : 24; // Limit Hours body. $output = $this->field_options_limit_hours_body( $field, $time_format ); printf( '<div class="wpforms-field-option-row wpforms-field-option-row-%1$s %2$s" id="wpforms-field-option-row-%3$d-%1$s" data-toggle="%4$s" data-toggle-value="1" data-field-id="%3$d">%5$s</div>', 'time_limit_hours_options', 'wpforms-panel-field-toggle-body', esc_attr( $field['id'] ), esc_attr( 'fields[' . (int) $field['id'] . '][time_limit_hours]' ), $output // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ); } /** * Generate an array of numeric options for date/time selectors. * * @since 1.9.4 * * @param integer $min Minimum value. * @param integer $max Maximum value. * @param integer $step Step. * * @return array */ private function get_selector_numeric_options( int $min, int $max, int $step = 1 ): array { $range = range( $min, $max, $step ); $options = []; foreach ( $range as $i ) { $value = str_pad( $i, 2, '0', STR_PAD_LEFT ); $options[ $value ] = $value; } return $options; } /** * Add class to field options wrapper to indicate if field confirmation is enabled. * * @since 1.9.4 * * @param string|mixed $css_class CSS class. * @param array $field Field data. * * @return string */ public function field_option_class( $css_class, array $field ): string { $css_class = (string) $css_class; if ( $this->type === $field['type'] ) { $date_type = ! empty( $field['date_type'] ) ? sanitize_html_class( $field['date_type'] ) : 'datepicker'; $css_class .= " wpforms-date-type-$date_type"; } return $css_class; } /** * Field preview inside the builder. * * @since 1.9.4 * * @param array $field Field data and settings. */ public function field_preview( $field ) { $date_placeholder = ! empty( $field['date_placeholder'] ) ? $field['date_placeholder'] : ''; $time_placeholder = ! empty( $field['time_placeholder'] ) ? $field['time_placeholder'] : ''; $format = ! empty( $field['format'] ) ? $field['format'] : self::DEFAULTS['format']; $date_type = ! empty( $field['date_type'] ) ? $field['date_type'] : 'datepicker'; $date_format = ! empty( $field['date_format'] ) ? $field['date_format'] : self::DEFAULTS['date_format']; if ( in_array( $date_format, $this->get_month_day_formats(), true ) ) { $date_first_select = 'MM'; $date_second_select = 'DD'; $date_third_select = 'YYYY'; } elseif ( in_array( $date_format, $this->get_day_month_formats(), true ) ) { $date_first_select = 'DD'; $date_second_select = 'MM'; $date_third_select = 'YYYY'; } else { $date_first_select = 'YYYY'; $date_second_select = 'MM'; $date_third_select = 'DD'; } // Label. $this->field_preview_option( 'label', $field, [ 'label_badge' => $this->get_field_preview_badge(), ] ); printf( '<div class="%s format-selected">', sanitize_html_class( 'format-selected-' . $format ) ); // Date. printf( '<div class="wpforms-date %s">', sanitize_html_class( 'wpforms-date-type-' . $date_type ) ); echo '<div class="wpforms-date-datepicker">'; printf( '<input type="text" placeholder="%s" class="primary-input" readonly>', esc_attr( $date_placeholder ) ); printf( '<label class="wpforms-sub-label">%s</label>', esc_html__( 'Date', 'wpforms-lite' ) ); echo '</div>'; echo '<div class="wpforms-date-dropdown">'; printf( '<select readonly class="first"><option>%s</option></select>', esc_html( $date_first_select ) ); printf( '<select readonly class="second"><option>%s</option></select>', esc_html( $date_second_select ) ); printf( '<select readonly class="third"><option>%s</option></select>', esc_html( $date_third_select ) ); printf( '<label class="wpforms-sub-label">%s</label>', esc_html__( 'Date', 'wpforms-lite' ) ); echo '</div>'; echo '</div>'; // Time. echo '<div class="wpforms-time">'; printf( '<input type="text" placeholder="%s" class="primary-input" readonly>', esc_attr( $time_placeholder ) ); printf( '<label class="wpforms-sub-label">%s</label>', esc_html__( 'Time', 'wpforms-lite' ) ); echo '</div>'; echo '</div>'; // Description. $this->field_preview_option( 'description', $field ); } /** * Get month-day date formats. * * @since 1.9.8.3 * * @return array */ private function get_month_day_formats(): array { return [ 'mm/dd/yyyy', self::DEFAULTS['date_format'], 'm.d.Y' ]; } /** * Get day-month date formats. * * @since 1.9.8.3 * * @return array */ private function get_day_month_formats(): array { return [ 'dd/mm/yyyy', self::ALT_DATE_FORMAT, 'd.m.Y' ]; } /** * Field display on the form front-end. * * @since 1.9.4 * * @param array $field Field data and settings. * @param array $deprecated Deprecated array of field attributes. * @param array $form_data Form data and settings. */ public function field_display( $field, $deprecated, $form_data ) { } /** * Field options: Limit Days body section. * * @since 1.9.4 * * @param array $days Array of days. * @param array $field Field data and settings. * * @return array Modified field data array. */ public function field_options_limit_days_body( array $days, array $field ): array { // Limit Days body. $output = ''; foreach ( $days as $day => $day_translation ) { $day_slug = 'date_limit_days_' . $day; // Set defaults. if ( ! isset( $field['date_format'] ) ) { $field[ $day_slug ] = $this->default_settings[ $day_slug ]; } $output .= '<label class="sub-label">'; $output .= $this->field_element( 'checkbox', $field, [ 'slug' => $day_slug, 'value' => ! empty( $field[ $day_slug ] ) ? '1' : '0', 'nodesc' => '1', 'class' => 'wpforms-field-options-column', ], false ); $output .= '<br>' . $day_translation . '</label>'; } printf( '<div class="wpforms-field-option-row wpforms-field-option-row-date_limit_days_options wpforms-panel-field-toggle-body wpforms-field-options-columns wpforms-field-options-columns-7 checkboxes-row" id="wpforms-field-option-row-%1$d-date_limit_days_options" data-toggle="%2$s" data-toggle-value="1" data-field-id="%1$d">%3$s</div>', esc_attr( $field['id'] ), esc_attr( 'fields[' . (int) $field['id'] . '][date_limit_days]' ), $output // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ); return $field; } /** * Field options: Limit Days - Disable Past Dates section. * * @since 1.9.4 * * @param array $field Field data. */ public function field_options_limit_days_disable_past_dates( array $field ): void { $output = $this->field_element( 'toggle', $field, [ 'slug' => 'date_disable_past_dates', 'value' => ! empty( $field['date_disable_past_dates'] ) ? '1' : '0', 'desc' => esc_html__( 'Disable Past Dates', 'wpforms-lite' ), 'tooltip' => esc_html__( 'Check this option to prevent any previous date from being selected.', 'wpforms-lite' ), ], false ); $this->field_element( 'row', $field, [ 'slug' => 'date_disable_past_dates', 'content' => $output, ] ); } /** * Field options: Limit Hours - body section. * * @since 1.9.4 * * @param array $field Field data. * @param int $time_format Time format. * * @return string */ private function field_options_limit_hours_body( array $field, int $time_format ): string { $output = ''; foreach ( [ 'start', 'end' ] as $option ) { $output .= '<div class="wpforms-field-options-columns wpforms-field-options-columns-4">'; // Open columns container. $slug = 'time_limit_hours_' . $option . '_hour'; $output .= $this->field_element( 'select', $field, [ 'slug' => $slug, 'value' => ! empty( $field[ $slug ] ) ? $field[ $slug ] : $this->default_settings[ $slug ], 'options' => $time_format === 12 ? $this->get_selector_numeric_options( 1, $time_format ) : $this->get_selector_numeric_options( 0, $time_format - 1 ), 'class' => 'wpforms-field-options-column', ], false ); $slug = 'time_limit_hours_' . $option . '_min'; $output .= $this->field_element( 'select', $field, [ 'slug' => $slug, 'value' => ! empty( $field[ $slug ] ) ? $field[ $slug ] : $this->default_settings[ $slug ], 'options' => $this->get_selector_numeric_options( 0, 59, 5 ), 'class' => 'wpforms-field-options-column', ], false ); $slug = 'time_limit_hours_' . $option . '_ampm'; $output .= $this->field_element( 'select', $field, [ 'slug' => $slug, 'value' => ! empty( $field[ $slug ] ) ? $field[ $slug ] : $this->default_settings[ $slug ], 'options' => [ 'am' => 'AM', 'pm' => 'PM', ], 'class' => [ 'wpforms-field-options-column', $time_format === 24 ? 'wpforms-hidden-strict' : '', ], ], false ); $slug = 'time_limit_hours_' . $option . '_hour'; $output .= $this->field_element( 'label', $field, [ 'slug' => $slug, 'value' => $option === 'start' ? esc_html__( 'Start Time', 'wpforms-lite' ) : esc_html__( 'End Time', 'wpforms-lite' ), 'class' => [ 'sub-label', 'wpforms-field-options-column', ], ], false ); $output .= sprintf( '<div class="%s wpforms-field-options-column"></div>', $time_format === 12 ? 'wpforms-hidden-strict' : '' ); $output .= '</div>'; // Close columns container. } return $output; } } Forms/Fields/Base/Frontend.php 0000644 00000001273 15174710275 0012216 0 ustar 00 <?php namespace WPForms\Forms\Fields\Base; use WPForms_Field; /** * Field's Frontend base class. * * @since 1.8.1 */ class Frontend { /** * Instance of the main WPForms_Field_{something} class. * * @since 1.8.1 * * @var WPForms_Field */ protected $field_obj; /** * Class constructor. * * @since 1.8.1 * * @param WPForms_Field $field_obj Instance of the WPForms_Field_{something} class. */ public function __construct( $field_obj ) { $this->field_obj = $field_obj; $this->init(); } /** * Initialize. * * @since 1.8.1 */ public function init() { $this->hooks(); } /** * Hooks. * * @since 1.8.1 */ protected function hooks() { } } Forms/Fields/Rating/Field.php 0000644 00000030366 15174710275 0012041 0 ustar 00 <?php namespace WPForms\Forms\Fields\Rating; use WPForms\Forms\Fields\Traits\ProField as ProFieldTrait; use WPForms_Field; /** * Rating field. * * @since 1.9.4 */ class Field extends WPForms_Field { use ProFieldTrait; /** * Default icon color. * * @since 1.9.4 */ protected const DEFAULT_ICON_COLOR = [ 'classic' => '#e27730', 'modern' => '#066aab', ]; /** * Primary class constructor. * * @since 1.9.4 */ public function init() { // Define field type information. $this->name = esc_html__( 'Rating', 'wpforms-lite' ); $this->keywords = esc_html__( 'review, emoji, star', 'wpforms-lite' ); $this->type = 'rating'; $this->icon = 'fa-star'; $this->order = 310; $this->group = 'fancy'; $this->default_settings = [ 'icon_color' => $this->get_default_icon_color(), ]; $this->init_pro_field(); $this->hooks(); } /** * Hooks. * * @since 1.9.4 */ protected function hooks(): void { add_action( 'wpforms_builder_enqueues', [ $this, 'builder_enqueues' ] ); } /** * Builder enqueues. * * @since 1.9.8 */ public function builder_enqueues(): void { $min = wpforms_get_min_suffix(); wp_enqueue_script( 'wpforms-rating-field', WPFORMS_PLUGIN_URL . "assets/js/admin/builder/fields/rating{$min}.js", [ 'wpforms-builder', 'wpforms-utils' ], WPFORMS_VERSION, false ); } /** * Field options panel inside the builder. * * @since 1.9.4 * * @param array $field Field settings. * * @noinspection PackedHashtableOptimizationInspection */ public function field_options( $field ) { /** * Basic field options. */ // Options open markup. $this->field_option( 'basic-options', $field, [ 'markup' => 'open', 'after_title' => $this->get_field_options_notice(), ] ); // Label. $this->field_option( 'label', $field ); // Description. $this->field_option( 'description', $field ); // Scale. $lbl = $this->field_element( 'label', $field, [ 'slug' => 'scale', 'value' => esc_html__( 'Scale', 'wpforms-lite' ), 'tooltip' => esc_html__( 'Select rating scale', 'wpforms-lite' ), ], false ); $fld = $this->field_element( 'select', $field, [ 'slug' => 'scale', 'value' => ! empty( $field['scale'] ) ? esc_attr( $field['scale'] ) : '5', 'options' => [ '2' => '2', '3' => '3', '4' => '4', '5' => '5', '6' => '6', '7' => '7', '8' => '8', '9' => '9', '10' => '10', ], ], false ); $this->field_element( 'row', $field, [ 'slug' => 'scale', 'content' => $lbl . $fld, ] ); // Required toggle. $this->field_option( 'required', $field ); // Options close markup. $this->field_option( 'basic-options', $field, [ 'markup' => 'close', ] ); /* * Advanced field options. */ // Options open markup. $this->field_option( 'advanced-options', $field, [ 'markup' => 'open', ] ); // Icon. $lbl = $this->field_element( 'label', $field, [ 'slug' => 'icon', 'value' => esc_html__( 'Icon', 'wpforms-lite' ), 'tooltip' => esc_html__( 'Select icon to display', 'wpforms-lite' ), ], false ); $fld = $this->field_element( 'select', $field, [ 'slug' => 'icon', 'value' => ! empty( $field['icon'] ) ? esc_attr( $field['icon'] ) : 'star', 'options' => [ 'star' => esc_html__( 'Star', 'wpforms-lite' ), 'heart' => esc_html__( 'Heart', 'wpforms-lite' ), 'thumb' => esc_html__( 'Thumb', 'wpforms-lite' ), 'smiley' => esc_html__( 'Smiley Face', 'wpforms-lite' ), ], ], false ); $this->field_element( 'row', $field, [ 'slug' => 'icon', 'content' => $lbl . $fld, ] ); // Icon size. $lbl = $this->field_element( 'label', $field, [ 'slug' => 'icon_size', 'value' => esc_html__( 'Icon Size', 'wpforms-lite' ), 'tooltip' => esc_html__( 'Select the size of the rating icon', 'wpforms-lite' ), ], false ); $fld = $this->field_element( 'select', $field, [ 'slug' => 'icon_size', 'value' => ! empty( $field['icon_size'] ) ? esc_attr( $field['icon_size'] ) : 'medium', 'options' => [ 'small' => esc_html__( 'Small', 'wpforms-lite' ), 'medium' => esc_html__( 'Medium', 'wpforms-lite' ), 'large' => esc_html__( 'Large', 'wpforms-lite' ), ], ], false ); $this->field_element( 'row', $field, [ 'slug' => 'icon_size', 'content' => $lbl . $fld, ] ); $this->score_labels( $field ); // Icon color picker. $lbl = $this->field_element( 'label', $field, [ 'slug' => 'icon_color', 'value' => esc_html__( 'Icon Color', 'wpforms-lite' ), 'tooltip' => esc_html__( 'Select the color for the rating icon', 'wpforms-lite' ), ], false ); $icon_color = isset( $field['icon_color'] ) ? wpforms_sanitize_hex_color( $field['icon_color'] ) : ''; $icon_color = empty( $icon_color ) ? $this->get_default_icon_color() : $icon_color; $fld = $this->field_element( 'color', $field, [ 'slug' => 'icon_color', 'value' => $icon_color, 'data' => [ 'fallback-color' => $icon_color, ], ], false ); $this->field_element( 'row', $field, [ 'slug' => 'icon_color', 'content' => $lbl . $fld, 'class' => 'color-picker-row', ] ); // Custom CSS classes. $this->field_option( 'css', $field ); // Hide label. $this->field_option( 'label_hide', $field ); // Options close markup. $this->field_option( 'advanced-options', $field, [ 'markup' => 'close', ] ); } /** * Score labels. * * @since 1.9.8 * * @param array $field Field settings. */ private function score_labels( array $field ): void { // Lowest score label. $lowest_label = $this->field_element( 'label', $field, [ 'slug' => 'lowest_label', 'value' => esc_html__( 'Lowest Score Label', 'wpforms-lite' ), 'tooltip' => esc_html__( 'This label indicates the lowest score on the scale.', 'wpforms-lite' ), ], false ); $lowest_field = $this->field_element( 'text', $field, [ 'slug' => 'lowest_label', 'value' => $field['lowest_label'] ?? '', ], false ); $this->field_element( 'row', $field, [ 'slug' => 'lowest_label', 'content' => $lowest_label . $lowest_field, ] ); // Highest score label. $highest_label = $this->field_element( 'label', $field, [ 'slug' => 'highest_label', 'value' => esc_html__( 'Highest Score Label', 'wpforms-lite' ), 'tooltip' => esc_html__( 'This label indicates the highest score on the scale.', 'wpforms-lite' ), ], false ); $highest_field = $this->field_element( 'text', $field, [ 'slug' => 'highest_label', 'value' => $field['highest_label'] ?? '', ], false ); $this->field_element( 'row', $field, [ 'slug' => 'highest_label', 'content' => $highest_label . $highest_field, ] ); // Label position. $label_position = $this->field_element( 'label', $field, [ 'slug' => 'label_position', 'value' => esc_html__( 'Label Position', 'wpforms-lite' ), 'tooltip' => esc_html__( 'Select the position of the label', 'wpforms-lite' ), ], false ); $select_position = $this->field_element( 'select', $field, [ 'slug' => 'label_position', 'value' => ! empty( $field['label_position'] ) ? esc_attr( $field['label_position'] ) : 'below', 'options' => [ 'above' => esc_html__( 'Above', 'wpforms-lite' ), 'below' => esc_html__( 'Below', 'wpforms-lite' ), ], ], false ); $this->field_element( 'row', $field, [ 'slug' => 'label_position', 'content' => $label_position . $select_position, ] ); } /** * Field preview inside the builder. * * @since 1.9.4 * * @param array $field Field settings. */ public function field_preview( $field ): void { // Label. $this->field_preview_option( 'label', $field, [ 'label_badge' => $this->get_field_preview_badge(), ] ); echo '<div class="wpforms-rating-field">'; $this->get_field_preview_icons( $field ); $this->get_field_preview_labels( $field ); echo '</div>'; // Description. $this->field_preview_option( 'description', $field ); } /** * Get field preview icons. * * @since 1.9.8 * * @param array $field Field settings. */ private function get_field_preview_icons( array $field ): void { // Define data. $scale = ! empty( $field['scale'] ) ? esc_attr( $field['scale'] ) : 5; $icon = ! empty( $field['icon'] ) ? esc_attr( $field['icon'] ) : 'star'; $icon_size = ! empty( $field['icon_size'] ) ? esc_attr( $field['icon_size'] ) : 'medium'; $icon_color = ! empty( $field['icon_color'] ) ? esc_attr( $field['icon_color'] ) : $this->get_default_icon_color(); $icon_class = $this->get_preview_icon_class( $icon ); // Set icon size. $icon_size_css = $this->get_icon_size_css( $icon_size ); echo '<div class="wpforms-rating-field-icons">'; // Primary input. for ( $i = 1; $i <= 10; $i++ ) { printf( '<i class="fa %s %s rating-icon" aria-hidden="true" style="color:%s; display:%s; font-size:%dpx;"></i>', esc_attr( $icon_class ), esc_attr( $icon_size ), esc_attr( $icon_color ), $i <= $scale ? 'inline-block' : 'none', esc_attr( $icon_size_css ) ); } echo '</div>'; } /** * Get preview icon class based on the selected icon. * * @since 1.9.8 * * @param string $icon Selected icon. * * @return string Icon class. */ private function get_preview_icon_class( string $icon ): string { $icon_class = ''; // Set icon class. switch ( $icon ) { case 'star': $icon_class = 'fa-star'; break; case 'heart': $icon_class = 'fa-heart'; break; case 'thumb': $icon_class = 'fa-thumbs-up'; break; case 'smiley': $icon_class = 'fa-smile-o'; break; } return $icon_class; } /** * Get field preview labels. * * @since 1.9.8 * * @param array $field Field settings. */ private function get_field_preview_labels( array $field ): void { // Lowest score label. $lowest_label = ! empty( $field['lowest_label'] ) ? esc_html( $field['lowest_label'] ) : ''; // Highest score label. $highest_label = ! empty( $field['highest_label'] ) ? esc_html( $field['highest_label'] ) : ''; $class = [ 'wpforms-rating-field-labels' ]; if ( ! empty( $field['label_position'] ) && $field['label_position'] === 'above' ) { $class[] = 'wpforms-rating-field-labels-position-above'; } if ( empty( $lowest_label ) && empty( $highest_label ) ) { $class[] = 'wpforms-hidden'; } echo '<div class=" ' . wpforms_sanitize_classes( $class, true ) . ' ">'; printf( '<span class="wpforms-rating-field-lowest-label wpforms-sub-label">%s</span>', esc_html( $lowest_label ) ); printf( '<span class="wpforms-rating-field-highest-label wpforms-sub-label">%s</span>', esc_html( $highest_label ) ); echo '</div>'; } /** * Field display on the form front-end. * * @since 1.9.4 * * @param array $field Field settings. * @param array $deprecated Deprecated, don't use. * @param array $form_data Form data and settings. */ public function field_display( $field, $deprecated, $form_data ) { } /** * Get icon size CSS value in pixels. * * @since 1.9.4 * * @param string $icon_size Icon size value. */ protected function get_icon_size_css( $icon_size ): string { $render_engine = wpforms_get_render_engine(); $icon_sizes = [ 'classic' => [ 'small' => '18', 'medium' => '28', 'large' => '38', ], 'modern' => [ 'small' => '16', 'medium' => '24', 'large' => '38', ], ]; $default = $render_engine === 'modern' ? '24' : '28'; return ! empty( $icon_sizes[ $render_engine ][ $icon_size ] ) ? $icon_sizes[ $render_engine ][ $icon_size ] : $default; } /** * Get default icon color. * * @since 1.9.4 * * @return string */ public function get_default_icon_color(): string { $render_engine = wpforms_get_render_engine(); return array_key_exists( $render_engine, self::DEFAULT_ICON_COLOR ) ? self::DEFAULT_ICON_COLOR[ $render_engine ] : self::DEFAULT_ICON_COLOR['modern']; } } Forms/Fields/PaymentTotal/Field.php 0000644 00000057155 15174710275 0013243 0 ustar 00 <?php namespace WPForms\Forms\Fields\PaymentTotal; use WPForms\Forms\Fields\Helpers\RequirementsAlerts; use WPForms_Builder_Panel_Settings; use WPForms_Field; /** * Total payment field. * * @since 1.8.2 */ class Field extends WPForms_Field { /** * Primary class constructor. * * @since 1.8.2 */ public function init() { // Define field type information. $this->name = esc_html__( 'Total', 'wpforms-lite' ); $this->keywords = esc_html__( 'store, ecommerce, pay, payment, sum', 'wpforms-lite' ); $this->type = 'payment-total'; $this->icon = 'fa-money'; $this->order = 110; $this->group = 'payment'; $this->allow_read_only = false; $this->hooks(); } /** * Hooks. * * @since 1.8.2 */ private function hooks(): void { // Define additional field properties. add_filter( "wpforms_field_properties_{$this->type}", [ $this, 'field_properties' ], 5, 3 ); // Recalculate total for a form. add_filter( 'wpforms_process_filter', [ $this, 'calculate_total' ], 10, 3 ); // Add classes to the builder field preview. add_filter( 'wpforms_field_preview_class', [ $this, 'preview_field_class' ], 10, 2 ); // Add a new option on the confirmation page. add_action( 'wpforms_form_settings_confirmations_single_after', [ $this, 'add_confirmation_setting' ], 10, 2 ); add_action( 'wpforms_lite_form_settings_confirmations_single_after', [ $this, 'add_confirmation_setting' ], 10, 2 ); add_action( 'wpforms_frontend_confirmation_message_after', [ $this, 'order_summary_confirmation' ], 10, 4 ); } /** * Define additional field properties. * * @since 1.8.2 * * @param array $properties Field properties. * @param array $field Field data and settings. * @param array $form_data Form data and settings. * * @return array * @noinspection PhpMissingParamTypeInspection * @noinspection PhpUnusedParameterInspection */ public function field_properties( $properties, $field, $form_data ) { // Input Primary: initial total is always zero. $properties['inputs']['primary']['attr']['value'] = '0'; // Input Primary: add class for targeting calculations. $properties['inputs']['primary']['class'][] = 'wpforms-payment-total'; // Input Primary: add a data attribute if total is required. if ( ! empty( $field['required'] ) ) { $properties['inputs']['primary']['data']['rule-required-payment'] = true; } // Check size. if ( ! empty( $field['size'] ) ) { $properties['container']['class'][] = 'wpforms-field-' . esc_attr( $field['size'] ); } // Input Primary: add class for targeting summary. if ( $this->is_summary_enabled( $field ) ) { $properties['container']['class'][] = 'wpforms-summary-enabled'; } // Unset for attribute for label. unset( $properties['label']['attr']['for'] ); return $properties; } /** * Whether the current field can be populated dynamically. * * @since 1.8.2 * * @param array $properties Field properties. * @param array $field Current field specific data. * * @return bool */ public function is_dynamic_population_allowed( $properties, $field ): bool { return false; } /** * Whether the current field can be populated dynamically. * * @since 1.8.2 * * @param array $properties Field properties. * @param array $field Current field specific data. * * @return bool */ public function is_fallback_population_allowed( $properties, $field ): bool { return false; } /** * Do not trust the posted total since that relies on JavaScript. * * Instead, we re-calculate on the server side. * * @since 1.8.2 * * @param array $fields List of fields with their data. * @param array $entry Submitted form data. * @param array $form_data Form data and settings. * * @return array */ public function calculate_total( $fields, $entry, $form_data ) { return self::calculate_total_static( $fields, $entry, $form_data ); } /** * Static version of calculate_total(). * * @since 1.8.4 * * @param array $fields List of fields with their data. * @param array $entry Submitted form data. * @param array $form_data Form data and settings. * * @return array * @noinspection PhpMissingParamTypeInspection * @noinspection PhpUnusedParameterInspection */ public static function calculate_total_static( $fields, $entry, $form_data ) { if ( ! is_array( $fields ) ) { return $fields; } // At this point we have passed processing and validation, so we know // the amounts in $fields are safe to use. $total = wpforms_get_total_payment( $fields ); $amount = wpforms_sanitize_amount( $total ); foreach ( $fields as $id => $field ) { if ( ! empty( $field['type'] ) && $field['type'] === 'payment-total' ) { $fields[ $id ]['value'] = wpforms_format_amount( $amount, true ); $fields[ $id ]['amount'] = wpforms_format_amount( $amount ); $fields[ $id ]['amount_raw'] = $amount; } } return $fields; } /** * Field options panel inside the builder. * * @since 1.8.2 * * @param array $field Field data and settings. */ public function field_options( $field ) { /* * Basic field options. */ // Options open markup. $args = [ 'markup' => 'open', ]; $this->field_option( 'basic-options', $field, $args ); // Label. $this->field_option( 'label', $field ); // Description. $this->field_option( 'description', $field ); // Enable Summary. $this->summary_option( $field ); // Summary Notice. $this->summary_option_notice( $field ); // Required toggle. $this->field_option( 'required', $field ); // Options close markup. $args = [ 'markup' => 'close', ]; $this->field_option( 'basic-options', $field, $args ); /* * Advanced field options. */ // Options open markup. $args = [ 'markup' => 'open', ]; $this->field_option( 'advanced-options', $field, $args ); // Size. $this->field_option( 'size', $field, [ 'exclude' => [ 'small' ], // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_exclude ] ); // Custom CSS classes. $this->field_option( 'css', $field ); // Hide label. $this->field_option( 'label_hide', $field ); // Options close markup. $args = [ 'markup' => 'close', ]; $this->field_option( 'advanced-options', $field, $args ); } /** * Field preview inside the builder. * * @since 1.8.2 * * @param array $field Field data and settings. */ public function field_preview( $field ) { // Label. $this->field_preview_option( 'label', $field ); [ $items, $foot, $total_width ] = $this->prepare_builder_preview_data(); // Summary preview. // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo wpforms_render( 'fields/total/summary-preview', [ 'items' => $items, 'foot' => $foot, 'total_width' => $total_width, ], true ); // Primary field. echo '<div class="wpforms-total-amount">' . esc_html( wpforms_format_amount( 0, true ) ) . '</div>'; // Description. $this->field_preview_option( 'description', $field ); } /** * Field display on the form front-end. * * @since 1.8.2 * * @param array $field Field data and settings. * @param array $deprecated Deprecated, not used parameter. * @param array $form_data Form data and settings. * * @noinspection HtmlWrongAttributeValue * @noinspection HtmlUnknownAttribute */ public function field_display( $field, $deprecated, $form_data ) { $primary = $field['properties']['inputs']['primary']; $type = ! empty( $field['required'] ) ? 'text' : 'hidden'; $attrs = $primary['attr']; if ( ! empty( $field['required'] ) ) { $attrs['style'] = 'position:absolute!important;clip:rect(0,0,0,0)!important;height:1px!important;width:1px!important;border:0!important;overflow:hidden!important;padding:0!important;margin:0!important;'; $attrs['readonly'] = 'readonly'; } // aria-errormessage attribute is not allowed for hidden inputs. unset( $attrs['aria-errormessage'] ); $is_summary_enabled = $this->is_summary_enabled( $field ); // Prepare data for the order summary preview if summary is enabled, or we are on the editor page. if ( $is_summary_enabled || wpforms_is_editor_page() ) { [ $items, $foot, $total_width ] = $this->prepare_payment_fields_data( $form_data ); } if ( $is_summary_enabled ) { /** * Allow filtering form data before displaying the order summary table. * * @since 1.9.3 * * @param array $form_data Form data. * * @return array */ $form_data = apply_filters( 'wpforms_forms_fields_payment_total_field_display_form_data', $form_data ); // Summary preview. // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo wpforms_render( 'fields/total/summary-preview', [ 'items' => $items, 'foot' => $foot, 'total_width' => $total_width, ], true ); } $amount = wpforms_format_amount( 0, true ); // If we are on the editor page, we need to get the total amount from the last item in the foot. if ( ! empty( $foot ) && wpforms_is_editor_page() ) { $foot_item = end( $foot ); $amount = $foot_item['amount'] ?? 0; } // Always print total to cover a case when a field is embedded into a Layout column with 25% width. $hidden_style = $is_summary_enabled ? 'display:none' : ''; // This displays the total the user sees. printf( '<div class="wpforms-payment-total" style="%1$s">%2$s</div>', esc_attr( $hidden_style ), esc_html( $amount ) ); // Hidden input for processing. printf( '<input type="%s" %s>', esc_attr( $type ), wpforms_html_attributes( $primary['id'], $primary['class'], $primary['data'], $attrs ) // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ); } /** * Validate field on form submitting. * * @since 1.8.2 * * @param int $field_id Field ID. * @param string $field_submit Submitted field value (raw data). * @param array $form_data Form data and settings. */ public function validate( $field_id, $field_submit, $form_data ) { // Basic required check - If a field is marked as required, check for entry data. if ( ! empty( $form_data['fields'][ $field_id ]['required'] ) && ( empty( $field_submit ) || wpforms_sanitize_amount( $field_submit ) <= 0 ) ) { wpforms()->obj( 'process' )->errors[ $form_data['id'] ][ $field_id ] = esc_html__( 'Payment is required.', 'wpforms-lite' ); } } /** * Format and sanitize field. * * @since 1.8.2 * * @param int $field_id Field ID. * @param string $field_submit Field value submitted by a user. * @param array $form_data Form data and settings. */ public function format( $field_id, $field_submit, $form_data ) { // Define data. $name = ! empty( $form_data['fields'][ $field_id ]['label'] ) ? $form_data['fields'][ $field_id ]['label'] : ''; $amount = wpforms_sanitize_amount( $field_submit ); // Set final field details. wpforms()->obj( 'process' )->fields[ $field_id ] = [ 'name' => sanitize_text_field( $name ), 'value' => wpforms_format_amount( $amount, true ), 'amount' => wpforms_format_amount( $amount ), 'amount_raw' => $amount, 'id' => absint( $field_id ), 'type' => sanitize_key( $this->type ), ]; } /** * Summary option. * * @since 1.8.7 * * @param array $field Field data and settings. */ private function summary_option( array $field ): void { $is_allowed = RequirementsAlerts::is_order_summary_allowed(); $toggle_data = [ 'slug' => 'summary', 'value' => $this->is_summary_enabled( $field ), 'desc' => esc_html__( 'Enable Summary', 'wpforms-lite' ), 'tooltip' => esc_html__( 'Enable order summary for this field.', 'wpforms-lite' ), ]; if ( ! $is_allowed ) { $toggle_data['attrs'] = [ 'disabled' => 'disabled' ]; $toggle_data['control-class'] = 'wpforms-toggle-control-disabled'; } $output = $this->field_element( 'toggle', $field, $toggle_data, false ); $this->field_element( 'row', $field, [ 'slug' => 'summary', 'content' => $output, ] ); if ( ! $is_allowed ) { $this->field_element( 'row', $field, [ 'slug' => 'summary_alert', 'content' => RequirementsAlerts::get_order_summary_alert(), ] ); } } /** * Summary notice on the options' tab. * * @since 1.8.7 * * @param array $field Field data and settings. */ private function summary_option_notice( array $field ): void { $notice = __( 'Example data is shown in the form editor. Actual products and totals will be displayed when you preview or embed your form.', 'wpforms-lite' ); $is_notice_hidden = ! $this->is_summary_enabled( $field ) ? 'wpforms-hidden' : ''; printf( '<div class="wpforms-alert-info wpforms-alert wpforms-total-summary-alert %1$s"> <p>%2$s</p> </div>', esc_attr( $is_notice_hidden ), esc_html( $notice ) ); } /** * Determine if a summary option is enabled. * * @since 1.8.7 * * @param array $field Field data and settings. */ private function is_summary_enabled( array $field ) { return ! empty( $field['summary'] ); } /** * Prepare fake fields data for builder preview. * * @since 1.8.7 * * @return array */ private function prepare_builder_preview_data(): array { $items = [ [ 'label' => __( 'Example Product 1', 'wpforms-lite' ), 'quantity' => 3, 'amount' => wpforms_format_amount( 30, true ), 'is_hidden' => false, ], [ 'label' => __( 'Example Product 2', 'wpforms-lite' ), 'quantity' => 2, 'amount' => wpforms_format_amount( 20, true ), 'is_hidden' => false, ], [ 'label' => __( 'Example Product 3', 'wpforms-lite' ), 'quantity' => 1, 'amount' => wpforms_format_amount( 10, true ), 'is_hidden' => false, ], ]; $total = 60; /** * Allow filtering items in the footer on the order summary table (builder screen). * * @since 1.8.7 * * @param array $fields Order summary footer. * @param int $total Fields total. */ $foot = (array) apply_filters( 'wpforms_forms_fields_payment_total_field_builder_order_summary_preview_foot', [], $total ); /** * Allow filtering builder order summary fields total. * * @since 1.8.7 * * @param string $total Fields total. */ $total = apply_filters( 'wpforms_forms_fields_payment_total_field_builder_order_summary_preview_total', $total ); $total = wpforms_format_amount( $total, true ); $foot[] = [ 'label' => __( 'Total', 'wpforms-lite' ), 'quantity' => '', 'amount' => $total, 'class' => 'wpforms-order-summary-preview-total', ]; $total_width = strlen( html_entity_decode( $total, ENT_COMPAT, 'UTF-8' ) ) + 4; /** * Allow filtering builder order summary total column width. * * @since 1.8.7 * * @param int $total_width Total column width. */ $total_width = (int) apply_filters( 'wpforms_forms_fields_payment_total_field_builder_order_summary_preview_total_width', $total_width ); return [ $items, $foot, $total_width ]; } /** * Prepare payment fields data for summary preview. * * @since 1.8.7 * * @param array $form_data Form data. * * @return array */ private function prepare_payment_fields_data( array $form_data ): array { $payment_fields = wpforms_payment_fields(); $fields = []; $foot = []; $total = 0; foreach ( $form_data['fields'] as $field ) { if ( ( ! isset( $field['price'] ) && empty( $field['choices'] ) ) || ! in_array( $field['type'], $payment_fields, true ) ) { continue; } $this->prepare_payment_field_choices( $field, $fields, $total ); $this->prepare_payment_field_single( $field, $fields, $total ); } /** * Allow filtering items in the order summary footer. * * @since 1.8.7 * * @param array $fields Fields. */ $foot = (array) apply_filters( 'wpforms_forms_fields_payment_total_field_order_summary_preview_foot', $foot ); $total = wpforms_format_amount( $total, true ); $foot[] = [ 'label' => __( 'Total', 'wpforms-lite' ), 'quantity' => '', 'amount' => $total, 'class' => 'wpforms-order-summary-preview-total', ]; return [ $fields, $foot, strlen( html_entity_decode( $total, ENT_COMPAT, 'UTF-8' ) ) + 3 ]; } /** * Prepare payment single data for summary preview. * * @since 1.8.7 * * @param array $field Field data. * @param array $fields Fields data. * @param float $total Fields total. */ private function prepare_payment_field_single( array $field, array &$fields, float &$total ): void { if ( ! empty( $field['choices'] ) ) { return; } $quantity = $this->get_payment_field_min_quantity( $field ); $field_amount = $this->get_payment_field_single_amount( $field, $quantity ); $classes = [ 'wpforms-order-summary-field' ]; $format = $field['format'] ?? ''; $is_conditionally_hidden = $this->is_conditionally_hidden( $field ); if ( $format === 'hidden' ) { $classes[] = 'wpforms-hidden'; } $fields[] = [ 'label' => ! empty( $field['label_hide'] ) ? '' : $field['label'], 'quantity' => $quantity, 'amount' => wpforms_format_amount( $field_amount, true ), 'is_hidden' => ! $quantity || $is_conditionally_hidden, 'class' => $classes, 'data' => [ 'field' => $field['id'], ], ]; if ( ! $is_conditionally_hidden ) { $total += $field_amount; } } /** * Prepare payment field choices data for summary preview. * * @since 1.8.7 * * @param array $field Field data. * @param array $fields Fields data. * @param float $total Fields total. */ private function prepare_payment_field_choices( array $field, array &$fields, float &$total ): void { if ( empty( $field['choices'] ) ) { return; } $quantity = $this->get_payment_field_min_quantity( $field ); $default_choice_key = $this->get_classic_dropdown_default_choice_key( $field ); $is_conditionally_hidden = $this->is_conditionally_hidden( $field ); foreach ( $field['choices'] as $key => $choice ) { $choice_amount = ! empty( $choice['value'] ) ? wpforms_sanitize_amount( $choice['value'] ) * $quantity : 0; $is_default = ! empty( $choice['default'] ) || ( isset( $default_choice_key ) && (int) $key === $default_choice_key ); /* translators: %s - item number. */ $choice_label = ! empty( $choice['label'] ) ? $choice['label'] : sprintf( esc_html__( 'Item %s', 'wpforms-lite' ), $key ); $fields[] = [ 'label' => ! empty( $field['label_hide'] ) ? $choice_label : $field['label'] . ' - ' . $choice_label, 'quantity' => $quantity, 'amount' => wpforms_format_amount( $choice_amount, true ), 'is_hidden' => ! $is_default || ! $quantity || $is_conditionally_hidden, 'class' => 'wpforms-order-summary-field', 'data' => [ 'field' => $field['id'], 'choice' => $key, ], ]; if ( $is_default && ! $is_conditionally_hidden ) { $total += $choice_amount; } } } /** * The `array_key_first` polyfill. * * @since 1.9.3 * * @param array|mixed $arr Input array. * * @return int|string|null */ private function array_key_first( $arr ) { $array = (array) $arr; return empty( $array ) ? null : array_keys( $array )[0]; } /** * Get the classic dropdown default choice key. * * @since 1.8.7 * * @param array $field Field Settings. * * @return int|null */ private function get_classic_dropdown_default_choice_key( array $field ) { if ( $field['type'] !== 'payment-select' || $field['style'] !== 'classic' || ! empty( $field['placeholder'] ) ) { return null; } foreach ( $field['choices'] as $key => $choice ) { if ( ! isset( $choice['default'] ) ) { continue; } return (int) $key; } return $this->array_key_first( $field['choices'] ); } /** * Get payment field minimum quantity. * * @since 1.8.7 * * @param array $field Field data. * * @return int */ private function get_payment_field_min_quantity( array $field ): int { if ( ! wpforms_payment_has_quantity( $field, $this->form_data ) || ! isset( $field['min_quantity'] ) ) { return 1; } // Ensure non-negative quantity. return max( 0, (int) $field['min_quantity'] ); } /** * Add a class to the builder field preview. * * @since 1.8.7 * * @param string $css Class names. * @param array $field Field properties. * * @return string */ public function preview_field_class( $css, $field ) { if ( $field['type'] !== $this->type ) { return $css; } if ( $this->is_summary_enabled( $field ) ) { $css .= ' wpforms-summary-enabled'; } return $css; } /** * Add an order summary to the confirmation settings. * * @since 1.8.7 * * @param WPForms_Builder_Panel_Settings $settings Settings. * @param int $field_id Field ID. */ public function add_confirmation_setting( $settings, int $field_id ): void { wpforms_panel_field( 'toggle', 'confirmations', 'message_order_summary', $settings->form_data, esc_html__( 'Show order summary after confirmation message', 'wpforms-lite' ), [ 'input_id' => 'wpforms-panel-field-confirmations-message_order_summary-' . $field_id, 'input_class' => 'wpforms-panel-field-confirmations-message_order_summary', 'parent' => 'settings', 'subsection' => $field_id, ] ); } /** * Show the order summary on the confirmation page. * * @since 1.8.7 * * @param array $confirmation Current confirmation data. * @param array $form_data Form data and settings. * @param array $fields Sanitized field data. * @param int $entry_id Entry id. */ public function order_summary_confirmation( array $confirmation, array $form_data, array $fields, int $entry_id ): void { if ( empty( $confirmation['message_order_summary'] ) ) { return; } $total_exists = false; foreach ( $fields as $field ) { if ( $field['type'] !== $this->type ) { continue; } $total_exists = true; break; } // Check if the total field exists on the form. if ( ! $total_exists ) { return; } echo '<div class="wpforms-confirmation-container-order-summary">'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo wpforms_process_smart_tags( '{order_summary}', $form_data, $fields, $entry_id, 'payment-total-order-summary-confirmation' ); echo '</div>'; } /** * Calculates the total amount for a single payment field based on its price and quantity. * * @since 1.9.5 * * @param array $field The payment field data containing the price. * @param int $quantity The quantity of the field specified. * * @return float|int The calculated total amount for the payment field. */ private function get_payment_field_single_amount( array $field, int $quantity ) { if ( empty( $field['price'] ) ) { return 0; } return wpforms_sanitize_amount( $field['price'] ) * $quantity; } /** * Determines if a field is conditionally hidden based on its settings and conditions. * * Note: This is a simplified implementation that assumes fields with 'show' conditional * logic are hidden by default, without evaluating the actual conditions. This approach * was chosen to avoid complex condition evaluation during form rendering. * * @since 1.9.5 * * @param array $field Field data, including conditional logic settings. * * @return bool True if the field is conditionally hidden, false otherwise. */ private function is_conditionally_hidden( array $field ): bool { return wpforms()->is_pro() && wpforms_conditional_logic_fields()->field_is_conditional( $field ) && ( $field['conditional_type'] ?? '' ) === 'show'; } } Forms/Fields/Divider/Field.php 0000644 00000006732 15174710275 0012203 0 ustar 00 <?php namespace WPForms\Forms\Fields\Divider; use WPForms\Forms\Fields\Traits\ProField as ProFieldTrait; use WPForms_Field; /** * Section Divider field. * * @since 1.9.4 */ class Field extends WPForms_Field { use ProFieldTrait; /** * Primary class constructor. * * @since 1.9.4 */ public function init() { // Define field type information. $this->name = esc_html__( 'Section Divider', 'wpforms-lite' ); $this->keywords = esc_html__( 'line, hr', 'wpforms-lite' ); $this->type = 'divider'; $this->icon = 'fa-arrows-h'; $this->order = 170; $this->group = 'fancy'; $this->allow_read_only = false; $this->default_settings = [ 'label_disable' => '1', ]; $this->init_pro_field(); $this->hooks(); } /** * Hooks. * * @since 1.9.4 */ protected function hooks() { } /** * Field options panel inside the builder. * * @since 1.9.4 * * @param array $field Field data. */ public function field_options( $field ) { /* * Basic field options. */ // Options open markup. $this->field_option( 'basic-options', $field, [ 'markup' => 'open', 'after_title' => $this->get_field_options_notice(), ] ); // Label. $this->field_option( 'label', $field ); // Description. $this->field_option( 'description', $field ); // Set label to the disabled. $args = [ 'type' => 'hidden', 'slug' => 'label_disable', 'value' => '1', ]; $this->field_element( 'text', $field, $args ); // Options close markup. $args = [ 'markup' => 'close', ]; $this->field_option( 'basic-options', $field, $args ); /* * Advanced field options. */ // Options open markup. $args = [ 'markup' => 'open', ]; $this->field_option( 'advanced-options', $field, $args ); // Custom CSS classes. $this->field_option( 'css', $field ); // Hide Divider Line toggle. $this->hide_divider_line_option( $field ); // Options close markup. $args = [ 'markup' => 'close', ]; $this->field_option( 'advanced-options', $field, $args ); } /** * Hide the Divider Line option. * * @since 1.9.7 * * @param array $field Field data. */ private function hide_divider_line_option( array $field ): void { $hide_divider_line_value = $field['hide_divider_line'] ?? '0'; $hide_divider_line = $this->field_element( 'toggle', $field, [ 'slug' => 'hide_divider_line', 'value' => $hide_divider_line_value, 'desc' => esc_html__( 'Hide Divider Line', 'wpforms-lite' ), 'tooltip' => esc_html__( 'Do not show the horizontal divider line.', 'wpforms-lite' ), ], false ); $this->field_element( 'row', $field, [ 'slug' => 'hide_divider_line', 'content' => $hide_divider_line, ] ); } /** * Field preview inside the builder. * * @since 1.9.4 * * @param array $field Field data. */ public function field_preview( $field ) { // Label. $this->field_preview_option( 'label', $field, [ 'label_badge' => $this->get_field_preview_badge(), ] ); // Description. $this->field_preview_option( 'description', $field ); } /** * Field display on the form front-end. * * @since 1.9.4 * * @param array $field Field data and settings. * @param array $deprecated Deprecated field attributes. Use field properties. * @param array $form_data Form data and settings. */ public function field_display( $field, $deprecated, $form_data ) { } } Forms/Fields/Password/Field.php 0000644 00000023766 15174710275 0012425 0 ustar 00 <?php namespace WPForms\Forms\Fields\Password; use WPForms\Forms\Fields\Traits\ProField as ProFieldTrait; use WPForms_Field; /** * Password field. * * @since 1.9.4 */ class Field extends WPForms_Field { use ProFieldTrait; /** * Primary class constructor. * * @since 1.9.4 */ public function init() { // Define field type information. $this->name = esc_html__( 'Password', 'wpforms-lite' ); $this->keywords = esc_html__( 'user', 'wpforms-lite' ); $this->type = 'password'; $this->icon = 'fa-lock'; $this->order = 95; $this->group = 'fancy'; $this->init_pro_field(); $this->hooks(); } /** * Hooks. * * @since 1.9.4 */ protected function hooks() { } /** * Field options panel inside the builder. * * @since 1.9.4 * * @param array $field Field data. * * @noinspection PackedHashtableOptimizationInspection */ public function field_options( $field ) { /* * Basic field options. */ // Options open markup. $this->field_option( 'basic-options', $field, [ 'markup' => 'open', 'after_title' => $this->get_field_options_notice(), ] ); // Label. $this->field_option( 'label', $field ); // Description. $this->field_option( 'description', $field ); // Required toggle. $this->field_option( 'required', $field ); // Confirmation toggle. $fld = $this->field_element( 'toggle', $field, [ 'slug' => 'confirmation', 'value' => isset( $field['confirmation'] ) ? '1' : '0', 'desc' => esc_html__( 'Enable Password Confirmation', 'wpforms-lite' ), 'tooltip' => esc_html__( 'Check this option to ask users to provide their password twice.', 'wpforms-lite' ), ], false ); $args = [ 'slug' => 'confirmation', 'content' => $fld, ]; $this->field_element( 'row', $field, $args ); // Password strength. $meter = $this->field_element( 'toggle', $field, [ 'slug' => 'password-strength', 'value' => isset( $field['password-strength'] ) ? '1' : '0', 'desc' => esc_html__( 'Enable Password Strength', 'wpforms-lite' ), 'tooltip' => esc_html__( 'Check this option to set minimum password strength.', 'wpforms-lite' ), ], false ); $args = [ 'slug' => 'password-strength', 'content' => $meter, ]; $this->field_element( 'row', $field, $args ); $strength_label = $this->field_element( 'label', $field, [ 'value' => esc_html__( 'Minimum Strength', 'wpforms-lite' ), 'tooltip' => esc_html__( 'Select minimum password strength level.', 'wpforms-lite' ), ], false ); $strength = $this->field_element( 'select', $field, [ 'slug' => 'password-strength-level', 'options' => [ '2' => esc_html__( 'Weak', 'wpforms-lite' ), '3' => esc_html__( 'Medium', 'wpforms-lite' ), '4' => esc_html__( 'Strong', 'wpforms-lite' ), ], 'value' => $field['password-strength-level'] ?? '3', ], false ); $args = [ 'slug' => 'password-strength-level', 'class' => ! isset( $field['password-strength'] ) ? 'wpforms-hidden' : '', 'content' => $strength_label . $strength, ]; $this->field_element( 'row', $field, $args ); $visibility = $this->field_element( 'toggle', $field, [ 'slug' => 'password-visibility', 'value' => isset( $field['password-visibility'] ) ? '1' : '0', 'desc' => esc_html__( 'Enable Password Visibility', 'wpforms-lite' ), 'tooltip' => esc_html__( 'Check this option to add a toggle for showing and hiding the password.', 'wpforms-lite' ), ], false ); $args = [ 'slug' => 'password-visibility', 'content' => $visibility, ]; $this->field_element( 'row', $field, $args ); // Options close markup. $args = [ 'markup' => 'close', ]; $this->field_option( 'basic-options', $field, $args ); /* * Advanced field options. */ // Options open markup. $args = [ 'markup' => 'open', ]; $this->field_option( 'advanced-options', $field, $args ); // Size. $this->field_option( 'size', $field ); // Placeholder. $this->field_option( 'placeholder', $field ); // Confirmation Placeholder. $lbl = $this->field_element( 'label', $field, [ 'slug' => 'confirmation_placeholder', 'value' => esc_html__( 'Confirmation Placeholder Text', 'wpforms-lite' ), 'tooltip' => esc_html__( 'Enter text for the confirmation field placeholder.', 'wpforms-lite' ), ], false ); $fld = $this->field_element( 'text', $field, [ 'slug' => 'confirmation_placeholder', 'value' => ! empty( $field['confirmation_placeholder'] ) ? esc_attr( $field['confirmation_placeholder'] ) : '', ], false ); $args = [ 'slug' => 'confirmation_placeholder', 'content' => $lbl . $fld, ]; $this->field_element( 'row', $field, $args ); // Default value. $this->field_option( 'default_value', $field ); // Custom CSS classes. $this->field_option( 'css', $field ); // Hide Label. $this->field_option( 'label_hide', $field ); // Hide sublabels. $this->field_option( 'sublabel_hide', $field ); // Options close markup. $args = [ 'markup' => 'close', ]; $this->field_option( 'advanced-options', $field, $args ); } /** * Field preview inside the builder. * * @since 1.9.4 * * @param array $field Current field specific data. * * @noinspection HtmlUnknownAttribute */ public function field_preview( $field ) { $placeholder = ! empty( $field['placeholder'] ) ? $field['placeholder'] : ''; $confirm_placeholder = ! empty( $field['confirmation_placeholder'] ) ? $field['confirmation_placeholder'] : ''; $default_value = ! empty( $field['default_value'] ) ? $field['default_value'] : ''; $confirm = ! empty( $field['confirmation'] ) ? 'enabled' : 'disabled'; $field_classes = [ 'wpforms-confirm', 'wpforms-confirm-' . $confirm, ]; if ( ! empty( $field['password-visibility'] ) ) { $field_classes[] = 'wpforms-field-password-visibility-enabled'; } // Label. $this->field_preview_option( 'label', $field, [ 'label_badge' => $this->get_field_preview_badge(), ] ); $icons = wpforms()->is_pro() ? ' <div class="wpforms-field-password-input-icon"> <svg class="wpforms-field-password-input-icon-invisible" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M288 32c-80.8 0-145.5 36.8-192.6 80.6C48.6 156 17.3 208 2.5 243.7c-3.3 7.9-3.3 16.7 0 24.6C17.3 304 48.6 356 95.4 399.4C142.5 443.2 207.2 480 288 480s145.5-36.8 192.6-80.6c46.8-43.5 78.1-95.4 93-131.1c3.3-7.9 3.3-16.7 0-24.6c-14.9-35.7-46.2-87.7-93-131.1C433.5 68.8 368.8 32 288 32zM144 256a144 144 0 1 1 288 0 144 144 0 1 1 -288 0zm144-64c0 35.3-28.7 64-64 64c-7.1 0-13.9-1.2-20.3-3.3c-5.5-1.8-11.9 1.6-11.7 7.4c.3 6.9 1.3 13.8 3.2 20.7c13.7 51.2 66.4 81.6 117.6 67.9s81.6-66.4 67.9-117.6c-11.1-41.5-47.8-69.4-88.6-71.1c-5.8-.2-9.2 6.1-7.4 11.7c2.1 6.4 3.3 13.2 3.3 20.3z"/></svg> <svg class="wpforms-field-password-input-icon-visible" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M38.8 5.1C28.4-3.1 13.3-1.2 5.1 9.2S-1.2 34.7 9.2 42.9l592 464c10.4 8.2 25.5 6.3 33.7-4.1s6.3-25.5-4.1-33.7L525.6 386.7c39.6-40.6 66.4-86.1 79.9-118.4c3.3-7.9 3.3-16.7 0-24.6c-14.9-35.7-46.2-87.7-93-131.1C465.5 68.8 400.8 32 320 32c-68.2 0-125 26.3-169.3 60.8L38.8 5.1zM223.1 149.5C248.6 126.2 282.7 112 320 112c79.5 0 144 64.5 144 144c0 24.9-6.3 48.3-17.4 68.7L408 294.5c8.4-19.3 10.6-41.4 4.8-63.3c-11.1-41.5-47.8-69.4-88.6-71.1c-5.8-.2-9.2 6.1-7.4 11.7c2.1 6.4 3.3 13.2 3.3 20.3c0 10.2-2.4 19.8-6.6 28.3l-90.3-70.8zM373 389.9c-16.4 6.5-34.3 10.1-53 10.1c-79.5 0-144-64.5-144-144c0-6.9 .5-13.6 1.4-20.2L83.1 161.5C60.3 191.2 44 220.8 34.5 243.7c-3.3 7.9-3.3 16.7 0 24.6c14.9 35.7 46.2 87.7 93 131.1C174.5 443.2 239.2 480 320 480c47.8 0 89.9-12.9 126.2-32.5L373 389.9z"/></svg> </div>' : ''; $field_markup = ' <div class="wpforms-field-password-input"> <input type="password" %1$s> %2$s </div>'; ?> <div class="<?php echo wpforms_sanitize_classes( $field_classes, true ); ?>"> <div class="wpforms-confirm-primary"> <?php printf( // The `$field_markup` variable is escaped above, we should escape only passed variables to placeholders. $field_markup, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped wpforms_html_attributes( '', [ 'primary-input' ], [], [ 'readonly' => 'readonly', 'placeholder' => $placeholder, 'value' => $default_value, ] ), $icons // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ); ?> <label class="wpforms-sub-label"><?php esc_html_e( 'Password', 'wpforms-lite' ); ?></label> </div> <div class="wpforms-confirm-confirmation"> <?php printf( // The `$field_markup` variable is escaped above, we should escape only passed variables to placeholders. $field_markup, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped wpforms_html_attributes( '', [ 'secondary-input' ], [], [ 'readonly' => 'readonly', 'placeholder' => $confirm_placeholder, ] ), $icons // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ); ?> <label class="wpforms-sub-label"><?php esc_html_e( 'Confirm Password', 'wpforms-lite' ); ?></label> </div> </div> <?php // Description. $this->field_preview_option( 'description', $field ); } /** * Field display on the form front-end. * * @since 1.9.4 * * @param array $field Field data and settings. * @param array $deprecated Deprecated field attributes. Use field properties. * @param array $form_data Form data and settings. */ public function field_display( $field, $deprecated, $form_data ) { } } Forms/Fields/PaymentMultiple/Field.php 0000644 00000036142 15174710275 0013744 0 ustar 00 <?php namespace WPForms\Forms\Fields\PaymentMultiple; use WPForms_Field; /** * Radio payment field. * * @since 1.8.2 */ class Field extends WPForms_Field { /** * Primary class constructor. * * @since 1.8.2 */ public function init() { // Define field type information. $this->name = esc_html__( 'Multiple Items', 'wpforms-lite' ); $this->keywords = esc_html__( 'product, store, ecommerce, pay, payment', 'wpforms-lite' ); $this->type = 'payment-multiple'; $this->icon = 'fa-list-ul'; $this->order = 50; $this->group = 'payment'; $this->defaults = [ 1 => [ 'label' => esc_html__( 'First Item', 'wpforms-lite' ), 'value' => '10', 'icon' => '', 'icon_style' => '', 'image' => '', 'default' => '', ], 2 => [ 'label' => esc_html__( 'Second Item', 'wpforms-lite' ), 'value' => '25', 'icon' => '', 'icon_style' => '', 'image' => '', 'default' => '', ], 3 => [ 'label' => esc_html__( 'Third Item', 'wpforms-lite' ), 'value' => '50', 'icon' => '', 'icon_style' => '', 'image' => '', 'default' => '', ], ]; $this->default_settings = [ 'choices' => $this->defaults, ]; $this->hooks(); } /** * Register hooks. * * @since 1.8.1 */ private function hooks() { // Customize HTML field values. add_filter( 'wpforms_html_field_value', [ $this, 'field_html_value' ], 10, 4 ); add_filter( "wpforms_{$this->type}_field_html_value_images", [ $this, 'field_html_value_images' ], 10, 3 ); // Define additional field properties. add_filter( "wpforms_field_properties_{$this->type}", [ $this, 'field_properties' ], 5, 3 ); // This field requires fieldset+legend instead of the field label. add_filter( "wpforms_frontend_modern_is_field_requires_fieldset_{$this->type}", '__return_true', PHP_INT_MAX, 2 ); } /** * Define additional field properties. * * @since 1.8.2 * * @param array $properties Field properties. * @param array $field Field settings. * @param array $form_data Form data and settings. * * @return array */ public function field_properties( $properties, $field, $form_data ) { // phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh // Define data. $form_id = absint( $form_data['id'] ); $field_id = absint( $field['id'] ); $choices = $field['choices']; // Remove primary input, unset for attribute for label. unset( $properties['inputs']['primary'], $properties['label']['attr']['for'] ); // Set input container (ul) properties. $properties['input_container'] = [ 'class' => [], 'data' => [], 'attr' => [], 'id' => "wpforms-{$form_id}-field_{$field_id}", ]; // Set input properties. foreach ( $choices as $key => $choice ) { $properties['inputs'][ $key ] = [ 'container' => [ 'attr' => [], 'class' => [ "choice-{$key}" ], 'data' => [], 'id' => '', ], 'label' => [ 'attr' => [ 'for' => "wpforms-{$form_id}-field_{$field_id}_{$key}", ], 'class' => [ 'wpforms-field-label-inline' ], 'data' => [], 'id' => '', 'text' => $choice['label'], ], 'attr' => [ 'name' => "wpforms[fields][{$field_id}]", 'value' => $key, ], 'class' => [ 'wpforms-payment-price' ], 'data' => [ 'amount' => wpforms_format_amount( wpforms_sanitize_amount( $choice['value'] ) ), ], 'id' => "wpforms-{$form_id}-field_{$field_id}_{$key}", 'icon' => $choice['icon'] ?? '', 'icon_style' => $choice['icon_style'] ?? '', 'image' => $choice['image'] ?? '', 'required' => ! empty( $field['required'] ) ? 'required' : '', 'default' => isset( $choice['default'] ), ]; } // Required class for pagebreak validation. if ( ! empty( $field['required'] ) ) { $properties['input_container']['class'][] = 'wpforms-field-required'; } // Custom properties if image choices are enabled. if ( ! empty( $field['choices_images'] ) ) { $properties['input_container']['class'][] = 'wpforms-image-choices'; $properties['input_container']['class'][] = 'wpforms-image-choices-' . sanitize_html_class( $field['choices_images_style'] ); foreach ( $properties['inputs'] as $key => $inputs ) { $properties['inputs'][ $key ]['container']['class'][] = 'wpforms-image-choices-item'; if ( in_array( $field['choices_images_style'], [ 'modern', 'classic' ], true ) ) { $properties['inputs'][ $key ]['class'][] = 'wpforms-screen-reader-element'; } } } elseif ( ! empty( $field['choices_icons'] ) ) { $properties = wpforms()->obj( 'icon_choices' )->field_properties( $properties, $field ); } // Add selected class for choices with defaults. foreach ( $properties['inputs'] as $key => $inputs ) { if ( ! empty( $inputs['default'] ) ) { $properties['inputs'][ $key ]['container']['class'][] = 'wpforms-selected'; } } return $properties; } /** * Get field populated single property value. * * @since 1.8.2 * * @param string $raw_value Value from a GET param, always a string. * @param string $input Represent a subfield inside the field. May be empty. * @param array $properties Field properties. * @param array $field Current field specific data. * * @return array Modified field properties. */ protected function get_field_populated_single_property_value( $raw_value, $input, $properties, $field ) { /* * When the form is submitted, we get only values (prices) from the Fallback. * As payment-multiple (radio) field doesn't support 'show_values' option - * we should transform value into label to check against using general logic in parent method. */ if ( ! is_string( $raw_value ) || empty( $field['choices'] ) || ! is_array( $field['choices'] ) ) { return $properties; } // The form submits only the sum, so shortcut for Dynamic. if ( ! is_numeric( $raw_value ) ) { return parent::get_field_populated_single_property_value( $raw_value, $input, $properties, $field ); } $get_value = wpforms_format_amount( wpforms_sanitize_amount( $raw_value ) ); foreach ( $field['choices'] as $choice ) { if ( isset( $choice['label'], $choice['value'] ) && wpforms_format_amount( wpforms_sanitize_amount( $choice['value'] ) ) === $get_value ) { $trans_value = $choice['label']; // Stop iterating over choices. break; } } if ( empty( $trans_value ) ) { return $properties; } return parent::get_field_populated_single_property_value( $trans_value, $input, $properties, $field ); } /** * Field options panel inside the builder. * * @since 1.8.2 * * @param array $field Field settings. */ public function field_options( $field ) { /* * Basic field options. */ // Options open markup. $this->field_option( 'basic-options', $field, [ 'markup' => 'open', ] ); // Label. $this->field_option( 'label', $field ); // Choices option. $this->field_option( 'choices_payments', $field ); // Show price after item labels. $fld = $this->field_element( 'toggle', $field, [ 'slug' => 'show_price_after_labels', 'value' => isset( $field['show_price_after_labels'] ) ? '1' : '0', 'desc' => esc_html__( 'Show Price After Item Labels', 'wpforms-lite' ), 'tooltip' => esc_html__( 'Check this option to show price of the item after the label.', 'wpforms-lite' ), ], false ); $args = [ 'slug' => 'show_price_after_labels', 'content' => $fld, ]; $this->field_element( 'row', $field, $args ); // Choices Images. $this->field_option( 'choices_images', $field ); // Hide Choices Images. $this->field_option( 'choices_images_hide', $field ); // Choice Images Style (theme). $this->field_option( 'choices_images_style', $field ); // Choices Icons. $this->field_option( 'choices_icons', $field ); // Choices Icons Color. $this->field_option( 'choices_icons_color', $field ); // Choices Icons Size. $this->field_option( 'choices_icons_size', $field ); // Choices Icons Style. $this->field_option( 'choices_icons_style', $field ); // Description. $this->field_option( 'description', $field ); // Required toggle. $this->field_option( 'required', $field ); // Options close markup. $this->field_option( 'basic-options', $field, [ 'markup' => 'close', ] ); /* * Advanced field options. */ // Options open markup. $this->field_option( 'advanced-options', $field, [ 'markup' => 'open', ] ); // Input columns. $this->field_option( 'input_columns', $field ); // Custom CSS classes. $this->field_option( 'css', $field ); // Hide label. $this->field_option( 'label_hide', $field ); // Options close markup. $this->field_option( 'advanced-options', $field, [ 'markup' => 'close', ] ); } /** * Field preview inside the builder. * * @since 1.8.2 * * @param array $field Field settings. */ public function field_preview( $field ) { // Label. $this->field_preview_option( 'label', $field ); // Choices. $this->field_preview_option( 'choices', $field ); // Description. $this->field_preview_option( 'description', $field ); } /** * Field display on the form front-end. * * @since 1.8.2 * * @param array $field Field settings. * @param array $deprecated Deprecated array. * @param array $form_data Form data and settings. * * @noinspection HtmlUnknownAttribute * @noinspection HtmlUnknownTarget */ public function field_display( $field, $deprecated, $form_data ) { // Define data. $container = $field['properties']['input_container']; $choices = $field['properties']['inputs']; printf( '<ul %s>', wpforms_html_attributes( $container['id'], $container['class'], $container['data'], $container['attr'] ) // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ); foreach ( $choices as $key => $choice ) { $label = $choice['label']['text'] ?? ''; /* translators: %s - item number. */ $label = $label !== '' ? $label : sprintf( esc_html__( 'Item %s', 'wpforms-lite' ), $key ); $label .= ! empty( $field['show_price_after_labels'] ) && isset( $choice['data']['amount'] ) ? $this->get_price_after_label( $choice['data']['amount'] ) : ''; printf( '<li %s>', wpforms_html_attributes( $choice['container']['id'], $choice['container']['class'], $choice['container']['data'], $choice['container']['attr'] ) // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ); if ( empty( $field['dynamic_choices'] ) && ! empty( $field['choices_images'] ) ) { // Image choices. printf( '<label %s>', wpforms_html_attributes( $choice['label']['id'], $choice['label']['class'], $choice['label']['data'], $choice['label']['attr'] ) // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ); echo '<span class="wpforms-image-choices-image">'; if ( ! empty( $choice['image'] ) ) { printf( '<img src="%s" alt="%s"%s>', esc_url( $choice['image'] ), esc_attr( $choice['label']['text'] ), ! empty( $choice['label']['text'] ) ? ' title="' . esc_attr( $choice['label']['text'] ) . '"' : '' ); } echo '</span>'; if ( $field['choices_images_style'] === 'none' ) { echo '<br>'; } printf( '<input type="radio" %s %s %s>', wpforms_html_attributes( $choice['id'], $choice['class'], $choice['data'], $choice['attr'] ), // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped esc_attr( $choice['required'] ), checked( '1', $choice['default'], false ) ); echo '<span class="wpforms-image-choices-label">' . wp_kses_post( $label ) . '</span>'; echo '</label>'; } elseif ( empty( $field['dynamic_choices'] ) && ! empty( $field['choices_icons'] ) ) { // Icon Choices. wpforms()->obj( 'icon_choices' )->field_display( $field, $choice, 'radio', $label ); } else { // Normal display. printf( '<input type="radio" %s %s %s>', wpforms_html_attributes( $choice['id'], $choice['class'], $choice['data'], $choice['attr'] ), // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped esc_attr( $choice['required'] ), checked( '1', $choice['default'], false ) ); printf( '<label %s>%s</label>', wpforms_html_attributes( $choice['label']['id'], $choice['label']['class'], $choice['label']['data'], $choice['label']['attr'] ), // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped wp_kses_post( $label ) ); } echo '</li>'; } echo '</ul>'; } /** * Validate field on submitting the form. * * @since 1.8.2 * * @param int $field_id Field ID. * @param mixed $field_submit Submitted field value (raw data). * @param array $form_data Form data and settings. */ public function validate( $field_id, $field_submit, $form_data ) { // Basic required check - If field is marked as required, check for entry data. if ( ! empty( $form_data['fields'][ $field_id ]['required'] ) && empty( $field_submit ) ) { wpforms()->obj( 'process' )->errors[ $form_data['id'] ][ $field_id ] = wpforms_get_required_label(); } // Validate that the option selected is real. if ( is_string( $field_submit ) && ! empty( $field_submit ) && empty( $form_data['fields'][ $field_id ]['choices'][ $field_submit ] ) ) { wpforms()->obj( 'process' )->errors[ $form_data['id'] ][ $field_id ] = esc_html__( 'Invalid payment option.', 'wpforms-lite' ); } } /** * Format and sanitize field. * * @since 1.8.2 * * @param int $field_id Field ID. * @param string $field_submit Submitted form data. * @param array $form_data Form data and settings. */ public function format( $field_id, $field_submit, $form_data ) { $field = $form_data['fields'][ $field_id ]; $name = sanitize_text_field( $field['label'] ); $value = ''; $amount = 0; $choice_label = ''; $image = ''; if ( ! empty( $field_submit ) && ! empty( $field['choices'][ $field_submit ] ) ) { $amount = wpforms_sanitize_amount( $field['choices'][ $field_submit ]['value'] ); $value = wpforms_format_amount( $amount, true ); if ( ! empty( $field['choices'][ $field_submit ]['label'] ) ) { $choice_label = sanitize_text_field( $field['choices'][ $field_submit ]['label'] ); $value = $choice_label . ' - ' . $value; } if ( ! empty( $field['choices_images'] ) ) { $image = ! empty( $field['choices'][ $field_submit ]['image'] ) ? esc_url_raw( $field['choices'][ $field_submit ]['image'] ) : ''; } } wpforms()->obj( 'process' )->fields[ $field_id ] = [ 'name' => $name, 'value' => $value, 'value_choice' => $choice_label, 'value_raw' => sanitize_text_field( $field_submit ), 'amount' => wpforms_format_amount( $amount ), 'amount_raw' => $amount, 'currency' => wpforms_get_currency(), 'image' => $image, 'id' => absint( $field_id ), 'type' => sanitize_key( $this->type ), ]; } } Forms/Fields/Content/Field.php 0000644 00000005453 15174710275 0012226 0 ustar 00 <?php namespace WPForms\Forms\Fields\Content; use WPForms\Forms\Fields\Traits\ProField as ProFieldTrait; use WPForms\Forms\Fields\Traits\ContentInput; use WPForms_Field; /** * The Content Field Class. * * @since 1.9.4 */ class Field extends WPForms_Field { use ProFieldTrait; use ContentInput; /** * Class initialization method. * * @since 1.9.4 */ public function init() { // Define field type information. $this->name = esc_html__( 'Content', 'wpforms-lite' ); $this->keywords = esc_html__( 'image, text, table, list, heading, wysiwyg, visual', 'wpforms-lite' ); $this->type = 'content'; $this->icon = 'fa-file-image-o'; $this->order = 180; $this->group = 'fancy'; $this->allow_read_only = false; $this->default_settings = [ 'label_disable' => '1', ]; $this->init_pro_field(); $this->hooks(); } /** * Register WP hooks. * * @since 1.9.4 */ protected function hooks() { } /** * Show field options in the builder left panel. * * @since 1.9.4 * * @param array $field Field data. */ public function field_options( $field ) { // Options open markup. $this->field_option( 'basic-options', $field, [ 'markup' => 'open', 'after_title' => $this->get_field_options_notice(), ] ); $this->field_option_content( $field ); // Set label to the disabled. $args = [ 'type' => 'hidden', 'slug' => 'label_disable', 'value' => '1', ]; $this->field_element( 'text', $field, $args ); // Options close markup. $this->field_option( 'basic-options', $field, [ 'markup' => 'close' ] ); // Options open markup. $this->field_option( 'advanced-options', $field, [ 'markup' => 'open' ] ); // Size. $this->field_option( 'size', $field ); // Custom CSS classes. $this->field_option( 'css', $field ); // Options close markup. $this->field_option( 'advanced-options', $field, [ 'markup' => 'close' ] ); } /** * Show the field preview in the builder right panel. * * @since 1.9.4 * * @param array $field Field data. */ public function field_preview( $field ) { if ( ! empty( $this->is_disabled_field ) ) { // Label. $field['label'] = empty( $field['label'] ) ? esc_html__( 'Content', 'wpforms-lite' ) : $field['label']; $this->field_preview_option( 'label', $field, [ 'label_badge' => $this->get_field_preview_badge(), ] ); } $this->content_input_preview( $field ); } /** * Field display on the form front-end. * * @since 1.9.4 * * @param array $field Field data and settings. * @param array $deprecated Deprecated field attributes. Use field properties instead. * @param array $form_data Form data and settings. */ public function field_display( $field, $deprecated, $form_data ) { } } Forms/Fields/Html/Field.php 0000644 00000011403 15174710275 0011510 0 ustar 00 <?php namespace WPForms\Forms\Fields\Html; use WPForms\Forms\Fields\Traits\ProField as ProFieldTrait; use WPForms_Field; /** * HTML block text field. * * @since 1.9.4 */ class Field extends WPForms_Field { use ProFieldTrait; /** * Primary class constructor. * * @since 1.9.4 */ public function init() { // Define field type information. $this->name = esc_html__( 'HTML', 'wpforms-lite' ); $this->keywords = esc_html__( 'code', 'wpforms-lite' ); $this->type = 'html'; $this->icon = 'fa-code'; $this->order = 185; $this->group = 'fancy'; $this->allow_read_only = false; $this->default_settings = [ 'name' => '', ]; $this->init_pro_field(); $this->hooks(); } /** * Hooks. * * @since 1.9.4 */ protected function hooks() { } /** * Extend from `parent::field_option()` to add `name` option. * * @since 1.9.4 * * @param string $option Field option to render. * @param array $field Field data and settings. * @param array $args Field preview arguments. * @param bool $do_echo Print or return the value. Print by default. * * @return string|null * @noinspection PhpMissingReturnTypeInspection * @noinspection ReturnTypeCanBeDeclaredInspection */ public function field_option( $option, $field, $args = [], $do_echo = true ) { // phpcs:ignore Universal.NamingConventions.NoReservedKeywordParameterNames.echoFound if ( $option !== 'name' ) { return parent::field_option( $option, $field, $args, $do_echo ); } $output = $this->field_element( 'label', $field, [ 'slug' => 'name', 'value' => esc_html__( 'Label', 'wpforms-lite' ), 'tooltip' => esc_html__( 'Enter text for the form field label. It will help identify your HTML blocks inside the form builder, but will not be displayed in the form.', 'wpforms-lite' ), ], false ); $output .= $this->field_element( 'text', $field, [ 'slug' => 'name', 'value' => ! empty( $field['name'] ) ? esc_attr( $field['name'] ) : '', ], false ); $output = $this->field_element( 'row', $field, [ 'slug' => 'name', 'content' => $output, ], false ); if ( $do_echo ) { echo $output; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped return null; } return $output; } /** * Field options panel inside the builder. * * @since 1.9.4 * * @param array $field Field settings. */ public function field_options( $field ) { /* * Basic field options. */ // Options open markup. $this->field_option( 'basic-options', $field, [ 'markup' => 'open', 'after_title' => $this->get_field_options_notice(), ] ); // Name (Label). $this->field_option( 'name', $field ); // Code. $this->field_option( 'code', $field ); // Set the label to disable. $args = [ 'type' => 'hidden', 'slug' => 'label_disable', 'value' => '1', ]; $this->field_element( 'text', $field, $args ); // Options close markup. $args = [ 'markup' => 'close', ]; $this->field_option( 'basic-options', $field, $args ); /* * Advanced field options. */ // Options open markup. $args = [ 'markup' => 'open', ]; $this->field_option( 'advanced-options', $field, $args ); // Custom CSS classes. $this->field_option( 'css', $field ); // Options close markup. $args = [ 'markup' => 'close', ]; $this->field_option( 'advanced-options', $field, $args ); } /** * Field preview inside the builder. * * @since 1.9.4 * * @param array $field Field settings. */ public function field_preview( $field ) { $label = ! empty( $field['name'] ) ? $field['name'] : ''; $label_badge = empty( $label ) ? '' : $this->get_field_preview_badge(); $code_badge = empty( $label ) ? $this->get_field_preview_badge() : ''; ?> <label class="label-title"> <div class="text"> <?php echo esc_html( $label ) . $label_badge; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> </div> <div class="grey"> <i class="fa fa-code"></i> <?php esc_html_e( 'HTML / Code Block', 'wpforms-lite' ); ?> <?php echo $code_badge; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> </div> </label> <div class="description"><?php esc_html_e( 'Contents of this field are not displayed in the form builder preview.', 'wpforms-lite' ); ?></div> <?php } /** * Field display on the form front-end. * * @since 1.9.4 * * @param array $field Field data and settings. * @param array $deprecated Deprecated field attributes. Use field properties. * @param array $form_data Form data and settings. * * @noinspection HtmlUnknownAttribute */ public function field_display( $field, $deprecated, $form_data ) { } } Forms/Fields/Camera/Field.php 0000644 00000030266 15174710275 0012004 0 ustar 00 <?php namespace WPForms\Forms\Fields\Camera; use WPForms_Field; use WPForms\Forms\Fields\Traits\CameraTrait; use WPForms\Forms\Fields\Traits\ProField as ProFieldTrait; use WPForms\Forms\Fields\Traits\AccessRestrictionsTrait; /** * Camera field. * * @since 1.9.8 */ class Field extends WPForms_Field { use ProFieldTrait; use CameraTrait; use AccessRestrictionsTrait; protected const STYLE_BUTTON = 'button'; protected const STYLE_LINK = 'link'; public const STYLE_CLASSIC = 'classic'; public const STYLE_MODERN = 'modern'; /** * Primary class constructor. * * @since 1.9.8 */ public function init() { // Define field type information. $this->name = esc_html__( 'Camera', 'wpforms-lite' ); $this->keywords = esc_html__( 'photo, image, capture, webcam', 'wpforms-lite' ); $this->type = 'camera'; $this->icon = 'fa-camera'; $this->order = 105; $this->group = 'fancy'; $this->default_settings = [ 'style' => 'button', ]; $this->init_pro_field(); $this->hooks(); } /** * Add hooks. * * @since 1.9.8 */ private function hooks(): void { add_action( 'wpforms_builder_enqueues', [ $this, 'builder_enqueues' ] ); } /** * Enqueue script for the admin form builder. * * @since 1.9.8 */ public function builder_enqueues(): void { $min = wpforms_get_min_suffix(); if ( ! wpforms_is_pro() ) { return; } wp_enqueue_script( 'wpforms-builder-file-upload-field', WPFORMS_PLUGIN_URL . "assets/pro/js/admin/builder/fields/file-upload{$min}.js", [ 'jquery', 'wpforms-builder' ], WPFORMS_VERSION, false ); wp_enqueue_script( 'wpforms-builder-camera', WPFORMS_PLUGIN_URL . "assets/pro/js/admin/builder/fields/camera{$min}.js", [ 'jquery', 'wpforms-builder' ], WPFORMS_VERSION, false ); // Localize strings for the camera field. wp_localize_script( 'wpforms-builder-camera', 'wpforms_camera_builder', [ 'button_link_text_label' => esc_html__( 'Button Link Text', 'wpforms-lite' ), 'link_text_label' => esc_html__( 'Link Text', 'wpforms-lite' ), 'button_link_text_tooltip' => esc_html__( 'Enter the text for the button link.', 'wpforms-lite' ), 'link_text_tooltip' => esc_html__( 'Enter the text for the link.', 'wpforms-lite' ), 'error_message' => esc_html__( 'Camera field with Link style cannot have empty Link Text. Please enter text or change style to Button.', 'wpforms-lite' ), 'error_title' => esc_html__( 'Missing Link Text', 'wpforms-lite' ), 'error_ok' => esc_html__( 'OK', 'wpforms-lite' ), ] ); } /** * Field options panel inside the builder. * * @since 1.9.8 * * @param array $field Field data. */ public function field_options( $field ) { /* * Basic field options. */ // Options open markup. $this->field_option( 'basic-options', $field, [ 'markup' => 'open', 'after_title' => $this->get_field_options_notice(), ] ); // Label. $this->field_option( 'label', $field ); // Description. $this->field_option( 'description', $field ); // Camera options. $this->add_camera_enabled_toggle( $field ); $this->add_camera_format_options( $field ); $this->add_camera_aspect_ratio_options( $field ); $this->add_camera_custom_ratio_options( $field ); $this->add_camera_time_limit_options( $field ); // Max file size. $this->add_max_file_size_options( $field ); // Required toggle. $this->field_option( 'required', $field ); // Options close markup. $this->field_option( 'basic-options', $field, [ 'markup' => 'close' ] ); // Advanced field options. $this->field_option( 'advanced-options', $field, [ 'markup' => 'open' ] ); // Style (Button or Link). $this->add_style_options( $field ); // Button link text. $this->add_button_link_text_options( $field ); // Custom CSS classes. $this->field_option( 'css', $field ); // Media Library toggle. $fld = $this->field_element( 'toggle', $field, [ 'slug' => 'media_library', 'value' => ! empty( $field['media_library'] ) ? 1 : '', 'desc' => esc_html__( 'Store Files in WordPress Media Library', 'wpforms-lite' ), 'tooltip' => esc_html__( 'Check this option to store the final uploaded file in the WordPress Media Library', 'wpforms-lite' ), 'class' => 'wpforms-camera-media-library', ], false ); $this->field_element( 'row', $field, [ 'slug' => 'media_library', 'content' => $fld, ] ); // Access Restrictions. $this->access_restrictions_options( $field ); // Hide label. $this->field_option( 'label_hide', $field ); // Options close markup. $this->field_option( 'advanced-options', $field, [ 'markup' => 'close' ] ); } /** * Field preview inside the builder. * * @since 1.9.8 * * @param array $field Field data. */ public function field_preview( $field ) { // Label. $this->field_preview_option( 'label', $field, [ 'label_badge' => $this->get_field_preview_badge(), ] ); $style = ! empty( $field['style'] ) ? $field['style'] : self::STYLE_BUTTON; $field_id = absint( $field['id'] ); $text = $field['button_link_text'] ?? esc_html__( 'Capture With Your Camera', 'wpforms-lite' ); // Always render both button and link, but hide/show based on the selected style. $button_class = $style === self::STYLE_BUTTON ? 'wpforms-camera-button wpforms-btn-secondary' : 'wpforms-camera-button wpforms-btn-secondary wpforms-hidden'; $link_class = $style === self::STYLE_LINK ? 'wpforms-camera-link' : 'wpforms-camera-link wpforms-hidden'; printf( '<button type="button" class="%s" id="%d">%s %s</button>', esc_attr( $button_class ), (int) $field_id, $this->get_camera_icon_svg(), // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped esc_html( $text ) ); printf( '<a href="#" class="%s" data-field-id="%d">%s</a>', esc_attr( $link_class ), (int) $field_id, esc_html( $text ) ); // Description. $this->field_preview_option( 'description', $field ); } /** * Field display on the form front-end. * * @since 1.9.8 * * @param array $field Field data and settings. * @param array $deprecated Deprecated field attributes. Use field properties. * @param array $form_data Form data and settings. */ public function field_display( $field, $deprecated, $form_data ) { // Implemented in Pro only. } /** * Add max file size options. * * @since 1.9.8 * * @param array $field Field data. */ private function add_max_file_size_options( array $field ): void { $lbl = $this->field_element( 'label', $field, [ 'slug' => 'max_size', 'value' => esc_html__( 'Max File Size', 'wpforms-lite' ), 'tooltip' => sprintf( /* translators: %s - max upload size. */ esc_html__( 'Enter the max size of each file, in megabytes, to allow. If left blank, the value defaults to the maximum size the server allows which is %s.', 'wpforms-lite' ), wpforms_max_upload() ), ], false ); $fld = $this->field_element( 'text', $field, [ 'slug' => 'max_size', 'type' => 'number', 'attrs' => [ 'min' => 1, 'max' => 512, 'step' => 1, 'pattern' => '[0-9]', ], 'value' => ! empty( $field['max_size'] ) ? abs( $field['max_size'] ) : '', ], false ); $this->field_element( 'row', $field, [ 'slug' => 'max_size', 'content' => $lbl . $fld, ] ); } /** * Add style options, Button or Link. * * @since 1.9.8 * * @param array $field Field data. */ private function add_style_options( array $field ): void { // Style (Button or Link). $lbl = $this->field_element( 'label', $field, [ 'slug' => 'style', 'value' => esc_html__( 'Style', 'wpforms-lite' ), 'tooltip' => esc_html__( 'Choose the style of the camera button.', 'wpforms-lite' ), ], false ); $fld = $this->field_element( 'select', $field, [ 'slug' => 'style', 'value' => ! empty( $field['style'] ) ? $field['style'] : self::STYLE_BUTTON, 'options' => [ self::STYLE_BUTTON => esc_html__( 'Button', 'wpforms-lite' ), self::STYLE_LINK => esc_html__( 'Link', 'wpforms-lite' ), ], ], false ); $this->field_element( 'row', $field, [ 'slug' => 'style', 'content' => $lbl . $fld, 'class' => 'wpforms-camera-style', ] ); } /** * Add button link text options. * * @since 1.9.8 * * @param array $field Field data. */ private function add_button_link_text_options( array $field ): void { $style = ! empty( $field['style'] ) ? $field['style'] : self::STYLE_BUTTON; // Button link text. $lbl = $this->field_element( 'label', $field, [ 'slug' => 'button_link_text', 'value' => $style === self::STYLE_BUTTON ? esc_html__( 'Button Link Text', 'wpforms-lite' ) : esc_html__( 'Link Text', 'wpforms-lite' ), 'tooltip' => $style === self::STYLE_BUTTON ? esc_html__( 'Enter the text for the button link.', 'wpforms-lite' ) : esc_html__( 'Enter the text for the link.', 'wpforms-lite' ), ], false ); $fld = $this->field_element( 'text', $field, [ 'slug' => 'button_link_text', 'value' => $field['button_link_text'] ?? esc_html__( 'Capture With Your Camera', 'wpforms-lite' ), ], false ); $this->field_element( 'row', $field, [ 'slug' => 'button_link_text', 'content' => $lbl . $fld, ] ); } /** * Get camera icon SVG. * * @since 1.9.8 * * @return string Camera icon SVG code. * @noinspection HtmlDeprecatedAttribute */ protected function get_camera_icon_svg(): string { return '<svg width="16" height="14" viewBox="0 0 16 14" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M4.65625 1.03125C4.875 0.40625 5.4375 0 6.09375 0H9.90625C10.5625 0 11.125 0.40625 11.3438 1.03125L11.6562 2H14C15.0938 2 16 2.90625 16 4V12C16 13.0938 15.0938 14 14 14H2C0.90625 14 0 13.0938 0 12V4C0 2.90625 0.90625 2 2 2H4.34375L4.65625 1.03125ZM8 5C6.34375 5 5 6.34375 5 8C5 9.65625 6.34375 11 8 11C9.65625 11 11 9.65625 11 8C11 6.34375 9.65625 5 8 5Z"/></svg>'; } /** * Get remove selected file icon SVG. * * @since 1.9.8 * * @return string Remove icon SVG code. * @noinspection HtmlDeprecatedAttribute */ protected function get_camera_remove_file_icon(): string { return '<svg width="13" height="15" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M4.121.914a.853.853 0 0 1 .82-.602H8.06c.382 0 .71.247.82.602l.246.711h2.625c.492 0 .875.383.875.875a.864.864 0 0 1-.875.875H1.25A.864.864 0 0 1 .375 2.5c0-.492.383-.875.875-.875h2.625l.246-.71Zm7.629 3.774-.574 8.832c-.055.683-.63 1.23-1.313 1.23H3.137c-.684 0-1.258-.547-1.313-1.23L1.25 4.688h10.5Z"/></svg>'; } /** * Check if the field is modern upload style. * * @since 1.9.8 * * @param array $field_data Field data. * * @return bool */ public static function is_modern_upload( $field_data ): bool { return isset( $field_data['style'] ) && $field_data['style'] === self::STYLE_MODERN; } /** * Format field value for display in Entries. * * @since 1.9.8 * * @param int $field_id Field ID. * @param mixed $field_submit Field value that was submitted. * @param array $form_data Form data and settings. */ public function format( $field_id, $field_submit, $form_data ) { $field_id = absint( $field_id ); $field_label = ! empty( $form_data['fields'][ $field_id ]['label'] ) ? sanitize_text_field( $form_data['fields'][ $field_id ]['label'] ) : ''; $style = ! empty( $form_data['fields'][ $field_id ]['style'] ) && $form_data['fields'][ $field_id ]['style'] === self::STYLE_MODERN ? self::STYLE_MODERN : self::STYLE_CLASSIC; if ( $style === self::STYLE_CLASSIC ) { wpforms()->obj( 'process' )->fields[ $field_id ] = [ 'name' => $field_label, 'value' => '', 'file' => '', 'file_original' => '', 'ext' => '', 'id' => $field_id, 'type' => $this->type, ]; return; } wpforms()->obj( 'process' )->fields[ $field_id ] = [ 'name' => $field_label, 'value' => '', 'value_raw' => '', 'id' => $field_id, 'type' => $this->type, 'style' => self::STYLE_MODERN, ]; } } Forms/Fields/Richtext/Field.php 0000644 00000011665 15174710275 0012410 0 ustar 00 <?php namespace WPForms\Forms\Fields\Richtext; use WPForms\Forms\Fields\Traits\ProField as ProFieldTrait; use WPForms_Field; /** * Rich Text field. * * @since 1.9.4 */ class Field extends WPForms_Field { use ProFieldTrait; /** * Primary class constructor. * * @since 1.9.4 */ public function init() { // Define field type information. $this->name = esc_html__( 'Rich Text', 'wpforms-lite' ); $this->keywords = esc_html__( 'image, text, table, list, heading, wysiwyg, visual', 'wpforms-lite' ); $this->type = 'richtext'; $this->icon = 'fa-pencil-square-o'; $this->order = 170; $this->group = 'fancy'; $this->init_pro_field(); $this->hooks(); } /** * Hooks. * * @since 1.9.4 */ protected function hooks() { } /** * Field options panel inside the builder. * * @since 1.9.4 * * @param array $field Field data and settings. */ public function field_options( $field ) { // Options open markup. $this->field_option( 'basic-options', $field, [ 'markup' => 'open', 'after_title' => $this->get_field_options_notice(), ] ); $this->field_option( 'label', $field ); $this->field_option( 'description', $field ); $this->field_element( 'row', $field, [ 'slug' => 'media_enabled', 'content' => $this->field_element( 'toggle', $field, [ 'slug' => 'media_enabled', 'value' => isset( $field['media_enabled'] ) ? '1' : '0', 'desc' => esc_html__( 'Allow Media Uploads', 'wpforms-lite' ), 'tooltip' => esc_html__( 'Check this option to allow uploading and embedding files.', 'wpforms-lite' ), ], false ), ] ); $media_library = $this->field_element( 'toggle', $field, [ 'slug' => 'media_library', 'value' => isset( $field['media_library'] ) ? '1' : '0', 'desc' => esc_html__( 'Store files in WordPress Media Library', 'wpforms-lite' ), 'tooltip' => esc_html__( 'Check this option to store files in the WordPress Media Library.', 'wpforms-lite' ), ], false ); $this->field_element( 'row', $field, [ 'slug' => 'media_controls', 'class' => ! isset( $field['media_enabled'] ) ? 'wpforms-hide' : '', 'content' => $media_library, ] ); $this->field_option( 'required', $field ); $this->field_option( 'basic-options', $field, [ 'markup' => 'close' ] ); $this->field_option( 'advanced-options', $field, [ 'markup' => 'open' ] ); $output_style = $this->field_element( 'label', $field, [ 'slug' => 'style', 'value' => esc_html__( 'Field Style', 'wpforms-lite' ), ], false ); $output_style .= $this->field_element( 'select', $field, [ 'slug' => 'style', 'value' => ! empty( $field['style'] ) ? esc_attr( $field['style'] ) : 'full', 'options' => [ 'full' => esc_html__( 'Full', 'wpforms-lite' ), 'basic' => esc_html__( 'Basic', 'wpforms-lite' ), ], ], false ); $this->field_element( 'row', $field, [ 'slug' => 'style', 'content' => $output_style, ] ); $this->field_option( 'size', $field ); $this->field_option( 'css', $field ); $this->field_option( 'label_hide', $field ); $this->field_option( 'advanced-options', $field, [ 'markup' => 'close' ] ); } /** * The field preview inside the builder. * * @since 1.9.4 * * @param array $field Field data and settings. */ public function field_preview( $field ) { // Label. $this->field_preview_option( 'label', $field, [ 'label_badge' => $this->get_field_preview_badge(), ] ); $style = ! empty( $field['style'] ) && $field['style'] === 'basic' ? 'wpforms-field-richtext-toolbar-basic' : ''; $media_enabled = ! empty( $field['media_enabled'] ) ? 'wpforms-field-richtext-media-enabled' : ''; ?> <div class="wpforms-richtext-wrap tmce-active"> <div class="wp-editor-tabs"> <button type="button" class="wp-switch-editor switch-tmce"><?php esc_html_e( 'Visual', 'wpforms-lite' ); ?></button> <button type="button" class="wp-switch-editor"><?php esc_html_e( 'Text', 'wpforms-lite' ); ?></button> </div> <div class="wp-editor-container "> <div class="mce-container-body"> <div class="mce-toolbar-grp <?php echo esc_attr( $style ); ?> <?php echo esc_attr( $media_enabled ); ?>"></div> </div> <textarea id="wpforms-richtext-<?php echo wpforms_validate_field_id( $field['id'] ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>"></textarea> <div class="mce-statusbar"> <i class="mce-ico mce-i-resize"></i> </div> </div> </div> <?php $this->field_preview_option( 'description', $field ); } /** * The field display on the form front-end. * * @since 1.9.4 * * @param array $field Field data and settings. * @param array $deprecated Field attributes. * @param array $form_data Form data and settings. */ public function field_display( $field, $deprecated, $form_data ) { } } Forms/Fields/PaymentSingle/Field.php 0000644 00000055453 15174710275 0013400 0 ustar 00 <?php namespace WPForms\Forms\Fields\PaymentSingle; /** * Single item payment field. * * @since 1.8.2 */ class Field extends \WPForms_Field { /** * User field format. * * @since 1.8.2 * * @var string */ const FORMAT_USER = 'user'; /** * Single field format. * * @since 1.8.2 * * @var string */ const FORMAT_SINGLE = 'single'; /** * Hidden field format. * * @since 1.8.2 * * @var string */ const FORMAT_HIDDEN = 'hidden'; /** * Minimum price default value. * * @since 1.8.6 * * @var int */ const MIN_PRICE_DEFAULT = 10; /** * Primary class constructor. * * @since 1.8.2 */ public function init() { // Define field type information. $this->name = esc_html__( 'Single Item', 'wpforms-lite' ); $this->keywords = esc_html__( 'product, store, ecommerce, pay, payment', 'wpforms-lite' ); $this->type = 'payment-single'; $this->icon = 'fa-file-o'; $this->order = 30; $this->group = 'payment'; $this->hooks(); } /** * Define additional field hooks. * * @since 1.8.2 */ private function hooks() { // Define additional field properties. add_filter( "wpforms_field_properties_{$this->type}", [ $this, 'field_properties' ], 5, 3 ); add_action( 'wpforms_display_field_after', [ $this, 'field_minimum_price_description' ], 10, 2 ); add_filter( 'wpforms_field_preview_class', [ $this, 'preview_field_class' ], 10, 2 ); // Customize HTML field value. add_filter( 'wpforms_html_field_value', [ $this, 'field_html_value' ], 10, 4 ); } /** * Define additional field properties. * * @since 1.8.2 * * @param array $properties Field properties. * @param array $field Field settings. * @param array $form_data Form data and settings. * * @return array */ public function field_properties( $properties, $field, $form_data ) { // phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh // Basic IDs. $form_id = absint( $form_data['id'] ); $field_id = absint( $field['id'] ); // Set options container (<select>) properties. $properties['input_container'] = [ 'class' => [ 'wpforms-payment-price' ], 'data' => [], 'id' => "wpforms-{$form_id}-field_{$field_id}", ]; // User format data and class. $field_format = ! empty( $field['format'] ) ? $field['format'] : self::FORMAT_SINGLE; if ( $this->is_user_defined( $field ) ) { $properties['inputs']['primary']['data']['rule-currency'] = '["$",false]'; $properties['inputs']['primary']['class'][] = 'wpforms-payment-user-input'; if ( ! empty( $field['min_price'] ) ) { $properties['inputs']['primary']['data']['rule-required-minimum-price'] = wpforms_sanitize_amount( $field['min_price'] ); } } // Null 'for' value for label as there no input for it. if ( ! $this->is_user_defined( $field ) ) { unset( $properties['label']['attr']['for'] ); } $properties['inputs']['primary']['class'][] = 'wpforms-payment-price'; // Check size. if ( ! empty( $field['size'] ) ) { $properties['inputs']['primary']['class'][] = 'wpforms-field-' . esc_attr( $field['size'] ); } $required = ! empty( $form_data['fields'][ $field_id ]['required'] ); if ( $required ) { $properties['inputs']['primary']['data']['rule-required-positive-number'] = true; } // Price. if ( ! empty( $field['price'] ) ) { $field_value = wpforms_sanitize_amount( $field['price'] ); } elseif ( $required && $field_format === self::FORMAT_SINGLE ) { $field_value = wpforms_format_amount( 0 ); } else { $field_value = ''; } $properties['inputs']['primary']['attr']['value'] = ! empty( $field_value ) ? wpforms_format_amount( $field_value, true ) : $field_value; // Single item and hidden format should hide the input field. if ( $this->is_hidden( $field ) ) { $properties['container']['class'][] = 'wpforms-field-hidden'; $properties['label']['class'][] = 'wpforms-hidden'; } if ( $this->is_payment_quantities_enabled( $field ) ) { $properties['container']['class'][] = ' wpforms-payment-quantities-enabled'; } return $properties; } /** * Get field populated single property value. * * @since 1.8.2 * * @param string $raw_value Value from a GET param, always a string. * @param string $input Represent a subfield inside the field. May be empty. * @param array $properties Field properties. * @param array $field Current field specific data. * * @return array Modified field properties. */ protected function get_field_populated_single_property_value( $raw_value, $input, $properties, $field ) { if ( ! is_string( $raw_value ) ) { return $properties; } if ( ! $this->is_user_defined( $field ) ) { return $properties; } $get_value = stripslashes( sanitize_text_field( $raw_value ) ); $get_value = ! empty( $get_value ) ? wpforms_sanitize_amount( $get_value ) : ''; $get_value_formatted = ! empty( $get_value ) ? wpforms_format_amount( $get_value ) : ''; // `primary` by default. if ( ! empty( $input ) && isset( $properties['inputs'][ $input ] ) ) { $properties['inputs'][ $input ]['attr']['value'] = $get_value_formatted; } return $properties; } /** * Field options panel inside the builder. * * @since 1.8.2 * * @param array $field Field data and settings. */ public function field_options( $field ) { /* * Basic field options. */ $this->field_option( 'basic-options', $field, [ 'markup' => 'open' ] ); $this->field_option( 'label', $field ); $this->field_option( 'description', $field ); $this->price_option( $field ); $this->format_option( $field ); $this->min_price_option( $field ); $this->field_option( 'quantity', $field, [ 'hidden' => ! $this->is_single_item( $field ) ] ); $this->field_option( 'required', $field ); $this->field_option( 'basic-options', $field, [ 'markup' => 'close' ] ); $this->field_option( 'advanced-options', $field, [ 'markup' => 'open' ] ); $this->field_option( 'size', $field ); $this->price_label_option( $field ); $visibility = ! empty( $field['format'] ) && $this->is_user_defined( $field ) ? '' : 'wpforms-hidden'; $this->field_option( 'placeholder', $field, [ 'class' => $visibility ] ); $this->field_option( 'css', $field ); $this->field_option( 'label_hide', $field ); $this->field_option( 'advanced-options', $field, [ 'markup' => 'close' ] ); } /** * Price label option. * * @since 1.8.8 * * @param array $field Field Data. * * @return void */ private function price_label_option( array $field ) { // Price display. $output = $this->field_element( 'label', $field, [ 'slug' => 'price_label', 'value' => esc_html__( 'Price Display', 'wpforms-lite' ), 'tooltip' => esc_html__( 'Specify how the price is displayed under the product name.', 'wpforms-lite' ), ], false ); $output .= $this->field_element( 'text', $field, [ 'slug' => 'price_label', 'class' => 'wpforms-single-item-price-label-display', 'value' => $this->get_single_item_price_label( $field ), ], false ); $this->field_element( 'row', $field, [ 'slug' => 'price_label', 'content' => $output, 'class' => $this->is_single_item( $field ) ? '' : 'wpforms-hidden', ] ); } /** * Get price label for single item type. * * @since 1.8.8 * * @param array $field Field data and settings. */ private function get_single_item_price_label( array $field ) { if ( ! isset( $field['price_label'] ) ) { return sprintf( /* translators: %s - Single item field price label. */ esc_html__( 'Price: %s', 'wpforms-lite' ), '{price}' ); } return $field['price_label']; } /** * Field price option. * * @since 1.8.6 * * @param array $field Field data and settings. */ private function price_option( $field ) { $price = ! empty( $field['price'] ) ? wpforms_format_amount( wpforms_sanitize_amount( $field['price'] ) ) : ''; $tooltip = esc_html__( 'Enter the price of the item, without a currency symbol.', 'wpforms-lite' ); $output = $this->field_element( 'label', $field, [ 'slug' => 'price', 'value' => esc_html__( 'Item Price', 'wpforms-lite' ), 'tooltip' => $tooltip, ], false ); $output .= $this->field_element( 'text', $field, [ 'slug' => 'price', 'value' => $price, 'class' => 'wpforms-money-input', 'placeholder' => wpforms_format_amount( 0 ), ], false ); $this->field_element( 'row', $field, [ 'slug' => 'price', 'content' => $output, ] ); } /** * Field format option. * * @since 1.8.6 * * @param array $field Field data and settings. */ private function format_option( $field ) { $format = ! empty( $field['format'] ) ? esc_attr( $field['format'] ) : self::FORMAT_SINGLE; $tooltip = esc_html__( 'Select the item type.', 'wpforms-lite' ); $options = [ self::FORMAT_SINGLE => esc_html__( 'Single Item', 'wpforms-lite' ), self::FORMAT_USER => esc_html__( 'User Defined', 'wpforms-lite' ), self::FORMAT_HIDDEN => esc_html__( 'Hidden', 'wpforms-lite' ), ]; $output = $this->field_element( 'label', $field, [ 'slug' => 'format', 'value' => esc_html__( 'Item Type', 'wpforms-lite' ), 'tooltip' => $tooltip, ], false ); $output .= $this->field_element( 'select', $field, [ 'slug' => 'format', 'value' => $format, 'options' => $options, ], false ); $this->field_element( 'row', $field, [ 'slug' => 'format', 'content' => $output, ] ); } /** * Field minimum price option. * * @since 1.8.6 * * @param array $field Field data and settings. */ private function min_price_option( $field ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing if ( isset( $_POST['action'] ) && $_POST['action'] === 'wpforms_new_field_payment-single' ) { // Use a default minimum price when adding new field. $min_price = wpforms_format_amount( self::MIN_PRICE_DEFAULT ); } elseif ( isset( $field['min_price'] ) ) { // Use saved minimum price if it exists. $min_price = wpforms_format_amount( wpforms_sanitize_amount( $field['min_price'] ) ); } else { // Use 0 as a fallback for old forms. $min_price = 0; } $tooltip = esc_html__( 'Enter the minimum price of the item, without a currency symbol.', 'wpforms-lite' ); $is_hidden = empty( $field['format'] ) || ! $this->is_user_defined( $field ) ? 'wpforms-hidden' : ''; $output = $this->field_element( 'label', $field, [ 'slug' => 'min_price', 'value' => esc_html__( 'Minimum Price', 'wpforms-lite' ), 'tooltip' => $tooltip, ], false ); $output .= $this->field_element( 'text', $field, [ 'slug' => 'min_price', 'value' => $min_price, 'data' => [ 'minimum-price' => self::MIN_PRICE_DEFAULT, ], 'class' => 'wpforms-money-input', ], false ); $notice = sprintf( /* translators: %1$s - the default minimum price. */ esc_html__( 'Requiring a minimum price of at least %1$s helps protect you against card testing by fraudsters.', 'wpforms-lite' ), esc_html( wpforms_format_amount( self::MIN_PRICE_DEFAULT, true ) ) ); $is_notice_hidden = $this->is_min_price_passed( $field ) || $is_hidden ? 'wpforms-hidden' : ''; $output .= sprintf( '<div class="wpforms-alert-warning wpforms-alert wpforms-item-minimum-price-alert %1$s"> <h4>%2$s</h4> <p>%3$s</p> </div>', esc_attr( $is_notice_hidden ), esc_html__( 'Security Recommendation', 'wpforms-lite' ), $notice ); $this->field_element( 'row', $field, [ 'slug' => 'min_price', 'content' => $output, 'class' => $is_hidden, ] ); } /** * Field preview inside the builder. * * @since 1.8.2 * * @param array $field Field data and settings. */ public function field_preview( $field ) { $price = ! empty( $field['price'] ) ? wpforms_format_amount( wpforms_sanitize_amount( $field['price'] ), true ) : wpforms_format_amount( 0, true ); $min_price = ! empty( $field['min_price'] ) ? wpforms_format_amount( wpforms_sanitize_amount( $field['min_price'] ), true ) : wpforms_format_amount( self::MIN_PRICE_DEFAULT, true ); $placeholder = ! empty( $field['placeholder'] ) ? $field['placeholder'] : wpforms_format_amount( 0 ); $format = ! empty( $field['format'] ) ? $field['format'] : self::FORMAT_SINGLE; $value = ! empty( $field['price'] ) ? wpforms_format_amount( wpforms_sanitize_amount( $field['price'] ) ) : ''; $is_single = $this->is_single_item( $field ); $single_label = str_replace( '{price}', '<span class="price">' . esc_html( $price ) . '</span>', wp_kses( $this->get_single_item_price_label( $field ), wpforms_builder_preview_get_allowed_tags() ) ); $this->field_preview_option( 'label', $field ); echo '<div class="format-selected-' . esc_attr( $format ) . ' format-selected">'; $hidden = ! $is_single ? 'wpforms-hidden' : ''; echo '<p class="item-price item-price-single ' . esc_attr( $hidden ) . '">'; echo wp_kses( '<span class="price-label">' . $single_label . '</span>', [ 'span' => [ 'class' => [], ], ] ); echo '</p>'; $hidden = ! $this->is_hidden( $field ) ? 'wpforms-hidden' : ''; echo '<p class="item-price item-price-hidden ' . esc_attr( $hidden ) . '">'; printf( wp_kses( /* translators: %1$s - Item Price value. */ __( 'Price: <span class="price">%1$s</span>', 'wpforms-lite' ), [ 'span' => [ 'class' => [], ], ] ), esc_html( $price ) ); echo '</p>'; $hidden = ! $is_single ? 'wpforms-hidden' : ''; $this->field_preview_option( 'quantity', $field, [ 'class' => $hidden ] ); echo '<div class="single-item-user-defined-block">'; printf( '<input type="text" placeholder="%s" class="primary-input" value="%s" readonly>', esc_attr( $placeholder ), esc_attr( $value ) ); $hidden = $this->is_min_price_passed( $field ) ? 'wpforms-hidden' : ''; echo '<i class="fa fa-exclamation-triangle ' . esc_attr( $hidden ) . '"></i>'; echo '</div>'; $this->field_preview_option( 'description', $field ); $hidden = ! isset( $field['min_price'] ) || empty( (float) wpforms_sanitize_amount( $field['min_price'] ) ) ? 'wpforms-hidden' : ''; echo '<div class="item-min-price ' . esc_attr( $hidden ) . '">'; printf( wp_kses( /* translators: %1$s - Minimum Price value. */ __( 'Minimum Price: <span class="min-price">%1$s</span>', 'wpforms-lite' ), [ 'span' => [ 'class' => [], ], ] ), esc_html( $min_price ) ); echo '</div>'; echo '<p class="item-price-hidden-note">'; esc_html_e( 'Note: Item type is set to hidden and will not be visible when viewing the form.', 'wpforms-lite' ); echo '</p>'; echo '</div>'; } /** * Field display on the form front-end. * * @since 1.8.2 * * @param array $field Field data and settings. * @param array $deprecated Deprecated field attributes. * @param array $form_data Form data and settings. */ public function field_display( $field, $deprecated, $form_data ) { // Shortcut for easier access. $primary = $field['properties']['inputs']['primary']; $field_format = ! empty( $field['format'] ) ? $field['format'] : self::FORMAT_SINGLE; // Placeholder attribute is only applicable to password, search, tel, text and url inputs, not hidden. // aria-errormessage attribute is not allowed for hidden inputs. if ( ! $this->is_user_defined( $field ) ) { unset( $primary['attr']['placeholder'], $primary['attr']['aria-errormessage'] ); } switch ( $field_format ) { case self::FORMAT_SINGLE: case self::FORMAT_HIDDEN: if ( $field_format === self::FORMAT_SINGLE ) { $price = ! empty( $field['price'] ) ? $field['price'] : 0; $field_label = str_replace( '{price}', '<span class="wpforms-price">' . esc_html( wpforms_format_amount( wpforms_sanitize_amount( $price ), true ) ) . '</span>', $this->get_single_item_price_label( $field ) ); echo '<div class="wpforms-single-item-price-content">'; echo '<div class="wpforms-single-item-price ' . wpforms_sanitize_classes( $primary['class'], true ) . '">'; echo wp_kses( $field_label, [ 'span' => [ 'class' => [], ], ] ); echo '</div>'; $this->display_quantity_dropdown( $field ); echo '</div>'; } // Primary price field. printf( '<input type="hidden" %s>', wpforms_html_attributes( $primary['id'], $primary['class'], $primary['data'], $primary['attr'] ) // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ); break; case self::FORMAT_USER: printf( '<input type="text" %s>', wpforms_html_attributes( $primary['id'], $primary['class'], $primary['data'], $primary['attr'] ) // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ); break; default: break; } } /** * Validate field on form submit. * * @since 1.8.2 * * @param int $field_id Field ID. * @param string $field_submit Submitted field value (raw data). * @param array $form_data Form data and settings. */ public function validate( $field_id, $field_submit, $form_data ) { $is_required = ! empty( $form_data['fields'][ $field_id ]['required'] ); // If field is required, check for data. if ( empty( $field_submit ) && $is_required ) { wpforms()->obj( 'process' )->errors[ $form_data['id'] ][ $field_id ] = wpforms_get_required_label(); return; } /** * Whether to validate amount or not of the Payment Single item field. * * @since 1.8.4 * * @param bool $validate Whether to validate amount or not. Default true. * @param int $field_id Field ID. * @param string $field_submit Field data submitted by a user. * @param array $form_data Form data and settings. */ $validate_amount = apply_filters( 'wpforms_forms_fields_payment_single_field_validate_amount', true, $field_id, $field_submit, $form_data ); // If field format is not user provided, validate the amount posted. if ( ! empty( $field_submit ) && $validate_amount && ! $this->is_user_defined( $form_data['fields'][ $field_id ] ) ) { $price = wpforms_sanitize_amount( $form_data['fields'][ $field_id ]['price'] ); $submit = wpforms_sanitize_amount( $field_submit ); if ( $price !== $submit ) { wpforms()->obj( 'process' )->errors[ $form_data['id'] ][ $field_id ] = esc_html__( 'Amount mismatch', 'wpforms-lite' ); } } // If field format is provided by user, additionally compare the amount with a minimum price. if ( ! empty( $field_submit ) && $validate_amount && $this->is_user_defined( $form_data['fields'][ $field_id ] ) ) { $submit = wpforms_sanitize_amount( $field_submit ); if ( $submit < 0 ) { wpforms()->obj( 'process' )->errors[ $form_data['id'] ][ $field_id ] = esc_html__( 'Amount can\'t be negative' , 'wpforms-lite' ); } if ( empty( $form_data['fields'][ $field_id ]['min_price'] ) && ! $is_required ) { return; } $min_price = wpforms_sanitize_amount( $form_data['fields'][ $field_id ]['min_price'] ); if ( $submit < $min_price ) { wpforms()->obj( 'process' )->errors[ $form_data['id'] ][ $field_id ] = esc_html__( 'Amount can\'t be less than the required minimum.' , 'wpforms-lite' ); } } } /** * Format and sanitize field. * * @since 1.8.2 * * @param int $field_id Field ID. * @param string $field_submit Field data submitted by a user. * @param array $form_data Form data and settings. */ public function format( $field_id, $field_submit, $form_data ) { $field = $form_data['fields'][ $field_id ]; $name = ! empty( $field['label'] ) ? sanitize_text_field( $field['label'] ) : ''; // Only trust the value if the field has the user defined format OR it is the entry preview. if ( $this->is_user_defined( $field ) || wpforms_is_ajax( 'wpforms_get_entry_preview' ) ) { $amount = wpforms_sanitize_amount( $field_submit ); } else { $amount = wpforms_sanitize_amount( $field['price'] ); } $field_data = [ 'name' => $name, 'value' => wpforms_format_amount( $amount, true ), 'amount' => wpforms_format_amount( $amount ), 'amount_raw' => $amount, 'currency' => wpforms_get_currency(), 'id' => absint( $field_id ), 'type' => sanitize_key( $this->type ), ]; if ( $this->is_payment_quantities_enabled( $field ) ) { $field_data['quantity'] = $this->get_submitted_field_quantity( $field, $form_data ); } wpforms()->obj( 'process' )->fields[ $field_id ] = $field_data; } /** * Display the minimum price description for the field. * * @since 1.8.6 * * @param array $field Field data and settings. * @param array $form_data Form data and settings. */ public function field_minimum_price_description( $field, $form_data ) { if ( ! $this->is_user_defined( $field ) || ! isset( $field['min_price'] ) || empty( (float) wpforms_sanitize_amount( $field['min_price'] ) ) ) { return; } $description = sprintf( /* translators: %1$s - Minimum Price value. */ __( 'Minimum Price: %1$s', 'wpforms-lite' ), wpforms_format_amount( wpforms_sanitize_amount( $field['min_price'] ), true ) ); printf( '<div class="wpforms-field-description">%s</div>', esc_html( $description ) ); } /** * Add class to the builder field preview. * * @since 1.8.6 * * @param string $css Class names. * @param array $field Field properties. * * @return string */ public function preview_field_class( $css, $field ) { $css = parent::preview_field_class( $css, $field ); if ( $field['type'] !== $this->type ) { return $css; } if ( ! $this->is_user_defined( $field ) ) { return $css; } if ( $this->is_min_price_passed( $field ) ) { return $css; } $css .= ' min-price-warning'; return $css; } /** * Define if format of field is User Defined. * * @since 1.8.6 * * @param array $field Field data. * * @return bool */ private function is_user_defined( $field ) { return ! empty( $field['format'] ) && $field['format'] === self::FORMAT_USER; } /** * Define if format of field is Single Item. * * @since 1.8.7 * * @param array $field Field data. * * @return bool */ private function is_single_item( $field ) { return empty( $field['format'] ) || $field['format'] === self::FORMAT_SINGLE; } /** * Define if format of field is Hidden. * * @since 1.8.8 * * @param array $field Field data. * * @return bool */ private function is_hidden( $field ) { return empty( $field['format'] ) || $field['format'] === self::FORMAT_HIDDEN; } /** * Define if minimum price is equal or more than default one. * * @since 1.8.6 * * @param array $field Field data. * * @return bool */ private function is_min_price_passed( $field ) { return isset( $field['min_price'] ) && (float) wpforms_sanitize_amount( $field['min_price'] ) >= (float) self::MIN_PRICE_DEFAULT; } } Forms/Fields/EntryPreview/Field.php 0000644 00000017124 15174710275 0013255 0 ustar 00 <?php namespace WPForms\Forms\Fields\EntryPreview; use WPForms\Forms\Fields\Traits\ProField as ProFieldTrait; use WPForms_Field; /** * Entry preview field. * * @since 1.9.4 */ class Field extends WPForms_Field { use ProFieldTrait; /** * Init. * * @since 1.9.4 */ public function init() { // Define field type information. $this->name = esc_html__( 'Entry Preview', 'wpforms-lite' ); $this->keywords = esc_html__( 'confirm', 'wpforms-lite' ); $this->type = 'entry-preview'; $this->icon = 'fa-file-text-o'; $this->order = 190; $this->group = 'fancy'; $this->allow_read_only = false; $this->init_pro_field(); $this->hooks(); } /** * Hooks. * * @since 1.9.4 */ protected function hooks() { add_filter( 'wpforms_builder_strings', [ $this, 'add_builder_strings' ], 10, 2 ); add_filter( 'wpforms_field_preview_display_duplicate_button', [ $this, 'field_display_duplicate_button' ], 10, 2 ); add_filter( 'wpforms_field_new_display_duplicate_button', [ $this, 'field_display_duplicate_button' ], 10, 2 ); } /** * Field options panel inside the builder. * * @since 1.9.4 * * @param array $field Field data. */ public function field_options( $field ) { // Options open markup. $this->field_option( 'basic-options', $field, [ 'markup' => 'open', 'after_title' => $this->get_field_options_notice(), ] ); if ( empty( $this->is_disabled_field ) ) { $this->field_element( 'row', $field, [ 'slug' => 'description', 'content' => sprintf( '<p class="note">%s</p>', esc_html__( 'Entry Preview must be displayed on its own page, without other fields. HTML fields are allowed.', 'wpforms-lite' ) ), ] ); } $this->field_element( 'row', $field, [ 'slug' => 'preview-notice-enable', 'content' => $this->field_element( 'toggle', $field, [ 'slug' => 'preview-notice-enable', // When we add the field to a form, it enabled by default. 'value' => ! empty( $field['preview-notice-enable'] ) || wp_doing_ajax(), 'desc' => esc_html__( 'Display Preview Notice', 'wpforms-lite' ), 'tooltip' => esc_html__( 'Check this option to show a message above the entry preview.', 'wpforms-lite' ), ], false ), ] ); $this->field_element( 'row', $field, [ 'slug' => 'preview-notice', 'content' => $this->field_element( 'label', $field, [ 'slug' => 'preview-notice', 'value' => esc_html__( 'Preview Notice', 'wpforms-lite' ), 'tooltip' => esc_html__( 'Fill in the message to show above the entry preview.', 'wpforms-lite' ), ], false ) . $this->field_element( 'textarea', $field, [ 'slug' => 'preview-notice', 'value' => $field['preview-notice'] ?? self::get_default_notice(), ], false ), ] ); $this->field_option( 'basic-options', $field, [ 'markup' => 'close' ] ); $this->field_option( 'advanced-options', $field, [ 'markup' => 'open' ] ); $this->field_element( 'row', $field, [ 'slug' => 'style', 'content' => $this->field_element( 'label', $field, [ 'slug' => 'style', 'value' => esc_html__( 'Style', 'wpforms-lite' ), 'tooltip' => esc_html__( 'Choose the entry preview display style.', 'wpforms-lite' ), ], false ) . $this->field_element( 'select', $field, [ 'slug' => 'style', 'value' => ! empty( $field['style'] ) ? $field['style'] : 'basic', 'options' => self::get_styles(), ], false ), ] ); $this->field_option( 'css', $field ); $this->field_option( 'advanced-options', $field, [ 'markup' => 'close' ] ); } /** * Create the field preview. * * @since 1.9.4 * * @param array $field Field data and settings. * * @noinspection HtmlUnknownAttribute*/ public function field_preview( $field ) { printf( '<label class="label-title"> <span class="text">%1$s</span>%2$s</label>', esc_html__( 'Entry Preview', 'wpforms-lite' ), $this->get_field_preview_badge() // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ); $is_new_field = wp_doing_ajax(); $notice = ! empty( $field['preview-notice-enable'] ) && isset( $field['preview-notice'] ) && ! wpforms_is_empty_string( $field['preview-notice'] ) ? force_balance_tags( $field['preview-notice'] ) : ''; $notice = $is_new_field || wpforms_is_empty_string( $notice ) ? self::get_default_notice() : $notice; $is_disabled = $is_new_field || ! empty( $field['preview-notice-enable'] ); printf( '<div class="wpforms-entry-preview-notice nl2br"%2$s>%1$s</div>', wp_kses_post( nl2br( $notice ) ), ! $is_disabled ? ' style="display: none"' : '' ); printf( '<div class="wpforms-alert wpforms-alert-info"%2$s> <p>%1$s</p> </div>', esc_html__( 'Entry preview will be displayed here and will contain all fields found on the previous page.', 'wpforms-lite' ), $is_disabled ? ' style="display: none"' : '' ); } /** * Display the field input elements on the frontend. * * @since 1.9.4 * * @param array $field Field data and settings. * @param array $deprecated Field attributes. * @param array $form_data Form data and settings. */ public function field_display( $field, $deprecated, $form_data ) { } /** * Add custom JS i18n strings for the builder. * * @since 1.9.4 * * @param array|mixed $strings List of strings. * @param array $form Current form. * * @return array * @noinspection PhpMissingParamTypeInspection * @noinspection PhpUnusedParameterInspection */ public function add_builder_strings( $strings, $form ): array { $strings = (array) $strings; $strings['entry_preview_require_page_break'] = esc_html__( 'Page breaks are required for entry previews to work. If you\'d like to remove page breaks, you\'ll have to first remove the entry preview field.', 'wpforms-lite' ); $strings['entry_preview_default_notice'] = self::get_default_notice(); $strings['entry_preview_require_previous_button'] = esc_html__( 'You can\'t hide the previous button because it is required for the entry preview field on this page.', 'wpforms-lite' ); return $strings; } /** * Get default notice. * * @since 1.9.4 * * @return string */ protected static function get_default_notice(): string { return sprintf( "<strong>%s</strong>\n%s", esc_html__( 'This is a preview of your submission. It has not been submitted yet!', 'wpforms-lite' ), esc_html__( 'Please take a moment to verify your information. You can also go back to make changes.', 'wpforms-lite' ) ); } /** * Get a list of available styles. * * @since 1.9.4 * * @return array */ protected static function get_styles(): array { return [ 'basic' => esc_html__( 'Basic', 'wpforms-lite' ), 'compact' => esc_html__( 'Compact', 'wpforms-lite' ), 'table' => esc_html__( 'Table', 'wpforms-lite' ), 'table_compact' => esc_html__( 'Table, Compact', 'wpforms-lite' ), ]; } /** * Disallow the field preview "Duplicate" button. * * @since 1.9.9 * * @param bool|mixed $display Display switch. * @param array $field Field settings. * * @return bool */ public function field_display_duplicate_button( $display, array $field ): bool { $type = $field['type'] ?? ''; if ( $type === $this->type ) { // Pagebreak fields cannot be duplicated. return false; } return (bool) $display; } } Forms/Fields/CreditCard/Field.php 0000644 00000015661 15174710275 0012622 0 ustar 00 <?php namespace WPForms\Forms\Fields\CreditCard; use WPForms\Forms\Fields\Traits\ProField as ProFieldTrait; use WPForms_Field; /** * Credit card field (legacy). * * @since 1.0.0 */ class Field extends WPForms_Field { use ProFieldTrait; /** * Primary class constructor. * * @since 1.0.0 */ public function init() { // Define field type information. $this->name = esc_html__( 'Credit Card', 'wpforms-lite' ); $this->type = 'credit-card'; $this->icon = 'fa-credit-card'; $this->order = 90; $this->group = 'payment'; $this->init_pro_field(); $this->hooks(); } /** * Hooks. * * @since 1.8.1 */ protected function hooks(): void { } /** * Field options panel inside the builder. * * @since 1.0.0 * * @param array $field Field settings. */ public function field_options( $field ) { /* * Basic field options. */ // Options open markup. $this->field_option( 'basic-options', $field, [ 'markup' => 'open', 'after_title' => $this->get_field_options_notice(), ] ); // Label. $this->field_option( 'label', $field ); // Description. $this->field_option( 'description', $field ); // Required toggle. $this->field_option( 'required', $field ); // Options close markup. $args = [ 'markup' => 'close', ]; $this->field_option( 'basic-options', $field, $args ); /* * Advanced field options. */ // Options open markup. $args = [ 'markup' => 'open', ]; $this->field_option( 'advanced-options', $field, $args ); // Size. $this->field_option( 'size', $field ); // Card Number. $cardnumber_placeholder = ! empty( $field['cardnumber_placeholder'] ) ? esc_attr( $field['cardnumber_placeholder'] ) : ''; printf( '<div class="wpforms-clear wpforms-field-option-row wpforms-field-option-row-cardnumber" id="wpforms-field-option-row-%1$d-cardnumber" data-subfield="cardnumber" data-field-id="%1$d">', absint( $field['id'] ) ); $this->field_element( 'label', $field, [ 'slug' => 'cardnumber_placeholder', 'value' => esc_html__( 'Card Number Placeholder Text', 'wpforms-lite' ), ] ); echo '<div class="placeholder">'; printf( '<input type="text" class="placeholder-update" id="wpforms-field-option-%1$d-cardnumber_placeholder" name="fields[%1$d][cardnumber_placeholder]" value="%2$s" data-field-id="%1$d" data-subfield="credit-card-cardnumber">', absint( $field['id'] ), esc_attr( $cardnumber_placeholder ) ); echo '</div>'; echo '</div>'; // CVC/Security Code. $cardcvc_placeholder = ! empty( $field['cardcvc_placeholder'] ) ? $field['cardcvc_placeholder'] : ''; printf( '<div class="wpforms-clear wpforms-field-option-row wpforms-field-option-row-cvc" id="wpforms-field-option-row-%1$d-cvc" data-subfield="cvc" data-field-id="%1$d">', absint( $field['id'] ) ); $this->field_element( 'label', $field, [ 'slug' => 'cardcvc_placeholder', 'value' => esc_html__( 'Security Code Placeholder Text', 'wpforms-lite' ), ] ); echo '<div class="placeholder">'; printf( '<input type="text" class="placeholder-update" id="wpforms-field-option-%1$d-cardcvc_placeholder" name="fields[%1$d][cardcvc_placeholder]" value="%2$s" data-field-id="%1$d" data-subfield="credit-card-cardcvc">', absint( $field['id'] ), esc_attr( $cardcvc_placeholder ) ); echo '</div>'; echo '</div>'; // Card Name. $cardname_placeholder = ! empty( $field['cardname_placeholder'] ) ? $field['cardname_placeholder'] : ''; printf( '<div class="wpforms-clear wpforms-field-option-row wpforms-field-option-row-cardname" id="wpforms-field-option-row-%1$d-cardname" data-subfield="cardname" data-field-id="%1$d">', absint( $field['id'] ) ); $this->field_element( 'label', $field, [ 'slug' => 'cardname_placeholder', 'value' => esc_html__( 'Name on Card Placeholder Text', 'wpforms-lite' ), ] ); echo '<div class="placeholder">'; printf( '<input type="text" class="placeholder-update" id="wpforms-field-option-%1$d-cardname_placeholder" name="fields[%1$d][cardname_placeholder]" value="%2$s" data-field-id="%1$d" data-subfield="credit-card-cardname">', absint( $field['id'] ), esc_attr( $cardname_placeholder ) ); echo '</div>'; echo '</div>'; // Custom CSS classes. $this->field_option( 'css', $field ); // Hide Label. $this->field_option( 'label_hide', $field ); // Hide sublabels. $this->field_option( 'sublabel_hide', $field ); // Options close markup. $args = [ 'markup' => 'close', ]; $this->field_option( 'advanced-options', $field, $args ); } /** * Field preview inside the builder. * * @since 1.0.0 * * @param array $field Field settings. */ public function field_preview( $field ) { // Define data. $number_placeholder = ! empty( $field['cardnumber_placeholder'] ) ? esc_attr( $field['cardnumber_placeholder'] ) : ''; $cvc_placeholder = ! empty( $field['cardcvc_placeholder'] ) ? esc_attr( $field['cardcvc_placeholder'] ) : ''; $name_placeholder = ! empty( $field['cardname_placeholder'] ) ? esc_attr( $field['cardname_placeholder'] ) : ''; // Label. $this->field_preview_option( 'label', $field, [ 'label_badge' => $this->get_field_preview_badge(), ] ); ?> <div class="format-selected format-selected-full"> <div class="wpforms-field-row"> <div class="wpforms-credit-card-cardnumber"> <label class="wpforms-sub-label"><?php esc_html_e( 'Card Number', 'wpforms-lite' ); ?></label> <input type="text" placeholder="<?php echo esc_attr( $number_placeholder ); ?>" readonly> </div> <div class="wpforms-credit-card-cardcvc"> <label class="wpforms-sub-label"><?php esc_html_e( 'Security Code', 'wpforms-lite' ); ?></label> <input type="text" placeholder="<?php echo esc_attr( $cvc_placeholder ); ?>" readonly> </div> </div> <div class="wpforms-field-row"> <div class="wpforms-credit-card-cardname"> <label class="wpforms-sub-label"><?php esc_html_e( 'Name on Card', 'wpforms-lite' ); ?></label> <input type="text" placeholder="<?php echo esc_attr( $name_placeholder ); ?>" readonly> </div> <div class="wpforms-credit-card-expiration"> <label class="wpforms-sub-label"><?php esc_html_e( 'Expiration', 'wpforms-lite' ); ?></label> <div class="wpforms-credit-card-cardmonth"> <select readonly> <option>MM</option> </select> </div> <span>/</span> <div class="wpforms-credit-card-cardyear"> <select readonly> <option>YY</option> </select> </div> </div> </div> </div> <?php // Description. $this->field_preview_option( 'description', $field ); } /** * Field display on the form front-end. * * @since 1.0.0 * * @param array $field Field data and settings. * @param array $deprecated Deprecated field attributes. Use field properties. * @param array $form_data Form data and settings. */ public function field_display( $field, $deprecated, $form_data ) { } } Forms/Fields/Hidden/Field.php 0000644 00000007160 15174710275 0012004 0 ustar 00 <?php namespace WPForms\Forms\Fields\Hidden; use WPForms\Forms\Fields\Traits\ProField as ProFieldTrait; use WPForms_Field; /** * Hidden text field. * * @since 1.9.4 */ class Field extends WPForms_Field { use ProFieldTrait; /** * Primary class constructor. * * @since 1.9.4 */ public function init() { // Define field type information. $this->name = esc_html__( 'Hidden Field', 'wpforms-lite' ); $this->type = 'hidden'; $this->icon = 'fa-eye-slash'; $this->order = 98; $this->group = 'fancy'; $this->allow_read_only = false; $this->default_settings = [ 'label_hide' => '1', ]; $this->init_pro_field(); $this->hooks(); } /** * Hooks. * * @since 1.9.4 */ protected function hooks(): void { add_filter( 'wpforms_field_new_class', [ $this, 'preview_field_new_class' ], 10, 2 ); } /** * Field options panel inside the builder. * * @since 1.9.4 * * @param array $field Field data and settings. */ public function field_options( $field ) { /* * Basic field options. */ // Options open markup. $this->field_option( 'basic-options', $field, [ 'markup' => 'open', 'after_title' => $this->get_field_options_notice(), ] ); // Label. $this->field_option( 'label', $field, [ 'tooltip' => esc_html__( 'Enter text for the form field label. Never displayed on the front-end.', 'wpforms-lite' ), ] ); // Set the label to disable. $this->field_element( 'text', $field, [ 'type' => 'hidden', 'slug' => 'label_disable', 'value' => '1', ] ); // Options close markup. $args = [ 'markup' => 'close', ]; $this->field_option( 'basic-options', $field, $args ); // Advanced options open markup. $this->field_option( 'advanced-options', $field, [ 'markup' => 'open', ] ); // Default value. $this->field_option( 'default_value', $field ); // Custom CSS classes. $this->field_option( 'css', $field ); // Hide Label. $this->field_option( 'label_hide', $field, [ 'class' => 'wpforms-disabled', ] ); // Advanced options close markup. $this->field_option( 'advanced-options', $field, [ 'markup' => 'close', ] ); } /** * Get a new field CSS class. * * @since 1.9.4 * * @param string|mixed $css_class Preview new field CSS class. * @param array $field Field data. * * @return string */ public function preview_field_new_class( $css_class, array $field ): string { $css_class = (string) $css_class; if ( empty( $field['type'] ) || $field['type'] !== $this->type ) { return $css_class; } return trim( $css_class . ' label_hide' ); } /** * Field preview inside the builder. * * @since 1.9.4 * * @param array $field Field data and settings. */ public function field_preview( $field ) { // Define data. $default_value = ! empty( $field['default_value'] ) ? $field['default_value'] : ''; // The Hidden field label is always hidden. $field['label_hide'] = '1'; // Label. $this->field_preview_option( 'label', $field, [ 'label_badge' => $this->get_field_preview_badge(), ] ); // Primary input. echo '<input type="text" class="primary-input" value="' . esc_attr( $default_value ) . '" readonly>'; } /** * Field display on the form front-end. * * @since 1.9.4 * * @param array $field Field data and settings. * @param array $deprecated Not used any more field attributes. * @param array $form_data Form data and settings. */ public function field_display( $field, $deprecated, $form_data ) { } } Forms/Fields/Addons/Coupon/Field.php 0000644 00000016445 15174710275 0013272 0 ustar 00 <?php namespace WPForms\Forms\Fields\Addons\Coupon; use WPForms\Forms\Fields\Traits\ProField as ProFieldTrait; use WPForms_Field; /** * Coupon Field class. * * @since 1.0.0 */ class Field extends WPForms_Field { use ProFieldTrait; /** * Whether the addon is active. * * @since 1.9.4 * * @var bool */ private $is_addon_active = false; /** * Define field type information. * * @since 1.9.4 */ public function init() { // Define field type information. $this->name = esc_html__( 'Coupon', 'wpforms-lite' ); $this->keywords = esc_html__( 'discount, sale', 'wpforms-lite' ); $this->type = 'payment-coupon'; $this->icon = 'fa-ticket'; $this->order = 100; $this->group = 'payment'; $this->addon_slug = 'coupons'; $this->is_addon_active = function_exists( 'wpforms_' . $this->addon_slug ); $this->init_pro_field(); $this->hooks(); } /** * Define field hooks. * * @since 1.9.4 */ protected function hooks() { add_filter( 'wpforms_field_new_display_duplicate_button', [ $this, 'field_display_duplicate_button' ], 20, 2 ); add_filter( 'wpforms_field_preview_display_duplicate_button', [ $this, 'field_display_duplicate_button' ], 20, 2 ); } /** * Disallow field preview "Duplicate" button. * * @since 1.9.4 * * @param bool|mixed $display Display switch. * @param array $field Field settings. * * @return bool */ public function field_display_duplicate_button( $display, array $field ) { return $field['type'] === $this->type ? false : $display; } /** * Define additional field options. * * @since 1.9.4 * * @param array $field Field data and settings. */ public function field_options( $field ) { // Options open markup. $this->field_option( 'basic-options', $field, [ 'markup' => 'open', 'after_title' => $this->get_field_options_notice(), ] ); $this->field_option( 'label', $field ); $this->field_option( 'description', $field ); $coupons = []; $form_coupons = []; if ( $this->is_addon_active ) { $coupons = wpforms_coupons()->get( 'repository' )->get_coupons( [ 'limit' => -1, 'fields' => 'id=>name', ] ); $form_coupons = wpforms_coupons()->get( 'repository' )->get_form_coupons( $this->get_form_id() ); } $warning = sprintf( '<p class="wpforms-alert wpforms-alert-warning%1$s">%2$s</p>', empty( $form_coupons ) && empty( $this->is_disabled_field ) ? '' : ' wpforms-hidden', esc_html__( 'You haven\'t selected any coupons that can be used with this form. Please choose at least one coupon.', 'wpforms-lite' ) ); $coupons_field_label = $this->field_element( 'label', $field, [ 'slug' => 'allowed_coupons', 'value' => esc_html__( 'Allowed Coupons', 'wpforms-lite' ), 'tooltip' => esc_html__( 'Choose coupons that can be used in the field.', 'wpforms-lite' ), ], false ); $coupons_field = $this->get_allowed_coupons_field( $coupons, $form_coupons, $field ); $allowed_forms_json = sprintf( '<input type="hidden" name="fields[%1$s][allowed_coupons_json]" class="wpforms-coupons-allowed_coupons_json" value="%2$s">', $field['id'], wp_json_encode( $form_coupons ) ); $this->field_element( 'row', $field, [ 'slug' => 'allowed_coupons', 'content' => $coupons_field_label . $coupons_field . $allowed_forms_json . $warning, ] ); $this->field_option( 'required', $field ); $this->field_option( 'basic-options', $field, [ 'markup' => 'close' ] ); $this->field_option( 'advanced-options', $field, [ 'markup' => 'open' ] ); $this->field_option( 'button_text', $field ); $button_text_label = $this->field_element( 'label', $field, [ 'slug' => 'button_text', 'value' => esc_html__( 'Button Text', 'wpforms-lite' ), 'tooltip' => esc_html__( 'Change button text.', 'wpforms-lite' ), ], false ); $button_text_field = $this->field_element( 'text', $field, [ 'slug' => 'button_text', 'value' => isset( $field['button_text'] ) && ! wpforms_is_empty_string( $field['button_text'] ) ? $field['button_text'] : esc_html__( 'Apply', 'wpforms-lite' ), ], false ); $this->field_element( 'row', $field, [ 'slug' => 'button_text', 'content' => $button_text_label . $button_text_field, ] ); $this->field_option( 'css', $field ); $this->field_option( 'label_hide', $field ); $this->field_option( 'advanced-options', $field, [ 'markup' => 'close' ] ); } /** * Get allowed coupons' field. * * @since 1.9.4 * * @param array $coupons Coupons. * @param array $form_coupons Form coupons. * @param array $field Field data. * * @return string * @noinspection HtmlUnknownAttribute */ private function get_allowed_coupons_field( array $coupons, array $form_coupons, array $field ): string { $output = sprintf( '<select id="wpforms-field-option-%1$d-%2$s" name="fields[%1$d][%2$s]" multiple>', $field['id'], 'allowed_coupons' ); foreach ( $coupons as $arg_key => $arg_option ) { $selected = selected( true, in_array( $arg_key, $form_coupons, true ), false ); $output .= sprintf( '<option value="%s" %s>%s</option>', esc_attr( $arg_key ), $selected, $arg_option ); } $output .= '</select>'; return $output; } /** * Field preview inside the builder. * * @since 1.9.4 * * @param array $field Field data. */ public function field_preview( $field ) { // Label. $this->field_preview_option( 'label', $field, [ 'label_badge' => $this->get_field_preview_badge(), ] ); $allowed_coupons = []; if ( $this->is_addon_active ) { $allowed_coupons = wpforms_coupons()->get( 'repository' )->get_form_coupons( $this->get_form_id() ); } printf( '<div class="wpforms-field-payment-coupon-wrapper"> <input type="text" class="wpforms-field-payment-coupon-input"> <button type="button" aria-live="assertive" class="wpforms-field-payment-coupon-button">%1$s</button> <i class="fa fa-exclamation-triangle%2$s"></i> </div>', esc_html( $this->get_button_text( $field ) ), empty( $allowed_coupons ) && empty( $this->is_disabled_field ) ? '' : ' wpforms-hidden' ); // Description. $this->field_preview_option( 'description', $field ); // Hide remaining elements. $this->field_preview_option( 'hide-remaining', $field ); } /** * Get form ID. In AJAX requests the $form_id property doesn't exist. * * @since 1.9.4 * * @return bool|int */ protected function get_form_id() { if ( $this->form_id ) { return $this->form_id; } // phpcs:ignore WordPress.Security.NonceVerification.Missing $this->form_id = isset( $_POST['id'] ) ? absint( $_POST['id'] ) : false; return $this->form_id; } /** * Get the apply button text. * * @since 1.9.4 * * @param array $field Field data. * * @return string */ protected function get_button_text( array $field ): string { return isset( $field['button_text'] ) && ! wpforms_is_empty_string( $field['button_text'] ) ? $field['button_text'] : __( 'Apply', 'wpforms-lite' ); } /** * Field display on the frontend. * * @since 1.9.4 * * @param array $field Field data. * @param array $deprecated Field attributes. * @param array $form_data Form data. */ public function field_display( $field, $deprecated, $form_data ) { } } Forms/Fields/Addons/LikertScale/Field.php 0000644 00000022750 15174710275 0014225 0 ustar 00 <?php namespace WPForms\Forms\Fields\Addons\LikertScale; use WPForms\Forms\Fields\Traits\ProField as ProFieldTrait; use WPForms_Field; /** * Likert Scale field. * * @since 1.9.4 */ class Field extends WPForms_Field { use ProFieldTrait; /** * Primary class constructor. * * @since 1.9.4 */ public function init() { // Define field type information. $this->name = esc_html__( 'Likert Scale', 'wpforms-lite' ); $this->keywords = esc_html__( 'survey, rating scale', 'wpforms-lite' ); $this->type = 'likert_scale'; $this->icon = 'fa-ellipsis-h'; $this->order = 400; $this->group = 'fancy'; $this->addon_slug = 'surveys-polls'; $this->default_settings = [ 'size' => 'large', 'style' => 'modern', 'survey' => '1', 'rows' => [ 1 => esc_html__( 'Item #1', 'wpforms-lite' ), 2 => esc_html__( 'Item #2', 'wpforms-lite' ), 3 => esc_html__( 'Item #3', 'wpforms-lite' ), ], 'columns' => [ 1 => esc_html__( 'Strongly Disagree', 'wpforms-lite' ), 2 => esc_html__( 'Disagree', 'wpforms-lite' ), 3 => esc_html__( 'Neutral', 'wpforms-lite' ), 4 => esc_html__( 'Agree', 'wpforms-lite' ), 5 => esc_html__( 'Strongly Agree', 'wpforms-lite' ), ], ]; $this->init_pro_field(); $this->hooks(); } /** * Add hooks. * * @since 1.9.4 */ protected function hooks() {} /** * Field options panel inside the builder. * * @since 1.9.4 * * @param array $field Field settings. */ public function field_options( $field ) { /** * Basic field options. */ // Options open markup. $this->field_option( 'basic-options', $field, [ 'markup' => 'open', 'after_title' => $this->get_field_options_notice(), ] ); // Label. $this->field_option( 'label', $field ); // Rows. $values = ! empty( $field['rows'] ) ? $field['rows'] : $this->default_settings['rows']; $lbl = $this->field_element( 'label', $field, [ 'slug' => 'rows', 'value' => esc_html__( 'Rows', 'wpforms-lite' ), 'tooltip' => esc_html__( 'Add rows to the likert scale.', 'wpforms-lite' ), ], false ); $fld = sprintf( '<ul id="wpforms-field-option-%1$d-rows-list" data-next-id="%2$s" class="choices-list wpforms-undo-redo-container %3$s" data-field-id="%1$d" data-field-type="%4$s" data-choice-type="%5$s">', esc_attr( $field['id'] ), max( array_keys( $values ) ) + 1, ! empty( $field['single_row'] ) ? 'wpforms-hidden' : '', $this->type, 'rows' ); foreach ( $values as $key => $value ) { $fld .= sprintf( '<li data-key="%d">', $key ); $fld .= '<span class="move"><i class="fa fa-grip-lines" aria-hidden="true"></i></span>'; $fld .= sprintf( '<input type="text" name="fields[%s][rows][%s]" value="%s" class="label">', esc_attr( $field['id'] ), $key, esc_attr( $value ) ); $fld .= '<a class="add" href="#" title="' . esc_attr__( 'Add likert scale row', 'wpforms-lite' ) . '"><i class="fa fa-plus-circle"></i></a>'; $fld .= '<a class="remove" href="# title="' . esc_attr__( 'Remove likert scale row', 'wpforms-lite' ) . '"><i class="fa fa-minus-circle"></i></a>'; $fld .= '</li>'; } $fld .= '</ul>'; $this->field_element( 'row', $field, [ 'slug' => 'rows', 'content' => $lbl . $fld, ] ); // Single rows. $this->field_element( 'row', $field, [ 'slug' => 'single_row', 'content' => $this->field_element( 'toggle', $field, [ 'slug' => 'single_row', 'value' => isset( $field['single_row'] ) ? '1' : '0', 'desc' => esc_html__( 'Make this a single-row rating scale', 'wpforms-lite' ), 'tooltip' => esc_html__( 'Check this option to make this a single-row rating scale and remove the row choices.', 'wpforms-lite' ), ], false ), ] ); // Multiple row responses. $this->field_element( 'row', $field, [ 'slug' => 'multiple_responses', 'content' => $this->field_element( 'toggle', $field, [ 'slug' => 'multiple_responses', 'value' => isset( $field['multiple_responses'] ) ? '1' : '0', 'desc' => esc_html__( 'Allow multiple responses per row', 'wpforms-lite' ), 'tooltip' => esc_html__( 'Check this option to allow multiple responses per row (uses checkboxes).', 'wpforms-lite' ), ], false ), ] ); // Columns. $values = ! empty( $field['columns'] ) ? $field['columns'] : $this->default_settings['columns']; $lbl = $this->field_element( 'label', $field, [ 'slug' => 'columns', 'value' => esc_html__( 'Columns', 'wpforms-lite' ), 'tooltip' => esc_html__( 'Add columns to the likert scale.', 'wpforms-lite' ), ], false ); $fld = sprintf( '<ul id="wpforms-field-option-%1$d-columns-list" data-next-id="%2$s" class="choices-list wpforms-undo-redo-container" data-field-id="%1$d" data-field-type="%3$s" data-choice-type="%4$s">', esc_attr( $field['id'] ), max( array_keys( $values ) ) + 1, $this->type, 'columns' ); foreach ( $values as $key => $value ) { $fld .= sprintf( '<li data-key="%d">', $key ); $fld .= '<span class="move"><i class="fa fa-grip-lines" aria-hidden="true"></i></span>'; $fld .= sprintf( '<input type="text" name="fields[%s][columns][%s]" value="%s">', $field['id'], $key, esc_attr( $value ) ); $fld .= '<a class="add" href="#" title="' . esc_attr__( 'Add likert scale column', 'wpforms-lite' ) . '"><i class="fa fa-plus-circle"></i></a>'; $fld .= '<a class="remove" href="# title="' . esc_attr__( 'Remove likert scale column', 'wpforms-lite' ) . '"><i class="fa fa-minus-circle"></i></a>'; $fld .= '</li>'; } $fld .= '</ul>'; $this->field_element( 'row', $field, [ 'slug' => 'columns', 'content' => $lbl . $fld, ] ); // Description. $this->field_option( 'description', $field ); // Required toggle. $this->field_option( 'required', $field ); // Options close markup. $this->field_option( 'basic-options', $field, [ 'markup' => 'close', ] ); /* * Advanced field options. */ // Options open markup. $this->field_option( 'advanced-options', $field, [ 'markup' => 'open', ] ); // Style (theme). $lbl = $this->field_element( 'label', $field, [ 'slug' => 'style', 'value' => esc_html__( 'Style', 'wpforms-lite' ), 'tooltip' => esc_html__( 'Select the style for the likert scale.', 'wpforms-lite' ), ], false ); $fld = $this->field_element( 'select', $field, [ 'slug' => 'style', 'value' => ! empty( $field['style'] ) ? esc_attr( $field['style'] ) : 'modern', 'options' => [ 'modern' => esc_html__( 'Modern', 'wpforms-lite' ), 'classic' => esc_html__( 'Classic', 'wpforms-lite' ), ], ], false ); $this->field_element( 'row', $field, [ 'slug' => 'style', 'content' => $lbl . $fld, ] ); // Size. $this->field_option( 'size', $field ); // Custom CSS classes. $this->field_option( 'css', $field ); // Hide label. $this->field_option( 'label_hide', $field ); // Options close markup. $this->field_option( 'advanced-options', $field, [ 'markup' => 'close', ] ); } /** * Field preview inside the builder. * * @since 1.9.4 * * @param array $field Field settings. */ public function field_preview( $field ) { // phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh // Define data. $rows = ! empty( $field['rows'] ) ? $field['rows'] : $this->default_settings['rows']; $columns = ! empty( $field['columns'] ) ? $field['columns'] : $this->default_settings['columns']; $input_type = ! empty( $field['multiple_responses'] ) ? 'checkbox' : 'radio'; $style = ! empty( $field['style'] ) ? sanitize_html_class( $field['style'] ) : 'modern'; $single = ! empty( $field['single_row'] ); $width = $single ? round( 100 / count( $columns ), 4 ) : round( 80 / count( $columns ), 4 ); // Label. $this->field_preview_option( 'label', $field, [ 'label_badge' => $this->get_field_preview_badge(), ] ); ?> <table class="<?php echo esc_attr( $style ); ?><?php echo $single ? ' single-row' : ''; ?>"> <thead> <tr> <?php if ( ! $single ) { echo '<th style="width:20%;"></th>'; } foreach ( $columns as $column ) { printf( '<th style="width:%d%%;">%s</th>', esc_attr( $width ), esc_html( sanitize_text_field( $column ) ) ); } ?> </tr> </thead> <tbody> <?php foreach ( $rows as $row ) { echo '<tr>'; if ( ! $single ) { echo '<th>' . esc_html( sanitize_text_field( $row ) ) . '</th>'; } /** * Column is needed for foreach syntax. * * @noinspection PhpUnusedLocalVariableInspection */ foreach ( $columns as $column ) { echo '<td>'; echo '<input type="' . esc_attr( $input_type ) . '" readonly>'; echo '<label></label>'; echo '</td>'; } echo '</tr>'; if ( $single ) { break; } } ?> </tbody> </table> <?php // Description. $this->field_preview_option( 'description', $field ); // Hide remaining elements. $this->field_preview_option( 'hide-remaining', $field ); } /** * Field display on the form front-end. * * @since 1.9.4 * * @param array $field Field settings. * @param array $deprecated Deprecated array. * @param array $form_data Form data and settings. */ public function field_display( $field, $deprecated, $form_data ) { } } Forms/Fields/Addons/Map/Field.php 0000644 00000054120 15174710275 0012534 0 ustar 00 <?php namespace WPForms\Forms\Fields\Addons\Map; use WPForms\Forms\Fields\Traits\ProField as ProFieldTrait; use WPForms_Field; use WPFormsGeolocation\Admin\Settings\Settings; /** * Map field. * * @since 1.9.9.3 */ class Field extends WPForms_Field { /** * Find Nearby Locations option key. * * @since 1.9.9.3 */ protected const NEARBY_LOCATIONS_KEY = 'wpforms_geolocation_find_nearby_locations'; /** * Search Radius option key. * * @since 1.9.9.3 */ protected const NEARBY_LOCATIONS_RADIUS_KEY = 'wpforms_geolocation_search_radius'; /** * Default search radius. * * @since 1.9.9.3 */ protected const DEFAULT_SEARCH_RADIUS = 25; use ProFieldTrait; /** * Whether the addon is active. * * @since 1.9.9.3 * * @var bool */ private $is_addon_active = false; /** * Determine if we should display the field options notice. * * @since 1.9.9.3 * * @var bool */ protected $display_field_options_notice = true; /** * Init class. * * @since 1.9.9.3 * * @noinspection ReturnTypeCanBeDeclaredInspection */ public function init() { // Define field type information. $this->name = esc_html__( 'Map', 'wpforms-lite' ); $this->keywords = esc_html__( 'map', 'wpforms-lite' ); $this->type = 'map'; $this->icon = 'fa-map-location-dot'; $this->order = 75; $this->group = 'fancy'; $this->addon_slug = 'geolocation'; $this->allow_read_only = false; $this->default_settings = [ 'hide_full_screen' => '1', 'hide_map_type' => '1', 'hide_location_info' => '1', 'hide_street_view' => '1', 'hide_camera_control' => '1', 'disable_mouse_zooming' => '1', 'show_in_entry' => '1', 'show_thumbnail_in_entry' => '1', 'search_radius' => self::DEFAULT_SEARCH_RADIUS, ]; $this->is_addon_active = function_exists( 'wpforms_' . $this->addon_slug ); $this->init_pro_field(); $this->hooks(); } /** * Define field hooks. * * @since 1.9.9.3 */ protected function hooks(): void {} /** * Define additional field options. * * @since 1.9.9.3 * * @param array $field Field data and settings. * * @noinspection ReturnTypeCanBeDeclaredInspection */ public function field_options( $field ) { $this->basic_field_options( (array) $field ); $this->advanced_field_options( (array) $field ); } /** * Basic field options. * * @since 1.9.9.3 * * @param array $field Field settings. * * @return void */ private function basic_field_options( array $field ): void { // Options open markup. $this->field_option( 'basic-options', $field, [ 'markup' => 'open', 'after_title' => $this->display_field_options_notice ? $this->get_field_options_notice() : '', ] ); $this->field_option( 'label', $field ); $this->field_option( 'description', $field ); $this->field_element( 'row', $field, [ 'slug' => 'choices', 'class' => 'wpforms-field-option-row-locations', 'content' => $this->get_location_options( $field ), ] ); $current_user_id = get_current_user_id(); $find_nearby_locations = (bool) get_user_meta( $current_user_id, self::NEARBY_LOCATIONS_KEY, true ); $nearby_locations_radius = (int) get_user_meta( $current_user_id, self::NEARBY_LOCATIONS_RADIUS_KEY, true ); $nearby_locations_radius = $nearby_locations_radius > 0 ? $nearby_locations_radius : self::DEFAULT_SEARCH_RADIUS; $this->field_element( 'row', $field, [ 'slug' => 'find_nearby_locations', 'content' => $this->field_element( 'toggle', $field, [ 'slug' => 'find_nearby_locations', 'value' => $find_nearby_locations ? '1' : '0', 'desc' => esc_html__( 'Find Nearby Locations', 'wpforms-lite' ), ], false ), ] ); $this->field_element( 'row', $field, [ 'slug' => 'search_radius', 'class' => ! $find_nearby_locations ? 'wpforms-hidden' : '', 'content' => $this->field_element( 'label', $field, [ 'slug' => 'search_radius', 'value' => esc_html__( 'Search Radius', 'wpforms-lite' ), ], false ) . $this->field_element( 'select', $field, [ 'slug' => 'search_radius', 'value' => $nearby_locations_radius, 'options' => $this->get_search_radius_km_options(), 'data' => [ 'miles-options' => wp_json_encode( $this->get_search_radius_miles_options() ), ], ], false ), ] ); $this->field_element( 'row', $field, [ 'slug' => 'show_locations_list', 'content' => $this->field_element( 'toggle', $field, [ 'slug' => 'show_locations_list', 'value' => isset( $field['show_locations_list'] ) ? '1' : '0', 'desc' => esc_html__( 'Show List of Locations', 'wpforms-lite' ), ], false ), ] ); $this->field_element( 'row', $field, [ 'slug' => 'allow_location_selection', 'content' => $this->field_element( 'toggle', $field, [ 'slug' => 'allow_location_selection', 'value' => isset( $field['allow_location_selection'] ) ? '1' : '0', 'desc' => esc_html__( 'Allow Location Selection', 'wpforms-lite' ), ], false ), ] ); $this->field_element( 'row', $field, [ 'slug' => 'zoom_level', 'content' => $this->field_element( 'label', $field, [ 'slug' => 'zoom_level', 'value' => esc_html__( 'Zoom Level', 'wpforms-lite' ), ], false ) . $this->field_element( 'select', $field, [ 'class' => 'wpforms-field-map-settings', 'data' => [ 'map-control' => 'zoom', ], 'slug' => 'zoom_level', 'value' => ! empty( $field['zoom_level'] ) && $field['zoom_level'] >= 0 && $field['zoom_level'] <= 22 ? (int) $field['zoom_level'] : 15, 'options' => range( 0, 22 ), ], false ), ] ); $this->field_option( 'basic-options', $field, [ 'markup' => 'close' ] ); } /** * Advanced field options. * * @since 1.9.9.3 * * @param array $field Field settings. * * @return void */ private function advanced_field_options( array $field ): void { // phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh $is_mapbox = $this->get_active_provider_slug() === 'mapbox-search'; $this->field_option( 'advanced-options', $field, [ 'markup' => 'open' ] ); $this->field_option( 'size', $field ); $this->field_option( 'css', $field ); printf( '<div class="wpforms-field-option-row-subtitle">%1$s</div>', esc_html__( 'Presentational Settings', 'wpforms-lite' ) ); $this->field_element( 'row', $field, [ 'slug' => 'hide_full_screen', 'content' => $this->field_element( 'toggle', $field, [ 'class' => 'wpforms-field-map-settings', 'data' => [ 'map-control' => 'fullscreenControl', ], 'slug' => 'hide_full_screen', 'value' => isset( $field['hide_full_screen'] ) ? '1' : '0', 'desc' => esc_html__( 'Hide Full Screen ', 'wpforms-lite' ), ], false ), ] ); if ( ! $is_mapbox ) { $this->field_element( 'row', $field, [ 'slug' => 'hide_map_type', 'content' => $this->field_element( 'toggle', $field, [ 'class' => 'wpforms-field-map-settings', 'data' => [ 'map-control' => 'mapTypeControl', ], 'slug' => 'hide_map_type', 'value' => isset( $field['hide_map_type'] ) ? '1' : '0', 'desc' => esc_html__( 'Hide Map Type ', 'wpforms-lite' ), ], false ), ] ); $this->field_element( 'row', $field, [ 'slug' => 'hide_location_info', 'content' => $this->field_element( 'toggle', $field, [ 'slug' => 'hide_location_info', 'value' => isset( $field['hide_location_info'] ) ? '1' : '0', 'desc' => esc_html__( 'Hide Location Info ', 'wpforms-lite' ), ], false ), ] ); $this->field_element( 'row', $field, [ 'slug' => 'hide_street_view', 'content' => $this->field_element( 'toggle', $field, [ 'class' => 'wpforms-field-map-settings', 'data' => [ 'map-control' => 'streetViewControl', ], 'slug' => 'hide_street_view', 'value' => isset( $field['hide_street_view'] ) ? '1' : '0', 'desc' => esc_html__( 'Hide Street View ', 'wpforms-lite' ), ], false ), ] ); printf( '<div class="wpforms-field-option-row-subtitle">%1$s</div>', esc_html__( 'Interactive Settings', 'wpforms-lite' ) ); $this->field_element( 'row', $field, [ 'slug' => 'hide_camera_control', 'content' => $this->field_element( 'toggle', $field, [ 'class' => 'wpforms-field-map-settings', 'data' => [ 'map-control' => 'cameraControl', ], 'slug' => 'hide_camera_control', 'value' => isset( $field['hide_camera_control'] ) ? '1' : '0', 'desc' => esc_html__( 'Hide Camera Control ', 'wpforms-lite' ), ], false ), ] ); } $this->field_element( 'row', $field, [ 'slug' => 'hide_zoom', 'content' => $this->field_element( 'toggle', $field, [ 'class' => 'wpforms-field-map-settings', 'data' => [ 'map-control' => 'zoomControl', ], 'slug' => 'hide_zoom', 'value' => isset( $field['hide_zoom'] ) ? '1' : '0', 'desc' => esc_html__( 'Hide Zoom ', 'wpforms-lite' ), ], false ), ] ); $this->field_element( 'row', $field, [ 'slug' => 'disable_dragging', 'content' => $this->field_element( 'toggle', $field, [ 'slug' => 'disable_dragging', 'value' => isset( $field['disable_dragging'] ) ? '1' : '0', 'desc' => esc_html__( 'Disable Dragging ', 'wpforms-lite' ), ], false ), ] ); $this->field_element( 'row', $field, [ 'slug' => 'disable_mouse_zooming', 'content' => $this->field_element( 'toggle', $field, [ 'slug' => 'disable_mouse_zooming', 'value' => isset( $field['disable_mouse_zooming'] ) ? '1' : '0', 'desc' => esc_html__( 'Disable Mouse Zooming ', 'wpforms-lite' ), ], false ), ] ); printf( '<div class="wpforms-field-option-row-subtitle">%1$s</div>', esc_html__( 'Other', 'wpforms-lite' ) ); $this->field_element( 'row', $field, [ 'slug' => 'show_in_entry', 'content' => $this->field_element( 'toggle', $field, [ 'slug' => 'show_in_entry', 'value' => isset( $field['show_in_entry'] ) ? '1' : '0', 'desc' => esc_html__( 'Show in Entry ', 'wpforms-lite' ), ], false ), ] ); $this->field_element( 'row', $field, [ 'slug' => 'show_thumbnail_in_entry', 'content' => $this->field_element( 'toggle', $field, [ 'slug' => 'show_thumbnail_in_entry', 'value' => isset( $field['show_thumbnail_in_entry'] ) ? '1' : '0', 'desc' => esc_html__( 'Show Thumbnail in Entry ', 'wpforms-lite' ), ], false ), ] ); $this->field_option( 'label_hide', $field ); $this->field_option( 'advanced-options', $field, [ 'markup' => 'close' ] ); } /** * Get active provider slug. * * @since 1.9.9.3 */ protected function get_active_provider_slug(): string { if ( ! class_exists( Settings::class ) ) { return ''; } return ( new Settings() )->get_current_provider(); } /** * Field preview inside the builder. * * @since 1.9.9.3 * * @param array $field Field data. * * @noinspection ReturnTypeCanBeDeclaredInspection */ public function field_preview( $field ) { $this->field_preview_option( 'label', $field, [ 'label_badge' => $this->get_field_preview_badge(), ] ); $size = $field['size'] ?? 'medium'; $field_id = $field['id'] ?? 0; $this->print_map( $size, $field_id ); $this->print_location_list_preview( $field ); $this->field_preview_option( 'description', $field ); $this->field_preview_option( 'hide-remaining', $field ); } /** * Print map HTML. * * @since 1.9.9.3 * * @param string $size Field size. * @param int $field_id Field ID. * * @noinspection UnnecessaryCastingInspection * @noinspection PhpCastIsUnnecessaryInspection */ protected function print_map( string $size, int $field_id ): void { printf( '<div class="wpforms-field-row wpforms-field-%1$s wpforms-geolocation-map" id="wpforms-field-%2$d-map"></div>', esc_attr( $size ), (int) $field_id ); } /** * Print location list preview. * * @since 1.9.9.3 * * @param array $field Field settings. * * @noinspection PhpUnusedLocalVariableInspection * @noinspection HtmlWrongAttributeValue */ private function print_location_list_preview( array $field ): void { $choices = $field['choices'] ?? []; $show_locations_list = ! empty( $field['show_locations_list'] ); $allow_location_selection = $show_locations_list && ! empty( $field['allow_location_selection'] ) && count( $choices ) > 1; printf( '<ul class="wpforms-field-map-choices wpforms-field-row%1$s">', ! $show_locations_list ? ' wpforms-hidden' : '' ); foreach ( $choices as $key => $choice ) { echo '<li>'; printf( '<input type="%1$s">', $allow_location_selection ? 'radio' : 'hidden' ); echo '<label>'; printf( '<span class="wpforms-field-map-location-name">%1$s</span>', isset( $choice['name'] ) ? esc_html( $choice['name'] ) : '' ); printf( '<span class="wpforms-field-map-location-address">%1$s</span>', isset( $choice['address'] ) ? esc_html( $choice['address'] ) : '' ); echo '</label>'; echo '</li>'; } echo '</ul>'; } /** * Determine if the current choice is a valid marker. * * @since 1.9.9.3 * * @param array $choice Choice data. */ protected function is_valid_marker( array $choice ): bool { if ( ! isset( $choice['latitude'], $choice['longitude'] ) ) { return false; } if ( wpforms_is_empty_string( $choice['latitude'] ) || wpforms_is_empty_string( $choice['longitude'] ) ) { return false; } if ( ! empty( $choice['marker_type'] ) && $choice['marker_type'] === 'image' && empty( $choice['image'] ) ) { return false; } if ( ( ! isset( $choice['name'] ) || wpforms_is_empty_string( $choice['name'] ) ) && ( ! isset( $choice['address'] ) || wpforms_is_empty_string( $choice['address'] ) ) ) { return false; } return true; } /** * Field display on the form front-end. * * @since 1.9.9.3 * * @param array $field Field settings. * @param array $deprecated Deprecated array. * @param array $form_data Form data and settings. * * @noinspection ReturnTypeCanBeDeclaredInspection */ public function field_display( $field, $deprecated, $form_data ) { } /** * Get Locations options HTML template. * * @since 1.9.9.3 * * @param array $field Field settings. * * @noinspection PhpCastIsUnnecessaryInspection * @noinspection UnnecessaryCastingInspection * * @return string */ private function get_location_options( array $field ): string { $field_id = ! empty( $field['id'] ) ? (int) $field['id'] : 0; $locations = $field['choices'] ?? [ [] ]; $next_id = max( array_keys( $locations ) ) + 1; ob_start(); $this->field_element( 'label', $field, [ 'slug' => 'locations', 'value' => esc_html__( 'Locations', 'wpforms-lite' ), ] ); printf( '<ul class="choices-list wpforms-undo-redo-container" data-next-id="%1$d" data-field-id="%2$d" data-field-type="location">', (int) $next_id, (int) $field_id ); foreach ( $locations as $location_index => $location ) { $this->print_location_row( $location, (int) $location_index, $field_id ); } echo '</ul>'; return ob_get_clean(); } /** * Print Locations options row. * * @since 1.9.9.3 * * @param array $location Location data. * @param int $location_index Index. * @param int $field_id Field ID. * * @return void * * @noinspection HtmlFormInputWithoutLabel */ private function print_location_row( array $location, int $location_index, int $field_id ): void { $location = wp_parse_args( array_filter( $location ), [ 'name' => '', 'address' => '', 'description' => '', 'marker_type' => 'icon', 'icon' => 'face-smile', 'icon_style' => 'regular', 'icon_color' => '#d63638', 'latitude' => '', 'longitude' => '', 'image' => '', 'size' => 'small', ] ); $base = sprintf( 'fields[%s][choices][%d]', wpforms_validate_field_id( $field_id ), absint( $location_index ) ); $id_base = sprintf( 'fields-%s-choices-%d-', wpforms_validate_field_id( $field_id ), absint( $location_index ) ); $has_image = ! empty( $location['image'] ); ?> <li data-key="<?php echo absint( $location_index ); ?>" class="wpforms-geolocation-map-field-location-size-<?php echo esc_attr( $location['size'] ); ?> wpforms-geolocation-map-field-location-<?php echo esc_attr( $location['marker_type'] ); ?>"> <span class="move"><i class="fa fa-grip-lines"></i></span> <input type="text" name="<?php echo esc_attr( $base ); ?>[name]" value="<?php echo esc_attr( $location['name'] ); ?>" data-1p-ignore="true" class="label wpforms-geolocation-map-field-location-name" placeholder="<?php esc_attr_e( 'Name', 'wpforms-lite' ); ?>"> <a class="add" href="#"><i class="fa fa-plus-circle"></i></a> <a class="remove" href="#"><i class="fa fa-minus-circle"></i></a> <input type="text" name="<?php echo esc_attr( $base ); ?>[address]" id="<?php echo esc_attr( $id_base ); ?>address" value="<?php echo esc_attr( $location['address'] ); ?>" class="wpforms-geolocation-map-field-location-address" placeholder="<?php esc_attr_e( 'Address', 'wpforms-lite' ); ?>"> <input type="hidden" name="<?php echo esc_attr( $base ); ?>[latitude]" value="<?php echo esc_attr( $location['latitude'] ); ?>" class="wpforms-geolocation-map-field-location-latitude"> <input type="hidden" name="<?php echo esc_attr( $base ); ?>[longitude]" value="<?php echo esc_attr( $location['longitude'] ); ?>" class="wpforms-geolocation-map-field-location-longitude"> <input type="text" name="<?php echo esc_attr( $base ); ?>[description]" value="<?php echo esc_attr( $location['description'] ); ?>" class="wpforms-geolocation-map-field-location-description" placeholder="<?php esc_attr_e( 'Description', 'wpforms-lite' ); ?>"> <select name="<?php echo esc_attr( $base ); ?>[marker_type]" class="wpforms-geolocation-map-field-location-marker-type"> <option value="icon" <?php selected( 'icon', $location['marker_type'] ); ?>><?php esc_html_e( 'Icon', 'wpforms-lite' ); ?></option> <option value="image" <?php selected( 'image', $location['marker_type'] ); ?>><?php esc_html_e( 'Image', 'wpforms-lite' ); ?></option> </select> <select name="<?php echo esc_attr( $base ); ?>[size]" class="wpforms-geolocation-map-field-location-size"> <option value="small" <?php selected( 'small', $location['size'] ); ?>><?php esc_html_e( 'Small', 'wpforms-lite' ); ?></option> <option value="medium" <?php selected( 'medium', $location['size'] ); ?>><?php esc_html_e( 'Medium', 'wpforms-lite' ); ?></option> <option value="large" <?php selected( 'large', $location['size'] ); ?>><?php esc_html_e( 'Large', 'wpforms-lite' ); ?></option> </select> <?php // Icon Choice. ?> <div class="wpforms-icon-select"> <i class="ic-fa-preview ic-fa-<?php echo esc_attr( $location['icon_style'] ); ?> ic-fa-<?php echo esc_attr( $location['icon'] ); ?>"></i> <span><?php echo esc_html( $location['icon'] ); ?></span> <input type="hidden" name="<?php echo esc_attr( $base ); ?>[icon]" value="<?php echo esc_attr( $location['icon'] ); ?>" class="source-icon"> <input type="hidden" name="<?php echo esc_attr( $base ); ?>[icon_style]" value="<?php echo esc_attr( $location['icon_style'] ); ?>" class="source-icon-style"> </div> <div class="wpforms-geolocation-map-field-location-icon-color wpforms-panel-field-color wpforms-panel-field-colorpicker"> <input type="text" name="<?php echo esc_attr( $base ); ?>[icon_color]" value="<?php echo esc_attr( $location['icon_color'] ); ?>" class="wpforms-color-picker" data-swatches="#D63638|#E27730|#FFB900|#00A32A|#0399ED|#036AAB|#7A30E2|#E230BB" data-fallback-color="<?php echo esc_attr( $location['icon_color'] ); ?>"> </div> <?php // Image Choice. ?> <div class="wpforms-image-upload"> <button class="wpforms-btn wpforms-btn-sm wpforms-btn-blue wpforms-btn-block wpforms-image-upload-add" data-after-upload="hide"<?php echo $has_image ? ' style="display:none;"' : ''; ?>><?php esc_html_e( 'Upload Image', 'wpforms-lite' ); ?></button> <input type="hidden" name="<?php echo esc_attr( $base ); ?>[image]" value="<?php echo esc_url_raw( $location['image'] ); ?>" class="source"> <div class="preview"><?php if ( $has_image ) { ?> <img src="<?php echo esc_url_raw( $location['image'] ); ?>"><a href="#" title="<?php esc_attr_e( 'Remove Image', 'wpforms-lite' ); ?>" class="wpforms-image-upload-remove"><i class="fa fa-trash-o"></i></a> <?php } ?></div> </div> </li> <?php } /** * Get search radius options in kilometers. * * @since 1.9.9.3 * * @return array */ private function get_search_radius_km_options(): array { return [ 10 => esc_html__( '10 km', 'wpforms-lite' ), 25 => esc_html__( '25 km', 'wpforms-lite' ), 50 => esc_html__( '50 km', 'wpforms-lite' ), 100 => esc_html__( '100 km', 'wpforms-lite' ), ]; } /** * Get search radius options in miles. * * @since 1.9.9.3 * * @return array */ private function get_search_radius_miles_options(): array { return [ 10 => esc_html__( '10 mi', 'wpforms-lite' ), 25 => esc_html__( '25 mi', 'wpforms-lite' ), 50 => esc_html__( '50 mi', 'wpforms-lite' ), 100 => esc_html__( '100 mi', 'wpforms-lite' ), ]; } } Forms/Fields/Addons/Signature/Field.php 0000644 00000007103 15174710275 0013757 0 ustar 00 <?php namespace WPForms\Forms\Fields\Addons\Signature; use WPForms\Forms\Fields\Traits\ProField as ProFieldTrait; use WPForms_Field; /** * Signature field. * * @since 1.9.4 */ class Field extends WPForms_Field { use ProFieldTrait; /** * Init class. * * @since 1.9.4 */ public function init() { // Define field type information. $this->name = esc_html__( 'Signature', 'wpforms-lite' ); $this->keywords = esc_html__( 'user, e-signature', 'wpforms-lite' ); $this->type = 'signature'; $this->icon = 'fa-pencil'; $this->order = 200; $this->group = 'fancy'; $this->addon_slug = 'signatures'; $this->default_settings = [ 'size' => 'large', ]; $this->init_pro_field(); $this->hooks(); } /** * Add hooks. * * @since 1.9.4 */ protected function hooks() { } /** * Field options panel inside the builder. * * @since 1.9.4 * * @param array $field Field settings. */ public function field_options( $field ) { /** * Basic field options. */ // Options open markup. $this->field_option( 'basic-options', $field, [ 'markup' => 'open', 'after_title' => $this->get_field_options_notice(), ] ); // Label. $this->field_option( 'label', $field ); // Description. $this->field_option( 'description', $field ); // Required toggle. $this->field_option( 'required', $field ); // Options close markup. $this->field_option( 'basic-options', $field, [ 'markup' => 'close', ] ); /* * Advanced field options. */ // Options open markup. $this->field_option( 'advanced-options', $field, [ 'markup' => 'open', ] ); // Ink color picker. $lbl = $this->field_element( 'label', $field, [ 'slug' => 'ink_color', 'value' => esc_html__( 'Ink Color', 'wpforms-lite' ), 'tooltip' => esc_html__( 'Select the color for the signature ink.', 'wpforms-lite' ), ], false ); $ink_color = isset( $field['ink_color'] ) ? wpforms_sanitize_hex_color( $field['ink_color'] ) : ''; $ink_color = empty( $ink_color ) ? '#000000' : $ink_color; $fld = $this->field_element( 'color', $field, [ 'slug' => 'ink_color', 'value' => $ink_color, 'data' => [ 'fallback-color' => $ink_color, ], ], false ); $this->field_element( 'row', $field, [ 'slug' => 'ink_color', 'content' => $lbl . $fld, 'class' => 'color-picker-row', ] ); // Custom CSS classes. $this->field_option( 'css', $field ); // Size. $this->field_option( 'size', $field ); // Hide label. $this->field_option( 'label_hide', $field ); // Options close markup. $this->field_option( 'advanced-options', $field, [ 'markup' => 'close', ] ); } /** * Field preview inside the builder. * * @since 1.9.4 * * @param array $field Field settings. */ public function field_preview( $field ) { // Label. $this->field_preview_option( 'label', $field, [ 'label_badge' => $this->get_field_preview_badge(), ] ); // Signature placeholder. echo '<div class="wpforms-signature-wrap"></div>'; // Description. $this->field_preview_option( 'description', $field ); // Hide remaining elements. $this->field_preview_option( 'hide-remaining', $field ); } /** * Field display on the form front-end. * * @since 1.9.4 * * @param array $field Field settings. * @param array $deprecated Deprecated array. * @param array $form_data Form data and settings. */ public function field_display( $field, $deprecated, $form_data ) { } } Forms/Fields/Addons/NetPromoterScore/Field.php 0000644 00000012767 15174710275 0015304 0 ustar 00 <?php namespace WPForms\Forms\Fields\Addons\NetPromoterScore; use WPForms\Forms\Fields\Traits\ProField as ProFieldTrait; use WPForms_Field; /** * Net Promoter Score field. * * @since 1.9.4 */ class Field extends WPForms_Field { use ProFieldTrait; /** * Primary class constructor. * * @since 1.9.4 */ public function init() { // Define field type information. $this->name = esc_html__( 'Net Promoter Score', 'wpforms-lite' ); $this->keywords = esc_html__( 'survey, nps', 'wpforms-lite' ); $this->type = 'net_promoter_score'; $this->icon = 'fa-tachometer'; $this->order = 410; $this->group = 'fancy'; $this->addon_slug = 'surveys-polls'; $this->default_settings = [ 'size' => 'large', 'survey' => '1', 'style' => 'modern', ]; $this->init_pro_field(); $this->hooks(); } /** * Add hooks. * * @since 1.9.4 */ protected function hooks() { } /** * Field options panel inside the builder. * * @since 1.9.4 * * @param array $field Field settings. */ public function field_options( $field ) { /** * Basic field options. */ // Options open markup. $this->field_option( 'basic-options', $field, [ 'markup' => 'open', 'after_title' => $this->get_field_options_notice(), ] ); // Label. $this->field_option( 'label', $field ); // Description. $this->field_option( 'description', $field ); // Required toggle. $this->field_option( 'required', $field ); // Options close markup. $this->field_option( 'basic-options', $field, [ 'markup' => 'close', ] ); /* * Advanced field options. */ // Options open markup. $this->field_option( 'advanced-options', $field, [ 'markup' => 'open', ] ); // Style (theme). $lbl = $this->field_element( 'label', $field, [ 'slug' => 'style', 'value' => esc_html__( 'Style', 'wpforms-lite' ), 'tooltip' => esc_html__( 'Select the style for the net promoter score.', 'wpforms-lite' ), ], false ); $fld = $this->field_element( 'select', $field, [ 'slug' => 'style', 'value' => ! empty( $field['style'] ) ? esc_attr( $field['style'] ) : 'modern', 'options' => [ 'modern' => esc_html__( 'Modern', 'wpforms-lite' ), 'classic' => esc_html__( 'Classic', 'wpforms-lite' ), ], ], false ); $this->field_element( 'row', $field, [ 'slug' => 'style', 'content' => $lbl . $fld, ] ); // Size. $this->field_option( 'size', $field ); // Start label. $lowest_lbl_label = $this->field_element( 'label', $field, [ 'slug' => 'lowest_label', 'value' => esc_html__( 'Lowest Score Label', 'wpforms-lite' ), ], false ); $lowest_lbl_field = $this->field_element( 'text', $field, [ 'slug' => 'lowest_label', 'value' => $field['lowest_label'] ?? esc_html__( 'Not at all Likely', 'wpforms-lite' ), ], false ); $this->field_element( 'row', $field, [ 'slug' => 'lowest_label', 'content' => $lowest_lbl_label . $lowest_lbl_field, ] ); // End label. $highest_lbl_label = $this->field_element( 'label', $field, [ 'slug' => 'highest_label', 'value' => esc_html__( 'Highest Score Label', 'wpforms-lite' ), ], false ); $highest_lbl_field = $this->field_element( 'text', $field, [ 'slug' => 'highest_label', 'value' => $field['highest_label'] ?? esc_html__( 'Extremely Likely', 'wpforms-lite' ), ], false ); $this->field_element( 'row', $field, [ 'slug' => 'highest_label', 'content' => $highest_lbl_label . $highest_lbl_field, ] ); // Custom CSS classes. $this->field_option( 'css', $field ); // Hide label. $this->field_option( 'label_hide', $field ); // Options close markup. $this->field_option( 'advanced-options', $field, [ 'markup' => 'close', ] ); } /** * Field preview inside the builder. * * @since 1.9.4 * * @param array $field Field settings. */ public function field_preview( $field ) { // Define data. $style = ! empty( $field['style'] ) ? sanitize_html_class( $field['style'] ) : 'modern'; // Label. $this->field_preview_option( 'label', $field, [ 'label_badge' => $this->get_field_preview_badge(), ] ); // Lowest/Highest labels. $lowest_label = $field['lowest_label'] ?? esc_html__( 'Not at all Likely', 'wpforms-lite' ); $highest_label = $field['highest_label'] ?? esc_html__( 'Extremely Likely', 'wpforms-lite' ); ?> <table class="<?php echo esc_attr( $style ); ?>"> <thead> <tr> <th colspan="11"> <span class="not-likely"><?php echo esc_html( $lowest_label ); ?></span> <span class="extremely-likely"><?php echo esc_html( $highest_label ); ?></span> </th> </tr> </thead> <tbody> <tr> <?php for ( $i = 0; $i < 11; $i++ ) { ?> <td> <input type="radio" readonly> <label><?php echo absint( $i ); ?></label> </td> <?php } ?> </tr> </tbody> </table> <?php // Description. $this->field_preview_option( 'description', $field ); // Hide remaining elements. $this->field_preview_option( 'hide-remaining', $field ); } /** * Field display on the form front-end. * * @since 1.9.4 * * @param array $field Field settings. * @param array $deprecated Deprecated array. * @param array $form_data Form data and settings. */ public function field_display( $field, $deprecated, $form_data ) { } } Forms/Fields/CustomCaptcha/Field.php 0000644 00000015414 15174710275 0013350 0 ustar 00 <?php namespace WPForms\Forms\Fields\CustomCaptcha; use WPForms\Forms\Fields\Traits\ProField as ProFieldTrait; use WPForms_Field; /** * Custom Captcha field. * * @since 1.9.4 */ class Field extends WPForms_Field { use ProFieldTrait; /** * The field type. * * @since 1.9.4 */ public const TYPE = 'captcha'; /** * Min & max values to participate in equation and operators. * * @since 1.9.4 * * @var array */ public $math; /** * Questions to ask. * * @since 1.9.4 * * @var array */ protected $qs; /** * * Init class. * * @since 1.9.4 */ public function init() { // Define field type information. $this->name = esc_html__( 'Custom Captcha', 'wpforms-lite' ); $this->keywords = esc_html__( 'spam, math, maths, question', 'wpforms-lite' ); $this->type = self::TYPE; $this->icon = 'fa-question-circle'; $this->order = 300; $this->group = 'fancy'; $this->allow_read_only = false; $this->qs = [ 1 => [ 'question' => esc_html__( 'What is 7+4?', 'wpforms-lite' ), 'answer' => esc_html__( '11', 'wpforms-lite' ), ], ]; $this->math = [ 'min' => 1, 'max' => 15, 'cal' => [ '+', '*' ], ]; $this->init_pro_field(); $this->hooks(); } /** * Register hooks. * * @since 1.9.4 */ protected function hooks() { } /** * Field options panel inside the builder. * * @since 1.9.4 * * @param array $field Field settings. */ public function field_options( $field ) { // Defaults. $format = ! empty( $field['format'] ) ? esc_attr( $field['format'] ) : 'math'; $qs = ! empty( $field['questions'] ) ? $field['questions'] : $this->qs; $qs = array_filter( $qs ); // Field is always required. $this->field_element( 'text', $field, [ 'type' => 'hidden', 'slug' => 'required', 'value' => '1', ] ); /* * Basic field options. */ // Options open markup. $this->field_option( 'basic-options', $field, [ 'markup' => 'open', 'after_title' => $this->get_field_options_notice(), ] ); // Label. $this->field_option( 'label', $field ); // Format. $lbl = $this->field_element( 'label', $field, [ 'slug' => 'format', 'value' => esc_html__( 'Type', 'wpforms-lite' ), 'tooltip' => esc_html__( 'Select type of captcha to use.', 'wpforms-lite' ), ], false ); $fld = $this->field_element( 'select', $field, [ 'slug' => 'format', 'value' => $format, 'options' => [ 'math' => esc_html__( 'Math', 'wpforms-lite' ), 'qa' => esc_html__( 'Question and Answer', 'wpforms-lite' ), ], ], false ); $this->field_element( 'row', $field, [ 'slug' => 'format', 'content' => $lbl . $fld, ] ); // Questions. $lbl = $this->field_element( 'label', $field, [ 'slug' => 'questions', 'value' => esc_html__( 'Questions and Answers', 'wpforms-lite' ), 'tooltip' => esc_html__( 'Add questions to ask the user. Questions are randomly selected.', 'wpforms-lite' ), ], false ); $fld = sprintf( '<ul id="wpforms-field-option-%1$d-questions-list" data-next-id="%2$s" data-field-id="%1$d" data-field-type="%3$s" class="choices-list wpforms-undo-redo-container">', esc_attr( $field['id'] ), max( array_keys( $qs ) ) + 1, esc_attr( $this->type ) ); foreach ( $qs as $key => $value ) { $fld .= '<li data-key="' . absint( $key ) . '">'; $fld .= sprintf( '<input type="text" name="fields[%1$d][questions][%2$s][question]" value="%3$s" data-prev-value="%3$s" class="question" placeholder="%4$s">', (int) $field['id'], esc_attr( $key ), esc_attr( $value['question'] ), esc_html__( 'Question', 'wpforms-lite' ) ); $fld .= '<a class="add" href="#"><i class="fa fa-plus-circle"></i></a><a class="remove" href="#"><i class="fa fa-minus-circle"></i></a>'; $fld .= sprintf( '<input type="text" name="fields[%d][questions][%s][answer]" value="%s" class="answer" placeholder="%s">', (int) $field['id'], esc_attr( $key ), esc_attr( $value['answer'] ), esc_html__( 'Answer', 'wpforms-lite' ) ); $fld .= '</li>'; } $fld .= '</ul>'; $this->field_element( 'row', $field, [ 'slug' => 'questions', 'content' => $lbl . $fld, 'class' => $format === 'math' ? 'wpforms-hidden' : '', ] ); // Description. $this->field_option( 'description', $field ); // Options close markup. $this->field_option( 'basic-options', $field, [ 'markup' => 'close', ] ); /* * Advanced field options. */ // Options open markup. $this->field_option( 'advanced-options', $field, [ 'markup' => 'open', ] ); // Size. $this->field_option( 'size', $field, [ 'class' => $format === 'math' ? 'wpforms-hidden' : '', ] ); // Custom CSS classes. $this->field_option( 'css', $field ); // Placeholder. $this->field_option( 'placeholder', $field ); // Hide Label. $this->field_option( 'label_hide', $field ); // Options close markup. $this->field_option( 'advanced-options', $field, [ 'markup' => 'close', ] ); } /** * Field preview inside the builder. * * @since 1.9.4 * * @param array $field Field settings. */ public function field_preview( $field ) { // Define data. $placeholder = ! empty( $field['placeholder'] ) ? $field['placeholder'] : ''; $format = ! empty( $field['format'] ) ? $field['format'] : 'math'; $num1 = wp_rand( $this->math['min'], $this->math['max'] ); $num2 = wp_rand( $this->math['min'], $this->math['max'] ); $cal = $this->math['cal'][ wp_rand( 0, count( $this->math['cal'] ) - 1 ) ]; $questions = ! empty( $field['questions'] ) ? $field['questions'] : $this->qs; // Label. $this->field_preview_option( 'label', $field, [ 'label_badge' => $this->get_field_preview_badge(), ] ); $first_question = array_shift( $questions ); ?> <div class="format-selected-<?php echo esc_attr( $format ); ?> format-selected"> <span class="wpforms-equation"><?php echo esc_html( "$num1 $cal $num2 = " ); ?></span> <p class="wpforms-question"><?php echo wp_kses( $first_question['question'], wpforms_builder_preview_get_allowed_tags() ); ?></p> <input type="text" placeholder="<?php echo esc_attr( $placeholder ); ?>" class="primary-input" readonly> </div> <?php // Description. $this->field_preview_option( 'description', $field ); } /** * Field display on the form front-end. * * @since 1.9.4 * * @param array $field Field settings. * @param array $deprecated Deprecated array. * @param array $form_data Form data and settings. */ public function field_display( $field, $deprecated, $form_data ) { } } Forms/Fields/Pagebreak/Field.php 0000644 00000041500 15174710275 0012466 0 ustar 00 <?php namespace WPForms\Forms\Fields\Pagebreak; use WPForms\Forms\Fields\Traits\ProField as ProFieldTrait; use WPForms_Field; /** * Pagebreak field. * * @since 1.9.4 */ class Field extends WPForms_Field { use ProFieldTrait; /** * Default indicator color. * * @since 1.9.4 */ private const DEFAULT_INDICATOR_COLOR = [ 'classic' => '#72b239', 'modern' => '#066aab', ]; /** * Pages information. * * @since 1.9.4 * * @var array|bool */ protected $pagebreak; /** * Primary class constructor. * * @since 1.9.4 */ public function init() { // Define field type information. $this->name = esc_html__( 'Page Break', 'wpforms-lite' ); $this->keywords = esc_html__( 'progress bar, multi step, multi part', 'wpforms-lite' ); $this->type = 'pagebreak'; $this->icon = 'fa-files-o'; $this->order = 160; $this->group = 'fancy'; $this->allow_read_only = false; $this->init_pro_field(); $this->hooks(); } /** * Hooks. * * @since 1.9.4 */ protected function hooks() { add_filter( 'wpforms_field_preview_class', [ $this, 'preview_field_class' ], 10, 2 ); add_filter( 'wpforms_field_preview_display_duplicate_button', [ $this, 'field_display_duplicate_button' ], 10, 2 ); add_filter( 'wpforms_field_new_display_duplicate_button', [ $this, 'field_display_duplicate_button' ], 10, 2 ); } /** * Field options panel inside the builder. * * @since 1.9.4 * * @param array $field Field data. */ public function field_options( $field ) { $position = ! empty( $field['position'] ) ? esc_attr( $field['position'] ) : ''; $position_class = ! empty( $field['position'] ) ? 'wpforms-pagebreak-' . $position : ''; $this->field_options_basic( $field, $position, $position_class ); $this->field_options_advanced( $field, $position, $position_class ); } /** * Advanced field options panel inside the builder. * * @since 1.9.4 * * @param array $field Field data. * @param string $position Position. * @param string $position_class Position CSS class. */ private function field_options_basic( array $field, string $position, string $position_class ): void { // Hidden field indicating the position. $this->field_element( 'text', $field, [ 'type' => 'hidden', 'slug' => 'position', 'value' => $position, 'class' => 'position', ] ); /* * Basic field options. */ // Options open markup. $this->field_option( 'basic-options', $field, [ 'markup' => 'open', 'class' => $position_class, 'after_title' => $this->get_field_options_notice(), ] ); $this->field_options_basic_top( $field, $position ); // Page Title, don't display for bottom page breaks. if ( $position !== 'bottom' ) { $lbl = $this->field_element( 'label', $field, [ 'slug' => 'title', 'value' => esc_html__( 'Page Title', 'wpforms-lite' ), 'tooltip' => esc_html__( 'Enter text for the page title.', 'wpforms-lite' ), ], false ); $fld = $this->field_element( 'text', $field, [ 'slug' => 'title', 'value' => ! empty( $field['title'] ) ? esc_attr( $field['title'] ) : '', ], false ); $indicator = ! empty( $field['indicator'] ) ? esc_attr( $field['indicator'] ) : 'progress'; $this->field_element( 'row', $field, [ 'slug' => 'title', 'content' => $lbl . $fld, 'class' => $indicator === 'none' ? 'wpforms-hidden' : '', ] ); } // Next label. if ( empty( $position ) ) { $lbl = $this->field_element( 'label', $field, [ 'slug' => 'next', 'value' => esc_html__( 'Next Label', 'wpforms-lite' ), 'tooltip' => esc_html__( 'Enter text for Next page navigation button.', 'wpforms-lite' ), ], false ); $fld = $this->field_element( 'text', $field, [ 'slug' => 'next', 'value' => ! empty( $field['next'] ) ? esc_attr( $field['next'] ) : esc_html__( 'Next', 'wpforms-lite' ), ], false ); $this->field_element( 'row', $field, [ 'slug' => 'next', 'content' => $lbl . $fld, ] ); } // Options are not available to top page breaks. if ( $position !== 'top' ) { // Previous button toggle. $fld = $this->field_element( 'toggle', $field, [ 'slug' => 'prev_toggle', // Backward compatibility for forms that were created before the toggle was added. 'value' => ! empty( $field['prev_toggle'] ) || ! empty( $field['prev'] ), 'desc' => esc_html__( 'Display Previous', 'wpforms-lite' ), 'tooltip' => esc_html__( 'Toggle displaying the Previous page navigation button.', 'wpforms-lite' ), ], false ); $this->field_element( 'row', $field, [ 'slug' => 'prev_toggle', 'content' => $fld, ] ); // Previous button label. $lbl = $this->field_element( 'label', $field, [ 'slug' => 'prev', 'value' => esc_html__( 'Previous Label', 'wpforms-lite' ), 'tooltip' => esc_html__( 'Enter text for Previous page navigation button.', 'wpforms-lite' ), ], false ); $fld = $this->field_element( 'text', $field, [ 'slug' => 'prev', 'value' => ! empty( $field['prev'] ) ? esc_attr( $field['prev'] ) : '', ], false ); $this->field_element( 'row', $field, [ 'slug' => 'prev', 'content' => $lbl . $fld, 'class' => empty( $field['prev_toggle'] ) ? 'wpforms-hidden' : '', ] ); } // Options close markup. $this->field_option( 'basic-options', $field, [ 'markup' => 'close', ] ); } /** * Generate the field UI for progress text configuration within a form. * * @since 1.9.7 * * @param array $field The field data used to generate the progress text UI elements. */ private function field_progress_text( array $field ): void { $lbl = $this->field_element( 'label', $field, [ 'slug' => 'progress_text', 'value' => esc_html__( 'Progress Text', 'wpforms-lite' ), 'tooltip' => esc_html__( 'Enter text for the progress indicator.', 'wpforms-lite' ), ], false ); $fld = $this->field_element( 'text', $field, [ 'slug' => 'progress_text', 'value' => ! empty( $field['progress_text'] ) ? esc_html( $field['progress_text'] ) : 'Step {current_page} of {last_page}', 'after' => esc_html__( 'Enter text to show the user\'s progress. You can use {current_page} and {last_page} to indicate the current and last steps.', 'wpforms-lite' ), ], false ); $indicator = ! empty( $field['indicator'] ) ? esc_attr( $field['indicator'] ) : 'progress'; $this->field_element( 'row', $field, [ 'slug' => 'progress_text', 'content' => $lbl . $fld, 'class' => $indicator !== 'progress' ? 'wpforms-hidden' : '', // Hide if the indicator is not set to progress. ] ); } /** * Field options panel inside the builder. * * @since 1.9.4 * * @param array $field Field data. * @param string $position Position. */ private function field_options_basic_top( array $field, string $position ): void { // Options specific to the top pagebreak. if ( $position !== 'top' ) { return; } // Indicator themes. $themes = [ 'progress' => esc_html__( 'Progress Bar', 'wpforms-lite' ), 'circles' => esc_html__( 'Circles', 'wpforms-lite' ), 'connector' => esc_html__( 'Connector', 'wpforms-lite' ), 'none' => esc_html__( 'None', 'wpforms-lite' ), ]; /** * Filter the available Pagebreak Indicator themes. * * @since 1.6.6 * * @param array $themes Available themes. */ $themes = apply_filters( 'wpforms_pagebreak_indicator_themes', $themes ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName $lbl = $this->field_element( 'label', $field, [ 'slug' => 'indicator', 'value' => esc_html__( 'Progress Indicator', 'wpforms-lite' ), 'tooltip' => esc_html__( 'Select theme for Page Indicator which is displayed at the top of the form.', 'wpforms-lite' ), ], false ); $indicator = ! empty( $field['indicator'] ) ? esc_attr( $field['indicator'] ) : 'progress'; $fld = $this->field_element( 'select', $field, [ 'slug' => 'indicator', 'value' => $indicator, 'options' => $themes, 'class' => 'wpforms-pagebreak-progress-indicator', ], false ); $this->field_element( 'row', $field, [ 'slug' => 'indicator', 'content' => $lbl . $fld, ] ); // Indicator color picker. $lbl = $this->field_element( 'label', $field, [ 'slug' => 'indicator_color', 'value' => esc_html__( 'Page Indicator Color', 'wpforms-lite' ), 'tooltip' => esc_html__( 'Select the primary color for the Page Indicator theme.', 'wpforms-lite' ), ], false ); $indicator_color = isset( $field['indicator_color'] ) ? wpforms_sanitize_hex_color( $field['indicator_color'] ) : self::get_default_indicator_color(); $fld = $this->field_element( 'color', $field, [ 'slug' => 'indicator_color', 'value' => $indicator_color, 'data' => [ 'fallback-color' => $indicator_color, ], ], false ); $indicator_color_classes = [ 'color-picker-row' ]; if ( $indicator === 'none' ) { $indicator_color_classes[] = 'wpforms-hidden'; } $this->field_element( 'row', $field, [ 'slug' => 'indicator_color', 'content' => $lbl . $fld, 'class' => $indicator_color_classes, ] ); $this->field_progress_text( $field ); } /** * Advanced field options panel inside the builder. * * @since 1.9.4 * * @param array $field Field data. * @param string $position Position. * @param string $position_class Position CSS class. */ private function field_options_advanced( array $field, string $position, string $position_class ): void { if ( $position === 'bottom' ) { return; } /** * Advanced field options. */ // Options open markup. $this->field_option( 'advanced-options', $field, [ 'markup' => 'open', 'class' => $position_class, ] ); // Navigation alignment, only available to the top. if ( $position === 'top' ) { $lbl = $this->field_element( 'label', $field, [ 'slug' => 'nav_align', 'value' => esc_html__( 'Page Navigation Alignment', 'wpforms-lite' ), 'tooltip' => esc_html__( 'Select the alignment for the Next/Previous page navigation buttons', 'wpforms-lite' ), ], false ); $fld = $this->field_element( 'select', $field, [ 'slug' => 'nav_align', 'value' => ! empty( $field['nav_align'] ) ? esc_attr( $field['nav_align'] ) : '', 'options' => [ 'left' => esc_html__( 'Left', 'wpforms-lite' ), 'right' => esc_html__( 'Right', 'wpforms-lite' ), '' => esc_html__( 'Center', 'wpforms-lite' ), 'split' => esc_html__( 'Split', 'wpforms-lite' ), ], ], false ); $this->field_element( 'row', $field, [ 'slug' => 'nav_align', 'content' => $lbl . $fld, ] ); // Scroll animation toggle. $fld = $this->field_element( 'toggle', $field, [ 'slug' => 'scroll_disabled', 'value' => ! empty( $field['scroll_disabled'] ), 'desc' => esc_html__( 'Disable Scroll Animation', 'wpforms-lite' ), 'tooltip' => esc_html__( 'By default, a user\'s view is pulled to the top of each form page. Set to ON to disable this animation.', 'wpforms-lite' ), ], false ); $this->field_element( 'row', $field, [ 'slug' => 'scroll_disabled', 'content' => $fld, ] ); } // Custom CSS classes. $this->field_option( 'css', $field ); // Options close markup. $this->field_option( 'advanced-options', $field, [ 'markup' => 'close', ] ); } /** * Field preview inside the builder. * * @since 1.9.4 * * @param array $field Field data. */ public function field_preview( $field ) { // phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh $nav_align = 'wpforms-pagebreak-buttons-left'; $prev = ! empty( $field['prev'] ) ? $field['prev'] : esc_html__( 'Previous', 'wpforms-lite' ); $prev_class = empty( $field['prev'] ) && empty( $field['prev_toggle'] ) ? 'wpforms-hidden' : ''; $next = ! empty( $field['next'] ) ? $field['next'] : esc_html__( 'Next', 'wpforms-lite' ); $next_class = empty( $next ) ? 'wpforms-hidden' : ''; $position = ! empty( $field['position'] ) ? $field['position'] : 'normal'; $title = ! empty( $field['title'] ) ? $field['title'] : ''; $label = $position === 'top' ? esc_html__( 'First Page / Progress Indicator', 'wpforms-lite' ) : ''; $label = $position === 'normal' && empty( $label ) ? esc_html__( 'Page Break', 'wpforms-lite' ) : $label; /** * Fires before the page break is displayed on the preview. * * @since 1.7.9 * * @param array $form_data Form data and settings. * @param array $field Field data. */ do_action( 'wpforms_field_page_break_field_preview_before', $this->form_data, $field ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName if ( $position !== 'top' ) { if ( empty( $this->form_data ) ) { $this->form_data = wpforms()->obj( 'form' )->get( $this->form_id, [ 'content_only' => true ] ); } if ( empty( $this->pagebreak ) ) { $this->pagebreak = wpforms_get_pagebreak_details( $this->form_data ); } if ( ! empty( $this->pagebreak['top']['nav_align'] ) ) { $nav_align = 'wpforms-pagebreak-buttons-' . $this->pagebreak['top']['nav_align']; } echo '<div class="wpforms-pagebreak-buttons ' . sanitize_html_class( $nav_align ) . '">'; printf( '<button class="wpforms-pagebreak-button wpforms-pagebreak-prev %s">%s</button>', sanitize_html_class( $prev_class ), esc_html( $prev ) ); if ( $position !== 'bottom' ) { printf( '<button class="wpforms-pagebreak-button wpforms-pagebreak-next %s">%s</button>', sanitize_html_class( $next_class ), esc_html( $next ) ); if ( $next_class !== 'wpforms-hidden' ) { /** This action is documented in includes/class-frontend.php. */ do_action( 'wpforms_display_submit_after', $this->form_data, 'next' ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName } } echo '</div>'; } // Visual divider. echo '<div class="wpforms-pagebreak-divider">'; if ( $position !== 'bottom' ) { printf( '<span class="pagebreak-label">%1$s <span class="wpforms-pagebreak-title">%2$s</span>%3$s</span>', esc_html( $label ), esc_html( $title ), $this->get_field_preview_badge() // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ); } echo '<span class="line"></span>'; echo '</div>'; /** * Fires after a page break is displayed on the preview. * * @since 1.7.9 * * @param array $form_data Form data and settings. * @param array $field Field data. */ do_action( 'wpforms_field_page_break_field_preview_after', $this->form_data, $field ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName } /** * Add a class to the builder field preview. * * @since 1.9.4 * * @param string|mixed $css CSS classes. * @param array $field Field data and settings. * * @return string */ public function preview_field_class( $css, $field ): string { $css = (string) $css; if ( $field['type'] !== 'pagebreak' ) { return $css; } if ( ! empty( $field['position'] ) && $field['position'] === 'top' ) { $css .= ' wpforms-field-stick wpforms-pagebreak-top'; } elseif ( ! empty( $field['position'] ) && $field['position'] === 'bottom' ) { $css .= ' wpforms-field-stick wpforms-pagebreak-bottom'; } else { $css .= ' wpforms-pagebreak-normal'; } return $css; } /** * Field display on the form front-end. * * @since 1.9.4 * * @param array $field Field data and settings. * @param array $deprecated Field attributes. * @param array $form_data Form data and settings. */ public function field_display( $field, $deprecated, $form_data ) { } /** * Get the default indicator color. * * @since 1.9.4 * * @return string */ public static function get_default_indicator_color(): string { $render_engine = wpforms_get_render_engine(); return array_key_exists( $render_engine, self::DEFAULT_INDICATOR_COLOR ) ? self::DEFAULT_INDICATOR_COLOR[ $render_engine ] : self::DEFAULT_INDICATOR_COLOR['modern']; } /** * Disallow the field preview "Duplicate" button. * * @since 1.9.9 * * @param bool|mixed $display Display switch. * @param array $field Field settings. * * @return bool */ public function field_display_duplicate_button( $display, array $field ): bool { $type = $field['type'] ?? ''; if ( $type === $this->type ) { // Pagebreak fields cannot be duplicated. return false; } return (bool) $display; } } Forms/Fields/Url/Field.php 0000644 00000006050 15174710275 0011350 0 ustar 00 <?php namespace WPForms\Forms\Fields\Url; use WPForms\Forms\Fields\Traits\ProField as ProFieldTrait; use WPForms_Field; /** * URL text field. * * @since 1.9.4 */ class Field extends WPForms_Field { use ProFieldTrait; /** * Primary class constructor. * * @since 1.9.4 */ public function init() { // Define field type information. $this->name = esc_html__( 'Website / URL', 'wpforms-lite' ); $this->keywords = esc_html__( 'uri, link, hyperlink', 'wpforms-lite' ); $this->type = 'url'; $this->icon = 'fa-link'; $this->order = 90; $this->group = 'fancy'; $this->init_pro_field(); $this->hooks(); } /** * Hooks. * * @since 1.9.4 */ protected function hooks() { } /** * Field options panel inside the builder. * * @since 1.9.4 * * @param array $field Field data. */ public function field_options( $field ) { /** * Basic field options. */ // Options open markup. $this->field_option( 'basic-options', $field, [ 'markup' => 'open', 'after_title' => $this->get_field_options_notice(), ] ); // Label. $this->field_option( 'label', $field ); // Description. $this->field_option( 'description', $field ); // Required toggle. $this->field_option( 'required', $field ); // Options close markup. $args = [ 'markup' => 'close', ]; $this->field_option( 'basic-options', $field, $args ); /* * Advanced field options. */ // Options open markup. $args = [ 'markup' => 'open', ]; $this->field_option( 'advanced-options', $field, $args ); // Size. $this->field_option( 'size', $field ); // Placeholder. $this->field_option( 'placeholder', $field ); // Default value. $this->field_option( 'default_value', $field ); // Custom CSS classes. $this->field_option( 'css', $field ); // Hide label. $this->field_option( 'label_hide', $field ); // Options close markup. $args = [ 'markup' => 'close', ]; $this->field_option( 'advanced-options', $field, $args ); } /** * Field preview inside the builder. * * @since 1.9.4 * * @param array $field Field data. */ public function field_preview( $field ) { // Define data. $placeholder = ! empty( $field['placeholder'] ) ? $field['placeholder'] : ''; $default_value = ! empty( $field['default_value'] ) ? $field['default_value'] : ''; // Label. $this->field_preview_option( 'label', $field, [ 'label_badge' => $this->get_field_preview_badge(), ] ); // Primary input. echo '<input type="url" placeholder="' . esc_attr( $placeholder ) . '" value="' . esc_attr( $default_value ) . '" class="primary-input" readonly>'; // Description. $this->field_preview_option( 'description', $field ); } /** * Field display on the form front-end. * * @since 1.9.4 * * @param array $field Field data and settings. * @param array $deprecated Deprecated field attributes. Use field properties. * @param array $form_data Form data and settings. */ public function field_display( $field, $deprecated, $form_data ) { } } Forms/Fields/PaymentSelect/Field.php 0000644 00000041562 15174710275 0013372 0 ustar 00 <?php namespace WPForms\Forms\Fields\PaymentSelect; use WPForms_Field; /** * Dropdown payment field. * * @since 1.8.2 */ class Field extends WPForms_Field { /** * Classic (old) style. * * @since 1.8.2 * * @var string */ public const STYLE_CLASSIC = 'classic'; /** * Modern style. * * @since 1.8.2 * * @var string */ public const STYLE_MODERN = 'modern'; /** * Primary class constructor. * * @since 1.8.2 */ public function init() { // Define field type information. $this->name = esc_html__( 'Dropdown Items', 'wpforms-lite' ); $this->keywords = esc_html__( 'product, store, ecommerce, pay, payment', 'wpforms-lite' ); $this->type = 'payment-select'; $this->icon = 'fa-caret-square-o-down'; $this->order = 70; $this->group = 'payment'; $this->defaults = [ 1 => [ 'label' => esc_html__( 'First Item', 'wpforms-lite' ), 'value' => '10', 'default' => '', ], 2 => [ 'label' => esc_html__( 'Second Item', 'wpforms-lite' ), 'value' => '25', 'default' => '', ], 3 => [ 'label' => esc_html__( 'Third Item', 'wpforms-lite' ), 'value' => '50', 'default' => '', ], ]; $this->default_settings = [ 'choices' => $this->defaults, ]; $this->hooks(); } /** * Register hooks. * * @since 1.8.2 */ private function hooks() { // Define additional field properties. add_filter( "wpforms_field_properties_{$this->type}", [ $this, 'field_properties' ], 5, 3 ); // Form frontend CSS enqueues. add_action( 'wpforms_frontend_css', [ $this, 'enqueue_frontend_css' ] ); // Form frontend JS enqueues. add_action( 'wpforms_frontend_js', [ $this, 'enqueue_frontend_js' ] ); // Customize HTML field value. add_filter( 'wpforms_html_field_value', [ $this, 'field_html_value' ], 10, 4 ); } /** * Define additional field properties. * * @since 1.8.2 * * @param array $properties Field properties. * @param array $field Field settings. * @param array $form_data Form data and settings. * * @return array */ public function field_properties( $properties, $field, $form_data ) { // Remove primary input. unset( $properties['inputs']['primary'] ); // Define data. $form_id = absint( $form_data['id'] ); $field_id = absint( $field['id'] ); $choices = $field['choices']; // Set options container (<select>) properties. $properties['input_container'] = [ 'class' => [ 'wpforms-payment-price' ], 'data' => [], 'id' => "wpforms-{$form_id}-field_{$field_id}", 'attr' => [ 'name' => "wpforms[fields][{$field_id}]", ], ]; // Set properties. foreach ( $choices as $key => $choice ) { $properties['inputs'][ $key ] = [ 'container' => [ 'attr' => [], 'class' => [ "choice-{$key}" ], 'data' => [], 'id' => '', ], 'label' => [ 'attr' => [ 'for' => "wpforms-{$form_id}-field_{$field_id}_{$key}", ], 'class' => [ 'wpforms-field-label-inline' ], 'data' => [], 'id' => '', 'text' => $choice['label'], ], 'attr' => [ 'value' => $choice['value'], 'data' => [ 'amount' => wpforms_format_amount( wpforms_sanitize_amount( $choice['value'] ) ), ], ], 'class' => [], 'data' => [], 'id' => "wpforms-{$form_id}-field_{$field_id}_{$key}", 'required' => ! empty( $field['required'] ) ? 'required' : '', 'default' => isset( $choice['default'] ), ]; } // Add a class that changes the field size. if ( ! empty( $field['size'] ) ) { $properties['input_container']['class'][] = 'wpforms-field-' . esc_attr( $field['size'] ); } // Required class for pagebreak validation. if ( ! empty( $field['required'] ) ) { $properties['input_container']['class'][] = 'wpforms-field-required'; } // Add additional class for container. if ( ! empty( $field['style'] ) && in_array( $field['style'], [ self::STYLE_CLASSIC, self::STYLE_MODERN ], true ) ) { $properties['container']['class'][] = "wpforms-field-select-style-{$field['style']}"; } if ( $this->is_payment_quantities_enabled( $field ) ) { $properties['container']['class'][] = ' wpforms-payment-quantities-enabled'; } return $properties; } /** * Get the value, that is used to prefill via dynamic or fallback population. * Based on field data and current properties. * * @since 1.8.2 * * @param string $raw_value Value from a GET param, always a string. * @param string $input Represent a subfield inside the field. May be empty. * @param array $properties Field properties. * @param array $field Current field specific data. * * @return array Modified field properties. */ protected function get_field_populated_single_property_value( $raw_value, $input, $properties, $field ) { /* * When the form is submitted, we get from Fallback only values (choice ID). * As payment-dropdown field doesn't support 'show_values' option - * we should transform value into label to check against using general logic in parent method. */ if ( ! is_string( $raw_value ) || empty( $field['choices'] ) || ! is_array( $field['choices'] ) ) { return $properties; } // The form submits only the choice ID, so shortcut for Dynamic when we have a label there. if ( ! is_numeric( $raw_value ) ) { return parent::get_field_populated_single_property_value( $raw_value, $input, $properties, $field ); } if ( ! empty( $field['choices'][ $raw_value ]['label'] ) && ! empty( $field['choices'][ $raw_value ]['value'] ) ) { return parent::get_field_populated_single_property_value( $field['choices'][ $raw_value ]['label'], $input, $properties, $field ); } return $properties; } /** * Field options panel inside the builder. * * @since 1.8.2 * * @param array $field Field settings. */ public function field_options( $field ) { /* * Basic field options. */ // Options open markup. $this->field_option( 'basic-options', $field, [ 'markup' => 'open' ] ); // Label. $this->field_option( 'label', $field ); // Choices option. $this->field_option( 'choices_payments', $field ); // Show price after item labels. $fld = $this->field_element( 'toggle', $field, [ 'slug' => 'show_price_after_labels', 'value' => isset( $field['show_price_after_labels'] ) ? '1' : '0', 'desc' => esc_html__( 'Show Price After Item Labels', 'wpforms-lite' ), 'tooltip' => esc_html__( 'Check this option to show price of the item after the label.', 'wpforms-lite' ), ], false ); $args = [ 'slug' => 'show_price_after_labels', 'content' => $fld, ]; $this->field_element( 'row', $field, $args ); // Quantity. $this->field_option( 'quantity', $field ); // Description. $this->field_option( 'description', $field ); // Required toggle. $this->field_option( 'required', $field ); // Options close markup. $this->field_option( 'basic-options', $field, [ 'markup' => 'close' ] ); /* * Advanced field options. */ // Options open markup. $this->field_option( 'advanced-options', $field, [ 'markup' => 'open' ] ); // Style. $lbl = $this->field_element( 'label', $field, [ 'slug' => 'style', 'value' => esc_html__( 'Style', 'wpforms-lite' ), 'tooltip' => esc_html__( 'Classic style is the default one generated by your browser. Modern has a fresh look and displays all selected options in a single row.', 'wpforms-lite' ), ], false ); $fld = $this->field_element( 'select', $field, [ 'slug' => 'style', 'value' => ! empty( $field['style'] ) ? $field['style'] : self::STYLE_CLASSIC, 'options' => [ self::STYLE_CLASSIC => esc_html__( 'Classic', 'wpforms-lite' ), self::STYLE_MODERN => esc_html__( 'Modern', 'wpforms-lite' ), ], ], false ); $this->field_element( 'row', $field, [ 'slug' => 'style', 'content' => $lbl . $fld, ] ); // Size. $this->field_option( 'size', $field ); // Placeholder. $this->field_option( 'placeholder', $field ); // Custom CSS classes. $this->field_option( 'css', $field ); // Hide label. $this->field_option( 'label_hide', $field ); // Options close markup. $this->field_option( 'advanced-options', $field, [ 'markup' => 'close' ] ); } /** * Field preview inside the builder. * * @since 1.8.2 * * @param array $field Field settings. */ public function field_preview( $field ) { // Label. $this->field_preview_option( 'label', $field ); // Prepare arguments. $args['modern'] = false; if ( ! empty( $field['style'] ) && $field['style'] === self::STYLE_MODERN ) { $args['modern'] = true; $args['class'] = 'choicesjs-select'; } // Choices. $this->field_preview_option( 'choices', $field, $args ); // Quantity. $this->field_preview_option( 'quantity', $field ); // Description. $this->field_preview_option( 'description', $field ); } /** * Field display on the form front-end. * * @since 1.8.2 * * @param array $field Field data and settings. * @param array $deprecated Deprecated array of field attributes. * @param array $form_data Form data and settings. * * @noinspection HtmlUnknownAttribute*/ public function field_display( $field, $deprecated, $form_data ) { // phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh $container = $field['properties']['input_container']; $field_placeholder = ! empty( $field['placeholder'] ) ? $field['placeholder'] : ''; $is_modern = ! empty( $field['style'] ) && $field['style'] === self::STYLE_MODERN; $choices = $field['properties']['inputs']; if ( ! empty( $field['required'] ) ) { $container['attr']['required'] = 'required'; } // Add a class for Choices.js initialization. if ( $is_modern ) { $container['class'][] = 'choicesjs-select'; // Add a size-class to data attribute - it is used when Choices.js is initialized. if ( ! empty( $field['size'] ) ) { $container['data']['size-class'] = 'wpforms-field-row wpforms-field-' . sanitize_html_class( $field['size'] ); } $container['data']['search-enabled'] = $this->is_choicesjs_search_enabled( count( $choices ) ); } $has_default = false; // Check to see if any of the options were selected by default. foreach ( $choices as $choice ) { if ( ! empty( $choice['default'] ) ) { $has_default = true; break; } } // Preselect default if no other choices were marked as default. printf( '<select %s>', wpforms_html_attributes( $container['id'], $container['class'], $container['data'], $container['attr'] ) // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ); // Optional placeholder. if ( ! empty( $field_placeholder ) || $is_modern ) { printf( '<option value="" class="placeholder" disabled %s>%s</option>', selected( false, $has_default, false ), esc_html( $field_placeholder ) ); } // Format string for option. if ( $is_modern ) { // The `data-custom-properties` is a Choices.js attribute, and it stores a copy of `data-amount` attribute. $option_format = '<option value="%1$s" data-amount="%2$s" data-custom-properties="%2$s" %3$s>%4$s</option>'; } else { $option_format = '<option value="%1$s" data-amount="%2$s" %3$s>%4$s</option>'; } // Build the select options. foreach ( $choices as $key => $choice ) { $amount = wpforms_format_amount( wpforms_sanitize_amount( $choice['attr']['value'] ) ); $label = $choice['label']['text'] ?? ''; /* translators: %s - item number. */ $label = $label !== '' ? $label : sprintf( esc_html__( 'Item %s', 'wpforms-lite' ), $key ); $label .= ! empty( $field['show_price_after_labels'] ) && isset( $choice['attr']['value'] ) ? ' - ' . wpforms_format_amount( wpforms_sanitize_amount( $choice['attr']['value'] ), true ) : ''; printf( $option_format, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped esc_attr( $key ), esc_attr( $amount ), selected( true, ! empty( $choice['default'] ), false ), esc_html( $label ) ); } echo '</select>'; $this->display_quantity_dropdown( $field ); } /** * Validate field on submitting the form. * * @since 1.8.2 * * @param int $field_id Field ID. * @param string $field_submit Submitted field value (raw data). * @param array $form_data Form data and settings. */ public function validate( $field_id, $field_submit, $form_data ) { // Basic required check - If field is marked as required, check for entry data. if ( ! empty( $form_data['fields'][ $field_id ]['required'] ) && empty( $field_submit ) ) { wpforms()->obj( 'process' )->errors[ $form_data['id'] ][ $field_id ] = wpforms_get_required_label(); } // Validate that the option selected is real. if ( ! empty( $field_submit ) && empty( $form_data['fields'][ $field_id ]['choices'][ $field_submit ] ) ) { wpforms()->obj( 'process' )->errors[ $form_data['id'] ][ $field_id ] = esc_html__( 'Invalid payment option', 'wpforms-lite' ); } } /** * Format and sanitize field. * * @since 1.8.2 * * @param int $field_id Field ID. * @param string $field_submit Submitted field value (selected option). * @param array $form_data Form data and settings. */ public function format( $field_id, $field_submit, $form_data ) { $choice_label = ''; $field = $form_data['fields'][ $field_id ]; $name = ! empty( $field['label'] ) ? sanitize_text_field( $field['label'] ) : ''; // Fetch the amount. if ( ! empty( $field['choices'][ $field_submit ]['value'] ) ) { $amount = wpforms_sanitize_amount( $field['choices'][ $field_submit ]['value'] ); } else { $amount = 0; } $value = wpforms_format_amount( $amount, true ); if ( empty( $field_submit ) ) { $value = ''; } elseif ( ! empty( $field['choices'][ $field_submit ]['label'] ) ) { $choice_label = sanitize_text_field( $field['choices'][ $field_submit ]['label'] ); $value = $choice_label . ' - ' . $value; } $field_data = [ 'name' => $name, 'value' => $value, 'value_choice' => $choice_label, 'value_raw' => sanitize_text_field( $field_submit ), 'amount' => wpforms_format_amount( $amount ), 'amount_raw' => $amount, 'currency' => wpforms_get_currency(), 'id' => absint( $field_id ), 'type' => sanitize_key( $this->type ), ]; if ( $this->is_payment_quantities_enabled( $field ) ) { $field_data['quantity'] = $this->get_submitted_field_quantity( $field, $form_data ); } wpforms()->obj( 'process' )->fields[ $field_id ] = $field_data; } /** * Form frontend CSS enqueues. * * @since 1.8.2 * * @param array $forms Forms on the current page. */ public function enqueue_frontend_css( $forms ) { $has_modern_select = false; foreach ( $forms as $form ) { if ( $this->is_field_style( $form, self::STYLE_MODERN ) ) { $has_modern_select = true; break; } } if ( $has_modern_select || wpforms()->obj( 'frontend' )->assets_global() ) { $min = wpforms_get_min_suffix(); wp_enqueue_style( 'wpforms-choicesjs', WPFORMS_PLUGIN_URL . "assets/css/choices{$min}.css", [], '10.2.0' ); } } /** * Form frontend JS enqueues. * * @since 1.8.2 * * @param array $forms Forms on the current page. */ public function enqueue_frontend_js( $forms ) { $has_modern_select = false; foreach ( $forms as $form ) { if ( $this->is_field_style( $form, self::STYLE_MODERN ) ) { $has_modern_select = true; break; } } if ( $has_modern_select || wpforms()->obj( 'frontend' )->assets_global() ) { $this->enqueue_choicesjs_once( $forms ); } } /** * Whether the provided form has a dropdown field with a specified style. * * @since 1.8.2 * * @param array $form Form data. * @param string $style Desired field style. * * @return bool */ protected function is_field_style( $form, $style ) { $is_field_style = false; if ( empty( $form['fields'] ) ) { return false; } foreach ( (array) $form['fields'] as $field ) { if ( ! empty( $field['type'] ) && $field['type'] === $this->type && ! empty( $field['style'] ) && sanitize_key( $style ) === $field['style'] ) { $is_field_style = true; break; } } return $is_field_style; } /** * Get field name for an ajax error message. * * @since 1.8.2 * * @param string|mixed $name Field name for error triggered. * @param array $field Field settings. * @param array $props List of properties. * @param string|string[] $error Error message. * * @return string * @noinspection PhpMissingReturnTypeInspection * @noinspection ReturnTypeCanBeDeclaredInspection */ public function ajax_error_field_name( $name, $field, $props, $error ) { $name = (string) $name; if ( ! isset( $field['type'] ) || $field['type'] !== $this->type ) { return $name; } return $props['input_container']['attr']['name'] ?? ''; } } Forms/Fields/Address/Field.php 0000644 00000070003 15174710275 0012172 0 ustar 00 <?php namespace WPForms\Forms\Fields\Address; use WPForms\Forms\Fields\Traits\ProField as ProFieldTrait; use WPForms_Field; /** * Address field. * * @since 1.9.4 */ class Field extends WPForms_Field { use ProFieldTrait; /** * Address schemes: 'us' or 'international' by default. * * @since 1.9.4 * * @var array */ public $schemes; /** * Primary class constructor. * * @since 1.9.4 */ public function init() { // Define field type information. $this->name = esc_html__( 'Address', 'wpforms-lite' ); $this->type = 'address'; $this->icon = 'fa-map-marker'; $this->order = 70; $this->group = 'fancy'; // Allow for additional or customizing address schemes. $default_schemes = [ 'us' => [ 'label' => esc_html__( 'US', 'wpforms-lite' ), 'address1_label' => esc_html__( 'Address Line 1', 'wpforms-lite' ), 'address2_label' => esc_html__( 'Address Line 2', 'wpforms-lite' ), 'city_label' => esc_html__( 'City', 'wpforms-lite' ), 'postal_label' => esc_html__( 'Zip Code', 'wpforms-lite' ), 'state_label' => esc_html__( 'State', 'wpforms-lite' ), 'states' => wpforms_us_states(), ], 'international' => [ 'label' => esc_html__( 'International', 'wpforms-lite' ), 'address1_label' => esc_html__( 'Address Line 1', 'wpforms-lite' ), 'address2_label' => esc_html__( 'Address Line 2', 'wpforms-lite' ), 'city_label' => esc_html__( 'City', 'wpforms-lite' ), 'postal_label' => esc_html__( 'Postal Code', 'wpforms-lite' ), 'state_label' => esc_html__( 'State / Province / Region', 'wpforms-lite' ), 'states' => '', 'country_label' => esc_html__( 'Country', 'wpforms-lite' ), 'countries' => wpforms_countries(), ], ]; /** * Allow modifying address schemes. * * @since 1.2.7 * * @param array $schemes Address schemes. */ $this->schemes = apply_filters( 'wpforms_address_schemes', $default_schemes ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName $this->init_pro_field(); $this->hooks(); } /** * Hooks. * * @since 1.9.4 */ protected function hooks() { } /** * Field options panel inside the builder. * * @since 1.9.4 * * @param array $field Field data. */ public function field_options( $field ) { // phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh /* * Basic field options. */ // Options open markup. $this->field_option( 'basic-options', $field, [ 'markup' => 'open', 'after_title' => $this->get_field_options_notice(), ] ); // Label. $this->field_option( 'label', $field ); // Address Scheme - was "format" key prior to 1.2.7. $scheme = ! empty( $field['scheme'] ) ? esc_attr( $field['scheme'] ) : 'us'; if ( empty( $scheme ) && ! empty( $field['format'] ) ) { $scheme = esc_attr( $field['format'] ); } $tooltip = esc_html__( 'Select scheme format for the address field.', 'wpforms-lite' ); $options = array_map( static function ( $s ) { return $s['label']; }, $this->schemes ); $output = $this->field_element( 'label', $field, [ 'slug' => 'scheme', 'value' => esc_html__( 'Scheme', 'wpforms-lite' ), 'tooltip' => $tooltip, ], false ); $output .= $this->field_element( 'select', $field, [ 'slug' => 'scheme', 'value' => $scheme, 'options' => $options, ], false ); $this->field_element( 'row', $field, [ 'slug' => 'scheme', 'content' => $output, ] ); // Description. $this->field_option( 'description', $field ); // Required toggle. $this->field_option( 'required', $field ); // Options close markup. $this->field_option( 'basic-options', $field, [ 'markup' => 'close', ] ); /* * Advanced field options. */ // Options open markup. $this->field_option( 'advanced-options', $field, [ 'markup' => 'open', ] ); // Size. $this->field_option( 'size', $field ); // Address Line 1. $address1_placeholder = ! empty( $field['address1_placeholder'] ) ? esc_attr( $field['address1_placeholder'] ) : ''; $address1_default = ! empty( $field['address1_default'] ) ? esc_attr( $field['address1_default'] ) : ''; printf( '<div class="wpforms-clear wpforms-field-option-row wpforms-field-option-row-address1" id="wpforms-field-option-row-%1$d-address1" data-subfield="address-1" data-field-id="%1$s">', wpforms_validate_field_id( $field['id'] ) // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ); $this->field_element( 'label', $field, [ 'slug' => 'address1_placeholder', 'value' => esc_html__( 'Address Line 1', 'wpforms-lite' ), ] ); // phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped echo '<div class="wpforms-field-options-columns-2 wpforms-field-options-columns">'; echo '<div class="placeholder wpforms-field-options-column">'; printf( '<input type="text" class="placeholder" id="wpforms-field-option-%1$s-address1_placeholder" name="fields[%1$s][address1_placeholder]" value="%2$s">', wpforms_validate_field_id( $field['id'] ), esc_attr( $address1_placeholder ) ); printf( '<label for="wpforms-field-option-%s-address1_placeholder" class="sub-label">%s</label>', wpforms_validate_field_id( $field['id'] ), esc_html__( 'Placeholder', 'wpforms-lite' ) ); echo '</div>'; echo '<div class="default wpforms-field-options-column">'; printf( '<input type="text" class="default" id="wpforms-field-option-%1$d-address1_default" name="fields[%1$s][address1_default]" value="%2$s">', wpforms_validate_field_id( $field['id'] ), esc_attr( $address1_default ) ); printf( '<label for="wpforms-field-option-%s-address1_default" class="sub-label">%s</label>', wpforms_validate_field_id( $field['id'] ), esc_html__( 'Default Value', 'wpforms-lite' ) ); echo '</div>'; echo '</div>'; // phpcs:enable WordPress.Security.EscapeOutput.OutputNotEscaped echo '</div>'; // Address Line 2. $address2_placeholder = ! empty( $field['address2_placeholder'] ) ? esc_attr( $field['address2_placeholder'] ) : ''; $address2_default = ! empty( $field['address2_default'] ) ? esc_attr( $field['address2_default'] ) : ''; $address2_hide = ! empty( $field['address2_hide'] ); printf( '<div class="wpforms-clear wpforms-field-option-row wpforms-field-option-row-address2" id="wpforms-field-option-row-%1$d-address2" data-subfield="address-2" data-field-id="%1$s">', wpforms_validate_field_id( $field['id'] ) // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ); echo '<div class="wpforms-field-header">'; $this->field_element( 'label', $field, [ 'slug' => 'address2_placeholder', 'value' => esc_html__( 'Address Line 2', 'wpforms-lite' ), ] ); $this->field_element( 'toggle', $field, [ 'slug' => 'address2_hide', 'value' => $address2_hide, 'desc' => esc_html__( 'Hide', 'wpforms-lite' ), 'title' => esc_html__( 'Turn On if you want to hide this sub field.', 'wpforms-lite' ), 'label-left' => true, 'control-class' => 'wpforms-field-option-in-label-right', 'class' => 'wpforms-subfield-hide', ] ); echo '</div>'; // phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped echo '<div class="wpforms-field-options-columns-2 wpforms-field-options-columns">'; echo '<div class="placeholder wpforms-field-options-column">'; printf( '<input type="text" class="placeholder" id="wpforms-field-option-%1$d-address2_placeholder" name="fields[%1$s][address2_placeholder]" value="%2$s">', wpforms_validate_field_id( $field['id'] ), esc_attr( $address2_placeholder ) ); printf( '<label for="wpforms-field-option-%s-address2_placeholder" class="sub-label">%s</label>', wpforms_validate_field_id( $field['id'] ), esc_html__( 'Placeholder', 'wpforms-lite' ) ); echo '</div>'; echo '<div class="default wpforms-field-options-column">'; printf( '<input type="text" class="default" id="wpforms-field-option-%1$d-address2_default" name="fields[%1$s][address2_default]" value="%2$s">', wpforms_validate_field_id( $field['id'] ), esc_attr( $address2_default ) ); printf( '<label for="wpforms-field-option-%s-address2_default" class="sub-label">%s</label>', wpforms_validate_field_id( $field['id'] ), esc_html__( 'Default Value', 'wpforms-lite' ) ); echo '</div>'; echo '</div>'; // phpcs:enable WordPress.Security.EscapeOutput.OutputNotEscaped echo '</div>'; // City. $city_placeholder = ! empty( $field['city_placeholder'] ) ? esc_attr( $field['city_placeholder'] ) : ''; $city_default = ! empty( $field['city_default'] ) ? esc_attr( $field['city_default'] ) : ''; printf( '<div class="wpforms-clear wpforms-field-option-row wpforms-field-option-row-city" id="wpforms-field-option-row-%1$s-city" data-subfield="city" data-field-id="%1$s">', wpforms_validate_field_id( $field['id'] ) // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ); $this->field_element( 'label', $field, [ 'slug' => 'city_placeholder', 'value' => esc_html__( 'City', 'wpforms-lite' ), ] ); // phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped echo '<div class="wpforms-field-options-columns-2 wpforms-field-options-columns">'; echo '<div class="placeholder wpforms-field-options-column">'; printf( '<input type="text" class="placeholder" id="wpforms-field-option-%1$s-city_placeholder" name="fields[%1$s][city_placeholder]" value="%2$s">', wpforms_validate_field_id( $field['id'] ), esc_attr( $city_placeholder ) ); printf( '<label for="wpforms-field-option-%s-city_placeholder" class="sub-label">%s</label>', wpforms_validate_field_id( $field['id'] ), esc_html__( 'Placeholder', 'wpforms-lite' ) ); echo '</div>'; echo '<div class="default wpforms-field-options-column">'; printf( '<input type="text" class="default" id="wpforms-field-option-%1$s-city_default" name="fields[%1$s][city_default]" value="%2$s">', wpforms_validate_field_id( $field['id'] ), esc_attr( $city_default ) ); printf( '<label for="wpforms-field-option-%s-city_default" class="sub-label">%s</label>', wpforms_validate_field_id( $field['id'] ), esc_html__( 'Default Value', 'wpforms-lite' ) ); echo '</div>'; echo '</div>'; // phpcs:enable WordPress.Security.EscapeOutput.OutputNotEscaped echo '</div>'; // State. $state_placeholder = ! empty( $field['state_placeholder'] ) ? $field['state_placeholder'] : ''; printf( '<div class="wpforms-clear wpforms-field-option-row wpforms-field-option-row-state" id="wpforms-field-option-row-%1$s-state" data-subfield="state" data-field-id="%1$s">', wpforms_validate_field_id( $field['id'] ) // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ); $this->field_element( 'label', $field, [ 'slug' => 'state_placeholder', 'value' => esc_html__( 'State / Province / Region', 'wpforms-lite' ), ] ); // phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped echo '<div class="wpforms-field-options-columns-2 wpforms-field-options-columns">'; echo '<div class="placeholder wpforms-field-options-column">'; printf( '<input type="text" class="placeholder" id="wpforms-field-option-%1$s-state_placeholder" name="fields[%1$s][state_placeholder]" value="%2$s">', wpforms_validate_field_id( $field['id'] ), esc_attr( $state_placeholder ) ); printf( '<label for="wpforms-field-option-%s-state_placeholder" class="sub-label">%s</label>', wpforms_validate_field_id( $field['id'] ), esc_html__( 'Placeholder', 'wpforms-lite' ) ); echo '</div>'; echo '<div class="default wpforms-field-options-column">'; $this->subfield_default( $field, 'state', 'states' ); printf( '<label for="wpforms-field-option-%s-state_default" class="sub-label">%s</label>', wpforms_validate_field_id( $field['id'] ), esc_html__( 'Default Value', 'wpforms-lite' ) ); echo '</div>'; echo '</div>'; // phpcs:enable WordPress.Security.EscapeOutput.OutputNotEscaped echo '</div>'; // ZIP/Postal. $postal_placeholder = ! empty( $field['postal_placeholder'] ) ? esc_attr( $field['postal_placeholder'] ) : ''; $postal_default = ! empty( $field['postal_default'] ) ? esc_attr( $field['postal_default'] ) : ''; $postal_hide = ! empty( $field['postal_hide'] ); $postal_visibility = ! isset( $this->schemes[ $scheme ]['postal_label'] ) ? 'wpforms-hidden' : ''; printf( '<div class="wpforms-clear wpforms-field-option-row wpforms-field-option-row-postal %1$s" id="wpforms-field-option-row-%2$s-postal" data-subfield="postal" data-field-id="%2$s">', sanitize_html_class( $postal_visibility ), wpforms_validate_field_id( $field['id'] ) // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ); echo '<div class="wpforms-field-header">'; $this->field_element( 'label', $field, [ 'slug' => 'postal_placeholder', 'value' => esc_html__( 'ZIP / Postal', 'wpforms-lite' ), ] ); $this->field_element( 'toggle', $field, [ 'slug' => 'postal_hide', 'value' => $postal_hide, 'desc' => esc_html__( 'Hide', 'wpforms-lite' ), 'title' => esc_html__( 'Turn On if you want to hide this sub field.', 'wpforms-lite' ), 'label-left' => true, 'control-class' => 'wpforms-field-option-in-label-right', 'class' => 'wpforms-subfield-hide', ] ); echo '</div>'; // phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped echo '<div class="wpforms-field-options-columns-2 wpforms-field-options-columns">'; echo '<div class="placeholder wpforms-field-options-column">'; printf( '<input type="text" class="placeholder" id="wpforms-field-option-%1$s-postal_placeholder" name="fields[%1$s][postal_placeholder]" value="%2$s">', wpforms_validate_field_id( $field['id'] ), esc_attr( $postal_placeholder ) ); printf( '<label for="wpforms-field-option-%s-postal_placeholder" class="sub-label">%s</label>', wpforms_validate_field_id( $field['id'] ), esc_html__( 'Placeholder', 'wpforms-lite' ) ); echo '</div>'; echo '<div class="default wpforms-field-options-column">'; printf( '<input type="text" class="default" id="wpforms-field-option-%1$s-postal_default" name="fields[%1$s][postal_default]" value="%2$s">', wpforms_validate_field_id( $field['id'] ), esc_attr( $postal_default ) ); printf( '<label for="wpforms-field-option-%s-postal_default" class="sub-label">%s</label>', wpforms_validate_field_id( $field['id'] ), esc_html__( 'Default Value', 'wpforms-lite' ) ); echo '</div>'; echo '</div>'; // phpcs:enable WordPress.Security.EscapeOutput.OutputNotEscaped echo '</div>'; // Country. $country_placeholder = ! empty( $field['country_placeholder'] ) ? $field['country_placeholder'] : ''; $country_hide = ! empty( $field['country_hide'] ); $country_visibility = ! isset( $this->schemes[ $scheme ]['countries'] ) ? 'wpforms-hidden' : ''; printf( '<div class="wpforms-clear wpforms-field-option-row wpforms-field-option-row-country %1$s" id="wpforms-field-option-row-%2$s-country" data-subfield="country" data-field-id="%2$s">', sanitize_html_class( $country_visibility ), wpforms_validate_field_id( $field['id'] ) // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ); echo '<div class="wpforms-field-header">'; $this->field_element( 'label', $field, [ 'slug' => 'country_placeholder', 'value' => esc_html__( 'Country', 'wpforms-lite' ), ] ); $this->field_element( 'toggle', $field, [ 'slug' => 'country_hide', 'value' => $country_hide, 'desc' => esc_html__( 'Hide', 'wpforms-lite' ), 'title' => esc_html__( 'Turn On if you want to hide this sub field.', 'wpforms-lite' ), 'label-left' => true, 'control-class' => 'wpforms-field-option-in-label-right', 'class' => 'wpforms-subfield-hide', ] ); echo '</div>'; // phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped echo '<div class="wpforms-field-options-columns-2 wpforms-field-options-columns">'; echo '<div class="placeholder wpforms-field-options-column">'; printf( '<input type="text" class="placeholder" id="wpforms-field-option-%1$s-country_placeholder" name="fields[%1$s][country_placeholder]" value="%2$s">', wpforms_validate_field_id( $field['id'] ), esc_attr( $country_placeholder ) ); printf( '<label for="wpforms-field-option-%s-country_placeholder" class="sub-label">%s</label>', wpforms_validate_field_id( $field['id'] ), esc_html__( 'Placeholder', 'wpforms-lite' ) ); echo '</div>'; echo '<div class="default wpforms-field-options-column">'; $this->subfield_default( $field, 'country', 'countries' ); printf( '<label for="wpforms-field-option-%s-country_default" class="sub-label">%s</label>', wpforms_validate_field_id( $field['id'] ), esc_html__( 'Default Value', 'wpforms-lite' ) ); echo '</div>'; echo '</div>'; // phpcs:enable WordPress.Security.EscapeOutput.OutputNotEscaped echo '</div>'; // Custom CSS classes. $this->field_option( 'css', $field ); // Hide label. $this->field_option( 'label_hide', $field ); // Hide sublabel. $this->field_option( 'sublabel_hide', $field ); // Options close markup. $this->field_option( 'advanced-options', $field, [ 'markup' => 'close', ] ); } /** * Field preview inside the builder. * * @since 1.9.4 * * @param array $field Field data. */ public function field_preview( $field ) { // phpcs:ignore Generic.Metrics.CyclomaticComplexity.MaxExceeded // Define data. $address1_placeholder = ! empty( $field['address1_placeholder'] ) ? $field['address1_placeholder'] : ''; $address1_default = ! empty( $field['address1_default'] ) ? $field['address1_default'] : ''; $address2_placeholder = ! empty( $field['address2_placeholder'] ) ? $field['address2_placeholder'] : ''; $address2_default = ! empty( $field['address2_default'] ) ? $field['address2_default'] : ''; $address2_hide = ! empty( $field['address2_hide'] ) ? 'wpforms-hide' : ''; $city_placeholder = ! empty( $field['city_placeholder'] ) ? $field['city_placeholder'] : ''; $city_default = ! empty( $field['city_default'] ) ? $field['city_default'] : ''; $postal_placeholder = ! empty( $field['postal_placeholder'] ) ? $field['postal_placeholder'] : ''; $postal_default = ! empty( $field['postal_default'] ) ? $field['postal_default'] : ''; $postal_hide = ! empty( $field['postal_hide'] ) ? 'wpforms-hide' : ''; $country_hide = ! empty( $field['country_hide'] ) ? 'wpforms-hide' : ''; $format = ! empty( $field['format'] ) ? $field['format'] : 'us'; $scheme_selected = ! empty( $field['scheme'] ) ? $field['scheme'] : $format; // Label. $this->field_preview_option( 'label', $field, [ 'label_badge' => $this->get_field_preview_badge(), ] ); // Field elements. foreach ( $this->schemes as $slug => $scheme ) { $address1_label = $scheme['address1_label'] ?? esc_html__( 'Address Line 1', 'wpforms-lite' ); $address2_label = $scheme['address2_label'] ?? esc_html__( 'Address Line 2', 'wpforms-lite' ); $city_label = $scheme['city_label'] ?? esc_html__( 'City', 'wpforms-lite' ); $state_label = $scheme['state_label'] ?? esc_html__( 'State / Province / Region', 'wpforms-lite' ); $postal_label = $scheme['postal_label'] ?? esc_html__( 'Postal Code', 'wpforms-lite' ); $country_label = $scheme['country_label'] ?? esc_html__( 'Country', 'wpforms-lite' ); $is_active_scheme = $slug === $scheme_selected; $scheme_hide_class = ! $is_active_scheme ? 'wpforms-hide' : ''; $state_placeholder = ! empty( $field['state_placeholder'] ) ? $field['state_placeholder'] : ''; $state_default = $is_active_scheme && ! empty( $field['state_default'] ) ? $field['state_default'] : ''; $country_placeholder = ! empty( $field['country_placeholder'] ) ? $field['country_placeholder'] : ''; $country_default = $is_active_scheme && ! empty( $field['country_default'] ) ? $field['country_default'] : ''; // Wrapper. printf( '<div class="wpforms-address-scheme wpforms-address-scheme-%s %s">', wpforms_sanitize_classes( $slug ), // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped wpforms_sanitize_classes( $scheme_hide_class ) // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ); // Row 1 - Address Line 1. printf( '<div class="wpforms-field-row wpforms-address-1"> <input type="text" placeholder="%s" value="%s" readonly> <label class="wpforms-sub-label">%s</label> </div>', esc_attr( $address1_placeholder ), esc_attr( $address1_default ), esc_html( $address1_label ) ); // Row 2 - Address Line 2. printf( '<div class="wpforms-field-row wpforms-address-2 %s"> <input type="text" placeholder="%s" value="%s" readonly> <label class="wpforms-sub-label">%s</label> </div>', wpforms_sanitize_classes( $address2_hide ), // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped esc_attr( $address2_placeholder ), esc_attr( $address2_default ), esc_html( $address2_label ) ); // Row 3 - City & State. echo '<div class="wpforms-field-row">'; // City. printf( '<div class="wpforms-city wpforms-one-half "> <input type="text" placeholder="%s" value="%s" readonly> <label class="wpforms-sub-label">%s</label> </div>', esc_attr( $city_placeholder ), esc_attr( $city_default ), esc_html( $city_label ) ); // State / Providence / Region. echo '<div class="wpforms-state wpforms-one-half last">'; if ( isset( $scheme['states'] ) && empty( $scheme['states'] ) ) { // State text input. printf( '<input type="text" placeholder="%s" value="%s" readonly>', esc_attr( $state_placeholder ), esc_attr( $state_default ) ); } elseif ( ! empty( $scheme['states'] ) && is_array( $scheme['states'] ) ) { $state_option = $this->dropdown_empty_value( (string) $state_label ); if ( ! empty( $state_placeholder ) ) { $state_option = $state_placeholder; } if ( $is_active_scheme && ! empty( $state_default ) ) { $state_option = $scheme['states'][ $state_default ]; } // State select. printf( '<select readonly> <option class="placeholder" selected>%s</option> </select>', esc_html( $state_option ) ); } printf( '<label class="wpforms-sub-label">%s</label>', esc_html( $state_label ) ); echo '</div>'; // End row 3 - City & State. echo '</div>'; // Row 4 - Zip & Country. echo '<div class="wpforms-field-row">'; // ZIP / Postal. printf( '<div class="wpforms-postal wpforms-one-half %s"> <input type="text" placeholder="%s" value="%s" readonly> <label class="wpforms-sub-label">%s</label> </div>', wpforms_sanitize_classes( $postal_hide ), // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped esc_attr( $postal_placeholder ), esc_attr( $postal_default ), esc_html( $postal_label ) ); // Country. printf( '<div class="wpforms-country wpforms-one-half last %s">', sanitize_html_class( $country_hide ) ); if ( isset( $scheme['countries'] ) && empty( $scheme['countries'] ) ) { // Country text input. printf( '<input type="text" placeholder="%s" value="%s" readonly>', esc_attr( $country_placeholder ), esc_attr( $country_default ) ); } elseif ( ! empty( $scheme['countries'] ) && is_array( $scheme['countries'] ) ) { $country_option = $this->dropdown_empty_value( (string) $country_label ); if ( ! empty( $country_placeholder ) ) { $country_option = $country_placeholder; } if ( $is_active_scheme && ! empty( $country_default ) ) { $country_option = $scheme['countries'][ $country_default ]; } // Country select. printf( '<select readonly><option class="placeholder" selected>%s</option></select>', esc_html( $country_option ) ); printf( '<label class="wpforms-sub-label">%s</label>', esc_html( $country_label ) ); } echo '</div>'; // End row 4 - Zip & Country. echo '</div>'; // End wrapper. echo '</div>'; } // Description. $this->field_preview_option( 'description', $field ); } /** * Field display on the form front-end. * * @since 1.9.4 * * @param array $field Field data and settings. * @param array $deprecated Deprecated field attributes. Use field properties instead. * @param array $form_data Form data and settings. */ public function field_display( $field, $deprecated, $form_data ) { } /** * Output "Default" option fields for State/Country subfields. * * The default value should be set only for the scheme it belongs to. * * @since 1.9.4 * * @param array $field Address field data. * @param string $subfield_slug Subfield slug, either `state` or `country`. * @param string $subfield_key Subfield key in `$scheme` data, either `states` or `countries`. * * @noinspection HtmlUnknownAttribute */ private function subfield_default( array $field, string $subfield_slug, string $subfield_key ): void { // Scheme or default value may not be set yet. $active_scheme = ! empty( $field['scheme'] ) ? $field['scheme'] : 'us'; $default_value = ! empty( $field[ "{$subfield_slug}_default" ] ) ? $field[ "{$subfield_slug}_default" ] : ''; foreach ( $this->schemes as $scheme_slug => $scheme_data ) { $subfield_label = empty( $scheme_data[ $subfield_slug . '_label' ] ) ? ucfirst( $subfield_slug ) : $scheme_data[ $subfield_slug . '_label' ]; $empty_value = $this->dropdown_empty_value( $subfield_label ); $is_active_scheme = $scheme_slug === $active_scheme; // If a scheme contains an array of values, we display a select dropdown. Otherwise, text input. if ( ! empty( $scheme_data[ $subfield_key ] ) && is_array( $scheme_data[ $subfield_key ] ) ) { $options_escaped = sprintf( '<option value="">%s</option>', esc_html( $empty_value ) ); foreach ( $scheme_data[ $subfield_key ] as $value => $label ) { $options_escaped .= sprintf( '<option value="%s"%s>%s</option>', esc_attr( $value ), $is_active_scheme ? selected( $default_value, $value, false ) : '', esc_html( $label ) ); } if ( $is_active_scheme ) { printf( '<select class="default" id="wpforms-field-option-%1$s-%2$s_default" name="fields[%1$s][%2$s_default]" data-scheme="%3$s">%4$s</select>', wpforms_validate_field_id( $field['id'] ), // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped esc_attr( $subfield_slug ), esc_attr( $scheme_slug ), $options_escaped // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ); continue; } printf( '<select class="default wpforms-hidden-strict" id="" name="" data-scheme="%s">%s</select>', esc_attr( $scheme_slug ), $options_escaped // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ); continue; } if ( $is_active_scheme ) { printf( '<input type="text" class="default" id="wpforms-field-option-%1$s-%2$s_default" name="fields[%1$s][%2$s_default]" value="%3$s" data-scheme="%4$s">', wpforms_validate_field_id( $field['id'] ), // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped esc_attr( $subfield_slug ), esc_attr( $default_value ), esc_attr( $scheme_slug ) ); continue; } printf( '<input type="text" class="default wpforms-hidden-strict" id="" name="" value="" data-scheme="%s">', esc_attr( $scheme_slug ) ); } } /** * Get a select dropdown "placeholder" option which is displayed if nothing is selected. * * @since 1.9.4 * * @param string $name Select field name, can be lowercase or uppercase. * * @return string */ protected function dropdown_empty_value( string $name ): string { return sprintf( /* translators: %s - subfield name, e.g., state, country. */ __( '--- Select %s ---', 'wpforms-lite' ), $name ); } } Forms/Fields/Address/Frontend.php 0000644 00000003030 15174710275 0012722 0 ustar 00 <?php namespace WPForms\Forms\Fields\Address; use WPForms\Forms\Fields\Base\Frontend as FrontendBase; /** * Address field frontend class. * * @since 1.9.5 */ class Frontend extends FrontendBase { /** * Register hooks. * * @since 1.9.5 * * @noinspection ReturnTypeCanBeDeclaredInspection */ public function hooks() { add_filter( 'wpforms_frontend_strings', [ $this, 'strings' ] ); add_action( 'wpforms_wp_footer', [ $this, 'assets_footer' ], 15 ); } /** * Add address field related settings to a wpforms_settings array. * * @since 1.9.5 * * @param array|mixed $strings The wpforms_settings array. * * @return array */ public function strings( $strings ): array { $strings = (array) $strings; /** * Modify the list of countries without states. * * @since 1.9.5 * * @param array $countries The list of country codes, defaults to [ 'GB', 'DE', 'CH', 'NL' ]. * * @return array */ $countries = (array) apply_filters( 'wpforms_forms_fields_address_frontend_strings_list_countries_without_states', [ 'GB', 'DE', 'CH', 'NL' ] ); $strings['address_field']['list_countries_without_states'] = array_map( 'strtoupper', $countries ); return $strings; } /** * Load the assets needed for the Address field. * * @since 1.9.5 */ public function assets_footer(): void { $min = wpforms_get_min_suffix(); wp_enqueue_script( 'wpforms-address-field', WPFORMS_PLUGIN_URL . "assets/js/frontend/fields/address{$min}.js", [ 'wpforms' ], WPFORMS_VERSION, true ); } } Forms/Fields/FileUpload/Field.php 0000644 00000030464 15174710275 0012640 0 ustar 00 <?php namespace WPForms\Forms\Fields\FileUpload; use WPForms\Forms\Fields\Traits\ProField as ProFieldTrait; use WPForms\Forms\Fields\Traits\CameraTrait; use WPForms\Forms\Fields\Traits\AccessRestrictionsTrait; use WPForms_Field; /** * File upload field. * * @since 1.9.4 */ class Field extends WPForms_Field { use ProFieldTrait; use CameraTrait; use AccessRestrictionsTrait; /** * Classic (old) style of the file uploader field. * * @since 1.9.4 * * @var string */ public const STYLE_CLASSIC = 'classic'; /** * Modern style of the file uploader field. * * @since 1.9.4 * * @var string */ public const STYLE_MODERN = 'modern'; /** * Maximum file number. * * @since 1.9.4 * * @var int */ private const MAX_FILE_NUM = 100; /** * Replaceable (either in PHP or JS) template for a maximum file number. * * @since 1.9.4 * * @var string */ protected const TEMPLATE_MAXFILENUM = '{maxFileNumber}'; /** * Primary class constructor. * * @since 1.9.4 */ public function init() { // Define field type information. $this->name = esc_html__( 'File Upload', 'wpforms-lite' ); $this->type = 'file-upload'; $this->icon = 'fa-upload'; $this->order = 100; $this->group = 'fancy'; $this->default_settings = [ 'style' => self::STYLE_MODERN, ]; $this->init_pro_field(); } /** * Field options panel inside the builder. * * @since 1.9.4 * * @param array $field Field data and settings. * * @noinspection HtmlUnknownTarget */ public function field_options( $field ) { $style = ! empty( $field['style'] ) ? $field['style'] : self::STYLE_MODERN; /* * Basic field options. */ // Options open markup. $this->field_option( 'basic-options', $field, [ 'markup' => 'open', 'after_title' => $this->get_field_options_notice(), ] ); // Label. $this->field_option( 'label', $field ); // Description. $this->field_option( 'description', $field ); // Allowed extensions. $lbl = $this->field_element( 'label', $field, [ 'slug' => 'extensions', 'value' => esc_html__( 'Allowed File Extensions', 'wpforms-lite' ), 'tooltip' => esc_html__( 'Enter the extensions you would like to allow, comma separated.', 'wpforms-lite' ), 'after_tooltip' => sprintf( '<a href="%1$s" class="after-label-description" target="_blank" rel="noopener noreferrer">%2$s</a>', esc_url( wpforms_utm_link( 'https://wpforms.com/docs/a-complete-guide-to-the-file-upload-field/#file-types', 'Field Options', 'File Upload Extensions Documentation' ) ), esc_html__( 'See More Details', 'wpforms-lite' ) ), ], false ); $fld = $this->field_element( 'text', $field, [ 'slug' => 'extensions', 'value' => ! empty( $field['extensions'] ) ? $field['extensions'] : '', ], false ); $this->field_element( 'row', $field, [ 'slug' => 'extensions', 'content' => $lbl . $fld, ] ); // Max file size. $lbl = $this->field_element( 'label', $field, [ 'slug' => 'max_size', 'value' => esc_html__( 'Max File Size', 'wpforms-lite' ), 'tooltip' => sprintf( /* translators: %s - max upload size. */ esc_html__( 'Enter the max size of each file, in megabytes, to allow. If left blank, the value defaults to the maximum size the server allows which is %s.', 'wpforms-lite' ), wpforms_max_upload() ), ], false ); $fld = $this->field_element( 'text', $field, [ 'slug' => 'max_size', 'type' => 'number', 'attrs' => [ 'min' => 1, 'max' => 512, 'step' => 1, 'pattern' => '[0-9]', ], 'value' => ! empty( $field['max_size'] ) ? abs( $field['max_size'] ) : '', ], false ); $this->field_element( 'row', $field, [ 'slug' => 'max_size', 'content' => $lbl . $fld, ] ); // Max file number. $lbl = $this->field_element( 'label', $field, [ 'slug' => 'max_file_number', 'value' => esc_html__( 'Max File Uploads', 'wpforms-lite' ), 'tooltip' => esc_html__( 'Enter the max number of files to allow. If left blank, the value defaults to 1.', 'wpforms-lite' ), ], false ); $fld = $this->field_element( 'text', $field, [ 'slug' => 'max_file_number', 'type' => 'number', 'attrs' => [ 'min' => 1, 'max' => self::MAX_FILE_NUM, 'step' => 1, 'pattern' => '[0-9]', ], 'value' => $this->get_max_file_number( $field ), ], false ); $this->field_element( 'row', $field, [ 'slug' => 'max_file_number', 'content' => $lbl . $fld, 'class' => $style === self::STYLE_CLASSIC ? 'wpforms-hidden' : '', ] ); // Required toggle. $this->field_option( 'required', $field ); // Options close markup. $this->field_option( 'basic-options', $field, [ 'markup' => 'close' ] ); /* * Advanced field options. */ // Options open markup. $this->field_option( 'advanced-options', $field, [ 'markup' => 'open' ] ); // Style. $lbl = $this->field_element( 'label', $field, [ 'slug' => 'style', 'value' => esc_html__( 'Style', 'wpforms-lite' ), 'tooltip' => esc_html__( 'Modern Style supports multiple file uploads, displays a drag-and-drop upload box, and uses AJAX. Classic Style supports single file upload and displays a traditional upload button.', 'wpforms-lite' ), ], false ); $fld = $this->field_element( 'select', $field, [ 'slug' => 'style', 'value' => $style, 'options' => [ self::STYLE_MODERN => esc_html__( 'Modern', 'wpforms-lite' ), self::STYLE_CLASSIC => esc_html__( 'Classic', 'wpforms-lite' ), ], ], false ); $this->field_element( 'row', $field, [ 'slug' => 'style', 'content' => $lbl . $fld, ] ); // Custom CSS classes. $this->field_option( 'css', $field ); // Media Library toggle. $fld = $this->field_element( 'toggle', $field, [ 'slug' => 'media_library', 'value' => ! empty( $field['media_library'] ) ? 1 : '', 'desc' => esc_html__( 'Store Files in WordPress Media Library', 'wpforms-lite' ), 'tooltip' => esc_html__( 'Check this option to store the final uploaded file in the WordPress Media Library', 'wpforms-lite' ), 'class' => 'wpforms-file-upload-media-library', ], false ); $this->field_element( 'row', $field, [ 'slug' => 'media_library', 'content' => $fld, ] ); // Access Restrictions. $this->access_restrictions_options( $field ); // Camera. $this->camera_options( $field ); // Hide Label. $this->field_option( 'label_hide', $field ); // Options close markup. $this->field_option( 'advanced-options', $field, [ 'markup' => 'close', ] ); } /** * Field preview panel inside the builder. * * @since 1.9.4 * * @param array $field Field data. */ public function field_preview( $field ) { // Label. $this->field_preview_option( 'label', $field, [ 'label_badge' => $this->get_field_preview_badge(), ] ); $modern_classes = [ 'wpforms-file-upload-builder-modern' ]; $classic_classes = [ 'wpforms-file-upload-builder-classic' ]; if ( empty( $field['style'] ) || $field['style'] !== self::STYLE_CLASSIC ) { $classic_classes[] = 'wpforms-hide'; } else { $modern_classes[] = 'wpforms-hide'; } $strings = $this->get_strings(); $max_file_number = $this->get_max_file_number( $field ); /** * Filter the classic camera text. * * @since 1.9.8 * * @param string $classic_camera The classic camera text. */ $classic_camera_text = (string) apply_filters( 'wpforms_forms_fields_file_upload_field_classic_camera_text', esc_html__( 'Capture With Your Camera', 'wpforms-lite' ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo wpforms_render( 'fields/file-upload/file-upload-backend', [ 'max_file_number' => $max_file_number, 'preview_hint' => str_replace( self::TEMPLATE_MAXFILENUM, $max_file_number, $strings['preview_hint'] ), 'modern_classes' => implode( ' ', $modern_classes ), 'classic_classes' => implode( ' ', $classic_classes ), 'is_camera' => ! empty( $field['camera_enabled'] ) ? 1 : '', 'classic_camera' => $classic_camera_text, ], true ); // Description. $this->field_preview_option( 'description', $field ); } /** * File Uploads specific strings. * * @since 1.9.4 * * @return array Field-specific strings. */ public function get_strings(): array { return [ 'preview_title_single' => sprintf( /* translators: %1$s: Choose File to Upload opening tag, %2$s: Choose File to Upload closing tag. */ esc_html__( 'Drag & Drop File or %1$sChoose File to Upload%2$s', 'wpforms-lite' ), '<span class="wpforms-file-upload-choose-file">', '</span>' ), 'preview_title_plural' => sprintf( /* translators: %1$s: Choose Files to Upload opening tag, %2$s: Choose Files to Upload closing tag. */ esc_html__( 'Drag & Drop Files or %1$sChoose Files to Upload%2$s', 'wpforms-lite' ), '<span class="wpforms-file-upload-choose-file">', '</span>' ), 'preview_title_single_camera' => sprintf( /* translators: %1$s: Choose File to Upload opening tag, %2$s: Closing tag, %3$s: Capture With Camera opening tag. */ esc_html__( 'Drag & Drop File, %1$sChoose File to Upload%2$s, or %3$sCapture With Camera%2$s', 'wpforms-lite' ), '<span class="wpforms-file-upload-choose-file">', '</span>', '<span class="wpforms-file-upload-capture-camera">' ), 'preview_title_plural_camera' => sprintf( /* translators: %1$s: Choose Files to Upload opening tag, %2$s: Closing tag, %3$s: Capture With Camera opening tag. */ esc_html__( 'Drag & Drop Files, %1$sChoose Files to Upload%2$s, or %3$sCapture With Camera%2$s', 'wpforms-lite' ), '<span class="wpforms-file-upload-choose-file">', '</span>', '<span class="wpforms-file-upload-capture-camera">' ), 'preview_hint' => sprintf( /* translators: % - max number of files as a template string (not a number), replaced by a number later. */ esc_html__( 'You can upload up to %s files.', 'wpforms-lite' ), self::TEMPLATE_MAXFILENUM ), 'password_match_error_title' => esc_html__( 'Passwords Do Not Match', 'wpforms-lite' ), 'password_match_error_text' => esc_html__( 'Please check the password for the following fields: {fields}', 'wpforms-lite' ), 'password_empty_error_title' => esc_html__( 'Passwords Are Empty', 'wpforms-lite' ), 'password_empty_error_text' => esc_html__( 'Please enter a password for the following fields: {fields}', 'wpforms-lite' ), 'notification_warning_title' => esc_html__( 'Cannot Enable Restrictions', 'wpforms-lite' ), 'notification_warning_text' => esc_html__( 'This field is attached to Notifications. In order to enable restrictions, please first remove it from File Upload Attachments in Notifications.', 'wpforms-lite' ), 'notification_error_title' => esc_html__( 'Cannot Enable Attachments', 'wpforms-lite' ), 'notification_error_text' => esc_html__( 'The following fields ({fields}) cannot be attached to notifications because restrictions are enabled for them.', 'wpforms-lite' ), 'all_user_roles_selected' => esc_html__( 'All User Roles already selected', 'wpforms-lite' ), 'incompatible_addon_text' => esc_html__( 'File Upload Restrictions can\'t be enabled because the current version of the Post Submissions addon is incompatible.', 'wpforms-lite' ), ]; } /** * Getting max file number. * * @since 1.9.4 * * @param array $field Field data. * * @return int * @noinspection PhpMissingParamTypeInspection */ protected function get_max_file_number( $field ): int { if ( empty( $field['max_file_number'] ) ) { return 1; } $max_file_number = absint( $field['max_file_number'] ); if ( $max_file_number < 1 ) { return 1; } if ( $max_file_number > self::MAX_FILE_NUM ) { return self::MAX_FILE_NUM; } return $max_file_number; } /** * Field display on the form front-end. * * @since 1.9.4 * * @param array $field Field data and settings. * @param array $deprecated Deprecated field attributes. Use field properties. * @param array $form_data Form data and settings. */ public function field_display( $field, $deprecated, $form_data ) { } } Forms/Fields/Traits/NumberField.php 0000644 00000016666 15174710275 0013243 0 ustar 00 <?php namespace WPForms\Forms\Fields\Traits; /** * Numbers and Number Slider Field trait, designed for use with `WPForms_Field`. * * @since 1.9.4 */ trait NumberField { /** * Enqueues required scripts for the form builder. * * @since 1.9.4 */ private function number_hooks() { add_action( 'wpforms_builder_enqueues', [ $this, 'number_builder_enqueues' ] ); } /** * Enqueue wpforms-number-field script. * * @since 1.9.4 * * @param string $view Current view. * * @noinspection PhpUnusedParameterInspection, PhpUnnecessaryCurlyVarSyntaxInspection */ public function number_builder_enqueues( $view ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found $min = wpforms_get_min_suffix(); wp_enqueue_script( 'wpforms-number-field', WPFORMS_PLUGIN_URL . "assets/js/admin/builder/fields/numbers{$min}.js", [ 'wpforms-builder', 'wpforms-utils' ], WPFORMS_VERSION, false ); } /** * Helper function to create field option elements. * * Field option elements are pieces that help create a field option. * They are used to quickly build field options. * * This method is intended to be used within classes that implement or extend * the `WPForms_Field` functionality. * * @since 1.9.4 * * @param string $option Field option to render. * @param array $field Field data and settings. * @param array $args Field preview arguments. * @param bool $echo_output Print or return the value. Print by default. * * @return mixed echo or return string */ abstract public function field_element( $option, $field, $args = [], $echo_output = true ); /** * Helper function to create a number field option element. * * @since 1.9.4 * * @param array $field Field data and settings. * @param array $args Field preview arguments. * @param bool $echo_output Whether to print the generated output. Default true. * * @return string */ private function field_number_element( $field, $args = [], $echo_output = true ) { //phpcs:ignore Generic.Metrics.CyclomaticComplexity.MaxExceeded if ( ! isset( $args['slug'], $args['label'] ) ) { return ''; } $slug = $args['slug']; $label = $args['label']; $value = $field[ $slug ] ?? $args['value'] ?? ''; $attrs = []; if ( isset( $args['min'] ) && is_numeric( $args['min'] ) ) { $attrs['min'] = (float) $args['min']; } if ( isset( $args['max'] ) && is_numeric( $args['max'] ) ) { $attrs['max'] = (float) $args['max']; } if ( isset( $args['step'] ) && ( $args['step'] === 'any' || ( is_numeric( $args['step'] ) && $args['step'] > 0 ) ) ) { $attrs['step'] = (string) $args['step']; } $number_label_markup = $this->field_element( 'label', $field, [ 'slug' => $slug, 'value' => $label, 'tooltip' => $args['tooltip'] ?? '', ], false ); $number_input_markup = $this->field_element( 'text', $field, [ 'type' => 'number', 'slug' => $slug, 'value' => is_numeric( $value ) ? (float) $value : '', 'attrs' => $attrs, 'class' => $args['class'] ?? '', ], false ); $output = $this->field_element( 'row', $field, [ 'slug' => $slug, 'content' => $number_label_markup . $number_input_markup, ], false ); if ( ! $output ) { return ''; } if ( $echo_output ) { // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo $output; } return $output; } /** * Helper function to create `min_max` field option markup. * * @since 1.9.4 * * @param array $field Field data and settings. * @param array $args Field preview arguments. * @param bool $echo_output Print or return the value. Print by default. * * @return string */ private function field_number_option_min_max( $field, $args, $echo_output = true ) { $class = $args['class'] ?? 'number_min_max'; $range_label_markup = $this->field_element( 'label', $field, [ 'slug' => 'min', 'value' => $args['label'] ?? esc_html__( 'Range', 'wpforms-lite' ), 'tooltip' => $args['tooltip'] ?? esc_html__( 'Define the minimum and the maximum values for the field.', 'wpforms-lite' ), ], false ); $min_value = $field['min'] ?? null; $input_min_args = [ 'type' => 'number', 'slug' => 'min', 'value' => is_numeric( $min_value ) ? (float) $min_value : '', 'class' => $class . '-min', 'attrs' => [ 'step' => 'any', ], ]; $range_input_min_markup = $this->field_element( 'text', $field, $input_min_args, false ); $max_value = $field['max'] ?? null; $input_max_args = [ 'type' => 'number', 'slug' => 'max', 'value' => is_numeric( $max_value ) ? (float) $max_value : '', 'class' => $class . '-max', 'attrs' => [ 'step' => 'any', ], ]; $range_input_max_markup = $this->field_element( 'text', $field, $input_max_args, false ); return $this->field_element( 'row', $field, [ 'slug' => 'min_max', 'content' => $range_label_markup . sprintf( '<div class="wpforms-input-row"> <div class="minimum">%s<label for="wpforms-field-option-%d-min" class="sub-label">%s</label></div> <div class="maximum">%s<label for="wpforms-field-option-%d-max" class="sub-label">%s</label></div> </div>', $range_input_min_markup, (int) $field['id'], esc_html__( 'Minimum', 'wpforms-lite' ), $range_input_max_markup, (int) $field['id'], esc_html__( 'Maximum', 'wpforms-lite' ) ), ], $echo_output ); } /** * Helper function to create `default_value` field option markup. * * @since 1.9.4 * * @param array $field Field data and settings. * @param array $args Field preview arguments. * @param bool $echo_output Print or return the value. Print by default. * * @return string */ private function field_number_option_default_value( $field, $args, $echo_output = true ) { $default_value_args = [ 'slug' => 'default_value', 'label' => esc_html__( 'Default Value', 'wpforms-lite' ), 'tooltip' => esc_html__( 'Enter a default value for this field.', 'wpforms-lite' ), 'class' => $args['class'] ?? '', 'value' => $args['value'] ?? '', 'min' => $field['min'] ?? '', 'max' => $field['max'] ?? '', 'step' => $field['step'] ?? '', ]; return $this->field_number_element( $field, $default_value_args, $echo_output ); } /** * Helper function to create `step` field option markup. * * @since 1.9.4 * * @param array $field Field data and settings. * @param array $args Field preview arguments. * @param bool $echo_output Print or return the value. Print by default. * * @return string */ private function field_number_option_step( $field, $args, $echo_output = true ) { $step_args = [ 'slug' => 'step', 'label' => esc_html__( 'Increment', 'wpforms-lite' ), 'tooltip' => $args['tooltip'] ?? esc_html__( 'Determines the increment between selectable values on the field.', 'wpforms-lite' ), 'class' => $args['class'] ?? '', 'min' => 0, 'step' => 'any', 'value' => 1, ]; $min = is_numeric( $field['min'] ?? null ) ? (float) $field['min'] : null; $max = is_numeric( $field['max'] ?? null ) ? (float) $field['max'] : null; if ( ! is_null( $min ) && ! is_null( $max ) ) { $step_args['max'] = $max - $min; } return $this->field_number_element( $field, $step_args, $echo_output ); } } Forms/Fields/Traits/FileMethodsTrait.php 0000644 00000004674 15174710275 0014252 0 ustar 00 <?php namespace WPForms\Forms\Fields\Traits; /** * File methods trait. * * @since 1.9.8 */ trait FileMethodsTrait { /** * File extensions that are not allowed. * * @since 1.9.8 * * @var array */ private $denylist = [ 'ade', 'adp', 'app', 'asp', 'bas', 'bat', 'cer', 'cgi', 'chm', 'cmd', 'com', 'cpl', 'crt', 'csh', 'csr', 'dll', 'drv', 'exe', 'fxp', 'flv', 'hlp', 'hta', 'htaccess', 'htm', 'html', 'htpasswd', 'inf', 'ins', 'isp', 'jar', 'js', 'jse', 'jsp', 'ksh', 'lnk', 'mdb', 'mde', 'mdt', 'mdw', 'msc', 'msi', 'msp', 'mst', 'ops', 'pcd', 'php', 'pif', 'pl', 'prg', 'ps1', 'ps2', 'py', 'rb', 'reg', 'scr', 'sct', 'sh', 'shb', 'shs', 'sys', 'swf', 'tmp', 'torrent', 'url', 'vb', 'vbe', 'vbs', 'vbscript', 'wsc', 'wsf', 'wsf', 'wsh', 'dfxp', 'onetmp', ]; /** * Get all allowed extensions. * Check against user-entered extensions. * * @since 1.9.8 * * @return array */ protected function get_extensions(): array { // Allowed file extensions by default. $default_extensions = $this->get_default_extensions(); // Allowed file extensions. $extensions = ! empty( $this->field_data['extensions'] ) ? explode( ',', $this->field_data['extensions'] ) : $default_extensions; return wpforms_chain( $extensions ) ->map( static function ( $ext ) { return strtolower( preg_replace( '/[^A-Za-z0-9_-]/', '', $ext ) ); } ) ->array_filter() ->array_intersect( $default_extensions ) ->value(); } /** * Determine the max-allowed file size in bytes as per field options. * * @since 1.9.8 * * @return int Number of bytes allowed. */ public function max_file_size(): int { if ( ! empty( $this->field_data['max_size'] ) ) { // Strip any suffix provided (e.g., M, MB, etc.), which leaves us with the raw MB value. $max_size = preg_replace( '/[^0-9.]/', '', $this->field_data['max_size'] ); return wpforms_size_to_bytes( $max_size . 'M' ); } return (int) wpforms_max_upload( true ); } /** * Get default extensions supported by WordPress * without those that we manually denylist. * * @since 1.9.8 * * @return array */ protected function get_default_extensions(): array { return wpforms_chain( get_allowed_mime_types() ) ->array_keys() ->implode( '|' ) ->explode( '|' ) ->array_diff( $this->denylist ) ->value(); } } Forms/Fields/Traits/ContentInput.php 0000644 00000037015 15174710275 0013470 0 ustar 00 <?php namespace WPForms\Forms\Fields\Traits; use WP_Post; /** * Trait ContentInput. * * @since 1.9.4 */ trait ContentInput { /** * Translatable strings. * * @since 1.9.4 * * @var null|array Translatable strings. */ private static $translatable_strings; /** * Constructor overloader to register trait-specific hooks. * * @since 1.9.4 * * @param bool $init Pass false to allow shortcutting the whole initialization, if needed. */ public function __construct( $init = true ) { if ( ! $init ) { return; } $this->content_input_hooks(); parent::__construct( $init ); } /** * Register hooks. * * @since 1.9.4 */ private function content_input_hooks(): void { add_action( 'wpforms_builder_enqueues', [ $this, 'builder_enqueues' ] ); add_action( 'wpforms_builder_print_footer_scripts', [ $this, 'content_editor_tools_template' ] ); add_filter( 'wpforms_builder_field_option_class', [ $this, 'builder_field_option_class' ], 10, 2 ); add_filter( 'wpforms_builder_strings', [ $this, 'content_builder_strings' ], 10, 2 ); add_filter( 'editor_stylesheets', [ $this, 'editor_stylesheets' ] ); add_filter( 'media_view_strings', [ $this, 'edit_media_view_strings' ], 10, 2 ); add_filter( 'teeny_mce_buttons', [ $this, 'teeny_mce_buttons' ], 10, 2 ); } /** * Content field option. * * @since 1.9.4 * * @param array $field Field data and settings. */ private function field_option_content( array $field ): void { $value = ( isset( $field['content'] ) && ! wpforms_is_empty_string( $field['content'] ) ) ? wp_kses( $field['content'], $this->get_allowed_html_tags() ) : ''; $output = $this->field_element( 'row', $field, [ 'slug' => 'content', 'content' => $this->get_content_editor( $value, $field ), ], false ); $output .= wpforms_render( 'fields/content/action-buttons', [ 'id' => $field['id'], 'preview' => $this->get_input_string( 'preview' ), 'expand' => $this->get_input_string( 'expand' ), ], true ); printf( '<div class="wpforms-expandable-editor">%s</div><div class="wpforms-expandable-editor-clear"></div>', $output ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } /** * Add class name to the field option top element. * * @since 1.9.4 * * @param string|mixed $css_class CSS classes. * @param array $field Field data. * * @return string */ public function builder_field_option_class( $css_class, $field ): string { $css_class = (string) $css_class; return $this->type === $field['type'] ? $css_class . ' wpforms-field-has-tinymce' : $css_class; } /** * Localized strings for `content-field` JS script. * * @since 1.9.4 * * @param array|mixed $strings Localized strings. * @param array $form The form element. * * @return array * @noinspection PhpUnusedParameterInspection */ public function content_builder_strings( $strings, $form ): array { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed $strings = (array) $strings; $strings['content_field'] = [ 'collapse' => wp_strip_all_tags( $this->get_input_string( 'collapse' ) ), 'expand' => wp_strip_all_tags( $this->get_input_string( 'expand' ) ), 'editor_default_value' => wp_kses( $this->get_input_string( 'editor_default_value' ), $this->get_allowed_html_tags() ), 'content_editor_plugins' => $this->content_editor_plugins(), 'content_editor_toolbar' => $this->content_editor_toolbar(), 'content_editor_css_url' => $this->content_css_url(), 'editor_height' => $this->get_editor_height(), 'allowed_html' => array_keys( $this->get_allowed_html_tags() ), 'invalid_elements' => $this->get_invalid_elements(), 'quicktags_buttons' => $this->get_quicktags_buttons(), 'body_class' => $this->get_editor_body_class(), ]; return $this->add_supported_field_type( $strings, $this->type ); } /** * Add editor stylesheet. * * @since 1.9.4 * * @param array|mixed $stylesheets Editor stylesheets. * * @return array */ public function editor_stylesheets( $stylesheets ): array { $stylesheets = (array) $stylesheets; if ( wpforms_is_admin_page( 'builder' ) ) { $stylesheets[] = $this->content_css_url(); } return $stylesheets; } /** * Edit some media view strings to reference a form instead of a page/post. * * @since 1.9.4 * * @param array|mixed $strings List of media view strings. * @param WP_Post $post Post object. * * @return array Modified media view strings. * @noinspection SqlResolve * @noinspection PhpMissingParamTypeInspection * @noinspection PhpUnusedParameterInspection */ public function edit_media_view_strings( $strings, $post ): array { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed $strings = (array) $strings; if ( wpforms_is_admin_page( 'builder' ) ) { $strings['insertIntoPost'] = esc_html__( /** @lang text */ 'Insert into form', 'wpforms-lite' ); $strings['uploadedToThisPost'] = esc_html__( 'Uploaded to this form', 'wpforms-lite' ); } return $strings; } /** * Remove fullscreen button if this is other tinymce editor instance than content field editor. * * @since 1.9.4 * * @param array|mixed $buttons Array of editor buttons. * @param string $editor_id Editor textarea ID. * * @return array */ public function teeny_mce_buttons( $buttons, $editor_id ): array { $buttons = (array) $buttons; $is_other_editor = strpos( $editor_id, 'wpforms_panel_' ) === 0 || $editor_id === 'entry_note'; $key = array_search( 'fullscreen', $buttons, true ); if ( $is_other_editor && $key !== false ) { unset( $buttons[ $key ] ); } return $buttons; } /** * Get default content editor plugins. * * @since 1.9.4 * * @return array Plugins array. */ private function content_editor_plugins(): array { $plugins = [ 'charmap', 'colorpicker', 'hr', 'link', 'image', 'lists', 'paste', 'tabfocus', 'textcolor', 'wordpress', 'wpemoji', 'wptextpattern', 'wpeditimage', ]; /** * Get content editor plugins filter. * * @since 1.7.8 * * @param array $plugins Plugins array. */ return (array) apply_filters( 'wpforms_builder_content_input_get_content_editor_plugins', $plugins ); } /** * Get default content editor toolbar. * * @since 1.9.4 * * @return array Toolbar buttons array. */ private function content_editor_toolbar(): array { $toolbar = [ 'formatselect', 'bold', 'italic', 'underline', 'strikethrough', 'forecolor', 'link', 'bullist', 'numlist', 'blockquote', 'alignleft', 'aligncenter', 'alignright', ]; /** * Get content editor toolbar buttons filter. * * @since 1.7.8 * * @param array $toolbar Toolbar buttons array. */ return (array) apply_filters( 'wpforms_builder_content_input_get_content_editor_toolbar', $toolbar ); } /** * Enqueue wpforms-content-field script. * * @since 1.9.4 * * @param string $view Current view. * * @noinspection PhpUnusedParameterInspection */ public function builder_enqueues( $view ): void { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found $wp_min = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min'; // Enqueue editor styles explicitly. Hack for broken styles when the Content field is deleted and Settings > Confirmation editor get broken. // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion wp_enqueue_style( 'wpforms-editor-styles', includes_url( "css/editor$wp_min.css" ) ); } /** * Content editor tools template. * * @since 1.9.4 */ public function content_editor_tools_template(): void { ?> <script type="text/html" id="tmpl-wpforms-content-editor-tools"> <div id="wp-wpforms-field-{{data.optionId}}-content-editor-tools" class="wp-editor-tools hide-if-no-js"> <div id="wp-wpforms-field-{{data.optionId}}-content-media-buttons" class="wp-media-buttons"> <button type="button" id="insert-media-button" class="button insert-media add_media" data-editor="wpforms-field-{{data.optionId}}-content"> <span class="wp-media-buttons-icon"></span> <?php esc_html_e( 'Add Media', 'wpforms-lite' ); ?> </button> </div> <div class="wp-editor-tabs"> <button type="button" id="wpforms-field-{{data.optionId}}-content-tmce" class="wp-switch-editor switch-tmce" data-wp-editor-id="wpforms-field-{{data.optionId}}-content"> <?php esc_html_e( 'Visual', 'wpforms-lite' ); ?> </button> <button type="button" id="wpforms-field-{{data.optionId}}-content-html" class="wp-switch-editor switch-html" data-wp-editor-id="wpforms-field-{{data.optionId}}-content"> <?php esc_html_e( 'Text', 'wpforms-lite' ); ?> </button> </div> </div> </script> <?php } /** * Register types in JS localization to use in WPFormsContentField. * * @since 1.9.4 * * @param array $strings Localized strings. * @param string $type Field type. * * @return array */ private function add_supported_field_type( $strings, $type ): array { $other_supported_field_types = $strings['content_input']['supported_field_types'] ?? []; $strings['content_input'] = [ 'supported_field_types' => array_merge( $other_supported_field_types, [ $type ] ), ]; return $strings; } /** * Get translatable string. * * @since 1.9.4 * * @param string $key String key. * * @return string */ private function get_input_string( $key ): string { if ( ! self::$translatable_strings ) { self::$translatable_strings = [ 'editor_default_value' => __( '<h4>Add Text and Images to Your Form With Ease</h4> <p>To get started, replace this text with your own.</p>', 'wpforms-lite' ), 'expand' => __( 'Expand Editor', 'wpforms-lite' ), 'collapse' => __( 'Collapse Editor', 'wpforms-lite' ), 'preview' => __( 'Update Preview', 'wpforms-lite' ), ]; } return self::$translatable_strings[ $key ] ?? ''; } /** * Show field preview in the right builder panel. * * @since 1.9.4 * * @param array $field Field data. */ private function content_input_preview( $field ): void { $content = $field['content'] ?? $this->get_input_string( 'editor_default_value' ); ?> <div class="wpforms-field-content-preview"> <?php echo wp_kses( $this->do_caption_shortcode( wpautop( $content ) ), $this->get_allowed_html_tags() ); ?> <div class="wpforms-field-content-preview-end"></div> </div> <?php } /** * Check if shortcode is [caption] and if not, return processed content string. * * @since 1.9.4 * * @param false|string $value Short-circuit return value. Either false or the value to replace the shortcode with. * @param string $tag Shortcode name. * @param array|string $attr Shortcode attributes array or empty string. * @param array $m Regular expression match array. * * @return false|string * @noinspection PhpUnusedParameterInspection * @noinspection PhpMissingParamTypeInspection */ public function short_circuit_shortcodes( $value, $tag, $attr, $m ) { return $tag !== 'caption' ? $m[0] : false; } /** * Check if shortcode is [caption] and if not, short-circuit processing the shortcode. * * @since 1.9.4 * * @param string $content Editor content. * * @return string */ protected function do_caption_shortcode( $content ): string { /** * Check if user allowed executing all shortcodes on content field value. * * @since 1.7.8 * * @param bool $bool Boolean if shortcodes should be executed. */ if ( apply_filters( 'wpforms_content_input_value_do_shortcode', false ) && ! wpforms_is_admin_page( 'builder' ) ) { return do_shortcode( $content ); } add_filter( 'pre_do_shortcode_tag', [ $this, 'short_circuit_shortcodes' ], 10, 4 ); $content = do_shortcode( $content ); remove_filter( 'pre_do_shortcode_tag', [ $this, 'short_circuit_shortcodes' ] ); return $content; } /** * Get TinyMCE editor for content field. * * @since 1.9.4 * * @param string $value Field value. * @param array $field Field data. * * @return string */ private function get_content_editor( $value, $field ): string { /* Heads up, if you are going to edit editor settings, bear in mind editor is instantiated in two places: - PHP instance in \WPForms\Admin\Builder\Traits\ContentInput::get_content_editor - JS instance in WPForms.Admin.Builder.ContentField.initTinyMCE */ $settings = [ 'media_buttons' => true, 'drag_drop_upload' => true, 'textarea_name' => "fields[{$field['id']}][content]", 'editor_height' => $this->get_editor_height(), 'editor_class' => ! empty( $field['required'] ) ? 'wpforms-field-required' : '', 'tinymce' => [ 'init_instance_callback' => $this->is_disabled_field ? '' : 'wpformsContentFieldTinyMCECallback', 'plugins' => implode( ',', $this->content_editor_plugins() ), 'toolbar1' => implode( ',', $this->content_editor_toolbar() ), 'invalid_elements' => $this->get_invalid_elements(), 'relative_urls' => false, 'remove_script_host' => false, 'object_resizing' => false, 'body_class' => $this->get_editor_body_class(), ], 'quicktags' => [ 'buttons' => $this->get_quicktags_buttons(), ], ]; ob_start(); wp_editor( $value, 'wpforms-field-option-' . $field['id'] . '-content', $settings ); return ob_get_clean(); } /** * Get invalid HTML in content editor. * * @since 1.9.4 * * @return string Invalid HTML elements. */ private function get_invalid_elements(): string { return 'form,input,textarea,select,option,script,embed,iframe'; } /** * Get the list of the `quicktags` buttons. * * @since 1.9.4 * * @return string Quicktags buttons. */ private function get_quicktags_buttons(): string { $quicktag_buttons = [ 'strong', 'em', 'block', 'del', 'ins', 'img', 'ul', 'ol', 'li', 'code', 'link', 'close', ]; /** * Get the list of the `quicktags` buttons filter. * * @since 1.7.8 * * @param string $quicktags_buttons Comma separated list of quicktags buttons. */ return implode( ',', apply_filters( 'wpforms_builder_content_input_get_quicktags_buttons', $quicktag_buttons ) ); } /** * Get content CSS url. * * @since 1.9.4 * * @return string */ private function content_css_url(): string { $min = wpforms_get_min_suffix(); return WPFORMS_PLUGIN_URL . "assets/css/builder/content-editor{$min}.css"; } /** * Get content editor height. * * @since 1.9.4 * * @retun int Editor textarea height. */ private function get_editor_height(): int { /** * Get content editor height filter. * * @since 1.7.8 * * @param int $height Editor textarea height. */ return (int) apply_filters( 'wpforms_builder_content_input_get_editor_height', 204 ); } /** * Get allowed HTML tags for Content Input Field. * * @since 1.9.4 * * @return array */ protected function get_allowed_html_tags(): array { /** * Filter allowed HTML tags in the content field input. * * @since 1.7.8 * * @param array $allowed_tags Allowed tags. */ return (array) apply_filters( 'wpforms_builder_content_input_get_allowed_html_tags', wpforms_get_allowed_html_tags_for_richtext_field() ); } /** * Get editor body class. * * @since 1.9.4 * * @return string */ private function get_editor_body_class(): string { return 'wpforms-content-field-editor-body'; } } Forms/Fields/Traits/ProField.php 0000644 00000041213 15174710275 0012535 0 ustar 00 <?php namespace WPForms\Forms\Fields\Traits; use WPForms\Admin\Education\Helpers; /** * Trait ProField. * * Mostly educational things for the Pro field in the Lite plugin. * * @since 1.9.4 */ trait ProField { /** * Is it the Pro plugin? * * @since 1.9.4 * * @var boolean */ protected $is_pro = false; /** * Whether the field is a Pro field. * * @since 1.9.4 * * @var boolean */ protected $is_pro_field = true; /** * Addon slug. * * @since 1.9.4 * * @var string */ protected $addon_slug; /** * Whether the Addon is initialized. * * @since 1.9.4 * * @var boolean */ protected $is_addon_initialized = false; /** * Whether the field is disabled. * * @since 1.9.4 * * @var boolean */ protected $is_disabled_field = true; /** * Addon educational data. * * @since 1.9.4 * * @var array */ protected $addon_edu_data = []; /** * Init Pro Field. * * @since 1.9.4 */ private function init_pro_field(): void { $this->is_pro = wpforms()->is_pro(); $this->is_addon_initialized = ! empty( $this->addon_slug ) && wpforms_is_addon_initialized( $this->addon_slug ); $this->is_disabled_field = $this->is_disabled_field(); // Add hooks. add_filter( 'admin_init', [ $this, 'admin_init_pro_field' ] ); add_filter( 'wpforms_builder_field_option_class', [ $this, 'filter_field_option_class' ], 10, 2 ); add_filter( "wpforms_admin_builder_ajax_save_form_field_$this->type", [ $this, 'filter_save_form_field_data' ], 10, 3 ); add_filter( 'wpforms_field_data', [ $this, 'filter_frontend_field_data' ], PHP_INT_MAX, 2 ); add_filter( 'wpforms_helpers_form_pro_fields', [ $this, 'filter_form_pro_fields' ], PHP_INT_MAX, 2 ); add_filter( 'wpforms_helpers_form_addons_edu_data', [ $this, 'filter_form_addons_edu_data' ], PHP_INT_MAX, 2 ); add_filter( 'wpforms_field_preview_display_duplicate_button', [ $this, 'filter_field_preview_display_duplicate_button' ], 10, 2 ); add_filter( 'wpforms_field_preview_class', [ $this, 'filter_field_preview_class' ], 10, 2 ); add_filter( 'wpforms_entry_save_data', [ $this, 'filter_entry_save_data' ], 10, 3 ); add_filter( 'wpforms_pro_admin_entries_table_facades_columns_get_field_columns_forbidden_fields', [ $this, 'filter_field_columns_forbidden_fields' ], 10, 2 ); add_filter( 'wpforms_pro_admin_entries_export_configuration', [ $this, 'filter_entries_export_configuration' ] ); add_filter( "wpforms_pro_admin_entries_edit_is_field_displayable_$this->type", [ $this, 'filter_is_field_displayable' ], 10, 3 ); } /** * Init Pro field on the ` admin_init ` hook. * * @since 1.9.4 */ public function admin_init_pro_field(): void { $this->addon_edu_data = $this->get_field_addon_edu_data(); } /** * Get the Pro field options tab CSS class. * * @since 1.9.4 * * @param string|mixed $css_class CSS class. * @param array $field Field data. * * @return string * @noinspection PhpMissingParamTypeInspection */ public function filter_field_option_class( $css_class, $field ): string { $css_class = (string) $css_class; if ( $field['type'] !== $this->type ) { return $css_class; } $css_class .= empty( $this->is_disabled_field ) ? '' : ' wpforms-field-is-pro'; return trim( $css_class ); } /** * Filter field data before saving the form. * * @since 1.9.4 * * @param array $field_data Field data. * @param array $form_data Forms data. * @param array $saved_form_data Saved form data. * * @noinspection PhpMissingParamTypeInspection * @noinspection PhpUnusedParameterInspection */ public function filter_save_form_field_data( $field_data, $form_data, $saved_form_data ) { if ( empty( $this->is_disabled_field ) ) { return $field_data; } $field_id = $field_data['id'] ?? ''; // Prevent changes in the field data if it's a Pro field in Lite. // The settings are disabled in the Form Builder, but users can still hijack the data. // Therefore, return the saved field data if it exists. return $saved_form_data['fields'][ $field_id ] ?? $field_data; } /** * Filter form pro fields array. * * @since 1.9.4 * * @param array|mixed $pro_fields Pro fields array. * @param array $field Field data. */ public function filter_form_pro_fields( $pro_fields, array $field ): array { $pro_fields = is_array( $pro_fields ) ? $pro_fields : []; if ( isset( $field['type'] ) && $field['type'] === $this->type ) { $pro_fields[] = $field; } return $pro_fields; } /** * Filter the form addons educational data array. * * @since 1.9.4 * * @param array|mixed $addons_edu_data Addons educational data. * @param array $field Field data. */ public function filter_form_addons_edu_data( $addons_edu_data, array $field ): array { $addons_edu_data = is_array( $addons_edu_data ) ? $addons_edu_data : []; if ( ! isset( $field['type'] ) || $field['type'] !== $this->type || empty( $this->addon_edu_data ) ) { return $addons_edu_data; } $addon = $this->addon_edu_data['slug'] ?? ''; $addons_edu_data[ $addon ] = $this->addon_edu_data; return $addons_edu_data; } /** * Get the Pro field options notice. * * @since 1.9.4 * * @noinspection HtmlUnknownAttribute */ private function get_field_options_notice(): string { if ( empty( $this->is_disabled_field ) ) { return ''; } [ $name, $title, $content, $button_label, $button_utm ] = $this->get_field_options_notice_texts(); $action = $this->addon_edu_data['action'] ?? 'upgrade'; $button_class = 'education-action-button'; $button_attr = ''; if ( $action !== 'upgrade' ) { $button_class = 'education-modal'; $button_attr = sprintf( 'data-nonce="%1$s" data-path="%2$s" data-url="%3$s" data-message="%4$s" data-field-type="%5$s" data-name="%6$s"', esc_attr( wp_create_nonce( 'wpforms-admin' ) ), $this->addon_edu_data['path'] ?? '', $this->addon_edu_data['url'] ?? '', $action === 'incompatible' ? $this->addon_edu_data['message'] : '', esc_attr( $this->type ), esc_attr( $name ) ); } return sprintf( '<div class="wpforms-field-option-field-title-notice"> <div class="wpforms-alert-info wpforms-alert wpforms-educational-alert"> <h4>%1$s</h4> <p>%2$s</p> <button class="wpforms-btn wpforms-btn-sm wpforms-btn-blue %3$s" data-action="%4$s" %6$s data-license="%7$s" data-utm-content="%8$s">%5$s</button> </div> </div>', $title, esc_html( $content ), esc_attr( $button_class ), esc_attr( $action ), esc_html( $button_label ), $button_attr, esc_attr( $this->addon_edu_data['license_level'] ?? 'pro' ), esc_attr( $button_utm ) ); } /** * Get the Pro field options notice texts. * * @since 1.9.4 */ private function get_field_options_notice_texts(): array { $action = $this->addon_edu_data['action'] ?? 'upgrade'; $addon_name = $this->addon_edu_data['title'] ?? ''; $name = $this->name; $titles = [ 'upgrade' => sprintf( /* translators: %1$s - Field name. */ esc_html__( '%1$s is a Pro Feature', 'wpforms-lite' ), $name ), 'incompatible' => esc_html__( 'Incompatible Addon', 'wpforms-lite' ), ]; $contents = [ 'upgrade' => sprintf( /* translators: %1$s - Field name. */ esc_html__( 'Upgrade to gain access to the %1$s field and dozens of other powerful features to help you build smarter forms and grow your business.', 'wpforms-lite' ), $name ), 'install' => sprintf( /* translators: %1$s - Addon name. */ esc_html__( 'You have access to the %1$s, but it\'s not currently installed.', 'wpforms-lite' ), $addon_name ), 'activate' => sprintf( /* translators: %1$s - Addon name. */ esc_html__( 'You have access to the %1$s, but it\'s not currently activated.', 'wpforms-lite' ), $addon_name ), 'incompatible' => sprintf( /* translators: %1$s - Addon name. */ esc_html__( 'The %1$s is not compatible with this version of WPForms and requires an update.', 'wpforms-lite' ), $addon_name ), ]; $button_labels = [ 'upgrade' => esc_html__( 'Upgrade to Pro', 'wpforms-lite' ), 'install' => esc_html__( 'Install Addon', 'wpforms-lite' ), 'activate' => esc_html__( 'Activate Addon', 'wpforms-lite' ), 'incompatible' => esc_html__( 'Update Addon', 'wpforms-lite' ), ]; // If it's not an upgrade action, use the addon data. if ( $action !== 'upgrade' ) { $name = $addon_name; $utm_name = $this->addon_edu_data['utm_content']; } else { $edu_fields = wpforms()->obj( 'education_fields' ); $edu_field = $edu_fields ? $edu_fields->get_field( $this->type ) : null; $utm_name = $edu_field['name_en'] ?? $this->type; // Fallback to the field type. } $button_utm = sprintf( 'AI Form - %1$s notice', esc_html( $utm_name ) ); return [ $name, $titles[ $action ] ?? $titles['upgrade'], $contents[ $action ] ?? $contents['upgrade'], $button_labels[ $action ] ?? $button_labels['upgrade'], $button_utm, ]; } /** * Determine if the field is disabled. * * @since 1.9.4 * @since 1.9.9.3 The method access modifier is changed from private to protected. */ protected function is_disabled_field(): bool { // It is a Pro field in Lite OR the addon is not initialized. return ! ( $this->is_pro && ( empty( $this->addon_slug ) || $this->is_addon_initialized ) ); } /** * Get a preview option. * * @since 1.9.4 * * @param string $option Option name. * @param array $field Field data. * @param array $args Additional arguments. * @param bool $do_echo Echo or return. * * @noinspection ReturnTypeCanBeDeclaredInspection * @noinspection PhpMultipleClassDeclarationsInspection */ public function field_preview_option( $option, $field, $args = [], $do_echo = true ) { // Hide remaining elements, prevent incompatible addon field elements from being displayed. if ( $option === 'hide-remaining' && ! empty( $this->is_disabled_field ) ) { echo '<div class="wpforms-field-hide-remaining"></div>'; return; } parent::field_preview_option( $option, $field, $args, $do_echo ); } /** * Get the Pro field preview badge. * * @since 1.9.4 */ private function get_field_preview_badge(): string { if ( empty( $this->is_disabled_field ) ) { return ''; } $action = $this->addon_edu_data['action'] ?? ''; if ( $action === 'incompatible' ) { return Helpers::get_badge( esc_html__( 'Update required', 'wpforms-lite' ) , 'lg', 'inline', 'red' ); } // If it's an addon field in Pro AND the addon is not initialized, show the ADDON badge. if ( in_array( $action, [ 'install' ,'activate' ], true ) ) { return Helpers::get_badge( 'Addon', 'lg', 'inline', 'orange' ); } return Helpers::get_badge( 'Pro', 'lg', 'inline', 'green' ); } /** * Get the addon educational data of the field. * * @since 1.9.4 * * @return array */ private function get_field_addon_edu_data(): array { if ( empty( $this->addon_slug ) || ! empty( $this->is_addon_initialized ) || ! is_admin() ) { return []; } $addons = Helpers::get_edu_addons(); return $addons[ 'wpforms-' . $this->addon_slug ] ?? []; } /** * Filter frontend field data to prevent rendering Pro fields in Lite. * * @since 1.9.4 * * @param array|mixed $field Field data. * @param array $form_data Form data. * * @return array * @noinspection PhpMissingParamTypeInspection * @noinspection PhpUnusedParameterInspection */ public function filter_frontend_field_data( $field, $form_data ): array { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed $field = (array) $field; $type = $field['type'] ?? ''; // If it's not a Pro field or the field type doesn't match, return the field data as is. if ( empty( $this->is_pro_field ) || $type !== $this->type ) { return $field; } // If it's a Pro field in Lite OR, // the addon is not initialized, // return an empty array to prevent rendering. if ( ! empty( $this->is_disabled_field ) ) { return []; } return $field; } /** * Disallow the field preview "Duplicate" button. * * @since 1.9.4 * * @param bool|mixed $display Display switch. * @param array $field Field settings. * * @return bool * @noinspection PhpMissingParamTypeInspection */ public function filter_field_preview_display_duplicate_button( $display, $field ): bool { if ( $field['type'] !== $this->type || empty( $this->is_disabled_field ) ) { return (bool) $display; } return false; } /** * Add a class to the field preview container. * * @since 1.9.4 * * @param string|mixed $css_class CSS class. * @param array $field Field settings. * * @return string * @noinspection PhpMissingParamTypeInspection */ public function filter_field_preview_class( $css_class, $field ): string { $css_class = (string) $css_class; if ( $field['type'] !== $this->type || empty( $this->is_disabled_field ) ) { return $css_class; } return trim( $css_class . ' wpforms-field-is-pro' ); } /** * Filter entry save data. * * @since 1.9.5 * * @param array|mixed $fields Entry fields data. * @param array $entry Entry data. * @param array $form_data Form data. * * @return array * @noinspection PhpUnusedParameterInspection */ public function filter_entry_save_data( $fields, array $entry, array $form_data ): array { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed $fields = (array) $fields; // If it's not a disabled Pro field, return the fields as is. if ( empty( $this->is_disabled_field ) ) { return $fields; } // Remove disabled Pro fields from the entry fields. foreach ( $fields as $field_id => $field ) { if ( isset( $field['type'] ) && $field['type'] === $this->type ) { unset( $fields[ $field_id ] ); } } return $fields; } /** * Filter forbidden columns on the Form Entries page. * * @since 1.9.5 * * @param array|mixed $forbidden_fields Entry fields data. * @param int|string $form_id Form ID. * * @return array * @noinspection PhpUnusedParameterInspection * @noinspection PhpUnusedLocalVariableInspection */ public function filter_field_columns_forbidden_fields( $forbidden_fields, $form_id ): array { $forbidden_fields = (array) $forbidden_fields; if ( empty( $this->is_disabled_field ) ) { return $forbidden_fields; } $form_data = $this->get_form_data( (int) $form_id ); if ( ! $form_data ) { return $forbidden_fields; } $fields = $form_data['fields'] ?? []; foreach ( $fields as $field_id => $field ) { if ( isset( $field['type'] ) && $field['type'] === $this->type ) { $forbidden_fields[] = $field['type']; } } return $forbidden_fields; } /** * Get form data by form ID and cache it. * * @since 1.9.5 * * @param int $form_id Form ID. * * @return array */ private function get_form_data( int $form_id ): array { $form_obj = wpforms()->obj( 'form' ); if ( ! $form_obj ) { return []; } // Cache the form data into static variable. static $cached_form_data = []; if ( isset( $cached_form_data[ $form_id ] ) ) { return $cached_form_data[ $form_id ]; } $cached_form_data[ $form_id ] = (array) $form_obj->get( $form_id, [ 'content_only' => true ] ); return $cached_form_data[ $form_id ]; } /** * Filter entries export configuration. * * @since 1.9.5 * * @param array $config Export configuration. * * @return array * @noinspection PhpMissingParamTypeInspection */ public function filter_entries_export_configuration( $config ): array { $config = (array) $config; // If it's not a disabled Pro field, return the config as is. if ( empty( $this->is_disabled_field ) ) { return $config; } if ( empty( $this->type ) ) { return $config; } $config['disallowed_fields'] = ! empty( $config['disallowed_fields'] ) ? (array) $config['disallowed_fields'] : []; // Add the disabled Pro field type to `disallowed_fields` if not already there. if ( ! in_array( $this->type, $config['disallowed_fields'], true ) ) { $config['disallowed_fields'][] = $this->type; } return $config; } /** * Filter if the field is displayable in the Entry Edit page. * * @since 1.9.5 * * @param bool|mixed $displayable Whether the field is displayable. * @param array $field Field data. * @param array $form_data Form data. * * @return bool * @noinspection PhpUnusedParameterInspection */ public function filter_is_field_displayable( $displayable, array $field, array $form_data ): bool { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed if ( ! $this->is_disabled_field ) { return (bool) $displayable; } return false; } } Forms/Fields/Traits/FileDisplayTrait.php 0000644 00000022566 15174710275 0014254 0 ustar 00 <?php namespace WPForms\Forms\Fields\Traits; /** * File Entry Preview Trait. * * Contains common methods for displaying file uploads in entries and emails. * * @since 1.9.8 */ trait FileDisplayTrait { /** * Format field value for display in Entries. * * @since 1.9.8 * * @param string|mixed $val Field value. * @param array $field Field data. * @param array $form_data Form data. * @param string $context Display context. * * @return string */ public function html_field_value( $val, array $field, array $form_data = [], string $context = '' ): string { $val = (string) $val; if ( $field['type'] !== $this->type ) { return $val; } $field = $this->entry_preview_prepare_field_value( $field, $form_data, $context ); // Return early if there is no value at all. if ( empty( $field['value'] ) && empty( $field['value_raw'] ) ) { return $val; } // Process modern uploader. if ( ! empty( $field['value_raw'] ) ) { return $this->process_modern_uploader( $field, $context ); } // Process classic uploader. if ( $this->is_entry_preview( $context ) ) { return $this->entry_preview_file_link_html( $field, $this->get_file_url( $field ) ); } return $this->get_file_link_html( $field, $context ); } /** * Get file link HTML. * * @since 1.9.8 * * @param array $file File data. * @param string $context Value display context. * * @return string * @noinspection HtmlUnknownTarget */ private function get_file_link_html( array $file, string $context ): string { $html = in_array( $context, [ 'email-html', 'entry-single' ], true ) ? $this->file_icon_html( $file ) : ''; $html .= sprintf( '<a href="%s" rel="noopener noreferrer" target="_blank" style="%s">%s</a>', esc_url( $this->get_file_url( $file ) ), $context === 'email-html' ? 'padding-left:10px;' : '', esc_html( $this->get_file_name( $file ) ) ); return $html; } /** * Get the URL of a file. * * @since 1.9.8 * * @param array $file File data. * @param array $args Additional query arguments. * * @return string */ public function get_file_url( array $file, array $args = [] ): string { $file_url = $file['value'] ?? ''; if ( ! empty( $file['protection_hash'] ) ) { $args = wp_parse_args( $args, [ 'wpforms_uploaded_file' => $file['protection_hash'], ] ); $file_url = add_query_arg( $args, home_url() ); } /** * Allow modifying the URL of a file. * * @since 1.9.8 * * @param string $file_url File URL. * @param array $file File data. */ return (string) apply_filters( 'wpforms_pro_fields_file_upload_get_file_url', $file_url, $file ); } /** * Get the name of a file. * * @since 1.9.8 * * @param array $file File data. * * @return string */ public function get_file_name( array $file ): string { if ( ! $this->is_file_protected( $file ) ) { return $file['file_original']; } $ext = $file['ext'] ?? ''; return sprintf( '%s.%s', hash( 'crc32b', $file['file_original'] ), $ext ); } /** * Check if the file is protected. * * @since 1.9.8 * * @param array $file_data File data. * * @return bool True if the file is protected, false otherwise. */ private function is_file_protected( array $file_data ): bool { return ! empty( $file_data['protection_hash'] ); } /** * Get file icon HTML. * * @since 1.9.8 * * @param array $file_data File data. * * @return string * @noinspection HtmlUnknownTarget */ public function file_icon_html( array $file_data ): string { $src = esc_url( $file_data['value'] ); $ext_types = wp_get_ext_types(); if ( $this->is_file_protected( $file_data ) || ! in_array( $file_data['ext'], $ext_types['image'], true ) ) { $src = wp_mime_type_icon( wp_ext2type( $file_data['ext'] ) ?? '' ); } elseif ( ! empty( $file_data['attachment_id'] ) ) { $image = wp_get_attachment_image_src( $file_data['attachment_id'], [ 16, 16 ], true ); $src = $image ? $image[0] : $src; } return sprintf( '<span class="file-icon"><img width="16" height="16" src="%s" alt="" /></span>', esc_url( $src ) ); } /** * Prepare field value for entry preview. * * @since 1.9.9 * * @param array $field Field data. * @param array $form_data Form data. * @param string $context Display context. * * @return array */ private function entry_preview_prepare_field_value( array $field, array $form_data, string $context ): array { if ( ! empty( $field['value'] ) || ! empty( $field['value_raw'] ) || ! $this->is_entry_preview( $context ) ) { return $field; } $this->form_id = absint( $form_data['id'] ); $this->field_id = absint( $field['id'] ); $input_name = $this->get_input_name(); // Modern uploader: data (JSON) in $_POST. $raw_json = isset( $_POST[ $input_name ] ) ? sanitize_text_field( wp_unslash( $_POST[ $input_name ] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing if ( $raw_json !== '' && wpforms_is_json( $raw_json ) ) { $files = json_decode( $raw_json, true ); if ( ! empty( $files ) ) { $field['value_raw'] = array_map( static function ( $file ) { $name = $file['name'] ?? ''; return [ 'value' => $file['url'] ?? '', 'file_original' => $name, 'ext' => strtolower( pathinfo( $name, PATHINFO_EXTENSION ) ), ]; }, $files ); } return $field; } // Classic uploader: data in $_FILES. $files = $_FILES[ $input_name ]['name'] ?? []; // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $files = is_array( $files ) ? $files : [ $files ]; $files = array_filter( array_map( 'sanitize_file_name', $files ) ); if ( ! empty( $files ) ) { $field['value_raw'] = array_map( static function ( $file ) { return [ 'value' => '', // No public URL available. 'file_original' => $file, 'ext' => strtolower( pathinfo( $file, PATHINFO_EXTENSION ) ), ]; }, $files ); } return $field; } /** * Process modern uploader. * * @since 1.9.9 * * @param array $field Field data. * @param string $context Value display context. * * @return string */ private function process_modern_uploader( array $field, string $context ): string { $values = $context === 'entry-table' ? array_slice( $field['value_raw'], 0, 3, true ) : $field['value_raw']; $html = ''; $submitted_fields = ! empty( $_POST['wpforms'] ) ? stripslashes_deep( $_POST['wpforms'] ) : []; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.NonceVerification.Missing $is_entry_preview = $this->is_entry_preview( $context ); foreach ( $values as $key => $file ) { $src = $this->get_file_url( $file ); // If the temp file doesn't exist, fallback to submitted field data if set. if ( $is_entry_preview && ! file_exists( $src ) && isset( $submitted_fields['complete'][ $field['id'] ]['value_raw'][ $key ] ) ) { $file = $submitted_fields['complete'][ $field['id'] ]['value_raw'][ $key ]; $src = $this->get_file_url( $file ); } // Normalize structure ( pre-submit uses url/name; post-submit uses value/file_original ). if ( empty( $file['value'] ) && ! empty( $file['url'] ) ) { $file['value'] = $file['url']; } if ( empty( $file['file_original'] ) && ! empty( $file['name'] ) ) { $file['file_original'] = $file['name']; } if ( empty( $file['ext'] ) ) { $source = $file['file_original'] ?? ( $file['name'] ?? '' ); $file['ext'] = strtolower( pathinfo( $source, PATHINFO_EXTENSION ) ); } if ( empty( $file['file_original'] ) ) { continue; } if ( $is_entry_preview ) { $html .= $this->entry_preview_file_link_html( $file, $src ); continue; } $html .= $this->get_file_link_html( $file, $context ) . '<br/>'; } if ( count( $values ) < count( $field['value_raw'] ) ) { $html .= '…'; } return $html; } /** * Get file link HTML for entry preview. * Show image previews, non-images as plain text. * * @since 1.9.9 * * @param array $file File data. * @param string $src File source. * * @return string */ private function entry_preview_file_link_html( array $file, string $src ): string { $filename = esc_html( $this->get_file_name( $file ) ); $is_image = in_array( $file['ext'] ?? '', wp_get_ext_types()['image'], true ); // If we have a URL, display an inline thumbnail preview. if ( $is_image && ! empty( $src ) ) { return sprintf( '<span class="wpforms-entry-preview-file is-image"><img src="%1$s" alt="%2$s"/><span class="wpforms-entry-preview-filename">%2$s</span></span>', esc_url( $src ), esc_html( $filename ) ); } // Show the file name otherwise. return sprintf( '<span class="wpforms-entry-preview-file">%1$s</span>', $filename ); } /** * Check if the context is an entry preview. * * @since 1.9.9 * * @param string $context Value display context. * * @return bool True if the context is entry preview, false otherwise. */ private function is_entry_preview( string $context ): bool { return $context === 'entry-preview'; } /** * Get the input name for the field. * * @since 1.9.9 * * @return string */ public function get_input_name(): string { return sprintf( 'wpforms_%d_%d', $this->form_id, $this->field_id ); } } Forms/Fields/Traits/AccessRestrictionsTrait.php 0000644 00000033066 15174710275 0015656 0 ustar 00 <?php namespace WPForms\Forms\Fields\Traits; /** * Access restrictions trait. * * @since 1.9.8 */ trait AccessRestrictionsTrait { /** * User roles. * * @since 1.9.8 * * @var array */ private $user_roles = []; /** * Add access restrictions options to the field. * * @since 1.9.8 * * @param array $field Field data and settings. */ private function access_restrictions_options( array $field ) { $access_restrictions = $this->field_element( 'toggle', $field, [ 'slug' => 'is_restricted', 'value' => ! empty( $field['is_restricted'] ) ? 1 : '', 'desc' => esc_html__( 'Enable File Access Restrictions', 'wpforms-lite' ), 'tooltip' => esc_html__( 'Choose who can access the uploaded files.', 'wpforms-lite' ), 'class' => $this->get_access_restrictions_toggle_class(), ], false ); $this->field_element( 'row', $field, [ 'slug' => 'access_restrictions', 'attrs' => $this->get_access_restrictions_options_attrs(), 'content' => $access_restrictions, ] ); // User Restriction. $this->user_restriction_options( $field ); // Password Protection. $this->password_protection_options( $field ); } /** * Get access restrictions toggle class. * * @since 1.9.8 * * @return string */ protected function get_access_restrictions_toggle_class(): string { return 'wpforms-file-upload-access-restrictions'; } /** * Get access restrictions options attributes. * * @since 1.9.8 * * @return array */ protected function get_access_restrictions_options_attrs(): array { return []; } /** * Add user restrictions options to the field. * * @since 1.9.8 * * @param array $field Field data and settings. */ private function user_restriction_options( array $field ) { $user_restrictions_value = $this->get_user_restrictions_value( $field ); $this->add_user_restrictions_select( $field, $user_restrictions_value ); $hide_user_restrictions = $this->should_hide_user_restrictions( $user_restrictions_value, $field ); $this->add_user_roles_restrictions( $field, $hide_user_restrictions ); $this->add_user_names_restrictions( $field, $hide_user_restrictions ); } /** * Get user restrictions value. * * @since 1.9.8 * * @param array $field Field data and settings. * * @return string */ private function get_user_restrictions_value( array $field ): string { return ! empty( $field['user_restrictions'] ) ? $field['user_restrictions'] : 'none'; } /** * Add user restrictions select to the field. * * @since 1.9.8 * * @param array $field Field data and settings. * @param string $user_restrictions_value User restrictions value. */ private function add_user_restrictions_select( array $field, string $user_restrictions_value ) { $label = $this->field_element( 'label', $field, [ 'slug' => 'user_restrictions', 'value' => esc_html__( 'User Restriction', 'wpforms-lite' ), ], false ); $select = $this->field_element( 'select', $field, [ 'slug' => 'user_restrictions', 'value' => $user_restrictions_value, 'options' => [ 'none' => esc_html__( 'None', 'wpforms-lite' ), 'logged' => esc_html__( 'Logged-in Users', 'wpforms-lite' ), ], 'class' => 'wpforms-file-upload-user-restrictions', ], false ); $this->field_element( 'row', $field, [ 'slug' => 'user_restrictions', 'content' => $label . $select, 'class' => $this->is_restricted( $field ) ? '' : 'wpforms-hidden', ] ); } /** * Check if user restrictions should be hidden. * * @since 1.9.8 * * @param string $user_restrictions_value User restrictions value. * @param array $field Field data and settings. * * @return bool */ private function should_hide_user_restrictions( string $user_restrictions_value, array $field ): bool { return $user_restrictions_value === 'none' || ! $this->is_restricted( $field ); } /** * Add user roles restrictions to the field. * * @since 1.9.8 * * @param array $field Field data and settings. * @param bool $hide_user_restrictions Should user restrictions be hidden. */ private function add_user_roles_restrictions( array $field, bool $hide_user_restrictions ) { $label = $this->field_element( 'label', $field, [ 'slug' => 'user_roles_restrictions', 'value' => esc_html__( 'User Roles', 'wpforms-lite' ), 'tooltip' => esc_html__( 'Select the user roles that can access the uploaded files.', 'wpforms-lite' ), ], false ); $select = $this->field_element( 'select-multiple', $field, [ 'slug' => 'user_roles_restrictions', 'value' => $this->get_selected_roles( $field ), 'desc' => esc_html__( 'All users with selected roles will be able to access the uploaded files.', 'wpforms-lite' ), 'options' => $this->get_user_roles(), 'choicesjs' => false, 'class' => 'wpforms-file-upload-user-roles-select', ], false ); $this->field_element( 'row', $field, [ 'slug' => 'user_roles_restrictions', 'content' => $label . $select, 'class' => $hide_user_restrictions ? 'wpforms-hidden' : '', ] ); } /** * Get selected roles. * * @since 1.9.8 * * @param array $field Field data and settings. * * @return array */ private function get_selected_roles( array $field ): array { $selected_roles = ! empty( $field['user_roles_restrictions'] ) ? json_decode( $field['user_roles_restrictions'], true ) : []; array_unshift( $selected_roles, 'administrator' ); return array_unique( $selected_roles ); } /** * Get user roles. * * @since 1.9.8 * * @return array */ private function get_user_roles(): array { if ( empty( $this->user_roles ) ) { $roles = get_editable_roles(); $this->user_roles = array_map( static function ( $item ) { return $item['name']; }, $roles ); } return $this->user_roles; } /** * Add user names restrictions to the field. * * @since 1.9.8 * * @param array $field Field data and settings. * @param bool $hide_user_restrictions Should user restrictions be hidden. */ private function add_user_names_restrictions( array $field, bool $hide_user_restrictions ) { $label = $this->field_element( 'label', $field, [ 'slug' => 'user_names_restrictions', 'value' => esc_html__( 'Users', 'wpforms-lite' ), 'tooltip' => esc_html__( 'Select the users that can access the uploaded files.', 'wpforms-lite' ), ], false ); $select = $this->field_element( 'select-multiple', $field, [ 'slug' => 'user_names_restrictions', 'value' => array_map( 'intval', $this->get_user_ids( $field ) ), 'options' => $this->get_user_list( $field ), 'choicesjs' => false, 'class' => 'wpforms-file-upload-user-names-select', ], false ); $this->field_element( 'row', $field, [ 'slug' => 'user_names_restrictions', 'content' => $label . $select, 'class' => $hide_user_restrictions ? 'wpforms-hidden' : '', ] ); } /** * Get user ids. * * @since 1.9.8 * * @param array $field Field data and settings. * * @return array */ private function get_user_ids( array $field ): array { return ! empty( $field['user_names_restrictions'] ) ? json_decode( $field['user_names_restrictions'], true ) : []; } /** * Get user list. * * @since 1.9.8 * * @param array $field Field data and settings. * * @return array */ private function get_user_list( array $field ): array { $user_ids = $this->get_user_ids( $field ); return $this->get_selected_users( $user_ids ); } /** * Get selected users. * * @since 1.9.8 * * @param array $user_ids User IDs. * * @return array */ private function get_selected_users( array $user_ids ): array { $selected_users = []; if ( ! empty( $user_ids ) ) { $users = get_users( [ 'include' => $user_ids, 'fields' => [ 'ID', 'display_name' ], 'orderby' => 'include', ] ); $selected_users = wp_list_pluck( $users, 'display_name', 'ID' ); } return $selected_users; } /** * Add password protection options to the field. * * @since 1.9.8 * * @param array $field Field data and settings. */ private function password_protection_options( array $field ) { $this->add_password_toggle( $field ); $this->add_password_label( $field ); $this->add_password_fields( $field ); } /** * Add password toggle to the field. * * @since 1.9.8 * * @param array $field Field data and settings. */ private function add_password_toggle( array $field ) { $password = $this->field_element( 'toggle', $field, [ 'slug' => 'is_protected', 'value' => ! empty( $field['is_protected'] ) ? 1 : '', 'desc' => esc_html__( 'Password Protection', 'wpforms-lite' ), 'tooltip' => esc_html__( 'Check this option to password protect the uploaded files.', 'wpforms-lite' ), 'class' => 'wpforms-file-upload-password-restrictions', ], false ); $this->field_element( 'row', $field, [ 'slug' => 'password_restrictions', 'content' => $password, 'class' => $this->is_restricted( $field ) ? '' : 'wpforms-hidden', ] ); } /** * Add password label to the field. * * @since 1.9.8 * * @param array $field Field data and settings. */ private function add_password_label( array $field ) { $password_label = $this->field_element( 'label', $field, [ 'slug' => 'protection_password_label', 'value' => esc_html__( 'Password', 'wpforms-lite' ), 'tooltip' => esc_html__( 'Set a password to protect the uploaded files.', 'wpforms-lite' ), ], false ); $this->field_element( 'row', $field, [ 'slug' => 'protection_password_label', 'content' => $password_label, 'class' => $this->is_protected( $field ) ? '' : 'wpforms-hidden', ] ); } /** * Add password fields to the field. * * @since 1.9.8 * * @param array $field Field data and settings. */ private function add_password_fields( array $field ) { $password_field_row = $this->get_password_field( $field ); $password_confirm_field_row = $this->get_password_confirm_field( $field ); $password_columns = $this->field_element( 'row', $field, [ 'content' => $password_field_row . $password_confirm_field_row, 'class' => [ 'wpforms-field-options-columns', ], ], false ); $this->field_element( 'row', $field, [ 'slug' => 'protection_password_columns', 'content' => $password_columns, 'class' => $this->is_protected( $field ) ? '' : 'wpforms-hidden', ] ); } /** * Add password field to the field. * * @since 1.9.8 * * @param array $field Field data and settings. */ private function get_password_field( array $field ): string { $clean_button = $this->field_element( 'button', $field, [ 'slug' => 'password_restrictions_clean_button', 'value' => '<i class="fa fa-times-circle fa-lg"></i>', 'class' => [ 'wpforms-file-upload-password-clean', 'wpforms-hidden', ], 'data' => [ 'field-id' => $field['id'], ], 'attrs' => [ 'tabindex' => '-1', ], ], false ); $password_field = $this->field_element( 'text', $field, [ 'slug' => 'protection_password', 'value' => ! empty( $field['protection_password'] ) ? $field['protection_password'] : '', 'after' => esc_html__( 'Enter Password', 'wpforms-lite' ), 'type' => 'password', 'class' => 'wpforms-file-upload-password', 'attrs' => [ 'autocomplete' => 'new-password', ], ], false ); return $this->field_element( 'row', $field, [ 'slug' => 'protection_password', 'content' => $password_field . $clean_button, ], false ); } /** * Add password confirm field to the field. * * @since 1.9.8 * * @param array $field Field data and settings. */ private function get_password_confirm_field( array $field ): string { $password_confirm_field = $this->field_element( 'text', $field, [ 'slug' => 'protection_password_confirm', 'value' => ! empty( $field['protection_password_confirm'] ) ? $field['protection_password_confirm'] : '', 'after' => esc_html__( 'Confirm Password', 'wpforms-lite' ), 'type' => 'password', 'class' => 'wpforms-file-upload-password-confirm', ], false ); $password_confirm_field_error = $this->field_element( 'row', $field, [ 'slug' => 'protection_password_confirm_error', 'content' => esc_html__( 'Passwords do not match', 'wpforms-lite' ), 'class' => [ 'wpforms-hidden', 'wpforms-error', 'wpforms-error-message', ], ], false ); return $this->field_element( 'row', $field, [ 'slug' => 'protection_password_confirm', 'content' => $password_confirm_field . $password_confirm_field_error, ], false ); } /** * Check if the field has access restrictions enabled. * * @since 1.9.8 * * @param array $field Field data and settings. * * @return bool True if the field has access restrictions enabled, false otherwise. */ private function is_restricted( array $field ): bool { return ! empty( $field['is_restricted'] ); } /** * Check if the field has password protection enabled. * * @since 1.9.8 * * @param array $field Field data and settings. * * @return bool True if the field has password protection enabled, false otherwise. */ private function is_protected( array $field ): bool { return ! empty( $field['is_protected'] ); } } Forms/Fields/Traits/ReadOnlyField.php 0000644 00000007176 15174710275 0013524 0 ustar 00 <?php namespace WPForms\Forms\Fields\Traits; /** * Trait ReadOnlyField. * * Methods for read-only fields. * * @since 1.9.8 */ trait ReadOnlyField { /** * Whether the Read-Only option is allowed. * * @since 1.9.8 * * @var bool */ protected $allow_read_only = true; /** * Init Read-Only field functionality. * * @since 1.9.8 */ public function read_only_init(): void { // Read-only field hooks. add_action( 'wpforms_field_options_bottom_advanced-options', [ $this, 'field_option_read_only_toggle' ], -10 ); add_filter( 'wpforms_field_properties', [ $this, 'read_only_field_properties' ], 100, 3 ); add_filter( "wpforms_admin_builder_ajax_save_form_field_{$this->type}", [ $this, 'read_only_save_form_field' ], 100, 3 ); add_filter( 'wpforms_frontend_strings', [ $this, 'read_only_frontend_strings' ] ); } /** * Display the Read-Only toggle on the Advanced Options tab. * * @since 1.9.8 * * @param array $field Field data. * * @return void */ public function field_option_read_only_toggle( array $field ): void { if ( $field['type'] !== $this->type || ! $this->allow_read_only ) { return; } $value = $field['read_only'] ?? '0'; $tooltip = esc_html__( 'Check this option to show the field’s value without allowing changes. It will still be submitted.', 'wpforms-lite' ); $output = $this->field_element( 'toggle', $field, [ 'slug' => 'read_only', 'value' => $value, 'desc' => esc_html__( 'Read-Only', 'wpforms-lite' ), 'tooltip' => $tooltip, ], false ); $this->field_element( 'row', $field, [ 'slug' => 'read_only', 'content' => $output, ] ); } /** * Add a Read-Only field CSS class. * * @since 1.9.8 * * @param array|mixed $properties Field properties. * @param array $field Field data and settings. * @param array $form_data Form data and settings. * * @return array * @noinspection PhpUnusedParameterInspection */ public function read_only_field_properties( $properties, array $field, array $form_data ): array { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed $properties = (array) $properties; if ( $field['type'] !== $this->type || ! $this->allow_read_only || empty( $field['read_only'] ) ) { return $properties; } $properties['container']['class'][] = 'wpforms-field-readonly'; return $properties; } /** * Filter field data before saving the form. * * @since 1.9.8 * * @param array $field_data Field data. * @param array $form_data Forms data. * @param array $saved_form_data Saved form data. * * @noinspection PhpMissingParamTypeInspection * @noinspection PhpUnusedParameterInspection */ public function read_only_save_form_field( $field_data, array $form_data, array $saved_form_data ): array { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed $field_data = (array) $field_data; // Unset the `required` field option if the field is Read-Only. if ( ! empty( $field_data['read_only'] ) ) { unset( $field_data['required'] ); } return $field_data; } /** * Add read-only related strings to the frontend. * * @since 1.9.8 * * @param array|mixed $strings Frontend strings. * * @return array Frontend strings. */ public function read_only_frontend_strings( $strings ): array { $strings = (array) $strings; $strings['readOnlyDisallowedFields'] = $strings['readOnlyDisallowedFields'] ?? []; if ( $this->allow_read_only ) { return $strings; } $strings['readOnlyDisallowedFields'][] = $this->type; return $strings; } } Forms/Fields/Traits/MultiFieldMenu.php 0000644 00000002504 15174710275 0013714 0 ustar 00 <?php namespace WPForms\Forms\Fields\Traits; /** * Trait MultiFieldMenu. * * Methods for multi-field menu functionality. * * @since 1.9.9 */ trait MultiFieldMenu { /** * Generate multi-field actions menu HTML. * * @since 1.9.9 * * @return string Multi-field menu HTML. */ public function get_multi_field_menu_html(): string { $items = [ 'duplicate-multi' => [ 'icon' => 'fa-files-o', 'label' => __( 'Duplicate Fields', 'wpforms-lite' ), ], 'delete-multi' => [ 'icon' => 'fa-trash-o', 'label' => __( 'Delete Fields', 'wpforms-lite' ), 'last' => true, ], ]; $divider = '<li class="wpforms-context-menu-list-divider"></li>'; $html = '<div class="wpforms-field-multi-field-menu">'; $html .= '<ul class="wpforms-context-menu-list">'; foreach ( $items as $action => $item ) { $html .= sprintf( '<li class="wpforms-context-menu-list-item" data-action="%1$s"> <span class="wpforms-context-menu-list-item-icon"> <i class="fa %2$s" aria-hidden="true"></i> </span> <span class="wpforms-context-menu-list-item-text">%3$s</span> </li> %4$s', esc_attr( $action ), esc_attr( $item['icon'] ), esc_html( $item['label'] ), empty( $item['last'] ) ? $divider : '' ); } $html .= '</ul>'; $html .= '</div>'; return $html; } } Forms/Fields/Traits/CameraTrait.php 0000644 00000025711 15174710275 0013232 0 ustar 00 <?php namespace WPForms\Forms\Fields\Traits; trait CameraTrait { /** * Add camera options to the field. * * @since 1.9.8 * * @param array $field Field data and settings. */ public function camera_options( array $field ): void { $this->add_camera_enabled_toggle( $field ); $this->add_camera_format_options( $field ); $this->add_camera_aspect_ratio_options( $field ); $this->add_camera_custom_ratio_options( $field ); $this->add_camera_time_limit_options( $field ); } /** * Add camera-enabled toggle. * * @since 1.9.8 * * @param array $field Field data and settings. */ private function add_camera_enabled_toggle( array $field ): void { // Check if this is a Camera field (not FileUpload with camera options). $is_camera_field = $this->type === 'camera'; $camera_enabled = $this->field_element( 'toggle', $field, [ 'slug' => 'camera_enabled', 'value' => $this->is_camera_enabled_for_field( $field ) ? 1 : '', 'desc' => esc_html__( 'Enable Camera', 'wpforms-lite' ), 'tooltip' => esc_html__( 'Check this option to enable the camera field.', 'wpforms-lite' ), 'class' => 'wpforms-file-upload-camera-enabled-toggle', ], false ); // Hide the toggle for the Camera field, show for FileUpload field. $row_class = [ 'wpforms-file-upload-camera-enabled-row' ]; if ( $is_camera_field ) { $row_class[] = 'wpforms-hidden'; } $this->field_element( 'row', $field, [ 'slug' => 'camera', 'content' => $camera_enabled, 'class' => $row_class, ] ); } /** * Add camera format options. * * @since 1.9.8 * * @param array $field Field data and settings. */ private function add_camera_format_options( array $field ): void { $format_label = $this->field_element( 'label', $field, [ 'slug' => 'camera_format', 'value' => esc_html__( 'Format', 'wpforms-lite' ), 'tooltip' => esc_html__( 'Select the camera format.', 'wpforms-lite' ), 'class' => 'wpforms-file-upload-camera-format-label', ], false ); $format_select = $this->field_element( 'select', $field, [ 'slug' => 'camera_format', 'value' => ! empty( $field['camera_format'] ) ? $field['camera_format'] : 'photo', 'options' => [ 'photo' => esc_html__( 'Photo', 'wpforms-lite' ), 'video' => esc_html__( 'Video', 'wpforms-lite' ), ], 'class' => 'wpforms-file-upload-camera-format-select', ], false ); // Check if the camera is enabled to determine visibility. $hidden_class = $this->is_camera_enabled_for_field( $field ) ? [] : [ 'wpforms-hidden' ]; $this->field_element( 'row', $field, [ 'slug' => 'camera_format', 'content' => $format_label . $format_select, 'class' => array_merge( [ 'wpforms-file-upload-camera-format' ], $hidden_class ), ] ); } /** * Add camera aspect ratio options. * * @since 1.9.8 * * @param array $field Field data and settings. */ private function add_camera_aspect_ratio_options( array $field ): void { $aspect_ratio_label = $this->field_element( 'label', $field, [ 'slug' => 'camera_aspect_ratio', 'value' => esc_html__( 'Aspect Ratio', 'wpforms-lite' ), 'tooltip' => esc_html__( 'Select the camera aspect ratio.', 'wpforms-lite' ), 'class' => 'wpforms-file-upload-camera-aspect-ratio-label', ], false ); // Build aspect ratio options - always include freeform. $aspect_ratio_options = [ 'original' => esc_html__( 'Original', 'wpforms-lite' ), 'custom' => esc_html__( 'Custom', 'wpforms-lite' ), 'freeform' => esc_html__( 'Freeform', 'wpforms-lite' ), 'landscape' => [ 'optgroup' => esc_html__( 'Landscape orientation', 'wpforms-lite' ), '16:9' => esc_html__( '16:9', 'wpforms-lite' ), '5:4' => esc_html__( '5:4', 'wpforms-lite' ), '3:2' => esc_html__( '3:2', 'wpforms-lite' ), ], 'portrait' => [ 'optgroup' => esc_html__( 'Portrait orientation', 'wpforms-lite' ), '9:16' => esc_html__( '9:16', 'wpforms-lite' ), '4:5' => esc_html__( '4:5', 'wpforms-lite' ), '2:3' => esc_html__( '2:3', 'wpforms-lite' ), ], ]; // Add class to hide freeform if a format is not a photo. $camera_format = ! empty( $field['camera_format'] ) ? $field['camera_format'] : 'photo'; $aspect_ratio_class = [ 'wpforms-file-upload-camera-aspect-ratio-select' ]; if ( $camera_format !== 'photo' ) { $aspect_ratio_class[] = 'wpforms-file-upload-camera-aspect-ratio-no-freeform'; } $aspect_ratio_select = $this->field_element( 'select', $field, [ 'slug' => 'camera_aspect_ratio', 'value' => ! empty( $field['camera_aspect_ratio'] ) ? $field['camera_aspect_ratio'] : 'original', 'options' => $aspect_ratio_options, 'class' => $aspect_ratio_class, ], false ); // Check if the camera is enabled to determine visibility. $hidden_class = $this->is_camera_enabled_for_field( $field ) ? [] : [ 'wpforms-hidden' ]; $this->field_element( 'row', $field, [ 'slug' => 'camera_aspect_ratio', 'content' => $aspect_ratio_label . $aspect_ratio_select, 'class' => array_merge( [ 'wpforms-file-upload-camera-aspect-ratio' ], $hidden_class ), ] ); } /** * Add camera custom ratio options. * * @since 1.9.8 * * @param array $field Field data and settings. */ private function add_camera_custom_ratio_options( array $field ): void { // Check if an aspect ratio is custom to determine visibility. $camera_aspect_ratio = ! empty( $field['camera_aspect_ratio'] ) ? $field['camera_aspect_ratio'] : 'original'; $custom_ratio_hidden_class = ( $this->is_camera_enabled_for_field( $field ) && $camera_aspect_ratio === 'custom' ) ? [] : [ 'wpforms-hidden' ]; // Ratio Width field. $ratio_width_field = '<div class="wpforms-file-upload-camera-ratio-width">' . $this->field_element( 'text', $field, [ 'slug' => 'camera_ratio_width', 'type' => 'number', 'value' => ! empty( $field['camera_ratio_width'] ) && $field['camera_ratio_width'] >= 1 ? $field['camera_ratio_width'] : '4', 'attrs' => [ 'min' => 1, 'step' => 1, ], 'after' => esc_html__( 'Ratio Width', 'wpforms-lite' ), 'class' => 'wpforms-file-upload-camera-ratio-width-input', ], false ) . '</div>'; // Ratio Height field. $ratio_height_field = '<div class="wpforms-file-upload-camera-ratio-height">' . $this->field_element( 'text', $field, [ 'slug' => 'camera_ratio_height', 'type' => 'number', 'value' => ! empty( $field['camera_ratio_height'] ) && $field['camera_ratio_height'] >= 1 ? $field['camera_ratio_height'] : '3', 'attrs' => [ 'min' => 1, 'step' => 1, ], 'after' => esc_html__( 'Ratio Height', 'wpforms-lite' ), 'class' => 'wpforms-file-upload-camera-ratio-height-input', ], false ) . '</div>'; $this->field_element( 'row', $field, [ 'slug' => 'camera_custom_ratio', 'content' => '<div class="wpforms-field-option-row-columns wpforms-field-option-row-columns-2 wpforms-file-upload-camera-ratio-columns">' . $ratio_width_field . $ratio_height_field . '</div>', 'class' => array_merge( [ 'wpforms-file-upload-camera-custom-ratio' ], $custom_ratio_hidden_class ), ] ); } /** * Add camera time limit options. * * @since 1.9.8 * * @param array $field Field data and settings. */ private function add_camera_time_limit_options( array $field ): void { $time_limit_label = $this->field_element( 'label', $field, [ 'slug' => 'camera_time_limit', 'value' => esc_html__( 'Time Limit', 'wpforms-lite' ), 'tooltip' => esc_html__( 'Set the time limit for camera recording.', 'wpforms-lite' ), 'class' => 'wpforms-file-upload-camera-time-limit-label', ], false ); // Minutes field. $minutes_field = '<div class="wpforms-file-upload-camera-time-limit-minutes">' . $this->field_element( 'text', $field, [ 'slug' => 'camera_time_limit_minutes', 'type' => 'number', 'value' => ! empty( $field['camera_time_limit_minutes'] ) && $field['camera_time_limit_minutes'] >= 0 ? $field['camera_time_limit_minutes'] : '1', 'attrs' => [ 'min' => 0, 'step' => 1, ], 'after' => esc_html__( 'Minutes', 'wpforms-lite' ), 'class' => 'wpforms-file-upload-camera-time-limit-minutes-input', ], false ) . '</div>'; // Seconds field. $seconds_field = '<div class="wpforms-file-upload-camera-time-limit-seconds">' . $this->field_element( 'text', $field, [ 'slug' => 'camera_time_limit_seconds', 'type' => 'number', 'value' => ! empty( $field['camera_time_limit_seconds'] ) && $field['camera_time_limit_seconds'] >= 0 && $field['camera_time_limit_seconds'] <= 59 ? $field['camera_time_limit_seconds'] : '30', 'attrs' => [ 'min' => 0, 'max' => 59, 'step' => 1, ], 'after' => esc_html__( 'Seconds', 'wpforms-lite' ), 'class' => 'wpforms-file-upload-camera-time-limit-seconds-input', ], false ) . '</div>'; // Check if a format is video to determine time limit visibility. $camera_format = ! empty( $field['camera_format'] ) ? $field['camera_format'] : 'photo'; $time_limit_hidden_class = ( $this->is_camera_enabled_for_field( $field ) && $camera_format === 'video' ) ? [] : [ 'wpforms-hidden' ]; $this->field_element( 'row', $field, [ 'slug' => 'camera_time_limit', 'content' => $time_limit_label . '<div class="wpforms-field-option-row-columns wpforms-field-option-row-columns-2 wpforms-file-upload-camera-time-limit-columns">' . $minutes_field . $seconds_field . '</div>', 'class' => array_merge( [ 'wpforms-file-upload-camera-time-limit' ], $time_limit_hidden_class ), ] ); } /** * Whether the provided form has a camera field. * * @since 1.9.8 * * @param array|mixed $form Form data. */ protected function is_camera_enabled( $form ): bool { if ( empty( $form['fields'] ) ) { return false; } foreach ( $form['fields'] as $field ) { if ( ! empty( $field['camera_enabled'] ) ) { return true; } } return false; } /** * Whether the field is a camera field or has camera enabled. * * @since 1.9.8 * * @param array $field Field data and settings. */ private function is_camera_enabled_for_field( array $field ): bool { return $this->type === 'camera' || ! empty( $field['camera_enabled'] ); } /** * Get the camera time limit in seconds. * * @since 1.9.8 * * @param array $field Field data. * * @return int Camera time limit in seconds. */ public function get_camera_time_limit( array $field ): int { $field = wp_parse_args( $field, [ 'camera_enabled' => false, 'camera_format' => '', 'camera_time_limit_minutes' => 0, 'camera_time_limit_seconds' => 0, ] ); if ( empty( $field['camera_enabled'] ) || $field['camera_format'] !== 'video' ) { return 0; } return absint( $field['camera_time_limit_minutes'] ) * 60 + absint( $field['camera_time_limit_seconds'] ); } } Forms/Fields/Traits/FileEntriesEditTrait.php 0000644 00000012275 15174710275 0015062 0 ustar 00 <?php namespace WPForms\Forms\Fields\Traits; /** * File Entries Edit Trait. * * Contains common methods for editing file upload entries. * * @since 1.9.8 */ trait FileEntriesEditTrait { /** * Enqueues for the Edit Entry page. * * @since 1.9.8 * * @noinspection ReturnTypeCanBeDeclaredInspection */ public function enqueues() { wp_enqueue_style( 'tooltipster', WPFORMS_PLUGIN_URL . 'assets/lib/jquery.tooltipster/jquery.tooltipster.min.css', null, '4.2.6' ); wp_enqueue_script( 'tooltipster', WPFORMS_PLUGIN_URL . 'assets/lib/jquery.tooltipster/jquery.tooltipster.min.js', [ 'jquery' ], '4.2.6', true ); } /** * Display the field on the Edit Entry page. * * @since 1.9.8 * * @param array $entry_field Entry field data. * @param array $field Field data and settings. * @param array $form_data Form data and settings. * * @noinspection ReturnTypeCanBeDeclaredInspection */ public function field_display( $entry_field, $field, $form_data ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed $html = ''; $is_media_file = isset( $field['media_library'] ); if ( method_exists( $this->field_object, 'is_modern_upload' ) && $this->field_object::is_modern_upload( $entry_field ) ) { // Check if there are any files in value_raw. if ( ! empty( $entry_field['value_raw'] ) && is_array( $entry_field['value_raw'] ) ) { foreach ( $entry_field['value_raw'] as $key => $field_data ) { $html .= $this->get_file_item_html( $field_data, $is_media_file, $key ); } } } else { $html .= $this->get_file_item_html( $entry_field, $is_media_file ); } echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } /** * Get HTML for the file item. * * @since 1.9.8 * * @param array $field_data Field data. * @param bool $is_media_file Is WP media. * @param int|string $key Key for multiple items. * * @return string * @noinspection HtmlUnknownTarget */ private function get_file_item_html( array $field_data, bool $is_media_file, $key = 0 ): string { $html = '<div class="file-entry">'; $html .= $this->field_object->file_icon_html( $field_data ); $html .= sprintf( '<a href="%s" target="_blank" rel="noopener noreferrer">%s</a>', esc_url( $this->field_object->get_file_url( $field_data ) ), esc_html( $this->field_object->get_file_name( $field_data ) ) ); $html .= sprintf( '<input type="hidden" name="wpforms[fields][%d][]" value="%s"/>', esc_attr( $field_data['id'] ), esc_attr( $key ) ); if ( $is_media_file ) { $title = sprintf( wp_kses( /* translators: %s - link to the Media Library. */ __( 'Please use the default <a href="%s">WordPress Media</a> interface to remove this file.', 'wpforms-lite' ), [ 'a' => [ 'href' => [], ], ] ), esc_url( admin_url( 'upload.php' ) ) ); $html .= sprintf( '<i class="fa fa-question-circle wpforms-help-tooltip" title="%s"></i>', esc_html( $title ) ); } else { $html .= $this->remove_button_html(); } $html .= '</div>'; return $html; } /** * Get the remove button HTML. * * @since 1.9.8 * * @return string */ private function remove_button_html(): string { return '<a class="delete button-link-delete" href=""><span class="dashicons dashicons-trash wpforms-trash-icon"></span></a>'; } /** * Format and sanitize a field while processing entry editing. * * @since 1.9.8 * * @param int $field_id Field ID. * @param mixed $field_submit Field value that was submitted. * @param mixed $field_data Existing field data. * @param array $form_data Form data and settings. * * @noinspection ReturnTypeCanBeDeclaredInspection*/ public function format( $field_id, $field_submit, $field_data, $form_data ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed if ( method_exists( $this->field_object, 'is_modern_upload' ) && ! $this->field_object::is_modern_upload( $field_data ) ) { if ( ! is_array( $field_submit ) ) { $field_data['value'] = ''; $field_data['file_original'] = ''; $field_data['ext'] = ''; } wpforms()->obj( 'process' )->fields[ $field_id ] = $field_data; return; } if ( ! isset( $field_data['value_raw'] ) || ! is_array( $field_submit ) ) { $field_data['value_raw'] = ''; $field_data['value'] = ''; wpforms()->obj( 'process' )->fields[ $field_id ] = $field_data; return; } $field_data['value_raw'] = array_intersect_key( $field_data['value_raw'], array_combine( $field_submit, $field_submit ) ); $field_data['value'] = implode( "\n", array_column( $field_data['value_raw'], 'value' ) ); wpforms()->obj( 'process' )->fields[ $field_id ] = $field_data; } /** * Skip validation. * * @since 1.9.8 * * @param int $field_id Field ID. * @param mixed $field_submit Field value that was submitted. * @param mixed $field_data Existing field data. * @param array $form_data Form data and settings. * * @noinspection ReturnTypeCanBeDeclaredInspection*/ public function validate( $field_id, $field_submit, $field_data, $form_data ) { } } Forms/Fields/Helpers/RequirementsAlerts.php 0000644 00000015263 15174710275 0015031 0 ustar 00 <?php namespace WPForms\Forms\Fields\Helpers; /** * Helpers for Requirements Alerts. * * Can be used for notifying about new features that addons are not supported. * * @since 1.8.7 */ class RequirementsAlerts { /** * Determine if the Product Quantities feature is allowed to use. * * @since 1.8.7 * * @return bool */ public static function is_product_quantities_allowed(): bool { return empty( self::get_addons_require_for_product_quantities() ); } /** * Determine if the Order Summary feature is allowed to use. * * @since 1.8.7 * * @return bool */ public static function is_order_summary_allowed(): bool { return ! self::is_pro() || ! defined( 'WPFORMS_COUPONS_VERSION' ) || version_compare( WPFORMS_COUPONS_VERSION, '1.2.0', '>=' ); } /** * Product Quantities feature: get an update required alert HTML. * * @since 1.8.7 * * @return string */ public static function get_product_quantities_alert(): string { $addons_require_update = self::get_addons_require_for_product_quantities(); // Generate update link when only one addon needs to be updated. if ( count( $addons_require_update ) === 1 ) { $update_url = self::get_addon_update_url( key( $addons_require_update ) ); } else { // Redirect to the Plugins admin page if multiple addons require an update. $update_url = admin_url( 'plugins.php?plugin_status=upgrade' ); } return self::get_update_alert( sprintf( /* translators: %1$s - addons list. */ __( 'The following addons require an update to support product quantities: %1$s', 'wpforms-lite' ), implode( ', ', $addons_require_update ) ), $update_url ); } /** * Order Summary feature: get an update required alert HTML. * * @since 1.8.7 * * @return string */ public static function get_order_summary_alert(): string { return self::get_update_alert( __( 'You\'re using an older version of the Coupons addon that does not support order summary.', 'wpforms-lite' ), self::get_addon_update_url( 'wpforms-coupons' ) ); } /** * Repeater field: determine if addon is allowed to use inside the repeater field. * * @since 1.8.9 * * @param string $addon_slug Addon slug. * * @return bool */ public static function is_inside_repeater_allowed( string $addon_slug ): bool { $requirements = [ 'wpforms-geolocation' => '2.10.0', 'wpforms-signatures' => '1.11.0', 'wpforms-form-abandonment' => '1.12.0', 'wpforms-save-resume' => '1.11.0', 'wpforms-lead-forms' => '1000', // @todo: We should adjust this value when the Lead Forms get the Repeater field support. 'wpforms-google-sheets' => '2.1.0', ]; if ( ! isset( $requirements[ $addon_slug ] ) ) { return true; } $version_constant = strtoupper( str_replace( '-', '_', $addon_slug ) ) . '_VERSION'; return self::is_pro() && defined( $version_constant ) && version_compare( constant( $version_constant ), $requirements[ $addon_slug ], '>=' ); } /** * Repeater field: get an update required alert HTML. * * @since 1.8.9 * * @param string $addon_name Addon name. * @param string $addon_slug Addon slug. * * @return string */ public static function get_repeater_alert( string $addon_name, string $addon_slug ): string { return self::get_update_alert( self::get_repeater_alert_text( $addon_name ), self::get_addon_update_url( $addon_slug ) ); } /** * Repeater field: get alert text. * * @since 1.8.9 * * @param string $addon_name Addon name. * * @return string */ public static function get_repeater_alert_text( string $addon_name ): string { return sprintf( /* translators: %1$s - addon name. */ __( 'You\'re using an older version of the %1$s addon that does not support the Repeater field.', 'wpforms-lite' ), $addon_name ); } /** * Retrieve a list of addons that require updating to support the Product Quantities feature. * * @since 1.8.7 * * @return array */ private static function get_addons_require_for_product_quantities(): array { static $addons; if ( ! is_null( $addons ) ) { return $addons; } $addons = []; // All addons require Pro and Top level licenses. if ( ! self::is_pro() ) { return $addons; } if ( defined( 'WPFORMS_COUPONS_VERSION' ) && version_compare( WPFORMS_COUPONS_VERSION, '1.2.0', '<' ) ) { $addons['wpforms-coupons'] = __( 'Coupons', 'wpforms-lite' ); } if ( defined( 'WPFORMS_PAYPAL_COMMERCE_VERSION' ) && version_compare( WPFORMS_PAYPAL_COMMERCE_VERSION, '1.9.0', '<' ) ) { $addons['wpforms-paypal-commerce'] = __( 'PayPal Commerce', 'wpforms-lite' ); } if ( defined( 'WPFORMS_PAYPAL_STANDARD_VERSION' ) && version_compare( WPFORMS_PAYPAL_STANDARD_VERSION, '1.10.0', '<' ) ) { $addons['wpforms-paypal-standard'] = __( 'PayPal Standard', 'wpforms-lite' ); } if ( defined( 'WPFORMS_SQUARE_VERSION' ) && version_compare( WPFORMS_SQUARE_VERSION, '1.9.0', '<' ) ) { $addons['wpforms-square'] = __( 'Square', 'wpforms-lite' ); } if ( defined( 'WPFORMS_SAVE_RESUME_VERSION' ) && version_compare( WPFORMS_SAVE_RESUME_VERSION, '1.9.0', '<' ) ) { $addons['wpforms-save-resume'] = __( 'Save and Resume', 'wpforms-lite' ); } return $addons; } /** * Get an update alert HTML. * * @since 1.8.7 * * @param string $message Alert message. * @param string $update_url Update button URL. * * @return string */ private static function get_update_alert( string $message, string $update_url ): string { $alert = sprintf( '<div class="wpforms-alert-message"> <h4>%1$s</h4> <p>%2$s</p> </div> <div class="wpforms-alert-buttons"> <a href="%3$s" target="_blank" rel="noopener noreferrer" class="wpforms-btn wpforms-btn-sm wpforms-btn-blue">%4$s</a> </div>', esc_html__( 'Update Required', 'wpforms-lite' ), esc_html( $message ), esc_url( $update_url ), esc_html__( 'Update Now', 'wpforms-lite' ) ); return sprintf( '<div class="wpforms-alert wpforms-alert-danger wpforms-alert-field-requirements">%1$s</div>', $alert // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ); } /** * Get addon update URL. * * @since 1.8.7 * * @param string $addon_slug Addon slug. * * @return string */ private static function get_addon_update_url( string $addon_slug ): string { $addon_path = sprintf( '%1$s/%1$s.php', $addon_slug ); return wp_nonce_url( self_admin_url( 'update.php?action=upgrade-plugin&plugin=' . $addon_path ), 'upgrade-plugin_' . $addon_path ); } /** * Determine if Pro or Top level license is used. * * @since 1.8.7 * * @return bool */ private static function is_pro(): bool { return in_array( wpforms_get_license_type(), [ 'pro', 'elite', 'agency', 'ultimate' ], true ); } } Forms/Honeypot.php 0000644 00000003374 15174710275 0010170 0 ustar 00 <?php namespace WPForms\Forms; /** * Class Honeypot. * * @since 1.6.2 */ class Honeypot { /** * Initialise the actions for the Honeypot. * * @since 1.6.2 */ public function init() { $this->hooks(); } /** * Register hooks. * * @since 1.6.2 */ public function hooks() { add_action( 'wpforms_frontend_output', [ $this, 'render' ], 15, 5 ); } /** * Return function to render the honeypot. * * @since 1.6.2 * * @param array $form_data Form data and settings. */ public function render( $form_data ) { if ( empty( $form_data['settings']['honeypot'] ) || '1' !== $form_data['settings']['honeypot'] ) { return; } $names = [ 'Name', 'Phone', 'Comment', 'Message', 'Email', 'Website' ]; echo '<div class="wpforms-field wpforms-field-hp">'; echo '<label for="wpforms-' . $form_data['id'] . '-field-hp" class="wpforms-field-label">' . $names[ array_rand( $names ) ] . '</label>'; // phpcs:ignore echo '<input type="text" name="wpforms[hp]" id="wpforms-' . $form_data['id'] . '-field-hp" class="wpforms-field-medium">'; // phpcs:ignore echo '</div>'; } /** * Validate honeypot. * * @since 1.6.2 * * @param array $form_data Form data. * @param array $fields Fields. * @param array $entry Form entry. * * @return bool|string False or an string with the error. */ public function validate( array $form_data, array $fields, array $entry ) { $honeypot = false; if ( ! empty( $form_data['settings']['honeypot'] ) && '1' === $form_data['settings']['honeypot'] && ! empty( $entry['hp'] ) ) { $honeypot = esc_html__( 'WPForms honeypot field triggered.', 'wpforms-lite' ); } return apply_filters( 'wpforms_process_honeypot', $honeypot, $fields, $entry, $form_data ); } } Forms/AntiSpam.php 0000644 00000023304 15174710275 0010072 0 ustar 00 <?php namespace WPForms\Forms; /** * Class Anti-Spam v3. * * This class is used for modern Anti-Spam approach. * * @since 1.9.0 */ class AntiSpam { /** * Field ID to insert the honeypot field before. * * @since 1.9.0 * * @var int */ private $insert_before_field_id = 1; /** * Array with IDs of all honeypot fields on the current page grouped by form IDs ([form_id => field_id]). * * @since 1.9.0.3 * * @var array */ private $forms_data = []; /** * Initialise the actions for the modern Anti-Spam. * * @since 1.9.0 */ public function init() { $this->hooks(); } /** * Register hooks. * * @since 1.9.0 */ private function hooks() { // Frontend hooks. add_filter( 'wpforms_frontend_strings', [ $this, 'add_frontend_strings' ] ); add_filter( 'wpforms_frontend_fields_base_level', [ $this, 'get_random_field' ], 20 ); add_action( 'wpforms_display_field_before', [ $this, 'maybe_insert_honeypot_field' ], 1, 2 ); add_action( 'wpforms_display_fields_after', [ $this, 'maybe_insert_honeypot_init_js' ] ); // Builder hooks. add_filter( 'wpforms_builder_panel_settings_init_form_data', [ $this, 'init_builder_settings_form_data' ] ); add_filter( 'wpforms_admin_builder_templates_apply_to_new_form_modify_data', [ $this, 'update_template_form_data' ] ); add_filter( 'wpforms_admin_builder_templates_apply_to_existing_form_modify_data', [ $this, 'update_template_form_data' ] ); add_filter( 'wpforms_templates_class_base_template_modify_data', [ $this, 'update_template_form_data' ] ); add_filter( 'wpforms_templates_class_base_template_replace_modify_data', [ $this, 'update_template_form_data' ] ); add_filter( 'wpforms_form_handler_convert_form_data', [ $this, 'update_template_form_data' ] ); } /** * Store a random field id to insert a honeypot field later. * * @since 1.9.0 * * @param array|mixed $fields_data Form fields data. * * @return array|mixed Form fields data. */ public function get_random_field( $fields_data ) { if ( ! is_array( $fields_data ) ) { return $fields_data; } $random_field_id = array_rand( $fields_data ); if ( ! empty( $random_field_id ) ) { $this->insert_before_field_id = $random_field_id; } return $fields_data; } /** * Insert honeypot field before a random field. * * @since 1.9.0 * * @param array $field Field. * @param array $form_data Form data. */ public function maybe_insert_honeypot_field( array $field, array $form_data ) { if ( $this->insert_before_field_id !== (int) $field['id'] || ! $this->is_honeypot_enabled( $form_data ) ) { return; } $honeypot_field_id = $this->get_honeypot_field_id( $form_data ); $form_id = (int) $form_data['id']; $label = $this->get_honeypot_label( $form_data ); $id_attr = sprintf( 'wpforms-%1$s-field_%2$s', $form_id, $honeypot_field_id ); $is_amp = wpforms_is_amp(); $this->forms_data[ $form_id ] = $honeypot_field_id; if ( $is_amp ) { echo '<amp-layout layout="nodisplay">'; } ?> <div id="<?php echo esc_attr( $id_attr ); ?>-container" class="wpforms-field wpforms-field-text" data-field-type="text" data-field-id="<?php echo esc_attr( $honeypot_field_id ); ?>" > <label class="wpforms-field-label" for="<?php echo esc_attr( $id_attr ); ?>" ><?php echo esc_html( $label ); ?></label> <input type="text" id="<?php echo esc_attr( $id_attr ); ?>" class="wpforms-field-medium" name="wpforms[fields][<?php echo esc_attr( $honeypot_field_id ); ?>]" > </div> <?php if ( $is_amp ) { echo '</amp-layout>'; } } /** * Insert the inline styles. * * @since 1.9.0 * * @param array $form_data Form data. * * @noinspection PhpUnusedParameterInspection */ public function maybe_insert_honeypot_init_js( array $form_data ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found if ( ! $this->forms_data || wpforms_is_amp() ) { return; } $ids = []; foreach ( $this->forms_data as $form_id => $honeypot_field_id ) { $ids[] = sprintf( '#wpforms-%1$d-field_%2$d-container', $form_id, $honeypot_field_id ); } if ( ! $ids ) { return; } $styles = sprintf( '%1$s { position: absolute !important; overflow: hidden !important; display: inline !important; height: 1px !important; width: 1px !important; z-index: -1000 !important; padding: 0 !important; } %1$s input { visibility: hidden; } #wpforms-conversational-form-page %1$s label { counter-increment: none; }', esc_attr( implode( ',', $ids ) ) ); // There must be no empty lines inside the script. Otherwise, wpautop adds <p> tags which break script execution. printf( "<script> ( function() { const style = document.createElement( 'style' ); style.appendChild( document.createTextNode( '%s' ) ); document.head.appendChild( style ); document.currentScript?.remove(); } )(); </script>", esc_js( $styles ) ); } /** * Get honeypot field label. * * @since 1.9.0 * * @param array $form_data Form data. */ private function get_honeypot_label( array $form_data ): string { $labels = []; foreach ( $form_data['fields'] ?? [] as $field ) { if ( ! empty( $field['label'] ) ) { $labels[] = $field['label']; } } $words = explode( ' ', implode( ' ', $labels ) ); $count_words = count( $words ); $label_keys = (array) array_rand( $words, min( $count_words, 3 ) ); shuffle( $label_keys ); $label_words = array_map( static function ( $key ) use ( $words ) { return $words[ $key ]; }, $label_keys ); return implode( ' ', $label_words ); } /** * Add strings to the frontend. * * @since 1.9.0 * * @param array|mixed $strings Frontend strings. * * @return array Frontend strings. */ public function add_frontend_strings( $strings ): array { $strings = (array) $strings; // Store the honeypot field ID for validation and adding inline styles. $strings['hn_data'] = $this->forms_data; return $strings; } /** * Validate whether the modern Anti-Spam is enabled. * * @since 1.9.0 * * @param array $form_data Form data. * @param array $fields Fields. * @param array $entry Form submission raw data ($_POST). * * @return bool True if the entry is valid, false otherwise. * @noinspection PhpUnusedParameterInspection */ public function validate( array $form_data, array $fields, array &$entry ): bool { // Bail out if the modern Anti-Spam is not enabled. if ( ! $this->is_honeypot_enabled( $form_data ) ) { return true; } $honeypot_fields = array_diff_key( $entry['fields'], $form_data['fields'] ); $is_valid = true; // Compatibility with the WPML plugin (WPFML addon). // In case the form contains an Entry Preview field, they add an extra field with ID 0 to the entry. if ( isset( $entry['fields'][0] ) && defined( 'WPML_WP_FORMS_VERSION' ) && wpforms_has_field_type( 'entry-preview', $form_data ) ) { unset( $honeypot_fields[0] ); } foreach ( $honeypot_fields as $key => $honeypot_field ) { // Remove the honeypot field from the entry. unset( $entry['fields'][ $key ] ); // If the honeypot field is not empty, the entry is invalid. if ( ! empty( $honeypot_field ) ) { $is_valid = false; } } return $is_valid; } /** * Check if the modern Anti-Spam is enabled. * * @since 1.9.0 * * @param array $form_data Form data. * * @return bool True if the modern Anti-Spam is enabled, false otherwise. */ private function is_honeypot_enabled( array $form_data ): bool { static $is_enabled; if ( isset( $is_enabled ) ) { return $is_enabled; } /** * Filters whether the modern Anti-Spam is enabled. * * @since 1.9.0 * * @param bool $is_enabled True if the modern Anti-Spam is enabled, false otherwise. */ $is_enabled = (bool) apply_filters( 'wpforms_forms_anti_spam_v3_is_honeypot_enabled', ! empty( $form_data['settings']['antispam_v3'] ) ); return $is_enabled; } /** * Get the honeypot field ID. * * @since 1.9.0 * * @param array $form_data Form data. * * @return int Honeypot field ID. */ private function get_honeypot_field_id( array $form_data ): int { $max_key = max( array_keys( $form_data['fields'] ) ); // Find the first available field ID. for ( $i = 1; $i <= $max_key; $i++ ) { if ( ! isset( $form_data['fields'][ $i ] ) ) { return $i; } } // If no available field ID found, use the max ID + 1. return $max_key + 1; } /** * Update the form data on the builder settings panel. * * @since 1.9.0 * * @param array|bool $form_data Form data. * * @return array|bool */ public function init_builder_settings_form_data( $form_data ) { if ( ! $form_data ) { return $form_data; } // Update default time limit duration for the existing form. if ( empty( $form_data['settings']['anti_spam']['time_limit']['enable'] ) ) { $form_data['settings']['anti_spam']['time_limit']['duration'] = '2'; } return $form_data; } /** * Update the template form data. Set the modern Anti-Spam setting. * * @since 1.9.0 * * @param array|mixed $form_data Form data. * * @return array */ public function update_template_form_data( $form_data ): array { $form_data = (array) $form_data; // Unset the old Anti-Spam setting. unset( $form_data['settings']['antispam'] ); // Enable the modern Anti-Spam setting. $form_data['settings']['antispam_v3'] = $form_data['settings']['antispam_v3'] ?? '1'; $form_data['settings']['anti_spam'] = $form_data['settings']['anti_spam'] ?? []; // Enable the time limit setting. $form_data['settings']['anti_spam']['time_limit'] = [ 'enable' => '1', 'duration' => '2', ]; return $form_data; } } Access/Capabilities.php 0000644 00000002535 15174710275 0011065 0 ustar 00 <?php namespace WPForms\Access; /** * Access/Capability management. * * @since 1.5.8 */ class Capabilities { /** * Init class. * * @since 1.5.8 */ public function init() { } /** * Init conditions. * * @since 1.5.8.2 */ public function init_allowed() { return false; } /** * Check permissions for currently logged in user. * * @since 1.5.8 * * @param array|string $caps Capability name(s). * @param int $id Optional. ID of the specific object to check against if capability is a "meta" cap. * "Meta" capabilities, e.g. 'edit_post', 'edit_user', etc., are capabilities used * by map_meta_cap() to map to other "primitive" capabilities, e.g. 'edit_posts', * edit_others_posts', etc. Accessed via func_get_args() and passed to WP_User::has_cap(), * then map_meta_cap(). * * @return bool */ public function current_user_can( $caps = [], $id = 0 ) { return \current_user_can( \wpforms_get_capability_manage_options() ); } /** * Get a first valid capability from an array of capabilities. * * @since 1.5.8 * * @param array $caps Array of capabilities to check. * * @return string */ public function get_menu_cap( $caps ) { return \wpforms_get_capability_manage_options(); } } API.php 0000644 00000001633 15174710275 0005702 0 ustar 00 <?php namespace WPForms; use WPForms\Admin\Tools\Views\Import; /** * Class API. * * @since 1.8.6 */ class API { /** * Registry. * Contains name of the class and method to be called. * For non-static methods, should contain the id to operate via wpforms->get( 'class' ). * * @todo Add non-static methods processing. * * @since 1.8.6 * * @var array[] */ private $registry = [ 'import_forms' => [ 'class' => Import::class, 'method' => 'import_forms', ], ]; /** * Magic method to call a method from registry. * * @since 1.8.6 * * @param string $name Method name. * @param array $args Arguments. * * @return mixed|null */ public function __call( string $name, array $args ) { $callback = $this->registry[ $name ] ?? null; if ( $callback === null ) { return null; } return call_user_func( [ $callback['class'], $callback['method'] ], ...$args ); } } Admin/Dashboard/Widget.php 0000644 00000020265 15174710275 0011435 0 ustar 00 <?php namespace WPForms\Admin\Dashboard; /** * Class Widget. * * @since 1.7.3 */ abstract class Widget { /** * Instance slug. * * @since 1.7.4 * * @var string */ const SLUG = 'dash_widget'; /** * Save a widget meta for a current user using AJAX. * * @since 1.7.4 */ public function save_widget_meta_ajax() { check_ajax_referer( 'wpforms_' . static::SLUG . '_nonce' ); $meta = ! empty( $_POST['meta'] ) ? sanitize_key( $_POST['meta'] ) : ''; $value = ! empty( $_POST['value'] ) ? absint( $_POST['value'] ) : 0; $this->widget_meta( 'set', $meta, $value ); exit(); } /** * Get/set a widget meta. * * @since 1.7.4 * * @param string $action Possible value: 'get' or 'set'. * @param string $meta Meta name. * @param int $value Value to set. * * @return mixed */ protected function widget_meta( $action, $meta, $value = 0 ) { $allowed_actions = [ 'get', 'set' ]; if ( ! in_array( $action, $allowed_actions, true ) ) { return false; } $defaults = [ 'timespan' => $this->get_timespan_default(), 'active_form_id' => 0, 'hide_recommended_block' => 0, 'hide_graph' => 0, 'color_scheme' => 1, // 1 - wpforms, 2 - wp 'graph_style' => 2, // 1 - bar, 2 - line ]; if ( ! array_key_exists( $meta, $defaults ) ) { return false; } $meta_key = 'wpforms_' . static::SLUG . '_' . $meta; $user_id = get_current_user_id(); if ( $action === 'get' ) { $meta_value = absint( get_user_meta( $user_id, $meta_key, true ) ); // Return a default value from $defaults if $meta_value is empty. return empty( $meta_value ) ? $defaults[ $meta ] : $meta_value; } $value = absint( $value ); if ( $action === 'set' && ! empty( $value ) ) { return update_user_meta( $user_id, $meta_key, $value ); } if ( $action === 'set' && empty( $value ) ) { return delete_user_meta( $user_id, $meta_key ); } return false; } /** * Get the default timespan option. * * @since 1.7.4 * * @return int|null */ protected function get_timespan_default() { $options = $this->get_timespan_options(); $default = reset( $options ); return is_numeric( $default ) ? $default : null; } /** * Get timespan options (in days). * * @since 1.7.4 * * @return array */ protected function get_timespan_options(): array { $default = [ 7, 30 ]; $options = $default; // Apply deprecated filters. if ( function_exists( 'apply_filters_deprecated' ) ) { // phpcs:disable WPForms.Comments.PHPDocHooks.RequiredHookDocumentation, WPForms.PHP.ValidateHooks.InvalidHookName $options = apply_filters_deprecated( 'wpforms_dash_widget_chart_timespan_options', [ $options ], '5.0', 'wpforms_dash_widget_timespan_options' ); $options = apply_filters_deprecated( 'wpforms_dash_widget_forms_list_timespan_options', [ $options ], '5.0', 'wpforms_dash_widget_timespan_options' ); // phpcs:enable WPForms.Comments.PHPDocHooks.RequiredHookDocumentation, WPForms.PHP.ValidateHooks.InvalidHookName } else { // phpcs:disable WPForms.Comments.PHPDocHooks.RequiredHookDocumentation, WPForms.PHP.ValidateHooks.InvalidHookName $options = apply_filters( 'wpforms_dash_widget_chart_timespan_options', $options ); $options = apply_filters( 'wpforms_dash_widget_forms_list_timespan_options', $options ); // phpcs:enable WPForms.Comments.PHPDocHooks.RequiredHookDocumentation, WPForms.PHP.ValidateHooks.InvalidHookName } if ( ! is_array( $options ) ) { $options = $default; } $widget_slug = static::SLUG; // phpcs:disable WPForms.Comments.PHPDocHooks.RequiredHookDocumentation, WPForms.PHP.ValidateHooks.InvalidHookName $options = apply_filters( "wpforms_{$widget_slug}_timespan_options", $options ); // phpcs:enable WPForms.Comments.PHPDocHooks.RequiredHookDocumentation, WPForms.PHP.ValidateHooks.InvalidHookName if ( ! is_array( $options ) ) { return []; } $options = array_filter( $options, 'is_numeric' ); return empty( $options ) ? $default : $options; } /** * Widget settings HTML. * * @since 1.7.4 * * @param bool $enabled Is form fields should be enabled. */ protected function widget_settings_html( $enabled = true ) { $graph_style = $this->widget_meta( 'get', 'graph_style' ); $color_scheme = $this->widget_meta( 'get', 'color_scheme' ); echo wpforms_render( // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped 'admin/dashboard/widget/settings', [ 'graph_style' => $graph_style, 'color_scheme' => $color_scheme, 'enabled' => $enabled, ], true ); } /** * Return randomly chosen one of the recommended plugins. * * @since 1.7.3 * * @return array */ final protected function get_recommended_plugin(): array { $plugins = [ 'google-analytics-for-wordpress/googleanalytics.php' => [ 'name' => __( 'MonsterInsights', 'wpforms-lite' ), 'slug' => 'google-analytics-for-wordpress', 'more' => 'https://www.monsterinsights.com/', 'pro' => [ 'file' => 'google-analytics-premium/googleanalytics-premium.php', ], ], 'all-in-one-seo-pack/all_in_one_seo_pack.php' => [ 'name' => __( 'AIOSEO', 'wpforms-lite' ), 'slug' => 'all-in-one-seo-pack', 'more' => 'https://aioseo.com/', 'pro' => [ 'file' => 'all-in-one-seo-pack-pro/all_in_one_seo_pack.php', ], ], 'coming-soon/coming-soon.php' => [ 'name' => __( 'SeedProd', 'wpforms-lite' ), 'slug' => 'coming-soon', 'more' => 'https://www.seedprod.com/', 'pro' => [ 'file' => 'seedprod-coming-soon-pro-5/seedprod-coming-soon-pro-5.php', ], ], 'wp-mail-smtp/wp_mail_smtp.php' => [ 'name' => __( 'WP Mail SMTP', 'wpforms-lite' ), 'slug' => 'wp-mail-smtp', 'more' => 'https://wpmailsmtp.com/', 'pro' => [ 'file' => 'wp-mail-smtp-pro/wp_mail_smtp.php', ], ], ]; $installed = get_plugins(); foreach ( $plugins as $id => $plugin ) { if ( isset( $installed[ $id ] ) ) { unset( $plugins[ $id ] ); } if ( isset( $plugin['pro']['file'], $installed[ $plugin['pro']['file'] ] ) ) { unset( $plugins[ $id ] ); } } return $plugins ? $plugins[ array_rand( $plugins ) ] : []; } /** * Timespan select HTML. * * @since 1.7.4 * * @param int $active_form_id Currently preselected form ID. * @param bool $enabled If the select menu items should be enabled. */ protected function timespan_select_html( $active_form_id, $enabled = true ) { ?> <select id="wpforms-dash-widget-timespan" class="wpforms-dash-widget-select-timespan" title="<?php esc_attr_e( 'Select timespan', 'wpforms-lite' ); ?>" <?php echo ! empty( $active_form_id ) ? 'data-active-form-id="' . absint( $active_form_id ) . '"' : ''; ?>> <?php $this->timespan_options_html( $this->get_timespan_options(), $enabled ); ?> </select> <?php } /** * Timespan select options HTML. * * @since 1.7.4 * * @param array $options Timespan options (in days). * @param bool $enabled If the select menu items should be enabled. */ protected function timespan_options_html( $options, $enabled = true ) { $timespan = $this->widget_meta( 'get', 'timespan' ); foreach ( $options as $option ) : ?> <option value="<?php echo absint( $option ); ?>" <?php selected( $timespan, absint( $option ) ); ?> <?php disabled( ! $enabled ); ?>> <?php /* translators: %d - number of days. */ ?> <?php echo esc_html( sprintf( _n( 'Last %d day', 'Last %d days', absint( $option ), 'wpforms-lite' ), absint( $option ) ) ); ?> </option> <?php endforeach; } /** * Check if the current page is a dashboard page. * * @since 1.8.3 * * @return bool */ protected function is_dashboard_page(): bool { global $pagenow; // phpcs:ignore WordPress.Security.NonceVerification.Recommended return $pagenow === 'index.php' && empty( $_GET['page'] ); } /** * Check if is a dashboard widget ajax request. * * @since 1.8.3 * * @return bool */ protected function is_dashboard_widget_ajax_request(): bool { // phpcs:ignore WordPress.Security.NonceVerification.Recommended return wpforms_is_admin_ajax() && isset( $_REQUEST['action'] ) && strpos( sanitize_key( $_REQUEST['action'] ), 'wpforms_dash_widget' ) !== false; } } Admin/Pages/UncannyAutomator.php 0000644 00000026741 15174710275 0012676 0 ustar 00 <?php namespace WPForms\Admin\Pages; use Uncanny_Automator\Automator_Load; use Uncanny_Automator_Pro\Automator_Pro_Load; /** * Uncanny Automator Subpage. * * @since 1.9.8.6 */ class UncannyAutomator extends Page { /** * Admin menu page slug. * * @since 1.9.8.6 * * @var string */ public const SLUG = 'wpforms-uncanny-automator'; /** * Configuration. * * @since 1.9.8.6 * * @var array */ protected $config = [ 'lite_plugin' => 'uncanny-automator/uncanny-automator.php', 'lite_wporg_url' => 'https://wordpress.org/plugins/uncanny-automator/', 'lite_download_url' => 'https://downloads.wordpress.org/plugin/uncanny-automator.zip', 'pro_plugin' => 'uncanny-automator-pro/uncanny-automator-pro.php', 'uncanny-automator_addon' => 'uncanny-automator-pro/uncanny-automator-pro.php', 'uncanny-automator_addon_page' => 'https://automatorplugin.com/?utm_source=wpformsplugin&utm_medium=link&utm_campaign=uncanny-automator-page', 'uncanny-automator_onboarding' => 'post-new.php?post_type=uo-recipe', ]; /** * Get the plugin name for use in IDs, CSS classes, and config keys. * * @since 1.9.8.6 * * @return string Plugin name. */ protected static function get_plugin_name(): string { return 'uncanny-automator'; } /** * Get heading title text. * * @since 1.9.8.6 * * @return string Heading title. */ protected function get_heading_title(): string { return esc_html__( 'Let Your Site Handle the Busywork.', 'wpforms-lite' ); } /** * Get heading alt text for logo. * * @since 1.9.8.6 * * @return string Heading alt text. */ protected function get_heading_alt_text(): string { return esc_attr__( 'WPForms ♥ Uncanny Automator', 'wpforms-lite' ); } /** * Get heading description strings. * * @since 1.9.8.6 * * @return array Array of description strings. */ protected function get_heading_strings(): array { return [ esc_html__( 'Automate tasks, save time, and keep everything running smoothly. Uncanny Automator connects your favorite tools so your site works smarter. No code. No stress.', 'wpforms-lite' ), ]; } /** * Get screenshot features list. * * @since 1.9.8.6 * * @return array Array of feature strings. */ protected function get_screenshot_features(): array { return [ 'Connect 200+ plugins and apps automatically: social media, memberships, courses, WooCommerce, CRMs, team chat, and much more.', 'Create users, assign access, and enroll in courses with no manual work.', 'Build multi-step workflows with delays and conditional logic, no code required.', 'Unlimited automations with no per-task fees.', ]; } /** * Get screenshot alt text. * * @since 1.9.8.6 * * @return string Alt text for screenshot image. */ protected function get_screenshot_alt_text(): string { return esc_attr__( 'Uncanny Automator screenshot', 'wpforms-lite' ); } /** * Generate and output step 'Result' section HTML. * * @since 1.9.8.6 * * @noinspection HtmlUnknownTarget */ protected function output_section_step_result(): void { $step = $this->get_data_step_result(); if ( empty( $step ) ) { return; } printf( '<section class="step step-result %1$s"> <aside class="num"> <img src="%2$s" alt="%3$s" /> <i class="loader hidden"></i> </aside> <div> <h2>%4$s</h2> <p>%5$s</p> <button class="button %6$s" data-url="%7$s">%8$s</button> </div> </section>', esc_attr( $step['section_class'] ), esc_url( WPFORMS_PLUGIN_URL . 'assets/images/' . $step['icon'] ), esc_attr__( 'Step 3', 'wpforms-lite' ), esc_html__( 'Save and Test Your Automation', 'wpforms-lite' ), esc_html__( 'Click Save Recipe, run a test, and watch your workflow run on its own, no code needed.', 'wpforms-lite' ), esc_attr( $step['button_class'] ), esc_url( $step['button_url'] ), esc_html( $step['button_text'] ) ); } /** * Step 'Result' data. * * @since 1.9.8.6 * * @return array Step data. */ protected function get_data_step_result(): array { // phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh $step = []; $step['icon'] = 'step-3.svg'; $step['section_class'] = $this->output_data['plugin_setup'] ? '' : 'grey'; $step['button_text'] = esc_html__( 'Learn More', 'wpforms-lite' ); $step['button_class'] = 'grey disabled'; $step['button_url'] = ''; $plugin_license_level = $this->get_license_level(); switch ( $plugin_license_level ) { case 'lite': $step['button_url'] = $this->config['uncanny-automator_addon_page']; $step['button_class'] = $this->output_data['plugin_setup'] ? 'button-primary' : 'grey disabled'; break; case 'pro': $addon_installed = array_key_exists( $this->config['uncanny-automator_addon'], $this->output_data['all_plugins'] ); $step['button_text'] = $addon_installed ? esc_html__( 'Uncanny Automator Pro Installed & Activated', 'wpforms-lite' ) : esc_html__( 'Install Now', 'wpforms-lite' ); $step['button_class'] = $this->output_data['plugin_setup'] ? 'grey disabled' : 'button-primary'; $step['icon'] = $addon_installed ? 'step-complete.svg' : 'step-3.svg'; break; } return $step; } /** * Retrieve the license level of the plugin. * * @since 1.9.8.6 * * @return string The plugin license level ('lite' or 'pro'). */ protected function get_license_level(): string { $plugin_license_level = 'lite'; if ( isset( $this->output_data['plugin_activated'] ) ) { // Check if premium features are available. if ( defined( 'AUTOMATOR_PRO_PLUGIN_VERSION' ) || class_exists( Automator_Pro_Load::class ) ) { $plugin_license_level = 'pro'; } } return $plugin_license_level; } /** * Whether the plugin is finished setup or not. * * @since 1.9.8.6 */ protected function is_plugin_finished_setup(): bool { if ( ! $this->is_plugin_configured() ) { return false; } return $this->get_license_level() === 'pro'; } /** * Get the heading for the setup step. * * @since 1.9.8.6 * * @return string Setup step heading. */ protected function get_setup_heading(): string { return esc_html__( 'Create Your First Automation (Recipe)', 'wpforms-lite' ); } /** * Get the description for the setup step. * * @since 1.9.8.6 * * @return string Setup step description. */ protected function get_setup_description(): string { return esc_html__( 'Open the Automator menu, click Add New, choose your trigger (e.g. form submission), and define your action (e.g. send email, update CRM).', 'wpforms-lite' ); } /** * Whether a plugin is configured or not. * * @since 1.9.8.6 * * @return bool True if plugin is configured properly. */ protected function is_plugin_configured(): bool { if ( ! $this->is_plugin_activated() ) { return false; } // Check if Uncanny Automator has been configured with basic settings. // The plugin is considered configured if there are recipes created. global $wpdb; // Check for Uncanny Automator posts (recipes). // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $recipes = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_type = %s AND post_status != %s", 'uo-recipe', 'trash' ) ); if ( (int) $recipes > 0 ) { return true; } // Check for basic Automator settings. $automator_settings = get_option( 'uap_automator_settings' ); return ! empty( $automator_settings ); } /** * Whether a plugin is active or not. * * @since 1.9.8.6 * * @return bool True if the plugin is active. */ protected function is_plugin_activated(): bool { return ( ( defined( 'AUTOMATOR_PLUGIN_VERSION' ) || class_exists( Automator_Load::class ) ) && ( is_plugin_active( $this->config['lite_plugin'] ) || is_plugin_active( $this->config['pro_plugin'] ) ) ); } /** * Whether a plugin is available (class/function exists). * * @since 1.9.8.6 * * @return bool True if a plugin is available. */ protected function is_plugin_available(): bool { return function_exists( 'Automator' ) || defined( 'AUTOMATOR_VERSION' ); } /** * Whether a pro-version is active. * * @since 1.9.8.6 * * @return bool True if a pro-version is active. */ protected function is_pro_active(): bool { return class_exists( 'Uncanny_Automator_Pro\Plugin' ) || defined( 'AUTOMATOR_PRO_VERSION' ); } /** * Get the heading for the installation step. * * @since 1.9.8.6 * * @return string Install step heading. */ protected function get_install_heading(): string { return esc_html__( 'Install and Activate Uncanny Automator', 'wpforms-lite' ); } /** * Get the description for the installation step. * * @since 1.9.8.6 * * @return string Install step description. */ protected function get_install_description(): string { return esc_html__( 'Connect Automator and start building automations that save hours every week.', 'wpforms-lite' ); } /** * Get the plugin title. * * @since 1.9.8.6 * * @return string Plugin title. */ protected function get_plugin_title(): string { return esc_html__( 'Uncanny Automator', 'wpforms-lite' ); } /** * Get the installation button text. * * @since 1.9.8.6 * * @return string Install button text. */ protected function get_install_button_text(): string { return esc_html__( 'Install Uncanny Automator', 'wpforms-lite' ); } /** * Get the text when a plugin is installed and activated. * * @since 1.9.8.6 * * @return string Installed & activated text. */ protected function get_installed_activated_text(): string { return esc_html__( 'Uncanny Automator Installed & Activated', 'wpforms-lite' ); } /** * Get the activate button text. * * @since 1.9.8.6 * * @return string Activate button text. */ protected function get_activate_text(): string { return esc_html__( 'Activate Uncanny Automator', 'wpforms-lite' ); } /** * Get the setup button text. * * @since 1.9.8.6 * * @return string Setup button text. */ protected function get_setup_button_text(): string { return esc_html__( 'Create Your First Recipe', 'wpforms-lite' ); } /** * Get the text when setup is completed. * * @since 1.9.8.6 * * @return string Setup completed text. */ protected function get_setup_completed_text(): string { return esc_html__( 'Recipe Created', 'wpforms-lite' ); } /** * Get the text when a pro-version is installed and activated. * * @since 1.9.8.6 * * @return string Pro installed and activated text. */ protected function get_pro_installed_activated_text(): string { return esc_html__( 'Uncanny Automator Pro Installed & Activated', 'wpforms-lite' ); } /** * Set the source of the plugin installation. * * @since 1.9.8.6 * * @param string $plugin_basename The basename of the plugin. */ public function plugin_activated( string $plugin_basename ): void { if ( $plugin_basename !== $this->config['lite_plugin'] ) { return; } $source = wpforms()->is_pro() ? 'WPForms' : 'WPForms Lite'; /** * Rewrite the get_plugin_name() default value. * * Use `uncannyautomator` instead of `uncanny-automator`. * This is necessary for maintaining consistency with the integration and the plugin itself. * * See: src/Integrations/UncannyAutomator/UncannyAutomator.php update_source() method. */ update_option( 'uncannyautomator_source', $source, false ); update_option( 'uncannyautomator_date', time(), false ); } } Admin/Pages/Duplicator.php 0000644 00000030072 15174710275 0011465 0 ustar 00 <?php namespace WPForms\Admin\Pages; /** * Duplicator Subpage. * * @since 1.9.8.6 */ class Duplicator extends Page { /** * Admin menu page slug. * * @since 1.9.8.6 * * @var string */ public const SLUG = 'wpforms-duplicator'; /** * Configuration. * * @since 1.9.8.6 * * @var array */ protected $config = [ 'lite_plugin' => 'duplicator/duplicator.php', 'lite_wporg_url' => 'https://wordpress.org/plugins/duplicator/', 'lite_download_url' => 'https://downloads.wordpress.org/plugin/duplicator.zip', 'pro_plugin' => 'duplicator-pro/duplicator-pro.php', 'duplicator_addon' => 'duplicator-pro/duplicator-pro.php', 'duplicator_addon_page' => 'https://duplicator.com/?utm_source=wpformsplugin&utm_medium=link&utm_campaign=duplicator-page', 'duplicator_onboarding' => 'admin.php?page=duplicator', ]; /** * Constructor. * * @since 1.9.8.6 */ public function __construct() { // Set the correct onboarding page based on the active version. if ( $this->is_pro_active() ) { $this->config['duplicator_onboarding'] = 'admin.php?page=duplicator-pro'; } parent::__construct(); } /** * Get the plugin name for use in IDs, CSS classes, and config keys. * * @since 1.9.8.6 * * @return string Plugin name. */ protected static function get_plugin_name(): string { return 'duplicator'; } /** * Get heading title text. * * @since 1.9.8.6 * * @return string Heading title. */ protected function get_heading_title(): string { return esc_html__( 'WPForms Collects It. Duplicator Protects It.', 'wpforms-lite' ); } /** * Get heading alt text for logo. * * @since 1.9.8.6 * * @return string Heading alt text. */ protected function get_heading_alt_text(): string { return esc_attr__( 'WPForms ♥ Duplicator', 'wpforms-lite' ); } /** * Get heading description strings. * * @since 1.9.8.6 * * @return array Array of description strings. */ protected function get_heading_strings(): array { return [ esc_html__( 'Every form entry lives in your database. One bad update, one crash, and it\'s gone. Duplicator backs up your entire site automatically so you can restore everything with one click.', 'wpforms-lite' ), esc_html__( 'Trusted by over 1.5 million websites.', 'wpforms-lite' ), ]; } /** * Get screenshot features list. * * @since 1.9.8.6 * * @return array Array of feature strings. */ protected function get_screenshot_features(): array { return [ 'Back up your entire site automatically: forms, entries, everything.', 'Restore your site with one click if anything goes wrong.', 'Store backups safely in Google Drive, Dropbox, or Amazon S3.', 'Schedule daily backups so you never have to think about it.', ]; } /** * Get screenshot alt text. * * @since 1.9.8.6 * * @return string Alt text for screenshot image. */ protected function get_screenshot_alt_text(): string { return esc_attr__( 'Duplicator screenshot', 'wpforms-lite' ); } /** * Generate and output step 'Result' section HTML. * * @since 1.9.8.6 * * @noinspection HtmlUnknownTarget */ protected function output_section_step_result(): void { $step = $this->get_data_step_result(); if ( empty( $step ) ) { return; } printf( '<section class="step step-result %1$s"> <aside class="num"> <img src="%2$s" alt="%3$s" /> </aside> <div> <h2>%4$s</h2> <p>%5$s</p> <button class="button %6$s" data-url="%7$s">%8$s</button> </div> </section>', esc_attr( $step['section_class'] ), esc_url( WPFORMS_PLUGIN_URL . 'assets/images/' . $step['icon'] ), esc_attr__( 'Step 3', 'wpforms-lite' ), esc_html__( 'Set Up Scheduled Cloud Backups', 'wpforms-lite' ), esc_html__( 'Keep your data safe forever with automatic daily backups to Google Drive, Dropbox, or Amazon S3.', 'wpforms-lite' ), esc_attr( $step['button_class'] ), esc_url( admin_url( $this->is_pro_active() ? 'admin.php?page=duplicator-pro-schedules' : 'admin.php?page=duplicator-schedules' ) ), esc_html( $step['button_text'] ) ); } /** * Whether the plugin is finished setup or not. * * @since 1.9.8.6 */ protected function is_plugin_finished_setup(): bool { if ( ! $this->is_plugin_configured() ) { return false; } $count = $this->get_package_count(); $schedule_count = 0; if ( $count && class_exists( '\Duplicator\Models\ScheduleEntity' ) && $this->is_pro_active() ) { $schedule_count = \Duplicator\Models\ScheduleEntity::count(); // phpcs:ignore WPForms.PHP.BackSlash.RemoveBackslash, WPForms.PHP.BackSlash.UseShortSyntax } return $count && $schedule_count; } /** * Generate and output footer section HTML. * * @since 1.9.8.6 */ protected function output_section_footer(): void { printf( '<section class="bottom"> <p>%s</p> </section>', esc_html__( 'Because the data you collect with WPForms is too valuable to lose.', 'wpforms-lite' ) ); } /** * Step 'Result' data. * * @since 1.9.8.6 * * @return array Step data. */ protected function get_data_step_result(): array { $count = $this->get_package_count(); $data = [ 'section_class' => $count ? '' : 'grey', 'button_class' => ! $count ? 'grey disabled' : 'button-primary', 'icon' => 'step-3.svg', 'button_text' => esc_html__( 'Set Up Cloud Backups', 'wpforms-lite' ), ]; if ( $count && class_exists( '\Duplicator\Models\ScheduleEntity' ) && $this->is_pro_active() ) { $schedule_count = \Duplicator\Models\ScheduleEntity::count(); // phpcs:ignore WPForms.PHP.BackSlash.RemoveBackslash, WPForms.PHP.BackSlash.UseShortSyntax $data['section_class'] = ''; $data['button_class'] = 'button-primary'; if ( $schedule_count ) { $data['icon'] = 'step-complete.svg'; $data['button_class'] = 'grey disabled'; $data['button_text'] = esc_html__( 'Cloud Backups Set Up', 'wpforms-lite' ); } } return $data; } /** * Whether a plugin is configured or not. * * @since 1.9.8.6 * * @return bool True if plugin is configured properly. */ protected function is_plugin_configured(): bool { if ( ! $this->is_plugin_activated() ) { return false; } $count = $this->get_package_count(); return $count > 0; } /** * Get the number of packages in the database. * * @since 1.9.8.6 * * @return int Number of packages. */ protected function get_package_count(): int { /** * Check if the plugin is available. * Since we are using a direct query to the database to get the number of records instead of built-in methods, * there is a chance of getting a non-zero value even if the plugin is turned off. */ if ( ! $this->is_plugin_available() ) { return 0; } // Check if Duplicator has been configured with basic settings. global $wpdb; // Check for the Duplicator packages table. $packages_table = $this->is_pro_active() ? $wpdb->prefix . 'duplicator_backups' : $wpdb->prefix . 'duplicator_packages'; // Use object caching to minimize direct DB queries here, as there is no core API // to check custom plugin table existence or its contents. $blog_id = function_exists( 'get_current_blog_id' ) ? get_current_blog_id() : 0; $table_exists_cache_key = "wpforms_dup_table_exists_{$blog_id}"; $package_count_cache_key = "wpforms_dup_package_count_{$blog_id}"; $table_exists = wp_cache_get( $table_exists_cache_key, 'wpforms' ); if ( $table_exists === false ) { // PHPCS: We must use a direct DB query because no WP API exists for custom tables. // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $table_exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $packages_table ) ) ); wp_cache_set( $table_exists_cache_key, $table_exists, 'wpforms', 60 ); } $package_count = 0; if ( $table_exists === $packages_table ) { $package_count = wp_cache_get( $package_count_cache_key, 'wpforms' ); if ( $package_count === false ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $package_count = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$packages_table}" ); wp_cache_set( $package_count_cache_key, $package_count, 'wpforms', 60 ); } } return (int) $package_count; } /** * Whether a plugin is active or not. * * @since 1.9.8.6 * * @return bool True if the plugin is active. */ protected function is_plugin_activated(): bool { return ( ( defined( 'DUPLICATOR_VERSION' ) || class_exists( 'Duplicator\Plugin' ) || class_exists( 'Duplicator\Pro\Requirements' ) ) && ( is_plugin_active( $this->config['lite_plugin'] ) || is_plugin_active( $this->config['pro_plugin'] ) ) ); } /** * Whether a plugin is available (class/function exists). * * @since 1.9.8.6 * * @return bool True if plugin is available. */ protected function is_plugin_available(): bool { return class_exists( 'Duplicator\Plugin' ) || defined( 'DUPLICATOR_VERSION' ) || class_exists( 'DUP_PRO_Plugin' ) || defined( 'DUPLICATOR_PRO_VERSION' ); } /** * Whether pro version is active. * * @since 1.9.8.6 * * @return bool True if pro version is active. */ protected function is_pro_active(): bool { return class_exists( 'DUP_PRO_Plugin' ) || defined( 'DUPLICATOR_PRO_VERSION' ); } /** * Get the heading for the install step. * * @since 1.9.8.6 * * @return string Install step heading. */ protected function get_install_heading(): string { return esc_html__( 'Install and Activate Duplicator', 'wpforms-lite' ); } /** * Get the description for the install step. * * @since 1.9.8.6 * * @return string Install step description. */ protected function get_install_description(): string { return esc_html__( 'Your first step toward bulletproof backups.', 'wpforms-lite' ); } /** * Get the plugin title. * * @since 1.9.8.6 * * @return string Plugin title. */ protected function get_plugin_title(): string { return esc_html__( 'Duplicator', 'wpforms-lite' ); } /** * Get the install button text. * * @since 1.9.8.6 * * @return string Install button text. */ protected function get_install_button_text(): string { return esc_html__( 'Install Duplicator', 'wpforms-lite' ); } /** * Get the text when a plugin is installed and activated. * * @since 1.9.8.6 * * @return string Installed & activated text. */ protected function get_installed_activated_text(): string { return esc_html__( 'Duplicator Installed & Activated', 'wpforms-lite' ); } /** * Get the activate button text. * * @since 1.9.8.6 * * @return string Activate button text. */ protected function get_activate_text(): string { return esc_html__( 'Activate Duplicator', 'wpforms-lite' ); } /** * Get the heading for the setup step. * * @since 1.9.8.6 * * @return string Setup step heading. */ protected function get_setup_heading(): string { return esc_html__( 'Create Your First Backup', 'wpforms-lite' ); } /** * Get the description for the setup step. * * @since 1.9.8.6 * * @return string Setup step description. */ protected function get_setup_description(): string { return esc_html__( 'Back up your site — forms, entries, settings, everything — in just one click.', 'wpforms-lite' ); } /** * Get the setup button text. * * @since 1.9.8.6 * * @return string Setup button text. */ protected function get_setup_button_text(): string { return esc_html__( 'Create First Backup', 'wpforms-lite' ); } /** * Get the text when setup is completed. * * @since 1.9.8.6 * * @return string Setup completed text. */ protected function get_setup_completed_text(): string { return esc_html__( 'Backup Created', 'wpforms-lite' ); } /** * Get the text when a pro-version is installed and activated. * * @since 1.9.8.6 * * @return string Pro installed and activated text. */ protected function get_pro_installed_activated_text(): string { return esc_html__( 'Duplicator Pro Installed & Activated', 'wpforms-lite' ); } } Admin/Pages/PrivacyCompliance.php 0000644 00000026344 15174710275 0012776 0 ustar 00 <?php namespace WPForms\Admin\Pages; /** * Privacy Compliance Subpage. * * @since 1.9.7.3 */ class PrivacyCompliance extends Page { /** * Admin menu page slug. * * @since 1.9.7.3 * * @var string */ public const SLUG = 'wpforms-wpconsent'; /** * Configuration. * * @since 1.9.7.3 * * @var array */ protected $config = [ 'lite_plugin' => 'wpconsent-cookies-banner-privacy-suite/wpconsent.php', 'lite_wporg_url' => 'https://wordpress.org/plugins/wpconsent-cookies-banner-privacy-suite/', 'lite_download_url' => 'https://downloads.wordpress.org/plugin/wpconsent-cookies-banner-privacy-suite.zip', 'pro_plugin' => 'wpconsent-premium/wpconsent-premium.php', 'wpconsent_addon' => 'wpconsent-premium/wpconsent-premium.php', 'wpconsent_addon_page' => 'https://wpconsent.com/?utm_source=wpformsplugin&utm_medium=link&utm_campaign=privacy-compliance-page', 'wpconsent_onboarding' => 'admin.php?page=wpconsent-onboarding', ]; /** * Get the plugin name for use in IDs, CSS classes, and config keys. * * @since 1.9.7.3 * * @return string Plugin name. */ protected static function get_plugin_name(): string { return 'wpconsent'; } /** * Hooks. * * @since 1.9.7.3 */ public function hooks(): void { if ( wp_doing_ajax() ) { remove_action( 'admin_init', 'wpconsent_maybe_redirect_onboarding', 9999 ); } parent::hooks(); } /** * Get heading image URL. * * @since 1.9.7.3 * * @return string Heading image URL. */ protected function get_heading_image_url(): string { return WPFORMS_PLUGIN_URL . 'assets/images/wpconsent/wpforms-wpconsent.svg'; } /** * Get heading title text. * * @since 1.9.7.3 * * @return string Heading title. */ protected function get_heading_title(): string { return esc_html__( 'Make Your Website Privacy-Compliant in Minutes', 'wpforms-lite' ); } /** * Get heading alt text for logo. * * @since 1.9.7.3 * * @return string Heading alt text. */ protected function get_heading_alt_text(): string { return esc_attr__( 'WPForms ♥ WPConsent', 'wpforms-lite' ); } /** * Get heading description strings. * * @since 1.9.7.3 * * @return array Array of description strings. */ protected function get_heading_strings(): array { return [ esc_html__( 'Build trust with clear, compliant privacy practices. WPConsent adds clean, professional banners and handles the technical side for you.', 'wpforms-lite' ), esc_html__( 'Built for transparency. Designed for ease.', 'wpforms-lite' ), ]; } /** * Get screenshot features list. * * @since 1.9.7.3 * * @return array Array of feature strings. */ protected function get_screenshot_features(): array { return [ esc_html__( 'A professional banner that fits your site.', 'wpforms-lite' ), esc_html__( 'Tools like Google Analytics and Facebook Pixel paused until consent.', 'wpforms-lite' ), esc_html__( 'Peace of mind knowing you’re aligned with global laws.', 'wpforms-lite' ), esc_html__( 'Self-hosted. Your data remains on your site.', 'wpforms-lite' ), ]; } /** * Get screenshot alt text. * * @since 1.9.7.3 * * @return string Alt text for screenshot image. */ protected function get_screenshot_alt_text(): string { return esc_attr__( 'WPConsent screenshot', 'wpforms-lite' ); } /** * Generate and output step 'Result' section HTML. * * @since 1.9.7.3 * * @noinspection HtmlUnknownTarget */ protected function output_section_step_result(): void { $step = $this->get_data_step_result(); if ( empty( $step ) ) { return; } printf( '<section class="step step-result %1$s"> <aside class="num"> <img src="%2$s" alt="%3$s" /> <i class="loader hidden"></i> </aside> <div> <h2>%4$s</h2> <p>%5$s</p> <button class="button %6$s" data-url="%7$s">%8$s</button> </div> </section>', esc_attr( $step['section_class'] ), esc_url( WPFORMS_PLUGIN_URL . 'assets/images/' . $step['icon'] ), esc_attr__( 'Step 3', 'wpforms-lite' ), esc_html__( 'Get Advanced Cookie Consent Features', 'wpforms-lite' ), esc_html__( 'With WPConsent Pro you can access advanced features like geolocation, popup layout, records of consent, multilanguage support, and more.', 'wpforms-lite' ), esc_attr( $step['button_class'] ), esc_url( $step['button_url'] ), esc_html( $step['button_text'] ) ); } /** * Step 'Result' data. * * @since 1.9.7.3 * * @return array Step data. * @noinspection PhpUndefinedFunctionInspection */ protected function get_data_step_result(): array { // phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh $step = []; $step['icon'] = 'step-3.svg'; $step['section_class'] = $this->output_data['plugin_setup'] ? '' : 'grey'; $step['button_text'] = esc_html__( 'Learn More', 'wpforms-lite' ); $step['button_class'] = 'grey disabled'; $step['button_url'] = ''; $plugin_license_level = $this->get_license_level(); switch ( $plugin_license_level ) { case 'lite': $step['button_url'] = $this->config['wpconsent_addon_page']; $step['button_class'] = $this->output_data['plugin_setup'] ? 'button-primary' : 'grey disabled'; break; case 'pro': $addon_installed = array_key_exists( $this->config['wpconsent_addon'], $this->output_data['all_plugins'] ); $step['button_text'] = $addon_installed ? esc_html__( 'WPConsent Pro Installed & Activated', 'wpforms-lite' ) : esc_html__( 'Install Now', 'wpforms-lite' ); $step['button_class'] = $this->output_data['plugin_setup'] ? 'grey disabled' : 'button-primary'; $step['icon'] = $addon_installed ? 'step-complete.svg' : 'step-3.svg'; break; } return $step; } /** * Retrieve the license level of the plugin. * * @since 1.9.8.6 * * @return string The plugin license level ('lite' or 'pro'). */ protected function get_license_level(): string { $plugin_license_level = 'lite'; // Check if premium features are available. if ( function_exists( 'wpconsent' ) ) { $wpconsent = wpconsent(); if ( isset( $wpconsent->license ) && method_exists( $wpconsent->license, 'is_active' ) ) { $plugin_license_level = $wpconsent->license->is_active() ? 'pro' : 'lite'; } } return $plugin_license_level; } /** * Whether the plugin is finished setup or not. * * @since 1.9.8.6 */ protected function is_plugin_finished_setup(): bool { if ( ! $this->is_plugin_configured() ) { return false; } return $this->get_license_level() === 'pro'; } /** * Set the source of the plugin installation. * * @since 1.9.8 * @deprecated 1.9.8.6 * * @param string $plugin_basename The basename of the plugin. */ public function privacy_compliance_activated( string $plugin_basename ): void { $this->plugin_activated( $plugin_basename ); } /** * Whether a plugin is configured or not. * * @since 1.9.7.3 * * @return bool True if plugin is configured properly. * @noinspection PhpUndefinedFunctionInspection */ protected function is_plugin_configured(): bool { if ( ! $this->is_plugin_activated() ) { return false; } // Check if WPConsent has been configured with basic settings. // The plugin is considered configured if the consent banner is enabled. if ( function_exists( 'wpconsent' ) ) { $wpconsent = wpconsent(); if ( isset( $wpconsent->settings ) ) { $enable_consent_banner = $wpconsent->settings->get_option( 'enable_consent_banner', 0 ); return ! empty( $enable_consent_banner ); } } return false; } /** * Whether a plugin is active or not. * * @since 1.9.7.3 * * @return bool True if plugin is active. */ protected function is_plugin_activated(): bool { return ( function_exists( 'wpconsent' ) && ( is_plugin_active( $this->config['lite_plugin'] ) || is_plugin_active( $this->config['pro_plugin'] ) ) ); } /** * Whether a plugin is available (class/function exists). * * @since 1.9.7.3 * * @return bool True if plugin is available. */ protected function is_plugin_available(): bool { return function_exists( 'wpconsent' ); } /** * Whether pro version is active. * * @since 1.9.7.3 * * @return bool True if pro version is active. * @noinspection PhpUndefinedFunctionInspection */ protected function is_pro_active(): bool { if ( ! function_exists( 'wpconsent' ) ) { return false; } $wpconsent = wpconsent(); return isset( $wpconsent->license ) && method_exists( $wpconsent->license, 'is_active' ) && $wpconsent->license->is_active(); } /** * Get the heading for the install step. * * @since 1.9.7.3 * * @return string Install step heading. */ protected function get_install_heading(): string { return esc_html__( 'Install & Activate WPConsent', 'wpforms-lite' ); } /** * Get the description for the install step. * * @since 1.9.7.3 * * @return string Install step description. */ protected function get_install_description(): string { return esc_html__( 'Install WPConsent from the WordPress.org plugin repository.', 'wpforms-lite' ); } /** * Get the plugin title. * * @since 1.9.7.3 * * @return string Plugin title. */ protected function get_plugin_title(): string { return esc_html__( 'WPConsent', 'wpforms-lite' ); } /** * Get the install button text. * * @since 1.9.7.3 * * @return string Install button text. */ protected function get_install_button_text(): string { return esc_html__( 'Install WPConsent', 'wpforms-lite' ); } /** * Get the text when a plugin is installed and activated. * * @since 1.9.7.3 * * @return string Installed & activated text. */ protected function get_installed_activated_text(): string { return esc_html__( 'WPConsent Installed & Activated', 'wpforms-lite' ); } /** * Get the activate button text. * * @since 1.9.7.3 * * @return string Activate button text. */ protected function get_activate_text(): string { return esc_html__( 'Activate WPConsent', 'wpforms-lite' ); } /** * Get the heading for the setup step. * * @since 1.9.7.3 * * @return string Setup step heading. */ protected function get_setup_heading(): string { return esc_html__( 'Set Up WPConsent', 'wpforms-lite' ); } /** * Get the description for the setup step. * * @since 1.9.7.3 * * @return string Setup step description. */ protected function get_setup_description(): string { return esc_html__( 'WPConsent has an intuitive setup wizard to guide you through the cookie consent configuration process.', 'wpforms-lite' ); } /** * Get the setup button text. * * @since 1.9.7.3 * * @return string Setup button text. */ protected function get_setup_button_text(): string { return esc_html__( 'Run Setup Wizard', 'wpforms-lite' ); } /** * Get the text when setup is completed. * * @since 1.9.7.3 * * @return string Setup completed text. */ protected function get_setup_completed_text(): string { return esc_html__( 'Setup Complete', 'wpforms-lite' ); } /** * Get the text when a pro-version is installed and activated. * * @since 1.9.7.3 * * @return string Pro installed and activated text. */ protected function get_pro_installed_activated_text(): string { return esc_html__( 'WPConsent Pro Installed & Activated', 'wpforms-lite' ); } } Admin/Pages/SugarCalendar.php 0000644 00000026325 15174710275 0012100 0 ustar 00 <?php namespace WPForms\Admin\Pages; /** * Sugar Calendar Subpage. * * @since 1.9.8.6 */ class SugarCalendar extends Page { /** * Admin menu page slug. * * @since 1.9.8.6 * * @var string */ public const SLUG = 'wpforms-sugar-calendar'; /** * Configuration. * * @since 1.9.8.6 * * @var array */ protected $config = [ 'lite_plugin' => 'sugar-calendar-lite/sugar-calendar-lite.php', 'lite_wporg_url' => 'https://wordpress.org/plugins/sugar-calendar-lite/', 'lite_download_url' => 'https://downloads.wordpress.org/plugin/sugar-calendar-lite.zip', 'pro_plugin' => 'sugar-calendar/sugar-calendar.php', 'sugar-calendar_addon' => 'sugar-calendar/sugar-calendar.php', 'sugar-calendar_addon_page' => 'https://sugarcalendar.com/?utm_source=wpformsplugin&utm_medium=link&utm_campaign=sugar-calendar-page', 'sugar-calendar_onboarding' => 'post-new.php?post_type=sc_event', ]; /** * Hooks. * * @since 1.9.8.6 */ public function hooks(): void { if ( wp_doing_ajax() ) { add_filter( 'default_option_sugar_calendar_prevent_redirect', '__return_true' ); } parent::hooks(); } /** * Get the plugin name for use in IDs, CSS classes, and config keys. * * @since 1.9.8.6 * * @return string Plugin name. */ protected static function get_plugin_name(): string { return 'sugar-calendar'; } /** * Get heading title text. * * @since 1.9.8.6 * * @return string Heading title. */ protected function get_heading_title(): string { return esc_html__( 'Taking Bookings? Put Them on a Calendar', 'wpforms-lite' ); } /** * Get heading alt text for logo. * * @since 1.9.8.6 * * @return string Heading alt text. */ protected function get_heading_alt_text(): string { return esc_attr__( 'WPForms ♥ Sugar Calendar', 'wpforms-lite' ); } /** * Get heading description strings. * * @since 1.9.8.6 * * @return array Array of description strings. */ protected function get_heading_strings(): array { return [ esc_html__( 'WPForms collects the "yes." Sugar Calendar shows the "when and where."', 'wpforms-lite' ), esc_html__( 'Together, they turn bookings into events your visitors can browse, sync, and show up for.', 'wpforms-lite' ), esc_html__( 'Simple, elegant, and built for your workflow.', 'wpforms-lite' ), ]; } /** * Get screenshot features list. * * @since 1.9.8.6 * * @return array Array of feature strings. */ protected function get_screenshot_features(): array { return [ esc_html__( 'Display events on beautiful calendars visitors can browse and filter.', 'wpforms-lite' ), esc_html__( 'Sell tickets with Stripe or WooCommerce integration.', 'wpforms-lite' ), esc_html__( 'Visitors can add events to Google, Apple, or Outlook calendars with one click.', 'wpforms-lite' ), esc_html__( 'Set up recurring events: daily, weekly, monthly, or custom patterns.', 'wpforms-lite' ), ]; } /** * Get screenshot alt text. * * @since 1.9.8.6 * * @return string Alt text for screenshot image. */ protected function get_screenshot_alt_text(): string { return esc_attr__( 'Sugar Calendar screenshot', 'wpforms-lite' ); } /** * Generate and output step 'Result' section HTML. * * @since 1.9.8.6 * * @noinspection HtmlUnknownTarget */ protected function output_section_step_result(): void { $step = $this->get_data_step_result(); if ( empty( $step ) ) { return; } printf( '<section class="step step-result %1$s"> <aside class="num"> <img src="%2$s" alt="%3$s" /> <i class="loader hidden"></i> </aside> <div> <h2>%4$s</h2> <p>%5$s</p> <button class="button %6$s" data-url="%7$s">%8$s</button> </div> </section>', esc_attr( $step['section_class'] ), esc_url( WPFORMS_PLUGIN_URL . 'assets/images/' . $step['icon'] ), esc_attr__( 'Step 3', 'wpforms-lite' ), esc_html__( 'Display Events on Your Site', 'wpforms-lite' ), esc_html__( 'Use the Calendar block or shortcode [sc_events_calendar] to embed events anywhere on your site.', 'wpforms-lite' ), esc_attr( $step['button_class'] ), esc_url( $step['button_url'] ), esc_html( $step['button_text'] ) ); } /** * Generate and output footer section HTML. * * @since 1.9.8.6 */ protected function output_section_footer(): void { printf( '<section class="bottom"> <p>%s</p> </section>', esc_html__( 'From the same team trusted by over 6 million sites.', 'wpforms-lite' ) ); } /** * Step 'Result' data. * * @since 1.9.8.6 * * @return array Step data. */ protected function get_data_step_result(): array { $step = $this->get_default_step_data(); $plugin_license_level = $this->get_plugin_license_level(); if ( $plugin_license_level === 'lite' ) { $this->apply_lite_step_data( $step ); } elseif ( $plugin_license_level === 'pro' ) { $this->apply_pro_step_data( $step ); } return $step; } /** * Whether the plugin is finished setup or not. * * @since 1.9.8.6 */ protected function is_plugin_finished_setup(): bool { if ( ! $this->is_plugin_configured() ) { return false; } return $this->get_plugin_license_level() === 'pro'; } /** * Get default step data. * * @since 1.9.8.6 * * @return array Default step data. */ private function get_default_step_data(): array { return [ 'icon' => 'step-3.svg', 'section_class' => $this->output_data['plugin_setup'] ? '' : 'grey', 'button_text' => esc_html__( 'Learn More', 'wpforms-lite' ), 'button_class' => 'grey disabled', 'button_url' => '', ]; } /** * Get plugin license level. * * @since 1.9.8.6 * * @return string License level ('lite', 'pro') or false if not activated. */ private function get_plugin_license_level(): string { if ( ! function_exists( 'sugar_calendar' ) ) { return 'lite'; } $sugar_calendar = sugar_calendar(); return $sugar_calendar->__get( 'is_pro' ) ? 'pro' : 'lite'; } /** * Apply lite version step data. * * @since 1.9.8.6 * * @param array $step Step data array (passed by reference). */ private function apply_lite_step_data( array &$step ): void { $step['button_url'] = $this->config['sugar-calendar_addon_page']; $step['button_class'] = $this->output_data['plugin_setup'] ? 'button-primary' : 'grey disabled'; } /** * Apply pro version step data. * * @since 1.9.8.6 * * @param array $step Step data array (passed by reference). */ private function apply_pro_step_data( array &$step ): void { $addon_installed = array_key_exists( $this->config['sugar-calendar_addon'], $this->output_data['all_plugins'] ); $configured = $this->is_plugin_configured(); $step['button_text'] = $addon_installed && $configured ? esc_html__( 'Sugar Calendar Pro Installed & Activated', 'wpforms-lite' ) : esc_html__( 'Install Now', 'wpforms-lite' ); $step['button_class'] = $this->output_data['plugin_setup'] || ! $configured ? 'grey disabled' : 'button-primary'; $step['icon'] = $addon_installed && $configured ? 'step-complete.svg' : 'step-3.svg'; } /** * Whether a plugin is configured or not. * * @since 1.9.8.6 * * @return bool True if plugin is configured properly. */ protected function is_plugin_configured(): bool { if ( ! $this->is_plugin_activated() ) { return false; } $events = get_posts( [ 'post_type' => 'sc_event', 'post_status' => 'any', 'posts_per_page' => 1, 'fields' => 'ids', ] ); return ! empty( $events ); } /** * Whether a plugin is active or not. * * @since 1.9.8.6 * * @return bool True if the plugin is active. */ protected function is_plugin_activated(): bool { return ( ( function_exists( 'sugar_calendar' ) || class_exists( 'Sugar_Calendar\Plugin' ) ) && ( is_plugin_active( $this->config['lite_plugin'] ) || is_plugin_active( $this->config['pro_plugin'] ) ) ); } /** * Whether a plugin is available (class/function exists). * * @since 1.9.8.6 * * @return bool True if plugin is available. */ protected function is_plugin_available(): bool { return class_exists( 'Sugar_Calendar\Plugin' ) || function_exists( 'sugar_calendar' ); } /** * Whether pro version is active. * * @since 1.9.8.6 * * @return bool True if pro version is active. */ protected function is_pro_active(): bool { if ( ! function_exists( 'sugar_calendar' ) ) { return false; } return sugar_calendar()->is_pro(); } /** * Get the heading for the install step. * * @since 1.9.8.6 * * @return string Install step heading. */ protected function get_install_heading(): string { return esc_html__( 'Install and Activate Sugar Calendar', 'wpforms-lite' ); } /** * Get the description for the install step. * * @since 1.9.8.6 * * @return string Install step description. */ protected function get_install_description(): string { return esc_html__( 'Bring your forms to life. Install Sugar Calendar and start creating events.', 'wpforms-lite' ); } /** * Get the plugin title. * * @since 1.9.8.6 * * @return string Plugin title. */ protected function get_plugin_title(): string { return esc_html__( 'Sugar Calendar', 'wpforms-lite' ); } /** * Get the install button text. * * @since 1.9.8.6 * * @return string Install button text. */ protected function get_install_button_text(): string { return esc_html__( 'Install Sugar Calendar', 'wpforms-lite' ); } /** * Get the text when a plugin is installed and activated. * * @since 1.9.8.6 * * @return string Installed & activated text. */ protected function get_installed_activated_text(): string { return esc_html__( 'Sugar Calendar Installed & Activated', 'wpforms-lite' ); } /** * Get the activate button text. * * @since 1.9.8.6 * * @return string Activate button text. */ protected function get_activate_text(): string { return esc_html__( 'Activate Sugar Calendar', 'wpforms-lite' ); } /** * Get the heading for the setup step. * * @since 1.9.8.6 * * @return string Setup step heading. */ protected function get_setup_heading(): string { return esc_html__( 'Create Your First Event', 'wpforms-lite' ); } /** * Get the description for the setup step. * * @since 1.9.8.6 * * @return string Setup step description. */ protected function get_setup_description(): string { return esc_html__( 'Add your first booking or class to your calendar in seconds. Clean, simple, and built right into WordPress.', 'wpforms-lite' ); } /** * Get the setup button text. * * @since 1.9.8.6 * * @return string Setup button text. */ protected function get_setup_button_text(): string { return esc_html__( 'Add First Event', 'wpforms-lite' ); } /** * Get the text when setup is completed. * * @since 1.9.8.6 * * @return string Setup completed text. */ protected function get_setup_completed_text(): string { return esc_html__( 'Event Created', 'wpforms-lite' ); } /** * Get the text when a pro-version is installed and activated. * * @since 1.9.8.6 * * @return string Pro installed and activated text. */ protected function get_pro_installed_activated_text(): string { return esc_html__( 'Sugar Calendar Pro Installed & Activated', 'wpforms-lite' ); } } Admin/Pages/SMTP.php 0000644 00000040044 15174710275 0010142 0 ustar 00 <?php namespace WPForms\Admin\Pages; /** * SMTP Sub-page. * * @since 1.5.7 */ class SMTP { /** * Admin menu page slug. * * @since 1.5.7 * * @var string */ const SLUG = 'wpforms-smtp'; /** * Configuration. * * @since 1.5.7 * * @var array */ private $config = [ 'lite_plugin' => 'wp-mail-smtp/wp_mail_smtp.php', 'lite_wporg_url' => 'https://wordpress.org/plugins/wp-mail-smtp/', 'lite_download_url' => 'https://downloads.wordpress.org/plugin/wp-mail-smtp.zip', 'pro_plugin' => 'wp-mail-smtp-pro/wp_mail_smtp.php', 'smtp_settings_url' => 'admin.php?page=wp-mail-smtp', 'smtp_wizard_url' => 'admin.php?page=wp-mail-smtp-setup-wizard', ]; /** * Runtime data used for generating page HTML. * * @since 1.5.7 * * @var array */ private $output_data = []; /** * Constructor. * * @since 1.5.7 */ public function __construct() { if ( ! wpforms_current_user_can() ) { return; } $this->hooks(); } /** * Hooks. * * @since 1.5.7 */ public function hooks() { if ( wp_doing_ajax() ) { add_action( 'wp_ajax_wpforms_smtp_page_check_plugin_status', [ $this, 'ajax_check_plugin_status' ] ); add_action( 'wpforms_plugin_activated', [ $this, 'smtp_activated' ] ); } // Check what page we are on. // phpcs:ignore WordPress.Security.NonceVerification.Recommended $page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : ''; // Only load if we are actually on the SMTP page. if ( $page !== self::SLUG ) { return; } add_action( 'admin_init', [ $this, 'redirect_to_smtp_settings' ] ); add_filter( 'wpforms_admin_header', '__return_false' ); add_action( 'wpforms_admin_page', [ $this, 'output' ] ); add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] ); // Hook for addons. do_action( 'wpforms_admin_pages_smtp_hooks' ); } /** * Enqueue JS and CSS files. * * @since 1.5.7 */ public function enqueue_assets() { $min = wpforms_get_min_suffix(); // Lity. wp_enqueue_style( 'wpforms-lity', WPFORMS_PLUGIN_URL . 'assets/lib/lity/lity.min.css', null, '3.0.0' ); wp_enqueue_script( 'wpforms-lity', WPFORMS_PLUGIN_URL . 'assets/lib/lity/lity.min.js', [ 'jquery' ], '3.0.0', true ); wp_enqueue_script( 'wpforms-admin-page-smtp', WPFORMS_PLUGIN_URL . "assets/js/admin/pages/smtp{$min}.js", [ 'jquery' ], WPFORMS_VERSION, true ); wp_localize_script( 'wpforms-admin-page-smtp', 'wpforms_pluginlanding', $this->get_js_strings() ); } /** * Set wp_mail_smtp_source option to 'wpforms' on WP Mail SMTP plugin activation. * * @since 1.8.7 * * @param string $plugin_basename Plugin basename. */ public function smtp_activated( $plugin_basename ) { if ( $plugin_basename !== $this->config['lite_plugin'] ) { return; } // If user came from some certain page to install WP Mail SMTP, we can get the source and write it instead of default one. $source = isset( $_POST['source'] ) ? sanitize_text_field( wp_unslash( $_POST['source'] ) ) : 'wpforms'; // phpcs:ignore WordPress.Security.NonceVerification.Missing update_option( 'wp_mail_smtp_source', $source ); } /** * JS Strings. * * @since 1.5.7 * * @return array Array of strings. */ protected function get_js_strings() { $error_could_not_install = sprintf( wp_kses( /* translators: %s - Lite plugin download URL. */ __( 'Could not install the plugin automatically. Please <a href="%s">download</a> it and install it manually.', 'wpforms-lite' ), [ 'a' => [ 'href' => true, ], ] ), esc_url( $this->config['lite_download_url'] ) ); $error_could_not_activate = sprintf( wp_kses( /* translators: %s - Lite plugin download URL. */ __( 'Could not activate the plugin. Please activate it on the <a href="%s">Plugins page</a>.', 'wpforms-lite' ), [ 'a' => [ 'href' => true, ], ] ), esc_url( admin_url( 'plugins.php' ) ) ); return [ 'installing' => esc_html__( 'Installing...', 'wpforms-lite' ), 'activating' => esc_html__( 'Activating...', 'wpforms-lite' ), 'activated' => esc_html__( 'WP Mail SMTP Installed & Activated', 'wpforms-lite' ), 'install_now' => esc_html__( 'Install Now', 'wpforms-lite' ), 'activate_now' => esc_html__( 'Activate Now', 'wpforms-lite' ), 'download_now' => esc_html__( 'Download Now', 'wpforms-lite' ), 'plugins_page' => esc_html__( 'Go to Plugins page', 'wpforms-lite' ), 'error_could_not_install' => $error_could_not_install, 'error_could_not_activate' => $error_could_not_activate, 'manual_install_url' => $this->config['lite_download_url'], 'manual_activate_url' => admin_url( 'plugins.php' ), 'smtp_settings' => esc_html__( 'Go to SMTP settings', 'wpforms-lite' ), 'smtp_wizard' => esc_html__( 'Open Setup Wizard', 'wpforms-lite' ), 'smtp_settings_url' => esc_url( $this->config['smtp_settings_url'] ), 'smtp_wizard_url' => esc_url( $this->config['smtp_wizard_url'] ), ]; } /** * Generate and output page HTML. * * @since 1.5.7 */ public function output() { echo '<div id="wpforms-admin-smtp" class="wrap wpforms-admin-wrap wpforms-admin-plugin-landing">'; $this->output_section_heading(); $this->output_section_screenshot(); $this->output_section_step_install(); $this->output_section_step_setup(); echo '</div>'; } /** * Generate and output heading section HTML. * * @since 1.5.7 */ protected function output_section_heading() { // Heading section. printf( '<section class="top"> <img class="img-top" src="%1$s" srcset="%2$s 2x" alt="%3$s"/> <h1>%4$s</h1> <p>%5$s</p> </section>', esc_url( WPFORMS_PLUGIN_URL . 'assets/images/smtp/wpforms-wpmailsmtp.png' ), esc_url( WPFORMS_PLUGIN_URL . 'assets/images/smtp/wpforms-wpmailsmtp@2x.png' ), esc_attr__( 'WPForms ♥ WP Mail SMTP', 'wpforms-lite' ), esc_html__( 'Making Email Deliverability Easy for WordPress', 'wpforms-lite' ), esc_html__( 'WP Mail SMTP fixes deliverability problems with your WordPress emails and form notifications. It\'s built by the same folks behind WPForms.', 'wpforms-lite' ) ); } /** * Generate and output screenshot section HTML. * * @since 1.5.7 */ protected function output_section_screenshot() { // Screenshot section. printf( '<section class="screenshot"> <div class="cont"> <img src="%1$s" alt="%2$s"/> <a href="%3$s" class="hover" data-lity></a> </div> <ul> <li>%4$s</li> <li>%5$s</li> <li>%6$s</li> <li>%7$s</li> </ul> </section>', esc_url( WPFORMS_PLUGIN_URL . 'assets/images/smtp/screenshot-tnail.png?ver=' . WPFORMS_VERSION ), esc_attr__( 'WP Mail SMTP screenshot', 'wpforms-lite' ), esc_url( WPFORMS_PLUGIN_URL . 'assets/images/smtp/screenshot-full.png?ver=' . WPFORMS_VERSION ), esc_html__( 'Improves email deliverability in WordPress.', 'wpforms-lite' ), esc_html__( 'Used by 4+ million websites.', 'wpforms-lite' ), esc_html__( 'Free mailers: SendLayer, SMTP.com, Brevo, Google Workspace / Gmail, Mailgun, Postmark, SendGrid.', 'wpforms-lite' ), esc_html__( 'Pro mailers: Amazon SES, Microsoft 365 / Outlook.com, Zoho Mail.', 'wpforms-lite' ) ); } /** * Generate and output step 'Install' section HTML. * * @since 1.5.7 */ protected function output_section_step_install() { $step = $this->get_data_step_install(); if ( empty( $step ) ) { return; } $button_format = '<button class="button %3$s" data-plugin="%1$s" data-action="%4$s" data-source="%5$s">%2$s</button>'; $button_allowed_html = [ 'button' => [ 'class' => true, 'data-plugin' => true, 'data-action' => true, 'data-source' => true, ], ]; if ( ! $this->output_data['plugin_installed'] && ! $this->output_data['pro_plugin_installed'] && ! wpforms_can_install( 'plugin' ) ) { $button_format = '<a class="link" href="%1$s" target="_blank" rel="nofollow noopener">%2$s <span aria-hidden="true" class="dashicons dashicons-external"></span></a>'; $button_allowed_html = [ 'a' => [ 'class' => true, 'href' => true, 'target' => true, 'rel' => true, ], 'span' => [ 'class' => true, 'aria-hidden' => true, ], ]; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended $source = isset( $_GET['source'] ) && $_GET['source'] === 'woocommerce' ? 'wpforms-woocommerce' : 'wpforms'; $button = sprintf( $button_format, esc_attr( $step['plugin'] ), esc_html( $step['button_text'] ), esc_attr( $step['button_class'] ), esc_attr( $step['button_action'] ), esc_attr( $source ) ); printf( '<section class="step step-install"> <aside class="num"> <img src="%1$s" alt="%2$s" /> <i class="loader hidden"></i> </aside> <div> <h2>%3$s</h2> <p>%4$s</p> %5$s </div> </section>', esc_url( WPFORMS_PLUGIN_URL . 'assets/images/' . $step['icon'] ), esc_attr__( 'Step 1', 'wpforms-lite' ), esc_html( $step['heading'] ), esc_html( $step['description'] ), wp_kses( $button, $button_allowed_html ) ); } /** * Generate and output step 'Setup' section HTML. * * @since 1.5.7 */ protected function output_section_step_setup() { $step = $this->get_data_step_setup(); if ( empty( $step ) ) { return; } printf( '<section class="step step-setup %1$s"> <aside class="num"> <img src="%2$s" alt="%3$s" /> <i class="loader hidden"></i> </aside> <div> <h2>%4$s</h2> <p>%5$s</p> <button class="button %6$s" data-url="%7$s">%8$s</button> </div> </section>', esc_attr( $step['section_class'] ), esc_url( WPFORMS_PLUGIN_URL . 'assets/images/' . $step['icon'] ), esc_attr__( 'Step 2', 'wpforms-lite' ), esc_html__( 'Set Up WP Mail SMTP', 'wpforms-lite' ), esc_html__( 'Select and configure your mailer.', 'wpforms-lite' ), esc_attr( $step['button_class'] ), esc_url( admin_url( $this->config['smtp_wizard_url'] ) ), esc_html( $step['button_text'] ) ); } /** * Step 'Install' data. * * @since 1.5.7 * * @return array Step data. */ protected function get_data_step_install() { $step = []; $step['heading'] = esc_html__( 'Install and Activate WP Mail SMTP', 'wpforms-lite' ); $step['description'] = esc_html__( 'Install WP Mail SMTP from the WordPress.org plugin repository.', 'wpforms-lite' ); $this->output_data['all_plugins'] = get_plugins(); $this->output_data['plugin_installed'] = array_key_exists( $this->config['lite_plugin'], $this->output_data['all_plugins'] ); $this->output_data['pro_plugin_installed'] = array_key_exists( $this->config['pro_plugin'], $this->output_data['all_plugins'] ); $this->output_data['plugin_activated'] = false; $this->output_data['plugin_setup'] = false; if ( ! $this->output_data['plugin_installed'] && ! $this->output_data['pro_plugin_installed'] ) { $step['icon'] = 'step-1.svg'; $step['button_text'] = esc_html__( 'Install WP Mail SMTP', 'wpforms-lite' ); $step['button_class'] = 'button-primary'; $step['button_action'] = 'install'; $step['plugin'] = $this->config['lite_download_url']; if ( ! wpforms_can_install( 'plugin' ) ) { $step['heading'] = esc_html__( 'WP Mail SMTP', 'wpforms-lite' ); $step['description'] = ''; $step['button_text'] = esc_html__( 'WP Mail SMTP on WordPress.org', 'wpforms-lite' ); $step['plugin'] = $this->config['lite_wporg_url']; } } else { $this->output_data['plugin_activated'] = $this->is_smtp_activated(); $this->output_data['plugin_setup'] = $this->is_smtp_configured(); $step['icon'] = $this->output_data['plugin_activated'] ? 'step-complete.svg' : 'step-1.svg'; $step['button_text'] = $this->output_data['plugin_activated'] ? esc_html__( 'WP Mail SMTP Installed & Activated', 'wpforms-lite' ) : esc_html__( 'Activate WP Mail SMTP', 'wpforms-lite' ); $step['button_class'] = $this->output_data['plugin_activated'] ? 'grey disabled' : 'button-primary'; $step['button_action'] = $this->output_data['plugin_activated'] ? '' : 'activate'; $step['plugin'] = $this->output_data['pro_plugin_installed'] ? $this->config['pro_plugin'] : $this->config['lite_plugin']; } return $step; } /** * Step 'Setup' data. * * @since 1.5.7 * * @return array Step data. */ protected function get_data_step_setup() { $step = [ 'icon' => 'step-2.svg', ]; if ( $this->output_data['plugin_activated'] ) { $step['section_class'] = ''; $step['button_class'] = 'button-primary'; $step['button_text'] = esc_html__( 'Open Setup Wizard', 'wpforms-lite' ); } else { $step['section_class'] = 'grey'; $step['button_class'] = 'grey disabled'; $step['button_text'] = esc_html__( 'Start Setup', 'wpforms-lite' ); } if ( $this->output_data['plugin_setup'] ) { $step['icon'] = 'step-complete.svg'; $step['button_text'] = esc_html__( 'Go to SMTP settings', 'wpforms-lite' ); } return $step; } /** * Ajax endpoint. Check plugin setup status. * Used to properly init step 'Setup' section after completing step 'Install'. * * @since 1.5.7 */ public function ajax_check_plugin_status() { // Security checks. if ( ! check_ajax_referer( 'wpforms-admin', 'nonce', false ) || ! wpforms_current_user_can() ) { wp_send_json_error( [ 'error' => esc_html__( 'You do not have permission.', 'wpforms-lite' ), ] ); } $result = []; if ( ! $this->is_smtp_activated() ) { wp_send_json_error( [ 'error' => esc_html__( 'Plugin unavailable.', 'wpforms-lite' ), ] ); } $result['setup_status'] = (int) $this->is_smtp_configured(); $result['license_level'] = wp_mail_smtp()->get_license_type(); // Prevent redirect to the WP Mail SMTP Setup Wizard on the fresh installs. // We need this workaround since WP Mail SMTP doesn't check whether the mailer is already configured when redirecting to the Setup Wizard on the first run. if ( $result['setup_status'] > 0 ) { update_option( 'wp_mail_smtp_activation_prevent_redirect', true ); } wp_send_json_success( $result ); } /** * Get $phpmailer instance. * * @since 1.5.7 * @since 1.6.1.2 Conditionally returns $phpmailer v5 or v6. * @since 1.8.7 Use always $phpmailer v6. * * @return \PHPMailer|\PHPMailer\PHPMailer\PHPMailer Instance of PHPMailer. */ protected function get_phpmailer() { global $phpmailer; if ( ! ( $phpmailer instanceof \PHPMailer\PHPMailer\PHPMailer ) ) { require_once ABSPATH . WPINC . '/PHPMailer/PHPMailer.php'; require_once ABSPATH . WPINC . '/PHPMailer/SMTP.php'; require_once ABSPATH . WPINC . '/PHPMailer/Exception.php'; $phpmailer = new \PHPMailer\PHPMailer\PHPMailer( true ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited } return $phpmailer; } /** * Whether WP Mail SMTP plugin configured or not. * * @since 1.5.7 * * @return bool True if some mailer is selected and configured properly. */ protected function is_smtp_configured() { if ( ! $this->is_smtp_activated() ) { return false; } $phpmailer = $this->get_phpmailer(); $mailer = \WPMailSMTP\Options::init()->get( 'mail', 'mailer' ); return ! empty( $mailer ) && $mailer !== 'mail' && wp_mail_smtp()->get_providers()->get_mailer( $mailer, $phpmailer )->is_mailer_complete(); } /** * Whether WP Mail SMTP plugin active or not. * * @since 1.5.7 * * @return bool True if SMTP plugin is active. */ protected function is_smtp_activated() { return function_exists( 'wp_mail_smtp' ) && ( is_plugin_active( $this->config['lite_plugin'] ) || is_plugin_active( $this->config['pro_plugin'] ) ); } /** * Redirect to SMTP settings page. * * @since 1.5.7 */ public function redirect_to_smtp_settings() { // Redirect to SMTP plugin if it is activated. if ( $this->is_smtp_configured() ) { wp_safe_redirect( admin_url( $this->config['smtp_settings_url'] ) ); exit; } } } Admin/Pages/Page.php 0000644 00000044710 15174710275 0010237 0 ustar 00 <?php namespace WPForms\Admin\Pages; /** * Abstract class for admin pages. * * @since 1.9.8.6 */ abstract class Page { /** * Admin menu page slug. * * @since 1.9.8.6 * * @var string */ public const SLUG = ''; /** * Configuration. * * @since 1.9.8.6 * * @var array */ protected $config = []; /** * Runtime data used for generating page HTML. * * @since 1.9.8.6 * * @var array */ protected $output_data = []; /** * Constructor. * * @since 1.9.8.6 */ public function __construct() { if ( ! wpforms_current_user_can() ) { return; } $this->hooks(); } /** * Hooks. * * @since 1.9.8.6 */ public function hooks(): void { $plugin = static::get_plugin_name(); if ( wp_doing_ajax() ) { add_action( "wp_ajax_wpforms_page_check_{$plugin}_status", [ $this, 'ajax_check_plugin_status' ] ); add_action( 'wpforms_plugin_activated', [ $this, 'plugin_activated' ] ); } // Check what page we are on. // phpcs:ignore WordPress.Security.NonceVerification.Recommended $page = isset( $_GET['page'] ) ? sanitize_key( $_GET['page'] ) : ''; // Only load if we are actually on the correct page. if ( $page !== static::SLUG ) { return; } add_filter( 'wpforms_admin_header', '__return_false' ); add_action( 'wpforms_admin_page', [ $this, 'output' ] ); add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] ); /** * Hook for addons. * * @since 1.9.8.6 */ do_action( 'wpforms_admin_pages_page_' . static::get_plugin_name() . '_hooks' ); } /** * Enqueue JS and CSS files. * * @since 1.9.8.6 */ public function enqueue_assets(): void { $min = wpforms_get_min_suffix(); // Lity. wp_enqueue_style( 'wpforms-lity', WPFORMS_PLUGIN_URL . 'assets/lib/lity/lity.min.css', null, '3.0.0' ); wp_enqueue_script( 'wpforms-lity', WPFORMS_PLUGIN_URL . 'assets/lib/lity/lity.min.js', [ 'jquery' ], '3.0.0', true ); // Custom styles for Lity image size limitation. wp_add_inline_style( 'wpforms-lity', ' .lity-image .lity-container { max-width: 1040px !important; } .lity-image img { max-width: 1040px !important; width: 100%; height: auto; } ' ); wp_enqueue_script( 'wpforms-admin-page-' . static::get_plugin_name(), WPFORMS_PLUGIN_URL . "assets/js/admin/pages/common{$min}.js", [ 'jquery' ], WPFORMS_VERSION, true ); wp_localize_script( 'wpforms-admin-page-' . static::get_plugin_name(), 'wpforms_pluginlanding', $this->get_js_strings() ); } /** * Generate and output page HTML. * * @since 1.9.8.6 */ public function output(): void { echo '<div id="wpforms-admin-' . esc_attr( static::get_plugin_name() ) . '" class="wrap wpforms-admin-wrap wpforms-admin-plugin-landing">'; $this->output_section_heading(); $this->output_section_screenshot(); $this->output_section_footer(); $this->output_section_step_install(); $this->output_section_step_setup(); $this->output_section_step_result(); echo '</div>'; } /** * Generate and output step 'Install' section HTML. * * @since 1.9.8.6 * * @noinspection HtmlUnknownTarget */ protected function output_section_step_install(): void { $step = $this->get_data_step_install(); if ( empty( $step ) ) { return; } $button_format = '<button class="button %3$s" data-plugin="%1$s" data-action="%4$s" data-provider="%5$s">%2$s</button>'; $button_allowed_html = [ 'button' => [ 'class' => true, 'data-plugin' => true, 'data-action' => true, 'data-provider' => true, ], ]; if ( ! $this->output_data['plugin_installed'] && ! $this->output_data['pro_plugin_installed'] && ! wpforms_can_install( 'plugin' ) ) { $button_format = '<a class="link" href="%1$s" target="_blank" rel="nofollow noopener">%2$s <span aria-hidden="true" class="dashicons dashicons-external"></span></a>'; $button_allowed_html = [ 'a' => [ 'class' => true, 'href' => true, 'target' => true, 'rel' => true, ], 'span' => [ 'class' => true, 'aria-hidden' => true, ], ]; } $plugin_attr = wpforms_is_url( $step['plugin'] ) ? esc_url( $step['plugin'] ) : esc_attr( $step['plugin'] ); $button = sprintf( $button_format, $plugin_attr, esc_html( $step['button_text'] ), esc_attr( $step['button_class'] ), esc_attr( $step['button_action'] ), esc_attr( static::get_plugin_name() ) ); printf( '<section class="step step-install"> <aside class="num"> <img src="%1$s" alt="%2$s" /> <i class="loader hidden"></i> </aside> <div> <h2>%3$s</h2> <p>%4$s</p> %5$s </div> </section>', esc_url( WPFORMS_PLUGIN_URL . 'assets/images/' . $step['icon'] ), esc_attr__( 'Step 1', 'wpforms-lite' ), esc_html( $step['heading'] ), esc_html( $step['description'] ), wp_kses( $button, $button_allowed_html ) ); } /** * Generate and output step 'Setup' section HTML. * * @since 1.9.8.6 * * @noinspection HtmlUnknownTarget */ protected function output_section_step_setup(): void { $step = $this->get_data_step_setup(); if ( empty( $step ) ) { return; } printf( '<section class="step step-setup %1$s"> <aside class="num"> <img src="%2$s" alt="%3$s" /> <i class="loader hidden"></i> </aside> <div> <h2>%4$s</h2> <p>%5$s</p> <button class="button %6$s" data-url="%7$s">%8$s</button> </div> </section>', esc_attr( $step['section_class'] ), esc_url( WPFORMS_PLUGIN_URL . 'assets/images/' . $step['icon'] ), esc_attr__( 'Step 2', 'wpforms-lite' ), esc_html( $step['heading'] ), esc_html( $step['description'] ), esc_attr( $step['button_class'] ), esc_url( admin_url( $this->config[ static::get_plugin_name() . '_onboarding' ] ) ), esc_html( $step['button_text'] ) ); } /** * Generate and output footer section HTML. * * @since 1.9.8.6 */ protected function output_section_footer(): void { // Default implementation - can be overridden by child classes. } /** * Step 'Install' data. * * @since 1.9.8.6 * * @return array Step data. */ protected function get_data_step_install(): array { $step = []; $step['heading'] = $this->get_install_heading(); $step['description'] = $this->get_install_description(); $this->output_data['all_plugins'] = get_plugins(); $this->output_data['plugin_installed'] = array_key_exists( $this->config['lite_plugin'], $this->output_data['all_plugins'] ); $this->output_data['plugin_activated'] = false; $this->output_data['pro_plugin_installed'] = array_key_exists( $this->config['pro_plugin'], $this->output_data['all_plugins'] ); $this->output_data['pro_plugin_activated'] = false; if ( ! $this->output_data['plugin_installed'] && ! $this->output_data['pro_plugin_installed'] ) { $step['icon'] = 'step-1.svg'; $step['button_text'] = $this->get_install_button_text(); $step['button_class'] = 'button-primary'; $step['button_action'] = 'install'; $step['plugin'] = $this->config['lite_download_url']; if ( ! wpforms_can_install( 'plugin' ) ) { $step['heading'] = $this->get_plugin_title(); $step['description'] = ''; $step['button_text'] = $this->get_plugin_title() . ' on WordPress.org'; $step['plugin'] = $this->config['lite_wporg_url']; } } else { $this->output_data['plugin_activated'] = is_plugin_active( $this->config['lite_plugin'] ) || is_plugin_active( $this->config['pro_plugin'] ); $step['icon'] = $this->output_data['plugin_activated'] ? 'step-complete.svg' : 'step-1.svg'; $step['button_text'] = $this->output_data['plugin_activated'] ? $this->get_installed_activated_text() : $this->get_activate_text(); $step['button_class'] = $this->output_data['plugin_activated'] ? 'grey disabled' : 'button-primary'; $step['button_action'] = $this->output_data['plugin_activated'] ? '' : 'activate'; $step['plugin'] = $this->output_data['pro_plugin_installed'] ? $this->config['pro_plugin'] : $this->config['lite_plugin']; $step['is_pro'] = $this->output_data['pro_plugin_installed']; } return $step; } /** * Step 'Setup' data. * * @since 1.9.8.6 * * @return array Step data. */ protected function get_data_step_setup(): array { $step = []; $this->output_data['plugin_setup'] = false; if ( $this->output_data['plugin_activated'] ) { $this->output_data['plugin_setup'] = $this->is_plugin_configured(); } $step['icon'] = 'step-2.svg'; $step['section_class'] = $this->output_data['plugin_activated'] ? '' : 'grey'; $step['heading'] = $this->get_setup_heading(); $step['description'] = $this->get_setup_description(); $step['button_text'] = $this->get_setup_button_text(); $step['button_class'] = 'grey disabled'; if ( $this->output_data['plugin_setup'] ) { $step['icon'] = 'step-complete.svg'; $step['section_class'] = ''; $step['button_text'] = $this->get_setup_completed_text(); } else { $step['button_class'] = $this->output_data['plugin_activated'] ? 'button-primary' : 'grey disabled'; } return $step; } /** * Ajax endpoint. Check plugin setup status. * Used to properly init the step 2 section after completing step 1. * * @since 1.9.8.6 */ public function ajax_check_plugin_status(): void { // Security checks. if ( ! check_ajax_referer( 'wpforms-admin', 'nonce', false ) || ! wpforms_current_user_can() ) { wp_send_json_error( [ 'error' => esc_html__( 'You do not have permission.', 'wpforms-lite' ) ] ); } $result = []; if ( ! $this->is_plugin_available() ) { wp_send_json_error( [ 'error' => esc_html__( 'Plugin unavailable.', 'wpforms-lite' ) ] ); } $result['setup_status'] = (int) $this->is_plugin_configured(); $result['license_level'] = 'lite'; $result['step3_button_url'] = $this->config[ static::get_plugin_name() . '_addon_page' ]; if ( $this->is_pro_active() ) { $result['license_level'] = 'pro'; } $result['result_status'] = $this->is_plugin_finished_setup(); $result['addon_installed'] = (int) array_key_exists( $this->config[ static::get_plugin_name() . '_addon' ], get_plugins() ); wp_send_json_success( $result ); } /** * Set the source of the plugin installation. * * @since 1.9.8.6 * * @param string $plugin_basename The basename of the plugin. */ public function plugin_activated( string $plugin_basename ): void { if ( $plugin_basename !== $this->config['lite_plugin'] ) { return; } $source = wpforms()->is_pro() ? 'WPForms' : 'WPForms Lite'; update_option( static::get_plugin_name() . '_source', $source, false ); update_option( static::get_plugin_name() . '_date', time(), false ); } /** * JS strings. * * @since 1.9.8.6 * * @return array Array of strings. * @noinspection HtmlUnknownTarget */ protected function get_js_strings(): array { $error_could_not_install = sprintf( wp_kses( /* translators: %1$s - Lite plugin download URL. */ __( 'Could not install the plugin automatically. Please <a href="%1$s">download</a> it and install it manually.', 'wpforms-lite' ), [ 'a' => [ 'href' => true, ], ] ), esc_url( $this->config['lite_download_url'] ?? '' ) ); $error_could_not_activate = sprintf( wp_kses( /* translators: %1$s - Plugins page URL. */ __( 'Could not activate the plugin. Please activate it on the <a href="%1$s">Plugins page</a>.', 'wpforms-lite' ), [ 'a' => [ 'href' => true, ], ] ), esc_url( admin_url( 'plugins.php' ) ) ); return [ 'installing' => esc_html__( 'Installing...', 'wpforms-lite' ), 'activating' => esc_html__( 'Activating...', 'wpforms-lite' ), 'activated' => $this->get_installed_activated_text(), 'activated_pro' => $this->get_pro_installed_activated_text(), 'install_now' => esc_html__( 'Install Now', 'wpforms-lite' ), 'activate_now' => esc_html__( 'Activate Now', 'wpforms-lite' ), 'download_now' => esc_html__( 'Download Now', 'wpforms-lite' ), 'plugins_page' => esc_html__( 'Go to Plugins page', 'wpforms-lite' ), 'error_could_not_install' => $error_could_not_install, 'error_could_not_activate' => $error_could_not_activate, static::get_plugin_name() . '_manual_install_url' => $this->config['lite_download_url'], static::get_plugin_name() . '_manual_activate_url' => admin_url( 'plugins.php' ), ]; } /** * Get the plugin name for use in IDs, CSS classes, and config keys. * * @since 1.9.8.6 * * @return string Plugin name. */ abstract protected static function get_plugin_name(): string; /** * Generate and output heading section HTML. * * @since 1.9.8.6 * * @noinspection HtmlUnknownTarget */ public function output_section_heading(): void { $strings = $this->get_heading_strings(); // Heading section. printf( '<section class="top"> <img class="img-top" src="%1$s" alt="%2$s"/> <h1>%3$s</h1> <p>%4$s</p> </section>', esc_url( $this->get_heading_image_url() ), esc_attr( $this->get_heading_alt_text() ), esc_html( $this->get_heading_title() ), esc_html( implode( ' ', $strings ) ) ); } /** * Get heading image URL. * * @since 1.9.8.6 * * @return string Heading image URL. */ protected function get_heading_image_url(): string { return WPFORMS_PLUGIN_URL . 'assets/images/' . static::get_plugin_name() . '/wpforms-' . static::get_plugin_name() . '.svg'; } /** * Get heading title text. * * @since 1.9.8.6 * * @return string Heading title. */ abstract protected function get_heading_title(): string; /** * Get heading alt text for logo. * * @since 1.9.8.6 * * @return string Heading alt text. */ abstract protected function get_heading_alt_text(): string; /** * Get heading description strings. * * @since 1.9.8.6 * * @return array Array of description strings. */ abstract protected function get_heading_strings(): array; /** * Generate and output screenshot section HTML. * * @since 1.9.8.6 */ protected function output_section_screenshot(): void { $features = $this->get_screenshot_features(); $list = ''; foreach ( $features as $feature ) { $list .= '<li>' . esc_html( $feature ) . '</li>'; } // Screenshot section. printf( '<section class="screenshot"> <div class="cont"> <img src="%1$s" alt="%2$s" srcset="%4$s 2x"/> <a href="%3$s" class="hover" data-lity></a> </div> <ul>%5$s</ul> </section>', esc_url( WPFORMS_PLUGIN_URL . 'assets/images/' . static::get_plugin_name() . '/screenshot-tnail.png' ), esc_attr( $this->get_screenshot_alt_text() ), esc_url( WPFORMS_PLUGIN_URL . 'assets/images/' . static::get_plugin_name() . '/screenshot-full@2x.png' ), esc_url( WPFORMS_PLUGIN_URL . 'assets/images/' . static::get_plugin_name() . '/screenshot-tnail@2x.png' ), wp_kses( $list, [ 'li' => [] ] ) ); } /** * Get screenshot features list. * * @since 1.9.8.6 * * @return array Array of feature strings. */ abstract protected function get_screenshot_features(): array; /** * Get screenshot alt text. * * @since 1.9.8.6 * * @return string Alt text for screenshot image. */ abstract protected function get_screenshot_alt_text(): string; /** * Generate and output step 'Result' section HTML. * * @since 1.9.8.6 */ abstract protected function output_section_step_result(): void; /** * Whether a plugin is configured or not. * * @since 1.9.8.6 * * @return bool True if a plugin is configured properly. */ abstract protected function is_plugin_configured(): bool; /** * Whether a plugin is active or not. * * @since 1.9.8.6 * * @return bool True if the plugin is active. */ abstract protected function is_plugin_activated(): bool; /** * Whether a plugin is finished setup or not. * * @since 1.9.8.6 * * @return bool True if the plugin is finished setup. */ abstract protected function is_plugin_finished_setup(): bool; /** * Whether a plugin is available (class/function exists). * * @since 1.9.8.6 * * @return bool True if a plugin is available. */ abstract protected function is_plugin_available(): bool; /** * Whether a pro-version is active. * * @since 1.9.8.6 * * @return bool True if a pro-version is active. */ abstract protected function is_pro_active(): bool; /** * Get the heading for the installation step. * * @since 1.9.8.6 * * @return string Install step heading. */ abstract protected function get_install_heading(): string; /** * Get the description for the installation step. * * @since 1.9.8.6 * * @return string Install step description. */ abstract protected function get_install_description(): string; /** * Get the plugin title. * * @since 1.9.8.6 * * @return string Plugin title. */ abstract protected function get_plugin_title(): string; /** * Get the installation button text. * * @since 1.9.8.6 * * @return string Install button text. */ abstract protected function get_install_button_text(): string; /** * Get the text when a plugin is installed and activated. * * @since 1.9.8.6 * * @return string Installed & activated text. */ abstract protected function get_installed_activated_text(): string; /** * Get the activate button text. * * @since 1.9.8.6 * * @return string Activate button text. */ abstract protected function get_activate_text(): string; /** * Get the heading for the setup step. * * @since 1.9.8.6 * * @return string Setup step heading. */ abstract protected function get_setup_heading(): string; /** * Get the description for the setup step. * * @since 1.9.8.6 * * @return string Setup step description. */ abstract protected function get_setup_description(): string; /** * Get the setup button text. * * @since 1.9.8.6 * * @return string Setup button text. */ abstract protected function get_setup_button_text(): string; /** * Get the text when setup is completed. * * @since 1.9.8.6 * * @return string Setup completed text. */ abstract protected function get_setup_completed_text(): string; /** * Get the text when a pro-version is installed and activated. * * @since 1.9.8.6 * * @return string Pro installed and activated text. */ abstract protected function get_pro_installed_activated_text(): string; } Admin/Pages/ConstantContact.php 0000644 00000003115 15174710275 0012462 0 ustar 00 <?php namespace WPForms\Admin\Pages; /** * Constant Contact Sub-page. * * @since 1.7.3 */ class ConstantContact { /** * Determine if the class is allowed to be loaded. * * @since 1.7.3 */ private function allow_load() { return wpforms_is_admin_page( 'page', 'constant-contact' ); } /** * Initialize class. * * @since 1.7.3 */ public function init() { if ( ! $this->allow_load() ) { return; } $this->hooks(); } /** * Hooks. * * @since 1.7.3 */ private function hooks() { add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] ); add_action( 'wpforms_admin_page', [ $this, 'view' ] ); } /** * Enqueue JS and CSS files. * * @since 1.7.3 */ public function enqueue_assets() { // Lity. wp_enqueue_style( 'wpforms-lity', WPFORMS_PLUGIN_URL . 'assets/lib/lity/lity.min.css', null, '3.0.0' ); wp_enqueue_script( 'wpforms-lity', WPFORMS_PLUGIN_URL . 'assets/lib/lity/lity.min.js', [ 'jquery' ], '3.0.0', true ); } /** * Page view. * * @since 1.7.3 */ public function view() { $sign_up_link = get_option( 'wpforms_constant_contact_signup', 'https://constant-contact.evyy.net/c/11535/341874/3411?sharedid=wpforms' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo wpforms_render( 'admin/pages/constant-contact', [ 'sign_up_link' => is_string( $sign_up_link ) ? $sign_up_link : '', 'wpbeginners_guide_link' => 'https://www.wpbeginner.com/beginners-guide/why-you-should-start-building-your-email-list-right-away', ], true ); } } Admin/Pages/Community.php 0000644 00000014253 15174710275 0011346 0 ustar 00 <?php namespace WPForms\Admin\Pages; /** * Community Sub-page. * * @since 1.5.6 */ class Community { /** * Admin menu page slug. * * @since 1.5.6 * * @var string */ const SLUG = 'wpforms-community'; /** * Constructor. * * @since 1.5.6 */ public function __construct() { if ( \wpforms_current_user_can() ) { $this->hooks(); } } /** * Hooks. * * @since 1.5.6 */ public function hooks() { // Check what page we are on. // phpcs:ignore WordPress.Security.NonceVerification.Recommended $page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : ''; // Only load if we are actually on the Community page. if ( self::SLUG !== $page ) { return; } add_action( 'wpforms_admin_page', [ $this, 'output' ] ); // Hook for addons. do_action( 'wpforms_admin_community_init' ); } /** * Page data. * * @since 1.5.6 */ public function get_blocks_data() { $type = wpforms()->is_pro() ? 'plugin' : 'liteplugin'; $data = []; $data['vip_circle'] = [ 'title' => esc_html__( 'WPForms VIP Circle Facebook Group', 'wpforms-lite' ), 'description' => esc_html__( 'Powered by the community, for the community. Anything and everything WPForms: Discussions. Questions. Tutorials. Insights and sneak peaks. Also, exclusive giveaways!', 'wpforms-lite' ), 'button_text' => esc_html__( 'Join WPForms VIP Circle', 'wpforms-lite' ), 'button_link' => 'https://www.facebook.com/groups/wpformsvip/', 'cover_bg_color' => '#E4F0F6', 'cover_img' => 'vip-circle.png', 'cover_img2x' => 'vip-circle@2x.png', ]; $data['announcements'] = [ 'title' => esc_html__( 'WPForms Announcements', 'wpforms-lite' ), 'description' => esc_html__( 'Check out the latest releases from WPForms. Our team is always innovating to bring you powerful features and functionality that are simple to use. Every release is designed with you in mind!', 'wpforms-lite' ), 'button_text' => esc_html__( 'View WPForms Announcements', 'wpforms-lite' ), 'button_link' => 'https://wpforms.com/blog/?utm_source=WordPress&utm_medium=Community&utm_campaign=' . esc_attr( $type ) . '&utm_content=Announcements', 'cover_bg_color' => '#EFF8E9', 'cover_img' => 'announcements.png', 'cover_img2x' => 'announcements@2x.png', ]; $data['youtube'] = [ 'title' => esc_html__( 'WPForms YouTube Channel', 'wpforms-lite' ), 'description' => esc_html__( 'Take a visual dive into everything WPForms has to offer. From simple contact forms to advanced payment forms and email marketing integrations, our extensive video collection covers it all.', 'wpforms-lite' ), 'button_text' => esc_html__( 'Visit WPForms YouTube Channel', 'wpforms-lite' ), 'button_link' => 'https://www.youtube.com/c/wpformsplugin', 'cover_bg_color' => '#FFE6E6', 'cover_img' => 'youtube.png', 'cover_img2x' => 'youtube@2x.png', ]; $data['dev_docs'] = [ 'title' => esc_html__( 'WPForms Developer Documentation', 'wpforms-lite' ), 'description' => esc_html__( 'Customize and extend WPForms with code. Our comprehensive developer resources include tutorials, snippets, and documentation on core actions, filters, functions, and more.', 'wpforms-lite' ), 'button_text' => esc_html__( 'View WPForms Dev Docs', 'wpforms-lite' ), 'button_link' => 'https://wpforms.com/developers/?utm_source=WordPress&utm_medium=Community&utm_campaign=' . esc_attr( $type ) . '&utm_content=Developers', 'cover_bg_color' => '#EBEBEB', 'cover_img' => 'dev-docs.png', 'cover_img2x' => 'dev-docs@2x.png', ]; $data['wpbeginner'] = [ 'title' => esc_html__( 'WPBeginner Engage Facebook Group', 'wpforms-lite' ), 'description' => esc_html__( 'Hang out with other WordPress experts and like minded website owners such as yourself! Hosted by WPBeginner, the largest free WordPress site for beginners.', 'wpforms-lite' ), 'button_text' => esc_html__( 'Join WPBeginner Engage', 'wpforms-lite' ), 'button_link' => 'https://www.facebook.com/groups/wpbeginner/', 'cover_bg_color' => '#FCEDE4', 'cover_img' => 'wpbeginner.png', 'cover_img2x' => 'wpbeginner@2x.png', ]; $data['suggest'] = [ 'title' => esc_html__( 'Suggest a Feature', 'wpforms-lite' ), 'description' => esc_html__( 'Do you have an idea or suggestion for WPForms? If you have thoughts on features, integrations, addons, or improvements - we want to hear it! We appreciate all feedback and insight from our users.', 'wpforms-lite' ), 'button_text' => esc_html__( 'Suggest a Feature', 'wpforms-lite' ), 'button_link' => 'https://wpforms.com/features/suggest/?utm_source=WordPress&utm_medium=Community&utm_campaign=' . esc_attr( $type ) . '&utm_content=Feature', 'cover_bg_color' => '#FFF9EF', 'cover_img' => 'suggest.png', 'cover_img2x' => 'suggest@2x.png', ]; return $data; } /** * Generate and output page HTML. * * @since 1.5.6 */ public function output() { ?> <div id="wpforms-admin-community" class="wrap wpforms-admin-wrap"> <h1 class="page-title"><?php esc_html_e( 'Community', 'wpforms-lite' ); ?></h1> <div class="items"> <?php $data = $this->get_blocks_data(); foreach ( $data as $item ) { printf( '<div class="item"> <a href="%6$s" target="_blank" rel="noopener noreferrer" class="item-cover" style="background-color: %s;" title="%4$s"><img class="item-img" src="%s" srcset="%s 2x" alt="%4$s"/></a> <h3 class="item-title">%s</h3> <p class="item-description">%s</p> <div class="item-footer"> <a class="wpforms-btn button-primary wpforms-btn-blue" href="%s" target="_blank" rel="noopener noreferrer">%s</a> </div> </div>', esc_attr( $item['cover_bg_color'] ), esc_url( WPFORMS_PLUGIN_URL . 'assets/images/community/' . $item['cover_img'] ), esc_url( WPFORMS_PLUGIN_URL . 'assets/images/community/' . $item['cover_img2x'] ), esc_html( $item['title'] ), esc_html( $item['description'] ), esc_url( $item['button_link'] ), esc_html( $item['button_text'] ) ); } ?> </div> </div> <?php } } Admin/Pages/Templates.php 0000644 00000006577 15174710275 0011332 0 ustar 00 <?php namespace WPForms\Admin\Pages; use WPForms\Admin\Traits\FormTemplates; /** * Main Templates page class. * * @since 1.7.7 */ class Templates { use FormTemplates; /** * Page slug. * * @since 1.7.7 * * @var string */ const SLUG = 'wpforms-templates'; /** * Initialize class. * * @since 1.7.7 */ public function init() { if ( ! wpforms_is_admin_page( 'templates' ) && ! wpforms_is_admin_ajax() ) { return; } $this->addons_obj = wpforms()->obj( 'addons' ); $this->hooks(); } /** * Register hooks. * * @since 1.7.7 */ private function hooks() { add_action( 'wpforms_admin_page', [ $this, 'output' ] ); add_action( 'admin_enqueue_scripts', [ $this, 'enqueues' ] ); } /** * Enqueue assets. * * @since 1.7.7 */ public function enqueues() { $min = wpforms_get_min_suffix(); wp_enqueue_style( 'wpforms-form-templates', WPFORMS_PLUGIN_URL . "assets/css/admin/admin-form-templates{$min}.css", [], WPFORMS_VERSION ); wp_enqueue_script( 'wpforms-admin-form-templates', WPFORMS_PLUGIN_URL . "assets/js/admin/pages/form-templates{$min}.js", [ 'underscore', 'wp-util' ], WPFORMS_VERSION, true ); wp_enqueue_style( 'tooltipster', WPFORMS_PLUGIN_URL . 'assets/lib/jquery.tooltipster/jquery.tooltipster.min.css', [], '4.2.6' ); wp_enqueue_script( 'tooltipster', WPFORMS_PLUGIN_URL . 'assets/lib/jquery.tooltipster/jquery.tooltipster.min.js', [ 'jquery', 'wpforms-admin-form-templates' ], '4.2.6', true ); wp_localize_script( 'wpforms-admin-form-templates', 'wpforms_admin_form_templates', [ 'nonce' => wp_create_nonce( 'wpforms-builder' ), 'openAIFormUrl' => admin_url( 'admin.php?page=wpforms-builder&view=setup&ai-form' ), ] ); } /** * Build the output for the Form Templates admin page. * * @since 1.7.7 */ public function output() { ?> <div id="wpforms-form-templates" class="wrap wpforms-admin-wrap"> <h1 class="page-title"><?php esc_html_e( 'Form Templates', 'wpforms-lite' ); ?></h1> <div class="wpforms-form-setup-content" > <div class="wpforms-setup-title"> <?php esc_html_e( 'Get a Head Start With Our Pre-Made Form Templates', 'wpforms-lite' ); ?> </div> <p class="wpforms-setup-desc secondary-text"> <?php printf( wp_kses( /* translators: %1$s - create template doc link; %2$s - Contact us page link. */ __( 'Choose a template to speed up the process of creating your form. You can also start with a <a href="#" class="wpforms-trigger-blank">blank form</a> or <a href="%1$s" target="_blank" rel="noopener noreferrer">create your own</a>. <br>Have a suggestion for a new template? <a href="%2$s" target="_blank" rel="noopener noreferrer">We’d love to hear it</a>!', 'wpforms-lite' ), [ 'strong' => [], 'br' => [], 'a' => [ 'href' => [], 'class' => [], 'target' => [], 'rel' => [], ], ] ), esc_url( wpforms_utm_link( 'https://wpforms.com/docs/how-to-create-a-custom-form-template/', 'Form Templates Subpage', 'Create Your Own Template' ) ), esc_url( wpforms_utm_link( 'https://wpforms.com/form-template-suggestion/', 'Form Templates Subpage', 'Form Template Suggestion' ) ) ); ?> </p> <?php $this->output_templates_content(); ?> </div> </div> <?php } } Admin/Blocks/Links.php 0000644 00000023721 15174710275 0010620 0 ustar 00 <?php namespace WPForms\Admin\Blocks; /** * Class for rendering links in the admin area. * * @since 1.9.7 */ class Links { /** * Render links. * * @since 1.9.7 * * @param array $utm_params UTM parameters to append to links. */ public static function render( array $utm_params ): void { $links = self::get_links( $utm_params ); echo '<div class="wpforms-links">'; foreach ( $links as $slug => $link ) { self::render_link( $slug, $link ); } echo '</div>'; } /** * Render a single link. * * @since 1.9.7 * * @param string $slug The slug for the link. * @param array $link The link data. */ private static function render_link( string $slug, array $link ): void { $url = $link['url'] ?? '#'; $text = $link['text'] ?? ''; $class = $link['class'] ?? ''; $target = $link['target'] ?? '_self'; $icon = $link['icon'] ?? ''; printf( '<a href="%1$s" target="%2$s" rel="noopener noreferrer" class="wpforms-link wpforms-link-%3$s %4$s">%6$s%5$s</a>', esc_url( $url ), esc_attr( $target ), esc_attr( $slug ), esc_attr( $class ), esc_html( $text ), wp_kses( $icon, [ 'svg' => [ 'xmlns' => true, 'width' => true, 'height' => true, 'viewbox' => true, 'fill' => true, ], 'path' => [ 'd' => true, 'fill' => true, ], ] ) ); } /** * Get links. * * @since 1.9.7 * * @param array $utm_params UTM parameters to append to links. * * @return array */ private static function get_links( array $utm_params ): array { return [ 'docs' => [ 'url' => self::get_utm_link( 'https://wpforms.com/docs/', $utm_params['docs'] ?? [] ), 'text' => esc_html__( 'Docs', 'wpforms-lite' ), 'target' => '_blank', 'icon' => '<svg xmlns="http://www.w3.org/2000/svg" width="13" height="17" viewBox="0 0 13 17" fill="none"><path d="M2.33765 14.5854H10.3376C10.5876 14.5854 10.8376 14.3667 10.8376 14.0854V5.08545H8.33765C7.77515 5.08545 7.33765 4.64795 7.33765 4.08545V1.58545H2.33765C2.0564 1.58545 1.83765 1.83545 1.83765 2.08545V14.0854C1.83765 14.3667 2.0564 14.5854 2.33765 14.5854ZM2.33765 0.0854492H7.4939C8.02515 0.0854492 8.52515 0.304199 8.90015 0.679199L11.7439 3.52295C12.1189 3.89795 12.3376 4.39795 12.3376 4.9292V14.0854C12.3376 15.2104 11.4314 16.0854 10.3376 16.0854H2.33765C1.21265 16.0854 0.337646 15.2104 0.337646 14.0854V2.08545C0.337646 0.991699 1.21265 0.0854492 2.33765 0.0854492ZM4.08765 8.08545H8.58765C8.9939 8.08545 9.33765 8.4292 9.33765 8.83545C9.33765 9.27295 8.9939 9.58545 8.58765 9.58545H4.08765C3.65015 9.58545 3.33765 9.27295 3.33765 8.83545C3.33765 8.4292 3.65015 8.08545 4.08765 8.08545ZM4.08765 11.0854H8.58765C8.9939 11.0854 9.33765 11.4292 9.33765 11.8354C9.33765 12.2729 8.9939 12.5854 8.58765 12.5854H4.08765C3.65015 12.5854 3.33765 12.2729 3.33765 11.8354C3.33765 11.4292 3.65015 11.0854 4.08765 11.0854Z" fill="#646970"/></svg>', ], 'videos' => [ 'url' => 'https://www.youtube.com/@wpforms/videos', 'text' => esc_html__( 'Videos', 'wpforms-lite' ), 'target' => '_blank', 'icon' => '<svg xmlns="http://www.w3.org/2000/svg" width="17" height="17" viewBox="0 0 17 17" fill="none"><path d="M14.8376 8.06982C14.8376 5.75732 13.5876 3.63232 11.5876 2.44482C9.5564 1.28857 7.08765 1.28857 5.08765 2.44482C3.0564 3.63232 1.83765 5.75732 1.83765 8.06982C1.83765 10.4136 3.0564 12.5386 5.08765 13.7261C7.08765 14.8823 9.5564 14.8823 11.5876 13.7261C13.5876 12.5386 14.8376 10.4136 14.8376 8.06982ZM0.337646 8.06982C0.337646 5.22607 1.83765 2.60107 4.33765 1.16357C6.8064 -0.273926 9.83765 -0.273926 12.3376 1.16357C14.8064 2.60107 16.3376 5.22607 16.3376 8.06982C16.3376 10.9448 14.8064 13.5698 12.3376 15.0073C9.83765 16.4448 6.8064 16.4448 4.33765 15.0073C1.83765 13.5698 0.337646 10.9448 0.337646 8.06982ZM6.21265 4.69482C6.4314 4.53857 6.7439 4.53857 6.96265 4.69482L11.4626 7.44482C11.6814 7.56982 11.8376 7.81982 11.8376 8.10107C11.8376 8.35107 11.6814 8.60107 11.4626 8.72607L6.96265 11.4761C6.7439 11.6323 6.4314 11.6323 6.21265 11.5073C5.96265 11.3511 5.83765 11.1011 5.83765 10.8511V5.35107C5.83765 5.06982 5.96265 4.81982 6.21265 4.69482Z" fill="#646970"/></svg>', ], 'support' => [ 'url' => wpforms()->is_pro() ? self::get_utm_link( 'https://wpforms.com/account/support/', $utm_params['support'] ?? [] ) : 'https://wordpress.org/support/plugin/wpforms-lite/', 'text' => wpforms()->is_pro() ? esc_html__( 'Support', 'wpforms-lite' ) : esc_html__( 'Support Forum', 'wpforms-lite' ), 'target' => '_blank', 'icon' => wpforms()->is_pro() ? '<svg xmlns="http://www.w3.org/2000/svg" width="17" height="17" viewBox="0 0 17 17" fill="none"><path d="M14.8376 8.06982C14.8376 5.75732 13.5876 3.63232 11.5876 2.44482C9.5564 1.28857 7.08765 1.28857 5.08765 2.44482C3.0564 3.63232 1.83765 5.75732 1.83765 8.06982C1.83765 10.4136 3.0564 12.5386 5.08765 13.7261C7.08765 14.8823 9.5564 14.8823 11.5876 13.7261C13.5876 12.5386 14.8376 10.4136 14.8376 8.06982ZM0.337646 8.06982C0.337646 5.22607 1.83765 2.60107 4.33765 1.16357C6.8064 -0.273926 9.83765 -0.273926 12.3376 1.16357C14.8064 2.60107 16.3376 5.22607 16.3376 8.06982C16.3376 10.9448 14.8064 13.5698 12.3376 15.0073C9.83765 16.4448 6.8064 16.4448 4.33765 15.0073C1.83765 13.5698 0.337646 10.9448 0.337646 8.06982ZM5.6189 5.25732C5.8689 4.53857 6.52515 4.06982 7.27515 4.06982H9.08765C10.1814 4.06982 11.0876 4.97607 11.0876 6.06982C11.0876 6.75732 10.6814 7.41357 10.0876 7.75732L9.08765 8.35107C9.0564 8.75732 8.7439 9.06982 8.33765 9.06982C7.90015 9.06982 7.58765 8.75732 7.58765 8.31982V7.91357C7.58765 7.63232 7.71265 7.38232 7.96265 7.25732L9.33765 6.47607C9.4939 6.38232 9.58765 6.22607 9.58765 6.06982C9.58765 5.78857 9.3689 5.60107 9.08765 5.60107H7.27515C7.1814 5.60107 7.08765 5.66357 7.0564 5.75732L7.02515 5.78857C6.90015 6.19482 6.46265 6.38232 6.08765 6.25732C5.6814 6.10107 5.4939 5.66357 5.6189 5.28857V5.25732ZM7.33765 11.0698C7.33765 10.5386 7.77515 10.0698 8.33765 10.0698C8.8689 10.0698 9.33765 10.5386 9.33765 11.0698C9.33765 11.6323 8.8689 12.0698 8.33765 12.0698C7.77515 12.0698 7.33765 11.6323 7.33765 11.0698Z" fill="#646970"/></svg>' : '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="16" viewBox="0 0 20 16" fill="none"><path d="M3.09554 9.83838C3.06625 9.89697 3.03695 9.95557 2.97836 9.98486C2.91976 10.1313 2.83187 10.2485 2.77328 10.395C3.27133 10.2778 3.74008 10.0728 4.20883 9.86768C4.35531 9.80908 4.4725 9.75049 4.58968 9.69189C4.88265 9.54541 5.17562 9.51611 5.49789 9.57471C5.84945 9.6333 6.20101 9.6626 6.61117 9.6626C9.42367 9.6626 11.2987 7.7876 11.2987 5.9126C11.2987 4.06689 9.42367 2.1626 6.61117 2.1626C3.76937 2.1626 1.92367 4.06689 1.92367 5.9126C1.92367 6.73291 2.24593 7.52393 2.86117 8.19775C3.30062 8.63721 3.38851 9.28174 3.09554 9.83838ZM6.61117 11.0688C6.11312 11.0688 5.67367 11.0396 5.20492 10.9517C5.08773 11.0103 4.94125 11.0981 4.79476 11.1567C3.74008 11.6255 2.48031 12.0063 1.22054 12.0063C0.927575 12.0063 0.663904 11.8599 0.546716 11.5962C0.458825 11.3325 0.488122 11.0396 0.6932 10.8345C1.10336 10.3657 1.45492 9.83838 1.77718 9.31104C1.80648 9.25244 1.83578 9.19385 1.83578 9.16455C1.01547 8.28564 0.517419 7.14307 0.517419 5.9126C0.517419 3.0708 3.24203 0.756348 6.61117 0.756348C9.95101 0.756348 12.7049 3.0708 12.7049 5.9126C12.7049 8.78369 9.95101 11.0688 6.61117 11.0688ZM13.1737 14.8188C10.7713 14.8188 8.69125 13.647 7.69515 11.9478C8.1932 11.8599 8.69125 11.7427 9.16 11.5962C9.92172 12.6509 11.3573 13.4126 13.1444 13.4126C13.5252 13.4126 13.9061 13.3833 14.2577 13.3247C14.5799 13.2661 14.8729 13.2954 15.1659 13.4419C15.283 13.5005 15.4002 13.5591 15.5467 13.6177C16.0155 13.8228 16.4842 13.9985 16.9823 14.145C16.9237 13.9985 16.8358 13.8813 16.7772 13.7349C16.7479 13.6763 16.6893 13.6177 16.66 13.5591C16.3963 13.0317 16.4842 12.3872 16.8944 11.9478C17.5096 11.2739 17.8612 10.4829 17.8612 9.6626C17.8612 7.93408 16.1912 6.14697 13.6424 5.94189V5.9126C13.6424 5.44385 13.5545 4.9751 13.4373 4.53564C16.6893 4.65283 19.2674 6.90869 19.2674 9.6626C19.2674 10.8931 18.7401 12.0356 17.9198 12.9146C17.9198 12.9731 17.9491 13.0024 17.9784 13.061C18.3006 13.5884 18.6522 14.1157 19.0623 14.5845C19.2674 14.7896 19.2967 15.0825 19.2088 15.3462C19.0916 15.6099 18.828 15.7563 18.5643 15.7563C17.3045 15.7563 16.0155 15.3755 14.9608 14.9067C14.8143 14.8481 14.6678 14.7603 14.5506 14.7017C14.0819 14.7896 13.6424 14.8188 13.1737 14.8188Z" fill="#50575E"/></svg>', ], 'whats-new' => [ 'text' => esc_html__( 'What’s New', 'wpforms-lite' ), 'class' => 'wpforms-splash-modal-open', 'icon' => '<svg xmlns="http://www.w3.org/2000/svg" width="17" height="20" viewBox="0 0 17 20" fill="none"><path d="M13.6014 1.63137L14.7985 6.09878C15.4146 6.22486 16.0232 6.80589 16.2497 7.65107C16.4762 8.49626 16.2477 9.33393 15.7771 9.75119L16.9661 14.1884C17.0712 14.5808 16.9268 15.0077 16.605 15.2557C16.2833 15.5037 15.8364 15.5264 15.4919 15.3275L13.8079 14.3552C11.9708 13.2946 9.79038 13.0053 7.73779 13.5553L7.4963 13.62L8.53158 17.4837C8.67717 18.027 8.33762 18.571 7.82447 18.7085L5.89262 19.2261C5.34929 19.3717 4.81346 19.0623 4.66788 18.519L3.6326 14.6553C2.54594 14.9465 1.47428 14.3277 1.18311 13.2411L0.406655 10.3433C0.123572 9.28681 0.734202 8.18497 1.82087 7.8938L5.92605 6.79382C7.97865 6.24383 9.7223 4.9031 10.783 3.06598L11.7633 1.41214C11.9622 1.06769 12.3605 0.863896 12.7632 0.917765C13.1659 0.971634 13.5044 1.26915 13.6014 1.63137ZM12.2924 4.47327C10.9482 6.58047 8.85851 8.07862 6.44369 8.72567L6.20221 8.79037L6.97867 11.6882L7.22015 11.6234C9.63496 10.9764 12.2019 11.2592 14.4195 12.412L12.2924 4.47327Z" fill="#646970"/></svg>', ], ]; } /** * Get UTM link. * * @since 1.9.7 * * @param string $url The URL to which UTM parameters will be added. * @param array $utm_params UTM parameters to append to the URL. * * @return string */ private static function get_utm_link( string $url, array $utm_params ): string { return wpforms_utm_link( $url, $utm_params['medium'] ?? '', $utm_params['content'] ?? '', $utm_params['term'] ?? '' ); } } Admin/Notice.php 0000644 00000023542 15174710275 0007545 0 ustar 00 <?php namespace WPForms\Admin; /** * Dismissible admin notices. * * @since 1.6.7.1 * * @example Dismissible - global: * \WPForms\Admin\Notice::error( * 'Fatal error!', * [ * 'dismiss' => \WPForms\Admin\Notice::DISMISS_GLOBAL, * 'slug' => 'fatal_error_3678975', * ] * ); * * @example Dismissible - per user: * \WPForms\Admin\Notice::warning( * 'Do something please.', * [ * 'dismiss' => \WPForms\Admin\Notice::DISMISS_USER, * 'slug' => 'do_something_1238943', * ] * ); * * @example Dismissible - global, add custom class to output and disable auto paragraph in text: * \WPForms\Admin\Notice::error( * 'Fatal error!', * [ * 'dismiss' => \WPForms\Admin\Notice::DISMISS_GLOBAL, * 'slug' => 'fatal_error_348975', * 'autop' => false, * 'class' => 'some-additional-class', * ] * ); * * @example Not dismissible: * \WPForms\Admin\Notice::success( 'Everything is good!' ); */ class Notice { /** * Not dismissible. * * Constant attended to use as the value of the $args['dismiss'] argument. * DISMISS_NONE means that the notice is not dismissible. * * @since 1.6.7.1 */ const DISMISS_NONE = 0; /** * Dismissible global. * * Constant attended to use as the value of the $args['dismiss'] argument. * DISMISS_GLOBAL means that the notice will have the dismiss button, and after clicking this button, the notice will be dismissed for all users. * * @since 1.6.7.1 */ const DISMISS_GLOBAL = 1; /** * Dismissible per user. * * Constant attended to use as the value of the $args['dismiss'] argument. * DISMISS_USER means that the notice will have the dismiss button, and after clicking this button, the notice will be dismissed only for the current user.. * * @since 1.6.7.1 */ const DISMISS_USER = 2; /** * Order of notices by type. * * @since 1.9.1 */ const ORDER = [ 'error', 'warning', 'info', 'success', ]; /** * Added notices. * * @since 1.6.7.1 * * @var array */ private $notices = []; /** * Init. * * @since 1.6.7.1 */ public function init() { $this->hooks(); } /** * Hooks. * * @since 1.6.7.1 */ public function hooks() { add_action( 'admin_notices', [ $this, 'display' ], PHP_INT_MAX ); add_action( 'wp_ajax_wpforms_notice_dismiss', [ $this, 'dismiss_ajax' ] ); } /** * Enqueue assets. * * @since 1.6.7.1 */ private function enqueues() { $min = wpforms_get_min_suffix(); wp_enqueue_script( 'wpforms-admin-notices', WPFORMS_PLUGIN_URL . "assets/js/admin/notices{$min}.js", [ 'jquery' ], WPFORMS_VERSION, true ); wp_localize_script( 'wpforms-admin-notices', 'wpforms_admin_notices', [ 'ajax_url' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'wpforms-admin' ), ] ); } /** * Display the notices. * * @since 1.6.7.1 */ public function display() { $dismissed_notices = get_user_meta( get_current_user_id(), 'wpforms_admin_notices', true ); $dismissed_notices = is_array( $dismissed_notices ) ? $dismissed_notices : []; $dismissed_notices = array_merge( $dismissed_notices, (array) get_option( 'wpforms_admin_notices', [] ) ); $dismissed_notices = array_filter( wp_list_pluck( $dismissed_notices, 'dismissed' ) ); $this->notices = array_diff_key( $this->notices, $dismissed_notices ); $output = implode( '', array_column( $this->sort_notices(), 'data' ) ); echo $output; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped // Enqueue script only when it's needed. if ( strpos( $output, 'is-dismissible' ) !== false ) { $this->enqueues(); } } /** * Sort notices by type. * * @since 1.9.1 * * @return array Notices. */ private function sort_notices(): array { $ordered_notices = []; foreach ( self::ORDER as $order ) { foreach ( $this->notices as $key => $notice ) { if ( $notice['type'] === $order ) { $ordered_notices[ $key ] = $notice; unset( $this->notices[ $key ] ); } } } // Notices that are not in the self::ORDER array merged to the end of array. return array_merge( $ordered_notices, $this->notices ); } /** * Add notice to the registry. * * @since 1.6.7.1 * * @param string $message Message to display. * @param string $type Type of the notice. Can be [ '' (default) | 'info' | 'error' | 'success' | 'warning' ]. * @param array $args The array of additional arguments. Please see the $defaults array below. */ public static function add( $message, $type = '', $args = [] ) { static $uniq_id = 0; $defaults = [ 'dismiss' => self::DISMISS_NONE, // Dismissible level: one of the self::DISMISS_* const. By default notice is not dismissible. 'slug' => '', // Slug. Should be unique if dismissible is not equal self::DISMISS_NONE. 'autop' => true, // `false` if not needed to pass message through wpautop(). 'class' => '', // Additional CSS class. ]; $args = wp_parse_args( $args, $defaults ); $dismissible = (int) $args['dismiss']; $dismissible = $dismissible > self::DISMISS_USER ? self::DISMISS_USER : $dismissible; $class = $dismissible > self::DISMISS_NONE ? ' is-dismissible' : ''; $global = ( $dismissible === self::DISMISS_GLOBAL ) ? 'global-' : ''; $slug = sanitize_key( $args['slug'] ); ++$uniq_id; $uniq_id += ( $uniq_id === (int) $slug ) ? 1 : 0; $notice = [ 'type' => $type, ]; $id = 'wpforms-notice-' . $global; $id .= empty( $slug ) ? $uniq_id : $slug; $type = ! empty( $type ) ? 'notice-' . esc_attr( sanitize_key( $type ) ) : ''; $class = empty( $args['class'] ) ? $class : $class . ' ' . esc_attr( sanitize_key( $args['class'] ) ); $message = $args['autop'] ? wpautop( $message ) : $message; $notice['data'] = sprintf( '<div class="notice wpforms-notice %s%s" id="%s">%s</div>', esc_attr( $type ), esc_attr( $class ), esc_attr( $id ), $message ); if ( empty( $slug ) ) { wpforms()->obj( 'notice' )->notices[] = $notice; } else { wpforms()->obj( 'notice' )->notices[ $slug ] = $notice; } } /** * Add info notice. * * @since 1.6.7.1 * * @param string $message Message to display. * @param array $args Array of additional arguments. Details in the self::add() method. */ public static function info( $message, $args = [] ) { self::add( $message, 'info', $args ); } /** * Add error notice. * * @since 1.6.7.1 * * @param string $message Message to display. * @param array $args Array of additional arguments. Details in the self::add() method. */ public static function error( $message, $args = [] ) { self::add( $message, 'error', $args ); } /** * Add success notice. * * @since 1.6.7.1 * * @param string $message Message to display. * @param array $args Array of additional arguments. Details in the self::add() method. */ public static function success( $message, $args = [] ) { self::add( $message, 'success', $args ); } /** * Add warning notice. * * @since 1.6.7.1 * * @param string $message Message to display. * @param array $args Array of additional arguments. Details in the self::add() method. */ public static function warning( $message, $args = [] ) { self::add( $message, 'warning', $args ); } /** * AJAX routine that updates dismissed notices meta data. * * @since 1.6.7.1 */ public function dismiss_ajax() { // Run a security check. check_ajax_referer( 'wpforms-admin', 'nonce' ); // Sanitize POST data. $post = array_map( 'sanitize_key', wp_unslash( $_POST ) ); // Update notices meta data. if ( strpos( $post['id'], 'global-' ) !== false ) { // Check for permissions. if ( ! wpforms_current_user_can() ) { wp_send_json_error(); } $notices = $this->dismiss_global( $post['id'] ); $level = self::DISMISS_GLOBAL; } else { $notices = $this->dismiss_user( $post['id'] ); $level = self::DISMISS_USER; } /** * Allows developers to apply additional logic to the dismissing notice process. * Executes after updating option or user meta (according to the notice level). * * @since 1.6.7.1 * * @param string $notice_id Notice ID (slug). * @param integer $level Notice level. * @param array $notices Dismissed notices. */ do_action( 'wpforms_admin_notice_dismiss_ajax', $post['id'], $level, $notices ); if ( ! wpforms_debug() ) { wp_send_json_success(); } wp_send_json_success( [ 'id' => $post['id'], 'time' => time(), 'level' => $level, 'notices' => $notices, ] ); } /** * AJAX sub-routine that updates dismissed notices option. * * @since 1.6.7.1 * * @param string $id Notice Id. * * @return array Notices. */ private function dismiss_global( $id ) { $id = str_replace( 'global-', '', $id ); $notices = get_option( 'wpforms_admin_notices', [] ); $notices[ $id ] = [ 'time' => time(), 'dismissed' => true, ]; update_option( 'wpforms_admin_notices', $notices, true ); return $notices; } /** * AJAX sub-routine that updates dismissed notices user meta. * * @since 1.6.7.1 * * @param string $id Notice Id. * * @return array Notices. */ private function dismiss_user( $id ) { $user_id = get_current_user_id(); $notices = get_user_meta( $user_id, 'wpforms_admin_notices', true ); $notices = ! is_array( $notices ) ? [] : $notices; $notices[ $id ] = [ 'time' => time(), 'dismissed' => true, ]; update_user_meta( $user_id, 'wpforms_admin_notices', $notices ); return $notices; } } Admin/Tools/Views/EntryAutomation.php 0000644 00000004073 15174710275 0013661 0 ustar 00 <?php namespace WPForms\Admin\Tools\Views; use WPForms\Admin\Education\Admin\Tools\EntryAutomation as Education; /** * Class EntryAutomation. * * @since 1.9.6.1 */ class EntryAutomation extends View { /** * View slug. * * @since 1.9.6.1 * * @var string */ protected $slug = 'entry-automation'; /** * Init view. * * @since 1.9.6.1 */ public function init(): void { ( new Education() )->init(); $this->hooks(); } /** * Register hooks. * * @since 1.9.6.1 */ private function hooks(): void { add_filter( 'admin_body_class', [ $this, 'body_classes' ] ); } /** * Add body classes for the view. * * @since 1.9.6.1 * * @param string $classes Existing body classes. * * @return string */ public function body_classes( string $classes ): string { if ( ! wpforms_is_admin_page( 'tools', 'entry-automation' ) ) { return $classes; } return $classes . ' wpforms-admin-tools-view-entry-automation'; } /** * Get view label. * * @since 1.9.6.1 * * @return string */ public function get_label(): string { return esc_html__( 'Entry Automation', 'wpforms-lite' ); } /** * Checking user capability to view. * * @since 1.9.6.1 * * @return bool */ public function check_capability(): bool { /** * Check if the user has the capability to view this entry automation. * * @since 1.9.6.1 * * @param bool $capability Whether the user has the capability to view this entry automation. */ return (bool) apply_filters( 'wpforms_admin_tools_views_entry_automation_check_capability', wpforms_current_user_can() && wpforms()->obj( 'addons' )->get_addon( 'entry-automation' ) ); } /** * Display view content. * * @since 1.9.6.1 */ public function display(): void { ?> <div class="tools wpforms-settings-row-entry-automation"> <div class="wpforms-entry-automation-content"> <?php /** * Display the content. * * @since 1.9.6.1 */ do_action( 'wpforms_admin_tools_views_entry_automation_display' ); ?> </div> </div> <?php } } Admin/Tools/Views/ActionScheduler.php 0000644 00000002571 15174710275 0013574 0 ustar 00 <?php namespace WPForms\Admin\Tools\Views; use ActionScheduler_AdminView; /** * Class ActionScheduler view. * * @since 1.6.6 */ class ActionScheduler extends View { /** * View slug. * * @since 1.6.6 * * @var string */ protected $slug = 'action-scheduler'; /** * Init view. * * @since 1.6.6 */ public function init() { if ( $this->admin_view_exists() ) { ActionScheduler_AdminView::instance()->process_admin_ui(); } } /** * Get link to the view. * * @since 1.6.9 * * @return string */ public function get_link() { return add_query_arg( [ 's' => 'wpforms', ], parent::get_link() ); } /** * Get view label. * * @since 1.6.6 * * @return string */ public function get_label() { return esc_html__( 'Scheduled Actions', 'wpforms-lite' ); } /** * Checking user capability to view. * * @since 1.6.6 * * @return bool */ public function check_capability() { return wpforms_current_user_can(); } /** * Display view content. * * @since 1.6.6 */ public function display() { if ( ! $this->admin_view_exists() ) { return; } ( new ActionSchedulerList() )->display_page(); } /** * Check if ActionScheduler_AdminView class exists. * * @since 1.6.6 * * @return bool */ private function admin_view_exists() { return class_exists( 'ActionScheduler_AdminView' ); } } Admin/Tools/Views/Logs.php 0000644 00000020312 15174710275 0011415 0 ustar 00 <?php namespace WPForms\Admin\Tools\Views; use WPForms\Logger\Log; use WPForms\Logger\ListTable; /** * Class Logs. * * @since 1.6.6 */ class Logs extends View { /** * View slug. * * @since 1.6.6 * * @var string */ protected $slug = 'logs'; /** * ListTable instance. * * @since 1.6.6 * * @var ListTable */ private $list_table = []; /** * Init view. * * @since 1.6.6 */ public function init() { $this->logs_controller(); } /** * Get view label. * * @since 1.6.6 * * @return string */ public function get_label() { return esc_html__( 'Logs', 'wpforms-lite' ); } /** * Checking user capability to view. * * @since 1.6.6 * * @return bool */ public function check_capability() { return wpforms_current_user_can(); } /** * Get ListTable instance. * * @since 1.6.6 * * @return ListTable */ private function get_list_table(): ListTable { if ( empty( $this->list_table ) ) { $log_obj = wpforms()->obj( 'log' ); if ( $log_obj ) { $this->list_table = $log_obj->get_list_table(); } } return $this->list_table; } /** * Display view content. * * @since 1.6.6 */ public function display() { ?> <form action="<?php echo esc_url( $this->get_link() ); ?>" method="POST"> <?php $this->nonce_field(); ?> <div class="wpforms-setting-row tools"> <h4><?php esc_html_e( 'Log Settings', 'wpforms-lite' ); ?></h4> <p><?php esc_html_e( 'Enable and configure the logging functionality while debugging behavior of various parts of the plugin, including form and entry processing.', 'wpforms-lite' ); ?></p> </div> <div class="wpforms-setting-row tools wpforms-setting-row-toggle wpforms-clear" id="wpforms-setting-row-logs-enable"> <div class="wpforms-setting-label"> <label for="wpforms-setting-logs-enable"><?php esc_html_e( 'Enable Logs', 'wpforms-lite' ); ?></label> </div> <div class="wpforms-setting-field"> <span class="wpforms-toggle-control"> <input type="checkbox" id="wpforms-setting-logs-enable" name="logs-enable" value="1" <?php checked( wpforms_setting( 'logs-enable' ) ); ?>> <label class="wpforms-toggle-control-icon" for="wpforms-setting-logs-enable"></label> <label for="wpforms-setting-logs-enable" class="wpforms-toggle-control-status" data-on="On" data-off="Off"> <?php wpforms_setting( 'logs-enable' ) ? esc_html_e( 'On', 'wpforms-lite' ) : esc_html_e( 'Off', 'wpforms-lite' ); ?> </label> </span> <p class="desc"> <?php esc_html_e( 'Start logging WPForms-related events. This is recommended only while debugging.', 'wpforms-lite' ); ?> </p> </div> </div> <div class="wpforms-logs-settings <?php echo wpforms_setting( 'logs-enable' ) ? '' : 'wpforms-hidden'; ?>"> <?php $this->types_block(); $this->user_roles_block(); $this->users_block(); ?> </div> <p class="submit"> <button class="wpforms-btn wpforms-btn-md wpforms-btn-orange" name="wpforms-settings-submit"> <?php esc_html_e( 'Save Settings', 'wpforms-lite' ); ?> </button> </p> </form> <?php $logs_list_table = $this->get_list_table(); if ( wpforms_setting( 'logs-enable' ) || $logs_list_table->get_total() ) { $logs_list_table->display_page(); } } /** * Types block. * * @since 1.6.6 */ private function types_block() { ?> <div class="wpforms-setting-row tools wpforms-setting-row-select wpforms-clear" id="wpforms-setting-row-log-types"> <div class="wpforms-setting-label"> <label for="wpforms-setting-logs-types"><?php esc_html_e( 'Log Types', 'wpforms-lite' ); ?></label> </div> <div class="wpforms-setting-field"> <span class="choicesjs-select-wrap"> <select id="wpforms-setting-logs-types" class="choicesjs-select" name="logs-types[]" multiple size="1"> <?php $log_types = wpforms_setting( 'logs-types', [] ); foreach ( Log::get_log_types() as $slug => $name ) { ?> <option value="<?php echo esc_attr( $slug ); ?>" <?php selected( in_array( $slug, $log_types, true ) ); ?> > <?php echo esc_html( $name ); ?> </option> <?php } ?> </select> </span> <p class="desc"><?php esc_html_e( 'Select the types of events you want to log. Everything is logged by default.', 'wpforms-lite' ); ?></p> </div> </div> <?php } /** * User roles block. * * @since 1.6.6 */ private function user_roles_block() { ?> <div class="wpforms-setting-row tools wpforms-setting-row-select wpforms-clear" id="wpforms-setting-row-log-user-roles"> <div class="wpforms-setting-label"> <label for="wpforms-setting-logs-user-roles"><?php esc_html_e( 'User Roles', 'wpforms-lite' ); ?></label> </div> <div class="wpforms-setting-field"> <span class="choicesjs-select-wrap"> <?php $logs_user_roles = wpforms_setting( 'logs-user-roles', [] ); $roles = wp_list_pluck( get_editable_roles(), 'name' ); ?> <select id="wpforms-setting-logs-user-roles" class="choicesjs-select" name="logs-user-roles[]" multiple size="1"> <?php foreach ( $roles as $slug => $name ) { ?> <option value="<?php echo esc_attr( $slug ); ?>" <?php selected( in_array( $slug, $logs_user_roles, true ) ); ?> > <?php echo esc_html( $name ); ?> </option> <?php } ?> </select> <span class="hidden" id="wpforms-setting-logs-user-roles-selectform-spinner"> <i class="fa fa-cog fa-spin fa-lg"></i> </span> </span> <p class="desc"> <?php esc_html_e( 'Select the user roles you want to log. All roles are logged by default.', 'wpforms-lite' ); ?> </p> </div> </div> <?php } /** * Users block. * * @since 1.6.6 */ private function users_block() { ?> <div class="wpforms-setting-row tools wpforms-setting-row-select wpforms-clear" id="wpforms-setting-row-log-users"> <div class="wpforms-setting-label"> <label for="wpforms-setting-logs-users"><?php esc_html_e( 'Users', 'wpforms-lite' ); ?></label> </div> <div class="wpforms-setting-field"> <span class="choicesjs-select-wrap"> <select id="wpforms-setting-logs-users" class="choicesjs-select" name="logs-users[]" multiple size="1"> <?php $users = get_users( [ 'fields' => [ 'ID', 'display_name' ] ] ); $users = wp_list_pluck( $users, 'display_name', 'ID' ); $logs_users = wpforms_setting( 'logs-users', [] ); foreach ( $users as $slug => $name ) { ?> <option value="<?php echo esc_attr( $slug ); ?>" <?php selected( in_array( $slug, $logs_users, true ) ); ?> > <?php echo esc_html( $name ); ?> </option> <?php } ?> </select> <span class="hidden" id="wpforms-setting-logs-users-selectform-spinner"> <i class="fa fa-cog fa-spin fa-lg"></i> </span> </span> <p class="desc"> <?php esc_html_e( 'Log events for specific users only. All users are logged by default.', 'wpforms-lite' ); ?> </p> </div> </div> <?php } /** * Controller. * * @since 1.6.6 */ private function logs_controller() { if ( $this->verify_nonce() ) { $this->update_settings(); } $logs_list_table = $this->get_list_table(); $logs_list_table->process_admin_ui(); } /** * Update settings. * * @since 1.8.7 * * @return void */ public function update_settings() { $settings = get_option( 'wpforms_settings' ); $was_enabled = ! empty( $settings['logs-enable'] ) ? $settings['logs-enable'] : 0; $settings['logs-enable'] = filter_input( INPUT_POST, 'logs-enable', FILTER_VALIDATE_BOOLEAN ); $logs_types = filter_input( INPUT_POST, 'logs-types', FILTER_SANITIZE_FULL_SPECIAL_CHARS, FILTER_REQUIRE_ARRAY ); $logs_user_roles = filter_input( INPUT_POST, 'logs-user-roles', FILTER_SANITIZE_FULL_SPECIAL_CHARS, FILTER_REQUIRE_ARRAY ); $logs_users = filter_input( INPUT_POST, 'logs-users', FILTER_SANITIZE_NUMBER_INT, FILTER_REQUIRE_ARRAY ); if ( $was_enabled ) { $settings['logs-types'] = $logs_types ? $logs_types : []; $settings['logs-user-roles'] = $logs_user_roles ? $logs_user_roles : []; $settings['logs-users'] = $logs_users ? array_map( 'absint', $logs_users ) : []; } wpforms_update_settings( $settings ); } } Admin/Tools/Views/CodeSnippets.php 0000644 00000005435 15174710275 0013122 0 ustar 00 <?php namespace WPForms\Admin\Tools\Views; use WPForms\Integrations\WPCode\WPCode; /** * Class WPCode view. * * @since 1.8.5 */ class CodeSnippets extends View { /** * View slug. * * @since 1.8.5 * * @var string */ protected $slug = 'wpcode'; /** * WPCode action required. * * @since 1.8.5 * * @var string */ private $action; /** * WPCode snippets. * * @since 1.8.5 * * @var array */ private $snippets; /** * WPCode plugin slug or download URL. * * @since 1.8.5 * * @var string */ private $plugin; /** * Whether WPCode action is required. * * @since 1.8.5 * * @var bool */ private $action_required; /** * Init view. * * @since 1.8.5 */ public function init() { $wpcode = new WPCode(); $this->snippets = $wpcode->load_wpforms_snippets(); $plugin_slug = $wpcode->is_pro_installed() ? $wpcode->pro_plugin_slug : $wpcode->lite_plugin_slug; $update_required = $wpcode->is_plugin_installed() && version_compare( $wpcode->plugin_version(), '2.0.10', '<' ); $installed_action = $update_required ? 'update' : 'activate'; $this->action_required = $update_required || ! $wpcode->is_plugin_installed() || ! $wpcode->is_plugin_active(); $this->action = $wpcode->is_plugin_installed() ? $installed_action : 'install'; $this->plugin = $this->action === 'activate' ? $plugin_slug : $wpcode->lite_download_url; $this->hooks(); } /** * Add hooks. * * @since 1.8.5 * * @return void */ private function hooks() { if ( $this->action !== 'update' ) { return; } add_filter( 'upgrader_package_options', static function ( $options ) { $options['clear_destination'] = true; return $options; } ); } /** * Get view label. * * @since 1.8.5 * * @return string * @noinspection PhpMissingReturnTypeInspection * @noinspection ReturnTypeCanBeDeclaredInspection */ public function get_label() { return esc_html__( 'Code Snippets', 'wpforms-lite' ); } /** * Checking user capability to view. * * @since 1.8.5 * * @return bool * @noinspection PhpMissingReturnTypeInspection * @noinspection ReturnTypeCanBeDeclaredInspection */ public function check_capability() { return wpforms_current_user_can(); } /** * Display view content. * * @since 1.8.5 * * @noinspection PhpMissingReturnTypeInspection * @noinspection ReturnTypeCanBeDeclaredInspection */ public function display() { // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo wpforms_render( 'integrations/wpcode/code-snippets', [ 'snippets' => $this->snippets, 'action_required' => $this->action_required, 'action' => $this->action, 'plugin' => $this->plugin, ], true ); } } Admin/Tools/Views/Importer.php 0000644 00000023255 15174710275 0012323 0 ustar 00 <?php namespace WPForms\Admin\Tools\Views; use WPForms\Admin\Tools\Importers; /** * Class Importer. * * @since 1.6.6 */ class Importer extends View { /** * View slug. * * @since 1.6.6 * * @var string */ protected $slug = 'import'; /** * Registered importers. * * @since 1.6.6 * * @var array */ public $importers = []; /** * Available forms for a specific importer. * * @since 1.6.6 * * @var array */ public $importer_forms = []; /** * Init view. * * @since 1.6.6 */ public function init() { $importers = new Importers(); $this->importers = $importers->get_importers(); if ( ! empty( $_GET['provider'] ) ) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended $this->importer_forms = $importers->get_importer_forms( sanitize_key( $_GET['provider'] ) );//phpcs:ignore WordPress.Security.NonceVerification.Recommended } // Load the Underscores templates for importers. add_action( 'admin_print_scripts', [ $this, 'importer_templates' ] ); } /** * Get view label. * * @since 1.6.6 * * @return string */ public function get_label() { return ''; } /** * Checking user capability to view. * * @since 1.6.6 * * @return bool */ public function check_capability() { return wpforms_current_user_can( 'create_forms' ); } /** * Checking if needs display in navigation. * * @since 1.6.6 * * @return bool */ public function hide_from_nav() { return true; } /** * Importer view content. * * @since 1.6.6 */ public function display() { $this->heading_block(); $this->forms_block(); $this->analyze_block(); $this->process_block(); } /** * Get provider. * * @since 1.6.6 * * @return string */ private function get_provider_name() { //phpcs:ignore WordPress.Security.NonceVerification.Recommended $slug = ! empty( $_GET['provider'] ) ? sanitize_key( $_GET['provider'] ) : ''; return isset( $this->importers[ $slug ] ) ? $this->importers[ $slug ]['name'] : ''; } /** * Heading block. * * @since 1.6.6 */ private function heading_block() { ?> <div class="wpforms-setting-row tools wpforms-clear section-heading no-desc"> <div class="wpforms-setting-field"> <h4><?php esc_html_e( 'Form Import', 'wpforms-lite' ); ?></h4> </div> </div> <?php } /** * Forms block. * * @since 1.6.6 */ private function forms_block() { ?> <div id="wpforms-importer-forms"> <div class="wpforms-setting-row tools"> <p><?php esc_html_e( 'Select the forms you would like to import.', 'wpforms-lite' ); ?></p> <div class="checkbox-multiselect-columns"> <div class="first-column"> <h5 class="header"><?php esc_html_e( 'Available Forms', 'wpforms-lite' ); ?></h5> <ul> <?php if ( empty( $this->importer_forms ) ) { echo '<li>' . esc_html__( 'No forms found.', 'wpforms-lite' ) . '</li>'; } else { foreach ( $this->importer_forms as $id => $form ) { printf( '<li><label><input type="checkbox" name="forms[]" value="%s">%s</label></li>', esc_attr( $id ), esc_attr( sanitize_text_field( $form ) ) ); } } ?> </ul> <?php if ( ! empty( $this->importer_forms ) ) : ?> <a href="#" class="all"><?php esc_html_e( 'Select All', 'wpforms-lite' ); ?></a> <?php endif; ?> </div> <div class="second-column"> <h5 class="header"><?php esc_html_e( 'Forms to Import', 'wpforms-lite' ); ?></h5> <ul></ul> </div> </div> </div> <?php if ( ! empty( $this->importer_forms ) ) : ?> <p class="submit"> <button class="wpforms-btn wpforms-btn-md wpforms-btn-orange" id="wpforms-importer-forms-submit"><?php esc_html_e( 'Import', 'wpforms-lite' ); ?></button> </p> <?php endif; ?> </div> <?php } /** * Analyze block. * * @since 1.6.6 */ private function analyze_block() { ?> <div id="wpforms-importer-analyze"> <p class="process-analyze"> <i class="fa fa-spinner fa-spin" aria-hidden="true"></i> <?php printf( wp_kses( /* translators: %s - provider name. */ __( 'Analyzing <span class="form-current">1</span> of <span class="form-total">0</span> forms from %s.', 'wpforms-lite' ), [ 'span' => [ 'class' => [], ], ] ), esc_attr( sanitize_text_field( $this->get_provider_name() ) ) ); ?> </p> <div class="upgrade"> <h5><?php esc_html_e( 'Heads up!', 'wpforms-lite' ); ?></h5> <p><?php esc_html_e( 'One or more of your forms contain fields that are not available in WPForms Lite. To properly import these fields, we recommend upgrading to WPForms Pro.', 'wpforms-lite' ); ?></p> <p><?php esc_html_e( 'You can continue with the import without upgrading, and we will do our best to match the fields. However, some of them will be omitted due to compatibility issues.', 'wpforms-lite' ); ?></p> <p> <a href="<?php echo esc_url( wpforms_admin_upgrade_link( 'tools-import' ) ); ?>" target="_blank" rel="noopener noreferrer" class="wpforms-btn wpforms-btn-md wpforms-btn-orange wpforms-upgrade-modal"><?php esc_html_e( 'Upgrade to WPForms Pro', 'wpforms-lite' ); ?></a> <a href="#" class="wpforms-btn wpforms-btn-md wpforms-btn-light-grey" id="wpforms-importer-continue-submit"><?php esc_html_e( 'Continue Import without Upgrading', 'wpforms-lite' ); ?></a> </p> <hr> <p><?php esc_html_e( 'Below is the list of form fields that may be impacted:', 'wpforms-lite' ); ?></p> </div> </div> <?php } /** * Process block. * * @since 1.6.6 */ private function process_block() { ?> <div id="wpforms-importer-process"> <p class="process-count"> <i class="fa fa-spinner fa-spin" aria-hidden="true"></i> <?php printf( wp_kses( /* translators: %s - provider name. */ __( 'Importing <span class="form-current">1</span> of <span class="form-total">0</span> forms from %s.', 'wpforms-lite' ), [ 'span' => [ 'class' => [], ], ] ), esc_attr( sanitize_text_field( $this->get_provider_name() ) ) ); ?> </p> <p class="process-completed"> <?php echo wp_kses( __( 'Congrats, the import process has finished! We have successfully imported <span class="forms-completed"></span> forms. You can review the results below.', 'wpforms-lite' ), [ 'span' => [ 'class' => [], ], ] ); ?> </p> <div class="status"></div> </div> <?php } /** * Various Underscores templates for form importing. * * @since 1.6.6 */ public function importer_templates() { ?> <script type="text/html" id="tmpl-wpforms-importer-upgrade"> <# _.each( data, function( item, key ) { #> <ul> <li class="form">{{ item.name }}</li> <# _.each( item.fields, function( val, key ) { #> <li>{{ val }}</li> <# }) #> </ul> <# }) #> </script> <script type="text/html" id="tmpl-wpforms-importer-status-error"> <div class="item"> <div class="wpforms-clear"> <span class="name"> <i class="status-icon fa fa-times" aria-hidden="true"></i> {{ data.name }} </span> </div> <p>{{ data.msg }}</p> </div> </script> <script type="text/html" id="tmpl-wpforms-importer-status-update"> <div class="item"> <div class="wpforms-clear"> <span class="name"> <# if ( ! _.isEmpty( data.upgrade_omit ) ) { #> <i class="status-icon fa fa-exclamation-circle" aria-hidden="true"></i> <# } else if ( ! _.isEmpty( data.upgrade_plain ) ) { #> <i class="status-icon fa fa-exclamation-triangle" aria-hidden="true"></i> <# } else if ( ! _.isEmpty( data.unsupported ) ) { #> <i class="status-icon fa fa-info-circle" aria-hidden="true"></i> <# } else { #> <i class="status-icon fa fa-check" aria-hidden="true"></i> <# } #> {{ data.name }} </span> <span class="actions"> <a href="{{ data.edit }}" target="_blank"><?php esc_html_e( 'Edit', 'wpforms-lite' ); ?></a> <span class="sep">|</span> <a href="{{ data.preview }}" target="_blank"><?php esc_html_e( 'Preview', 'wpforms-lite' ); ?></a> </span> </div> <# if ( ! _.isEmpty( data.upgrade_omit ) ) { #> <p><?php esc_html_e( 'The following fields are available in PRO and were not imported:', 'wpforms-lite' ); ?></p> <ul> <# _.each( data.upgrade_omit, function( val, key ) { #> <li>{{ val }}</li> <# }) #> </ul> <# } #> <# if ( ! _.isEmpty( data.upgrade_plain ) ) { #> <p><?php esc_html_e( 'The following fields are available in PRO and were imported as text fields:', 'wpforms-lite' ); ?></p> <ul> <# _.each( data.upgrade_plain, function( val, key ) { #> <li>{{ val }}</li> <# }) #> </ul> <# } #> <# if ( ! _.isEmpty( data.unsupported ) ) { #> <p><?php esc_html_e( 'The following fields are not supported and were not imported:', 'wpforms-lite' ); ?></p> <ul> <# _.each( data.unsupported, function( val, key ) { #> <li>{{ val }}</li> <# }) #> </ul> <# } #> <# if ( ! _.isEmpty( data.upgrade_plain ) || ! _.isEmpty( data.upgrade_omit ) ) { #> <p> <?php esc_html_e( 'Upgrade to the PRO plan to import these fields.', 'wpforms-lite' ); ?><br><br> <a href="<?php echo esc_url( wpforms_admin_upgrade_link( 'tools-import' ) ); ?>" class="wpforms-btn wpforms-btn-orange wpforms-btn-md wpforms-upgrade-modal" target="_blank" rel="noopener noreferrer"> <?php esc_html_e( 'Upgrade Now', 'wpforms-lite' ); ?> </a> </p> <# } #> </div> </script> <?php } } Admin/Tools/Views/System.php 0000644 00000032410 15174710275 0011777 0 ustar 00 <?php // phpcs:ignore Generic.Commenting.DocComment.MissingShort /** @noinspection PhpIllegalPsrClassPathInspection */ namespace WPForms\Admin\Tools\Views; use WPForms\Helpers\DB; /** * Class System. * * @since 1.6.6 */ class System extends View { /** * View slug. * * @since 1.6.6 * * @var string */ protected $slug = 'system'; /** * Init view. * * @since 1.6.6 */ public function init() {} /** * Get view label. * * @since 1.6.6 * * @return string */ public function get_label() { return esc_html__( 'System Info', 'wpforms-lite' ); } /** * Checking user capability to view. * * @since 1.6.6 * * @return bool */ public function check_capability() { return wpforms_current_user_can(); } /** * System view content. * * @since 1.6.6 */ public function display() { ?> <div class="wpforms-setting-row tools wpforms-settings-row-system-information"> <h4 id="form-export"><?php esc_html_e( 'System Information', 'wpforms-lite' ); ?></h4> <textarea id="wpforms-system-information" class="info-area" readonly> <?php echo esc_textarea( $this->get_system_info() ); ?> </textarea> <button type="button" id="wpforms-system-information-copy" class="wpforms-btn wpforms-btn-md wpforms-btn-light-grey"> <?php esc_html_e( 'Copy System Information', 'wpforms-lite' ); ?> </button> </div> <div class="wpforms-setting-row tools wpforms-settings-row-test-ssl"> <h4 id="ssl-verify"><?php esc_html_e( 'Test SSL Connections', 'wpforms-lite' ); ?></h4> <p class="desc"><?php esc_html_e( 'Click the button below to verify your web server can perform SSL connections successfully.', 'wpforms-lite' ); ?></p> <button type="button" id="wpforms-ssl-verify" class="wpforms-btn wpforms-btn-md wpforms-btn-orange"> <?php esc_html_e( 'Test Connection', 'wpforms-lite' ); ?> </button> </div> <?php DB::flush_existing_tables_cache(); if ( DB::custom_tables_exist() ) { return; } ?> <div class="wpforms-setting-row tools wpforms-settings-row-recreate-tables"> <h4 id="recreate-tables"><?php esc_html_e( 'Recreate custom tables', 'wpforms-lite' ); ?></h4> <p class="desc"><?php esc_html_e( 'Click the button below to recreate WPForms custom database tables.', 'wpforms-lite' ); ?></p> <button type="button" id="wpforms-recreate-tables" class="wpforms-btn wpforms-btn-md wpforms-btn-orange"> <?php esc_html_e( 'Recreate Tables', 'wpforms-lite' ); ?> </button> </div> <?php } /** * Get system information. * * Based on a function from Easy Digital Downloads by Pippin Williamson. * * @link https://github.com/easydigitaldownloads/easy-digital-downloads/blob/master/includes/admin/tools.php#L470 * * @since 1.6.6 * * @return string */ public function get_system_info() { $data = '### Begin System Info ###' . "\n\n"; $data .= $this->wpforms_info(); $data .= $this->site_info(); $data .= $this->wp_info(); $data .= $this->uploads_info(); $data .= $this->plugins_info(); $data .= $this->server_info(); $data .= "\n" . '### End System Info ###'; return $data; } /** * Get WPForms info. * * @since 1.6.6 * * @return string */ private function wpforms_info() { $activated = get_option( 'wpforms_activated', [] ); $data = '-- WPForms Info' . "\n\n"; if ( ! empty( $activated['pro'] ) ) { $data .= 'Pro: ' . $this->get_formatted_datetime( $activated['pro'] ) . "\n"; } if ( ! empty( $activated['lite'] ) ) { $data .= 'Lite: ' . $this->get_formatted_datetime( $activated['lite'] ) . "\n"; } $data .= 'Lite Connect: ' . $this->get_lite_connect_info() . "\n"; return $data; } /** * Get Site info. * * @since 1.6.6 * * @return string */ private function site_info() { $data = "\n" . '-- Site Info' . "\n\n"; $data .= 'Site URL: ' . site_url() . "\n"; $data .= 'Home URL: ' . home_url() . "\n"; $data .= 'Multisite: ' . ( is_multisite() ? 'Yes' : 'No' ) . "\n"; return $data; } /** * Get WordPress Configuration info. * * @since 1.6.6 * * @return string */ private function wp_info() { global $wpdb; $theme_data = wp_get_theme(); $theme = $theme_data->name . ' ' . $theme_data->version; $data = "\n" . '-- WordPress Configuration' . "\n\n"; $data .= 'Version: ' . get_bloginfo( 'version' ) . "\n"; $data .= 'Language: ' . get_locale() . "\n"; $data .= 'User Language: ' . get_user_locale() . "\n"; $data .= 'Permalink Structure: ' . ( get_option( 'permalink_structure' ) ? get_option( 'permalink_structure' ) : 'Default' ) . "\n"; $data .= 'Active Theme: ' . $theme . "\n"; $data .= 'Show On Front: ' . get_option( 'show_on_front' ) . "\n"; // Only show page specs if the front page is set to 'page'. if ( get_option( 'show_on_front' ) === 'page' ) { $front_page_id = get_option( 'page_on_front' ); $blog_page_id = get_option( 'page_for_posts' ); $data .= 'Page On Front: ' . ( $front_page_id ? get_the_title( $front_page_id ) . ' (#' . $front_page_id . ')' : 'Unset' ) . "\n"; $data .= 'Page For Posts: ' . ( $blog_page_id ? get_the_title( $blog_page_id ) . ' (#' . $blog_page_id . ')' : 'Unset' ) . "\n"; } $data .= 'ABSPATH: ' . ABSPATH . "\n"; $data .= 'Table Prefix: ' . 'Length: ' . strlen( $wpdb->prefix ) . ' Status: ' . ( strlen( $wpdb->prefix ) > 16 ? 'ERROR: Too long' : 'Acceptable' ) . "\n"; //phpcs:ignore $data .= 'WP_DEBUG: ' . ( defined( 'WP_DEBUG' ) ? WP_DEBUG ? 'Enabled' : 'Disabled' : 'Not set' ) . "\n"; $data .= 'WPFORMS_DEBUG: ' . ( defined( 'WPFORMS_DEBUG' ) ? WPFORMS_DEBUG ? 'Enabled' : 'Disabled' : 'Not set' ) . "\n"; $data .= 'Memory Limit: ' . WP_MEMORY_LIMIT . "\n"; $data .= 'Registered Post Stati: ' . implode( ', ', get_post_stati() ) . "\n"; $data .= 'Revisions: ' . ( WP_POST_REVISIONS ? WP_POST_REVISIONS > 1 ? 'Limited to ' . WP_POST_REVISIONS : 'Enabled' : 'Disabled' ) . "\n"; return $data; } /** * Get Uploads/Constants info. * * @since 1.6.6 * * @return string */ private function uploads_info() { // @todo WPForms configuration/specific details. $data = "\n" . '-- WordPress Uploads/Constants' . "\n\n"; $data .= 'WP_CONTENT_DIR: ' . ( defined( 'WP_CONTENT_DIR' ) ? WP_CONTENT_DIR ? WP_CONTENT_DIR : 'Disabled' : 'Not set' ) . "\n"; $data .= 'WP_CONTENT_URL: ' . ( defined( 'WP_CONTENT_URL' ) ? WP_CONTENT_URL ? WP_CONTENT_URL : 'Disabled' : 'Not set' ) . "\n"; $data .= 'UPLOADS: ' . ( defined( 'UPLOADS' ) ? UPLOADS ? UPLOADS : 'Disabled' : 'Not set' ) . "\n"; $uploads_dir = wp_upload_dir(); $data .= 'wp_uploads_dir() path: ' . $uploads_dir['path'] . "\n"; $data .= 'wp_uploads_dir() url: ' . $uploads_dir['url'] . "\n"; $data .= 'wp_uploads_dir() basedir: ' . $uploads_dir['basedir'] . "\n"; $data .= 'wp_uploads_dir() baseurl: ' . $uploads_dir['baseurl'] . "\n"; return $data; } /** * Get Plugins info. * * @since 1.6.6 * * @return string */ private function plugins_info() { // Get plugins that have an update. $data = $this->mu_plugins(); $data .= $this->installed_plugins(); $data .= $this->multisite_plugins(); return $data; } /** * Get MU Plugins info. * * @since 1.6.6 * * @return string */ private function mu_plugins() { $data = ''; // Must-use plugins. // NOTE: MU plugins can't show updates! $muplugins = get_mu_plugins(); if ( ! empty( $muplugins ) && count( $muplugins ) > 0 ) { $data = "\n" . '-- Must-Use Plugins' . "\n\n"; foreach ( $muplugins as $plugin_data ) { $data .= $plugin_data['Name'] . ': ' . $plugin_data['Version'] . "\n"; } } return $data; } /** * Get Installed Plugins info. * * @since 1.6.6 * * @return string */ private function installed_plugins() { $updates = get_plugin_updates(); // WordPress active plugins. $data = "\n" . '-- WordPress Active Plugins' . "\n\n"; $plugins = get_plugins(); $active_plugins = get_option( 'active_plugins', [] ); foreach ( $plugins as $plugin_path => $plugin ) { if ( ! in_array( $plugin_path, $active_plugins, true ) ) { continue; } $update = ( array_key_exists( $plugin_path, $updates ) ) ? ' (needs update - ' . $updates[ $plugin_path ]->update->new_version . ')' : ''; $data .= $plugin['Name'] . ': ' . $plugin['Version'] . $update . "\n"; } // WordPress inactive plugins. $data .= "\n" . '-- WordPress Inactive Plugins' . "\n\n"; foreach ( $plugins as $plugin_path => $plugin ) { if ( in_array( $plugin_path, $active_plugins, true ) ) { continue; } $update = ( array_key_exists( $plugin_path, $updates ) ) ? ' (needs update - ' . $updates[ $plugin_path ]->update->new_version . ')' : ''; $data .= $plugin['Name'] . ': ' . $plugin['Version'] . $update . "\n"; } return $data; } /** * Get Multisite Plugins info. * * @since 1.6.6 * * @return string */ private function multisite_plugins() { $data = ''; if ( ! is_multisite() ) { return $data; } $updates = get_plugin_updates(); // WordPress Multisite active plugins. $data = "\n" . '-- Network Active Plugins' . "\n\n"; $plugins = wp_get_active_network_plugins(); $active_plugins = get_site_option( 'active_sitewide_plugins', [] ); foreach ( $plugins as $plugin_path ) { $plugin_base = plugin_basename( $plugin_path ); if ( ! array_key_exists( $plugin_base, $active_plugins ) ) { continue; } $update = ( array_key_exists( $plugin_path, $updates ) ) ? ' (needs update - ' . $updates[ $plugin_path ]->update->new_version . ')' : ''; $plugin = get_plugin_data( $plugin_path ); $data .= $plugin['Name'] . ': ' . $plugin['Version'] . $update . "\n"; } return $data; } /** * Get Server info. * * @since 1.6.6 * * @return string */ private function server_info() { global $wpdb; // Server configuration (really just versions). $data = "\n" . '-- Webserver Configuration' . "\n\n"; $data .= 'PHP Version: ' . PHP_VERSION . "\n"; $data .= 'MySQL Version: ' . $wpdb->db_version() . "\n"; $data .= 'Webserver Info: ' . ( isset( $_SERVER['SERVER_SOFTWARE'] ) ? sanitize_text_field( wp_unslash( $_SERVER['SERVER_SOFTWARE'] ) ) : '' ) . "\n"; // PHP configs... now we're getting to the important stuff. $data .= "\n" . '-- PHP Configuration' . "\n\n"; $data .= 'Memory Limit: ' . ini_get( 'memory_limit' ) . "\n"; $data .= 'Upload Max Size: ' . ini_get( 'upload_max_filesize' ) . "\n"; $data .= 'Post Max Size: ' . ini_get( 'post_max_size' ) . "\n"; $data .= 'Upload Max Filesize: ' . ini_get( 'upload_max_filesize' ) . "\n"; $data .= 'Time Limit: ' . ini_get( 'max_execution_time' ) . "\n"; $data .= 'Max Input Vars: ' . ini_get( 'max_input_vars' ) . "\n"; $data .= 'Display Errors: ' . ( ini_get( 'display_errors' ) ? 'On (' . ini_get( 'display_errors' ) . ')' : 'N/A' ) . "\n"; // PHP extensions and such. $data .= "\n" . '-- PHP Extensions' . "\n\n"; $data .= 'cURL: ' . ( function_exists( 'curl_init' ) ? 'Supported' : 'Not Supported' ) . "\n"; $data .= 'fsockopen: ' . ( function_exists( 'fsockopen' ) ? 'Supported' : 'Not Supported' ) . "\n"; $data .= 'SOAP Client: ' . ( class_exists( 'SoapClient', false ) ? 'Installed' : 'Not Installed' ) . "\n"; $data .= 'Suhosin: ' . ( extension_loaded( 'suhosin' ) ? 'Installed' : 'Not Installed' ) . "\n"; // Session stuff. $data .= "\n" . '-- Session Configuration' . "\n\n"; $data .= 'Session: ' . ( isset( $_SESSION ) ? 'Enabled' : 'Disabled' ) . "\n"; // The rest of this is only relevant if session is enabled. if ( isset( $_SESSION ) ) { $data .= 'Session Name: ' . esc_html( ini_get( 'session.name' ) ) . "\n"; $data .= 'Cookie Path: ' . esc_html( ini_get( 'session.cookie_path' ) ) . "\n"; $data .= 'Save Path: ' . esc_html( ini_get( 'session.save_path' ) ) . "\n"; $data .= 'Use Cookies: ' . ( ini_get( 'session.use_cookies' ) ? 'On' : 'Off' ) . "\n"; $data .= 'Use Only Cookies: ' . ( ini_get( 'session.use_only_cookies' ) ? 'On' : 'Off' ) . "\n"; } return $data; } /** * Get Lite Connect status info string. * * @since 1.7.5 * * @return string */ private function get_lite_connect_info() { $lc_enabled = wpforms_setting( 'lite-connect-enabled' ); $lc_enabled_since = wpforms_setting( 'lite-connect-enabled-since' ); $date = $this->get_formatted_datetime( $lc_enabled_since ); if ( $lc_enabled ) { $string = $lc_enabled_since ? 'Backup is enabled since ' . $date : 'Backup is enabled'; } else { $string = $lc_enabled_since ? 'Backup is not enabled. Previously was enabled since ' . $date : 'Backup is not enabled'; } return $string; } /** * Get formatted datetime. * * @since 1.8.5 * * @param int|string $date Date. * * @return string */ private function get_formatted_datetime( $date ) { return sprintf( '%1$s at %2$s (GMT)', gmdate( 'M j, Y', $date ), gmdate( 'g:ia', $date ) ); } } Admin/Tools/Views/Import.php 0000644 00000026546 15174710275 0012002 0 ustar 00 <?php namespace WPForms\Admin\Tools\Views; use WP_Error; use WPForms\Helpers\File; use WPForms\Admin\Tools\Importers; use WPForms\Admin\Tools\Tools; use WPForms_Form_Handler; use WPForms\Admin\Notice; /** * Class Import. * * @since 1.6.6 */ class Import extends View { /** * View slug. * * @since 1.6.6 * * @var string */ protected $slug = 'import'; /** * Registered importers. * * @since 1.6.6 * * @var array */ public $importers = []; /** * Checking user capability to view. * * @since 1.6.6 * * @return bool */ public function check_capability() { return wpforms_current_user_can( 'create_forms' ); } /** * Determine whether user has the "unfiltered_html" capability. * * By default, the "unfiltered_html" permission is only given to * Super Admins, Administrators and Editors. * * @since 1.7.9 * * @return bool */ private function check_unfiltered_html_capability() { return current_user_can( 'unfiltered_html' ); } /** * Init view. * * @since 1.6.6 */ public function init() { // Bail early, in case the current user lacks the `unfiltered_html` capability. if ( ! $this->check_unfiltered_html_capability() ) { $this->error_unfiltered_html_import_message(); return; } $this->hooks(); $this->importers = ( new Importers() )->get_importers(); } /** * Register hooks. * * @since 1.7.9 */ private function hooks() { add_action( 'wpforms_tools_init', [ $this, 'import_process' ] ); } /** * Get view label. * * @since 1.6.6 * * @return string */ public function get_label() { return esc_html__( 'Import', 'wpforms-lite' ); } /** * Import process. * * @since 1.6.6 */ public function import_process() { // phpcs:disable WordPress.Security.NonceVerification.Missing if ( empty( $_POST['action'] ) || $_POST['action'] !== 'import_form' || empty( $_FILES['file']['tmp_name'] ) || ! isset( $_POST['submit-import'] ) || ! $this->verify_nonce() ) { return; } // phpcs:enable WordPress.Security.NonceVerification.Missing $this->process(); } /** * Import view content. * * @since 1.6.6 */ public function display() { // Bail early, in case the current user lacks the `unfiltered_html` capability. if ( ! $this->check_unfiltered_html_capability() ) { return; } $this->success_import_message(); $this->wpforms_block(); $this->other_forms_block(); } /** * Success import message. * * @since 1.6.6 */ private function success_import_message() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( isset( $_GET['wpforms_notice'] ) && $_GET['wpforms_notice'] === 'forms-imported' ) { ?> <div class="updated notice is-dismissible"> <p> <?php esc_html_e( 'Import was successfully finished.', 'wpforms-lite' ); ?> <?php if ( wpforms_current_user_can( 'view_forms' ) ) { printf( wp_kses( /* translators: %s - forms list page URL. */ __( 'You can go and <a href="%s">check your forms</a>.', 'wpforms-lite' ), [ 'a' => [ 'href' => [] ] ] ), esc_url( admin_url( 'admin.php?page=wpforms-overview' ) ) ); } ?> </p> </div> <?php } } /** * Error message for users with no `unfiltered_html` permission. * * @since 1.7.9 */ private function error_unfiltered_html_import_message() { Notice::error( sprintf( wp_kses( /* translators: %s - WPForms contact page URL. */ __( 'You can’t import forms because you don’t have unfiltered HTML permissions. Please contact your site administrator or <a href="%s" target="_blank" rel="noopener noreferrer">reach out to our support team</a>.', 'wpforms-lite' ), [ 'a' => [ 'href' => [], 'target' => [], 'rel' => [], ], ] ), 'https://wpforms.com/contact/' ) ); } /** * WPForms section. * * @since 1.6.6 */ private function wpforms_block() { ?> <div class="wpforms-setting-row tools wpforms-settings-row-divider"> <h4><?php esc_html_e( 'WPForms Import', 'wpforms-lite' ); ?></h4> <p><?php esc_html_e( 'Select a WPForms export file.', 'wpforms-lite' ); ?></p> <form method="post" enctype="multipart/form-data" action="<?php echo esc_attr( $this->get_link() ); ?>"> <div class="wpforms-file-upload"> <input type="file" name="file" id="wpforms-tools-form-import" class="inputfile" data-multiple-caption="{count} <?php esc_attr_e( 'files selected', 'wpforms-lite' ); ?>" accept=".json" /> <label for="wpforms-tools-form-import"> <span class="fld"><span class="placeholder"><?php esc_html_e( 'No file chosen', 'wpforms-lite' ); ?></span></span> <strong class="wpforms-btn wpforms-btn-md wpforms-btn-light-grey"> <i class="fa fa-cloud-upload"></i><?php esc_html_e( 'Choose a File', 'wpforms-lite' ); ?> </strong> </label> </div> <input type="hidden" name="action" value="import_form"> <button name="submit-import" class="wpforms-btn wpforms-btn-md wpforms-btn-orange" id="wpforms-import" aria-disabled="true"> <?php esc_html_e( 'Import', 'wpforms-lite' ); ?> </button> <?php $this->nonce_field(); ?> </form> </div> <?php } /** * WPForms section. * * @since 1.6.6 */ private function other_forms_block() { ?> <div class="wpforms-setting-row tools" id="wpforms-importers"> <h4><?php esc_html_e( 'Import from Other Form Plugins', 'wpforms-lite' ); ?></h4> <p><?php esc_html_e( 'Not happy with other WordPress contact form plugins?', 'wpforms-lite' ); ?></p> <p><?php esc_html_e( 'WPForms makes it easy for you to switch by allowing you import your third-party forms with a single click.', 'wpforms-lite' ); ?></p> <div class="wpforms-importers-wrap"> <?php if ( empty( $this->importers ) ) { ?> <p><?php esc_html_e( 'No form importers are currently enabled.', 'wpforms-lite' ); ?> </p> <?php } else { ?> <form action="<?php echo esc_url( admin_url( 'admin.php' ) ); ?>"> <span class="choicesjs-select-wrap"> <select id="wpforms-tools-form-other-import" class="choicesjs-select" name="provider" data-search="<?php echo esc_attr( wpforms_choices_js_is_search_enabled( $this->importers ) ); ?>" required> <option value=""><?php esc_html_e( 'Select previous contact form plugin...', 'wpforms-lite' ); ?></option> <?php foreach ( $this->importers as $importer ) { $status = ''; if ( empty( $importer['installed'] ) ) { $status = esc_html__( 'Not Installed', 'wpforms-lite' ); } elseif ( empty( $importer['active'] ) ) { $status = esc_html__( 'Not Active', 'wpforms-lite' ); } printf( '<option value="%s" %s>%s %s</option>', esc_attr( $importer['slug'] ), ! empty( $status ) ? 'disabled' : '', esc_html( $importer['name'] ), ! empty( $status ) ? '(' . esc_html( $status ) . ')' : '' // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ); } ?> </select> </span> <input type="hidden" name="page" value="<?php echo esc_attr( Tools::SLUG ); ?>"> <input type="hidden" name="view" value="importer"> <button class="wpforms-btn wpforms-btn-md wpforms-btn-orange" id="wpforms-import-other" aria-disabled="true"> <?php esc_html_e( 'Import', 'wpforms-lite' ); ?> </button> </form> <?php } ?> </div> </div> <?php } /** * Import processing. * * @since 1.6.6 */ private function process() { // phpcs:ignore WPForms.PHP.HooksMethod.InvalidPlaceForAddingHooks // Add filter of the link rel attr to avoid JSON damage. add_filter( 'wp_targeted_link_rel', '__return_empty_string', 50, 1 ); $ext = ''; // phpcs:disable WordPress.Security.NonceVerification.Missing if ( isset( $_FILES['file']['name'] ) ) { $ext = strtolower( pathinfo( sanitize_text_field( wp_unslash( $_FILES['file']['name'] ) ), PATHINFO_EXTENSION ) ); } if ( $ext !== 'json' ) { wp_die( esc_html__( 'Please upload a valid .json form export file.', 'wpforms-lite' ), esc_html__( 'Error', 'wpforms-lite' ), [ 'response' => 400, ] ); } // The wp_unslash() function breaks upload on Windows. // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.NonceVerification.Missing $filename = isset( $_FILES['file']['tmp_name'] ) ? sanitize_text_field( $_FILES['file']['tmp_name'] ) : ''; // phpcs:enable WordPress.Security.NonceVerification.Missing $result = self::import_forms( $filename ); if ( $result !== null ) { wp_die( esc_html( $result->get_error_message() ), esc_html__( 'Error', 'wpforms-lite' ), [ 'response' => 400, ] ); } wp_safe_redirect( add_query_arg( [ 'wpforms_notice' => 'forms-imported' ] ) ); exit; } /** * Import forms from file. * Should be static for external use. * * @since 1.8.6 * * @param string $filename File containing forms to be imported. * * @return null|WP_Error */ public static function import_forms( string $filename ) { if ( ! current_user_can( 'unfiltered_html' ) ) { return new WP_Error( 'no_permission', __( 'The unfiltered HTML permissions are required to import form.', 'wpforms-lite' ) ); } // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents $forms = json_decode( File::remove_utf8_bom( file_get_contents( $filename ) ), true ); if ( empty( $forms ) || ! is_array( $forms ) ) { return new WP_Error( 'bad_json', __( 'Please upload a valid .json form export file.', 'wpforms-lite' ) ); } if ( ! self::save_forms( $forms ) ) { return new WP_Error( 'no_permission', __( 'There was an error saving your form. Please check your file and try again.', 'wpforms-lite' ) ); } return null; } /** * Save forms. * * @since 1.8.6 * * @param array $forms Forms. * * @return bool */ private static function save_forms( array $forms ): bool { foreach ( $forms as $form ) { $title = ! empty( $form['settings']['form_title'] ) ? $form['settings']['form_title'] : ''; $desc = ! empty( $form['settings']['form_desc'] ) ? $form['settings']['form_desc'] : ''; $new_id = wp_insert_post( [ 'post_title' => wp_slash( $title ), 'post_status' => 'publish', 'post_type' => 'wpforms', 'post_excerpt' => wp_slash( $desc ), ] ); // When we cannot insert one form into the DB, or update it, // we will have a similar issue with the following form in the JSON file. // So, it is better to bail out and inform the user that we cannot proceed. if ( ! $new_id ) { return false; } $form['id'] = $new_id; if ( ! self::update_form( $form ) ) { return false; } } return true; } /** * Update form. * * @since 1.8.6 * * @param array $form Form. * * @return bool */ private static function update_form( array $form ): bool { if ( wpforms_is_form_data_slashing_enabled() ) { $form = wp_slash( $form ); } $result = wp_update_post( [ 'ID' => $form['id'], 'post_content' => wpforms_encode( $form ), ] ); if ( ! $result ) { return false; } if ( empty( $form['settings']['form_tags'] ) ) { return true; } $result = wp_set_post_terms( $form['id'], implode( ',', (array) $form['settings']['form_tags'] ), WPForms_Form_Handler::TAGS_TAXONOMY ); if ( ! $result ) { return false; } return true; } } Admin/Tools/Views/Export.php 0000644 00000022643 15174710275 0012003 0 ustar 00 <?php namespace WPForms\Admin\Tools\Views; /** * Class Export. * * @since 1.6.6 */ class Export extends View { /** * View slug. * * @since 1.6.6 * * @var string */ protected $slug = 'export'; /** * Template code if generated. * * @since 1.6.6 * * @var string */ private $template = ''; /** * Existed forms. * * @since 1.6.6 * * @var [] */ private $forms = []; /** * Init view. * * @since 1.6.6 */ public function init() { add_action( 'wpforms_tools_init', [ $this, 'process' ] ); } /** * Get view label. * * @since 1.6.6 * * @return string */ public function get_label() { return esc_html__( 'Export', 'wpforms-lite' ); } /** * Export process. * * @since 1.6.6 */ public function process() { if ( empty( $_POST['action'] ) || //phpcs:ignore WordPress.Security.NonceVerification ! isset( $_POST['submit-export'] ) || //phpcs:ignore WordPress.Security.NonceVerification ! $this->verify_nonce() ) { return; } if ( $_POST['action'] === 'export_form' && ! empty( $_POST['forms'] ) ) { //phpcs:ignore WordPress.Security.NonceVerification $this->process_form(); } if ( $_POST['action'] === 'export_template' && ! empty( $_POST['form'] ) ) { //phpcs:ignore WordPress.Security.NonceVerification $this->process_template(); } } /** * Checking user capability to view. * * @since 1.6.6 * * @return bool */ public function check_capability() { return wpforms_current_user_can( [ 'edit_forms', 'view_entries' ] ); } /** * Get available forms. * * @since 1.6.6 * * @return array */ public function get_forms() { $forms = wpforms()->obj( 'form' )->get( '', [ 'orderby' => 'title' ] ); return ! empty( $forms ) ? $forms : []; } /** * Export view content. * * @since 1.6.6 */ public function display() { $this->forms = $this->get_forms(); if ( empty( $this->forms ) ) { echo wpforms_render( 'admin/empty-states/no-forms' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped return; } do_action( 'wpforms_admin_tools_export_top' ); $this->forms_export_block(); $this->form_template_export_block(); do_action( 'wpforms_admin_tools_export_bottom' ); } /** * Forms export block. * * @since 1.6.6 */ private function forms_export_block() { ?> <div class="wpforms-setting-row tools wpforms-settings-row-divider"> <h4 id="form-export"><?php esc_html_e( 'Export Forms', 'wpforms-lite' ); ?></h4> <p><?php esc_html_e( 'Use form export files to create a backup of your forms or to import forms to another site.', 'wpforms-lite' ); ?></p> <?php if ( ! empty( $this->forms ) ) { ?> <form method="post" action="<?php echo esc_attr( $this->get_link() ); ?>"> <?php $this->forms_select_html( 'wpforms-tools-form-export', 'forms[]', esc_html__( 'Select Form(s)', 'wpforms-lite' ) ); ?> <input type="hidden" name="action" value="export_form"> <?php $this->nonce_field(); ?> <button name="submit-export" class="wpforms-btn wpforms-btn-md wpforms-btn-orange" id="wpforms-export-form" aria-disabled="true"> <?php esc_html_e( 'Export', 'wpforms-lite' ); ?> </button> </form> <?php } else { ?> <p><?php esc_html_e( 'You need to create a form before you can use form export.', 'wpforms-lite' ); ?></p> <?php } ?> </div> <?php } /** * Forms export block. * * @since 1.6.6 */ private function form_template_export_block() { ?> <div class="wpforms-setting-row tools"> <h4 id="template-export"><?php esc_html_e( 'Export a Form Template', 'wpforms-lite' ); ?></h4> <?php if ( $this->template ) { $doc_link = sprintf( wp_kses( /* translators: %s - WPForms.com docs URL. */ __( 'For more information <a href="%s" target="_blank" rel="noopener noreferrer">see our documentation</a>.', 'wpforms-lite' ), [ 'a' => [ 'href' => [], 'target' => [], 'rel' => [], ], ] ), 'https://wpforms.com/docs/how-to-create-a-custom-form-template/' ); ?> <p><?php esc_html_e( 'The following code can be used to register your custom form template. Copy and paste the following code to your theme\'s functions.php file or include it within an external file.', 'wpforms-lite' ); ?><p> <p><?php echo $doc_link; //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?><p> <textarea class="info-area" readonly><?php echo esc_textarea( $this->template ); ?></textarea> <?php } ?> <p><?php esc_html_e( 'Select a form to generate PHP code that can be used to register a custom form template.', 'wpforms-lite' ); ?></p> <?php if ( ! empty( $this->forms ) ) { ?> <form method="post" action="<?php echo esc_attr( $this->get_link() ); ?>"> <?php $this->forms_select_html( 'wpforms-tools-form-template', 'form', esc_html__( 'Select a Template', 'wpforms-lite' ), false ); ?> <input type="hidden" name="action" value="export_template"> <?php $this->nonce_field(); ?> <button name="submit-export" class="wpforms-btn wpforms-btn-md wpforms-btn-orange" id="wpforms-export-template" aria-disabled="true"> <?php esc_html_e( 'Export Template', 'wpforms-lite' ); ?> </button> </form> <?php } else { ?> <p><?php esc_html_e( 'You need to create a form before you can generate a template.', 'wpforms-lite' ); ?></p> <?php } ?> </div> <?php } /** * Forms selector. * * @since 1.6.6 * * @param string $select_id Select id. * @param string $select_name Select name. * @param string $placeholder Placeholder. * @param bool $multiple Is multiple select. */ private function forms_select_html( $select_id, $select_name, $placeholder, $multiple = true ) { ?> <span class="choicesjs-select-wrap"> <select id="<?php echo esc_attr( $select_id ); ?>" class="choicesjs-select" name="<?php echo esc_attr( $select_name ); ?>" <?php if ( $multiple ) { //phpcs:ignore ?> multiple size="1" <?php } ?> data-search="<?php echo esc_attr( wpforms_choices_js_is_search_enabled( $this->forms ) ); ?>"> <option value=""><?php echo esc_attr( $placeholder ); ?></option> <?php foreach ( $this->forms as $form ) { ?> <option value="<?php echo absint( $form->ID ); ?>"><?php echo esc_html( $form->post_title ); ?></option> <?php } ?> </select> </span> <?php } /** * Export processing. * * @since 1.6.6 */ private function process_form() { $export = []; $forms = get_posts( [ 'post_type' => 'wpforms', 'nopaging' => true, 'post__in' => isset( $_POST['forms'] ) ? array_map( 'intval', $_POST['forms'] ) : [], //phpcs:ignore WordPress.Security.NonceVerification ] ); foreach ( $forms as $form ) { $export[] = wpforms_decode( $form->post_content ); } ignore_user_abort( true ); wpforms_set_time_limit(); nocache_headers(); header( 'Content-Type: application/json; charset=utf-8' ); header( 'Content-Disposition: attachment; filename=wpforms-form-export-' . current_time( 'm-d-Y' ) . '.json' ); header( 'Expires: 0' ); echo wp_json_encode( $export ); exit; } /** * Export template processing. * * @since 1.6.6 */ private function process_template(): void { // Nonce is checked in the caller: process() method. //phpcs:ignore WordPress.Security.NonceVerification.Missing $form_id = isset( $_POST['form'] ) ? absint( $_POST['form'] ) : 0; $form_obj = wpforms()->obj( 'form' ); if ( ! $form_obj || ! $form_id ) { return; } $form_data = $form_obj->get( $form_id, [ 'content_only' => true ] ); // Define basic data with strict validation. $name = sanitize_text_field( $form_data['settings']['form_title'] ?? '' ); $desc = sanitize_text_field( $form_data['settings']['form_desc'] ?? '' ); $slug = sanitize_key( str_replace( [ ' ', '-' ], '_', trim( $name ) ) ); if ( ! $slug ) { // Slug is always empty when the $form_data is not valid. return; } $class = 'WPForms_Template_' . $slug; $data = $this->get_template_data( $slug, $form_data ); // Build the final template string. $this->template = <<<EOT if ( class_exists( 'WPForms_Template', false ) ) : /** * {$name} * Template for WPForms. */ class {$class} extends WPForms_Template { /** * Primary class constructor. * * @since 1.0.0 */ public function init() { // Template name \$this->name = '{$name}'; // Template slug \$this->slug = '{$slug}'; // Template description \$this->description = '{$desc}'; // Template field and settings \$this->data = {$data}; } } new {$class}(); endif; EOT; } /** * Get template data. * * @since 1.9.5 * * @param string $slug Template slug. * @param array|mixed $form_data Form data. * * @return string */ private function get_template_data( string $slug, $form_data ): string { // Format template field and settings data. $data = []; $data['meta']['template'] = $slug; $data['fields'] = isset( $form_data['fields'] ) && is_array( $form_data['fields'] ) ? wpforms_array_remove_empty_strings( $form_data['fields'] ) : []; $data['settings'] = isset( $form_data['settings'] ) && is_array( $form_data['settings'] ) ? wpforms_array_remove_empty_strings( $form_data['settings'] ) : []; $template_data = (string) var_export( $data, true ); //phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_var_export $template_data = str_replace( ' ', "\t", $template_data ); return preg_replace( '/([\t\r\n]+?)array/', 'array', $template_data ); } } Admin/Tools/Views/ActionSchedulerList.php 0000644 00000004623 15174710275 0014430 0 ustar 00 <?php namespace WPForms\Admin\Tools\Views; use ActionScheduler as Scheduler; use ActionScheduler_ListTable; /** * Action Scheduler list table. * * @since 1.7.6 */ class ActionSchedulerList extends ActionScheduler_ListTable { /** * ActionSchedulerList constructor. * * @since 1.7.6 */ public function __construct() { parent::__construct( Scheduler::store(), Scheduler::logger(), Scheduler::runner() ); $this->process_actions(); } /** * Display the table heading. * * @since 1.7.6 */ protected function display_header() { ?> <h1><?php echo esc_html__( 'Scheduled Actions', 'wpforms-lite' ); ?></h1> <p> <?php echo sprintf( wp_kses( /* translators: %s - Action Scheduler website URL. */ __( 'WPForms is using the <a href="%s" target="_blank" rel="noopener noreferrer">Action Scheduler</a> library, which allows it to queue and process bigger tasks in the background without making your site slower for your visitors. Below you can see the list of all tasks and their status. This table can be very useful when debugging certain issues.', 'wpforms-lite' ), [ 'a' => [ 'href' => [], 'rel' => [], 'target' => [], ], ] ), 'https://actionscheduler.org/' ); ?> </p> <p> <?php echo esc_html__( 'Action Scheduler library is also used by other plugins, like WP Mail SMTP and WooCommerce, so you might see tasks that are not related to our plugin in the table below.', 'wpforms-lite' ); ?> </p> <?php // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( ! empty( $_GET['s'] ) ) { ?> <div id="wpforms-reset-filter"> <?php echo wp_kses( sprintf( /* translators: %s - search term. */ __( 'Search results for <strong>%s</strong>', 'wpforms-lite' ), // phpcs:ignore WordPress.Security.NonceVerification.Recommended sanitize_text_field( wp_unslash( $_GET['s'] ) ) ), [ 'strong' => [], ] ); ?> <a href="<?php echo esc_url( remove_query_arg( 's' ) ); ?>"> <span class="reset fa fa-times-circle"></span> </a> </div> <?php } } /** * Return the sortable column specified for this request to order the results by, if any. * * @since 1.9.1 * * @return string[] */ protected function get_request_query_args_to_persist() { return array_merge( parent::get_request_query_args_to_persist(), [ 'view' ] ); } } Admin/Tools/Views/View.php 0000644 00000003320 15174710275 0011423 0 ustar 00 <?php namespace WPForms\Admin\Tools\Views; use WPForms\Admin\Tools\Tools; /** * Single View class. * * @since 1.6.6 */ abstract class View { /** * View slug. * * @since 1.6.6 * * @var string */ protected $slug; /** * Init. * * @since 1.6.6 */ abstract public function init(); /** * Get link to the view page. * * @since 1.6.6 * * @return string */ public function get_link() { return add_query_arg( [ 'page' => Tools::SLUG, 'view' => $this->slug, ], admin_url( 'admin.php' ) ); } /** * Get view label. * * @since 1.6.6 * * @return string */ abstract public function get_label(); /** * Checking user capability to view. * * @since 1.6.6 * * @return bool */ abstract public function check_capability(); /** * Checking if needs display in navigation. * * @since 1.6.6 * * @return bool */ public function hide_from_nav() { return false; } /** * Checking if navigation needs display. * * @since 1.6.6 * * @return bool */ public function show_nav() { return true; } /** * Display nonce field. * * @since 1.6.6 */ public function nonce_field() { wp_nonce_field( 'wpforms_' . $this->slug . '_nonce', 'wpforms-tools-' . $this->slug . '-nonce' ); } /** * Verify nonce field. * * @since 1.6.6 */ public function verify_nonce(): bool { $nonce_name = 'wpforms-tools-' . $this->slug . '-nonce'; $nonce = isset( $_POST[ $nonce_name ] ) ? sanitize_text_field( wp_unslash( $_POST[ $nonce_name ] ) ) : ''; return (bool) wp_verify_nonce( $nonce, 'wpforms_' . $this->slug . '_nonce' ); } /** * Display view content. * * @since 1.6.6 */ abstract public function display(); } Admin/Tools/Importers.php 0000644 00000002477 15174710275 0011414 0 ustar 00 <?php namespace WPForms\Admin\Tools; use WPForms\Admin\Tools\Importers\ContactForm7; use WPForms\Admin\Tools\Importers\NinjaForms; use WPForms\Admin\Tools\Importers\PirateForms; /** * Load the different form importers. * * @since 1.6.6 */ class Importers { /** * Available importers. * * @since 1.6.6 * * @var array */ private $importers = []; /** * Load default form importers. * * @since 1.6.6 */ public function load() { if ( empty( $this->importers ) ) { $this->importers = [ 'contact-form-7' => new ContactForm7(), 'ninja-forms' => new NinjaForms(), 'pirate-forms' => new PirateForms(), ]; } } /** * Load default form importers. * * @since 1.6.6 * * @return array */ public function get_importers() { $this->load(); $importers = []; foreach ( $this->importers as $importer ) { $importers = $importer->register( $importers ); } return apply_filters( 'wpforms_importers', $importers ); } /** * Get a importer forms. * * @since 1.6.6 * * @param string $provider Provider. * * @return array */ public function get_importer_forms( $provider ) { if ( isset( $this->importers[ $provider ] ) ) { return apply_filters( "wpforms_importer_forms_{$provider}", $this->importers[ $provider ]->get_forms() ); } return []; } } Admin/Tools/Tools.php 0000644 00000007417 15174710275 0010527 0 ustar 00 <?php namespace WPForms\Admin\Tools; /** * Main Tools class. * * @since 1.6.6 */ class Tools { /** * Tools page slug. * * @since 1.6.6 */ const SLUG = 'wpforms-tools'; /** * Available pages. * * @since 1.6.6 * * @var array */ private $views = []; /** * The current view. * * @since 1.6.6 * * @var null|\WPForms\Admin\Tools\Views\View */ private $view; /** * The active view slug. * * @since 1.6.6 * * @var string */ private $active_view_slug; /** * Initialize class. * * @since 1.6.6 */ public function init() { if ( ! $this->is_tools_page() ) { return; } $this->init_view(); $this->hooks(); } /** * Check if we're on tools page. * * @since 1.6.6 * * @return bool */ private function is_tools_page() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $page = isset( $_GET['page'] ) ? sanitize_key( $_GET['page'] ) : ''; // Only load if we are actually on the settings page. return $page === self::SLUG; } /** * Init current view. * * @since 1.6.6 */ private function init_view() { $view_ids = array_keys( $this->get_views() ); // Determine the current active settings tab. // phpcs:ignore WordPress.Security.NonceVerification.Recommended $this->active_view_slug = ! empty( $_GET['view'] ) ? sanitize_key( $_GET['view'] ) : 'import'; // If the user tries to load an invalid view - fallback to the first available. if ( ! in_array( $this->active_view_slug, $view_ids, true ) && ! has_action( 'wpforms_tools_display_tab_' . $this->active_view_slug ) ) { $this->active_view_slug = reset( $view_ids ); } if ( isset( $this->views[ $this->active_view_slug ] ) ) { $this->view = $this->views[ $this->active_view_slug ]; $this->view->init(); } } /** * Get Views. * * @since 1.6.6 * * @return array */ public function get_views() { if ( empty( $this->views ) ) { $this->views = [ 'import' => new Views\Import(), 'importer' => new Views\Importer(), 'export' => new Views\Export(), 'entry-automation' => new Views\EntryAutomation(), 'system' => new Views\System(), 'action-scheduler' => new Views\ActionScheduler(), 'logs' => new Views\Logs(), 'wpcode' => new Views\CodeSnippets(), ]; } $this->views = apply_filters( 'wpforms_tools_views', $this->views ); return array_filter( $this->views, static function ( $view ) { return $view->check_capability(); } ); } /** * Register hooks. * * @since 1.6.6 */ public function hooks() { add_action( 'wpforms_admin_page', [ $this, 'output' ] ); // Hook for addons. do_action( 'wpforms_tools_init' ); } /** * Build the output for the Tools admin page. * * @since 1.6.6 */ public function output() { ?> <div id="wpforms-tools" class="wrap wpforms-admin-wrap wpforms-tools-tab-<?php echo esc_attr( $this->active_view_slug ); ?>"> <?php if ( $this->view && $this->view->show_nav() ) { echo '<ul class="wpforms-admin-tabs">'; foreach ( $this->views as $slug => $view ) { if ( $view->hide_from_nav() || ! $view->check_capability() ) { continue; } echo '<li>'; printf( '<a href="%1$s" class="%2$s">%3$s</a>', esc_url( $view->get_link() ), sanitize_html_class( $this->active_view_slug === $slug ? 'active' : '' ), esc_html( $view->get_label() ) ); echo '</li>'; } echo '</ul>'; } ?> <h1 class="wpforms-h1-placeholder"></h1> <div class="wpforms-admin-content wpforms-admin-settings"> <?php if ( $this->view ) { $this->view->display(); } else { do_action( 'wpforms_tools_display_tab_' . $this->active_view_slug ); } ?> </div> </div> <?php } } Admin/Tools/Importers/NinjaForms.php 0000644 00000040167 15174710275 0013460 0 ustar 00 <?php // phpcs:disable Generic.Commenting.DocComment.MissingShort /** @noinspection PhpUndefinedClassInspection */ /** @noinspection PhpUndefinedFunctionInspection */ // phpcs:enable Generic.Commenting.DocComment.MissingShort namespace WPForms\Admin\Tools\Importers; use NF_Database_Models_Action; use NF_Database_Models_Field; use NF_Database_Models_Form; /** * Ninja Forms Importer class. * * @since 1.6.6 */ class NinjaForms extends Base { /** * Define required properties. * * @since 1.6.6 */ public function init() { $this->name = 'Ninja Forms'; $this->slug = 'ninja-forms'; $this->path = 'ninja-forms/ninja-forms.php'; } /** * Get ALL THE FORMS. * * @since 1.6.6 * * @return NF_Database_Models_Form[] */ public function get_forms() { $forms_final = []; if ( ! $this->is_active() ) { return $forms_final; } // phpcs:ignore WordPress.WP.Capabilities.Unknown, WPForms.Comments.PHPDocHooks.RequiredHookDocumentation, WPForms.PHP.ValidateHooks.InvalidHookName if ( ! current_user_can( apply_filters( 'ninja_forms_admin_all_forms_capabilities', 'manage_options' ) ) ) { return $forms_final; } $forms = Ninja_Forms()->form()->get_forms(); if ( ! empty( $forms ) ) { foreach ( $forms as $form ) { if ( ! $form instanceof NF_Database_Models_Form ) { continue; } $forms_final[ $form->get_id() ] = $form->get_setting( 'title' ); } } return $forms_final; } /** * Get a single form. * * @since 1.6.6 * * @param int $id Form ID. * * @return array */ public function get_form( $id ) { $form = []; $form['settings'] = Ninja_Forms()->form( $id )->get()->get_settings(); $fields = Ninja_Forms()->form( $id )->get_fields(); $actions = Ninja_Forms()->form( $id )->get_actions(); foreach ( $fields as $field ) { if ( ! $field instanceof NF_Database_Models_Field ) { continue; } $form['fields'][] = array_merge( [ 'id' => $field->get_id(), ], $field->get_settings() ); } foreach ( $actions as $action ) { if ( ! $action instanceof NF_Database_Models_Action ) { continue; } $form['actions'][] = $action->get_settings(); } return $form; } /** * Import a single form using AJAX. * * @since 1.6.6 */ public function import_form() { // phpcs:ignore Generic.Metrics.CyclomaticComplexity.MaxExceeded, Generic.Metrics.NestingLevel.MaxExceeded // Run a security check. check_ajax_referer( 'wpforms-admin', 'nonce' ); // Check for permissions. if ( ! wpforms_current_user_can( 'create_forms' ) ) { wp_send_json_error(); } // Define some basic information. $analyze = isset( $_POST['analyze'] ); $nf_id = ! empty( $_POST['form_id'] ) ? (int) $_POST['form_id'] : 0; $nf_form = $this->get_form( $nf_id ); $nf_form_name = $nf_form['settings']['title']; $nf_recaptcha = false; $nf_recaptcha_type = 'v2'; $fields_pro_plain = [ 'phone', 'date' ]; $fields_pro_omit = [ 'html', 'divider' ]; $fields_unsupported = [ 'spam', 'starrating', 'listmultiselect', 'hidden', 'total', 'shipping', 'quantity', 'product' ]; $upgrade_plain = []; $upgrade_omit = []; $unsupported = []; $form = [ 'id' => '', 'field_id' => '', 'fields' => [], 'settings' => [ 'form_title' => $nf_form_name, 'form_desc' => '', 'submit_text' => esc_html__( 'Submit', 'wpforms-lite' ), 'submit_text_processing' => esc_html__( 'Sending', 'wpforms-lite' ), 'antispam_v3' => '1', 'notification_enable' => '1', 'notifications' => [ 1 => [ 'notification_name' => esc_html__( 'Notification 1', 'wpforms-lite' ), 'email' => '{admin_email}', 'subject' => sprintf( /* translators: %s - form name. */ esc_html__( 'New Entry: %s', 'wpforms-lite' ), $nf_form_name ), 'sender_name' => get_bloginfo( 'name' ), 'sender_address' => '{admin_email}', 'replyto' => '', 'message' => '{all_fields}', ], ], 'confirmations' => [ 1 => [ 'type' => 'message', 'message' => esc_html__( 'Thanks for contacting us! We will be in touch with you shortly.', 'wpforms-lite' ), 'message_scroll' => '1', ], ], 'import_form_id' => $nf_id, ], ]; // If the form does not contain fields, bail. if ( empty( $nf_form['fields'] ) ) { wp_send_json_success( [ 'error' => true, 'name' => sanitize_text_field( $nf_form_name ), 'msg' => esc_html__( 'No form fields found.', 'wpforms-lite' ), ] ); } // Convert fields. foreach ( $nf_form['fields'] as $nf_field ) { // Try to determine field label to use. $label = $this->get_field_label( $nf_field ); // Next, check if the field is unsupported. // If unsupported, make note and then continue to the next field. if ( in_array( $nf_field['type'], $fields_unsupported, true ) ) { $unsupported[] = $label; continue; } // Now check if this installation is Lite. // If it is Lite, and it's a field type not included, make a note then continue to the next // field. if ( in_array( $nf_field['type'], $fields_pro_plain, true ) && ! wpforms()->is_pro() ) { $upgrade_plain[] = $label; } if ( in_array( $nf_field['type'], $fields_pro_omit, true ) && ! wpforms()->is_pro() ) { $upgrade_omit[] = $label; continue; } // Determine the next field ID to assign. if ( empty( $form['fields'] ) ) { $field_id = 1; } else { $field_id = (int) max( array_keys( $form['fields'] ) ) + 1; } switch ( $nf_field['type'] ) { // Single line text, address, city, first name, last name, // zipcode, email, number, textarea fields. case 'textbox': case 'address': case 'city': case 'firstname': case 'lastname': case 'zip': case 'email': case 'number': case 'textarea': $type = 'text'; if ( $nf_field['type'] === 'email' ) { $type = 'email'; } elseif ( $nf_field['type'] === 'number' ) { $type = 'number'; } elseif ( $nf_field['type'] === 'textarea' ) { $type = 'textarea'; } $form['fields'][ $field_id ] = [ 'id' => $field_id, 'type' => $type, 'label' => $label, 'description' => ! empty( $nf_field['desc_text'] ) ? $nf_field['desc_text'] : '', 'size' => 'medium', 'required' => ! empty( $nf_field['required'] ) ? '1' : '', 'placeholder' => ! empty( $nf_field['placeholder'] ) ? $nf_field['placeholder'] : '', 'default_value' => ! empty( $nf_field['default'] ) ? $nf_field['default'] : '', 'nf_key' => $nf_field['key'], ]; break; // Single checkbox field. case 'checkbox': $form['fields'][ $field_id ] = [ 'id' => $field_id, 'type' => 'checkbox', 'label' => esc_html__( 'Single Checkbox Field', 'wpforms-lite' ), 'choices' => [ 1 => [ 'label' => $label, 'value' => '', ], ], 'description' => ! empty( $nf_field['desc_text'] ) ? $nf_field['desc_text'] : '', 'size' => 'medium', 'required' => ! empty( $nf_field['required'] ) ? '1' : '', 'label_hide' => '1', 'nf_key' => $nf_field['key'], ]; break; // Multi-check field, radio, select, state, and country fields. case 'listcheckbox': case 'listradio': case 'listselect': case 'liststate': case 'listcountry': $type = 'select'; if ( $nf_field['type'] === 'listcheckbox' ) { $type = 'checkbox'; } elseif ( $nf_field['type'] === 'listradio' ) { $type = 'radio'; } $choices = []; if ( $nf_field['type'] === 'listcountry' ) { $countries = wpforms_countries(); foreach ( $countries as $key => $country ) { $choices[] = [ 'label' => $country, 'value' => $key, 'default' => isset( $nf_field['default'] ) && $nf_field['default'] === $key ? '1' : '', ]; } } else { foreach ( $nf_field['options'] as $option ) { $choices[] = [ 'label' => $option['label'], 'value' => $option['value'], ]; } } $form['fields'][ $field_id ] = [ 'id' => $field_id, 'type' => $type, 'label' => $label, 'choices' => $choices, 'description' => ! empty( $nf_field['desc_text'] ) ? $nf_field['desc_text'] : '', 'size' => 'medium', 'required' => ! empty( $nf_field['required'] ) ? '1' : '', 'nf_key' => $nf_field['key'], ]; break; // HTML field. case 'html': $form['fields'][ $field_id ] = [ 'id' => $field_id, 'type' => 'html', 'code' => ! empty( $nf_field['default'] ) ? $nf_field['default'] : '', 'label_disable' => '1', 'nf_key' => $nf_field['key'], ]; break; // Divider field. case 'hr': $form['fields'][ $field_id ] = [ 'id' => $field_id, 'type' => 'divider', 'label' => '', 'description' => '', 'label_disable' => '1', 'nf_key' => $nf_field['key'], ]; break; // Phone number field. case 'phone': $type = wpforms()->is_pro() ? 'phone' : 'text'; $form['fields'][ $field_id ] = [ 'id' => $field_id, 'type' => $type, 'label' => $label, 'format' => ! empty( $nf_field['mask'] ) && $nf_field['mask'] === '(999) 999-9999' ? 'us' : 'international', 'description' => ! empty( $nf_field['desc_text'] ) ? $nf_field['desc_text'] : '', 'size' => 'medium', 'required' => ! empty( $nf_field['required'] ) ? '1' : '', 'placeholder' => ! empty( $nf_field['placeholder'] ) ? $nf_field['placeholder'] : '', 'default_value' => ! empty( $nf_field['default'] ) ? $nf_field['default'] : '', 'nf_key' => $nf_field['key'], ]; break; // Date field. case 'date': $type = wpforms()->is_pro() ? 'date-time' : 'text'; $form['fields'][ $field_id ] = [ 'id' => $field_id, 'type' => $type, 'label' => $label, 'description' => ! empty( $nf_field['desc_text'] ) ? $nf_field['desc_text'] : '', 'format' => 'date', 'size' => 'medium', 'required' => ! empty( $nf_field['required'] ) ? '1' : '', 'date_placeholder' => '', 'date_format' => 'm/d/Y', 'date_type' => 'datepicker', 'time_format' => 'g:i A', 'time_interval' => 30, 'nf_key' => $nf_field['key'], ]; break; // ReCAPTCHA field. case 'recaptcha': $nf_recaptcha = true; if ( $nf_field['size'] === 'invisible' ) { $nf_recaptcha_type = 'invisible'; } } } // If we are only analyzing the form, we can stop here and return the // details about this form. if ( $analyze ) { wp_send_json_success( [ 'name' => $nf_form_name, 'upgrade_plain' => $upgrade_plain, 'upgrade_omit' => $upgrade_omit, ] ); } // Settings. // Confirmation message. foreach ( $nf_form['actions'] as $action ) { if ( $action['type'] === 'successmessage' ) { $form['settings']['confirmations'][1]['message'] = $this->get_smarttags( $action['message'], $form['fields'] ); } } // ReCAPTCHA. if ( $nf_recaptcha ) { // If the user has already defined v2 reCAPTCHA keys in the WPForms // settings, use those. $site_key = wpforms_setting( 'recaptcha-site-key', '' ); $secret_key = wpforms_setting( 'recaptcha-secret-key', '' ); // Try to abstract keys from NF. if ( empty( $site_key ) || empty( $secret_key ) ) { $nf_settings = get_option( 'ninja_forms_settings' ); if ( ! empty( $nf_settings['recaptcha_site_key'] ) && ! empty( $nf_settings['recaptcha_secret_key'] ) ) { $wpforms_settings = get_option( 'wpforms_settings', [] ); $wpforms_settings['recaptcha-site-key'] = $nf_settings['recaptcha_site_key']; $wpforms_settings['recaptcha-secret-key'] = $nf_settings['recaptcha_secret_key']; $wpforms_settings['recaptcha-type'] = $nf_recaptcha_type; update_option( 'wpforms_settings', $wpforms_settings ); } } if ( ! empty( $site_key ) && ! empty( $secret_key ) ) { $form['settings']['recaptcha'] = '1'; } } // Setup email notifications. $action_count = 1; $action_defaults = [ 'notification_name' => esc_html__( 'Notification', 'wpforms-lite' ) . " $action_count", 'email' => '{admin_email}', 'subject' => sprintf( /* translators: %s - form name. */ esc_html__( 'New Entry: %s', 'wpforms-lite' ), $nf_form_name ), 'sender_name' => get_bloginfo( 'name' ), 'sender_address' => '{admin_email}', 'replyto' => '', 'message' => '{all_fields}', ]; foreach ( $nf_form['actions'] as $action ) { if ( $action['type'] !== 'email' ) { continue; } $action_defaults['notification_name'] = esc_html__( 'Notification', 'wpforms-lite' ) . " $action_count"; $form['settings']['notifications'][ $action_count ] = $action_defaults; if ( ! empty( $action['label'] ) ) { $form['settings']['notifications'][ $action_count ]['notification_name'] = $action['label']; } if ( ! empty( $action['to'] ) ) { $form['settings']['notifications'][ $action_count ]['email'] = $this->get_smarttags( $action['to'], $form['fields'] ); } if ( ! empty( $action['reply_to'] ) ) { $form['settings']['notifications'][ $action_count ]['replyto'] = $this->get_smarttags( $action['reply_to'], $form['fields'] ); } if ( ! empty( $action['email_subject'] ) ) { $form['settings']['notifications'][ $action_count ]['subject'] = $this->get_smarttags( $action['email_subject'], $form['fields'] ); } if ( ! empty( $action['email_message'] ) ) { $form['settings']['notifications'][ $action_count ]['message'] = $this->get_smarttags( $action['email_message'], $form['fields'] ); } if ( ! empty( $action['from_name'] ) ) { $form['settings']['notifications'][ $action_count ]['sender_name'] = $this->get_smarttags( $action['from_name'], $form['fields'] ); } if ( ! empty( $action['from_address'] ) ) { $form['settings']['notifications'][ $action_count ]['sender_address'] = $this->get_smarttags( $action['from_address'], $form['fields'] ); } ++$action_count; } $this->add_form( $form, $unsupported, $upgrade_plain, $upgrade_omit ); } /** * Get the field label. * * @since 1.6.6 * * @param array $field Field data. * * @return string */ public function get_field_label( $field ) { if ( ! empty( $field['label'] ) ) { $label = sanitize_text_field( $field['label'] ); } else { $label = sprintf( /* translators: %s - field type. */ esc_html__( '%s Field', 'wpforms-lite' ), ucfirst( $field['type'] ) ); } return trim( $label ); } /** * Replace 3rd-party form provider tags/shortcodes with our own Smart Tags. * * @since 1.6.6 * * @param string $text Text to look for Smart Tags in. * @param array $fields List of fields to process Smart Tags in. * * @return string */ public function get_smarttags( $text, $fields ) { preg_match_all( '/\{(.+?)\}/', $text, $tags ); if ( empty( $tags[1] ) ) { return $text; } foreach ( $tags[1] as $tag ) { $tag_formatted = str_replace( 'field:', '', $tag ); foreach ( $fields as $field ) { if ( ! empty( $field['nf_key'] ) && $field['nf_key'] === $tag_formatted ) { $text = str_replace( '{' . $tag . '}', '{field_id="' . $field['id'] . '"}', $text ); } } if ( in_array( $tag, [ 'wp:admin_email', 'system:admin_email' ], true ) ) { $text = str_replace( [ '{wp:admin_email}', '{system:admin_email}' ], '{admin_email}', $text ); } if ( $tag === 'all_fields_table' || $tag === 'fields_table' ) { $text = str_replace( '{' . $tag . '}', '{all_fields}', $text ); } } return $text; } } Admin/Tools/Importers/ContactForm7.php 0000644 00000046150 15174710275 0013716 0 ustar 00 <?php // phpcs:ignore Generic.Commenting.DocComment.MissingShort /** @noinspection PhpUndefinedClassInspection */ namespace WPForms\Admin\Tools\Importers; use WPCF7_ContactForm; use WPCF7_FormTag; /** * Contact Form 7 Importer class. * * @since 1.6.6 */ class ContactForm7 extends Base { /** * Define required properties. * * @since 1.6.6 */ public function init() { $this->name = 'Contact Form 7'; $this->slug = 'contact-form-7'; $this->path = 'contact-form-7/wp-contact-form-7.php'; } /** * Get ALL THE FORMS. * * @since 1.6.6 */ public function get_forms() { $forms_final = []; if ( ! $this->is_active() ) { return $forms_final; } // phpcs:ignore WordPress.WP.Capabilities.Unknown if ( ! current_user_can( 'wpcf7_read_contact_forms' ) ) { return $forms_final; } $forms = WPCF7_ContactForm::find( [ 'posts_per_page' => -1 ] ); if ( empty( $forms ) ) { return $forms_final; } foreach ( $forms as $form ) { if ( ! empty( $form ) && ( $form instanceof WPCF7_ContactForm ) ) { $forms_final[ $form->id() ] = $form->title(); } } return $forms_final; } /** * Get a single form. * * @since 1.6.6 * * @param int $id Form ID. * * @return WPCF7_ContactForm|bool */ public function get_form( $id ) { $form = WPCF7_ContactForm::find( [ 'posts_per_page' => 1, 'p' => $id, ] ); if ( ! empty( $form[0] ) && ( $form[0] instanceof WPCF7_ContactForm ) ) { return $form[0]; } return false; } /** * Import a single form using AJAX. * * @since 1.6.6 */ public function import_form() { // phpcs:ignore Generic.Metrics.CyclomaticComplexity.MaxExceeded, Generic.Metrics.NestingLevel.MaxExceeded // Run a security check. check_ajax_referer( 'wpforms-admin', 'nonce' ); // Check for permissions. if ( ! wpforms_current_user_can( 'create_forms' ) ) { wp_send_json_error(); } // Define some basic information. $analyze = isset( $_POST['analyze'] ); $cf7_id = ! empty( $_POST['form_id'] ) ? (int) $_POST['form_id'] : 0; $cf7_form = $this->get_form( $cf7_id ); if ( ! $cf7_form ) { wp_send_json_error( [ 'error' => true, 'name' => esc_html__( 'Unknown Form', 'wpforms-lite' ), 'msg' => esc_html__( 'The form you are trying to import does not exist.', 'wpforms-lite' ), ] ); } $cf7_form_name = $cf7_form->title(); $cf7_fields = $cf7_form->scan_form_tags(); $cf7_properties = $cf7_form->get_properties(); $cf7_recaptcha = false; $fields_pro_plain = [ 'url', 'tel', 'date' ]; $fields_pro_omit = [ 'file' ]; $fields_unsupported = [ 'quiz', 'hidden' ]; $upgrade_plain = []; $upgrade_omit = []; $unsupported = []; $form = [ 'id' => '', 'field_id' => '', 'fields' => [], 'settings' => [ 'form_title' => $cf7_form_name, 'form_desc' => '', 'submit_text' => esc_html__( 'Submit', 'wpforms-lite' ), 'submit_text_processing' => esc_html__( 'Sending', 'wpforms-lite' ), 'antispam_v3' => '1', 'notification_enable' => '1', 'notifications' => [ 1 => [ 'notification_name' => esc_html__( 'Notification 1', 'wpforms-lite' ), 'email' => '{admin_email}', 'subject' => sprintf( /* translators: %s - form name. */ esc_html__( 'New Entry: %s', 'wpforms-lite' ), $cf7_form_name ), 'sender_name' => get_bloginfo( 'name' ), 'sender_address' => '{admin_email}', 'replyto' => '', 'message' => '{all_fields}', ], ], 'confirmations' => [ 1 => [ 'type' => 'message', 'message' => esc_html__( 'Thanks for contacting us! We will be in touch with you shortly.', 'wpforms-lite' ), 'message_scroll' => '1', ], ], 'import_form_id' => $cf7_id, ], ]; // If the form does not contain fields, bail. if ( empty( $cf7_fields ) ) { wp_send_json_success( [ 'error' => true, 'name' => sanitize_text_field( $cf7_form_name ), 'msg' => esc_html__( 'No form fields found.', 'wpforms-lite' ), ] ); } // Convert fields. foreach ( $cf7_fields as $cf7_field ) { if ( ! $cf7_field instanceof WPCF7_FormTag ) { continue; } // Try to determine field label to use. $label = $this->get_field_label( $cf7_properties['form'], $cf7_field->type, $cf7_field->name ); // Next, check if the field is unsupported. // If supported, make note and then continue to the next field. if ( in_array( $cf7_field->basetype, $fields_unsupported, true ) ) { $unsupported[] = $label; continue; } // Now check if this installation is Lite. // If it is Lite, and it's a field type not included, make a note then continue to the next field. if ( in_array( $cf7_field->basetype, $fields_pro_plain, true ) && ! wpforms()->is_pro() ) { $upgrade_plain[] = $label; } if ( in_array( $cf7_field->basetype, $fields_pro_omit, true ) && ! wpforms()->is_pro() ) { $upgrade_omit[] = $label; continue; } // Determine the next field ID to assign. if ( empty( $form['fields'] ) ) { $field_id = 1; } else { $field_id = (int) max( array_keys( $form['fields'] ) ) + 1; } switch ( $cf7_field->basetype ) { // Plain text, email, URL, number, and textarea fields. case 'text': case 'email': case 'url': case 'number': case 'textarea': $type = $cf7_field->basetype; if ( $type === 'url' && ! wpforms()->is_pro() ) { $type = 'text'; } $form['fields'][ $field_id ] = [ 'id' => $field_id, 'type' => $type, 'label' => $label, 'size' => 'medium', 'required' => $cf7_field->is_required() ? '1' : '', 'placeholder' => $this->get_field_placeholder_default( $cf7_field ), 'default_value' => $this->get_field_placeholder_default( $cf7_field, 'default' ), 'cf7_name' => $cf7_field->name, ]; break; // Phone number field. case 'tel': $form['fields'][ $field_id ] = [ 'id' => $field_id, 'type' => 'phone', 'label' => $label, 'format' => 'international', 'size' => 'medium', 'required' => $cf7_field->is_required() ? '1' : '', 'placeholder' => $this->get_field_placeholder_default( $cf7_field ), 'default_value' => $this->get_field_placeholder_default( $cf7_field, 'default' ), 'cf7_name' => $cf7_field->name, ]; break; // Date field. case 'date': $type = wpforms()->is_pro() ? 'date-time' : 'text'; $form['fields'][ $field_id ] = [ 'id' => $field_id, 'type' => $type, 'label' => $label, 'format' => 'date', 'size' => 'medium', 'required' => $cf7_field->is_required() ? '1' : '', 'date_placeholder' => '', 'date_format' => 'm/d/Y', 'date_type' => 'datepicker', 'time_format' => 'g:i A', 'time_interval' => 30, 'cf7_name' => $cf7_field->name, ]; break; // Select, radio, and checkbox fields. case 'select': case 'radio': case 'checkbox': $choices = []; $options = (array) $cf7_field->labels; foreach ( $options as $option ) { $choices[] = [ 'label' => $option, 'value' => '', ]; } $form['fields'][ $field_id ] = [ 'id' => $field_id, 'type' => $cf7_field->basetype, 'label' => $label, 'choices' => $choices, 'size' => 'medium', 'required' => $cf7_field->is_required() ? '1' : '', 'cf7_name' => $cf7_field->name, ]; if ( $cf7_field->basetype === 'select' && $cf7_field->has_option( 'include_blank' ) ) { $form['fields'][ $field_id ]['placeholder'] = '---'; } break; // File upload field. case 'file': $extensions = ''; $max_size = ''; $file_types = $cf7_field->get_option( 'filetypes' ); $limit = $cf7_field->get_option( 'limit' ); if ( ! empty( $file_types[0] ) ) { $extensions = implode( ',', explode( '|', strtolower( preg_replace( '/[^A-Za-z0-9|]/', '', strtolower( $file_types[0] ) ) ) ) ); } if ( ! empty( $limit[0] ) ) { $limit = $limit[0]; $mb = ( strpos( $limit, 'm' ) !== false ); $kb = ( strpos( $limit, 'kb' ) !== false ); $limit = (int) preg_replace( '/[^0-9]/', '', $limit ); if ( $mb ) { $max_size = $limit; } elseif ( $kb ) { $max_size = round( $limit / 1024, 1 ); } else { $max_size = round( $limit / 1048576, 1 ); } } $form['fields'][ $field_id ] = [ 'id' => $field_id, 'type' => 'file-upload', 'label' => $label, 'size' => 'medium', 'extensions' => $extensions, 'max_size' => $max_size, 'required' => $cf7_field->is_required() ? '1' : '', 'cf7_name' => $cf7_field->name, ]; break; // Acceptance field. case 'acceptance': $form['fields'][ $field_id ] = [ 'id' => $field_id, 'type' => 'checkbox', 'label' => esc_html__( 'Acceptance Field', 'wpforms-lite' ), 'choices' => [ 1 => [ 'label' => $label, 'value' => '', ], ], 'size' => 'medium', 'required' => '1', 'label_hide' => '1', 'cf7_name' => $cf7_field->name, ]; break; // ReCAPTCHA field. case 'recaptcha': $cf7_recaptcha = true; } } // If we are only analyzing the form, we can stop here and return the // details about this form. if ( $analyze ) { wp_send_json_success( [ 'name' => $cf7_form_name, 'upgrade_plain' => $upgrade_plain, 'upgrade_omit' => $upgrade_omit, ] ); } // Settings. // Confirmation message. if ( ! empty( $cf7_properties['messages']['mail_sent_ok'] ) ) { $form['settings']['confirmation_message'] = $cf7_properties['messages']['mail_sent_ok']; } // ReCAPTCHA. if ( $cf7_recaptcha ) { // If the user has already defined v2 reCAPTCHA keys in the WPForms // settings, use those. $site_key = wpforms_setting( 'recaptcha-site-key', '' ); $secret_key = wpforms_setting( 'recaptcha-secret-key', '' ); $type = wpforms_setting( 'recaptcha-type', 'v2' ); // Try to abstract keys from CF7. if ( empty( $site_key ) || empty( $secret_key ) ) { $cf7_settings = get_option( 'wpcf7' ); if ( ! empty( $cf7_settings['recaptcha'] ) && is_array( $cf7_settings['recaptcha'] ) ) { foreach ( $cf7_settings['recaptcha'] as $key => $val ) { if ( ! empty( $key ) && ! empty( $val ) ) { $site_key = $key; $secret_key = $val; } } $wpforms_settings = get_option( 'wpforms_settings', [] ); $wpforms_settings['recaptcha-site-key'] = $site_key; $wpforms_settings['recaptcha-secret-key'] = $secret_key; update_option( 'wpforms_settings', $wpforms_settings ); } } // Don't enable reCAPTCHA if user had configured invisible reCAPTCHA. if ( $type === 'v2' && ! empty( $site_key ) && ! empty( $secret_key ) ) { $form['settings']['recaptcha'] = '1'; } } // Setup email notifications. if ( ! empty( $cf7_properties['mail']['subject'] ) ) { $form['settings']['notifications'][1]['subject'] = $this->get_smarttags( $cf7_properties['mail']['subject'], $form['fields'] ); } if ( ! empty( $cf7_properties['mail']['recipient'] ) ) { $form['settings']['notifications'][1]['email'] = $this->get_smarttags( $cf7_properties['mail']['recipient'], $form['fields'] ); } if ( ! empty( $cf7_properties['mail']['body'] ) ) { $form['settings']['notifications'][1]['message'] = $this->get_smarttags( $cf7_properties['mail']['body'], $form['fields'] ); } if ( ! empty( $cf7_properties['mail']['additional_headers'] ) ) { $form['settings']['notifications'][1]['replyto'] = $this->get_replyto( $cf7_properties['mail']['additional_headers'], $form['fields'] ); } if ( ! empty( $cf7_properties['mail']['sender'] ) ) { $sender = $this->get_sender_details( $cf7_properties['mail']['sender'], $form['fields'] ); if ( $sender ) { $form['settings']['notifications'][1]['sender_name'] = $sender['name']; $form['settings']['notifications'][1]['sender_address'] = $sender['address']; } } if ( ! empty( $cf7_properties['mail_2'] ) && (int) $cf7_properties['mail_2']['active'] === 1 ) { // Check if a secondary notification is enabled, if so set defaults // and set it up. $form['settings']['notifications'][2] = [ 'notification_name' => esc_html__( 'Notification 2', 'wpforms-lite' ), 'email' => '{admin_email}', 'subject' => sprintf( /* translators: %s - form name. */ esc_html__( 'New Entry: %s', 'wpforms-lite' ), $cf7_form_name ), 'sender_name' => get_bloginfo( 'name' ), 'sender_address' => '{admin_email}', 'replyto' => '', 'message' => '{all_fields}', ]; if ( ! empty( $cf7_properties['mail_2']['subject'] ) ) { $form['settings']['notifications'][2]['subject'] = $this->get_smarttags( $cf7_properties['mail_2']['subject'], $form['fields'] ); } if ( ! empty( $cf7_properties['mail_2']['recipient'] ) ) { $form['settings']['notifications'][2]['email'] = $this->get_smarttags( $cf7_properties['mail_2']['recipient'], $form['fields'] ); } if ( ! empty( $cf7_properties['mail_2']['body'] ) ) { $form['settings']['notifications'][2]['message'] = $this->get_smarttags( $cf7_properties['mail_2']['body'], $form['fields'] ); } if ( ! empty( $cf7_properties['mail_2']['additional_headers'] ) ) { $form['settings']['notifications'][2]['replyto'] = $this->get_replyto( $cf7_properties['mail_2']['additional_headers'], $form['fields'] ); } if ( ! empty( $cf7_properties['mail_2']['sender'] ) ) { $sender = $this->get_sender_details( $cf7_properties['mail_2']['sender'], $form['fields'] ); if ( $sender ) { $form['settings']['notifications'][2]['sender_name'] = $sender['name']; $form['settings']['notifications'][2]['sender_address'] = $sender['address']; } } } $this->add_form( $form, $unsupported, $upgrade_plain, $upgrade_omit ); } /** * Lookup and return the placeholder or default value. * * @since 1.6.6 * * @param object $field Field object. * @param string $type Type of the field. * * @return string */ public function get_field_placeholder_default( $field, $type = 'placeholder' ) { $placeholder = ''; $default_value = (string) reset( $field->values ); if ( $field->has_option( 'placeholder' ) || $field->has_option( 'watermark' ) ) { $placeholder = $default_value; $default_value = ''; } if ( $type === 'placeholder' ) { return $placeholder; } return $default_value; } /** * Get the field label. * * @since 1.6.6 * * @param string $form Form data and settings. * @param string $type Field type. * @param string $name Field name. * * @return string */ public function get_field_label( $form, $type, $name = '' ) { preg_match_all( '/<label>([ \w\S\r\n\t]+?)<\/label>/', $form, $matches ); foreach ( $matches[1] as $match ) { $match = trim( str_replace( "\n", '', $match ) ); preg_match( '/\[(?:' . preg_quote( $type, '/' ) . ') ' . $name . '(?:[ ](.*?))?(?:[\r\n\t ](\/))?\]/', $match, $input_match ); if ( ! empty( $input_match[0] ) ) { return strip_shortcodes( sanitize_text_field( str_replace( $input_match[0], '', $match ) ) ); } } $label = sprintf( /* translators: %1$s - field type, %2$s - field name if available. */ esc_html__( '%1$s Field %2$s', 'wpforms-lite' ), ucfirst( $type ), ! empty( $name ) ? "($name)" : '' ); return trim( $label ); } /** * Replace 3rd-party form provider tags/shortcodes with our own Smart Tags. * * @since 1.6.6 * * @param string $text Text to look for Smart Tags in. * @param array $fields List of fields to process Smart Tags in. * * @return string */ public function get_smarttags( $text, $fields ) { preg_match_all( '/\[(.+?)\]/', $text, $tags ); if ( empty( $tags[1] ) ) { return $text; } // Process form-tags and mail-tags. foreach ( $tags[1] as $tag ) { foreach ( $fields as $field ) { if ( ! empty( $field['cf7_name'] ) && $field['cf7_name'] === $tag ) { $text = str_replace( '[' . $tag . ']', '{field_id="' . $field['id'] . '"}', $text ); } } } /* * Process CF7 tags that we can map with WPForms alternatives. * Replace those CF7 that are used in Notifications by default and that we can't leave empty. * We are not replacing certain special CF7 tags: [_user_url], [_post_name], [_time], [_user_agent]. * Without them some logic may be broken, and for user it will be harder to stop missing strings. * With them - they can see strange text and will be able to understand, based on the tag name, * which value is expected there. */ return str_replace( [ '[_site_title]', '[_site_description]', '[_site_url]', '[_user_display_name]', '[_user_nickname]', '[_user_last_name]', '[_user_first_name]', '[_user_email]', '[_user_login]', '[_site_admin_email]', '[_post_author_email]', '[_post_author]', '[_url]', '[_post_url]', '[_post_title]', '[_post_id]', '[_serial_number]', '[_date]', '[_remote_ip]', ], [ get_bloginfo( 'name' ), get_bloginfo( 'description' ), get_bloginfo( 'url' ), '{user_full_name}', '{user_display}', '{user_last_name}', '{user_first_name}', '{user_email}', '{user_display}', '{admin_email}', '{author_email}', '{author_display}', '{page_url}', '{page_url}', '{page_title}', '{page_id}', '{entry_id}', '{date format="m/d/Y"}', '{user_ip}', ], $text ); } /** * Find Reply-To in headers if provided. * * @since 1.6.6 * * @param string $headers CF7 email headers. * @param array $fields List of fields. * * @return string */ public function get_replyto( $headers, $fields ) { // phpcs:ignore Generic.Metrics.NestingLevel.MaxExceeded if ( strpos( $headers, 'Reply-To:' ) !== false ) { preg_match( '/Reply-To: \[(.+?)\]/', $headers, $tag ); if ( ! empty( $tag[1] ) ) { foreach ( $fields as $field ) { if ( ! empty( $field['cf7_name'] ) && $field['cf7_name'] === $tag[1] ) { return '{field_id="' . $field['id'] . '"}'; } } } } return ''; } /** * Sender information. * * @since 1.6.6 * * @param string $sender Sender strings in "Name <email@example.com>" format. * @param array $fields List of fields. * * @return bool|array */ public function get_sender_details( $sender, $fields ) { preg_match( '/(.+?)\<(.+?)\>/', $sender, $tag ); if ( ! empty( $tag[1] ) && ! empty( $tag[2] ) ) { return [ 'name' => $this->get_smarttags( $tag[1], $fields ), 'address' => $this->get_smarttags( $tag[2], $fields ), ]; } return false; } } Admin/Tools/Importers/PirateForms.php 0000644 00000044622 15174710275 0013645 0 ustar 00 <?php // phpcs:ignore Generic.Commenting.DocComment.MissingShort /** @noinspection PhpUndefinedClassInspection */ namespace WPForms\Admin\Tools\Importers; use PirateForms_Util; use WP_Ajax_Upgrader_Skin; use WP_Query; use WPForms\Helpers\PluginSilentUpgrader; /** * Pirate Forms Importer class. * * @since 1.6.6 */ class PirateForms extends Base { /** * Direct URL to download the latest version of WP Mail SMTP plugin from WP.org repo. * * @since 1.6.6 * * @var string */ private const URL_SMTP_ZIP = 'https://downloads.wordpress.org/plugin/wp-mail-smtp.zip'; /** * WP Mail SMTP plugin basename. * * @since 1.6.6 * * @var string */ private const SLUG_SMTP_PLUGIN = 'wp-mail-smtp/wp_mail_smtp.php'; /** * Default PirateForms smart tags. * * @since 1.6.6 * * @var array */ public static $tags = [ '[email]', ]; /** * Define required properties. * * @since 1.6.6 */ public function init() { $this->name = 'Pirate Forms'; $this->slug = 'pirate-forms'; $this->path = 'pirate-forms/pirate-forms.php'; } /** * Get ALL THE FORMS. * We need only IDs and names here. * * @since 1.6.6 * * @return array */ public function get_forms() { if ( ! current_user_can( 'edit_published_posts' ) ) { return []; } // Union those arrays, as array_merge() does keys reindexing. $forms = $this->get_default_forms() + $this->get_pro_forms(); // Sort by IDs ASC. ksort( $forms ); return $forms; } /** * Pirate Forms has a default form, which doesn't have an ID. * * @since 1.6.6 * * @return array */ protected function get_default_forms() { $form = PirateForms_Util::get_form_options(); // Make sure that it's there and not broken. if ( empty( $form ) ) { return []; } return [ 0 => esc_html__( 'Default Form', 'wpforms-lite' ) ]; } /** * Copy-paste from Pro plugin code, it doesn't have API to get this data easily. * * @since 1.6.6 * * @return array */ protected function get_pro_forms() { $forms = []; $query = new WP_Query( [ 'post_type' => 'pf_form', 'post_status' => 'publish', 'posts_per_page' => - 1, 'update_post_meta_cache' => false, 'update_post_term_cache' => false, ] ); if ( $query->have_posts() ) { while ( $query->have_posts() ) { $query->the_post(); $forms[ get_the_ID() ] = get_the_title(); } } return $forms; } /** * Get a single form options. * * @since 1.6.6 * * @param int $id Form ID. * * @return array */ public function get_form( $id ) { return PirateForms_Util::get_form_options( (int) $id ); } /** * Import a single form using AJAX. * * @since 1.6.6 */ public function import_form() { // phpcs:ignore Generic.Metrics.CyclomaticComplexity.MaxExceeded, Generic.Metrics.NestingLevel.MaxExceeded // Run a security check. check_ajax_referer( 'wpforms-admin', 'nonce' ); // Check for permissions. if ( ! wpforms_current_user_can( 'create_forms' ) ) { wp_send_json_error(); } $analyze = isset( $_POST['analyze'] ); $pf_form_id = isset( $_POST['form_id'] ) ? (int) $_POST['form_id'] : 0; $pf_form = $this->get_form( $pf_form_id ); $pf_fields_custom = PirateForms_Util::get_post_meta( $pf_form_id, 'custom' ); $pf_fields_default = [ 'name', 'email', 'subject', 'message', 'attachment', 'checkbox', 'recaptcha', ]; $fields_pro_plain = [ 'tel' ]; // Convert them in Lite to the closest Standard alternatives. $fields_pro_omit = [ 'label', 'file', 'attachment' ]; // Strict PRO fields with no Lite alternatives. $upgrade_plain = []; $upgrade_omit = []; $unsupported = []; $fields = []; if ( ! empty( $pf_fields_custom[0] ) ) { $pf_fields_custom = $pf_fields_custom[0]; } else { $pf_fields_custom = []; } if ( empty( $pf_form_id ) ) { $pf_form_name = esc_html__( 'Default Form', 'wpforms-lite' ); } else { $pf_form_name = get_post_field( 'post_title', $pf_form_id ); } // phpcs:ignore WPForms.Comments.PHPDocHooks.RequiredHookDocumentation, WPForms.PHP.ValidateHooks.InvalidHookName $pf_form_name = wpforms_decode_string( apply_filters( 'the_title', $pf_form_name, $pf_form_id ) ); // Prepare all DEFAULT fields. foreach ( $pf_fields_default as $field ) { // Ignore fields that are not displayed or not added at all. if ( empty( $pf_form[ 'pirateformsopt_' . $field . '_field' ] ) ) { continue; } // Ignore certain fields as they are dealt with later. if ( $field === 'recaptcha' ) { continue; } $required = $pf_form[ 'pirateformsopt_' . $field . '_field' ] === 'req' ? '1' : ''; $label = ! empty( $pf_form[ 'pirateformsopt_label_' . $field ] ) ? $pf_form[ 'pirateformsopt_label_' . $field ] : ucwords( $field ); // If it is Lite, and it's a field type not included, make a note then continue to the next field. if ( in_array( $field, $fields_pro_plain, true ) && ! wpforms()->is_pro() ) { $upgrade_plain[] = $label; } if ( in_array( $field, $fields_pro_omit, true ) && ! wpforms()->is_pro() ) { $upgrade_omit[] = $label; continue; } // Determine the next field ID to assign. if ( empty( $fields ) ) { $field_id = 1; } else { $field_id = (int) max( array_keys( $fields ) ) + 1; } // Separately process certain fields. switch ( $field ) { case 'name': case 'email': case 'subject': case 'message': $type = $field; if ( $field === 'subject' ) { $type = 'text'; } elseif ( $field === 'message' ) { $type = 'textarea'; } $fields[ $field_id ] = [ 'id' => $field_id, 'type' => $type, 'label' => $label, 'required' => $required, 'size' => 'medium', ]; if ( $field === 'name' ) { $fields[ $field_id ]['format'] = 'simple'; } break; case 'checkbox': $fields[ $field_id ] = [ 'id' => $field_id, 'type' => 'checkbox', 'label' => esc_html__( 'Single Checkbox Field', 'wpforms-lite' ), 'choices' => [ 1 => [ 'label' => $label, 'value' => '', ], ], 'size' => 'medium', 'required' => $required, 'label_hide' => true, ]; break; case 'attachment': case 'file': $fields[ $field_id ] = [ 'id' => $field_id, 'type' => 'file-upload', 'label' => $label, 'required' => $required, 'label_hide' => true, ]; // If PF attachments were saved into FS, we need to save them in WP Media. // That will allow admins to easily delete it if needed. if ( ! empty( $pf_form['pirateformsopt_save_attachment'] ) && $pf_form['pirateformsopt_save_attachment'] === 'yes' ) { $fields[ $field_id ]['media_library'] = true; } break; } } // Prepare all CUSTOM fields. foreach ( $pf_fields_custom as $field ) { // Ignore fields that are not displayed. if ( empty( $field['display'] ) ) { continue; } $required = $field['display'] === 'req' ? '1' : ''; // Possible values in PF: 'yes', 'req'. $label = sanitize_text_field( $field['label'] ); // If it is Lite, and it's a field type not included, make a note then continue to the next field. if ( in_array( $field['type'], $fields_pro_plain, true ) && ! wpforms()->is_pro() ) { $upgrade_plain[] = $label; } if ( in_array( $field['type'], $fields_pro_omit, true ) && ! wpforms()->is_pro() ) { $upgrade_omit[] = $label; continue; } // Determine the next field ID to assign. if ( empty( $fields ) ) { $field_id = 1; } else { $field_id = (int) max( array_keys( $fields ) ) + 1; } switch ( $field['type'] ) { case 'text': case 'textarea': case 'number': case 'tel': $type = $field['type']; if ( $field['type'] === 'textarea' ) { $type = 'textarea'; } if ( $field['type'] === 'tel' ) { $type = 'phone'; } $fields[ $field_id ] = [ 'id' => $field_id, 'type' => $type, 'label' => $label, 'required' => $required, 'size' => 'medium', ]; if ( $field['type'] === 'tel' ) { $fields[ $field_id ]['format'] = 'international'; } break; case 'checkbox': $fields[ $field_id ] = [ 'id' => $field_id, 'type' => 'checkbox', 'label' => esc_html__( 'Single Checkbox Field', 'wpforms-lite' ), 'choices' => [ 1 => [ 'label' => $label, 'value' => '', ], ], 'size' => 'medium', 'required' => $required, 'label_hide' => true, ]; break; case 'select': case 'multiselect': $options = []; $i = 1; $type = 'select'; if ( $field['type'] === 'multiselect' ) { $type = 'checkbox'; } foreach ( explode( PHP_EOL, $field['options'] ) as $option ) { $options[ $i ] = [ 'label' => $option, 'value' => '', 'image' => '', ]; ++$i; } $fields[ $field_id ] = [ 'id' => $field_id, 'type' => $type, 'label' => $label, 'required' => $required, 'size' => 'medium', 'choices' => $options, ]; break; case 'label': $fields[ $field_id ] = [ 'id' => $field_id, 'type' => 'html', 'code' => $field['label'], 'label_disable' => true, ]; break; case 'file': $fields[ $field_id ] = [ 'id' => $field_id, 'type' => 'file-upload', 'label' => $label, 'required' => $required, 'label_hide' => true, ]; // If PF attachments were saved into FS, we need to save them in WP Media. // That will allow admins to easily delete it if needed. if ( ! empty( $pf_form['pirateformsopt_save_attachment'] ) && $pf_form['pirateformsopt_save_attachment'] === 'yes' ) { $fields[ $field_id ]['media_library'] = true; } break; } } // If we are analyzing the form (in Lite only), // we can stop here and return the details about this form. if ( $analyze ) { wp_send_json_success( [ 'name' => $pf_form_name, 'upgrade_plain' => $upgrade_plain, 'upgrade_omit' => $upgrade_omit, ] ); } // Make sure we have imported some fields. if ( empty( $fields ) ) { wp_send_json_success( [ 'error' => true, 'name' => $pf_form_name, 'msg' => esc_html__( 'No form fields found.', 'wpforms-lite' ), ] ); } // Create a form array, that holds all the data. $form = [ 'id' => '', 'field_id' => '', 'fields' => $fields, 'settings' => [ 'form_title' => $pf_form_name, 'form_desc' => '', 'submit_text' => stripslashes( $pf_form['pirateformsopt_label_submit_btn'] ), 'submit_text_processing' => esc_html__( 'Sending', 'wpforms-lite' ), 'notification_enable' => '1', 'notifications' => [ 1 => [ 'notification_name' => esc_html__( 'Default Notification', 'wpforms-lite' ), 'email' => $pf_form['pirateformsopt_email_recipients'], 'subject' => sprintf( /* translators: %s - form name. */ esc_html__( 'New Entry: %s', 'wpforms-lite' ), $pf_form_name ), 'sender_name' => get_bloginfo( 'name' ), 'sender_address' => $this->get_smarttags( $pf_form['pirateformsopt_email'], $fields ), 'replyto' => '', 'message' => '{all_fields}', ], ], 'confirmations' => [ 1 => [ 'type' => empty( $pf_form['pirateformsopt_thank_you_url'] ) ? 'message' : 'page', 'page' => (int) $pf_form['pirateformsopt_thank_you_url'], 'message' => ! empty( $pf_form['pirateformsopt_label_submit'] ) ? $pf_form['pirateformsopt_label_submit'] : esc_html__( 'Thanks for contacting us! We will be in touch with you shortly.', 'wpforms-lite' ), 'message_scroll' => '1', ], ], 'disable_entries' => $pf_form['pirateformsopt_store'] === 'yes' ? '0' : '1', 'import_form_id' => $pf_form_id, ], ]; // Do not save user IP address and UA. if ( empty( $pf_form['pirateformsopt_store_ip'] ) || $pf_form['pirateformsopt_store_ip'] !== 'yes' ) { $wpforms_settings = get_option( 'wpforms_settings', [] ); $wpforms_settings['gdpr'] = true; update_option( 'wpforms_settings', $wpforms_settings ); $form['settings']['disable_ip'] = true; } // Save recaptcha keys. if ( ! empty( $pf_form['pirateformsopt_recaptcha_field'] ) && $pf_form['pirateformsopt_recaptcha_field'] === 'yes' ) { // If the user has already defined v2 reCAPTCHA keys, use those. $site_key = wpforms_setting( 'recaptcha-site-key', '' ); $secret_key = wpforms_setting( 'recaptcha-secret-key', '' ); // Try to abstract keys from PF. if ( empty( $site_key ) || empty( $secret_key ) ) { if ( ! empty( $pf_form['pirateformsopt_recaptcha_sitekey'] ) && ! empty( $pf_form['pirateformsopt_recaptcha_secretkey'] ) ) { $wpforms_settings = get_option( 'wpforms_settings', [] ); $wpforms_settings['recaptcha-site-key'] = $pf_form['pirateformsopt_recaptcha_sitekey']; $wpforms_settings['recaptcha-secret-key'] = $pf_form['pirateformsopt_recaptcha_secretkey']; $wpforms_settings['recaptcha-type'] = 'v2'; update_option( 'wpforms_settings', $wpforms_settings ); } } if ( ( ! empty( $site_key ) && ! empty( $secret_key ) ) || ( ! empty( $wpforms_settings['recaptcha-site-key'] ) && ! empty( $wpforms_settings['recaptcha-secret-key'] ) ) ) { $form['settings']['recaptcha'] = '1'; } } $this->import_smtp( $pf_form_id, $form ); $this->add_form( $form, $unsupported, $upgrade_plain, $upgrade_omit ); } /** * Replace 3rd-party form provider tags/shortcodes with our own Smart Tags. * See: PirateForms_Util::get_magic_tags() for all PF tags. * * @since 1.6.6 * * @param string $text String to process the smart tag in. * @param array $fields List of fields for the form. * * @return string */ public function get_smarttags( $text, $fields ) { // phpcs:ignore Generic.Metrics.NestingLevel.MaxExceeded foreach ( self::$tags as $tag ) { $wpf_tag = ''; if ( $tag === '[email]' ) { foreach ( $fields as $field ) { if ( $field['type'] === 'email' ) { $wpf_tag = '{field_id="' . $field['id'] . '"}'; break; } } } $text = str_replace( $tag, $wpf_tag, $text ); } return $text; } /** * Import SMTP settings from Default form only. * * @since 1.6.6 * * @param int $pf_form_id PirateForms form ID. * @param array $form WPForms form array. */ protected function import_smtp( $pf_form_id, $form ) { // At this point we import only default form SMTP settings. if ( $pf_form_id !== 0 ) { return; } $pf_form = $this->get_form( 0 ); // Use only if enabled. if ( empty( $pf_form['pirateformsopt_use_smtp'] ) || $pf_form['pirateformsopt_use_smtp'] !== 'yes' ) { return; } // If a user has WP Mail SMTP already activated - do nothing as it's most likely already configured. if ( is_plugin_active( self::SLUG_SMTP_PLUGIN ) ) { return; } // Check that we successfully installed and activated the plugin. if ( ! $this->install_activate_smtp() ) { return; } /* * Finally, start the settings importing. */ // WP Mail SMTP 1.x and PHP 5.3+ are allowed. Older WPMS versions are ignored. if ( ! function_exists( 'wp_mail_smtp' ) ) { return; } // TODO: change to \WPMailSMTP\Options in future. $options = get_option( 'wp_mail_smtp', [] ); $options['mail']['from_email'] = $this->get_smarttags( $pf_form['pirateformsopt_email'], $form['fields'] ); $options['mail']['mailer'] = 'smtp'; $options['smtp']['host'] = $pf_form['pirateformsopt_smtp_host']; $options['smtp']['port'] = $pf_form['pirateformsopt_smtp_port']; $options['smtp']['encryption'] = empty( $pf_form['pirateformsopt_use_secure'] ) ? 'none' : $pf_form['pirateformsopt_use_secure']; $options['smtp']['auth'] = ! empty( $pf_form['pirateformsopt_use_smtp_authentication'] ) && $pf_form['pirateformsopt_use_smtp_authentication'] === 'yes'; $options['smtp']['user'] = $pf_form['pirateformsopt_smtp_username']; $options['smtp']['pass'] = $pf_form['pirateformsopt_smtp_password']; update_option( 'wp_mail_smtp', $options ); } /** * Do all the voodoo to install and activate the WP Mail SMTP plugin behind the scene. * No user interaction is needed. * * @since 1.6.6 * * @return bool */ protected function install_activate_smtp() { // phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh, WPForms.PHP.HooksMethod.InvalidPlaceForAddingHooks /* * Check installation. * If installed but not activated - bail. * We don't want to break current site email deliverability. */ if ( ! function_exists( 'get_plugins' ) ) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } // FALSE will bail the import. if ( array_key_exists( self::SLUG_SMTP_PLUGIN, get_plugins() ) ) { return false; } /* * Let's try to install. */ $url = add_query_arg( [ 'provider' => $this->slug, 'page' => 'wpforms-tools', 'view' => 'importer', ], admin_url( 'admin.php' ) ); $creds = request_filesystem_credentials( esc_url_raw( $url ), '', false, false ); // Check for file system permissions. if ( $creds === false ) { return false; } if ( ! WP_Filesystem( $creds ) ) { return false; } // Do not allow WordPress to search/download translations, as this will break JS output. remove_action( 'upgrader_process_complete', [ 'Language_Pack_Upgrader', 'async_upgrade' ], 20 ); // Create the plugin upgrader with our custom skin. $installer = new PluginSilentUpgrader( new WP_Ajax_Upgrader_Skin() ); // Error check. if ( ! method_exists( $installer, 'install' ) ) { return false; } $installer->install( self::URL_SMTP_ZIP ); // Flush the cache and return the newly installed plugin basename. wp_cache_flush(); if ( $installer->plugin_info() ) { $plugin_basename = $installer->plugin_info(); // Activate, do not redirect, run the plugin activation routine. $activated = activate_plugin( $plugin_basename ); if ( ! is_wp_error( $activated ) ) { return true; } } return false; } } Admin/Tools/Importers/ImporterInterface.php 0000644 00000001603 15174710275 0015024 0 ustar 00 <?php namespace WPForms\Admin\Tools\Importers; /** * Interface WPForms_Importer_Interface to handle common methods for all importers. * * @since 1.6.6 */ interface ImporterInterface { /** * Define required properties. * * @since 1.6.6 */ public function init(); /** * Get ALL THE FORMS. * * @since 1.6.6 */ public function get_forms(); /** * Get a single form. * * @since 1.6.6 * * @param int $id Form ID. */ public function get_form( $id ); /** * Import a single form using AJAX. * * @since 1.6.6 */ public function import_form(); /** * Replace 3rd-party form provider tags/shortcodes with our own Smart Tags. * * @since 1.6.6 * * @param string $text Text to look for Smart Tags in. * @param array $fields List of fields to process Smart Tags in. * * @return string */ public function get_smarttags( $text, $fields ); } Admin/Tools/Importers/Base.php 0000644 00000006724 15174710275 0012265 0 ustar 00 <?php namespace WPForms\Admin\Tools\Importers; /** * Base Importer class. * * @since 1.6.6 */ abstract class Base implements ImporterInterface { /** * Importer name. * * @since 1.6.6 * * @var string */ public $name; /** * Importer name in slug format. * * @since 1.6.6 * * @var string */ public $slug; /** * Importer plugin path. * * @since 1.6.6 * * @var string */ public $path; /** * Primary class constructor. * * @since 1.6.6 */ public function __construct() { $this->init(); // Import a specific form with AJAX. add_action( "wp_ajax_wpforms_import_form_{$this->slug}", [ $this, 'import_form' ] ); } /** * Add to list of registered importers. * * @since 1.6.6 * * @param array $importers List of supported importers. * * @return array */ public function register( $importers = [] ) { $importers[ $this->slug ] = [ 'name' => $this->name, 'slug' => $this->slug, 'path' => $this->path, 'installed' => file_exists( trailingslashit( WP_PLUGIN_DIR ) . $this->path ), 'active' => $this->is_active(), ]; return $importers; } /** * If the importer source is available. * * @since 1.6.6 * * @return bool */ protected function is_active() { return is_plugin_active( $this->path ); } /** * Add the new form to the database and return AJAX data. * * @since 1.6.6 * * @param array $form Form to import. * @param array $unsupported List of unsupported fields. * @param array $upgrade_plain List of fields, that are supported inside the paid WPForms, but not in Lite. * @param array $upgrade_omit No field alternative in WPForms. */ public function add_form( $form, $unsupported = [], $upgrade_plain = [], $upgrade_omit = [] ) { // Create empty form so we have an ID to work with. $form_id = wp_insert_post( [ 'post_status' => 'publish', 'post_type' => 'wpforms', ] ); if ( empty( $form_id ) || is_wp_error( $form_id ) ) { wp_send_json_success( [ 'error' => true, 'name' => sanitize_text_field( $form['settings']['form_title'] ), 'msg' => esc_html__( 'There was an error while creating a new form.', 'wpforms-lite' ), ] ); } $form['id'] = $form_id; $form['field_id'] = count( $form['fields'] ) + 1; // Update the form with all our compiled data. wpforms()->obj( 'form' )->update( $form_id, $form ); // Make note that this form has been imported. $this->track_import( $form['settings']['import_form_id'], $form_id ); // Build and send final AJAX response! wp_send_json_success( [ 'name' => $form['settings']['form_title'], 'edit' => esc_url_raw( admin_url( 'admin.php?page=wpforms-builder&view=fields&form_id=' . $form_id ) ), 'preview' => wpforms_get_form_preview_url( $form_id ), 'unsupported' => $unsupported, 'upgrade_plain' => $upgrade_plain, 'upgrade_omit' => $upgrade_omit, ] ); } /** * After a form has been successfully imported we track it, so that in the * future we can alert users if they try to import a form that has already * been imported. * * @since 1.6.6 * * @param int $source_id Imported plugin form ID. * @param int $wpforms_id WPForms form ID. */ public function track_import( $source_id, $wpforms_id ) { $imported = get_option( 'wpforms_imported', [] ); $imported[ $this->slug ][ $wpforms_id ] = $source_id; update_option( 'wpforms_imported', $imported, false ); } } Admin/Base/Tables/DataObjects/ColumnBase.php 0000644 00000004157 15174710275 0014624 0 ustar 00 <?php namespace WPForms\Admin\Base\Tables\DataObjects; /** * Column data object base class. * * @since 1.8.6 */ abstract class ColumnBase { /** * Column ID. * * @since 1.8.6 * * @var string|int */ protected $id; /** * Column label. * * @since 1.8.6 * * @var string */ protected $label; /** * Label HTML markup. * * @since 1.8.6 * * @var string */ protected $label_html; /** * Is column draggable. * * @since 1.8.6 * * @var bool */ protected $is_draggable; /** * Column type. * * @since 1.8.6 * * @var string */ protected $type; /** * Is column readonly. * * @since 1.8.6 * * @var bool */ protected $readonly; /** * Column constructor. * * @since 1.8.6 * * @param int|string $id Column ID. * @param array $settings Column settings. */ public function __construct( $id, array $settings ) { $this->id = $id; $this->label = $settings['label'] ?? ''; $this->label_html = empty( $settings['label_html'] ) ? $this->label : $settings['label_html']; $this->is_draggable = $settings['draggable'] ?? true; $this->type = empty( $settings['type'] ) ? $id : $settings['type']; $this->readonly = $settings['readonly'] ?? false; } /** * Get column ID. * * @since 1.8.6 * * @return string|int */ public function get_id() { return $this->id; } /** * Get column label. * * @since 1.8.6 * * @return string */ public function get_label(): string { return $this->label; } /** * Get column label HTML. * * @since 1.8.6 * * @return string */ public function get_label_html(): string { return $this->label_html; } /** * Get the column type. * * @since 1.8.6 * * @return string */ public function get_type(): string { return $this->type; } /** * Is column draggable. * * @since 1.8.6 * * @return bool */ public function is_draggable(): bool { return $this->is_draggable; } /** * Is column readonly. * * @since 1.8.6 * * @return bool */ public function is_readonly(): bool { return $this->readonly; } } Admin/Base/Tables/Facades/ColumnsBase.php 0000644 00000003073 15174710275 0014146 0 ustar 00 <?php namespace WPForms\Admin\Base\Tables\Facades; /** * Column facade class. * * Hides the complexity of columns' collection behind a simple interface. * * @since 1.8.6 */ abstract class ColumnsBase { /** * Get columns. * * Returns all possible columns. * * @since 1.8.6 * * @return array Array of columns as objects. */ protected static function get_all(): array { return []; } /** * Get columns' keys for the columns which user selected to be displayed. * * It returns an array of keys in the order they should be displayed. * It returns draggable and non-draggable columns. * * @since 1.8.6 * * @return array */ public static function get_selected_columns_keys(): array { return []; } /** * Check if the form has selected columns. * * @since 1.8.6 * * @return bool */ public static function has_selected_columns(): bool { return ! empty( static::get_selected_columns_keys() ); } /** * Get columns' keys for the columns which the user has not selected to be displayed. * * It returns draggable and non-draggable columns. * * @since 1.8.6 * * @return array */ public static function get_not_selected_columns_keys(): array { $selected = static::get_selected_columns_keys(); $all = array_keys( static::get_all() ); return array_diff( $all, $selected ); } /** * Validate column key. * * @since 1.8.6 * * @param string|int $key Column key. * * @return bool */ public static function validate_column_key( $key ): bool { return isset( static::get_all()[ $key ] ); } } Admin/Loader.php 0000644 00000003234 15174710275 0007526 0 ustar 00 <?php namespace WPForms\Admin; /** * Class Loader gives ability to track/load all admin modules. * * @since 1.5.0 */ class Loader { /** * Get the instance of a class and store it in itself. * * @since 1.5.0 */ public static function get_instance() { static $instance; if ( ! $instance ) { $instance = new self(); } return $instance; } /** * Loader constructor. * * @since 1.5.0 */ public function __construct() { $core_class_names = [ 'Connect', 'FlyoutMenu', 'Builder\LicenseAlert', 'Builder\Builder', 'Pages\Community', 'Pages\SMTP', 'Pages\Analytics', 'Entries\PrintPreview', ]; $class_names = \apply_filters( 'wpforms_admin_classes_available', $core_class_names ); foreach ( $class_names as $class_name ) { $this->register_class( $class_name ); } } /** * Register a new class. * * @since 1.5.0 * * @param string $class_name Class name to register. */ public function register_class( $class_name ) { $class_name = sanitize_text_field( $class_name ); // Load Lite class if exists. if ( class_exists( 'WPForms\Lite\Admin\\' . $class_name ) && ! wpforms()->is_pro() ) { $class_name = 'WPForms\Lite\Admin\\' . $class_name; new $class_name(); return; } // Load Pro class if exists. if ( class_exists( 'WPForms\Pro\Admin\\' . $class_name ) && wpforms()->is_pro() ) { $class_name = 'WPForms\Pro\Admin\\' . $class_name; new $class_name(); return; } // Load general class if neither Pro nor Lite class exists. if ( class_exists( __NAMESPACE__ . '\\' . $class_name ) ) { $class_name = __NAMESPACE__ . '\\' . $class_name; new $class_name(); } } } Admin/Education/Pointers/Payment.php 0000644 00000007204 15174710275 0013454 0 ustar 00 <?php namespace WPForms\Admin\Education\Pointers; use WPForms\Integrations\Stripe; use WPForms\Admin\Payments\Views\Overview\Page as PaymentsPage; use WPForms\Migrations\Base as MigrationsBase; /** * Education class for handling Payments education pointer functionality. * * This class extends the abstract Pointers class and provides functionality * specific to the Payments feature in WPForms. * * @since 1.8.8 */ class Payment extends Pointer { /** * Unique ID for the pointer. * * @since 1.8.8 * * @var string */ protected $pointer_id = 'admin_menu_payments'; /** * Selector for the pointer. * * @since 1.8.8 * * @var string */ protected $selector = '[href$="-payments"]'; /** * Make sure that the pointer is visible across other dashboard pages. * * @since 1.8.8 * * @var bool */ protected $top_level_visible = true; /** * Determine if the Payments feature pointer is allowed to load. * * Checks various conditions to determine if the Payments feature pointer * should be allowed to load for the current user. * * @since 1.8.8 * * @return bool */ protected function allow_load(): bool { // Bail early if the user doesn't have a Lite, Basic, or Plus license. if ( ! in_array( $this->get_license_type(), [ 'lite', 'basic', 'plus' ], true ) ) { return false; } // Bail early if it has been less than 90 days since activation or the installation wasn't upgraded. if ( ! get_option( MigrationsBase::PREVIOUS_CORE_VERSION_OPTION_NAME ) || wpforms_get_activated_timestamp() > ( time() - 90 * DAY_IN_SECONDS ) ) { return false; } // Bail early if a Stripe account is connected. if ( Stripe\Helpers::has_stripe_keys() ) { return false; } // Bail early if the user doesn't have the capability to manage options. if ( ! wpforms_current_user_can() ) { return false; } // Bail early if there are no published forms. $forms_obj = wpforms()->obj( 'form' ); return $forms_obj && $forms_obj->forms_exist(); } /** * Enqueue assets for the pointer. * * @since 1.8.8 */ public function enqueue_assets() { // Enqueue the pointer static assets. parent::enqueue_assets(); $min = wpforms_get_min_suffix(); wp_enqueue_script( 'wpforms-education-pointers-payment', WPFORMS_PLUGIN_URL . "assets/js/admin/education/pointers/payment{$min}.js", [ 'wp-pointer' ], WPFORMS_VERSION, true ); $admin_l10n = [ 'pointer' => sanitize_key( $this->pointer_id ), 'nonce' => sanitize_text_field( $this->get_nonce_token() ), ]; wp_localize_script( 'wpforms-education-pointers-payment', 'wpforms_education_pointers_payment', $admin_l10n ); } /** * Set arguments for the Payments feature pointer. * * @since 1.8.8 * * @noinspection HtmlUnknownTarget */ protected function set_args() { $this->args['title'] = __( 'Payment and Donation Forms are here!', 'wpforms-lite' ); $this->args['message'] = sprintf( /* translators: %1$s - Payments page URL. */ __( 'Now available for you: create forms that accept credit cards, Apple Pay, and Google Pay payments. Visit our new <a href="%1$s" id="wpforms-education-pointers-payments">Payments area</a> to get started.', 'wpforms-lite' ), esc_url( PaymentsPage::get_url() ) ); } /** * Retrieve the current installation license type in the lowercase. * If no license type is found, defaults to 'lite'. * * @since 1.8.8 * * @return string */ private function get_license_type(): string { $type = wpforms_get_license_type(); // Set the default to 'lite' if no license type is detected. if ( empty( $type ) ) { $type = 'lite'; } return $type; } } Admin/Education/Pointers/Pointer.php 0000644 00000024237 15174710275 0013464 0 ustar 00 <?php namespace WPForms\Admin\Education\Pointers; /** * Abstract class representing Pointers functionality. * * This abstract class provides a foundation for implementing pointers in WPForms. * Child classes should extend this class and implement the necessary methods to set properties and allow loading. * * The class separates concerns by implementing methods for different functionalities such as initializing pointers, * handling interactions, printing scripts, etc., which enhances code maintainability and security. * Additionally, the class is designed to be abstract, allowing for customization and extension while enforcing certain security measures in child classes. * * @since 1.8.8 */ abstract class Pointer { /** * Unique ID for the pointer. * * @since 1.8.8 * * @var string */ protected $pointer_id; /** * Selector for the pointer. * * @since 1.8.8 * * @var string */ protected $selector; /** * Arguments for the pointer. * * @since 1.8.8 * * @var array */ protected $args; /** * Top-level menu selector. * * @since 1.8.8 * * @var string */ private $top_level_menu = '#toplevel_page_wpforms-overview'; /** * Determines whether the pointer should be visible outside the "WPForms" primary menu. * Note that setting this property to true will display the pointer on other dashboard pages as well. * * @since 1.8.8 * * @var string */ protected $top_level_visible = false; /** * Option name for storing interactions with pointers. * * @since 1.8.8 */ private const OPTION_NAME = 'wpforms_pointers'; /** * Initialize the pointer. * * @since 1.8.8 */ public function init(): void { // If loading is not allowed, or if the pointer is already dismissed, return. if ( ! $this->allow_display() || ! $this->allow_load() ) { return; } // Set initial arguments. $this->set_initial_args(); // Register hooks. $this->hooks(); } /** * Check if the pointer is already dismissed or interacted with. * * @since 1.8.8 * * @return bool */ private function allow_display(): bool { // If the pointer ID is empty, return. // Check if announcements are allowed to be displayed. if ( empty( $this->pointer_id ) || wpforms_setting( 'hide-announcements' ) ) { return false; } // Get pointers. $pointers = (array) get_option( self::OPTION_NAME, [] ); // Check if the pointer ID exists in the engagement list. if ( isset( $pointers['engagement'] ) && in_array( $this->pointer_id, (array) $pointers['engagement'], true ) ) { return false; } // Check if the pointer ID exists in the dismissed list. if ( isset( $pointers['dismiss'] ) && in_array( $this->pointer_id, (array) $pointers['dismiss'], true ) ) { return false; } return true; } /** * Register hooks for the pointer. * * @since 1.8.8 */ private function hooks(): void { // Enqueue assets. add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] ); // Print the pointer script. add_action( 'admin_print_footer_scripts', [ $this, 'print_script' ] ); // Add Ajax callback for the engagement. add_action( 'wp_ajax_wpforms_education_pointers_engagement', [ $this, 'engagement_callback' ] ); // Add Ajax callback for dismissing the pointer. add_action( 'wp_ajax_wpforms_education_pointers_dismiss', [ $this, 'dismiss_callback' ] ); } /** * Enqueue assets for the pointer. * * @since 1.8.8 */ public function enqueue_assets() { // Enqueue the pointer CSS. wp_enqueue_style( 'wp-pointer' ); // Enqueue the pointer script. wp_enqueue_script( 'wp-pointer' ); } /** * Print the pointer script. * * @since 1.8.8 */ public function print_script(): void { // Encode the $args array into JSON format. $encoded_args = $this->get_prepared_args(); if ( empty( $encoded_args ) ) { return; } // Sanitize pointer ID and selector. $pointer_id = sanitize_text_field( $this->pointer_id ); $selector = sanitize_text_field( $this->get_selector() ); // Get the admin-ajax URL. $ajaxurl = esc_url_raw( admin_url( 'admin-ajax.php' ) ); // Create nonce for the pointer. $nonce = sanitize_text_field( $this->get_nonce_token() ); // Menu flyout selector. $menu_flyout = "{$this->top_level_menu}:not(.wp-menu-open)"; // Inline CSS style id. $inline_css_id = "wpforms-{$pointer_id}-inline-css"; // The type of echo being used in this PHP code is a HEREDOC syntax. // HEREDOC allows you to create strings that span multiple lines without // needing to concatenate them with dots (.) as you would with double quotes. // phpcs:disable echo <<<HTML <script type="text/javascript"> ( function( $ ) { let options = $encoded_args, setup; if ( ! options ) { return; } options = $.extend( options, { show: function() { if ( ! $( '#$inline_css_id' ).length && $( '$menu_flyout' ).length ) { $( '<style id="$inline_css_id">' ).text( '$menu_flyout:after, $menu_flyout .wp-submenu-wrap{ display: none }' ).appendTo( 'head' ); } }, close: function() { $( '#$inline_css_id' ).remove(); $.post( '$ajaxurl', { pointer_id: '$pointer_id', _ajax_nonce: '$nonce', action: 'wpforms_education_pointers_dismiss', } ); } } ); setup = function() { $( '$selector' ).first().pointer( options ).pointer( 'open' ); }; if ( options.position && options.position.defer_loading ) { $( window ).on( 'load.wp-pointers', setup ); } else { $( function() { setup(); } ); } } )( jQuery ); </script> HTML; // phpcs:enable } /** * Callback function for engaging with a pointer. * * This function is triggered via AJAX when a user interacts with a pointer, indicating engagement. * * @since 1.8.8 */ public function engagement_callback(): void { check_ajax_referer( $this->pointer_id, '_ajax_nonce' ); if ( ! wpforms_current_user_can() ) { wp_send_json_error(); } [ $pointer_id, $pointers ] = $this->handle_pointer_interaction(); // Add the current pointer to the engagement list. $pointers['engagement'][] = $pointer_id; // Update the pointer state. update_option( self::OPTION_NAME, $pointers ); // Indicate that the pointer was engaged. wp_send_json_success(); } /** * Ajax callback for dismissing the pointer. * * @since 1.8.8 */ public function dismiss_callback(): void { check_ajax_referer( $this->pointer_id, '_ajax_nonce' ); if ( ! wpforms_current_user_can() ) { wp_send_json_error(); } [ $pointer_id, $pointers ] = $this->handle_pointer_interaction(); // Add the current pointer to the dismissed list. $pointers['dismiss'][] = $pointer_id; // Update the pointer state. update_option( self::OPTION_NAME, $pointers ); // Indicate that the pointer was dismissed. wp_send_json_success(); } /** * Get nonce for the pointer. * * @since 1.8.8 * * @return string */ protected function get_nonce_token(): string { return wp_create_nonce( $this->pointer_id ); } /** * Handle pointer interaction via AJAX. * * @since 1.8.8 * * @return array Pointer ID and pointers state. */ private function handle_pointer_interaction(): array { // Check if the request is valid. check_ajax_referer( $this->pointer_id ); // Get the pointer ID from the request. $pointer_id = isset( $_POST['pointer_id'] ) ? sanitize_key( $_POST['pointer_id'] ) : ''; // If the pointer ID is empty, return an error response. if ( empty( $pointer_id ) ) { wp_send_json_error(); } // Get the current pointers state. $pointers = (array) get_option( self::OPTION_NAME, [ 'engagement' => [], 'dismiss' => [], ] ); return [ $pointer_id, $pointers ]; } /** * Set initial arguments to use in a pointer. * * @since 1.8.8 */ private function set_initial_args(): void { // Set default arguments. $this->args = [ 'content' => '', 'pointerWidth' => 395, 'position' => [ 'edge' => 'left', 'align' => 'center', ], ]; // Set additional arguments for the pointer. $this->set_args(); } /** * Retrieves the selector based on conditions. * * @since 1.8.8 * * @return string */ private function get_selector(): string { // If the sublevel menu is defined, and it's an admin page, return the combined selector. if ( ! empty( $this->selector ) && wpforms_is_admin_page() ) { return "{$this->top_level_menu} {$this->selector}"; } // Default returns the top-level menu. return $this->top_level_menu; } /** * Prepare and encode args for the pointer. * * @since 1.8.8 * * @return string */ private function get_prepared_args(): string { // Retrieve title and message from an argument array, fallback to empty strings if not set. $title = $this->args['title'] ?? ''; $message = $this->args['message'] ?? ''; // Return early if both title and message are empty. if ( empty( $message ) ) { return ''; } // Pointer markup uses <h3> tag for the title and <p> tag for the message. $content = ! empty( $title ) ? sprintf( '<h3>%s</h3>', esc_html( $title ) ) : ''; $content .= sprintf( '<p style="font-size:14px">%s</p>', wp_kses( $message, $this->get_allowed_html() ) ); $this->args['content'] = $content; // Unset title and message to clean up an argument array. unset( $this->args['title'], $this->args['message'] ); // If RTL and position edge are 'left', switch it to 'right'. if ( ! empty( $this->args['position']['edge'] ) && $this->args['position']['edge'] === 'left' && is_rtl() ) { $this->args['position']['edge'] = 'right'; } // Encode arguments array to JSON. return wp_json_encode( $this->args ); } /** * Get allowed HTML tags for wp_kses. * * @since 1.8.8 * * @return array */ private function get_allowed_html(): array { return [ 'a' => [ 'id' => [], 'class' => [], 'href' => [], 'target' => [], 'rel' => [], ], 'strong' => [], 'em' => [], 'br' => [], ]; } /** * Check if loading of the pointer is allowed. * * @since 1.8.8 * * @return bool */ abstract protected function allow_load(): bool; /** * Set arguments for the pointer. * * @since 1.8.8 */ abstract protected function set_args(); } Admin/Education/EducationInterface.php 0000644 00000000636 15174710275 0013772 0 ustar 00 <?php namespace WPForms\Admin\Education; /** * Interface EducationInterface defines required methods for Education features to work properly. * * @since 1.6.6 */ interface EducationInterface { /** * Indicate if current Education feature is allowed to load. * * @since 1.6.6 * * @return bool */ public function allow_load(); /** * Init. * * @since 1.6.6 */ public function init(); } Admin/Education/AddonsItemBase.php 0000644 00000006751 15174710275 0013064 0 ustar 00 <?php namespace WPForms\Admin\Education; use WPForms\Admin\Addons\Addons; use WPForms\Requirements\Requirements; /** * Base class for all "addon item" type Education features. * * @since 1.6.6 */ abstract class AddonsItemBase implements EducationInterface { /** * Instance of the Education\Core class. * * @since 1.6.6 * * @var Core */ protected $education; /** * Instance of the Education\Addons class. * * @since 1.6.6 * * @var Addons */ protected $addons; /** * Template name for rendering single addon item. * * @since 1.6.6 * * @var string */ protected $single_addon_template; /** * Indicate if the current Education feature is allowed to load. * Should be called from the child feature class. * * @since 1.6.6 * * @return bool * * @noinspection PhpMissingReturnTypeInspection * @noinspection ReturnTypeCanBeDeclaredInspection */ abstract public function allow_load(); /** * Init. * * @since 1.6.6 * * @noinspection ReturnTypeCanBeDeclaredInspection */ public function init() { if ( ! $this->allow_load() ) { return; } // Store the instance of the Education core class. $this->education = wpforms()->obj( 'education' ); // Store the instance of the Education\Addons class. $this->addons = wpforms()->obj( 'addons' ); // Define hooks. $this->hooks(); } /** * Hooks. * * @since 1.6.6 */ abstract public function hooks(); /** * Display single addon item. * * @since 1.6.6 * * @param array $addon Addon data. * * @noinspection ReturnTypeCanBeDeclaredInspection * @noinspection PhpMissingParamTypeInspection */ protected function display_single_addon( $addon ) { /** * Filter to disallow addons to be displayed in the Education feature. * * @since 1.8.2 * * @param bool $display Whether to hide the addon. * @param array $slug Addon data. */ $is_disallowed = (bool) apply_filters( 'wpforms_admin_education_addons_item_base_display_single_addon_hide', false, $addon ); if ( empty( $addon ) || $is_disallowed ) { return; } echo wpforms_render( // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped $this->single_addon_template, $addon, true ); } /** * Prepare field data-attributes for the education actions. * E.g., install, activate, incompatible. * * @since 1.9.4 * * @param array $addon Current addon information. * * @return array */ protected function prepare_field_action_data( array $addon ): array { if ( empty( $addon['plugin_allow'] ) ) { return []; } if ( $addon['action'] === 'install' ) { return [ 'data' => [ 'action' => 'install', 'name' => $addon['modal_name'], 'url' => $addon['url'], 'nonce' => wp_create_nonce( 'wpforms-admin' ), 'license' => $addon['license_level'], ], 'class' => 'education-modal', ]; } if ( $addon['action'] === 'activate' ) { return [ 'data' => [ 'action' => 'activate', 'name' => sprintf( /* translators: %s - addon name. */ esc_html__( '%s addon', 'wpforms-lite' ), $addon['name'] ), 'path' => $addon['path'], 'nonce' => wp_create_nonce( 'wpforms-admin' ), ], 'class' => 'education-modal', ]; } if ( $addon['action'] === 'incompatible' ) { return [ 'data' => [ 'action' => 'incompatible', 'message' => Requirements::get_instance()->get_notice( $addon['path'] ), ], 'class' => 'education-modal', ]; } return []; } } Admin/Education/AddonsListBase.php 0000644 00000002756 15174710275 0013102 0 ustar 00 <?php namespace WPForms\Admin\Education; /** * Base class for all "addons list" type Education features. * * @since 1.6.6 */ abstract class AddonsListBase extends AddonsItemBase { /** * Display addons. * * @since 1.6.6 */ public function display_addons() { array_map( [ $this, 'display_single_addon' ], (array) $this->get_addons() ); } /** * Get addons. * * @since 1.6.6 * * @return array Addons array. */ abstract protected function get_addons(); /** * Ensure that we do not display activated addon items if those addons are not allowed according to the current license. * * @since 1.6.6 * * @param string $hook Hook name. */ protected function filter_not_allowed_addons( $hook ) { $edu_addons = wp_list_pluck( $this->get_addons(), 'slug' ); foreach ( $edu_addons as $i => $addon ) { $edu_addons[ $i ] = strtolower( preg_replace( '/[^a-zA-Z0-9]+/', '', $addon ) ); } if ( empty( $GLOBALS['wp_filter'][ $hook ]->callbacks ) ) { return; } foreach ( $GLOBALS['wp_filter'][ $hook ]->callbacks as $priority => $hooks ) { foreach ( $hooks as $name => $arr ) { $class = ! empty( $arr['function'][0] ) && is_object( $arr['function'][0] ) ? strtolower( get_class( $arr['function'][0] ) ) : ''; $class = explode( '\\', $class )[0]; $class = preg_replace( '/[^a-zA-Z0-9]+/', '', $class ); if ( in_array( $class, $edu_addons, true ) ) { unset( $GLOBALS['wp_filter'][ $hook ]->callbacks[ $priority ][ $name ] ); } } } } } Admin/Education/Helpers.php 0000644 00000007036 15174710275 0011641 0 ustar 00 <?php namespace WPForms\Admin\Education; /** * Helpers class. * * @since 1.8.5 */ class Helpers { /** * Get badge HTML. * * @since 1.8.5 * @since 1.8.6 Added `$icon` parameter. * * @param string $text Badge text. * @param string $size Badge size. * @param string $position Badge position. * @param string $color Badge color. * @param string $shape Badge shape. * @param string $icon Badge icon name in Font Awesome "format", e.g. `fa-check`, defaults to empty string. * * @return string */ public static function get_badge( string $text, string $size = 'sm', string $position = 'inline', string $color = 'titanium', string $shape = 'rounded', string $icon = '' ): string { if ( ! empty( $icon ) ) { $icon = self::get_inline_icon( $icon ); } return sprintf( '<span class="wpforms-badge wpforms-badge-%1$s wpforms-badge-%2$s wpforms-badge-%3$s wpforms-badge-%4$s">%5$s%6$s</span>', esc_attr( $size ), esc_attr( $position ), esc_attr( $color ), esc_attr( $shape ), wp_kses( $icon, [ 'i' => [ 'class' => [], 'aria-hidden' => [], ], ] ), esc_html( $text ) ); } /** * Print badge HTML. * * @since 1.8.5 * @since 1.8.6 Added `$icon` parameter. * * @param string $text Badge text. * @param string $size Badge size. * @param string $position Badge position. * @param string $color Badge color. * @param string $shape Badge shape. * @param string $icon Badge icon name in Font Awesome "format", e.g. `fa-check`, defaults to empty string. */ public static function print_badge( string $text, string $size = 'sm', string $position = 'inline', string $color = 'titanium', string $shape = 'rounded', string $icon = '' ) { // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo self::get_badge( $text, $size, $position, $color, $shape, $icon ); } /** * Get addon badge HTML. * * @since 1.8.9 * * @param array $addon Addon data. * * @return string */ public static function get_addon_badge( array $addon ): string { // List of possible badges. $badges = [ 'recommended' => [ 'text' => esc_html__( 'Recommended', 'wpforms-lite' ), 'color' => 'green', 'icon' => 'fa-star', ], 'new' => [ 'text' => esc_html__( 'New', 'wpforms-lite' ), 'color' => 'blue', ], 'featured' => [ 'text' => esc_html__( 'Featured', 'wpforms-lite' ), 'color' => 'orange', ], ]; $badge = []; // Get first badge that exists. foreach ( $badges as $key => $value ) { if ( ! empty( $addon[ $key ] ) ) { $badge = $value; break; } } if ( empty( $badge ) ) { return ''; } return self::get_badge( $badge['text'], 'sm', 'inline', $badge['color'], 'rounded', $badge['icon'] ?? '' ); } /** * Get inline icon HTML. * * @since 1.8.6 * * @param string $name Font Awesome icon name, e.g. `fa-check`. * * @return string HTML markup for the icon element. */ public static function get_inline_icon( string $name ): string { return sprintf( '<i class="fa %1$s" aria-hidden="true"></i>', esc_attr( $name ) ); } /** * Get available education addons. * * @since 1.9.4 * * @return array */ public static function get_edu_addons(): array { static $addons = null; if ( $addons !== null ) { return $addons; } $addons_obj = wpforms()->obj( 'addons' ); if ( ! $addons_obj ) { $addons = []; return $addons; } $addons = $addons_obj->get_available(); return $addons; } } Admin/Education/Builder/Calculations.php 0000644 00000020070 15174710275 0014237 0 ustar 00 <?php namespace WPForms\Admin\Education\Builder; use WPForms\Admin\Education\AddonsItemBase; use WPForms\Admin\Education\Helpers; use WPForms\Integrations\AI\Helpers as AIHelpers; /** * Builder/Calculations Education feature for Lite and Pro. * * @since 1.8.4.1 */ class Calculations extends AddonsItemBase { /** * Support calculations in these field types. * * @since 1.8.4.1 * * @var array */ public const ALLOWED_FIELD_TYPES = [ 'text', 'textarea', 'number', 'hidden', 'payment-single' ]; /** * Field types that should display educational notice in the basic field options tab. * * @since 1.8.4.1 * * @var array */ public const BASIC_OPTIONS_NOTICE_FIELD_TYPES = [ 'number', 'payment-single' ]; /** * Indicate if the current Education feature is allowed to load. * * @since 1.8.4.1 * * @return bool * * @noinspection PhpMissingReturnTypeInspection * @noinspection ReturnTypeCanBeDeclaredInspection */ public function allow_load() { return wpforms_is_admin_page( 'builder' ) || wpforms_is_admin_ajax(); } /** * Hooks. * * @since 1.8.4.1 * * @noinspection ReturnTypeCanBeDeclaredInspection */ public function hooks() { add_action( 'wpforms_field_options_bottom_basic-options', [ $this, 'basic_options' ], 20, 2 ); add_action( 'wpforms_field_options_bottom_advanced-options', [ $this, 'advanced_options' ], 20, 2 ); } /** * Display notice on basic options. * * @since 1.8.4.1 * * @param array $field Field data. * @param object $instance Builder instance. * * @noinspection HtmlUnknownTarget * @noinspection ReturnTypeCanBeDeclaredInspection * @noinspection PhpMissingParamTypeInspection * @noinspection HtmlUnknownAnchorTarget */ public function basic_options( $field, $instance ) { // Display notice in basic options only in numbers and payment-single fields. if ( ! in_array( $field['type'], self::BASIC_OPTIONS_NOTICE_FIELD_TYPES, true ) ) { return; } $dismissed = get_user_meta( get_current_user_id(), 'wpforms_dismissed', true ); $form_id = $instance->form_id ?? 0; $dismiss_section = "builder-form-$form_id-field-options-calculations-notice"; // Check whether it is dismissed. if ( ! empty( $dismissed[ 'edu-' . $dismiss_section ] ) ) { return; } // Display notice only if Calculations addon is released (available in the `addons.json` file). $addon = $this->addons->get_addon( 'calculations' ); if ( ! $addon ) { return; } if ( AIHelpers::is_disabled() || ( wpforms_version_compare( $addon['version'] ?? '1.5.0', '1.5.0', '<=' ) ) ) { $this->print_standard_education( $dismiss_section ); return; } $badge = esc_html__( 'NEW FEATURE', 'wpforms-lite' ); $notice_header = esc_html__( 'AI Calculations Are Here!', 'wpforms-lite' ); $notice = sprintf( wp_kses( /* translators: %1$s - link to the WPForms.com doc article. */ __( 'Easily create advanced calculations with WPForms AI. Head over to the <a href="#advanced-tab">Advanced Tab</a> to get started or read <a href="%1$s" target="_blank" rel="noopener noreferrer">our documentation</a> to learn more.', 'wpforms-lite' ), [ 'a' => [ 'href' => [], 'rel' => [], 'target' => [], ], ] ), esc_url( wpforms_utm_link( 'https://wpforms.com/docs/generating-calculation-formulas-with-wpforms-ai/', 'Calculations Education', 'Calculations Documentation' ) ) ); printf( '<div class="wpforms-alert-ai wpforms-alert wpforms-educational-alert wpforms-calculations wpforms-field-educational-alert wpforms-dismiss-container"> <span class="wpforms-badge wpforms-badge-sm wpforms-badge-block wpforms-badge-purple wpforms-badge-rounded"> %5$s </span> <button type="button" class="wpforms-dismiss-button" title="%1$s" data-section="%2$s"></button> <h3>%4$s</h3> <p>%3$s</p> </div>', esc_html__( 'Dismiss this notice.', 'wpforms-lite' ), esc_attr( $dismiss_section ), $notice, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped $notice_header, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped $badge // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ); } /** * Print standard education notice. * * @since 1.9.4 * * @param string $dismiss_section Dismiss section. * * @noinspection HtmlUnknownAnchorTarget * @noinspection HtmlUnknownTarget */ private function print_standard_education( string $dismiss_section ): void { $notice = sprintf( wp_kses( /* translators: %1$s - link to the WPForms.com doc article. */ __( 'Easily perform calculations based on user input. Head over to the <a href="#advanced-tab">Advanced Tab</a> to get started or read <a href="%1$s" target="_blank" rel="noopener noreferrer">our documentation</a> to learn more.', 'wpforms-lite' ), [ 'a' => [ 'href' => [], 'rel' => [], 'target' => [], ], ] ), esc_url( wpforms_utm_link( 'https://wpforms.com/docs/calculations-addon/', 'Calculations Education', 'Calculations Documentation' ) ) ); printf( '<div class="wpforms-alert-info wpforms-alert wpforms-educational-alert wpforms-calculations wpforms-field-educational-alert wpforms-dismiss-container"> <button type="button" class="wpforms-dismiss-button" title="%1$s" data-section="%2$s"></button> <p>%3$s</p> </div>', esc_html__( 'Dismiss this notice.', 'wpforms-lite' ), esc_attr( $dismiss_section ), $notice // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ); } /** * Display advanced options. * * @since 1.8.4.1 * * @param array $field Field data. * @param object $instance Builder instance. * * @noinspection ReturnTypeCanBeDeclaredInspection * @noinspection PhpMissingParamTypeInspection */ public function advanced_options( $field, $instance ) { if ( ! in_array( $field['type'], self::ALLOWED_FIELD_TYPES, true ) ) { return; } $addon = $this->addons->get_addon( 'calculations' ); if ( ! $this->is_edu_required_by_status( $addon ) ) { return; } $row_args = $this->get_row_attributes( $addon ); $row_args['content'] = $instance->field_element( 'toggle', $field, $this->get_field_attributes( $addon ), false ); $instance->field_element( 'row', $field, $row_args ); } /** * Get row attributes. * * @since 1.8.4.1 * * @param array $addon Addon data. * * @return array */ private function get_row_attributes( array $addon ): array { $data = $this->prepare_field_action_data( $addon ); $default = [ 'slug' => 'calculation_is_enabled', ]; if ( ! empty( $data ) ) { return wp_parse_args( $data, $default ); } return wp_parse_args( [ 'data' => [ 'action' => 'upgrade', 'name' => esc_html__( 'Calculations', 'wpforms-lite' ), 'utm-content' => 'Enable Calculations', 'license' => $addon['license_level'], ], 'class' => 'education-modal', ], $default ); } /** * Get attributes for the Enable Calculation field. * * @since 1.8.4.1 * * @param array $addon Addon data. * * @return array */ private function get_field_attributes( array $addon ): array { $default = [ 'slug' => 'calculation_is_enabled', 'value' => '0', 'desc' => esc_html__( 'Enable Calculation', 'wpforms-lite' ), ]; if ( $addon['plugin_allow'] ) { return $default; } return wp_parse_args( [ 'desc' => sprintf( '%1$s%2$s', esc_html__( 'Enable Calculation', 'wpforms-lite' ), Helpers::get_badge( $addon['license_level'], 'sm', 'inline', 'slate' ) ), 'attrs' => [ 'disabled' => 'disabled', ], ], $default ); } /** * Determine if we require displaying educational items according to the addon status. * * @since 1.8.4.1 * * @param array $addon Addon data. * * @return bool */ private function is_edu_required_by_status( array $addon ): bool { return ! ( empty( $addon ) || empty( $addon['action'] ) || empty( $addon['status'] ) || ( $addon['status'] === 'active' && $addon['action'] !== 'upgrade' ) ); } } Admin/Education/Builder/Settings.php 0000644 00000002322 15174710275 0013416 0 ustar 00 <?php namespace WPForms\Admin\Education\Builder; use \WPForms\Admin\Education; /** * Builder/Settings Education feature. * * @since 1.6.6 */ class Settings extends Education\Builder\Panel { /** * Panel slug. * * @since 1.6.6 * * @return string **/ protected function get_name() { return 'settings'; } /** * Hooks. * * @since 1.6.6 */ public function hooks() { add_filter( 'wpforms_builder_settings_sections', [ $this, 'filter_addons' ], 1 ); add_action( 'wpforms_builder_after_panel_sidebar', [ $this, 'display' ], 100, 2 ); } /** * Display settings addons. * * @since 1.6.6 * * @param object $form Current form. * @param string $panel Panel slug. */ public function display( $form, $panel ) { if ( empty( $form ) || $this->get_name() !== $panel ) { return; } $this->display_addons(); } /** * Ensure that we do not display activated addon items if those addons are not allowed according to the current license. * * @since 1.6.6 * * @param array $sections Settings sections. * * @return array */ public function filter_addons( $sections ) { $this->filter_not_allowed_addons( 'wpforms_builder_settings_sections' ); return $sections; } } Admin/Education/Builder/Providers.php 0000644 00000002630 15174710275 0013575 0 ustar 00 <?php namespace WPForms\Admin\Education\Builder; use \WPForms\Admin\Education; /** * Builder/Providers Education feature. * * @since 1.6.6 */ class Providers extends Education\Builder\Panel { /** * Panel slug. * * @since 1.6.6 * * @return string **/ protected function get_name() { return 'providers'; } /** * Hooks. * * @since 1.6.6 */ public function hooks() { add_action( 'wpforms_providers_panel_sidebar', [ $this, 'filter_addons' ], 1 ); add_action( 'wpforms_providers_panel_sidebar', [ $this, 'display_addons' ], 500 ); } /** * Ensure that we do not display activated addon items if those addons are not allowed according to the current license. * * @since 1.6.6 */ public function filter_addons() { $this->filter_not_allowed_addons( 'wpforms_providers_panel_sidebar' ); } /** * Get addons for the Marketing panel. * * @since 1.7.7.2 */ protected function get_addons() { $addons = parent::get_addons(); /** * Google Sheets uses Providers API. All providers are automatically * added to the Marketing tab in the builder. We don't need the addon * on the Marketing tab because the addon is already added to * the builder's Settings tab. */ foreach ( $addons as $key => $addon ) { if ( isset( $addon['slug'] ) && $addon['slug'] === 'wpforms-google-sheets' ) { unset( $addons[ $key ] ); break; } } return $addons; } } Admin/Education/Builder/Geolocation.php 0000644 00000006633 15174710275 0014072 0 ustar 00 <?php namespace WPForms\Admin\Education\Builder; use WPForms\Admin\Education\AddonsItemBase; use WPForms\Admin\Education\Helpers; /** * Builder/Geolocation Education feature for Lite and Pro. * * @since 1.6.6 */ class Geolocation extends AddonsItemBase { /** * Indicate if the current Education feature is allowed to load. * * @since 1.6.6 * * @return bool * * @noinspection ReturnTypeCanBeDeclaredInspection * @noinspection PhpMissingReturnTypeInspection */ public function allow_load() { return wpforms_is_admin_page( 'builder' ) || wp_doing_ajax(); } /** * Hooks. * * @since 1.6.6 * * @noinspection ReturnTypeCanBeDeclaredInspection */ public function hooks() { add_action( 'wpforms_field_options_bottom_advanced-options', [ $this, 'geolocation_options' ], 10, 2 ); } /** * Display geolocation options. * * @since 1.6.6 * * @param array $field Field data. * @param object $instance Builder instance. * * @noinspection ReturnTypeCanBeDeclaredInspection * @noinspection PhpMissingParamTypeInspection */ public function geolocation_options( $field, $instance ) { if ( ! in_array( $field['type'], [ 'text', 'address' ], true ) ) { return; } $addon = $this->addons->get_addon( 'geolocation' ); if ( empty( $addon ) || empty( $addon['action'] ) || empty( $addon['status'] ) || ( $addon['status'] === 'active' && $addon['action'] !== 'upgrade' ) ) { return; } $row_args = $this->get_address_autocomplete_row_attributes( $addon ); $row_args['content'] = $instance->field_element( 'toggle', $field, $this->get_address_autocomplete_field_attributes( $addon ), false ); $instance->field_element( 'row', $field, $row_args ); } /** * Get attributes for address autocomplete row. * * @since 1.6.6 * * @param array $addon Current addon information. * * @return array */ private function get_address_autocomplete_row_attributes( array $addon ): array { $data = $this->prepare_field_action_data( $addon ); $default = [ 'slug' => 'enable_address_autocomplete', ]; if ( ! empty( $data ) ) { return wp_parse_args( $data, $default ); } return wp_parse_args( [ 'data' => [ 'action' => 'upgrade', 'name' => esc_html__( 'Address Autocomplete', 'wpforms-lite' ), 'utm-content' => 'Address Autocomplete', 'licence' => 'pro', 'message' => esc_html__( 'We\'re sorry, Address Autocomplete is part of the Geolocation Addon and not available on your plan. Please upgrade to the PRO plan to unlock all these awesome features.', 'wpforms-lite' ), ], 'class' => 'education-modal', ], $default ); } /** * Get attributes for address autocomplete field. * * @since 1.6.6 * * @param array $addon Current addon information. * * @return array */ private function get_address_autocomplete_field_attributes( array $addon ): array { $default = [ 'slug' => 'enable_address_autocomplete', 'value' => '0', 'desc' => esc_html__( 'Enable Address Autocomplete', 'wpforms-lite' ), ]; if ( $addon['plugin_allow'] ) { return $default; } return wp_parse_args( [ 'desc' => sprintf( '%1$s%2$s', esc_html__( 'Enable Address Autocomplete', 'wpforms-lite' ), Helpers::get_badge( 'Pro', 'sm', 'inline', 'slate' ) ), 'attrs' => [ 'disabled' => 'disabled', ], ], $default ); } } Admin/Education/Builder/Panel.php 0000644 00000002421 15174710275 0012655 0 ustar 00 <?php namespace WPForms\Admin\Education\Builder; use \WPForms\Admin\Education\AddonsListBase; /** * Base class for Builder/Settings, Builder/Providers, Builder/Payments Education features. * * @since 1.6.6 */ abstract class Panel extends AddonsListBase { /** * Panel slug. Should be redefined in the real Builder/Panel class. * * @since 1.6.6 * * @return string **/ abstract protected function get_name(); /** * Indicate if current Education feature is allowed to load. * * @since 1.6.6 * * @return bool */ public function allow_load() { // Load only in the Form Builder. return wpforms_is_admin_page( 'builder' ) && ! empty( $this->get_name() ); } /** * Get addons for the current panel. * * @since 1.6.6 */ protected function get_addons() { return $this->addons->get_by_path( 'form_builder.category', $this->get_name() ); } /** * Template name for rendering single addon item. * * @since 1.6.6 * * @return string */ protected function get_single_addon_template() { return 'education/builder/' . $this->get_name() . '-item'; } /** * Display addons. * * @since 1.6.6 */ public function display_addons() { $this->single_addon_template = $this->get_single_addon_template(); parent::display_addons(); } } Admin/Education/Builder/PDF.php 0000644 00000012557 15174710275 0012242 0 ustar 00 <?php namespace WPForms\Admin\Education\Builder; use WPForms\Admin\Education\Helpers; /** * PDF educational popup. * * @since 1.9.7.3 */ class PDF { /** * Addon slug. * * @since 1.9.7.3 * * @var string */ private $slug = 'wpforms-pdf'; /** * Addon data. * * @since 1.9.7.3 * * @var array */ private $addon_data; /** * Initialize. * * @since 1.9.7.3 */ public function init(): void { $this->addon_data = $this->get_addon_data(); if ( ! $this->should_show_popup() ) { return; } $this->hooks(); } /** * Should show popup. * * @since 1.9.7.3 * * @return bool */ private function should_show_popup(): bool { if ( ! wpforms_is_admin_page( 'builder' ) && ! wpforms_is_admin_ajax() ) { return false; } if ( ! current_user_can( wpforms_get_capability_manage_options() ) ) { return false; } $challenge = wpforms()->obj( 'challenge' ); if ( ! $challenge || $challenge->challenge_active() ) { return false; } return $this->is_popup_visible(); } /** * Is popup visible. * * @since 1.9.7.3 * * @return bool */ private function is_popup_visible(): bool { $action = $this->addon_data['action'] ?? 'install'; if ( empty( $this->addon_data ) || ( $action === 'install' && empty( $this->addon_data['url'] ) ) // The install action requires a valid URL. ) { return false; } $meta = get_user_meta( get_current_user_id(), 'wpforms_dismissed', true ); return empty( $meta['edu-builder-pdf'] ); } /** * Hooks. * * @since 1.9.7.3 */ private function hooks(): void { add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_scripts' ] ); add_filter( 'wpforms_builder_output_before_toolbar', [ $this, 'popup_html' ] ); } /** * Enqueue scripts. * * @since 1.9.7.3 */ public function enqueue_scripts(): void { $min = wpforms_get_min_suffix(); wp_enqueue_script( 'wpforms-pdf-popup', WPFORMS_PLUGIN_URL . "assets/js/admin/education/pdf$min.js", [], WPFORMS_VERSION, true ); } /** * Popup HTML. * * @since 1.9.7.3 * * @param string|mixed $html HTML. * * @return string * @noinspection HtmlUnknownTarget */ public function popup_html( $html ): string { $html = (string) $html; $popup = sprintf( '<div id="wpforms-pdf-popup" class="wpforms-pdf-popup wpforms-hidden wpforms-dismiss-container" role="dialog" aria-modal="true" aria-labelledby="wpforms-pdf-popup-title" aria-describedby="wpforms-pdf-popup-description"> <div class="wpforms-pdf-popup-content"> <div class="icon"> <img src="%1$s" alt="PDF Icon"> </div> <div class="close-popup wpforms-dismiss-button dashicons-no-alt" data-section="builder-pdf"></div> <div class="badge">%2$s</div> <h2 id="wpforms-pdf-popup-title">%3$s</h2> <p id="wpforms-pdf-popup-description">%4$s</p> %5$s </div> </div>', esc_url( WPFORMS_PLUGIN_URL . 'assets/images/pdf-education/pdf.svg' ), __( 'NEW FEATURE', 'wpforms-lite' ), __( 'PDF Addon', 'wpforms-lite' ), __( 'Easily turn form entry data into beautifully designed PDFs and attach them to notifications.', 'wpforms-lite' ), $this->get_button_html() ); return $popup . $html; } /** * Get button HTML. * * @since 1.9.7.3 * * @return string * @noinspection HtmlUnknownAttribute */ private function get_button_html(): string { $addon = $this->addon_data; $action = $addon['action'] ?? 'switch'; [ $button_label, $button_utm, $button_class, $button_attr ] = $this->get_button_data( $action, $addon ); return sprintf( '<button class="wpforms-btn wpforms-btn-sm wpforms-btn-orange %1$s" data-action="%2$s" %4$s data-license="%5$s" data-utm-content="%6$s">%3$s</button>', esc_attr( $button_class ), esc_attr( $action ), esc_html( $button_label ), $button_attr, esc_attr( $addon['license_level'] ?? 'pro' ), esc_attr( $button_utm ) ); } /** * Get addon data. * * @since 1.9.7.3 * * @return array */ private function get_addon_data(): array { /** * Filter the slug for the PDF educational popup. * * @since 1.9.7.3 * * @param string $slug The slug for the PDF educational popup. */ $slug = apply_filters( 'wpforms_admin_education_builder_pdf_get_addon_data_slug', $this->slug ); $addons = Helpers::get_edu_addons(); return $addons[ $slug ] ?? []; } /** * Get button data. * * @since 1.9.7.3 * * @param string $action Action type (switch, upgrade, etc.). * @param array $addon Addon data. * * @return array */ protected function get_button_data( string $action, array $addon ): array { $button_label = $action === 'upgrade' ? esc_html__( 'Upgrade to Pro', 'wpforms-lite' ) : esc_html__( 'Try it Out', 'wpforms-lite' ); $button_utm = 'PDF Addon Pop-up'; $button_class = 'education-action-button'; $button_attr = ''; if ( $action === 'switch' ) { $button_class = 'education-modal education-switch-button'; $button_attr = 'data-target="wpforms-pdf"'; } elseif ( $action !== 'upgrade' ) { $button_class = 'education-modal'; $button_attr = sprintf( 'data-nonce="%1$s" data-path="%2$s" data-url="%3$s" data-message="" data-name="%4$s"', esc_attr( wp_create_nonce( 'wpforms-admin' ) ), $addon['path'] ?? '', $addon['url'] ?? '', esc_html__( 'WPForms PDF Addon', 'wpforms-lite' ) ); } return [ $button_label, $button_utm, $button_class, $button_attr ]; } } Admin/Education/Builder/Payments.php 0000644 00000002347 15174710275 0013425 0 ustar 00 <?php namespace WPForms\Admin\Education\Builder; use \WPForms\Admin\Education; /** * Builder/Payments Education feature. * * @since 1.6.6 */ class Payments extends Education\Builder\Panel { /** * Panel slug. * * @since 1.6.6 * * @return string **/ protected function get_name() { return 'payments'; } /** * Hooks. * * @since 1.6.6 */ public function hooks() { add_action( 'wpforms_payments_panel_sidebar', [ $this, 'filter_addons' ], 1 ); add_action( 'wpforms_payments_panel_sidebar', [ $this, 'display_addons' ], 500 ); } /** * Get addons for the Payments panel. * * @since 1.7.7.2 * * @return array */ protected function get_addons() { return $this->addons->get_by_path( 'form_builder.category', $this->get_name() ); } /** * Template name for rendering single addon item. * * @since 1.6.6 * * @return string */ protected function get_single_addon_template() { return 'education/builder/providers-item'; } /** * Ensure that we do not display activated addon items if those addons are not allowed according to the current license. * * @since 1.6.6 */ public function filter_addons() { $this->filter_not_allowed_addons( 'wpforms_payments_panel_sidebar' ); } } Admin/Education/Builder/Quiz.php 0000644 00000012717 15174710275 0012557 0 ustar 00 <?php namespace WPForms\Admin\Education\Builder; use WPForms\Admin\Education\AddonsItemBase; use WPForms\Admin\Education\Helpers; /** * Builder/Quiz Education feature for Lite and Pro. * * @since 1.9.9 */ class Quiz extends AddonsItemBase { /** * Indicate if the current Education feature is allowed to load. * * @since 1.9.9 * * @return bool * * @noinspection ReturnTypeCanBeDeclaredInspection * @noinspection PhpMissingReturnTypeInspection */ public function allow_load() { return wpforms_is_admin_page( 'builder' ) || wp_doing_ajax(); } /** * Hooks. * * @since 1.9.9 * * @noinspection ReturnTypeCanBeDeclaredInspection */ public function hooks() { add_action( 'wpforms_field_options_before_description', [ $this, 'quiz_fields' ], 10, 2 ); } /** * Display the Enable Quiz option. * * @since 1.9.9 * * @param array $field Field data. * @param object $instance Builder instance. * * @noinspection ReturnTypeCanBeDeclaredInspection * @noinspection PhpMissingParamTypeInspection * @noinspection HtmlUnknownTarget */ public function quiz_fields( $field, $instance ) { if ( ! in_array( $field['type'], [ 'radio', 'checkbox', 'select' ], true ) ) { return; } $addon = $this->addons->get_addon( 'quiz' ); if ( empty( $addon ) || empty( $addon['action'] ) || empty( $addon['status'] ) || ( $addon['status'] === 'active' && $addon['action'] !== 'upgrade' ) ) { return; } $form_id = ! empty( $instance->form_id ) ? (int) $instance->form_id : 0; $row_args = $this->get_enable_quiz_row_attributes( $addon, $form_id ); $row_args['content'] = $instance->field_element( 'toggle', $field, $this->get_enable_quiz_field_attributes( $addon ), false ); $instance->field_element( 'row', $field, $row_args ); $dismissed = get_user_meta( get_current_user_id(), 'wpforms_dismissed', true ); $dismiss_section = "builder-form-$form_id-field-options-quiz-notice"; // Check whether it is dismissed. if ( ! empty( $dismissed[ 'edu-' . $dismiss_section ] ) ) { return; } $badge = esc_html__( 'NEW FEATURE', 'wpforms-lite' ); $notice_header = esc_html__( 'Turn Your Form Into a Quiz', 'wpforms-lite' ); $notice = sprintf( wp_kses( /* translators: %1$s - link to the WPForms.com doc article. */ __( 'Easily create interactive quizzes. Add true or false, multiple choice, or checkbox questions. Set correct answers and automatically score submissions. <a href="%1$s" target="_blank" rel="noopener noreferrer">Learn more about the Quiz Addon</a>', 'wpforms-lite' ), [ 'a' => [ 'href' => [], 'rel' => [], 'target' => [], ], ] ), esc_url( wpforms_utm_link( 'https://wpforms.com/docs/quiz-addon/', 'Quiz Education', 'Quiz Documentation' ) ) ); printf( '<div class="wpforms-alert wpforms-alert-info wpforms-educational-alert wpforms-field-educational-alert wpforms-dismiss-container"> <span class="wpforms-badge wpforms-badge-sm wpforms-badge-block wpforms-badge-green wpforms-badge-rounded"> %5$s </span> <button type="button" class="wpforms-dismiss-button" title="%1$s" data-section="%2$s"></button> <h3>%4$s</h3> <p>%3$s</p> </div>', esc_html__( 'Dismiss this notice.', 'wpforms-lite' ), esc_attr( $dismiss_section ), $notice, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped $notice_header, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped $badge // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ); } /** * Get attributes for the `Enable Quiz` field option row. * * @since 1.9.9 * * @param array $addon Current addon information. * @param int $form_id Form ID. * * @return array */ private function get_enable_quiz_row_attributes( array $addon, int $form_id ): array { $data = $this->prepare_field_action_data( $addon ); $default = [ 'slug' => 'enable_quiz', ]; if ( ! empty( $data ) ) { $data = wp_parse_args( $data, $default ); $data['data']['redirect-url'] = add_query_arg( [ 'page' => 'wpforms-builder', 'view' => 'settings', 'form_id' => $form_id, 'section' => 'quiz', ], admin_url( 'admin.php' ) ); return $data; } return wp_parse_args( [ 'data' => [ 'action' => 'upgrade', 'name' => esc_html__( 'Quiz Addon', 'wpforms-lite' ), 'utm-content' => 'Quiz Addon', 'licence' => 'pro', 'message' => esc_html__( 'We\'re sorry, Enable Quiz is part of the Quiz Addon and not available on your plan. Please upgrade to the PRO plan to unlock all these awesome features.', 'wpforms-lite' ), ], 'class' => 'education-modal', ], $default ); } /** * Get attributes for the `Enable Quiz` field option. * * @since 1.9.9 * * @param array $addon Current addon information. * * @return array */ private function get_enable_quiz_field_attributes( array $addon ): array { $default = [ 'slug' => 'enable_quiz', 'value' => '0', 'desc' => esc_html__( 'Include in Quiz Scoring', 'wpforms-lite' ), ]; if ( $addon['plugin_allow'] ) { return $default; } return wp_parse_args( [ 'desc' => sprintf( '%1$s%2$s', esc_html__( 'Include in Quiz Scoring', 'wpforms-lite' ), Helpers::get_badge( 'Pro', 'sm', 'inline', 'slate' ) ), 'attrs' => [ 'disabled' => 'disabled', ], ], $default ); } } Admin/Education/Builder/Captcha.php 0000644 00000012264 15174710275 0013167 0 ustar 00 <?php namespace WPForms\Admin\Education\Builder; use \WPForms\Admin\Education\EducationInterface; /** * Builder/ReCaptcha Education feature. * * @since 1.6.6 */ class Captcha implements EducationInterface { /** * Indicate if current Education feature is allowed to load. * * @since 1.6.6 */ public function allow_load() { return wp_doing_ajax(); } /** * Init. * * @since 1.6.6 */ public function init() { if ( ! $this->allow_load() ) { return; } // Define hooks. $this->hooks(); } /** * Hooks. * * @since 1.6.6 */ public function hooks() { add_action( 'wp_ajax_wpforms_update_field_captcha', [ $this, 'captcha_field_callback' ] ); } /** * Targeting on hCaptcha/reCAPTCHA field button in the builder. * * @since 1.6.6 */ public function captcha_field_callback() { // Run a security check. check_ajax_referer( 'wpforms-builder', 'nonce' ); // Check for form ID. if ( empty( $_POST['id'] ) ) { wp_send_json_error( esc_html__( 'No form ID found.', 'wpforms-lite' ) ); } $form_id = absint( $_POST['id'] ); // Check for permissions. if ( ! wpforms_current_user_can( 'edit_form_single', $form_id ) ) { wp_send_json_error( esc_html__( 'You do not have permission.', 'wpforms-lite' ) ); } // Get an actual form data. $form_data = wpforms()->obj( 'form' )->get( $form_id, [ 'content_only' => true ] ); // Check that CAPTCHA is configured in the settings. $captcha_settings = wpforms_get_captcha_settings(); $captcha_name = $this->get_captcha_name( $captcha_settings ); if ( empty( $form_data ) || empty( $captcha_name ) ) { wp_send_json_error( esc_html__( 'Something wrong. Please try again later.', 'wpforms-lite' ) ); } // Prepare a result array. $data = $this->get_captcha_result_mockup( $captcha_settings ); if ( empty( $captcha_settings['site_key'] ) || empty( $captcha_settings['secret_key'] ) ) { // If CAPTCHA is not configured in the WPForms plugin settings. $data['current'] = 'not_configured'; } elseif ( ! isset( $form_data['settings']['recaptcha'] ) || $form_data['settings']['recaptcha'] !== '1' ) { // If CAPTCHA is configured in WPForms plugin settings, but wasn't set in form settings. $data['current'] = 'configured_not_enabled'; } else { // If CAPTCHA is configured in WPForms plugin and form settings. $data['current'] = 'configured_enabled'; } wp_send_json_success( $data ); } /** * Retrieve the CAPTCHA name. * * @since 1.6.6 * * @param array $settings The CAPTCHA settings. * * @return string */ private function get_captcha_name( $settings ) { if ( empty( $settings['provider'] ) ) { return ''; } if ( empty( $settings['site_key'] ) && empty( $settings['secret_key'] ) ) { return esc_html__( 'CAPTCHA', 'wpforms-lite' ); } if ( $settings['provider'] === 'hcaptcha' ) { return esc_html__( 'hCaptcha', 'wpforms-lite' ); } if ( $settings['provider'] === 'turnstile' ) { return esc_html__( 'Cloudflare Turnstile', 'wpforms-lite' ); } $recaptcha_names = [ 'v2' => esc_html__( 'Google Checkbox v2 reCAPTCHA', 'wpforms-lite' ), 'invisible' => esc_html__( 'Google Invisible v2 reCAPTCHA', 'wpforms-lite' ), 'v3' => esc_html__( 'Google v3 reCAPTCHA', 'wpforms-lite' ), ]; return isset( $recaptcha_names[ $settings['recaptcha_type'] ] ) ? $recaptcha_names[ $settings['recaptcha_type'] ] : ''; } /** * Get CAPTCHA callback result mockup. * * @since 1.6.6 * * @param array $settings The CAPTCHA settings. * * @return array */ private function get_captcha_result_mockup( $settings ) { $captcha_name = $this->get_captcha_name( $settings ); if ( empty( $captcha_name ) ) { wp_send_json_error( esc_html__( 'Something wrong. Please, try again later.', 'wpforms-lite' ) ); } return [ 'current' => false, 'cases' => [ 'not_configured' => [ 'title' => esc_html__( 'Heads up!', 'wpforms-lite' ), 'content' => sprintf( wp_kses( /* translators: %1$s - CAPTCHA settings page URL, %2$s - WPForms.com doc URL. */ __( 'Please complete the setup in your <a href="%1$s" target="_blank">WPForms Settings</a>, and check out <a href="%2$s" target="_blank" rel="noopener noreferrer">our guide</a> to learn about available CAPTCHA solutions.', 'wpforms-lite' ), [ 'a' => [ 'href' => true, 'rel' => true, 'target' => true, ], ] ), esc_url( admin_url( 'admin.php?page=wpforms-settings&view=captcha' ) ), esc_url( wpforms_utm_link( 'https://wpforms.com/docs/setup-captcha-wpforms/', 'builder-modal', 'Captcha Documentation' ) ) ), ], 'configured_not_enabled' => [ 'title' => false, /* translators: %s - CAPTCHA name. */ 'content' => sprintf( esc_html__( '%s has been enabled for this form. Don\'t forget to save your form!', 'wpforms-lite' ), $captcha_name ), ], 'configured_enabled' => [ 'title' => false, /* translators: %s - CAPTCHA name. */ 'content' => sprintf( esc_html__( 'Are you sure you want to disable %s for this form?', 'wpforms-lite' ), $captcha_name ), 'cancel' => true, ], ], 'provider' => $settings['provider'], ]; } } Admin/Education/Builder/Fields.php 0000644 00000003140 15174710275 0013023 0 ustar 00 <?php namespace WPForms\Admin\Education\Builder; use WPForms\Admin\Education\AddonsItemBase; use WPForms\Admin\Education\Fields as EducationFields; /** * Base class for Builder/Fields Education feature. * * @since 1.6.6 */ abstract class Fields extends AddonsItemBase { /** * Instance of the Education\Fields class. * * @since 1.6.6 * * @var EducationFields */ protected $fields; /** * Indicate if current Education feature is allowed to load. * * @since 1.6.6 * * @return bool */ public function allow_load(): bool { return wp_doing_ajax() || wpforms_is_admin_page( 'builder' ); } /** * Init. * * @since 1.6.6 */ public function init(): void { parent::init(); // Store the instance of the Education\Fields class. $this->fields = wpforms()->obj( 'education_fields' ); } /** * Print the form preview notice. * * @since 1.9.4 * * @param array $texts Notice texts. */ protected function print_form_preview_notice( $texts ): void { printf( '<div class="wpforms-alert %1$s wpforms-alert-dismissible wpforms-pro-fields-notice wpforms-dismiss-container"> <div class="wpforms-alert-message"> <h3>%2$s</h3> <p>%3$s</p> </div> <div class="wpforms-alert-buttons"> <button type="button" class="wpforms-dismiss-button" data-section="%4$s" title="%5$s" /> </div> </div>', esc_attr( $texts['class'] ), esc_html( $texts['title'] ), $texts['content'], // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped esc_html( $texts['dismiss_section'] ), esc_attr__( 'Dismiss this notice', 'wpforms-lite' ) ); } } Admin/Education/StringsTrait.php 0000644 00000015113 15174710275 0012667 0 ustar 00 <?php namespace WPForms\Admin\Education; /** * Strings trait. * * @since 1.8.8 */ trait StringsTrait { /** * Localize common strings. * * @since 1.6.6 * * @return array */ protected function get_js_strings(): array { $strings = []; $name = '%name%'; $strings['ok'] = esc_html__( 'Ok', 'wpforms-lite' ); $strings['cancel'] = esc_html__( 'Cancel', 'wpforms-lite' ); $strings['close'] = esc_html__( 'Close', 'wpforms-lite' ); $strings['ajax_url'] = admin_url( 'admin-ajax.php' ); $strings['nonce'] = wp_create_nonce( 'wpforms-education' ); $strings['activate_prompt'] = '<p>' . esc_html( sprintf( /* translators: %s - addon name. */ __( 'The %s is installed but not activated. Would you like to activate it?', 'wpforms-lite' ), $name ) ) . '</p>'; $strings['activate_confirm'] = esc_html__( 'Yes, Activate', 'wpforms-lite' ); $strings['addon_activated'] = esc_html__( 'Addon activated', 'wpforms-lite' ); $strings['plugin_activated'] = esc_html__( 'Plugin activated', 'wpforms-lite' ); $strings['activating'] = esc_html__( 'Activating', 'wpforms-lite' ); $strings['install_prompt'] = '<p>' . esc_html( sprintf( /* translators: %s - addon name. */ __( 'The %s is not installed. Would you like to install and activate it?', 'wpforms-lite' ), $name ) ) . '</p>'; $strings['install_confirm'] = esc_html__( 'Yes, Install and Activate', 'wpforms-lite' ); $strings['installing'] = esc_html__( 'Installing', 'wpforms-lite' ); $strings['save_prompt'] = esc_html__( 'Almost done! Would you like to save and refresh the form builder?', 'wpforms-lite' ); $strings['save_confirm'] = esc_html__( 'Yes, save and refresh', 'wpforms-lite' ); $strings['saving'] = esc_html__( 'Saving ...', 'wpforms-lite' ); // Check if the user can install addons. // Includes license check. $can_install_addons = wpforms_can_install( 'addon' ); // Check if the user can install plugins. // Only checks if the user has the capability. // Needed to display the correct message for non-admin users. $can_install_plugins = current_user_can( 'install_plugins' ); $strings['can_install_addons'] = $can_install_addons && $can_install_plugins; if ( ! $can_install_addons ) { $strings['install_prompt'] = '<p>' . esc_html( sprintf( /* translators: %s - addon name. */ __( 'The %s is not installed. Please install and activate it to use this feature.', 'wpforms-lite' ), $name ) ) . '</p>'; } if ( ! $can_install_plugins ) { /* translators: %s - addon name. */ $strings['install_prompt'] = '<p>' . esc_html( sprintf( /* translators: %s - addon name. */ __( 'The %s is not installed. Please contact the site administrator.', 'wpforms-lite' ), $name ) ) . '</p>'; } // Check if the user can activate plugins. $can_activate_plugins = current_user_can( 'activate_plugins' ); $strings['can_activate_addons'] = $can_activate_plugins; if ( ! $can_activate_plugins ) { /* translators: %s - addon name. */ $strings['activate_prompt'] = '<p>' . esc_html( sprintf( __( 'The %s is not activated. Please contact the site administrator.', 'wpforms-lite' ), $name ) ) . '</p>'; } $upgrade_utm_medium = wpforms_is_admin_page() ? 'Settings - Integration' : 'Builder - Settings'; if ( wpforms_is_block_editor() ) { $upgrade_utm_medium = 'gutenberg'; } $strings['upgrade'] = [ 'pro' => $this->get_upgrade_strings( 'Pro', $name, $upgrade_utm_medium ), 'elite' => $this->get_upgrade_strings( 'Elite', $name, $upgrade_utm_medium ), ]; $strings['upgrade_bonus'] = wpautop( wp_kses( __( '<strong>Bonus:</strong> WPForms Lite users get <span>50% off</span> regular price, automatically applied at checkout.', 'wpforms-lite' ), [ 'strong' => [], 'span' => [], ] ) ); $strings['thanks_for_interest'] = esc_html__( 'Thanks for your interest in WPForms Pro!', 'wpforms-lite' ); /** * Filters the education strings. * * @since 1.6.6 * * @param array $strings Education strings. * * @return array */ return (array) apply_filters( 'wpforms_admin_education_strings', $strings ); } /** * Get upgrade strings. * * @since 1.8.8 * * @param string $level Upgrade level. * @param string $name Addon name. * @param string $upgrade_utm_medium UTM medium for the upgrade link. * * @return array * @noinspection HtmlUnknownTarget */ private function get_upgrade_strings( string $level, string $name, string $upgrade_utm_medium ): array { // phpcs:ignore WPForms.Formatting.EmptyLineAfterFunctionDeclaration.AddEmptyLineAfterFunctionDeclaration return [ 'title' => esc_html( sprintf( /* translators: %s - level name, either Pro or Elite. */ __( 'is a %s Feature', 'wpforms-lite' ), $level ) ), 'title_plural' => esc_html( sprintf( /* translators: %s - level name, either Pro or Elite. */ __( 'are a %s Feature', 'wpforms-lite' ), $level ) ), 'message' => '<p>' . esc_html( sprintf( /* translators: %1$s - addon name, %2$s - level name, either Pro or Elite. */ __( 'We\'re sorry, the %1$s is not available on your plan. Please upgrade to the %2$s plan to unlock all these awesome features.', 'wpforms-lite' ), $name, $level ) ) . '</p>', 'message_plural' => '<p>' . esc_html( sprintf( /* translators: %1$s - addon name, %2$s - level name, either Pro or Elite. */ __( 'We\'re sorry, %1$s are not available on your plan. Please upgrade to the %2$s plan to unlock all these awesome features.', 'wpforms-lite' ), $name, $level ) ) . '</p>', 'doc' => sprintf( '<a href="%1$s" target="_blank" rel="noopener noreferrer" class="already-purchased">%2$s</a>', esc_url( wpforms_utm_link( 'https://wpforms.com/docs/upgrade-wpforms-lite-paid-license/#installing-wpforms', $upgrade_utm_medium, '%name%' ) ), esc_html__( 'Already purchased?', 'wpforms-lite' ) ), 'button' => esc_html( sprintf( /* translators: %s - level name, either Pro or Elite. */ __( 'Upgrade to %s', 'wpforms-lite' ), $level ) ), 'url' => wpforms_admin_upgrade_link( $upgrade_utm_medium ), 'url_template' => wpforms_is_admin_page( 'templates' ) ? wpforms_admin_upgrade_link( 'Form Templates Subpage' ) : wpforms_admin_upgrade_link( 'builder-modal-template' ), 'url_themes' => wpforms_admin_upgrade_link( 'Builder Themes' ), 'modal' => wpforms_get_upgrade_modal_text( strtolower( $level ) ), ]; } } Admin/Education/Core.php 0000644 00000005743 15174710275 0011132 0 ustar 00 <?php namespace WPForms\Admin\Education; /** * Education core. * * @since 1.6.6 */ class Core { use StringsTrait; /** * Indicate if Education core is allowed to load. * * @since 1.6.6 * * @return bool */ public function allow_load(): bool { return wp_doing_ajax() || wpforms_is_admin_page() || wpforms_is_admin_page( 'builder' ); } /** * Init. * * @since 1.6.6 */ public function init() { // Only proceed if allowed. if ( ! $this->allow_load() ) { return; } $this->hooks(); } /** * Hooks. * * @since 1.6.6 */ protected function hooks() { if ( wp_doing_ajax() ) { add_action( 'wp_ajax_wpforms_education_dismiss', [ $this, 'ajax_dismiss' ] ); return; } add_action( 'admin_enqueue_scripts', [ $this, 'enqueues' ] ); } /** * Load enqueues. * * @since 1.6.6 */ public function enqueues() { $min = wpforms_get_min_suffix(); wp_enqueue_script( 'wpforms-admin-education-core', WPFORMS_PLUGIN_URL . "assets/js/admin/education/core{$min}.js", [ 'jquery', 'jquery-confirm' ], WPFORMS_VERSION, true ); wp_localize_script( 'wpforms-admin-education-core', 'wpforms_education', $this->get_js_strings() ); } /** * Ajax handler for the education dismisses buttons. * * @since 1.6.6 */ public function ajax_dismiss() { // Run a security check. check_ajax_referer( 'wpforms-education', 'nonce' ); // Section is the identifier of the education feature. // For example, in Builder/DidYouKnow feature used 'builder-did-you-know-notifications' // and 'builder-did-you-know-confirmations'. $section = ! empty( $_POST['section'] ) ? sanitize_key( $_POST['section'] ) : ''; if ( empty( $section ) ) { wp_send_json_error( [ 'error' => esc_html__( 'Please specify a section.', 'wpforms-lite' ) ] ); } // Check for permissions. if ( ! $this->current_user_can() ) { wp_send_json_error( [ 'error' => esc_html__( 'You do not have permission to perform this action.', 'wpforms-lite' ) ] ); } $user_id = get_current_user_id(); $dismissed = get_user_meta( $user_id, 'wpforms_dismissed', true ); if ( empty( $dismissed ) ) { $dismissed = []; } $dismissed[ 'edu-' . $section ] = time(); update_user_meta( $user_id, 'wpforms_dismissed', $dismissed ); wp_send_json_success(); } /** * Whether the current user can perform an action. * * @since 1.8.0 * * @return bool */ private function current_user_can(): bool { // phpcs:ignore WordPress.Security.NonceVerification.Missing $page = ! empty( $_POST['page'] ) ? sanitize_key( $_POST['page'] ) : ''; // key is the same as $current_screen->id and the JS global 'pagenow', value - capability name(s). $caps = [ 'toplevel_page_wpforms-overview' => [ 'view_forms' ], 'wpforms_page_wpforms-builder' => [ 'edit_forms' ], 'wpforms_page_wpforms-entries' => [ 'view_entries' ], ]; return isset( $caps[ $page ] ) ? wpforms_current_user_can( $caps[ $page ] ) : wpforms_current_user_can(); } } Admin/Education/Fields.php 0000644 00000024515 15174710275 0011446 0 ustar 00 <?php namespace WPForms\Admin\Education; /** * Fields data holder. * * @since 1.6.6 */ class Fields { /** * All fields data. * * @since 1.6.6 * * @var array */ protected $fields; /** * All fields data. * * @since 1.6.6 * * @return array All possible fields. */ private function get_all(): array { if ( ! empty( $this->fields ) ) { return $this->fields; } $this->fields = [ [ 'icon' => 'fa-phone', 'name' => esc_html__( 'Phone', 'wpforms-lite' ), 'name_en' => 'Phone', 'type' => 'phone', 'group' => 'fancy', 'order' => '50', ], [ 'icon' => 'fa-map-marker', 'name' => esc_html__( 'Address', 'wpforms-lite' ), 'name_en' => 'Address', 'type' => 'address', 'group' => 'fancy', 'order' => '70', ], [ 'icon' => 'fa-map-location-dot', 'name' => esc_html__( 'Map', 'wpforms-lite' ), 'name_en' => 'Map', 'type' => 'Map', 'group' => 'fancy', 'addon' => 'wpforms-geolocation', 'order' => '75', ], [ 'icon' => 'fa-calendar-o', 'name' => esc_html__( 'Date / Time', 'wpforms-lite' ), 'name_en' => 'Date / Time', 'type' => 'date-time', 'group' => 'fancy', 'order' => '80', ], [ 'icon' => 'fa-link', 'name' => esc_html__( 'Website / URL', 'wpforms-lite' ), 'name_en' => 'Website / URL', 'type' => 'url', 'group' => 'fancy', 'order' => '90', ], [ 'icon' => 'fa-upload', 'name' => esc_html__( 'File Upload', 'wpforms-lite' ), 'name_en' => 'File Upload', 'type' => 'file-upload', 'group' => 'fancy', 'order' => '100', ], [ 'icon' => 'fa-camera', 'name' => esc_html__( 'Camera', 'wpforms-lite' ), 'name_en' => 'Camera', 'type' => 'camera', 'group' => 'fancy', 'order' => '105', ], [ 'icon' => 'fa-lock', 'name' => esc_html__( 'Password', 'wpforms-lite' ), 'name_en' => 'Password', 'type' => 'password', 'group' => 'fancy', 'order' => '95', ], [ 'icon' => 'fa-columns', 'name' => esc_html__( 'Layout', 'wpforms-lite' ), 'name_en' => 'Layout', 'type' => 'layout', 'group' => 'fancy', 'order' => '140', ], [ 'icon' => 'fa-list', 'name' => esc_html__( 'Repeater', 'wpforms-lite' ), 'name_en' => 'Repeater', 'type' => 'repeater', 'group' => 'fancy', 'order' => '150', ], [ 'icon' => 'fa-files-o', 'name' => esc_html__( 'Page Break', 'wpforms-lite' ), 'name_en' => 'Page Break', 'type' => 'pagebreak', 'group' => 'fancy', 'order' => '160', ], [ 'icon' => 'fa-arrows-h', 'name' => esc_html__( 'Section Divider', 'wpforms-lite' ), 'name_en' => 'Section Divider', 'type' => 'divider', 'group' => 'fancy', 'order' => '170', ], [ 'icon' => 'fa-pencil-square-o', 'name' => esc_html__( 'Rich Text', 'wpforms-lite' ), 'name_en' => 'Rich Text', 'type' => 'richtext', 'group' => 'fancy', 'order' => '170', ], [ 'icon' => 'fa-file-image-o', 'name' => esc_html__( 'Content', 'wpforms-lite' ), 'name_en' => 'Content', 'type' => 'content', 'group' => 'fancy', 'order' => '180', ], [ 'icon' => 'fa-code', 'name' => esc_html__( 'HTML', 'wpforms-lite' ), 'name_en' => 'HTML', 'type' => 'html', 'group' => 'fancy', 'order' => '185', ], [ 'icon' => 'fa-file-text-o', 'name' => esc_html__( 'Entry Preview', 'wpforms-lite' ), 'name_en' => 'Entry Preview', 'type' => 'entry-preview', 'group' => 'fancy', 'order' => '190', ], [ 'icon' => 'fa-star', 'name' => esc_html__( 'Rating', 'wpforms-lite' ), 'name_en' => 'Rating', 'type' => 'rating', 'group' => 'fancy', 'order' => '310', ], [ 'icon' => 'fa-eye-slash', 'name' => esc_html__( 'Hidden Field', 'wpforms-lite' ), 'name_en' => 'Hidden Field', 'type' => 'hidden', 'group' => 'fancy', 'order' => '98', ], [ 'icon' => 'fa-question-circle', 'name' => esc_html__( 'Custom Captcha', 'wpforms-lite' ), 'keywords' => esc_html__( 'spam, math, maths, question', 'wpforms-lite' ), 'name_en' => 'Custom Captcha', 'type' => 'captcha', 'group' => 'fancy', 'addon' => 'wpforms-captcha', 'order' => '300', ], [ 'icon' => 'fa-pencil', 'name' => esc_html__( 'Signature', 'wpforms-lite' ), 'keywords' => esc_html__( 'user, e-signature', 'wpforms-lite' ), 'name_en' => 'Signature', 'type' => 'signature', 'group' => 'fancy', 'addon' => 'wpforms-signatures', 'order' => '200', ], [ 'icon' => 'fa-ellipsis-h', 'name' => esc_html__( 'Likert Scale', 'wpforms-lite' ), 'keywords' => esc_html__( 'survey, rating scale', 'wpforms-lite' ), 'name_en' => 'Likert Scale', 'type' => 'likert_scale', 'group' => 'fancy', 'addon' => 'wpforms-surveys-polls', 'order' => '400', ], [ 'icon' => 'fa-tachometer', 'name' => esc_html__( 'Net Promoter Score', 'wpforms-lite' ), 'keywords' => esc_html__( 'survey, nps', 'wpforms-lite' ), 'name_en' => 'Net Promoter Score', 'type' => 'net_promoter_score', 'group' => 'fancy', 'addon' => 'wpforms-surveys-polls', 'order' => '410', ], [ 'icon' => 'fa-credit-card', 'name' => esc_html__( 'PayPal Commerce', 'wpforms-lite' ), 'keywords' => esc_html__( 'store, ecommerce, credit card, pay, payment, debit card', 'wpforms-lite' ), 'name_en' => 'PayPal Commerce', 'type' => 'paypal-commerce', 'group' => 'payment', 'addon' => 'wpforms-paypal-commerce', 'order' => '89', ], [ 'icon' => 'fa-credit-card', 'name' => esc_html__( 'Authorize.Net', 'wpforms-lite' ), 'keywords' => esc_html__( 'store, ecommerce, credit card, pay, payment, debit card', 'wpforms-lite' ), 'name_en' => 'Authorize.Net', 'type' => 'authorize_net', 'group' => 'payment', 'addon' => 'wpforms-authorize-net', 'order' => '95', ], [ 'icon' => 'fa-ticket', 'name' => esc_html__( 'Coupon', 'wpforms-lite' ), 'keywords' => esc_html__( 'discount, sale', 'wpforms-lite' ), 'name_en' => 'Coupon', 'type' => 'payment-coupon', 'group' => 'payment', 'addon' => 'wpforms-coupons', 'order' => '100', ], ]; $captcha = $this->get_captcha(); if ( ! empty( $captcha ) ) { $this->fields[] = $captcha; } return $this->fields; } /** * Get Captcha field data. * * @since 1.6.6 * * @return array Captcha field data. */ private function get_captcha(): array { $captcha_settings = wpforms_get_captcha_settings(); if ( empty( $captcha_settings['provider'] ) ) { return []; } $captcha = [ 'hcaptcha' => [ 'name' => 'hCaptcha', 'icon' => 'fa-question-circle-o', ], 'recaptcha' => [ 'name' => 'reCAPTCHA', 'icon' => 'fa-google', ], 'turnstile' => [ 'name' => 'Turnstile', 'icon' => 'fa-question-circle-o', ], ]; if ( ! empty( $captcha_settings['site_key'] ) || ! empty( $captcha_settings['secret_key'] ) ) { $captcha_name = $captcha[ $captcha_settings['provider'] ]['name']; $captcha_icon = $captcha[ $captcha_settings['provider'] ]['icon']; } else { $captcha_name = 'CAPTCHA'; $captcha_icon = 'fa-question-circle-o'; } return [ 'icon' => $captcha_icon, 'name' => $captcha_name, 'name_en' => $captcha_name, 'keywords' => esc_html__( 'captcha, spam, antispam', 'wpforms-lite' ), 'type' => 'captcha_' . $captcha_settings['provider'], 'group' => 'standard', 'order' => 180, 'class' => 'not-draggable', ]; } /** * Get filtered fields data. * * Usage: * get_filtered( [ 'group' => 'payment' ] ) - fields from the 'payment' group. * get_filtered( [ 'addon' => 'surveys-polls' ] ) - fields of the addon 'surveys-polls'. * get_filtered( [ 'type' => 'payment-total' ] ) - field 'payment-total'. * * @since 1.6.6 * * @param array $args Arguments array. * * @return array Fields data filtered according to given arguments. */ private function get_filtered( array $args = [] ): array { $default_args = [ 'group' => '', 'addon' => '', 'type' => '', ]; $args = array_filter( wp_parse_args( $args, $default_args ) ); $fields = $this->get_all(); $filtered_fields = []; foreach ( $args as $prop => $prop_val ) { foreach ( $fields as $field ) { if ( ! empty( $field[ $prop ] ) && $field[ $prop ] === $prop_val ) { $filtered_fields[] = $field; } } } return $filtered_fields; } /** * Get fields by group. * * @since 1.6.6 * * @param string $group Fields group (standard, fancy or payment). * * @return array. */ public function get_by_group( string $group ): array { return $this->get_filtered( [ 'group' => $group ] ); } /** * Get fields by addon. * * @since 1.6.6 * * @param string $addon Addon slug. * * @return array. */ public function get_by_addon( string $addon ): array { return $this->get_filtered( [ 'addon' => $addon ] ); } /** * Get field by type. * * @since 1.6.6 * * @param string $type Field type. * * @return array Single field data. Empty array if field is not available. */ public function get_field( string $type ): array { $fields = $this->get_filtered( [ 'type' => $type ] ); return ! empty( $fields[0] ) ? $fields[0] : []; } /** * Set key value of each field (conditionally). * * @since 1.6.6 * * @param array $fields Fields data. * @param string $key Key. * @param string $value Value. * @param string $condition Condition. * * @return array Updated field data. */ public function set_values( array $fields, string $key, string $value, string $condition ): array { if ( empty( $fields ) || empty( $key ) ) { return $fields; } foreach ( $fields as $f => $field ) { switch ( $condition ) { case 'empty': $fields[ $f ][ $key ] = empty( $field[ $key ] ) ? $value : $field[ $key ]; break; default: $fields[ $f ][ $key ] = $value; } } return $fields; } } Admin/Education/Admin/Tools/EntryAutomation.php 0000644 00000010437 15174710275 0015530 0 ustar 00 <?php namespace WPForms\Admin\Education\Admin\Tools; /** * Entry Automation Education class. * * @since 1.9.6.1 */ class EntryAutomation { /** * Education init. * * @since 1.9.6.1 */ public function init(): void { $this->hooks(); } /** * Load hooks. * * @since 1.9.6.1 */ private function hooks(): void { add_action( 'wpforms_admin_tools_views_entry_automation_display', [ $this, 'display' ] ); } /** * Get the template data. * * @since 1.9.6.1 * * @return array */ private function get_template_data(): array { $images_url = WPFORMS_PLUGIN_URL . 'assets/images/entry-automation/'; $utm_medium = 'Tools - Entry Automation'; $utm_content = 'Entry Automation Addon'; $addon = wpforms()->obj( 'addons' )->get_addon( 'entry-automation' ); $upgrade_link = $addon['action'] === 'upgrade' ? sprintf( /* translators: %1$s - WPForms.com Upgrade page URL. */ ' <strong><a href="%1$s" target="_blank" rel="noopener noreferrer" class="wpforms-upgrade-link">%2$s</a></strong>', esc_url( wpforms_admin_upgrade_link( $utm_medium, $utm_content ) ), esc_html__( 'Upgrade to WPForms Elite', 'wpforms-lite' ) ) : ''; $params = [ 'features' => [ __( 'Automated Task Scheduling', 'wpforms-lite' ), __( 'Scheduled Exports', 'wpforms-lite' ), __( 'Task Chaining', 'wpforms-lite' ), __( 'Automated Deletions', 'wpforms-lite' ), __( 'Enhanced Data Management', 'wpforms-lite' ), __( 'Robust Failsafes', 'wpforms-lite' ), ], 'images' => [ [ 'url' => $images_url . 'education.png', 'url2x' => $images_url . 'education.png', 'title' => '', ], ], 'utm_medium' => $utm_medium, 'utm_content' => $utm_content, 'upgrade_link_text' => esc_html__( 'Upgrade to WPForms Elite', 'wpforms-lite' ), 'heading_title' => __( 'Tired of manually exporting and deleting entries? Wish you could schedule these actions for optimal efficiency?', 'wpforms-lite' ), /* translators: %1$s - WPForms.com Upgrade page URL. */ 'heading_description' => '<p>' . esc_html__( 'Entry Automation introduces powerful, automated task chaining, allowing you to seamlessly schedule exports and deletions, ensuring your data is managed precisely how you need it. Chain multiple tasks together for complex workflows – export to CSV, then automatically delete after a specified period, for example. We\'ve built robust failsafes to guarantee data integrity, so you can automate with confidence, knowing your valuable entries are always protected. Take back your time and let our addon handle the heavy lifting, keeping your WPForms data organized and secure, automatically.', 'wpforms-lite' ) . '</p>' . '<p>' . wp_kses( $upgrade_link, [ 'a' => [ 'href' => [], 'rel' => [], 'target' => [], 'class' => [], ], 'strong' => [], ] ) . '</p>', 'features_description' => __( 'Powerful Automation Features', 'wpforms-lite' ), ]; return isset( $addon ) ? array_merge( $params, $addon ) : $params; } /** * Check if the addon is active. * * @since 1.9.6.1 * * @return bool */ private function is_addon_active(): bool { /** * Check if the addon is active. * * @since 1.9.6.1 * * @param bool $is_active Whether the addon is active. */ return (bool) apply_filters( 'wpforms_admin_education_admin_tools_entry_automation_is_addon_active', wpforms()->obj( 'addons' )->is_active( 'entry-automation' ) ); } /** * Display education content. * * @since 1.9.6.1 */ public function display(): void { // Display the education content only if the addon is not active. if ( $this->is_addon_active() ) { return; } $this->enqueue(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo wpforms_render( 'education/admin/page', $this->get_template_data(), true ); } /** * Enqueue scripts and styles. * * @since 1.9.6.1 */ private function enqueue(): void { // Lity. wp_enqueue_style( 'wpforms-lity', WPFORMS_PLUGIN_URL . 'assets/lib/lity/lity.min.css', null, '3.0.0' ); wp_enqueue_script( 'wpforms-lity', WPFORMS_PLUGIN_URL . 'assets/lib/lity/lity.min.js', [ 'jquery' ], '3.0.0', true ); } } Admin/Education/Admin/EditPost.php 0000644 00000013657 15174710275 0013030 0 ustar 00 <?php namespace WPForms\Admin\Education\Admin; use WP_Post; use WPForms\Admin\Education\EducationInterface; /** * Admin/EditPost Education feature. * * @since 1.8.1 */ class EditPost implements EducationInterface { /** * Determine if the website has some forms. * * @since 1.8.1 * * @var bool */ private $has_forms; /** * Indicate if edit post education is allowed to load. * * @since 1.8.1 * * @return bool */ public function allow_load() { if ( ! is_admin() ) { return false; } if ( ! wpforms_current_user_can( 'view_forms' ) ) { return false; } // Skip it if it's the Challenge flow. if ( wpforms()->obj( 'challenge' )->is_form_embed_page() ) { return false; } $form_embed_wizard = wpforms()->obj( 'form_embed_wizard' ); // Skip it if it's the Form Embed Wizard flow. if ( $form_embed_wizard->is_form_embed_page( 'edit' ) && $form_embed_wizard->get_meta() ) { return false; } $user_id = get_current_user_id(); $dismissed = get_user_meta( $user_id, 'wpforms_dismissed', true ); return empty( $dismissed['edu-edit-post-notice'] ); } /** * Initialize. * * @since 1.8.1 */ public function init() { if ( ! $this->allow_load() ) { return; } // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.SuppressFilters_suppress_filters $this->has_forms = (bool) wpforms()->obj( 'form' )->get( '', [ 'numberposts' => 1, 'nopaging' => false, 'fields' => 'ids', 'no_found_rows' => true, 'update_post_meta_cache' => false, 'update_post_term_cache' => false, 'suppress_filters' => true, // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.SuppressFilters_suppress_filters ] ); $this->hooks(); } /** * Add hooks. * * @since 1.8.1 */ private function hooks() { add_action( 'edit_form_after_title', [ $this, 'classic_editor_notice' ] ); add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_styles' ] ); add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_scripts' ] ); } /** * Is gutenberg Editor. * * @since 1.8.1 * * @return bool */ private function is_gutenberg_editor() { return (bool) get_current_screen()->is_block_editor(); } /** * Enqueue styles. * * @since 1.8.1 */ public function enqueue_styles() { $min = wpforms_get_min_suffix(); wp_enqueue_style( 'wpforms-edit-post-education', WPFORMS_PLUGIN_URL . "assets/css/admin/edit-post-education{$min}.css", [], WPFORMS_VERSION ); } /** * Enqueue scripts. * * @since 1.8.1 */ public function enqueue_scripts() { $min = wpforms_get_min_suffix(); wp_enqueue_script( 'wpforms-edit-post-education', WPFORMS_PLUGIN_URL . "assets/js/admin/education/edit-post.es5{$min}.js", [ 'jquery', 'underscore' ], WPFORMS_VERSION, true ); $strings = [ 'ajax_url' => admin_url( 'admin-ajax.php' ), 'education_nonce' => wp_create_nonce( 'wpforms-education' ), ]; if ( $this->is_gutenberg_editor() ) { $strings = array_merge( $strings, $this->get_gutenberg_strings() ); } wp_localize_script( 'wpforms-edit-post-education', 'wpforms_edit_post_education', $strings ); } /** * Get Gutenberg i18n strings. * * @since 1.8.1 * * @return array */ private function get_gutenberg_strings() { $strings = [ 'is_gutenberg' => true, 'gutenberg_notice' => [ 'template' => $this->get_gutenberg_notice_template(), 'button' => __( 'Get Started', 'wpforms-lite' ), ], ]; if ( ! $this->has_forms ) { $strings['gutenberg_notice']['url'] = add_query_arg( 'page', 'wpforms-overview', admin_url( 'admin.php' ) ); return $strings; } $strings['gutenberg_guide'] = [ [ 'image' => WPFORMS_PLUGIN_URL . '/assets/images/edit-post-education-page-1.png', 'title' => __( 'Easily add your contact form', 'wpforms-lite' ), 'content' => __( 'Oh hey, it looks like you\'re working on a contact page. Don\'t forget to embed your contact form. Click the plus icon above and search for WPForms.', 'wpforms-lite' ), ], [ 'image' => WPFORMS_PLUGIN_URL . '/assets/images/edit-post-education-page-2.png', 'title' => __( 'Embed your form', 'wpforms-lite' ), 'content' => __( 'Then click on the WPForms block to embed your desired contact form.', 'wpforms-lite' ), ], ]; return $strings; } /** * Add notice to classic editor. * * @since 1.8.1 * * @param WP_Post $post Add notice to classic editor. */ public function classic_editor_notice( $post ) { $message = $this->has_forms ? __( 'Don\'t forget to embed your contact form. Simply click the Add Form button below.', 'wpforms-lite' ) : sprintf( /* translators: %1$s - link to create a new form. */ __( 'Did you know that with <a href="%1$s" target="_blank" rel="noopener noreferrer">WPForms</a>, you can create an easy-to-use contact form in a matter of minutes?', 'wpforms-lite' ), esc_url( add_query_arg( 'page', 'wpforms-overview', admin_url( 'admin.php' ) ) ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo wpforms_render( 'education/admin/edit-post/classic-notice', [ 'message' => $message, ], true ); } /** * Get Gutenberg notice template. * * @since 1.8.1 * * @return string */ private function get_gutenberg_notice_template() { $message = $this->has_forms ? __( 'You\'ve already created a form, now add it to the page so your customers can get in touch.', 'wpforms-lite' ) : sprintf( /* translators: %1$s - link to create a new form. */ __( 'Did you know that with <a href="%1$s" target="_blank" rel="noopener noreferrer">WPForms</a>, you can create an easy-to-use contact form in a matter of minutes?', 'wpforms-lite' ), esc_url( add_query_arg( 'page', 'wpforms-overview', admin_url( 'admin.php' ) ) ) ); return wpforms_render( 'education/admin/edit-post/notice', [ 'message' => $message, ], true ); } } Admin/Education/Admin/Settings/Integrations.php 0000644 00000002555 15174710275 0015536 0 ustar 00 <?php namespace WPForms\Admin\Education\Admin\Settings; use \WPForms\Admin\Education\AddonsListBase; /** * Base class for Admin/Integrations feature for Lite and Pro. * * @since 1.6.6 */ class Integrations extends AddonsListBase { /** * Template for rendering single addon item. * * @since 1.6.6 * * @var string */ protected $single_addon_template = 'education/admin/settings/integrations-item'; /** * Hooks. * * @since 1.6.6 */ public function hooks() { add_action( 'wpforms_settings_providers', [ $this, 'filter_addons' ], 1 ); add_action( 'wpforms_settings_providers', [ $this, 'display_addons' ], 500 ); } /** * Indicate if current Education feature is allowed to load. * * @since 1.6.6 * * @return bool */ public function allow_load() { return wpforms_is_admin_page( 'settings', 'integrations' ); } /** * Get addons for the Settings/Integrations tab. * * @since 1.6.6 * * @return array Addons data. */ protected function get_addons() { return $this->addons->get_by_path( 'settings_integrations.category', 'crm|email-marketing|integration' ); } /** * Ensure that we do not display activated addon items if those addons are not allowed according to the current license. * * @since 1.6.6 */ public function filter_addons() { $this->filter_not_allowed_addons( 'wpforms_settings_providers' ); } } Admin/Education/Admin/Settings/SMTP.php 0000644 00000002217 15174710275 0013646 0 ustar 00 <?php namespace WPForms\Admin\Education\Admin\Settings; use WPForms\Admin\Education\EducationInterface; /** * SMTP education notice. * * @since 1.8.1 */ class SMTP implements EducationInterface { /** * Indicate if Education core is allowed to load. * * @since 1.8.1 * * @return bool */ public function allow_load() { if ( ! wpforms_can_install( 'plugin' ) || ! wpforms_can_activate( 'plugin' ) ) { return false; } $user_id = get_current_user_id(); $dismissed = get_user_meta( $user_id, 'wpforms_dismissed', true ); if ( ! empty( $dismissed['edu-smtp-notice'] ) ) { return false; } $active_plugins = get_option( 'active_plugins', [] ); $allowed_plugins = [ 'wp-mail-smtp/wp_mail_smtp.php', 'wp-mail-smtp-pro/wp_mail_smtp.php', ]; return ! array_intersect( $active_plugins, $allowed_plugins ); } /** * Init. * * @since 1.8.1 */ public function init() { } /** * Get notice template. * * @since 1.8.1 * * @return string */ public function get_template() { if ( ! $this->allow_load() ) { return ''; } return wpforms_render( 'education/admin/settings/smtp-notice' ); } } Admin/Education/Admin/Settings/Geolocation.php 0000644 00000007562 15174710275 0015336 0 ustar 00 <?php namespace WPForms\Admin\Education\Admin\Settings; use WPForms\Admin\Education\AddonsItemBase; /** * Admin/Settings/Geolocation Education feature for Lite and Pro. * * @since 1.6.6 */ class Geolocation extends AddonsItemBase { /** * Slug. * * @since 1.6.6 */ const SLUG = 'geolocation'; /** * Hooks. * * @since 1.6.6 */ public function hooks() { add_action( 'admin_enqueue_scripts', [ $this, 'enqueues' ] ); add_filter( 'wpforms_settings_defaults', [ $this, 'add_sections' ] ); } /** * Indicate if current Education feature is allowed to load. * * @since 1.6.6 * * @return bool */ public function allow_load() { return wpforms_is_admin_page( 'settings', 'geolocation' ); } /** * Enqueues. * * @since 1.6.6 */ public function enqueues() { // Lity - lightbox for images. wp_enqueue_style( 'wpforms-lity', WPFORMS_PLUGIN_URL . 'assets/lib/lity/lity.min.css', null, '3.0.0' ); wp_enqueue_script( 'wpforms-lity', WPFORMS_PLUGIN_URL . 'assets/lib/lity/lity.min.js', [ 'jquery' ], '3.0.0', true ); } /** * Preview of education features for customers with not enough permissions. * * @since 1.6.6 * * @param array $settings Settings sections. * * @return array */ public function add_sections( $settings ) { $addon = $this->addons->get_addon( 'geolocation' ); if ( empty( $addon ) || empty( $addon['status'] ) || empty( $addon['action'] ) ) { return $settings; } $settings[ self::SLUG ][ self::SLUG . '-page' ] = [ 'id' => self::SLUG . '-page', 'content' => wpforms_render( 'education/admin/page', $this->template_data(), true ), 'type' => 'content', 'no_label' => true, 'class' => [ 'wpforms-education-container-page' ], ]; return $settings; } /** * Get the template data. * * @since 1.8.6 * * @return array */ private function template_data(): array { $addon = $this->addons->get_addon( 'geolocation' ); $images_url = WPFORMS_PLUGIN_URL . 'assets/images/geolocation-education/'; $params = [ 'features' => [ __( 'City', 'wpforms-lite' ), __( 'Latitude/Longitude', 'wpforms-lite' ), __( 'Google Places API', 'wpforms-lite' ), __( 'Country', 'wpforms-lite' ), __( 'Address Autocomplete', 'wpforms-lite' ), __( 'Mapbox API', 'wpforms-lite' ), __( 'Postal/Zip Code', 'wpforms-lite' ), __( 'Embedded Map in Forms', 'wpforms-lite' ), ], 'images' => [ [ 'url' => $images_url . 'entry-location.jpg', 'url2x' => $images_url . 'entry-location@2x.jpg', 'title' => __( 'Location Info in Entries', 'wpforms-lite' ), ], [ 'url' => $images_url . 'address-autocomplete.jpg', 'url2x' => $images_url . 'address-autocomplete@2x.jpg', 'title' => __( 'Address Autocomplete Field', 'wpforms-lite' ), ], [ 'url' => $images_url . 'smart-address-field.jpg', 'url2x' => $images_url . 'smart-address-field@2x.jpg', 'title' => __( 'Smart Address Field', 'wpforms-lite' ), ], ], 'utm_medium' => 'Settings - Geolocation', 'utm_content' => 'Geolocation Addon', 'heading_title' => __( 'Geolocation', 'wpforms-lite' ), 'heading_description' => sprintf( '<p>%1$s</p>', __( 'Do you want to learn more about visitors who fill out your online forms? Our geolocation addon allows you to collect and store your website visitors geolocation data along with their form submission. This insight can help you to be better informed and turn more leads into customers. Furthermore, add a smart address field that autocompletes using the Google Places API.', 'wpforms-lite' ) ), 'badge' => __( 'Pro', 'wpforms-lite' ), 'features_description' => __( 'Powerful location-based insights and features…', 'wpforms-lite' ), ]; return array_merge( $params, $addon ); } } Admin/Revisions.php 0000644 00000027525 15174710275 0010312 0 ustar 00 <?php namespace WPForms\Admin; use WP_Post; /** * Form Revisions. * * @since 1.7.3 */ class Revisions { /** * Current Form Builder panel view. * * @since 1.7.3 * * @var string */ private $view = 'revisions'; /** * Current Form ID. * * @since 1.7.3 * * @var int|false */ private $form_id = false; /** * Current Form. * * @since 1.7.3 * * @var WP_Post|null */ private $form; /** * Current Form Revision ID. * * @since 1.7.3 * * @var int|false */ private $revision_id = false; /** * Current Form Revision. * * @since 1.7.3 * * @var WP_Post|null */ private $revision; /** * Whether revisions panel was already viewed by the user at least once. * * @since 1.7.3 * * @var bool */ private $viewed; /** * Initialize the class if preconditions are met. * * @since 1.7.3 * * @return void */ public function init() { if ( ! $this->allow_load() ) { return; } // phpcs:disable WordPress.Security.NonceVerification.Recommended if ( isset( $_REQUEST['view'] ) ) { $this->view = sanitize_key( $_REQUEST['view'] ); } if ( isset( $_REQUEST['revision_id'] ) ) { $this->revision_id = absint( $_REQUEST['revision_id'] ); } // phpcs:enable WordPress.Security.NonceVerification.Recommended if ( ! $this->can_access_form() ) { return; } if ( $this->revision_id && wp_revisions_enabled( $this->form ) ) { $this->revision = wp_get_post_revision( $this->revision_id ); // Bail if we don't have a valid revision. if ( ! $this->revision instanceof WP_Post || $this->revision->post_parent !== $this->form_id || $this->revision->ID !== $this->revision_id ) { return; } } $this->hooks(); } /** * Whether it is allowed to load under certain conditions. * * - numeric, non-zero form ID provided, * - the form with this ID exists and was successfully fetched, * - we're in the Form Builder or processing an ajax request. * * @since 1.7.3 * * @return bool */ private function allow_load() { if ( ! ( wpforms_is_admin_page( 'builder' ) || wp_doing_ajax() ) ) { return false; } // phpcs:disable WordPress.Security.NonceVerification.Recommended $id = wp_doing_ajax() && isset( $_REQUEST['id'] ) ? absint( $_REQUEST['id'] ) : false; $id = isset( $_REQUEST['form_id'] ) && ! is_array( $_REQUEST['form_id'] ) ? absint( $_REQUEST['form_id'] ) : $id; // phpcs:enable WordPress.Security.NonceVerification.Recommended $this->form_id = $id; $form_handler = wpforms()->obj( 'form' ); if ( ! $form_handler ) { return false; } $this->form = $form_handler->get( $this->form_id ); return $this->form_id && $this->form instanceof WP_Post; } /** * Hook into WordPress lifecycle. * * @since 1.7.3 */ private function hooks() { // Restore a revision. The `admin_init` action has already fired, `current_screen` fires before headers are sent. add_action( 'current_screen', [ $this, 'process_restore' ] ); // Refresh a rendered list of revisions on the frontend. add_action( 'wp_ajax_wpforms_get_form_revisions', [ $this, 'fetch_revisions_list' ] ); // Mark Revisions panel as viewed when viewed for the first time. Hides the error badge. add_action( 'wp_ajax_wpforms_mark_panel_viewed', [ $this, 'mark_panel_viewed' ] ); // Back-compat for forms created with revisions disabled. add_action( 'wpforms_builder_init', [ $this, 'maybe_create_initial_revision' ] ); // Pass localized strings to frontend. add_filter( 'wpforms_builder_strings', [ $this, 'get_localized_strings' ], 10, 2 ); } /** * Get current revision, if available. * * @since 1.7.3 * * @return WP_Post|null */ public function get_revision() { return $this->revision; } /** * Get formatted date or time. * * @since 1.7.3 * * @param string $datetime UTC datetime from the post object. * @param string $part What to return - date or time, defaults to date. * * @return string */ public function get_formatted_datetime( $datetime, $part = 'date' ) { if ( $part === 'time' ) { return wpforms_time_format( $datetime, '', true ); } // M j format needs to keep one-line date. return wpforms_date_format( $datetime, 'M j', true ); } /** * Get admin (Form Builder) base URL with additional query args. * * @since 1.7.3 * * @param array $query_args Additional query args to append to the base URL. * * @return string */ public function get_url( $query_args = [] ) { $defaults = [ 'page' => 'wpforms-builder', 'view' => $this->view, 'form_id' => $this->form_id, ]; return add_query_arg( wp_parse_args( $query_args, $defaults ), admin_url( 'admin.php' ) ); } /** * Determine if Revisions panel was previously viewed by current user. * * @since 1.7.3 * * @return bool */ public function panel_viewed() { if ( $this->viewed === null ) { $this->viewed = (bool) get_user_meta( get_current_user_id(), 'wpforms_revisions_disabled_notice_dismissed', true ); } return $this->viewed; } /** * Mark Revisions panel as viewed by current user. * * @since 1.7.3 */ public function mark_panel_viewed() { // Run a security check. check_ajax_referer( 'wpforms-builder', 'nonce' ); if ( ! $this->panel_viewed() ) { $this->viewed = update_user_meta( get_current_user_id(), 'wpforms_revisions_disabled_notice_dismissed', true ); } wp_send_json_success( [ 'updated' => $this->viewed ] ); } /** * Get a rendered list of all revisions. * * @since 1.7.3 * * @return string */ public function render_revisions_list() { return wpforms_render( 'builder/revisions/list', $this->prepare_template_render_arguments(), true ); } /** * Prepare all arguments for the template to be rendered. * * Note: All data is escaped in the template. * * @since 1.7.3 * * @return array */ private function prepare_template_render_arguments() { $args = [ 'active_class' => $this->revision ? '' : ' active', 'current_version_url' => $this->get_url(), 'author_id' => $this->form->post_author, 'revisions' => [], 'show_avatars' => get_option( 'show_avatars' ), ]; $revisions = wp_get_post_revisions( $this->form_id ); if ( empty( $revisions ) ) { return $args; } // WordPress always orders entries by `post_date` column, which contains a date and time in site's timezone configured in settings. // This setting is per site, not per user, and it's not expected to be changed. However, if it was changed for whatever reason, // the order of revisions will be incorrect. This is definitely an edge case, but we can prevent this from ever happening // by sorting the results using `post_date_gmt` or `post_modified_gmt`, which contains UTC date and never changes. uasort( $revisions, static function ( $a, $b ) { return strtotime( $a->post_modified_gmt ) > strtotime( $b->post_modified_gmt ) ? -1 : 1; } ); // The first revision is always identical to the current version and should not be displayed in the list. $current_revision = array_shift( $revisions ); // Display the author of current version instead of a form author. $args['author_id'] = $current_revision->post_author; foreach ( $revisions as $revision ) { $time_diff = sprintf( /* translators: %s - relative time difference, e.g. "5 minutes", "12 days". */ __( '%s ago', 'wpforms-lite' ), human_time_diff( strtotime( $revision->post_modified_gmt . ' +0000' ) ) ); $date_time = sprintf( /* translators: %1$s - date, %2$s - time when item was created, e.g. "Oct 22 at 11:11am". */ __( '%1$s at %2$s', 'wpforms-lite' ), $this->get_formatted_datetime( $revision->post_modified_gmt ), $this->get_formatted_datetime( $revision->post_modified_gmt, 'time' ) ); $args['revisions'][] = [ 'active_class' => $this->revision && $this->revision->ID === $revision->ID ? ' active' : '', 'url' => $this->get_url( [ 'revision_id' => $revision->ID, ] ), 'author_id' => $revision->post_author, 'time_diff' => $time_diff, 'date_time' => $date_time, ]; } return $args; } /** * Fetch a list of revisions via ajax. * * @since 1.7.3 */ public function fetch_revisions_list() { // Run a security check. check_ajax_referer( 'wpforms-builder', 'nonce' ); wp_send_json_success( [ 'html' => $this->render_revisions_list(), ] ); } /** * Restore the revision (if needed) and reload the Form Builder. * * @since 1.7.3 * * @return void */ public function process_restore() { $is_restore_request = isset( $_GET['action'] ) && $_GET['action'] === 'restore_revision'; // Bail early. if ( ! $is_restore_request || ! $this->form_id || ! $this->form || ! $this->revision_id || ! $this->revision || ! check_admin_referer( 'restore_revision', 'wpforms_nonce' ) ) { return; } if ( ! $this->can_access_form() ) { wp_die( esc_html__( 'You do not have permission to restore revisions for this form.', 'wpforms-lite' ) ); } if ( ! $this->revision instanceof WP_Post || $this->revision->post_parent !== $this->form_id || $this->revision->ID !== $this->revision_id ) { wp_die( esc_html__( 'Invalid revision. The revision does not belong to this form.', 'wpforms-lite' ) ); } $restored_id = wp_restore_post_revision( $this->revision ); if ( $restored_id ) { wp_safe_redirect( wpforms()->obj( 'revisions' )->get_url( [ 'form_id' => $restored_id, ] ) ); exit; } } /** * Create initial revision for existing form. * * When a new form is created with revisions enabled, WordPress immediately creates first revision which is identical to the form. But when * a form was created with revisions disabled, this initial revision does not exist. Revisions are saved after post update, so modifying * a form that have no initial revision will update the post first, then a revision of this updated post will be saved. The version of * the form that existed before this update is now gone. To avoid losing this pre-revisions state, we create this initial revision * when the Form Builder loads, if needed. * * @since 1.7.3 * * @return void */ public function maybe_create_initial_revision() { // On new form creation there's no revisions yet, bail. Also, when revisions are disabled. // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( isset( $_GET['newform'] ) || ! wp_revisions_enabled( $this->form ) ) { return; } $revisions = wp_get_post_revisions( $this->form_id, [ 'fields' => 'ids', 'numberposts' => 1, ] ); if ( $revisions ) { return; } $initial_revision_id = wp_save_post_revision( $this->form_id ); $initial_revision = wp_get_post_revision( $initial_revision_id ); // Initial revision should belong to the author of the original form. if ( $initial_revision->post_author !== $this->form->post_author ) { wp_update_post( [ 'ID' => $initial_revision_id, 'post_author' => $this->form->post_author, ] ); } } /** * Pass localized strings to frontend. * * @since 1.7.3 * * @param array $strings All strings that will be passed to frontend. * @param WP_Post $form Current form object. * * @return array */ public function get_localized_strings( $strings, $form ) { $strings['revision_update_confirm'] = esc_html__( 'You’re about to save a form revision. Continuing will make this the current version.', 'wpforms-lite' ); return $strings; } /** * Check if the current user has permission to access the form and its revisions. * * @since 1.9.5 * * @return bool */ private function can_access_form(): bool { if ( ! wpforms_current_user_can( 'view_form_single', $this->form_id ) ) { return false; } if ( ! wpforms_current_user_can( 'edit_form_single', $this->form_id ) ) { return false; } return true; } } Admin/Builder/TemplateSingleCache.php 0000644 00000012602 15174710275 0013546 0 ustar 00 <?php namespace WPForms\Admin\Builder; use WPForms\Helpers\CacheBase; use WPForms\Tasks\Actions\AsyncRequestTask; /** * Single template cache handler. * * @since 1.6.8 */ class TemplateSingleCache extends CacheBase { /** * Template Id (hash). * * @since 1.6.8 * * @var string */ private $id; /** * License data (`key` and `type`). * * @since 1.6.8 * * @var array */ private $license; /** * Determine if the class is allowed to load. * * @since 1.6.8 * * @return bool */ protected function allow_load() { $has_permissions = wpforms_current_user_can( [ 'create_forms', 'edit_forms' ] ); $allowed_requests = wpforms_is_admin_ajax() || wpforms_is_admin_page( 'builder' ) || wpforms_is_admin_page( 'templates' ); $allow = wp_doing_cron() || wpforms_doing_wp_cli() || ( $has_permissions && $allowed_requests ); // phpcs:disable WPForms.PHP.ValidateHooks.InvalidHookName /** * Whether to allow to load this class. * * @since 1.7.2 * * @param bool $allow True or false. */ return (bool) apply_filters( 'wpforms_admin_builder_templatesinglecache_allow_load', $allow ); // phpcs:enable WPForms.PHP.ValidateHooks.InvalidHookName } /** * Re-initialize object with the particular template. * * @since 1.6.8 * * @param string $template_id Template ID (hash). * @param array $license License data. * * @return TemplateSingleCache */ public function instance( $template_id, $license ) { $this->id = $template_id; $this->license = $license; $this->init(); return $this; } /** * Provide settings. * * @since 1.6.8 * * @return array Settings array. */ protected function setup() { return [ // Remote source URL. 'remote_source' => $this->remote_source(), // Cache file. 'cache_file' => $this->get_cache_file_name(), // This filter is documented in wpforms/src/Admin/Builder/TemplatesCache.php. // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName, WPForms.Comments.PHPDocHooks.RequiredHookDocumentation 'cache_ttl' => (int) apply_filters( 'wpforms_admin_builder_templates_cache_ttl', WEEK_IN_SECONDS ), ]; } /** * Generate single template remote URL. * * @since 1.6.8 * * @param bool $cache True if the cache arg should be appended to the URL. * * @return string */ private function remote_source( $cache = false ) { if ( ! isset( $this->license['key'] ) ) { return ''; } $args = [ 'license' => $this->license['key'], 'site' => site_url(), ]; if ( $cache ) { $args['cache'] = 1; } return add_query_arg( $args, 'https://wpforms.com/templates/api/get/' . $this->id ); } /** * Get cached data. * * @since 1.8.2 * * @return array Cached data. */ public function get() { $data = parent::get(); if ( ! $this->updated ) { $this->update_usage_tracking(); } return $data; } /** * Sends a request to update the form template usage tracking database. * * @since 1.7.5 */ private function update_usage_tracking() { $tasks = wpforms()->obj( 'tasks' ); if ( ! $tasks ) { return; } $url = $this->remote_source( true ); $args = [ 'blocking' => false ]; $tasks->create( AsyncRequestTask::ACTION )->async()->params( $url, $args )->register(); } /** * Get cache directory path. * * @since 1.6.8 */ protected function get_cache_dir() { return parent::get_cache_dir() . 'templates/'; } /** * Generate single template cache file name. * * @since 1.6.8 * * @return string. */ private function get_cache_file_name() { return sanitize_key( $this->id ) . '.json'; } /** * Prepare data to store in a local cache. * * @since 1.6.8 * * @param array $data Raw data received by the remote request. * * @return array Prepared data for caching. */ protected function prepare_cache_data( $data ): array { if ( empty( $data ) || ! is_array( $data ) || empty( $data['status'] ) || $data['status'] !== 'success' || empty( $data['data'] ) ) { return []; } $cache_data = $data['data']; $cache_data['data'] = empty( $cache_data['data'] ) ? [] : $cache_data['data']; $cache_data['data']['settings'] = empty( $cache_data['data']['settings'] ) ? [] : $cache_data['data']['settings']; $cache_data['data']['settings']['ajax_submit'] = '1'; // Strip the word "Template" from the end of the template name and form title setting. $cache_data['name'] = preg_replace( '/\sTemplate$/', '', $cache_data['name'] ); $cache_data['data']['settings']['form_title'] = $cache_data['name']; // Unset `From Name` field of the notification settings. // By default, the builder will use the `blogname` option value. unset( $cache_data['data']['settings']['notifications'][1]['sender_name'] ); return $cache_data; } /** * Wipe cache of an empty templates. * * @since 1.7.5 */ public function wipe_empty_templates_cache() { $cache_dir = $this->get_cache_dir(); $files = glob( $cache_dir . '*.json' ); foreach ( $files as $filename ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents $content = file_get_contents( $filename ); if ( empty( $content ) || trim( $content ) === '[]' ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink unlink( $filename ); } } } } Admin/Builder/TemplatesCache.php 0000644 00000013240 15174710275 0012566 0 ustar 00 <?php namespace WPForms\Admin\Builder; use WPForms\Helpers\CacheBase; use WPForms\Helpers\File; /** * Form templates cache handler. * * @since 1.6.8 */ class TemplatesCache extends CacheBase { /** * Templates list content cache files. * * @since 1.8.6 * * @var array */ const CONTENT_CACHE_FILES = [ 'admin-page' => 'templates-admin-page.html', 'builder' => 'templates-builder.html', ]; /** * List of plugins that can use the templates cache. * * @since 1.8.7 * * @var array */ const PLUGINS = [ 'wpforms', 'wpforms-lite', ]; /** * Determine if the class is allowed to load. * * @since 1.6.8 * * @return bool */ protected function allow_load(): bool { $has_permissions = wpforms_current_user_can( [ 'create_forms', 'edit_forms' ] ); $allowed_requests = wpforms_is_admin_ajax() || wpforms_is_admin_page( 'builder' ) || wpforms_is_admin_page( 'templates' ) || wpforms_is_admin_page( 'tools', 'action-scheduler' ); $allow = wp_doing_cron() || wpforms_doing_wp_cli() || ( $has_permissions && $allowed_requests ); /** * Whether to load this class. * * @since 1.7.2 * * @param bool $allow True or false. */ return (bool) apply_filters( 'wpforms_admin_builder_templatescache_allow_load', $allow ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName } /** * Initialize the class. * * @since 1.8.7 */ public function init() { // phpcs:ignore WPForms.PHP.HooksMethod.InvalidPlaceForAddingHooks parent::init(); // Upgrade cached templates data after the plugin update. add_action( 'upgrader_process_complete', [ $this, 'upgrade_templates' ] ); } /** * Upgrade cached templates data after the plugin update. * * @since 1.8.7 * * @param object $upgrader WP_Upgrader instance. */ public function upgrade_templates( $upgrader ) { if ( $this->allow_update_cache( $upgrader ) ) { $this->update( true ); } } /** * Determine if allowed to update the cache. * * @since 1.8.7 * * @param object $upgrader WP_Upgrader instance. * * @return bool */ private function allow_update_cache( $upgrader ): bool { $result = $upgrader->result ?? null; // Check if plugin was updated. if ( ! $result ) { return false; } // Check if updated plugin is WPForms. if ( ! in_array( $result['destination_name'], self::PLUGINS, true ) ) { return false; } return true; } /** * Provide settings. * * @since 1.6.8 * * @return array Settings array. */ protected function setup() { return [ // Remote source URL. 'remote_source' => 'https://wpforms.com/templates/api/get/', // Cache file. 'cache_file' => 'templates.json', /** * Time-to-live of the templates cache files in seconds. * * This applies to `uploads/wpforms/cache/templates.json` * and all *.json files in `uploads/wpforms/cache/templates/` directory. * * @since 1.6.8 * * @param integer $cache_ttl Cache time-to-live, in seconds. * Default value: WEEK_IN_SECONDS. */ 'cache_ttl' => (int) apply_filters( 'wpforms_admin_builder_templates_cache_ttl', WEEK_IN_SECONDS ), // Scheduled update action. 'update_action' => 'wpforms_admin_builder_templates_cache_update', ]; } /** * Prepare data to store in a local cache. * * @since 1.6.8 * * @param array $data Raw data received by the remote request. * * @return array Prepared data for caching. */ protected function prepare_cache_data( $data ): array { if ( empty( $data ) || ! is_array( $data ) || empty( $data['status'] ) || $data['status'] !== 'success' || empty( $data['data'] ) ) { return []; } $cache_data = $data['data']; // Strip the word "Template" from the end of each template name. foreach ( $cache_data['templates'] as $slug => $template ) { $cache_data['templates'][ $slug ]['name'] = preg_replace( '/\sTemplate$/', '', $template['name'] ); } return $cache_data; } /** * Update the cache. * * @since 1.8.6 * * @param bool $force Whether to force update the cache. * * @return bool */ public function update( bool $force = false ): bool { $result = parent::update( $force ); if ( ! $result ) { return false; } $this->wipe_content_cache(); return $result; } /** * Get cached templates content. * * @since 1.8.6 * * @return string */ public function get_content_cache(): string { // phpcs:ignore Universal.Operators.DisallowShortTernary.Found return File::get_contents( $this->get_content_cache_file() ) ?: ''; } /** * Save templates content cache. * * @since 1.8.6 * * @param string|mixed $content Templates content. * * @return bool */ public function save_content_cache( $content ): bool { return File::put_contents( $this->get_content_cache_file(), (string) $content ); } /** * Wipe cached templates content. * * @since 1.8.6 */ public function wipe_content_cache() { $cache_dir = $this->get_cache_dir(); // Delete the template content cache files. They will be regenerated on the first visit. foreach ( self::CONTENT_CACHE_FILES as $file ) { $cache_file = $cache_dir . $file; if ( is_file( $cache_file ) && is_readable( $cache_file ) ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink unlink( $cache_file ); } } } /** * Get templates content cache file path. * * @since 1.8.6 * * @return string */ private function get_content_cache_file(): string { $context = wpforms_is_admin_page( 'templates' ) ? 'admin-page' : 'builder'; return File::get_cache_dir() . self::CONTENT_CACHE_FILES[ $context ]; } } Admin/Builder/Help.php 0000644 00000152536 15174710275 0010610 0 ustar 00 <?php namespace WPForms\Admin\Builder; /** * Form Builder Help Screen. * * @since 1.6.3 */ class Help { /** * Docs data. * * @since 1.6.4 * * @var array */ private $docs; /** * Initialize class. * * @since 1.6.3 */ public function init() { // Terminate initialization if not in builder. if ( ! wpforms_current_user_can( [ 'create_forms', 'edit_forms' ] ) || ! wpforms_is_admin_page( 'builder' ) ) { return; } $builder_help_cache = wpforms()->obj( 'builder_help_cache' ); $this->docs = $builder_help_cache ? $builder_help_cache->get() : []; $this->hooks(); } /** * Hooks. * * @since 1.6.3 */ private function hooks() { add_action( 'wpforms_builder_enqueues', [ $this, 'enqueues' ] ); add_action( 'wpforms_admin_page', [ $this, 'output' ], 20 ); } /** * Enqueue assets. * * @since 1.6.3 */ public function enqueues() { $min = wpforms_get_min_suffix(); wp_enqueue_script( 'wpforms-builder-help', WPFORMS_PLUGIN_URL . "assets/js/admin/builder/help{$min}.js", [ 'wpforms-builder' ], WPFORMS_VERSION, true ); wp_localize_script( 'wpforms-builder-help', 'wpforms_builder_help', $this->get_localized_data() ); } /** * Get localized data. * * @since 1.6.3 * * @return array Localized data. */ public function get_localized_data() { return [ 'docs' => $this->docs, 'categories' => $this->get_categories(), 'context' => [ 'terms' => $this->get_context_terms(), 'docs' => $this->get_context_docs(), ], ]; } /** * Get categories. * * @return array Categories data. * @since 1.6.3 * */ public function get_categories() { return [ 'getting-started' => esc_html__( 'Getting Started', 'wpforms-lite' ), 'form-creation' => esc_html__( 'Form Creation', 'wpforms-lite' ), 'entry-management' => esc_html__( 'Entry Management', 'wpforms-lite' ), 'form-management' => esc_html__( 'Form Management', 'wpforms-lite' ), 'marketing-integrations' => esc_html__( 'Marketing Integrations', 'wpforms-lite' ), 'payment-forms' => esc_html__( 'Payment Forms', 'wpforms-lite' ), 'payment-processing' => esc_html__( 'Payment Processing', 'wpforms-lite' ), 'spam-prevention-and-security' => esc_html__( 'Spam Prevention and Security', 'wpforms-lite' ), 'extending-functionality' => esc_html__( 'Extending Functionality', 'wpforms-lite' ), 'troubleshooting-and-support' => esc_html__( 'Troubleshooting and Support', 'wpforms-lite' ), ]; } /** * Get context search terms. * * @since 1.6.3 * * @return array Search terms by context. */ public function get_context_terms() { return [ 'new_form' => 'add form', 'setup' => 'form template', 'fields/add_fields' => 'add fields', 'fields/field_options' => 'field options', 'fields/field_options/text' => 'single line text', 'fields/field_options/textarea' => 'paragraph text', 'fields/field_options/number-slider' => 'number slider', 'fields/field_options/select' => 'dropdown', 'fields/field_options/radio' => 'multiple choice', 'fields/field_options/checkbox' => 'checkboxes', 'fields/field_options/gdpr-checkbox' => 'gdpr agreement', 'fields/field_options/email' => 'email', 'fields/field_options/address' => 'address', 'fields/field_options/url' => 'website/url', 'fields/field_options/name' => 'name', 'fields/field_options/hidden' => 'hidden', 'fields/field_options/html' => 'html', 'fields/field_options/content' => 'content', 'fields/field_options/pagebreak' => 'page break', 'fields/field_options/entry-preview' => 'entry preview', 'fields/field_options/password' => 'password', 'fields/field_options/date-time' => 'date time', 'fields/field_options/divider' => 'section divider', 'fields/field_options/phone' => 'phone', 'fields/field_options/number' => 'numbers', 'fields/field_options/file-upload' => 'file upload', 'fields/field_options/captcha' => 'custom captcha', 'fields/field_options/rating' => 'rating', 'fields/field_options/richtext' => 'rich text', 'fields/field_options/layout' => 'layout', 'fields/field_options/likert_scale' => 'likert scale', 'fields/field_options/payment-single' => 'single item', 'fields/field_options/payment-multiple' => 'multiple items', 'fields/field_options/payment-checkbox' => 'checkbox items', 'fields/field_options/payment-select' => 'dropdown items', 'fields/field_options/payment-total' => 'total', 'fields/field_options/paypal-commerce' => 'paypal checkout', 'fields/field_options/stripe-credit-card' => 'stripe credit card', 'fields/field_options/authorize_net' => 'authorize.net credit card', 'fields/field_options/square' => 'square credit card', 'fields/field_options/signature' => 'signature', 'fields/field_options/net_promoter_score' => 'net promoter score', 'fields/field_options/payment-coupon' => 'coupon', 'fields/field_options/repeater' => 'repeater', 'settings/general' => 'settings', 'settings/anti_spam' => 'spam', 'settings/themes' => 'themes', 'settings/notifications' => 'notification emails', 'settings/confirmation' => 'confirmation message', 'settings/lead_forms' => 'lead forms', 'settings/form_abandonment' => 'form abandonment', 'settings/post_submissions' => 'post submissions', 'settings/user_registration' => 'user registration', 'settings/surveys_polls' => 'surveys and polls', 'settings/conversational_forms' => 'conversational forms', 'settings/form_locker' => 'form locker', 'settings/form_pages' => 'form pages', 'settings/save_resume' => 'save and resume', 'settings/google_sheets' => 'google sheets', 'settings/dropbox' => 'dropbox', 'settings/google_calendar' => 'google calendar', 'settings/airtable' => 'airtable', 'settings/google_drive' => 'google drive', 'settings/notion' => 'notion', 'settings/webhooks' => 'webhooks', 'settings/entry_automation' => 'entry automation', 'settings/pdf' => 'pdf', 'settings/quiz' => 'quiz', 'providers' => '', 'providers/aweber' => 'aweber', 'providers/activecampaign' => 'activecampaign', 'providers/campaign_monitor' => 'campaign monitor', 'providers/constant_contact' => 'constant contact', 'providers/convertkit' => 'kit', 'providers/drip' => 'drip', 'providers/getresponse' => 'getresponse', 'providers/getresponse_v3' => 'getresponse', 'providers/mailchimp' => 'mailchimp', 'providers/mailchimpv3' => 'mailchimp', 'providers/mailerlite' => 'mailerlite', 'providers/mailpoet' => 'mailpoet', 'providers/make' => 'make', 'providers/n8n' => 'n8n', 'providers/zapier' => 'zapier', 'providers/salesforce' => 'salesforce', 'providers/sendinblue' => 'brevo', 'providers/slack' => 'slack', 'providers/hubspot' => 'hubspot', 'providers/twilio' => 'twilio', 'providers/pipedrive' => 'pipedrive', 'providers/zoho_crm' => 'zoho crm', 'providers/zoho-crm' => 'zoho crm', 'payments' => '', 'payments/paypal_commerce' => 'paypal commerce', 'payments/paypal_standard' => 'paypal standard', 'payments/stripe' => 'stripe', 'payments/authorize_net' => 'authorize.net', 'payments/square' => 'square', 'revisions' => 'revisions', ]; } /** * Get context (recommended) docs links. * * @since 1.6.3 * * @return array Docs links by search terms. */ public function get_context_docs_links() { return [ 'add form' => [ '/docs/creating-first-form/', '/docs/how-to-choose-the-right-form-field-for-your-forms/', '/docs/how-to-customize-the-submit-button/', '/docs/generating-forms-with-wpforms-ai/', ], 'new form' => [ '/docs/creating-first-form/', '/docs/how-to-choose-the-right-form-field-for-your-forms/', '/docs/how-to-customize-the-submit-button/', '/docs/generating-forms-with-wpforms-ai/', ], 'create form' => [ '/docs/creating-first-form/', '/docs/how-to-choose-the-right-form-field-for-your-forms/', '/docs/how-to-customize-the-submit-button/', '/docs/generating-forms-with-wpforms-ai/', ], 'form template' => [ '/docs/how-to-create-a-custom-form-template/', '/docs/generating-forms-with-wpforms-ai/', ], 'add fields' => [ '/docs/how-to-choose-the-right-form-field-for-your-forms/', ], 'recaptcha' => [ '/docs/setup-captcha-wpforms/', ], 'spam' => [ '/docs/how-to-prevent-spam-in-wpforms/', '/docs/setup-captcha-wpforms/', '/docs/how-to-install-and-use-custom-captcha-addon-in-wpforms/', '/docs/setting-up-akismet-anti-spam-protection/', '/docs/viewing-and-managing-spam-entries/', ], 'themes' => [ '/docs/styling-your-forms/', ], 'fields' => [ '/docs/how-to-choose-the-right-form-field-for-your-forms/', ], 'field options' => [ '/docs/how-to-customize-form-field-options/', ], 'field settings' => [ '/docs/how-to-customize-form-field-options/', ], 'conditional logic' => [ '/docs/how-to-use-conditional-logic-with-wpforms/', '/docs/setup-form-notification-wpforms/', '/docs/setup-form-confirmation-wpforms/', ], 'single line text' => [ '/docs/how-to-limit-words-or-characters-in-a-form-field/', '/docs/how-to-use-custom-input-masks/', '/docs/how-to-customize-form-field-options/', '/docs/how-to-use-conditional-logic-with-wpforms/', '/docs/how-to-customize-the-style-of-individual-form-fields/', '/docs/calculations-addon/', ], 'paragraph' => [ '/docs/how-to-limit-words-or-characters-in-a-form-field/', '/docs/how-to-customize-form-field-options/', '/docs/how-to-use-conditional-logic-with-wpforms/', '/docs/how-to-customize-the-style-of-individual-form-fields/', '/docs/calculations-addon/', ], 'paragraph text' => [ '/docs/how-to-limit-words-or-characters-in-a-form-field/', '/docs/how-to-customize-form-field-options/', '/docs/how-to-use-conditional-logic-with-wpforms/', '/docs/how-to-customize-the-style-of-individual-form-fields/', '/docs/calculations-addon/', ], 'textarea' => [ '/docs/how-to-limit-words-or-characters-in-a-form-field/', '/docs/how-to-customize-form-field-options/', '/docs/how-to-use-conditional-logic-with-wpforms/', '/docs/how-to-customize-the-style-of-individual-form-fields/', '/docs/calculations-addon/', ], 'input mask' => [ '/docs/how-to-use-custom-input-masks/', ], 'limit words' => [ '/docs/how-to-limit-words-or-characters-in-a-form-field/', ], 'limit characters' => [ '/docs/how-to-limit-words-or-characters-in-a-form-field/', ], 'style' => [ '/docs/how-to-style-wpforms-with-custom-css-beginners-guide/', '/docs/how-to-customize-the-style-of-individual-form-fields/', '/docs/how-to-add-custom-css-to-your-wpforms/', ], 'custom css' => [ '/docs/how-to-style-wpforms-with-custom-css-beginners-guide/', '/docs/how-to-customize-the-style-of-individual-form-fields/', '/docs/how-to-add-custom-css-to-your-wpforms/', ], 'css' => [ '/docs/how-to-style-wpforms-with-custom-css-beginners-guide/', '/docs/how-to-customize-the-style-of-individual-form-fields/', '/docs/how-to-add-custom-css-to-your-wpforms/', ], 'dropdown' => [ '/docs/how-to-allow-multiple-selections-to-a-dropdown-field-in-wpforms/', '/docs/how-to-customize-form-field-options/', '/docs/how-to-use-conditional-logic-with-wpforms/', '/docs/how-to-customize-the-style-of-individual-form-fields/', '/docs/generating-form-choices-with-wpforms-ai/', ], 'select' => [ '/docs/how-to-allow-multiple-selections-to-a-dropdown-field-in-wpforms/', '/docs/how-to-customize-form-field-options/', '/docs/how-to-use-conditional-logic-with-wpforms/', '/docs/how-to-customize-the-style-of-individual-form-fields/', '/docs/generating-form-choices-with-wpforms-ai/', ], 'multiple options' => [ '/docs/how-to-allow-multiple-selections-to-a-dropdown-field-in-wpforms/', '/docs/how-to-customize-form-field-options/', '/docs/how-to-use-conditional-logic-with-wpforms/', '/docs/how-to-customize-the-style-of-individual-form-fields/', '/docs/generating-form-choices-with-wpforms-ai/', ], 'bulk add' => [ '/docs/how-to-bulk-add-choices-for-multiple-choice-checkbox-and-dropdown-fields/', ], 'multiple columns' => [ '/docs/how-to-use-the-layout-field-in-wpforms/', '/docs/how-to-create-a-multi-column-layout-for-radio-buttons-and-checkboxes/', ], 'columns' => [ '/docs/how-to-use-the-layout-field-in-wpforms/', '/docs/how-to-create-a-multi-column-layout-for-radio-buttons-and-checkboxes/', ], 'randomize' => [ '/docs/how-to-randomize-checkbox-and-multiple-choice-options/', ], 'image choices' => [ '/docs/how-to-add-image-choices-to-fields/', ], 'icon choices' => [ '/docs/using-icon-choices/', ], 'multiple choice' => [ '/docs/how-to-bulk-add-choices-for-multiple-choice-checkbox-and-dropdown-fields/', '/docs/how-to-create-a-multi-column-layout-for-radio-buttons-and-checkboxes/', '/docs/how-to-randomize-checkbox-and-multiple-choice-options/', '/docs/how-to-add-image-choices-to-fields/', '/docs/using-icon-choices/', '/docs/how-to-customize-form-field-options/', '/docs/how-to-use-conditional-logic-with-wpforms/', '/docs/how-to-customize-the-style-of-individual-form-fields/', '/docs/generating-form-choices-with-wpforms-ai/', ], 'radio' => [ '/docs/how-to-bulk-add-choices-for-multiple-choice-checkbox-and-dropdown-fields/', '/docs/how-to-create-a-multi-column-layout-for-radio-buttons-and-checkboxes/', '/docs/how-to-randomize-checkbox-and-multiple-choice-options/', '/docs/how-to-add-image-choices-to-fields/', '/docs/using-icon-choices/', '/docs/how-to-customize-form-field-options/', '/docs/how-to-use-conditional-logic-with-wpforms/', '/docs/how-to-customize-the-style-of-individual-form-fields/', '/docs/generating-form-choices-with-wpforms-ai/', ], 'checkboxes' => [ '/docs/how-to-bulk-add-choices-for-multiple-choice-checkbox-and-dropdown-fields/', '/docs/how-to-add-a-terms-of-service-checkbox-to-a-form/', '/docs/how-to-create-a-multi-column-layout-for-radio-buttons-and-checkboxes/', '/docs/how-to-randomize-checkbox-and-multiple-choice-options/', '/docs/how-to-add-image-choices-to-fields/', '/docs/using-icon-choices/', '/docs/how-to-customize-form-field-options/', '/docs/how-to-use-conditional-logic-with-wpforms/', '/docs/how-to-customize-the-style-of-individual-form-fields/', '/docs/generating-form-choices-with-wpforms-ai/', ], 'checkbox' => [ '/docs/how-to-bulk-add-choices-for-multiple-choice-checkbox-and-dropdown-fields/', '/docs/how-to-add-a-terms-of-service-checkbox-to-a-form/', '/docs/how-to-create-a-multi-column-layout-for-radio-buttons-and-checkboxes/', '/docs/how-to-randomize-checkbox-and-multiple-choice-options/', '/docs/how-to-add-image-choices-to-fields/', '/docs/using-icon-choices/', '/docs/how-to-customize-form-field-options/', '/docs/how-to-use-conditional-logic-with-wpforms/', '/docs/how-to-customize-the-style-of-individual-form-fields/', '/docs/generating-form-choices-with-wpforms-ai/', ], 'gdpr' => [ '/docs/how-to-create-gdpr-compliant-forms/', '/docs/how-to-customize-form-field-options/', '/docs/how-to-use-conditional-logic-with-wpforms/', '/docs/how-to-customize-the-style-of-individual-form-fields/', ], 'gdpr agreement' => [ '/docs/how-to-create-gdpr-compliant-forms/', '/docs/how-to-customize-form-field-options/', '/docs/how-to-use-conditional-logic-with-wpforms/', '/docs/how-to-customize-the-style-of-individual-form-fields/', ], 'number slider' => [ '/docs/how-to-add-a-number-slider-field-to-wpforms/', '/docs/how-to-customize-form-field-options/', '/docs/how-to-use-conditional-logic-with-wpforms/', '/docs/how-to-customize-the-style-of-individual-form-fields/', ], 'range' => [ '/docs/how-to-add-a-number-slider-field-to-wpforms/', '/docs/how-to-customize-form-field-options/', '/docs/how-to-use-conditional-logic-with-wpforms/', '/docs/how-to-customize-the-style-of-individual-form-fields/', ], 'email' => [ '/docs/setup-form-notification-wpforms/', '/docs/customizing-form-notification-emails/', '/docs/how-to-create-conditional-form-notifications-in-wpforms/', '/docs/troubleshooting-email-notifications/', '/docs/how-to-fix-wordpress-contact-form-not-sending-email-with-smtp/', '/docs/how-to-customize-form-field-options/', '/docs/how-to-use-conditional-logic-with-wpforms/', '/docs/how-to-customize-the-style-of-individual-form-fields/', ], 'address' => [ '/docs/how-to-customize-the-address-field/', '/docs/how-to-customize-form-field-options/', '/docs/how-to-use-conditional-logic-with-wpforms/', '/docs/how-to-customize-the-style-of-individual-form-fields/', ], 'field' => [ '/docs/how-to-customize-form-field-options/', '/docs/how-to-use-conditional-logic-with-wpforms/', '/docs/how-to-customize-the-style-of-individual-form-fields/', ], 'state' => [ '/docs/how-to-customize-the-address-field/', '/docs/how-to-customize-form-field-options/', '/docs/how-to-use-conditional-logic-with-wpforms/', '/docs/how-to-customize-the-style-of-individual-form-fields/', ], 'province' => [ '/docs/how-to-customize-the-address-field/', '/docs/how-to-customize-form-field-options/', '/docs/how-to-use-conditional-logic-with-wpforms/', '/docs/how-to-customize-the-style-of-individual-form-fields/', ], 'region' => [ '/docs/how-to-customize-the-address-field/', '/docs/how-to-customize-form-field-options/', '/docs/how-to-use-conditional-logic-with-wpforms/', '/docs/how-to-customize-the-style-of-individual-form-fields/', ], 'city' => [ '/docs/how-to-customize-the-address-field/', '/docs/how-to-customize-form-field-options/', '/docs/how-to-use-conditional-logic-with-wpforms/', '/docs/how-to-customize-the-style-of-individual-form-fields/', ], 'country' => [ '/docs/how-to-customize-the-address-field/', '/docs/how-to-customize-form-field-options/', '/docs/how-to-use-conditional-logic-with-wpforms/', '/docs/how-to-customize-the-style-of-individual-form-fields/', ], 'zip code' => [ '/docs/how-to-customize-the-address-field/', '/docs/how-to-customize-form-field-options/', '/docs/how-to-use-conditional-logic-with-wpforms/', '/docs/how-to-customize-the-style-of-individual-form-fields/', ], 'postal code' => [ '/docs/how-to-customize-the-address-field/', '/docs/how-to-customize-form-field-options/', '/docs/how-to-use-conditional-logic-with-wpforms/', '/docs/how-to-customize-the-style-of-individual-form-fields/', ], 'hidden' => [ '/docs/how-to-choose-the-right-form-field-for-your-forms/', '/docs/how-to-use-smart-tags-in-wpforms/', '/docs/how-to-use-conditional-logic-with-wpforms/', '/docs/calculations-addon/', ], 'rating' => [ '/docs/how-to-add-a-rating-field-to-wpforms/', '/docs/how-to-install-and-use-the-surveys-and-polls-addon/', '/docs/how-to-customize-form-field-options/', '/docs/how-to-use-conditional-logic-with-wpforms/', '/docs/how-to-customize-the-style-of-individual-form-fields/', ], 'star' => [ '/docs/how-to-add-a-rating-field-to-wpforms/', '/docs/how-to-install-and-use-the-surveys-and-polls-addon/', '/docs/how-to-customize-form-field-options/', '/docs/how-to-use-conditional-logic-with-wpforms/', '/docs/how-to-customize-the-style-of-individual-form-fields/', ], 'rich text' => [ '/docs/how-to-use-the-rich-text-field-in-wpforms/', ], 'wysiwyg' => [ '/docs/how-to-use-the-rich-text-field-in-wpforms/', ], 'editor' => [ '/docs/how-to-use-the-rich-text-field-in-wpforms/', ], 'rich editor' => [ '/docs/how-to-use-the-rich-text-field-in-wpforms/', ], 'layout' => [ '/docs/how-to-use-the-layout-field-in-wpforms/', ], 'two columns' => [ '/docs/how-to-use-the-layout-field-in-wpforms/', '/docs/using-the-repeater-field/', ], 'three columns' => [ '/docs/how-to-use-the-layout-field-in-wpforms/', '/docs/using-the-repeater-field/', ], 'four columns' => [ '/docs/how-to-use-the-layout-field-in-wpforms/', '/docs/using-the-repeater-field/', ], 'fields horizontally' => [ '/docs/how-to-use-the-layout-field-in-wpforms/', '/docs/using-the-repeater-field/', ], 'fields in a row' => [ '/docs/how-to-use-the-layout-field-in-wpforms/', '/docs/using-the-repeater-field/', ], 'repeater' => [ '/docs/using-the-repeater-field/', ], 'repeatable' => [ '/docs/using-the-repeater-field/', ], 'replicate fields' => [ '/docs/using-the-repeater-field/', ], 'page break' => [ '/docs/how-to-create-multi-page-forms-in-wpforms/', '/docs/how-to-customize-form-field-options/', '/docs/how-to-customize-the-style-of-individual-form-fields/', ], 'page' => [ '/docs/how-to-create-multi-page-forms-in-wpforms/', '/docs/how-to-customize-form-field-options/', '/docs/how-to-customize-the-style-of-individual-form-fields/', ], 'entry preview' => [ '/docs/how-to-show-entry-previews-in-wpforms/', '/docs/how-to-customize-form-field-options/', '/docs/how-to-customize-the-style-of-individual-form-fields/', ], 'break' => [ '/docs/how-to-create-multi-page-forms-in-wpforms/', '/docs/how-to-customize-form-field-options/', '/docs/how-to-customize-the-style-of-individual-form-fields/', ], 'multi-page' => [ '/docs/how-to-create-multi-page-forms-in-wpforms/', '/docs/how-to-customize-form-field-options/', '/docs/how-to-customize-the-style-of-individual-form-fields/', ], 'password' => [ '/docs/how-to-customize-form-field-options/', '/docs/how-to-use-conditional-logic-with-wpforms/', '/docs/how-to-customize-the-style-of-individual-form-fields/', ], 'name' => [ '/docs/how-to-customize-form-field-options/', '/docs/how-to-use-conditional-logic-with-wpforms/', '/docs/how-to-customize-the-style-of-individual-form-fields/', ], 'first' => [ '/docs/how-to-customize-form-field-options/', '/docs/how-to-use-conditional-logic-with-wpforms/', '/docs/how-to-customize-the-style-of-individual-form-fields/', ], 'last' => [ '/docs/how-to-customize-form-field-options/', '/docs/how-to-use-conditional-logic-with-wpforms/', '/docs/how-to-customize-the-style-of-individual-form-fields/', ], 'surname' => [ '/docs/how-to-customize-form-field-options/', '/docs/how-to-use-conditional-logic-with-wpforms/', '/docs/how-to-customize-the-style-of-individual-form-fields/', ], 'custom captcha' => [ '/docs/how-to-install-and-use-custom-captcha-addon-in-wpforms/', '/docs/how-to-customize-form-field-options/', '/docs/how-to-use-conditional-logic-with-wpforms/', '/docs/how-to-customize-the-style-of-individual-form-fields/', ], 'numbers' => [ '/docs/how-to-customize-form-field-options/', '/docs/how-to-use-conditional-logic-with-wpforms/', '/docs/how-to-customize-the-style-of-individual-form-fields/', '/docs/calculations-addon/', ], 'website/url' => [ '/docs/how-to-customize-form-field-options/', '/docs/how-to-use-conditional-logic-with-wpforms/', '/docs/how-to-customize-the-style-of-individual-form-fields/', ], 'website' => [ '/docs/how-to-customize-form-field-options/', '/docs/how-to-use-conditional-logic-with-wpforms/', '/docs/how-to-customize-the-style-of-individual-form-fields/', ], 'url' => [ '/docs/how-to-customize-form-field-options/', '/docs/how-to-use-conditional-logic-with-wpforms/', '/docs/how-to-customize-the-style-of-individual-form-fields/', ], 'html' => [ '/docs/how-to-customize-form-field-options/', '/docs/how-to-use-conditional-logic-with-wpforms/', '/docs/how-to-customize-the-style-of-individual-form-fields/', ], 'content' => [ 'docs/using-the-content-field/', ], 'code' => [ '/docs/how-to-customize-form-field-options/', '/docs/how-to-use-conditional-logic-with-wpforms/', '/docs/how-to-customize-the-style-of-individual-form-fields/', ], 'date/time' => [ '/docs/how-to-customize-form-field-options/', '/docs/how-to-use-conditional-logic-with-wpforms/', '/docs/how-to-customize-the-style-of-individual-form-fields/', '/docs/how-to-customize-the-date-time-field-in-wpforms/', ], 'date' => [ '/docs/how-to-customize-form-field-options/', '/docs/how-to-use-conditional-logic-with-wpforms/', '/docs/how-to-customize-the-style-of-individual-form-fields/', '/docs/how-to-customize-the-date-time-field-in-wpforms/', ], 'time' => [ '/docs/how-to-customize-form-field-options/', '/docs/how-to-use-conditional-logic-with-wpforms/', '/docs/how-to-customize-the-style-of-individual-form-fields/', '/docs/how-to-customize-the-date-time-field-in-wpforms/', ], 'calendar' => [ '/docs/how-to-customize-form-field-options/', '/docs/how-to-use-conditional-logic-with-wpforms/', '/docs/how-to-customize-the-style-of-individual-form-fields/', '/docs/how-to-customize-the-date-time-field-in-wpforms/', ], 'section divider' => [ '/docs/how-to-customize-form-field-options/', '/docs/how-to-use-conditional-logic-with-wpforms/', '/docs/how-to-customize-the-style-of-individual-form-fields/', ], 'section' => [ '/docs/how-to-customize-form-field-options/', '/docs/how-to-use-conditional-logic-with-wpforms/', '/docs/how-to-customize-the-style-of-individual-form-fields/', ], 'divider' => [ '/docs/how-to-customize-form-field-options/', '/docs/how-to-use-conditional-logic-with-wpforms/', '/docs/how-to-customize-the-style-of-individual-form-fields/', ], 'header' => [ '/docs/how-to-customize-form-field-options/', '/docs/how-to-use-conditional-logic-with-wpforms/', '/docs/how-to-customize-the-style-of-individual-form-fields/', ], 'phone' => [ '/docs/how-to-customize-form-field-options/', '/docs/how-to-use-conditional-logic-with-wpforms/', '/docs/how-to-customize-the-style-of-individual-form-fields/', ], 'telephone' => [ '/docs/how-to-customize-form-field-options/', '/docs/how-to-use-conditional-logic-with-wpforms/', '/docs/how-to-customize-the-style-of-individual-form-fields/', ], 'mobile' => [ '/docs/how-to-customize-form-field-options/', '/docs/how-to-use-conditional-logic-with-wpforms/', '/docs/how-to-customize-the-style-of-individual-form-fields/', ], 'file upload' => [ '/docs/a-complete-guide-to-the-file-upload-field/', '/docs/how-to-allow-additional-file-upload-types/', '/docs/how-to-customize-form-field-options/', '/docs/how-to-use-conditional-logic-with-wpforms/', '/docs/how-to-customize-the-style-of-individual-form-fields/', ], 'file' => [ '/docs/a-complete-guide-to-the-file-upload-field/', '/docs/how-to-allow-additional-file-upload-types/', '/docs/how-to-customize-form-field-options/', '/docs/how-to-use-conditional-logic-with-wpforms/', '/docs/how-to-customize-the-style-of-individual-form-fields/', ], 'upload' => [ '/docs/a-complete-guide-to-the-file-upload-field/', '/docs/how-to-allow-additional-file-upload-types/', '/docs/how-to-customize-form-field-options/', '/docs/how-to-use-conditional-logic-with-wpforms/', '/docs/how-to-customize-the-style-of-individual-form-fields/', ], 'signature' => [ '/docs/how-to-install-and-use-the-signature-addon-in-wpforms/', '/docs/how-to-customize-form-field-options/', '/docs/how-to-use-conditional-logic-with-wpforms/', '/docs/how-to-customize-the-style-of-individual-form-fields/', ], 'likert scale' => [ '/docs/how-to-add-a-likert-scale-field-to-wpforms/', '/docs/how-to-install-and-use-the-surveys-and-polls-addon/', '/docs/how-to-customize-form-field-options/', '/docs/how-to-use-conditional-logic-with-wpforms/', '/docs/how-to-customize-the-style-of-individual-form-fields/', ], 'likert' => [ '/docs/how-to-add-a-likert-scale-field-to-wpforms/', '/docs/how-to-install-and-use-the-surveys-and-polls-addon/', '/docs/how-to-customize-form-field-options/', '/docs/how-to-use-conditional-logic-with-wpforms/', '/docs/how-to-customize-the-style-of-individual-form-fields/', ], 'scale' => [ '/docs/how-to-add-a-likert-scale-field-to-wpforms/', '/docs/how-to-install-and-use-the-surveys-and-polls-addon/', '/docs/how-to-customize-form-field-options/', '/docs/how-to-use-conditional-logic-with-wpforms/', '/docs/how-to-customize-the-style-of-individual-form-fields/', ], 'net promoter score' => [ '/docs/how-to-add-a-net-promoter-score-field-to-wpforms/', '/docs/how-to-install-and-use-the-surveys-and-polls-addon/', '/docs/how-to-customize-form-field-options/', '/docs/how-to-use-conditional-logic-with-wpforms/', '/docs/how-to-customize-the-style-of-individual-form-fields/', ], 'net' => [ '/docs/how-to-add-a-net-promoter-score-field-to-wpforms/', '/docs/how-to-install-and-use-the-surveys-and-polls-addon/', '/docs/how-to-customize-form-field-options/', '/docs/how-to-use-conditional-logic-with-wpforms/', '/docs/how-to-customize-the-style-of-individual-form-fields/', ], 'promoter' => [ '/docs/how-to-add-a-net-promoter-score-field-to-wpforms/', '/docs/how-to-install-and-use-the-surveys-and-polls-addon/', '/docs/how-to-customize-form-field-options/', '/docs/how-to-use-conditional-logic-with-wpforms/', '/docs/how-to-customize-the-style-of-individual-form-fields/', ], 'score' => [ '/docs/how-to-add-a-net-promoter-score-field-to-wpforms/', '/docs/how-to-install-and-use-the-surveys-and-polls-addon/', '/docs/how-to-customize-form-field-options/', '/docs/how-to-use-conditional-logic-with-wpforms/', '/docs/how-to-customize-the-style-of-individual-form-fields/', ], 'nps' => [ '/docs/how-to-add-a-net-promoter-score-field-to-wpforms/', '/docs/how-to-install-and-use-the-surveys-and-polls-addon/', '/docs/how-to-customize-form-field-options/', '/docs/how-to-use-conditional-logic-with-wpforms/', '/docs/how-to-customize-the-style-of-individual-form-fields/', ], 'coupon' => [ '/docs/coupons-addon/', ], 'discount' => [ '/docs/coupons-addon/', ], 'payment' => [ '/docs/viewing-and-managing-payments/', '/docs/how-to-install-and-use-the-stripe-addon-with-wpforms/', '/docs/paypal-commerce-addon/', '/docs/install-use-paypal-addon-wpforms/', '/docs/how-to-install-and-use-the-authorize-net-addon-with-wpforms/', '/docs/how-to-create-a-donation-form-with-multiple-amounts/', '/docs/how-to-allow-users-to-choose-a-payment-method-on-your-form/', ], 'price' => [ '/docs/viewing-and-managing-payments/', '/docs/how-to-install-and-use-the-stripe-addon-with-wpforms/', '/docs/paypal-commerce-addon/', '/docs/install-use-paypal-addon-wpforms/', '/docs/how-to-install-and-use-the-authorize-net-addon-with-wpforms/', '/docs/how-to-create-a-donation-form-with-multiple-amounts/', '/docs/how-to-allow-users-to-choose-a-payment-method-on-your-form/', ], 'cost' => [ '/docs/viewing-and-managing-payments/', '/docs/how-to-install-and-use-the-stripe-addon-with-wpforms/', '/docs/paypal-commerce-addon/', '/docs/install-use-paypal-addon-wpforms/', '/docs/how-to-install-and-use-the-authorize-net-addon-with-wpforms/', '/docs/how-to-create-a-donation-form-with-multiple-amounts/', '/docs/how-to-allow-users-to-choose-a-payment-method-on-your-form/', ], 'single item' => [ '/docs/viewing-and-managing-payments/', '/docs/how-to-install-and-use-the-stripe-addon-with-wpforms/', '/docs/paypal-commerce-addon/', '/docs/install-use-paypal-addon-wpforms/', '/docs/how-to-install-and-use-the-authorize-net-addon-with-wpforms/', '/docs/how-to-create-a-donation-form-with-multiple-amounts/', '/docs/how-to-allow-users-to-choose-a-payment-method-on-your-form/', '/docs/how-to-customize-form-field-options/', '/docs/how-to-use-conditional-logic-with-wpforms/', '/docs/how-to-customize-the-style-of-individual-form-fields/', '/docs/calculations-addon/', ], 'multiple items' => [ '/docs/viewing-and-managing-payments/', '/docs/how-to-install-and-use-the-stripe-addon-with-wpforms/', '/docs/paypal-commerce-addon/', '/docs/install-use-paypal-addon-wpforms/', '/docs/how-to-install-and-use-the-authorize-net-addon-with-wpforms/', '/docs/how-to-create-a-donation-form-with-multiple-amounts/', '/docs/how-to-allow-users-to-choose-a-payment-method-on-your-form/', '/docs/how-to-add-image-choices-to-fields/', '/docs/using-icon-choices/', '/docs/how-to-customize-form-field-options/', '/docs/how-to-use-conditional-logic-with-wpforms/', '/docs/how-to-customize-the-style-of-individual-form-fields/', ], 'checkbox items' => [ '/docs/viewing-and-managing-payments/', '/docs/how-to-install-and-use-the-stripe-addon-with-wpforms/', '/docs/paypal-commerce-addon/', '/docs/install-use-paypal-addon-wpforms/', '/docs/how-to-install-and-use-the-authorize-net-addon-with-wpforms/', '/docs/how-to-create-a-donation-form-with-multiple-amounts/', '/docs/how-to-allow-users-to-choose-a-payment-method-on-your-form/', '/docs/how-to-add-image-choices-to-fields/', '/docs/using-icon-choices/', '/docs/how-to-customize-form-field-options/', '/docs/how-to-use-conditional-logic-with-wpforms/', '/docs/how-to-customize-the-style-of-individual-form-fields/', ], 'dropdown items' => [ '/docs/viewing-and-managing-payments/', '/docs/how-to-install-and-use-the-stripe-addon-with-wpforms/', '/docs/paypal-commerce-addon/', '/docs/install-use-paypal-addon-wpforms/', '/docs/how-to-install-and-use-the-authorize-net-addon-with-wpforms/', '/docs/how-to-create-a-donation-form-with-multiple-amounts/', '/docs/how-to-allow-users-to-choose-a-payment-method-on-your-form/', '/docs/how-to-customize-form-field-options/', '/docs/how-to-use-conditional-logic-with-wpforms/', '/docs/how-to-customize-the-style-of-individual-form-fields/', ], 'total' => [ '/docs/viewing-and-managing-payments/', '/docs/how-to-require-payment-total-with-a-wordpress-form/', '/docs/how-to-install-and-use-the-stripe-addon-with-wpforms/', '/docs/paypal-commerce-addon/', '/docs/install-use-paypal-addon-wpforms/', '/docs/how-to-install-and-use-the-authorize-net-addon-with-wpforms/', '/docs/how-to-create-a-donation-form-with-multiple-amounts/', '/docs/how-to-allow-users-to-choose-a-payment-method-on-your-form/', '/docs/how-to-customize-form-field-options/', '/docs/how-to-use-conditional-logic-with-wpforms/', '/docs/how-to-customize-the-style-of-individual-form-fields/', ], 'paypal checkout' => [ '/docs/paypal-commerce-addon/', '/docs/testing-payments-with-the-paypal-commerce-addon/', '/docs/how-to-customize-form-field-options/', '/docs/how-to-use-conditional-logic-with-wpforms/', '/docs/viewing-and-managing-payments/', ], 'stripe credit card' => [ '/docs/how-to-install-and-use-the-stripe-addon-with-wpforms/', '/docs/how-to-test-stripe-payments-on-your-site/', '/docs/how-to-customize-form-field-options/', '/docs/how-to-use-conditional-logic-with-wpforms/', '/docs/viewing-and-managing-payments/', ], 'authorize.net credit card' => [ '/docs/how-to-install-and-use-the-authorize-net-addon-with-wpforms/', '/docs/how-to-customize-form-field-options/', '/docs/how-to-use-conditional-logic-with-wpforms/', '/docs/viewing-and-managing-payments/', ], 'square credit card' => [ '/docs/how-to-install-and-use-the-square-addon-with-wpforms/', '/docs/how-to-test-square-payments-on-your-site/', '/docs/how-to-customize-form-field-options/', '/docs/how-to-use-conditional-logic-with-wpforms/', '/docs/viewing-and-managing-payments/', ], 'settings' => [ '/docs/creating-first-form/', '/docs/setup-form-notification-wpforms/', '/docs/setup-form-confirmation-wpforms/', ], 'submit' => [ '/docs/how-to-customize-the-submit-button/', ], 'button' => [ '/docs/how-to-customize-the-submit-button/', ], 'dynamic population' => [ '/developers/how-to-enable-dynamic-field-population/', ], 'offline' => [ '/docs/how-to-enable-ajax-form-submissions/', ], 'offline forms' => [ '/docs/how-to-enable-ajax-form-submissions/', ], 'notification' => [ '/docs/setup-form-notification-wpforms/', '/docs/customizing-form-notification-emails/', '/docs/how-to-create-conditional-form-notifications-in-wpforms/', '/docs/troubleshooting-email-notifications/', '/docs/how-to-fix-wordpress-contact-form-not-sending-email-with-smtp/', '/docs/pdf-addon/', ], 'notifications' => [ '/docs/setup-form-notification-wpforms/', '/docs/customizing-form-notification-emails/', '/docs/how-to-create-conditional-form-notifications-in-wpforms/', '/docs/troubleshooting-email-notifications/', '/docs/how-to-fix-wordpress-contact-form-not-sending-email-with-smtp/', '/docs/pdf-addon/', ], 'notification email' => [ '/docs/setup-form-notification-wpforms/', '/docs/customizing-form-notification-emails/', '/docs/how-to-create-conditional-form-notifications-in-wpforms/', '/docs/troubleshooting-email-notifications/', '/docs/how-to-fix-wordpress-contact-form-not-sending-email-with-smtp/', '/docs/pdf-addon/', ], 'notification emails' => [ '/docs/setup-form-notification-wpforms/', '/docs/customizing-form-notification-emails/', '/docs/how-to-create-conditional-form-notifications-in-wpforms/', '/docs/troubleshooting-email-notifications/', '/docs/how-to-fix-wordpress-contact-form-not-sending-email-with-smtp/', '/docs/pdf-addon/', ], 'confirmation' => [ '/docs/setup-form-confirmation-wpforms/', '/docs/how-to-create-conditional-form-confirmations/', ], 'confirmation message' => [ '/docs/setup-form-confirmation-wpforms/', '/docs/how-to-create-conditional-form-confirmations/', ], 'redirect' => [ '/docs/setup-form-confirmation-wpforms/', '/docs/how-to-create-conditional-form-confirmations/', ], 'go to url (redirect)' => [ '/docs/setup-form-confirmation-wpforms/', '/docs/how-to-create-conditional-form-confirmations/', ], 'confirmation page' => [ '/docs/setup-form-confirmation-wpforms/', '/docs/how-to-create-conditional-form-confirmations/', ], 'conditional confirmation' => [ '/docs/setup-form-confirmation-wpforms/', '/docs/how-to-create-conditional-form-confirmations/', ], 'calculation' => [ '/docs/calculations-addon/', '/docs/building-formulas-with-the-calculations-addon/', '/calculations-formula-cheatsheet/', ], 'calculations' => [ '/docs/calculations-addon/', '/docs/building-formulas-with-the-calculations-addon/', '/calculations-formula-cheatsheet/', ], 'formula' => [ '/docs/calculations-addon/', '/docs/building-formulas-with-the-calculations-addon/', '/calculations-formula-cheatsheet/', ], 'conditional calculation' => [ '/docs/calculations-addon/', '/docs/building-formulas-with-the-calculations-addon/', '/calculations-formula-cheatsheet/', ], 'lead forms' => [ '/docs/lead-forms-addon/', ], 'form abandonment' => [ '/docs/how-to-install-and-use-form-abandonment-with-wpforms/', ], 'abandonment' => [ '/docs/how-to-install-and-use-form-abandonment-with-wpforms/', ], 'abandon' => [ '/docs/how-to-install-and-use-form-abandonment-with-wpforms/', ], 'lead capture' => [ '/docs/how-to-install-and-use-form-abandonment-with-wpforms/', ], 'post submissions' => [ '/docs/how-to-install-and-use-the-post-submissions-addon-in-wpforms/', ], 'guest post' => [ '/docs/how-to-install-and-use-the-post-submissions-addon-in-wpforms/', ], 'user submission' => [ '/docs/how-to-install-and-use-the-post-submissions-addon-in-wpforms/', ], 'blog' => [ '/docs/how-to-install-and-use-the-post-submissions-addon-in-wpforms/', ], 'post' => [ '/docs/how-to-install-and-use-the-post-submissions-addon-in-wpforms/', ], 'user registration' => [ '/docs/how-to-install-and-use-user-registration-addon-with-wpforms/', '/docs/how-to-set-up-custom-user-meta-fields/', ], 'register' => [ '/docs/how-to-install-and-use-user-registration-addon-with-wpforms/', '/docs/how-to-set-up-custom-user-meta-fields/', ], 'registration' => [ '/docs/how-to-install-and-use-user-registration-addon-with-wpforms/', '/docs/how-to-set-up-custom-user-meta-fields/', ], 'user meta' => [ '/docs/how-to-install-and-use-user-registration-addon-with-wpforms/', '/docs/how-to-set-up-custom-user-meta-fields/', ], 'user' => [ '/docs/how-to-install-and-use-user-registration-addon-with-wpforms/', '/docs/how-to-set-up-custom-user-meta-fields/', ], 'surveys' => [ '/docs/how-to-install-and-use-the-surveys-and-polls-addon/', ], 'polls' => [ '/docs/how-to-install-and-use-the-surveys-and-polls-addon/', ], 'surveys and polls' => [ '/docs/how-to-install-and-use-the-surveys-and-polls-addon/', ], 'conversational forms' => [ '/docs/how-to-install-and-use-the-conversational-forms-addon/', ], 'conversational' => [ '/docs/how-to-install-and-use-the-conversational-forms-addon/', ], 'form locker' => [ '/docs/how-to-install-and-use-the-form-locker-addon-in-wpforms/', '/developers/how-to-display-remaining-entry-limit-number/', ], 'password protection' => [ '/docs/how-to-install-and-use-the-form-locker-addon-in-wpforms/', '/developers/how-to-display-remaining-entry-limit-number/', ], 'entry limit' => [ '/docs/how-to-install-and-use-the-form-locker-addon-in-wpforms/', '/developers/how-to-display-remaining-entry-limit-number/', ], 'scheduling' => [ '/docs/how-to-install-and-use-the-form-locker-addon-in-wpforms/', '/developers/how-to-display-remaining-entry-limit-number/', ], 'restrict access' => [ '/docs/how-to-install-and-use-the-form-locker-addon-in-wpforms/', '/developers/how-to-display-remaining-entry-limit-number/', ], 'limit' => [ '/docs/how-to-install-and-use-the-form-locker-addon-in-wpforms/', '/developers/how-to-display-remaining-entry-limit-number/', ], 'schedule' => [ '/docs/how-to-install-and-use-the-form-locker-addon-in-wpforms/', '/developers/how-to-display-remaining-entry-limit-number/', ], 'restrict' => [ '/docs/how-to-install-and-use-the-form-locker-addon-in-wpforms/', '/developers/how-to-display-remaining-entry-limit-number/', ], 'form pages' => [ '/docs/how-to-install-and-use-the-form-pages-addon/', ], 'save' => [ '/docs/how-to-install-and-use-the-save-and-resume-addon-with-wpforms/', ], 'resume' => [ '/docs/how-to-install-and-use-the-save-and-resume-addon-with-wpforms/', ], 'continue' => [ '/docs/how-to-install-and-use-the-save-and-resume-addon-with-wpforms/', ], 'save and resume' => [ '/docs/how-to-install-and-use-the-save-and-resume-addon-with-wpforms/', ], 'save and continue' => [ '/docs/how-to-install-and-use-the-save-and-resume-addon-with-wpforms/', ], 'webhooks' => [ '/docs/how-to-install-and-use-the-webhooks-addon-with-wpforms/', ], 'aweber' => [ '/docs/install-use-aweber-addon-wpforms/', ], 'campaign monitor' => [ '/docs/how-to-install-and-use-campaign-monitor-addon-with-wpforms/', ], 'constant contact' => [ '/docs/how-to-connect-constant-contact-with-wpforms/', ], 'convertkit' => [ '/docs/convertkit-addon/', ], 'drip' => [ '/docs/how-to-install-and-use-the-drip-addon-in-wpforms/', ], 'dropbox' => [ '/docs/dropbox-addon/', ], 'google-calendar' => [ '/docs/google-calendar-addon/', ], 'google-drive' => [ '/docs/google-drive-addon/', ], 'getresponse' => [ '/docs/how-to-install-and-use-getresponse-addon-with-wpforms/', ], 'google sheets' => [ '/docs/google-sheets-addon/', '/docs/google-permissions/', ], 'mailchimp' => [ '/docs/install-use-mailchimp-addon-wpforms/', ], 'mailerlite' => [ '/docs/install-use-mailerlite-addon-wpforms/', ], 'mailpoet' => [ '/docs/mailpoet-addon/', ], 'make' => [ '/docs/make-addon/', ], 'zapier' => [ '/docs/how-to-install-and-use-zapier-addon-with-wpforms/', ], 'pipedrive' => [ '/docs/pipedrive-addon/', ], 'salesforce' => [ '/docs/how-to-install-and-use-the-salesforce-addon-with-wpforms/', ], 'sendinblue' => [ '/docs/how-to-install-and-use-the-sendinblue-addon-with-wpforms/', ], 'slack' => [ '/docs/slack-addon/', ], 'hubspot' => [ '/docs/how-to-install-and-use-the-hubspot-addon-in-wpforms/', ], 'twilio' => [ '/docs/twilio-addon/', ], 'zoho crm' => [ '/docs/zoho-crm-addon/', ], 'integrate' => [ '/docs/how-to-install-and-use-zapier-addon-with-wpforms/', '/docs/how-to-install-and-use-the-webhooks-addon-with-wpforms/', '/docs/google-sheets-addon/', '/docs/n8n-addon/', ], 'integration' => [ '/docs/how-to-install-and-use-zapier-addon-with-wpforms/', '/docs/how-to-install-and-use-the-webhooks-addon-with-wpforms/', '/docs/google-sheets-addon/', '/docs/n8n-addon/', ], 'crm' => [ '/docs/how-to-install-and-use-zapier-addon-with-wpforms/', '/docs/how-to-install-and-use-the-webhooks-addon-with-wpforms/', ], 'api' => [ '/docs/how-to-install-and-use-zapier-addon-with-wpforms/', '/docs/how-to-install-and-use-the-webhooks-addon-with-wpforms/', '/docs/google-sheets-addon/', '/docs/n8n-addon/', ], 'paypal commerce' => [ '/docs/paypal-commerce-addon/', '/docs/testing-payments-with-the-paypal-commerce-addon/', ], 'paypal standard' => [ '/docs/install-use-paypal-addon-wpforms/', '/docs/how-to-test-paypal-payments-before-accepting-real-payments/', '/docs/how-to-allow-users-to-choose-a-payment-method-on-your-form/', ], 'stripe' => [ '/docs/using-stripe-with-wpforms-lite/', '/docs/how-to-install-and-use-the-stripe-addon-with-wpforms/', '/docs/how-to-test-stripe-payments-on-your-site/', ], 'authorize' => [ '/docs/how-to-install-and-use-the-authorize-net-addon-with-wpforms/', ], 'authorize.net' => [ '/docs/how-to-install-and-use-the-authorize-net-addon-with-wpforms/', ], 'square' => [ '/docs/how-to-install-and-use-the-square-addon-with-wpforms/', '/docs/how-to-test-square-payments-on-your-site/', ], 'revisions' => [ '/docs/how-to-use-form-revisions-in-wpforms/', ], 'ai' => [ '/docs/generating-form-choices-with-wpforms-ai/', '/docs/generating-forms-with-wpforms-ai/', ], 'entry automation' => [ '/docs/entry-automation-addon/', ], 'pdf' => [ '/docs/pdf-addon/', ], 'n8n' => [ '/docs/n8n-addon/', ], 'notion' => [ '/docs/notion-addon/', ], 'airtable' => [ '/docs/airtable-addon/', ], 'quiz' => [ '/docs/quiz-addon/', ], ]; } /** * Get context (recommended) docs. * * @since 1.6.3 * * @return array Docs recommended by search terms. */ public function get_context_docs() { if ( empty( $this->docs ) ) { return []; } $docs_links = $this->get_context_docs_links(); $docs = []; foreach ( $docs_links as $word => $links ) { $docs[ $word ] = $this->get_doc_ids( $links ); } return $docs; } /** * Get doc id. * * @since 1.8.3 * * @param string $link Absolute link to the doc without the domain part. * * @return int Doc id. */ private function get_doc_id_int( $link ) { if ( empty( $this->docs ) ) { return 0; } foreach ( $this->docs as $id => $doc ) { if ( ! empty( $doc['url'] ) && $doc['url'] === 'https://wpforms.com' . $link ) { return $id; } } return 0; } /** * Get doc ids. * * @since 1.6.3 * * @param array $links Array of the doc links. * * @return array Doc ids. */ public function get_doc_ids( $links ) { $ids = []; foreach ( $links as $link ) { $ids[] = $this->get_doc_id_int( $link ); } return $ids; } /** * Output help modal markup. * * @since 1.6.3 */ public function output() { // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo wpforms_render( 'builder/help', [ 'settings' => [ 'docs_url' => 'https://wpforms.com/docs/', 'support_ticket_url' => 'https://wpforms.com/account/support/', 'upgrade_url' => 'https://wpforms.com/pricing/', ], ], true ); } } Admin/Builder/Addons.php 0000644 00000024244 15174710275 0011122 0 ustar 00 <?php namespace WPForms\Admin\Builder; use WPForms\Requirements\Requirements; /** * Addons class. * * @since 1.9.2 */ class Addons { /** * List of addon options. * * @since 1.9.2 */ private const FIELD_OPTIONS = [ 'calculations' => [ 'calculation_code', 'calculation_code_js', 'calculation_code_php', 'calculation_is_enabled', ], 'form-locker' => [ 'unique_answer', ], 'geolocation' => [ 'display_map', 'enable_address_autocomplete', 'map_position', ], 'surveys-polls' => [ 'survey', ], 'quiz' => [ 'quiz_enabled', 'choices' => [ 'quiz_personality', 'quiz_weight', ], ], ]; /** * Initialize. * * @since 1.9.2 * * @noinspection ReturnTypeCanBeDeclaredInspection */ public function init() { $this->hooks(); } /** * Add hooks. * * @since 1.9.2 */ private function hooks(): void { add_filter( 'wpforms_save_form_args', [ $this, 'save_disabled_addons_options' ], 10, 3 ); } /** * Field's options added by an addon can be deleted when the addon is deactivated or have incompatible status. * The options are fully controlled by the addon when addon is active and compatible. * * @since 1.9.2 * * @param array|mixed $post_data Post data. * * @return array */ public function save_disabled_addons_options( $post_data ): array { $post_data = (array) $post_data; $form_obj = wpforms()->obj( 'form' ); $form_data = json_decode( stripslashes( $post_data['post_content'] ?? '' ), true ); $form_id = $form_data['id'] ?? ''; if ( ! $form_obj || ! $form_id ) { return $post_data; } $previous_form_data = $form_obj->get( $form_id, [ 'content_only' => true ] ); $not_validated_addons = Requirements::get_instance()->get_not_validated_addons(); if ( empty( $previous_form_data ) || empty( $not_validated_addons ) ) { return $post_data; } foreach ( $not_validated_addons as $path ) { $slug = str_replace( 'wpforms-', '', basename( $path, '.php' ) ); $this->preserve_addon( $slug, $form_data, $previous_form_data ); } $this->preserve_providers( $form_data, $previous_form_data ); $this->preserve_payments( $form_data, $previous_form_data ); $post_data['post_content'] = wpforms_encode( $form_data ); return $post_data; } /** * Preserve addon fields, settings, panels, notifications, etc. * * @since 1.9.3 * * @param string $slug Addon slug. * @param array $form_data Form data. * @param array $previous_form_data Previous form data. * * @return void */ private function preserve_addon( string $slug, array &$form_data, array $previous_form_data ): void { if ( ! empty( $form_data['fields'] ) && ! empty( $previous_form_data['fields'] ) ) { $this->preserve_addon_fields_settings( $slug, $form_data['fields'], $previous_form_data['fields'] ); } $this->preserve_addon_panel( $slug, $form_data, $previous_form_data ); if ( ! empty( $form_data['settings'] ) && ! empty( $previous_form_data['settings'] ) ) { $this->preserve_addon_settings( $slug, $form_data['settings'], $previous_form_data['settings'] ); } if ( ! empty( $form_data['settings']['notifications'] ) && ! empty( $previous_form_data['settings']['notifications'] ) ) { $this->preserve_addon_notifications( $slug, $form_data['settings']['notifications'], $previous_form_data['settings']['notifications'] ); } } /** * Preserve addon fields. * * @since 1.9.5 * * @param string $slug Addon slug. * @param array $new_fields Form fields settings. * @param array $previous_fields Previous form fields settings. * * @return void */ private function preserve_addon_fields_settings( string $slug, array &$new_fields, array $previous_fields ): void { foreach ( $previous_fields as $field_id => $previous_field ) { $new_field = $new_fields[ $field_id ] ?? []; if ( empty( $new_field ) ) { continue; } $this->preserve_addon_field_settings( $slug, $new_field, $previous_field ); $new_fields[ $field_id ] = $new_field; } } /** * Preserve addon field. * * @since 1.9.5 * * @param string $slug Addon slug. * @param array $new_field Previous form fields settings. * @param array $previous_field Form fields settings. * * @return void */ private function preserve_addon_field_settings( string $slug, array &$new_field, array $previous_field ): void { $prefix = $this->prepare_prefix( $slug ); $changed_settings = array_diff_key( $previous_field, $new_field ); $preserve_fields = self::FIELD_OPTIONS[ $slug ] ?? []; foreach ( $changed_settings as $setting_name => $setting_value ) { if ( strpos( $setting_name, $prefix ) === 0 || in_array( $setting_name, $preserve_fields, true ) ) { $new_field[ $setting_name ] = $setting_value; } } if ( ! empty( $preserve_fields['choices'] ) && is_array( $preserve_fields['choices'] ) && ! empty( $new_field['choices'] ) && is_array( $new_field['choices'] ) ) { $this->preserve_addon_field_choices_settings( $preserve_fields['choices'], $new_field, $previous_field ); } } /** * Preserve addon field choices settings. * * @since 1.9.9 * * @param array $choice_settings Choice settings. * @param array $new_field Previous form fields settings. * @param array $previous_field Form fields settings. * * @return void */ private function preserve_addon_field_choices_settings( array $choice_settings, array &$new_field, array $previous_field ): void { if ( ! isset( $previous_field['choices'] ) || ! is_array( $previous_field['choices'] ) ) { return; } $previous_choices = $previous_field['choices']; foreach ( $new_field['choices'] as $choice_id => $choice ) { foreach ( $choice_settings as $setting_name ) { if ( isset( $previous_choices[ $choice_id ][ $setting_name ] ) ) { $new_field['choices'][ $choice_id ][ $setting_name ] = $previous_choices[ $choice_id ][ $setting_name ]; } } } } /** * Preserve addon panel. * * @since 1.9.3 * * @param string $slug Addon slug. * @param array $form_data Form data. * @param array $previous_form_data Previous form data. */ private function preserve_addon_panel( string $slug, array &$form_data, array $previous_form_data ): void { $panel = $this->prepare_prefix( $slug ); // The addon settings stored its own panel, e.g., $form_data[lead_forms], $form_data[webhooks], etc. if ( ! empty( $previous_form_data[ $panel ] ) ) { $form_data[ $panel ] = $previous_form_data[ $panel ]; } } /** * Preserve addon settings stored inside the settings panel with a specific prefix. * e.g. $form_data[settings][{$prefix}_enabled], $form_data[settings][{$prefix}_email], etc. * * @since 1.9.4 * * @param string $slug Addon option prefix. * @param array $new_settings Form settings. * @param array $previous_settings Previous form settings. */ private function preserve_addon_settings( string $slug, array &$new_settings, array $previous_settings ): void { $prefix = $this->prepare_prefix( $slug ); static $legacy_options = [ 'offline_forms' => [ 'offline_form' ], 'user_registration' => [ 'user_login_hide', 'user_reset_hide' ], 'surveys_polls' => [ 'survey_enable', 'poll_enable' ], ]; // BC: User Registration addon has `registration_` prefix instead of `user_registration`. if ( $prefix === 'user_registration' ) { $prefix = 'registration'; } foreach ( $previous_settings as $setting_name => $value ) { if ( strpos( $setting_name, $prefix ) === 0 ) { $new_settings[ $setting_name ] = $value; continue; } // BC: The options don't have a prefix and hard-coded in the `$legacy_options` variable. if ( isset( $legacy_options[ $prefix ] ) && in_array( $setting_name, $legacy_options[ $prefix ], true ) ) { $new_settings[ $setting_name ] = $value; } } } /** * Preserve addon notifications. * * @since 1.9.4 * * @param string $slug Addon slug. * @param array $new_notifications List of form notifications. * @param array $previous_notifications Previously saved list of form notifications. * * @return void */ private function preserve_addon_notifications( string $slug, array &$new_notifications, array $previous_notifications ): void { $prefix = $this->prepare_prefix( $slug ); foreach ( $previous_notifications as $notification_id => $notification_settings ) { if ( empty( $new_notifications[ $notification_id ] ) ) { continue; } $changed_notification_settings = array_diff_key( $notification_settings, $new_notifications[ $notification_id ] ); foreach ( $changed_notification_settings as $setting_name => $value ) { if ( strpos( $setting_name, $prefix ) === 0 ) { $new_notifications[ $notification_id ][ $setting_name ] = $value; } } } } /** * Preserve Providers that are not active. * * @since 1.9.4 * * @param array $form_data Form data. * @param array $previous_form_data Previous form data. */ private function preserve_providers( array &$form_data, array $previous_form_data ): void { if ( empty( $previous_form_data['providers'] ) ) { return; } $active_providers = wpforms_get_providers_available(); foreach ( $previous_form_data['providers'] as $slug => $provider ) { if ( ! empty( $active_providers[ $slug ] ) ) { continue; } $form_data['providers'][ $slug ] = $provider; } } /** * Preserve Payments providers that are not active. * * @since 1.9.4 * * @param array $form_data Form data. * @param array $previous_form_data Previous form data. */ private function preserve_payments( array &$form_data, array $previous_form_data ): void { if ( empty( $previous_form_data['payments'] ) ) { return; } foreach ( $previous_form_data['payments'] as $slug => $value ) { if ( ! empty( $form_data['payments'][ $slug ] ) ) { continue; } $form_data['payments'][ $slug ] = $value; } } /** * Convert slug to a addon prefix. * * @since 1.9.4 * * @param string $slug Addon slug. * * @return string */ private function prepare_prefix( string $slug ): string { return str_replace( '-', '_', $slug ); } } Admin/Builder/ImageUpload.php 0000644 00000001425 15174710275 0012075 0 ustar 00 <?php namespace WPForms\Admin\Builder; /** * Image Upload functionality for the Form Builder Settings. * * @since 1.9.7.3 */ class ImageUpload { /** * Initialize class. * * @since 1.9.7.3 */ public function init(): void { $this->hooks(); } /** * Register hooks. * * @since 1.9.7.3 */ public function hooks(): void { add_action( 'wpforms_builder_enqueues', [ $this, 'enqueues' ] ); } /** * Enqueue assets for the Form Builder. * * @since 1.9.7.3 */ public function enqueues(): void { $min = wpforms_get_min_suffix(); wp_enqueue_script( 'wpforms-builder-settings-image-upload', WPFORMS_PLUGIN_URL . "assets/js/admin/builder/image-upload{$min}.js", [ 'wp-util', 'wpforms-builder-settings' ], WPFORMS_VERSION, true ); } } Admin/Builder/Ajax/SaveForm.php 0000644 00000002177 15174710275 0012320 0 ustar 00 <?php namespace WPForms\Admin\Builder\Ajax; /** * Save the form data. * * @since 1.9.4 */ class SaveForm { /** * The form fields processing while saving the form. * * @since 1.9.4 * * @param array $fields Form fields data. * @param array $form_data Form data. * * @return array */ public function process_fields( array $fields, array $form_data ): array { $form_obj = wpforms()->obj( 'form' ); if ( ! $form_obj || empty( $fields ) || empty( $form_data['id'] ) ) { return $fields; } $saved_form_data = $form_obj->get( $form_data['id'], [ 'content_only' => true ] ); foreach ( $fields as $field_id => $field_data ) { if ( empty( $field_data['type'] ) ) { continue; } /** * Filter field settings before saving the form. * * @since 1.9.4 * * @param array $field_data Field data. * @param array $form_data Forms data. * @param array $saved_form_data Saved form data. */ $fields[ $field_id ] = apply_filters( "wpforms_admin_builder_ajax_save_form_field_{$field_data['type']}", $field_data, $form_data, $saved_form_data ); } return $fields; } } Admin/Builder/Ajax/PanelLoader.php 0000644 00000006771 15174710275 0012770 0 ustar 00 <?php namespace WPForms\Admin\Builder\Ajax; /** * Form Builder Panel Loader AJAX actions. * * @since 1.8.6 */ class PanelLoader { /** * Determine if the class is allowed to load. * * @since 1.8.6 * * @return bool */ private function allow_load(): bool { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $action = isset( $_REQUEST['action'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['action'] ) ) : ''; // Load only in the case of AJAX calls form the Form Builder. return wpforms_is_admin_ajax() && strpos( $action, 'wpforms_builder_' ) === 0; } /** * Initialize class. * * @since 1.8.6 */ public function init(): void { if ( ! $this->allow_load() ) { return; } $this->hooks(); } /** * Hooks. * * @since 1.8.6 */ private function hooks(): void { add_action( 'wp_ajax_wpforms_builder_load_panel', [ $this, 'load_panel_content' ] ); } /** * Save tags. * * @since 1.8.6 */ public function load_panel_content(): void { check_ajax_referer( 'wpforms-builder', 'nonce' ); $form_id = absint( filter_input( INPUT_POST, 'form_id', FILTER_SANITIZE_NUMBER_INT ) ); if ( ! wpforms_current_user_can( 'edit_forms', $form_id ) ) { wp_send_json_error( esc_html__( 'You do not have permission to perform this action.', 'wpforms-lite' ) ); } $data = $this->get_prepared_data( 'load_panel' ); $panel = $data['panel'] ?? ''; $panel_class = '\WPForms_Builder_Panel_' . ucfirst( $panel ); $panel_obj = $this->get_panel_obj( $panel_class, $panel ); ob_start(); $panel_obj->panel_output( [], $panel ); $panel_content = ob_get_clean(); wp_send_json_success( $panel_content ); } /** * Get panel object. * * @since 1.9.4 * * @param string $panel_class Panel class name. * @param string $panel Panel name. * * @return object */ private function get_panel_obj( string $panel_class, string $panel ) { if ( ! class_exists( $panel_class ) ) { // Load panel base class. require_once WPFORMS_PLUGIN_DIR . 'includes/admin/builder/panels/class-base.php'; $file = WPFORMS_PLUGIN_DIR . "includes/admin/builder/panels/class-{$panel}.php"; $file_pro = WPFORMS_PLUGIN_DIR . "pro/includes/admin/builder/panels/class-{$panel}.php"; if ( file_exists( $file_pro ) && wpforms()->is_pro() ) { require_once $file_pro; } elseif ( file_exists( $file ) ) { require_once $file; } } $panel_obj = $panel_class::instance(); if ( ! method_exists( $panel_obj, 'panel_content' ) ) { wp_send_json_error( esc_html__( 'Invalid panel.', 'wpforms-lite' ) ); } return $panel_obj; } /** * Get prepared data before perform ajax action. * * @since 1.8.6 * * @param string $action Action: `save` OR `delete`. * * @return array * @noinspection PhpSameParameterValueInspection */ private function get_prepared_data( string $action ): array { // Run a security check. if ( ! check_ajax_referer( 'wpforms-builder', 'nonce', false ) ) { wp_send_json_error( esc_html__( 'Most likely, your session expired. Please reload the page.', 'wpforms-lite' ) ); } // Check for permissions. if ( ! wpforms_current_user_can( 'edit_forms' ) ) { wp_send_json_error( esc_html__( 'You are not allowed to perform this action.', 'wpforms-lite' ) ); } $data = []; if ( $action === 'load_panel' ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $data['panel'] = ! empty( $_POST['panel'] ) ? sanitize_key( $_POST['panel'] ) : ''; } return $data; } } Admin/Builder/Notifications/Advanced/EmailTemplate.php 0000644 00000012517 15174710275 0016753 0 ustar 00 <?php namespace WPForms\Admin\Builder\Notifications\Advanced; use WPForms_Builder_Panel_Settings; use WPForms\Emails\Helpers; use WPForms\Admin\Education\Helpers as EducationHelpers; /** * Email Template. * This class will register the Email Template field in the "Notification" → "Advanced" settings. * The Email Template field will allow users to override the default email template for a specific notification. * * @since 1.8.5 */ class EmailTemplate { /** * Initialize class. * * @since 1.8.5 */ public function init() { $this->hooks(); } /** * Hooks. * * @since 1.8.5 */ private function hooks() { add_action( 'wpforms_builder_enqueues', [ $this, 'builder_assets' ] ); add_action( 'wpforms_builder_print_footer_scripts', [ $this, 'builder_footer_scripts' ] ); add_filter( 'wpforms_lite_admin_education_builder_notifications_advanced_settings_content', [ $this, 'settings' ], 5, 3 ); add_filter( 'wpforms_pro_admin_builder_notifications_advanced_settings_content', [ $this, 'settings' ], 5, 3 ); } /** * Enqueue assets for the builder. * * @since 1.8.5 */ public function builder_assets() { $min = wpforms_get_min_suffix(); wp_enqueue_script( 'wpforms-builder-email-template', WPFORMS_PLUGIN_URL . "assets/js/admin/builder/email-template{$min}.js", [ 'jquery', 'jquery-confirm', 'wpforms-builder' ], WPFORMS_VERSION, true ); wp_localize_script( 'wpforms-builder-email-template', 'wpforms_builder_email_template', [ 'is_pro' => wpforms()->is_pro(), 'templates' => Helpers::get_email_template_choices( false ), ] ); } /** * Output Email Template modal. * * @since 1.8.5 */ public function builder_footer_scripts() { // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo wpforms_render( 'builder/notifications/email-template-modal', [ 'pro_badge' => ! wpforms()->is_pro() ? EducationHelpers::get_badge( 'Pro' ) : '', ], true ); } /** * Add Email Template settings. * * @since 1.8.5 * * @param string $content Notification → Advanced content. * @param WPForms_Builder_Panel_Settings $settings Builder panel settings. * @param int $id Notification id. * * @return string */ public function settings( $content, $settings, $id ) { // Retrieve email template choices and disabled choices. // A few of the email templates are only available in the Pro version and will be disabled for non-Pro users. // The disabled choices will be added to the select field with a "(Pro)" label appended to the name. list( $options, $disabled_options ) = $this->get_email_template_options(); // Add Email Template field. $content .= wpforms_panel_field( 'select', 'notifications', 'template', $settings->form_data, esc_html__( 'Email Template', 'wpforms-lite' ), [ 'default' => '', 'options' => $options, 'disabled_options' => $disabled_options, 'class' => 'wpforms-panel-field-email-template-wrap', 'input_class' => 'wpforms-panel-field-email-template', 'parent' => 'settings', 'subsection' => $id, 'after' => $this->get_template_modal_link_content(), 'tooltip' => esc_html__( 'Override the default email template for this specific notification.', 'wpforms-lite' ), ], false ); return $content; } /** * Get Email template choices. * * This function will return an array of email template choices and an array of disabled choices. * The disabled choices are templates that are only available in the Pro version. * * @since 1.8.5 * * @return array */ private function get_email_template_options() { // Retrieve the available email template choices. $choices = Helpers::get_email_template_choices( false ); // If there are no templates or the choices are not an array, return empty arrays. if ( empty( $choices ) || ! is_array( $choices ) ) { return [ [], [] ]; } // Check if the Pro version is active. $is_pro = wpforms()->is_pro(); // Initialize arrays for options and disabled options. $options = []; $disabled_options = []; // Iterate through the templates and build the $options array. foreach ( $choices as $key => $choice ) { $value = esc_attr( $key ); $name = esc_html( $choice['name'] ); $is_disabled = ! $is_pro && isset( $choice['is_pro'] ) && $choice['is_pro']; // If the option is disabled for non-Pro users, add it to the disabled options array. if ( $is_disabled ) { $disabled_options[] = $value; } // Build the $options array with appropriate labels. // Pro badge labels are not meant to be translated. $options[ $key ] = $is_disabled ? sprintf( '%s (Pro)', $name ) : $name; } // Add an empty option to the beginning of the $options array. // This is a placeholder option that will be replaced with the default template name. $options = array_merge( [ '' => esc_html__( 'Default Template', 'wpforms-lite' ) ], $options ); // Return the options and disabled options arrays. return [ $options, $disabled_options ]; } /** * Get Email template modal link content. * * @since 1.8.5 * * @return string */ private function get_template_modal_link_content() { return wpforms_render( 'builder/notifications/email-template-link' ); } } Admin/Builder/HelpCache.php 0000644 00000002246 15174710275 0011524 0 ustar 00 <?php namespace WPForms\Admin\Builder; use WPForms\Helpers\CacheBase; /** * Form Builder Help Cache. * * @since 1.8.2 */ class HelpCache extends CacheBase { /** * Remote source URL. * * @since 1.9.3 * * @var string */ const REMOTE_SOURCE = 'https://wpformsapi.com/feeds/v1/docs/'; /** * Determine if the class is allowed to load. * * @since 1.8.2 * * @return bool */ protected function allow_load() { if ( wp_doing_cron() || wpforms_doing_wp_cli() ) { return true; } if ( ! wpforms_current_user_can( [ 'create_forms', 'edit_forms' ] ) ) { return false; } return wpforms_is_admin_page( 'builder' ); } /** * Setup settings and other things. * * @since 1.8.2 */ protected function setup() { return [ 'remote_source' => self::REMOTE_SOURCE, 'cache_file' => 'docs.json', /** * Allow modifying Help Docs cache TTL (time to live). * * @since 1.6.3 * * @param int $cache_ttl Cache TTL in seconds. Defaults to 1 week. */ 'cache_ttl' => (int) apply_filters( 'wpforms_admin_builder_help_cache_ttl', WEEK_IN_SECONDS ), 'update_action' => 'wpforms_builder_help_cache_update', ]; } } Admin/Builder/ContextMenu.php 0000644 00000001671 15174710275 0012162 0 ustar 00 <?php namespace WPForms\Admin\Builder; /** * Context Menu class. * * @since 1.8.6 */ class ContextMenu { /** * Init class. * * @since 1.8.6 */ public function init() { $this->hooks(); } /** * Register hooks. * * @since 1.8.6 */ protected function hooks() { add_action( 'wpforms_builder_enqueues', [ $this, 'enqueues' ] ); add_action( 'wpforms_admin_page', [ $this, 'output' ], 20 ); } /** * Enqueue assets. * * @since 1.8.6 */ public function enqueues() { $min = wpforms_get_min_suffix(); wp_enqueue_script( 'wpforms-builder-context-menu', WPFORMS_PLUGIN_URL . "assets/js/admin/builder/context-menu{$min}.js", [ 'wpforms-builder' ], WPFORMS_VERSION, true ); } /** * Output context menu markup. * * @since 1.8.6 */ public function output() { // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo wpforms_render( 'builder/field-context-menu' ); } } Admin/Builder/Shortcuts.php 0000644 00000006353 15174710275 0011711 0 ustar 00 <?php namespace WPForms\Admin\Builder; /** * Form Builder Keyboard Shortcuts modal content. * * @since 1.6.9 */ class Shortcuts { /** * Initialize class. * * @since 1.6.9 */ public function init(): void { // Terminate initialization if not in the builder. if ( ! wpforms_is_admin_page( 'builder' ) ) { return; } $this->hooks(); } /** * Hooks. * * @since 1.6.9 */ private function hooks(): void { add_filter( 'wpforms_builder_strings', [ $this, 'builder_strings' ] ); add_action( 'wpforms_admin_page', [ $this, 'output' ], 30 ); } /** * Get a shortcut list. * * @since 1.6.9 * * @return array */ private function get_list(): array { return [ 'left' => [ 'ctrl s' => __( 'Save Form', 'wpforms-lite' ), 'ctrl p' => __( 'Preview Form', 'wpforms-lite' ), 'ctrl b' => __( 'Embed Form', 'wpforms-lite' ), 'ctrl f' => __( 'Search Fields', 'wpforms-lite' ), 'ctrl c' => __( 'Copy Fields', 'wpforms-lite' ), 'ctrl v' => __( 'Paste Fields', 'wpforms-lite' ), 'd' => __( 'Duplicate Fields', 'wpforms-lite' ), ], 'right' => [ 'ctrl z' => __( 'Undo', 'wpforms-lite' ), 'ctrl shift z' => __( 'Redo', 'wpforms-lite' ), 'ctrl h' => __( 'Open Help', 'wpforms-lite' ), 'ctrl t' => __( 'Toggle Sidebar', 'wpforms-lite' ), // It is 'alt s' on Windows/Linux, dynamically changed in the modal in admin-builder.js openKeyboardShortcutsModal(). 'ctrl e' => __( 'View Entries', 'wpforms-lite' ), 'ctrl q' => __( 'Close Builder', 'wpforms-lite' ), 'delete' => __( 'Delete Fields', 'wpforms-lite' ), ], ]; } /** * Add Form builder strings. * * @since 1.6.9 * * @param array|mixed $strings Form Builder strings. * * @return array */ public function builder_strings( $strings ): array { $strings = (array) $strings; $strings['shortcuts_modal_title'] = esc_html__( 'Keyboard Shortcuts', 'wpforms-lite' ); $strings['shortcuts_modal_msg'] = esc_html__( 'Handy shortcuts for common actions in the builder.', 'wpforms-lite' ); return $strings; } /** * Generate and output shortcuts modal content as the wp.template. * * @since 1.6.9 */ public function output(): void { echo ' <script type="text/html" id="tmpl-wpforms-builder-keyboard-shortcuts"> <div class="wpforms-columns wpforms-columns-2">'; foreach ( $this->get_list() as $list ) { echo "<ul class='wpforms-column'>"; foreach ( $list as $key => $label ) { $key_parts = explode( ' ', $key ); if ( count( $key_parts ) > 1 ) { printf( '<li> %1$s <span class="shortcut-key shortcut-key-%2$s"> <i>%3$s</i><i>%4$s</i><i>%5$s</i> </span> </li>', esc_html( $label ), esc_html( str_replace( ' ', '-', $key ) ), esc_html( $key_parts[0] ), esc_html( $key_parts[1] ?? '' ), esc_html( $key_parts[2] ?? '' ) ); } else { // Single key like 'delete' or 'd'. printf( '<li> %1$s <span class="shortcut-key shortcut-key-%2$s"> <i>%2$s</i> </span> </li>', esc_html( $label ), esc_html( $key ) ); } } echo '</ul>'; } echo ' </div> </script>'; } } Admin/Builder/Templates.php 0000644 00000106513 15174710275 0011650 0 ustar 00 <?php namespace WPForms\Admin\Builder; use WP_Query; /** * Templates class. * * @since 1.6.8 */ class Templates { /** * Templates hash option. * * @since 1.8.6 * * @var string */ const TEMPLATES_HASH_OPTION = 'wpforms_templates_hash'; /** * Favorite templates option. * * @since 1.7.7 * * @var string */ const FAVORITE_TEMPLATES_OPTION = 'wpforms_favorite_templates'; /** * All templates data from API. * * @since 1.6.8 * * @var array */ private $api_templates = []; /** * Template categories data. * * @since 1.6.8 * * @var array */ private $categories; /** * Template subcategories data. * * @since 1.8.4 * * @var array */ private $subcategories; /** * License data. * * @since 1.6.8 * * @var array */ private $license; /** * All licenses list. * * @since 1.6.8 * * @var array */ private $all_licenses; /** * Favorite templates list. * * @since 1.8.6 * * @var array */ private $favorites_list; /** * Templates hash. * * @since 1.8.6 * * @var string */ private $hash; /** * Determine if the class is allowed to load. * * @since 1.6.8 * * @return bool */ private function allow_load() { $has_permissions = wpforms_current_user_can( [ 'create_forms', 'edit_forms' ] ); $allowed_requests = wpforms_is_admin_ajax() || wpforms_is_admin_page( 'builder' ) || wpforms_is_admin_page( 'templates' ); $allow = $has_permissions && $allowed_requests; /** * Whether to allow the form templates functionality to load. * * @since 1.7.2 * * @param bool $allow True or false. */ return (bool) apply_filters( 'wpforms_admin_builder_templates_allow_load', $allow ); } /** * Initialize class. * * @since 1.6.8 */ public function init() { if ( ! $this->allow_load() ) { return; } $this->init_license_data(); $this->init_templates_data(); $this->hooks(); } /** * Hooks. * * @since 1.6.8 */ protected function hooks() { add_action( 'admin_init', [ $this, 'create_form_on_request' ], 100 ); add_filter( 'wpforms_form_templates_core', [ $this, 'add_templates_to_setup_panel' ], 20 ); add_filter( 'wpforms_create_form_args', [ $this, 'apply_to_new_form' ], 10, 2 ); add_filter( 'wpforms_save_form_args', [ $this, 'apply_to_existing_form' ], 10, 3 ); add_action( 'admin_print_scripts', [ $this, 'upgrade_banner_template' ] ); add_action( 'admin_print_scripts', [ $this, 'upgrade_lite_banner_template' ] ); add_action( 'admin_enqueue_scripts', [ $this, 'enqueues' ] ); add_action( 'wp_ajax_wpforms_templates_favorite', [ $this, 'ajax_save_favorites' ] ); add_filter( 'wpforms_form_templates', [ $this, 'add_addons_templates' ] ); } /** * Enqueue assets for the Setup panel. * * @since 1.7.7 */ public function enqueues() { $min = wpforms_get_min_suffix(); wp_enqueue_script( 'listjs', WPFORMS_PLUGIN_URL . 'assets/lib/list.min.js', [ 'jquery' ], '2.3.0', false ); wp_enqueue_script( 'wpforms-form-templates', WPFORMS_PLUGIN_URL . "assets/js/admin/builder/form-templates{$min}.js", [ 'underscore', 'wp-util', 'listjs' ], WPFORMS_VERSION, true ); $strings = [ 'ajaxurl' => admin_url( 'admin-ajax.php' ), 'admin_nonce' => wp_create_nonce( 'wpforms-admin' ), 'nonce' => wp_create_nonce( 'wpforms-form-templates' ), 'can_install_addons' => wpforms_can_install( 'addon' ), 'activating' => esc_html__( 'Activating', 'wpforms-lite' ), 'cancel' => esc_html__( 'Cancel', 'wpforms-lite' ), 'heads_up' => esc_html__( 'Heads Up!', 'wpforms-lite' ), 'install_confirm' => esc_html__( 'Install and activate', 'wpforms-lite' ), 'activate_confirm' => esc_html__( 'Activate', 'wpforms-lite' ), 'ok' => esc_html__( 'Ok', 'wpforms-lite' ), 'template_addons_error' => esc_html__( 'Could not install OR activate all the required addons. Please download from wpforms.com and install them manually. Would you like to use the template anyway?', 'wpforms-lite' ), 'use_template' => esc_html__( 'Yes, use template', 'wpforms-lite' ), 'delete_template' => esc_html__( 'Yes, Delete', 'wpforms-lite' ), 'delete_template_title' => esc_html__( 'Delete Form Template', 'wpforms-lite' ), 'delete_template_content' => esc_html__( 'Are you sure you want to delete this form template? This cannot be undone.', 'wpforms-lite' ), ]; if ( $strings['can_install_addons'] ) { /* translators: %1$s - template name, %2$s - addon name(s). */ $strings['template_addon_prompt'] = esc_html( sprintf( __( 'The %1$s template requires the %2$s. Would you like to install and activate it?', 'wpforms-lite' ), '%template%', '%addons%' ) ); /* translators: %1$s - template name, %2$s - addon name(s). */ $strings['template_addons_prompt'] = esc_html( sprintf( __( 'The %1$s template requires the %2$s. Would you like to install and activate all the required addons?', 'wpforms-lite' ), '%template%', '%addons%' ) ); /* translators: %1$s - template name, %2$s - addon name(s). */ $strings['template_addon_activate'] = esc_html( sprintf( __( 'The %1$s template requires the %2$s. Would you like to activate it?', 'wpforms-lite' ), '%template%', '%addons%' ) ); } else { /* translators: %s - addon name(s). */ $single_form = esc_html( sprintf( __( "To use all of the features in this template, you'll need the %s. Contact your site administrator to install it, then try opening this template again.", 'wpforms-lite' ), '%addons%' ) ); $strings['template_addon_prompt'] = $single_form; $strings['template_addon_activate'] = $single_form; /* translators: %s - addon name(s). */ $strings['template_addons_prompt'] = esc_html( sprintf( __( "To use all of the features in this template, you'll need the %s. Contact your site administrator to install them, then try opening this template again.", 'wpforms-lite' ), '%addons%' ) ); } wp_localize_script( 'wpforms-form-templates', 'wpforms_form_templates', $strings ); wp_localize_script( 'wpforms-form-templates', 'wpforms_addons', $this->get_localized_addons() ); } /** * Get localized addons. * * @since 1.8.2 * * @return array */ private function get_localized_addons() { return wpforms_chain( wpforms()->obj( 'addons' )->get_available() ) ->map( static function( $addon ) { return [ 'title' => $addon['title'], 'action' => $addon['action'], 'url' => $addon['url'], ]; } ) ->value(); } /** * Init license data. * * @since 1.6.8 */ private function init_license_data() { $this->all_licenses = [ 'lite', 'basic', 'plus', 'pro', 'elite', 'agency', 'ultimate' ]; // User license data. $this->license['key'] = wpforms_get_license_key(); $this->license['type'] = wpforms_get_license_type(); $this->license['type'] = in_array( $this->license['type'], [ 'agency', 'ultimate' ], true ) ? 'elite' : $this->license['type']; $this->license['type'] = empty( $this->license['type'] ) ? 'lite' : $this->license['type']; $this->license['index'] = array_search( $this->license['type'], $this->all_licenses, true ); } /** * Init templates and categories data. * * @since 1.6.8 */ private function init_templates_data() { // Get cached templates data. $cache_obj = wpforms()->obj( 'builder_templates_cache' ); if ( ! $cache_obj ) { return; } $cache_data = $cache_obj->get(); $templates_all = ! empty( $cache_data['templates'] ) ? $this->sort_templates_by_created_at( $cache_data['templates'] ) : []; $this->categories = ! empty( $cache_data['categories'] ) ? $cache_data['categories'] : []; $this->subcategories = ! empty( $cache_data['subcategories'] ) ? $cache_data['subcategories'] : []; $this->init_api_templates( $templates_all ); } /** * Sort templates by their created_at value in ascending order. * * @since 1.8.4 * * @param array $templates Templates to be sorted. * * @return array Sorted templates. */ private function sort_templates_by_created_at( array $templates ): array { uasort( $templates, static function ( $template_a, $template_b ) { if ( $template_a['created_at'] === $template_b['created_at'] ) { return 0; } return $template_a['created_at'] < $template_b['created_at'] ? -1 : 1; } ); return $templates; } /** * Determine if user's license level has access to the template. * * @since 1.6.8 * * @param array $template Template data. * * @return bool */ private function has_access( $template ) { if ( ! empty( $template['has_access'] ) ) { return true; } $template_licenses = empty( $template['license'] ) ? [] : array_map( 'strtolower', (array) $template['license'] ); $has_access = true; foreach ( $template_licenses as $template_license ) { $has_access = $this->license['index'] >= array_search( $template_license, $this->all_licenses, true ); if ( $has_access ) { break; } } return $has_access; } /** * Get favorites templates list. * * @since 1.7.7 * * @param bool $all Optional. True for getting all favorites lists. False by default. * * @return array */ public function get_favorites_list( $all = false ) { $favorites_list = (array) get_option( self::FAVORITE_TEMPLATES_OPTION, [] ); if ( $all ) { return $favorites_list; } $user_id = get_current_user_id(); return isset( $favorites_list[ $user_id ] ) ? $favorites_list[ $user_id ] : []; } /** * Update favorites templates list. * * @since 1.8.6 */ public function update_favorites_list() { $this->favorites_list = $this->get_favorites_list(); } /** * Determine if template is marked as favorite. * * @since 1.7.7 * * @param string $template_slug Template slug. * * @return bool */ public function is_favorite( $template_slug ) { if ( $this->favorites_list === null ) { $this->update_favorites_list(); } return isset( $this->favorites_list[ $template_slug ] ); } /** * Save favorites templates. * * @since 1.7.7 */ public function ajax_save_favorites(): void { if ( ! $this->is_valid_ajax_request() ) { wp_send_json_error(); } [ $template_slug, $favorite ] = $this->get_ajax_input(); $favorites = $this->get_favorites_list( true ); $user_id = get_current_user_id(); $is_favorite = $favorite === 'true'; $is_exists = isset( $favorites[ $user_id ][ $template_slug ] ); if ( $is_favorite && $is_exists ) { wp_send_json_success(); } if ( $is_favorite ) { $favorites[ $user_id ][ $template_slug ] = true; } elseif ( $is_exists ) { unset( $favorites[ $user_id ][ $template_slug ] ); } update_option( self::FAVORITE_TEMPLATES_OPTION, $favorites ); // Update and save the template content cache. $templates_cache_obj = wpforms()->obj( 'builder_templates_cache' ); if ( $templates_cache_obj ) { $templates_cache_obj->wipe_content_cache(); } wp_send_json_success(); } /** * Get AJAX input. * * @since 1.9.6 * * @return array */ protected function get_ajax_input(): array { // Nonce is checked in the is_valid_ajax_request() method. // phpcs:disable WordPress.Security.NonceVerification.Missing $template_slug = isset( $_POST['slug'] ) ? sanitize_text_field( wp_unslash( $_POST['slug'] ) ) : ''; $favorite = isset( $_POST['favorite'] ) ? sanitize_key( wp_unslash( $_POST['favorite'] ) ) : ''; return [ $template_slug, $favorite ]; // phpcs:enable WordPress.Security.NonceVerification.Missing } /** * Determine if the AJAX request is valid. * * @since 1.9.5 * * @return bool */ private function is_valid_ajax_request(): bool { return check_ajax_referer( 'wpforms-form-templates', 'nonce', false ) && wpforms_current_user_can( 'create_forms' ) && isset( $_POST['slug'], $_POST['favorite'] ); } /** * Determine if the template exists and the customer has access to it. * * @since 1.7.5.3 * * @param string $slug Template slug or ID. * * @return bool */ public function is_valid_template( $slug ) { $template = $this->get_template_by_id( $slug ); if ( ! $template ) { return ! empty( $this->get_template_by_slug( $slug ) ); } $has_cache = wpforms()->obj( 'builder_template_single' )->instance( $template['id'], $this->license )->get(); return $this->has_access( $template ) && $has_cache; } /** * Determine license level of the template. * * @since 1.6.8 * * @param array $template Template data. * * @return string */ private function get_license_level( $template ) { $licenses_pro = [ 'basic', 'plus', 'pro' ]; $licenses_template = (array) $template['license']; if ( empty( $template['license'] ) || in_array( 'lite', $licenses_template, true ) ) { return ''; } foreach ( $licenses_pro as $license ) { if ( in_array( $license, $licenses_template, true ) ) { return 'pro'; } } return 'elite'; } /** * Get categories data. * * @since 1.6.8 * * @return array */ public function get_categories() { return $this->categories; } /** * Get subcategories data. * * @since 1.8.4 * * @return array */ public function get_subcategories() { return $this->subcategories; } /** * Get templates data. * * @since 1.6.8 * * @return array */ public function get_templates(): array { static $templates = []; if ( ! empty( $templates ) ) { return $templates; } // phpcs:disable WPForms.PHP.ValidateHooks.InvalidHookName /** * Form templates available in the WPForms core plugin. * * @since 1.4.0 * * @param array $templates Core templates data. */ $core_templates = (array) apply_filters( 'wpforms_form_templates_core', [] ); /** * Form templates available with the WPForms addons. * Allows developers to provide additional templates with an addons. * * @since 1.4.0 * * @param array $templates Addons templates data. */ $additional_templates = (array) apply_filters( 'wpforms_form_templates', [] ); // phpcs:enable WPForms.PHP.ValidateHooks.InvalidHookName $templates = array_merge( $core_templates, $additional_templates ); // Generate and store the templates' hash. $this->hash = wp_hash( wp_json_encode( $templates ) ); return $templates; } /** * Get templates' hash. * * @since 1.8.6 * * @return string */ public function get_hash(): string { if ( ! $this->hash ) { $this->get_templates(); } return $this->hash; } /** * Get single template data. * * @since 1.6.8 * * @param string $slug Template slug OR Id. * * @return array */ public function get_template( $slug ) { $template = $this->get_template_by_slug( $slug ); if ( ! $template ) { $template = $this->get_template_by_id( $slug ); } if ( empty( $template ) ) { return []; } if ( empty( $template['id'] ) ) { return $template; } // Attempt to get template with form data (if available). $full_template = wpforms() ->obj( 'builder_template_single' ) ->instance( $template['id'], $this->license ) ->get(); if ( ! empty( $full_template['data'] ) ) { return $full_template; } return $template; } /** * Get template data by slug. * * @since 1.7.5.3 * * @param string $slug Template slug. * * @return array */ private function get_template_by_slug( $slug ) { foreach ( $this->get_templates() as $template ) { if ( ! empty( $template['slug'] ) && $template['slug'] === $slug ) { return $template; } } return []; } /** * Get template data by Id. * * @since 1.6.8 * * @param string $id Template id. * * @return array */ private function get_template_by_id( $id ) { foreach ( $this->api_templates as $template ) { if ( ! empty( $template['id'] ) && $template['id'] === $id ) { return $template; } } return []; } /** * Add templates to the list on the Setup panel. * * @since 1.6.8 * * @param array $templates Templates list. * * @return array */ public function add_templates_to_setup_panel( $templates ) { return array_merge( $templates, $this->api_templates ); } /** * Add template data when form is created. * * @since 1.6.8 * * @param array $args Create form arguments. * @param array $data Template data. * * @return array */ public function apply_to_new_form( $args, $data ) { if ( empty( $data ) || empty( $data['template'] ) ) { return $args; } $template = $this->get_template( $data['template'] ); if ( empty( $template['data'] ) || ! $this->has_access( $template ) ) { return $args; } $template['data']['meta']['template'] = $template['id'] ?? $template['slug']; $template['data']['meta']['category'] = $data['category'] ?? 'all'; $template['data']['meta']['subcategory'] = $data['subcategory'] ?? 'all'; // Enable Notifications by default. $template['data']['settings']['notification_enable'] = isset( $template['data']['settings']['notification_enable'] ) ? $template['data']['settings']['notification_enable'] : 1; // Unset settings that should be defined locally. unset( $template['data']['settings']['form_title'], $template['data']['settings']['conversational_forms_title'], $template['data']['settings']['form_pages_title'] ); // Unset certain values for each Notification, since: // - Email Subject Line field (subject) depends on the form name that is generated from the template name and form_id. // - From Name field (sender_name) depends on the blog name and can be replaced by WP Mail SMTP plugin. // - From Email field (sender_address) depends on the internal logic and can be replaced by WP Mail SMTP plugin. if ( ! empty( $template['data']['settings']['notifications'] ) ) { foreach ( (array) $template['data']['settings']['notifications'] as $key => $notification ) { unset( $template['data']['settings']['notifications'][ $key ]['subject'], $template['data']['settings']['notifications'][ $key ]['sender_name'], $template['data']['settings']['notifications'][ $key ]['sender_address'] ); } } /** * Allow modifying form data when a template is applied to the new form. * * @since 1.9.0 * * @param array $form_data New form data. * @param array $template Template data. */ $template['data'] = (array) apply_filters( 'wpforms_admin_builder_templates_apply_to_new_form_modify_data', $template['data'], $template ); // Encode template data to post content. $args['post_content'] = wpforms_encode( $template['data'] ); return $args; } /** * Add template data when form is updated. * * @since 1.6.8 * * @param array $form Form post data. * @param array $data Form data. * @param array $args Update form arguments. * * @return array */ public function apply_to_existing_form( $form, $data, $args ) { if ( empty( $args ) || empty( $args['template'] ) ) { return $form; } $template = $this->get_template( $args['template'] ); if ( empty( $template['data'] ) || ! $this->has_access( $template ) ) { return $form; } $form_data = wpforms_decode( wp_unslash( $form['post_content'] ) ); // Something is wrong with the form data. if ( empty( $form_data ) ) { return $form; } // Compile the new form data preserving needed data from the existing form. $new = $template['data']; $new['id'] = $form['ID'] ?? 0; $new['field_id'] = $form_data['field_id'] ?? 0; $new['settings'] = $form_data['settings'] ?? []; $new['payments'] = $form_data['payments'] ?? []; $new['meta'] = $form_data['meta'] ?? []; $template_id = $template['id'] ?? ''; // Preserve template ID `wpforms-user-template-{$form_id}` when overwriting it with another template. if ( wpforms_is_form_template( $form['ID'] ) ) { $template_id = $form_data['meta']['template'] ?? ''; } $new['meta']['template'] = $template_id; $new['meta']['category'] = ! empty( $args['category'] ) ? sanitize_text_field( $args['category'] ) : 'all'; $new['meta']['subcategory'] = ! empty( $args['subcategory'] ) ? sanitize_text_field( $args['subcategory'] ) : 'all'; /** * Allow modifying form data when a new template is applied. * * @since 1.7.9 * * @param array $new Updated form data. * @param array $form_data Current form data. * @param array $template Template data. */ $new = (array) apply_filters( 'wpforms_admin_builder_templates_apply_to_existing_form_modify_data', $new, $form_data, $template ); // Update the form with new data. $form['post_content'] = wpforms_encode( $new ); return $form; } /** * Create a form on request. * * @since 1.6.8 */ public function create_form_on_request() { $template = $this->get_template_on_request(); // Just return if template not found OR user doesn't have access. if ( empty( $template['has_access'] ) ) { return; } // Check if the template requires some addons. if ( $this->check_template_required_addons( $template ) ) { return; } // Set form title equal to the template's name. $form_title = ! empty( $template['name'] ) ? $template['name'] : esc_html__( 'New form', 'wpforms-lite' ); $title_query = new WP_Query( [ 'post_type' => 'wpforms', 'title' => $form_title, 'posts_per_page' => 1, 'fields' => 'ids', 'update_post_meta_cache' => false, 'update_post_term_cache' => false, 'no_found_rows' => true, ] ); $title_exists = $title_query->post_count > 0; $form_id = wpforms()->obj( 'form' )->add( $form_title, [], [ 'template' => $template['id'], ] ); // Return if something wrong. if ( ! $form_id ) { return; } // Update form title if duplicated. if ( $title_exists ) { wpforms()->obj( 'form' )->update( $form_id, [ 'settings' => [ 'form_title' => $form_title . ' (ID #' . $form_id . ')', ], ] ); } $this->create_form_on_request_redirect( $form_id ); } /** * Get template data before creating a new form on request. * * @since 1.6.8 * * @return array|bool Template OR false. */ private function get_template_on_request() { if ( ! wpforms_is_admin_page( 'builder' ) || ! wpforms_is_admin_page( 'templates' ) ) { return false; } if ( ! wpforms_current_user_can( 'create_forms' ) ) { return false; } $form_id = isset( $_GET['form_id'] ) ? (int) $_GET['form_id'] : false; // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( ! empty( $form_id ) ) { return false; } $view = isset( $_GET['view'] ) ? sanitize_key( $_GET['view'] ) : 'setup'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( $view !== 'setup' ) { return false; } $template_id = isset( $_GET['template_id'] ) ? sanitize_key( $_GET['template_id'] ) : false; // phpcs:ignore WordPress.Security.NonceVerification.Recommended // Attempt to get the template. $template = $this->get_template( $template_id ); // Just return if template is not found. if ( empty( $template ) ) { return false; } return $template; } /** * Redirect after creating the form. * * @since 1.6.8 * * @param integer $form_id Form ID. */ private function create_form_on_request_redirect( $form_id ) { // Redirect to the builder if possible. if ( wpforms_current_user_can( 'edit_form_single', $form_id ) ) { wp_safe_redirect( add_query_arg( [ 'view' => 'fields', 'form_id' => $form_id, 'newform' => '1', ], admin_url( 'admin.php?page=wpforms-builder' ) ) ); exit; } // Redirect to the forms overview admin page if possible. if ( wpforms_current_user_can( 'view_forms' ) ) { wp_safe_redirect( admin_url( 'admin.php?page=wpforms-overview' ) ); exit; } // Finally, redirect to the admin dashboard. wp_safe_redirect( admin_url() ); exit; } /** * Check if the template requires some addons and then redirect to the builder for further interaction if needed. * * @since 1.6.8 * * @param array $template Template data. * * @return bool True if template requires some addons that are not yet installed and/or activated. */ private function check_template_required_addons( $template ) { // Return false if none addons required. if ( empty( $template['addons'] ) ) { return false; } $required_addons = wpforms()->obj( 'addons' )->get_by_slugs( $template['addons'] ); foreach ( $required_addons as $i => $addon ) { if ( empty( $addon['action'] ) || ! in_array( $addon['action'], [ 'install', 'activate' ], true ) ) { unset( $required_addons[ $i ] ); } } // Return false if not need to install or activate any addons. // We can proceed with creating the form directly in this process. if ( empty( $required_addons ) ) { return false; } // Otherwise return true. return true; } /** * Render the upgrade banner template. * * This method generates the HTML template for the upgrade banner, which includes * a title, description, and a button that links to the upgrade page. * * @param string $title The title to be displayed in the banner. * @param string $description The description to be displayed in the banner. * * @since 1.9.4 */ private function render_upgrade_banner_template( string $title, string $description ): void { $medium = wpforms_is_admin_page( 'templates' ) ? 'Form Templates Subpage' : 'Builder Templates'; ?> <script type="text/html" id="tmpl-wpforms-templates-upgrade-banner"> <div class="wpforms-template-upgrade-banner"> <div class="wpforms-template-content"> <h3> <?php echo esc_html( $title ); ?> </h3> <p> <?php echo esc_html( $description ); ?> </p> </div> <div class="wpforms-template-upgrade-button"> <a href="<?php echo esc_url( wpforms_admin_upgrade_link( $medium, 'Upgrade to Pro' ) ); ?>" class="wpforms-btn wpforms-btn-orange wpforms-btn-md" target="_blank" rel="noopener noreferrer"> <?php esc_html_e( 'Upgrade to Pro', 'wpforms-lite' ); ?> </a> </div> </div> </script> <?php } /** * Render upgrade banner for basic and plus versions. * * @since 1.7.7 */ public function upgrade_banner_template(): void { if ( in_array( wpforms_get_license_type(), [ 'pro', 'elite', 'agency', 'ultimate' ], true ) || ! wpforms()->is_pro() ) { return; } $title = sprintf( /* translators: %d - templates count. */ esc_html__( 'Get Access to Our Complete Library of %d+ Form Templates', 'wpforms-lite' ), esc_html( floor( count( $this->get_templates() ) / 1000 ) * 1000 ) ); $description = esc_html__( 'Save time and reduce effort with our pre-built form templates covering popular use-cases in business operations, customer service, feedback, marketing, registrations, event planning, non-profit, healthcare, and education.', 'wpforms-lite' ); $this->render_upgrade_banner_template( $title, $description ); } /** * Render upgrade banner for lite version. * * @since 1.9.4 */ public function upgrade_lite_banner_template(): void { if ( wpforms()->is_pro() ) { return; } $title = sprintf( /* translators: %d - templates count. */ esc_html__( 'Get Access to Our Library of %d+ Pre-Made Form Templates', 'wpforms-lite' ), esc_html( floor( count( $this->get_templates() ) / 1000 ) * 1000 ) ); $description = esc_html__( 'Never start from scratch again! While WPForms Lite allows you to create any type of form, you can save even more time with WPForms Pro. Upgrade to access hundreds more form templates and advanced form fields.', 'wpforms-lite' ); $this->render_upgrade_banner_template( $title, $description ); } /** * Add additional addons templates. * * @since 1.8.9 * * @param array $templates Templates list. * * @return array */ public function add_addons_templates( array $templates ): array { // Add User Registration templates only if the addon is not active. if ( ! wpforms()->obj( 'addons' )->is_active( 'user-registration' ) ) { $templates = $this->add_user_registration_templates( $templates ); } // Add Post Submissions templates only if the addon is not active. if ( ! wpforms()->obj( 'addons' )->is_active( 'post-submissions' ) ) { $templates = $this->add_post_submissions_templates( $templates ); } // Add Survey and Poll templates only if the addon is not active. if ( ! wpforms()->obj( 'addons' )->is_active( 'surveys-polls' ) ) { $templates = $this->add_surveys_polls_templates( $templates ); } return $templates; } /** * Add User Registration templates. * * @since 1.8.9 * * @param array $templates Templates list. * * @return array */ private function add_user_registration_templates( array $templates ): array { $user_registration_templates = [ [ 'name' => esc_html__( 'User Registration Form', 'wpforms-lite' ), 'slug' => 'user_registration', 'addons' => [ 'user-registration' ], 'license' => $this->get_license_level( [ 'license' => [ 'pro' ] ] ), 'has_access' => $this->has_access( [ 'license' => [ 'pro' ] ] ), 'source' => 'wpforms-addon', 'description' => esc_html__( 'Create customized WordPress user registration forms and add them anywhere on your website.', 'wpforms-lite' ), ], [ 'name' => esc_html__( 'User Login Form', 'wpforms-lite' ), 'slug' => 'user_login', 'addons' => [ 'user-registration' ], 'license' => $this->get_license_level( [ 'license' => [ 'pro' ] ] ), 'has_access' => $this->has_access( [ 'license' => [ 'pro' ] ] ), 'source' => 'wpforms-addon', 'description' => esc_html__( 'Allow your users to easily log in to your site with their username and password.', 'wpforms-lite' ), ], [ 'name' => esc_html__( 'User Password Reset Form', 'wpforms-lite' ), 'slug' => 'user_reset', 'addons' => [ 'user-registration' ], 'license' => $this->get_license_level( [ 'license' => [ 'pro' ] ] ), 'has_access' => $this->has_access( [ 'license' => [ 'pro' ] ] ), 'source' => 'wpforms-addon', 'description' => esc_html__( 'Allow your users to easily reset their password.', 'wpforms-lite' ), ], ]; return array_merge( $templates, $user_registration_templates ); } /** * Add Post Submissions templates. * * @since 1.8.9 * * @param array $templates Templates list. * * @return array */ private function add_post_submissions_templates( array $templates ): array { $post_submissions_templates = [ [ 'name' => esc_html__( 'Blog Post Submission Form', 'wpforms-lite' ), 'slug' => 'post_submission', 'addons' => [ 'post-submissions' ], 'license' => $this->get_license_level( [ 'license' => [ 'pro' ] ] ), 'has_access' => $this->has_access( [ 'license' => [ 'pro' ] ] ), 'source' => 'wpforms-addon', 'description' => esc_html__( 'User-submitted content made easy. Allow your users to submit guest blog posts in WordPress. You can add and remove fields as needed.', 'wpforms-lite' ), ], ]; return array_merge( $templates, $post_submissions_templates ); } /** * Add Surveys and Polls templates. * * @since 1.8.9 * * @param array $templates Templates list. * * @return array */ private function add_surveys_polls_templates( array $templates ): array { $surveys_polls_templates = [ [ 'name' => esc_html__( 'Survey Form', 'wpforms-lite' ), 'slug' => 'survey', 'addons' => [ 'surveys-polls' ], 'license' => $this->get_license_level( [ 'license' => [ 'pro' ] ] ), 'has_access' => $this->has_access( [ 'license' => [ 'pro' ] ] ), 'source' => 'wpforms-addon', 'description' => esc_html__( 'Collect customer feedback, then generate survey reports to determine satisfaction and spot trends.', 'wpforms-lite' ), ], [ 'name' => esc_html__( 'Poll Form', 'wpforms-lite' ), 'slug' => 'poll', 'addons' => [ 'surveys-polls' ], 'license' => $this->get_license_level( [ 'license' => [ 'pro' ] ] ), 'has_access' => $this->has_access( [ 'license' => [ 'pro' ] ] ), 'source' => 'wpforms-addon', 'description' => esc_html__( 'Ask visitors a question and display the results after they provide an answer.', 'wpforms-lite' ), ], [ 'name' => esc_html__( 'NPS Survey Simple Form', 'wpforms-lite' ), 'slug' => 'nps-survey-simple', 'addons' => [ 'surveys-polls' ], 'license' => $this->get_license_level( [ 'license' => [ 'pro' ] ] ), 'has_access' => $this->has_access( [ 'license' => [ 'pro' ] ] ), 'source' => 'wpforms-addon', 'description' => esc_html__( 'Find out if your clients or customers would recommend you to someone else with this basic Net Promoter Score survey template.', 'wpforms-lite' ), ], [ 'name' => esc_html__( 'NPS Survey Enhanced Form', 'wpforms-lite' ), 'slug' => 'nps-survey-enhanced', 'addons' => [ 'surveys-polls' ], 'license' => $this->get_license_level( [ 'license' => [ 'pro' ] ] ), 'has_access' => $this->has_access( [ 'license' => [ 'pro' ] ] ), 'source' => 'wpforms-addon', 'description' => esc_html__( 'Measure customer loyalty and find out exactly what they are thinking with this enhanced Net Promoter Score survey template.', 'wpforms-lite' ), ], ]; return array_merge( $templates, $surveys_polls_templates ); } /** * Init API templates. * * @since 1.9.1 * * @param array $templates_all All templates. * * @return void */ private function init_api_templates( array $templates_all ) { // Higher priority templates slugs. // These remote templates are the replication of the default templates, // which were previously included with the WPForms plugin. $higher_templates_slugs = [ 'simple-contact-form-template', 'request-a-quote-form-template', 'donation-form-template', 'billing-order-form-template', 'newsletter-signup-form-template', 'suggestion-form-template', ]; $templates_access_higher = []; $templates_access = []; $templates_deny_higher = []; $templates_deny = []; /** * The form template was moved to wpforms/includes/templates/class-simple-contact-form.php file. * * @since 1.7.5.3 */ unset( $templates_all['simple-contact-form-template'] ); foreach ( $templates_all as $i => $template ) { $template['has_access'] = $this->has_access( $template ); $template['favorite'] = $this->is_favorite( $i ); $template['license'] = $this->get_license_level( $template ); $template['source'] = 'wpforms-api'; $template['categories'] = ! empty( $template['categories'] ) ? array_keys( $template['categories'] ) : []; $is_higher = in_array( $i, $higher_templates_slugs, true ); if ( $template['has_access'] ) { if ( $is_higher ) { $templates_access_higher[ $i ] = $template; } else { $templates_access[ $i ] = $template; } } elseif ( $is_higher ) { $templates_deny_higher[ $i ] = $template; } else { $templates_deny[ $i ] = $template; } } // Sort higher priority templates according to the slug order. $templates_access_higher = array_replace( array_flip( $higher_templates_slugs ), $templates_access_higher ); $templates_access_higher = array_filter( $templates_access_higher, 'is_array' ); // Finally, merge templates from API. $this->api_templates = array_merge( $templates_access_higher, $templates_access, $templates_deny_higher, $templates_deny ); } } Admin/Builder/Settings/Themes.php 0000644 00000103357 15174710275 0012742 0 ustar 00 <?php namespace WPForms\Admin\Builder\Settings; use WPForms\Frontend\CSSVars; use WPForms\Integrations\Gutenberg\RestApi; use WPForms\Integrations\Gutenberg\ThemesData; use WPForms_Builder_Panel_Settings; /** * Themes panel. * * @since 1.8.8 */ class Themes { /** * Form data. * * @since 1.9.7 * * @var array */ public $form_data; /** * CSS vars class instance. * * @since 1.9.7 * * @var CSSVars */ protected $css_vars_obj; /** * Rest API class instance. * * @since 1.9.7 * * @var ThemesData */ protected $themes_data_obj; /** * Size options for themes settings. * * @since 1.9.7 * * @var array */ protected $size_options; /** * Border type options for themes settings. * * @since 1.9.7 * * @var array */ private $border_options; /** * Is admin. * * @since 1.9.7 * * @var bool */ private $is_admin; /** * Whether a modern engine is enabled. * * @since 1.9.7 * * @var bool */ private $is_modern; /** * Whether full style is used. * * @since 1.9.7 * * @var bool */ private $is_full_styles; /** * Init class. * * @since 1.8.8 */ public function init(): void { $this->css_vars_obj = wpforms()->obj( 'css_vars' ); $this->is_admin = current_user_can( 'manage_options' ); $this->is_modern = wpforms_get_render_engine() === 'modern'; $this->is_full_styles = (int) wpforms_setting( 'disable-css', '1' ) === 1; $this->size_options = [ 'small' => esc_html__( 'Small', 'wpforms-lite' ), 'medium' => esc_html__( 'Medium', 'wpforms-lite' ), 'large' => esc_html__( 'Large', 'wpforms-lite' ), ]; $this->border_options = [ 'none' => esc_html__( 'None', 'wpforms-lite' ), 'solid' => esc_html__( 'Solid', 'wpforms-lite' ), 'dashed' => esc_html__( 'Dashed', 'wpforms-lite' ), 'dotted' => esc_html__( 'Dotted', 'wpforms-lite' ), ]; $this->hooks(); } /** * Register hooks. * * @since 1.8.8 */ protected function hooks(): void { // If the current user can't add posts, he can't save themes either. Enqueue no-access assets. if ( ! current_user_can( 'edit_posts' ) ) { add_action( 'admin_enqueue_scripts', [ $this, 'enqueues_no_access' ] ); add_filter( 'wpforms_builder_panel_sidebar_section_classes', [ $this, 'add_pro_class' ], 10, 3 ); return; } add_action( 'wpforms_form_settings_panel_content', [ $this, 'panel_content' ] ); add_action( 'wpforms_builder_panel_sidebar_after', [ $this, 'sidebar_content' ], 10, 2 ); add_action( 'admin_enqueue_scripts', [ $this, 'enqueues' ] ); } /** * Enqueue assets for the builder themes. * * @since 1.9.7 */ public function enqueues(): void { $min = wpforms_get_min_suffix(); wp_enqueue_script( 'wpforms-builder-themes', WPFORMS_PLUGIN_URL . "assets/js/admin/builder/themes/builder-themes{$min}.js", [ 'wpforms-builder', 'wp-api-fetch' ], WPFORMS_VERSION, true ); wp_enqueue_style( 'wpforms-full', WPFORMS_PLUGIN_URL . "assets/css/frontend/modern/wpforms-full{$min}.css", [], WPFORMS_VERSION ); wp_localize_script( 'wpforms-builder-themes', 'wpforms_builder_themes', $this->get_localize_data() ); wp_add_inline_style( 'wpforms-full', $this->css_vars_obj->get_root_vars_css() ); } /** * Enqueue assets for the builder themes for the users who don't have access to the theme settings. * * @since 1.9.8 */ public function enqueues_no_access(): void { $min = wpforms_get_min_suffix(); wp_enqueue_script( 'wpforms-builder-themes-no-access', WPFORMS_PLUGIN_URL . "assets/js/admin/builder/themes/builder-themes-no-access{$min}.js", [ 'wpforms-builder', 'wp-api-fetch' ], WPFORMS_VERSION, true ); wp_localize_script( 'wpforms-builder-themes-no-access', 'wpforms_builder_themes_no_access', $this->get_localize_data() ); } /** * Add a class to the themes section if the user doesn't have access to it. * * @since 1.9.8 * * @param array $classes Sidebar section classes. * @param string $name Sidebar section name. * @param string $slug Sidebar section slug. * * @return array * @noinspection PhpUnusedParameterInspection */ public function add_pro_class( array $classes, string $name, string $slug ): array { if ( $slug !== 'themes' ) { return $classes; } return array_merge( $classes, [ 'wpforms-panel-sidebar-section-no-access' ] ); } /** * Get localize data. * * @since 1.9.7 * * @return array */ protected function get_localize_data(): array { return [ 'modules' => $this->get_modules(), 'sizes' => [ 'field-size' => CSSVars::FIELD_SIZE, 'label-size' => CSSVars::LABEL_SIZE, 'button-size' => CSSVars::BUTTON_SIZE, 'container-shadow-size' => CSSVars::CONTAINER_SHADOW_SIZE, ], 'strings' => [ 'heads_up' => esc_html__( 'Heads Up!', 'wpforms-lite' ), 'themes_error' => esc_html__( 'Error loading themes. Please try again later.', 'wpforms-lite' ), 'button_background' => esc_html__( 'Button Background', 'wpforms-lite' ), 'button_text' => esc_html__( 'Button Text', 'wpforms-lite' ), 'copy_paste_error' => esc_html__( 'There was an error parsing your JSON code. Please check your code and try again.', 'wpforms-lite' ), 'field_label' => esc_html__( 'Field Label', 'wpforms-lite' ), 'field_sublabel' => esc_html__( 'Field Sublabel', 'wpforms-lite' ), 'field_border' => esc_html__( 'Field Border', 'wpforms-lite' ), 'theme_delete_title' => esc_html__( 'Delete Form Theme', 'wpforms-lite' ), // Translators: %1$s: Theme name. 'theme_delete_confirm' => esc_html__( 'Are you sure you want to delete the %1$s theme?', 'wpforms-lite' ), 'theme_delete_cant_undone' => esc_html__( 'This cannot be undone.', 'wpforms-lite' ), 'theme_delete_yes' => esc_html__( 'Yes, Delete', 'wpforms-lite' ), 'theme_copy' => esc_html__( 'Copy', 'wpforms-lite' ), 'theme_custom' => esc_html__( 'Custom Theme', 'wpforms-lite' ), 'theme_noname' => esc_html__( 'Noname Theme', 'wpforms-lite' ), 'pro_sections' => [ 'background' => esc_html__( 'Background Styles', 'wpforms-lite' ), 'container' => esc_html__( 'Container Styles', 'wpforms-lite' ), 'themes' => esc_html__( 'Themes', 'wpforms-lite' ), ], 'permission_modal' => [ 'title' => esc_html__( 'Insufficient Permissions', 'wpforms-lite' ), 'content' => esc_html__( "Sorry, your user role doesn't have permission to access this feature.", 'wpforms-lite' ), 'confirm' => esc_html__( 'OK', 'wpforms-lite' ), ], ], 'isAdmin' => $this->is_admin, 'isPro' => wpforms()->is_pro(), 'isModern' => $this->is_modern, 'isFullStyles' => $this->is_full_styles, 'route_namespace' => RestApi::ROUTE_NAMESPACE, ]; } /** * Get Form Builder themes modules. * * @since 1.9.7 * * @return array Modules list. */ public function get_modules(): array { $min = wpforms_get_min_suffix(); return [ [ 'name' => 'common', 'path' => "./modules/common{$min}.js", ], [ 'name' => 'themes', 'path' => "./modules/themes{$min}.js", ], [ 'name' => 'stockPhotos', 'path' => "./modules/stock-photos{$min}.js", ], [ 'name' => 'background', 'path' => "./modules/background{$min}.js", ], [ 'name' => 'advancedSettings', 'path' => "./modules/advanced-settings{$min}.js", ], ]; } /** * Add a content for `Themes` panel. * * @since 1.8.8 * * @param WPForms_Builder_Panel_Settings $instance Settings panel instance. * * @noinspection HtmlUnknownTarget */ public function panel_content( WPForms_Builder_Panel_Settings $instance ): void { $this->form_data = $instance->form_data; $url = wpforms_utm_link( 'https://wpforms.com/docs/styling-your-forms/', 'Builder Themes', 'Description Link' ); ?> <div class="wpforms-panel-content-section wpforms-panel-content-section-themes"> <div class="wpforms-panel-content-section-themes-inner"> <div class="wpforms-panel-content-section-themes-top"> <div class="wpforms-panel-content-section-title"> <?php esc_html_e( 'Form Themes', 'wpforms-lite' ); ?> </div> <div class="wpforms-panel-content-section-themes-preview"> <p class="wpforms-panel-content-section-themes-preview-description"> <?php echo wp_kses_post( sprintf( /* translators: %s - URL to the documentation. */ __( 'Customize the look and feel of your form with premade themes or simple style settings that allow you to use your own colors to match your brand. Themes and style settings are also available in the Block Editor and Elementor, where you can see a realtime preview. <a href="%s" target="_blank">Learn more about styling your forms</a>.', 'wpforms-lite' ), $url ) ); ?> </p> <div class="wpforms-alert wpforms-alert-warning wpforms-alert-warning-wide wpforms-builder-themes-preview-notice"> <h4> <?php esc_html_e( 'Preview only', 'wpforms-lite' ); ?> </h4> <p> <?php esc_html_e( 'The fields shown below are for demo purposes and do not reflect the fields in your actual form.', 'wpforms-lite' ); ?> </p> </div> <?php echo wpforms_render( // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped 'builder/themes/notices', [ 'is_modern' => $this->is_modern, 'is_full_styles' => $this->is_full_styles, ], true ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo wpforms_render( 'builder/themes/preview' ); ?> </div> </div> <!-- .wpforms-panel-content-section-themes-top --> </div> <!-- .wpforms-panel-content-section-themes-inner --> </div> <!-- .wpforms-panel-content-section-themes --> <?php } /** * Add content for the Themes Sidebar. * * @param object $form Current form object. * @param string $slug Current panel slug. * * @since 1.9.7 */ public function sidebar_content( $form, $slug ): void { if ( $slug !== 'settings' ) { return; } $form_obj = wpforms()->obj( 'form' ); if ( ! $form_obj || ! isset( $form->ID ) ) { return; } $form_data = $form_obj->get( $form->ID, [ 'content_only' => true ] ); $this->form_data = $form_data; $this->show_sidebar_html(); } /** * Show sidebar HTML. * * @since 1.9.7 */ private function show_sidebar_html(): void { ?> <div id="wpforms-builder-themes-sidebar" class="wpforms-hidden"> <div class="wpforms-builder-themes-sidebar-head"> <button id="wpforms-builder-themes-back"> <?php esc_html_e( 'Back to Settings', 'wpforms-lite' ); ?></button> </div> <div id="wpforms-builder-themes-sidebar-tabs"> <a href="#" class="active"><?php esc_html_e( 'General', 'wpforms-lite' ); ?></a> <?php if ( $this->is_admin ) : ?> <a href="#"><?php esc_html_e( 'Advanced', 'wpforms-lite' ); ?></a> <?php endif; ?> </div> <div class="wpforms-builder-themes-sidebar-content"> <div class="wpforms-builder-themes-sidebar-general wpforms-builder-themes-sidebar-tab-content"> <?php $this->show_sidebar_themes(); ?> <div class="wpforms-builder-themes-restricted <?php echo esc_attr( ! $this->is_admin ? 'wpforms-hidden' : '' ); ?>"> <?php $this->show_sidebar_field_styles(); ?> <?php $this->show_sidebar_label_styles(); ?> <?php $this->show_sidebar_button_styles(); ?> <?php $this->show_sidebar_container_styles(); ?> <?php $this->show_sidebar_background_styles(); ?> <?php $this->show_sidebar_other_styles(); ?> </div> </div> <div class="wpforms-builder-themes-sidebar-advanced wpforms-builder-themes-sidebar-tab-content wpforms-hidden"> <?php $this->show_sidebar_advanced(); ?> </div> </div> </div> <?php } /** * Show sidebar themes. * * @since 1.9.7 * * @return void */ private function show_sidebar_themes(): void { ?> <div class="wpforms-add-fields-group"> <a href="#" class="wpforms-add-fields-heading" data-group="themes"> <span><?php esc_html_e( 'Themes', 'wpforms-lite' ); ?></span> <i class="fa fa-angle-down"></i> </a> <div class="wpforms-add-fields-buttons"> <?php wpforms_panel_field( 'text', 'themes', 'wpformsTheme', $this->form_data, esc_html__( 'Theme', 'wpforms-lite' ), [ 'parent' => 'settings', 'type' => 'hidden', 'value' => $this->form_data['settings']['themes']['wpformsTheme'] ?? 'default', 'class' => 'wpforms-hidden', ] ); wpforms_panel_field( 'text', 'themes', 'isCustomTheme', $this->form_data, false, [ 'parent' => 'settings', 'type' => 'hidden', 'value' => $this->form_data['settings']['themes']['isCustomTheme'] ?? '', 'class' => 'wpforms-hidden', ] ); ?> <div class="wpforms-builder-themes-control"></div> <?php wpforms_panel_field( 'text', 'themes', 'themeName', $this->form_data, esc_html__( 'Theme Name', 'wpforms-lite' ), [ 'parent' => 'settings', 'type' => 'text', 'value' => $this->form_data['settings']['themes']['themeName'] ?? '', 'class' => 'wpforms-hidden', ] ); ?> <button id="wpforms-builder-themer-remove-theme" class="wpforms-hidden"><?php esc_html_e( 'Delete Theme', 'wpforms-lite' ); ?></button> </div> </div> <?php } /** * Show sidebar field styles. * * @since 1.9.7 * * @return void */ private function show_sidebar_field_styles(): void { ?> <div class="wpforms-add-fields-group"> <a href="#" class="wpforms-add-fields-heading" data-group="field_styles"> <span><?php esc_html_e( 'Field Styles', 'wpforms-lite' ); ?></span> <i class="fa fa-angle-down"></i> </a> <div class="wpforms-add-fields-buttons"> <div class="wpforms-builder-themes-fields-row"> <?php wpforms_panel_field( 'select', 'themes', 'fieldSize', $this->form_data, esc_html__( 'Size', 'wpforms-lite' ), [ 'parent' => 'settings', 'options' => $this->size_options, 'value' => $this->form_data['settings']['themes']['fieldSize'] ?? 'medium', ] ); wpforms_panel_field( 'select', 'themes', 'fieldBorderStyle', $this->form_data, esc_html__( 'Border', 'wpforms-lite' ), [ 'parent' => 'settings', 'options' => $this->border_options, 'value' => $this->form_data['settings']['themes']['fieldBorderStyle'] ?? 'solid', ] ); ?> </div> <div class="wpforms-builder-themes-fields-row"> <?php wpforms_panel_field( 'text', 'themes', 'fieldBorderSize', $this->form_data, esc_html__( 'Border Size', 'wpforms-lite' ), [ 'parent' => 'settings', 'type' => 'number', 'min' => 0, 'value' => $this->form_data['settings']['themes']['fieldBorderSize'] ?? '1', 'input_class' => 'wpforms-builder-themes-number-input', 'class' => 'wpforms-builder-themes-number-input-wrapper', ] ); wpforms_panel_field( 'text', 'themes', 'fieldBorderRadius', $this->form_data, esc_html__( 'Border Radius', 'wpforms-lite' ), [ 'parent' => 'settings', 'type' => 'number', 'min' => 0, 'value' => $this->form_data['settings']['themes']['fieldBorderRadius'] ?? '3', 'input_class' => 'wpforms-builder-themes-number-input', 'class' => 'wpforms-builder-themes-number-input-wrapper', ] ); ?> </div> <div class="wpforms-builder-themes-fields-row"> <?php wpforms_panel_field( 'color', 'themes', 'fieldBackgroundColor', $this->form_data, esc_html__( 'Background', 'wpforms-lite' ), [ 'parent' => 'settings', 'value' => $this->form_data['settings']['themes']['fieldBackgroundColor'] ?? CSSVars::ROOT_VARS['field-background-color'], ] ); wpforms_panel_field( 'color', 'themes', 'fieldBorderColor', $this->form_data, esc_html__( 'Border', 'wpforms-lite' ), [ 'parent' => 'settings', 'value' => $this->form_data['settings']['themes']['fieldBorderColor'] ?? CSSVars::ROOT_VARS['field-border-color'], ] ); wpforms_panel_field( 'color', 'themes', 'fieldTextColor', $this->form_data, esc_html__( 'Text', 'wpforms-lite' ), [ 'parent' => 'settings', 'value' => $this->form_data['settings']['themes']['fieldTextColor'] ?? CSSVars::ROOT_VARS['field-text-color'], ] ); ?> </div> </div> </div> <?php } /** * Show sidebar label styles. * * @since 1.9.7 * * @return void */ private function show_sidebar_label_styles(): void { ?> <div class="wpforms-add-fields-group"> <a href="#" class="wpforms-add-fields-heading" data-group="label_styles"> <span><?php esc_html_e( 'Label Styles', 'wpforms-lite' ); ?></span> <i class="fa fa-angle-down"></i> </a> <div class="wpforms-add-fields-buttons"> <div class="wpforms-builder-themes-fields-row"> <?php wpforms_panel_field( 'select', 'themes', 'labelSize', $this->form_data, esc_html__( 'Size', 'wpforms-lite' ), [ 'parent' => 'settings', 'options' => $this->size_options, 'value' => $this->form_data['settings']['themes']['labelSize'] ?? 'medium', ] ); ?> </div> <div class="wpforms-builder-themes-fields-row"> <?php wpforms_panel_field( 'color', 'themes', 'labelColor', $this->form_data, esc_html__( 'Label', 'wpforms-lite' ), [ 'parent' => 'settings', 'value' => $this->form_data['settings']['themes']['labelColor'] ?? CSSVars::ROOT_VARS['label-color'], ] ); wpforms_panel_field( 'color', 'themes', 'labelSublabelColor', $this->form_data, esc_html__( 'Sublabel', 'wpforms-lite' ), [ 'parent' => 'settings', 'value' => $this->form_data['settings']['themes']['labelSublabelColor'] ?? CSSVars::ROOT_VARS['label-sublabel-color'], ] ); wpforms_panel_field( 'color', 'themes', 'labelErrorColor', $this->form_data, esc_html__( 'Error', 'wpforms-lite' ), [ 'parent' => 'settings', 'value' => $this->form_data['settings']['themes']['labelErrorColor'] ?? CSSVars::ROOT_VARS['label-error-color'], ] ); ?> </div> </div> </div> <?php } /** * Show sidebar button styles. * * @since 1.9.7 * * @return void */ private function show_sidebar_button_styles(): void { ?> <div class="wpforms-add-fields-group"> <a href="#" class="wpforms-add-fields-heading" data-group="button_styles"> <span><?php esc_html_e( 'Button Styles', 'wpforms-lite' ); ?></span> <i class="fa fa-angle-down"></i> </a> <div class="wpforms-add-fields-buttons"> <div class="wpforms-builder-themes-fields-row"> <?php wpforms_panel_field( 'select', 'themes', 'buttonSize', $this->form_data, esc_html__( 'Size', 'wpforms-lite' ), [ 'parent' => 'settings', 'options' => $this->size_options, 'value' => $this->form_data['settings']['themes']['buttonSize'] ?? 'medium', ] ); wpforms_panel_field( 'select', 'themes', 'buttonBorderStyle', $this->form_data, esc_html__( 'Border', 'wpforms-lite' ), [ 'parent' => 'settings', 'options' => $this->border_options, 'value' => $this->form_data['settings']['themes']['buttonBorderStyle'] ?? CSSVars::ROOT_VARS['button-border-style'], ] ); ?> </div> <div class="wpforms-builder-themes-fields-row"> <?php wpforms_panel_field( 'text', 'themes', 'buttonBorderSize', $this->form_data, esc_html__( 'Border Size', 'wpforms-lite' ), [ 'parent' => 'settings', 'type' => 'number', 'min' => 0, 'value' => $this->form_data['settings']['themes']['buttonBorderSize'] ?? '1', 'input_class' => 'wpforms-builder-themes-number-input', 'class' => 'wpforms-builder-themes-number-input-wrapper', ] ); wpforms_panel_field( 'text', 'themes', 'buttonBorderRadius', $this->form_data, esc_html__( 'Border Radius', 'wpforms-lite' ), [ 'parent' => 'settings', 'type' => 'number', 'min' => 0, 'value' => $this->form_data['settings']['themes']['buttonBorderRadius'] ?? '3', 'input_class' => 'wpforms-builder-themes-number-input', 'class' => 'wpforms-builder-themes-number-input-wrapper', ] ); ?> </div> <div class="wpforms-builder-themes-fields-row"> <?php wpforms_panel_field( 'color', 'themes', 'buttonBackgroundColor', $this->form_data, esc_html__( 'Background', 'wpforms-lite' ), [ 'parent' => 'settings', 'value' => $this->form_data['settings']['themes']['buttonBackgroundColor'] ?? CSSVars::ROOT_VARS['button-background-color'], ] ); wpforms_panel_field( 'color', 'themes', 'buttonBorderColor', $this->form_data, esc_html__( 'Border', 'wpforms-lite' ), [ 'parent' => 'settings', 'value' => $this->form_data['settings']['themes']['buttonBorderColor'] ?? CSSVars::ROOT_VARS['button-border-color'], ] ); wpforms_panel_field( 'color', 'themes', 'buttonTextColor', $this->form_data, esc_html__( 'Text', 'wpforms-lite' ), [ 'parent' => 'settings', 'value' => $this->form_data['settings']['themes']['buttonTextColor'] ?? CSSVars::ROOT_VARS['button-text-color'], ] ); ?> </div> </div> </div> <?php } /** * Show sidebar container styles. * * @since 1.9.7 * * @return void */ private function show_sidebar_container_styles(): void { ?> <div class="wpforms-add-fields-group"> <a href="#" class="wpforms-add-fields-heading" data-group="container_styles"> <span><?php esc_html_e( 'Container Styles', 'wpforms-lite' ); ?></span> <i class="fa fa-angle-down"></i> </a> <div class="wpforms-add-fields-buttons wpforms-builder-themes-pro-section"> <div class="wpforms-builder-themes-fields-row"> <?php wpforms_panel_field( 'text', 'themes', 'containerPadding', $this->form_data, esc_html__( 'Padding', 'wpforms-lite' ), [ 'parent' => 'settings', 'type' => 'number', 'min' => 0, 'value' => $this->form_data['settings']['themes']['containerPadding'] ?? '0', 'input_class' => 'wpforms-builder-themes-number-input', 'class' => 'wpforms-builder-themes-number-input-wrapper', ] ); wpforms_panel_field( 'select', 'themes', 'containerBorderStyle', $this->form_data, esc_html__( 'Border', 'wpforms-lite' ), [ 'parent' => 'settings', 'options' => $this->border_options, 'value' => $this->form_data['settings']['themes']['containerBorderStyle'] ?? CSSVars::ROOT_VARS['container-border-style'], ] ); ?> </div> <div class="wpforms-builder-themes-fields-row"> <?php wpforms_panel_field( 'text', 'themes', 'containerBorderWidth', $this->form_data, esc_html__( 'Border Size', 'wpforms-lite' ), [ 'parent' => 'settings', 'type' => 'number', 'min' => 0, 'value' => $this->form_data['settings']['themes']['containerBorderWidth'] ?? '1', 'input_class' => 'wpforms-builder-themes-number-input', 'class' => 'wpforms-builder-themes-number-input-wrapper', ] ); wpforms_panel_field( 'text', 'themes', 'containerBorderRadius', $this->form_data, esc_html__( 'Border Radius', 'wpforms-lite' ), [ 'parent' => 'settings', 'type' => 'number', 'min' => 0, 'value' => $this->form_data['settings']['themes']['containerBorderRadius'] ?? '3', 'input_class' => 'wpforms-builder-themes-number-input', 'class' => 'wpforms-builder-themes-number-input-wrapper', ] ); ?> </div> <div class="wpforms-builder-themes-fields-row"> <?php wpforms_panel_field( 'color', 'themes', 'containerBorderColor', $this->form_data, esc_html__( 'Border', 'wpforms-lite' ), [ 'parent' => 'settings', 'value' => $this->form_data['settings']['themes']['containerBorderColor'] ?? CSSVars::ROOT_VARS['container-border-color'], ] ); wpforms_panel_field( 'select', 'themes', 'containerShadowSize', $this->form_data, esc_html__( 'Shadow', 'wpforms-lite' ), [ 'parent' => 'settings', 'options' => [ 'none' => esc_html__( 'None', 'wpforms-lite' ), 'small' => esc_html__( 'Small', 'wpforms-lite' ), 'medium' => esc_html__( 'Medium', 'wpforms-lite' ), 'large' => esc_html__( 'Large', 'wpforms-lite' ), ], 'value' => $this->form_data['settings']['themes']['containerShadowSize'] ?? CSSVars::CONTAINER_SHADOW_SIZE['none']['box-shadow'], ] ); ?> </div> </div> </div> <?php } /** * Show sidebar background styles. * * @since 1.9.7 * * @return void */ private function show_sidebar_background_styles(): void { ?> <div class="wpforms-add-fields-group"> <a href="#" class="wpforms-add-fields-heading" data-group="background_styles"><span><?php esc_html_e( 'Background Styles', 'wpforms-lite' ); ?></span><i class="fa fa-angle-down"></i></a> <div class="wpforms-add-fields-buttons wpforms-builder-themes-pro-section"> <div class="wpforms-builder-themes-fields-row"> <?php wpforms_panel_field( 'color', 'themes', 'backgroundColor', $this->form_data, esc_html__( 'Color', 'wpforms-lite' ), [ 'parent' => 'settings', 'value' => $this->form_data['settings']['themes']['backgroundColor'] ?? CSSVars::ROOT_VARS['background-color'], ] ); ?> </div> <div class="wpforms-builder-themes-fields-row"> <?php wpforms_panel_field( 'select', 'themes', 'backgroundImage', $this->form_data, esc_html__( 'Image', 'wpforms-lite' ), [ 'parent' => 'settings', 'options' => [ 'none' => esc_html__( 'None', 'wpforms-lite' ), 'library' => esc_html__( 'Media Library', 'wpforms-lite' ), 'stock' => esc_html__( 'Stock Photo', 'wpforms-lite' ), ], 'value' => $this->form_data['settings']['themes']['backgroundImage'] ?? 'none', ] ); wpforms_panel_field( 'select', 'themes', 'backgroundPosition', $this->form_data, esc_html__( 'Position', 'wpforms-lite' ), [ 'parent' => 'settings', 'options' => [ 'top left' => esc_html__( 'Top Left', 'wpforms-lite' ), 'top center' => esc_html__( 'Top Center', 'wpforms-lite' ), 'top right' => esc_html__( 'Top Right', 'wpforms-lite' ), 'center left' => esc_html__( 'Center Left', 'wpforms-lite' ), 'center center' => esc_html__( 'Center Center', 'wpforms-lite' ), 'center right' => esc_html__( 'Center Right', 'wpforms-lite' ), 'bottom left' => esc_html__( 'Bottom Left', 'wpforms-lite' ), 'bottom center' => esc_html__( 'Bottom Center', 'wpforms-lite' ), 'bottom right' => esc_html__( 'Bottom Right', 'wpforms-lite' ), ], 'value' => $this->form_data['settings']['themes']['backgroundPosition'] ?? CSSVars::ROOT_VARS['background-position'], ] ); ?> </div> <div class="wpforms-builder-themes-fields-row wpforms-builder-themes-conditional-hide"> <?php wpforms_panel_field( 'select', 'themes', 'backgroundRepeat', $this->form_data, esc_html__( 'Repeat', 'wpforms-lite' ), [ 'parent' => 'settings', 'options' => [ 'no-repeat' => esc_html__( 'No Repeat', 'wpforms-lite' ), 'repeat' => esc_html__( 'Tile', 'wpforms-lite' ), 'repeat-x' => esc_html__( 'Repeat X', 'wpforms-lite' ), 'repeat-y' => esc_html__( 'Repeat Y', 'wpforms-lite' ), ], 'value' => $this->form_data['settings']['themes']['backgroundRepeat'] ?? CSSVars::ROOT_VARS['background-repeat'], ] ); wpforms_panel_field( 'select', 'themes', 'backgroundSizeMode', $this->form_data, esc_html__( 'Size', 'wpforms-lite' ), [ 'parent' => 'settings', 'options' => [ 'dimensions' => esc_html__( 'Dimensions', 'wpforms-lite' ), 'cover' => esc_html__( 'Cover', 'wpforms-lite' ), ], 'value' => $this->form_data['settings']['themes']['backgroundSizeMode'] ?? CSSVars::ROOT_VARS['background-size'], ] ); wpforms_panel_field( 'text', 'themes', 'backgroundSize', $this->form_data, false, [ 'parent' => 'settings', 'type' => 'hidden', 'value' => $this->form_data['settings']['themes']['backgroundSize'] ?? CSSVars::ROOT_VARS['background-size'], 'class' => 'wpforms-hidden', ] ); ?> </div> <div class="wpforms-builder-themes-fields-row wpforms-builder-themes-conditional-hide"> <?php wpforms_panel_field( 'text', 'themes', 'backgroundWidth', $this->form_data, esc_html__( 'Width', 'wpforms-lite' ), [ 'parent' => 'settings', 'type' => 'number', 'min' => 0, 'value' => $this->form_data['settings']['themes']['backgroundWidth'] ?? '100', 'input_class' => 'wpforms-builder-themes-number-input', 'class' => 'wpforms-builder-themes-number-input-wrapper', ] ); wpforms_panel_field( 'text', 'themes', 'backgroundHeight', $this->form_data, esc_html__( 'Height', 'wpforms-lite' ), [ 'parent' => 'settings', 'type' => 'number', 'min' => 0, 'value' => $this->form_data['settings']['themes']['backgroundHeight'] ?? '100', 'input_class' => 'wpforms-builder-themes-number-input', 'class' => 'wpforms-builder-themes-number-input-wrapper', ] ); ?> </div> <div class="wpforms-builder-themes-background-selector wpforms-hidden"> <?php wpforms_panel_field( 'text', 'themes', 'backgroundUrl', $this->form_data, false, [ 'parent' => 'settings', 'type' => 'hidden', 'value' => $this->form_data['settings']['themes']['backgroundUrl'] ?? CSSVars::ROOT_VARS['background-url'], 'class' => 'wpforms-hidden', ] ); ?> <button class="wpforms-builder-themes-bg-image-choose wpforms-hidden"><?php esc_html_e( 'Choose Image', 'wpforms-lite' ); ?></button> <div class="wpforms-builder-themes-bg-image-preview wpforms-hidden"></div> <button class="wpforms-builder-themes-bg-image-remove wpforms-hidden"><?php esc_html_e( 'Remove Image', 'wpforms-lite' ); ?></button> </div> </div> </div> <?php } /** * Show sidebar background styles. * * @since 1.9.7 * * @return void */ private function show_sidebar_other_styles(): void { ?> <div class="wpforms-add-fields-group wpforms-hidden"> <a href="#" class="wpforms-add-fields-heading" data-group="other_styles"><span><?php esc_html_e( 'Other Styles', 'wpforms-lite' ); ?></span><i class="fa fa-angle-down"></i></a> <div class="wpforms-add-fields-buttons"> <?php wpforms_panel_field( 'color', 'themes', 'fieldMenuColor', $this->form_data, false, [ 'parent' => 'settings', 'value' => $this->form_data['settings']['themes']['fieldMenuColor'] ?? CSSVars::ROOT_VARS['field-menu-color'], ] ); wpforms_panel_field( 'color', 'themes', 'pageBreakColor', $this->form_data, false, [ 'parent' => 'settings', 'value' => $this->form_data['settings']['themes']['pageBreakColor'] ?? CSSVars::ROOT_VARS['page-break-color'], ] ); ?> </div> </div> <?php } /** * Show sidebar background styles. * * @since 1.9.7 * * @return void */ private function show_sidebar_advanced(): void { if ( ! $this->is_admin ) { return; } ?> <?php wpforms_panel_field( 'textarea', 'themes', 'customCss', $this->form_data, esc_html__( 'Custom CSS', 'wpforms-lite' ), [ 'parent' => 'settings', 'value' => $this->form_data['settings']['themes']['customCss'] ?? '', 'after' => sprintf( '<span class="wpforms-panel-field-after">%s</span>', __( 'Further customize the look of this form without having to edit theme files.', 'wpforms-lite' ) ), ] ); wpforms_panel_field( 'textarea', 'themes', 'copyPasteJsonValue', $this->form_data, esc_html__( 'Copy / Paste Style Settings', 'wpforms-lite' ), [ 'parent' => 'settings', 'value' => $this->form_data['settings']['themes']['copyPasteJsonValue'] ?? '', 'after' => sprintf( '<span class="wpforms-panel-field-after">%s</span>', __( 'If you\'ve copied style settings from another form, you can paste them here to add the same styling to this form. Any current style settings will be overwritten.', 'wpforms-lite' ) ), ] ); ?> <?php } } Admin/Builder/AntiSpam.php 0000644 00000030762 15174710275 0011430 0 ustar 00 <?php namespace WPForms\Admin\Builder; use WPForms\Forms\Akismet; use WPForms_Builder_Panel_Settings; /** * AntiSpam class. * * @since 1.7.8 */ class AntiSpam { /** * Form data and settings. * * @since 1.7.8 * * @var array */ private $form_data; /** * Init class. * * @since 1.7.8 */ public function init() { $this->hooks(); } /** * Register hooks. * * @since 1.7.8 */ protected function hooks() { add_action( 'wpforms_form_settings_panel_content', [ $this, 'panel_content' ], 10, 2 ); } /** * Add a content for `Spam Protection and Security` panel. * * @since 1.7.8 * * @param WPForms_Builder_Panel_Settings $instance Settings panel instance. */ public function panel_content( $instance ) { $this->form_data = $this->update_settings_form_data( $instance->form_data ); echo '<div class="wpforms-panel-content-section wpforms-panel-content-section-anti_spam">'; echo '<div class="wpforms-panel-content-section-title">'; esc_html_e( 'Spam Protection and Security', 'wpforms-lite' ); echo '</div>'; $antispam = wpforms_panel_field( 'toggle', 'settings', 'antispam_v3', $this->form_data, __( 'Enable modern anti-spam protection', 'wpforms-lite' ), [ 'value' => (int) ! empty( $this->form_data['settings']['antispam_v3'] ), 'tooltip' => __( 'Turn on invisible modern spam protection.', 'wpforms-lite' ), ], false ); wpforms_panel_fields_group( $antispam, [ 'description' => __( 'Behind-the-scenes spam filtering that\'s invisible to your visitors.', 'wpforms-lite' ), 'title' => __( 'Protection', 'wpforms-lite' ), ] ); if ( ! empty( $this->form_data['settings']['antispam'] ) && empty( $this->form_data['settings']['antispam_v3'] ) ) { wpforms_panel_field( 'toggle', 'settings', 'antispam', $this->form_data, __( 'Enable anti-spam protection', 'wpforms-lite' ), [ 'tooltip' => __( 'Turn on invisible spam protection.', 'wpforms-lite' ), ] ); } if ( ! empty( $this->form_data['settings']['honeypot'] ) && empty( $this->form_data['settings']['antispam_v3'] ) ) { wpforms_panel_field( 'toggle', 'settings', 'honeypot', $this->form_data, __( 'Enable anti-spam honeypot', 'wpforms-lite' ) ); } $this->akismet_settings(); $this->store_spam_entries_settings(); $this->time_limit_settings(); $this->captcha_settings(); // Hidden setting to store blocked entries by filtering as a spam. // This setting is needed to keep backward compatibility with old forms. wpforms_panel_field( 'checkbox', 'anti_spam', 'filtering_store_spam', $this->form_data, '', [ 'parent' => 'settings', 'class' => 'wpforms-hidden', ] ); /** * Fires once in the end of content panel before Also Available section. * * @since 1.7.8 * * @param array $form_data Form data and settings. */ do_action( 'wpforms_admin_builder_anti_spam_panel_content', $this->form_data ); wpforms_panel_fields_group( $this->get_also_available_block(), [ 'unfoldable' => true, 'default' => 'opened', 'group' => 'also_available', 'title' => __( 'Also Available', 'wpforms-lite' ), 'borders' => [ 'top' ], ] ); echo '</div>'; } /** * Update the form data on the builder settings panel. * * @since 1.9.2 * * @param array $form_data Form data. * * @return array */ private function update_settings_form_data( array $form_data ): array { if ( ! $form_data ) { return $form_data; } // Update `Filtering` store spam entries behaviour. // Enable for new forms and old forms without any `Filtering` setting enabled. if ( empty( $form_data['settings']['anti_spam']['filtering_store_spam'] ) && empty( $form_data['settings']['anti_spam']['country_filter']['enable'] ) && empty( $form_data['settings']['anti_spam']['keyword_filter']['enable'] ) ) { $form_data['settings']['anti_spam']['filtering_store_spam'] = true; } return $form_data; } /** * Output the *CAPTCHA settings. * * @since 1.7.8 */ private function captcha_settings() { $captcha_settings = wpforms_get_captcha_settings(); if ( empty( $captcha_settings['provider'] ) || $captcha_settings['provider'] === 'none' ) { return; } if ( $captcha_settings['provider'] !== 'hcaptcha' && ( empty( $captcha_settings['site_key'] ) || empty( $captcha_settings['secret_key'] ) ) ) { return; } if ( $captcha_settings['provider'] === 'hcaptcha' && empty( $captcha_settings['site_key'] ) ) { return; } $captcha_types = [ 'hcaptcha' => __( 'Enable hCaptcha', 'wpforms-lite' ), 'turnstile' => __( 'Enable Cloudflare Turnstile', 'wpforms-lite' ), 'recaptcha' => [ 'v2' => __( 'Enable Google Checkbox v2 reCAPTCHA', 'wpforms-lite' ), 'invisible' => __( 'Enable Google Invisible v2 reCAPTCHA', 'wpforms-lite' ), 'v3' => __( 'Enable Google v3 reCAPTCHA', 'wpforms-lite' ), ], ]; $is_recaptcha = $captcha_settings['provider'] === 'recaptcha'; $captcha_types = $is_recaptcha ? $captcha_types['recaptcha'] : $captcha_types; $captcha_key = $is_recaptcha ? $captcha_settings['recaptcha_type'] : $captcha_settings['provider']; $label = ! empty( $captcha_types[ $captcha_key ] ) ? $captcha_types[ $captcha_key ] : ''; $recaptcha = wpforms_panel_field( 'toggle', 'settings', 'recaptcha', $this->form_data, $label, [ 'data' => [ 'provider' => $captcha_settings['provider'], ], 'tooltip' => __( 'Enable third-party CAPTCHAs to prevent form submissions from bots.', 'wpforms-lite' ), ], false ); wpforms_panel_fields_group( $recaptcha, [ 'description' => __( 'Automated tests that help to prevent bots from submitting your forms.', 'wpforms-lite' ), 'title' => __( 'CAPTCHA', 'wpforms-lite' ), 'borders' => [ 'top' ], ] ); } /** * Output the Spam Entries Store settings. * * @since 1.8.3 */ public function store_spam_entries_settings() { if ( ! wpforms()->is_pro() ) { return; } $disable_entries = $this->form_data['settings']['disable_entries'] ?? 0; wpforms_panel_field( 'toggle', 'settings', 'store_spam_entries', $this->form_data, __( 'Store spam entries in the database', 'wpforms-lite' ), [ 'value' => $this->form_data['settings']['store_spam_entries'] ?? 0, 'class' => $disable_entries ? 'wpforms-hidden' : '', ] ); } /** * Output the Time Limit settings. * * @since 1.8.3 */ private function time_limit_settings() { wpforms_panel_field( 'toggle', 'anti_spam', 'enable', $this->form_data, __( 'Enable minimum time to submit', 'wpforms-lite' ), [ 'parent' => 'settings', 'subsection' => 'time_limit', 'tooltip' => __( 'Set a minimum amount of time a user must spend on a form before submitting.', 'wpforms-lite' ), 'input_class' => 'wpforms-panel-field-toggle-next-field', ] ); wpforms_panel_field( 'text', 'anti_spam', 'duration', $this->form_data, __( 'Minimum time to submit', 'wpforms-lite' ), [ 'parent' => 'settings', 'subsection' => 'time_limit', 'type' => 'number', 'min' => 1, 'default' => 2, 'after' => sprintf( '<span class="wpforms-panel-field-after">%s</span>', __( 'seconds', 'wpforms-lite' ) ), ] ); } /** * Output the Akismet settings. * * @since 1.7.8 */ private function akismet_settings() { if ( ! Akismet::is_installed() ) { return; } $args = []; if ( ! Akismet::is_configured() ) { $args['data']['akismet-status'] = 'akismet_no_api_key'; } if ( ! Akismet::is_activated() ) { $args['data']['akismet-status'] = 'akismet_not_activated'; } // If Akismet isn't available, disable the Akismet toggle. if ( isset( $args['data'] ) ) { $args['input_class'] = 'wpforms-akismet-disabled'; $args['value'] = '0'; } wpforms_panel_field( 'toggle', 'settings', 'akismet', $this->form_data, __( 'Enable Akismet anti-spam protection', 'wpforms-lite' ), $args ); } /** * Get the Also Available block. * * @since 1.7.8 * * @return string */ private function get_also_available_block() { $get_started_button_text = __( 'Get Started →', 'wpforms-lite' ); $upgrade_to_pro_text = __( 'Upgrade to Pro', 'wpforms-lite' ); $captcha_settings = wpforms_get_captcha_settings(); $upgrade_url = 'https://wpforms.com/lite-upgrade/'; $utm_medium = 'Builder Settings'; $blocks = [ 'country_filter' => [ 'logo' => WPFORMS_PLUGIN_URL . 'assets/images/anti-spam/country-filter.svg', 'title' => __( 'Country Filter', 'wpforms-lite' ), 'description' => __( 'Stop spam at its source. Allow or deny entries from specific countries.', 'wpforms-lite' ), 'link' => wpforms_utm_link( $upgrade_url, $utm_medium, 'Country Filter Feature' ), 'link_text' => $upgrade_to_pro_text, 'class' => 'wpforms-panel-content-also-available-item-upgrade-to-pro', 'show' => ! wpforms()->is_pro(), ], 'keyword_filter' => [ 'logo' => WPFORMS_PLUGIN_URL . 'assets/images/anti-spam/keyword-filter.svg', 'title' => __( 'Keyword Filter', 'wpforms-lite' ), 'description' => __( 'Block form entries that contain specific words or phrases that you define.', 'wpforms-lite' ), 'link' => wpforms_utm_link( $upgrade_url, $utm_medium, 'Keyword Filter Feature' ), 'link_text' => $upgrade_to_pro_text, 'class' => 'wpforms-panel-content-also-available-item-upgrade-to-pro', 'show' => ! wpforms()->is_pro(), ], 'custom_captcha' => [ 'logo' => WPFORMS_PLUGIN_URL . 'assets/images/anti-spam/custom-captcha.svg', 'title' => __( 'Custom Captcha', 'wpforms-lite' ), 'description' => __( 'Ask custom questions or require your visitor to answer a random math puzzle.', 'wpforms-lite' ), 'link' => wpforms()->is_pro() ? '#' : wpforms_utm_link( $upgrade_url, $utm_medium, 'Custom Captcha Addon' ), 'link_text' => wpforms()->is_pro() ? __( 'Add to Form', 'wpforms-lite' ) : $upgrade_to_pro_text, 'class' => wpforms()->is_pro() ? 'wpforms-panel-content-also-available-item-add-captcha' : 'wpforms-panel-content-also-available-item-upgrade-to-pro', 'show' => true, ], 'reCAPTCHA' => [ 'logo' => WPFORMS_PLUGIN_URL . 'assets/images/anti-spam/recaptcha.svg', 'title' => 'reCAPTCHA', 'description' => __( 'Add Google\'s free anti-spam service and choose between visible or invisible CAPTCHAs.','wpforms-lite' ), 'link' => wpforms_utm_link( 'https://wpforms.com/docs/how-to-set-up-and-use-recaptcha-in-wpforms/', $utm_medium, 'reCAPTCHA Feature' ), 'link_text' => $get_started_button_text, 'show' => $captcha_settings['provider'] !== 'recaptcha' || empty( wpforms_setting( 'captcha-provider' ) ), ], 'hCaptcha' => [ 'logo' => WPFORMS_PLUGIN_URL . 'assets/images/anti-spam/hcaptcha.svg', 'title' => 'hCaptcha', 'description' => __( 'Turn on free, privacy-oriented spam prevention that displays a visual CAPTCHA.','wpforms-lite' ), 'link' => wpforms_utm_link( 'https://wpforms.com/docs/how-to-set-up-and-use-hcaptcha-in-wpforms/', $utm_medium, 'hCaptcha Feature' ), 'link_text' => $get_started_button_text, 'show' => $captcha_settings['provider'] !== 'hcaptcha', ], 'turnstile' => [ 'logo' => WPFORMS_PLUGIN_URL . 'assets/images/anti-spam/cloudflare.svg', 'title' => 'Cloudflare Turnstile', 'description' => __( 'Enable free, CAPTCHA-like spam protection that protects data privacy.','wpforms-lite' ), 'link' => wpforms_utm_link( 'https://wpforms.com/docs/setting-up-cloudflare-turnstile/', $utm_medium, 'Cloudflare Turnstile Feature' ), 'link_text' => $get_started_button_text, 'show' => $captcha_settings['provider'] !== 'turnstile', ], 'akismet' => [ 'logo' => WPFORMS_PLUGIN_URL . 'assets/images/anti-spam/akismet.svg', 'title' => 'Akismet', 'description' => __( 'Integrate the powerful spam-fighting service trusted by millions of sites.','wpforms-lite' ), 'link' => wpforms_utm_link( 'https://wpforms.com/docs/setting-up-akismet-anti-spam-protection/', $utm_medium, 'Akismet Feature' ), 'link_text' => $get_started_button_text, 'show' => ! Akismet::is_installed(), ], ]; return wpforms_render( 'builder/antispam/also-available', [ 'blocks' => $blocks ], true ); } } Admin/Splash/SplashTrait.php 0000644 00000014337 15174710275 0012016 0 ustar 00 <?php namespace WPForms\Admin\Splash; use WPForms\Migrations\Base as MigrationsBase; trait SplashTrait { /** * Default plugin version. * * @since 1.9.3 * * @var string */ private $default_plugin_version = '1.8.6'; // The last version before the "What's New?" feature. /** * Previous plugin version. * * @since 1.9.3 * * @var string */ private $previous_plugin_version; /** * Latest splash version. * * @since 1.9.3 * * @var string */ private $latest_splash_version; /** * Get the latest splash version. * * @since 1.8.7 * * @return string Splash version. */ private function get_latest_splash_version(): string { if ( $this->latest_splash_version ) { return $this->latest_splash_version; } $this->latest_splash_version = get_option( 'wpforms_splash_version', '' ); // Create option if it doesn't exist. if ( empty( $this->latest_splash_version ) ) { $this->latest_splash_version = $this->default_plugin_version; update_option( 'wpforms_splash_version', $this->latest_splash_version ); } return $this->latest_splash_version; } /** * Update option with the latest splash version. * * @since 1.8.7 */ private function update_splash_version(): void { update_option( 'wpforms_splash_version', $this->get_major_version( WPFORMS_VERSION ) ); } /** * Get user license type. * * @since 1.8.8 * * @return string */ private function get_user_license(): string { $license = wpforms_get_license_type(); if ( empty( $license ) ) { $license = 'lite'; } /** * License type used for splash screen. * * @since 1.8.8 * * @param string $license License type. */ return (string) apply_filters( 'wpforms_admin_splash_splashtrait_get_user_license', $license ); } /** * Get user version. * * @since 1.9.7 * * @return string User version. */ private function get_user_version(): string { /** * User version used for splash screen. * * @since 1.9.7 * * @param string $version User version. */ return (string) apply_filters( 'wpforms_admin_splash_splashtrait_get_user_version', $this->get_major_version( WPFORMS_VERSION ) ); } /** * Get default splash modal data. * * @since 1.8.7 * * @return array Splash modal data. */ private function get_default_data(): array { return [ 'license' => $this->get_user_license(), 'display_notice' => false, 'update_url' => $this->get_update_url(), 'buttons' => [ 'get_started' => __( 'Get Started', 'wpforms-lite' ), 'learn_more' => __( 'Learn More', 'wpforms-lite' ), ], 'header' => [ 'image' => WPFORMS_PLUGIN_URL . 'assets/images/splash/sullie.svg', 'title' => __( 'What’s New in WPForms', 'wpforms-lite' ), 'description' => __( 'We\'ve added some great new features to help grow your business and generate more leads. Here are the highlights...', 'wpforms-lite' ), ], 'footer' => [ 'title' => __( 'Start Building Smarter WordPress Forms', 'wpforms-lite' ), 'description' => __( 'Add advanced form fields and conditional logic, plus offer more payment options, manage entries, and connect to your favorite marketing tools – all when you purchase a premium plan.', 'wpforms-lite' ), 'upgrade' => [ 'text' => __( 'Upgrade to Pro Today', 'wpforms-lite' ), 'url' => wpforms_admin_upgrade_link( 'splash-modal', 'Upgrade to Pro Today' ), ], ], ]; } /** * Get plugin update URL. * * @since 1.9.7 * * @return string Update URL. */ private function get_update_url(): string { $plugin_slug = defined( 'WPFORMS_PLUGIN_DIR' ) ? plugin_basename( WPFORMS_PLUGIN_DIR ) : 'wpforms'; $plugin_path = $plugin_slug . '/wpforms.php'; return wp_nonce_url( self_admin_url( 'update.php?action=upgrade-plugin&plugin=' . $plugin_path ), 'upgrade-plugin_' . $plugin_path ); } /** * Prepare buttons. * * @since 1.8.7 * * @param array $buttons Buttons. * * @return array Prepared buttons. */ private function prepare_buttons( array $buttons ): array { return array_map( function ( $button ) { return [ 'url' => $this->prepare_url( $button['url'] ), 'text' => $button['text'], ]; }, $buttons ); } /** * Prepare URL. * * @since 1.8.7 * * @param string $url URL. * * @return string Prepared URL. */ private function prepare_url( string $url ): string { $replace_tags = [ '{admin_url}' => admin_url(), '{license_key}' => wpforms_get_license_key(), ]; return str_replace( array_keys( $replace_tags ), array_values( $replace_tags ), $url ); } /** * Get block layout. * * @since 1.8.7 * * @param array $image Image data. * * @return string Block layout. */ private function get_block_layout( array $image ): string { $image_type = $image['type'] ?? 'icon'; switch ( $image_type ) { case 'icon': $layout = 'one-third-two-thirds'; break; case 'illustration': $layout = 'fifty-fifty'; break; default: $layout = 'full-width'; break; } return $layout; } /** * Get a major version. * * @since 1.8.7.2 * * @param string $version Version. * * @return string Major version. */ private function get_major_version( $version ): string { // Allow only digits and dots. $clean_version = preg_replace( '/[^0-9.]/', '.', $version ); // Get version parts. $version_parts = explode( '.', $clean_version ); // If a version has more than 3 parts - use only first 3. Get block data only for major versions. if ( count( $version_parts ) > 3 ) { $version = implode( '.', array_slice( $version_parts, 0, 3 ) ); } return $version; } /** * Get the WPForms plugin previous version. * * @since 1.8.8 * * @return string Previous WPForms version. */ private function get_previous_plugin_version(): string { if ( $this->previous_plugin_version ) { return $this->previous_plugin_version; } $this->previous_plugin_version = get_option( MigrationsBase::PREVIOUS_CORE_VERSION_OPTION_NAME, '' ); if ( empty( $this->previous_plugin_version ) ) { $this->previous_plugin_version = $this->default_plugin_version; // The last version before the "What's New?" feature. } return $this->previous_plugin_version; } } Admin/Splash/SplashUpgrader.php 0000644 00000002205 15174710275 0012473 0 ustar 00 <?php namespace WPForms\Admin\Splash; use WPForms\Migrations\Base as MigrationsBase; /** * Splash upgrader. * * @since 1.8.7 */ class SplashUpgrader { use SplashTrait; /** * Initialize class. * * @since 1.8.7 */ public function init(): void { $this->hooks(); } /** * Hooks. * * @since 1.8.7 */ private function hooks(): void { // Update splash data after plugin update. add_action( 'wpforms_migrations_base_core_upgraded', [ $this, 'update_splash_data_on_migration' ], 10, 2 ); } /** * Update splash modal data on migration. * * @since 1.8.8 * * @param string|mixed $previous_version Previous plugin version. * @param MigrationsBase $migrations_obj Migrations object. * * @noinspection PhpUnusedParameterInspection */ public function update_splash_data_on_migration( $previous_version, MigrationsBase $migrations_obj ): void { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed // Force update splash data cache. $splash_cache_obj = wpforms()->obj( 'splash_cache' ); if ( ! $splash_cache_obj ) { return; } $splash_cache_obj->update( true ); } } Admin/Splash/SplashCache.php 0000644 00000006573 15174710275 0011741 0 ustar 00 <?php namespace WPForms\Admin\Splash; use WPForms\Helpers\CacheBase; /** * Splash cache handler. * * @since 1.8.7 */ class SplashCache extends CacheBase { use SplashTrait; /** * Remote source URL. * * @since 1.8.7 * * @var string */ public const REMOTE_SOURCE = 'https://wpformsapi.com/feeds/v1/splash/'; /** * Determine if the class is allowed to load. * * @since 1.8.7 * * @return bool */ protected function allow_load(): bool { return is_admin() || wp_doing_cron() || wpforms_doing_wp_cli(); } /** * Provide settings. * * @since 1.8.7 * * @return array Settings array. */ protected function setup(): array { return [ // Remote source URL. 'remote_source' => $this->get_remote_source(), // Splash cache file name. 'cache_file' => 'splash.json', /** * Time-to-live of the splash cache file in seconds. * * This applies to `uploads/wpforms/cache/splash.json` file. * * @since 1.8.7 * * @param integer $cache_ttl Cache time-to-live, in seconds. * Default value: WEEK_IN_SECONDS. */ 'cache_ttl' => (int) apply_filters( 'wpforms_admin_splash_cache_ttl', WEEK_IN_SECONDS ), ]; } /** * Get remote source URL. * * @since 1.8.7 * * @return string */ protected function get_remote_source(): string { return defined( 'WPFORMS_SPLASH_REMOTE_SOURCE' ) ? WPFORMS_SPLASH_REMOTE_SOURCE : self::REMOTE_SOURCE; } /** * Prepare splash modal data. * * @since 1.8.7 * * @param array $data Splash modal data. */ protected function prepare_cache_data( $data ): array { if ( empty( $data ) || ! is_array( $data ) ) { return []; } $blocks = $this->prepare_blocks( $data ); if ( empty( $blocks ) ) { return []; } $prepared_data['blocks'] = $blocks; return $prepared_data; } /** * Prepare blocks. * * @since 1.8.7 * * @param array $data Splash modal data. * * @return array Prepared blocks. */ private function prepare_blocks( array $data ): array { $user_license = $this->get_user_license(); $user_version = $this->get_user_version(); // Filter data by plugin version. $blocks = array_filter( $data, static function ( $block ) use ( $user_license ) { // Return only blocks that match the user license. return in_array( $user_license, $block['type'] ?? [], true ); } ); // Get the latest 10 blocks. $blocks = array_slice( $blocks, 0, 10 ); // Reset indexes. $blocks = array_values( $blocks ); return array_map( function ( $block ) use ( $user_version ) { $block_version = $block['version'] ?? ''; // Prepare buttons URLs. $block['buttons'] = $this->prepare_buttons( $block['btns'] ?? [] ); // Change main button URL if the block version is greater than the user version. if ( version_compare( $block_version, $user_version, '>' ) ) { $block['buttons']['main'] = [ 'url' => $this->get_update_url(), 'text' => __( 'Update Now', 'wpforms-lite' ), ]; } // If the block version is less than the user version, set 'new' to false. if ( version_compare( $block_version, $user_version, '<' ) ) { $block['new'] = false; } // Set layout based on an image type. $block['layout'] = $this->get_block_layout( $block['img'] ); unset( $block['btns'] ); return $block; }, $blocks, array_keys( $blocks ) ) ?? []; } } Admin/Splash/SplashScreen.php 0000644 00000022363 15174710275 0012150 0 ustar 00 <?php namespace WPForms\Admin\Splash; /** * What's New class. * * @since 1.8.7 */ class SplashScreen { use SplashTrait; /** * Splash data. * * @since 1.8.7 * * @var array */ private $splash_data = []; /** * Whether it is a new WPForms installation. * * @since 1.8.8 * * @var bool */ private $is_new_install; /** * Initialize class. * * @since 1.8.7 */ public function init() { $this->hooks(); } /** * Hooks. * * @since 1.8.7 */ private function hooks() { add_action( 'admin_init', [ $this, 'initialize_splash_data' ], 15 ); add_action( 'admin_enqueue_scripts', [ $this, 'admin_enqueue_scripts' ] ); add_action( 'admin_footer', [ $this, 'admin_footer' ] ); add_filter( 'removable_query_args', [ $this, 'removable_query_args' ] ); add_action( 'update_option_wpforms_license', [ $this, 'reset_splash_data' ] ); } /** * Initialize splash data. * * @since 1.8.7 */ public function initialize_splash_data() { if ( ! $this->is_allow_splash() ) { return; } if ( empty( $this->splash_data ) ) { $cached_data_obj = wpforms()->obj( 'splash_cache' ); $cached_data = $cached_data_obj ? $cached_data_obj->get() : null; if ( empty( $cached_data ) ) { return; } $default_data = $this->get_default_data(); $this->splash_data = wp_parse_args( $cached_data, $default_data ); } } /** * Enqueue assets. * * @since 1.8.7 */ public function admin_enqueue_scripts() { $min = wpforms_get_min_suffix(); // jQuery confirm. wp_register_script( 'jquery-confirm', WPFORMS_PLUGIN_URL . 'assets/lib/jquery.confirm/jquery-confirm.min.js', [ 'jquery' ], '1.0.0', true ); wp_register_style( 'jquery-confirm', WPFORMS_PLUGIN_URL . 'assets/lib/jquery.confirm/jquery-confirm.min.css', [], '1.0.0' ); wp_register_script( 'wpforms-splash-modal', WPFORMS_PLUGIN_URL . "assets/js/admin/splash/modal{$min}.js", [ 'jquery', 'wp-util' ], WPFORMS_VERSION, true ); wp_register_style( 'wpforms-splash-modal', WPFORMS_PLUGIN_URL . "assets/css/admin/admin-splash-modal{$min}.css", [], WPFORMS_VERSION ); wp_localize_script( 'wpforms-splash-modal', 'wpforms_splash_data', [ 'nonce' => wp_create_nonce( 'wpforms_dash_widget_nonce' ), 'triggerForceOpen' => $this->should_open_splash(), ] ); } /** * Output splash modal. * * @since 1.8.7 */ public function admin_footer(): void { if ( $this->is_splash_empty() || ! $this->is_allow_splash() ) { return; } $this->render_modal(); } /** * Check if splash data is empty. * * @since 1.8.7 * @since 1.8.8 Changed method visibility from private to public. * * @return bool True if empty, false otherwise. */ public function is_splash_empty(): bool { if ( empty( $this->splash_data ) ) { return true; } return empty( $this->splash_data['blocks'] ); } /** * Render splash modal. * * @since 1.8.7 * * @param array $data Splash modal data. */ public function render_modal( array $data = [] ) { wp_enqueue_script( 'jquery-confirm' ); wp_enqueue_style( 'jquery-confirm' ); wp_enqueue_script( 'wpforms-splash-modal' ); wp_enqueue_style( 'wpforms-splash-modal' ); if ( $this->should_open_splash() ) { $this->update_splash_version(); } $data = ! empty( $data ) ? $data : $this->splash_data; if ( ! $this->is_plugin_version_up_to_date() ) { $data['display_notice'] = true; } // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo wpforms_render( 'admin/splash/modal', $data, true ); } /** * Check if the plugin version is up to date. * * @since 1.9.7 * * @return bool True if up to date, false otherwise. */ private function is_plugin_version_up_to_date(): bool { if ( ! empty( $this->splash_data['blocks'] ) && is_array( $this->splash_data['blocks'] ) ) { // Get the first block and check its version. $first_block = reset( $this->splash_data['blocks'] ); // If the first block has a version, and it's greater than the current user version, that means the splash contains features that are not available to the user. if ( ! empty( $first_block['version'] ) && version_compare( $first_block['version'], $this->get_user_version(), '>' ) ) { return false; } } return true; } /** * Add a splash link to footer. * * @since 1.8.7 * @deprecated 1.9.7 * * @param string|mixed $content Footer content. * * @return string Footer content. */ public function add_splash_link( $content ): string { return $content; } /** * Check if splash modal can be displayed manually, via a link. * Used in footer and in form builder context menu. * * @since 1.8.8 * @deprecated 1.9.7 * * @return bool */ public function is_available_for_display(): bool { return true; } /** * Check if splash modal is allowed. * Only allow in Form Builder, WPForms pages, and the Dashboard. * * @since 1.8.7 * * @return bool True if allowed, false otherwise. */ public function is_allow_splash(): bool { return wpforms_is_admin_page( 'builder' ) || wpforms_is_admin_page() || $this->is_dashboard(); } /** * Check if the current page is the dashboard. * * @since 1.8.8 * * @return bool True if it is the dashboard, false otherwise. */ private function is_dashboard(): bool { global $pagenow; return $pagenow === 'index.php'; } /** * Check if splash modal should be forced open. * * @since 1.8.8 * * @return bool True if it should be forced open, false otherwise. */ private function is_force_open(): bool { // phpcs:ignore WordPress.Security.NonceVerification.Recommended return sanitize_key( $_GET['wpforms_action'] ?? '' ) === 'preview-splash-screen'; } /** * Check if splash modal should be opened. * * @since 1.8.7 * * @return bool True if splash should open, false otherwise. */ private function should_open_splash(): bool { // Skip if announcements are hidden, or it is the dashboard page. if ( $this->is_dashboard() || $this->hide_splash_modal() ) { return false; } // Allow if a splash version different from the current plugin major version, and it's not a new installation. $should_open_splash = $this->get_latest_splash_version() !== $this->get_major_version( WPFORMS_VERSION ) && ( ! $this->is_new_install() || $this->is_force_open() ); if ( ! $should_open_splash ) { return false; } // Skip if user on the builder page and the Challenge can be started. if ( wpforms_is_admin_page( 'builder' ) ) { return $this->is_allow_builder_splash(); } return true; } /** * Check if splash modal should be allowed on the builder page. * If the Challenge can be started, the splash modal should not be displayed. * * @since 1.9.0 * * @return bool True if allowed, false otherwise. */ private function is_allow_builder_splash(): bool { $challenge = wpforms()->obj( 'challenge' ); return ! ( $challenge->challenge_force_start() || $challenge->challenge_can_start() ); } /** * Check if the plugin is newly installed. * * Get all migrations that have run. * If the only migration with a timestamp is the current version, it's a new installation. * * @since 1.8.8 * * @return bool True if new install, false otherwise. */ private function is_new_install(): bool { if ( isset( $this->is_new_install ) ) { return $this->is_new_install; } $option_name = wpforms()->is_pro() ? 'wpforms_versions' : 'wpforms_versions_lite'; $migrations_run = get_option( $option_name, [] ); if ( empty( $migrations_run ) ) { return true; } unset( $migrations_run[ WPFORMS_VERSION ] ); $this->is_new_install = empty( end( $migrations_run ) ); return $this->is_new_install; } /** * Determine if the current update is a minor update. * * This method checks the version history of migrations run and compares * the last recorded version with the current version to determine if * the update is minor or major. * * @since 1.9.0 * * @return bool True if it's a minor update, false otherwise. */ private function is_minor_update(): bool { return $this->get_major_version( $this->get_previous_plugin_version() ) === $this->get_major_version( WPFORMS_VERSION ); } /** * Check if splash modal should be hidden. * * @since 1.8.8 * * @return bool True if hidden, false otherwise. */ private function hide_splash_modal(): bool { /** * Force to hide splash modal. * * @since 1.8.8 * * @param bool $hide_splash_modal True to hide, false otherwise. */ return (bool) apply_filters( 'wpforms_admin_splash_screen_hide_splash_modal', wpforms_setting( 'hide-announcements' ) ); } /** * Remove certain arguments from a query string that WordPress should always hide for users. * * @since 1.8.8 * * @param array $removable_query_args An array of parameters to remove from the URL. * * @return array Extended/filtered array of parameters to remove from the URL. */ public function removable_query_args( $removable_query_args ) { $removable_query_args[] = 'wpforms_action'; return $removable_query_args; } /** * Reset splash data after license update. * * @since 1.9.7 */ public function reset_splash_data(): void { // Force update splash data cache. wpforms()->obj( 'splash_cache' )->update( true ); } } Admin/FormEmbedWizard.php 0000644 00000026777 15174710275 0011362 0 ustar 00 <?php namespace WPForms\Admin; use WP_Post; /** * Embed Form in a Page wizard. * * @since 1.6.2 */ class FormEmbedWizard { /** * Max search results count of 'Select Page' dropdown. * * @since 1.7.9 * * @var int */ const MAX_SEARCH_RESULTS_DROPDOWN_PAGES_COUNT = 20; /** * Post statuses of pages in 'Select Page' dropdown. * * @since 1.7.9 * * @var string[] */ const POST_STATUSES_OF_DROPDOWN_PAGES = [ 'publish', 'pending' ]; /** * Initialize class. * * @since 1.6.2 */ public function init() { // Form Embed Wizard should load only in the Form Builder and on the Edit/Add Page screen. if ( ! wpforms_is_admin_page( 'builder' ) && ! wpforms_is_admin_ajax() && ! $this->is_form_embed_page() ) { return; } $this->hooks(); } /** * Register hooks. * * @since 1.6.2 * @since 1.7.9 Add hook for searching pages in embed wizard via AJAX. */ public function hooks() { add_action( 'admin_enqueue_scripts', [ $this, 'enqueues' ] ); add_action( 'admin_footer', [ $this, 'output' ] ); add_filter( 'default_title', [ $this, 'embed_page_title' ], 10, 2 ); add_filter( 'default_content', [ $this, 'embed_page_content' ], 10, 2 ); add_action( 'wp_ajax_wpforms_admin_form_embed_wizard_embed_page_url', [ $this, 'get_embed_page_url_ajax' ] ); add_action( 'wp_ajax_wpforms_admin_form_embed_wizard_search_pages_choicesjs', [ $this, 'get_search_result_pages_ajax' ] ); } /** * Enqueue assets. * * @since 1.6.2 * @since 1.7.9 Add 'underscore' as dependency. */ public function enqueues() { $min = wpforms_get_min_suffix(); if ( $this->is_form_embed_page() && $this->get_meta() && ! $this->is_challenge_active() ) { wp_enqueue_style( 'wpforms-admin-form-embed-wizard', WPFORMS_PLUGIN_URL . "assets/css/form-embed-wizard{$min}.css", [], WPFORMS_VERSION ); wp_enqueue_style( 'tooltipster', WPFORMS_PLUGIN_URL . 'assets/lib/jquery.tooltipster/jquery.tooltipster.min.css', null, '4.2.6' ); wp_enqueue_script( 'tooltipster', WPFORMS_PLUGIN_URL . 'assets/lib/jquery.tooltipster/jquery.tooltipster.min.js', [ 'jquery' ], '4.2.6', true ); } wp_enqueue_script( 'wpforms-admin-form-embed-wizard', WPFORMS_PLUGIN_URL . "assets/js/admin/form-embed-wizard{$min}.js", [ 'jquery', 'underscore' ], WPFORMS_VERSION, false ); wp_localize_script( 'wpforms-admin-form-embed-wizard', 'wpforms_admin_form_embed_wizard', [ 'nonce' => wp_create_nonce( 'wpforms_admin_form_embed_wizard_nonce' ), 'is_edit_page' => (int) $this->is_form_embed_page( 'edit' ), 'video_url' => esc_url( sprintf( 'https://youtube.com/embed/%s?rel=0&showinfo=0', wpforms_is_gutenberg_active() ? '_29nTiDvmLw' : 'IxGVz3AjEe0' ) ), ] ); } /** * Output HTML. * * @since 1.6.2 */ public function output() { // We don't need to output tooltip if Challenge is active. if ( $this->is_form_embed_page() && $this->is_challenge_active() ) { $this->delete_meta(); return; } // We don't need to output tooltip if it's not an embed flow. if ( $this->is_form_embed_page() && ! $this->get_meta() ) { return; } $template = $this->is_form_embed_page() ? 'admin/form-embed-wizard/tooltip' : 'admin/form-embed-wizard/popup'; $args = []; if ( ! $this->is_form_embed_page() ) { $args['user_can_edit_pages'] = current_user_can( 'edit_pages' ); $args['dropdown_pages'] = $this->get_select_dropdown_pages_html(); } // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo wpforms_render( $template, $args ); $this->delete_meta(); } /** * Check if Challenge is active. * * @since 1.6.4 * * @return boolean */ public function is_challenge_active() { static $challenge_active = null; if ( $challenge_active === null ) { $challenge = wpforms()->obj( 'challenge' ); $challenge_active = method_exists( $challenge, 'challenge_active' ) ? $challenge->challenge_active() : false; } return $challenge_active; } /** * Check if the current page is a form embed page. * * @since 1.6.2 * * @param string $type Type of the embed page to check. Can be '', 'add' or 'edit'. By default is empty string. * * @return boolean */ public function is_form_embed_page( $type = '' ) { global $pagenow; $type = $type === 'add' || $type === 'edit' ? $type : ''; if ( $pagenow !== 'post.php' && $pagenow !== 'post-new.php' ) { return false; } // phpcs:disable WordPress.Security.NonceVerification.Recommended $post_id = empty( $_GET['post'] ) ? 0 : (int) $_GET['post']; $post_type = empty( $_GET['post_type'] ) ? '' : sanitize_key( $_GET['post_type'] ); $action = empty( $_GET['action'] ) ? 'add' : sanitize_key( $_GET['action'] ); // phpcs:enable if ( $pagenow === 'post-new.php' && ( empty( $post_type ) || $post_type !== 'page' ) ) { return false; } if ( $pagenow === 'post.php' && ( empty( $post_id ) || get_post_type( $post_id ) !== 'page' ) ) { return false; } $meta = $this->get_meta(); $embed_page = ! empty( $meta['embed_page'] ) ? (int) $meta['embed_page'] : 0; if ( 'add' === $action && 0 === $embed_page && $type !== 'edit' ) { return true; } if ( ! empty( $post_id ) && $embed_page === $post_id && $type !== 'add' ) { return true; } return false; } /** * Set user's embed meta data. * * @since 1.6.2 * * @param array $data Data array to set. */ public function set_meta( $data ) { update_user_meta( get_current_user_id(), 'wpforms_admin_form_embed_wizard', $data ); } /** * Get user's embed meta data. * * @since 1.6.2 * * @return array User's embed meta data. */ public function get_meta() { return get_user_meta( get_current_user_id(), 'wpforms_admin_form_embed_wizard', true ); } /** * Delete user's embed meta data. * * @since 1.6.2 */ public function delete_meta() { delete_user_meta( get_current_user_id(), 'wpforms_admin_form_embed_wizard' ); } /** * Get embed page URL via AJAX. * * @since 1.6.2 */ public function get_embed_page_url_ajax() { // Run a security check. check_admin_referer( 'wpforms_admin_form_embed_wizard_nonce' ); // Check for permissions. if ( ! wpforms_current_user_can( 'edit_forms' ) ) { wp_send_json_error( esc_html__( 'You are not allowed to perform this action.', 'wpforms-lite' ) ); } $page_id = ! empty( $_POST['pageId'] ) ? absint( $_POST['pageId'] ) : 0; $meta = $this->prepare_meta_data( $page_id ); $this->set_meta( $meta ); // Update challenge option to properly continue challenge on the embed page. $this->update_challenge_option( $meta ); wp_send_json_success( $meta['url'] ); } /** * Prepare meta data for the embed page. * * @since 1.9.4 * * @param int $page_id Page ID. * * @return array */ private function prepare_meta_data( int $page_id ): array { if ( ! empty( $page_id ) ) { $url = get_edit_post_link( $page_id, '' ); $meta = [ 'embed_page' => $page_id, ]; } else { $url = add_query_arg( 'post_type', 'page', admin_url( 'post-new.php' ) ); $meta = [ 'embed_page' => 0, 'embed_page_title' => ! empty( $_POST['pageTitle'] ) ? sanitize_text_field( wp_unslash( $_POST['pageTitle'] ) ) : '', // phpcs:ignore WordPress.Security.NonceVerification.Missing ]; } $meta['form_id'] = ! empty( $_POST['formId'] ) ? absint( $_POST['formId'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Missing $meta['url'] = $url; return $meta; } /** * Update challenge option to properly continue challenge on the embed page. * * @since 1.9.4 * * @param array $meta Meta data. */ private function update_challenge_option( array $meta ): void { if ( $this->is_challenge_active() ) { $challenge = wpforms()->obj( 'challenge' ); if ( $challenge && method_exists( $challenge, 'set_challenge_option' ) ) { $challenge->set_challenge_option( [ 'embed_page' => $meta['embed_page'] ] ); } } } /** * Set default title for the new page. * * @since 1.6.2 * * @param string $post_title Default post title. * @param \WP_Post $post Post object. * * @return string New default post title. */ public function embed_page_title( $post_title, $post ) { $meta = $this->get_meta(); $this->delete_meta(); return empty( $meta['embed_page_title'] ) ? $post_title : $meta['embed_page_title']; } /** * Embed the form to the new page. * * @since 1.6.2 * * @param string $post_content Default post content. * @param \WP_Post $post Post object. * * @return string Embedding string (shortcode or GB component code). */ public function embed_page_content( $post_content, $post ) { $meta = $this->get_meta(); $form_id = ! empty( $meta['form_id'] ) ? $meta['form_id'] : 0; $page_id = ! empty( $meta['embed_page'] ) ? $meta['embed_page'] : 0; if ( ! empty( $page_id ) || empty( $form_id ) ) { return $post_content; } if ( wpforms_is_gutenberg_active() ) { $pattern = '<!-- wp:wpforms/form-selector {"formId":"%d"} /-->'; } else { $pattern = '[wpforms id="%d" title="false" description="false"]'; } return sprintf( $pattern, absint( $form_id ) ); } /** * Generate select with pages which are available to edit for current user. * * @since 1.6.6 * @since 1.7.9 Refactor to use ChoicesJS instead of `wp_dropdown_pages()`. * * @return string */ private function get_select_dropdown_pages_html() { $dropdown_pages = wpforms_search_posts( '', [ 'count' => self::MAX_SEARCH_RESULTS_DROPDOWN_PAGES_COUNT, 'post_status' => self::POST_STATUSES_OF_DROPDOWN_PAGES, ] ); if ( empty( $dropdown_pages ) ) { return ''; } $total_pages = 0; $wp_count_pages = (array) wp_count_posts( 'page' ); foreach ( $wp_count_pages as $page_status => $pages_count ) { if ( in_array( $page_status, self::POST_STATUSES_OF_DROPDOWN_PAGES, true ) ) { $total_pages += $pages_count; } } // Include so we can use `\wpforms_settings_select_callback()`. require_once WPFORMS_PLUGIN_DIR . 'includes/admin/settings-api.php'; return wpforms_settings_select_callback( [ 'id' => 'form-embed-wizard-choicesjs-select-pages', 'type' => 'select', 'choicesjs' => true, 'options' => wp_list_pluck( $dropdown_pages, 'post_title', 'ID' ), 'data' => [ 'use_ajax' => $total_pages > self::MAX_SEARCH_RESULTS_DROPDOWN_PAGES_COUNT, ], ] ); } /** * Get search result pages for ChoicesJS via AJAX. * * @since 1.7.9 */ public function get_search_result_pages_ajax() { // Run a security check. if ( ! check_ajax_referer( 'wpforms_admin_form_embed_wizard_nonce', false, false ) ) { wp_send_json_error( [ 'msg' => esc_html__( 'Your session expired. Please reload the builder.', 'wpforms-lite' ), ] ); } // Check for permissions. if ( ! wpforms_current_user_can( 'edit_forms' ) ) { wp_send_json_error( esc_html__( 'You are not allowed to perform this action.', 'wpforms-lite' ) ); } if ( ! array_key_exists( 'search', $_GET ) ) { wp_send_json_error( [ 'msg' => esc_html__( 'Incorrect usage of this operation.', 'wpforms-lite' ), ] ); } $result_pages = wpforms_search_pages_for_dropdown( sanitize_text_field( wp_unslash( $_GET['search'] ) ), [ 'count' => self::MAX_SEARCH_RESULTS_DROPDOWN_PAGES_COUNT, 'post_status' => self::POST_STATUSES_OF_DROPDOWN_PAGES, ] ); if ( empty( $result_pages ) ) { wp_send_json_error( [] ); } wp_send_json_success( $result_pages ); } } Admin/Notifications/EventDriven.php 0000644 00000054006 15174710275 0013365 0 ustar 00 <?php namespace WPForms\Admin\Notifications; use WPForms\Migrations\Base as MigrationsBase; /** * Class EventDriven. * * @since 1.7.5 */ class EventDriven { /** * WPForms version when the Event Driven feature has been introduced. * * @since 1.7.5 * * @var string */ public const FEATURE_INTRODUCED = '1.7.5'; /** * Expected date format for notifications. * * @since 1.7.5 * * @var string */ private const DATE_FORMAT = 'Y-m-d H:i:s'; /** * Common UTM parameters. * * @since 1.7.5 * * @var array */ private const UTM_PARAMS = [ 'utm_source' => 'WordPress', 'utm_medium' => 'Event Notification', ]; /** * Common targets for date logic. * * Available items: * - upgraded (upgraded to the latest version) * - activated * - forms_first_created * - X.X.X.X (upgraded to a specific version) * - pro (activated/installed) * - lite (activated/installed) * * @since 1.7.5 * * @var array */ private const DATE_LOGIC = [ 'upgraded', 'activated', 'forms_first_created' ]; /** * Timestamps. * * @since 1.7.5 * * @var array */ private $timestamps = []; /** * Initialize class. * * @since 1.7.5 */ public function init(): void { if ( ! $this->allow_load() ) { return; } $this->hooks(); } /** * Indicate if this is allowed to load. * * @since 1.7.5 * * @return bool */ private function allow_load(): bool { $notifications_obj = wpforms()->obj( 'notifications' ); $has_access = $notifications_obj && $notifications_obj->has_access(); return $has_access || wp_doing_cron(); } /** * Hooks. * * @since 1.7.5 */ private function hooks(): void { add_filter( 'wpforms_admin_notifications_update_data', [ $this, 'update_events' ] ); } /** * Add Event Driven notifications before saving them in a database. * * @since 1.7.5 * * @param array|mixed $data Notification data. * * @return array */ public function update_events( $data ): array { $data = (array) $data; $updated = []; /** * Allow developers to turn on debug mode: store all notifications and then show all of them. * * @since 1.7.5 * * @param bool $is_debug True if it's a debug mode. Default: false. */ $is_debug = (bool) apply_filters( 'wpforms_admin_notifications_event_driven_update_events_debug', false ); $wpforms_notifications = wpforms()->obj( 'notifications' ); foreach ( $this->get_notifications() as $slug => $notification ) { $is_processed = ! empty( $data['events'][ $slug ]['start'] ); $is_conditional_ok = ! ( isset( $notification['condition'] ) && $notification['condition'] === false ); // If it's a debug mode, OR valid notification has been already processed - skip running logic checks and save it. if ( $is_debug || ( $is_processed && $is_conditional_ok && $wpforms_notifications && $wpforms_notifications->is_valid( $data['events'][ $slug ] ) ) ) { unset( $notification['date_logic'], $notification['offset'], $notification['condition'] ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date $notification['start'] = $is_debug ? date( self::DATE_FORMAT ) : $data['events'][ $slug ]['start']; $updated[ $slug ] = $notification; continue; } // Ignore if a condition is not passed conditional checks. if ( ! $is_conditional_ok ) { continue; } $timestamp = $this->get_timestamp_by_date_logic( $this->prepare_date_logic( $notification ) ); if ( empty( $timestamp ) ) { continue; } // Probably, notification should be visible after some time. $offset = empty( $notification['offset'] ) ? 0 : absint( $notification['offset'] ); // Set a start date when the notification will be shown. // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date $notification['start'] = date( self::DATE_FORMAT, $timestamp + $offset ); // Ignore if notification data is not valid. if ( ! $wpforms_notifications->is_valid( $notification ) ) { continue; } // Remove unnecessary values, mark the notification as active, and save it. unset( $notification['date_logic'], $notification['offset'], $notification['condition'] ); $updated[ $slug ] = $notification; } $data['events'] = $updated; return $data; } /** * Prepare and retrieve date logic. * * @since 1.7.5 * * @param array|mixed $notification Notification data. * * @return array */ private function prepare_date_logic( $notification ): array { $date_logic = empty( $notification['date_logic'] ) || ! is_array( $notification['date_logic'] ) ? self::DATE_LOGIC : $notification['date_logic']; return array_filter( array_filter( $date_logic, 'is_string' ) ); } /** * Retrieve a notification timestamp based on date logic. * * @since 1.7.5 * * @param array $args Date logic. * * @return int */ private function get_timestamp_by_date_logic( array $args ): int { foreach ( $args as $target ) { if ( ! empty( $this->timestamps[ $target ] ) ) { return $this->timestamps[ $target ]; } $timestamp = (int) call_user_func( $this->get_timestamp_callback( $target ), $target ); if ( ! empty( $timestamp ) ) { $this->timestamps[ $target ] = $timestamp; return $timestamp; } } return 0; } /** * Retrieve a callback that determines the necessary timestamp. * * @since 1.7.5 * * @param string $target Date logic target. * * @return callable */ private function get_timestamp_callback( string $target ) { $raw_target = $target; // As $target should be a part of name for callback method, // this regular expression allows lowercase characters, numbers, and underscore. $target = strtolower( preg_replace( '/[^a-z0-9_]/', '', $target ) ); // Basic callback. $callback = [ $this, 'get_timestamp_' . $target ]; // Determine if a special version number is passed. // Uses the regular expression to check a SemVer string. // @link https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string. if ( preg_match( '/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:\.([1-9\d*]))?(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/', $raw_target ) ) { $callback = [ $this, 'get_timestamp_upgraded' ]; } // If a callback is callable, return it. Otherwise, return fallback. return is_callable( $callback ) ? $callback : '__return_zero'; } /** * Retrieve a timestamp when WPForms was upgraded. * * @since 1.7.5 * * @param string $version WPForms version. * * @return int|false Unix timestamp. False on failure. */ private function get_timestamp_upgraded( string $version ) { if ( $version === 'upgraded' ) { $version = WPFORMS_VERSION; } $timestamp = wpforms_get_upgraded_timestamp( $version ); if ( $timestamp === false ) { return false; } // Return a current timestamp if no luck to return a migration's timestamp. return $timestamp <= 0 ? time() : $timestamp; } /** * Retrieve a timestamp when WPForms was first installed/activated. * * @since 1.7.5 * * @return int|false Unix timestamp. False on failure. * @noinspection PhpUnusedPrivateMethodInspection */ private function get_timestamp_activated() { return wpforms_get_activated_timestamp(); } /** * Retrieve a timestamp when Lite was first installed. * * @since 1.7.5 * * @return int|false Unix timestamp. False on failure. * @noinspection PhpUnusedPrivateMethodInspection */ private function get_timestamp_lite() { $activated = (array) get_option( 'wpforms_activated', [] ); return ! empty( $activated['lite'] ) ? absint( $activated['lite'] ) : false; } /** * Retrieve a timestamp when Pro was first installed. * * @since 1.7.5 * * @return int|false Unix timestamp. False on failure. * @noinspection PhpUnusedPrivateMethodInspection */ private function get_timestamp_pro() { $activated = (array) get_option( 'wpforms_activated', [] ); return ! empty( $activated['pro'] ) ? absint( $activated['pro'] ) : false; } /** * Retrieve a timestamp when a first form was created. * * @since 1.7.5 * * @return int|false Unix timestamp. False on failure. * @noinspection PhpUnusedPrivateMethodInspection */ private function get_timestamp_forms_first_created() { $timestamp = get_option( 'wpforms_forms_first_created' ); return ! empty( $timestamp ) ? absint( $timestamp ) : false; } /** * Retrieve a number of entries. * * @since 1.7.5 * * @return int */ private function get_entry_count(): int { static $count; if ( is_int( $count ) ) { return $count; } global $wpdb; $count = 0; $entry_handler = wpforms()->obj( 'entry' ); $entry_meta_handler = wpforms()->obj( 'entry_meta' ); if ( ! $entry_handler || ! $entry_meta_handler ) { return $count; } $query = "SELECT COUNT( $entry_handler->primary_key ) FROM $entry_handler->table_name WHERE $entry_handler->primary_key NOT IN ( SELECT entry_id FROM $entry_meta_handler->table_name WHERE type = 'backup_id' );"; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared $count = (int) $wpdb->get_var( $query ); return $count; } /** * Retrieve forms. * * @since 1.7.5 * * @param int $posts_per_page The number of forms to return. * * @return array * @noinspection PhpSameParameterValueInspection */ private function get_forms( int $posts_per_page ): array { $form_obj = wpforms()->obj( 'form' ); $forms = $form_obj ? $form_obj->get( '', [ 'posts_per_page' => $posts_per_page, 'nopaging' => false, 'update_post_meta_cache' => false, 'update_post_term_cache' => false, 'cap' => false, ] ) : null; return ! empty( $forms ) ? (array) $forms : []; } /** * Determine if the user has at least 1 form. * * @since 1.7.5 * * @return bool */ private function has_form(): bool { return ! empty( $this->get_forms( 1 ) ); } /** * Determine if it is a new user. * * @since 1.7.5 * * @return bool */ private function is_new_user(): bool { // Check if this is an update or first install. return ! get_option( MigrationsBase::PREVIOUS_CORE_VERSION_OPTION_NAME ); } /** * Determine if it's an English site. * * @since 1.7.5 * * @return bool * @noinspection PhpUnusedPrivateMethodInspection */ private function is_english_site(): bool { static $result; if ( is_bool( $result ) ) { return $result; } $locales = array_unique( array_map( [ $this, 'language_to_iso' ], [ get_locale(), get_user_locale() ] ) ); $result = count( $locales ) === 1 && $locales[0] === 'en'; return $result; } /** * Convert language to ISO. * * @since 1.7.5 * * @param string|mixed $lang Language value. * * @return string */ private function language_to_iso( $lang ): string { $lang = (string) $lang; return $lang === '' ? $lang : explode( '_', $lang )[0]; } /** * Retrieve a modified URL query string. * * @since 1.7.5 * * @param array $args An associative array of query variables. * @param string $url A URL to act upon. * * @return string */ private function add_query_arg( array $args, string $url ): string { return add_query_arg( array_merge( $this->get_utm_params(), array_map( 'rawurlencode', $args ) ), $url ); } /** * Retrieve UTM parameters for Event Driven notifications links. * * @since 1.7.5 * * @return array */ private function get_utm_params(): array { static $utm_params; if ( ! $utm_params ) { $utm_params = [ 'utm_source' => self::UTM_PARAMS['utm_source'], 'utm_medium' => rawurlencode( self::UTM_PARAMS['utm_medium'] ), 'utm_campaign' => wpforms()->is_pro() ? 'plugin' : 'liteplugin', ]; } return $utm_params; } /** * Retrieve Event Driven notifications. * * @since 1.7.5 * * @return array */ private function get_notifications(): array { return [ 'welcome-message' => [ 'id' => 'welcome-message', 'title' => esc_html__( 'Welcome to WPForms!', 'wpforms-lite' ), 'content' => sprintf( /* translators: %s - number of templates. */ esc_html__( 'We’re grateful that you chose WPForms for your website! Now that you’ve installed the plugin, you’re less than 5 minutes away from publishing your first form. To make it easy, we’ve got %s form templates to get you started!', 'wpforms-lite' ), '2000+' ), 'btns' => [ 'main' => [ 'url' => admin_url( 'admin.php?page=wpforms-builder' ), 'text' => esc_html__( 'Start Building', 'wpforms-lite' ), ], 'alt' => [ 'url' => $this->add_query_arg( [ 'utm_content' => 'Welcome Read the Guide' ], 'https://wpforms.com/docs/creating-first-form/' ), 'text' => esc_html__( 'Read the Guide', 'wpforms-lite' ), ], ], 'type' => [ 'lite', 'basic', 'plus', 'pro', 'agency', 'elite', 'ultimate', ], // Immediately after activation (new users only, not upgrades). 'condition' => $this->is_new_user(), ], 'wp-mail-smtp-education' => [ 'id' => 'wp-mail-smtp-education', 'title' => esc_html__( 'Don’t Miss Your Form Notification Emails!', 'wpforms-lite' ), 'content' => esc_html__( 'Did you know that many WordPress sites are not properly configured to send emails? With the free WP Mail SMTP plugin, you can easily optimize your site to send emails, avoid the spam folder, and make sure your emails land in the recipient’s inbox every time.', 'wpforms-lite' ), 'btns' => [ 'main' => [ 'url' => admin_url( 'admin.php?page=wpforms-smtp' ), 'text' => esc_html__( 'Install Now', 'wpforms-lite' ), ], 'alt' => [ 'url' => $this->add_query_arg( [ 'utm_content' => 'WP Mail SMTP Learn More' ], 'https://wpforms.com/docs/how-to-set-up-smtp-using-the-wp-mail-smtp-plugin/' ), 'text' => esc_html__( 'Learn More', 'wpforms-lite' ), ], ], // 3 days after activation/upgrade. 'offset' => 3 * DAY_IN_SECONDS, 'condition' => ! function_exists( 'wp_mail_smtp' ), ], 'join-vip-circle' => [ 'id' => 'join-vip-circle', 'title' => esc_html__( 'Want to Be a VIP? Join Now!', 'wpforms-lite' ), 'content' => esc_html__( 'Running a WordPress site can be challenging. But help is just around the corner! Our Facebook group contains tons of tips and help to get your business growing! When you join our VIP Circle, you’ll get instant access to tips, tricks, and answers from a community of loyal WPForms users. Best of all, membership is 100% free!', 'wpforms-lite' ), 'btns' => [ 'main' => [ 'url' => 'https://www.facebook.com/groups/wpformsvip/', 'text' => esc_html__( 'Join Now', 'wpforms-lite' ), ], ], // 30 days after activation/upgrade. 'offset' => 30 * DAY_IN_SECONDS, ], 'survey-reports' => [ 'id' => 'survey-reports', 'title' => esc_html__( 'Want to Know What Your Customers Really Think?', 'wpforms-lite' ), 'content' => esc_html__( 'Nothing beats real feedback from your customers and visitors. That’s why many small businesses love our awesome Surveys and Polls addon. Instantly unlock full survey reporting right in your WordPress dashboard. And don’t forget: building a survey is easy with our pre-made templates, so you could get started within a few minutes!', 'wpforms-lite' ), 'btns' => [ 'main' => [ 'license' => [ 'lite' => [ 'url' => $this->add_query_arg( [ 'utm_content' => 'Surveys and Polls Upgrade Lite', 'utm_locale' => wpforms_sanitize_key( get_locale() ), ], 'https://wpforms.com/lite-upgrade/' ), 'text' => esc_html__( 'Upgrade Now', 'wpforms-lite' ), ], 'basic' => [ 'url' => $this->add_query_arg( [ 'utm_content' => 'Surveys and Polls Upgrade Basic' ], 'https://wpforms.com/account/licenses/' ), 'text' => esc_html__( 'Upgrade Now', 'wpforms-lite' ), ], 'plus' => [ 'url' => $this->add_query_arg( [ 'utm_content' => 'Surveys and Polls Upgrade Basic' ], 'https://wpforms.com/account/licenses/' ), 'text' => esc_html__( 'Upgrade Now', 'wpforms-lite' ), ], 'pro' => [ 'url' => admin_url( 'admin.php?page=wpforms-addons' ), 'text' => esc_html__( 'Install Now', 'wpforms-lite' ), ], 'elite' => [ 'url' => admin_url( 'admin.php?page=wpforms-addons' ), 'text' => esc_html__( 'Install Now', 'wpforms-lite' ), ], ], ], 'alt' => [ 'url' => $this->add_query_arg( [ 'utm_content' => 'Surveys and Polls Learn More' ], 'https://wpforms.com/docs/how-to-install-and-use-the-surveys-and-polls-addon/' ), 'text' => esc_html__( 'Learn More', 'wpforms-lite' ), ], ], // 45 days after activation/upgrade. 'offset' => 45 * DAY_IN_SECONDS, 'condition' => ! defined( 'WPFORMS_SURVEYS_POLLS_VERSION' ), ], 'form-abandonment' => [ 'id' => 'form-abandonment', 'title' => esc_html__( 'Get More Leads From Your Forms!', 'wpforms-lite' ), 'content' => esc_html__( 'Are your forms converting fewer visitors than you hoped? Often, visitors quit forms partway through. That can prevent you from getting all the leads you deserve to capture. With our Form Abandonment addon, you can capture partial entries even if your visitor didn’t hit Submit! From there, it’s easy to follow up with leads and turn them into loyal customers.', 'wpforms-lite' ), 'btns' => [ 'main' => [ 'license' => [ 'lite' => [ 'url' => $this->add_query_arg( [ 'utm_content' => 'Form Abandonment Upgrade Lite', 'utm_locale' => wpforms_sanitize_key( get_locale() ), ], 'https://wpforms.com/lite-upgrade/' ), 'text' => esc_html__( 'Upgrade Now', 'wpforms-lite' ), ], 'basic' => [ 'url' => $this->add_query_arg( [ 'utm_content' => 'Form Abandonment Upgrade Basic' ], 'https://wpforms.com/account/licenses/' ), 'text' => esc_html__( 'Upgrade Now', 'wpforms-lite' ), ], 'plus' => [ 'url' => $this->add_query_arg( [ 'utm_content' => 'Form Abandonment Upgrade Basic' ], 'https://wpforms.com/account/licenses/' ), 'text' => esc_html__( 'Upgrade Now', 'wpforms-lite' ), ], 'pro' => [ 'url' => admin_url( 'admin.php?page=wpforms-addons' ), 'text' => esc_html__( 'Install Now', 'wpforms-lite' ), ], 'elite' => [ 'url' => admin_url( 'admin.php?page=wpforms-addons' ), 'text' => esc_html__( 'Install Now', 'wpforms-lite' ), ], ], ], 'alt' => [ 'url' => $this->add_query_arg( [ 'utm_content' => 'Form Abandonment Learn More' ], 'https://wpforms.com/docs/how-to-install-and-use-form-abandonment-with-wpforms/' ), 'text' => esc_html__( 'Learn More', 'wpforms-lite' ), ], ], // 60 days after activation/upgrade. 'offset' => 60 * DAY_IN_SECONDS, 'condition' => ! defined( 'WPFORMS_FORM_ABANDONMENT_VERSION' ), ], 'ideas' => [ 'id' => 'ideas', 'title' => esc_html__( 'What’s Your Dream WPForms Feature?', 'wpforms-lite' ), 'content' => esc_html__( 'If you could add just one feature to WPForms, what would it be? We want to know! Our team is busy surveying valued customers like you as we plan the year ahead. We’d love to know which features would take your business to the next level! Do you have a second to share your idea with us?', 'wpforms-lite' ), 'btns' => [ 'main' => [ 'url' => 'https://wpforms.com/share-your-idea/', 'text' => esc_html__( 'Share Your Idea', 'wpforms-lite' ), ], ], // 120 days after activation/upgrade. 'offset' => 120 * DAY_IN_SECONDS, 'condition' => $this->has_form(), ], 'user-insights' => [ 'id' => 'user-insights', 'title' => esc_html__( 'Congratulations! You Just Got Your 100th Form Entry!', 'wpforms-lite' ), 'content' => esc_html__( 'You just hit 100 entries… and this is just the beginning! Now it’s time to dig into the data and figure out what makes your visitors tick. The User Journey addon shows you what your visitors looked at before submitting your form. Now you can easily find which areas of your site are triggering form conversions.', 'wpforms-lite' ), 'btns' => [ 'main' => [ 'license' => [ 'lite' => [ 'url' => $this->add_query_arg( [ 'utm_content' => 'User Journey Upgrade Lite', 'utm_locale' => wpforms_sanitize_key( get_locale() ), ], 'https://wpforms.com/lite-upgrade/' ), 'text' => esc_html__( 'Upgrade Now', 'wpforms-lite' ), ], 'basic' => [ 'url' => $this->add_query_arg( [ 'utm_content' => 'User Journey Upgrade Basic' ], 'https://wpforms.com/account/licenses/' ), 'text' => esc_html__( 'Upgrade Now', 'wpforms-lite' ), ], 'plus' => [ 'url' => $this->add_query_arg( [ 'utm_content' => 'User Journey Upgrade Basic' ], 'https://wpforms.com/account/licenses/' ), 'text' => esc_html__( 'Upgrade Now', 'wpforms-lite' ), ], 'pro' => [ 'url' => admin_url( 'admin.php?page=wpforms-addons' ), 'text' => esc_html__( 'Install Now', 'wpforms-lite' ), ], 'elite' => [ 'url' => admin_url( 'admin.php?page=wpforms-addons' ), 'text' => esc_html__( 'Install Now', 'wpforms-lite' ), ], ], ], ], 'condition' => ! defined( 'WPFORMS_USER_JOURNEY_VERSION' ) && $this->get_entry_count() >= 100, ], ]; } } Admin/Notifications/Notifications.php 0000644 00000041614 15174710275 0013746 0 ustar 00 <?php namespace WPForms\Admin\Notifications; /** * Notifications. * * @since 1.7.5 */ class Notifications { /** * Source of notifications content. * * @since 1.7.5 * * @var string */ const SOURCE_URL = 'https://wpformsapi.com/feeds/v1/notifications'; /** * Array of license types, that are considered being Elite level. * * @since 1.7.5 * * @var array */ const LICENSES_ELITE = [ 'agency', 'ultimate', 'elite' ]; /** * Option value. * * @since 1.7.5 * * @var bool|array */ public $option = false; /** * Current license type. * * @since 1.7.5 * * @var string */ private $license_type; /** * Initialize class. * * @since 1.7.5 */ public function init() { $this->hooks(); } /** * Register hooks. * * @since 1.7.5 */ public function hooks() { add_action( 'wpforms_admin_notifications_update', [ $this, 'update' ] ); if ( ! wpforms_is_admin_ajax() && ! is_admin() ) { return; } add_action( 'wpforms_overview_enqueue', [ $this, 'enqueues' ] ); add_action( 'wpforms_admin_overview_before_table', [ $this, 'output' ] ); add_action( 'deactivate_plugin', [ $this, 'delete' ], 10, 2 ); add_action( 'wp_ajax_wpforms_notification_dismiss', [ $this, 'dismiss' ] ); } /** * Check if user has access and is enabled. * * @since 1.7.5 * @since 1.8.2 Added AS task support. * * @return bool */ public function has_access() { $has_access = ! wpforms_setting( 'hide-announcements', false ); if ( ! wp_doing_cron() && ! wpforms_doing_wp_cli() ) { $has_access = $has_access && wpforms_current_user_can( 'view_forms' ); } /** * Allow modifying state if a user has access. * * @since 1.6.0 * * @param bool $access True if user has access. */ return (bool) apply_filters( 'wpforms_admin_notifications_has_access', $has_access ); } /** * Get option value. * * @since 1.7.5 * * @param bool $cache Reference property cache if available. * * @return array */ public function get_option( $cache = true ) { if ( $this->option && $cache ) { return $this->option; } $option = (array) get_option( 'wpforms_notifications', [] ); $this->option = [ 'update' => ! empty( $option['update'] ) ? (int) $option['update'] : 0, 'feed' => ! empty( $option['feed'] ) ? (array) $option['feed'] : [], 'events' => ! empty( $option['events'] ) ? (array) $option['events'] : [], 'dismissed' => ! empty( $option['dismissed'] ) ? (array) $option['dismissed'] : [], ]; return $this->option; } /** * Fetch notifications from feed. * * @since 1.7.5 * * @return array */ public function fetch_feed() { $response = wp_remote_get( self::SOURCE_URL, [ 'timeout' => 10, 'user-agent' => wpforms_get_default_user_agent(), ] ); if ( is_wp_error( $response ) ) { return []; } $body = wp_remote_retrieve_body( $response ); if ( empty( $body ) ) { return []; } return $this->verify( json_decode( $body, true ) ); } /** * Verify notification data before it is saved. * * @since 1.7.5 * * @param array $notifications Array of notifications items to verify. * * @return array */ public function verify( $notifications ) { $data = []; if ( ! is_array( $notifications ) || empty( $notifications ) ) { return $data; } foreach ( $notifications as $notification ) { // Ignore if one of the conditional checks is true: // // 1. notification message is empty. // 2. license type does not match. // 3. notification is expired. // 4. notification has already been dismissed. // 5. notification existed before installing WPForms. // (Prevents bombarding the user with notifications after activation). if ( empty( $notification['content'] ) || ! $this->is_license_type_match( $notification ) || $this->is_expired( $notification ) || $this->is_dismissed( $notification ) || $this->is_existed( $notification ) ) { continue; } $data[] = $notification; } return $data; } /** * Verify saved notification data for active notifications. * * @since 1.7.5 * * @param array $notifications Array of notifications items to verify. * * @return array */ public function verify_active( $notifications ) { if ( ! is_array( $notifications ) || empty( $notifications ) ) { return []; } $current_timestamp = time(); // Remove notifications that are not active. foreach ( $notifications as $key => $notification ) { if ( ( ! empty( $notification['start'] ) && $current_timestamp < strtotime( $notification['start'] ) ) || ( ! empty( $notification['end'] ) && $current_timestamp > strtotime( $notification['end'] ) ) ) { unset( $notifications[ $key ] ); } } return $notifications; } /** * Get notification data. * * @since 1.7.5 * * @return array */ public function get() { if ( ! $this->has_access() ) { return []; } $option = $this->get_option(); // Update notifications using async task. if ( empty( $option['update'] ) || time() > $option['update'] + DAY_IN_SECONDS ) { $tasks = wpforms()->obj( 'tasks' ); if ( ! $tasks->is_scheduled( 'wpforms_admin_notifications_update' ) !== false ) { $tasks ->create( 'wpforms_admin_notifications_update' ) ->async() ->params() ->register(); } } $feed = ! empty( $option['feed'] ) ? $this->verify_active( $option['feed'] ) : []; $events = ! empty( $option['events'] ) ? $this->verify_active( $option['events'] ) : []; return array_merge( $feed, $events ); } /** * Get notification count. * * @since 1.7.5 * * @return int */ public function get_count() { return count( $this->get() ); } /** * Add a new Event Driven notification. * * @since 1.7.5 * * @param array $notification Notification data. */ public function add( $notification ) { if ( ! $this->is_valid( $notification ) ) { return; } $option = $this->get_option(); // Notification ID already exists. if ( ! empty( $option['events'][ $notification['id'] ] ) ) { return; } update_option( 'wpforms_notifications', [ 'update' => $option['update'], 'feed' => $option['feed'], 'events' => array_merge( $notification, $option['events'] ), 'dismissed' => $option['dismissed'], ] ); } /** * Determine if notification data is valid. * * @since 1.7.5 * * @param array $notification Notification data. * * @return bool */ public function is_valid( $notification ) { if ( empty( $notification['id'] ) ) { return false; } return ! empty( $this->verify( [ $notification ] ) ); } /** * Determine if notification has already been dismissed. * * @since 1.7.5 * * @param array $notification Notification data. * * @return bool */ private function is_dismissed( $notification ) { $option = $this->get_option(); // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict return ! empty( $option['dismissed'] ) && in_array( $notification['id'], $option['dismissed'] ); } /** * Determine if license type is match. * * @since 1.7.5 * * @param array $notification Notification data. * * @return bool */ private function is_license_type_match( $notification ) { // A specific license type is not required. if ( empty( $notification['type'] ) ) { return true; } return in_array( $this->get_license_type(), (array) $notification['type'], true ); } /** * Determine if notification is expired. * * @since 1.7.5 * * @param array $notification Notification data. * * @return bool */ private function is_expired( $notification ) { return ! empty( $notification['end'] ) && time() > strtotime( $notification['end'] ); } /** * Determine if notification existed before installing WPForms. * * @since 1.7.5 * * @param array $notification Notification data. * * @return bool */ private function is_existed( $notification ) { $activated = wpforms_get_activated_timestamp(); return ! empty( $activated ) && ! empty( $notification['start'] ) && $activated > strtotime( $notification['start'] ); } /** * Update notification data from feed. * * @since 1.7.5 * @since 1.7.8 Added `wp_cache_flush()` call when the option has been updated. * @since 1.8.2 Don't fire the update action when it disabled or was fired recently. */ public function update() { if ( ! $this->has_access() ) { return; } $option = $this->get_option(); // Double-check the last update time to prevent multiple requests. if ( ! empty( $option['update'] ) && time() < $option['update'] + DAY_IN_SECONDS ) { return; } $data = [ 'feed' => $this->fetch_feed(), 'events' => $option['events'], 'dismissed' => $option['dismissed'], ]; // phpcs:disable WPForms.PHP.ValidateHooks.InvalidHookName /** * Allow changing notification data before it will be updated in database. * * @since 1.7.5 * * @param array $data New notification data. */ $data = (array) apply_filters( 'wpforms_admin_notifications_update_data', $data ); // phpcs:enable WPForms.PHP.ValidateHooks.InvalidHookName $data['update'] = time(); update_option( 'wpforms_notifications', $data ); } /** * Remove notification data from database before a plugin is deactivated. * * @since 1.7.5 * * @param string $plugin Path to the plugin file relative to the plugins directory. * @param bool $network_deactivating Whether the plugin is deactivated for all sites in the network * or just the current site. Multisite only. Default false. */ public function delete( $plugin, $network_deactivating ) { $wpforms_plugins = [ 'wpforms-lite/wpforms.php', 'wpforms/wpforms.php', ]; if ( ! in_array( $plugin, $wpforms_plugins, true ) ) { return; } delete_option( 'wpforms_notifications' ); } /** * Enqueue assets on Form Overview admin page. * * @since 1.7.5 */ public function enqueues() { if ( ! $this->get_count() ) { return; } $min = wpforms_get_min_suffix(); wp_enqueue_style( 'wpforms-admin-notifications', WPFORMS_PLUGIN_URL . "assets/css/admin-notifications{$min}.css", [ 'wpforms-lity' ], WPFORMS_VERSION ); wp_enqueue_script( 'wpforms-admin-notifications', WPFORMS_PLUGIN_URL . "assets/js/admin/admin-notifications{$min}.js", [ 'jquery', 'wpforms-lity' ], WPFORMS_VERSION, true ); // Lity. wp_enqueue_style( 'wpforms-lity', WPFORMS_PLUGIN_URL . 'assets/lib/lity/lity.min.css', [], WPFORMS_VERSION ); wp_enqueue_script( 'wpforms-lity', WPFORMS_PLUGIN_URL . 'assets/lib/lity/lity.min.js', [ 'jquery' ], WPFORMS_VERSION, true ); } /** * Output notifications on Form Overview admin area. * * @since 1.7.5 */ public function output() { // Leave early if there are no forms. if ( ! wpforms()->obj( 'form' )->forms_exist() ) { return; } $notifications = $this->get(); if ( empty( $notifications ) ) { return; } $notifications_html = ''; $current_class = ' current'; $content_allowed_tags = $this->get_allowed_tags(); foreach ( $notifications as $notification ) { // Prepare required arguments. $notification = wp_parse_args( $notification, [ 'id' => 0, 'title' => '', 'content' => '', 'video' => '', ] ); $title = $this->get_component_data( $notification['title'] ); $content = $this->get_component_data( $notification['content'] ); if ( ! $title && ! $content ) { continue; } // Notification HTML. $notifications_html .= sprintf( '<div class="wpforms-notifications-message%5$s" data-message-id="%4$s"> <h3 class="wpforms-notifications-title">%1$s%6$s</h3> <div class="wpforms-notifications-content">%2$s</div> %3$s </div>', esc_html( $title ), wp_kses( wpautop( $content ), $content_allowed_tags ), $this->get_notification_buttons_html( $notification ), esc_attr( $notification['id'] ), esc_attr( $current_class ), $this->get_video_badge_html( $this->get_component_data( $notification['video'] ) ) ); // Only first notification is current. $current_class = ''; } // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo wpforms_render( 'admin/notifications', [ 'notifications' => [ 'count' => count( $notifications ), 'html' => $notifications_html, ], ], true ); } /** * Get the allowed HTML tags and their attributes. * * @since 1.8.8 * * @return array */ public function get_allowed_tags(): array { return [ 'br' => [], 'em' => [], 'strong' => [], 'span' => [ 'style' => [], ], 'p' => [ 'id' => [], 'class' => [], ], 'a' => [ 'href' => [], 'target' => [], 'rel' => [], ], ]; } /** * Retrieve notification's buttons HTML. * * @since 1.7.5 * * @param array $notification Notification data. * * @return string */ private function get_notification_buttons_html( $notification ) { $html = ''; if ( empty( $notification['btns'] ) || ! is_array( $notification['btns'] ) ) { return $html; } foreach ( $notification['btns'] as $btn_type => $btn ) { $btn = $this->get_component_data( $btn ); if ( ! $btn ) { continue; } $url = $this->prepare_btn_url( $btn ); $target = ! empty( $btn['target'] ) ? $btn['target'] : '_blank'; $target = ! empty( $url ) && strpos( $url, home_url() ) === 0 ? '_self' : $target; $html .= sprintf( '<a href="%1$s" class="button button-%2$s"%3$s>%4$s</a>', esc_url( $url ), $btn_type === 'main' ? 'primary' : 'secondary', $target === '_blank' ? ' target="_blank" rel="noopener noreferrer"' : '', ! empty( $btn['text'] ) ? esc_html( $btn['text'] ) : '' ); } return ! empty( $html ) ? sprintf( '<div class="wpforms-notifications-buttons">%s</div>', $html ) : ''; } /** * Retrieve notification's component data by a license type. * * @since 1.7.5 * * @param mixed $data Component data. * * @return false|mixed */ private function get_component_data( $data ) { if ( empty( $data['license'] ) ) { return $data; } $license_type = $this->get_license_type(); if ( in_array( $license_type, self::LICENSES_ELITE, true ) ) { $license_type = 'elite'; } return ! empty( $data['license'][ $license_type ] ) ? $data['license'][ $license_type ] : false; } /** * Retrieve the current installation license type (always lowercase). * * @since 1.7.5 * * @return string */ private function get_license_type() { if ( $this->license_type ) { return $this->license_type; } $this->license_type = wpforms_get_license_type(); if ( ! $this->license_type ) { $this->license_type = 'lite'; } return $this->license_type; } /** * Dismiss notification via AJAX. * * @since 1.7.5 */ public function dismiss() { // Check for required param, security and access. if ( empty( $_POST['id'] ) || ! check_ajax_referer( 'wpforms-admin', 'nonce', false ) || ! $this->has_access() ) { wp_send_json_error(); } $id = sanitize_key( $_POST['id'] ); $type = is_numeric( $id ) ? 'feed' : 'events'; $option = $this->get_option(); $option['dismissed'][] = $id; $option['dismissed'] = array_unique( $option['dismissed'] ); // Remove notification. if ( is_array( $option[ $type ] ) && ! empty( $option[ $type ] ) ) { foreach ( $option[ $type ] as $key => $notification ) { if ( (string) $notification['id'] === (string) $id ) { unset( $option[ $type ][ $key ] ); break; } } } update_option( 'wpforms_notifications', $option ); wp_send_json_success(); } /** * Prepare button URL. * * @since 1.7.5 * * @param array $btn Button data. * * @return string */ private function prepare_btn_url( $btn ) { if ( empty( $btn['url'] ) ) { return ''; } $replace_tags = [ '{admin_url}' => admin_url(), '{license_key}' => wpforms_get_license_key(), ]; return str_replace( array_keys( $replace_tags ), array_values( $replace_tags ), $btn['url'] ); } /** * Get the notification's video badge HTML. * * @since 1.7.5 * * @param string $video_url Valid video URL. * * @return string */ private function get_video_badge_html( $video_url ) { $video_url = wp_http_validate_url( $video_url ); if ( empty( $video_url ) ) { return ''; } $data_attr_lity = wp_is_mobile() ? '' : 'data-lity'; return sprintf( '<a class="wpforms-notifications-badge" href="%1$s" %2$s> <svg fill="none" viewBox="0 0 15 13" aria-hidden="true"> <path fill="#fff" d="M4 2.5h7v8H4z"/> <path fill="#D63638" d="M14.2 10.5v-8c0-.4-.2-.8-.5-1.1-.3-.3-.7-.5-1.1-.5H2.2c-.5 0-.8.2-1.1.5-.4.3-.5.7-.5 1.1v8c0 .4.2.8.5 1.1.3.3.6.5 1 .5h10.5c.4 0 .8-.2 1.1-.5.3-.3.5-.7.5-1.1Zm-8.8-.8V3.3l4.8 3.2-4.8 3.2Z"/> </svg> %3$s </a>', esc_url( $video_url ), esc_attr( $data_attr_lity ), esc_html__( 'Watch Video', 'wpforms-lite' ) ); } } Admin/Addons/Addons.php 0000644 00000032033 15174710275 0010737 0 ustar 00 <?php namespace WPForms\Admin\Addons; /** * Addons data handler. * * @since 1.6.6 */ class Addons { /** * Basic license. * * @since 1.8.2 */ const BASIC = 'basic'; /** * Plus license. * * @since 1.8.2 */ const PLUS = 'plus'; /** * Pro license. * * @since 1.8.2 */ const PRO = 'pro'; /** * Elite license. * * @since 1.8.2 */ const ELITE = 'elite'; /** * Agency license. * * @since 1.8.2 */ const AGENCY = 'agency'; /** * Ultimate license. * * @since 1.8.2 */ const ULTIMATE = 'ultimate'; /** * Addons cache object. * * @since 1.6.6 * * @var AddonsCache */ private $cache; /** * All Addons data. * * @since 1.6.6 * * @var array */ private $addons; /** * WPForms addons text domains. * * @since 1.9.2 * * @var array */ private $addons_text_domains = []; /** * WPForms addons titles. * * @since 1.9.2 * * @var array */ private $addons_titles = []; /** * Determine if the class is allowed to load. * * @since 1.6.6 * * @return bool */ public function allow_load() { global $pagenow; $has_permissions = wpforms_current_user_can( [ 'create_forms', 'edit_forms' ] ); $allowed_pages = in_array( $pagenow ?? '', [ 'plugins.php', 'update-core.php', 'plugin-install.php' ], true ); $allowed_ajax = $pagenow === 'admin-ajax.php' && isset( $_POST['action'] ) && $_POST['action'] === 'update-plugin'; // phpcs:ignore WordPress.Security.NonceVerification.Missing $allowed_requests = $allowed_pages || $allowed_ajax || wpforms_is_admin_ajax() || wpforms_is_admin_page() || wpforms_is_admin_page( 'builder' ); return $has_permissions && $allowed_requests; } /** * Initialize class. * * @since 1.6.6 */ public function init() { if ( ! $this->allow_load() ) { return; } $this->cache = wpforms()->obj( 'addons_cache' ); global $pagenow; // Force update addons cache if we are on the update-core.php page. // This is necessary to update addons data while checking for all available updates. if ( $pagenow === 'update-core.php' ) { $this->cache->update( true ); } $this->addons = $this->cache->get(); $this->populate_addons_data(); $this->hooks(); } /** * Hooks. * * @since 1.6.6 */ protected function hooks() { global $pagenow; /** * Fire before admin addons init. * * @since 1.6.7 */ do_action( 'wpforms_admin_addons_init' ); // Filter Gettext only on Plugin list and Updates pages. if ( $pagenow === 'update-core.php' || $pagenow === 'plugins.php' ) { add_action( 'gettext', [ $this, 'filter_gettext' ], 10, 3 ); } } /** * Get all addons data as array. * * @since 1.6.6 * * @param bool $force_cache_update Determine if we need to update cache. Default is `false`. * * @return array */ public function get_all( bool $force_cache_update = false ) { if ( ! $this->allow_load() ) { return []; } if ( $force_cache_update ) { $this->cache->update( true ); $this->addons = $this->cache->get(); } // WPForms 1.8.7 core includes Custom Captcha. // The Custom Captcha addon will only work on WPForms 1.8.6 and earlier versions. unset( $this->addons['wpforms-captcha'] ); return $this->get_sorted_addons(); } /** * Get sorted addons data. * Recommended addons will be displayed first, * then new addons, then featured addons, * and then all other addons. * * @since 1.8.9 * * @return array */ private function get_sorted_addons(): array { if ( empty( $this->addons ) ) { return []; } $recommended = array_filter( $this->addons, static function ( $addon ) { return ! empty( $addon['recommended'] ); } ); $new = array_filter( $this->addons, static function ( $addon ) { return ! empty( $addon['new'] ); } ); $featured = array_filter( $this->addons, static function ( $addon ) { return ! empty( $addon['featured'] ); } ); return array_merge( $recommended, $new, $featured, $this->addons ); } /** * Get filtered addons data. * * Usage: * ->get_filtered( $this->addons, [ 'category' => 'payments' ] ) - addons for the payments panel. * ->get_filtered( $this->addons, [ 'license' => 'elite' ] ) - addons available for 'elite' license. * * @since 1.6.6 * * @param array $addons Raw addons data. * @param array $args Arguments array. * * @return array Addons data filtered according to given arguments. */ private function get_filtered( array $addons, array $args ): array { $args = wp_parse_args( $args, [ 'category' => '', 'license' => '', ] ); $args = array_map( 'strtolower', $args ); $filtered_addons = []; foreach ( $addons as $addon ) { foreach ( $args as $arg_key => $arg_value ) { $addon_value = wpforms_array_get_by_path( $addon, $arg_key, '' ); if ( is_array( $addon_value ) && // We cannot use preg_quote here, as $arg_value could contain regex like 'crm|email-marketing|integration'. preg_grep( '/^' . $arg_value . '$/', $addon_value ) ) { $filtered_addons[] = $addon; } } } return $filtered_addons; } /** * Get available addons data by category. * * @since 1.6.6 * * @param string $category Addon category. * * @return array. */ public function get_by_category( string $category ) { return $this->get_by_path( 'category', $category ); } /** * Get available addons data by path. * * @since 1.9.8.6 * * @param string $path Path in addons multidimensional array. * May be 'category' or 'form_builder.category' or 'settings_integrations.category', etc. * @param string $value Addons multidimensional array value we are looking for in the path. * * @return array */ public function get_by_path( string $path, $value ): array { return $this->get_filtered( $this->get_available(), [ $path => $value ] ); } /** * Get available addons data by license. * * @since 1.6.6 * * @param string $license Addon license. * * @return array. * @noinspection PhpUnused */ public function get_by_license( string $license ) { return $this->get_filtered( $this->get_available(), [ 'license' => $license ] ); } /** * Get available addons data by slugs. * * @since 1.6.8 * * @param array|mixed $slugs Addon slugs. * * @return array */ public function get_by_slugs( $slugs ) { if ( empty( $slugs ) || ! is_array( $slugs ) ) { return []; } $result_addons = []; foreach ( $slugs as $slug ) { $addon = $this->get_addon( $slug ); if ( ! empty( $addon ) ) { $result_addons[] = $addon; } } return $result_addons; } /** * Get available addon data by slug. * * @since 1.6.6 * * @param string|bool $slug Addon slug can be both "wpforms-drip" and "drip". * * @return array Single addon data. Empty array if addon is not found. */ public function get_addon( $slug ) { $slug = (string) $slug; $slug = 'wpforms-' . str_replace( 'wpforms-', '', sanitize_key( $slug ) ); $addon = $this->get_available()[ $slug ] ?? []; // In case if addon is "not available" let's try to get and prepare addon data from all addons. if ( empty( $addon ) ) { $addon = ! empty( $this->addons[ $slug ] ) ? $this->prepare_addon_data( $this->addons[ $slug ] ) : []; } return $addon; } /** * Check if addon is active. * * @since 1.8.9 * * @param string $slug Addon slug. * * @return bool */ public function is_active( string $slug ): bool { $addon = $this->get_addon( $slug ); return isset( $addon['status'] ) && $addon['status'] === 'active'; } /** * Get license level of the addon. * * @since 1.6.6 * * @param array|string $addon Addon data array OR addon slug. * * @return string License level: pro | elite. */ private function get_license_level( $addon ) { if ( empty( $addon ) ) { return ''; } $levels = [ self::BASIC, self::PLUS, self::PRO, self::ELITE, self::AGENCY, self::ULTIMATE ]; $license = ''; $addon_license = $this->get_addon_license( $addon ); foreach ( $levels as $level ) { if ( in_array( $level, $addon_license, true ) ) { $license = $level; break; } } if ( empty( $license ) ) { return ''; } return in_array( $license, [ self::BASIC, self::PLUS, self::PRO ], true ) ? self::PRO : self::ELITE; } /** * Get addon license. * * @since 1.8.2 * * @param array|string $addon Addon data array OR addon slug. * * @return array */ private function get_addon_license( $addon ) { $addon = is_string( $addon ) ? $this->get_addon( $addon ) : $addon; return $this->default_data( $addon, 'license', [] ); } /** * Determine if a user's license level has access. * * @since 1.6.6 * * @param array|string $addon Addon data array OR addon slug. * * @return bool */ protected function has_access( $addon ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found return false; } /** * Return array of addons available to display. All data is prepared and normalized. * "Available to display" means that addon needs to be displayed as an education item (addon is not installed or not activated). * * @since 1.6.6 * * @return array */ public function get_available() { static $available_addons = []; if ( $available_addons ) { return $available_addons; } if ( empty( $this->addons ) || ! is_array( $this->addons ) ) { return []; } $available_addons = array_map( [ $this, 'prepare_addon_data' ], $this->addons ); $available_addons = array_filter( $available_addons, static function ( $addon ) { return isset( $addon['status'], $addon['plugin_allow'] ) && ( $addon['status'] !== 'active' || ! $addon['plugin_allow'] ); } ); return $available_addons; } /** * Prepare addon data. * * @since 1.6.6 * * @param array|mixed $addon Addon data. * * @return array Extended addon data. */ protected function prepare_addon_data( $addon ) { if ( empty( $addon ) ) { return []; } $addon['title'] = $this->default_data( $addon, 'title', '' ); $addon['slug'] = $this->default_data( $addon, 'slug', '' ); // We need the cleared name of the addon, without the 'addon' suffix, for further use. $addon['name'] = preg_replace( '/ addon$/i', '', $addon['title'] ); $addon['modal_name'] = sprintf( /* translators: %s - addon name. */ esc_html__( '%s addon', 'wpforms-lite' ), $addon['name'] ); $addon['clear_slug'] = str_replace( 'wpforms-', '', $addon['slug'] ); $addon['utm_content'] = ucwords( str_replace( '-', ' ', $addon['clear_slug'] ) ); $addon['license'] = $this->default_data( $addon, 'license', [] ); $addon['license_level'] = $this->get_license_level( $addon ); $addon['icon'] = $this->default_data( $addon, 'icon', '' ); $addon['path'] = sprintf( '%1$s/%1$s.php', $addon['slug'] ); $addon['video'] = $this->default_data( $addon, 'video', '' ); $addon['plugin_allow'] = $this->has_access( $addon ); $addon['status'] = 'missing'; $addon['action'] = 'upgrade'; $addon['page_url'] = $this->default_data( $addon, 'url', '' ); $addon['doc_url'] = $this->default_data( $addon, 'doc', '' ); $addon['url'] = ''; static $nonce = ''; $nonce = empty( $nonce ) ? wp_create_nonce( 'wpforms-admin' ) : $nonce; $addon['nonce'] = $nonce; return $addon; } /** * Get default data. * * @since 1.8.2 * * @param array|mixed $addon Addon data. * @param string $key Key. * @param mixed $default_data Default data. * * @return array|string|mixed */ private function default_data( $addon, string $key, $default_data ) { if ( is_string( $default_data ) ) { return ! empty( $addon[ $key ] ) ? $addon[ $key ] : $default_data; } if ( is_array( $default_data ) ) { return ! empty( $addon[ $key ] ) ? (array) $addon[ $key ] : $default_data; } return $addon[ $key ] ?? ''; } /** * Populate addons data. * * @since 1.9.2 * * @return void */ private function populate_addons_data() { foreach ( $this->addons as $addon ) { $this->addons_text_domains[] = $addon['slug']; $this->addons_titles[] = 'WPForms ' . str_replace( ' Addon', '', $addon['title'] ); } } /** * Filter Gettext. * * This filter allows us to prevent empty translations from being returned * on the `plugins` page for addon name and description. * * @since 1.9.2 * * @param string|mixed $translation Translated text. * @param string|mixed $text Text to translate. * @param string|mixed $domain Text domain. * * @return string Translated text. */ public function filter_gettext( $translation, $text, $domain ): string { $translation = (string) $translation; $text = (string) $text; $domain = (string) $domain; if ( ! in_array( $domain, $this->addons_text_domains, true ) ) { return $translation; } // Prevent empty translations from being returned and don't translate addon names. if ( ! trim( $translation ) || in_array( $text, $this->addons_titles, true ) ) { $translation = $text; } return $translation; } } Admin/Addons/AddonsCache.php 0000644 00000005624 15174710275 0011671 0 ustar 00 <?php namespace WPForms\Admin\Addons; use WPForms\Helpers\CacheBase; /** * Addons cache handler. * * @since 1.6.6 */ class AddonsCache extends CacheBase { /** * Remote source URL. * * @since 1.8.9 * * @var string */ const REMOTE_SOURCE = 'https://wpformsapi.com/feeds/v1/addons/'; /** * Determine if the class is allowed to load. * * @since 1.6.8 * * @return bool */ protected function allow_load() { if ( wp_doing_cron() || wpforms_doing_wp_cli() ) { return true; } $has_permissions = wpforms_current_user_can( [ 'create_forms', 'edit_forms' ] ); $allowed_requests = wpforms_is_admin_ajax() || wpforms_is_admin_page() || wpforms_is_admin_page( 'builder' ); return $has_permissions && $allowed_requests; } /** * Provide settings. * * @since 1.6.6 * * @return array Settings array. */ protected function setup() { return [ // Remote source URL. 'remote_source' => $this->get_remote_source(), // Addons cache file name. 'cache_file' => 'addons.json', /** * Time-to-live of the addons cache file in seconds. * * This applies to `uploads/wpforms/cache/addons.json` file. * * @since 1.6.8 * * @param integer $cache_ttl Cache time-to-live, in seconds. * Default value: WEEK_IN_SECONDS. */ 'cache_ttl' => (int) apply_filters( 'wpforms_admin_addons_cache_ttl', WEEK_IN_SECONDS ), // Scheduled update action. 'update_action' => 'wpforms_admin_addons_cache_update', ]; } /** * Get remote source URL. * * @since 1.8.9 * * @return string */ protected function get_remote_source(): string { return defined( 'WPFORMS_ADDONS_REMOTE_SOURCE' ) ? WPFORMS_ADDONS_REMOTE_SOURCE : self::REMOTE_SOURCE; } /** * Prepare addons data to store in a local cache - * generate addons icon image file name for further use. * * @since 1.6.6 * * @param array $data Raw addons data. * * @return array Prepared data for caching (with icons). */ protected function prepare_cache_data( $data ): array { if ( empty( $data ) || ! is_array( $data ) ) { return []; } $addons_cache = []; foreach ( $data as $addon ) { // Addon icon. $addon['icon'] = str_replace( 'wpforms-', 'addon-icon-', $addon['slug'] ) . '.png'; // Special case when plugin addon renamed, for instance: // Sendinblue to Brevo, or ConvertKit to Kit, // but we keep the old slug for compatibility. foreach ( [ 'wpforms-sendinblue' => [ 'old' => 'sendinblue', 'new' => 'brevo', ], 'wpforms-convertkit' => [ 'old' => 'convertkit', 'new' => 'kit', ], ] as $slug => $renamed ) { if ( $addon['slug'] === $slug ) { $addon['icon'] = str_replace( $renamed['old'], $renamed['new'], $addon['icon'] ); } } // Use slug as a key for further usage. $addons_cache[ $addon['slug'] ] = $addon; } return $addons_cache; } } Admin/Challenge.php 0000644 00000043157 15174710275 0010212 0 ustar 00 <?php namespace WPForms\Admin; /** * Challenge and guide a user to set up a first form once WPForms is installed. * * @since 1.5.0 * @since 1.6.2 Challenge v2 */ class Challenge { /** * Number of minutes to complete the Challenge. * * @since 1.5.0 * * @var int */ protected $minutes = 5; /** * Initialize. * * @since 1.6.2 */ public function init() { if ( current_user_can( wpforms_get_capability_manage_options() ) ) { $this->hooks(); } } /** * Hooks. * * @since 1.5.0 */ public function hooks() { add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_scripts' ] ); add_action( 'wpforms_builder_init', [ $this, 'init_challenge' ] ); add_action( 'admin_footer', [ $this, 'challenge_html' ] ); add_action( 'wpforms_welcome_intro_after', [ $this, 'welcome_html' ] ); add_action( 'wp_ajax_wpforms_challenge_save_option', [ $this, 'save_challenge_option_ajax' ] ); add_action( 'wp_ajax_wpforms_challenge_send_contact_form', [ $this, 'send_contact_form_ajax' ] ); } /** * Check if the current page is related to Challenge. * * @since 1.5.0 */ public function is_challenge_page() { return wpforms_is_admin_page() || $this->is_builder_page() || $this->is_form_embed_page(); } /** * Check if the current page is a forms builder page related to Challenge. * * @since 1.5.0 * * @return bool */ public function is_builder_page() { if ( ! wpforms_is_admin_page( 'builder' ) ) { return false; } if ( ! $this->challenge_active() && ! $this->challenge_inited() ) { return false; } $step = (int) $this->get_challenge_option( 'step' ); $form_id = (int) $this->get_challenge_option( 'form_id' ); if ( $form_id && $step < 2 ) { return false; } $current_form_id = isset( $_GET['form_id'] ) ? (int) $_GET['form_id'] : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended $is_new_form = isset( $_GET['newform'] ) ? (int) $_GET['newform'] : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( $is_new_form && $step !== 2 ) { return false; } if ( ! $is_new_form && $form_id !== $current_form_id && $step >= 2 ) { // In case if user skipped the Challenge by closing the browser window or exiting the builder, // we need to set the previous Challenge as `canceled`. // Otherwise, the Form Embed Wizard will think that the Challenge is active. $this->set_challenge_option( [ 'status' => 'skipped', 'finished_date_gmt' => current_time( 'mysql', true ), ] ); return false; } return true; } /** * Check if the current page is a form embed page edit related to Challenge. * * @since 1.5.0 * * @return bool */ public function is_form_embed_page() { if ( ! function_exists( 'get_current_screen' ) || ! is_admin() || ! is_user_logged_in() ) { return false; } $screen = get_current_screen(); if ( ! isset( $screen->id ) || $screen->id !== 'page' || ! $this->challenge_active() ) { return false; } $step = $this->get_challenge_option( 'step' ); if ( ! in_array( $step, [ 3, 4, 5 ], true ) ) { return false; } $embed_page = $this->get_challenge_option( 'embed_page' ); $is_embed_page = false; if ( isset( $screen->action ) && $screen->action === 'add' && $embed_page === 0 ) { $is_embed_page = true; } if ( isset( $_GET['post'] ) && $embed_page === (int) $_GET['post'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $is_embed_page = true; } if ( $is_embed_page && $step < 4 ) { $this->set_challenge_option( [ 'step' => 4 ] ); } return $is_embed_page; } /** * Load scripts and styles. * * @since 1.5.0 */ public function enqueue_scripts() { if ( ! $this->challenge_can_start() && ! $this->challenge_active() ) { return; } $min = wpforms_get_min_suffix(); if ( $this->is_challenge_page() ) { wp_enqueue_style( 'wpforms-challenge', WPFORMS_PLUGIN_URL . "assets/css/challenge{$min}.css", [], WPFORMS_VERSION ); wp_enqueue_script( 'wpforms-challenge-admin', WPFORMS_PLUGIN_URL . "assets/js/admin/challenge/challenge-admin{$min}.js", [ 'jquery' ], WPFORMS_VERSION, true ); wp_localize_script( 'wpforms-challenge-admin', 'wpforms_challenge_admin', [ 'nonce' => wp_create_nonce( 'wpforms_challenge_ajax_nonce' ), 'minutes_left' => absint( $this->minutes ), 'option' => $this->get_challenge_option(), 'frozen_tooltip' => esc_html__( 'Challenge is frozen.', 'wpforms-lite' ), ] ); } if ( $this->is_builder_page() || $this->is_form_embed_page() ) { wp_enqueue_style( 'tooltipster', WPFORMS_PLUGIN_URL . 'assets/lib/jquery.tooltipster/jquery.tooltipster.min.css', null, '4.2.6' ); wp_enqueue_script( 'tooltipster', WPFORMS_PLUGIN_URL . 'assets/lib/jquery.tooltipster/jquery.tooltipster.min.js', [ 'jquery' ], '4.2.6', true ); wp_enqueue_script( 'wpforms-challenge-core', WPFORMS_PLUGIN_URL . "assets/js/admin/challenge/challenge-core{$min}.js", [ 'jquery', 'tooltipster', 'wpforms-challenge-admin', 'wpforms-generic-utils' ], WPFORMS_VERSION, true ); } if ( $this->is_builder_page() ) { wp_enqueue_script( 'wpforms-challenge-builder', WPFORMS_PLUGIN_URL . "assets/js/admin/challenge/challenge-builder{$min}.js", [ 'jquery', 'tooltipster', 'wpforms-challenge-core', 'wpforms-builder' ], WPFORMS_VERSION, true ); } if ( $this->is_form_embed_page() ) { wp_enqueue_style( 'wpforms-font-awesome', WPFORMS_PLUGIN_URL . 'assets/lib/font-awesome/css/all.min.css', null, '7.0.1' ); // FontAwesome v4 compatibility shims. wp_enqueue_style( 'wpforms-font-awesome-v4-shim', WPFORMS_PLUGIN_URL . 'assets/lib/font-awesome/css/v4-shims.min.css', null, '4.7.0' ); wp_enqueue_script( 'wpforms-challenge-embed', WPFORMS_PLUGIN_URL . "assets/js/admin/challenge/challenge-embed{$min}.js", [ 'jquery', 'tooltipster', 'wpforms-challenge-core' ], WPFORMS_VERSION, true ); } } /** * Get 'wpforms_challenge' option schema. * * @since 1.5.0 * * @return array */ public function get_challenge_option_schema() { return [ 'status' => '', 'step' => 0, 'user_id' => get_current_user_id(), 'form_id' => 0, 'embed_page' => 0, 'embed_page_title' => '', 'started_date_gmt' => '', 'finished_date_gmt' => '', 'seconds_spent' => 0, 'seconds_left' => 0, 'feedback_sent' => false, 'feedback_contact_me' => false, 'window_closed' => '', ]; } /** * Get Challenge parameter(s) from Challenge option. * * @since 1.5.0 * * @param array|string|null $query Query using 'wpforms_challenge' schema keys. * * @return array|mixed */ public function get_challenge_option( $query = null ) { if ( ! $query ) { return get_option( 'wpforms_challenge' ); } $return_single = false; if ( ! is_array( $query ) ) { $return_single = true; $query = [ $query ]; } $query = array_flip( $query ); $option = get_option( 'wpforms_challenge' ); if ( ! $option || ! is_array( $option ) ) { return array_intersect_key( $this->get_challenge_option_schema(), $query ); } $result = array_intersect_key( $option, $query ); if ( $return_single ) { $result = reset( $result ); } return $result; } /** * Set Challenge parameter(s) to Challenge option. * * @since 1.5.0 * * @param array $query Query using 'wpforms_challenge' schema keys. */ public function set_challenge_option( $query ) { if ( empty( $query ) || ! is_array( $query ) ) { return; } $schema = $this->get_challenge_option_schema(); $replace = array_intersect_key( $query, $schema ); if ( ! $replace ) { return; } // Validate and sanitize the data. foreach ( $replace as $key => $value ) { if ( in_array( $key, [ 'step', 'user_id', 'form_id', 'embed_page', 'seconds_spent', 'seconds_left' ], true ) ) { $replace[ $key ] = absint( $value ); continue; } if ( in_array( $key, [ 'feedback_sent', 'feedback_contact_me' ], true ) ) { $replace[ $key ] = wp_validate_boolean( $value ); continue; } $replace[ $key ] = sanitize_text_field( $value ); } $option = get_option( 'wpforms_challenge' ); $option = ! $option || ! is_array( $option ) ? $schema : $option; update_option( 'wpforms_challenge', array_merge( $option, $replace ) ); } /** * Check if any forms are present on a site. * * @since 1.5.0 * * @retun bool */ public function website_has_forms() { // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.SuppressFilters_suppress_filters return (bool) wpforms()->obj( 'form' )->get( '', [ 'numberposts' => 1, 'nopaging' => false, 'fields' => 'ids', 'no_found_rows' => true, 'update_post_meta_cache' => false, 'update_post_term_cache' => false, 'suppress_filters' => true, // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.SuppressFilters_suppress_filters ] ); } /** * Check if Challenge was started. * * @since 1.5.0 * * @return bool */ public function challenge_started() { return $this->get_challenge_option( 'status' ) === 'started'; } /** * Check if Challenge was initialized. * * @since 1.6.2 * * @return bool */ public function challenge_inited() { return $this->get_challenge_option( 'status' ) === 'inited'; } /** * Check if Challenge was paused. * * @since 1.6.2 * * @return bool */ public function challenge_paused() { return $this->get_challenge_option( 'status' ) === 'paused'; } /** * Check if Challenge was finished. * * @since 1.5.0 * * @return bool */ public function challenge_finished() { $status = $this->get_challenge_option( 'status' ); return in_array( $status, [ 'completed', 'canceled', 'skipped' ], true ); } /** * Check if Challenge is in progress. * * @since 1.5.0 * * @return bool */ public function challenge_active() { return ( $this->challenge_inited() || $this->challenge_started() || $this->challenge_paused() ) && ! $this->challenge_finished(); } /** * Force Challenge to start. * * @since 1.6.2 * * @return bool */ public function challenge_force_start() { /** * Allow force start Challenge for testing purposes. * * @since 1.6.2.2 * * @param bool $is_forced True if Challenge should be started. False by default. */ return (bool) apply_filters( 'wpforms_admin_challenge_force_start', false ); } /** * Check if Challenge can be started. * * @since 1.5.0 * * @return bool */ public function challenge_can_start() { static $can_start = null; if ( $can_start !== null ) { return $can_start; } if ( $this->challenge_force_skip() ) { $can_start = false; } // Challenge is only available on WPForms admin pages or Builder page. if ( ! wpforms_is_admin_page() && ! wpforms_is_admin_page( 'builder' ) ) { $can_start = false; // No need to check something else in this case. return false; } // The challenge should not start if this is the Forms' Overview page. if ( wpforms_is_admin_page( 'overview' ) ) { $can_start = false; // No need to check something else in this case. return false; } // Force start the Challenge. if ( $this->challenge_force_start() && ! $this->is_builder_page() && ! $this->is_form_embed_page() ) { $can_start = true; // No need to check something else in this case. return true; } if ( $this->challenge_finished() ) { $can_start = false; } if ( $this->website_has_forms() ) { $can_start = false; } if ( $can_start === null ) { $can_start = true; } return $can_start; } /** * Start the Challenge in Form Builder. * * @since 1.5.0 */ public function init_challenge() { if ( ! $this->challenge_can_start() ) { return; } $this->set_challenge_option( wp_parse_args( [ 'status' => 'inited' ], $this->get_challenge_option_schema() ) ); } /** * Include Challenge HTML. * * @since 1.5.0 */ public function challenge_html() { if ( $this->challenge_force_skip() || ( $this->challenge_finished() && ! $this->challenge_force_start() ) ) { return; } if ( wpforms_is_admin_page() && ! wpforms_is_admin_page( 'getting-started' ) && $this->challenge_can_start() ) { // Before showing the Challenge in the `start` state we should reset the option. // In this way we ensure the Challenge will not appear somewhere in the builder where it is not should be. $this->set_challenge_option( [ 'status' => '' ] ); $this->challenge_modal_html( 'start' ); } if ( $this->is_builder_page() ) { $this->challenge_modal_html( 'progress' ); $this->challenge_builder_templates_html(); } if ( $this->is_form_embed_page() ) { $this->challenge_modal_html( 'progress' ); $this->challenge_embed_templates_html(); } } /** * Include Challenge main modal window HTML. * * @since 1.5.0 * * @param string $state State of Challenge ('start' or 'progress'). */ public function challenge_modal_html( $state ) { echo wpforms_render( // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped 'admin/challenge/modal', [ 'state' => $state, 'step' => $this->get_challenge_option( 'step' ), 'minutes' => $this->minutes, ], true ); } /** * Include Challenge HTML templates specific to Form Builder. * * @since 1.5.0 */ public function challenge_builder_templates_html() { echo wpforms_render( 'admin/challenge/builder' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } /** * Include Challenge HTML templates specific to form embed page. * * @since 1.5.0 */ public function challenge_embed_templates_html() { /** * Filter the content of the Challenge Congrats popup footer. * * @since 1.7.4 * * @param string $footer Footer markup. */ $congrats_popup_footer = apply_filters( 'wpforms_admin_challenge_embed_template_congrats_popup_footer', '' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo wpforms_render( 'admin/challenge/embed', [ 'minutes' => $this->minutes, 'congrats_popup_footer' => $congrats_popup_footer, ], true ); } /** * Include Challenge CTA on WPForms welcome activation screen. * * @since 1.5.0 */ public function welcome_html() { if ( $this->challenge_can_start() ) { echo wpforms_render( 'admin/challenge/welcome' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } } /** * Save Challenge data via AJAX. * * @since 1.5.0 */ public function save_challenge_option_ajax() { check_admin_referer( 'wpforms_challenge_ajax_nonce' ); if ( empty( $_POST['option_data'] ) ) { wp_send_json_error(); } $schema = $this->get_challenge_option_schema(); $query = []; foreach ( $schema as $key => $value ) { if ( isset( $_POST['option_data'][ $key ] ) ) { $query[ $key ] = sanitize_text_field( wp_unslash( $_POST['option_data'][ $key ] ) ); } } if ( empty( $query ) ) { wp_send_json_error(); } if ( ! empty( $query['status'] ) && $query['status'] === 'started' ) { $query['started_date_gmt'] = current_time( 'mysql', true ); } if ( ! empty( $query['status'] ) && in_array( $query['status'], [ 'completed', 'canceled', 'skipped' ], true ) ) { $query['finished_date_gmt'] = current_time( 'mysql', true ); } if ( ! empty( $query['status'] ) && $query['status'] === 'skipped' ) { $query['started_date_gmt'] = current_time( 'mysql', true ); $query['finished_date_gmt'] = $query['started_date_gmt']; } $this->set_challenge_option( $query ); wp_send_json_success(); } /** * Send contact form to wpforms.com via AJAX. * * @since 1.5.0 */ public function send_contact_form_ajax() { check_admin_referer( 'wpforms_challenge_ajax_nonce' ); $url = 'https://wpforms.com/wpforms-challenge-feedback/'; $message = ! empty( $_POST['contact_data']['message'] ) ? sanitize_textarea_field( wp_unslash( $_POST['contact_data']['message'] ) ) : ''; $email = ''; if ( ( ! empty( $_POST['contact_data']['contact_me'] ) && $_POST['contact_data']['contact_me'] === 'true' ) || wpforms()->is_pro() ) { $current_user = wp_get_current_user(); $email = $current_user->user_email; $this->set_challenge_option( [ 'feedback_contact_me' => true ] ); } if ( empty( $message ) && empty( $email ) ) { wp_send_json_error(); } $data = [ 'body' => [ 'wpforms' => [ 'id' => 296355, 'submit' => 'wpforms-submit', 'fields' => [ 2 => $message, 3 => $email, 4 => $this->get_challenge_license_type(), 5 => wpforms()->version, 6 => wpforms_get_license_key(), ], ], ], ]; $response = wp_remote_post( $url, $data ); if ( is_wp_error( $response ) ) { wp_send_json_error(); } $this->set_challenge_option( [ 'feedback_sent' => true ] ); wp_send_json_success(); } /** * Get the current WPForms license type as it pertains to the challenge feedback form. * * @since 1.8.1 * * @return string The currently active license type. */ private function get_challenge_license_type() { $license_type = wpforms_get_license_type(); if ( $license_type === false ) { $license_type = wpforms()->is_pro() ? 'Unknown' : 'Lite'; } return ucfirst( $license_type ); } /** * Force WPForms Challenge to skip. * * @since 1.7.6 * * @return bool */ private function challenge_force_skip() { return defined( 'WPFORMS_SKIP_CHALLENGE' ) && WPFORMS_SKIP_CHALLENGE; } } Admin/Payments/Views/PaymentsViewsInterface.php 0000644 00000001100 15174710275 0015642 0 ustar 00 <?php namespace WPForms\Admin\Payments\Views; interface PaymentsViewsInterface { /** * Initialize class. * * @since 1.8.2 */ public function init(); /** * Check if the current user has the capability to view the page. * * @since 1.8.2 * * @return bool */ public function current_user_can(); /** * Page heading content. * * @since 1.8.2 */ public function heading(); /** * Page content. * * @since 1.8.2 */ public function display(); /** * Get the Tab label. * * @since 1.8.2.2 */ public function get_tab_label(); } Admin/Payments/Views/Overview/Chart.php 0000644 00000020054 15174710275 0014063 0 ustar 00 <?php namespace WPForms\Admin\Payments\Views\Overview; use WPForms\Admin\Helpers\Datepicker; /** * Payment Overview Chart class. * * @since 1.8.2 */ class Chart { /** * Default payments summary report stat card. * * @since 1.8.2 */ const ACTIVE_REPORT = 'total_payments'; /** * Whether the chart should be displayed. * * @since 1.8.2 * * @return bool */ private function allow_load() { $disallowed_views = [ 's', // Search. 'type', // Payment type. 'status', // Payment status. 'gateway', // Payment gateway. 'subscription_status', // Subscription status. 'form_id', // Form ID. 'coupon_id', // Coupon ID. ]; // Avoid displaying the chart when filtering of payment records is performed. // phpcs:disable WordPress.Security.NonceVerification.Recommended return array_reduce( array_keys( $_GET ), static function ( $carry, $key ) use ( $disallowed_views ) { if ( ! $carry ) { return false; } return ! in_array( $key, $disallowed_views, true ) || empty( $_GET[ $key ] ); }, true ); // phpcs:enable WordPress.Security.NonceVerification.Recommended } /** * Display the chart. * * @since 1.8.2 */ public function display() { // If the chart should not be displayed, leave early. if ( ! $this->allow_load() ) { return; } // Output HTML elements on the page. $this->output_top_bar(); $this->output_test_mode_banner(); $this->output_chart(); } /** * Handles output of the overview page top-bar. * * Includes: * 1. Heading. * 2. Datepicker filter. * 3. Chart theme customization settings. * * @since 1.8.2 */ private function output_top_bar() { list( $choices, $chosen_filter, $value ) = Datepicker::process_datepicker_choices(); ?> <div class="wpforms-overview-top-bar"> <div class="wpforms-overview-top-bar-heading"> <h2><?php esc_html_e( 'Payments Summary', 'wpforms-lite' ); ?></h2> </div> <div class="wpforms-overview-top-bar-filters"> <?php // Output "Mode Toggle" template. ( new ModeToggle() )->display(); // Output "Datepicker" form template. // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo wpforms_render( 'admin/components/datepicker', [ 'id' => 'payments', 'action' => Page::get_url(), 'chosen_filter' => $chosen_filter, 'choices' => $choices, 'value' => $value, 'hidden_fields' => [ 'statcard' ], ], true ); ?> <div class="wpforms-overview-chart-settings"> <?php // Output "Settings" template. // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo wpforms_render( 'admin/dashboard/widget/settings', array_merge( $this->get_chart_settings(), [ 'enabled' => true ] ), true ); ?> </div> </div> </div> <?php } /** * Display a banner when viewing test data. * * @since 1.8.2 * * @return void */ private function output_test_mode_banner() { // Determine if we are viewing test data. if ( Page::get_mode() !== 'test' ) { return; } ?> <div class="wpforms-payments-viewing-test-mode"> <p> <?php esc_html_e( 'Viewing Test Data', 'wpforms-lite' ); ?> </p> </div> <?php } /** * Handles output of the overview page chart (graph). * * @since 1.8.2 */ private function output_chart() { // phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped echo '<div class="wpforms-payments-overview-stats">'; echo wpforms_render( 'admin/components/chart', [ 'id' => 'payments', 'notice' => [ 'heading' => esc_html__( 'No payments for selected period', 'wpforms-lite' ), 'description' => esc_html__( 'Please select a different period or check back later.', 'wpforms-lite' ), ], ], true ); echo wpforms_render( 'admin/payments/reports', $this->get_reports_template_args(), true ); echo '</div>'; // phpcs:enable WordPress.Security.EscapeOutput.OutputNotEscaped } /** * Get the user’s preferences for displaying of the graph. * * @since 1.8.2 * * @return array */ public function get_chart_settings() { $graph_style = get_user_meta( get_current_user_id(), 'wpforms_dash_widget_graph_style', true ); return [ 'graph_style' => $graph_style ? absint( $graph_style ) : 2, // Line. ]; } /** * Get the stat cards for the payment summary report. * * Note that "funnel" is used to filter the payments, and can take the following values: * - in: payments that match the given criteria. * - not_in: payments that do not match the given criteria. * * @since 1.8.2 * * @return array */ public static function stat_cards() { return [ 'total_payments' => [ 'label' => esc_html__( 'Total Payments', 'wpforms-lite' ), 'button_classes' => [ 'total-payments', ], ], 'total_sales' => [ 'label' => esc_html__( 'Total Sales', 'wpforms-lite' ), 'funnel' => [ 'not_in' => [ 'status' => [ 'failed' ], 'subscription_status' => [ 'failed' ], ], ], 'button_classes' => [ 'total-sales', 'is-amount', ], ], 'total_refunded' => [ 'label' => esc_html__( 'Total Refunded', 'wpforms-lite' ), 'has_count' => true, 'meta_key' => 'refunded_amount', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key 'button_classes' => [ 'total-refunded', 'is-amount', ], ], 'total_subscription' => [ 'label' => esc_html__( 'New Subscriptions', 'wpforms-lite' ), 'condition' => wpforms()->obj( 'payment_queries' )->has_subscription(), 'has_count' => true, 'funnel' => [ 'in' => [ 'type' => [ 'subscription' ], ], 'not_in' => [ 'subscription_status' => [ 'failed' ], ], ], 'button_classes' => [ 'total-subscription', 'is-amount', ], ], 'total_renewal_subscription' => [ 'label' => esc_html__( 'Subscription Renewals', 'wpforms-lite' ), 'condition' => wpforms()->obj( 'payment_queries' )->has_subscription(), 'has_count' => true, 'funnel' => [ 'in' => [ 'type' => [ 'renewal' ], ], 'not_in' => [ 'subscription_status' => [ 'failed' ], ], ], 'button_classes' => [ 'total-renewal-subscription', 'is-amount', ], ], 'total_coupons' => [ 'label' => esc_html__( 'Coupons Redeemed', 'wpforms-lite' ), 'meta_key' => 'coupon_id', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key 'funnel' => [ 'not_in' => [ 'status' => [ 'failed' ], 'subscription_status' => [ 'failed' ], ], ], 'button_classes' => [ 'total-coupons', ], ], ]; } /** * Retrieves the arguments for the reports template. * * @since 1.8.8 * * @return array */ private function get_reports_template_args(): array { // Retrieve the stat cards. $stat_cards = self::stat_cards(); // Set default arguments. $args = [ 'current' => self::ACTIVE_REPORT, 'statcards' => $stat_cards, ]; // Check if the statcard is set in the URL. // phpcs:disable WordPress.Security.NonceVerification.Recommended if ( empty( $_GET['statcard'] ) ) { return $args; } // Sanitize and retrieve the tab value from the URL. $active_report = sanitize_text_field( wp_unslash( $_GET['statcard'] ) ); // phpcs:enable WordPress.Security.NonceVerification.Recommended // If the statcard is not valid, return default arguments. if ( ! isset( $stat_cards[ $active_report ] ) ) { return $args; } // If the statcard is not going to be displayed, return default arguments. if ( isset( $stat_cards[ $active_report ]['condition'] ) && ! $stat_cards[ $active_report ]['condition'] ) { return $args; } // Set the current statcard. $args['current'] = $active_report; return $args; } } Admin/Payments/Views/Overview/Coupon.php 0000644 00000011475 15174710275 0014274 0 ustar 00 <?php namespace WPForms\Admin\Payments\Views\Overview; use WPForms\Admin\Payments\Payments; /** * Generic functionality for interacting with the Coupons data. * * @since 1.8.4 */ class Coupon { /** * Initialize the Coupon class. * * @since 1.8.4 */ public function init() { $this->hooks(); } /** * Attach hooks for filtering payments by coupon ID. * * @since 1.8.4 */ private function hooks() { // This filter has been added for backward compatibility with older versions of the Coupons addon. add_filter( 'wpforms_admin_payments_views_overview_table_get_columns', [ $this, 'remove_legacy_coupon_column' ], 99, 1 ); // Bail early if the current page is not the Payments page // or if no coupon ID is given in the URL. if ( ! self::is_coupon() ) { return; } add_filter( 'wpforms_db_payments_payment_get_payments_query_after_where', [ $this, 'filter_by_coupon_id' ], 10, 2 ); add_filter( 'wpforms_db_payments_queries_count_all_query_after_where', [ $this, 'filter_by_coupon_id' ], 10, 2 ); add_filter( 'wpforms_admin_payments_views_overview_filters_renewals_by_subscription_id_query_after_where', [ $this, 'filter_by_coupon_id' ], 10, 2 ); add_filter( 'wpforms_admin_payments_views_overview_search_inner_join_query', [ $this, 'join_search_by_coupon_id' ], 10, 2 ); } /** * Remove the legacy coupon column from the Payments page. * * This function has been added for backward compatibility with older versions of the Coupons addon. * The legacy coupon column is no longer used by the Coupons addon. * * @since 1.8.4 * * @param array $columns List of columns to be displayed on the Payments page. * * @return array */ public function remove_legacy_coupon_column( $columns ) { // Bail early if the Coupons addon is not active. if ( ! $this->is_addon_active() ) { return $columns; } // Remove the legacy coupon column from the Payments page. unset( $columns['coupon_id'] ); return $columns; } /** * Retrieve payment entries based on a given coupon ID. * * @since 1.8.4 * * @param string $after_where SQL query after the WHERE clause. * @param array $args Query arguments. * * @return string */ public function filter_by_coupon_id( $after_where, $args ) { // Check if the query is for the Payments Overview table. // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( empty( $args['table_query'] ) ) { return $after_where; } // Retrieve the coupon ID from the URL. // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.NonceVerification.Recommended $coupon_id = absint( $_GET['coupon_id'] ); global $wpdb; $table_name = wpforms()->obj( 'payment_meta' )->table_name; // Prepare and return the modified SQL query. // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared return $wpdb->prepare( " AND EXISTS ( SELECT 1 FROM {$table_name} AS pm_coupon WHERE pm_coupon.payment_id = p.id AND pm_coupon.meta_key = 'coupon_id' AND pm_coupon.meta_value = %d )", $coupon_id ); } /** * Further filter down the search results by coupon ID. * * @since 1.8.4 * * @param string $query The SQL JOIN clause. * @param int $n The number of the JOIN clause. * * @return string */ public function join_search_by_coupon_id( $query, $n ) { // Retrieve the coupon ID from the URL. // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.NonceVerification.Recommended $coupon_id = absint( $_GET['coupon_id'] ); // Retrieve the global database instance. global $wpdb; $n = absint( $n ); $table_name = wpforms()->obj( 'payment_meta' )->table_name; // Build the derived query using a prepared statement. // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared $derived_query = $wpdb->prepare( "RIGHT JOIN ( SELECT payment_id, meta_key, meta_value FROM {$table_name} WHERE meta_key = 'coupon_id' AND meta_value = %d ) AS pm_coupon{$n} ON p.id = pm_coupon{$n}.payment_id", $coupon_id ); // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared // Combine the original query and the derived query. return "$query $derived_query"; } /** * Determine if the overview page is being viewed, and coupon ID is given. * * @since 1.8.4 * * @return bool */ public static function is_coupon() { // Check if the URL parameters contain a coupon ID and if the current page is the Payments page. // phpcs:ignore WordPress.Security.NonceVerification.Recommended return ! empty( $_GET['coupon_id'] ) && ! empty( $_GET['page'] ) && $_GET['page'] === Payments::SLUG; } /** * Determine whether the addon is activated. * * @since 1.8.4 * * @return bool */ private function is_addon_active() { return function_exists( 'wpforms_coupons' ); } } Admin/Payments/Views/Overview/Ajax.php 0000644 00000041752 15174710275 0013715 0 ustar 00 <?php namespace WPForms\Admin\Payments\Views\Overview; use DateTimeImmutable; // phpcs:ignore WPForms.PHP.UseStatement.UnusedUseStatement use wpdb; use WPForms\Db\Payments\ValueValidator; use WPForms\Admin\Helpers\Chart as ChartHelper; use WPForms\Admin\Helpers\Datepicker; /** * "Payments" overview page inside the admin, which lists all payments. * This page will be accessible via "WPForms" → "Payments". * * When requested data is sent via Ajax, this class is responsible for exchanging datasets. * * @since 1.8.2 */ class Ajax { /** * Database table name. * * @since 1.8.2 * * @var string */ private $table_name; /** * Temporary storage for the stat cards. * * @since 1.8.4 * * @var array */ private $stat_cards; /** * Hooks. * * @since 1.8.2 */ public function hooks() { add_action( 'wp_ajax_wpforms_payments_overview_refresh_chart_dataset_data', [ $this, 'get_chart_dataset_data' ] ); add_action( 'wp_ajax_wpforms_payments_overview_save_chart_preference_settings', [ $this, 'save_chart_preference_settings' ] ); add_filter( 'wpforms_db_payments_payment_add_secondary_where_conditions_args', [ $this, 'modify_secondary_where_conditions_args' ] ); } /** * Generate and return the data for our dataset data. * * @since 1.8.2 */ public function get_chart_dataset_data() { // Run a security check. check_ajax_referer( 'wpforms_payments_overview_nonce' ); // Check for permissions. if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( esc_html__( 'You are not allowed to perform this action.', 'wpforms-lite' ) ); } $report = ! empty( $_POST['report'] ) ? sanitize_text_field( wp_unslash( $_POST['report'] ) ) : null; $dates = ! empty( $_POST['dates'] ) ? sanitize_text_field( wp_unslash( $_POST['dates'] ) ) : null; $fallback = [ 'data' => [], 'reports' => [], ]; // If the report type or dates for the timespan are missing, leave early. if ( ! $report || ! $dates ) { wp_send_json_error( $fallback ); } // Validates and creates date objects of given timespan string. $timespans = Datepicker::process_string_timespan( $dates ); // If the timespan is not validated, leave early. if ( ! $timespans ) { wp_send_json_error( $fallback ); } // Extract start and end timespans in local (site) and UTC timezones. list( $start_date, $end_date, $utc_start_date, $utc_end_date ) = $timespans; // Payment table name. $this->table_name = wpforms()->obj( 'payment' )->table_name; // Get the stat cards. $this->stat_cards = Chart::stat_cards(); // Get the payments in the given timespan. $results = $this->get_payments_in_timespan( $utc_start_date, $utc_end_date, $report ); // In case the database's results were empty, leave early. if ( $report === Chart::ACTIVE_REPORT && empty( $results ) ) { wp_send_json_error( $fallback ); } // Process the results and return the data. // The first element of the array is the total number of entries, the second is the data. list( , $data ) = ChartHelper::process_chart_dataset_data( $results, $start_date, $end_date ); // Sends the JSON response back to the Ajax request, indicating success. wp_send_json_success( [ 'data' => $data, 'reports' => $this->get_payments_summary_in_timespan( $start_date, $end_date ), ] ); } /** * Save the user's preferred graph style and color scheme. * * @since 1.8.2 */ public function save_chart_preference_settings() { // Run a security check. check_ajax_referer( 'wpforms_payments_overview_nonce' ); // Check for permissions. if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( esc_html__( 'You are not allowed to perform this action.', 'wpforms-lite' ) ); } $graph_style = isset( $_POST['graphStyle'] ) ? absint( $_POST['graphStyle'] ) : 2; // Line. update_user_meta( get_current_user_id(), 'wpforms_dash_widget_graph_style', $graph_style ); exit(); } /** * Retrieve and create payment entries from the database within the specified time frame (timespan). * * @global wpdb $wpdb Instantiation of the wpdb class. * * @since 1.8.2 * * @param DateTimeImmutable $start_date Start date for the timespan preferably in UTC. * @param DateTimeImmutable $end_date End date for the timespan preferably in UTC. * @param string $report Payment summary stat card name. i.e. "total_payments". * * @return array */ private function get_payments_in_timespan( $start_date, $end_date, $report ) { // Ensure given timespan dates are in UTC timezone. list( $utc_start_date, $utc_end_date ) = Datepicker::process_timespan_mysql( [ $start_date, $end_date ] ); // If the time period is not a date object, leave early. if ( ! ( $start_date instanceof DateTimeImmutable ) || ! ( $end_date instanceof DateTimeImmutable ) ) { return []; } // Get the database instance. global $wpdb; // SELECT clause to construct the SQL statement. $column_clause = $this->get_stats_column_clause( $report ); // JOIN clause to construct the SQL statement for metadata. $join_by_meta = $this->add_join_by_meta( $report ); // WHERE clauses for items query statement. $where_clause = $this->get_stats_where_clause( $report ); // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared return $wpdb->get_results( $wpdb->prepare( "SELECT date_created_gmt AS day, $column_clause AS count FROM $this->table_name AS p {$join_by_meta} WHERE 1=1 $where_clause AND date_created_gmt BETWEEN %s AND %s GROUP BY day ORDER BY day ASC", [ $utc_start_date->format( Datepicker::DATETIME_FORMAT ), $utc_end_date->format( Datepicker::DATETIME_FORMAT ), ] ), ARRAY_A ); // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared } /** * Fetch and generate payment summary reports from the database. * * @global wpdb $wpdb Instantiation of the wpdb class. * * @since 1.8.2 * * @param DateTimeImmutable $start_date Start date for the timespan preferably in UTC. * @param DateTimeImmutable $end_date End date for the timespan preferably in UTC. * * @return array */ private function get_payments_summary_in_timespan( $start_date, $end_date ) { // Ensure given timespan dates are in UTC timezone. list( $utc_start_date, $utc_end_date ) = Datepicker::process_timespan_mysql( [ $start_date, $end_date ] ); // If the time period is not a date object, leave early. if ( ! ( $start_date instanceof DateTimeImmutable ) || ! ( $end_date instanceof DateTimeImmutable ) ) { return []; } // Get the database instance. global $wpdb; list( $clause, $query ) = $this->prepare_sql_summary_reports( $utc_start_date, $utc_end_date ); $group_by = Chart::ACTIVE_REPORT; // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $results = $wpdb->get_row( "SELECT $clause FROM (SELECT $query) AS results GROUP BY $group_by", ARRAY_A ); // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared return $this->maybe_format_amounts( $results ); } /** * Generate SQL statements to create a derived (virtual) table for the report stat cards. * * @global wpdb $wpdb Instantiation of the wpdb class. * * @since 1.8.2 * * @param DateTimeImmutable $start_date Start date for the timespan. * @param DateTimeImmutable $end_date End date for the timespan. * * @return array */ private function prepare_sql_summary_reports( $start_date, $end_date ) { // In case there are no report stat cards defined, leave early. if ( empty( $this->stat_cards ) ) { return [ '', '' ]; } global $wpdb; $clause = []; // SELECT clause. $query = []; // Query statement for the derived table. // Validates and creates date objects for the previous time spans. $prev_timespans = Datepicker::get_prev_timespan_dates( $start_date, $end_date ); // If the timespan is not validated, leave early. if ( ! $prev_timespans ) { return [ '', '' ]; } list( $prev_start_date, $prev_end_date ) = $prev_timespans; // Get the default number of decimals for the payment currency. $current_currency = wpforms_get_currency(); $currency_decimals = wpforms_get_currency_decimals( $current_currency ); // Loop through the reports and create the SQL statements. foreach ( $this->stat_cards as $report => $attributes ) { // Skip stat card, if it's not supposed to be displayed or disabled (upsell). if ( ( isset( $attributes['condition'] ) && ! $attributes['condition'] ) || in_array( 'disabled', $attributes['button_classes'], true ) ) { continue; } // Determine whether the number of rows has to be counted. $has_count = isset( $attributes['has_count'] ) && $attributes['has_count']; // SELECT clause to construct the SQL statement. $column_clause = $this->get_stats_column_clause( $report, $has_count ); // JOIN clause to construct the SQL statement for metadata. $join_by_meta = $this->add_join_by_meta( $report ); // WHERE clauses for items query statement. $where_clause = $this->get_stats_where_clause( $report ); // Get the current and previous values for the report. $current_value = "TRUNCATE($report,$currency_decimals)"; $prev_value = "TRUNCATE({$report}_prev,$currency_decimals)"; // Add the current and previous reports to the SELECT clause. $clause[] = $report; $clause[] = "ROUND( ( ( $current_value - $prev_value ) / $current_value ) * 100 ) AS {$report}_delta"; // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.MissingReplacements $query[] = $wpdb->prepare( "( SELECT $column_clause FROM $this->table_name AS p {$join_by_meta} WHERE 1=1 $where_clause AND date_created_gmt BETWEEN %s AND %s ) AS $report, ( SELECT $column_clause FROM $this->table_name AS p {$join_by_meta} WHERE 1=1 $where_clause AND date_created_gmt BETWEEN %s AND %s ) AS {$report}_prev", [ $start_date->format( Datepicker::DATETIME_FORMAT ), $end_date->format( Datepicker::DATETIME_FORMAT ), $prev_start_date->format( Datepicker::DATETIME_FORMAT ), $prev_end_date->format( Datepicker::DATETIME_FORMAT ), ] ); // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.MissingReplacements } return [ implode( ',', $clause ), implode( ',', $query ), ]; } /** * Helper method to build where clause used to construct the SQL statement. * * @since 1.8.2 * * @param string $report Payment summary stat card name. i.e. "total_payments". * * @return string */ private function get_stats_where_clause( $report ) { // Get the default WHERE clause from the Payments database class. $clause = wpforms()->obj( 'payment' )->add_secondary_where_conditions(); // If the report doesn't have any additional funnel arguments, leave early. if ( ! isset( $this->stat_cards[ $report ]['funnel'] ) ) { return $clause; } // Get the where arguments for the report. $where_args = (array) $this->stat_cards[ $report ]['funnel']; // If the where arguments are empty, leave early. if ( empty( $where_args ) ) { return $clause; } return $this->prepare_sql_where_clause( $where_args, $clause ); } /** * Prepare SQL where clause for the given funnel arguments. * * @since 1.8.4 * * @param array $where_args Array of where arguments. * @param string $clause SQL where clause. * * @return string */ private function prepare_sql_where_clause( $where_args, $clause ) { $allowed_funnels = [ 'in', 'not_in' ]; $filtered_where_args = array_filter( $where_args, static function ( $key ) use ( $allowed_funnels ) { return in_array( $key, $allowed_funnels, true ); }, ARRAY_FILTER_USE_KEY ); // Leave early if the filtered where arguments are empty. if ( empty( $filtered_where_args ) ) { return $clause; } // Loop through the where arguments and add them to the clause. foreach ( $filtered_where_args as $operator => $columns ) { foreach ( $columns as $column => $values ) { if ( ! is_array( $values ) ) { continue; } // Skip if the value is not valid. $valid_values = array_filter( $values, static function ( $item ) use ( $column ) { return ValueValidator::is_valid( $item, $column ); } ); $placeholders = wpforms_wpdb_prepare_in( $valid_values ); $clause .= $operator === 'in' ? " AND {$column} IN ({$placeholders})" : " AND {$column} NOT IN ({$placeholders})"; } } return $clause; } /** * Helper method to build column clause used to construct the SQL statement. * * @since 1.8.2 * * @param string $report Stats card chart type (name). i.e. "total_payments". * @param bool $with_count Whether to concatenate the count to the clause. * * @return string */ private function get_stats_column_clause( $report, $with_count = false ) { // Default column clause. // Count the number of rows as fast as possible. $default = 'COUNT(*)'; // If the report has a meta key, then count the number of unique rows for the meta table. if ( isset( $this->stat_cards[ $report ]['meta_key'] ) ) { $default = 'COUNT(pm.id)'; } /** * Filters the column clauses for the stat cards. * * @since 1.8.2 * * @param array $clauses Array of column clauses. */ $clauses = (array) apply_filters( 'wpforms_admin_payments_views_overview_ajax_stats_column_clauses', [ 'total_payments' => "FORMAT({$default},0)", 'total_sales' => 'IFNULL(SUM(total_amount),0)', 'total_refunded' => 'IFNULL(SUM(pm.meta_value),0)', 'total_subscription' => 'IFNULL(SUM(total_amount),0)', 'total_renewal_subscription' => 'IFNULL(SUM(total_amount),0)', 'total_coupons' => "FORMAT({$default},0)", ] ); $clause = isset( $clauses[ $report ] ) ? $clauses[ $report ] : $default; // Several stat cards might include the count of payment records. if ( $with_count ) { $clause = "CONCAT({$clause}, ' (', {$default}, ')')"; } return $clause; } /** * Add join by meta table. * * @since 1.8.4 * * @param string $report Stats card chart type (name). i.e. "total_payments". * * @return string */ private function add_join_by_meta( $report ) { // Leave early if the meta key is empty. if ( ! isset( $this->stat_cards[ $report ]['meta_key'] ) ) { return ''; } // Retrieve the global database instance. global $wpdb; // Retrieve the meta table name. $meta_table_name = wpforms()->obj( 'payment_meta' )->table_name; return $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared "LEFT JOIN {$meta_table_name} AS pm ON p.id = pm.payment_id AND pm.meta_key = %s", $this->stat_cards[ $report ]['meta_key'] ); } /** * Modify arguments of secondary where clauses. * * @since 1.8.2 * * @param array $args Query arguments. * * @return array */ public function modify_secondary_where_conditions_args( $args ) { // Set a current mode. if ( ! isset( $args['mode'] ) ) { $args['mode'] = Page::get_mode(); } return $args; } /** * Maybe format the amounts for the given stat cards. * * @since 1.8.4 * * @param array $results Query results. * * @return array */ private function maybe_format_amounts( $results ) { // If the input is empty, leave early. if ( empty( $results ) ) { return []; } foreach ( $results as $key => $value ) { // If the given stat card doesn't have a button class, leave early. // If the given stat card doesn't have a button class of "is-amount," leave early. if ( ! isset( $this->stat_cards[ $key ]['button_classes'] ) || ! in_array( 'is-amount', $this->stat_cards[ $key ]['button_classes'], true ) ) { continue; } // Split the input by space to look for the count. $input_arr = (array) explode( ' ', $value ); // If the given stat card doesn't have a count, leave early. if ( empty( $this->stat_cards[ $key ]['has_count'] ) || ! isset( $input_arr[1] ) ) { // Format the given amount and split the input by space. $results[ $key ] = wpforms_format_amount( $value, true ); continue; } // The fields are stored as a `decimal` in the DB, and appears here as the string. // But all strings values, passed to wpforms_format_amount() are sanitized. // There is no need to sanitize it, as it is already a regular numeric string. $amount = wpforms_format_amount( (float) ( $input_arr[0] ?? $value ), true ); // Format the amount with the concatenation of count in parentheses. // Example: 2185.52000000 (79). $results[ $key ] = sprintf( '%s <span>%s</span>', esc_html( $amount ), esc_html( $input_arr[1] ) // 1: Would be count of the records. ); } return $results; } } Admin/Payments/Views/Overview/Page.php 0000644 00000030210 15174710275 0013671 0 ustar 00 <?php namespace WPForms\Admin\Payments\Views\Overview; use WPForms\Admin\Helpers\Datepicker; use WPForms\Db\Payments\ValueValidator; use WPForms\Admin\Payments\Payments; use WPForms\Admin\Payments\Views\PaymentsViewsInterface; use WPForms\Integrations\Stripe\Helpers as StripeHelpers; use WPForms\Integrations\Square\Helpers as SquareHelpers; /** * Payments Overview Page class. * * @since 1.8.2 */ class Page implements PaymentsViewsInterface { /** * Payments table. * * @since 1.8.2 * * @var Table */ private $table; /** * Payments chart. * * @since 1.8.2 * * @var Chart */ private $chart; /** * Initialize class. * * @since 1.8.2 */ public function init() { if ( ! $this->has_any_mode_payment() ) { return; } $this->chart = new Chart(); $this->table = new Table(); $this->table->prepare_items(); $this->clean_request_uri(); $this->hooks(); } /** * Register hooks. * * @since 1.8.2 */ private function hooks() { add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] ); } /** * Get the tab label. * * @since 1.8.2.2 * * @return string */ public function get_tab_label() { return __( 'Overview', 'wpforms-lite' ); } /** * Enqueue scripts and styles. * * @since 1.8.2 */ public function enqueue_assets() { $min = wpforms_get_min_suffix(); wp_enqueue_style( 'wpforms-flatpickr', WPFORMS_PLUGIN_URL . 'assets/lib/flatpickr/flatpickr.min.css', [], '4.6.9' ); wp_enqueue_script( 'wpforms-flatpickr', WPFORMS_PLUGIN_URL . 'assets/lib/flatpickr/flatpickr.min.js', [ 'jquery' ], '4.6.9', true ); wp_enqueue_style( 'wpforms-multiselect-checkboxes', WPFORMS_PLUGIN_URL . 'assets/lib/wpforms-multiselect/wpforms-multiselect-checkboxes.min.css', [], '1.0.0' ); wp_enqueue_script( 'wpforms-multiselect-checkboxes', WPFORMS_PLUGIN_URL . 'assets/lib/wpforms-multiselect/wpforms-multiselect-checkboxes.min.js', [], '1.0.0', true ); wp_enqueue_script( 'wpforms-chart', WPFORMS_PLUGIN_URL . 'assets/lib/chart.min.js', [ 'moment' ], '4.5.1', true ); wp_enqueue_script( 'wpforms-chart-adapter-moment', WPFORMS_PLUGIN_URL . 'assets/lib/chartjs-adapter-moment.min.js', [ 'moment', 'wpforms-chart' ], '1.0.1', true ); wp_enqueue_script( 'wpforms-admin-payments-overview', WPFORMS_PLUGIN_URL . "assets/js/admin/payments/overview{$min}.js", [ 'jquery', 'wpforms-flatpickr', 'wpforms-chart' ], WPFORMS_VERSION, true ); $admin_l10n = [ 'settings' => $this->chart->get_chart_settings(), 'locale' => sanitize_key( wpforms_get_language_code() ), 'nonce' => wp_create_nonce( 'wpforms_payments_overview_nonce' ), 'date_format' => sanitize_text_field( Datepicker::get_wp_date_format_for_momentjs() ), 'delimiter' => Datepicker::TIMESPAN_DELIMITER, 'report' => Chart::ACTIVE_REPORT, 'currency' => sanitize_text_field( wpforms_get_currency() ), 'decimals' => absint( wpforms_get_currency_decimals( wpforms_get_currency() ) ), 'i18n' => [ 'label' => esc_html__( 'Payments', 'wpforms-lite' ), 'delete_button' => esc_html__( 'Delete', 'wpforms-lite' ), 'subscription_delete_confirm' => $this->get_subscription_delete_confirmation_message(), 'no_dataset' => [ 'total_payments' => esc_html__( 'No payments for selected period', 'wpforms-lite' ), 'total_sales' => esc_html__( 'No sales for selected period', 'wpforms-lite' ), 'total_refunded' => esc_html__( 'No refunds for selected period', 'wpforms-lite' ), 'total_subscription' => esc_html__( 'No new subscriptions for selected period', 'wpforms-lite' ), 'total_renewal_subscription' => esc_html__( 'No subscription renewals for the selected period', 'wpforms-lite' ), 'total_coupons' => esc_html__( 'No coupons applied during the selected period', 'wpforms-lite' ), ], ], 'page_uri' => $this->get_current_uri(), ]; wp_localize_script( 'wpforms-admin-payments-overview', // Script handle the data will be attached to. 'wpforms_admin_payments_overview', // Name for the JavaScript object. $admin_l10n ); } /** * Retrieve a Payment Overview URI. * * @since 1.8.2 * * @return string */ private function get_current_uri() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $query = $_GET; unset( $query['mode'], $query['paged'] ); return add_query_arg( $query, self::get_url() ); } /** * Determine whether the current user has the capability to view the page. * * @since 1.8.2 * * @return bool */ public function current_user_can() { return wpforms_current_user_can(); } /** * Page heading. * * @since 1.8.2 */ public function heading() { Helpers::get_default_heading(); } /** * Page content. * * @since 1.8.2 */ public function display() { // If there are no payments at all, display an empty state. if ( ! $this->has_any_mode_payment() ) { $this->display_empty_state(); return; } // Display the page content, including the chart and the table. $this->chart->display(); $this->table->display(); } /** * Get the URL of the page. * * @since 1.8.2 * * @return string */ public static function get_url() { static $url; if ( $url ) { return $url; } $url = add_query_arg( [ 'page' => Payments::SLUG, ], admin_url( 'admin.php' ) ); return $url; } /** * Get payment mode. * * Use only for logged-in users. Returns mode from user meta data or from the $_GET['mode'] parameter. * * @since 1.8.2 * * @return string */ public static function get_mode(): string { static $mode; if ( ! self::is_valid_context_for_mode() ) { return 'live'; } if ( $mode ) { return $mode; } $mode = self::get_mode_from_request(); $user_id = get_current_user_id(); $meta_key = 'wpforms-payments-mode'; if ( self::is_mode_valid_and_nonce_verified( $mode ) ) { update_user_meta( $user_id, $meta_key, $mode ); return $mode; } $mode = (string) get_user_meta( $user_id, $meta_key, true ); if ( empty( $mode ) || ! Helpers::is_test_payment_exists() ) { $mode = 'live'; } return $mode; } /** * Check if the context is valid for payment mode. * * @since 1.9.5 * * @return bool */ private static function is_valid_context_for_mode(): bool { return wpforms_is_admin_ajax() || wpforms_is_admin_page( 'payments' ) || wpforms_is_admin_page( 'entries' ); } /** * Retrieve the payment mode from the request. * * @since 1.9.5 * * @return string */ private static function get_mode_from_request(): string { // Nonce is checked in the `is_mode_valid_and_nonce_verified` method. // phpcs:ignore WordPress.Security.NonceVerification.Recommended return isset( $_GET['mode'] ) ? sanitize_key( $_GET['mode'] ) : ''; } /** * Determine if the mode is valid and the nonce is verified. * * @since 1.9.5 * * @param string $mode Payment mode to validate. * * @return bool */ private static function is_mode_valid_and_nonce_verified( string $mode ): bool { // phpcs:ignore WordPress.Security.NonceVerification.Recommended return ValueValidator::is_valid( $mode, 'mode' ) && isset( $_GET['_wpnonce'] ) && wp_verify_nonce( sanitize_key( $_GET['_wpnonce'] ), 'wpforms_payments_overview_nonce' ); } /** * Display one of the empty states. * * @since 1.8.2 */ private function display_empty_state() { // If a payment gateway is configured, output no payments state. if ( $this->is_gateway_configured() ) { // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo wpforms_render( 'admin/empty-states/payments/no-payments', [ 'cta_url' => add_query_arg( [ 'page' => 'wpforms-overview', ], 'admin.php' ), ], true ); return; } // Otherwise, output get started state. $is_upgraded = StripeHelpers::is_allowed_license_type(); $message = __( "First you need to set up a payment gateway. We've partnered with <strong>Stripe and Square</strong> to bring easy payment forms to everyone. ", 'wpforms-lite' ); $message .= $is_upgraded ? sprintf( /* translators: %s - WPForms Addons admin page URL. */ __( 'Other payment gateways such as <strong>PayPal</strong> and <strong>Authorize.Net</strong> can be installed from the <a href="%s">Addons screen</a>.', 'wpforms-lite' ), esc_url( add_query_arg( [ 'page' => 'wpforms-addons', ], admin_url( 'admin.php' ) ) ) ) : sprintf( /* translators: %s - WPForms.com Upgrade page URL. */ __( "If you'd like to use another payment gateway, please consider <a href='%s'>upgrading to WPForms Pro</a>.", 'wpforms-lite' ), esc_url( wpforms_admin_upgrade_link( 'Payments Dashboard', 'Splash - Upgrade to Pro Text' ) ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo wpforms_render( 'admin/empty-states/payments/get-started', [ 'message' => $message, 'version' => $is_upgraded ? 'pro' : 'lite', 'cta_url' => add_query_arg( [ 'page' => 'wpforms-settings', 'view' => 'payments', ], admin_url( 'admin.php' ) ), ], true ); } /** * Determine whether Stripe or Square payment gateway is configured. * * @since 1.8.2 * * @return bool */ private function is_gateway_configured(): bool { /** * Allow to modify a status whether Stripe or Square payment gateway is configured. * * @since 1.8.2 * * @param bool $is_configured True if Stripe or Square payment gateway is configured. */ return (bool) apply_filters( 'wpforms_admin_payments_views_overview_page_gateway_is_configured', StripeHelpers::has_stripe_keys() || SquareHelpers::is_square_configured() ); } /** * Determine whether there are payments of any modes. * * @since 1.8.2 * * @return bool */ private function has_any_mode_payment() { static $has_any_mode_payment; if ( $has_any_mode_payment !== null ) { return $has_any_mode_payment; } $has_any_mode_payment = count( wpforms()->obj( 'payment' )->get_payments( [ 'mode' => 'any', 'number' => 1, ] ) ) > 0; // Check on trashed payments. if ( ! $has_any_mode_payment ) { $has_any_mode_payment = count( wpforms()->obj( 'payment' )->get_payments( [ 'mode' => 'any', 'number' => 1, 'is_published' => 0, ] ) ) > 0; } return $has_any_mode_payment; } /** * To avoid recursively, remove the previous variables from the REQUEST_URI. * * @since 1.8.2 */ private function clean_request_uri() { if ( isset( $_SERVER['REQUEST_URI'] ) ) { // phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.NonceVerification.Recommended $_SERVER['REQUEST_URI'] = remove_query_arg( [ '_wpnonce', '_wp_http_referer', 'action', 'action2', 'payment_id' ], wp_unslash( $_SERVER['REQUEST_URI'] ) ); if ( empty( $_GET['s'] ) ) { $_SERVER['REQUEST_URI'] = remove_query_arg( [ 'search_where', 'search_mode', 's' ], wp_unslash( $_SERVER['REQUEST_URI'] ) ); } // phpcs:enable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.NonceVerification.Recommended } } /** * Get the subscription delete confirmation message. * The returned message is used in the JavaScript file and shown in a "Heads up!" modal. * * @since 1.8.4 * * @return string */ private function get_subscription_delete_confirmation_message() { $help_link = wpforms_utm_link( 'https://wpforms.com/docs/viewing-and-managing-payments/#deleting-parent-subscription', 'Delete Payment', 'Learn More' ); return sprintf( wp_kses( /* translators: WPForms.com docs page URL. */ __( 'Deleting one or more selected payments may prevent processing of future subscription renewals. Payment filtering may also be affected. <a href="%1$s" rel="noopener" target="_blank">Learn More</a>', 'wpforms-lite' ), [ 'a' => [ 'href' => [], 'rel' => [], 'target' => [], ], ] ), esc_url( $help_link ) ); } } Admin/Payments/Views/Overview/Helpers.php 0000644 00000005136 15174710275 0014430 0 ustar 00 <?php namespace WPForms\Admin\Payments\Views\Overview; use WPForms\Db\Payments\ValueValidator; /** * Helper methods for the Overview page. * * @since 1.8.2 */ class Helpers { /** * Get subscription description. * * @since 1.8.2 * * @param string $payment_id Payment id. * @param string $amount Payment amount. * * @return string */ public static function get_subscription_description( $payment_id, $amount ) { // Get the subscription period for the payment. $period = wpforms()->obj( 'payment_meta' )->get_single( $payment_id, 'subscription_period' ); $intervals = ValueValidator::get_allowed_subscription_intervals(); // If the subscription period is not set or not allowed, return the amount only. if ( ! isset( $intervals[ $period ] ) ) { return $amount; } // Use "/" as a separator between the amount and the subscription period. return $amount . ' / ' . $intervals[ $period ]; } /** * Return a placeholder text "N/A" when there is no actual data to display. * * @since 1.8.2 * * @param string $with_wrapper Wrap the text within a span tag for styling purposes. Default: true. * * @return string */ public static function get_placeholder_na_text( $with_wrapper = true ) { $text = __( 'N/A', 'wpforms-lite' ); // Check if the text should be wrapped within a span tag. if ( $with_wrapper ) { return sprintf( '<span class="payment-placeholder-text-none">%s</span>', $text ); } return $text; } /** * Get the default heading for the Payments pages. * * @since 1.8.2.2 * * @param string $help_link Help link. */ public static function get_default_heading( $help_link = '' ) { if ( ! $help_link ) { $help_link = 'https://wpforms.com/docs/viewing-and-managing-payments/'; } echo '<span class="wpforms-payments-overview-help">'; printf( '<a href="%s" target="_blank"><i class="fa fa-question-circle-o"></i>%s</a>', esc_url( wpforms_utm_link( $help_link, 'Payments Dashboard', 'Manage Payments Documentation' ) ), esc_html__( 'Help', 'wpforms-lite' ) ); echo '</span>'; } /** * Look for at least one payment in test mode. * * @since 1.9.0 * * @return bool */ public static function is_test_payment_exists(): bool { $published = wpforms()->obj( 'payment' )->get_payments( [ 'mode' => 'test', 'number' => 1, ] ); if ( $published ) { return true; } // Check for trashed payments. return ! empty( wpforms()->obj( 'payment' )->get_payments( [ 'mode' => 'test', 'number' => 1, 'is_published' => 0, ] ) ); } } Admin/Payments/Views/Overview/ModeToggle.php 0000644 00000001255 15174710275 0015052 0 ustar 00 <?php namespace WPForms\Admin\Payments\Views\Overview; /** * Payments Overview Mode Toggle class. * * @since 1.8.2 */ class ModeToggle { /** * Determine if the toggle should be displayed and render it. * * @since 1.8.2 */ public function display() { // Bail early if no payments are found in test mode. if ( ! Helpers::is_test_payment_exists() ) { return; } $this->render(); } /** * Display the toggle button. * * @since 1.8.2 */ private function render() { // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo wpforms_render( 'admin/payments/mode-toggle', [ 'mode' => Page::get_mode(), ], true ); } } Admin/Payments/Views/Overview/Filters.php 0000644 00000011452 15174710275 0014434 0 ustar 00 <?php namespace WPForms\Admin\Payments\Views\Overview; /** * Class for extending SQL queries for filtering payments by multicheckbox fields. * * @since 1.8.4 */ class Filters { /** * Initialize the Filters class. * * @since 1.8.4 */ public function init() { $this->hooks(); } /** * Attach hooks for filtering payments by multicheckbox fields. * * @since 1.8.4 */ private function hooks() { add_filter( 'wpforms_db_payments_payment_get_payments_query_after_where', [ $this, 'add_renewals_by_subscription_id' ], 10, 2 ); add_filter( 'wpforms_db_payments_queries_count_all_query_after_where', [ $this, 'count_renewals_by_subscription_id' ], 10, 2 ); add_filter( 'wpforms_db_payments_queries_count_if_exists_after_where', [ $this, 'exists_renewals_by_subscription_id' ], 10, 2 ); } /** * Add renewals to the query. * * @since 1.8.4 * * @param string $after_where SQL query. * @param array $args Query arguments. * * @return string */ public function add_renewals_by_subscription_id( $after_where, $args ) { $query = $this->query_renewals_by_subscription_id( $args ); if ( empty( $query ) ) { return $after_where; // Return early if $query is empty. } return "{$after_where} UNION {$query}"; } /** * Add renewals to the count query. * * @since 1.8.4 * * @param string $after_where SQL query. * @param array $args Query arguments. * * @return string */ public function count_renewals_by_subscription_id( $after_where, $args ) { $query = $this->query_renewals_by_subscription_id( $args, 'COUNT(*)' ); if ( empty( $query ) ) { return $after_where; // Return early if $query is empty. } return "{$after_where} UNION ALL {$query}"; } /** * Add renewals to the exists query. * * @since 1.8.4 * * @param string $after_where SQL query. * @param array $args Query arguments. * * @return string */ public function exists_renewals_by_subscription_id( $after_where, $args ) { $query = $this->query_renewals_by_subscription_id( $args, '1' ); if ( empty( $query ) ) { return $after_where; // Return early if $query is empty. } return "{$after_where} UNION ALL {$query}"; } /** * Query renewals by subscription ID. * * @since 1.8.4 * * @param array $args Query arguments. * @param string $selector SQL selector. * * @return string */ private function query_renewals_by_subscription_id( $args, $selector = 'p.*' ) { // Check if essential arguments are missing. if ( empty( $args['table_query'] ) || empty( $args['subscription_status'] ) ) { return ''; } // Check if the query type is not 'renewal'. if ( ! empty( $args['type'] ) && ! in_array( 'renewal', explode( '|', $args['type'] ), true ) ) { return ''; } $payment_handle = wpforms()->obj( 'payment' ); $subscription_statuses = explode( '|', $args['subscription_status'] ); $placeholders = wpforms_wpdb_prepare_in( $subscription_statuses ); // This is needed to avoid the count_all method from adding the WHERE clause for the other types. $args['type'] = 'renewal'; // Remove the subscription_status argument from the query. // The primary reason for this is that the subscription_status has to be checked in the subquery. unset( $args['subscription_status'] ); // Prepare the query. $query[] = "SELECT {$selector} FROM {$payment_handle->table_name} as p"; /** * Append custom query parts before the WHERE clause. * * This hook allows external code to extend the SQL query by adding custom conditions * immediately before the WHERE clause. * * @since 1.8.4 * * @param string $where Before the WHERE clause in the database query. * @param array $args Query arguments. * * @return string */ $query[] = apply_filters( 'wpforms_admin_payments_views_overview_filters_renewals_by_subscription_id_query_before_where', '', $args ); // Add the WHERE clause. $query[] = 'WHERE 1=1'; $query[] = $payment_handle->add_columns_where_conditions( $args ); $query[] = $payment_handle->add_secondary_where_conditions( $args ); $query[] = "AND EXISTS ( SELECT 1 FROM {$payment_handle->table_name} as subquery_p WHERE subquery_p.subscription_id = p.subscription_id AND subquery_p.subscription_status IN ({$placeholders}) )"; /** * Append custom query parts after the WHERE clause. * * This hook allows external code to extend the SQL query by adding custom conditions * immediately after the WHERE clause. * * @since 1.8.4 * * @param string $where After the WHERE clause in the database query. * @param array $args Query arguments. * * @return string */ $query[] = apply_filters( 'wpforms_admin_payments_views_overview_filters_renewals_by_subscription_id_query_after_where', '', $args ); return implode( ' ', $query ); } } Admin/Payments/Views/Overview/BulkActions.php 0000644 00000010551 15174710275 0015241 0 ustar 00 <?php namespace WPForms\Admin\Payments\Views\Overview; use WPForms\Admin\Notice; /** * Bulk actions on the Payments Overview page. * * @since 1.8.2 */ class BulkActions { /** * Allowed actions. * * @since 1.8.2 * * @const array */ const ALLOWED_ACTIONS = [ 'trash', 'restore', 'delete', ]; /** * Payments ids. * * @since 1.8.2 * * @var array */ private $ids; /** * Current action. * * @since 1.8.2 * * @var string */ private $action; /** * Init. * * @since 1.8.2 */ public function init() { $this->process(); } /** * Get the current action selected from the bulk actions dropdown. * * @since 1.8.2 * * @return string|false The action name or False if no action was selected */ private function current_action() { // phpcs:disable WordPress.Security.NonceVerification.Recommended if ( isset( $_REQUEST['action'] ) && $_REQUEST['action'] !== '-1' ) { return sanitize_key( $_REQUEST['action'] ); } if ( isset( $_REQUEST['action2'] ) && $_REQUEST['action2'] !== '-1' ) { return sanitize_key( $_REQUEST['action2'] ); } // phpcs:enable WordPress.Security.NonceVerification.Recommended return false; } /** * Process bulk actions. * * @since 1.8.2 */ private function process() { if ( empty( $_GET['_wpnonce'] ) || empty( $_GET['payment_id'] ) ) { return; } if ( ! wp_verify_nonce( sanitize_key( $_GET['_wpnonce'] ), 'bulk-wpforms_page_wpforms-payments' ) ) { wp_die( esc_html__( 'Your session expired. Please reload the page.', 'wpforms-lite' ) ); } $this->ids = array_map( 'absint', (array) $_GET['payment_id'] ); $this->action = $this->current_action(); if ( empty( $this->ids ) || ! $this->action || ! $this->is_allowed_action( $this->action ) ) { return; } $this->process_action(); } /** * Process a bulk action. * * @since 1.8.2 */ private function process_action() { $method = "process_action_{$this->action}"; // Check that we have a method for this action. if ( ! method_exists( $this, $method ) ) { return; } $processed = 0; foreach ( $this->ids as $id ) { $processed = $this->$method( $id ) ? $processed + 1 : $processed; } if ( ! $processed ) { return; } $this->display_bulk_action_message( $processed ); } /** * Trash the payment. * * @since 1.8.2 * * @param int $id Payment ID to trash. * * @return bool */ private function process_action_trash( $id ) { return wpforms()->obj( 'payment' )->update( $id, [ 'is_published' => 0 ] ); } /** * Restore the payment. * * @since 1.8.2 * * @param int $id Payment ID to restore from trash. * * @return bool */ private function process_action_restore( $id ) { return wpforms()->obj( 'payment' )->update( $id, [ 'is_published' => 1 ] ); } /** * Delete the payment. * * @since 1.8.2 * * @param int $id Payment ID to delete. * * @return bool */ private function process_action_delete( $id ) { return wpforms()->obj( 'payment' )->delete( $id ); } /** * Display a bulk action message. * * @since 1.8.2 * * @param int $count Count of processed payment IDs. */ private function display_bulk_action_message( $count ) { switch ( $this->action ) { case 'delete': /* translators: %d - number of deleted payments. */ $message = sprintf( _n( '%d payment was successfully permanently deleted.', '%d payments were successfully permanently deleted.', $count, 'wpforms-lite' ), number_format_i18n( $count ) ); break; case 'restore': /* translators: %d - number of restored payments. */ $message = sprintf( _n( '%d payment was successfully restored.', '%d payments were successfully restored.', $count, 'wpforms-lite' ), number_format_i18n( $count ) ); break; case 'trash': /* translators: %d - number of trashed payments. */ $message = sprintf( _n( '%d payment was successfully moved to the Trash.', '%d payments were successfully moved to the Trash.', $count, 'wpforms-lite' ), number_format_i18n( $count ) ); break; default: $message = ''; } if ( empty( $message ) ) { return; } Notice::success( $message ); } /** * Determine whether the action is allowed. * * @since 1.8.2 * * @param string $action Action name. * * @return bool */ private function is_allowed_action( $action ) { return in_array( $action, self::ALLOWED_ACTIONS, true ); } } Admin/Payments/Views/Overview/Traits/ResetNotices.php 0000644 00000017721 15174710275 0016706 0 ustar 00 <?php namespace WPForms\Admin\Payments\Views\Overview\Traits; use WPForms\Admin\Payments\Views\Overview\Coupon; use WPForms\Admin\Payments\Views\Overview\Search; use WPForms\Db\Payments\ValueValidator; /** * This file is part of the Table class and contains methods responsible for * displaying notices on the Payments Overview page. * * @since 1.8.4 */ trait ResetNotices { /** * Show reset filter box. * * @since 1.8.4 */ private function show_reset_filter() { $applied_filters = [ $this->get_search_reset_filter(), $this->get_status_reset_filter(), $this->get_coupon_reset_filter(), $this->get_form_reset_filter(), $this->get_type_reset_filter(), $this->get_gateway_reset_filter(), $this->get_subscription_status_reset_filter(), ]; $applied_filters = array_filter( $applied_filters ); // Let's not show the reset filter notice if there are no applied filters. if ( empty( $applied_filters ) ) { return; } // Output the reset filter notice. // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo wpforms_render( 'admin/payments/reset-filter-notice', [ 'total' => $this->get_valid_status_count_from_request(), 'applied_filters' => $applied_filters, ], true ); } /** * Show search reset filter. * * @since 1.8.4 * * @return array */ private function get_search_reset_filter() { // Do not show the reset filter notice on the search results page. if ( ! Search::is_search() ) { return []; } $search_where = $this->get_search_where( $this->get_search_where_key() ); $search_mode = $this->get_search_mode( $this->get_search_mode_key() ); return [ 'reset_url' => remove_query_arg( [ 's', 'search_where', 'search_mode', 'paged' ] ), 'results' => sprintf( ' %s <em>%s</em> %s "<em>%s</em>"', __( 'where', 'wpforms-lite' ), esc_html( $search_where ), esc_html( $search_mode ), // It's important to escape the search term here for security. // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized esc_html( isset( $_GET['s'] ) ? wp_unslash( $_GET['s'] ) : '' ) ), ]; } /** * Show status reset filter. * * @since 1.8.4 * * @return array */ private function get_status_reset_filter() { // Do not show the reset filter notice on the status results page. // phpcs:disable WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized if ( empty( $this->get_valid_status_from_request() ) || $this->is_trash_view() ) { return []; } $statuses = ValueValidator::get_allowed_one_time_statuses(); // Leave early if the status is not found. if ( ! isset( $statuses[ $this->get_valid_status_from_request() ] ) ) { return []; } return [ 'reset_url' => remove_query_arg( [ 'status' ] ), 'results' => sprintf( ' %s "<em>%s</em>"', __( 'with the status', 'wpforms-lite' ), $statuses[ $this->get_valid_status_from_request() ] ), ]; } /** * Show coupon reset filter. * * @since 1.8.4 * * @return array */ private function get_coupon_reset_filter() { // Do not show the reset filter notice on the coupon results page. if ( ! Coupon::is_coupon() ) { return []; } // Get the payment meta with the specified coupon ID. $payment_meta = wpforms()->obj( 'payment_meta' )->get_all_by_meta( 'coupon_id', // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.NonceVerification.Recommended absint( $_GET['coupon_id'] ) ); // If the coupon info is empty, exit the function. if ( empty( $payment_meta['coupon_info'] ) ) { return []; } return [ 'reset_url' => remove_query_arg( [ 'coupon_id', 'paged' ] ), 'results' => sprintf( ' %s "<em>%s</em>"', __( 'with the coupon', 'wpforms-lite' ), $this->get_coupon_name_by_info( $payment_meta['coupon_info']->value ) ), ]; } /** * Show form reset filter. * * @since 1.8.4 * * @return array */ private function get_form_reset_filter() { // Do not show the reset filter notice on the form results page. // phpcs:disable WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized if ( empty( $_GET['form_id'] ) ) { return []; } // Retrieve the form with the specified ID. $form = wpforms()->obj( 'form' )->get( absint( $_GET['form_id'] ) ); // phpcs:enable WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized // If the form is not found or not published, exit the function. if ( ! $form || $form->post_status !== 'publish' ) { return []; } return [ 'reset_url' => remove_query_arg( [ 'form_id', 'paged' ] ), 'results' => sprintf( ' %s "<em>%s</em>"', __( 'with the form titled', 'wpforms-lite' ), ! empty( $form->post_title ) ? $form->post_title : $form->post_name ), ]; } /** * Show type reset filter. * * @since 1.8.4 * * @return array */ private function get_type_reset_filter() { // Do not show the reset filter notice on the type results page. // phpcs:disable WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized if ( empty( $_GET['type'] ) ) { return []; } $allowed_types = ValueValidator::get_allowed_types(); $type = explode( '|', sanitize_text_field( wp_unslash( $_GET['type'] ) ) ); // phpcs:enable WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized return [ 'reset_url' => remove_query_arg( [ 'type', 'paged' ] ), 'results' => sprintf( ' %s "<em>%s</em>"', _n( 'with the type', 'with the types', count( $type ), 'wpforms-lite' ), implode( ', ', array_intersect_key( $allowed_types, array_flip( $type ) ) ) ), ]; } /** * Show gateway reset filter. * * @since 1.8.4 * * @return array */ private function get_gateway_reset_filter() { // Do not show the reset filter notice on the gateway results page. // phpcs:disable WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized if ( empty( $_GET['gateway'] ) ) { return []; } $allowed_gateways = ValueValidator::get_allowed_gateways(); $gateway = explode( '|', sanitize_text_field( wp_unslash( $_GET['gateway'] ) ) ); // phpcs:enable WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized return [ 'reset_url' => remove_query_arg( [ 'gateway', 'paged' ] ), 'results' => sprintf( ' %s "<em>%s</em>"', _n( 'with the gateway', 'with the gateways', count( $gateway ), 'wpforms-lite' ), implode( ', ', array_intersect_key( $allowed_gateways, array_flip( $gateway ) ) ) ), ]; } /** * Show subscription status reset filter. * * @since 1.8.4 * * @return array */ private function get_subscription_status_reset_filter() { // Do not show the reset filter notice on the subscription status results page. // phpcs:disable WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized if ( empty( $_GET['subscription_status'] ) ) { return []; } $allowed_subscription_statuses = ValueValidator::get_allowed_subscription_statuses(); $subscription_status = explode( '|', sanitize_text_field( wp_unslash( $_GET['subscription_status'] ) ) ); // phpcs:enable WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized return [ 'reset_url' => remove_query_arg( [ 'subscription_status', 'paged' ] ), 'results' => sprintf( ' %s "<em>%s</em>"', _n( 'with the subscription status', 'with the subscription statuses', count( $subscription_status ), 'wpforms-lite' ), implode( ', ', array_intersect_key( $allowed_subscription_statuses, array_flip( $subscription_status ) ) ) ), ]; } } Admin/Payments/Views/Overview/Table.php 0000644 00000111532 15174710275 0014053 0 ustar 00 <?php namespace WPForms\Admin\Payments\Views\Overview; if ( ! defined( 'ABSPATH' ) ) { exit; } use WPForms\Db\Payments\ValueValidator; use WPForms\Db\Payments\Queries; if ( ! class_exists( 'WP_List_Table' ) ) { require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php'; } /** * Payments Overview Table class. * * @since 1.8.2 */ class Table extends \WP_List_Table { /** * Trait for using notices. * * @since 1.8.4 */ use Traits\ResetNotices; /** * Payment type: one-time. * * @since 1.8.2 * * @var string */ const ONE_TIME = 'one-time'; /** * Payment status: trash. * * @since 1.8.2 * * @var string */ const TRASH = 'trash'; /** * Total number of payments. * * @since 1.8.2 * * @var array */ private $counts; /** * Table query arguments. * * @since 1.8.4 * * @var array */ private $table_query_args = []; /** * Retrieve the table columns. * * @since 1.8.2 * * @return array $columns Array of all the list table columns. */ public function get_columns() { static $columns; if ( ! empty( $columns ) ) { return $columns; } $columns = [ 'cb' => '<input type="checkbox" />', 'title' => esc_html__( 'Payment', 'wpforms-lite' ), 'date' => esc_html__( 'Date', 'wpforms-lite' ), ]; if ( wpforms()->obj( 'payment_queries' )->has_different_values( 'gateway' ) ) { $columns['gateway'] = esc_html__( 'Gateway', 'wpforms-lite' ); } if ( wpforms()->obj( 'payment_queries' )->has_different_values( 'type' ) ) { $columns['type'] = esc_html__( 'Type', 'wpforms-lite' ); } if ( wpforms()->obj( 'payment_meta' )->is_valid_meta_by_meta_key( 'coupon_id' ) ) { $columns['coupon'] = esc_html__( 'Coupon', 'wpforms-lite' ); } $columns['total'] = esc_html__( 'Total', 'wpforms-lite' ); if ( wpforms()->obj( 'payment_queries' )->has_subscription() ) { $columns['subscription'] = esc_html__( 'Subscription', 'wpforms-lite' ); } $columns['form'] = esc_html__( 'Form', 'wpforms-lite' ); $columns['status'] = esc_html__( 'Status', 'wpforms-lite' ); /** * Filters the columns in the Payments Overview table. * * @since 1.8.2 * * @param array $columns Array of columns. */ return (array) apply_filters( 'wpforms_admin_payments_views_overview_table_get_columns', $columns ); } /** * Determine whether it is a trash view. * * @since 1.8.2 * * @return bool */ private function is_trash_view() { return $this->is_current_view( 'trash' ); } /** * Define the table's sortable columns. * * @since 1.8.2 * * @return array Array of all the sortable columns. */ protected function get_sortable_columns() { return [ 'title' => [ 'id', false ], 'date' => [ 'date', false ], 'total' => [ 'total', false ], ]; } /** * Prepare the table with different parameters, pagination, columns and table elements. * * @since 1.8.2 */ public function prepare_items() { $page = $this->get_pagenum(); $per_page = $this->get_items_per_page( 'wpforms_payments_per_page', 20 ); $data_args = [ 'number' => $per_page, 'offset' => $per_page * ( $page - 1 ), 'orderby' => $this->get_order_by(), 'search' => $this->get_search_query(), 'search_conditions' => $this->get_search_conditions(), 'status' => $this->get_valid_status_from_request(), 'is_published' => $this->is_trash_view() ? 0 : 1, ]; // Set the table query arguments for later use. $this->table_query_args = $this->prepare_table_query_args( $data_args ); // Retrieve the payment records for the given data arguments. $this->items = wpforms()->obj( 'payment' )->get_payments( $this->table_query_args ); // Setup the counts. $this->setup_counts(); // Check if we can continue. $this->can_prepare_records(); // Get the proper total number of records depending on the current status view. $total_items = $this->get_valid_status_count_from_request(); $total_pages = ceil( $total_items / $per_page ); // Finalize pagination. $this->set_pagination_args( [ 'total_items' => $total_items, 'total_pages' => (int) $total_pages, 'per_page' => $per_page, ] ); } /** * Prepare the query arguments for the overview table. * * @since 1.8.4 * * @param array $args Array of data arguments. * * @return array */ private function prepare_table_query_args( $args = [] ) { // phpcs:disable WordPress.Security.NonceVerification.Recommended return wp_parse_args( $args, [ 'table_query' => true, 'order' => isset( $_GET['order'] ) ? sanitize_key( $_GET['order'] ) : 'DESC', 'form_id' => isset( $_GET['form_id'] ) ? absint( $_GET['form_id'] ) : '', 'type' => isset( $_GET['type'] ) ? sanitize_text_field( wp_unslash( $_GET['type'] ) ) : '', 'gateway' => isset( $_GET['gateway'] ) ? sanitize_text_field( wp_unslash( $_GET['gateway'] ) ) : '', 'subscription_status' => isset( $_GET['subscription_status'] ) ? sanitize_text_field( wp_unslash( $_GET['subscription_status'] ) ) : '', ] ); // phpcs:enable WordPress.Security.NonceVerification.Recommended } /** * Message to be displayed when there are no payments. * * @since 1.8.2 */ public function no_items() { if ( $this->is_trash_view() ) { esc_html_e( 'No payments found in the trash.', 'wpforms-lite' ); return; } if ( $this->is_current_view( 'search' ) ) { esc_html_e( 'No payments found, please try a different search.', 'wpforms-lite' ); return; } esc_html_e( 'No payments found.', 'wpforms-lite' ); } /** * Generates content for a single row of the table. * * @since 1.8.4 * * @param array $item Item data. */ public function single_row( $item ) { // Leave the default row if the item is not a subscription. if ( empty( $item['subscription_id'] ) || empty( $item['subscription_status'] ) ) { parent::single_row( $item ); return; } $has_renewal = wpforms()->obj( 'payment_queries' )->if_subscription_has_renewal( $item['subscription_id'] ); // Leave the default row if the subscription has no renewal. if ( ! $has_renewal ) { parent::single_row( $item ); return; } echo '<tr class="subscription-has-renewal">'; $this->single_row_columns( $item ); echo '</tr>'; } /** * Column default values. * * @since 1.8.2 * * @param array $item Item data. * @param string $column_name Column name. * * @return string */ protected function column_default( $item, $column_name ) { if ( method_exists( $this, "get_column_{$column_name}" ) ) { return $this->{"get_column_{$column_name}"}( $item ); } if ( isset( $item[ $column_name ] ) ) { return esc_html( $item[ $column_name ] ); } /** * Allow to filter default column value. * * @since 1.8.2 * * @param string $value Default column value. * @param array $item Item data. * @param string $column_name Column name. */ return apply_filters( 'wpforms_admin_payments_views_overview_table_column_default_value', '', $item, $column_name ); } /** * Define the checkbox column. * * @since 1.8.2 * * @param array $item The current item. * * @return string */ protected function column_cb( $item ) { return '<input type="checkbox" name="payment_id[]" value="' . absint( $item['id'] ) . '" />'; } /** * Prepare the items and display the table. * * @since 1.8.2 */ public function display() { ?> <form id="wpforms-payments-table" method="GET" action="<?php echo esc_url( Page::get_url() ); ?>"> <?php $this->display_hidden_fields(); $this->show_reset_filter(); $this->views(); $this->search_box( esc_html__( 'Search Payments', 'wpforms-lite' ), 'wpforms-payments-search-input' ); parent::display(); ?> </form> <?php } /** * Extra filtering controls to be displayed between bulk actions and pagination. * * @since 1.8.4 * * @param string $which Position of the extra controls: 'top' or 'bottom'. */ protected function extra_tablenav( $which ) { // We only want to show the extra controls on the top. if ( $which !== 'top' ) { return; } $tablenav_data = [ 'type' => [ 'data' => ValueValidator::get_allowed_types(), 'plural_label' => __( 'types', 'wpforms-lite' ), ], 'gateway' => [ 'data' => ValueValidator::get_allowed_gateways(), 'plural_label' => __( 'gateways', 'wpforms-lite' ), ], 'subscription_status' => [ 'data' => ValueValidator::get_allowed_subscription_statuses(), 'plural_label' => __( 'subscriptions', 'wpforms-lite' ), ], ]; // Special case for showing all available types, gateways and subscription statuses. if ( ! $this->has_items() ) { unset( $this->table_query_args['type'], $this->table_query_args['gateway'], $this->table_query_args['subscription_status'] ); } // Output the reset filter notice. // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo wpforms_render( 'admin/payments/tablenav-filters', [ 'filters' => $this->prepare_extra_tablenav_filters( $tablenav_data ), ], true ); } /** * Iterate through each given filter option and remove the ones that don't have any records. * * @since 1.8.4 * * @param array $tablenav_data Array of filter options. * * @return string */ private function prepare_extra_tablenav_filters( $tablenav_data ) { $rendered_nav_data = []; foreach ( $tablenav_data as $nav_key => $nav_attributes ) { $filtered_data = []; // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $selected = isset( $_GET[ $nav_key ] ) ? explode( '|', wp_unslash( $_GET[ $nav_key ] ) ) : []; foreach ( $nav_attributes['data'] as $attribute_key => $attribute_value ) { $query_args = array_merge( $this->table_query_args, [ $nav_key => $attribute_key ] ); if ( in_array( $attribute_key, $selected, true ) || wpforms()->obj( 'payment_queries' )->if_exists( $query_args ) ) { $filtered_data[ $attribute_key ] = $attribute_value; } } $selected = array_filter( $selected, static function ( $value ) use ( $filtered_data ) { return isset( $filtered_data[ $value ] ); } ); if ( empty( $filtered_data ) || ( count( $filtered_data ) === 1 && empty( $selected ) ) ) { continue; } $rendered_nav_data[] = wpforms_render( 'admin/payments/tablenav-filter-multiselect', [ 'selected' => $selected, 'options' => $filtered_data, 'name' => $nav_key, 'data_settings' => [ 'i18n' => [ 'multiple' => sprintf( /* translators: %s - plural label. */ __( 'Multiple %s selected', 'wpforms-lite' ), esc_attr( $nav_attributes['plural_label'] ) ), 'all' => sprintf( /* translators: %s - plural label. */ __( 'All %s', 'wpforms-lite' ), esc_attr( $nav_attributes['plural_label'] ) ), ], ], ], true ); } return implode( '', $rendered_nav_data ); } /** * Display the search box. * * @since 1.8.2 * * @param string $text The 'submit' button label. * @param string $input_id ID attribute value for the search input field. */ public function search_box( $text, $input_id ) { $search_where = $this->get_search_where_key(); $search_mode = $this->get_search_mode_key(); ?> <p class="search-box"> <label class="screen-reader-text" for="search_where"><?php esc_html_e( 'Select which field to use when searching for payments', 'wpforms-lite' ); ?></label> <select name="search_where"> <option value="<?php echo esc_attr( Search::TITLE ); ?>" <?php selected( $search_where, Search::TITLE ); ?> ><?php echo esc_html( $this->get_search_where( Search::TITLE ) ); ?></option> <option value="<?php echo esc_attr( Search::TRANSACTION_ID ); ?>" <?php selected( $search_where, Search::TRANSACTION_ID ); ?> ><?php echo esc_html( $this->get_search_where( Search::TRANSACTION_ID ) ); ?></option> <option value="<?php echo esc_attr( Search::SUBSCRIPTION_ID ); ?>" <?php selected( $search_where, Search::SUBSCRIPTION_ID ); ?> ><?php echo esc_html( $this->get_search_where( Search::SUBSCRIPTION_ID ) ); ?></option> <option value="<?php echo esc_attr( Search::EMAIL ); ?>" <?php selected( $search_where, Search::EMAIL ); ?> ><?php echo esc_html( $this->get_search_where( Search::EMAIL ) ); ?></option> <option value="<?php echo esc_attr( Search::CREDIT_CARD ); ?>" <?php selected( $search_where, Search::CREDIT_CARD ); ?> ><?php echo esc_html( $this->get_search_where( Search::CREDIT_CARD ) ); ?></option> <option value="<?php echo esc_attr( Search::ANY ); ?>" <?php selected( $search_where, Search::ANY ); ?> ><?php echo esc_html( $this->get_search_where( Search::ANY ) ); ?></option> </select> <label class="screen-reader-text" for="search_mode"><?php esc_html_e( 'Select which comparison method to use when searching for payments', 'wpforms-lite' ); ?></label> <select name="search_mode"> <option value="<?php echo esc_attr( Search::MODE_CONTAINS ); ?>" <?php selected( $search_mode, Search::MODE_CONTAINS ); ?> ><?php echo esc_html( $this->get_search_mode( Search::MODE_CONTAINS ) ); ?></option> <option value="<?php echo esc_attr( Search::MODE_EQUALS ); ?>" <?php selected( $search_mode, Search::MODE_EQUALS ); ?> ><?php echo esc_html( $this->get_search_mode( Search::MODE_EQUALS ) ); ?></option> <option value="<?php echo esc_attr( Search::MODE_STARTS ); ?>" <?php selected( $search_mode, Search::MODE_STARTS ); ?> ><?php echo esc_html( $this->get_search_mode( Search::MODE_STARTS ) ); ?></option> </select> <label class="screen-reader-text" for="<?php echo esc_attr( $input_id ); ?>"><?php echo esc_html( $text ); ?></label> <input type="search" id="<?php echo esc_attr( $input_id ); ?>" name="s" value="<?php echo esc_attr( $this->get_search_query() ); ?>" /> <input type="submit" class="button" value="<?php echo esc_attr( $text ); ?>" /> </p> <?php } /** * Get bulk actions to be displayed in bulk action dropdown. * * @since 1.8.2 * * @return array */ protected function get_bulk_actions() { if ( $this->is_trash_view() ) { return [ 'restore' => esc_html__( 'Restore', 'wpforms-lite' ), 'delete' => esc_html__( 'Delete Permanently', 'wpforms-lite' ), ]; } return [ 'trash' => esc_html__( 'Move to Trash', 'wpforms-lite' ), ]; } /** * Generates the table navigation above or below the table. * * @since 1.8.2 * * @param string $which The location of the bulk actions: 'top' or 'bottom'. */ protected function display_tablenav( $which ) { if ( $this->has_items() ) { parent::display_tablenav( $which ); return; } echo '<div class="tablenav ' . esc_attr( $which ) . '">'; if ( $this->is_trash_view() ) { echo '<div class="alignleft actions bulkactions">'; $this->bulk_actions(); echo '</div>'; } $this->extra_tablenav( $which ); echo '<br class="clear" />'; echo '</div>'; } /** * List of CSS classes for the "WP_List_Table" table tag. * * @global string $mode List table view mode. * * @since 1.8.2 * * @return array */ protected function get_table_classes() { global $mode; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited $mode = get_user_setting( 'posts_list_mode', 'list' ); $mode_class = esc_attr( 'table-view-' . $mode ); $classes = [ 'widefat', 'striped', 'wpforms-table-list', 'wpforms-table-list-payments', $mode_class, ]; // For styling purposes, we'll add a dedicated class name for determining the number of visible columns. // The ideal threshold for applying responsive styling is set at "5" columns based on the need for "Tablet" view. $columns_class = $this->get_column_count() > 5 ? 'many' : 'few'; $classes[] = "has-{$columns_class}-columns"; return $classes; } /** * Get valid status from request. * * @since 1.8.2 * * @return string */ private function get_valid_status_from_request() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash return ! empty( $_REQUEST['status'] ) && ( ValueValidator::is_valid( $_REQUEST['status'], 'status' ) || $_REQUEST['status'] === self::TRASH ) ? $_REQUEST['status'] : ''; } /** * Get number of payments for the current status. * Note that this function also validates the status internally. * * @since 1.8.4 * * @return string */ private function get_valid_status_count_from_request() { // Retrieve the current status. $current_status = $this->get_valid_status_from_request(); return $current_status && isset( $this->counts[ $current_status ] ) ? $this->counts[ $current_status ] : $this->counts['total']; } /** * Get search where value. * * @since 1.8.2 * * @param string $search_key Search where key. * * @return string Return default search where value if not valid key provided. */ private function get_search_where( $search_key ) { $allowed_values = $this->get_allowed_search_where(); return $search_key && isset( $allowed_values[ $search_key ] ) ? $allowed_values[ $search_key ] : $allowed_values[ Search::TITLE ]; } /** * Get search where key. * * @since 1.8.2 * * @return string */ private function get_search_where_key() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $where_key = isset( $_GET['search_where'] ) ? sanitize_key( $_GET['search_where'] ) : ''; return isset( $this->get_allowed_search_where()[ $where_key ] ) ? $where_key : Search::TITLE; } /** * Get allowed search where values. * * @since 1.8.2 * * @return array */ private function get_allowed_search_where() { static $search_values; if ( ! $search_values ) { $search_values = [ Search::TITLE => __( 'Payment Title', 'wpforms-lite' ), Search::TRANSACTION_ID => __( 'Transaction ID', 'wpforms-lite' ), Search::EMAIL => __( 'Customer Email', 'wpforms-lite' ), Search::SUBSCRIPTION_ID => __( 'Subscription ID', 'wpforms-lite' ), Search::CREDIT_CARD => __( 'Last 4 digits of credit card', 'wpforms-lite' ), Search::ANY => __( 'Any payment field', 'wpforms-lite' ), ]; } return $search_values; } /** * Get search where value. * * @since 1.8.2 * * @param string $mode_key Search mode key. * * @return string Return default search mode value if not valid key provided. */ private function get_search_mode( $mode_key ) { $allowed_modes = $this->get_allowed_search_modes(); return $mode_key && isset( $allowed_modes[ $mode_key ] ) ? $allowed_modes[ $mode_key ] : $allowed_modes[ Search::MODE_EQUALS ]; } /** * Get search mode key. * * @since 1.8.2 * * @return string */ private function get_search_mode_key() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $where_mode = isset( $_GET['search_mode'] ) ? sanitize_key( $_GET['search_mode'] ) : ''; return isset( $this->get_allowed_search_modes()[ $where_mode ] ) ? $where_mode : Search::MODE_CONTAINS; } /** * Get allowed search mode params. * * @since 1.8.2 * * @return array */ private function get_allowed_search_modes() { static $search_modes; if ( ! $search_modes ) { $search_modes = [ Search::MODE_CONTAINS => __( 'contains', 'wpforms-lite' ), Search::MODE_EQUALS => __( 'equals', 'wpforms-lite' ), Search::MODE_STARTS => __( 'starts with', 'wpforms-lite' ), ]; } return $search_modes; } /** * Prepare counters. * * @since 1.8.2 */ private function setup_counts() { // Define the general views with their respective arguments. $views = [ 'published' => [ 'is_published' => 1, 'status' => '', ], 'trash' => [ 'is_published' => 0, 'status' => '', ], ]; // Generate filterable status views with their respective arguments. foreach ( ValueValidator::get_allowed_one_time_statuses() as $status => $label ) { $views[ $status ] = [ 'is_published' => 1, 'status' => $status, ]; } // Calculate the counts for each view and store them in the $this->counts array. foreach ( $views as $status => $status_args ) { $this->counts[ $status ] = wpforms()->obj( 'payment_queries' )->count_all( array_merge( $this->table_query_args, $status_args ) ); } // If the current view is the trash view, set the 'total' count to the 'trash' count. if ( $this->is_trash_view() ) { $this->counts['total'] = $this->counts['trash']; return; } // Otherwise, set the 'total' count to the 'published' count. $this->counts['total'] = $this->counts['published']; } /** * Get the orderby value. * * @since 1.8.2 * * @return string */ private function get_order_by() { // phpcs:disable WordPress.Security.NonceVerification.Recommended if ( ! isset( $_GET['orderby'] ) ) { return 'id'; } if ( $_GET['orderby'] === 'date' ) { return 'date_updated_gmt'; } if ( $_GET['orderby'] === 'total' ) { return 'total_amount'; } return 'id'; // phpcs:enable WordPress.Security.NonceVerification.Recommended } /** * Get payment column value. * * @since 1.8.2 * * @param array $item Payment item. * * @return string */ private function get_column_title( array $item ) { $title = $this->get_payment_title( $item ); $na_status = empty( $title ) ? sprintf( '<span class="payment-title-is-empty">- %s</span>', Helpers::get_placeholder_na_text() ) : ''; if ( ! $item['is_published'] ) { return sprintf( '<span>#%1$d %2$s</span> %3$s', $item['id'], esc_html( $title ), $na_status ); } $single_url = add_query_arg( [ 'page' => 'wpforms-payments', 'view' => 'payment', 'payment_id' => absint( $item['id'] ), ], admin_url( 'admin.php' ) ); return sprintf( '<a href="%1$s">#%2$d %3$s</a> %4$s', esc_url( $single_url ), $item['id'], esc_html( $title ), $na_status ); } /** * Get date column value. * * @since 1.8.2 * * @param array $item Payment item. * * @return string */ private function get_column_date( array $item ): string { $item_date_gmt = $item['date_updated_gmt']; $item_date = get_date_from_gmt( $item_date_gmt, 'Y-m-d H:i' ); $item_timestamp = strtotime( $item_date ); $item_timestamp_gmt = strtotime( $item_date_gmt ); // Check if the $timestamp represents a time within the last 24 hours and is not in the future. if ( $item_timestamp_gmt <= time() ) { /* translators: %s - relative time difference, e.g. "5 minutes", "12 days". */ $human = sprintf( esc_html__( '%s ago', 'wpforms-lite' ), human_time_diff( $item_timestamp_gmt ) ); } else { $human = wpforms_datetime_format( $item_timestamp, 'M j, Y', false ); } return sprintf( '<span title="%s">%s</span>', wpforms_datetime_format( $item_timestamp, 'Y-m-d H:i', false ), $human ); } /** * Get gateway column value. * * @since 1.8.2 * * @param array $item Payment item. * * @return string */ private function get_column_gateway( array $item ) { if ( ! isset( $item['gateway'] ) || ! ValueValidator::is_valid( $item['gateway'], 'gateway' ) ) { return ''; } return ValueValidator::get_allowed_gateways()[ $item['gateway'] ]; } /** * Get total column value. * * @since 1.8.2 * * @param array $item Payment item. * * @return string */ private function get_column_total( array $item ) { return esc_html( $this->get_formatted_amount_from_item( $item ) ); } /** * Get form column value. * * @since 1.8.2 * * @param array $item Payment item. * * @return string */ private function get_column_form( array $item ) { // Display "N/A" placeholder text if the form is not found or not published. if ( empty( $item['form_id'] ) || get_post_status( $item['form_id'] ) !== 'publish' ) { return Helpers::get_placeholder_na_text(); } $form = wpforms()->obj( 'form' )->get( $item['form_id'] ); if ( ! $form || $form->post_status !== 'publish' ) { return Helpers::get_placeholder_na_text(); } // Display the form name with a link to the form builder. $name = ! empty( $form->post_title ) ? $form->post_title : $form->post_name; $url = add_query_arg( 'form_id', absint( $form->ID ), remove_query_arg( 'paged' ) ); return sprintf( '<a href="%s">%s</a>', esc_url( $url ), wp_kses_post( $name ) ); } /** * Get status column value. * * @since 1.8.2 * * @param array $item Payment item. * * @return string */ private function get_column_status( array $item ) { if ( ! isset( $item['status'] ) || ! ValueValidator::is_valid( $item['status'], 'status' ) ) { return Helpers::get_placeholder_na_text(); } return sprintf( wp_kses( '<span class="wpforms-payment-status status-%1$s">%2$s</span>', [ 'span' => [ 'class' => [], ], 'i' => [ 'class' => [], 'title' => [], ], ] ), strtolower( $item['status'] ), $item['status'] === 'partrefund' ? __( '% Refunded', 'wpforms-lite' ) : ValueValidator::get_allowed_statuses()[ $item['status'] ] ); } /** * Get subscription column value. * * @since 1.8.2 * * @param array $item Payment item. * * @return string */ private function get_column_subscription( array $item ) { if ( $item['type'] === self::ONE_TIME ) { return Helpers::get_placeholder_na_text(); } $amount = $this->get_formatted_amount_from_item( $item ); $description = Helpers::get_subscription_description( $item['id'], $amount ); $status = $this->get_subscription_status( $item ); return sprintf( '<span class="wpforms-subscription-status status-%1$s" title="%2$s">%3$s</span>', sanitize_html_class( $status ), $status ? ValueValidator::get_allowed_subscription_statuses()[ $status ] : '', $description ); } /** * Get type column value. * * @since 1.8.2 * * @param array $item Payment item. * * @return string */ private function get_column_type( array $item ) { if ( ! isset( $item['type'] ) || ! ValueValidator::is_valid( $item['type'], 'type' ) ) { return Helpers::get_placeholder_na_text(); } return ValueValidator::get_allowed_types()[ $item['type'] ]; } /** * Show the coupon code used for the payment. * If the coupon code is not found, show N/A. * * @since 1.8.4 * * @param array $item Payment item. * * @return string */ private function get_column_coupon( $item ) { $payment_meta = wpforms()->obj( 'payment_meta' )->get_all( $item['id'] ); // If the coupon info is empty, show N/A. if ( empty( $payment_meta['coupon_info'] ) || empty( $payment_meta['coupon_id'] ) ) { return Helpers::get_placeholder_na_text(); } $url = add_query_arg( 'coupon_id', $payment_meta['coupon_id']->value, remove_query_arg( 'paged' ) ); return sprintf( '<a href="%1$s" aria-label="%2$s">%3$s</a>', esc_url( $url ), esc_attr__( 'Filter entries by coupon', 'wpforms-lite' ), esc_html( $this->get_coupon_name_by_info( $payment_meta['coupon_info']->value ) ) ); } /** * Get subscription status. * * @since 1.8.4 * * @param array $item Payment item. * * @return string */ private function get_subscription_status( $item ) { if ( ! in_array( $item['type'], [ 'subscription', 'renewal' ], true ) ) { return ''; } if ( $item['type'] === 'subscription' ) { return $item['subscription_status']; } // For renewals, get subscription status from the parent subscription. $parent_subscription = ( new Queries() )->get_subscription( $item['subscription_id'] ); return ! empty( $parent_subscription->subscription_status ) ? $parent_subscription->subscription_status : ''; } /** * Get payment title. * * @param array $item Payment item. * * @since 1.8.2 * * @return string */ private function get_payment_title( array $item ) { if ( empty( $item['title'] ) ) { return ''; } return ' - ' . $item['title']; } /** * Get subscription icon. * * @since 1.8.2 * * @param array $item Payment item. * * @return string */ private function get_subscription_status_icon( array $item ) { if ( empty( $item['subscription_id'] ) ) { return ''; } return '<span class="dashicons dashicons-marker"></span>'; } /** * Get formatted amount from item. * * @since 1.8.2 * * @param array $item Payment item. * * @return string */ private function get_formatted_amount_from_item( $item ) { if ( empty( $item['total_amount'] ) ) { return ''; } return wpforms_format_amount( wpforms_sanitize_amount( $item['total_amount'], $item['currency'] ), true, $item['currency'] ); } /** * Get selectors which will be displayed over the bulk action menu. * * @since 1.8.2 * * @return array */ protected function get_views() { $base = remove_query_arg( [ 'status', 'paged' ] ); $is_trash_view = $this->is_trash_view(); $views = [ 'all' => sprintf( '<a href="%s"%s>%s <span class="count">(%d)</span></a>', esc_url( $base ), $this->is_current_view( 'all' ) ? ' class="current"' : '', esc_html__( 'All', 'wpforms-lite' ), (int) $this->counts['published'] ), ]; // Iterate through the filterable statuses and add them to the "$views" array. $views = array_merge( $views, $this->get_views_for_filterable_statuses( $base ) ); /** This filter is documented in \WPForms\Admin\Payments\Views\Overview\Table::display_tablenav(). */ if ( $this->counts['trash'] || $is_trash_view ) { $views['trash'] = sprintf( '<a href="%s"%s>%s <span class="count">(%d)</span></a>', esc_url( add_query_arg( [ 'status' => 'trash' ], $base ) ), $is_trash_view ? ' class="current"' : '', esc_html__( 'Trash', 'wpforms-lite' ), (int) $this->counts['trash'] ); } return array_filter( $views ); } /** * Determine whether it is a passed view. * * @since 1.8.2 * * @param string $view Current view to validate. * * @return bool */ private function is_current_view( $view ) { // phpcs:disable WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash if ( $view === 'trash' && isset( $_GET['status'] ) && $_GET['status'] === self::TRASH ) { return true; } if ( ( $view === 'search' || $view === 'all' ) && Search::is_search() ) { return ! isset( $_GET['status'] ); } if ( ValueValidator::is_valid( $view, 'status' ) && isset( $_GET['status'] ) && $_GET['status'] === $view ) { return true; } if ( $view === 'all' && ! isset( $_GET['status'] ) && ! Search::is_search() ) { return true; } // phpcs:enable WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash return false; } /** * Get value provided in search field. * * @since 1.8.2 * * @return string */ private function get_search_query() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotValidated return Search::is_search() ? sanitize_text_field( wp_unslash( $_GET['s'] ) ) : ''; } /** * Get search conditions. * * @since 1.8.2 * * @return array */ private function get_search_conditions() { if ( ! Search::is_search() ) { return []; } return [ 'search_where' => $this->get_search_where_key(), 'search_mode' => $this->get_search_mode_key(), ]; } /** * This function is responsible for determining whether the table items could be displayed. * * @since 1.8.4 */ private function can_prepare_records() { // phpcs:disable WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash if ( isset( $_GET['form_id'] ) && get_post_status( $_GET['form_id'] ) !== 'publish' ) { wp_safe_redirect( Page::get_url() ); exit; } if ( isset( $_GET['status'] ) && $_GET['status'] !== $this->get_valid_status_from_request() ) { wp_safe_redirect( Page::get_url() ); exit; } if ( isset( $_GET['coupon_id'] ) && ! wpforms()->obj( 'payment_meta' )->is_valid_meta( 'coupon_id', absint( $_GET['coupon_id'] ) ) ) { wp_safe_redirect( Page::get_url() ); exit; } // Validate the "type," "gateway," and "subscription_status" parameters. foreach ( [ 'type', 'gateway', 'subscription_status' ] as $column_name ) { // Leave the loop if the parameter is not set. if ( empty( $_GET[ $column_name ] ) ) { continue; } foreach ( explode( '|', $_GET[ $column_name ] ) as $value ) { if ( ! ValueValidator::is_valid( $value, $column_name ) ) { wp_safe_redirect( Page::get_url() ); exit; } } } // phpcs:enable WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash } /** * Display table form's hidden fields. * * @since 1.8.2 */ private function display_hidden_fields() { ?> <input type="hidden" name="page" value="wpforms-payments"> <input type="hidden" name="paged" value="1"> <?php $this->display_status_hidden_field(); $this->display_order_hidden_fields(); $this->display_coupon_id_hidden_field(); $this->display_form_id_hidden_field(); } /** * Display hidden field with status value. * * @since 1.8.2 */ private function display_status_hidden_field() { $status = $this->get_valid_status_from_request(); // Bail early if status is not valid. if ( ! $status ) { return; } // Output the hidden field. // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo wpforms_render( 'admin/payments/hidden-field', [ 'name' => 'status', 'value' => $status, ], true ); } /** * Display hidden fields with order and orderby values. * * @since 1.8.2 */ private function display_order_hidden_fields() { // phpcs:disable WordPress.Security.NonceVerification.Recommended foreach ( [ 'orderby', 'order' ] as $param ) { // Skip if param is not set. if ( empty( $_GET[ $param ] ) ) { continue; } // Output the hidden field. // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo wpforms_render( 'admin/payments/hidden-field', [ 'name' => $param, 'value' => sanitize_text_field( wp_unslash( $_GET[ $param ] ) ), ], true ); } // phpcs:enable WordPress.Security.NonceVerification.Recommended } /** * Display hidden field with coupon ID value. * * @since 1.8.4 */ private function display_coupon_id_hidden_field() { // phpcs:disable WordPress.Security.NonceVerification.Recommended if ( empty( $_GET['coupon_id'] ) ) { return; } // Output the hidden field. // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo wpforms_render( 'admin/payments/hidden-field', [ 'name' => 'coupon_id', 'value' => absint( $_GET['coupon_id'] ), ], true ); // phpcs:enable WordPress.Security.NonceVerification.Recommended } /** * Display hidden field with form ID value. * * @since 1.8.4 */ private function display_form_id_hidden_field() { // phpcs:disable WordPress.Security.NonceVerification.Recommended if ( empty( $_GET['form_id'] ) ) { return; } // Output the hidden field. // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo wpforms_render( 'admin/payments/hidden-field', [ 'name' => 'form_id', 'value' => absint( $_GET['form_id'] ), ], true ); // phpcs:enable WordPress.Security.NonceVerification.Recommended } /** * Get the coupon name from the coupon info. * * @since 1.8.4 * * @param string $coupon_info Coupon information. * * @return string */ private function get_coupon_name_by_info( $coupon_info ) { // Extract the coupon code from the coupon info using regex. if ( preg_match( '/^(.+)/i', $coupon_info, $coupon_code ) ) { return $coupon_code[0]; } return Helpers::get_placeholder_na_text(); } /** * Get the filterable statuses views for the overview table. * * @since 1.8.4 * * @param string $base Base URL for the view links. * * @return array */ private function get_views_for_filterable_statuses( $base ) { $views = []; $statuses = ValueValidator::get_allowed_one_time_statuses(); // Remove the "Partially Refunded" status from the views. unset( $statuses['partrefund'] ); foreach ( $statuses as $status => $label ) { // Skip if the count is zero and the view is not the current status. if ( ! $this->counts[ $status ] && ! $this->is_current_view( $status ) ) { continue; } // Add the view link to the $views array with the status as the key. $views[ $status ] = sprintf( '<a href="%s"%s>%s <span class="count">(%d)</span></a>', esc_url( add_query_arg( [ 'status' => $status ], $base ) ), $this->is_current_view( $status ) ? ' class="current"' : '', esc_html( $label ), (int) $this->counts[ $status ] ); } return $views; } } Admin/Payments/Views/Overview/Search.php 0000644 00000017247 15174710275 0014241 0 ustar 00 <?php namespace WPForms\Admin\Payments\Views\Overview; /** * Search related methods for Payment and Payment Meta. * * @since 1.8.2 */ class Search { /** * Credit card meta key. * * @since 1.8.2 * * @var string */ const CREDIT_CARD = 'credit_card_last4'; /** * Customer email meta key. * * @since 1.8.2 * * @var string */ const EMAIL = 'customer_email'; /** * Payment title column name. * * @since 1.8.2 * * @var string */ const TITLE = 'title'; /** * Transaction ID column name. * * @since 1.8.2 * * @var string */ const TRANSACTION_ID = 'transaction_id'; /** * Subscription ID column name. * * @since 1.8.2 * * @var string */ const SUBSCRIPTION_ID = 'subscription_id'; /** * Any column indicator key. * * @since 1.8.2 * * @var string */ const ANY = 'any'; /** * Equals mode. * * @since 1.8.2 * * @var string */ const MODE_EQUALS = 'equals'; /** * Starts with mode. * * @since 1.8.2 * * @var string */ const MODE_STARTS = 'starts'; /** * Contains mode. * * @since 1.8.2 * * @var string */ const MODE_CONTAINS = 'contains'; /** * Init. * * @since 1.8.2 */ public function init() { if ( ! self::is_search() ) { return; } $this->hooks(); } /** * Hooks. * * @since 1.8.2 */ private function hooks() { add_filter( 'wpforms_db_payments_queries_count_all_query_before_where', [ $this, 'add_search_where_conditions' ], 10, 2 ); add_filter( 'wpforms_db_payments_payment_get_payments_query_before_where', [ $this, 'add_search_where_conditions' ], 10, 2 ); add_filter( 'wpforms_admin_payments_views_overview_filters_renewals_by_subscription_id_query_before_where', [ $this, 'add_search_where_conditions' ], 10, 2 ); } /** * Check if search query. * * @since 1.8.2 * * @return bool */ public static function is_search() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended return ! empty( $_GET['s'] ); } /** * Add search where conditions. * * @since 1.8.2 * * @param string $where Query where string. * @param array $args Query arguments. * * @return string */ public function add_search_where_conditions( $where, $args ) { if ( empty( $args['search'] ) ) { return $where; } if ( ! empty( $args['search_conditions']['search_mode'] ) && $args['search_conditions']['search_mode'] === self::MODE_CONTAINS ) { $to_search = explode( ' ', $args['search'] ); } else { $to_search = [ $args['search'] ]; } $query = []; foreach ( $to_search as $counter => $single ) { $query[] = $this->add_single_search_condition( $single, $args, $counter ); } return implode( ' ', $query ); } /** * Add single search condition. * * @since 1.8.2 * * @param string $word Single searched part. * @param array $args Query arguments. * @param int $n Word counter. * * @return string */ private function add_single_search_condition( $word, $args, $n ) { if ( empty( $word ) ) { return ''; } $mode = $this->prepare_mode( $args ); $where = $this->prepare_where( $args ); list( $operator, $word ) = $this->prepare_operator_and_word( $word, $mode ); $column = $this->prepare_column( $where ); if ( in_array( $column, [ self::EMAIL, self::CREDIT_CARD ], true ) ) { return $this->select_from_meta_table( $column, $operator, $word, $n ); } if ( $column === self::ANY ) { return $this->select_from_any( $operator, $word, $n ); } $payment_table = wpforms()->obj( 'payment' )->table_name; $query = "SELECT id FROM {$payment_table} WHERE {$payment_table}.{$column} {$operator} {$word}"; return $this->wrap_in_inner_join( $query, $n ); } /** * Prepare search mode part. * * @since 1.8.2 * * @param array $args Query arguments. * * @return string Mode part for search. */ private function prepare_mode( $args ) { return isset( $args['search_conditions']['search_mode'] ) ? $args['search_conditions']['search_mode'] : self::MODE_EQUALS; } /** * Prepare search where part. * * @since 1.8.2 * * @param array $args Query arguments. * * @return string Where part for search. */ private function prepare_where( $args ) { return isset( $args['search_conditions']['search_where'] ) ? $args['search_conditions']['search_where'] : self::TITLE; } /** * Prepare operator and word parts. * * @since 1.8.2 * * @param string $word Single word. * @param string $mode Search mode. * * @return array Array with operator and word parts for search. */ private function prepare_operator_and_word( $word, $mode ) { global $wpdb; if ( $mode === self::MODE_CONTAINS ) { return [ 'LIKE', $wpdb->prepare( '%s', '%' . $wpdb->esc_like( $word ) . '%' ), ]; } if ( $mode === self::MODE_STARTS ) { return [ 'LIKE', $wpdb->prepare( '%s', $wpdb->esc_like( $word ) . '%' ), ]; } return [ '=', $wpdb->prepare( '%s', $word ), ]; } /** * Prepare column to search in. * * @since 1.8.2 * * @param string $where Search where. * * @return string Column to search in. */ private function prepare_column( $where ) { if ( in_array( $where, [ self::TRANSACTION_ID, self::SUBSCRIPTION_ID, self::EMAIL, self::CREDIT_CARD, self::ANY ], true ) ) { return $where; } return self::TITLE; } /** * Prepare select part to select from payments meta table. * * @since 1.8.2 * * @param string $meta_key Meta key. * @param string $operator Comparison operator. * @param string $word Word to search. * @param int $n Word count. * * @return string * @noinspection CallableParameterUseCaseInTypeContextInspection */ private function select_from_meta_table( $meta_key, $operator, $word, $n ) { global $wpdb; $payment_table = wpforms()->obj( 'payment' )->table_name; $meta_table = wpforms()->obj( 'payment_meta' )->table_name; $meta_key = $wpdb->prepare( '%s', $meta_key ); $query = "SELECT id FROM $payment_table WHERE id IN ( SELECT DISTINCT payment_id FROM $meta_table WHERE meta_value $operator $word AND meta_key = $meta_key )"; return $this->wrap_in_inner_join( $query, $n ); } /** * Prepare select part to select from places from both tables. * * @since 1.8.2 * * @param string $operator Comparison operator. * @param string $word Word to search. * @param int $n Word count. * * @return string */ private function select_from_any( $operator, $word, $n ) { $payment_table = wpforms()->obj( 'payment' )->table_name; $meta_table = wpforms()->obj( 'payment_meta' )->table_name; $query = sprintf( "SELECT id FROM {$payment_table} WHERE ( {$payment_table}.%s {$operator} {$word} OR {$payment_table}.%s {$operator} {$word} OR {$payment_table}.%s {$operator} {$word} OR id IN ( SELECT DISTINCT payment_id FROM {$meta_table} WHERE meta_value {$operator} {$word} AND meta_key IN ( '%s', '%s' ) ) )", self::TITLE, self::TRANSACTION_ID, self::SUBSCRIPTION_ID, self::CREDIT_CARD, self::EMAIL ); return $this->wrap_in_inner_join( $query, $n ); } /** * Wrap the query in INNER JOIN part. * * @since 1.8.2 * * @param string $query Partial query. * @param int $n Word count. * * @return string */ private function wrap_in_inner_join( $query, $n ) { /** * Filter to modify the inner join query. * * @since 1.8.4 * * @param string $query Partial query. * @param int $n The number of the JOIN clause. */ return apply_filters( 'wpforms_admin_payments_views_overview_search_inner_join_query', sprintf( 'INNER JOIN ( %1$s ) AS p%2$d ON p.id = p%2$d.id', $query, $n ), $n ); } } Admin/Payments/Views/Coupons/Education.php 0000644 00000010541 15174710275 0014555 0 ustar 00 <?php namespace WPForms\Admin\Payments\Views\Coupons; use WPForms\Admin\Payments\Views\Overview\Helpers; use WPForms\Admin\Payments\Views\PaymentsViewsInterface; /** * Payments Coupons Education class. * * @since 1.8.2.2 */ class Education implements PaymentsViewsInterface { /** * Coupons addon data. * * @since 1.8.2.2 * * @var array */ private $addon; /** * Initialize class. * * @since 1.8.2.2 */ public function init() { $this->hooks(); } /** * Register hooks. * * @since 1.8.2.2 */ private function hooks() { add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_scripts' ] ); } /** * Get the page label. * * @since 1.8.2.2 * * @return string */ public function get_tab_label() { return __( 'Coupons', 'wpforms-lite' ); } /** * Enqueue scripts. * * @since 1.8.2.2 */ public function enqueue_scripts() { // Lity - lightbox for images. wp_enqueue_style( 'wpforms-lity', WPFORMS_PLUGIN_URL . 'assets/lib/lity/lity.min.css', null, '3.0.0' ); wp_enqueue_script( 'wpforms-lity', WPFORMS_PLUGIN_URL . 'assets/lib/lity/lity.min.js', [ 'jquery' ], '3.0.0', true ); } /** * Check if the current user has the capability to view the page. * * @since 1.8.2.2 * * @return bool */ public function current_user_can() { if ( ! wpforms_current_user_can() ) { return false; } $this->addon = wpforms()->obj( 'addons' )->get_addon( 'coupons' ); if ( empty( $this->addon ) || empty( $this->addon['status'] ) || empty( $this->addon['action'] ) ) { return false; } return true; } /** * Page heading content. * * @since 1.8.2.2 */ public function heading() { Helpers::get_default_heading(); } /** * Page content. * * @since 1.8.2.2 */ public function display() { // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo wpforms_render( 'education/admin/page', $this->template_data(), true ); } /** * Get the template data. * * @since 1.8.6 * * @return array */ private function template_data(): array { $images_url = WPFORMS_PLUGIN_URL . 'assets/images/coupons-education/'; $utm_medium = 'Payments - Coupons'; $utm_content = 'Coupons Addon'; $upgrade_link = $this->addon['action'] === 'upgrade' ? sprintf( /* translators: %1$s - WPForms.com Upgrade page URL. */ ' <strong><a href="%1$s" target="_blank" rel="noopener noreferrer">%2$s</a></strong>', esc_url( wpforms_admin_upgrade_link( $utm_medium, $utm_content ) ), esc_html__( 'Upgrade to WPForms Pro', 'wpforms-lite' ) ) : ''; $params = [ 'features' => [ __( 'Custom Coupon Codes', 'wpforms-lite' ), __( 'Percentage or Fixed Discounts', 'wpforms-lite' ), __( 'Start and End Dates', 'wpforms-lite' ), __( 'Maximum Usage Limit', 'wpforms-lite' ), __( 'Once Per Email Address Limit', 'wpforms-lite' ), __( 'Usage Statistics', 'wpforms-lite' ), ], 'images' => [ [ 'url' => $images_url . 'coupons-addon-thumbnail-01.png', 'url2x' => $images_url . 'coupons-addon-screenshot-01.png', 'title' => __( 'Coupons Overview', 'wpforms-lite' ), ], [ 'url' => $images_url . 'coupons-addon-thumbnail-02.png', 'url2x' => $images_url . 'coupons-addon-screenshot-02.png', 'title' => __( 'Coupon Settings', 'wpforms-lite' ), ], ], 'utm_medium' => $utm_medium, 'utm_content' => $utm_content, 'upgrade_link' => $upgrade_link, 'heading_description' => '<p>' . sprintf( /* translators: %1$s - WPForms.com Upgrade page URL. */ esc_html__( 'With the Coupons addon, you can offer customers discounts using custom coupon codes. Create your own percentage or fixed rate discount, then add the Coupon field to any payment form. When a customer enters your unique code, they’ll receive the specified discount. You can also add limits to restrict when coupons are available and how often they can be used. The Coupons addon requires a license level of Pro or higher.%s', 'wpforms-lite' ), wp_kses( $upgrade_link, [ 'a' => [ 'href' => [], 'rel' => [], 'target' => [], ], 'strong' => [], ] ) ) . '</p>', 'features_description' => __( 'Easy to Use, Yet Powerful', 'wpforms-lite' ), ]; return array_merge( $params, $this->addon ); } } Admin/Payments/Views/Single.php 0000644 00000104170 15174710275 0012437 0 ustar 00 <?php namespace WPForms\Admin\Payments\Views; use WPForms\Admin\Payments\ScreenOptions; use WPForms\Admin\Payments\Views\Overview\Helpers; use WPForms\Db\Payments\ValueValidator; use WPForms_Field_Layout; /** * Payments Overview Page class. * * @since 1.8.2 */ class Single implements PaymentsViewsInterface { /** * Abort. Bail on proceeding to process the page. * * @since 1.8.2 * * @var bool */ private $abort = false; /** * The human readable error message. * * @since 1.8.2 * * @var string */ private $abort_message; /** * Payment object. * * @since 1.8.2 * * @var object */ private $payment; /** * Payment meta. * * @since 1.8.2 * * @var array */ private $payment_meta; /** * Subscription object, if applicable. * * @since 1.8.4 * * @var object */ private $subscription; /** * Subscription meta, if applicable. * * @since 1.8.4 * * @var array */ private $subscription_meta; /** * Subscription renewal payments, if applicable. * This is an array of payment objects. * * @since 1.8.4 * * @var array */ private $renewals = []; /** * Initialize class. * * @since 1.8.2 */ public function init() { $this->setup(); $this->hooks(); } /** * Register hooks. * * @since 1.8.2 */ private function hooks() { add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] ); } /** * Get the tab label. * * @since 1.8.2.2 * * @return string */ public function get_tab_label() { return ''; } /** * Enqueue scripts and styles. * * @since 1.8.2 */ public function enqueue_assets() { $min = wpforms_get_min_suffix(); wp_enqueue_style( 'tooltipster', WPFORMS_PLUGIN_URL . 'assets/lib/jquery.tooltipster/jquery.tooltipster.min.css', null, '4.2.6' ); wp_enqueue_script( 'tooltipster', WPFORMS_PLUGIN_URL . 'assets/lib/jquery.tooltipster/jquery.tooltipster.min.js', [ 'jquery' ], '4.2.6', true ); wp_enqueue_script( 'wpforms-admin-payments-single', WPFORMS_PLUGIN_URL . "assets/js/admin/payments/single{$min}.js", [ 'tooltipster' ], WPFORMS_VERSION, true ); wp_localize_script( 'wpforms-admin-payments-single', 'wpforms_admin_payments_single', [ 'payment_delete_confirm' => esc_html__( 'Are you sure you want to delete this payment and all its information (details, notes, etc.)?', 'wpforms-lite' ), 'payment_refund_confirm' => esc_html__( 'Are you sure you want to refund this payment?', 'wpforms-lite' ), 'payment_cancel_confirm' => esc_html__( 'Are you sure you want to cancel this subscription?', 'wpforms-lite' ), 'payment_refund_success' => esc_html__( 'Payment was successfully refunded!', 'wpforms-lite' ), 'payment_cancel_success' => esc_html__( 'Subscription was successfully canceled!', 'wpforms-lite' ), ] ); } /** * Setup data. * * @since 1.8.2 */ private function setup() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $payment_id = ! empty( $_GET['payment_id'] ) ? absint( $_GET['payment_id'] ) : 0; if ( ! $payment_id ) { $this->abort_message = esc_html__( 'It looks like the provided payment ID is not valid.', 'wpforms-lite' ); $this->abort = true; return; } $this->payment = wpforms()->obj( 'payment' )->get( $payment_id ); // No payment was found. if ( empty( $this->payment ) ) { $this->abort_message = esc_html__( 'It looks like the payment you are trying to access is no longer available.', 'wpforms-lite' ); $this->abort = true; return; } // Payment in the Trash. if ( ! $this->payment->is_published ) { $this->abort_message = esc_html__( "You can't edit this payment because it's in the trash.", 'wpforms-lite' ); $this->abort = true; return; } $this->payment_meta = wpforms()->obj( 'payment_meta' )->get_all( $payment_id ); // Retrieve the subscription renewal payments, if applicable. if ( ! empty( $this->payment->subscription_id ) ) { // Assign renewals to reduce queries and reuse later. list( $this->subscription, $this->renewals ) = wpforms()->obj( 'payment_queries' )->get_subscription_payment_history( $this->payment->subscription_id, $this->payment->currency ); if ( ! empty( $this->subscription ) ) { $this->subscription_meta = wpforms()->obj( 'payment_meta' )->get_all( $this->subscription->id ); } } } /** * Check if the current user has the capability to view the page. * * @since 1.8.2 * * @return bool */ public function current_user_can() { return wpforms_current_user_can(); } /** * Page heading. * * @since 1.8.2 */ public function heading() { if ( $this->abort ) { return; } $payment_prev = wpforms()->obj( 'payment_queries' )->get_prev( $this->payment->id, [ 'mode' => $this->payment->mode ] ); $payment_next = wpforms()->obj( 'payment_queries' )->get_next( $this->payment->id, [ 'mode' => $this->payment->mode ] ); $prev_url = ! empty( $payment_prev ) ? add_query_arg( [ 'page' => 'wpforms-payments', 'view' => 'payment', 'payment_id' => (int) $payment_prev->id, ], admin_url( 'admin.php' ) ) : ''; $next_url = ! empty( $payment_next ) ? add_query_arg( [ 'page' => 'wpforms-payments', 'view' => 'payment', 'payment_id' => (int) $payment_next->id, ], admin_url( 'admin.php' ) ) : ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo wpforms_render( 'admin/payments/single/heading-navigation', [ 'count' => (int) wpforms()->obj( 'payment_queries' )->count_all( [ 'mode' => $this->payment->mode ] ), 'prev_count' => (int) wpforms()->obj( 'payment_queries' )->get_prev_count( $this->payment->id, [ 'mode' => $this->payment->mode ] ), 'prev_url' => $prev_url, 'prev_class' => empty( $payment_prev ) ? 'inactive' : '', 'next_url' => $next_url, 'next_class' => empty( $payment_next ) ? 'inactive' : '', 'overview_url' => add_query_arg( [ 'page' => 'wpforms-payments', ], admin_url( 'admin.php' ) ), ], true ); } /** * Page content. * * @since 1.8.2 */ public function display() { if ( $this->abort ) { echo '<div class="wpforms-admin-content">'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo wpforms_render( 'admin/payments/single/no-payment', [ 'message' => $this->abort_message, ], true ); echo '</div>'; return; } $screen_options = ScreenOptions::get_single_page_options(); echo '<div id="poststuff"><div id="post-body" class="metabox-holder columns-2">'; echo '<div id="post-body-content">'; $this->payment_details(); $this->education_details(); $this->subscription_details(); $this->subscription_payment_history(); if ( ! empty( $screen_options['advanced'] ) ) { $this->advanced_details(); } $this->entry_details(); echo '</div>'; echo '<div id="postbox-container-1" class="postbox-container">'; $this->details(); if ( ! empty( $screen_options['log'] ) ) { $this->log(); } echo '</div></div></div>'; } /** * Payment details output. * * @since 1.8.2 */ private function payment_details() { $payment_type_class = ! empty( $this->payment->subscription_id ) ? 'subscription' : 'one-time'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo wpforms_render( 'admin/payments/single/payment-details', [ 'id' => 'wpforms-payment-info', 'class' => 'payment-details', 'title' => __( 'Payment Details', 'wpforms-lite' ), 'payment_id' => "#{$this->payment->id}", 'gateway_link' => $this->get_gateway_transaction_link(), 'gateway_text' => sprintf( /* translators: %s - payment gateway name. */ __( 'View in %s', 'wpforms-lite' ), $this->get_gateway_name() ), 'gateway_name' => $this->payment->gateway, 'gateway_action_text' => __( 'Refund', 'wpforms-lite' ), 'gateway_action_slug' => 'refund', 'gateway_action_link' => $this->get_gateway_action_link( 'refund' ), 'payment_id_raw' => $this->payment->id, 'status' => $this->payment->status, 'status_label' => $this->get_status_label(), 'disabled' => $this->payment->status === 'refunded', 'stat_cards' => [ 'total' => [ 'label' => esc_html__( 'Total', 'wpforms-lite' ), 'value' => wpforms_format_amount( wpforms_sanitize_amount( $this->payment->total_amount, $this->payment->currency ), true, $this->payment->currency ), 'button_classes' => [ 'total', 'is-amount', ], ], 'type' => [ 'label' => esc_html__( 'Type', 'wpforms-lite' ), 'value' => $this->get_payment_type(), 'button_classes' => [ $payment_type_class, ], ], 'method' => [ 'label' => esc_html__( 'Method', 'wpforms-lite' ), 'value' => $this->get_payment_method(), 'button_classes' => [ 'method', ], 'tooltip' => $this->get_payment_method_details(), ], 'coupon' => [ 'label' => esc_html__( 'Coupon', 'wpforms-lite' ), 'value' => $this->get_coupon_value(), 'button_classes' => [ 'coupon', 'upsell', ], 'tooltip' => nl2br( $this->get_coupon_info() ), ], ], ], true ); } /** * Subscription details output. * * @since 1.8.2 */ private function subscription_details() { if ( empty( $this->subscription ) ) { return; } // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo wpforms_render( 'admin/payments/single/payment-details', [ 'id' => 'wpforms-subscription-details', 'class' => 'subscription-details', 'title' => __( 'Subscription Details', 'wpforms-lite' ), 'gateway_link' => $this->get_gateway_subscription_link(), 'gateway_text' => sprintf( /* translators: %s - payment gateway name. */ __( 'View in %s', 'wpforms-lite' ), $this->get_gateway_name() ), 'gateway_name' => $this->payment->gateway, 'gateway_action_text' => __( 'Cancel', 'wpforms-lite' ), 'gateway_action_slug' => 'cancel', 'gateway_action_link' => $this->get_gateway_action_link( 'cancel' ), 'payment_id_raw' => $this->subscription->id, 'status' => $this->subscription->subscription_status, 'status_label' => ValueValidator::get_allowed_subscription_statuses()[ $this->subscription->subscription_status ], 'disabled' => $this->subscription->subscription_status === 'cancelled', 'stat_cards' => [ 'total' => [ 'label' => esc_html__( 'Lifetime Total', 'wpforms-lite' ), 'value' => $this->get_subscription_lifetime_total(), 'button_classes' => [ 'lifetime-total', 'is-amount', ], ], 'cycle' => [ 'label' => esc_html__( 'Billing Cycle', 'wpforms-lite' ), 'value' => $this->get_subscription_cycle(), 'button_classes' => [ 'cycle', ], ], 'billed' => [ 'label' => esc_html__( 'Times Billed', 'wpforms-lite' ), 'value' => $this->get_subscription_times_billed(), 'button_classes' => [ 'cycle', ], ], 'renewal' => [ 'label' => esc_html__( 'Renewal Date', 'wpforms-lite' ), 'value' => $this->get_renewal_date(), 'button_classes' => [ 'date', ], ], ], ], true ); } /** * Subscription payment history output. * * @since 1.8.4 */ private function subscription_payment_history() { // Early bail if no subscription ID. if ( empty( $this->payment->subscription_id ) ) { return; } // Early bail if no subscription or renewals found. // "$this->renewals" is set in the "setup" method. if ( empty( $this->renewals ) ) { return; } // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo wpforms_render( 'admin/payments/single/payment-history', [ 'title' => __( 'Payment History', 'wpforms-lite' ), 'renewals' => $this->renewals, 'types' => ValueValidator::get_allowed_subscription_types(), 'statuses' => ValueValidator::get_allowed_statuses(), 'placeholder_na_text' => Helpers::get_placeholder_na_text( false ), 'single_url' => add_query_arg( [ 'page' => 'wpforms-payments', 'view' => 'payment', ], admin_url( 'admin.php' ) ), ], true ); } /** * Get Subscription cycle. * * @since 1.8.2 * * @return string */ private function get_subscription_cycle() { $allowed_intervals = ValueValidator::get_allowed_subscription_intervals(); if ( isset( $this->subscription_meta['subscription_period']->value, $allowed_intervals[ $this->subscription_meta['subscription_period']->value ] ) ) { $amount = wpforms_format_amount( wpforms_sanitize_amount( $this->payment->total_amount, $this->payment->currency ), true, $this->payment->currency ); $interval = $allowed_intervals[ $this->subscription_meta['subscription_period']->value ]; return "{$amount} / {$interval}"; } return Helpers::get_placeholder_na_text( false ); } /** * Get Subscription lifetime total. * * @since 1.8.4 * * @return string */ private function get_subscription_lifetime_total() { return wpforms_format_amount( (float) $this->subscription->total_amount + array_sum( array_column( $this->renewals, 'total_amount' ) ), true, $this->payment->currency ); } /** * Get Subscription times billed. * * @since 1.8.4 * * @return int|string */ private function get_subscription_times_billed() { // Display "N/A", in case no subscription ID is found. if ( empty( $this->payment->subscription_id ) ) { return Helpers::get_placeholder_na_text( false ); } // Add the initial subscription payment object to the renewal array. // The "+1" has to be added, because the initial subscription payment is not included in the renewals array. if ( ! empty( $this->subscription ) ) { $this->renewals[] = $this->subscription; } return count( $this->renewals ); } /** * Get Subscription renewal date. * * @since 1.8.2 * * @return string */ private function get_renewal_date() { if ( $this->payment->subscription_status === 'cancelled' || $this->is_renewal_of_cancelled_subscription() ) { return Helpers::get_placeholder_na_text( false ); } $converted_periods = [ 'daily' => '+1 day', 'weekly' => '+1 week', 'monthly' => '+1 month', 'quarterly' => '+3 month', 'semiyearly' => '+6 month', 'yearly' => '+1 year', ]; if ( ! isset( $this->subscription_meta['subscription_period']->value, $converted_periods[ $this->subscription_meta['subscription_period']->value ] ) ) { return ''; } return gmdate( 'M d, Y', strtotime( $this->payment->date_updated_gmt . $converted_periods[ $this->subscription_meta['subscription_period']->value ] ) ); } /** * Is renewal of cancelled subscription. * * @since 1.8.4 * * @return bool */ private function is_renewal_of_cancelled_subscription() { return $this->payment->type === 'renewal' && $this->subscription->subscription_status === 'cancelled'; } /** * Get payment type name. * i.e. One-time, Subscription, etc. * * @since 1.8.4 * * @return string */ private function get_payment_type() { if ( isset( $this->payment->type ) && ValueValidator::is_valid( $this->payment->type, 'type' ) ) { return ValueValidator::get_allowed_types()[ $this->payment->type ]; } return Helpers::get_placeholder_na_text( false ); } /** * Get payment method type. * * @since 1.8.2 * * @return string */ private function get_payment_method() { $method = isset( $this->payment_meta['credit_card_method'] ) ? ucfirst( $this->payment_meta['credit_card_method']->value ) : ''; if ( $method ) { return $method; } return isset( $this->payment_meta['method_type'] ) ? ucfirst( $this->payment_meta['method_type']->value ) : Helpers::get_placeholder_na_text( false ); } /** * Get payment method details. * * @since 1.8.2 * * @return string */ private function get_payment_method_details() { if ( ! isset( $this->payment_meta['method_type'] ) || $this->payment_meta['method_type']->value !== 'card' || empty( $this->payment_meta['credit_card_last4'] ) || empty( $this->payment_meta['credit_card_expires'] ) ) { return ''; } $credit_card_last = 'xxxx xxxx xxxx ' . $this->payment_meta['credit_card_last4']->value; $expires_in = sprintf( /* translators: %s - credit card expiry date. */ __( 'Expires %s', 'wpforms-lite' ), $this->payment_meta['credit_card_expires']->value ); $output = '<div>'; if ( ! empty( $this->payment_meta['credit_card_name'] ) ) { $output .= '<span>' . esc_html( $this->payment_meta['credit_card_name']->value ) . '</span></br>'; } $output .= '<span>' . esc_html( $credit_card_last ) . '</span></br>'; $output .= '<span>' . esc_html( $expires_in ) . '</span>'; $output .= '</div>'; return $output; } /** * Get coupon info. * * @since 1.8.2.2 * * @return string */ private function get_coupon_info() { $coupon_info = ! empty( $this->payment_meta['coupon_info']->value ) ? $this->payment_meta['coupon_info']->value : ''; /** * Allow modifying coupon info. * * @since 1.8.2.2 * * @param string $coupon_info Coupon info. * @param object $payment Payment object. * @param array $payment_meta Payment meta. */ return apply_filters( 'wpforms_admin_payments_views_single_get_coupon_info', $coupon_info, $this->payment, $this->payment_meta ); } /** * Get coupon value. * * @since 1.8.2.2 * * @return string */ private function get_coupon_value() { return ! empty( $this->payment_meta['coupon_value']->value ) ? sprintf( '-%s', $this->payment_meta['coupon_value']->value ) : ''; } /** * Education notice for lite users output. * * @since 1.8.2 */ private function education_details() { if ( in_array( wpforms_get_license_type(), [ 'pro', 'elite', 'agency', 'ultimate' ], true ) ) { return; } $dismissed = get_user_meta( get_current_user_id(), 'wpforms_dismissed', true ); if ( ! empty( $dismissed['edu-single-payment'] ) ) { return; } // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo wpforms_render( 'education/admin/payments/single-page' ); } /** * Advanced details output. * * @since 1.8.2 */ private function advanced_details() { /** * Allow to modify a single payment page advanced details list. * * @since 1.8.2 * * @param array $list Advanced details to show. * @param object $payment Payment object. */ $details_list = (array) apply_filters( 'wpforms_admin_payments_views_single_advanced_details_list', [ 'transaction_id' => [ 'label' => __( 'Transaction ID', 'wpforms-lite' ), 'link' => $this->get_gateway_transaction_link(), 'value' => $this->payment->transaction_id, ], 'subscription_id' => [ 'label' => __( 'Subscription ID', 'wpforms-lite' ), 'link' => $this->get_gateway_subscription_link(), 'value' => $this->payment->subscription_id, ], 'customer_id' => [ 'label' => __( 'Customer ID', 'wpforms-lite' ), 'link' => $this->get_gateway_customer_link(), 'value' => $this->payment->customer_id, ], 'customer_ip' => [ 'label' => __( 'Customer IP Address', 'wpforms-lite' ), 'value' => ! empty( $this->payment_meta['ip_address']->value ) ? $this->payment_meta['ip_address']->value : false, ], 'payment_method' => [ 'label' => __( 'Payment Method', 'wpforms-lite' ), 'value' => $this->get_payment_method_details(), ], 'coupon_info' => [ 'label' => __( 'Coupon', 'wpforms-lite' ), 'value' => $this->get_coupon_info(), ], ], $this->payment ); // Skip empty details. $details_list = array_filter( $details_list, static function ( $item ) { return ! empty( $item['value'] ); } ); // Return early if there are no details. if ( empty( $details_list ) ) { return; } // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo wpforms_render( 'admin/payments/single/advanced-details', [ 'details_list' => $details_list, ], true ); } /** * Entry details output. * * @since 1.8.2 */ private function entry_details() { // phpcs:ignore WPForms.PHP.HooksMethod.InvalidPlaceForAddingHooks $entry_id_title = ''; $fields = ''; $entry_status = ''; // Grab submitted values from the entry if it exists. if ( ! empty( $this->payment->entry_id ) && wpforms()->is_pro() ) { $entry = wpforms()->obj( 'entry' )->get( $this->payment->entry_id ); if ( $entry ) { $fields = wpforms_decode( $entry->fields ); $entry_id_title .= "#{$this->payment->entry_id}"; $entry_status = $entry->status; } } // Otherwise, grab submitted values from the payment meta if it exists. if ( empty( $fields ) && ! empty( $this->payment_meta['fields'] ) ) { $fields = wpforms_decode( $this->payment_meta['fields']->value ); } // Bail early if there are submitted values. if ( empty( $fields ) ) { return; } /** * Allow modifying the form data before rendering the entry details. * * @since 1.8.9 * * @param array $form_data Form data. * @param array $fields Entry fields. */ $form_data = apply_filters( 'wpforms_admin_payments_views_single_form_data', wpforms()->obj( 'form' )->get( $this->payment->form_id, [ 'content_only' => true ] ), $fields ); add_filter( 'wp_kses_allowed_html', [ $this, 'modify_allowed_tags_payment_field_value' ], 10, 2 ); /** * Allow modifying the entry fields before rendering the entry details. * * @since 1.8.9 * * @param array $entry_fields Entry fields. * @param array $form_data Form data. */ $entry_fields = apply_filters( 'wpforms_admin_payments_views_single_fields', $this->prepare_entry_fields( $fields, $form_data ), $form_data ); $entry_output = wpforms_render( 'admin/payments/single/entry-details', [ 'entry_fields' => $entry_fields, 'form_data' => $form_data, 'entry_id_title' => $entry_id_title, 'entry_id' => $this->payment->entry_id, 'entry_status' => $entry_status, 'entry_url' => add_query_arg( [ 'page' => 'wpforms-entries', 'view' => 'details', 'entry_id' => $this->payment->entry_id, ], admin_url( 'admin.php' ) ), ], true ); remove_filter( 'wp_kses_allowed_html', [ $this, 'modify_allowed_tags_payment_field_value' ] ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo $entry_output; } /** * Prepare entry fields. * * @since 1.8.2 * * @param array $fields Entry fields. * @param array $form_data Form data. * * @return array */ private function prepare_entry_fields( $fields, $form_data ) { // phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh if ( empty( $form_data['fields'] ) || empty( $fields ) ) { return []; } $prepared_fields = []; // Display the fields and their values. foreach ( $form_data['fields'] as $key => $field_data ) { if ( empty( $field_data['type'] ) ) { continue; } $field_type = $field_data['type']; // Add repeater and layout fields as is. if ( in_array( $field_type, [ 'repeater', 'layout' ], true ) && wpforms()->is_pro() ) { $prepared_fields[ $key ] = $field_data; continue; } $field = $fields[ $field_data['id'] ] ?? []; if ( empty( $field ) || ! isset( $field['id'] ) ) { continue; } // phpcs:disable WPForms.PHP.ValidateHooks.InvalidHookName /** This filter is documented in /src/Pro/Admin/Entries/Edit.php */ if ( $this->payment->entry_id && ! (bool) apply_filters( "wpforms_pro_admin_entries_edit_is_field_displayable_{$field_type}", true, $field, $form_data ) ) { continue; } $field_value = isset( $field['value'] ) ? $field['value'] : ''; /** This filter is documented in src/SmartTags/SmartTag/FieldHtmlId.php.*/ $prepared_fields[ $key ]['field_value'] = apply_filters( 'wpforms_html_field_value', wp_strip_all_tags( $field_value ), $field, $form_data, 'payment-single' ); // phpcs:enable WPForms.PHP.ValidateHooks.InvalidHookName $prepared_fields[ $key ]['field_class'] = sanitize_html_class( 'wpforms-field-' . $field_type ); $prepared_fields[ $key ]['type'] = $field_type; $prepared_fields[ $key ]['id'] = $field_data['id']; $prepared_fields[ $key ]['field_name'] = ! empty( $field['name'] ) ? $field['name'] : sprintf( /* translators: %d - field ID. */ esc_html__( 'Field ID #%d', 'wpforms-lite' ), absint( $field['id'] ) ); $is_empty_value = wpforms_is_empty_string( $field_value ); $is_empty_quantity = isset( $field['quantity'] ) && ! $field['quantity']; if ( $is_empty_value ) { $prepared_fields[ $key ]['field_value'] = esc_html__( 'Empty', 'wpforms-lite' ); } if ( $is_empty_value || $is_empty_quantity ) { $prepared_fields[ $key ]['field_class'] .= ' empty'; } } return $prepared_fields; } /** * Allow additional tags for the wp_kses_post function. * * @since 1.8.2 * * @param array $allowed_html List of allowed HTML. * @param string $context Context name. * * @return array */ public function modify_allowed_tags_payment_field_value( $allowed_html, $context ) { if ( $context !== 'post' ) { return $allowed_html; } $allowed_html['iframe'] = [ 'data-src' => [], 'class' => [], ]; return $allowed_html; } /** * Details metabox output. * * @since 1.8.2 */ private function details() { $form_edit_link = $this->get_form_edit_link(); $date = sprintf( /* translators: %1$s - date, %2$s - time when item was created, e.g. "Oct 22, 2022 at 11:11 am". */ __( '%1$s at %2$s', 'wpforms-lite' ), wpforms_date_format( $this->payment->date_created_gmt, 'M j, Y', true ), wpforms_time_format( $this->payment->date_created_gmt, '', true ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo wpforms_render( 'admin/payments/single/details', [ 'payment' => $this->payment, 'submitted' => $date, 'gateway_name' => $this->get_gateway_name(), 'gateway_link' => $this->get_gateway_dashboard_link(), 'form_edit_link' => ! empty( $form_edit_link ) ? $form_edit_link : Helpers::get_placeholder_na_text(), 'test_mode' => $this->payment->mode === 'test', 'delete_link' => wp_nonce_url( add_query_arg( [ 'page' => 'wpforms-payments', 'action' => 'delete', 'payment_id' => $this->payment->id, ], admin_url( 'admin.php' ) ), 'bulk-wpforms_page_wpforms-payments' ), ], true ); } /** * Logs metabox output. * * @since 1.8.2 */ private function log() { // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo wpforms_render( 'admin/payments/single/log', [ 'logs' => wpforms()->obj( 'payment_meta' )->get_all_by( 'log', $this->payment->id ), ], true ); } // TODO: Remove hardcoded values in methods below after all payment addons updated to use new filters. /** * Get gateway transaction link. * * @since 1.8.2 * * @return string */ private function get_gateway_transaction_link() { /** * Allow to modify a single payment page gateway transaction link. * * @since 1.8.2 * * @param string $link Gateway transaction link. * @param object $payment Payment object. */ $link = apply_filters( 'wpforms_admin_payments_views_single_gateway_transaction_link', '', $this->payment ); if ( $link ) { return $link; } if ( ! $this->payment->transaction_id ) { return ''; } switch ( $this->payment->gateway ) { case 'stripe': $link = 'payments/'; break; case 'paypal_standard': case 'paypal_commerce': $link = 'activity/payment/'; break; case 'square': $link = 'sales/transactions/'; break; default: $link = ''; break; } if ( ! $link ) { return $this->get_gateway_dashboard_link(); } return $this->get_gateway_dashboard_link() . $link . $this->payment->transaction_id; } /** * Get gateway subscription link. * * @since 1.8.2 * * @return string */ private function get_gateway_subscription_link() { /** * Allow to modify a single payment page gateway subscription link. * * @since 1.8.2 * * @param string $link Gateway subscription link. * @param object $payment Payment object. */ $link = apply_filters( 'wpforms_admin_payments_views_single_gateway_subscription_link', '', $this->payment ); if ( $link ) { return $link; } switch ( $this->payment->gateway ) { case 'stripe': $link = 'subscriptions/'; break; case 'paypal_commerce': $link = 'billing/subscriptions/'; break; case 'square': $link = 'subscriptions/'; break; default: $link = ''; break; } if ( ! $link ) { return $this->get_gateway_dashboard_link(); } return $this->get_gateway_dashboard_link() . $link . $this->payment->subscription_id; } /** * Get gateway customer link. * * @since 1.8.2 * * @return string */ private function get_gateway_customer_link() { /** * Allow to modify a single payment page gateway customer link. * * @since 1.8.2 * * @param string $link Gateway customer link. * @param object $payment Payment object. */ $link = apply_filters( 'wpforms_admin_payments_views_single_gateway_customer_link', '', $this->payment ); if ( $link ) { return $link; } switch ( $this->payment->gateway ) { case 'stripe': $link = 'customers/'; break; case 'square': $link = 'customers/directory/customer/'; break; default: $link = ''; break; } if ( ! $link ) { return $this->get_gateway_dashboard_link(); } return $this->get_gateway_dashboard_link() . $link . $this->payment->customer_id; } /** * Get gateway dashboard link. * * @since 1.8.2 * * @return string */ private function get_gateway_dashboard_link() { // phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh /** * Allow to modify a single payment page gateway dashboard link. * * @since 1.8.2 * * @param string $link Gateway dashboard link. * @param object $payment Payment object. */ $link = apply_filters( 'wpforms_admin_payments_views_single_gateway_dashboard_link', '', $this->payment ); if ( $link ) { return $link; } $is_test_mode = $this->payment->mode === 'test'; // Backward compatibility until all addons has been updated. switch ( $this->payment->gateway ) { case 'stripe': $link = $is_test_mode ? 'https://dashboard.stripe.com/test/' : 'https://dashboard.stripe.com/'; break; case 'paypal_standard': case 'paypal_commerce': $link = $is_test_mode ? 'https://www.sandbox.paypal.com/myaccount/summary/' : 'https://www.paypal.com/myaccount/summary/'; break; case 'authorize_net': $link = $is_test_mode ? 'https://sandbox.authorize.net/' : 'https://account.authorize.net/'; break; case 'square': $link = $is_test_mode ? 'https://squareupsandbox.com/dashboard/' : 'https://squareup.com/t/cmtp_performance/pr_developers/d_partnerships/p_0010L00001tJz7nQAC/?route=dashboard/'; break; default: $link = ''; break; } return $link; } /** * Get gateway action link. * * @since 1.8.2 * * @param string $action Action. * * @return string */ private function get_gateway_action_link( $action ) { /** * Allow to modify a single payment page gateway action link. * * @since 1.8.2 * * @param string $link Gateway action link. * @param string $action Action to perform. * @param object $payment Payment object. */ $link = apply_filters( 'wpforms_admin_payments_views_single_gateway_action_link', '', $action, $this->payment ); if ( $link ) { return $link; } // Backward compatibility until all addons has been updated. if ( $action === 'refund' ) { return $this->get_gateway_transaction_link(); } return $this->get_gateway_subscription_link(); } /** * Retrieve a readable payment gateway name. * * @since 1.8.2 * * @return string */ private function get_gateway_name() { $gateway_name = Helpers::get_placeholder_na_text( false ); if ( isset( $this->payment->gateway ) && ValueValidator::is_valid( $this->payment->gateway, 'gateway' ) ) { $gateway_name = ValueValidator::get_allowed_gateways()[ $this->payment->gateway ]; } return $gateway_name; } /** * Retrieve a readable payment status label. * * @since 1.8.4 * * @return string */ private function get_status_label() { $label = ValueValidator::get_allowed_one_time_statuses()[ $this->payment->status ]; if ( $this->payment->status !== 'partrefund' ) { return $label; } $refunded_amount = isset( $this->payment_meta['refunded_amount']->value ) ? wpforms_sanitize_amount( $this->payment_meta['refunded_amount']->value, $this->payment->currency ) : 0; $label .= ' <span>('; $label .= wpforms_format_amount( $refunded_amount, true, $this->payment->currency ); $label .= ')</span>'; return $label; } /** * If the form is still available, return a link to edit it. * Otherwise, return an empty string. * * @since 1.8.4 * * @return string */ private function get_form_edit_link() { // Leave early if no form ID is found. if ( ! $this->payment->form_id ) { return ''; } $form = wpforms()->obj( 'form' )->get( $this->payment->form_id ); // Leave early if form is no longer available. if ( ! $form || $form->post_status !== 'publish' ) { return ''; } $name = ! empty( $form->post_title ) ? $form->post_title : $form->post_name; $url = add_query_arg( [ 'view' => 'fields', 'page' => 'wpforms-builder', 'form_id' => $this->payment->form_id, ], admin_url( 'admin.php' ) ); return sprintf( '<a href="%1$s" class="wpforms-link">%2$s</a>', esc_url( $url ), wp_kses_post( $name ) ); } } Admin/Payments/ScreenOptions.php 0000644 00000011337 15174710275 0012716 0 ustar 00 <?php namespace WPForms\Admin\Payments; use WP_Screen; /** * Payments screen options. * * @since 1.8.2 */ class ScreenOptions { /** * Screen id. * * @since 1.8.2 */ const SCREEN_ID = 'wpforms_page_wpforms-payments'; /** * Screen option name. * * @since 1.8.2 */ const PER_PAGE = 'wpforms_payments_per_page'; /** * Screen option name. * * @since 1.8.2 */ const SINGLE = 'wpforms_payments_single'; /** * Initialize. * * @since 1.8.2 */ public function init() { $this->hooks(); } /** * Hooks. * * @since 1.8.2 */ private function hooks() { // Setup screen options - this needs to run early. add_action( 'load-wpforms_page_wpforms-payments', [ $this, 'screen_options' ] ); add_filter( 'screen_settings', [ $this, 'single_screen_settings' ], 10, 2 ); add_filter( 'set-screen-option', [ $this, 'screen_options_set' ], 10, 3 ); add_filter( 'set_screen_option_wpforms_payments_per_page', [ $this, 'screen_options_set' ], 10, 3 ); add_filter( 'set_screen_option_wpforms_payments_single', [ $this, 'screen_options_set' ], 10, 3 ); } /** * Add per-page screen option to the Payments table. * * @since 1.8.2 */ public function screen_options() { $screen = get_current_screen(); if ( ! isset( $screen->id ) || $screen->id !== self::SCREEN_ID ) { return; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( ! empty( $_GET['view'] ) && $_GET['view'] !== 'payments' ) { return; } /** * Filter the number of payments per page default value. * * Notice, the filter will be applied to default value in Screen Options only and still will be able to provide other value. * If you want to change the number of payments per page, use the `wpforms_payments_per_page` filter. * * @since 1.8.2 * * @param int $per_page Number of payments per page. */ $per_page = (int) apply_filters( 'wpforms_admin_payments_screen_options_per_page_default', 20 ); add_screen_option( 'per_page', [ 'label' => esc_html__( 'Number of payments per page:', 'wpforms-lite' ), 'option' => self::PER_PAGE, 'default' => $per_page, ] ); } /** * Returns the screen options markup for the payment single page. * * @since 1.8.2 * * @param string $status The current screen settings. * @param WP_Screen $args WP_Screen object. * * @return string */ public function single_screen_settings( $status, $args ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( $args->id !== self::SCREEN_ID || empty( $_GET['view'] ) || $_GET['view'] !== 'payment' ) { return $status; } $screen_options = self::get_single_page_options(); $advanced_options = [ 'advanced' => __( 'Advanced details', 'wpforms-lite' ), 'log' => __( 'Log', 'wpforms-lite' ), ]; $output = '<fieldset class="metabox-prefs">'; $output .= '<legend>' . esc_html__( 'Additional information', 'wpforms-lite' ) . '</legend>'; $output .= '<div>'; foreach ( $advanced_options as $key => $label ) { $output .= sprintf( '<input name="%1$s" type="checkbox" id="%1$s" value="true" %2$s /><label for="%1$s">%3$s</label>', esc_attr( $key ), ! empty( $screen_options[ $key ] ) ? 'checked="checked"' : '', esc_html( $label ) ); } $output .= '</div></fieldset>'; $output .= '<p class="submit">'; $output .= '<input type="hidden" name="wp_screen_options[option]" value="wpforms_payments_single">'; $output .= '<input type="hidden" name="wp_screen_options[value]" value="true">'; $output .= '<input type="submit" name="screen-options-apply" id="screen-options-apply" class="button button-primary" value="' . esc_html__( 'Apply', 'wpforms-lite' ) . '">'; $output .= wp_nonce_field( 'screen-options-nonce', 'screenoptionnonce', false, false ); $output .= '</p>'; return $output; } /** * Get single page screen options. * * @since 1.8.2 * * @return false|mixed */ public static function get_single_page_options() { return get_user_option( self::SINGLE ); } /** * Payments table per-page screen option value. * * @since 1.8.2 * * @param mixed $status The value to save instead of the option value. * @param string $option Screen option name. * @param mixed $value Screen option value. * * @return mixed */ public function screen_options_set( $status, $option, $value ) { if ( $option === self::PER_PAGE ) { return $value; } // phpcs:disable WordPress.Security.NonceVerification.Missing if ( $option === self::SINGLE ) { return [ 'advanced' => isset( $_POST['advanced'] ) && (bool) $_POST['advanced'], 'log' => isset( $_POST['log'] ) && (bool) $_POST['log'], ]; } // phpcs:enable WordPress.Security.NonceVerification.Missing return $status; } } Admin/Payments/Payments.php 0000644 00000014063 15174710275 0011722 0 ustar 00 <?php namespace WPForms\Admin\Payments; use WPForms\Admin\Payments\Views\Coupons\Education; use WPForms\Admin\Payments\Views\Overview\BulkActions; use WPForms\Admin\Payments\Views\Single; use WPForms\Admin\Payments\Views\Overview\Page; use WPForms\Admin\Payments\Views\Overview\Coupon; use WPForms\Admin\Payments\Views\Overview\Filters; use WPForms\Admin\Payments\Views\Overview\Search; /** * Payments class. * * @since 1.8.2 */ class Payments { /** * Payments page slug. * * @since 1.8.2 * * @var string */ const SLUG = 'wpforms-payments'; /** * Available views (pages). * * @since 1.8.2 * * @var array */ private $views = []; /** * The current page slug. * * @since 1.8.2 * * @var string */ private $active_view_slug; /** * The current page view. * * @since 1.8.2 * * @var null|\WPForms\Admin\Payments\Views\PaymentsViewsInterface */ private $view; /** * Initialize class. * * @since 1.8.2 */ public function init() { if ( ! wpforms_is_admin_page( 'payments' ) ) { return; } $this->update_request_uri(); ( new ScreenOptions() )->init(); ( new Coupon() )->init(); ( new Filters() )->init(); ( new Search() )->init(); ( new BulkActions() )->init(); $this->hooks(); } /** * Initialize the active view. * * @since 1.8.2 */ public function init_view() { $view_ids = array_keys( $this->get_views() ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended $this->active_view_slug = isset( $_GET['view'] ) ? sanitize_key( $_GET['view'] ) : 'payments'; // If the user tries to load an invalid view - fallback to the first available. if ( ! in_array( $this->active_view_slug, $view_ids, true ) ) { $this->active_view_slug = reset( $view_ids ); } if ( ! isset( $this->views[ $this->active_view_slug ] ) ) { return; } $this->view = $this->views[ $this->active_view_slug ]; $this->view->init(); } /** * Get available views. * * @since 1.8.2 * * @return array */ private function get_views() { if ( ! empty( $this->views ) ) { return $this->views; } $views = [ 'coupons' => new Education(), ]; /** * Allow to extend payment views. * * @since 1.8.2 * * @param array $views Array of views where key is slug. */ $this->views = (array) apply_filters( 'wpforms_admin_payments_payments_get_views', $views ); $this->views['payments'] = new Page(); $this->views['payment'] = new Single(); // Payments view should be the first one. $this->views = array_merge( [ 'payments' => $this->views['payments'] ], $this->views ); $this->views = array_filter( $this->views, static function ( $view ) { return $view->current_user_can(); } ); return $this->views; } /** * Register hooks. * * @since 1.8.2 */ private function hooks() { add_action( 'wpforms_admin_page', [ $this, 'output' ] ); add_action( 'current_screen', [ $this, 'init_view' ] ); add_filter( 'wpforms_db_payments_payment_add_secondary_where_conditions_args', [ $this, 'modify_secondary_where_conditions_args' ] ); } /** * Output the page. * * @since 1.8.2 */ public function output() { if ( empty( $this->view ) ) { return; } ?> <div id="wpforms-payments" class="wrap wpforms-admin-wrap wpforms-payments-wrap wpforms-payments-wrap-<?php echo esc_attr( $this->active_view_slug ); ?>"> <h1 class="page-title"> <?php esc_html_e( 'Payments', 'wpforms-lite' ); ?> <?php $this->view->heading(); ?> </h1> <?php if ( ! empty( $this->view->get_tab_label() ) ) : ?> <div class="wpforms-tabs-wrapper"> <?php $this->display_tabs(); ?> </div> <?php endif; ?> <div class="wpforms-admin-content wpforms-admin-settings"> <?php $this->view->display(); ?> </div> </div> <?php } /** * Display tabs. * * @since 1.8.2.2 */ private function display_tabs() { $views = $this->get_views(); // Remove views that should not be displayed. $views = array_filter( $views, static function ( $view ) { return ! empty( $view->get_tab_label() ); } ); // If there is only one view - no need to display tabs. if ( count( $views ) === 1 ) { return; } ?> <nav class="nav-tab-wrapper"> <?php foreach ( $views as $slug => $view ) : ?> <a href="<?php echo esc_url( $this->get_tab_url( $slug ) ); ?>" class="nav-tab <?php echo $slug === $this->active_view_slug ? 'nav-tab-active' : ''; ?>"> <?php echo esc_html( $view->get_tab_label() ); ?> </a> <?php endforeach; ?> </nav> <?php } /** * Get tab URL. * * @since 1.8.2.2 * * @param string $tab Tab slug. * * @return string */ private function get_tab_url( $tab ) { return add_query_arg( [ 'page' => self::SLUG, 'view' => $tab, ], admin_url( 'admin.php' ) ); } /** * Modify arguments of secondary where clauses. * * @since 1.8.2 * * @param array $args Query arguments. * * @return array */ public function modify_secondary_where_conditions_args( $args ) { // Set a current mode. if ( ! isset( $args['mode'] ) ) { $args['mode'] = Page::get_mode(); } return $args; } /** * Update view param in request URI. * * Backward compatibility for old URLs. * * @since 1.8.4 */ private function update_request_uri() { // phpcs:disable WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash if ( ! isset( $_GET['view'], $_SERVER['REQUEST_URI'] ) ) { return; } $old_new = [ 'single' => 'payment', 'overview' => 'payments', ]; if ( ! array_key_exists( $_GET['view'], $old_new ) || in_array( $_GET['view'], $old_new, true ) ) { return; } wp_safe_redirect( str_replace( 'view=' . $_GET['view'], 'view=' . $old_new[ $_GET['view'] ], esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) ) ); // phpcs:enable WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash exit; } } Admin/SiteHealth.php 0000644 00000005425 15174710275 0010356 0 ustar 00 <?php namespace WPForms\Admin; /** * Site Health WPForms Info. * * @since 1.5.5 */ class SiteHealth { /** * Init Site Health. * * @since 1.5.5 */ final public function init() { $this->hooks(); } /** * Integration hooks. * * @since 1.5.5 */ protected function hooks() { add_filter( 'debug_information', [ $this, 'add_info_section' ] ); } /** * Add WPForms section to Info tab. * * @since 1.5.5 * * @param array $debug_info Array of all information. * * @return array Array with added WPForms info section. */ public function add_info_section( $debug_info ) { $wpforms = [ 'label' => 'WPForms', 'fields' => [ 'version' => [ 'label' => esc_html__( 'Version', 'wpforms-lite' ), 'value' => WPFORMS_VERSION, ], ], ]; // Install date. $activated = get_option( 'wpforms_activated', [] ); if ( ! empty( $activated['lite'] ) ) { $wpforms['fields']['lite'] = [ 'label' => esc_html__( 'Lite install date', 'wpforms-lite' ), 'value' => wpforms_datetime_format( $activated['lite'], '', true ), ]; } if ( ! empty( $activated['pro'] ) ) { $wpforms['fields']['pro'] = [ 'label' => esc_html__( 'Pro install date', 'wpforms-lite' ), 'value' => wpforms_datetime_format( $activated['pro'], '', true ), ]; } // Permissions for the upload directory. $upload_dir = wpforms_upload_dir(); $wpforms['fields']['upload_dir'] = [ 'label' => esc_html__( 'Uploads directory', 'wpforms-lite' ), 'value' => empty( $upload_dir['error'] ) && ! empty( $upload_dir['path'] ) && wp_is_writable( $upload_dir['path'] ) ? esc_html__( 'Writable', 'wpforms-lite' ) : esc_html__( 'Not writable', 'wpforms-lite' ), ]; // DB tables. $db_tables = wpforms()->get_existing_custom_tables(); if ( $db_tables ) { $db_tables_str = empty( $db_tables ) ? esc_html__( 'Not found', 'wpforms-lite' ) : implode( ', ', $db_tables ); $wpforms['fields']['db_tables'] = [ 'label' => esc_html__( 'DB tables', 'wpforms-lite' ), 'value' => $db_tables_str, 'private' => true, ]; } // Total forms. $wpforms['fields']['total_forms'] = [ 'label' => esc_html__( 'Total forms', 'wpforms-lite' ), 'value' => wp_count_posts( 'wpforms' )->publish, ]; if ( ! wpforms()->is_pro() ) { $forms = wpforms()->obj( 'form' )->get( '', [ 'fields' => 'ids' ] ); if ( empty( $forms ) || ! is_array( $forms ) ) { $forms = []; } $count = 0; foreach ( $forms as $form_id ) { $count += (int) get_post_meta( $form_id, 'wpforms_entries_count', true ); } $wpforms['fields']['total_submissions'] = [ 'label' => esc_html__( 'Total submissions (since v1.5.0)', 'wpforms-lite' ), 'value' => $count, ]; } $debug_info['wpforms'] = $wpforms; return $debug_info; } } Admin/AdminBarMenu.php 0000644 00000043674 15174710275 0010636 0 ustar 00 <?php namespace WPForms\Admin; use WP_Admin_Bar; /** * WPForms admin bar menu. * * @since 1.6.0 */ class AdminBarMenu { /** * Initialize class. * * @since 1.6.0 */ public function init() { if ( ! $this->has_access() ) { return; } $this->hooks(); } /** * Register hooks. * * @since 1.6.0 */ public function hooks() { add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_css' ] ); add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_css' ] ); add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_js' ] ); add_action( 'admin_bar_menu', [ $this, 'register' ], 999 ); add_action( 'wpforms_wp_footer_end', [ $this, 'menu_forms_data_html' ] ); } /** * Determine whether the current user has access to see the admin bar menu. * * @since 1.6.0 * * @return bool */ public function has_access(): bool { $access = false; if ( is_admin_bar_showing() && wpforms_current_user_can() && ! wpforms_setting( 'hide-admin-bar', false ) ) { $access = true; } /** * Filters whether the current user has access to see the admin bar menu. * * @since 1.6.0 * * @param bool $access Whether the current user has access to see the admin bar menu. */ return (bool) apply_filters( 'wpforms_admin_adminbarmenu_has_access', $access ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName } /** * Determine whether new notifications are available. * * @since 1.6.0 * * @return bool */ public function has_notifications() { return wpforms()->obj( 'notifications' )->get_count(); } /** * Enqueue CSS styles. * * @since 1.6.0 */ public function enqueue_css() { $min = wpforms_get_min_suffix(); wp_enqueue_style( 'wpforms-admin-bar', WPFORMS_PLUGIN_URL . "assets/css/admin-bar{$min}.css", [], WPFORMS_VERSION ); // Apply WordPress pre/post 5.7 accent color, only when admin bar is displayed on the frontend or we're // inside the Form Builder - it does not load some WP core admin styles, including themes. if ( wpforms_is_admin_page( 'builder' ) || ! is_admin() ) { wp_add_inline_style( 'wpforms-admin-bar', sprintf( '#wpadminbar .wpforms-menu-notification-counter, #wpadminbar .wpforms-menu-notification-indicator { background-color: %s !important; color: #ffffff !important; }', version_compare( get_bloginfo( 'version' ), '5.7', '<' ) ? '#ca4a1f' : '#d63638' ) ); } } /** * Enqueue JavaScript files. * * @since 1.6.5 */ public function enqueue_js() { wp_add_inline_script( 'admin-bar', "( function() { function wpforms_admin_bar_menu_init() { var template = document.getElementById( 'tmpl-wpforms-admin-menubar-data' ), notifications = document.getElementById( 'wp-admin-bar-wpforms-notifications' ); if ( ! template ) { return; } if ( ! notifications ) { var menu = document.getElementById( 'wp-admin-bar-wpforms-menu-default' ); if ( ! menu ) { return; } menu.insertAdjacentHTML( 'afterBegin', template.innerHTML ); } else { notifications.insertAdjacentHTML( 'afterend', template.innerHTML ); } }; document.addEventListener( 'DOMContentLoaded', wpforms_admin_bar_menu_init ); }() );", 'before' ); } /** * Register and render admin bar menu items. * * @since 1.6.0 * * @param WP_Admin_Bar $wp_admin_bar WordPress Admin Bar object. */ public function register( WP_Admin_Bar $wp_admin_bar ) { $items = (array) apply_filters( 'wpforms_admin_adminbarmenu_register', [ 'main_menu', 'notification_menu', 'all_forms_menu', 'add_new_menu', 'all_payments_menu', 'settings_menu', 'tools_menu', 'community_menu', 'support_menu', ], $wp_admin_bar ); foreach ( $items as $item ) { $this->{ $item }( $wp_admin_bar ); do_action( "wpforms_admin_adminbarmenu_register_{$item}_after", $wp_admin_bar ); } $this->register_settings_submenu( $wp_admin_bar ); $this->register_tools_submenu( $wp_admin_bar ); } /** * Register Settings submenu. * * @since 1.9.2 * * @param WP_Admin_Bar $wp_admin_bar WordPress Admin Bar object. */ private function register_settings_submenu( WP_Admin_Bar $wp_admin_bar ) { /** * Filters the Settings submenu items. * * @since 1.9.2 * * @param array $items Array of submenu items. * @param WP_Admin_Bar $wp_admin_bar WordPress Admin Bar object. */ $items = (array) apply_filters( 'wpforms_admin_bar_menu_register_settings_submenu', [ 'wpforms-general-settings' => [ 'title' => __( 'General', 'wpforms-lite' ), 'path' => 'admin.php?page=wpforms-settings&view=general', ], 'wpforms-email-settings' => [ 'title' => __( 'Email', 'wpforms-lite' ), 'path' => 'admin.php?page=wpforms-settings&view=email', ], 'wpforms-captcha-settings' => [ 'title' => __( 'CAPTCHA', 'wpforms-lite' ), 'path' => 'admin.php?page=wpforms-settings&view=captcha', ], 'wpforms-validation-settings' => [ 'title' => __( 'Validation', 'wpforms-lite' ), 'path' => 'admin.php?page=wpforms-settings&view=validation', ], 'wpforms-payments-settings' => [ 'title' => __( 'Payments', 'wpforms-lite' ), 'path' => 'admin.php?page=wpforms-settings&view=payments', ], 'wpforms-integrations-settings' => [ 'title' => __( 'Integrations', 'wpforms-lite' ), 'path' => 'admin.php?page=wpforms-settings&view=integrations', ], 'wpforms-geolocation-settings' => [ 'title' => __( 'Geolocation', 'wpforms-lite' ), 'path' => 'admin.php?page=wpforms-settings&view=geolocation', ], 'wpforms-access-settings' => [ 'title' => __( 'Access Control', 'wpforms-lite' ), 'path' => 'admin.php?page=wpforms-settings&view=access', ], 'wpforms-misc-settings' => [ 'title' => __( 'Misc', 'wpforms-lite' ), 'path' => 'admin.php?page=wpforms-settings&view=misc', ], ], $wp_admin_bar ); foreach ( $items as $item_id => $args ) { $wp_admin_bar->add_menu( [ 'parent' => 'wpforms-settings', 'id' => sanitize_key( $item_id ), 'title' => esc_html( $args['title'] ), 'href' => admin_url( $args['path'] ), ] ); /** * Fires after the Settings submenu item is registered. * * @since 1.9.2 * * @param WP_Admin_Bar $wp_admin_bar WordPress Admin Bar object. */ do_action( "wpforms_admin_bar_menu_register_settings_submenu_{$item_id}_after", $wp_admin_bar ); } } /** * Register Tools submenu. * * @since 1.9.3 * * @param WP_Admin_Bar $wp_admin_bar WordPress Admin Bar object. */ private function register_tools_submenu( WP_Admin_Bar $wp_admin_bar ) { /** * Filters the Tools submenu items. * * @since 1.9.3 * * @param array $items Array of submenu items. * @param WP_Admin_Bar $wp_admin_bar WordPress Admin Bar object. * * @return array */ $items = (array) apply_filters( 'wpforms_admin_bar_menu_register_tools_submenu', [ 'wpforms-tools-import' => [ 'title' => esc_html__( 'Import', 'wpforms-lite' ), 'path' => 'admin.php?page=wpforms-tools&view=import', ], 'wpforms-tools-export' => [ 'title' => esc_html__( 'Export', 'wpforms-lite' ), 'path' => 'admin.php?page=wpforms-tools&view=export', ], 'wpforms-tools-entry-automation' => [ 'title' => esc_html__( 'Entry Automation', 'wpforms-lite' ), 'path' => 'admin.php?page=wpforms-tools&view=entry-automation', ], 'wpforms-tools-system' => [ 'title' => esc_html__( 'System Info', 'wpforms-lite' ), 'path' => 'admin.php?page=wpforms-tools&view=system', ], 'wpforms-tools-action-scheduler' => [ 'title' => esc_html__( 'Scheduled Actions', 'wpforms-lite' ), 'path' => 'admin.php?page=wpforms-tools&view=action-scheduler&s=wpforms', ], 'wpforms-tools-logs' => [ 'title' => esc_html__( 'Logs', 'wpforms-lite' ), 'path' => 'admin.php?page=wpforms-tools&view=logs', ], 'wpforms-tools-wpcode' => [ 'title' => esc_html__( 'Code Snippets', 'wpforms-lite' ), 'path' => 'admin.php?page=wpforms-tools&view=wpcode', ], ], $wp_admin_bar ); foreach ( $items as $item_id => $args ) { $wp_admin_bar->add_menu( [ 'parent' => 'wpforms-tools', 'id' => sanitize_key( $item_id ), 'title' => esc_html( $args['title'] ), 'href' => admin_url( $args['path'] ), ] ); /** * Fires after the Tools submenu item is registered. * * @since 1.9.2 * * @param WP_Admin_Bar $wp_admin_bar WordPress Admin Bar object. */ do_action( "wpforms_admin_bar_menu_register_tools_submenu_{$item_id}_after", $wp_admin_bar ); } $this->register_action_scheduler_submenu( $wp_admin_bar ); } /** * Register Action Scheduler submenu. * * @since 1.9.3 * * @param WP_Admin_Bar $wp_admin_bar WordPress Admin Bar object. */ private function register_action_scheduler_submenu( WP_Admin_Bar $wp_admin_bar ) { /** * Filters the Action Scheduler submenu items. * * @since 1.9.3 * * @param array $items Array of submenu items. * @param WP_Admin_Bar $wp_admin_bar WordPress Admin Bar object. * * @return array */ $items = apply_filters( 'wpforms_admin_bar_menu_register_action_scheduler_submenu', [ 'wpforms-tools-action-scheduler-all' => [ 'title' => esc_html__( 'View All', 'wpforms-lite' ), 'path' => 'admin.php?page=wpforms-tools&view=action-scheduler&s=wpforms&orderby=hook&order=desc', ], 'wpforms-tools-action-scheduler-complete' => [ 'title' => esc_html__( 'Completed Actions', 'wpforms-lite' ), 'path' => 'admin.php?page=wpforms-tools&view=action-scheduler&s=wpforms&status=complete&orderby=hook&order=desc', ], 'wpforms-tools-action-scheduler-failed' => [ 'title' => esc_html__( 'Failed Actions', 'wpforms-lite' ), 'path' => 'admin.php?page=wpforms-tools&view=action-scheduler&s=wpforms&status=failed&orderby=hook&order=desc', ], 'wpforms-tools-action-scheduler-pending' => [ 'title' => esc_html__( 'Pending Actions', 'wpforms-lite' ), 'path' => 'admin.php?page=wpforms-tools&view=action-scheduler&s=wpforms&status=pending&orderby=hook&order=desc', ], 'wpforms-tools-action-scheduler-past-due' => [ 'title' => esc_html__( 'Past Due Actions', 'wpforms-lite' ), 'path' => 'admin.php?page=wpforms-tools&view=action-scheduler&s=wpforms&status=past-due&orderby=hook&order=desc', ], ], $wp_admin_bar ); foreach ( $items as $item_id => $args ) { $wp_admin_bar->add_menu( [ 'parent' => 'wpforms-tools-action-scheduler', 'id' => sanitize_key( $item_id ), 'title' => esc_html( $args['title'] ), 'href' => admin_url( $args['path'] ), ] ); /** * Fires after the Action Scheduler submenu item is registered. * * @since 1.9.3 * * @param WP_Admin_Bar $wp_admin_bar WordPress Admin Bar object. */ do_action( "wpforms_admin_bar_menu_register_action_scheduler_submenu_{$item_id}_after", $wp_admin_bar ); } } /** * Render primary top-level admin bar menu item. * * @since 1.6.0 * * @param WP_Admin_Bar $wp_admin_bar WordPress Admin Bar object. */ public function main_menu( WP_Admin_Bar $wp_admin_bar ) { $indicator = ''; $notifications = $this->has_notifications(); if ( $notifications ) { $count = $notifications < 10 ? $notifications : '!'; $indicator = ' <div class="wp-core-ui wp-ui-notification wpforms-menu-notification-counter">' . $count . '</div>'; } $wp_admin_bar->add_menu( [ 'id' => 'wpforms-menu', 'title' => 'WPForms' . $indicator, 'href' => admin_url( 'admin.php?page=wpforms-overview' ), ] ); } /** * Render Notifications admin bar menu item. * * @since 1.6.0 * * @param WP_Admin_Bar $wp_admin_bar WordPress Admin Bar object. */ public function notification_menu( WP_Admin_Bar $wp_admin_bar ) { if ( ! $this->has_notifications() ) { return; } $wp_admin_bar->add_menu( [ 'parent' => 'wpforms-menu', 'id' => 'wpforms-notifications', 'title' => esc_html__( 'Notifications', 'wpforms-lite' ) . ' <div class="wp-core-ui wp-ui-notification wpforms-menu-notification-indicator"></div>', 'href' => admin_url( 'admin.php?page=wpforms-overview' ), ] ); } /** * Render All Forms admin bar menu item. * * @since 1.6.0 * * @param WP_Admin_Bar $wp_admin_bar WordPress Admin Bar object. */ public function all_forms_menu( WP_Admin_Bar $wp_admin_bar ) { $wp_admin_bar->add_menu( [ 'parent' => 'wpforms-menu', 'id' => 'wpforms-forms', 'title' => esc_html__( 'All Forms', 'wpforms-lite' ), 'href' => admin_url( 'admin.php?page=wpforms-overview' ), ] ); } /** * Render All Payments admin bar menu item. * * @since 1.8.4 * * @param WP_Admin_Bar $wp_admin_bar WordPress Admin Bar object. */ public function all_payments_menu( WP_Admin_Bar $wp_admin_bar ) { $wp_admin_bar->add_menu( [ 'parent' => 'wpforms-menu', 'id' => 'wpforms-payments', 'title' => esc_html__( 'Payments', 'wpforms-lite' ), 'href' => add_query_arg( [ 'page' => 'wpforms-payments', ], admin_url( 'admin.php' ) ), ] ); } /** * Render Add New admin bar menu item. * * @since 1.6.0 * * @param WP_Admin_Bar $wp_admin_bar WordPress Admin Bar object. */ public function add_new_menu( WP_Admin_Bar $wp_admin_bar ) { $wp_admin_bar->add_menu( [ 'parent' => 'wpforms-menu', 'id' => 'wpforms-add-new', 'title' => esc_html__( 'Add New Form', 'wpforms-lite' ), 'href' => admin_url( 'admin.php?page=wpforms-builder' ), ] ); } /** * Render Settings admin bar menu item. * * @since 1.9.2 * * @param WP_Admin_Bar $wp_admin_bar WordPress Admin Bar object. */ public function settings_menu( WP_Admin_Bar $wp_admin_bar ) { $wp_admin_bar->add_menu( [ 'parent' => 'wpforms-menu', 'id' => 'wpforms-settings', 'title' => esc_html__( 'Settings', 'wpforms-lite' ), 'href' => admin_url( 'admin.php?page=wpforms-settings' ), ] ); } /** * Add Tools menu to the admin bar. * * @since 1.9.3 * * @param WP_Admin_Bar $wp_admin_bar The admin bar object. */ public function tools_menu( WP_Admin_Bar $wp_admin_bar ) { $wp_admin_bar->add_menu( [ 'parent' => 'wpforms-menu', 'id' => 'wpforms-tools', 'title' => esc_html__( 'Tools', 'wpforms-lite' ), 'href' => admin_url( 'admin.php?page=wpforms-tools' ), ] ); } /** * Render Community admin bar menu item. * * @since 1.6.0 * * @param WP_Admin_Bar $wp_admin_bar WordPress Admin Bar object. */ public function community_menu( WP_Admin_Bar $wp_admin_bar ) { $wp_admin_bar->add_menu( [ 'parent' => 'wpforms-menu', 'id' => 'wpforms-community', 'title' => esc_html__( 'Community', 'wpforms-lite' ), 'href' => 'https://www.facebook.com/groups/wpformsvip/', 'meta' => [ 'target' => '_blank', 'rel' => 'noopener noreferrer', ], ] ); } /** * Render Support admin bar menu item. * * @since 1.6.0 * @since 1.7.4 Update the `Support` item title to `Help Docs`. * * @param WP_Admin_Bar $wp_admin_bar WordPress Admin Bar object. */ public function support_menu( WP_Admin_Bar $wp_admin_bar ) { $href = add_query_arg( [ 'utm_campaign' => wpforms()->is_pro() ? 'plugin' : 'liteplugin', 'utm_medium' => 'admin-bar', 'utm_source' => 'WordPress', 'utm_content' => 'Documentation', ], 'https://wpforms.com/docs/' ); $wp_admin_bar->add_menu( [ 'parent' => 'wpforms-menu', 'id' => 'wpforms-help-docs', 'title' => esc_html__( 'Help Docs', 'wpforms-lite' ), 'href' => $href, 'meta' => [ 'target' => '_blank', 'rel' => 'noopener noreferrer', ], ] ); } /** * Get form data for JS to modify the admin bar menu. * * @since 1.6.5 * @since 1.8.4 Added the View Payments link. * * @param array $forms Forms array. * * @return array */ protected function get_forms_data( $forms ) { $data = [ 'has_notifications' => $this->has_notifications(), 'edit_text' => esc_html__( 'Edit Form', 'wpforms-lite' ), 'entry_text' => esc_html__( 'View Entries', 'wpforms-lite' ), 'payment_text' => esc_html__( 'View Payments', 'wpforms-lite' ), 'survey_text' => esc_html__( 'Survey Results', 'wpforms-lite' ), 'forms' => [], ]; $admin_url = admin_url( 'admin.php' ); foreach ( $forms as $form ) { $form_id = absint( $form['id'] ); if ( empty( $form_id ) ) { continue; } /* translators: %d - form ID. */ $form_title = sprintf( esc_html__( 'Form ID: %d', 'wpforms-lite' ), $form_id ); if ( ! empty( $form['settings']['form_title'] ) ) { $form_title = wp_html_excerpt( sanitize_text_field( $form['settings']['form_title'] ), 99, '…' ); } $has_payments = wpforms()->obj( 'payment' )->get_by( 'form_id', $form_id ); $data['forms'][] = apply_filters( 'wpforms_admin_adminbarmenu_get_form_data', [ 'form_id' => $form_id, 'title' => $form_title, 'edit_url' => add_query_arg( [ 'page' => 'wpforms-builder', 'view' => 'fields', 'form_id' => $form_id, ], $admin_url ), 'payments_url' => $has_payments ? add_query_arg( [ 'page' => 'wpforms-payments', 'form_id' => $form_id, ], $admin_url ) : '', ] ); } return $data; } /** * Add form(s) data to the page. * * @since 1.6.5 * * @param array $forms Forms array. */ public function menu_forms_data_html( $forms ) { if ( empty( $forms ) ) { return; } // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo wpforms_render( 'admin-bar-menu', [ 'forms_data' => $this->get_forms_data( $forms ), ], true ); } } Admin/Forms/Page.php 0000644 00000022057 15174710275 0010266 0 ustar 00 <?php namespace WPForms\Admin\Forms; use WPForms\Admin\Forms\Table\Facades\Columns; use WPForms\Admin\Traits\HasScreenOptions; /** * Primary overview page inside the admin which lists all forms. * * @since 1.8.6 */ class Page { use HasScreenOptions; /** * Overview Table instance. * * @since 1.8.6 * * @var ListTable */ private $overview_table; /** * Primary class constructor. * * @since 1.8.6 */ public function __construct() { $this->screen_options_id = 'wpforms_forms_overview_screen_options'; $this->screen_options = [ 'pagination' => [ 'heading' => esc_html__( 'Pagination', 'wpforms-lite' ), 'options' => [ [ 'label' => esc_html__( 'Number of forms per page:', 'wpforms-lite' ), 'option' => 'per_page', 'default' => wpforms()->obj( 'form' )->get_count_per_page(), 'type' => 'number', 'args' => [ 'min' => 1, 'max' => 999, 'step' => 1, 'maxlength' => 3, ], ], ], ], 'view' => [ 'heading' => esc_html__( 'View', 'wpforms-lite' ), 'options' => [ [ 'label' => esc_html__( 'Show form templates', 'wpforms-lite' ), 'option' => 'show_form_templates', 'default' => true, 'type' => 'checkbox', 'checked' => true, ], ], ], ]; $this->init_screen_options( wpforms_is_admin_page( 'overview' ) ); $this->hooks(); } /** * Hooks. * * @since 1.8.6 */ private function hooks() { // Reset columns settings. add_filter( 'manage_toplevel_page_wpforms-overview_columns', [ $this, 'screen_settings_columns' ] ); // Rewrite forms per page value from Form Overview page screen options. add_filter( 'wpforms_forms_per_page', [ $this, 'get_wpforms_forms_per_page' ] ); } /** * Check if the template visibility option is enabled. * * @since 1.8.8 * * @return bool */ public function overview_show_form_templates() { return get_user_option( $this->screen_options_id . '_view_show_form_templates' ); } /** * Get forms per page value from Form Overview page screen options. * * @since 1.8.8 * * @return int */ public function get_wpforms_forms_per_page() { return get_user_option( $this->screen_options_id . '_pagination_per_page' ); } /** * Determine if the user is viewing the overview page, if so, party on. * * @since 1.8.6 */ public function init() { // phpcs:ignore WPForms.PHP.HooksMethod.InvalidPlaceForAddingHooks // Only load if we are actually on the overview page. if ( ! wpforms_is_admin_page( 'overview' ) ) { return; } // Avoid recursively include _wp_http_referer in the REQUEST_URI. $this->remove_referer(); add_action( 'current_screen', [ $this, 'init_overview_table' ], 5 ); add_action( 'admin_enqueue_scripts', [ $this, 'enqueues' ] ); add_action( 'wpforms_admin_page', [ $this, 'output' ] ); add_action( 'wpforms_admin_page', [ $this, 'field_column_setting' ] ); /** * Fires after the form overview page initialization. * * @since 1.0.0 */ do_action( 'wpforms_overview_init' ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName } /** * Init overview table class. * * @since 1.8.6 */ public function init_overview_table() { $this->overview_table = ListTable::get_instance(); } /** * Remove previous `_wp_http_referer` variable from the REQUEST_URI. * * @since 1.8.6 */ private function remove_referer() { if ( isset( $_SERVER['REQUEST_URI'] ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $_SERVER['REQUEST_URI'] = remove_query_arg( '_wp_http_referer', wp_unslash( $_SERVER['REQUEST_URI'] ) ); } } /** * Add per-page screen option to the Forms table. * * @since 1.8.6 * * @depecated 1.8.8 Use HasScreenOptions trait instead. */ public function screen_options() { _deprecated_function( __METHOD__, '1.8.8 of the WPForms plugin' ); } /** * Filter screen settings columns data. * * @since 1.8.6 * * @param array $columns Columns. * * @return array * @noinspection PhpMissingParamTypeInspection */ public function screen_settings_columns( $columns ): array { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found return []; } /** * Enqueue assets for the overview page. * * @since 1.8.6 */ public function enqueues() { $min = wpforms_get_min_suffix(); wp_enqueue_script( 'wpforms-htmx', WPFORMS_PLUGIN_URL . 'assets/lib/htmx.min.js', [], WPFORMS_VERSION, true ); wp_enqueue_script( 'wpforms-admin-forms-overview', WPFORMS_PLUGIN_URL . "assets/js/admin/forms/overview{$min}.js", [ 'jquery', 'underscore', 'wpforms-htmx' ], WPFORMS_VERSION, true ); wp_enqueue_style( 'wpforms-admin-list-table-ext', WPFORMS_PLUGIN_URL . "assets/css/admin-list-table-ext{$min}.css", [], WPFORMS_VERSION ); wp_enqueue_script( 'wpforms-admin-list-table-ext', WPFORMS_PLUGIN_URL . "assets/js/admin/share/list-table-ext{$min}.js", [ 'jquery', 'jquery-ui-sortable', 'underscore', 'wpforms-admin', 'wpforms-multiselect-checkboxes' ], WPFORMS_VERSION, true ); /** * Fires after enqueue the forms overview page assets. * * @since 1.0.0 */ do_action( 'wpforms_overview_enqueue' ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName } /** * Determine if it is an empty state. * * @since 1.8.6 */ private function is_empty_state() { // phpcs:disable WordPress.Security.NonceVerification.Recommended return empty( $this->overview_table->items ) && ! isset( $_GET['search']['term'] ) && ! isset( $_GET['status'] ) && ! isset( $_GET['tags'] ) && array_sum( wpforms()->obj( 'forms_views' )->get_count() ) === 0; // phpcs:enable WordPress.Security.NonceVerification.Recommended } /** * Build the output for the overview page. * * @since 1.8.6 */ public function output() { ?> <div id="wpforms-overview" class="wrap wpforms-admin-wrap"> <h1 class="page-title"> <?php esc_html_e( 'Forms Overview', 'wpforms-lite' ); ?> <?php if ( wpforms_current_user_can( 'create_forms' ) ) : ?> <a href="<?php echo esc_url( admin_url( 'admin.php?page=wpforms-builder&view=setup' ) ); ?>" class="page-title-action wpforms-btn add-new-h2 wpforms-btn-orange" data-action="add"> <svg viewBox="0 0 14 14" class="page-title-action-icon"> <path d="M14 5.385v3.23H8.615V14h-3.23V8.615H0v-3.23h5.385V0h3.23v5.385H14Z"/> </svg> <span class="page-title-action-text"><?php esc_html_e( 'Add New', 'wpforms-lite' ); ?></span> </a> <?php endif; ?> </h1> <div class="wpforms-admin-content"> <?php $this->overview_table->prepare_items(); /** * Fires before forms overview list table output. * * @since 1.6.0.1 */ do_action( 'wpforms_admin_overview_before_table' ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName if ( $this->is_empty_state() ) { // Output no forms screen. echo wpforms_render( 'admin/empty-states/no-forms' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } else { ?> <form id="wpforms-overview-table" method="get" action="<?php echo esc_url( admin_url( 'admin.php?page=wpforms-overview' ) ); ?>"> <input type="hidden" name="post_type" value="wpforms" /> <input type="hidden" name="page" value="wpforms-overview" /> <?php $this->overview_table->search_box( esc_html__( 'Search Forms', 'wpforms-lite' ), 'wpforms-overview-search' ); $this->overview_table->views(); $this->overview_table->display(); ?> </form> <?php } ?> </div> </div> <?php } /** * Settings for field column personalization. * * @since 1.8.6 */ public function field_column_setting() { // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo $this->get_columns_multiselect(); } /** * Get columns multiselect menu. * * @since 1.8.6 * * @return string HTML menu markup. */ private function get_columns_multiselect(): string { $columns = Columns::get_columns(); $selected_keys = Columns::get_selected_columns_keys(); $options = ''; $html = ' <div id="wpforms-list-table-ext-edit-columns-select-container" class="wpforms-hidden wpforms-forms-overview-page"> <form method="post" action=""> <input type="hidden" name="action" value="wpforms_admin_forms_overview_save_columns_order"/> <select name="fields[]" id="wpforms-forms-table-edit-columns-select" class="wpforms-forms-table-edit-columns-select wpforms-list-table-ext-edit-columns-select" multiple="multiple"> <optgroup label="' . esc_html__( 'Columns', 'wpforms-lite' ) . '"> %s </optgroup> </select> </form> </div> '; foreach ( $columns as $column ) { $selected = in_array( $column->get_id(), $selected_keys, true ) ? 'selected' : ''; $disabled = $column->is_readonly() ? 'disabled="true"' : ''; $options .= sprintf( '<option value="%s" %s %s>%s</option>', esc_attr( $column->get_id() ), $selected, $disabled, esc_html( $column->get_label() ) ); } return sprintf( $html, $options ); } } Admin/Forms/UserTemplates.php 0000644 00000027737 15174710275 0012221 0 ustar 00 <?php namespace WPForms\Admin\Forms; use WP_Post; use WPForms\Admin\Notice; use WPForms\Pro\Tasks\Actions\PurgeTemplateEntryTask; /** * User Templates class. * * @since 1.8.8 */ class UserTemplates { /** * Initialize class. * * @since 1.8.8 */ public function init() { $this->hooks(); } /** * Hooks. * * @since 1.8.8 */ public function hooks() { add_action( 'init', [ $this, 'register_post_type' ] ); // Add template states. add_filter( 'display_post_states', [ $this, 'add_template_states' ], 10, 2 ); // Modify get form args on the overview page. add_filter( 'wpforms_get_form_args', [ $this, 'add_template_post_type' ] ); // Modify Show Templates user option. add_filter( 'get_user_option_wpforms_forms_overview_show_form_templates', [ $this, 'get_forms_overview_show_form_templates_option' ] ); // Disable payment processing for user templates. add_filter( 'wpforms_process_before_form_data', [ $this, 'process_before_form_data' ] ); // Add user templates to the form templates list. add_filter( 'wpforms_form_templates', [ $this, 'add_form_templates' ] ); // AJAX handler for deleting user templates. add_action( 'wp_ajax_wpforms_user_template_remove', [ $this, 'ajax_remove_user_template' ] ); // Disable Lite Connect integration for user templates on form submission. add_action( 'wpforms_process', [ $this, 'process_entry' ], 10, 3 ); if ( wpforms()->is_pro() ) { // Add notices about entry(ies) being purged. add_action( 'admin_notices', [ $this, 'get_template_entries_notice' ] ); add_action( 'admin_notices', [ $this, 'get_template_entry_notice' ] ); // Add purge entry task. add_filter( 'wpforms_tasks_get_tasks', [ $this, 'add_purge_entry_task' ] ); // Disable edit entry for templates. add_filter( 'wpforms_current_user_can', [ $this, 'disable_edit_entry' ], 10, 3 ); } } /** * Register the `wpforms-template` post type. * * @since 1.8.8 */ public function register_post_type() { /** * Filter the arguments for the `wpforms-template` post type. * * @since 1.8.8 * * @param array $args Post type arguments. */ $args = apply_filters( // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName 'wpforms_template_post_type_args', [ 'label' => 'WPForms Template', 'public' => false, 'exclude_from_search' => true, 'show_ui' => false, 'show_in_admin_bar' => false, 'rewrite' => false, 'query_var' => false, 'can_export' => false, 'supports' => [ 'title', 'author', 'revisions' ], 'capability_type' => 'wpforms_form', // Not using 'capability_type' anywhere. It just has to be custom for security reasons. 'map_meta_cap' => false, // Don't let WP to map meta caps to have a granular control over this process via 'map_meta_cap' filter. ] ); register_post_type( 'wpforms-template', $args ); } /** * Add template states. * * @since 1.8.8 * * @param array $post_states Array of post states. * @param WP_Post $post Post object. * * @return array */ public function add_template_states( $post_states, $post ) { if ( ! ( wpforms_is_admin_page( 'overview' ) || wpforms_is_admin_page( 'entries' ) ) ) { return $post_states; } // No need to show template states on the templates page. if ( wpforms_is_admin_page( 'overview' ) && wpforms()->obj( 'forms_views' )->get_current_view() === 'templates' ) { return $post_states; } if ( $post->post_type === 'wpforms-template' ) { $post_states['wpforms_template'] = __( 'Template', 'wpforms-lite' ); } return $post_states; } /** * Disable edit entry for templates. * * @since 1.8.8 * * @param bool $user_can Whether the current user can perform the given capability. * @param string $caps Capability name. * @param int $id The ID of the object to check against. * * @return bool Whether the current user can perform the given capability. */ public function disable_edit_entry( bool $user_can, $caps, $id ): bool { if ( $caps === 'edit_entries_form_single' && wpforms_is_form_template( $id ) ) { $user_can = false; } return $user_can; } /** * Display admin notice for the entries page. * * @since 1.8.8 */ public function get_template_entries_notice() { if ( ! wpforms_is_admin_page( 'entries', 'list' ) ) { return; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended $form_id = ! empty( $_REQUEST['form_id'] ) ? absint( $_REQUEST['form_id'] ) : 0; // The notice should be displayed only for form templates. if ( ! wpforms_is_form_template( $form_id ) ) { return; } // If there are no entries, we don't need to display the notice on the empty state page. $entries = wpforms()->obj( 'entry' )->get_entries( [ 'form_id' => $form_id, 'limit' => 1, ] ); if ( empty( $entries ) ) { return; } /** This filter is documented in wpforms/src/Pro/Tasks/Actions/PurgeTemplateEntryTask.php */ $delay = (int) apply_filters( 'wpforms_pro_tasks_actions_purge_template_entry_task_delay', DAY_IN_SECONDS ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName Notice::warning( sprintf( /* translators: %s - delay in formatted time. */ esc_html__( 'Form template entries are for testing purposes and will be automatically deleted after %s.', 'wpforms-lite' ), // The `- 1` hack is to avoid the "1 day" message in favor of "24 hours". human_time_diff( time(), time() + $delay - 1 ) ) ); } /** * Display admin notice for the entry page. * * @since 1.8.8 */ public function get_template_entry_notice() { if ( ! wpforms_is_admin_page( 'entries', 'details' ) ) { return; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended $entry_id = ! empty( $_REQUEST['entry_id'] ) ? absint( $_REQUEST['entry_id'] ) : 0; $entry = wpforms()->obj( 'entry' )->get( $entry_id ); // If entry does not exist, we don't need to display the notice on the empty state page. if ( empty( $entry ) ) { return; } // The notice should be displayed only for form template entry. if ( ! wpforms_is_form_template( $entry->form_id ) ) { return; } $meta = wpforms()->obj( 'entry_meta' )->get_meta( [ 'entry_id' => absint( $entry_id ), 'type' => 'purge_template_entry_task', 'number' => 1, ] ); if ( empty( $meta ) ) { return; } $task = wpforms_json_decode( $meta[0]->data,true ); if ( empty( $task['timestamp'] ) ) { return; } Notice::warning( sprintf( /* translators: %s - delay in formatted time. */ esc_html__( 'Form template entries are for testing purposes. This entry will be automatically deleted in %s.', 'wpforms-lite' ), human_time_diff( time(), $task['timestamp'] ) ) ); } /** * Get the Show Templates user option. * * If the user has not set the Show Templates screen option, it will default to showing templates. * In this case, we want to show templates by default. * * @since 1.8.8 * * @return bool Whether to show templates by default. */ public function get_forms_overview_show_form_templates_option(): bool { $screen_options = get_user_option( 'wpforms_forms_overview_options' ); $result = $screen_options['wpforms_forms_overview_show_form_templates'] ?? true; return $result !== '0'; // phpcs:ignore WPForms.Formatting.EmptyLineBeforeReturn.RemoveEmptyLineBeforeReturnStatement } /** * Add `wpforms-template` post type to the form args. * * @since 1.8.8 * * @param array $args Form arguments. * * @return array */ public function add_template_post_type( array $args ): array { // Only add the post type to the form args on the overview page. if ( ! wpforms_is_admin_page( 'overview' ) ) { return $args; } // Only add the template post type if the Show Templates screen option is enabled // and `post_type` is not already set. if ( ! isset( $args['post_type'] ) && wpforms()->obj( 'forms_overview' )->overview_show_form_templates() ) { $args['post_type'] = wpforms()->obj( 'form' )::POST_TYPES; } return $args; } /** * Add user templates to the form templates list. * * @since 1.8.8 * * @param array $templates Form templates. * * @return array Form templates. */ public function add_form_templates( array $templates ): array { $user_templates = wpforms()->obj( 'form' )->get( '', [ 'post_type' => 'wpforms-template' ] ); if ( empty( $user_templates ) ) { return $templates; } foreach ( $user_templates as $template ) { $template_data = wpforms_decode( $template->post_content ); $edit_url = add_query_arg( [ 'page' => 'wpforms-builder', 'form_id' => $template->ID, ], admin_url( 'admin.php' ) ); $create_url = add_query_arg( [ 'page' => 'wpforms-builder', 'form_id' => $template->ID, 'action' => 'template_to_form', '_wpnonce' => wp_create_nonce( 'wpforms_template_to_form_form_nonce' ), ], admin_url( 'admin.php' ) ); $templates[] = [ 'name' => $template->post_title, 'slug' => 'wpforms-user-template-' . $template->ID, 'action_text' => wpforms_is_admin_page( 'builder' ) || wp_doing_ajax() ? esc_html__( 'Use Template', 'wpforms-lite' ) : esc_html__( 'Create Form', 'wpforms-lite' ), 'edit_action_text' => esc_html__( 'Edit Template', 'wpforms-lite' ), 'description' => ! empty( $template_data['settings']['template_description'] ) ? $template_data['settings']['template_description'] : '', 'source' => 'wpforms-user-template', 'create_url' => $create_url, 'edit_url' => $edit_url, 'categories' => [ 'user' ], 'has_access' => true, 'data' => $template_data, 'post_id' => $template->ID, ]; } return $templates; } /** * AJAX handler for removing user templates. * * @since 1.8.8 */ public function ajax_remove_user_template(): void { // Run a security check. check_ajax_referer( 'wpforms-form-templates', 'nonce' ); $template_id = isset( $_POST['template'] ) ? absint( $_POST['template'] ) : 0; if ( ! $template_id ) { wp_send_json_error(); } // Check for permissions for the specific template. if ( ! wpforms_current_user_can( 'delete_form_single', $template_id ) ) { wp_send_json_error( esc_html__( 'You do not have permission to delete this template.', 'wpforms-lite' ) ); } // Verify the post exists and is a template. $template = get_post( $template_id ); if ( ! $template || $template->post_type !== 'wpforms-template' ) { wp_send_json_error( esc_html__( 'Template not found.', 'wpforms-lite' ) ); } // Delete the template. $result = wp_delete_post( $template_id, true ); if ( ! $result ) { wp_send_json_error( esc_html__( 'Failed to delete the template.', 'wpforms-lite' ) ); } wp_send_json_success(); } /** * Add purge entry task. * * @since 1.8.8 * * @param array $tasks Task class list. */ public function add_purge_entry_task( $tasks ) { $tasks[] = PurgeTemplateEntryTask::class; return $tasks; } /** * Modify the form data before it is processed to disable payment processing. * * @since 1.8.8 * * @param array $form_data Form data. * * @return array */ public function process_before_form_data( $form_data ) { if ( ! isset( $form_data['id'] ) ) { return $form_data; } if ( wpforms_is_form_template( $form_data['id'] ) ) { $form_data['payments'] = []; } return $form_data; } /** * Disable Lite Connect integration for user templates while processing submission. * * @since 1.8.8 * * @param array $fields Form fields. * @param array $entry Form entry. * @param array $form_data Form data. */ public function process_entry( array $fields, array $entry, array $form_data ) { // phpcs:ignore WPForms.PHP.HooksMethod.InvalidPlaceForAddingHooks if ( ! wpforms_is_form_template( $form_data['id'] ) ) { return; } add_filter( 'wpforms_integrations_lite_connect_is_allowed', '__return_false' ); } } Admin/Forms/Tags.php 0000644 00000036145 15174710275 0010313 0 ustar 00 <?php namespace WPForms\Admin\Forms; use WP_Post; use WPForms_Form_Handler; /** * Tags on All Forms page. * * @since 1.7.5 */ class Tags { /** * Current tags filter. * * @since 1.7.5 * * @var array */ private $tags_filter; /** * Current view slug. * * @since 1.7.5 * * @var string */ private $current_view; /** * Base URL. * * @since 1.7.5 * * @var string */ private $base_url; /** * Determine if the class is allowed to load. * * @since 1.7.5 * * @return bool */ private function allow_load() { // Load only on the `All Forms` admin page. return wpforms_is_admin_page( 'overview' ); } /** * Initialize class. * * @since 1.7.5 */ public function init() { // In case of AJAX call we need to initialize base URL only. if ( wp_doing_ajax() ) { $this->base_url = admin_url( 'admin.php?page=wpforms-overview' ); } if ( ! $this->allow_load() ) { return; } $this->update_view_vars(); $this->hooks(); } /** * Hooks. * * @since 1.7.5 */ private function hooks() { add_action( 'init', [ $this, 'update_tags_filter' ] ); add_action( 'admin_enqueue_scripts', [ $this, 'enqueues' ] ); add_action( 'wpforms_admin_overview_before_rows', [ $this, 'bulk_edit_tags' ] ); add_filter( 'wpforms_get_multiple_forms_args', [ $this, 'get_forms_args' ] ); add_filter( 'wpforms_admin_forms_bulk_actions_get_dropdown_items', [ $this, 'add_bulk_action' ], 10, 2 ); add_filter( 'wpforms_overview_table_columns', [ $this, 'filter_columns' ] ); } /** * Init view-related variables. * * @since 1.7.5 */ private function update_view_vars() { $views_object = wpforms()->obj( 'forms_views' ); $this->current_view = $views_object->get_current_view(); $view_config = $views_object->get_view_by_slug( $this->current_view ); $this->base_url = remove_query_arg( [ 'tags', 'search', 'action', 'action2', '_wpnonce', 'form_id', 'paged', '_wp_http_referer' ], $views_object->get_base_url() ); // Base URL should contain variable according to the current view. if ( isset( $view_config['get_var'], $view_config['get_var_value'] ) && $this->current_view !== 'all' ) { $this->base_url = add_query_arg( $view_config['get_var'], $view_config['get_var_value'], $this->base_url ); } // Base URL fallback. $this->base_url = empty( $this->base_url ) ? admin_url( 'admin.php?page=wpforms-overview' ) : $this->base_url; } /** * Update tags filter value. * * @since 1.7.5 */ public function update_tags_filter() { // Do not need to update this property while doing AJAX. if ( wp_doing_ajax() ) { return; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash $tags = isset( $_GET['tags'] ) ? sanitize_text_field( wp_unslash( rawurldecode( $_GET['tags'] ) ) ) : ''; $tags_slugs = explode( ',', $tags ); $tags_filter = array_filter( self::get_all_tags_choices(), static function( $tag ) use ( $tags_slugs ) { return in_array( trim( rawurldecode( $tag['slug'] ) ), $tags_slugs, true ); } ); $this->tags_filter = array_map( 'absint', wp_list_pluck( $tags_filter, 'value' ) ); } /** * Enqueue assets. * * @since 1.7.5 */ public function enqueues() { wp_enqueue_script( 'wpforms-admin-forms-overview-choicesjs', WPFORMS_PLUGIN_URL . 'assets/lib/choices.min.js', [], '10.2.0', true ); wp_localize_script( 'wpforms-admin-forms-overview-choicesjs', 'wpforms_admin_forms_overview', [ 'choicesjs_config' => self::get_choicesjs_config(), 'edit_tags_form' => $this->get_column_tags_form(), 'all_tags_choices' => self::get_all_tags_choices(), 'strings' => $this->get_localize_strings(), ] ); } /** * Get Choices.js configuration. * * @since 1.7.5 */ public static function get_choicesjs_config() { return [ 'removeItemButton' => true, 'shouldSort' => false, 'loadingText' => esc_html__( 'Loading...', 'wpforms-lite' ), 'noResultsText' => esc_html__( 'No results found', 'wpforms-lite' ), 'noChoicesText' => esc_html__( 'No tags to choose from', 'wpforms-lite' ), 'searchEnabled' => true, 'searchChoices' => true, 'searchFloor' => 1, 'searchResultLimit' => 100, 'searchFields' => [ 'label' ], 'allowHTML' => true, // These `fuseOptions` options enable the search of chars not only from the beginning of the tags. 'fuseOptions' => [ 'threshold' => 0.1, 'distance' => 1000, 'location' => 2, ], ]; } /** * Get all tags (terms) as items for Choices.js. * * @since 1.7.5 * * @return array */ public static function get_all_tags_choices() { static $choices = null; if ( is_array( $choices ) ) { return $choices; } $choices = []; $tags = get_terms( [ 'taxonomy' => WPForms_Form_Handler::TAGS_TAXONOMY, 'hide_empty' => false, ] ); foreach ( $tags as $tag ) { $choices[] = [ 'value' => (string) $tag->term_id, 'slug' => $tag->slug, 'label' => sanitize_term_field( 'name', $tag->name, $tag->term_id, WPForms_Form_Handler::TAGS_TAXONOMY, 'display' ), 'count' => (int) $tag->count, ]; } return $choices; } /** * Determine if the Tags column is hidden. * * @since 1.7.5 * * @return bool */ private function is_tags_column_hidden() { $overview_table = ListTable::get_instance(); $columns = $overview_table->__call( 'get_column_info', [] ); return isset( $columns[1] ) && in_array( 'tags', $columns[1], true ); } /** * Get localize strings. * * @since 1.7.5 */ private function get_localize_strings() { return [ 'nonce' => wp_create_nonce( 'wpforms-admin-forms-overview-nonce' ), 'is_tags_column_hidden' => $this->is_tags_column_hidden(), 'base_url' => admin_url( 'admin.php?' . wp_parse_url( $this->base_url, PHP_URL_QUERY ) ), 'add_new_tag' => esc_html__( 'Press Enter or "," key to add new tag', 'wpforms-lite' ), 'error' => esc_html__( 'Something wrong. Please try again later.', 'wpforms-lite' ), 'all_tags' => esc_html__( 'All Tags', 'wpforms-lite' ), 'bulk_edit_one_form' => wp_kses( __( '<strong>1 form</strong> selected for Bulk Edit.', 'wpforms-lite' ), [ 'strong' => [] ] ), 'bulk_edit_n_forms' => wp_kses( /* translators: %d - number of forms selected for Bulk Edit. */ __( '<strong>%d forms</strong> selected for Bulk Edit.', 'wpforms-lite' ), [ 'strong' => [] ] ), 'manage_tags_title' => esc_html__( 'Manage Tags', 'wpforms-lite' ), 'manage_tags_desc' => esc_html__( 'Delete tags that you\'re no longer using. Deleting a tag will remove it from a form, but will not delete the form itself.', 'wpforms-lite' ), 'manage_tags_save' => esc_html__( 'Delete Tags', 'wpforms-lite' ), 'manage_tags_one_tag' => wp_kses( __( 'You have <strong>1 tag</strong> selected for deletion.', 'wpforms-lite' ), [ 'strong' => [] ] ), 'manage_tags_n_tags' => wp_kses( /* translators: %d - number of forms selected for Bulk Edit. */ __( 'You have <strong>%d tags</strong> selected for deletion.', 'wpforms-lite' ), [ 'strong' => [] ] ), 'manage_tags_no_tags' => wp_kses( __( 'There are no tags to delete.<br>Please create at least one by adding it to any form.', 'wpforms-lite' ), [ 'br' => [] ] ), 'manage_tags_one_deleted' => esc_html__( '1 tag was successfully deleted.', 'wpforms-lite' ), /* translators: %d - number of deleted tags. */ 'manage_tags_n_deleted' => esc_html__( '%d tags were successfully deleted.', 'wpforms-lite' ), 'manage_tags_result_title' => esc_html__( 'Almost done!', 'wpforms-lite' ), 'manage_tags_result_text' => esc_html__( 'In order to update the tags in the forms list, please refresh the page.', 'wpforms-lite' ), 'manage_tags_btn_refresh' => esc_html__( 'Refresh', 'wpforms-lite' ), ]; } /** * Determine if tags are editable. * * @since 1.7.5 * * @param int|null $form_id Form ID. * * @return bool */ private function is_editable( $form_id = null ) { if ( $this->current_view === 'trash' ) { return false; } if ( ! empty( $form_id ) && ! wpforms_current_user_can( 'edit_form_single', $form_id ) ) { return false; } if ( empty( $form_id ) && ! wpforms_current_user_can( 'edit_forms' ) ) { return false; } return true; } /** * Generate Tags column markup. * * @since 1.7.5 * * @param WP_Post $form Form. * * @return string */ public function column_tags( $form ) { $terms = get_the_terms( $form->ID, WPForms_Form_Handler::TAGS_TAXONOMY ); $data = $this->get_tags_data( $terms ); return $this->get_column_tags_links( $data['tags_links'], $data['tags_ids'], $form->ID ) . $this->get_column_tags_form( $data['tags_options'] ); } /** * Generate tags data. * * @since 1.7.5 * * @param array $terms Tags terms. * * @return array */ public function get_tags_data( $terms ) { if ( ! is_array( $terms ) ) { $taxonomy_object = get_taxonomy( WPForms_Form_Handler::TAGS_TAXONOMY ); return [ 'tags_links' => sprintf( '<span aria-hidden="true">—</span><span class="screen-reader-text">%s</span>', esc_html( isset( $taxonomy_object->labels->no_terms ) ? $taxonomy_object->labels->no_terms : '—' ) ), 'tags_ids' => '', 'tags_options' => '', ]; } $tags_links = []; $tags_ids = []; $tags_options = []; $terms = empty( $terms ) ? [] : (array) $terms; foreach ( $terms as $tag ) { $filter_url = add_query_arg( 'tags', rawurlencode( $tag->slug ), $this->base_url ); $tags_links[] = sprintf( '<a href="%1$s">%2$s</a>', esc_url( $filter_url ), esc_html( $tag->name ) ); $tags_ids[] = $tag->term_id; $tags_options[] = sprintf( '<option value="%1$s" selected>%2$s</option>', esc_attr( $tag->term_id ), esc_html( $tag->name ) ); } return [ /* translators: used between list items, there is a space after the comma. */ 'tags_links' => implode( __( ', ', 'wpforms-lite' ), $tags_links ), 'tags_ids' => implode( ',', array_filter( $tags_ids ) ), 'tags_options' => implode( '', $tags_options ), ]; } /** * Get form tags links list markup. * * @since 1.7.5 * * @param string $tags_links Tags links. * @param string $tags_ids Tags IDs. * @param int $form_id Form ID. * * @return string */ private function get_column_tags_links( $tags_links = '', $tags_ids = '', $form_id = 0 ) { $edit_link = ''; if ( $this->is_editable( $form_id ) ) { $edit_link = sprintf( '<a href="#" class="wpforms-column-tags-edit">%s</a>', esc_html__( 'Edit', 'wpforms-lite' ) ); } return sprintf( '<div class="wpforms-column-tags-links" data-form-id="%1$d" data-is-editable="%2$s" data-tags="%3$s"> <div class="wpforms-column-tags-links-list">%4$s</div> %5$s </div>', absint( $form_id ), $this->is_editable( $form_id ) ? '1' : '0', esc_attr( $tags_ids ), $tags_links, $edit_link ); } /** * Get edit tags form markup in the Tags column. * * @since 1.7.5 * * @param string $tags_options Tags options. * * @return string */ private function get_column_tags_form( $tags_options = '' ) { return sprintf( '<div class="wpforms-column-tags-form wpforms-hidden"> <select multiple>%1$s</select> <i class="dashicons dashicons-dismiss wpforms-column-tags-edit-cancel" title="%2$s"></i> <i class="dashicons dashicons-yes-alt wpforms-column-tags-edit-save" title="%3$s"></i> <i class="wpforms-spinner spinner wpforms-hidden"></i> </div>', $tags_options, esc_attr__( 'Cancel', 'wpforms-lite' ), esc_attr__( 'Save changes', 'wpforms-lite' ) ); } /** * Extra controls to be displayed between bulk actions and pagination. * * @since 1.7.5 * * @param string $which The location of the table navigation: 'top' or 'bottom'. * @param ListTable $overview_table Instance of the ListTable class. */ public function extra_tablenav( $which, $overview_table ) { if ( $this->current_view === 'trash' ) { return; } $all_tags = self::get_all_tags_choices(); $is_column_hidden = $this->is_tags_column_hidden(); $is_hidden = $is_column_hidden || empty( $all_tags ); $tags_options = ''; if ( $this->is_filtered() ) { $tags = get_terms( [ 'taxonomy' => WPForms_Form_Handler::TAGS_TAXONOMY, 'hide_empty' => false, 'include' => $this->tags_filter, ] ); foreach ( $tags as $tag ) { $tags_options .= sprintf( '<option value="%1$s" selected>%2$s</option>', esc_attr( $tag->term_id ), esc_html( $tag->name ) ); } } printf( '<div class="wpforms-tags-filter %1$s"> <select multiple size="1" data-tags-filter="1"> <option placeholder>%2$s</option> %3$s </select> <button type="button" class="button">%4$s</button> </div> <button type="button" class="button wpforms-manage-tags %1$s">%5$s</button>', esc_attr( $is_hidden ? 'wpforms-hidden' : '' ), esc_html( empty( $tags_options ) ? __( 'All Tags', 'wpforms-lite' ) : '' ), wp_kses( $tags_options, [ 'option' => [ 'value' => [], 'selected' => [], ], ] ), esc_attr__( 'Filter', 'wpforms-lite' ), esc_attr__( 'Manage Tags', 'wpforms-lite' ) ); } /** * Render and display Bulk Edit Tags form. * * @since 1.7.5 * * @param ListTable $list_table Overview list table object. */ public function bulk_edit_tags( $list_table ) { $columns = $list_table->get_columns(); echo wpforms_render( // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped 'admin/forms/bulk-edit-tags', [ 'columns' => count( $columns ), ], true ); } /** * Determine whether a filtering is performing. * * @since 1.7.5 * * @return bool */ private function is_filtered() { return ! empty( $this->tags_filter ); } /** * Pass the tags to the arguments array. * * @since 1.7.5 * * @param array $args Get posts arguments. * * @return array */ public function get_forms_args( $args ) { if ( $args['post_status'] === 'trash' || ! $this->is_filtered() ) { return $args; } // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query $args['tax_query'] = [ [ 'taxonomy' => WPForms_Form_Handler::TAGS_TAXONOMY, 'field' => 'term_id', 'terms' => $this->tags_filter, ], ]; return $args; } /** * Add item to Bulk Actions dropdown. * * @since 1.7.5 * * @param array $items Dropdown items. * * @return array */ public function add_bulk_action( $items ) { if ( $this->is_editable() ) { $items['edit_tags'] = esc_html__( 'Edit Tags', 'wpforms-lite' ); } return $items; } /** * Filter list table columns. * * @since 1.7.5 * * @param string[] $columns Array of columns. * * @return array */ public function filter_columns( $columns ) { if ( $this->current_view === 'trash' ) { unset( $columns['tags'] ); } return $columns; } } Admin/Forms/Views.php 0000644 00000043420 15174710275 0010504 0 ustar 00 <?php namespace WPForms\Admin\Forms; use WP_Post; /** * List table views. * * @since 1.7.3 */ class Views { /** * Current view slug. * * @since 1.7.3 * * @var string */ private $current_view; /** * Views settings. * * @since 1.7.3 * * @var array */ private $views; /** * Count forms in different views. * * @since 1.7.3 * * @var array */ private $count; /** * Base URL. * * @since 1.7.3 * * @var string */ private $base_url; /** * Show form templates. * * @since 1.8.8 * * @var bool */ private $show_form_templates; /** * Views configuration. * * @since 1.7.3 */ private function configuration() { if ( ! empty( $this->views ) ) { return; } // Define views. $views = [ 'all' => [ 'title' => __( 'All', 'wpforms-lite' ), 'get_var' => '', 'get_var_value' => '', ], 'trash' => [ 'title' => __( 'Trash', 'wpforms-lite' ), 'get_var' => 'status', 'get_var_value' => 'trash', 'args' => [ 'post_status' => 'trash', ], ], ]; $this->show_form_templates = wpforms()->obj( 'forms_overview' )->overview_show_form_templates(); // Add Forms and Templates views if Show Templates setting is enabled. if ( $this->show_form_templates ) { $views = wpforms_array_insert( $views, [ 'forms' => [ 'title' => __( 'Forms', 'wpforms-lite' ), 'get_var' => 'type', 'get_var_value' => 'form', 'args' => [ 'post_type' => 'wpforms', ], ], 'templates' => [ 'title' => __( 'Templates', 'wpforms-lite' ), 'get_var' => 'type', 'get_var_value' => 'template', 'args' => [ 'post_type' => 'wpforms-template', ], ], ], 'all' ); } // phpcs:disable WPForms.Comments.ParamTagHooks.InvalidParamTagsQuantity /** * Filters configuration of the Forms Overview table views. * * @since 1.7.3 * * @param array $views { * Views array. * * @param array $view { * Each view is the array with three elements: * * @param string $title View title. * @param string $get_var URL query variable name. * @param string $get_var_value URL query variable value. * @param array $args Additional arguments to be passed to `wpforms()->obj( 'form' )->get()` method. * } * ... * } */ $this->views = apply_filters( 'wpforms_admin_forms_views_configuration', $views ); // phpcs:enable WPForms.Comments.ParamTagHooks.InvalidParamTagsQuantity } /** * Determine if the class is allowed to load. * * @since 1.7.3 * * @return bool */ private function allow_load(): bool { // Load only on the `All Forms` admin page. return wpforms_is_admin_page( 'overview' ); } /** * Initialize class. * * @since 1.7.3 */ public function init() { if ( ! $this->allow_load() ) { return; } $this->configuration(); $this->update_current_view(); $this->update_base_url(); $this->hooks(); } /** * Hooks. * * @since 1.7.3 */ private function hooks() { add_filter( 'wpforms_overview_table_update_count', [ $this, 'update_count' ], 10, 2 ); add_filter( 'wpforms_overview_table_update_count_all', [ $this, 'update_count' ], 10, 2 ); add_filter( 'wpforms_overview_table_prepare_items_args', [ $this, 'prepare_items_args' ], 100 ); add_filter( 'wpforms_overview_row_actions', [ $this, 'row_actions_all' ], 9, 2 ); add_filter( 'wpforms_overview_row_actions', [ $this, 'row_actions_trash' ], PHP_INT_MAX, 2 ); add_filter( 'wpforms_admin_forms_search_search_reset_block_message', [ $this, 'search_reset_message' ], 10, 4 ); } /** * Determine and save current view slug. * * @since 1.7.3 */ private function update_current_view() { if ( ! is_array( $this->views ) ) { return; } $this->current_view = 'all'; foreach ( $this->views as $slug => $view ) { if ( // phpcs:disable WordPress.Security.NonceVerification.Recommended isset( $_GET[ $view['get_var'] ] ) && $view['get_var_value'] === sanitize_key( $_GET[ $view['get_var'] ] ) // phpcs:enable WordPress.Security.NonceVerification.Recommended ) { $this->current_view = $slug; return; } } } /** * Update Base URL. * * @since 1.7.3 */ private function update_base_url() { if ( ! is_array( $this->views ) ) { return; } $get_vars = wp_list_pluck( $this->views, 'get_var' ); $get_vars = array_merge( $get_vars, [ 'paged', 'trashed', 'restored', 'deleted', 'duplicated', ] ); $this->base_url = remove_query_arg( $get_vars ); } /** * Get current view slug. * * @since 1.7.3 * * @return string */ public function get_current_view(): string { return $this->current_view; } /** * Get base URL. * * @since 1.7.5 * * @return string */ public function get_base_url(): string { return $this->base_url; } /** * Get view configuration by slug. * * @since 1.7.5 * * @param string $slug View slug. * * @return array */ public function get_view_by_slug( string $slug ): array { return $this->views[ $slug ] ?? []; // phpcs:ignore WPForms.Formatting.EmptyLineBeforeReturn.RemoveEmptyLineBeforeReturnStatement } /** * Update count. * * @since 1.7.3 * * @param array $count Number of forms in different views. * @param array $args Get forms arguments. * * @return array */ public function update_count( $count, $args ) { $defaults = [ 'nopaging' => true, 'no_found_rows' => true, 'update_post_meta_cache' => false, 'update_post_term_cache' => false, 'fields' => 'ids', 'post_status' => 'publish', 'post_type' => wpforms()->obj( 'form' )::POST_TYPES, ]; $args = array_merge( $args, $defaults ); $count['all'] = $this->get_all_items_count( $args ); $count['trash'] = $this->get_trashed_forms_count( $args ); // Count forms and templates separately only if Show Templates screen setting is enabled. if ( $this->show_form_templates ) { $count['forms'] = $this->get_forms_count( $args ); $count['templates'] = $this->get_form_templates_count( $args ); } // Store in class property for further use. $this->count = $count; return $count; } /** * Get count of all items. * * May include only forms or both forms and form templates, depending on the * Screen Options settings whether to show form templates or not. * * @since 1.8.8 * * @param array $args Get forms arguments. * * @return int Number of forms and templates. */ private function get_all_items_count( array $args ): int { if ( ! $this->show_form_templates ) { $args['post_type'] = 'wpforms'; } $all_items = wpforms()->obj( 'form' )->get( '', $args ); return is_array( $all_items ) ? count( $all_items ) : 0; } /** * Get count of forms. * * @since 1.8.8 * * @param array $args Get forms arguments. * * @return int Number of published forms. */ private function get_forms_count( array $args ): int { $args['post_type'] = 'wpforms'; $forms = wpforms()->obj( 'form' )->get( '', $args ); return is_array( $forms ) ? count( $forms ) : 0; } /** * Get count of form templates. * * @since 1.8.8 * * @param array $args Get forms arguments. * * @return int Number of published templates. */ private function get_form_templates_count( array $args ): int { $args['post_type'] = 'wpforms-template'; $templates = wpforms()->obj( 'form' )->get( '', $args ); return is_array( $templates ) ? count( $templates ) : 0; } /** * Get count of trashed items. * * May include only forms or both forms and form templates, depending on the * Screen Options settings whether to show form templates or not. * * @since 1.8.8 * * @param array $args Get forms arguments. * * @return int Number of trashed forms. */ private function get_trashed_forms_count( array $args ): int { if ( ! $this->show_form_templates ) { $args['post_type'] = 'wpforms'; } $args['post_status'] = 'trash'; $forms = wpforms()->obj( 'form' )->get( '', $args ); return is_array( $forms ) ? count( $forms ) : 0; } /** * Get counts of forms in different views. * * @since 1.7.3 * * @return array */ public function get_count(): array { return $this->count; } /** * Prepare items arguments for list table. * * @since 1.8.8 * * @param array $args Get multiple forms arguments. * * @return array */ public function prepare_items_args( $args ): array { $view_args = $this->views[ $this->current_view ]['args'] ?? []; if ( ! empty( $view_args ) ) { $args = array_merge( $args, $view_args ); } return $args; } /** * Get forms from Trash when preparing items for list table. * * @since 1.7.3 * * @depecated 1.8.8 The `prepare_items_args()` now handles all cases, uses `$this->views`. * * @param array $args Get multiple forms arguments. * * @return array */ public function prepare_items_trash( $args ) { _deprecated_function( __METHOD__, '1.8.8 of the WPForms plugin' ); return $args; } /** * Generate views items. * * @since 1.7.3 * * @return array */ public function get_views(): array { if ( ! is_array( $this->views ) ) { return []; } $views = []; foreach ( $this->views as $slug => $view ) { if ( $slug === 'trash' && $this->current_view !== 'trash' && empty( $this->count[ $slug ] ) ) { continue; } $views[ $slug ] = $this->get_view_markup( $slug ); } /** * Filters the Forms Overview table views links. * * @since 1.7.3 * * @param array $views Views links. * @param array $count Count forms in different views. */ return apply_filters( 'wpforms_admin_forms_views_get_views', $views, $this->count ); } /** * Generate single view item. * * @since 1.7.3 * * @param string $slug View slug. * * @return string */ private function get_view_markup( string $slug ): string { if ( empty( $this->views[ $slug ] ) ) { return ''; } $view = $this->views[ $slug ]; return sprintf( '<a href="%1$s"%2$s>%3$s <span class="count">(%4$d)</span></a>', $slug === 'all' ? esc_url( $this->base_url ) : esc_url( add_query_arg( $view['get_var'], $view['get_var_value'], $this->base_url ) ), $this->current_view === $slug ? ' class="current"' : '', esc_html( $view['title'] ), empty( $this->count[ $slug ] ) ? 0 : absint( $this->count[ $slug ] ) ); } /** * Row actions for views "All", "Forms", "Templates". * * @since 1.7.3 * * @param array $row_actions Row actions. * @param WP_Post $form Form object. * * @return array */ public function row_actions_all( $row_actions, $form ) { // phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh // Modify row actions only for these views. $allowed_views = [ 'all', 'forms', 'templates' ]; if ( ! in_array( $this->current_view, $allowed_views, true ) ) { return $row_actions; } $is_form_template = wpforms_is_form_template( $form ); $row_actions = []; // Edit. if ( wpforms_current_user_can( 'edit_form_single', $form->ID ) ) { $row_actions['edit'] = sprintf( '<a href="%s" title="%s">%s</a>', esc_url( add_query_arg( [ 'view' => 'fields', 'form_id' => $form->ID, ], admin_url( 'admin.php?page=wpforms-builder' ) ) ), $is_form_template ? esc_attr__( 'Edit this template', 'wpforms-lite' ) : esc_attr__( 'Edit this form', 'wpforms-lite' ), esc_html__( 'Edit', 'wpforms-lite' ) ); } // Entries. if ( wpforms_current_user_can( 'view_entries_form_single', $form->ID ) ) { $row_actions['entries'] = sprintf( '<a href="%s" title="%s">%s</a>', esc_url( add_query_arg( [ 'view' => 'list', 'form_id' => $form->ID, ], admin_url( 'admin.php?page=wpforms-entries' ) ) ), esc_attr__( 'View entries', 'wpforms-lite' ), esc_html__( 'Entries', 'wpforms-lite' ) ); } // Payments. if ( wpforms_current_user_can( wpforms_get_capability_manage_options(), $form->ID ) && wpforms()->obj( 'payment' )->get_by( 'form_id', $form->ID ) ) { $row_actions['payments'] = sprintf( '<a href="%s" title="%s">%s</a>', esc_url( add_query_arg( [ 'page' => 'wpforms-payments', 'form_id' => $form->ID, ], admin_url( 'admin.php' ) ) ), esc_attr__( 'View payments', 'wpforms-lite' ), esc_html__( 'Payments', 'wpforms-lite' ) ); } // Preview. if ( wpforms_current_user_can( 'view_form_single', $form->ID ) ) { $row_actions['preview_'] = sprintf( '<a href="%s" title="%s" target="_blank" rel="noopener noreferrer">%s</a>', esc_url( wpforms_get_form_preview_url( $form->ID ) ), esc_attr__( 'View preview', 'wpforms-lite' ), esc_html__( 'Preview', 'wpforms-lite' ) ); } // Duplicate. if ( wpforms_current_user_can( 'create_forms' ) && wpforms_current_user_can( 'view_form_single', $form->ID ) ) { $row_actions['duplicate'] = sprintf( '<a href="%1$s" title="%2$s" data-type="%3$s">%4$s</a>', esc_url( wp_nonce_url( add_query_arg( [ 'action' => 'duplicate', 'form_id' => $form->ID, ], $this->base_url ), 'wpforms_duplicate_form_nonce' ) ), $is_form_template ? esc_attr__( 'Duplicate this template', 'wpforms-lite' ) : esc_attr__( 'Duplicate this form', 'wpforms-lite' ), $is_form_template ? 'template' : 'form', esc_html__( 'Duplicate', 'wpforms-lite' ) ); } // Trash. if ( wpforms_current_user_can( 'delete_form_single', $form->ID ) ) { $query_arg = [ 'action' => 'trash', 'form_id' => $form->ID, ]; if ( $this->current_view !== 'all' ) { $query_arg['type'] = $this->current_view === 'templates' ? 'template' : 'form'; } $row_actions['trash'] = sprintf( '<a href="%s" title="%s">%s</a>', esc_url( wp_nonce_url( add_query_arg( $query_arg, $this->base_url ), 'wpforms_trash_form_nonce' ) ), $is_form_template ? esc_attr__( 'Move this form template to trash', 'wpforms-lite' ) : esc_attr__( 'Move this form to trash', 'wpforms-lite' ), esc_html__( 'Trash', 'wpforms-lite' ) ); } return $row_actions; } /** * Row actions for view "Trash". * * @since 1.7.3 * * @param array $row_actions Row actions. * @param WP_Post $form Form object. * * @return array */ public function row_actions_trash( $row_actions, $form ) { if ( $this->current_view !== 'trash' || ! wpforms_current_user_can( 'delete_form_single', $form->ID ) ) { return $row_actions; } $is_form_template = wpforms_is_form_template( $form ); $row_actions = []; // Restore form. $row_actions['restore'] = sprintf( '<a href="%s" title="%s">%s</a>', esc_url( wp_nonce_url( add_query_arg( [ 'action' => 'restore', 'form_id' => $form->ID, 'status' => 'trash', ], $this->base_url ), 'wpforms_restore_form_nonce' ) ), $is_form_template ? esc_attr__( 'Restore this template', 'wpforms-lite' ) : esc_attr__( 'Restore this form', 'wpforms-lite' ), esc_html__( 'Restore', 'wpforms-lite' ) ); // Delete permanently. $row_actions['delete'] = sprintf( '<a href="%1$s" title="%2$s" data-type="%3$s">%4$s</a>', esc_url( wp_nonce_url( add_query_arg( [ 'action' => 'delete', 'form_id' => $form->ID, 'status' => 'trash', ], $this->base_url ), 'wpforms_delete_form_nonce' ) ), $is_form_template ? esc_attr__( 'Delete this template permanently', 'wpforms-lite' ) : esc_attr__( 'Delete this form permanently', 'wpforms-lite' ), $is_form_template ? 'template' : 'form', esc_html__( 'Delete Permanently', 'wpforms-lite' ) ); return $row_actions; } /** * Search reset message. * * @since 1.7.3 * * @param string $message Search reset block message. * @param string $search_term Search term. * @param array $count Count forms in different views. * @param string $current_view Current view. * * @return string */ public function search_reset_message( $message, $search_term, $count, $current_view ) { if ( $current_view !== 'trash' ) { return $message; } $count['trash'] = ! empty( $count['trash'] ) ? $count['trash'] : 0; return sprintf( wp_kses( /* translators: %1$d - number of forms found in the trash, %2$s - search term. */ _n( 'Found <strong>%1$d form</strong> in <em>the trash</em> containing <em>"%2$s"</em>', 'Found <strong>%1$d forms</strong> in <em>the trash</em> containing <em>"%2$s"</em>', (int) $count['trash'], 'wpforms-lite' ), [ 'strong' => [], 'em' => [], ] ), (int) $count['trash'], esc_html( $search_term ) ); } /** * Extra controls to be displayed between bulk actions and pagination. * * @since 1.7.3 * * @param string $which The location of the table navigation: 'top' or 'bottom'. */ public function extra_tablenav( $which ) { if ( ! wpforms_current_user_can( 'delete_form_single' ) ) { return; } if ( $this->current_view !== 'trash' ) { return; } // Preserve current view after applying bulk action. echo '<input type="hidden" name="status" value="trash">'; // Display Empty Trash button. printf( '<a href="%1$s" class="button delete-all">%2$s</a>', esc_url( wp_nonce_url( add_query_arg( [ 'action' => 'empty_trash', 'form_id' => 1, // Technically, `empty_trash` is one of the bulk actions, therefore we need to provide fake form_id to proceed. 'status' => 'trash', ], $this->base_url ), 'wpforms_empty_trash_form_nonce' ) ), esc_html__( 'Empty Trash', 'wpforms-lite' ) ); } } Admin/Forms/Ajax/Tags.php 0000644 00000015254 15174710275 0011174 0 ustar 00 <?php namespace WPForms\Admin\Forms\Ajax; use WPForms_Form_Handler; /** * Tags AJAX actions on All Forms page. * * @since 1.7.5 */ class Tags { /** * Determine if the new tag was added during processing submitted tags. * * @since 1.7.5 * * @var bool */ private $is_new_tag_added; /** * Determine if the class is allowed to load. * * @since 1.7.5 * * @return bool */ private function allow_load() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $action = isset( $_REQUEST['action'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['action'] ) ) : ''; // Load only in the case of AJAX calls on Forms Overview page. return wp_doing_ajax() && strpos( $action, 'wpforms_admin_forms_overview_' ) === 0; } /** * Initialize class. * * @since 1.7.5 */ public function init() { if ( ! $this->allow_load() ) { return; } $this->hooks(); } /** * Hooks. * * @since 1.7.5 */ private function hooks() { add_action( 'wp_ajax_wpforms_admin_forms_overview_save_tags', [ $this, 'save_tags' ] ); add_action( 'wp_ajax_wpforms_admin_forms_overview_delete_tags', [ $this, 'delete_tags' ] ); } /** * Save tags. * * @since 1.7.5 */ public function save_tags() { $data = $this->get_prepared_data( 'save' ); $tags_ids = $this->get_processed_tags( $data['tags'] ); $tags_labels = wp_list_pluck( $data['tags'], 'label' ); // Set tags to each form. $this->set_tags_to_forms( $data['forms'], $tags_ids, $tags_labels ); $tags_obj = wpforms()->obj( 'forms_tags' ); $terms = get_the_terms( array_pop( $data['forms'] ), WPForms_Form_Handler::TAGS_TAXONOMY ); $tags_data = $tags_obj->get_tags_data( $terms ); if ( ! empty( $this->is_new_tag_added ) ) { $tags_data['all_tags_choices'] = $tags_obj->get_all_tags_choices(); } wp_send_json_success( $tags_data ); } /** * Delete tags. * * @since 1.7.5 */ public function delete_tags(): void { $form_obj = wpforms()->obj( 'form' ); $data = $this->get_prepared_data( 'delete' ); $deleted = 0; $labels = []; foreach ( $data['tags'] as $tag_id ) { $term = get_term_by( 'term_id', $tag_id, WPForms_Form_Handler::TAGS_TAXONOMY, ARRAY_A ); $labels[] = $term['name']; // Delete tag (term). if ( wp_delete_term( $tag_id, WPForms_Form_Handler::TAGS_TAXONOMY ) === true ) { ++$deleted; } } // Get forms marked by the tags. $args = [ 'fields' => 'ids', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query 'tax_query' => [ [ 'taxonomy' => WPForms_Form_Handler::TAGS_TAXONOMY, 'field' => 'term_id', 'terms' => array_map( 'absint', $data['tags'] ), ], ], ]; $forms = $form_obj ? (array) $form_obj->get( 0, $args ) : []; // Remove tags from the settings of the forms. foreach ( $forms as $form_id ) { $form_data = $form_obj->get( $form_id, [ 'content_only' => true ] ); if ( empty( $form_data['settings']['form_tags'] ) || ! is_array( $form_data['settings']['form_tags'] ) ) { continue; } $form_data['settings']['form_tags'] = array_diff( $form_data['settings']['form_tags'], $labels ); $form_obj->update( $form_id, $form_data ); } wp_send_json_success( [ 'deleted' => $deleted ] ); } /** * Get processed tags. * * @since 1.7.5 * * @param array $tags_data Submitted tags data. * * @return array Tags IDs list. */ public function get_processed_tags( $tags_data ) { if ( ! is_array( $tags_data ) ) { return []; } $tags_ids = []; // Process the tags' data. foreach ( $tags_data as $tag ) { $term = get_term( $tag['value'], WPForms_Form_Handler::TAGS_TAXONOMY ); // In the case when the term is not found, we should create the new term. if ( empty( $term ) || is_wp_error( $term ) ) { $new_term = wp_insert_term( sanitize_text_field( $tag['label'] ), WPForms_Form_Handler::TAGS_TAXONOMY ); $tag['value'] = ! is_wp_error( $new_term ) && isset( $new_term['term_id'] ) ? $new_term['term_id'] : 0; $this->is_new_tag_added = $this->is_new_tag_added || $tag['value'] > 0; } if ( ! empty( $tag['value'] ) ) { $tags_ids[] = absint( $tag['value'] ); } } return $tags_ids; } /** * Get prepared data before perform ajax action. * * @since 1.7.5 * * @param string $action Action: `save` OR `delete`. * * @return array */ private function get_prepared_data( string $action ): array { // Run a security check. if ( ! check_ajax_referer( 'wpforms-admin-forms-overview-nonce', 'nonce', false ) ) { wp_send_json_error( esc_html__( 'Most likely, your session expired. Please reload the page.', 'wpforms-lite' ) ); } // Check for permissions. if ( ! wpforms_current_user_can( 'edit_others_forms' ) ) { wp_send_json_error( esc_html__( 'You are not allowed to perform this action.', 'wpforms-lite' ) ); } $data = [ // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized 'tags' => ! empty( $_POST['tags'] ) ? map_deep( (array) wp_unslash( $_POST['tags'] ), 'sanitize_text_field' ) : [], ]; if ( $action === 'save' ) { $data['forms'] = $this->get_allowed_forms(); } return $data; } /** * Get allowed forms. * * @since 1.7.5 * * @return array Allowed form IDs. */ private function get_allowed_forms() { // phpcs:disable WordPress.Security.NonceVerification.Missing if ( empty( $_POST['forms'] ) ) { wp_send_json_error( esc_html__( 'No forms selected when trying to add a tag to them.', 'wpforms-lite' ) ); } $forms_all = array_filter( array_map( 'absint', (array) $_POST['forms'] ) ); $forms_allowed = []; // phpcs:enable WordPress.Security.NonceVerification.Missing foreach ( $forms_all as $form_id ) { if ( wpforms_current_user_can( 'edit_form_single', $form_id ) ) { $forms_allowed[] = $form_id; } } if ( empty( $forms_allowed ) ) { wp_send_json_error( esc_html__( 'You are not allowed to perform this action.', 'wpforms-lite' ) ); } return $forms_allowed; } /** * Set tags to each form in the list. * * @since 1.7.5 * * @param array $forms_ids Forms IDs list. * @param array $tags_ids Tags IDs list. * @param array $tags_labels Tags labels list. */ private function set_tags_to_forms( $forms_ids, $tags_ids, $tags_labels ) { $form_obj = wpforms()->obj( 'form' ); foreach ( $forms_ids as $form_id ) { wp_set_post_terms( $form_id, $tags_ids, WPForms_Form_Handler::TAGS_TAXONOMY ); // Store tags labels in the form settings. $form_data = $form_obj->get( $form_id, [ 'content_only' => true ] ); $form_data['settings']['form_tags'] = $tags_labels; $form_obj->update( $form_id, $form_data ); } } } Admin/Forms/Ajax/Columns.php 0000644 00000004261 15174710275 0011712 0 ustar 00 <?php namespace WPForms\Admin\Forms\Ajax; use WPForms\Admin\Forms\Table\Facades; /** * Columns AJAX actions on Forms Overview list page. * * @since 1.8.6 */ class Columns { /** * Determine if the class is allowed to load. * * @since 1.8.6 * * @return bool */ private function allow_load(): bool { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $action = isset( $_REQUEST['action'] ) ? sanitize_key( wp_unslash( $_REQUEST['action'] ) ) : ''; // Load only in the case of AJAX calls on Forms Overview page. return wpforms_is_admin_ajax() && strpos( $action, 'wpforms_admin_forms_overview_' ) === 0; } /** * Initialize class. * * @since 1.8.6 */ public function init(): void { if ( ! $this->allow_load() ) { return; } $this->hooks(); } /** * Hooks. * * @since 1.8.6 */ private function hooks(): void { add_action( 'wp_ajax_wpforms_admin_forms_overview_save_columns_order', [ $this, 'save_order' ] ); } /** * Save columns' order. * * @since 1.8.6 */ public function save_order(): void { check_ajax_referer( 'wpforms-admin', 'nonce' ); if ( ! wpforms_current_user_can( 'view_forms' ) ) { wp_send_json_error( esc_html__( 'You do not have permission to perform this action.', 'wpforms-lite' ) ); } $data = $this->get_prepared_data(); // Prepare the new columns' order. $columns = []; foreach ( $data['columns'] as $column ) { $columns[] = str_replace( '-foot', '', $column ); } $result = Facades\Columns::sanitize_and_save_columns( $columns ); if ( $result === false ) { wp_send_json_error( esc_html__( 'Cannot save columns order.', 'wpforms-lite' ) ); } wp_send_json_success(); } /** * Get prepared data before perform ajax action. * * @since 1.8.6 * * @return array */ private function get_prepared_data(): array { // Run a security check. if ( ! check_ajax_referer( 'wpforms-admin', 'nonce', false ) ) { wp_send_json_error( esc_html__( 'Most likely, your session expired. Please reload the page.', 'wpforms-lite' ) ); } return [ 'columns' => ! empty( $_POST['columns'] ) ? map_deep( (array) wp_unslash( $_POST['columns'] ), 'sanitize_key' ) : [], ]; } } Admin/Forms/Table/DataObjects/Column.php 0000644 00000000305 15174710275 0014051 0 ustar 00 <?php namespace WPForms\Admin\Forms\Table\DataObjects; use WPForms\Admin\Base\Tables\DataObjects\ColumnBase; /** * Column data object. * * @since 1.8.6 */ class Column extends ColumnBase {} Admin/Forms/Table/Facades/Columns.php 0000644 00000017411 15174710275 0013405 0 ustar 00 <?php namespace WPForms\Admin\Forms\Table\Facades; use WPForms\Admin\Base\Tables\Facades\ColumnsBase; use WPForms\Admin\Forms\Table\DataObjects\Column; use WPForms\Integrations\LiteConnect\LiteConnect; /** * Column facade class. * * Hides the complexity of columns' collection behind a simple interface. * * @since 1.8.6 */ class Columns extends ColumnsBase { /** * Saved columns order user meta name. * * @since 1.8.6 */ const COLUMNS_USER_META_NAME = 'wpforms_overview_table_columns'; /** * Legacy saved columns order user meta name. * * @since 1.8.6 */ const LEGACY_COLUMNS_USER_META_NAME = 'managetoplevel_page_wpforms-overviewcolumnshidden'; /** * Get columns. * * Returns all possible columns for the Forms table. * * @since 1.8.6 * * @return Column[] Array of columns as objects. */ protected static function get_all(): array { static $columns = null; if ( ! $columns ) { $columns = self::get_columns(); } return $columns; } /** * Get forms' list table columns. * * @since 1.8.6 * * @return Column[] Array of columns as objects. */ public static function get_columns(): array { $columns_data = [ 'id' => [ 'label' => esc_html__( 'ID', 'wpforms-lite' ), ], 'name' => [ 'label' => esc_html__( 'Name', 'wpforms-lite' ), 'readonly' => true, ], 'tags' => [ 'label' => esc_html__( 'Tags', 'wpforms-lite' ), ], 'author' => [ 'label' => esc_html__( 'Author', 'wpforms-lite' ), ], 'shortcode' => [ 'label' => esc_html__( 'Shortcode', 'wpforms-lite' ), ], 'created' => [ 'label' => esc_html__( 'Date', 'wpforms-lite' ), ], 'entries' => [ 'label' => esc_html__( 'Entries', 'wpforms-lite' ), ], ]; // In Lite, we should not show Entries column if Lite Connect is not enabled. if ( ! wpforms()->is_pro() && ! ( LiteConnect::is_allowed() && LiteConnect::is_enabled() ) ) { unset( $columns_data['entries'] ); } /** * Filters the forms overview table columns data. * * @since 1.8.6 * * @param array[] $columns Columns data. */ $columns_data = apply_filters( 'wpforms_admin_forms_table_facades_columns_data', $columns_data ); $columns_data = self::set_columns_data_defaults( $columns_data ); $columns = []; foreach ( $columns_data as $id => $column ) { $columns[ $id ] = new Column( $id, $column ); } return $columns; } /** * Get columns' keys for the columns which user selected to be displayed. * * It returns an array of keys in the order they should be displayed. * It returns draggable and non-draggable columns. * * @since 1.8.6 * * @return array */ public static function get_selected_columns_keys(): array { $user_id = get_current_user_id(); $user_meta_columns = get_user_meta( $user_id, self::COLUMNS_USER_META_NAME, true ); $user_meta_legacy_columns_hidden = get_user_meta( $user_id, self::LEGACY_COLUMNS_USER_META_NAME, true ); $user_meta_columns = $user_meta_columns ? $user_meta_columns : []; $user_meta_legacy_columns_hidden = $user_meta_legacy_columns_hidden ? $user_meta_legacy_columns_hidden : []; // Make form id column hidden by default. if ( empty( $user_meta_columns ) ) { $user_meta_legacy_columns_hidden[] = 'id'; } // Always include readonly columns. $user_meta_columns = array_unique( array_merge( $user_meta_columns, self::get_readonly_columns_keys() ) ); if ( ! empty( $user_meta_columns ) && empty( $user_meta_legacy_columns_hidden ) ) { return $user_meta_columns; } // If custom order is not saved, let's check if there is a legacy user meta-option. // It is a kind of migration from legacy user meta-option to the new one. $user_meta_columns = array_diff( array_keys( self::get_all() ), $user_meta_legacy_columns_hidden ); // Update user meta option. if ( update_user_meta( $user_id, self::COLUMNS_USER_META_NAME, $user_meta_columns ) ) { // Remove legacy user meta-option. delete_user_meta( $user_id, self::LEGACY_COLUMNS_USER_META_NAME ); } return $user_meta_columns; } /** * Get draggable columns ordered keys. * * It will return custom order if user has already saved it, otherwise it will return default order. * * @since 1.8.6 * * @return array */ private static function get_draggable_ordered_keys(): array { // First, let's check if user has already saved custom order. $custom_order = self::get_selected_columns_keys(); $all_columns = self::get_all(); if ( $custom_order ) { // If a user has saved custom order, let's filter out columns which are not draggable. return array_filter( $custom_order, static function ( $column ) use ( $all_columns ) { return isset( $all_columns[ $column ] ) && $all_columns[ $column ]->is_draggable(); } ); } // If a user has not saved custom order, let's use the default order. $draggable = array_filter( $all_columns, static function ( $column ) { return $column->is_draggable(); } ); return array_keys( $draggable ); } /** * Save columns keys array into user meta. * * @since 1.8.6 * * @param array $columns_keys Array of columns keys in desired display order. * * @return bool */ public static function sanitize_and_save_columns( array $columns_keys ): bool { $columns_keys = array_map( [ __CLASS__, 'sanitize_column_key' ], $columns_keys ); $columns_keys = array_filter( $columns_keys, [ __CLASS__, 'validate_column_key' ] ); // Add readonly columns. $columns_keys = array_unique( array_merge( $columns_keys, self::get_readonly_columns_keys() ) ); $user_id = get_current_user_id(); $user_meta_columns = get_user_meta( $user_id, self::COLUMNS_USER_META_NAME, true ); // If user has already saved custom order, let's check if it has been changed. if ( $user_meta_columns === $columns_keys ) { return true; } // Update user meta option. return update_user_meta( $user_id, self::COLUMNS_USER_META_NAME, $columns_keys ); } /** * Sanitize column key. * * @since 1.8.6 * * @param string $key Column key. * * @return string */ public static function sanitize_column_key( string $key ): string { return sanitize_key( $key ); } /** * Get columns' data ready to use in the list table object. * * @since 1.8.6 * * @return array */ public static function get_list_table_columns(): array { $columns = [ 'cb' => '<input type="checkbox" />', ]; $order = self::get_draggable_ordered_keys(); $all_columns = self::get_all(); foreach ( $order as $column_id ) { $columns[ $column_id ] = $all_columns[ $column_id ]->get_label_html(); } /** * Filters the forms overview table columns. * * @since 1.0.0 * * @param array $columns Columns data. */ $columns = apply_filters( 'wpforms_overview_table_columns', $columns ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName // Add empty column for the cog icon. $columns['cog'] = ''; return $columns; } /** * Get readonly columns keys. * * @since 1.8.6 * * @return array */ private static function get_readonly_columns_keys(): array { $readonly = array_filter( self::get_all(), static function ( $column ) { return $column->is_readonly(); } ); return array_keys( $readonly ); } /** * Set columns data defaults. * * @since 1.8.6 * * @param array $columns_data Columns data. * * @return array */ private static function set_columns_data_defaults( array $columns_data ): array { return array_map( static function ( $column ) { $column['type'] = $column['type'] ?? ''; $column['draggable'] = $column['draggable'] ?? true; $column['label_html'] = $column['label_html'] ?? ''; $column['readonly'] = $column['readonly'] ?? false; return $column; }, $columns_data ); } } Admin/Forms/BulkActions.php 0000644 00000026376 15174710275 0011640 0 ustar 00 <?php namespace WPForms\Admin\Forms; use WPForms\Admin\Notice; /** * Bulk actions on All Forms page. * * @since 1.7.3 */ class BulkActions { /** * Allowed actions. * * @since 1.7.3 * * @const array */ const ALLOWED_ACTIONS = [ 'trash', 'restore', 'delete', 'duplicate', 'empty_trash', ]; /** * Forms ids. * * @since 1.7.3 * * @var array */ private $ids; /** * Current action. * * @since 1.7.3 * * @var string */ private $action; /** * Current view. * * @since 1.7.3 * * @var string */ private $view; /** * Determine if the class is allowed to load. * * @since 1.7.3 * * @return bool */ private function allow_load() { // Load only on the `All Forms` admin page. return wpforms_is_admin_page( 'overview' ); } /** * Initialize class. * * @since 1.7.3 */ public function init() { if ( ! $this->allow_load() ) { return; } $this->view = wpforms()->obj( 'forms_views' )->get_current_view(); $this->hooks(); } /** * Hooks. * * @since 1.7.3 */ private function hooks() { add_action( 'load-toplevel_page_wpforms-overview', [ $this, 'notices' ] ); add_action( 'load-toplevel_page_wpforms-overview', [ $this, 'process' ] ); add_filter( 'removable_query_args', [ $this, 'removable_query_args' ] ); } /** * Process the bulk actions. * * @since 1.7.3 */ public function process() { // phpcs:disable WordPress.Security.NonceVerification.Recommended $this->ids = isset( $_GET['form_id'] ) ? array_map( 'absint', (array) $_GET['form_id'] ) : []; $this->action = isset( $_REQUEST['action'] ) ? sanitize_key( $_REQUEST['action'] ) : false; if ( $this->action === '-1' ) { $this->action = ! empty( $_REQUEST['action2'] ) ? sanitize_key( $_REQUEST['action2'] ) : false; } // phpcs:enable WordPress.Security.NonceVerification.Recommended if ( empty( $this->ids ) || empty( $this->action ) ) { return; } // Check exact action values. if ( ! in_array( $this->action, self::ALLOWED_ACTIONS, true ) ) { return; } if ( empty( $_GET['_wpnonce'] ) ) { return; } // Check the nonce. if ( ! wp_verify_nonce( sanitize_key( $_GET['_wpnonce'] ), 'bulk-forms' ) && ! wp_verify_nonce( sanitize_key( $_GET['_wpnonce'] ), 'wpforms_' . $this->action . '_form_nonce' ) ) { return; } // Finally, we can process the action. $this->process_action(); } /** * Process action. * * @since 1.7.3 * * @uses process_action_trash * @uses process_action_restore * @uses process_action_delete * @uses process_action_duplicate * @uses process_action_empty_trash */ private function process_action() { $method = "process_action_{$this->action}"; // Check that we have a method for this action. if ( ! method_exists( $this, $method ) ) { return; } if ( empty( $this->ids ) || ! is_array( $this->ids ) ) { return; } $query_args = []; if ( count( $this->ids ) === 1 ) { $query_args['type'] = wpforms_is_form_template( $this->ids[0] ) ? 'template' : 'form'; } $result = []; foreach ( $this->ids as $id ) { $result[ $id ] = $this->$method( $id ); } $count_result = count( array_keys( array_filter( $result ) ) ); // Empty trash action returns count of deleted forms. if ( $method === 'process_action_empty_trash' ) { $count_result = $result[1] ?? 0; } $query_args[ rtrim( $this->action, 'e' ) . 'ed' ] = $count_result; // Unset get vars and perform redirect to avoid action reuse. wp_safe_redirect( add_query_arg( $query_args, remove_query_arg( [ 'action', 'action2', '_wpnonce', 'form_id', 'paged', '_wp_http_referer' ] ) ) ); exit; } /** * Trash the form. * * @since 1.7.3 * * @param int $id Form ID to trash. * * @return bool */ private function process_action_trash( $id ) { return wpforms()->obj( 'form' )->update_status( $id, 'trash' ); } /** * Restore the form. * * @since 1.7.3 * * @param int $id Form ID to restore from trash. * * @return bool */ private function process_action_restore( $id ) { return wpforms()->obj( 'form' )->update_status( $id, 'publish' ); } /** * Delete the form. * * @since 1.7.3 * * @param int $id Form ID to delete. * * @return bool */ private function process_action_delete( $id ) { return wpforms()->obj( 'form' )->delete( $id ); } /** * Duplicate the form. * * @since 1.7.3 * * @param int $id Form ID to duplicate. * * @return bool */ private function process_action_duplicate( $id ) { if ( ! wpforms_current_user_can( 'create_forms' ) ) { return false; } if ( ! wpforms_current_user_can( 'view_form_single', $id ) ) { return false; } return wpforms()->obj( 'form' )->duplicate( $id ); } /** * Empty trash. * * @since 1.7.3 * * @param int $id Form ID. This parameter is not used in this method, * but we need to keep it here because all the `process_action_*` methods * should be called with the $id parameter. * * @return bool */ private function process_action_empty_trash( $id ) { // Empty trash is actually the "delete all forms in trash" action. // So, after the execution we should display the same notice as for the `delete` action. $this->action = 'delete'; return wpforms()->obj( 'form' )->empty_trash(); } /** * Define bulk actions available for forms overview table. * * @since 1.7.3 * * @return array */ public function get_dropdown_items() { $items = []; if ( wpforms_current_user_can( 'delete_forms' ) ) { if ( $this->view === 'trash' ) { $items = [ 'restore' => esc_html__( 'Restore', 'wpforms-lite' ), 'delete' => esc_html__( 'Delete Permanently', 'wpforms-lite' ), ]; } else { $items = [ 'trash' => esc_html__( 'Move to Trash', 'wpforms-lite' ), ]; } } // phpcs:disable WPForms.Comments.ParamTagHooks.InvalidParamTagsQuantity /** * Filters the Bulk Actions dropdown items. * * @since 1.7.5 * * @param array $items Dropdown items. */ $items = apply_filters( 'wpforms_admin_forms_bulk_actions_get_dropdown_items', $items ); // phpcs:enable WPForms.Comments.ParamTagHooks.InvalidParamTagsQuantity if ( empty( $items ) ) { // We should have dummy item, otherwise, WP will hide the Bulk Actions Dropdown, // which is not good from a design point of view. return [ '' => '—', ]; } return $items; } /** * Admin notices. * * @since 1.7.3 */ public function notices() { // phpcs:disable WordPress.Security.NonceVerification $results = [ 'trashed' => ! empty( $_REQUEST['trashed'] ) ? sanitize_key( $_REQUEST['trashed'] ) : false, 'restored' => ! empty( $_REQUEST['restored'] ) ? sanitize_key( $_REQUEST['restored'] ) : false, 'deleted' => ! empty( $_REQUEST['deleted'] ) ? sanitize_key( $_REQUEST['deleted'] ) : false, 'duplicated' => ! empty( $_REQUEST['duplicated'] ) ? sanitize_key( $_REQUEST['duplicated'] ) : false, 'type' => ! empty( $_REQUEST['type'] ) ? sanitize_key( $_REQUEST['type'] ) : 'form', ]; // phpcs:enable WordPress.Security.NonceVerification // Display notice in case of error. if ( in_array( 'error', $results, true ) ) { Notice::add( esc_html__( 'Security check failed. Please try again.', 'wpforms-lite' ), 'error' ); return; } $this->notices_success( $results ); } /** * Admin success notices. * * @since 1.7.3 * * @param array $results Action results data. */ private function notices_success( array $results ) { $type = $results['type'] ?? ''; if ( ! in_array( $type, [ 'form', 'template' ], true ) ) { return; } $method = "get_notice_success_for_{$type}"; $actions = [ 'trashed', 'restored', 'deleted', 'duplicated' ]; foreach ( $actions as $action ) { $count = (int) $results[ $action ]; if ( ! $count ) { continue; } $notice = $this->$method( $action, $count ); if ( ! $notice ) { continue; } Notice::add( $notice, 'info' ); } } /** * Remove certain arguments from a query string that WordPress should always hide for users. * * @since 1.7.3 * * @param array $removable_query_args An array of parameters to remove from the URL. * * @return array Extended/filtered array of parameters to remove from the URL. */ public function removable_query_args( $removable_query_args ) { $removable_query_args[] = 'trashed'; $removable_query_args[] = 'restored'; $removable_query_args[] = 'deleted'; $removable_query_args[] = 'duplicated'; return $removable_query_args; } /** * Get notice success message for form. * * @since 1.9.2.3 * * @param string $action Action type. * @param int $count Count of forms. * * @return string * @noinspection PhpUnusedPrivateMethodInspection */ private function get_notice_success_for_form( string $action, int $count ): string { switch ( $action ) { case 'restored': /* translators: %1$d - restored forms count. */ $notice = _n( '%1$d form was successfully restored.', '%1$d forms were successfully restored.', $count, 'wpforms-lite' ); break; case 'deleted': /* translators: %1$d - deleted forms count. */ $notice = _n( '%1$d form was successfully permanently deleted.', '%1$d forms were successfully permanently deleted.', $count, 'wpforms-lite' ); break; case 'duplicated': /* translators: %1$d - duplicated forms count. */ $notice = _n( '%1$d form was successfully duplicated.', '%1$d forms were successfully duplicated.', $count, 'wpforms-lite' ); break; case 'trashed': /* translators: %1$d - trashed forms count. */ $notice = _n( '%1$d form was successfully moved to Trash.', '%1$d forms were successfully moved to Trash.', $count, 'wpforms-lite' ); break; default: // phpcs:ignore WPForms.Formatting.EmptyLineBeforeReturn.AddEmptyLineBeforeReturnStatement return ''; } return sprintf( $notice, $count ); } /** * Get notice success message for template. * * @since 1.9.2.3 * * @param string $action Action type. * @param int $count Count of forms. * * @return string * @noinspection PhpUnusedPrivateMethodInspection */ private function get_notice_success_for_template( string $action, int $count ): string { switch ( $action ) { case 'restored': /* translators: %1$d - restored templates count. */ $notice = _n( '%1$d template was successfully restored.', '%1$d templates were successfully restored.', $count, 'wpforms-lite' ); break; case 'deleted': /* translators: %1$d - deleted templates count. */ $notice = _n( '%1$d template was successfully permanently deleted.', '%1$d templates were successfully permanently deleted.', $count, 'wpforms-lite' ); break; case 'duplicated': /* translators: %1$d - duplicated templates count. */ $notice = _n( '%1$d template was successfully duplicated.', '%1$d templates were successfully duplicated.', $count, 'wpforms-lite' ); break; case 'trashed': /* translators: %1$d - trashed templates count. */ $notice = _n( '%1$d template was successfully moved to Trash.', '%1$d templates were successfully moved to Trash.', $count, 'wpforms-lite' ); break; default: // phpcs:ignore WPForms.Formatting.EmptyLineBeforeReturn.AddEmptyLineBeforeReturnStatement return ''; } return sprintf( $notice, $count ); } } Admin/Forms/ListTable.php 0000644 00000040270 15174710275 0011272 0 ustar 00 <?php namespace WPForms\Admin\Forms; use WP_List_Table; use WP_Post; use WP_Screen; use WPForms\Admin\Forms\Table\Facades\Columns; use WPForms\Forms\Locator; use WPForms\Integrations\LiteConnect\LiteConnect; use WPForms\Integrations\LiteConnect\Integration as LiteConnectIntegration; // IMPORTANT NOTICE: // This line is needed to prevent fatal errors in the third-party plugins. // We know about Jetpack (probably others also) can load WP classes during cron jobs or something similar. require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php'; /** * Generate the table on the plugin overview page. * * @since 1.8.6 */ class ListTable extends WP_List_Table { /** * Number of forms to show per page. * * @since 1.8.6 * * @var int */ public $per_page; /** * Number of forms in different views. * * @since 1.8.6 * * @var array */ private $count; /** * Current view. * * @since 1.8.6 * * @var string */ private $view; /** * Primary class constructor. * * @since 1.8.6 */ public function __construct() { // Utilize the parent constructor to build the main class properties. parent::__construct( [ 'singular' => 'form', 'plural' => 'forms', 'ajax' => false, ] ); $this->hooks(); // Determine the current view. $this->view = wpforms()->obj( 'forms_views' )->get_current_view(); /** * Filters the default number of forms to show per page. * * @since 1.0.0 * * @param int $forms_per_page Number of forms to show per page. */ $this->per_page = (int) apply_filters( 'wpforms_overview_per_page', 20 ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName } /** * Register hooks. * * @since 1.8.6 */ private function hooks() { add_filter( 'default_hidden_columns', [ $this, 'default_hidden_columns' ], 10, 2 ); } /** * Get the instance of a class and store it in itself. * * @since 1.8.6 */ public static function get_instance() { static $instance; if ( ! $instance ) { $instance = new self(); } return $instance; } /** * Retrieve the table columns. * * @since 1.8.6 * * @return array $columns Array of all the list table columns. */ public function get_columns() { return Columns::get_list_table_columns(); } /** * Render the checkbox column. * * @since 1.8.6 * * @param WP_Post $form Form. * * @return string */ public function column_cb( $form ) { return '<input type="checkbox" name="form_id[]" value="' . absint( $form->ID ) . '" />'; } /** * Render the columns. * * @since 1.8.6 * * @param WP_Post $form CPT object as a form representation. * @param string $column_name Column Name. * * @return string */ public function column_default( $form, $column_name ) { // phpcs:ignore Generic.Metrics.CyclomaticComplexity switch ( $column_name ) { case 'id': $value = $form->ID; break; case 'shortcode': $value = '[wpforms id="' . $form->ID . '"]'; if ( wpforms_is_form_template( $form->ID ) ) { $value = __( 'N/A', 'wpforms-lite' ); } break; // This slug is not changed to 'date' for backward compatibility. case 'created': if ( gmdate( 'Ymd', strtotime( $form->post_date ) ) === gmdate( 'Ymd', strtotime( $form->post_modified ) ) ) { $value = wp_kses( sprintf( /* translators: %1$s - Post created date. */ __( 'Created<br/>%1$s', 'wpforms-lite' ), esc_html( wpforms_datetime_format( $form->post_date ) ) ), [ 'br' => [] ] ); } else { $value = wp_kses( sprintf( /* translators: %1$s - Post modified date. */ __( 'Last Modified<br/>%1$s', 'wpforms-lite' ), esc_html( wpforms_datetime_format( $form->post_modified ) ) ), [ 'br' => [] ] ); } break; case 'entries': $value = sprintf( '<span class="wpforms-lite-connect-entries-count"><a href="%s" data-title="%s">%s%d</a></span>', esc_url( admin_url( 'admin.php?page=wpforms-entries' ) ), esc_attr__( 'Entries are securely backed up in the cloud. Upgrade to restore.', 'wpforms-lite' ), '<svg viewBox="0 0 16 12"><path d="M10.8 2c1.475 0 2.675 1.175 2.775 2.625C15 5.125 16 6.475 16 8a3.6 3.6 0 0 1-3.6 3.6H4a3.98 3.98 0 0 1-4-4 4.001 4.001 0 0 1 2.475-3.7A4.424 4.424 0 0 1 6.8.4c1.4 0 2.625.675 3.425 1.675C10.4 2.025 10.6 2 10.8 2ZM4 10.4h8.4a2.4 2.4 0 0 0 0-4.8.632.632 0 0 0-.113.013.678.678 0 0 1-.112.012c.125-.25.225-.525.225-.825 0-.875-.725-1.6-1.6-1.6a1.566 1.566 0 0 0-1.05.4 3.192 3.192 0 0 0-2.95-2 3.206 3.206 0 0 0-3.2 3.2v.05A2.757 2.757 0 0 0 1.2 7.6 2.795 2.795 0 0 0 4 10.4Zm6.752-4.624a.64.64 0 1 0-.905-.905L6.857 7.86 5.38 6.352a.64.64 0 1 0-.914.896l1.93 1.97a.64.64 0 0 0 .91.004l3.446-3.446Z"/></svg>', LiteConnectIntegration::get_form_entries_count( $form->ID ) ); break; case 'modified': $value = get_post_modified_time( get_option( 'date_format' ), false, $form ); break; case 'author': $value = ''; $author = get_userdata( $form->post_author ); if ( ! $author ) { break; } $value = $author->display_name; $user_edit_url = get_edit_user_link( $author->ID ); if ( ! empty( $user_edit_url ) ) { $value = '<a href="' . esc_url( $user_edit_url ) . '">' . esc_html( $value ) . '</a>'; } break; case 'php': $value = '<code style="display:block;font-size:11px;">if( function_exists( \'wpforms_get\' ) ){ wpforms_get( ' . $form->ID . ' ); }</code>'; break; default: $value = ''; } /** * Filters the Forms Overview list table culumn value. * * @since 1.0.0 * * @param string $value Column value. * @param WP_Post $form CPT object as a form representation. * @param string $column_name Column Name. */ return apply_filters( 'wpforms_overview_table_column_value', $value, $form, $column_name ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName } /** * Filter the default list of hidden columns. * * @since 1.8.6 * * @param string[] $hidden Array of IDs of columns hidden by default. * @param WP_Screen $screen WP_Screen object of the current screen. * * @return string[] */ public function default_hidden_columns( $hidden, $screen ) { if ( $screen->id !== 'toplevel_page_wpforms-overview' ) { return $hidden; } if ( Columns::has_selected_columns() ) { return []; } return [ 'tags', 'author', Locator::COLUMN_NAME, ]; } /** * Render the form name column with action links. * * @since 1.8.6 * * @param WP_Post $form Form. * * @return string */ public function column_name( $form ) { $title = $this->get_column_name_title( $form ); $states = _post_states( $form, false ); $actions = $this->get_column_name_row_actions( $form ); // Build the row action links and return the value. return $title . $states . $actions; } /** * Render the form tags column. * * @since 1.8.6 * * @param WP_Post $form Form. * * @return string */ public function column_tags( $form ) { return wpforms()->obj( 'forms_tags' )->column_tags( $form ); } /** * Get the form name HTML for the form name column. * * @since 1.8.6 * * @param WP_Post $form Form object. * * @return string */ protected function get_column_name_title( $form ) { $title = ! empty( $form->post_title ) ? $form->post_title : $form->post_name; $name = sprintf( '<span><strong>%s</strong></span>', esc_html( $title ) ); if ( $this->view === 'trash' ) { return $name; } if ( wpforms_current_user_can( 'view_form_single', $form->ID ) ) { $name = sprintf( '<a href="%s" title="%s" class="row-title" target="_blank" rel="noopener noreferrer"><strong>%s</strong></a>', esc_url( wpforms_get_form_preview_url( $form->ID ) ), esc_attr__( 'View preview', 'wpforms-lite' ), esc_html( $title ) ); } if ( wpforms_current_user_can( 'view_entries_form_single', $form->ID ) ) { $name = sprintf( '<a href="%s" title="%s"><strong>%s</strong></a>', esc_url( add_query_arg( [ 'view' => 'list', 'form_id' => $form->ID, ], admin_url( 'admin.php?page=wpforms-entries' ) ) ), esc_attr__( 'View entries', 'wpforms-lite' ), esc_html( $title ) ); } if ( wpforms_current_user_can( 'edit_form_single', $form->ID ) ) { $name = sprintf( '<a href="%s" title="%s"><strong>%s</strong></a>', esc_url( add_query_arg( [ 'view' => 'fields', 'form_id' => $form->ID, ], admin_url( 'admin.php?page=wpforms-builder' ) ) ), esc_attr__( 'Edit This Form', 'wpforms-lite' ), esc_html( $title ) ); } return $name; } /** * Get the row actions HTML for the form name column. * * @since 1.8.6 * * @param WP_Post $form Form object. * * @return string */ protected function get_column_name_row_actions( $form ) { // phpcs:disable WPForms.Comments.PHPDocHooks.RequiredHookDocumentation, WPForms.PHP.ValidateHooks.InvalidHookName /** * Filters row action links on the 'All Forms' admin page. * * @since 1.0.0 * * @param array $row_actions An array of action links for a given form. * @param WP_Post $form Form object. */ return $this->row_actions( apply_filters( 'wpforms_overview_row_actions', [], $form ) ); // phpcs:enable WPForms.Comments.PHPDocHooks.RequiredHookDocumentation, WPForms.PHP.ValidateHooks.InvalidHookName } /** * Define bulk actions available for our table listing. * * @since 1.8.6 * * @return array */ public function get_bulk_actions() { return wpforms()->obj( 'forms_bulk_actions' )->get_dropdown_items(); } /** * Generate the table navigation above or below the table. * * @since 1.8.6 * * @param string $which The location of the table navigation: 'top' or 'bottom'. */ protected function display_tablenav( $which ) { // If there are some forms just call the parent method. if ( $this->has_items() ) { parent::display_tablenav( $which ); return; } // Otherwise, display bulk actions menu and "0 items" on the right (pagination). ?> <div class="tablenav <?php echo esc_attr( $which ); ?>"> <div class="alignleft actions bulkactions"> <?php $this->bulk_actions( $which ); ?> </div> <?php $this->extra_tablenav( $which ); if ( $which === 'top' ) { $this->pagination( $which ); } ?> <br class="clear" /> </div> <?php } /** * Extra controls to be displayed between bulk actions and pagination. * * @since 1.8.6 * * @param string $which The location of the table navigation: 'top' or 'bottom'. */ protected function extra_tablenav( $which ) { wpforms()->obj( 'forms_tags' )->extra_tablenav( $which, $this ); wpforms()->obj( 'forms_views' )->extra_tablenav( $which ); } /** * Message to be displayed when there are no forms. * * @since 1.8.6 */ public function no_items() { wpforms()->obj( 'forms_views' )->get_current_view() === 'templates' ? esc_html_e( 'No form templates found.', 'wpforms-lite' ) : esc_html_e( 'No forms found.', 'wpforms-lite' ); } /** * Fetch and set up the final data for the table. * * @since 1.8.6 */ public function prepare_items() { // Set up the columns. $columns = $this->get_columns(); // Hidden columns (none). $hidden = get_hidden_columns( $this->screen ); // Define which columns can be sorted - form name, author, date. $sortable = [ 'id' => [ 'ID', false ], 'name' => [ 'title', false ], 'author' => [ 'author', false ], 'created' => [ 'date', false ], ]; // Set column headers. $this->_column_headers = [ $columns, $hidden, $sortable ]; // phpcs:disable WordPress.Security.NonceVerification.Recommended $page = $this->get_pagenum(); $order = isset( $_GET['order'] ) && $_GET['order'] === 'asc' ? 'ASC' : 'DESC'; $orderby = isset( $_GET['orderby'] ) ? sanitize_key( $_GET['orderby'] ) : 'ID'; $per_page = $this->get_items_per_page( 'wpforms_forms_per_page', $this->per_page ); // phpcs:enable WordPress.Security.NonceVerification.Recommended if ( $orderby === 'date' ) { $orderby = [ 'modified' => $order, 'date' => $order, ]; } $args = [ 'orderby' => $orderby, 'order' => $order, 'nopaging' => false, 'posts_per_page' => $per_page, 'paged' => $page, 'no_found_rows' => false, 'post_status' => 'publish', ]; /** * Filters the `get_posts()` arguments while preparing items for the forms overview table. * * @since 1.7.3 * * @param array $args Arguments array. */ $args = (array) apply_filters( 'wpforms_overview_table_prepare_items_args', $args ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName // Giddy up. $this->items = wpforms()->obj( 'form' )->get( '', $args ); $per_page = $args['posts_per_page'] ?? $this->get_items_per_page( 'wpforms_forms_per_page', $this->per_page ); $this->update_count( $args ); $count_current_view = empty( $this->count[ $this->view ] ) ? 0 : $this->count[ $this->view ]; // Finalize pagination. $this->set_pagination_args( [ 'total_items' => $count_current_view, 'per_page' => $per_page, 'total_pages' => (int) ceil( $count_current_view / $per_page ), ] ); } /** * Calculate and update form counts. * * @since 1.8.6 * * @param array $args Get forms arguments. */ private function update_count( $args ) { /** * Allow counting forms filtered by a given search criteria. * * If result will not contain `all` key, count All Forms without filtering will be performed. * * @since 1.7.2 * * @param array $count Contains counts of forms in different views. * @param array $args Arguments of the `get_posts`. */ $this->count = (array) apply_filters( 'wpforms_overview_table_update_count', [], $args ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName // We do not need to perform all forms count if we have the result already. if ( isset( $this->count['all'] ) ) { return; } // Count all forms. $this->count['all'] = wpforms_current_user_can( 'wpforms_view_others_forms' ) ? (int) wp_count_posts( 'wpforms' )->publish : (int) count_user_posts( get_current_user_id(), 'wpforms', true ); /** * Filters forms count data after counting all forms. * * This filter executes only if the result of `wpforms_overview_table_update_count` filter * doesn't contain `all` key. * * @since 1.7.3 * * @param array $count Contains counts of forms in different views. * @param array $args Arguments of the `get_posts`. */ $this->count = (array) apply_filters( 'wpforms_overview_table_update_count_all', $this->count, $args ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName } /** * Display the pagination. * * @since 1.8.6 * * @param string $which The location of the table pagination: 'top' or 'bottom'. */ protected function pagination( $which ) { if ( $this->has_items() ) { parent::pagination( $which ); return; } printf( '<div class="tablenav-pages one-page"> <span class="displaying-num">%s</span> </div>', esc_html__( '0 items', 'wpforms-lite' ) ); } /** * Extending the `display_rows()` method in order to add hooks. * * @since 1.8.6 */ public function display_rows() { /** * Fires before displaying the table rows. * * @since 1.5.6.2 * * @param ListTable $list_table_obj ListTable instance. */ do_action( 'wpforms_admin_overview_before_rows', $this ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName parent::display_rows(); /** * Fires after displaying the table rows. * * @since 1.5.6.2 * * @param ListTable $list_table_obj ListTable instance. */ do_action( 'wpforms_admin_overview_after_rows', $this ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName } /** * Forms search markup. * * @since 1.8.6 * * @param string $text The 'submit' button label. * @param string $input_id ID attribute value for the search input field. */ public function search_box( $text, $input_id ) { wpforms()->obj( 'forms_search' )->search_box( $text, $input_id ); } /** * Get the list of views available on forms overview table. * * @since 1.8.6 */ protected function get_views() { return wpforms()->obj( 'forms_views' )->get_views(); } } Admin/Forms/Search.php 0000644 00000015000 15174710275 0010605 0 ustar 00 <?php namespace WPForms\Admin\Forms; /** * Search Forms feature. * * @since 1.7.2 */ class Search { /** * Current search term. * * @since 1.7.2 * * @var string */ private $term; /** * Current search term escaped. * * @since 1.7.2 * * @var string */ private $term_escaped; /** * Determine if the class is allowed to load. * * @since 1.7.2 * * @return bool */ private function allow_load() { // Load only on the `All Forms` admin page and only if the search should be performed. return wpforms_is_admin_page( 'overview' ) && $this->is_search(); } /** * Initialize class. * * @since 1.7.2 */ public function init() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $this->term = isset( $_GET['search']['term'] ) ? sanitize_text_field( wp_unslash( $_GET['search']['term'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $this->term_escaped = isset( $_GET['search']['term'] ) ? esc_html( wp_unslash( $_GET['search']['term'] ) ) : ''; if ( ! $this->allow_load() ) { return; } $this->hooks(); } /** * Hooks. * * @since 1.7.2 */ private function hooks() { // Use filter to add the search term to the get forms arguments. add_filter( 'wpforms_get_multiple_forms_args', [ $this, 'get_forms_args' ] ); // Encapsulate search into posts_where. add_action( 'wpforms_form_handler_get_multiple_before_get_posts', [ $this, 'before_get_posts' ] ); add_action( 'wpforms_form_handler_get_multiple_after_get_posts', [ $this, 'after_get_posts' ], 10, 2 ); } /** * Determine whether a search is performing. * * @since 1.7.2 * * @return bool */ private function is_search() { return ! wpforms_is_empty_string( $this->term_escaped ); } /** * Pass the search term to the arguments array. * * @since 1.7.2 * * @param array $args Get posts arguments. * * @return array */ public function get_forms_args( $args ) { if ( is_numeric( $this->term ) ) { $args['post__in'] = [ absint( $this->term ) ]; } else { $args['search']['term'] = $this->term; $args['search']['term_escaped'] = $this->term_escaped; } return $args; } /** * Before get_posts() call routine. * * @since 1.7.2 * * @param array $args Arguments of the `get_posts()`. */ public function before_get_posts( $args ) { // The `posts_where` hook is very general and has broad usage across the WP core and tons of plugins. // Therefore, in order to do not break something, // we should add this hook right before the call of `get_posts()` inside \WPForms_Form_Handler::get_multiple(). add_filter( 'posts_where', [ $this, 'search_by_term_where' ], 10, 2 ); } /** * After get_posts() call routine. * * @since 1.7.2 * * @param array $args Arguments of the get_posts(). * @param array $forms Forms data. Result of getting multiple forms. */ public function after_get_posts( $args, $forms ) { // The `posts_where` hook is very general and has broad usage across the WP core and tons of plugins. // Therefore, in order to do not break something, // we should remove this hook right after the call of `get_posts()` inside \WPForms_Form_Handler::get_multiple(). remove_filter( 'posts_where', [ $this, 'search_by_term_where' ] ); } /** * Modify the WHERE clause of the SQL query in order to search forms by given term. * * @since 1.7.2 * * @param string $where WHERE clause. * @param \WP_Query $wp_query The WP_Query instance. * * @return string */ public function search_by_term_where( $where, $wp_query ) { if ( is_numeric( $this->term ) ) { return $where; } global $wpdb; // When user types only HTML tag (<section> for example), the sanitized term we will be empty. // In this case, it's better to return an empty result set than all the forms. It's not the same as the empty search term. if ( wpforms_is_empty_string( $this->term ) && ! wpforms_is_empty_string( $this->term_escaped ) ) { $where .= ' AND 1<>1'; } if ( wpforms_is_empty_string( $this->term ) ) { return $where; } // Prepare the WHERE clause to search form title and description. $where .= $wpdb->prepare( " AND ( $wpdb->posts.post_title LIKE %s OR $wpdb->posts.post_excerpt LIKE %s )", '%' . $wpdb->esc_like( esc_html( $this->term ) ) . '%', '%' . $wpdb->esc_like( $this->term ) . '%' ); return $where; } /** * Forms search markup. * * @since 1.7.2 * * @param string $text The 'submit' button label. * @param string $input_id ID attribute value for the search input field. */ public function search_box( $text, $input_id ) { $search_term = wpforms_is_empty_string( $this->term ) ? $this->term_escaped : $this->term; // Display search reset block. $this->search_reset_block( $search_term ); // Display search box. // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo wpforms_render( 'admin/forms/search-box', [ 'term_input_id' => $input_id . '-term', 'text' => $text, 'search_term' => $search_term, ], true ); } /** * Forms search reset block. * * @since 1.7.2 * * @param string $search_term Current search term. */ private function search_reset_block( $search_term ) { if ( wpforms_is_empty_string( $search_term ) ) { return; } $views = wpforms()->obj( 'forms_views' ); $count = $views->get_count(); $view = $views->get_current_view(); $count['all'] = ! empty( $count['all'] ) ? $count['all'] : 0; $message = sprintf( wp_kses( /* translators: %1$d - number of forms found, %2$s - search term. */ _n( 'Found <strong>%1$d form</strong> containing <em>"%2$s"</em>', 'Found <strong>%1$d forms</strong> containing <em>"%2$s"</em>', (int) $count['all'], 'wpforms-lite' ), [ 'strong' => [], 'em' => [], ] ), (int) $count['all'], esc_html( $search_term ) ); /** * Filters the message in the search reset block. * * @since 1.7.3 * * @param string $message Message text. * @param string $search_term Search term. * @param array $count Count forms in different views. * @param string $view Current view. */ $message = apply_filters( 'wpforms_admin_forms_search_search_reset_block_message', $message, $search_term, $count, $view ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo wpforms_render( 'admin/forms/search-reset', [ 'message' => $message, ], true ); } } Admin/Traits/FormTemplates.php 0000644 00000044573 15174710275 0012363 0 ustar 00 <?php namespace WPForms\Admin\Traits; use WPForms\Admin\Addons\Addons; use WPForms\Admin\Builder\Templates; /** * Form Templates trait. * * @since 1.7.7 */ trait FormTemplates { /** * Addons data handler class instance. * * @since 1.7.7 * * @var Addons */ private $addons_obj; /** * Is addon templates available? * * @since 1.7.7 * * @var bool */ private $is_addon_templates_available = false; /** * Is custom templates available? * * @since 1.7.7 * * @var bool */ private $is_custom_templates_available = false; /** * Prepared templates list. * * @since 1.7.7 * * @var array */ private $prepared_templates = []; /** * Output templates content section. * * @since 1.7.7 */ private function output_templates_content() { $templates_hash = wpforms()->obj( 'builder_templates' )->get_hash(); $templates_hash_option = get_option( Templates::TEMPLATES_HASH_OPTION, '' ); // Compare the current hash and the previous one to detect changes in the template list. if ( $templates_hash !== $templates_hash_option ) { // Update the hash in the option. update_option( Templates::TEMPLATES_HASH_OPTION, $templates_hash ); // Wipe both caches - for the admin page and for the Form Builder. wpforms()->obj( 'builder_templates_cache' )->wipe_content_cache(); } // Attempt to get cached content. $content = wpforms()->obj( 'builder_templates_cache' )->get_content_cache(); if ( empty( $content ) ) { $content = $this->generate_templates_content_cache(); } // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo $content; } /** * Generate and save cached templates content. * * @since 1.8.6 * * @retur string */ public function generate_templates_content_cache() { $this->prepare_templates_data(); ob_start(); ?> <div class="wpforms-setup-templates"> <div class="wpforms-setup-templates-sidebar"> <div class="wpforms-setup-templates-search-wrap"> <i class="fa fa-search"></i> <label> <input type="text" id="wpforms-setup-template-search" value="" placeholder="<?php esc_attr_e( 'Search Templates', 'wpforms-lite' ); ?>"> </label> </div> <ul class="wpforms-setup-templates-categories"> <?php $this->template_categories(); ?> </ul> </div> <div id="wpforms-setup-templates-list"> <div class="list"> <?php $this->template_select_options(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> </div> <div class="wpforms-templates-no-results"> <p> <?php esc_html_e( "Sorry, we didn't find any templates that match your criteria.", 'wpforms-lite' ); ?> </p> </div> <div class="wpforms-user-templates-empty-state wpforms-hidden"> <?php $this->user_template_empty_state(); ?> </div> </div> </div> <?php $content = ob_get_clean(); wpforms()->obj( 'builder_templates_cache' )->save_content_cache( $content ); return $content; } /** * Prepare templates data for output. * * @since 1.7.7 */ private function prepare_templates_data() { $templates = wpforms()->obj( 'builder_templates' )->get_templates(); if ( empty( $templates ) ) { return; } wpforms()->obj( 'builder_templates' )->update_favorites_list(); // Loop through each available template. foreach ( $templates as $id => $template ) { $this->prepared_templates[ $id ] = $this->prepare_template_render_arguments( $template ); } } /** * Generate and display categories menu. * * @since 1.7.7 */ private function template_categories() { $templates_count = $this->get_count_in_categories(); $generic_categories = [ 'all' => esc_html__( 'All Templates', 'wpforms-lite' ), ]; if ( isset( $templates_count['all'], $templates_count['available'] ) && $templates_count['all'] !== $templates_count['available'] ) { $generic_categories['available'] = esc_html__( 'Available Templates', 'wpforms-lite' ); } $generic_categories['favorites'] = esc_html__( 'Favorite Templates', 'wpforms-lite' ); $generic_categories['new'] = esc_html__( 'New Templates', 'wpforms-lite' ); $generic_categories['user'] = esc_html__( 'My Templates', 'wpforms-lite' ); $this->output_categories( $generic_categories, $templates_count ); printf( '<li class="divider"></li>' ); $common_categories = []; if ( $this->is_custom_templates_available ) { $common_categories['custom'] = esc_html__( 'Custom Templates', 'wpforms-lite' ); } if ( $this->is_addon_templates_available ) { $common_categories['addons'] = esc_html__( 'Addon Templates', 'wpforms-lite' ); } $categories = array_merge( $common_categories, wpforms()->obj( 'builder_templates' )->get_categories() ); $this->output_categories( $categories, $templates_count ); } /** * Output categories list. * * @since 1.7.7 * * @param array $categories Categories list. * @param array $templates_count Templates count by categories. * * @noinspection HtmlUnknownAttribute*/ private function output_categories( $categories, $templates_count ) { $all_subcategories = wpforms()->obj( 'builder_templates' )->get_subcategories(); foreach ( $categories as $slug => $name ) { $class = ''; if ( $slug === 'all' ) { $class = 'class="active"'; } elseif ( empty( $templates_count[ $slug ] ) && $slug !== 'user' ) { // WPForms user templates are always available. $class = 'class="wpforms-hidden"'; } $count = $templates_count[ $slug ] ?? '0'; printf( '<li data-category="%1$s" %2$s data-count="%4$s"><div>%3$s<span>%4$s</span><i class="fa fa-chevron-down chevron"></i></div>%5$s</li>', esc_attr( $slug ), $class, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped esc_html( $name ), esc_html( $count ), $this->output_subcategories( $all_subcategories, $slug, $templates_count['subcategories'] ) // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ); } } /** * Output subcategories list. * * @since 1.8.4 * * @param array $all_subcategories Subcategories list. * @param array $parent_slug Parent category slug. * @param array $subcategories_count Subcategories count. */ private function output_subcategories( $all_subcategories, $parent_slug, $subcategories_count ) { $subcategories = []; $output = ''; foreach ( $all_subcategories as $subcategory_slug => $subcategory ) { if ( $subcategory['parent'] === $parent_slug ) { $subcategories[ $subcategory_slug ] = $subcategory; } } if ( ! empty( $subcategories ) ) { $output .= '<ul class="wpforms-setup-templates-subcategories">'; foreach ( $subcategories as $slug => $subcategory ) { $count = $subcategories_count[ $slug ] ?? '0'; $output .= sprintf( '<li data-subcategory="%1$s"><span>%2$s</span><span>%3$s</span></li>', esc_attr( $slug ), esc_html( $subcategory['name'] ), esc_html( $count ) ); } $output .= '</ul>'; } return $output; } /** * Generate a block of templates to choose from. * * @since 1.7.7 * * @param array $templates Deprecated. * @param string $slug Deprecated. * * @noinspection PhpMissingParamTypeInspection * @noinspection PhpUnusedParameterInspection */ public function template_select_options( $templates = [], $slug = '' ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed /** * Action hook before the list of form templates. * * @since 1.9.3 * * @param array $templates List of form templates. */ do_action( 'wpforms_admin_form_templates_list_before', $templates ); /** * Filter the number of templates to display. * * Useful for speeding up the setup panel loading while debugging. * * @since 1.9.2 * * @param int|bool $limit Number of templates to display. */ $limit = apply_filters( 'wpforms_builder_setup_templates_limit', false ); if ( $limit ) { $this->prepared_templates = array_slice( $this->prepared_templates, 0, (int) $limit, true ); } foreach ( $this->prepared_templates as $template ) { echo wpforms_render( // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped 'builder/templates-item', $template, true ); } } /** * Output user templates empty state. * * @since 1.8.8 */ private function user_template_empty_state() { // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo wpforms_render( 'admin/empty-states/no-user-templates' ); } /** * Prepare arguments for rendering template item. * * @since 1.7.7 * * @param array $template Template data. * * @return array Arguments. */ private function prepare_template_render_arguments( $template ) { // phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh $template['plugin_dir'] = $template['plugin_dir'] ?? ''; $template['source'] = $this->get_template_source( $template ); $template['url'] = ! empty( $template['url'] ) ? $template['url'] : ''; $template['has_access'] = ! empty( $template['license'] ) ? $template['has_access'] : true; $template['favorite'] = $template['favorite'] ?? wpforms()->obj( 'builder_templates' )->is_favorite( $template['slug'] ); $args = []; $args['template_id'] = ! empty( $template['id'] ) ? $template['id'] : $template['slug']; $args['categories'] = $this->get_template_categories( $template ); $args['subcategories'] = $this->get_template_subcategories( $template ); $args['fields'] = $this->get_template_fields( $template ); $args['demo_url'] = ''; if ( ! empty( $template['url'] ) ) { $medium = wpforms_is_admin_page( 'templates' ) ? 'Form Templates Subpage' : 'builder-templates'; $args['demo_url'] = wpforms_utm_link( $template['url'], $medium, $template['name'] ); } $template_license = ! empty( $template['license'] ) ? $template['license'] : ''; $template_name = sprintf( /* translators: %s - form template name. */ esc_html__( '%s template', 'wpforms-lite' ), esc_html( $template['name'] ) ); $args['badge_text'] = ''; $args['license_class'] = ''; $args['education_class'] = ''; $args['education_attributes'] = ''; if ( $template['source'] === 'wpforms-addon' ) { $args['badge_text'] = esc_html__( 'Addon', 'wpforms-lite' ); // At least one addon template available. $this->is_addon_templates_available = true; } if ( $template['source'] === 'wpforms-custom' ) { $args['badge_text'] = esc_html__( 'Custom', 'wpforms-lite' ); // At least one custom template available. $this->is_custom_templates_available = true; } $args['create_url'] = ''; $args['edit_url'] = ''; $args['edit_action_text'] = ''; $args['is_open'] = false; $args['can_create'] = wpforms_current_user_can( 'create_forms' ); $args['can_edit'] = wpforms_current_user_can( 'edit_forms' ); $args['can_delete'] = wpforms_current_user_can( 'delete_forms' ); $args['post_id'] = ! empty( $template['post_id'] ) ? $template['post_id'] : ''; if ( $template['source'] === 'wpforms-user-template' ) { $args['create_url'] = esc_url( $template['create_url'] ); $args['edit_url'] = esc_url( $template['edit_url'] ); $args['edit_action_text'] = $template['edit_action_text']; // phpcs:ignore WordPress.Security.NonceVerification.Recommended $args['is_open'] = wpforms_is_admin_page( 'builder' ) && isset( $_GET['form_id'] ) && (int) $_GET['form_id'] === $template['post_id']; $ownership = get_current_user_id() === (int) get_post_field( 'post_author', $args['post_id'] ) ? 'own' : 'others'; $args['can_edit'] = wpforms_current_user_can( "edit_{$ownership}_forms", $args['post_id'] ); $args['can_delete'] = wpforms_current_user_can( "delete_{$ownership}_forms", $args['post_id'] ); } $args['action_text'] = $this->get_action_button_text( $template ); if ( empty( $template['has_access'] ) ) { $args['license_class'] = ' pro'; $args['badge_text'] = $template_license; $args['education_class'] = ' education-modal'; $args['education_attributes'] = sprintf( ' data-name="%1$s" data-license="%2$s" data-action="upgrade"', esc_attr( $template_name ), esc_attr( $template_license ) ); } $args['addons_attributes'] = $this->prepare_addons_attributes( $template ); $args['selected'] = ! empty( $this->form_data['meta']['template'] ) && $this->form_data['meta']['template'] === $args['template_id']; $args['badge_class'] = ! empty( $args['badge_text'] ) ? ' badge' : ''; $args['template'] = $template; return $args; } /** * Get action button text. * * @since 1.7.7 * * @param array $template Template data. * * @return string */ private function get_action_button_text( $template ) { if ( ! empty( $template['action_text'] ) ) { return $template['action_text']; } if ( $template['slug'] === 'blank' ) { return __( 'Create Blank Form', 'wpforms-lite' ); } if ( wpforms_is_admin_page( 'templates' ) ) { return __( 'Create Form', 'wpforms-lite' ); } return __( 'Use Template', 'wpforms-lite' ); } /** * Generate addon attributes. * * @since 1.7.7 * * @param array $template Template data. * * @return string Addon attributes. */ private function prepare_addons_attributes( $template ) { $addons_attributes = ''; $required_addons = false; $already_installed = []; if ( ! empty( $template['addons'] ) && is_array( $template['addons'] ) ) { $required_addons = $this->addons_obj->get_by_slugs( $template['addons'] ); foreach ( $required_addons as $i => $addon ) { if ( ! isset( $addon['action'], $addon['title'], $addon['slug'] ) || ! in_array( $addon['action'], [ 'install', 'activate' ], true ) ) { unset( $required_addons[ $i ] ); } if ( $addon['action'] === 'activate' ) { $already_installed[] = $addon['slug']; } } } if ( ! empty( $required_addons ) ) { $addons_names = implode( ', ', wp_list_pluck( $required_addons, 'title' ) ); $addons_slugs = implode( ',', wp_list_pluck( $required_addons, 'slug' ) ); $installed_slugs = implode( ',', $already_installed ); $addons_attributes = sprintf( ' data-addons-names="%1$s" data-addons="%2$s" data-installed="%3$s"', esc_attr( $addons_names ), esc_attr( $addons_slugs ), esc_attr( $installed_slugs ) ); } return $addons_attributes; } /** * Determine a template source. * * @since 1.7.7 * * @param array $template Template data. * * @return string Template source. */ private function get_template_source( $template ) { if ( ! empty( $template['source'] ) ) { return $template['source']; } $source = 'wpforms-addon'; static $addons = null; if ( $addons === null ) { $addons = array_keys( $this->addons_obj->get_all() ); } if ( $template['plugin_dir'] === 'wpforms' || $template['plugin_dir'] === 'wpforms-lite' ) { $source = 'wpforms-core'; } if ( $source !== 'wpforms-core' && ! in_array( $template['plugin_dir'], $addons, true ) ) { $source = 'wpforms-custom'; } return $source; } /** * Determine template categories. * * @since 1.7.7 * * @param array $template Template data. * * @return string Template categories coma separated. */ private function get_template_categories( $template ) { $categories = ! empty( $template['categories'] ) ? (array) $template['categories'] : []; $source = $this->get_template_source( $template ); if ( $source === 'wpforms-addon' ) { $categories[] = 'addons'; } if ( $source === 'wpforms-custom' ) { $categories[] = 'custom'; } if ( isset( $template['created_at'] ) && strtotime( $template['created_at'] ) > strtotime( '-3 Months' ) ) { $categories[] = 'new'; } return implode( ',', $categories ); } /** * Determine template subcategories. * * @since 1.8.4 * * @param array $template Template data. * * @return string Template subcategories coma separated. */ private function get_template_subcategories( $template ) { $subcategories = ! empty( $template['subcategories'] ) ? (array) $template['subcategories'] : []; $subcategories = array_keys( $subcategories ); return implode( ',', $subcategories ); } /** * Determine template fields. * * @since 1.8.6 * * @param array $template Template data. * * @return string Template fields, comma separated. */ private function get_template_fields( array $template ): string { $fields = ! empty( $template['fields'] ) ? (array) $template['fields'] : []; /** * Filter template fields. * * @since 1.8.6 * * @param array $fields Template fields. */ $fields = (array) apply_filters( 'wpforms_setup_template_fields', $fields ); return implode( ',', $fields ); } /** * Get categories templates count. * * @since 1.7.7 * * @return array */ private function get_count_in_categories() { $all_categories = []; $available_templates_count = 0; $favorites_templates_count = 0; $user_templates_count = 0; foreach ( $this->prepared_templates as $template_data ) { $template = $template_data['template']; $categories = explode( ',', $template_data['categories'] ); if ( $template['has_access'] ) { ++$available_templates_count; } if ( $template['favorite'] ) { ++$favorites_templates_count; } if ( $template['source'] === 'wpforms-user-template' ) { ++$user_templates_count; } if ( is_array( $categories ) ) { array_push( $all_categories, ...$categories ); continue; } $all_categories[] = $categories; } $categories_count = array_count_values( $all_categories ); $categories_count['all'] = count( $this->prepared_templates ); $categories_count['available'] = $available_templates_count; $categories_count['favorites'] = $favorites_templates_count; $categories_count['user'] = $user_templates_count; $categories_count['subcategories'] = $this->get_count_in_subcategories(); return $categories_count; } /** * Get subcategories templates count. * * @since 1.8.7 * * @return array */ private function get_count_in_subcategories(): array { $all_subcategories = []; foreach ( $this->prepared_templates as $template_data ) { $subcategories = explode( ',', $template_data['subcategories'] ); if ( is_array( $subcategories ) ) { array_push( $all_subcategories, ...$subcategories ); continue; } $all_subcategories[] = $subcategories; } return array_count_values( $all_subcategories ); } } Admin/Traits/HasScreenOptions.php 0000644 00000011655 15174710275 0013023 0 ustar 00 <?php namespace WPForms\Admin\Traits; /** * Enables screen options for the current screen. * * @since 1.8.8 */ trait HasScreenOptions { /** * A configuration array for the screen options. * * @var array Array of screen options. * * @since 1.8.8 */ private $screen_options; /** * Screen options ID. * * @var string Screen options ID. * * @since 1.8.8 */ private $screen_options_id; /** * Initialize the screen options. * * This method should be called during init of the class that uses this trait. * If the class itself is allowed to load, it should set $allowed to true. * * @param bool $allowed Whether to allow screen options or not. * * @since 1.8.8 */ public function init_screen_options( bool $allowed = false ) { // This should always run. $this->filter_screen_options(); if ( ! $allowed ) { return; } add_action( 'admin_head', [ $this, 'add_screen_options' ] ); add_filter( 'screen_settings', [ $this, 'render_screen_options' ], 10, 2 ); add_filter( "set_screen_option_{$this->screen_options_id}", [ $this, 'save_screen_options' ], 10, 2 ); add_filter( 'screen_options_show_submit', '__return_true' ); } /** * Filter screen options. * * @since 1.8.8 */ public function filter_screen_options() { $options = get_user_option( $this->screen_options_id ); foreach ( $this->screen_options as $group => $options_group ) { foreach ( $options_group['options'] as $option ) { add_filter( "get_user_option_{$this->screen_options_id}_{$group}_{$option['option']}", function () use ( $option, $group, $options ) { $key = $group . '_' . $option['option']; return $options[ $key ] ?? $option['default']; } ); } } } /** * Configure screen options. * * @since 1.8.8 */ public function add_screen_options() { foreach ( $this->screen_options as $group => $options_group ) { foreach ( $options_group['options'] as $option ) { add_screen_option( $group . '_' . $option['option'], [ 'label' => $option['label'], 'option' => $option['option'], 'default' => $option['default'], ] ); } } } /** * Save screen options. * * @since 1.8.8 * * @param mixed $status Status of the screen option. * @param string $option Option name. * * @return mixed */ public function save_screen_options( $status, $option ) { // phpcs:ignore Generic.Metrics.NestingLevel.MaxExceeded if ( $option === $this->screen_options_id ) { $value = []; foreach ( $this->screen_options as $group => $options_group ) { $options = $options_group['options']; foreach ( $options as $group_option ) { $key = $group . '_' . $group_option['option']; if ( ! isset( $_POST[ $key ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing $value[ $key ] = false; continue; } $value[ $key ] = ! empty( $_POST[ $key ] ) ? sanitize_text_field( wp_unslash( $_POST[ $key ] ) ) : $group_option['default']; // phpcs:ignore WordPress.Security.NonceVerification.Missing } } return $value; } return $status; } /** * Render screen options. * * @since 1.8.8 * * @param string $screen_settings HTML markup of custom screen settings. * * @return string */ public function render_screen_options( $screen_settings ) { // phpcs:ignore Generic.Metrics.NestingLevel.MaxExceeded foreach ( $this->screen_options as $group => $options_group ) { $screen_settings .= '<fieldset>'; $screen_settings .= '<legend>' . esc_html( $options_group['heading'] ) . '</legend>'; foreach ( $options_group['options'] as $option ) { $option_value = get_user_option( "{$this->screen_options_id}_{$group}_{$option['option']}" ); $key = $group . '_' . $option['option']; switch ( $option['type'] ) { case 'checkbox': $screen_settings .= sprintf( '<label> <input type="checkbox" id="%1$s" name="%1$s" value="1" %2$s>%3$s </label>', $key, checked( (bool) $option_value, true, false ), esc_html( $option['label'] ) ); break; case 'number': $screen_settings .= sprintf( '<label for="%1$s">%2$s</label> <input type="number" id="%1$s" name="%1$s" value="%3$s" %4$s>', $key, esc_html( $option['label'] ), esc_attr( $option_value ), wpforms_html_attributes( '', [], [], $option['args'] ?? [] ) ); break; case 'text': $screen_settings .= sprintf( '<label for="%1$s">%2$s</label> <input type="text" id="%1$s" name="%1$s" value="%3$s">', $key, esc_html( $option['label'] ), esc_attr( $option_value ) ); break; } } $screen_settings .= sprintf( '<input name="wp_screen_options[option]" type="hidden" value="%s"><input name="wp_screen_options[value]" type="hidden" value="">', $this->screen_options_id ); $screen_settings .= '</fieldset>'; } return $screen_settings; } } Admin/Helpers/Chart.php 0000644 00000010464 15174710275 0010766 0 ustar 00 <?php namespace WPForms\Admin\Helpers; use DateInterval; use DatePeriod; use DateTimeImmutable; /** * Chart dataset processing helper methods. * * @since 1.8.2 */ class Chart { /** * Default date format. * * @since 1.8.2 */ const DATE_FORMAT = 'Y-m-d'; /** * Default date-time format. * * @since 1.8.2 */ const DATETIME_FORMAT = 'Y-m-d H:i:s'; /** * Processes the provided dataset to make sure the formatting needed for the "Chart.js" instance is provided. * * @since 1.8.2 * * @param array $query Dataset retrieved from the database. * @param DateTimeImmutable $start_date Start date for the timespan. * @param DateTimeImmutable $end_date End date for the timespan. * * @return array */ public static function process_chart_dataset_data( $query, $start_date, $end_date ) { // Bail early if the given query contains no records to iterate. if ( ! is_array( $query ) || empty( $query ) ) { return [ 0, [] ]; } $dataset = []; $timezone = wp_timezone(); // Retrieve the timezone object for the site. $mysql_timezone = timezone_open( 'UTC' ); // In the database, all datetime are stored in UTC. foreach ( $query as $row ) { $row_day = isset( $row['day'] ) ? sanitize_text_field( $row['day'] ) : ''; $row_count = isset( $row['count'] ) ? abs( (float) $row['count'] ) : 0; // Skip the rest of the current iteration if the date (day) is unavailable. if ( empty( $row_day ) ) { continue; } // Since we won’t need the initial datetime instances after the query, // there is no need to create immutable date objects. $row_datetime = date_create_from_format( self::DATETIME_FORMAT, $row_day, $mysql_timezone ); // Skip the rest of the current iteration if the date creation function fails. if ( ! $row_datetime ) { continue; } $row_datetime->setTimezone( $timezone ); $row_date_formatted = $row_datetime->format( self::DATE_FORMAT ); // We must take into account entries submitted at different hours of the day, // because it is possible that more than one entry could be submitted on a given day. if ( ! isset( $dataset[ $row_date_formatted ] ) ) { $dataset[ $row_date_formatted ] = $row_count; continue; } $dataset_count = $dataset[ $row_date_formatted ]; $dataset[ $row_date_formatted ] = $dataset_count + $row_count; } return self::format_chart_dataset_data( $dataset, $start_date, $end_date ); } /** * Format given forms dataset to ensure correct data structure is parsed for serving the "chart.js" instance. * i.e., [ '2023-02-11' => [ 'day' => '2023-02-11', 'count' => 12 ] ]. * * @since 1.8.2 * * @param array $dataset Dataset for the chart. * @param DateTimeImmutable $start_date Start date for the timespan. * @param DateTimeImmutable $end_date End date for the timespan. * * @return array */ private static function format_chart_dataset_data( $dataset, $start_date, $end_date ) { // In the event that there is no dataset to process, leave early. if ( empty( $dataset ) ) { return [ 0, [] ]; } $interval = new DateInterval( 'P1D' ); // Variable that store the date interval of period 1 day. $period = new DatePeriod( $start_date, $interval, $end_date ); // Used for iteration between start and end date period. $data = []; // Placeholder for the actual chart dataset data. $total_entries = 0; $has_non_zero_count = false; // Use loop to store date into array. foreach ( $period as $date ) { $date_formatted = $date->format( self::DATE_FORMAT ); $count = isset( $dataset[ $date_formatted ] ) ? (float) $dataset[ $date_formatted ] : 0; $total_entries += $count; $data[ $date_formatted ] = [ 'day' => $date_formatted, 'count' => $count, ]; // This check helps determine whether there is at least one non-zero count value in the dataset being processed. // It's used to optimize the function's behavior and to decide whether to include certain data in the returned result. if ( $count > 0 && ! $has_non_zero_count ) { $has_non_zero_count = true; } } return [ $total_entries, $has_non_zero_count ? $data : [], // We will return an empty array to indicate that there is no data to display. ]; } } Admin/Helpers/Datepicker.php 0000644 00000033706 15174710275 0012004 0 ustar 00 <?php namespace WPForms\Admin\Helpers; use DateTimeImmutable; /** * Timespan and popover date-picker helper methods. * * @since 1.8.2 */ class Datepicker { /** * Number of timespan days by default. * "Last 30 Days", by default. * * @since 1.8.2 */ const TIMESPAN_DAYS = '30'; /** * Timespan (date range) delimiter. * * @since 1.8.2 */ const TIMESPAN_DELIMITER = ' - '; /** * Default date format. * * @since 1.8.2 */ const DATE_FORMAT = 'Y-m-d'; /** * Default date-time format. * * @since 1.8.2 */ const DATETIME_FORMAT = 'Y-m-d H:i:s'; /** * Sets the timespan (or date range) selected. * * Includes: * 1. Start date object in WP timezone. * 2. End date object in WP timezone. * 3. Number of "Last X days", if applicable, otherwise returns "custom". * 4. Label associated with the selected date filter choice. @see "get_date_filter_choices". * * @since 1.8.2 * * @return array */ public static function process_timespan() { $dates = (string) filter_input( INPUT_GET, 'date', FILTER_SANITIZE_FULL_SPECIAL_CHARS ); // Return default timespan if dates are empty. if ( empty( $dates ) ) { return self::get_timespan_dates( self::TIMESPAN_DAYS ); } $dates = self::maybe_validate_string_timespan( $dates ); list( $start_date, $end_date ) = explode( self::TIMESPAN_DELIMITER, $dates ); // Return default timespan if start date is more recent than end date. if ( strtotime( $start_date ) > strtotime( $end_date ) ) { return self::get_timespan_dates( self::TIMESPAN_DAYS ); } $timezone = wp_timezone(); // Retrieve the timezone string for the site. $start_date = date_create_immutable( $start_date, $timezone ); $end_date = date_create_immutable( $end_date, $timezone ); // Return default timespan if date creation fails. if ( ! $start_date || ! $end_date ) { return self::get_timespan_dates( self::TIMESPAN_DAYS ); } // Set time to 0:0:0 for start date and 23:59:59 for end date. $start_date = $start_date->setTime( 0, 0, 0 ); $end_date = $end_date->setTime( 23, 59, 59 ); $days_diff = ''; $current_date = date_create_immutable( 'now', $timezone )->setTime( 23, 59, 59 ); // Calculate days difference only if end date is equal to current date. if ( ! $current_date->diff( $end_date )->format( '%a' ) ) { $days_diff = $end_date->diff( $start_date )->format( '%a' ); } list( $days, $timespan_label ) = self::get_date_filter_choices( $days_diff ); return [ $start_date, // WP timezone. $end_date, // WP timezone. $days, // e.g., 22. $timespan_label, // e.g., Custom. ]; } /** * Sets the timespan (or date range) for performing mysql queries. * * Includes: * 1. Start date object in WP timezone. * 2. End date object in WP timezone. * * @param null|array $timespan Given timespan (dates) preferably in WP timezone. * * @since 1.8.2 * * @return array */ public static function process_timespan_mysql( $timespan = null ) { // Retrieve and validate timespan if none is given. if ( empty( $timespan ) || ! is_array( $timespan ) ) { $timespan = self::process_timespan(); } list( $start_date, $end_date ) = $timespan; // Ideally should be in WP timezone. // If the time period is not a date object, return empty values. if ( ! ( $start_date instanceof DateTimeImmutable ) || ! ( $end_date instanceof DateTimeImmutable ) ) { return [ '', '' ]; } // If given timespan is already in UTC timezone, return as it is. if ( date_timezone_get( $start_date )->getName() === 'UTC' && date_timezone_get( $end_date )->getName() === 'UTC' ) { return [ $start_date, // UTC timezone. $end_date, // UTC timezone. ]; } $mysql_timezone = timezone_open( 'UTC' ); return [ $start_date->setTimezone( $mysql_timezone ), // UTC timezone. $end_date->setTimezone( $mysql_timezone ), // UTC timezone. ]; } /** * Helper method to generate WP and UTC based date-time instances. * * Includes: * 1. Start date object in WP timezone. * 2. End date object in WP timezone. * 3. Start date object in UTC timezone. * 4. End date object in UTC timezone. * * @since 1.8.2 * * @param string $dates Given timespan (dates) in string. i.e. "2023-01-16 - 2023-02-15". * * @return bool|array */ public static function process_string_timespan( $dates ) { $dates = self::maybe_validate_string_timespan( $dates ); list( $start_date, $end_date ) = explode( self::TIMESPAN_DELIMITER, $dates ); // Return false if the start date is more recent than the end date. if ( strtotime( $start_date ) > strtotime( $end_date ) ) { return false; } $timezone = wp_timezone(); // Retrieve the timezone object for the site. $start_date = date_create_immutable( $start_date, $timezone ); $end_date = date_create_immutable( $end_date, $timezone ); // Return false if the date creation fails. if ( ! $start_date || ! $end_date ) { return false; } // Set the time to 0:0:0 for the start date and 23:59:59 for the end date. $start_date = $start_date->setTime( 0, 0, 0 ); $end_date = $end_date->setTime( 23, 59, 59 ); // Since we will need the initial datetime instances after the query, // we need to return new objects when modifications made. // Convert the dates to UTC timezone. $mysql_timezone = timezone_open( 'UTC' ); $utc_start_date = $start_date->setTimezone( $mysql_timezone ); $utc_end_date = $end_date->setTimezone( $mysql_timezone ); return [ $start_date, // WP timezone. $end_date, // WP timezone. $utc_start_date, // UTC timezone. $utc_end_date, // UTC timezone. ]; } /** * Sets the timespan (or date range) for performing mysql queries. * * Includes: * 1. A list of date filter options for the datepicker module. * 2. Currently selected filter or date range values. Last "X" days, or i.e. Feb 8, 2023 - Mar 9, 2023. * 3. Assigned timespan dates. * * @param null|array $timespan Given timespan (dates) preferably in WP timezone. * * @since 1.8.2 * * @return array */ public static function process_datepicker_choices( $timespan = null ) { // Retrieve and validate timespan if none is given. if ( empty( $timespan ) || ! is_array( $timespan ) ) { $timespan = self::process_timespan(); } list( $start_date, $end_date, $days ) = $timespan; $filters = self::get_date_filter_choices(); $selected = isset( $filters[ $days ] ) ? $days : 'custom'; $value = self::concat_dates( $start_date, $end_date ); $chosen_filter = $selected === 'custom' ? $value : $filters[ $selected ]; $choices = []; foreach ( $filters as $choice => $label ) { $timespan_dates = self::get_timespan_dates( $choice ); $checked = checked( $selected, $choice, false ); $choices[] = sprintf( '<label class="%s">%s<input type="radio" aria-hidden="true" name="timespan" value="%s" %s></label>', $checked ? 'is-selected' : '', esc_html( $label ), esc_attr( self::concat_dates( ...$timespan_dates ) ), esc_attr( $checked ) ); } return [ $choices, $chosen_filter, $value, ]; } /** * Based on the specified date-time range, calculates the comparable prior time period to estimate trends. * * Includes: * 1. Start date object in the given (original) timezone. * 2. End date object in the given (original) timezone. * * @since 1.8.2 * @since 1.8.8 Added $days_diff optional parameter. * * @param DateTimeImmutable $start_date Start date for the timespan. * @param DateTimeImmutable $end_date End date for the timespan. * @param null|int $days_diff Optional. Number of days in the timespan. If provided, it won't be calculated. * * @return bool|array */ public static function get_prev_timespan_dates( $start_date, $end_date, $days_diff = null ) { if ( ! ( $start_date instanceof DateTimeImmutable ) || ! ( $end_date instanceof DateTimeImmutable ) ) { return false; } // Calculate $days_diff if not provided. if ( ! is_numeric( $days_diff ) ) { $days_diff = $end_date->diff( $start_date )->format( '%a' ); } // If $days_diff is non-positive, set $days_modifier to 1; otherwise, use $days_diff. $days_modifier = max( (int) $days_diff, 1 ); return [ $start_date->modify( "-{$days_modifier} day" ), $start_date->modify( '-1 second' ), ]; } /** * Get the site's date format from WordPress settings and convert it to a format compatible with Moment.js. * * @since 1.8.5.4 * * @return string */ public static function get_wp_date_format_for_momentjs() { // Get the date format from WordPress settings. $date_format = get_option( 'date_format', 'F j, Y' ); // Define a mapping of PHP date format characters to Moment.js format characters. $format_mapping = [ 'd' => 'DD', 'D' => 'ddd', 'j' => 'D', 'l' => 'dddd', 'S' => '', // PHP's S (English ordinal suffix) is not directly supported in Moment.js. 'w' => 'd', 'z' => '', // PHP's z (Day of the year) is not directly supported in Moment.js. 'W' => '', // PHP's W (ISO-8601 week number of year) is not directly supported in Moment.js. 'F' => 'MMMM', 'm' => 'MM', 'M' => 'MMM', 'n' => 'M', 't' => '', // PHP's t (Number of days in the given month) is not directly supported in Moment.js. 'L' => '', // PHP's L (Whether it's a leap year) is not directly supported in Moment.js. 'o' => 'YYYY', 'Y' => 'YYYY', 'y' => 'YY', 'a' => 'a', 'A' => 'A', 'B' => '', // PHP's B (Swatch Internet time) is not directly supported in Moment.js. 'g' => 'h', 'G' => 'H', 'h' => 'hh', 'H' => 'HH', 'i' => 'mm', 's' => 'ss', 'u' => '', // PHP's u (Microseconds) is not directly supported in Moment.js. 'e' => '', // PHP's e (Timezone identifier) is not directly supported in Moment.js. 'I' => '', // PHP's I (Whether or not the date is in daylight saving time) is not directly supported in Moment.js. 'O' => '', // PHP's O (Difference to Greenwich time (GMT) without colon) is not directly supported in Moment.js. 'P' => '', // PHP's P (Difference to Greenwich time (GMT) with colon) is not directly supported in Moment.js. 'T' => '', // PHP's T (Timezone abbreviation) is not directly supported in Moment.js. 'Z' => '', // PHP's Z (Timezone offset in seconds) is not directly supported in Moment.js. 'c' => 'YYYY-MM-DD', // PHP's c (ISO 8601 date) is not directly supported in Moment.js. 'r' => 'ddd, DD MMM YYYY', // PHP's r (RFC 2822 formatted date) is not directly supported in Moment.js. 'U' => '', // PHP's U (Seconds since the Unix Epoch) is not directly supported in Moment.js. ]; // Convert PHP format to JavaScript format. $momentjs_format = strtr( $date_format, $format_mapping ); // Use 'MMM D, YYYY' as a fallback if the conversion is not available. return empty( $momentjs_format ) ? 'MMM D, YYYY' : $momentjs_format; } /** * The number of days is converted to the start and end date range. * * @since 1.8.2 * * @param string $days Timespan days. * * @return array */ private static function get_timespan_dates( $days ) { list( $timespan_key, $timespan_label ) = self::get_date_filter_choices( $days ); // Bail early, if the given number of days is NOT a number nor a numeric string. if ( ! is_numeric( $days ) ) { return [ '', '', $timespan_key, $timespan_label ]; } $end_date = date_create_immutable( 'now', wp_timezone() ); $start_date = $end_date; if ( (int) $days > 0 ) { $start_date = $start_date->modify( "-{$days} day" ); } $start_date = $start_date->setTime( 0, 0, 0 ); $end_date = $end_date->setTime( 23, 59, 59 ); return [ $start_date, // WP timezone. $end_date, // WP timezone. $timespan_key, // i.e. 30. $timespan_label, // i.e. Last 30 days. ]; } /** * Check the delimiter to see if the end date is specified. * We can assume that the start and end dates are the same if the end date is missing. * * @since 1.8.2 * * @param string $dates Given timespan (dates) in string. i.e. "2023-01-16 - 2023-02-15" or "2023-01-16". * * @return string */ private static function maybe_validate_string_timespan( $dates ) { // "-" (en dash) is used as a delimiter for the datepicker module. if ( strpos( $dates, self::TIMESPAN_DELIMITER ) !== false ) { return $dates; } return $dates . self::TIMESPAN_DELIMITER . $dates; } /** * Returns a list of date filter options for the datepicker module. * * @since 1.8.2 * * @param string|null $key Optional. Key associated with available filters. * * @return array */ private static function get_date_filter_choices( $key = null ) { // Available date filters. $choices = [ '0' => esc_html__( 'Today', 'wpforms-lite' ), '1' => esc_html__( 'Yesterday', 'wpforms-lite' ), '7' => esc_html__( 'Last 7 days', 'wpforms-lite' ), '30' => esc_html__( 'Last 30 days', 'wpforms-lite' ), '90' => esc_html__( 'Last 90 days', 'wpforms-lite' ), '365' => esc_html__( 'Last 1 year', 'wpforms-lite' ), 'custom' => esc_html__( 'Custom', 'wpforms-lite' ), ]; // Bail early, and return the full list of options. if ( is_null( $key ) ) { return $choices; } // Return the "Custom" filter if the given key is not found. $key = isset( $choices[ $key ] ) ? $key : 'custom'; return [ $key, $choices[ $key ] ]; } /** * Concatenate given dates into a single string. i.e. "2023-01-16 - 2023-02-15". * * @since 1.8.2 * * @param DateTimeImmutable $start_date Start date. * @param DateTimeImmutable $end_date End date. * @param int|string $fallback Fallback value if dates are not valid. * * @return string */ private static function concat_dates( $start_date, $end_date, $fallback = '' ) { // Bail early, if the given dates are not valid. if ( ! ( $start_date instanceof DateTimeImmutable ) || ! ( $end_date instanceof DateTimeImmutable ) ) { return $fallback; } return implode( self::TIMESPAN_DELIMITER, [ $start_date->format( self::DATE_FORMAT ), $end_date->format( self::DATE_FORMAT ), ] ); } } Admin/Settings/Email.php 0000644 00000051130 15174710275 0011145 0 ustar 00 <?php namespace WPForms\Admin\Settings; use WPForms\Emails\Helpers; use WPForms\Emails\Notifications; use WPForms\Admin\Education\Helpers as EducationHelpers; /** * Email setting page. * Settings will be accessible via “WPForms” → “Settings” → “Email”. * * @since 1.8.5 */ class Email { /** * Content is plain text type. * * @since 1.8.5 * * @var bool */ private $plain_text; /** * Temporary storage for the style overrides preferences. * * @since 1.8.6 * * @var array */ private $style_overrides; /** * Determines if the user has the education modal. * If true, the value will be used to add the Education modal class to the setting controls. * This is only available in the free version. * * @since 1.8.6 * * @var string */ private $has_education; /** * Determines if the user has the legacy template. * If true, the value will be used to add the Legacy template class to the setting controls. * * @since 1.8.6 * * @var string */ private $has_legacy_template; /** * Initialize class. * * @since 1.8.5 */ public function init() { $this->hooks(); } /** * Hooks. * * @since 1.8.5 */ private function hooks() { add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] ); add_filter( 'wpforms_update_settings', [ $this, 'maybe_update_settings' ] ); add_filter( 'wpforms_settings_tabs', [ $this, 'register_settings_tabs' ], 5 ); add_filter( 'wpforms_settings_defaults', [ $this, 'register_settings_fields' ], 5 ); } /** * Enqueue scripts and styles. * Static resources are enqueued only on the "Email" settings page. * * @since 1.8.5 */ public function enqueue_assets() { // Leave if the current page is not the "Email" settings page. if ( ! $this->is_settings_page() ) { return; } $min = wpforms_get_min_suffix(); wp_enqueue_script( 'wpforms-contrast-checker', WPFORMS_PLUGIN_URL . "assets/js/admin/share/contrast-checker{$min}.js", [], WPFORMS_VERSION, true ); wp_enqueue_script( 'wpforms-xor', WPFORMS_PLUGIN_URL . "assets/js/admin/share/xor{$min}.js", [], WPFORMS_VERSION, true ); wp_enqueue_script( 'wpforms-admin-email-settings', WPFORMS_PLUGIN_URL . "assets/js/admin/email/settings{$min}.js", [ 'jquery', 'wpforms-admin', 'wp-escape-html', 'wp-url', 'choicesjs', 'wpforms-contrast-checker', 'wpforms-xor' ], WPFORMS_VERSION, true ); wp_localize_script( 'wpforms-admin-email-settings', 'wpforms_admin_email_settings', [ 'contrast_fail' => esc_html__( 'This color combination may be hard to read. Try increasing the contrast between the body and text colors.', 'wpforms-lite' ), ] ); } /** * Maybe update settings. * * @since 1.8.5 * * @param array $settings Admin area settings list. * * @return array */ public function maybe_update_settings( $settings ) { // Leave if the current page is not the "Email" settings page. if ( ! $this->is_settings_page() ) { return $settings; } // Remove the appearance mode switcher from the settings array. unset( $settings['email-appearance'] ); // Backup the Pro version background color setting to the free version. // This is needed to keep the background color when the Pro version is deactivated. if ( wpforms()->is_pro() && ! Helpers::is_legacy_html_template() ) { $settings['email-background-color'] = sanitize_hex_color( $settings['email-color-scheme']['email_background_color'] ); $settings['email-background-color-dark'] = sanitize_hex_color( $settings['email-color-scheme-dark']['email_background_color_dark'] ); return $settings; } // Backup the free version background color setting to the Pro version. // This is needed to keep the background color when the Pro version is activated. $settings['email-color-scheme']['email_background_color'] = sanitize_hex_color( $settings['email-background-color'] ); $settings['email-color-scheme-dark']['email_background_color_dark'] = sanitize_hex_color( $settings['email-background-color-dark'] ); return $settings; } /** * Register "Email" settings tab. * * @since 1.8.5 * * @param array $tabs Admin area tabs list. * * @return array */ public function register_settings_tabs( $tabs ) { $payments = [ 'email' => [ 'form' => true, 'name' => esc_html__( 'Email', 'wpforms-lite' ), 'submit' => esc_html__( 'Save Settings', 'wpforms-lite' ), ], ]; return wpforms_array_insert( $tabs, $payments, 'general' ); } /** * Register "Email" settings fields. * * @since 1.8.5 * * @param array $settings Admin area settings list. * * @return array */ public function register_settings_fields( $settings ) { $this->plain_text = Helpers::is_plain_text_template(); $this->has_education = ! wpforms()->is_pro() ? 'education-modal' : ''; $this->style_overrides = Helpers::get_current_template_style_overrides(); $this->has_legacy_template = Helpers::is_legacy_html_template() ? 'legacy-template' : ''; $preview_link = $this->get_current_template_preview_link(); // Add Email settings. $settings['email'] = [ 'email-heading' => [ 'id' => 'email-heading', 'content' => $this->get_heading_content(), 'type' => 'content', 'no_label' => true, 'class' => [ 'section-heading', 'no-desc' ], ], 'email-template' => [ 'id' => 'email-template', 'name' => esc_html__( 'Template', 'wpforms-lite' ), 'class' => [ 'wpforms-email-template', 'wpforms-card-image-group' ], 'type' => 'email_template', 'default' => Notifications::DEFAULT_TEMPLATE, 'options' => Helpers::get_email_template_choices(), 'value' => Helpers::get_current_template_name(), ], // The reason that we're using the 'content' type is to avoid saving the value in the option storage. // The value is used only for the appearance mode switcher, and merely acts as a switch between dark and light mode controls. 'email-appearance' => [ 'id' => 'email-appearance', 'name' => esc_html__( 'Appearance', 'wpforms-lite' ), 'desc' => esc_html__( 'Modern email clients support viewing emails in light and dark modes. You can upload a header image and customize the style for each appearance mode independently to ensure an optimal reading experience.', 'wpforms-lite' ), 'type' => 'radio', 'class' => [ 'wpforms-setting-row-radio', 'hide-for-template-none', 'email-appearance-mode-toggle', $this->has_legacy_template ], 'default' => 'light', 'is_hidden' => $this->plain_text, 'options' => [ 'light' => esc_html__( 'Light', 'wpforms-lite' ), 'dark' => esc_html__( 'Dark', 'wpforms-lite' ), ], ], 'email-preview' => [ 'id' => 'email-preview', 'type' => 'content', 'is_hidden' => empty( $preview_link ), 'content' => $preview_link, ], 'sending-heading' => [ 'id' => 'sending-heading', 'content' => '<h4>' . esc_html__( 'Sending', 'wpforms-lite' ) . '</h4>', 'type' => 'content', 'no_label' => true, 'class' => [ 'section-heading', 'no-desc' ], ], 'email-async' => [ 'id' => 'email-async', 'name' => esc_html__( 'Optimize Email Sending', 'wpforms-lite' ), 'desc' => sprintf( wp_kses( /* translators: %1$s - WPForms.com Email settings documentation URL. */ __( 'Send emails asynchronously, which can make processing faster but may delay email delivery by a minute or two. <a href="%1$s" target="_blank" rel="noopener noreferrer" class="wpforms-learn-more">Learn More</a>', 'wpforms-lite' ), [ 'a' => [ 'href' => [], 'target' => [], 'rel' => [], 'class' => [], ], ] ), esc_url( wpforms_utm_link( 'https://wpforms.com/docs/a-complete-guide-to-wpforms-settings/#email', 'Settings - Email', 'Optimize Email Sending Documentation' ) ) ), 'type' => 'toggle', 'status' => true, ], 'email-carbon-copy' => [ 'id' => 'email-carbon-copy', 'name' => esc_html__( 'Carbon Copy', 'wpforms-lite' ), 'desc' => esc_html__( 'Enable the ability to CC: email addresses in the form notification settings.', 'wpforms-lite' ), 'type' => 'toggle', 'status' => true, ], ]; // Add the style controls. $settings['email'] = $this->add_appearance_controls( $settings['email'] ); // Maybe add the Legacy template notice. $settings['email'] = $this->maybe_add_legacy_notice( $settings['email'] ); return $settings; } /** * Maybe add the legacy template notice. * * @since 1.8.5 * * @param array $settings Email settings. * * @return array */ private function maybe_add_legacy_notice( $settings ) { if ( ! $this->is_settings_page() || ! Helpers::is_legacy_html_template() ) { return $settings; } $content = '<div class="notice-info"><p>'; $content .= sprintf( wp_kses( /* translators: %1$s - WPForms.com Email settings legacy template documentation URL. */ __( 'Some style settings are not available when using the Legacy template. <a href="%1$s" target="_blank" rel="noopener noreferrer">Learn More</a>', 'wpforms-lite' ), [ 'a' => [ 'href' => [], 'target' => [], 'rel' => [], ], ] ), esc_url( wpforms_utm_link( 'https://wpforms.com/docs/customizing-form-notification-emails/#legacy-template', 'Settings - Email', 'Legacy Template' ) ) ); $content .= '</p></div>'; // Add the background color control after the header image. return wpforms_array_insert( $settings, [ 'email-legacy-notice' => [ 'id' => 'email-legacy-notice', 'content' => $content, 'type' => 'content', 'class' => 'wpforms-email-legacy-notice', ], ], 'email-template' ); } /** * Add appearance controls. * This will include controls for both Light and Dark modes. * * @since 1.8.6 * * @param array $settings Email settings. * * @return array */ private function add_appearance_controls( $settings ) { // Education modal arguments. $education_args = [ 'action' => 'upgrade' ]; // New settings for the Light mode. $new_setting = [ 'email-header-image' => [ 'id' => 'email-header-image', 'name' => esc_html__( 'Header Image', 'wpforms-lite' ), 'desc' => esc_html__( 'Upload or choose a logo to be displayed at the top of email notifications.', 'wpforms-lite' ), 'class' => [ 'wpforms-email-header-image', 'hide-for-template-none', 'has-preview-changes', 'email-light-mode', $this->get_external_header_image_class() ], 'type' => 'image', 'is_hidden' => $this->plain_text, 'show_remove' => true, ], 'email-header-image-size' => [ 'id' => 'email-header-image-size', 'no_label' => true, 'type' => 'select', 'class' => [ 'wpforms-email-header-image-size', 'has-preview-changes', 'email-light-mode' ], 'is_hidden' => true, 'choicesjs' => false, 'default' => 'medium', 'options' => [ 'small' => esc_html__( 'Small', 'wpforms-lite' ), 'medium' => esc_html__( 'Medium', 'wpforms-lite' ), 'large' => esc_html__( 'Large', 'wpforms-lite' ), ], ], 'email-color-scheme' => [ 'id' => 'email-color-scheme', 'name' => esc_html__( 'Color Scheme', 'wpforms-lite' ), 'class' => [ 'email-color-scheme', 'hide-for-template-none', 'has-preview-changes', 'email-light-mode', $this->has_education, $this->has_legacy_template ], 'type' => 'color_scheme', 'is_hidden' => $this->plain_text, 'education_badge' => $this->get_pro_education_badge(), 'data_attributes' => $this->has_education ? array_merge( [ 'name' => esc_html__( 'Color Scheme', 'wpforms-lite' ) ], $education_args ) : [], 'colors' => $this->get_color_scheme_controls(), ], 'email-typography' => [ 'id' => 'email-typography', 'name' => esc_html__( 'Typography', 'wpforms-lite' ), 'desc' => esc_html__( 'Choose the style that’s applied to all text in email notifications.', 'wpforms-lite' ), 'class' => [ 'hide-for-template-none', 'has-preview-changes', 'email-typography', 'email-light-mode', $this->has_education, $this->has_legacy_template ], 'education_badge' => $this->get_pro_education_badge(), 'data_attributes' => $this->has_education ? array_merge( [ 'name' => esc_html__( 'Typography', 'wpforms-lite' ) ], $education_args ) : [], 'type' => 'select', 'is_hidden' => $this->plain_text, 'choicesjs' => true, 'default' => 'sans-serif', 'options' => [ 'sans-serif' => esc_html__( 'Sans Serif', 'wpforms-lite' ), 'serif' => esc_html__( 'Serif', 'wpforms-lite' ), ], ], ]; // Add background color control if the Pro version is not active or Legacy template is selected. $new_setting = $this->maybe_add_background_color_control( $new_setting ); return wpforms_array_insert( $settings, $this->add_appearance_dark_mode_controls( $new_setting ), 'email-appearance' ); } /** * Add appearance dark mode controls. * * This function will duplicate the default "Light" color * controls to create corresponding controls for dark mode. * * @since 1.8.6 * * @param array $settings Email settings. * * @return array */ private function add_appearance_dark_mode_controls( $settings ) { // Duplicate and modify each item for dark mode. foreach ( $settings as $key => $item ) { // Duplicate the item with '-dark' added to the key. $dark_key = "{$key}-dark"; $settings[ $dark_key ] = $item; // Modify the 'name' within the duplicated item. if ( isset( $settings[ $dark_key ]['id'] ) ) { $settings[ $dark_key ]['id'] .= '-dark'; $classes = &$settings[ $dark_key ]['class']; $classes[] = 'email-dark-mode'; $classes[] = 'wpforms-hide'; // Remove classes related to light mode. $classes = array_filter( $classes, static function ( $class_name ) { return $class_name !== 'email-light-mode' && $class_name !== 'has-external-image-url'; } ); } // Override the description for the header image control. if ( $key === 'email-header-image' ) { $settings[ $dark_key ]['desc'] = esc_html__( 'Upload or choose a logo to be displayed at the top of email notifications. Light mode image will be used if not set.', 'wpforms-lite' ); } // Override the background color control attributes. if ( $key === 'email-background-color' ) { $settings[ $dark_key ]['default'] = sanitize_hex_color( $this->style_overrides['email_background_color_dark'] ); $settings[ $dark_key ]['data']['fallback-color'] = sanitize_hex_color( $this->style_overrides['email_background_color_dark'] ); } // Override the color scheme control attributes. if ( $key === 'email-color-scheme' ) { $settings[ $dark_key ]['colors'] = $this->get_color_scheme_controls( true ); } } return $settings; } /** * Get Email settings heading content. * * @since 1.8.5 * * @return string */ private function get_heading_content() { return wpforms_render( 'admin/settings/email-heading' ); } /** * Get Email settings education badge. * This is only available in the free version. * * @since 1.8.6 * * @return string */ private function get_pro_education_badge() { // Leave early if the user has the Lite version. if ( empty( $this->has_education ) ) { return ''; } // Output the education badge. return EducationHelpers::get_badge( 'Pro' ); } /** * Generate color scheme controls for the color picker. * * @since 1.8.6 * * @param bool $is_dark_mode Whether the color scheme is for dark mode. * * @return array */ private function get_color_scheme_controls( $is_dark_mode = false ) { // Append '_dark' to keys if it's for dark mode. $is_dark_mode_suffix = $is_dark_mode ? '_dark' : ''; // Data attributes to disable extensions from appearing in the input field. $color_scheme_data = [ '1p-ignore' => 'true', // 1Password ignore. 'lp-ignore' => 'true', // LastPass ignore. ]; $colors = []; $controls = [ "email_background_color{$is_dark_mode_suffix}" => esc_html__( 'Background', 'wpforms-lite' ), "email_body_color{$is_dark_mode_suffix}" => esc_html__( 'Body', 'wpforms-lite' ), "email_text_color{$is_dark_mode_suffix}" => esc_html__( 'Text', 'wpforms-lite' ), "email_links_color{$is_dark_mode_suffix}" => esc_html__( 'Links', 'wpforms-lite' ), ]; foreach ( $controls as $key => $label ) { // Construct the color controls array. $colors[ $key ] = [ 'name' => $label, 'data' => array_merge( [ 'fallback-color' => $this->style_overrides[ $key ], ], $color_scheme_data ), ]; } return $colors; } /** * Get current email template hyperlink. * * @since 1.8.5 * * @return string */ private function get_current_template_preview_link() { // Leave if the user has the legacy template is set or the user doesn't have the capability. if ( ! wpforms_current_user_can() || Helpers::is_legacy_html_template() ) { return ''; } $template_name = Helpers::get_current_template_name(); $current_template = Notifications::get_available_templates( $template_name ); // Return empty string if the current template is not found. // Leave early if the preview link is empty. if ( ! isset( $current_template['path'] ) || ! class_exists( $current_template['path'] ) || empty( $current_template['preview'] ) ) { return ''; } return sprintf( wp_kses( /* translators: %1$s - Email template preview URL. */ __( '<a href="%1$s" class="wpforms-btn-preview" target="_blank" rel="noopener">Preview Email Template</a>', 'wpforms-lite' ), [ 'a' => [ 'class' => true, 'href' => true, 'target' => true, 'rel' => true, ], ] ), esc_url( $current_template['preview'] ) ); } /** * Maybe add the background color control to the email settings. * This is only available in the free version. * * @since 1.8.5 * * @param array $settings Email settings. * * @return array */ private function maybe_add_background_color_control( $settings ) { // Leave as is if the Pro version is active and no legacy template available. if ( ! Helpers::is_legacy_html_template() && wpforms()->is_pro() ) { return $settings; } // Add the background color control after the header image. return wpforms_array_insert( $settings, [ 'email-background-color' => [ 'id' => 'email-background-color', 'name' => esc_html__( 'Background Color', 'wpforms-lite' ), 'desc' => esc_html__( 'Customize the background color of the email template.', 'wpforms-lite' ), 'class' => [ 'email-background-color', 'has-preview-changes', 'email-light-mode' ], 'type' => 'color', 'is_hidden' => $this->plain_text, 'default' => '#e9eaec', 'data' => [ 'fallback-color' => $this->style_overrides['email_background_color'], '1p-ignore' => 'true', // 1Password ignore. 'lp-ignore' => 'true', // LastPass ignore. ], ], ], 'email-color-scheme', 'before' ); } /** * Gets the class for the header image control. * * This is used to determine if the header image is external. * Legacy header image control was allowing external URLs. * * Note that this evaluation is only available for the "Light" mode, * as the "Dark" mode is a new feature and doesn't have the legacy header image control. * * @since 1.8.5 * * @return string */ private function get_external_header_image_class() { $header_image_url = wpforms_setting( 'email-header-image', '' ); // If the header image URL is empty, return an empty string. if ( empty( $header_image_url ) ) { return ''; } $site_url = home_url(); // Get the current site's URL. // Get the hosts of the site URL and the header image URL. $site_url_host = wp_parse_url( $site_url, PHP_URL_HOST ); $header_image_url_host = wp_parse_url( $header_image_url, PHP_URL_HOST ); // Check if the header image URL host is different from the site URL host. if ( $header_image_url_host && $site_url_host && $header_image_url_host !== $site_url_host ) { return 'has-external-image-url'; } return ''; // If none of the conditions match, return an empty string. } /** * Determine if the current page is the "Email" settings page. * * @since 1.8.5 * * @return bool */ private function is_settings_page() { return wpforms_is_admin_page( 'settings', 'email' ); } } Admin/Settings/ModernMarkup.php 0000644 00000011175 15174710275 0012527 0 ustar 00 <?php namespace WPForms\Admin\Settings; use WPForms\Helpers\Transient; /** * Modern Markup setting element. * * @since 1.8.1 */ class ModernMarkup { /** * Initialize class. * * @since 1.8.1 */ public function init() { $this->hooks(); } /** * Hooks. * * @since 1.8.1 */ public function hooks() { add_action( 'wpforms_create_form', [ $this, 'clear_transient' ] ); add_action( 'wpforms_save_form', [ $this, 'clear_transient' ] ); add_action( 'wpforms_delete_form', [ $this, 'clear_transient' ] ); add_action( 'wpforms_form_handler_update_status', [ $this, 'clear_transient' ] ); // Only continue if we are actually on the settings page. if ( ! wpforms_is_admin_page( 'settings' ) ) { return; } add_filter( 'wpforms_settings_defaults', [ $this, 'register_field' ] ); } /** * Register setting field. * * @since 1.8.1 * * @param array|mixed $settings Settings data. * * @return array * @noinspection HtmlUnknownTarget * @noinspection NullPointerExceptionInspection */ public function register_field( $settings ): array { /** * Allows to show/hide the Modern Markup setting field on the Settings page. * * @since 1.8.1 * * @param mixed $is_hidden Whether the setting must be hidden. */ $is_hidden = apply_filters( 'wpforms_admin_settings_modern_markup_register_field_is_hidden', wpforms_setting( 'modern-markup-hide-setting' ) ); if ( ! empty( $is_hidden ) ) { return $settings; } $settings = (array) $settings; $modern_markup = [ 'id' => 'modern-markup', 'name' => esc_html__( 'Use Modern Markup', 'wpforms-lite' ), 'desc' => sprintf( wp_kses( /* translators: %s - WPForms.com form markup setting URL. */ __( 'Check this option to use modern markup, which has increased accessibility and allows you to easily customize your forms in the block editor. <a href="%s" target="_blank" rel="noopener noreferrer">Read our form markup documentation</a> to learn more.', 'wpforms-lite' ), [ 'a' => [ 'href' => [], 'target' => [], 'rel' => [], 'class' => [], ], ] ), wpforms_utm_link( 'https://wpforms.com/docs/styling-your-forms/', 'settings-license', 'Form Markup Documentation' ) ), 'type' => 'toggle', 'status' => true, ]; $is_disabled_transient = Transient::get( 'modern_markup_setting_disabled' ); // Transient doesn't set or expired. if ( $is_disabled_transient === false ) { $forms = wpforms()->obj( 'form' )->get( '', [ 'post_status' => 'publish' ] ); $is_disabled_transient = ( ! empty( $forms ) && wpforms_has_field_type( 'credit-card', $forms, true ) ) ? '1' : '0'; // Re-check all the forms for the CC field once per day. Transient::set( 'modern_markup_setting_disabled', $is_disabled_transient, DAY_IN_SECONDS ); } /** * Allows enabling/disabling the Modern Markup setting field on the Settings page. * * @since 1.8.1 * * @param mixed $is_disabled Whether the Modern Markup setting must be disabled. */ $is_disabled = (bool) apply_filters( 'wpforms_admin_settings_modern_markup_register_field_is_disabled', ! empty( $is_disabled_transient ) ); $current_value = wpforms_setting( 'modern-markup' ); // In the case, when it is disabled because of the legacy CC field, add the corresponding description. if ( $is_disabled && ! empty( $is_disabled_transient ) && empty( $current_value ) ) { $modern_markup['disabled'] = true; $modern_markup['disabled_desc'] = sprintf( wp_kses( /* translators: %s - WPForms Stripe addon URL. */ __( '<strong>You cannot use modern markup because you’re using the deprecated Credit Card field.</strong> If you’d like to use modern markup, replace your credit card field with a payment gateway like <a href="%s" target="_blank" rel="noopener noreferrer">Stripe</a>.', 'wpforms-lite' ), [ 'a' => [ 'href' => [], 'target' => [], 'rel' => [], ], 'strong' => [], ] ), 'https://wpforms.com/docs/how-to-install-and-use-the-stripe-addon-with-wpforms' ); } $modern_markup = [ 'modern-markup' => $modern_markup, ]; $settings['general'] = wpforms_list_insert_after( $settings['general'], 'disable-css', $modern_markup ); return $settings; } /** * Clear transient in the case when the form is created/saved/deleted. * So, next time when the user opens the Settings page, * the Modern Markup setting will check for the legacy Credit Card field in all the forms again. * * @since 1.8.1 */ public function clear_transient() { Transient::delete( 'modern_markup_setting_disabled' ); } } Admin/Settings/Captcha/Page.php 0000644 00000023414 15174710275 0012341 0 ustar 00 <?php namespace WPForms\Admin\Settings\Captcha; use WPForms\Admin\Notice; /** * CAPTCHA setting page. * * @since 1.8.0 */ class Page { /** * Slug identifier for admin page view. * * @since 1.8.0 * * @var string */ const VIEW = 'captcha'; /** * Saved CAPTCHA settings. * * @since 1.8.0 * * @var array */ private $settings; /** * All available captcha types. * * @since 1.8.0 * * @var array */ private $captchas; /** * Initialize class. * * @since 1.8.0 */ public function init() { // Only load if we are actually on the settings page. if ( ! wpforms_is_admin_page( 'settings' ) ) { return; } // Listen the previous reCAPTCHA page and safely redirect from it. if ( wpforms_is_admin_page( 'settings', 'recaptcha' ) ) { wp_safe_redirect( add_query_arg( 'view', self::VIEW, admin_url( 'admin.php?page=wpforms-settings' ) ) ); exit; } $this->init_settings(); $this->hooks(); } /** * Init CAPTCHA settings. * * @since 1.8.0 */ public function init_settings() { $this->settings = wp_parse_args( wpforms_get_captcha_settings(), [ 'provider' => 'none' ] ); /** * Filter available captcha for the settings page. * * @since 1.8.0 * * @param array $captcha Array where key is captcha name and value is captcha class instance. * @param array $settings Array of settings. */ $this->captchas = apply_filters( 'wpforms_admin_settings_captcha_page_init_settings_available_captcha', [ 'hcaptcha' => new HCaptcha(), 'recaptcha' => new ReCaptcha(), 'turnstile' => new Turnstile(), ], $this->settings ); foreach ( $this->captchas as $captcha ) { $captcha->init(); } } /** * Hooks. * * @since 1.8.0 */ public function hooks() { add_filter( 'wpforms_settings_tabs', [ $this, 'register_settings_tabs' ], 5, 1 ); add_filter( 'wpforms_settings_defaults', [ $this, 'register_settings_fields' ], 5, 1 ); add_action( 'wpforms_settings_updated', [ $this, 'updated' ] ); add_action( 'wpforms_settings_enqueue', [ $this, 'enqueues' ] ); add_action( 'admin_enqueue_scripts', [ $this, 'apply_noconflict' ], 9999 ); } /** * Register CAPTCHA settings tab. * * @since 1.8.0 * * @param array $tabs Admin area tabs list. * * @return array */ public function register_settings_tabs( $tabs ) { $captcha = [ self::VIEW => [ 'name' => esc_html__( 'CAPTCHA', 'wpforms-lite' ), 'form' => true, 'submit' => esc_html__( 'Save Settings', 'wpforms-lite' ), ], ]; return wpforms_array_insert( $tabs, $captcha, 'email' ); } /** * Register CAPTCHA settings fields. * * @since 1.8.0 * * @param array $settings Admin area settings list. * * @return array */ public function register_settings_fields( $settings ) { $settings[ self::VIEW ] = [ self::VIEW . '-heading' => [ 'id' => self::VIEW . '-heading', 'content' => '<h4>' . esc_html__( 'CAPTCHA', 'wpforms-lite' ) . '</h4><p>' . esc_html__( 'A CAPTCHA is an anti-spam technique which helps to protect your website from spam and abuse while letting real people pass through with ease.', 'wpforms-lite' ) . '</p>', 'type' => 'content', 'no_label' => true, 'class' => [ 'wpforms-setting-captcha-heading', 'section-heading' ], ], self::VIEW . '-provider' => [ 'id' => self::VIEW . '-provider', 'type' => 'radio', 'default' => 'none', 'options' => [ 'hcaptcha' => 'hCaptcha', 'recaptcha' => 'reCAPTCHA', 'turnstile' => 'Turnstile', 'none' => esc_html__( 'None', 'wpforms-lite' ), ], 'desc' => sprintf( wp_kses( /* translators: %s - WPForms.com CAPTCHA comparison page URL. */ __( 'Not sure which service is right for you? <a href="%s" target="_blank" rel="noopener noreferrer">Check out our comparison</a> for more details.', 'wpforms-lite' ), [ 'a' => [ 'href' => [], 'target' => [], 'rel' => [], ], ] ), esc_url( wpforms_utm_link( 'https://wpforms.com/docs/setup-captcha-wpforms/', 'Settings - Captcha', 'Captcha Comparison Documentation' ) ) ), ], ]; // Add settings fields for each of available captcha types. foreach ( $this->captchas as $captcha ) { $settings[ self::VIEW ] = array_merge( $settings[ self::VIEW ], $captcha->get_settings_fields() ); } $settings[ self::VIEW ] = array_merge( $settings[ self::VIEW ], [ 'recaptcha-noconflict' => [ 'id' => 'recaptcha-noconflict', 'name' => esc_html__( 'No-Conflict Mode', 'wpforms-lite' ), 'desc' => esc_html__( 'Forcefully remove other CAPTCHA occurrences in order to prevent conflicts. Only enable this option if your site is having compatibility issues or instructed by support.', 'wpforms-lite' ), 'type' => 'toggle', 'status' => true, ], self::VIEW . '-preview' => [ 'id' => self::VIEW . '-preview', 'name' => esc_html__( 'Preview', 'wpforms-lite' ), 'content' => '<p class="desc wpforms-captcha-preview-desc">' . esc_html__( 'Please save settings to generate a preview of your CAPTCHA here.', 'wpforms-lite' ) . '</p>', 'type' => 'content', 'class' => [ 'wpforms-hidden' ], ], ] ); if ( $this->settings['provider'] === 'hcaptcha' || $this->settings['provider'] === 'turnstile' || ( $this->settings['provider'] === 'recaptcha' && $this->settings['recaptcha_type'] === 'v2' ) ) { // phpcs:disable WPForms.PHP.ValidateHooks.InvalidHookName /** * Modify captcha settings data. * * @since 1.6.4 * * @param array $data Array of settings. */ $data = apply_filters( 'wpforms_admin_pages_settings_captcha_data', [ 'sitekey' => $this->settings['site_key'], 'theme' => $this->settings['theme'], ] ); // phpcs:enable WPForms.PHP.ValidateHooks.InvalidHookName // Prepare HTML for CAPTCHA preview. $placeholder_description = $settings[ self::VIEW ][ self::VIEW . '-preview' ]['content']; $captcha_description = esc_html__( 'This CAPTCHA is generated using your site and secret keys. If an error is displayed, please double-check your keys.', 'wpforms-lite' ); $captcha_preview = sprintf( '<div class="wpforms-captcha-container" style="pointer-events:none!important;cursor:default!important;"> <div %s></div> <input type="text" name="wpforms-captcha-hidden" class="wpforms-recaptcha-hidden" style="position:absolute!important;clip:rect(0,0,0,0)!important;height:1px!important;width:1px!important;border:0!important;overflow:hidden!important;padding:0!important;margin:0!important;"> </div>', wpforms_html_attributes( '', [ 'wpforms-captcha', 'wpforms-captcha-' . $this->settings['provider'] ], $data ) ); $settings[ self::VIEW ][ self::VIEW . '-preview' ]['content'] = sprintf( '<div class="wpforms-captcha-preview"> %1$s <p class="desc">%2$s</p> </div> <div class="wpforms-captcha-placeholder wpforms-hidden">%3$s</div>', $captcha_preview, $captcha_description, $placeholder_description ); $settings[ self::VIEW ][ self::VIEW . '-preview' ]['class'] = []; } return $settings; } /** * Re-init CAPTCHA settings when plugin settings were updated. * * @since 1.8.0 */ public function updated() { $this->init_settings(); $this->notice(); } /** * Display notice about the CAPTCHA preview. * * @since 1.8.0 */ private function notice() { if ( ! wpforms_is_admin_page( 'settings', self::VIEW ) || ! $this->is_captcha_preview_ready() ) { return; } Notice::info( esc_html__( 'A preview of your CAPTCHA is displayed below. Please view to verify the CAPTCHA settings are correct.', 'wpforms-lite' ) ); } /** * Check if CAPTCHA config is ready to display a preview. * * @since 1.8.0 * * @return bool */ private function is_captcha_preview_ready() { $current_captcha = $this->get_current_captcha(); if ( ! $current_captcha ) { return false; } return $current_captcha->is_captcha_preview_ready(); } /** * Enqueue assets for the CAPTCHA settings page. * * @since 1.8.0 */ public function enqueues() { $current_captcha = $this->get_current_captcha(); if ( ! $current_captcha ) { return; } $current_captcha->enqueues(); } /** * Get current active captcha object. * * @since 1.8.0 * * @return object|string */ private function get_current_captcha() { return ! empty( $this->captchas[ $this->settings['provider'] ] ) ? $this->captchas[ $this->settings['provider'] ] : ''; } /** * Use the CAPTCHA no-conflict mode. * * When enabled in the WPForms settings, forcefully remove all other * CAPTCHA enqueues to prevent conflicts. Filter can be used to target * specific pages, etc. * * @since 1.6.4 */ public function apply_noconflict() { if ( ! wpforms_is_admin_page( 'settings', self::VIEW ) || empty( wpforms_setting( 'recaptcha-noconflict' ) ) || /** * Allow/disallow applying non-conflict mode for captcha scripts. * * @since 1.6.4 * * @param boolean $allow True/false. Default: true. */ ! apply_filters( 'wpforms_admin_settings_captcha_apply_noconflict', true ) // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName ) { return; } $scripts = wp_scripts(); $urls = [ 'google.com/recaptcha', 'gstatic.com/recaptcha', 'hcaptcha.com/1', 'challenges.cloudflare.com/turnstile' ]; foreach ( $scripts->queue as $handle ) { // Skip the WPForms JavaScript assets. if ( ! isset( $scripts->registered[ $handle ] ) || false !== strpos( $scripts->registered[ $handle ]->handle, 'wpforms' ) ) { return; } foreach ( $urls as $url ) { if ( false !== strpos( $scripts->registered[ $handle ]->src, $url ) ) { wp_dequeue_script( $handle ); wp_deregister_script( $handle ); break; } } } } } Admin/Settings/Captcha/ReCaptcha.php 0000644 00000005562 15174710275 0013323 0 ustar 00 <?php namespace WPForms\Admin\Settings\Captcha; /** * ReCaptcha settings class. * * @since 1.8.0 */ class ReCaptcha extends Captcha { /** * Captcha variable used for JS invoking. * * @since 1.8.0 * * @var string */ protected static $api_var = 'grecaptcha'; /** * Get captcha key name. * * @since 1.8.0 * * @var string */ protected static $slug = 'recaptcha'; /** * The ReCAPTCHA Javascript URL-resource. * * @since 1.8.0 * * @var string */ protected static $url = 'https://www.google.com/recaptcha/api.js'; /** * Array of captcha settings fields. * * @since 1.8.0 * * @return array[] */ public function get_settings_fields() { return [ 'recaptcha-heading' => [ 'id' => 'recaptcha-heading', 'content' => $this->get_field_desc(), 'type' => 'content', 'no_label' => true, 'class' => [ 'wpforms-setting-recaptcha', 'section-heading', 'specific-note' ], ], 'recaptcha-type' => [ 'id' => 'recaptcha-type', 'name' => esc_html__( 'Type', 'wpforms-lite' ), 'type' => 'radio', 'default' => 'v2', 'options' => [ 'v2' => esc_html__( 'Checkbox reCAPTCHA v2', 'wpforms-lite' ), 'invisible' => esc_html__( 'Invisible reCAPTCHA v2', 'wpforms-lite' ), 'v3' => esc_html__( 'reCAPTCHA v3', 'wpforms-lite' ), ], 'class' => [ 'wpforms-setting-recaptcha' ], ], 'recaptcha-site-key' => [ 'id' => 'recaptcha-site-key', 'name' => esc_html__( 'Site Key', 'wpforms-lite' ), 'type' => 'text', ], 'recaptcha-secret-key' => [ 'id' => 'recaptcha-secret-key', 'name' => esc_html__( 'Secret Key', 'wpforms-lite' ), 'type' => 'text', ], 'recaptcha-fail-msg' => [ 'id' => 'recaptcha-fail-msg', 'name' => esc_html__( 'Fail Message', 'wpforms-lite' ), 'desc' => esc_html__( 'Displays to users who fail the verification process.', 'wpforms-lite' ), 'type' => 'text', 'default' => esc_html__( 'Google reCAPTCHA verification failed, please try again later.', 'wpforms-lite' ), ], 'recaptcha-v3-threshold' => [ 'id' => 'recaptcha-v3-threshold', 'name' => esc_html__( 'Score Threshold', 'wpforms-lite' ), 'desc' => esc_html__( 'reCAPTCHA v3 returns a score (1.0 is very likely a good interaction, 0.0 is very likely a bot). If the score is less than or equal to this threshold, the form submission will be blocked and the message above will be displayed.', 'wpforms-lite' ), 'type' => 'number', 'attr' => [ 'step' => '0.1', 'min' => '0.0', 'max' => '1.0', ], 'default' => esc_html__( '0.4', 'wpforms-lite' ), 'class' => $this->settings['provider'] === 'recaptcha' && $this->settings['recaptcha_type'] === 'v3' ? [ 'wpforms-setting-recaptcha' ] : [ 'wpforms-setting-recaptcha', 'wpforms-hidden' ], ], ]; } } Admin/Settings/Captcha/HCaptcha.php 0000644 00000003076 15174710275 0013142 0 ustar 00 <?php namespace WPForms\Admin\Settings\Captcha; /** * HCaptcha settings class. * * @since 1.8.0 */ class HCaptcha extends Captcha { /** * Captcha variable used for JS invoking. * * @since 1.8.0 * * @var string */ protected static $api_var = 'hcaptcha'; /** * Get captcha key name. * * @since 1.8.0 * * @var string */ protected static $slug = 'hcaptcha'; /** * The hCaptcha Javascript URL-resource. * * @since 1.8.0 * * @var string */ protected static $url = 'https://hcaptcha.com/1/api.js'; /** * Array of captcha settings fields. * * @since 1.8.0 * * @return array[] */ public function get_settings_fields() { return [ 'hcaptcha-heading' => [ 'id' => 'hcaptcha-heading', 'content' => $this->get_field_desc(), 'type' => 'content', 'no_label' => true, 'class' => [ 'section-heading', 'specific-note' ], ], 'hcaptcha-site-key' => [ 'id' => 'hcaptcha-site-key', 'name' => esc_html__( 'Site Key', 'wpforms-lite' ), 'type' => 'text', ], 'hcaptcha-secret-key' => [ 'id' => 'hcaptcha-secret-key', 'name' => esc_html__( 'Secret Key', 'wpforms-lite' ), 'type' => 'text', ], 'hcaptcha-fail-msg' => [ 'id' => 'hcaptcha-fail-msg', 'name' => esc_html__( 'Fail Message', 'wpforms-lite' ), 'desc' => esc_html__( 'Displays to users who fail the verification process.', 'wpforms-lite' ), 'type' => 'text', 'default' => esc_html__( 'hCaptcha verification failed, please try again later.', 'wpforms-lite' ), ], ]; } } Admin/Settings/Captcha/Turnstile.php 0000644 00000004764 15174710275 0013465 0 ustar 00 <?php namespace WPForms\Admin\Settings\Captcha; /** * Cloudflare Turnstile settings class. * * @since 1.8.0 */ class Turnstile extends Captcha { /** * Captcha variable used for JS invoking. * * @since 1.8.0 * * @var string */ protected static $api_var = 'turnstile'; /** * Captcha key name. * * @since 1.8.0 * * @var string */ protected static $slug = 'turnstile'; /** * The Turnstile Javascript URL-resource. * * @since 1.8.0 * * @var string */ protected static $url = 'https://challenges.cloudflare.com/turnstile/v0/api.js'; /** * Inline script for captcha initialization JS code. * * @since 1.8.0 * * @return string */ protected function get_inline_script() { return /** @lang JavaScript */ 'const wpformsCaptcha = jQuery( ".wpforms-captcha" ); if ( wpformsCaptcha.length > 0 ) { var widgetID = ' . static::$api_var . '.render( ".wpforms-captcha", { "refresh-expired": "auto" } ); wpformsCaptcha.attr( "data-captcha-id", widgetID); jQuery( document ).trigger( "wpformsSettingsCaptchaLoaded" ); }'; } /** * Array of captcha settings fields. * * @since 1.8.0 * * @return array[] */ public function get_settings_fields() { return [ 'turnstile-heading' => [ 'id' => 'turnstile-heading', 'content' => $this->get_field_desc(), 'type' => 'content', 'no_label' => true, 'class' => [ 'section-heading', 'specific-note' ], ], 'turnstile-site-key' => [ 'id' => 'turnstile-site-key', 'name' => esc_html__( 'Site Key', 'wpforms-lite' ), 'type' => 'text', ], 'turnstile-secret-key' => [ 'id' => 'turnstile-secret-key', 'name' => esc_html__( 'Secret Key', 'wpforms-lite' ), 'type' => 'text', ], 'turnstile-fail-msg' => [ 'id' => 'turnstile-fail-msg', 'name' => esc_html__( 'Fail Message', 'wpforms-lite' ), 'desc' => esc_html__( 'Displays to users who fail the verification process.', 'wpforms-lite' ), 'type' => 'text', 'default' => esc_html__( 'Cloudflare Turnstile verification failed, please try again later.', 'wpforms-lite' ), ], 'turnstile-theme' => [ 'id' => 'turnstile-theme', 'name' => esc_html__( 'Type', 'wpforms-lite' ), 'type' => 'select', 'default' => 'auto', 'options' => [ 'auto' => esc_html__( 'Auto', 'wpforms-lite' ), 'light' => esc_html__( 'Light', 'wpforms-lite' ), 'dark' => esc_html__( 'Dark', 'wpforms-lite' ), ], ], ]; } } Admin/Settings/Captcha/Captcha.php 0000644 00000010051 15174710275 0013021 0 ustar 00 <?php namespace WPForms\Admin\Settings\Captcha; /** * Base captcha settings class. * * @since 1.8.0 */ abstract class Captcha { /** * Saved CAPTCHA settings. * * @since 1.8.0 * * @var array */ protected $settings; /** * List of required static properties. * * @since 1.8.0 * * @var array */ private $required_static_properties = [ 'api_var', 'slug', 'url', ]; /** * Initialize class. * * @since 1.8.0 */ public function init() { $this->settings = wp_parse_args( wpforms_get_captcha_settings(), [ 'provider' => 'none' ] ); foreach ( $this->required_static_properties as $property ) { if ( empty( static::${$property} ) ) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error trigger_error( sprintf( 'The $%s static property is required for a %s class', esc_html( $property ), __CLASS__ ), E_USER_ERROR ); } } } /** * Array of captcha settings fields. * * @since 1.8.0 * * @return array[] */ abstract public function get_settings_fields(); /** * Get API request url for the captcha preview. * * @since 1.8.0 * * @return string */ public function get_api_url() { $url = static::$url; if ( ! empty( $url ) ) { $url = add_query_arg( $this->get_api_url_query_arg(), $url ); } /** * Filter API URL. * * @since 1.6.4 * * @param string $url API URL. * @param array $settings Captcha settings array. */ return apply_filters( 'wpforms_admin_settings_captcha_get_api_url', $url, $this->settings ); } /** * Enqueue assets for the CAPTCHA settings page. * * @since 1.8.0 */ public function enqueues() { /** * Allow/disallow to enquire captcha settings. * * @since 1.6.4 * * @param boolean $allow True/false. Default: false. */ $disable_enqueues = apply_filters( 'wpforms_admin_settings_captcha_enqueues_disable', false ); if ( $disable_enqueues || ! $this->is_captcha_preview_ready() ) { return; } $api_url = $this->get_api_url(); $provider_name = $this->settings['provider']; $handle = "wpforms-settings-{$provider_name}"; // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion wp_enqueue_script( $handle, $api_url, [ 'jquery' ], null, true ); wp_add_inline_script( $handle, $this->get_inline_script() ); } /** * Inline script for initialize captcha JS code. * * @since 1.8.0 * * @return string */ protected function get_inline_script() { return /** @lang JavaScript */ 'var wpformsSettingsCaptchaLoad = function() { jQuery( ".wpforms-captcha" ).each( function( index, el ) { var widgetID = ' . static::$api_var . '.render( el ); jQuery( el ).attr( "data-captcha-id", widgetID ); } ); jQuery( document ).trigger( "wpformsSettingsCaptchaLoaded" ); };'; } /** * Check if CAPTCHA config is ready to display a preview. * * @since 1.8.0 * * @return bool */ public function is_captcha_preview_ready() { return ( ( $this->settings['provider'] === static::$slug || ( $this->settings['provider'] === 'recaptcha' && $this->settings['recaptcha_type'] === 'v2' ) ) && ! empty( $this->settings['site_key'] ) && ! empty( $this->settings['secret_key'] ) ); } /** * Retrieve query arguments for the CAPTCHA API URL. * * @since 1.8.0 * * @return array */ protected function get_api_url_query_arg() { /** * Modify captcha api url parameters. * * @since 1.8.0 * * @param array $params Array of parameters. * @param array $params Saved CAPTCHA settings. */ return (array) apply_filters( 'wpforms_admin_settings_captcha_get_api_url_query_arg', [ 'onload' => 'wpformsSettingsCaptchaLoad', 'render' => 'explicit', ], $this->settings ); } /** * Heading description. * * @since 1.8.0 * * @return string */ public function get_field_desc() { $content = wpforms_render( 'admin/settings/' . static::$slug . '-description' ); return wpforms_render( 'admin/settings/specific-note', [ 'content' => $content ], true ); } } Admin/Settings/Payments.php 0000644 00000003775 15174710275 0011732 0 ustar 00 <?php namespace WPForms\Admin\Settings; /** * Payments setting page. * Settings will be accessible via “WPForms” → “Settings” → “Payments”. * * @since 1.8.2 */ class Payments { /** * Initialize class. * * @since 1.8.2 */ public function init() { $this->hooks(); } /** * Hooks. * * @since 1.8.2 */ private function hooks() { add_filter( 'wpforms_settings_tabs', [ $this, 'register_settings_tabs' ], 5 ); add_filter( 'wpforms_settings_defaults', [ $this, 'register_settings_fields' ], 5 ); } /** * Register "Payments" settings tab. * * @since 1.8.2 * * @param array $tabs Admin area tabs list. * * @return array */ public function register_settings_tabs( $tabs ) { $payments = [ 'payments' => [ 'name' => esc_html__( 'Payments', 'wpforms-lite' ), 'form' => true, 'submit' => esc_html__( 'Save Settings', 'wpforms-lite' ), ], ]; return wpforms_array_insert( $tabs, $payments, 'validation' ); } /** * Register "Payments" settings fields. * * @since 1.8.2 * * @param array $settings Admin area settings list. * * @return array */ public function register_settings_fields( $settings ) { $currency_option = []; $currencies = wpforms_get_currencies(); // Format currencies for select element. foreach ( $currencies as $code => $currency ) { $currency_option[ $code ] = sprintf( '%s (%s %s)', $currency['name'], $code, $currency['symbol'] ); } $settings['payments'] = [ 'heading' => [ 'id' => 'payments-heading', 'content' => '<h4>' . esc_html__( 'Payments', 'wpforms-lite' ) . '</h4>', 'type' => 'content', 'no_label' => true, 'class' => [ 'section-heading', 'no-desc' ], ], 'currency' => [ 'id' => 'currency', 'name' => esc_html__( 'Currency', 'wpforms-lite' ), 'type' => 'select', 'choicesjs' => true, 'search' => true, 'default' => 'USD', 'options' => $currency_option, ], ]; return $settings; } } Admin/FlyoutMenu.php 0000644 00000007007 15174710275 0010431 0 ustar 00 <?php namespace WPForms\Admin; /** * Admin Flyout Menu. * * @since 1.5.7 */ class FlyoutMenu { /** * Constructor. * * @since 1.5.7 */ public function __construct() { if ( ! \wpforms_is_admin_page() || \wpforms_is_admin_page( 'builder' ) ) { return; } if ( ! \apply_filters( 'wpforms_admin_flyoutmenu', true ) ) { return; } // Check if WPForms Challenge can be displayed. if ( wpforms()->obj( 'challenge' )->challenge_can_start() ) { return; } $this->hooks(); } /** * Hooks. * * @since 1.5.7 */ public function hooks() { add_action( 'admin_footer', [ $this, 'output' ] ); } /** * Output menu. * * @since 1.5.7 */ public function output() { printf( '<div id="wpforms-flyout"> <div id="wpforms-flyout-items"> %1$s </div> <a href="#" class="wpforms-flyout-button wpforms-flyout-head"> <div class="wpforms-flyout-label">%2$s</div> <img src="%3$s" alt="%2$s" data-active="%4$s" /> </a> </div>', $this->get_items_html(), // phpcs:ignore \esc_attr__( 'See Quick Links', 'wpforms-lite' ), \esc_url( \WPFORMS_PLUGIN_URL . 'assets/images/admin-flyout-menu/sullie-default.svg' ), \esc_url( \WPFORMS_PLUGIN_URL . 'assets/images/admin-flyout-menu/sullie-active.svg' ) ); } /** * Generate menu items HTML. * * @since 1.5.7 * * @return string Menu items HTML. */ private function get_items_html() { $items = array_reverse( $this->menu_items() ); $items_html = ''; foreach ( $items as $item_key => $item ) { $items_html .= sprintf( '<a href="%1$s" target="_blank" rel="noopener noreferrer" class="wpforms-flyout-button wpforms-flyout-item wpforms-flyout-item-%2$d"%5$s%6$s> <div class="wpforms-flyout-label">%3$s</div> <i class="fa %4$s"></i> </a>', \esc_url( $item['url'] ), (int) $item_key, \esc_html( $item['title'] ), \sanitize_html_class( $item['icon'] ), ! empty( $item['bgcolor'] ) ? ' style="background-color: ' . \esc_attr( $item['bgcolor'] ) . '"' : '', ! empty( $item['hover_bgcolor'] ) ? ' onMouseOver="this.style.backgroundColor=\'' . \esc_attr( $item['hover_bgcolor'] ) . '\'" onMouseOut="this.style.backgroundColor=\'' . \esc_attr( $item['bgcolor'] ) . '\'"' : '' ); } return $items_html; } /** * Menu items data. * * @since 1.5.7 */ private function menu_items() { $is_pro = wpforms()->is_pro(); $utm_campaign = $is_pro ? 'plugin' : 'liteplugin'; $items = [ [ 'title' => \esc_html__( 'Upgrade to WPForms Pro', 'wpforms-lite' ), 'url' => wpforms_admin_upgrade_link( 'Flyout Menu', 'Upgrade to WPForms Pro' ), 'icon' => 'fa-star', 'bgcolor' => '#E1772F', 'hover_bgcolor' => '#ff8931', ], [ 'title' => \esc_html__( 'Support & Docs', 'wpforms-lite' ), 'url' => 'https://wpforms.com/docs/?utm_source=WordPress&utm_medium=Flyout Menu&utm_campaign=' . $utm_campaign . '&utm_content=Support', 'icon' => 'fa-life-ring', ], [ 'title' => \esc_html__( 'Join Our Community', 'wpforms-lite' ), 'url' => 'https://www.facebook.com/groups/wpformsvip/', 'icon' => 'fa-comments', ], [ 'title' => \esc_html__( 'Suggest a Feature', 'wpforms-lite' ), 'url' => 'https://wpforms.com/features/suggest/?utm_source=WordPress&utm_medium=Flyout Menu&utm_campaign=' . $utm_campaign . '&utm_content=Feature', 'icon' => 'fa-lightbulb-o', ], ]; if ( $is_pro ) { array_shift( $items ); } return \apply_filters( 'wpforms_admin_flyout_menu_items', $items ); } } Helpers/PluginSilentUpgrader.php 0000644 00000056446 15174710275 0013016 0 ustar 00 <?php namespace WPForms\Helpers; if ( ! defined( 'ABSPATH' ) ) { exit; } use WP_Error; use WP_Upgrader; use WP_Filesystem_Base; /** \WP_Upgrader class */ require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php'; /** \Plugin_Upgrader class */ require_once ABSPATH . 'wp-admin/includes/class-plugin-upgrader.php'; /** * In WP 5.3 a PHP 5.6 splat operator (...$args) was added to \WP_Upgrader_Skin::feedback(). * We need to remove all calls to *Skin::feedback() method, as we can't override it in own Skins * without breaking support for PHP 5.3-5.5. * * @internal Please do not use this class outside of core WPForms development. May be removed at any time. * * @since 1.5.6.1 */ class PluginSilentUpgrader extends \Plugin_Upgrader { /** * Run an upgrade/installation. * * Attempt to download the package (if it is not a local file), unpack it, and * install it in the destination folder. * * @since 1.5.6.1 * * @param array $options { * Array or string of arguments for upgrading/installing a package. * * @type string $package The full path or URI of the package to install. * Default empty. * @type string $destination The full path to the destination folder. * Default empty. * @type bool $clear_destination Whether to delete any files already in the * destination folder. Default false. * @type bool $clear_working Whether to delete the files form the working * directory after copying to the destination. * Default false. * @type bool $abort_if_destination_exists Whether to abort the installation if the destination * folder already exists. When true, `$clear_destination` * should be false. Default true. * @type bool $is_multi Whether this run is one of multiple upgrade/installation * actions being performed in bulk. When true, the skin * WP_Upgrader::header() and WP_Upgrader::footer() * aren't called. Default false. * @type array $hook_extra Extra arguments to pass to the filter hooks called by * WP_Upgrader::run(). * } * @return array|false|WP_error The result from self::install_package() on success, otherwise a WP_Error, * or false if unable to connect to the filesystem. */ public function run( $options ) { $defaults = [ 'package' => '', // Please always pass this. 'destination' => '', // And this 'clear_destination' => false, 'abort_if_destination_exists' => true, // Abort if the Destination directory exists, Pass clear_destination as false please 'clear_working' => true, 'is_multi' => false, 'hook_extra' => [], // Pass any extra $hook_extra args here, this will be passed to any hooked filters. ]; $options = wp_parse_args( $options, $defaults ); /** * Filter the package options before running an update. * * See also {@see 'upgrader_process_complete'}. * * @since 4.3.0 * * @param array $options { * Options used by the upgrader. * * @type string $package Package for update. * @type string $destination Update location. * @type bool $clear_destination Clear the destination resource. * @type bool $clear_working Clear the working resource. * @type bool $abort_if_destination_exists Abort if the Destination directory exists. * @type bool $is_multi Whether the upgrader is running multiple times. * @type array $hook_extra { * Extra hook arguments. * * @type string $action Type of action. Default 'update'. * @type string $type Type of update process. Accepts 'plugin', 'theme', or 'core'. * @type bool $bulk Whether the update process is a bulk update. Default true. * @type string $plugin Path to the plugin file relative to the plugins directory. * @type string $theme The stylesheet or template name of the theme. * @type string $language_update_type The language pack update type. Accepts 'plugin', 'theme', * or 'core'. * @type object $language_update The language pack update offer. * } * } */ $options = apply_filters( 'upgrader_package_options', $options ); if ( ! $options['is_multi'] ) { // call $this->header separately if running multiple times $this->skin->header(); } // Connect to the Filesystem first. $res = $this->fs_connect( [ WP_CONTENT_DIR, $options['destination'] ] ); // Mainly for non-connected filesystem. if ( ! $res ) { if ( ! $options['is_multi'] ) { $this->skin->footer(); } return false; } $this->skin->before(); if ( is_wp_error( $res ) ) { $this->skin->error( $res ); $this->skin->after(); if ( ! $options['is_multi'] ) { $this->skin->footer(); } return $res; } /* * Download the package (Note, This just returns the filename * of the file if the package is a local file) */ $download = $this->download_package( $options['package'], true ); // Allow for signature soft-fail. // WARNING: This may be removed in the future. if ( is_wp_error( $download ) && $download->get_error_data( 'softfail-filename' ) ) { // Don't output the 'no signature could be found' failure message for now. if ( (string) $download->get_error_code() !== 'signature_verification_no_signature' || WP_DEBUG ) { // Outout the failure error as a normal feedback, and not as an error: //$this->skin->feedback( $download->get_error_message() ); // Report this failure back to WordPress.org for debugging purposes. wp_version_check( [ 'signature_failure_code' => $download->get_error_code(), 'signature_failure_data' => $download->get_error_data(), ] ); } // Pretend this error didn't happen. $download = $download->get_error_data( 'softfail-filename' ); } if ( is_wp_error( $download ) ) { $this->skin->error( $download ); $this->skin->after(); if ( ! $options['is_multi'] ) { $this->skin->footer(); } return $download; } $delete_package = ( (string) $download !== (string) $options['package'] ); // Do not delete a "local" file. // Unzips the file into a temporary directory. $working_dir = $this->unpack_package( $download, $delete_package ); if ( is_wp_error( $working_dir ) ) { $this->skin->error( $working_dir ); $this->skin->after(); if ( ! $options['is_multi'] ) { $this->skin->footer(); } return $working_dir; } // With the given options, this installs it to the destination directory. $result = $this->install_package( [ 'source' => $working_dir, 'destination' => $options['destination'], 'clear_destination' => $options['clear_destination'], 'abort_if_destination_exists' => $options['abort_if_destination_exists'], 'clear_working' => $options['clear_working'], 'hook_extra' => $options['hook_extra'], ] ); $this->skin->set_result( $result ); if ( is_wp_error( $result ) ) { $this->skin->error( $result ); //$this->skin->feedback( 'process_failed' ); } else { // Installation succeeded. //$this->skin->feedback( 'process_success' ); } $this->skin->after(); if ( ! $options['is_multi'] ) { /** * Fire when the upgrader process is complete. * * See also {@see 'upgrader_package_options'}. * * @since 3.6.0 * @since 3.7.0 Added to WP_Upgrader::run(). * @since 4.6.0 `$translations` was added as a possible argument to `$hook_extra`. * * @param WP_Upgrader $this WP_Upgrader instance. In other contexts, $this, might be a * Theme_Upgrader, Plugin_Upgrader, Core_Upgrade, or * Language_Pack_Upgrader instance. * @param array $hook_extra { * Array of bulk item update data. * * @type string $action Type of action. Default 'update'. * @type string $type Type of update process. Accepts 'plugin', 'theme', 'translation', or 'core'. * @type bool $bulk Whether the update process is a bulk update. Default true. * @type array $plugins Array of the basename paths of the plugins' main files. * @type array $themes The theme slugs. * @type array $translations { * Array of translations update data. * * @type string $language The locale the translation is for. * @type string $type Type of translation. Accepts 'plugin', 'theme', or 'core'. * @type string $slug Text domain the translation is for. The slug of a theme/plugin or * 'default' for core translations. * @type string $version The version of a theme, plugin, or core. * } * } */ do_action( 'upgrader_process_complete', $this, $options['hook_extra'] ); $this->skin->footer(); } return $result; } /** * Toggle maintenance mode for the site. * * Create/delete the maintenance file to enable/disable maintenance mode. * * @since 2.8.0 * * @global WP_Filesystem_Base $wp_filesystem Subclass * * @param bool $enable True to enable maintenance mode, false to disable. */ public function maintenance_mode( $enable = false ) { global $wp_filesystem; $file = $wp_filesystem->abspath() . '.maintenance'; if ( $enable ) { //$this->skin->feedback( 'maintenance_start' ); // Create maintenance file to signal that we are upgrading $maintenance_string = '<?php $upgrading = ' . time() . '; ?>'; $wp_filesystem->delete( $file ); $wp_filesystem->put_contents( $file, $maintenance_string, FS_CHMOD_FILE ); } elseif ( ! $enable && $wp_filesystem->exists( $file ) ) { //$this->skin->feedback( 'maintenance_end' ); $wp_filesystem->delete( $file ); } } /** * Download a package. * * @since 2.8.0 * @since 5.5.0 Added the `$hook_extra` parameter. * * @param string $package The URI of the package. If this is the full path to an * existing local file, it will be returned untouched. * @param bool $check_signatures Whether to validate file signatures. Default false. * @param array $hook_extra Extra arguments to pass to the filter hooks. Default empty array. * @return string|WP_Error The full path to the downloaded package file, or a WP_Error object. */ public function download_package( $package, $check_signatures = false, $hook_extra = [] ) { /** * Filters whether to return the package. * * @since 3.7.0 * @since 5.5.0 Added the `$hook_extra` parameter. * * @param bool $reply Whether to bail without returning the package. * Default false. * @param string $package The package file name. * @param WP_Upgrader $this The WP_Upgrader instance. * @param array $hook_extra Extra arguments passed to hooked filters. */ $reply = apply_filters( 'upgrader_pre_download', false, $package, $this, $hook_extra ); if ( false !== $reply ) { return $reply; } if ( ! preg_match( '!^(http|https|ftp)://!i', $package ) && file_exists( $package ) ) { // Local file or remote? return $package; // Must be a local file. } if ( empty( $package ) ) { return new WP_Error( 'no_package', $this->strings['no_package'] ); } //$this->skin->feedback( 'downloading_package', $package ); $download_file = download_url( $package, 300, $check_signatures ); if ( is_wp_error( $download_file ) && ! $download_file->get_error_data( 'softfail-filename' ) ) { return new WP_Error( 'download_failed', $this->strings['download_failed'], $download_file->get_error_message() ); } return $download_file; } /** * Unpack a compressed package file. * * @since 2.8.0 * * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass. * * @param string $package Full path to the package file. * @param bool $delete_package Optional. Whether to delete the package file after attempting * to unpack it. Default true. * @return string|WP_Error The path to the unpacked contents, or a WP_Error on failure. */ public function unpack_package( $package, $delete_package = true ) { global $wp_filesystem; //$this->skin->feedback( 'unpack_package' ); $upgrade_folder = $wp_filesystem->wp_content_dir() . 'upgrade/'; //Clean up contents of upgrade directory beforehand. $upgrade_files = $wp_filesystem->dirlist( $upgrade_folder ); if ( ! empty( $upgrade_files ) ) { foreach ( $upgrade_files as $file ) { $wp_filesystem->delete( $upgrade_folder . $file['name'], true ); } } // We need a working directory - Strip off any .tmp or .zip suffixes $working_dir = $upgrade_folder . basename( basename( $package, '.tmp' ), '.zip' ); // Clean up working directory if ( $wp_filesystem->is_dir( $working_dir ) ) { $wp_filesystem->delete( $working_dir, true ); } // Unzip package to working directory $result = unzip_file( $package, $working_dir ); // Once extracted, delete the package if required. if ( $delete_package ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink unlink( $package ); } if ( is_wp_error( $result ) ) { $wp_filesystem->delete( $working_dir, true ); if ( $result->get_error_code() === 'incompatible_archive' ) { return new WP_Error( 'incompatible_archive', $this->strings['incompatible_archive'], $result->get_error_data() ); } return $result; } return $working_dir; } /** * Install a package. * * Copies the contents of a package form a source directory, and installs them in * a destination directory. Optionally removes the source. It can also optionally * clear out the destination folder if it already exists. * * @since 2.8.0 * * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass. * @global array $wp_theme_directories * * @param array|string $args { * Optional. Array or string of arguments for installing a package. Default empty array. * * @type string $source Required path to the package source. Default empty. * @type string $destination Required path to a folder to install the package in. * Default empty. * @type bool $clear_destination Whether to delete any files already in the destination * folder. Default false. * @type bool $clear_working Whether to delete the files form the working directory * after copying to the destination. Default false. * @type bool $abort_if_destination_exists Whether to abort the installation if * the destination folder already exists. Default true. * @type array $hook_extra Extra arguments to pass to the filter hooks called by * WP_Upgrader::install_package(). Default empty array. * } * * @return array|WP_Error The result (also stored in `WP_Upgrader::$result`), or a WP_Error on failure. */ public function install_package( $args = [] ) { global $wp_filesystem, $wp_theme_directories; $defaults = [ 'source' => '', // Please always pass this 'destination' => '', // and this 'clear_destination' => false, 'clear_working' => false, 'abort_if_destination_exists' => true, 'hook_extra' => [], ]; $args = wp_parse_args( $args, $defaults ); // These were previously extract()'d. $source = $args['source']; $destination = $args['destination']; $clear_destination = $args['clear_destination']; wpforms_set_time_limit( 300 ); if ( empty( $source ) || empty( $destination ) ) { return new WP_Error( 'bad_request', $this->strings['bad_request'] ); } //$this->skin->feedback( 'installing_package' ); /** * Filter the install response before the installation has started. * * Returning a truthy value, or one that could be evaluated as a WP_Error * will effectively short-circuit the installation, returning that value * instead. * * @since 2.8.0 * * @param bool|WP_Error $response Response. * @param array $hook_extra Extra arguments passed to hooked filters. */ $res = apply_filters( 'upgrader_pre_install', true, $args['hook_extra'] ); if ( is_wp_error( $res ) ) { return $res; } // Retain the Original source and destinations. $remote_source = $args['source']; $local_destination = $destination; $source_files = array_keys( $wp_filesystem->dirlist( $remote_source ) ); $remote_destination = $wp_filesystem->find_folder( $local_destination ); $count_source_files = count( $source_files ); // Locate which directory to copy to the new folder, This is based on the actual folder holding the files. if ( $count_source_files === 1 && $wp_filesystem->is_dir( trailingslashit( $args['source'] ) . $source_files[0] . '/' ) ) { // Only one folder? Then we want its contents. $source = trailingslashit( $args['source'] ) . trailingslashit( $source_files[0] ); } elseif ( $count_source_files === 0 ) { return new WP_Error( 'incompatible_archive_empty', $this->strings['incompatible_archive'], $this->strings['no_files'] ); // There are no files? } else { // It's only a single file, the upgrader will use the folder name of this file as the destination folder. Folder name is based on zip filename. $source = trailingslashit( $args['source'] ); } /** * Filter the source file location for the upgrade package. * * @since 2.8.0 * @since 4.4.0 The $hook_extra parameter became available. * * @param string $source File source location. * @param string $remote_source Remote file source location. * @param WP_Upgrader $this WP_Upgrader instance. * @param array $hook_extra Extra arguments passed to hooked filters. */ $source = apply_filters( 'upgrader_source_selection', $source, $remote_source, $this, $args['hook_extra'] ); if ( is_wp_error( $source ) ) { return $source; } // Has the source location changed? If so, we need a new source_files list. if ( $source !== $remote_source ) { $source_files = array_keys( $wp_filesystem->dirlist( $source ) ); } /* * Protection against deleting files in any important base directories. * Theme_Upgrader & Plugin_Upgrader also trigger this, as they pass the * destination directory (WP_PLUGIN_DIR / wp-content/themes) intending * to copy the directory into the directory, whilst they pass the source * as the actual files to copy. */ $protected_directories = [ ABSPATH, WP_CONTENT_DIR, WP_PLUGIN_DIR, WP_CONTENT_DIR . '/themes' ]; if ( is_array( $wp_theme_directories ) ) { $protected_directories = array_merge( $protected_directories, $wp_theme_directories ); } if ( in_array( $destination, $protected_directories ) ) { $remote_destination = trailingslashit( $remote_destination ) . trailingslashit( basename( $source ) ); $destination = trailingslashit( $destination ) . trailingslashit( basename( $source ) ); } if ( $clear_destination ) { // We're going to clear the destination if there's something there. $removed = $this->clear_destination( $remote_destination ); /** * Filter whether the upgrader cleared the destination. * * @since 2.8.0 * * @param mixed $removed Whether the destination was cleared. true on success, WP_Error on failure. * @param string $local_destination The local package destination. * @param string $remote_destination The remote package destination. * @param array $hook_extra Extra arguments passed to hooked filters. */ $removed = apply_filters( 'upgrader_clear_destination', $removed, $local_destination, $remote_destination, $args['hook_extra'] ); if ( is_wp_error( $removed ) ) { return $removed; } } elseif ( $args['abort_if_destination_exists'] && $wp_filesystem->exists( $remote_destination ) ) { // If we're not clearing the destination folder and something exists there already, Bail. // But first check to see if there are actually any files in the folder. $_files = $wp_filesystem->dirlist( $remote_destination ); if ( ! empty( $_files ) ) { $wp_filesystem->delete( $remote_source, true ); // Clear out the source files. return new WP_Error( 'folder_exists', $this->strings['folder_exists'], $remote_destination ); } } // Create destination if needed. if ( ! $wp_filesystem->exists( $remote_destination ) ) { if ( ! $wp_filesystem->mkdir( $remote_destination, FS_CHMOD_DIR ) ) { return new WP_Error( 'mkdir_failed_destination', $this->strings['mkdir_failed'], $remote_destination ); } } // Copy new version of item into place. $result = copy_dir( $source, $remote_destination ); if ( is_wp_error( $result ) ) { if ( $args['clear_working'] ) { $wp_filesystem->delete( $remote_source, true ); } return $result; } // Clear the Working folder? if ( $args['clear_working'] ) { $wp_filesystem->delete( $remote_source, true ); } $destination_name = basename( str_replace( $local_destination, '', $destination ) ); if ( $destination_name === '.' ) { $destination_name = ''; } $this->result = compact( 'source', 'source_files', 'destination', 'destination_name', 'local_destination', 'remote_destination', 'clear_destination' ); /** * Filter the installation response after the installation has finished. * * @since 2.8.0 * * @param bool $response Installation response. * @param array $hook_extra Extra arguments passed to hooked filters. * @param array $result Installation result data. */ $res = apply_filters( 'upgrader_post_install', true, $args['hook_extra'], $this->result ); if ( is_wp_error( $res ) ) { $this->result = $res; return $res; } // Bombard the calling function will all the info which we've just used. return $this->result; } /** * Install a plugin package. * * @since 1.6.3 * * @param string $package The full local path or URI of the package. * @param array $args Optional. Other arguments for installing a plugin package. Default empty array. * * @return bool|\WP_Error True if the installation was successful, false or a WP_Error otherwise. */ public function install( $package, $args = [] ) { $result = parent::install( $package, $args ); if ( true === $result ) { do_action( 'wpforms_plugin_installed', $package ); } return $result; } } Helpers/CacheBase.php 0000644 00000026101 15174710275 0010466 0 ustar 00 <?php namespace WPForms\Helpers; use WPForms\Tasks\Tasks; /** * Remote data cache handler. * * Usage example in `WPForms\Admin\Addons\AddonsCache` and `WPForms\Admin\Builder\TemplatesCache`. * * @since 1.6.8 */ abstract class CacheBase { /** * Encrypt a cached file. * * @since 1.8.7 */ protected const ENCRYPT = false; /** * Request lock time, min. * * @since 1.8.7 */ private const REQUEST_LOCK_TIME = 15; /** * A class id or array of cache class ids to sync updates with. * * @since 1.8.9 */ protected const SYNC_WITH = []; /** * The current class is syncing updates now. * * @since 1.8.9 * * @var bool */ private $syncing_updates = false; /** * Indicates whether the cache was updated during the current run. * * @since 1.6.8 * * @var bool */ protected $updated = false; /** * Settings. * * @since 1.6.8 * * @var array */ protected $settings; /** * Cache key. * * @since 1.8.2 * * @var string */ private $cache_key; /** * Cache dir. * * @since 1.8.2 * * @var string */ private $cache_dir; /** * Cache file. * * @since 1.8.2 * * @var string */ private $cache_file; /** * Determine if the class is allowed to load. * * @since 1.6.8 * * @return bool */ abstract protected function allow_load(); /** * Initialize. * * @since 1.6.8 */ public function init() { // Init settings before allow_load() as settings are used in get(). $this->update_settings(); $this->cache_key = $this->settings['cache_file']; $this->cache_dir = $this->get_cache_dir(); // See comment in the method. $this->cache_file = $this->cache_dir . $this->settings['cache_file']; // Do not update caches on heartbeat events. // phpcs:ignore WordPress.Security.NonceVerification.Missing $action = isset( $_POST['action'] ) ? sanitize_text_field( wp_unslash( $_POST['action'] ) ) : ''; if ( $action === 'heartbeat' ) { return; } if ( ! $this->allow_load() ) { return; } // Quit if settings weren't provided. if ( empty( $this->settings['remote_source'] ) || empty( $this->settings['cache_file'] ) ) { return; } $this->hooks(); } /** * Base hooks. * * @since 1.6.8 */ private function hooks(): void { add_action( 'shutdown', [ $this, 'cache_dir_complete' ] ); if ( empty( $this->settings['update_action'] ) ) { return; } // Schedule recurring updates. add_action( 'admin_init', [ $this, 'schedule_update_cache' ] ); add_action( $this->settings['update_action'], [ $this, 'update' ] ); // Sync cache updates. add_action( 'wpforms_helpers_cache_base_sync_updates', [ $this, 'sync_updates' ] ); } /** * Sync cache updates. * * If one update has been done, run the update for other caches. * * @since 1.8.9 * * @noinspection PhpCastIsUnnecessaryInspection * @noinspection UnnecessaryCastingInspection */ public function sync_updates(): void { // Prevent infinite loop. if ( $this->syncing_updates ) { foreach ( (array) static::SYNC_WITH as $classname ) { $cache = wpforms()->obj( $classname ); if ( ! $cache instanceof self ) { continue; } $cache->update( true ); } } } /** * Set up settings. * * @since 1.6.8 */ private function update_settings(): void { $default_settings = [ // Remote source URL. // For instance: 'https://wpformsapi.com/feeds/v1/addons/'. 'remote_source' => '', // Request timeout in seconds. 'timeout' => 10, // Cache file. // Just file name. For instance: 'addons.json'. 'cache_file' => '', // Cache time to live in seconds. 'cache_ttl' => WEEK_IN_SECONDS, // Scheduled update action. // For instance: 'wpforms_admin_addons_cache_update'. 'update_action' => '', // Additional query args for the remote source URL. 'query_args' => [], ]; $this->settings = wp_parse_args( $this->setup(), $default_settings ); } /** * Provide settings. * * @since 1.6.8 * * @return array Settings array. */ abstract protected function setup(); /** * Get a cache directory path. * * @since 1.6.8 * * @return string */ protected function get_cache_dir() { return File::get_cache_dir(); } /** * Get data from cache or from API call. * * @since 1.8.2 * * @return array */ public function get() { $cache = $this->get_from_cache(); if ( ! empty( $cache ) && ! $this->is_expired_cache() ) { return $cache; } $this->update(); return $this->get_from_cache(); } /** * Determine if the cache is expired. * * @since 1.8.2 * * @return bool */ private function is_expired_cache(): bool { return $this->cache_time() + $this->settings['cache_ttl'] < time(); } /** * Get cache creation time. * * @since 1.8.2 * * @return int */ private function cache_time(): int { return (int) Transient::get( $this->cache_key ); } /** * Determine if the cache file exists. * * @since 1.8.2 * * @return bool */ private function exists(): bool { return is_file( $this->cache_file ) && is_readable( $this->cache_file ); } /** * Get cache from a cache file. * * @since 1.8.2 * * @return array */ private function get_from_cache(): array { if ( ! $this->exists() ) { return []; } $content = File::get_contents( $this->cache_file ); // Do not decrypt non-encrypted legacy files, they will be encrypted on the scheduled update. if ( static::ENCRYPT && ! wpforms_is_json( $content ) ) { $content = Crypto::decrypt( $content ); } return (array) json_decode( $content, true ); } /** * Update cache. * * @since 1.8.2 * * @param bool $force Force update. * * @return bool */ public function update( bool $force = false ): bool { if ( ! $force && time() < $this->cache_time() + self::REQUEST_LOCK_TIME * MINUTE_IN_SECONDS ) { return false; } Transient::set( $this->cache_key, time(), $this->settings['cache_ttl'] ); if ( ! wp_mkdir_p( $this->cache_dir ) ) { return false; } $data = $this->perform_remote_request(); $content = wp_json_encode( $data ); $this->maybe_update_transient( $data ); if ( static::ENCRYPT ) { $content = Crypto::encrypt( $content ); } if ( ! File::put_contents( $this->cache_file, $content ) ) { return false; } if ( ! $this->syncing_updates ) { $this->syncing_updates = true; /** * Action hook after the cache has been updated. * * @since 1.8.9 */ do_action( 'wpforms_helpers_cache_base_sync_updates' ); } $this->updated = true; return true; } /** * Get data from API. * * @since 1.8.2 * * @return array */ protected function perform_remote_request(): array { $query_args = $this->settings['query_args'] ?? []; $request_url = add_query_arg( $query_args, $this->settings['remote_source'] ); $user_agent = wpforms_get_default_user_agent(); $request = wp_remote_get( $request_url, [ 'timeout' => $this->settings['timeout'], 'user-agent' => $user_agent, ] ); $request_url_log = remove_query_arg( [ 'tgm-updater-key' ], $request_url ); // Log if the request failed. if ( is_wp_error( $request ) ) { $this->add_log( 'Cached data: HTTP request error', [ 'class' => static::class, 'request_url' => $request_url_log, 'error' => $request->get_error_message(), 'error_data' => $request->get_error_data(), ], 'error' ); return []; } $response_code = wp_remote_retrieve_response_code( $request ); $raw_headers = wp_remote_retrieve_headers( $request ); $response_headers = is_object( $raw_headers ) ? $raw_headers->getAll() : (array) $raw_headers; $response_body = wp_remote_retrieve_body( $request ); $response_body_len = strlen( $response_body ); $response_body_log = $response_body_len > 1024 ? "(First 1 kB):\n" . substr( trim( $response_body ), 0, 1024 ) . '...' : trim( $response_body ); $response_body_log = esc_html( $response_body_log ); $log_data = [ 'class' => static::class, 'request_url' => $request_url_log, 'code' => $response_code, 'headers' => $response_headers, 'content_length' => $response_body_len, 'body' => $response_body_log, ]; // Log the response details in debug mode. if ( wpforms_debug() ) { $this->add_log( 'Cached data: Response details', $log_data ); } // Log the error if the response code is not 2xx or 3xx. if ( $response_code > 399 ) { $this->add_log( 'Cached data: HTTP request error', $log_data, 'error' ); return []; } $json = trim( $response_body ); $data = json_decode( $json, true ); if ( empty( $data ) ) { $message = $data === null ? 'Invalid JSON' : 'Empty JSON'; $log_data = array_merge( $log_data, [ 'json_result' => $message, 'cache_file' => $this->settings['cache_file'], 'remote_source' => $this->settings['remote_source'], ] ); $this->add_log( 'Cached data: ' . $message, $log_data, 'error' ); return []; } return $this->prepare_cache_data( $data ); } /** * Add log. * * @since 1.8.9 * * @param string $title Log title. * @param array $data Log data. * @param string $type Log type. */ protected function add_log( string $title, array $data, string $type = 'log' ): void { wpforms_log( $title, $data, [ 'type' => [ $type ], ] ); } /** * Schedule updates. * * @since 1.6.8 */ public function schedule_update_cache(): void { // Just skip if not need to register a scheduled action. if ( empty( $this->settings['update_action'] ) ) { return; } $tasks = wpforms()->obj( 'tasks' ); if ( ! $tasks instanceof Tasks || $tasks->is_scheduled( $this->settings['update_action'] ) !== false ) { return; } $tasks->create( $this->settings['update_action'] ) ->recurring( time() + $this->settings['cache_ttl'], $this->settings['cache_ttl'] ) ->params() ->register(); } /** * Complete the cache directory. * * @since 1.6.8 */ public function cache_dir_complete(): void { if ( ! $this->updated ) { return; } wpforms_create_upload_dir_htaccess_file(); wpforms_create_cache_dir_htaccess_file(); wpforms_create_index_html_file( $this->cache_dir ); wpforms_create_index_php_file( $this->cache_dir ); } /** * Invalidate cache. * * @since 1.8.7 */ public function invalidate_cache(): void { Transient::delete( $this->cache_key ); } /** * Prepare data to store in a local cache. * * @since 1.6.8 * * @param array|mixed $data Raw data received by the remote request. * * @return array Prepared data for caching. */ protected function prepare_cache_data( $data ): array { if ( empty( $data ) || ! is_array( $data ) ) { return []; } return $data; } /** * Maybe update transient duration time. * * Allows updating transient duration time if it's less than expiration time. * To do this, overwrite this method in child classes. * * @since 1.8.7 * * @param array $data Data received by the remote request. * * @return bool|array */ protected function maybe_update_transient( array $data ) { return $data; } } Helpers/Form.php 0000644 00000002631 15174710275 0007575 0 ustar 00 <?php namespace WPForms\Helpers; /** * Form helpers. * * @since 1.9.4 */ class Form { /** * Get form pro-fields array. * * @since 1.9.4 * * @param array|mixed $form_data Form data. * * @return array Pro fields array. */ public static function get_form_pro_fields( $form_data ): array { $fields = $form_data['fields'] ?? []; $pro_fields = []; foreach ( $fields as $field_data ) { /** * Filter form pro fields array. * * @since 1.9.4 * * @param array $pro_fields Pro-fields data. * @param array $field_data Field data. */ $pro_fields = apply_filters( 'wpforms_helpers_form_pro_fields', $pro_fields, $field_data ); } return $pro_fields; } /** * Get form addons educational data. * * @since 1.9.4 * * @param array|mixed $form_data Form data. * * @return array The form addons educational data. */ public static function get_form_addons_edu_data( $form_data ): array { $fields = $form_data['fields'] ?? []; $addons_edu_data = []; foreach ( $fields as $field_data ) { /** * Filter the form addons educational data. * * @since 1.9.4 * * @param array $addons_edu_data The form addons educational data. * @param array $field_data Field data. */ $addons_edu_data = apply_filters( 'wpforms_helpers_form_addons_edu_data', $addons_edu_data, $field_data ); } return $addons_edu_data; } } Helpers/Chain.php 0000644 00000020470 15174710275 0007715 0 ustar 00 <?php namespace WPForms\Helpers; use BadFunctionCallException; /** * Chain monad, useful for chaining a certain array or string related functions. * * @since 1.5.6 * * @method Chain array_change_key_case() * @method Chain array_chunk() * @method Chain array_column() * @method Chain array_combine() * @method Chain array_count_values() * @method Chain array_diff_assoc() * @method Chain array_diff_key() * @method Chain array_diff_uassoc() * @method Chain array_diff_ukey() * @method Chain array_diff(array $var) * @method Chain array_fill_keys() * @method Chain array_fill() * @method Chain array_filter() * @method Chain array_flip() * @method Chain array_intersect_assoc() * @method Chain array_intersect_key() * @method Chain array_intersect_uassoc() * @method Chain array_intersect_ukey() * @method Chain array_intersect(array $var) * @method Chain array_key_first() * @method Chain array_key_last() * @method Chain array_keys() * @method Chain array_map() * @method Chain array_merge_recursive() * @method Chain array_merge(array $var) * @method Chain array_pad() * @method Chain array_pop() * @method Chain array_product() * @method Chain array_rand() * @method Chain array_reduce() * @method Chain array_replace_recursive() * @method Chain array_replace() * @method Chain array_reverse() * @method Chain array_shift() * @method Chain array_slice() * @method Chain array_splice() * @method Chain array_sum() * @method Chain array_udiff_assoc() * @method Chain array_udiff_uassoc() * @method Chain array_udiff() * @method Chain array_uintersect_assoc() * @method Chain array_uintersect_uassoc() * @method Chain array_uintersect() * @method Chain array_unique() * @method Chain array_values() * @method Chain count() * @method Chain current() * @method Chain end() * @method Chain key() * @method Chain next() * @method Chain prev() * @method Chain range() * @method Chain reset() * @method Chain ltrim() * @method Chain rtrim() * @method Chain md5() * @method Chain str_getcsv() * @method Chain str_ireplace() * @method Chain str_pad() * @method Chain str_repeat() * @method Chain str_rot13() * @method Chain str_shuffle() * @method Chain str_split() * @method Chain str_word_count() * @method Chain strcasecmp() * @method Chain strchr() * @method Chain strcmp() * @method Chain strcoll() * @method Chain strcspn() * @method Chain strip_tags() * @method Chain stripcslashes() * @method Chain stripos() * @method Chain stripslashes() * @method Chain stristr() * @method Chain strlen() * @method Chain strnatcasecmp() * @method Chain strnatcmp() * @method Chain strncasecmp() * @method Chain strncmp() * @method Chain strpbrk() * @method Chain strpos() * @method Chain strrchr() * @method Chain strrev() * @method Chain strripos() * @method Chain strrpos() * @method Chain strspn() * @method Chain strstr() * @method Chain strtok() * @method Chain strtolower() * @method Chain strtoupper() * @method Chain strtr() * @method Chain substr_compare() * @method Chain substr_count() * @method Chain substr_replace() * @method Chain substr() * @method Chain trim() * @method Chain ucfirst() * @method Chain ucwords() * @method Chain vfprintf() * @method Chain vprintf() * @method Chain vsprintf() * @method Chain wordwrap() */ class Chain { /** * Current value. * * @since 1.5.6 * * @var mixed */ private $value; /** * Class constructor. * * @since 1.5.6 * * @param mixed $value Current value to start working with. */ public function __construct( $value ) { $this->value = $value; } /** * Bind some function to value. * * @since 1.5.6 * * @param mixed $fn Some function. * * @return Chain */ public function bind( $fn ) { $this->value = $fn( $this->value ); return $this; } /** * Get value. * * @since 1.5.6 * * @return mixed */ public function value() { return $this->value; } /** * Magic call. * * @since 1.5.6 * * @param string $name Method name. * @param array $params Parameters. * * @throws BadFunctionCallException Invalid function is called. * * @return Chain */ public function __call( $name, $params ) { if ( in_array( $name, $this->allowed_methods(), true ) ) { $params = $params === null ? [] : $params; array_unshift( $params, $this->value ); $this->value = call_user_func_array( $name, array_values( $params ) ); return $this; } throw new BadFunctionCallException( esc_html( "Provided function { $name } is not allowed. See Chain::allowed_methods()." ) ); } /** * Join array elements with a string. * * @since 1.5.6 * * @param string $glue Defaults to an empty string. * * @return Chain */ public function implode( $glue = '' ) { $this->value = implode( $glue, $this->value ); return $this; } /** * Split a string by a string. * * @since 1.5.6 * * @param string $delimiter The boundary string. * * @return Chain */ public function explode( $delimiter ) { $this->value = explode( $delimiter, $this->value ); return $this; } /** * Apply the callback to the elements of the given arrays. * * @since 1.5.6 * * @param callable $cb Callback. * * @return Chain */ public function map( $cb ) { $this->value = array_map( $cb, $this->value ); return $this; } /** * Pop array. * * @since 1.5.6 * * @return Chain */ public function pop() { $this->value = array_pop( $this->value ); return $this; } /** * Run first or second callback based on a condition. * * @since 1.5.6 * * @param callable $condition Condition function. * @param callable $true_result If condition will return true we run this function. * @param callable $false_result If condition will return false we run this function. * * @return Chain */ public function iif( $condition, $true_result, $false_result = null ) { if ( ! is_callable( $false_result ) ) { $false_result = function() { return ''; }; } $this->value = array_map( function( $el ) use ( $condition, $true_result, $false_result ) { if ( call_user_func( $condition, $el ) ) { return call_user_func( $true_result, $el ); } return call_user_func( $false_result, $el ); }, $this->value ); return $this; } /** * All allowed methods to work with data. * * @since 1.5.6 * * @return array */ public function allowed_methods() { return [ 'array_change_key_case', 'array_chunk', 'array_column', 'array_combine', 'array_count_values', 'array_diff_assoc', 'array_diff_key', 'array_diff_uassoc', 'array_diff_ukey', 'array_diff', 'array_fill_keys', 'array_fill', 'array_filter', 'array_flip', 'array_intersect_assoc', 'array_intersect_key', 'array_intersect_uassoc', 'array_intersect_ukey', 'array_intersect', 'array_key_first', 'array_key_last', 'array_keys', 'array_map', 'array_merge_recursive', 'array_merge', 'array_pad', 'array_pop', 'array_product', 'array_rand', 'array_reduce', 'array_replace_recursive', 'array_replace', 'array_reverse', 'array_shift', 'array_slice', 'array_splice', 'array_sum', 'array_udiff_assoc', 'array_udiff_uassoc', 'array_udiff', 'array_uintersect_assoc', 'array_uintersect_uassoc', 'array_uintersect', 'array_unique', 'array_values', 'count', 'current', 'end', 'key', 'next', 'prev', 'range', 'reset', 'implode', 'ltrim', 'rtrim', 'md5', 'str_getcsv', 'str_ireplace', 'str_pad', 'str_repeat', 'str_rot13', 'str_shuffle', 'str_split', 'str_word_count', 'strcasecmp', 'strchr', 'strcmp', 'strcoll', 'strcspn', 'strip_tags', 'stripcslashes', 'stripos', 'stripslashes', 'stristr', 'strlen', 'strnatcasecmp', 'strnatcmp', 'strncasecmp', 'strncmp', 'strpbrk', 'strpos', 'strrchr', 'strrev', 'strripos', 'strrpos', 'strspn', 'strstr', 'strtok', 'strtolower', 'strtoupper', 'strtr', 'substr_compare', 'substr_count', 'substr_replace', 'substr', 'trim', 'ucfirst', 'ucwords', 'vfprintf', 'vprintf', 'vsprintf', 'wordwrap', ]; } /** * Create myself. * * @since 1.5.6 * * @param mixed $value Current. * * @return Chain */ public static function of( $value = null ) { return new self( $value ); } } Helpers/Crypto.php 0000644 00000005563 15174710275 0010161 0 ustar 00 <?php namespace WPForms\Helpers; /** * Class for encryption functionality. * * @since 1.6.1.2 * * @link https://www.php.net/manual/en/intro.sodium.php */ class Crypto { /** * Get a secret key for encrypt/decrypt. * * @since 1.6.1.2 * * @return string */ public static function get_secret_key() { $secret_key = get_option( 'wpforms_crypto_secret_key' ); // If we already have the secret, send it back. if ( false !== $secret_key ) { return base64_decode( $secret_key ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode } // We don't have a secret, so let's generate one. $secret_key = sodium_crypto_secretbox_keygen(); add_option( 'wpforms_crypto_secret_key', base64_encode( $secret_key ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode return $secret_key; } /** * Encrypt a message. * * @since 1.6.1.2 * * @param string $message Message to encrypt. * @param string $key Encryption key. * * @return string */ public static function encrypt( $message, $key = '' ) { // Create a nonce for this operation. It will be stored and recovered in the message itself. $nonce = random_bytes( SODIUM_CRYPTO_SECRETBOX_NONCEBYTES ); if ( empty( $key ) ) { $key = self::get_secret_key(); } // Encrypt message and combine with nonce. $cipher = base64_encode( // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode $nonce . sodium_crypto_secretbox( $message, $nonce, $key ) ); try { sodium_memzero( $message ); sodium_memzero( $key ); } catch ( \Exception $e ) { return $cipher; } return $cipher; } /** * Decrypt a message. * * @since 1.6.1.2 * * @param string $encrypted Encrypted message. * @param string $key Encryption key. * * @return string */ public static function decrypt( $encrypted, $key = '' ) { // Unpack base64 message. $decoded = base64_decode( (string) $encrypted ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode if ( false === $decoded ) { return false; } if ( mb_strlen( $decoded, '8bit' ) < ( SODIUM_CRYPTO_SECRETBOX_NONCEBYTES + SODIUM_CRYPTO_SECRETBOX_MACBYTES ) ) { return false; } // Pull nonce and ciphertext out of unpacked message. $nonce = mb_substr( $decoded, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, '8bit' ); $ciphertext = mb_substr( $decoded, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, null, '8bit' ); if ( empty( $key ) ) { $key = self::get_secret_key(); } // Decrypt it. $message = sodium_crypto_secretbox_open( $ciphertext, $nonce, $key ); // Check for decrpytion failures. if ( false === $message ) { return false; } try { sodium_memzero( $ciphertext ); sodium_memzero( $key ); } catch ( \Exception $e ) { return $message; } return $message; } } Helpers/File.php 0000644 00000020422 15174710275 0007547 0 ustar 00 <?php namespace WPForms\Helpers; use WP_Filesystem_Base; // phpcs:ignore WPForms.PHP.UseStatement.UnusedUseStatement /** * Class File. * * @since 1.6.5 */ class File { /** * Remove UTF-8 BOM signature if it presents. * * @since 1.6.5 * * @param string $str String to process. * * @return string * @noinspection SpellCheckingInspection */ public static function remove_utf8_bom( $str ): string { if ( strpos( bin2hex( $str ), 'efbbbf' ) === 0 ) { $str = substr( $str, 3 ); } return $str; } /** * Get current filesystem. * * @since 1.8.6 * * @return WP_Filesystem_Base|null */ public static function get_filesystem(): ?WP_Filesystem_Base { global $wp_filesystem; static $is_filesystem_setup; if ( $is_filesystem_setup ) { return $wp_filesystem; } // We have to start the buffer to prevent output // when the file system is ssh/FTP but not configured. ob_start(); if ( ! function_exists( 'request_filesystem_credentials' ) ) { require_once ABSPATH . 'wp-admin/includes/file.php'; } // The current page URL. $url = home_url( esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ?? '' ) ) ); $credentials = request_filesystem_credentials( $url, '', false, false ); ob_end_clean(); if ( $credentials === false || ! WP_Filesystem( $credentials ) ) { wpforms_log( 'WP_Filesystem Error', 'File system isn\'t configured.', [ 'type' => [ 'error' ] ] ); return null; } $is_filesystem_setup = true; return $wp_filesystem; } /** * Get file contents. * * @since 1.8.6 * * @param string $file File path. * * @return string|false */ public static function get_contents( $file ) { $filesystem = self::get_filesystem(); if ( ! $filesystem || ! $filesystem->is_readable( $file ) || $filesystem->is_dir( $file ) ) { return false; } return $filesystem->size( $file ) > 0 ? $filesystem->get_contents( $file ) : ''; } /** * Save file contents. * * @since 1.8.6 * * @param string $file File path. * @param string $content File content. * * @return bool */ public static function put_contents( $file, $content ): bool { $filesystem = self::get_filesystem(); if ( ! $filesystem ) { return false; } return $filesystem->put_contents( $file, $content ); } /** * Determine whether a file or directory exists. * * @since 1.9.1 * * @param string $path Path to a file or directory. * * @return bool Whether $path exists or not. */ public static function exists( string $path ): bool { $filesystem = self::get_filesystem(); if ( ! $filesystem ) { return false; } return $filesystem->exists( $path ); } /** * Copies a file. * * @since 1.9.1 * * @param string $source Path to the source file. * @param string $destination Path to the destination file. * @param bool $overwrite Optional. Whether to overwrite the destination file if it exists. * Default false. * * @return bool True on success, false on failure. */ public static function copy( string $source, string $destination, bool $overwrite = false ): bool { $filesystem = self::get_filesystem(); if ( ! $filesystem ) { return false; } return $filesystem->copy( $source, $destination, $overwrite ); } /** * Move a file or files from source to destination. * * @since 1.8.8 * * @param string $source Source file or glob pattern. * @param string $destination Destination file or directory. * * @return bool */ public static function move( string $source, string $destination ): bool { $filesystem = self::get_filesystem(); if ( ! $filesystem ) { return false; } foreach ( glob( $source ) as $filename ) { $move = $filesystem->move( $filename, $destination . basename( $filename ), true ); if ( ! $move ) { return false; } } return true; } /** * Delete a file or directory. * * @since 1.8.8 * * @param string $file Path to the file or directory. * * @return bool */ public static function delete( string $file ): bool { $filesystem = self::get_filesystem(); if ( ! $filesystem ) { return false; } return $filesystem->delete( $file, true ); } /** * Create a directory. * * @since 1.8.8 * * @param string $dir Path directory. * * @return bool True on success, false on failure. If the directory already exists, this method will return true. */ public static function mkdir( string $dir ): bool { $filesystem = self::get_filesystem(); if ( ! $filesystem ) { return false; } if ( $filesystem->is_dir( $dir ) ) { return true; } return $filesystem->mkdir( $dir ); } /** * Gets details for files in a directory or a specific file. * * @since 1.8.8 * * @param string $dir Path directory. * * @return array|bool */ public static function dirlist( string $dir ) { $filesystem = self::get_filesystem(); if ( ! $filesystem || ! $filesystem->is_dir( $dir ) ) { return false; } return $filesystem->dirlist( $dir, false ); } /** * Get the upload directory path. * * @since 1.8.7 * * @return string */ public static function get_upload_dir(): string { static $upload_dir; if ( $upload_dir ) { /** * Since wpforms_upload_dir() relies on hooks, and hooks can be added unpredictably, * we need to cache the result of this method. * Otherwise, it is a risk to save a cache file to one dir and try to get from another. */ return $upload_dir; } $wpforms_upload_dir = wpforms_upload_dir(); $wpforms_upload_path = ! empty( $wpforms_upload_dir['path'] ) ? $wpforms_upload_dir['path'] : WP_CONTENT_DIR . '/uploads/wpforms'; $upload_dir = trailingslashit( wp_normalize_path( $wpforms_upload_path ) ); return $upload_dir; } /** * Get the upload directory URL. * * @since 1.9.7.3 * * @return string */ public static function get_upload_url(): string { static $upload_url; if ( $upload_url ) { /** * Since wpforms_upload_dir() relies on hooks, and hooks can be added unpredictably, * we need to cache the result of this method. * Otherwise, it is a risk to save a cache file to one dir and try to get from another. */ return $upload_url; } $wpforms_upload_dir = wpforms_upload_dir(); return ! empty( $wpforms_upload_dir['url'] ) ? $wpforms_upload_dir['url'] : WP_CONTENT_URL . '/uploads/wpforms'; } /** * Get the cache directory path. * * @since 1.8.6 * * @return string */ public static function get_cache_dir(): string { static $cache_dir; if ( $cache_dir ) { /** * Since wpforms_upload_dir() relies on hooks, and hooks can be added unpredictably, * we need to cache the result of this method. * Otherwise, it is a risk to save a cache file to one dir and try to get from another. */ return $cache_dir; } $cache_dir = self::get_upload_dir() . 'cache/'; return $cache_dir; } /** * Check whether the file is already updated. * * @since 1.8.7 * * @param string $filename Filename. * @param string $cache_key Cache key. * * @return bool */ public static function is_file_updated( string $filename, string $cache_key = '' ): bool { $filename = wp_normalize_path( $filename ); $cache_key = $cache_key ? $cache_key : 'wpforms_' . $filename . '_file'; if ( ! is_file( $filename ) ) { return false; } $cached_stat = Transient::get( $cache_key ); $stat = array_intersect_key( stat( $filename ), [ 'size' => 0, 'mtime' => 0, 'ctime' => 0, ] ); if ( $cached_stat === $stat ) { return true; } // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged, WordPress.WP.AlternativeFunctions.unlink_unlink @unlink( $filename ); return false; } /** * Save file updated stat. * * @since 1.8.7 * * @param string $filename Filename. * @param string $cache_key Cache key. * * @return void */ public static function save_file_updated_stat( string $filename, string $cache_key = '' ): void { $filename = wp_normalize_path( $filename ); $cache_key = $cache_key ? $cache_key : 'wpforms_' . $filename . '_file'; clearstatcache( true, $filename ); $stat = array_intersect_key( stat( $filename ), [ 'size' => 0, 'mtime' => 0, 'ctime' => 0, ] ); Transient::set( $cache_key, $stat ); } } Helpers/Templates.php 0000644 00000014412 15174710275 0010630 0 ustar 00 <?php namespace WPForms\Helpers; /** * Template related helper methods. * * @since 1.5.4 */ class Templates { /** * Return a list of paths to check for template locations * * @since 1.5.4 * * @return array */ public static function get_theme_template_paths() { $template_dir = 'wpforms'; $file_paths = [ 1 => trailingslashit( get_stylesheet_directory() ) . $template_dir, 10 => trailingslashit( get_template_directory() ) . $template_dir, 200 => trailingslashit( WPFORMS_PLUGIN_DIR ) . 'templates', ]; $file_paths = \apply_filters( 'wpforms_helpers_templates_get_theme_template_paths', $file_paths ); // Sort the file paths based on priority. \ksort( $file_paths, SORT_NUMERIC ); return \array_map( 'trailingslashit', $file_paths ); } /** * Locate a template and return the path for inclusion. * * @since 1.5.4 * * @param string $template_name Template name. * * @return string */ public static function locate( $template_name ) { // Trim off any slashes from the template name. $template_name = \ltrim( $template_name, '/' ); if ( empty( $template_name ) ) { return \apply_filters( 'wpforms_helpers_templates_locate', '', $template_name ); } $located = ''; // Try locating this template file by looping through the template paths. foreach ( self::get_theme_template_paths() as $template_path ) { if ( \file_exists( $template_path . $template_name ) ) { $located = $template_path . $template_name; break; } } return \apply_filters( 'wpforms_helpers_templates_locate', $located, $template_name ); } /** * Include a template. * Use 'require' if $args are passed or 'load_template' if not. * * @since 1.5.4 * * @param string $template_name Template name. * @param array $args Arguments. * @param bool $extract Extract arguments. * * @throws \RuntimeException If extract() tries to modify the scope. */ public static function include_html( $template_name, $args = [], $extract = false ) { $template_name .= '.php'; // Allow 3rd party plugins to filter template file from their plugin. $located = \apply_filters( 'wpforms_helpers_templates_include_html_located', self::locate( $template_name ), $template_name, $args, $extract ); $args = \apply_filters( 'wpforms_helpers_templates_include_html_args', $args, $template_name, $extract ); if ( empty( $located ) || ! \is_readable( $located ) ) { return; } // Load template WP way if no arguments were passed. if ( empty( $args ) ) { \load_template( $located, false ); return; } $extract = \apply_filters( 'wpforms_helpers_templates_include_html_extract_args', $extract, $template_name, $args ); if ( $extract && \is_array( $args ) ) { $created_vars_count = extract( $args, EXTR_SKIP ); // phpcs:ignore WordPress.PHP.DontExtract // Protecting existing scope from modification. if ( count( $args ) !== $created_vars_count ) { throw new \RuntimeException( 'Extraction failed: variable names are clashing with the existing ones.' ); } } require $located; } /** * Like self::include_html, but returns the HTML instead of including. * * @since 1.5.4 * * @param string $template_name Template name. * @param array $args Arguments. * @param bool $extract Extract arguments. * * @return string */ public static function get_html( $template_name, $args = [], $extract = false ) { \ob_start(); self::include_html( $template_name, $args, $extract ); return \ob_get_clean(); } /** * Validate that a file path is safe and within the expected path(s). * * Author Scott Kingsley Clark, Pods Framework. * Refactored to reduce cyclomatic complexity. * * @since 1.7.5.5 * * @link https://github.com/pods-framework/pods/commit/ea53471e58e638dec06957edc38f9fa86607652c * * @param string $path The file path. * @param null|array|string $paths_to_check The list of path types to check, defaults to just checking 'wpforms'. * Available: 'wpforms', 'plugins', 'theme', * or 'all' to check all supported paths. * * @return false|string False if the path was not allowed or did not exist, otherwise it returns the normalized path. */ public static function validate_safe_path( $path, $paths_to_check = null ) { static $available_checks; if ( ! $available_checks ) { $available_checks = [ 'wpforms' => realpath( WPFORMS_PLUGIN_DIR ), 'plugins' => [ realpath( WP_PLUGIN_DIR ), realpath( WPMU_PLUGIN_DIR ), ], 'theme' => [ realpath( get_stylesheet_directory() ), realpath( get_template_directory() ), ], ]; $available_checks['plugins'] = array_unique( array_filter( $available_checks['plugins'] ) ); $available_checks['theme'] = array_unique( array_filter( $available_checks['theme'] ) ); $available_checks = array_filter( $available_checks ); } $paths_to_check = $paths_to_check === null ? [ 'wpforms' ] : $paths_to_check; $paths_to_check = $paths_to_check === 'all' ? array_keys( $available_checks ) : $paths_to_check; $paths_to_check = (array) $paths_to_check; if ( empty( $paths_to_check ) ) { return false; } $path = wp_normalize_path( trim( (string) $path ) ); $match_count = 1; // Replace the ../ usage as many times as it may need to be replaced. while ( $match_count ) { $path = str_replace( '../', '', $path, $match_count ); } $path = realpath( $path ); foreach ( $paths_to_check as $check_type ) { if ( self::has_match( $path, $available_checks, $check_type ) ) { return $path; } } return false; } /** * Whether path matches. * * @since 1.7.5.5 * * @param string|bool $path Path. * @param array $available_checks Available checks. * @param string $check_type Check type. * * @return bool */ private static function has_match( $path, $available_checks, $check_type ) { if ( ! $path || ! isset( $available_checks[ $check_type ] ) ) { return false; } $check_type_paths = (array) $available_checks[ $check_type ]; foreach ( $check_type_paths as $path_to_check ) { if ( 0 === strpos( $path, $path_to_check ) && file_exists( $path ) ) { return true; } } return false; } } Helpers/Transient.php 0000644 00000016541 15174710275 0010646 0 ustar 00 <?php namespace WPForms\Helpers; /** * WPForms Transients implementation. * * @since 1.6.3.1 */ class Transient { /** * Transient option name prefix. * * @since 1.6.3.1 * * @var string */ const OPTION_PREFIX = '_wpforms_transient_'; /** * Transient timeout option name prefix. * * @since 1.6.3.1 * * @var string */ const TIMEOUT_PREFIX = '_wpforms_transient_timeout_'; /** * Get the value of a transient. * * If the transient does not exist, does not have a value, or has expired, * then the return value will be false. * * @since 1.6.3.1 * * @param string $transient Transient name. Expected to not be SQL-escaped. * * @return mixed Value of transient. */ public static function get( $transient ) { $transient_option = self::OPTION_PREFIX . $transient; $transient_timeout = self::TIMEOUT_PREFIX . $transient; $alloptions = wp_load_alloptions(); // If option is not in alloptions, it is not autoloaded and thus has a timeout to check. if ( ! isset( $alloptions[ $transient_option ] ) ) { $is_expired = self::is_expired( $transient ); } // Return the data if it's not expired. if ( empty( $is_expired ) ) { return self::get_option( $transient ); } delete_option( $transient_option ); delete_option( $transient_timeout ); return false; } /** * Set/update the value of a transient. * * You do not need to serialize values. If the value needs to be serialized, then * it will be serialized before it is set. * * @since 1.6.3.1 * * @param string $transient Transient name. Expected to not be SQL-escaped. Must be * 164 characters or fewer. * @param mixed $value Transient value. Must be serializable if non-scalar. * Expected to not be SQL-escaped. * @param int $expiration Optional. Time until expiration in seconds. Default 0 (no expiration). * * @return bool False if value was not set and true if value was set. */ public static function set( $transient, $value, $expiration = 0 ) { if ( false === self::get_option( $transient ) ) { return self::add( $transient, $value, $expiration ); } return self::update( $transient, $value, $expiration ); } /** * Create a new transient with a given value. * * Internal method, use Transient::set() instead. * * @since 1.6.3.1 * * @param string $transient Transient name. Expected to not be SQL-escaped. Must be * 164 characters or fewer. * @param mixed $value Transient value. Must be serializable if non-scalar. * Expected to not be SQL-escaped. * @param int $expiration Optional. Time until expiration in seconds. Default 0 (no expiration). * * @return bool False if value was not set and true if value was set. */ private static function add( $transient, $value, $expiration ) { if ( $expiration ) { add_option( self::TIMEOUT_PREFIX . $transient, time() + $expiration, '', 'no' ); } // If there's an expiration, the option won't be autoloaded. return add_option( self::OPTION_PREFIX . $transient, $value, '', $expiration ? 'no' : 'yes' ); } /** * Update the value of a transient. * * Internal method, use Transient::set() instead. * * @since 1.6.3.1 * * @param string $transient Transient name. Expected to not be SQL-escaped. Must be * 164 characters or fewer. * @param mixed $value Transient value. Must be serializable if non-scalar. * Expected to not be SQL-escaped. * @param int $expiration Optional. Time until expiration in seconds. Default 0 (no expiration). * * @return bool False if value was not set and true if value was set. */ private static function update( $transient, $value, $expiration ) { $transient_option = self::OPTION_PREFIX . $transient; $transient_timeout = self::TIMEOUT_PREFIX . $transient; if ( ! $expiration ) { return update_option( $transient_option, $value ); } $timeout = self::get_timeout( $transient ); if ( $timeout !== false ) { update_option( $transient_timeout, time() + $expiration ); return update_option( $transient_option, $value ); } // If expiration is requested, but the transient has no timeout option, // delete, then re-create transient rather than update. delete_option( $transient_option ); add_option( $transient_timeout, time() + $expiration, '', 'no' ); return add_option( $transient_option, $value, '', 'no' ); } /** * Delete a transient. * * @since 1.6.3.1 * * @param string $transient Transient name. Expected to not be SQL-escaped. * * @return bool true if successful, false otherwise */ public static function delete( $transient ) { $result = delete_option( self::OPTION_PREFIX . $transient ); if ( $result ) { delete_option( self::TIMEOUT_PREFIX . $transient ); } return $result; } /** * Delete all WPForms transients. * * @since 1.6.3.1 * * @return int|false Number of rows affected/selected or false on error */ public static function delete_all() { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching return $wpdb->query( $wpdb->prepare( "DELETE FROM $wpdb->options WHERE option_name LIKE %s", $wpdb->esc_like( self::OPTION_PREFIX ) . '%' ) ); } /** * Delete all expired WPForms transients. * * The multi-table delete syntax is used to delete the transient record * from table 'a', and the corresponding transient_timeout record from table 'b'. * * @since 1.6.3.1 * * @return int|false Number of rows affected/selected or false on error */ public static function delete_all_expired() { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching return $wpdb->query( $wpdb->prepare( "DELETE a, b FROM $wpdb->options a, $wpdb->options b WHERE a.option_name LIKE %s AND a.option_name NOT LIKE %s AND b.option_name = CONCAT( %s, SUBSTRING( a.option_name, %d ) ) AND b.option_value < %d", $wpdb->esc_like( self::OPTION_PREFIX ) . '%', $wpdb->esc_like( self::TIMEOUT_PREFIX ) . '%', self::TIMEOUT_PREFIX, strlen( self::OPTION_PREFIX ) + 1, time() ) ); } /** * Check if transient is expired. * * @since 1.6.3.1 * * @param string $transient Transient name. Expected to not be SQL-escaped. * * @return bool true if expired, false otherwise */ public static function is_expired( $transient ) { $timeout = self::get_timeout( $transient ); // If there's no timeout data found, the transient is considered to be valid. if ( $timeout === false ) { return false; } if ( $timeout >= time() ) { return false; } return true; } /** * Get a transient option value. * * @since 1.6.3.1 * * @param string $transient Transient name. Expected to not be SQL-escaped. * * @return mixed Value set for the option. */ private static function get_option( $transient ) { return get_option( self::OPTION_PREFIX . $transient ); } /** * Get a transient timeout option value. * * @since 1.6.3.1 * * @param string $transient Transient name. Expected to not be SQL-escaped. * * @return mixed Value set for the option. */ private static function get_timeout( $transient ) { return get_option( self::TIMEOUT_PREFIX . $transient ); } } Helpers/DB.php 0000644 00000015141 15174710275 0007157 0 ustar 00 <?php // phpcs:ignore Generic.Commenting.DocComment.MissingShort /** @noinspection PhpIllegalPsrClassPathInspection */ namespace WPForms\Helpers; // phpcs:ignore WPForms.PHP.UseStatement.UnusedUseStatement use WPForms_DB; use WPForms_Lite; use WPForms_Pro; /** * DB helpers. * * @since 1.8.7 */ class DB { /** * Existing tables transient name. * * @since 1.8.7 * @since 1.9.0 Changed from 'wpforms_existing_tables' to 'existing_tables' * * @var string */ const EXISTING_TABLES_TRANSIENT_NAME = 'existing_tables'; /** * Existing tables transient expiration, sec. * * @since 1.8.7 * * @var int * @noinspection SummerTimeUnsafeTimeManipulationInspection */ const EXISTING_TABLES_TRANSIENT_EXPIRATION = WEEK_IN_SECONDS; // A week. /** * Existing tables. * * @since 1.8.7 * * @var array */ private static $existing_tables = []; /** * Get the list of existing tables and cache the result. * * @since 1.8.7 * * @param string $table_name Table name. Can have SQL wildcard. * * @return array List of table names. */ public static function get_existing_tables( string $table_name ): array { global $wpdb; /** * Filters existence of a table before a request to the database is executed. * * @since 1.8.7 * * @param array $tables Existing tables with given table name. * @param string $table_name Table name. */ $tables = (array) apply_filters( 'wpforms_helpers_db_pre_get_existing_tables', [], $table_name ); if ( $tables ) { return $tables; } $tables = self::get_existing_tables_cache( $table_name ); if ( $tables ) { return $tables; } // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $tables = $wpdb->get_results( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table_name ), 'ARRAY_N' ); // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $tables = ! empty( $tables ) ? wp_list_pluck( $tables, 0 ) : []; self::set_existing_tables_cache( $tables, $table_name ); return self::$existing_tables[ $table_name ] ?? []; } /** * Get the list of all existing custom tables starting with `wpforms_*` and cache the result. * * @since 1.8.7 * * @return array List of table names. */ public static function get_existing_custom_tables(): array { global $wpdb; return self::get_existing_tables( "{$wpdb->prefix}wpforms_%" ); } /** * Check if the database table exists and cache the result. * * @since 1.8.7 * * @param string $table_name Table name. Can have SQL wildcard. * * @return bool */ public static function table_exists( string $table_name ): bool { /** * Filters existence of a table before a request to the database is executed. * * @since 1.8.7 * * @param integer $exists Table exists. * @param string $table_name Table name. */ if ( apply_filters( 'wpforms_helpers_db_pre_table_exists', false, $table_name ) ) { return true; } foreach ( self::get_existing_tables( $table_name ) as $existing_table ) { if ( self::wildcard_match( $table_name, $existing_table ) ) { return true; } } return false; } /** * Get the list of existing tables from cache. * * @since 1.8.7 * * @param string $table_name Table name. Can have SQL wildcard. * * @return array List of table names. */ private static function get_existing_tables_cache( string $table_name ): array { $tables = Transient::get( self::EXISTING_TABLES_TRANSIENT_NAME ); self::$existing_tables = $tables ? $tables : []; return self::$existing_tables[ $table_name ] ?? []; } /** * Set existing tables cache. * * @since 1.8.7 * * @param array $tables Existing tables with given table name. * @param string $table_name Table name. * * @return void */ private static function set_existing_tables_cache( array $tables, string $table_name ) { if ( empty( $tables ) ) { return; } self::$existing_tables[ $table_name ] = $tables; /** * Filters existing tables transient expiration time. * * @since 1.8.7 * * @param integer $expiration Expiration time. */ $expiration = apply_filters( 'wpforms_helpers_db_existing_tables_transient_expiration', self::EXISTING_TABLES_TRANSIENT_EXPIRATION ); Transient::set( self::EXISTING_TABLES_TRANSIENT_NAME, self::$existing_tables, $expiration ); } /** * Flush existing tables cache. * * @since 1.9.0 * * @return void */ public static function flush_existing_tables_cache() { self::$existing_tables = []; Transient::delete( self::EXISTING_TABLES_TRANSIENT_NAME ); } /** * Wildcard match. * Works as MySQL LIKE match. * * @since 1.8.7 * * @param string $pattern Pattern. * @param string $subject String to search into. * * @return false|int */ private static function wildcard_match( string $pattern, string $subject ) { $regex = str_replace( [ '%', '_' ], // MySQL wildcard chars. [ '.*', '.' ], // Regexp chars. preg_quote( $pattern, '/' ) ); return preg_match( '/^' . $regex . '$/is', $subject ); } /** * Check if all custom tables exist. * * @since 1.9.0 * * @return bool True if all custom tables exist. False if any is missing. */ public static function custom_tables_exist(): bool { global $wpdb; $existing_tables = self::get_existing_custom_tables(); $custom_tables = wpforms()->is_pro() ? WPForms_Pro::CUSTOM_TABLES : WPForms_Lite::CUSTOM_TABLES; foreach ( $custom_tables as $table_name => $handler_class ) { if ( ! in_array( $wpdb->prefix . $table_name, $existing_tables, true ) ) { return false; } } return true; } /** * Create all custom DB tables. * * @since 1.9.0 * * @param bool $flush_cache Clear existing custom tables cache. * * @noinspection PhpPossiblePolymorphicInvocationInspection */ public static function create_custom_tables( bool $flush_cache = false ) { global $wpdb; if ( $flush_cache ) { self::flush_existing_tables_cache(); } $existing_tables = self::get_existing_custom_tables(); $custom_tables = wpforms()->is_pro() ? WPForms_Pro::CUSTOM_TABLES : WPForms_Lite::CUSTOM_TABLES; $created = false; foreach ( $custom_tables as $table_name => $handler_class ) { if ( in_array( $wpdb->prefix . $table_name, $existing_tables, true ) ) { continue; } /** * Child class of WPForms_DB. * * @var $handler WPForms_DB */ $handler = new $handler_class(); // Create a table. $handler->create_table(); $created = true; } if ( $created ) { Transient::delete( self::EXISTING_TABLES_TRANSIENT_NAME ); } } } Db/Payments/Payment.php 0000644 00000032224 15174710275 0011033 0 ustar 00 <?php namespace WPForms\Db\Payments; use WPForms_DB; /** * Class for the Payments database table. * * @since 1.8.2 */ class Payment extends WPForms_DB { /** * Primary class constructor. * * @since 1.8.2 */ public function __construct() { parent::__construct(); $this->table_name = self::get_table_name(); $this->primary_key = 'id'; $this->type = 'payment'; } /** * Get the table name. * * @since 1.8.2 * * @return string */ public static function get_table_name() { global $wpdb; return $wpdb->prefix . 'wpforms_payments'; } /** * Get table columns. * * @since 1.8.2 * * @return array */ public function get_columns() { return [ 'id' => '%d', 'form_id' => '%d', 'status' => '%s', 'subtotal_amount' => '%f', 'discount_amount' => '%f', 'total_amount' => '%f', 'currency' => '%s', 'entry_id' => '%d', 'gateway' => '%s', 'type' => '%s', 'mode' => '%s', 'transaction_id' => '%s', 'customer_id' => '%s', 'subscription_id' => '%s', 'subscription_status' => '%s', 'title' => '%s', 'date_created_gmt' => '%s', 'date_updated_gmt' => '%s', 'is_published' => '%d', ]; } /** * Default column values. * * @since 1.8.2 * * @return array */ public function get_column_defaults() { $date = gmdate( 'Y-m-d H:i:s' ); return [ 'form_id' => 0, 'status' => '', 'subtotal_amount' => 0, 'discount_amount' => 0, 'total_amount' => 0, 'currency' => '', 'entry_id' => 0, 'gateway' => '', 'type' => '', 'mode' => '', 'transaction_id' => '', 'customer_id' => '', 'subscription_id' => '', 'subscription_status' => '', 'title' => '', 'date_created_gmt' => $date, 'date_updated_gmt' => $date, 'is_published' => 1, ]; } /** * Insert a new payment into the database. * * @since 1.8.2 * * @param array $data Column data. * @param string $type Optional. Data type context. * * @return int ID for the newly inserted payment. Zero otherwise. */ public function add( $data, $type = '' ) { // Return early if the status is not allowed. // TODO: consider validating other properties as well or get rid of it. if ( isset( $data['status'] ) && ! ValueValidator::is_valid( $data['status'], 'status' ) ) { return 0; } // Use database type identifier if a context is empty. $type = empty( $type ) ? $this->type : $type; return parent::add( $data, $type ); } /** * Retrieve a payment from the database based on a given payment ID. * * @since 1.8.2 * * @param int $payment_id Payment ID. * @param array $args Additional arguments. * * @return object|null */ public function get( $payment_id, $args = [] ) { if ( ! $this->current_user_can( $payment_id, $args ) && wpforms()->obj( 'access' )->init_allowed() ) { return null; } $payment = parent::get( $payment_id ); return $payment ? $this->cast_amounts_to_float( $payment ) : null; } /** * Retrieve a row based on column value. * * @since 1.8.7 * * @param string $column Column name. * @param int|string $value Column value. * * @return object|null Database query result, object or null on failure. */ public function get_by( $column, $value ) { $payment = parent::get_by( $column, $value ); return $payment ? $this->cast_amounts_to_float( $payment ) : null; } /** * Cast amounts to float in the given payment data object. * * @since 1.8.7 * * @param object $payment Payment ID. * * @return object */ private function cast_amounts_to_float( $payment ) { if ( empty( $payment ) || ! is_object( $payment ) ) { return $payment; } // Amounts is stored in DB as decimal(26,8), but appear here as strings. // Therefore, they should be cast to float to avoid further multi-time currency conversion. $payment->subtotal_amount = $payment->subtotal_amount ? (float) $payment->subtotal_amount : 0; $payment->discount_amount = $payment->discount_amount ? (float) $payment->discount_amount : 0; $payment->total_amount = $payment->total_amount ? (float) $payment->total_amount : 0; return $payment; } /** * Update an existing payment in the database. * * @since 1.8.2 * * @param string $payment_id Payment ID. * @param array $data Array of columns and associated data to update. * @param string $where Column to match against in the WHERE clause. If empty, $primary_key will be used. * @param string $type Data type context. * @param array $args Additional arguments. * * @return bool */ public function update( $payment_id, $data = [], $where = '', $type = '', $args = [] ) { if ( ! $this->current_user_can( $payment_id, $args ) ) { return false; } // TODO: consider validating other properties as well or get rid of it. if ( isset( $data['status'] ) && ! ValueValidator::is_valid( $data['status'], 'status' ) ) { return false; } // Use database type identifier if a context is empty. $type = empty( $type ) ? $this->type : $type; return parent::update( $payment_id, $data, $where, $type ); } /** * Delete a payment from the database, also removes payment meta. * * @since 1.8.2 * * @param int $payment_id Payment ID. * @param array $args Additional arguments. * * @return bool False if the payment and meta could not be deleted, true otherwise. */ public function delete( $payment_id = 0, $args = [] ): bool { if ( ! $this->current_user_can( $payment_id, $args ) ) { return false; } $is_payment_deleted = parent::delete( $payment_id ); $is_meta_deleted = wpforms()->obj( 'payment_meta' )->delete_by( 'payment_id', $payment_id ); return $is_payment_deleted && $is_meta_deleted; } /** * Retrieve a list of payments. * * @since 1.8.2 * * @param array $args Arguments. * * @return array */ public function get_payments( $args = [] ) { global $wpdb; $args = $this->sanitize_get_payments_args( $args ); if ( ! $this->current_user_can( 0, $args ) ) { return []; } // Prepare query. $query[] = "SELECT p.* FROM {$this->table_name} as p"; /** * Filter the query for get_payments method before the WHERE clause. * * @since 1.8.2 * * @param string $where Before the WHERE clause in DB query. * @param array $args Query arguments. * * @return string */ $query[] = apply_filters( 'wpforms_db_payments_payment_get_payments_query_before_where', '', $args ); $query[] = 'WHERE 1=1'; $query[] = $this->add_columns_where_conditions( $args ); $query[] = $this->add_secondary_where_conditions( $args ); /** * Extend the query for the get_payments method after the WHERE clause. * * This hook provides the flexibility to modify the SQL query by appending custom conditions * right after the WHERE clause. * * @since 1.8.4 * * @param string $where After the WHERE clause in the database query. * @param array $args Query arguments. * * @return string */ $query[] = apply_filters( 'wpforms_db_payments_payment_get_payments_query_after_where', '', $args ); // Order. $query[] = sprintf( 'ORDER BY %s', sanitize_sql_orderby( "{$args['orderby']} {$args['order']}" ) ); // Limit. $query[] = $wpdb->prepare( 'LIMIT %d, %d', $args['offset'], $args['number'] ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared $result = $wpdb->get_results( implode( ' ', $query ), ARRAY_A ); // Get results. return ! $result ? [] : $result; } /** * Create the table. * * @since 1.8.2 */ public function create_table() { global $wpdb; $charset_collate = $wpdb->get_charset_collate(); /** * To avoid any possible issues during migration from entries to payments' table, * all data types are preserved. * * Note: there must be two spaces between the words PRIMARY KEY and the definition of primary key. * * @link https://codex.wordpress.org/Creating_Tables_with_Plugins#Creating_or_Updating_the_Table */ $query = "CREATE TABLE $this->table_name ( id bigint(20) NOT NULL AUTO_INCREMENT, form_id bigint(20) NOT NULL, status varchar(10) NOT NULL DEFAULT '', subtotal_amount decimal(26,8) NOT NULL DEFAULT 0, discount_amount decimal(26,8) NOT NULL DEFAULT 0, total_amount decimal(26,8) NOT NULL DEFAULT 0, currency varchar(3) NOT NULL DEFAULT '', entry_id bigint(20) NOT NULL DEFAULT 0, gateway varchar(20) NOT NULL DEFAULT '', type varchar(12) NOT NULL DEFAULT '', mode varchar(4) NOT NULL DEFAULT '', transaction_id varchar(40) NOT NULL DEFAULT '', customer_id varchar(40) NOT NULL DEFAULT '', subscription_id varchar(40) NOT NULL DEFAULT '', subscription_status varchar(10) NOT NULL DEFAULT '', title varchar(255) NOT NULL DEFAULT '', date_created_gmt datetime NOT NULL, date_updated_gmt datetime NOT NULL, is_published tinyint(1) NOT NULL DEFAULT 1, PRIMARY KEY (id), KEY form_id (form_id), KEY status (status(8)), KEY total_amount (total_amount), KEY type (type(8)), KEY transaction_id (transaction_id(32)), KEY customer_id (customer_id(32)), KEY subscription_id (subscription_id(32)), KEY subscription_status (subscription_status(8)), KEY title (title(64)) ) $charset_collate;"; require_once ABSPATH . 'wp-admin/includes/upgrade.php'; dbDelta( $query ); } /** * Check if the current user has capabilities to manage payments. * * @since 1.8.2 * * @param int $payment_id Payment ID. * @param array $args Additional arguments. * * @return bool * @noinspection IfReturnReturnSimplificationInspection */ private function current_user_can( $payment_id, $args = [] ) { $manage_cap = wpforms_get_capability_manage_options(); if ( ! isset( $args['cap'] ) ) { $args['cap'] = $manage_cap; } if ( ! empty( $args['cap'] ) && ! wpforms_current_user_can( $args['cap'], $payment_id ) ) { return false; } return true; } /** * Construct where clauses for selected columns. * * @since 1.8.4 * * @param array $args Query arguments. * * @return string */ public function add_columns_where_conditions( $args = [] ) { // Allowed columns for filtering. $allowed_cols = [ 'form_id', 'entry_id', 'status', 'subscription_status', 'type', 'gateway', ]; $where = ''; // Determine if this is a table query. $is_table_query = ! empty( $args['table_query'] ); $keys_to_validate = [ 'status', 'subscription_status', 'type', 'gateway' ]; foreach ( $args as $key => $value ) { if ( empty( $value ) || ! in_array( $key, $allowed_cols, true ) ) { continue; } // Explode values if needed. $values = explode( '|', $value ); // Run some keys through the "ValueValidator" class to make sure they are valid. if ( in_array( $key, $keys_to_validate, true ) ) { $values = array_filter( $values, static function ( $v ) use ( $key ) { return ValueValidator::is_valid( $v, $key ); } ); } // Skip if no valid values found. if ( empty( $values ) ) { continue; } // Merge "Partially Refunded" status with "Refunded" status. if ( $is_table_query && $key === 'status' && in_array( 'refunded', $values, true ) ) { $values[] = 'partrefund'; } $placeholders = wpforms_wpdb_prepare_in( $values ); // Prepare and add to WHERE clause. $where .= " AND {$key} IN ({$placeholders})"; } return $where; } /** * Construct secondary where clauses. * * @since 1.8.2 * * @param array $args Query arguments. * * @return string */ public function add_secondary_where_conditions( $args = [] ) { global $wpdb; /** * Filter arguments needed for all query. * * @since 1.8.2 * * @param array $args Query arguments. */ $args = (array) apply_filters( 'wpforms_db_payments_payment_add_secondary_where_conditions_args', $args ); $args = wp_parse_args( (array) $args, [ 'currency' => wpforms_get_currency(), 'mode' => 'live', 'is_published' => 1, ] ); $where = ''; // If it's a valid mode, add it to a WHERE clause. if ( ValueValidator::is_valid( $args['mode'], 'mode' ) ) { $where .= $wpdb->prepare( ' AND mode = %s', $args['mode'] ); } $where .= $wpdb->prepare( ' AND currency = %s', $args['currency'] ); $where .= $wpdb->prepare( ' AND is_published = %d', $args['is_published'] ); return $where; } /** * Sanitize query arguments for get_payments() method. * * @since 1.8.2 * * @param array $args Query arguments. * * @return array */ private function sanitize_get_payments_args( $args ) { $defaults = [ 'number' => 20, 'offset' => 0, 'orderby' => 'id', 'order' => 'DESC', ]; $args = wp_parse_args( (array) $args, $defaults ); // Sanitize. $args['number'] = absint( $args['number'] ); $args['offset'] = absint( $args['offset'] ); if ( $args['number'] === 0 ) { $args['number'] = $defaults['number']; } return $args; } } Db/Payments/ValueValidator.php 0000644 00000010546 15174710275 0012343 0 ustar 00 <?php namespace WPForms\Db\Payments; /** * ValueValidator class. * * This class is used to validate values for the Payments DB table. * * @since 1.8.2 */ class ValueValidator { /** * Check if value is valid for the given column. * * @since 1.8.2 * * @param string $value Value to check if is valid. * @param string $column Database column name. * * @return bool */ public static function is_valid( $value, $column ) { $method = 'get_allowed_' . self::get_plural_column_name( $column ); if ( ! method_exists( __CLASS__, $method ) ) { return false; } return isset( self::$method()[ $value ] ); } /** * Get allowed modes. * * @since 1.8.2 * * @return array */ private static function get_allowed_modes() { return [ 'live' => esc_html__( 'Live', 'wpforms-lite' ), 'test' => esc_html__( 'Test', 'wpforms-lite' ), ]; } /** * Get allowed gateways. * * @since 1.8.2 * * @return array */ public static function get_allowed_gateways() { /** * Filter allowed gateways. * * @since 1.8.2 * * @param array $gateways Array of allowed gateways. */ return (array) apply_filters( 'wpforms_db_payments_value_validator_get_allowed_gateways', [ 'paypal_standard' => esc_html__( 'PayPal Standard', 'wpforms-lite' ), 'paypal_commerce' => esc_html__( 'PayPal Commerce', 'wpforms-lite' ), 'stripe' => esc_html__( 'Stripe', 'wpforms-lite' ), 'square' => esc_html__( 'Square', 'wpforms-lite' ), 'authorize_net' => esc_html__( 'Authorize.net', 'wpforms-lite' ), ] ); } /** * Get allowed statuses. * * @since 1.8.2 * * @return array */ public static function get_allowed_statuses() { return array_merge( self::get_allowed_one_time_statuses(), self::get_allowed_subscription_statuses() ); } /** * Get allowed one-time payment statuses. * * @since 1.8.4 * * @return array */ public static function get_allowed_one_time_statuses() { return [ 'processed' => __( 'Processed', 'wpforms-lite' ), 'completed' => __( 'Completed', 'wpforms-lite' ), 'pending' => __( 'Pending', 'wpforms-lite' ), 'failed' => __( 'Failed', 'wpforms-lite' ), 'refunded' => __( 'Refunded', 'wpforms-lite' ), 'partrefund' => __( 'Partially Refunded', 'wpforms-lite' ), ]; } /** * Get allowed subscription statuses. * * @since 1.8.2 * * @return array */ public static function get_allowed_subscription_statuses() { return [ 'active' => __( 'Active', 'wpforms-lite' ), 'cancelled' => __( 'Cancelled', 'wpforms-lite' ), 'not-synced' => __( 'Not Synced', 'wpforms-lite' ), 'failed' => __( 'Failed', 'wpforms-lite' ), ]; } /** * Get allowed types. * * @since 1.8.2 * * @return array */ public static function get_allowed_types() { return array_merge( [ 'one-time' => __( 'One-Time', 'wpforms-lite' ), ], self::get_allowed_subscription_types() ); } /** * Get allowed subscription types. * * @since 1.8.2 * * @return array */ public static function get_allowed_subscription_types() { return [ 'subscription' => __( 'Subscription', 'wpforms-lite' ), 'renewal' => __( 'Renewal', 'wpforms-lite' ), ]; } /** * Get allowed subscription intervals. * The measurement of time between billing occurrences for an automated recurring billing subscription. * * @since 1.8.2 * * @return array */ public static function get_allowed_subscription_intervals() { return [ 'daily' => esc_html__( 'day', 'wpforms-lite' ), 'weekly' => esc_html__( 'week', 'wpforms-lite' ), 'monthly' => esc_html__( 'month', 'wpforms-lite' ), 'quarterly' => esc_html__( 'quarter', 'wpforms-lite' ), 'semiyearly' => esc_html__( 'semi-year', 'wpforms-lite' ), 'yearly' => esc_html__( 'year', 'wpforms-lite' ), ]; } /** * Map singular to plural column names. * * @since 1.8.2 * * @param string $column Column name. * * @return string */ private static function get_plural_column_name( $column ) { $map = [ 'mode' => 'modes', 'gateway' => 'gateways', 'status' => 'statuses', 'type' => 'types', 'subscription_type' => 'subscription_types', 'subscription_status' => 'subscription_statuses', ]; return isset( $map[ $column ] ) ? $map[ $column ] : $column; } } Db/Payments/Meta.php 0000644 00000026770 15174710275 0010315 0 ustar 00 <?php namespace WPForms\Db\Payments; use WPForms_DB; /** * Class for the Payment Meta database table. * * @since 1.8.2 */ class Meta extends WPForms_DB { /** * Primary class constructor. * * @since 1.8.2 */ public function __construct() { parent::__construct(); $this->table_name = self::get_table_name(); $this->primary_key = 'id'; $this->type = 'payment_meta'; } /** * Get the table name. * * @since 1.8.2 * * @return string */ public static function get_table_name() { global $wpdb; return $wpdb->prefix . 'wpforms_payment_meta'; } /** * Get table columns. * * @since 1.8.2 * * @return array */ public function get_columns() { return [ 'id' => '%d', 'payment_id' => '%d', 'meta_key' => '%s', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key 'meta_value' => '%s', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value ]; } /** * Default column values. * * @since 1.8.2 * * @return array */ public function get_column_defaults() { return [ 'payment_id' => 0, 'meta_key' => '', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key 'meta_value' => '', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value ]; } /** * Create the table. * * @since 1.8.2 */ public function create_table() { global $wpdb; $charset_collate = $wpdb->get_charset_collate(); $max_index_length = self::MAX_INDEX_LENGTH; /** * Note: there must be two spaces between the words PRIMARY KEY and the definition of primary key. * * @link https://codex.wordpress.org/Creating_Tables_with_Plugins#Creating_or_Updating_the_Table */ $query = "CREATE TABLE $this->table_name ( id bigint(20) NOT NULL AUTO_INCREMENT, payment_id bigint(20) NOT NULL, meta_key varchar(255), meta_value longtext, PRIMARY KEY (id), KEY payment_id (payment_id), KEY meta_key (meta_key($max_index_length)), KEY meta_value (meta_value($max_index_length)) ) $charset_collate;"; require_once ABSPATH . 'wp-admin/includes/upgrade.php'; dbDelta( $query ); } /** * Insert payment meta's. * * @since 1.8.2 * * @param int $payment_id Payment ID. * @param array $meta Payment meta to be inserted. */ public function bulk_add( $payment_id, $meta ) { global $wpdb; $values = []; foreach ( $meta as $meta_key => $meta_value ) { // Empty strings are skipped. if ( $meta_value === '' ) { continue; } $values[] = $wpdb->prepare( '( %d, %s, %s )', $payment_id, $meta_key, maybe_serialize( $meta_value ) ); } if ( ! $values ) { return; } $values = implode( ', ', $values ); // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.NoCaching $wpdb->query( "INSERT INTO $this->table_name ( payment_id, meta_key, meta_value ) VALUES $values" ); // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.NoCaching } /** * Update or add payment meta. * * If the meta key already exists for given payment id, update the meta value. Otherwise, add the meta key and value. * * @since 1.8.4 * * @param int $payment_id Payment ID. * @param string $meta_key Payment meta key. * @param mixed $meta_value Payment meta value. * * @return bool */ public function update_or_add( $payment_id, $meta_key, $meta_value ) { // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.SlowDBQuery.slow_db_query_meta_key, WordPress.DB.SlowDBQuery.slow_db_query_meta_value $row = $this->get_last_by( $meta_key, $payment_id ); if ( $row ) { return $this->update( $row->id, [ 'meta_value' => maybe_serialize( $meta_value ) ], '', $this->type ); } return (bool) $this->add( [ 'payment_id' => $payment_id, 'meta_key' => $meta_key, 'meta_value' => maybe_serialize( $meta_value ), ], $this->type ); // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.SlowDBQuery.slow_db_query_meta_key, WordPress.DB.SlowDBQuery.slow_db_query_meta_value } /** * Add payment log. * * @since 1.8.4 * * @param int $payment_id Payment ID. * @param string $content Log content. * * @return bool */ public function add_log( $payment_id, $content ) { return (bool) $this->add( [ 'payment_id' => $payment_id, 'meta_key' => 'log', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key 'meta_value' => wp_json_encode( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value [ 'value' => wp_kses_post( $content ), 'date' => gmdate( 'Y-m-d H:i:s' ), ] ), ], $this->type ); } /** * Get single payment meta. * * @since 1.8.2 * * @param int $payment_id Payment ID. * @param string|null $meta_key Payment meta to be retrieved. * * @return mixed Meta value. */ public function get_single( $payment_id, $meta_key ) { global $wpdb; // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.NoCaching $meta_value = $wpdb->get_var( $wpdb->prepare( "SELECT meta_value FROM $this->table_name WHERE payment_id = %d AND meta_key = %s ORDER BY id DESC LIMIT 1", $payment_id, $meta_key ) ); // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.NoCaching return maybe_unserialize( $meta_value ); } /** * Get all payment meta. * * @since 1.8.2 * * @param int $payment_id Payment ID. * * @return array|null */ public function get_all( $payment_id ) { global $wpdb; // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.NoCaching return $wpdb->get_results( $wpdb->prepare( "SELECT meta_key, meta_value as value FROM $this->table_name WHERE payment_id = %d ORDER BY id DESC", $payment_id ), OBJECT_K ); // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.NoCaching } /** * Retrieve all rows based on meta_key value. * * @since 1.8.2 * * @param string $meta_key Meta key value. * @param int $payment_id Payment ID. * * @return object|null */ public function get_all_by( $meta_key, $payment_id ) { global $wpdb; if ( empty( $meta_key ) ) { return null; } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching return $wpdb->get_results( $wpdb->prepare( "SELECT meta_value as value FROM $this->table_name WHERE payment_id = %d AND meta_key = %s ORDER BY id DESC", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared $payment_id, $meta_key ), ARRAY_A ); } /** * Check if there are valid entries with a specific meta key. * * @since 1.8.4 * * @param string $meta_key The meta key to check. * * @return bool */ public function is_valid_meta_by_meta_key( $meta_key ) { // Check if the meta key is empty and return false. if ( empty( $meta_key ) ) { return false; } // Retrieve the global database instance. global $wpdb; $payment_handler = wpforms()->obj( 'payment' ); $payment_table_name = $payment_handler->table_name; $secondary_where_clause = $payment_handler->add_secondary_where_conditions(); // Prepare and execute the SQL query to check if there are valid entries with the given meta key. // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.NoCaching return (bool) $wpdb->get_var( $wpdb->prepare( "SELECT 1 FROM {$this->table_name} AS pm WHERE meta_key = %s AND meta_value IS NOT NULL AND EXISTS (SELECT 1 FROM {$payment_table_name} AS p WHERE p.id = pm.payment_id {$secondary_where_clause}) LIMIT 1", $meta_key ) ); // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.NoCaching } /** * Check if the given meta key and value exist in the payment meta table. * * @since 1.8.4 * * @param string $meta_key Meta key value. * @param string $meta_value Meta value. * * @return bool */ public function is_valid_meta( $meta_key, $meta_value ) { // Check if the meta key or value is empty and return false. if ( empty( $meta_key ) || empty( $meta_value ) ) { return false; } // Retrieve the global database instance. global $wpdb; // Prepare and execute the SQL query to check if the given meta key and value exist in the payment meta table. // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared return (bool) $wpdb->get_var( $wpdb->prepare( "SELECT EXISTS( SELECT 1 FROM {$this->table_name} WHERE meta_key = %s AND meta_value = %s )", $meta_key, $meta_value ) ); // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared } /** * Retrieve payment meta data by given meta key and value. * * @since 1.8.4 * * @param string $meta_key Meta key value. * @param string $meta_value Meta value. * * @return array */ public function get_all_by_meta( $meta_key, $meta_value ) { // Check if the meta key or value is empty and return null. if ( empty( $meta_key ) || empty( $meta_value ) ) { return []; } // Retrieve the global database instance. global $wpdb; // Prepare and execute the SQL query to retrieve payment meta data based on the given meta key and value. // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared return $wpdb->get_results( $wpdb->prepare( "SELECT meta_key, meta_value AS value FROM {$this->table_name} WHERE payment_id = ( SELECT payment_id FROM {$this->table_name} WHERE meta_key = %s AND meta_value = %s LIMIT 1 )", $meta_key, $meta_value ), OBJECT_K ); // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared } /** * Get row from the payment meta table for given payment id and meta key. * * @since 1.8.4 * * @param string $meta_key Meta key value. * @param int $payment_id Payment ID. * * @return object|null */ public function get_last_by( $meta_key, $payment_id ) { global $wpdb; // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.SlowDBQuery.slow_db_query_meta_key return $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $this->table_name WHERE payment_id = %d AND meta_key = %s ORDER BY id DESC LIMIT 1", $payment_id, $meta_key ) ); // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.SlowDBQuery.slow_db_query_meta_key } } Db/Payments/UpdateHelpers.php 0000644 00000002637 15174710275 0012170 0 ustar 00 <?php namespace WPForms\Db\Payments; /** * Payment values update helpers class. * * @since 1.8.4 */ class UpdateHelpers { /** * Refund payment in database. * * @since 1.8.4 * * @param Payment $payment_db Payment DB object. * @param int $refunded_amount Refunded amount with cent separated. * @param string $log Log message. * * @return bool */ public static function refund_payment( $payment_db, $refunded_amount, $log = '' ) { $status = $refunded_amount < $payment_db->total_amount ? 'partrefund' : 'refunded'; if ( ! wpforms()->obj( 'payment' )->update( $payment_db->id, [ 'status' => $status ] ) ) { return false; } if ( ! wpforms()->obj( 'payment_meta' )->update_or_add( $payment_db->id, 'refunded_amount', $refunded_amount ) ) { return false; } if ( $log ) { wpforms()->obj( 'payment_meta' )->add_log( $payment_db->id, $log ); } return true; } /** * Cancel subscription in database. * * @since 1.8.4 * * @param int $payment_id Payment ID. * @param string $log Log message. * * @return bool */ public static function cancel_subscription( $payment_id, $log = '' ) { if ( ! wpforms()->obj( 'payment' )->update( $payment_id, [ 'subscription_status' => 'cancelled' ] ) ) { return false; } if ( $log ) { wpforms()->obj( 'payment_meta' )->add_log( $payment_id, $log ); } return true; } } Db/Payments/Queries.php 0000644 00000027137 15174710275 0011042 0 ustar 00 <?php namespace WPForms\Db\Payments; /** * Class for the Payments database queries. * * @since 1.8.2 */ class Queries extends Payment { /** * Check if given payment table column has different values. * * @since 1.8.2 * * @param string $column Column name. * * @return bool */ public function has_different_values( $column ) { global $wpdb; $subquery[] = "SELECT $column FROM $this->table_name WHERE 1=1"; $subquery[] = $this->add_secondary_where_conditions(); $subquery[] = 'LIMIT 1'; $subquery = implode( ' ', $subquery ); $query[] = "SELECT $column FROM $this->table_name WHERE 1=1"; $query[] = $this->add_secondary_where_conditions(); $query[] = "AND $column != ( $subquery )"; $query[] = 'LIMIT 1'; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared $result = $wpdb->get_var( implode( ' ', $query ) ); return ! empty( $result ); } /** * Check if there is a subscription payment. * * @since 1.8.2 * * @return bool */ public function has_subscription() { return $this->if_exists( [ 'type' => implode( '|', array_keys( ValueValidator::get_allowed_subscription_types() ) ), ] ); } /** * Retrieve the number of all payments. * * @since 1.8.2 * * @param array $args Redefine query parameters by providing own arguments. * * @return int Number of payments or count of payments. */ public function count_all( $args = [] ) { // Retrieve the global database instance. global $wpdb; $query[] = 'SELECT SUM(count) AS total_count FROM ('; $query[] = "SELECT COUNT(*) AS count FROM {$this->table_name} as p"; /** * Add parts to the query for count_all method before the WHERE clause. * * @since 1.8.2 * * @param string $where Before the WHERE clause in DB query. * @param array $args Query arguments. * * @return string */ $query[] = apply_filters( 'wpforms_db_payments_queries_count_all_query_before_where', '', $args ); $query[] = 'WHERE 1=1'; $query[] = $this->add_columns_where_conditions( $args ); $query[] = $this->add_secondary_where_conditions( $args ); /** * Append custom query parts after the WHERE clause for the count_all method. * * This hook allows external code to extend the SQL query by adding custom conditions * immediately after the WHERE clause. * * @since 1.8.4 * * @param string $where After the WHERE clause in the database query. * @param array $args Query arguments. * * @return string */ $query[] = apply_filters( 'wpforms_db_payments_queries_count_all_query_after_where', '', $args ); // Close the subquery. $query[] = ') AS counts;'; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared return (int) $wpdb->get_var( implode( ' ', $query ) ); } /** * Whether at least one payment exists with the given arguments. * * @since 1.8.4 * * @param array $args Optionally, you can redefine query parameters by providing custom arguments. * * @return bool False if no results found. */ public function if_exists( $args = [] ) { // Retrieve the global database instance. global $wpdb; $query[] = "SELECT 1 FROM {$this->table_name}"; /** * Add parts to the query for if_exists method before the WHERE clause. * * @since 1.8.4 * * @param string $where Before the WHERE clause in DB query. * @param array $args Query arguments. * * @return string */ $query[] = apply_filters( 'wpforms_db_payments_queries_count_if_exists_before_where', '', $args ); $query[] = 'WHERE 1=1'; $query[] = $this->add_columns_where_conditions( $args ); $query[] = $this->add_secondary_where_conditions( $args ); /** * Append custom query parts after the WHERE clause for the if_exists method. * * This hook allows external code to extend the SQL query by adding custom conditions * immediately after the WHERE clause. * * @since 1.8.4 * * @param string $where After the WHERE clause in the database query. * @param array $args Query arguments. * * @return string */ $query[] = apply_filters( 'wpforms_db_payments_queries_count_if_exists_after_where', '', $args ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared return (bool) $wpdb->get_var( implode( ' ', $query ) ); } /** * Get next payment. * * @since 1.8.2 * * @param int $payment_id Payment ID. * @param array $args Where conditions. * * @return object|null Object from DB values or null. */ public function get_next( $payment_id, $args = [] ) { global $wpdb; if ( empty( $payment_id ) ) { return null; } $query[] = "SELECT * FROM {$this->table_name}"; // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared $query[] = $wpdb->prepare( "WHERE $this->primary_key > %d", $payment_id ); $query[] = $this->add_secondary_where_conditions( $args ); $query[] = "ORDER BY $this->primary_key LIMIT 1"; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.NoCaching return $wpdb->get_row( implode( ' ', $query ) ); } /** * Get previous payment. * * @since 1.8.2 * * @param int $payment_id Payment ID. * @param array $args Where conditions. * * @return object|null Object from DB values or null. */ public function get_prev( $payment_id, $args = [] ) { global $wpdb; if ( empty( $payment_id ) ) { return null; } $query[] = "SELECT * FROM $this->table_name"; // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared $query[] = $wpdb->prepare( "WHERE $this->primary_key < %d", $payment_id ); $query[] = $this->add_secondary_where_conditions( $args ); $query[] = "ORDER BY $this->primary_key DESC LIMIT 1"; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.NoCaching return $wpdb->get_row( implode( ' ', $query ) ); } /** * Get previous payments count. * * @since 1.8.2 * * @param int $payment_id Payment ID. * @param array $args Where conditions. * * @return int */ public function get_prev_count( $payment_id, $args = [] ) { global $wpdb; if ( empty( $payment_id ) ) { return 0; } $query[] = "SELECT COUNT( $this->primary_key ) FROM $this->table_name"; // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared $query[] = $wpdb->prepare( "WHERE $this->primary_key < %d", $payment_id ); $query[] = $this->add_secondary_where_conditions( $args ); $query[] = "ORDER BY $this->primary_key ASC"; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.NoCaching return (int) $wpdb->get_var( implode( ' ', $query ) ); } /** * Get subscription payment history for the given subscription ID. * This function returns an array of subscription payment object and renewal payments associated with the subscription. * * @global wpdb $wpdb Instantiation of the wpdb class. * * @since 1.8.4 * * @param string $subscription_id Subscription ID. * @param string $currency Currency that the payment was made in. * * @return array Array of payment objects. */ public function get_subscription_payment_history( $subscription_id, $currency = '' ) { $subscription = null; $renewals = []; // Bail early if the subscription ID is empty. if ( empty( $subscription_id ) ) { return [ $subscription, $renewals ]; } // Get the currency, if not provided. if ( empty( $currency ) ) { $currency = wpforms_get_currency(); } // Get the database instance. global $wpdb; // Get the general where clause. $where_clause = $this->add_secondary_where_conditions( [ 'currency' => $currency ] ); // Construct the query using a prepared statement. // Execute the query and fetch the results. // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $results = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$this->table_name} WHERE subscription_id = %s AND (type = 'subscription' OR type = 'renewal') {$where_clause} ORDER BY type ASC, date_created_gmt DESC", $subscription_id ) ); // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared // Search for the subscription object in the "$results" array. foreach ( $results as $key => $result ) { if ( $result->type === 'subscription' ) { $subscription = $result; unset( $results[ $key ] ); break; // Exit the loop after finding the subscription object. } } // Assign the remaining results to renewals. $renewals = $results; return [ $subscription, $renewals ]; } /** * Determine if given subscription has a renewal payment. * * @global wpdb $wpdb Instantiation of the wpdb class. * * @since 1.8.4 * * @param string $subscription_id Subscription ID. * * @return bool True if the subscription has a renewal payment, false otherwise. */ public function if_subscription_has_renewal( $subscription_id ) { // Bail early if the subscription ID is empty. if ( empty( $subscription_id ) ) { return false; } // Get the database instance. global $wpdb; $query[] = "SELECT 1 FROM {$this->table_name} AS s"; $query[] = 'WHERE s.subscription_id = %s'; $query[] = "AND s.type = 'subscription'"; $query[] = 'AND EXISTS('; $query[] = "SELECT 1 FROM {$this->table_name} AS r"; $query[] = 'WHERE s.subscription_id = r.subscription_id'; $query[] = "AND r.type = 'renewal'"; $query[] = ')'; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare return (bool) $wpdb->get_var( $wpdb->prepare( implode( ' ', $query ), $subscription_id ) ); } /** * Get subscription payment for given subscription ID. * * @since 1.8.4 * * @param string $subscription_id Subscription ID. * * @return object|null */ public function get_subscription( $subscription_id ) { global $wpdb; $query[] = "SELECT * FROM {$this->table_name}"; $query[] = "WHERE subscription_id = %s AND type = 'subscription'"; $query[] = 'ORDER BY id DESC LIMIT 1'; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare return $wpdb->get_row( $wpdb->prepare( implode( ' ', $query ), $subscription_id ) ); } /** * Get renewal payment for given invoice ID. * * @since 1.8.4 * * @param string $invoice_id Invoice ID. * * @return object|null */ public function get_renewal_by_invoice_id( $invoice_id ) { global $wpdb; $meta_table_name = wpforms()->obj( 'payment_meta' )->table_name; $query[] = "SELECT p.* FROM {$this->table_name} as p"; $query[] = "INNER JOIN {$meta_table_name} as pm ON p.id = pm.payment_id"; $query[] = "WHERE pm.meta_key = 'invoice_id' AND pm.meta_value = %s"; $query[] = 'ORDER BY p.id DESC LIMIT 1'; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare return $wpdb->get_row( $wpdb->prepare( implode( ' ', $query ), $invoice_id ) ); } } Integrations/Divi/Divi.php 0000644 00000024651 15174710275 0011532 0 ustar 00 <?php namespace WPForms\Integrations\Divi; use WPForms\Integrations\Divi\Interfaces\LocalizedDataInterface; use WPForms_Field_Select; use WPForms\Integrations\IntegrationInterface; /** * Class Divi. * * @since 1.6.3 */ class Divi implements IntegrationInterface { /** * Instance of the legacy module. * * @since 1.9.9 * * @var WPFormsSelector|null */ private $legacy_module; /** * Instance of the modern module. * * @since 1.9.9 * * @var WPFormsSelectorModern|null * @noinspection PhpPrivateFieldCanBeLocalVariableInspection */ private $modern_module; /** * Indicate if the current integration is allowed to load. * * @since 1.6.3 * * @return bool */ public function allow_load(): bool { return wpforms_is_divi_active(); } /** * Load an integration. * * @since 1.6.3 */ public function load() { $this->hooks(); } /** * Hooks. * * @since 1.6.3 */ public function hooks(): void { add_action( 'et_builder_ready', [ $this, 'register_module' ] ); add_action( 'wp_enqueue_scripts', [ $this, 'frontend_styles' ], 12 ); // Register module. add_action( 'divi_module_library_modules_dependency_tree', [ $this, 'register_modern_selector' ] ); if ( wp_doing_ajax() ) { add_action( 'wp_ajax_wpforms_divi_preview', [ $this, 'preview' ] ); } if ( $this->is_divi_builder() ) { add_action( 'wp_enqueue_scripts', [ $this, 'builder_styles' ], 12 ); add_action( 'wp_enqueue_scripts', [ $this, 'builder_scripts' ] ); add_filter( 'wpforms_global_assets', '__return_true' ); add_filter( 'wpforms_frontend_missing_assets_error_js_disable', '__return_true', PHP_INT_MAX ); // Hide CAPTCHA badge in Divi Builder. add_filter( 'wpforms_frontend_recaptcha_disable', '__return_true' ); } } /** * Register modern selector dependency. * * @since 1.9.9 * * @param object $dependency_tree Dependency tree object. */ public function register_modern_selector( object $dependency_tree ): void { if ( ! $this->is_divi_5_or_higher() ) { return; } $this->modern_module = new WPFormsSelectorModern(); if ( $this->is_divi_builder() ) { $this->insert( $this->modern_module ); } $dependency_tree->add_dependency( $this->modern_module ); } /** * Check if Divi 5 or higher is active. * * @since 1.9.9 * * @return bool * @noinspection PhpUndefinedConstantInspection */ protected function is_divi_5_or_higher(): bool { if ( ! defined( 'ET_BUILDER_VERSION' ) ) { return false; } return version_compare( ET_BUILDER_VERSION, '5.0.0', '>=' ); } /** * Determine if a current page is opened in the Divi Builder. * * @since 1.6.3 * * @return bool */ private function is_divi_builder(): bool { return ! empty( $_GET['et_fb'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended } /** * Get the current style name. * * Overwrite styles for the Divi Builder. * * @since 1.6.3 * * @return string */ public function get_current_styles_name(): string { $disable_css = absint( wpforms_setting( 'disable-css', 1 ) ); if ( $disable_css === 3 ) { return ''; } $styles_name = wpforms_get_render_engine() . '-'; $styles_name .= $disable_css === 1 ? 'full' : 'base'; return $styles_name; } /** * Determine if the Divi Builder plugin is loaded. * * @since 1.6.3 * * @return bool */ protected function is_divi_plugin_loaded(): bool { return self::is_divi_loaded(); } /** * Helper method to check if the Divi plugin is loaded. * * @since 1.8.5 * * @return bool */ public static function is_divi_loaded(): bool { if ( ! is_singular() ) { return false; } return defined( 'ET_BUILDER_PLUGIN_ACTIVE' ) || defined( 'ET_BUILDER_THEME' ); } /** * WPForms frontend styles special for Divi. * * @since 1.8.1 */ protected function divi_frontend_styles() { $min = wpforms_get_min_suffix(); $styles_name = $this->get_current_styles_name(); wp_enqueue_style( 'wpforms-choicesjs', WPFORMS_PLUGIN_URL . "assets/css/integrations/divi/choices{$min}.css", [], WPForms_Field_Select::CHOICES_VERSION ); if ( empty( $styles_name ) ) { return; } // Load CSS per global setting. wp_register_style( "wpforms-divi-{$styles_name}", WPFORMS_PLUGIN_URL . "assets/css/integrations/divi/wpforms-{$styles_name}{$min}.css", [], WPFORMS_VERSION ); } /** * Register frontend styles. * Required for the plugin version of builder only. * * @since 1.6.3 */ public function frontend_styles() { if ( ! $this->is_divi_plugin_loaded() ) { return; } if ( $this->allow_frontend_styles() ) { $this->divi_frontend_styles(); } } /** * Load styles. * * @since 1.6.3 */ public function builder_styles() { $min = wpforms_get_min_suffix(); wp_enqueue_style( 'wpforms-integrations', WPFORMS_PLUGIN_URL . "assets/css/admin-integrations{$min}.css", null, WPFORMS_VERSION ); $this->divi_frontend_styles(); } /** * Load scripts. * * @since 1.6.3 */ public function builder_scripts(): void { if ( ! $this->legacy_module ) { return; } $min = wpforms_get_min_suffix(); wp_enqueue_script( 'wpforms-divi', WPFORMS_PLUGIN_URL . "assets/js/integrations/divi/formselector.es5{$min}.js", [ 'react', 'react-dom' ], WPFORMS_VERSION, true ); $this->insert( $this->legacy_module ); wp_localize_script( 'wpforms-divi', 'wpforms_divi_builder', $this->legacy_module->get_localized_data() ); } /** * Injects localized data into the provided form selector to be used in the frontend. * * @since 1.9.9 * * @param LocalizedDataInterface $form_selector Interface instance to set localized data for forms. */ private function insert( LocalizedDataInterface $form_selector ): void { $form_selector->set_localized_data( [ 'ajax_url' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'wpforms_divi_builder' ), 'placeholder' => WPFORMS_PLUGIN_URL . 'assets/images/wpforms-logo.svg', 'block_empty_url' => WPFORMS_PLUGIN_URL . 'assets/images/empty-states/no-forms.svg', 'block_empty_text' => wp_kses( __( 'You can use <b>WPForms</b> to build contact forms, surveys, payment forms, and more with just a few clicks.', 'wpforms-lite' ), [ 'b' => [], ] ), 'get_started_url' => esc_url( admin_url( 'admin.php?page=wpforms-builder' ) ), 'get_started_text' => esc_html__( 'Get Started', 'wpforms-lite' ), 'guide_url' => esc_url( wpforms_utm_link( 'https://wpforms.com/docs/creating-first-form/', 'Divi', 'Create Your First Form Documentation' ) ), 'guide_text' => esc_html__( 'comprehensive guide', 'wpforms-lite' ), 'help_text' => esc_html__( 'Need some help? Check out our', 'wpforms-lite' ), ] ); } /** * Register module. * * @since 1.6.3 */ public function register_module(): void { if ( ! class_exists( 'ET_Builder_Module' ) || ( $this->is_divi_5_or_higher() && $this->is_divi_builder() ) ) { return; } $this->legacy_module = new WPFormsSelector(); } /** * Ajax handler for the form preview. * * @since 1.6.3 */ public function preview(): void { // phpcs:ignore WPForms.PHP.HooksMethod.InvalidPlaceForAddingHooks check_ajax_referer( 'wpforms_divi_builder', 'nonce' ); $form_id = absint( filter_input( INPUT_POST, 'form_id', FILTER_SANITIZE_NUMBER_INT ) ); if ( $form_id ) { $form_obj = wpforms()->obj( 'form' ); $form = $form_obj ? $form_obj->get( $form_id ) : null; $author = $form ? (int) $form->post_author : 0; $cap = $author === get_current_user_id() ? 'wpforms_view_own_forms' : 'wpforms_view_others_forms'; $has_permission = wpforms_current_user_can( $cap, $form_id ); } else { $has_permission = wpforms_current_user_can( [ 'wpforms_view_own_forms', 'wpforms_view_others_forms' ] ); } if ( ! $has_permission ) { wp_send_json_error( esc_html__( 'You do not have permission to preview form.', 'wpforms-lite' ) ); } // Disable Anti Spam v3 honeypot. add_filter( 'wpforms_forms_anti_spam_v3_is_honeypot_enabled', '__return_false' ); add_filter( 'wpforms_frontend_container_class', static function ( $classes ) { $classes[] = 'wpforms-gutenberg-form-selector'; $classes[] = 'wpforms-container-full'; return $classes; } ); add_action( 'wpforms_frontend_output', static function () { echo '<fieldset disabled>'; }, 3 ); add_action( 'wpforms_frontend_output', static function () { echo '</fieldset>'; // This empty image is needed to execute JS code that triggers the custom event. // Unfortunately, the < script > tag doesn't work in the Divi Builder. echo '<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==" alt="Empty" height="0" width="0" onLoad="jQuery( document ).trigger( \'wpformsDiviModuleDisplay\' );" />'; }, 30 ); /** * Allows showing/hiding form title and description. * * @since 1.6.3.1 * * @param bool $show_title Show form title. * @param int $form_id Form ID. */ $show_title = (bool) apply_filters( // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName 'wpforms_divi_builder_form_title', 'on' === filter_input( INPUT_POST, 'show_title', FILTER_SANITIZE_FULL_SPECIAL_CHARS ), $form_id ); /** * Allows showing/hiding form description. * * @since 1.6.3.1 * * @param bool $show_desc Show form description. * @param int $form_id Form ID. */ $show_desc = (bool) apply_filters( // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName 'wpforms_divi_builder_form_desc', 'on' === filter_input( INPUT_POST, 'show_desc', FILTER_SANITIZE_FULL_SPECIAL_CHARS ), $form_id ); wp_send_json_success( do_shortcode( sprintf( '[wpforms id="%1$d" title="%2$s" description="%3$s"]', $form_id, $show_title, $show_desc ) ) ); } /** * Allow frontend styles. * * @since 1.9.8.6 * * @return bool */ protected function allow_frontend_styles(): bool { $frontend_obj = wpforms()->obj( 'frontend' ); if ( ! $frontend_obj ) { return false; } global $post; $content = $post->post_content ?? ''; return ( $frontend_obj->assets_global() || has_shortcode( $content, 'wpforms' ) || has_shortcode( $content, 'wpforms_selector' ) || ( function_exists( 'has_block' ) && has_block( 'wpforms/form-selector' ) ) ); } } Integrations/Divi/WPFormsSelector.php 0000644 00000011324 15174710275 0013666 0 ustar 00 <?php // phpcs:ignore Generic.Commenting.DocComment.MissingShort /** @noinspection PhpUndefinedClassInspection */ namespace WPForms\Integrations\Divi; use ET_Builder_Module; use WP_Post; use WPForms\Integrations\Divi\Interfaces\FormsResolverInterface; use WPForms\Integrations\Divi\Interfaces\LocalizedDataInterface; use WPForms\Integrations\Divi\Traits\FormsResolverTrait; use WPForms\Integrations\Divi\Traits\LocalizedDataTrait; /** * Class WPFormsSelector. * * @since 1.6.3 */ class WPFormsSelector extends ET_Builder_Module implements LocalizedDataInterface, FormsResolverInterface { use LocalizedDataTrait; use FormsResolverTrait; /** * Module slug. * * @since 1.6.3 * * @var string */ public $slug = 'wpforms_selector'; /** * VB support. * * @since 1.6.3 * * @var string */ public $vb_support = 'on'; /** * Module name. * * @since 1.6.3 * * @var string */ public $name; /** * Init module. * * @since 1.6.3 */ public function init(): void { $this->name = esc_html__( 'WPForms', 'wpforms-lite' ); } /** * Adds a form to the option array, using the form's ID as the key and a decoded title as the value. * * @since 1.9.9 * * @param array $options The option array to be updated. * @param WP_Post $form The form object containing the ID and title to be added. * * @return array Updated options array with the form added. */ public function add_form_in_options( array $options, WP_Post $form ): array { $options[ $form->ID ] = htmlspecialchars_decode( $form->post_title, ENT_QUOTES ); return $options; } /** * Get a list of settings. * * @since 1.6.3 * * @return array */ public function get_fields(): array { $forms = $this->get_form_options(); $default_value = ''; if ( ! empty( $forms ) ) { $forms[0] = esc_html__( 'Select form', 'wpforms-lite' ); $default_value = 0; } return [ 'form_id' => [ 'label' => esc_html__( 'Form', 'wpforms-lite' ), 'type' => 'select', 'option_category' => 'basic_option', 'toggle_slug' => 'main_content', 'options' => $forms, 'default' => $default_value, ], 'show_title' => [ 'label' => esc_html__( 'Show Title', 'wpforms-lite' ), 'type' => 'yes_no_button', 'option_category' => 'basic_option', 'toggle_slug' => 'main_content', 'options' => [ 'off' => esc_html__( 'Off', 'wpforms-lite' ), 'on' => esc_html__( 'On', 'wpforms-lite' ), ], ], 'show_desc' => [ 'label' => esc_html__( 'Show Description', 'wpforms-lite' ), 'option_category' => 'basic_option', 'type' => 'yes_no_button', 'toggle_slug' => 'main_content', 'options' => [ 'off' => esc_html__( 'Off', 'wpforms-lite' ), 'on' => esc_html__( 'On', 'wpforms-lite' ), ], ], ]; } /** * Disable advanced fields configuration. * * @since 1.6.3 * * @return array */ public function get_advanced_fields_config(): array { return [ 'link_options' => false, 'text' => false, 'background' => false, 'borders' => false, 'box_shadow' => false, 'button' => false, 'filters' => false, 'fonts' => false, ]; } /** * Render module on the frontend. * * @since 1.6.3 * * @param array $attrs List of unprocessed attributes. * @param string $content Content being processed. * @param string $render_slug Slug of module that is used for rendering output. * * @return string * @noinspection PhpMissingParamTypeInspection * @noinspection PhpUnusedParameterInspection */ public function render( $attrs, $content = null, $render_slug = '' ): string { if ( empty( $this->props['form_id'] ) ) { return ''; } $form_id = absint( $this->props['form_id'] ); $show_title = ( $this->props['show_title'] ?? '' ) === 'on'; $show_desc = ( $this->props['show_desc'] ?? '' ) === 'on'; return do_shortcode( sprintf( '[wpforms id="%1$s" title="%2$s" description="%3$s"]', $form_id, /** * Filters form title display flag. * * @since 1.6.3 * * @param bool $show_title Show form title. * @param int $form_id Form ID. */ (bool) apply_filters( 'wpforms_divi_builder_form_title', $show_title, $form_id ), // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName /** * Filters form description display flag. * * @since 1.6.3 * * @param bool $show_desc Show form description. * @param int $form_id Form ID. */ (bool) apply_filters( 'wpforms_divi_builder_form_desc', $show_desc, $form_id ) // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName ) ); } } Integrations/Divi/WPFormsSelectorModern.php 0000644 00000024026 15174710275 0015036 0 ustar 00 <?php // phpcs:disable Generic.Commenting.DocComment.MissingShort /** @noinspection PhpUndefinedNamespaceInspection */ /** @noinspection PhpUndefinedClassInspection */ // phpcs:enable Generic.Commenting.DocComment.MissingShort namespace WPForms\Integrations\Divi; use ET\Builder\Framework\DependencyManagement\Interfaces\DependencyInterface; use ET\Builder\Framework\Utility\HTMLUtility; use ET\Builder\FrontEnd\Module\Style; use ET\Builder\Packages\Module\Module; use ET\Builder\Packages\Module\Options\Css\CssStyle; use ET\Builder\Packages\ModuleLibrary\ModuleRegistration; use ET\Builder\VisualBuilder\Assets\PackageBuildManager; use WP_Post; use WPForms\Integrations\Divi\Interfaces\FormsResolverInterface; use WPForms\Integrations\Divi\Interfaces\LocalizedDataInterface; use WPForms\Integrations\Divi\Traits\FormsResolverTrait; use WPForms\Integrations\Divi\Traits\LocalizedDataTrait; use WP_Block_Type_Registry; /** * WPForms Divi 5 Module. * * @since 1.9.9 */ class WPFormsSelectorModern implements DependencyInterface, LocalizedDataInterface, FormsResolverInterface { use LocalizedDataTrait; use FormsResolverTrait; /** * Defines the module type identifier for the WPForms Divi form selector. * * @since 1.9.9 */ private const MODULE_TYPE = 'wpforms/divi-form-selector'; /** * This function registers and initiates all the logic the class implements. * * @since 1.9.9 */ public function load(): void { $this->hooks(); } /** * Registers a hook to enqueue visual builder assets before the Divi visual builder loads scripts. * * @since 1.9.9 */ private function hooks(): void { add_action( 'init', [ $this, 'register_module' ] ); add_action( 'divi_visual_builder_assets_before_enqueue_scripts', [ $this, 'enqueue_visual_builder_assets' ] ); } /** * Register module. * * @since 1.9.9 */ public static function register_module(): void { // Path to module metadata that is shared between Frontend and Visual Builder. $module_json_folder_path = __DIR__; ModuleRegistration::register_module( $module_json_folder_path, [ 'render_callback' => [ __CLASS__, 'render_callback' ], ] ); } /** * Adds a form into the provided options array with its ID as the key and label derived from the form's title. * * @since 1.9.9 * * @param array $options The option array to be updated. * @param WP_Post $form The form post object containing the ID and title. * * @return array The updated options array including the new form entry. */ public function add_form_in_options( array $options, WP_Post $form ): array { $options[ $form->ID ] = [ 'label' => htmlspecialchars_decode( $form->post_title, ENT_QUOTES ), ]; return $options; } /** * Render callback for the module. * * @since 1.9.9 * * @param array $attrs Module attributes. * @param string $content Module content. * @param object $block Block object. * @param object $elements Elements helper object. * * @return string Rendered module HTML. * @noinspection PhpUnusedParameterInspection */ public static function render_callback( array $attrs, string $content, object $block, object $elements ): string { $new_attrs = $block->parsed_block['attrs']; // Get attribute values through $attrs (proper way). $form_id = absint( $new_attrs['formId']['desktop']['value'] ?? 0 ); $show_title = isset( $new_attrs['showTitle']['desktop']['value'] ) && $new_attrs['showTitle']['desktop']['value'] === 'on'; $show_desc = isset( $new_attrs['showDescription']['desktop']['value'] ) && $new_attrs['showDescription']['desktop']['value'] === 'on'; /** * Filter whether to display the form title for the Divi module. * * @since 1.6.3 * * @param bool $show_title Whether to show the form title. * @param int $form_id Form ID. */ $show_title = (bool) apply_filters( 'wpforms_divi_builder_form_title', $show_title, $form_id ); // phpcs:disable WPForms.PHP.ValidateHooks.InvalidHookName /** * Filter whether to display the form description for the Divi module. * * @since 1.6.3 * * @param bool $show_desc Whether to show the form description. * @param int $form_id Form ID. */ $show_desc = (bool) apply_filters( 'wpforms_divi_builder_form_desc', $show_desc, $form_id ); // phpcs:disable WPForms.PHP.ValidateHooks.InvalidHookName // If no form selected, return the empty string. if ( empty( $form_id ) ) { return ''; } // Generate the form shortcode output. $form_output = do_shortcode( sprintf( '[wpforms id="%1$s" title="%2$s" description="%3$s"]', $form_id, $show_title, $show_desc ) ); // Wrap content in module_inner. $module_inner = HTMLUtility::render( [ 'tag' => 'div', 'attributes' => [ 'class' => 'et_pb_module_inner', ], 'childrenSanitizer' => 'et_core_esc_previously', 'children' => $form_output, ] ); // Get style components. TODO: check if this is needed. $module_elements = $elements->style_components( [ 'attrName' => 'module', ] ); // Combine all components. $module_container_children = $module_elements . $module_inner; // Render the final module through Module::render(). return Module::render( [ 'orderIndex' => $block->parsed_block['orderIndex'], 'storeInstance' => $block->parsed_block['storeInstance'], 'attrs' => $new_attrs, 'elements' => $elements, 'id' => $block->parsed_block['id'], 'moduleClassName' => 'wpforms_divi_form_selector', 'name' => $block->block_type->name, 'classnamesFunction' => [ __CLASS__, 'module_classnames' ], 'moduleCategory' => $block->block_type->category, 'stylesComponent' => [ __CLASS__, 'module_styles' ], 'scriptDataComponent' => [ __CLASS__, 'module_script_data' ], // TODO: check if this is needed. 'children' => $module_container_children, ] ); } /** * Module classnames function. * * @since 1.9.9 * * @param array $args Arguments for generating classnames. * * @return array CSS classes. * @noinspection PhpUnusedParameterInspection */ public static function module_classnames( array $args ): array { return []; } /** * Module styles function. * * @since 1.9.9 * * @param array $args Arguments for generating styles. */ public static function module_styles( array $args ): void { $attrs = $args['attrs'] ?? []; $elements = $args['elements']; $settings = $args['settings'] ?? []; $default_printed_style_attrs = $args['defaultPrintedStyleAttrs'] ?? []; Style::add( [ 'id' => $args['id'], 'name' => $args['name'], 'orderIndex' => $args['orderIndex'], 'storeInstance' => $args['storeInstance'], 'styles' => [ $elements->style( [ 'attrName' => 'module', 'styleProps' => [ 'defaultPrintedStyleAttrs' => $default_printed_style_attrs['module']['decoration'] ?? [], 'disabledOn' => [ 'disabledModuleVisibility' => $settings['disabledModuleVisibility'] ?? null, ], ], ] ), CssStyle::style( [ 'selector' => $args['orderClass'], 'attr' => $attrs['css'] ?? [], 'cssFields' => self::get_custom_css(), ] ), ], ] ); } /** * Retrieves the custom CSS associated with the registered block type. * * @since 1.9.9 * * @return array Custom CSS fields. */ private static function get_custom_css(): array { $instance = WP_Block_Type_Registry::get_instance(); if ( ! $instance ) { return []; } $block_type = $instance->get_registered( self::MODULE_TYPE ); // phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase if ( ! $block_type || ! isset( $block_type->customCssFields ) ) { return []; } return $block_type->customCssFields; // phpcs:enable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase } /** * Module script data function. * * @since 1.9.9 * * @param array $args Arguments for generating script data. * * @return array Script data. * @noinspection PhpUnusedParameterInspection */ public static function module_script_data( array $args ): array { return [ 'data' => [], ]; } /** * Retrieves and merges localized data with additional configuration options. * * @since 1.9.9 * * @return array Merged an array of localized data and additional configurations. */ public function get_localized_data(): array { return array_merge( $this->localized_data, [ 'forms' => $this->get_form_options(), 'settingsGroup' => [ 'label' => esc_html__( 'Form Settings', 'wpforms-lite' ), ], 'formId' => [ 'placeholder' => esc_html__( 'Select form', 'wpforms-lite' ), 'label' => esc_html__( 'Form', 'wpforms-lite' ), ], 'showTitle' => [ 'label' => esc_html__( 'Show Title', 'wpforms-lite' ), ], 'showDesc' => [ 'label' => esc_html__( 'Show Description', 'wpforms-lite' ), ], ] ); } /** * Enqueues the Visual Builder assets for the D5 Tutorial Simple Quick Module * if the Front Builder and D5 Builder are enabled. * * @since 1.9.9 * * @noinspection PhpUndefinedFunctionInspection */ public function enqueue_visual_builder_assets(): void { if ( ! et_core_is_fb_enabled() || ! et_builder_d5_enabled() ) { return; } $min = wpforms_get_min_suffix(); PackageBuildManager::register_package_build( [ 'name' => 'wpforms-divi', 'version' => WPFORMS_VERSION, 'script' => [ 'src' => WPFORMS_PLUGIN_URL . "assets/js/integrations/divi/modern/formselector.es5{$min}.js", 'deps' => [ 'react', 'jquery', 'divi-module-library', 'wp-hooks', 'divi-rest', ], 'enqueue_top_window' => false, 'enqueue_app_window' => true, 'data_app_window' => $this->get_localized_data(), ], ] ); } } Integrations/Divi/conversion-outline.json 0000644 00000001320 15174710275 0014647 0 ustar 00 { "advanced": { "animation": "animation", "disabled_on": "disabledOn", "module": "module", "overflow": "overflow", "position_fields": "position", "scroll": "scroll", "sticky": "sticky", "text_shadow": { "default": "text.textShadow" }, "transform": "transform", "transition": "transition", "z_index": "zIndex", "max_width": "sizing", "height": "sizing", "margin_padding": "spacing" }, "css": { "before": "css.*.before", "main_element": "css.*.mainElement", "after": "css.*.after", "free_form": "css.*.freeForm" }, "module": { "form_id": "formId.*", "show_title": "showTitle.*", "show_desc": "showDescription.*" } } Integrations/Divi/Interfaces/FormsResolverInterface.php 0000644 00000001717 15174710275 0017351 0 ustar 00 <?php namespace WPForms\Integrations\Divi\Interfaces; use WP_Post; /** * Interface FormsResolverInterface. * * Defines methods for resolving and managing WPForms forms in Divi integration. * * @since 1.9.9 */ interface FormsResolverInterface { /** * Get all available forms. * * @since 1.9.9 * * @return array Array of WP_Post objects representing forms. */ public function get_forms(): array; /** * Add a form to the option array. * * @since 1.9.9 * * @param array $options Existing options array. * @param WP_Post $form Form WP_Post object to add. * * @return array Updated options array with the form added. */ public function add_form_in_options( array $options, WP_Post $form ): array; /** * Get form options for all available forms. * * Retrieves all forms and formats them as an option array. * * @since 1.9.9 * * @return array Array of form options. */ public function get_form_options(): array; } Integrations/Divi/Interfaces/LocalizedDataInterface.php 0000644 00000001100 15174710275 0017223 0 ustar 00 <?php namespace WPForms\Integrations\Divi\Interfaces; /** * Interface for managing localized data for Divi modules. * * @since 1.9.9 */ interface LocalizedDataInterface { /** * Get the localized data. * * @since 1.9.9 * * @return array Localized data array. */ public function get_localized_data(): array; /** * Set the localized data. * * @since 1.9.9 * * @param array $script_data Localized data to set. * * @return self Returns the instance for method chaining. */ public function set_localized_data( array $script_data ): object; } Integrations/Divi/module.json 0000644 00000001777 15174710275 0012312 0 ustar 00 { "name": "wpforms/divi-form-selector", "d4Shortcode": "wpforms_selector", "title": "WPForms", "titles": "Forms", "moduleIcon": "wpforms/divi-form-selector", "category": "module", "attributes": { "module": { "type": "object", "selector": "{{selector}}", "default": {}, "defaultPrintedStyle": {}, "settings": { "advanced": { "htmlAttributes": {} }, "decoration": { "animation": {}, "sizing": {}, "spacing": {}, "transform": {}, "conditions": {}, "disabledOn": {}, "position": {}, "scroll": {}, "transition": {} } } }, "formId": { "type": "string", "default": "0" }, "showTitle": { "type": "string", "default": "off" }, "showDescription": { "type": "string", "default": "off" } }, "customCssFields": {}, "settings": { "design": "auto", "advanced": "auto" } } Integrations/Divi/Traits/LocalizedDataTrait.php 0000644 00000001403 15174710275 0015577 0 ustar 00 <?php namespace WPForms\Integrations\Divi\Traits; /** * Trait for managing localized data for Divi modules. * * @since 1.9.9 */ trait LocalizedDataTrait { /** * Localized data storage. * * @since 1.9.9 * * @var array */ protected $localized_data = []; /** * Get the localized data. * * @since 1.9.9 * * @return array Localized data array. */ public function get_localized_data(): array { return $this->localized_data; } /** * Set the localized data. * * @since 1.9.9 * * @param array $script_data Localized data to set. * * @return self Returns the instance for method chaining. */ public function set_localized_data( array $script_data ): object { $this->localized_data = $script_data; return $this; } } Integrations/Divi/Traits/FormsResolverTrait.php 0000644 00000002314 15174710275 0015711 0 ustar 00 <?php namespace WPForms\Integrations\Divi\Traits; /** * Trait FormsResolverTrait. * * Provides implementation for resolving WPForms forms in Divi integration. * * @since 1.9.9 */ trait FormsResolverTrait { /** * Get all available forms. * * Retrieves all forms from the database ordered by descending ID. * * @since 1.9.9 * * @return array Array of WP_Post objects representing forms, or empty array if a form object is unavailable. */ public function get_forms(): array { // Get all forms for the editor. $forms = wpforms()->obj( 'form' ) ? wpforms()->obj( 'form' )->get( '', [ 'order' => 'DESC' ] ) : []; // If $forms is false, return an empty array. return $forms ? (array) $forms : []; } /** * Get form options for all available forms. * * Retrieves all forms and formats them as an option array by iterating * through each form and adding it to the options using add_form_in_options(). * * @since 1.9.9 * * @return array Array of form options. */ public function get_form_options(): array { $forms = $this->get_forms(); $options = []; foreach ( $forms as $form ) { $options = $this->add_form_in_options( $options, $form ); } return $options; } } Integrations/ConstantContact/V3/Api/Api.php 0000644 00000024470 15174710275 0014562 0 ustar 00 <?php namespace WPForms\Integrations\ConstantContact\V3\Api; use RuntimeException; use InvalidArgumentException; use WPForms\Integrations\ConstantContact\V3\Core; use WPForms\Integrations\ConstantContact\V3\ConstantContact; use WPForms\Integrations\ConstantContact\V3\Api\Http\Request; /** * Class Api. * * @since 1.9.3 */ class Api { /** * Account data. * * @since 1.9.3 * * @var array */ private $account; /** * Request object. * * @since 1.9.3 * * @var Request */ private $request; /** * API Constructor. * * @since 1.9.3 * * @param array $account Account data. * * @throws InvalidArgumentException If arguments are invalid. */ public function __construct( array $account ) { if ( empty( $account['access_token'] ) ) { throw new InvalidArgumentException( 'Access token cannot be empty.' ); } if ( empty( $account['refresh_token'] ) ) { throw new InvalidArgumentException( 'Refresh token cannot be empty.' ); } if ( empty( $account['id'] ) ) { throw new InvalidArgumentException( 'Account ID cannot be empty.' ); } $this->account = $account; $this->refresh_access_token(); $this->request = new Request( $this->account['access_token'] ); } /** * Get custom fields in a specific format based on provided arguments. * * @since 1.9.3 * * @param string|null $field The field to extract from each custom field. If null, returns all custom fields. * @param string|null $index_key The key to index the returned array by. If null, returns a numerically indexed array. * * @return array */ public function get_custom_fields( ?string $field = null, ?string $index_key = null ): array { $custom_fields = $this->request->get( 'v3/contact_custom_fields', [ 'limit' => 100 ] ); $custom_fields = $custom_fields->get_body(); if ( empty( $custom_fields['custom_fields'] ) || ! is_array( $custom_fields['custom_fields'] ) ) { return []; } $custom_fields = $custom_fields['custom_fields']; if ( is_null( $field ) ) { return $custom_fields; } // Return plucked fields based on provided arguments. return wp_list_pluck( $custom_fields, $field, $index_key ); } /** * Register a custom field. * * @since 1.9.3 * * @param string $name Name of the custom field. * * @return string */ public function register_custom_field( string $name ): string { $body = [ 'label' => $name, 'type' => 'string', ]; $response = $this->request->post( 'v3/contact_custom_fields', $body ); return $response->get_body()['custom_field_id'] ?? ''; } /** * Get account summary. * * @since 1.9.3 * * @return array * * @throws RuntimeException A request was failed. */ public function get_account_summary(): array { $response = $this->request->get( 'v3/account/summary' ); if ( $response->has_errors() ) { throw new RuntimeException( esc_html( $response::get_error_message() ) ); } return $response->get_body(); } /** * Search contact. * * @since 1.9.3 * * @param array $contact_data Contact data. * * @return array Contact data array. * * @throws RuntimeException A request was failed. */ private function search_contact( array $contact_data ): array { $this->validate_contact_email( $contact_data ); $args = [ 'limit' => 1, 'email' => $contact_data['email_address'], ]; $response = $this->request->get( 'v3/contacts', $args ); $body = $response->get_body(); if ( empty( $body['contacts'][0] ) || ! is_array( $body['contacts'][0] ) ) { throw new RuntimeException( 'Contact not found.' ); } return $body['contacts'][0]; } /** * Create or update contact. * * @since 1.9.3 * * @param array $contact_data Contact data. * * @return array * * @throws RuntimeException A request was failed. */ public function subscribe_contact( array $contact_data ): array { $this->validate_subscribe_contact( $contact_data ); $response = $this->request->post( 'v3/contacts/sign_up_form', $contact_data ); $body = $response->get_body(); if ( $response->has_errors() ) { throw new RuntimeException( esc_html( $response::get_error_message() ) ); } if ( empty( $body['contact_id'] ) || empty( $body['action'] ) ) { throw new RuntimeException( 'Account was not created.' ); } return $body; } /** * Validate fields for subscribing action. * * @since 1.9.3 * * @param array $contact_data Contact data. * * @throws InvalidArgumentException If the email address is empty. */ private function validate_subscribe_contact( array $contact_data ) { $this->validate_contact_email( $contact_data ); foreach ( [ 'first_name', 'last_name', 'job_title', 'company_name', 'phone_number' ] as $key ) { if ( isset( $contact_data[ $key ] ) && ! is_string( $contact_data[ $key ] ) ) { throw new InvalidArgumentException( sprintf( 'The "%s" argument should be a string.', esc_html( $key ) ) ); } } if ( isset( $contact_data['street_address'] ) ) { foreach ( (array) $contact_data['street_address'] as $key => $value ) { if ( ! is_string( $value ) ) { throw new InvalidArgumentException( sprintf( 'The "%s" argument should be a string.', esc_html( $key ) ) ); } } } } /** * Validate contact email. * * @since 1.9.3 * * @param array $contact_data Contact data. * * @throws InvalidArgumentException If the email address is empty. */ private function validate_contact_email( array $contact_data ) { if ( empty( $contact_data['email_address'] ) || ! is_email( $contact_data['email_address'] ) ) { throw new InvalidArgumentException( 'Email address is required.' ); } } /** * Delete contact. * * @since 1.9.3 * * @param array $contact_data Contact data. * * @return array Array with contact ID and action, empty array if no contact was found. * * @throws RuntimeException A request was failed. */ public function delete_contact( array $contact_data ): array { $contact = $this->search_contact( $contact_data ); if ( empty( $contact['contact_id'] ) ) { throw new RuntimeException( 'Contact not found.' ); } $endpoint = 'v3/contacts/' . $contact['contact_id']; $response = $this->request->delete( $endpoint ); if ( $response->has_errors() ) { throw new RuntimeException( esc_html( $response::get_error_message() ) ); } return [ 'contact_id' => $contact['contact_id'], 'action' => 'delete', 'response' => $response->get_body(), ]; } /** * Unsubscribe contact. * * @since 1.9.3 * * @param array $contact_data Contact data. * * @return array * * @throws InvalidArgumentException If some arguments are used wrong. * @throws RuntimeException A request was failed. */ public function unsubscribe_contact( array $contact_data ): array { if ( isset( $contact_data['opt_out_reason'] ) && ! is_string( $contact_data['opt_out_reason'] ) ) { throw new InvalidArgumentException( sprintf( 'The "%s" argument should be a string.', 'opt_out_reason' ) ); } $contact = $this->search_contact( $contact_data ); if ( empty( $contact['contact_id'] ) ) { throw new RuntimeException( 'Contact not found.' ); } $request_data = wp_parse_args( $contact, [ 'first_name' => '', 'last_name' => '', 'company_name' => '', 'job_title' => '', 'street_address' => '', ] ); $request_data['email_address'] = [ 'address' => $contact['email_address']['address'] ?? '', 'permission_to_send' => 'unsubscribed', 'opt_out_reason' => $contact_data['opt_out_reason'] ?? '', ]; $request_data['update_source'] = 'Contact'; $response = $this->request->put( "v3/contacts/{$contact['contact_id']}", $request_data ); if ( $response->has_errors() ) { throw new RuntimeException( esc_html( $response::get_error_message() ) ); } return [ 'contact_id' => $contact['contact_id'], 'action' => 'unsubscribe', 'response' => $response->get_body(), ]; } /** * Check if the access token is expired. * * @since 1.9.3 * * @return bool */ private function is_expired_token(): bool { $expires_in = $this->account['expires_in'] ?? 0; /** * Adding one minute to cover a very rare case when a few seconds are left, * and the site runs multiple API requests. * The last one could be outdated. */ return ( time() + MINUTE_IN_SECONDS ) > $expires_in; } /** * Refresh access token. * * @since 1.9.3 * * @throws InvalidArgumentException If the token cannot be refreshed. */ private function refresh_access_token() { if ( ! $this->is_expired_token() ) { return; } $response = wp_safe_remote_get( add_query_arg( [ 'api-version' => 'v3', 'refresh_token' => $this->account['refresh_token'], ], ConstantContact::get_middleware_url() ) ); $response_body = json_decode( wp_remote_retrieve_body( $response ), true ); if ( empty( $response_body['access_token'] ) ) { throw new InvalidArgumentException( esc_html__( 'Cannot refresh the token.', 'wpforms-lite' ) ); } $this->account = array_merge( $this->account, [ 'access_token' => $response_body['access_token'], 'refresh_token' => $response_body['refresh_token'] ?? '', 'expires_in' => time() + (int) ( $response_body['expires_in'] ?? 0 ), ] ); wpforms_update_providers_options( Core::SLUG, $this->account, $this->account['id'] ); } /** * Get a contact list. * * @since 1.9.3 * * @return array */ public function get_contact_list(): array { $response = $this->request->get( 'v3/contact_lists', [ 'limit' => 1000 ] ); $body = $response->get_body(); $lists = $body['lists'] ?? []; // Replace in lists key list_id with id. return array_map( static function ( $contact_list ) { return [ 'id' => $contact_list['list_id'] ?? '', 'label' => $contact_list['name'] ?? '', ]; }, $lists ); } /** * Get list ids in v2 to v3 format. * * @since 1.9.3 * * @param array $lists List received from Constant Contact v2. * * @return array */ public function get_contact_list_xrefs( array $lists ): array { $ids = implode( ',', wp_list_pluck( $lists, 'id' ) ); $response = $this->request->get( 'v3/contact_lists/list_id_xrefs', [ 'sequence_ids' => $ids, 'limit' => 1000, ] ); $body = $response->get_body(); $lists = $body['xrefs'] ?? []; return wp_list_pluck( $lists, 'list_id', 'sequence_id' ); } } Integrations/ConstantContact/V3/Api/Http/Response.php 0000644 00000005352 15174710275 0016564 0 ustar 00 <?php namespace WPForms\Integrations\ConstantContact\V3\Api\Http; // phpcs:ignore WPForms.PHP.UseStatement.UnusedUseStatement use WP_Error; use RuntimeException; /** * Wrapper class to parse responses. * * @since 1.9.3 */ class Response { /** * Input data. * * @since 1.9.3 * * @var array|WP_Error */ private $input; /** * Error message. * * @since 1.9.3 * * @var string */ private static $error; /** * Request constructor. * * @since 1.9.3 * * @param array|WP_Error $input The response data. */ public function __construct( $input ) { $this->input = $input; self::$error = $this->has_errors() ? $this->get_error_from_body() : ''; } /** * Get an error message. * * @since 1.9.3 * * @return string */ public static function get_error_message(): string { return self::$error; } /** * Retrieve only the response code from the raw response. * * @since 1.9.3 * * @return int The response code as an integer. */ public function get_response_code(): int { return absint( wp_remote_retrieve_response_code( $this->input ) ); } /** * Retrieve only the response message from the raw response. * * @since 1.9.3 * * @return string The response message. */ public function get_response_message(): string { if ( $this->has_errors() ) { return 'Response error'; } $body = $this->get_body(); if ( ! empty( $body['message'] ) ) { return $body['message']; } return wp_remote_retrieve_response_message( $this->input ); } /** * Retrieve only the body from the raw response. * * @since 1.9.3 * * @throws RuntimeException If the response has errors. * * @return array The body of the response. */ public function get_body(): array { if ( $this->has_errors() ) { $error = $this->get_error_from_body(); throw new RuntimeException( esc_html( $error ) ); } return (array) json_decode( wp_remote_retrieve_body( $this->input ), true ); } /** * Whether we received errors in the response. * * @since 1.9.3 * * @return bool True if response has errors. */ public function has_errors(): bool { $code = $this->get_response_code(); return $code < 200 || $code > 299; } /** * Get an error message from the body. * * @since 1.9.3 * * @return string */ private function get_error_from_body(): string { if ( ! $this->has_errors() ) { return ''; } $body = json_decode( wp_remote_retrieve_body( $this->input ), true ); if ( isset( $body['error_message'] ) ) { return $body['error_message']; } $messages = []; foreach ( $body as $id => $value ) { $messages[] = $value['error_message'] ?? ''; if ( $id === 'message' ) { $messages[] = $value; } } return implode( ', ', $messages ); } } Integrations/ConstantContact/V3/Api/Http/Request.php 0000644 00000005613 15174710275 0016416 0 ustar 00 <?php namespace WPForms\Integrations\ConstantContact\V3\Api\Http; use WPForms\Integrations\ConstantContact\V3\ConstantContact; /** * HTTP requests class. * * @since 1.9.3 */ class Request { /** * Base URL. * * @since 1.9.3 * * @var string */ private $base_url; /** * Access token. * * @since 1.9.3 * * @var string */ private $access_token; /** * Constructor. * * @since 1.9.3 * * @param string $access_token Access token. */ public function __construct( string $access_token ) { $this->access_token = $access_token; $this->base_url = ConstantContact::get_api_url(); } /** * Perform a request. * * @since 1.9.3 * * @param string $method Method. * @param string $endpoint Endpoint to attach to the base URL. * @param array $args Submitted arguments. * * @return Response */ private function request( string $method, string $endpoint, array $args = [] ): Response { $request_args = [ 'method' => $method, 'timeout' => 5, 'headers' => $this->get_headers(), ]; if ( $args ) { $request_args['body'] = wp_json_encode( $args ); } /** * Allow modifying the HTTP request arguments. * * @since 1.9.3 * * @param array $args List of request arguments. */ $request_args = (array) apply_filters( 'wpforms_integrations_constant_contact_v3_api_http_request_args', $request_args ); $response = wp_remote_request( $this->base_url . $endpoint, $request_args ); return new Response( $response ); } /** * GET request. * * @since 1.9.3 * * @param string $endpoint Endpoint to attach to the base URL. * @param array $args Query arguments. * * @return Response */ public function get( string $endpoint, array $args = [] ): Response { $endpoint = add_query_arg( $args, $endpoint ); return $this->request( 'GET', $endpoint ); } /** * POST request. * * @since 1.9.3 * * @param string $endpoint Endpoint to attach to the base URL. * @param array $args Submitted arguments. * * @return Response */ public function post( string $endpoint, array $args = [] ): Response { return $this->request( 'POST', $endpoint, $args ); } /** * Send DELETE request. * * @since 1.9.3 * * @param string $endpoint Endpoint. * * @return Response */ public function delete( string $endpoint ): Response { return $this->request( 'DELETE', $endpoint ); } /** * PUT request. * * @since 1.9.3 * * @param string $endpoint Endpoint. * @param array $args Submitted arguments. * * @return Response */ public function put( string $endpoint, array $args = [] ): Response { return $this->request( 'PUT', $endpoint, $args ); } /** * Get headers. * * @since 1.9.3 * * @return array */ private function get_headers(): array { return [ 'Authorization' => 'Bearer ' . $this->access_token, 'Content-Type' => 'application/json', ]; } } Integrations/ConstantContact/V3/Auth.php 0000644 00000013402 15174710275 0014232 0 ustar 00 <?php namespace WPForms\Integrations\ConstantContact\V3; use Exception; use RuntimeException; use WPForms\Integrations\ConstantContact\V3\Api\Api; /** * Class Auth. * * @since 1.9.3 */ class Auth { /** * Nonce. * * @since 1.9.3 */ const NONCE = 'wpforms-constant-contact-v3'; /** * Add hooks. * * @since 1.9.3 */ public function hooks() { add_action( 'wpforms_builder_enqueues', [ $this, 'enqueue_scripts' ] ); add_action( 'wpforms_settings_enqueue', [ $this, 'enqueue_scripts' ] ); add_action( 'wp_ajax_wpforms_constant_contact_popup_auth', [ $this, 'ajax_handle_auth' ] ); } /** * Load scripts. * * @since 1.9.3 */ public function enqueue_scripts() { $min = wpforms_get_min_suffix(); wp_enqueue_script( 'wpforms-constant-contact-v3-auth', WPFORMS_PLUGIN_URL . "assets/js/integrations/constant-contact-v3/auth{$min}.js", [ 'jquery' ], WPFORMS_VERSION, true ); wp_localize_script( 'wpforms-constant-contact-v3-auth', 'WPFormsConstantContactV3AuthVars', [ 'auth_url' => self::get_auth_url(), 'ajax_url' => admin_url( 'admin-ajax.php' ), 'page_url' => $this->get_page_url(), 'nonce' => wp_create_nonce( self::NONCE ), 'strings' => [ 'wait' => esc_html__( 'Please wait a moment...', 'wpforms-lite' ), 'error' => esc_html__( 'There was an error while processing your request. Please try again.', 'wpforms-lite' ), ], ] ); } /** * Handle Auth popup. * * @since 1.9.3 */ public function ajax_handle_auth() { try { if ( ! wpforms_current_user_can() ) { wp_send_json_error( esc_html__( 'You do not have permission to perform this action.', 'wpforms-lite' ) ); } $account = $this->create_account(); $this->validate_account( $account ); wpforms_update_providers_options( Core::SLUG, $account, $account['id'] ); wp_send_json_success( $account['id'] ); } catch ( Exception $e ) { wp_send_json_error( $e->getMessage() ); } } /** * Receive and validate access and refresh tokens. * * @since 1.9.3 * * @return array * * @throws RuntimeException Invalid code. */ private function get_code(): array { check_ajax_referer( self::NONCE, 'nonce' ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $response = json_decode( wp_unslash( $_POST['data'] ?? '' ), true ); $invalid_code_message = __( 'Invalid code.', 'wpforms-lite' ); if ( empty( $response ) || empty( $response['code'] ) ) { throw new RuntimeException( esc_html( $invalid_code_message ) ); } $code = json_decode( $response['code'], true ); if ( empty( $code['access_token'] ) ) { throw new RuntimeException( esc_html( $invalid_code_message ) ); } return $code; } /** * Validate account. * * @since 1.9.3 * * @param array $account Account data. * * @throws RuntimeException Invalid account. */ private function validate_account( array $account ) { if ( empty( $account['email'] ) ) { throw new RuntimeException( esc_html__( 'Invalid account.', 'wpforms-lite' ) ); } $accounts = wpforms_get_providers_options( Core::SLUG ); if ( empty( $accounts ) ) { return; } $emails = wp_list_pluck( $accounts, 'id', 'email' ); if ( isset( $emails[ $account['email'] ] ) && $emails[ $account['email'] ] !== $account['id'] ) { throw new RuntimeException( esc_html__( 'This email is already connected.', 'wpforms-lite' ) ); } } /** * Build an option array. * * @since 1.9.3 * * @return array * @noinspection NonSecureUniqidUsageInspection */ private function create_account(): array { $code = $this->get_code(); $time = time(); $account = [ 'id' => uniqid(), 'date' => $time, 'access_token' => $code['access_token'], 'refresh_token' => $code['refresh_token'] ?? '', 'expires_in' => $time + (int) ( $code['expires_in'] ?? 0 ), ]; $account_summary = ( new Api( $account ) )->get_account_summary(); $account['email'] = $account_summary['contact_email'] ?? ''; $account['label'] = $this->get_label( $account_summary ); /** * Filters the account data after it was created. * * @since 1.9.3 * * @param array $account Account data. */ return (array) apply_filters( 'wpforms_integrations_constant_contact_v3_auth_create_account_data', $account ); } /** * Get APP data needed for auth in the sing-up popup. * * @since 1.9.3 * * @return string */ public static function get_auth_url(): string { return add_query_arg( [ 'client_id' => ConstantContact::get_api_key(), 'scope' => 'offline_access account_read contact_data', 'redirect_uri' => add_query_arg( 'api-version', 'v3', ConstantContact::get_middleware_url() ), 'state' => 'WPForms-' . wp_rand( 1000, 9999 ), 'response_type' => 'code', 'prompt' => 'login', ], ConstantContact::SIGN_UP ); } /** * Get label. * * @since 1.9.3 * * @param array $account_summary Account summary. * * @return string */ private function get_label( array $account_summary ): string { $email_part = $account_summary['contact_email'] ?? ''; $org_part = $account_summary['organization_name'] ?? ''; if ( empty( $email_part ) && empty( $org_part ) ) { return ''; } if ( empty( $email_part ) ) { return $org_part; } if ( empty( $org_part ) ) { return $email_part; } return "$email_part / $org_part"; } /** * Get the URL to the providers' page with the focus on the CC v3 integration. * * @since 1.9.3 * * @return string */ private function get_page_url(): string { return add_query_arg( [ 'page' => 'wpforms-settings', 'view' => 'integrations', 'wpforms-integration' => Core::SLUG, ], admin_url( 'admin.php' ) ); } } Integrations/ConstantContact/V3/ConstantContact.php 0000644 00000006240 15174710275 0016440 0 ustar 00 <?php // phpcs:ignore Generic.Commenting.DocComment.MissingShort /** @noinspection PhpUndefinedConstantInspection */ namespace WPForms\Integrations\ConstantContact\V3; use WPForms\Providers\Providers; use WPForms\Integrations\ConstantContact\V3\Migration\Migration; use WPForms\Integrations\IntegrationInterface; /** * Class ConstantContact. * * @since 1.9.3 */ class ConstantContact implements IntegrationInterface { /** * Current integration version. * * @since 1.9.3 */ const VERSION_OPTION = 'wpforms_constant_contact_version'; /** * API key. * * @since 1.9.3 * * @var string */ const API_KEY = '551ccf74-4e2d-4649-8f58-e5a973789b94'; /** * API URL. * * @since 1.9.3 */ const API_URL = 'https://api.cc.email/'; /** * Sign up URL. * * @since 1.9.3 */ const SIGN_UP = 'https://authz.constantcontact.com/oauth2/default/v1/authorize'; /** * Indicate if current integration is allowed to load. * * @since 1.9.3 * * @return bool */ public function allow_load(): bool { return true; } /** * Load the integration. * * @since 1.9.3 */ public function load() { ( new Migration() )->init(); ( new Auth() )->hooks(); if ( self::get_current_version() !== 3 && empty( wpforms_get_providers_options( Core::SLUG ) ) ) { return; } Providers::get_instance()->register( Core::get_instance() ); } /** * Return an actual working constant contact version. * By default, it is 2. * * @since 1.9.3 * * @return int */ public static function get_current_version(): int { $current_version = get_option( self::VERSION_OPTION, false ); if ( $current_version !== false ) { return (int) $current_version; } $current_version = empty( wpforms_get_providers_options( 'constant-contact' ) ) ? 3 : 2; update_option( self::VERSION_OPTION, $current_version ); return $current_version; } /** * Get the API key. * * @since 1.9.3 * * @return string */ public static function get_api_key(): string { return defined( 'WPFORMS_CONSTANT_CONTACT_API_KEY' ) ? (string) WPFORMS_CONSTANT_CONTACT_API_KEY : self::API_KEY; } /** * Get the API URL. * * @since 1.9.3 * * @return string */ public static function get_api_url(): string { return self::API_URL; } /** * Get the redirect URI. * * @since 1.9.3 * * @return string */ public static function get_middleware_url(): string { return defined( 'WPFORMS_CONSTANT_CONTACT_MIDDLEWARE_URL' ) && WPFORMS_CONSTANT_CONTACT_MIDDLEWARE_URL ? WPFORMS_CONSTANT_CONTACT_MIDDLEWARE_URL : 'https://wpforms.com/oauth/constant-contact/'; } /** * Get the list of predefined custom fields. * * @since 1.9.3 * * @return array */ public static function get_predefined_custom_fields(): array { $fields = [ 'first_name' => __( 'First Name', 'wpforms-lite' ), 'last_name' => __( 'Last Name', 'wpforms-lite' ), 'phone' => __( 'Phone', 'wpforms-lite' ), 'job_title' => __( 'Job Title', 'wpforms-lite' ), 'company_name' => __( 'Company Name', 'wpforms-lite' ), ]; if ( wpforms()->is_pro() ) { $fields['address'] = __( 'Address', 'wpforms-lite' ); } return $fields; } } Integrations/ConstantContact/V3/Migration/Migration.php 0000644 00000042523 15174710275 0017221 0 ustar 00 <?php namespace WPForms\Integrations\ConstantContact\V3\Migration; use WP_Post; use RuntimeException; use WPForms_Constant_Contact; use WPForms\Integrations\ConstantContact\V3\Core; use WPForms\Integrations\ConstantContact\V3\Auth; use WPForms\Integrations\ConstantContact\V3\Api\Api; use WPForms\Integrations\ConstantContact\V3\ConstantContact; /** * Migration class. * * The loader for the rest of classes in the namespace and manager * of the migration process. * * @since 1.9.3 */ class Migration { /** * List of migrated list ids in v2 => v3 format. * * @since 1.9.3 * * @var array */ private $lists = []; /** * New account data. * * @since 1.9.3 * * @var array */ private $new_account; /** * Form data and settings. * * @since 1.9.3 * * @var array */ private $form_data; /** * Index of the first name custom field in the new account. * * @since 1.9.3 * * @var int|null */ private $first_name_index; /** * Index of the last name custom field in the new account. * * @since 1.9.3 * * @var int|null */ private $last_name_index; /** * Init. * * @since 1.9.3 */ public function init() { $this->force_migration(); if ( ConstantContact::get_current_version() >= 3 ) { return; } $this->display_prompt(); $this->hooks(); } /** * Hooks. * * @since 1.9.3 */ private function hooks() { // Add ajax action. add_action( 'wp_ajax_wpforms_constant_contact_migration_prompt', [ $this, 'ajax_start_migration' ] ); add_action( 'update_option_wpforms_providers', [ $this, 'update_providers_options_after' ], 10, 2 ); add_filter( 'wpforms_integrations_constant_contact_v3_auth_create_account_data', [ $this, 'migrate_account_finish' ] ); } /** * Force migration. * * @since 1.9.3 */ private function force_migration() { if ( ! wpforms_is_admin_page( 'settings', 'integrations' ) ) { return; } if ( ! current_user_can( 'manage_options' ) ) { return; } $key = 'constant_contact-force-migration'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( ! isset( $_GET[ $key ] ) ) { return; } if ( isset( $_SERVER['REQUEST_URI'] ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $_SERVER['REQUEST_URI'] = remove_query_arg( $key, wp_unslash( $_SERVER['REQUEST_URI'] ) ); } delete_option( ConstantContact::VERSION_OPTION ); } /** * Display migration prompt. * * @since 1.9.3 */ private function display_prompt() { if ( ! wpforms_is_admin_page( 'settings', 'integrations' ) ) { return; } if ( $this->migrated_accounts_exist() ) { return; } $notice_obj = wpforms()->obj( 'notice' ); if ( ! $notice_obj ) { return; } $notice_obj::error( wp_kses( sprintf( /* translators: %1$s - link to the migration page, %2$s - closing HTML tag. */ __( 'You need to migrate your existing forms to the new version of the Constant Contact addon. Please %1$s click here%2$s to start the migration.', 'wpforms-lite' ), '<a href="#" rel="noopener noreferrer" id="wpforms-settings-constant-contact-v3-migration-prompt-link">', '</a>' ), [ 'a' => [ 'href' => [], 'rel' => [], 'id' => [], ], ] ) ); } /** * Replace account ID if it was migrated. * * @since 1.9.3 * * @param array $new_account New account data. * * @return array */ public function migrate_account_finish( array $new_account ): array { $accounts = wpforms_get_providers_options( Core::SLUG ); foreach ( $accounts as $account_id => $account ) { if ( $account['email'] === $new_account['email'] && ! empty( $account['accounts'] ) ) { $new_account['id'] = $account_id; $this->new_account = $new_account; $this->migrate_forms( $account ); break; } } return $new_account; } /** * Finish migration by setting the version to 3. * * @since 1.9.3 */ public static function finish_migration() { update_option( ConstantContact::VERSION_OPTION, 3 ); } /** * Update providers options after migration. * * @since 1.9.3 * * @param mixed $old_value Old providers options. * @param mixed $new_value New providers options. * * @noinspection PhpUnusedParameterInspection */ public function update_providers_options_after( $old_value, $new_value ) { if ( empty( wpforms_get_providers_options( 'constant-contact' ) ) ) { self::finish_migration(); return; } if ( ! is_array( $new_value ) || empty( $new_value[ Core::SLUG ] ) ) { return; } if ( $this->migrated_accounts_exist() ) { return; } self::finish_migration(); } /** * Check if some migrated accounts have been already created. * * @since 1.9.3 * * @return bool */ private function migrated_accounts_exist(): bool { $accounts = wpforms_get_providers_options( Core::SLUG ); foreach ( $accounts as $account ) { if ( ! empty( $account['accounts'] ) ) { return true; } } return false; } /** * Migrate all accounts. * * @since 1.9.3 */ public function ajax_start_migration() { check_ajax_referer( Auth::NONCE, 'nonce' ); if ( ! wpforms_current_user_can() ) { wp_send_json_error( esc_html__( 'You do not have permission to perform this action.', 'wpforms-lite' ) ); } $accounts = wpforms_get_providers_options(); // No accounts to migrate. if ( empty( $accounts['constant-contact'] ) ) { self::finish_migration(); wp_send_json_success(); } foreach ( $accounts['constant-contact'] as $account_id => $account ) { $this->migrate_account_start( $account_id, $account, $accounts ); } // If no accounts were migrated because v2 accounts were invalid, we switch to the new version. if ( empty( $accounts[ Core::SLUG ] ) ) { self::finish_migration(); wp_send_json_success(); } update_option( 'wpforms_providers', $accounts ); wp_send_json_success(); } /** * Migrate a specific v2 account to v3. * * @since 1.9.3 * * @param string $account_id Account ID. * @param array $account Current account data. * @param array $accounts List of all providers' accounts. */ private function migrate_account_start( string $account_id, array $account, array &$accounts ) { static $migrated_access_tokens = []; // It was possible to create an account without an access token. if ( empty( $account['access_token'] ) ) { return; } // It was possible to create a few accounts with the same access token. // We merge them into one in the new version. if ( isset( $migrated_access_tokens[ $account['access_token'] ] ) ) { $created_account_id = $migrated_access_tokens[ $account['access_token'] ]; $accounts['constant-contact-v3'][ $created_account_id ]['accounts'][] = $account_id; return; } $email = $this->get_account_email( $account ); // We skip an account if we can't receive email, in the case the access_token isn't valid. if ( empty( $email ) ) { return; } $migrated_access_tokens[ $account['access_token'] ] = $account_id; $accounts['constant-contact-v3'][ $account_id ] = [ 'id' => $account_id, 'accounts' => [ $account_id ], 'access_token' => $account['access_token'], 'date' => 0, 'label' => $account['label'] ?? $email, 'email' => $email, ]; } /** * Get email from an account. * * @since 1.9.3 * * @param array $account Account data. * * @return string */ private function get_account_email( array $account ): string { $old_provider = new WPForms_Constant_Contact(); $old_provider->access_token = $account['access_token']; $account_info = $old_provider->get_account_information(); if ( is_wp_error( $account_info ) ) { return ''; } return $account_info['email'] ?? ''; } /** * Migrate forms. * * @since 1.9.3 * * @param array $old_account Old account. * * @return void */ private function migrate_forms( array $old_account ) { if ( ! isset( $old_account['accounts'], $old_account['access_token'] ) ) { return; } $forms = $this->get_forms( (array) $old_account['accounts'] ); if ( empty( $forms ) ) { return; } $this->lists = $this->get_lists_xhref( $this->new_account, $old_account['access_token'] ); foreach ( $forms as $form ) { $this->migrate_form( $form ); } } /** * Get migrated forms. * * @since 1.9.3 * * @param array $old_account_ids Old v2 account ids. * * @return array * @noinspection SqlResolve */ private function get_forms( array $old_account_ids ): array { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $forms = $wpdb->get_col( $wpdb->prepare( 'SELECT ID FROM ' . $wpdb->posts . ' WHERE post_type = "wpforms" AND post_content REGEXP %s', implode( '|', $old_account_ids ) ) ); if ( empty( $forms ) ) { return []; } $form_ids = array_map( 'absint', $forms ); $form_obj = wpforms()->obj( 'form' ); if ( ! $form_obj ) { return []; } return (array) $form_obj->get( '', [ 'numberposts' => -1, 'orderby' => 'post__in', 'post__in' => $form_ids, 'update_post_meta_cache' => false, 'update_post_term_cache' => false, 'no_found_rows' => true, ] ); } /** * Copy connections from v2 to v3 in proper format. * * @since 1.9.3 * * @param WP_Post $form Form object. */ private function migrate_form( WP_Post $form ) { $this->form_data = wpforms_decode( $form->post_content ); // Nothing to migrate. if ( empty( $this->form_data['providers']['constant-contact'] ) ) { return; } $migrated_connections = $this->form_data['providers'][ Core::SLUG ] ?? []; // All connections were migrated but account migration was interrupted by timeout or an error. if ( count( $this->form_data['providers']['constant-contact'] ) === count( $migrated_connections ) ) { return; } $this->form_data['providers'][ Core::SLUG ] = array_merge( $migrated_connections, $this->get_new_connections() ); $form_obj = wpforms()->obj( 'form' ); if ( ! $form_obj ) { return; } $form_obj->update( $this->form_data['id'], $this->form_data ); } /** * Modify v2 connections to v3. * * @since 1.9.3 * * @return array */ private function get_new_connections(): array { $old_connections = $this->form_data['providers']['constant-contact'] ?? []; $new_connections = []; foreach ( $old_connections as $connection_id => $connection ) { $new_connection_id = str_replace( 'connection_', '', $connection_id ); $connection = wp_parse_args( $connection, [ 'connection_name' => '', 'account_id' => '', 'list_id' => '', 'fields' => [], 'conditional_logic' => '', 'conditional_type' => '', 'conditionals' => [], ] ); // The connection is related to another account, skip it. if ( $this->new_account['id'] !== $connection['account_id'] ) { continue; } reset( $this->lists ); $new_connections[ $new_connection_id ] = [ 'id' => $new_connection_id, 'name' => $connection['connection_name'], 'account_id' => $connection['account_id'], 'action' => 'subscribe', 'list' => $this->lists[ $connection['list_id'] ] ?? key( $this->lists ), 'email' => explode( '.', $connection['fields']['email'] ?? '' )[0], 'fields_meta' => $this->get_connection_custom_fields( $connection['fields'] ), 'conditional_logic' => $connection['conditional_logic'], 'conditional_type' => $connection['conditional_type'], 'conditionals' => $connection['conditionals'], ]; } return $new_connections; } /** * Get custom fields. * * @since 1.9.3 * * @param array $custom_fields Custom fields v2. * * @return array */ private function get_connection_custom_fields( array $custom_fields ): array { $fields_meta = []; $custom_fields = $this->sort_custom_fields( $custom_fields ); foreach ( $custom_fields as $key => $value ) { if ( $key === 'email' ) { continue; } $value_parts = explode( '.', $value ); $field_id = $value_parts[0]; if ( wpforms_is_empty_string( $field_id ) ) { continue; } $fields_meta = $this->update_fields_meta( $fields_meta, $field_id, $key, $value_parts ); } return $fields_meta; } /** * Move $custom_fields['full_name'] at the beginning of the array. * * Thanks to this, if first name and last name are defined, next iterations * of this array will replace full_name - backward compatibility sustained. * * @since 1.9.3 * * @param array $custom_fields Custom fields. * * @return array */ private function sort_custom_fields( array $custom_fields ): array { if ( ! isset( $custom_fields['full_name'] ) || wpforms_is_empty_string( $custom_fields['full_name'] ) ) { return $custom_fields; } $full_name = $custom_fields['full_name']; unset( $custom_fields['full_name'] ); return [ 'full_name' => $full_name ] + $custom_fields; } /** * Update fields meta. * * @since 1.9.3 * * @param array $fields_meta Fields meta. * @param string $field_id Field ID. * @param string $key Key. * @param array $value_parts Value parts. * * @return array */ private function update_fields_meta( array $fields_meta, string $field_id, string $key, array $value_parts ): array { if ( $this->form_data['fields'][ $field_id ]['type'] === 'name' ) { $name_field = $this->handle_name_field( $fields_meta, $field_id, $key, $value_parts ); if ( is_array( $name_field ) ) { return $name_field; } $field_id = $name_field; } $keys_to_rename = [ 'work_phone' => 'phone', 'url' => $this->get_url_field_id(), ]; $new_key = $keys_to_rename[ $key ] ?? $key; $fields_meta[ $this->get_meta_next_index( $fields_meta, $new_key ) ] = [ 'name' => $new_key, 'field_id' => $field_id, ]; return $fields_meta; } /** * Handle name field. * * @since 1.9.3 * * @param array $fields_meta Fields meta. * @param string $field_id Field ID. * @param string $key Key. * @param array $value_parts Value parts. * * @return string|array */ private function handle_name_field( array $fields_meta, string $field_id, string $key, array $value_parts ) { if ( $value_parts[1] === 'value' ) { $value_parts[1] = 'full'; } if ( $key === 'full_name' ) { return $this->update_full_name( $fields_meta, $field_id, $value_parts ); } $field_id .= '.' . $value_parts[1]; return $field_id; } /** * Update full name meta. * * @since 1.9.3 * * @param array $fields_meta Fields meta. * @param string $field_id Field ID. * @param array $value_parts Value parts. * * @return array */ private function update_full_name( array $fields_meta, string $field_id, array $value_parts ): array { $field = $this->form_data['fields'][ $field_id ] ?? []; $is_simple = ! isset( $field['format'] ) || $field['format'] === 'simple'; $first_name_field_id = $is_simple ? $field_id . '.' . $value_parts[1] : $field_id . '.first'; $fields_meta[] = [ 'name' => 'first_name', 'field_id' => $first_name_field_id, ]; $this->first_name_index = count( $fields_meta ) - 1; if ( $is_simple ) { return $fields_meta; } $last_name_field_id = $field_id . '.last'; $fields_meta[] = [ 'name' => 'last_name', 'field_id' => $last_name_field_id, ]; $this->last_name_index = count( $fields_meta ) - 1; return $fields_meta; } /** * Get next index for a custom field. * * @since 1.9.3 * * @param array $fields_meta Fields meta. * @param string $key Key. */ private function get_meta_next_index( array $fields_meta, string $key ): int { if ( $key === 'first_name' ) { return $this->first_name_index ?? count( $fields_meta ); } if ( $key === 'last_name' ) { return $this->last_name_index ?? count( $fields_meta ); } return count( $fields_meta ); } /** * Get URL custom field ID from the new account. * * Returns the id in the new format. * * @since 1.9.3 * * @return string */ private function get_url_field_id(): string { static $field_id; if ( $field_id ) { return $field_id; } $custom_fields = ( new Api( $this->new_account ) )->get_custom_fields( 'custom_field_id', 'name' ); $field_id = $custom_fields['custom_field_1'] ?? $this->register_url_field(); return $field_id; } /** * Get an array of list v2 ids to v3 ids. * * @since 1.9.3 * * @param array $new_account New account data. * @param string $access_token_v2 Access token for v2. * * @return array * * @throws RuntimeException Can't receive v2 lists and finish migration. */ private function get_lists_xhref( array $new_account, string $access_token_v2 ): array { $old_provider = new WPForms_Constant_Contact(); $old_provider->access_token = $access_token_v2; $old_lists = $old_provider->api_lists(); if ( is_wp_error( $old_lists ) ) { throw new RuntimeException( esc_html__( 'Can\'t receive v2 lists and finish migration.', 'wpforms-lite' ) ); } return ( new Api( $new_account ) )->get_contact_list_xrefs( (array) $old_lists ); } /** * Register URL custom field. * * @since 1.9.3 * * @return string */ private function register_url_field(): string { return ( new Api( $this->new_account ) )->register_custom_field( 'Website / URL' ); } } Integrations/ConstantContact/V3/Core.php 0000644 00000004527 15174710275 0014231 0 ustar 00 <?php // phpcs:ignore Generic.Commenting.DocComment.MissingShort /** @noinspection PhpUndefinedConstantInspection */ namespace WPForms\Integrations\ConstantContact\V3; use WPForms\Integrations\ConstantContact\V3\Settings\FormBuilder; use WPForms\Integrations\ConstantContact\V3\Settings\PageIntegrations; use WPForms\Providers\Provider\Core as ProviderCore; /** * Class Constant Contact V3 Core. * * @since 1.9.3 */ class Core extends ProviderCore { /** * Priority for a provider, that will affect loading/placement order. * * @since 1.9.3 */ const PRIORITY = 14; /** * Unique provider slug. * * @since 1.9.3 * * @var string */ const SLUG = 'constant-contact-v3'; /** * Core constructor. * * @since 1.9.3 */ public function __construct() { parent::__construct( [ 'slug' => self::SLUG, 'name' => $this->get_name(), 'icon' => WPFORMS_PLUGIN_URL . 'assets/images/icon-provider-constant-contact.png', ] ); } /** * Provide an instance of the object, that should process the submitted entry. * It will use data from an already saved entry to pass it further to a Provider. * * @since 1.9.3 * * @return Process */ public function get_process(): Process { static $process; if ( ! $process ) { $process = new Process( $this ); } return $process; } /** * Provide an instance of the object, that should display provider settings * on Settings > Integrations page in the admin area. * * @since 1.9.3 * * @return PageIntegrations */ public function get_page_integrations(): PageIntegrations { static $integration; if ( ! $integration ) { $integration = new PageIntegrations( static::get_instance() ); } return $integration; } /** * Provide an instance of the object, that should display provider settings in the Form Builder. * * @since 1.9.3 * * @return FormBuilder */ public function get_form_builder(): FormBuilder { static $builder; if ( ! $builder ) { $builder = new FormBuilder( $this ); } return $builder; } /** * Provider account name. * * Adds "(V3)" to the name if WPFORMS_DEBUG is defined. * * @since 1.9.3 * * @return string */ private function get_name(): string { $base = 'Constant Contact'; if ( ! defined( 'WPFORMS_DEBUG' ) || ! WPFORMS_DEBUG ) { return $base; } return $base . ' (V3)'; } } Integrations/ConstantContact/V3/Settings/FormBuilder.php 0000644 00000024421 15174710275 0017346 0 ustar 00 <?php namespace WPForms\Integrations\ConstantContact\V3\Settings; use Exception; use WPForms\Integrations\ConstantContact\V3\Api\Api; use WPForms\Integrations\ConstantContact\V3\Auth; use WPForms\Integrations\ConstantContact\V3\ConstantContact; use WPForms\Providers\Provider\Settings\FormBuilder as FormBuilderAbstract; /** * Class FormBuilder. * * @since 1.9.3 */ class FormBuilder extends FormBuilderAbstract { /** * Register all hooks (actions and filters) here. * * @since 1.9.3 */ protected function init_hooks() { // phpcs:ignore WPForms.PHP.HooksMethod.InvalidPlaceForAddingHooks parent::init_hooks(); add_filter( 'wpforms_providers_settings_builder_ajax_connections_get_' . $this->core->slug, [ $this, 'ajax_connections_get' ] ); if ( is_admin() ) { add_filter( "wpforms_providers_provider_settings_formbuilder_display_content_default_screen_{$this->core->slug}", [ $this, 'builder_settings_default_content' ] ); } add_filter( 'wpforms_save_form_args', [ $this, 'save_form' ], 11, 3 ); } /** * Display content inside the panel sidebar area. * * @since 1.9.3 */ public function display_sidebar() { if ( ConstantContact::get_current_version() !== 3 ) { return; } parent::display_sidebar(); } /** * Enqueue JavaScript and CSS files if needed. * When extending - include the `parent::enqueue_assets();` not to break things! * * @since 1.9.3 */ public function enqueue_assets() { parent::enqueue_assets(); $min = wpforms_get_min_suffix(); wp_enqueue_script( 'wpforms-constant-contact-v3-builder', WPFORMS_PLUGIN_URL . "assets/js/integrations/constant-contact-v3/builder{$min}.js", [ 'underscore', 'wpforms-admin-builder-providers', 'wpforms-constant-contact-v3-auth' ], WPFORMS_VERSION, true ); } /** * Pre-process provider data before saving it in form_data when editing a form. * * @since 1.9.3 * * @param array|mixed $form Form array which is usable with `wp_update_post()`. * @param array $data Data retrieved from $_POST and processed. * @param array $args Empty by default. May have custom data not intended to be saved, but used for processing. * * @return array * @noinspection PhpMissingParamTypeInspection * @noinspection PhpUnusedParameterInspection */ public function save_form( $form, $data, $args ): array { $form = (array) $form; // Get a filtered (or modified by another addon) form content. $form_data = json_decode( stripslashes( $form['post_content'] ), true ); // Provider exists. if ( ! empty( $form_data['providers'][ $this->core->slug ] ) ) { $modified_post_content = $this->modify_form_data( $form_data ); if ( ! empty( $modified_post_content ) ) { $form['post_content'] = wpforms_encode( $modified_post_content ); return $form; } } /* * This part works when modification is locked or current filter was called on NOT a Providers panel. * Then we need to restore provider connections from the previous form content. */ // Get a "previous" form content (current content is still not saved). $prev_form = ! empty( $data['id'] ) ? wpforms()->obj( 'form' )->get( $data['id'], [ 'content_only' => true ] ) : []; if ( ! empty( $prev_form['providers'][ $this->core->slug ] ) ) { $provider = $prev_form['providers'][ $this->core->slug ]; if ( ! isset( $form_data['providers'] ) ) { $form_data = array_merge( $form_data, [ 'providers' => [] ] ); } $form_data['providers'] = array_merge( (array) $form_data['providers'], [ $this->core->slug => $provider ] ); $form['post_content'] = wpforms_encode( $form_data ); } return $form; } /** * Prepare modifications for form content if it's not locked. * * @since 1.9.3 * * @param array $form_data Form content. * * @return array|null */ protected function modify_form_data( array $form_data ) { /** * The connection is locked. * Why? User clicked the "Save" button when one of the AJAX requests * for data retrieval from API was in progress or failed. */ if ( isset( $form_data['providers'][ $this->core->slug ]['__lock__'] ) && absint( $form_data['providers'][ $this->core->slug ]['__lock__'] ) === 1 ) { return null; } // Modify content as we need, done by reference. foreach ( $form_data['providers'][ $this->core->slug ] as $connection_id => $connection ) { if ( $connection_id === '__lock__' ) { unset( $form_data['providers'][ $this->core->slug ]['__lock__'] ); } } return $form_data; } /** * Rewrite the Add New Account button to trigger Auth popup instead of default authorization flow. * * @since 1.9.3 */ protected function display_content_header() { if ( ! empty( wpforms_get_providers_options( $this->core->slug ) ) ) { parent::display_content_header(); return; } ?> <div class="wpforms-builder-provider-title wpforms-panel-content-section-title"> <?php echo esc_html( $this->core->name ); ?> <button type="button" class="wpforms-builder-provider-title-add wpforms-builder-constant-contact-v3-provider-sign-up"> <?php esc_html_e( 'Add New Account', 'wpforms-lite' ); ?> </button> </div> <?php } /** * Get the list of all saved connections. * * @since 1.9.3 * * @return array Return null on any kind of error. Array of data otherwise. */ public function ajax_connections_get(): array { $data = [ 'actions' => [ 'subscribe' => __( 'Subscribe', 'wpforms-lite' ), 'unsubscribe' => __( 'Unsubscribe', 'wpforms-lite' ), 'delete' => __( 'Delete subscriber', 'wpforms-lite' ), ], 'actions_fields' => [ 'subscribe' => [ 'email' => [ 'label' => __( 'Email', 'wpforms-lite' ), 'type' => 'select', 'map' => 'email', 'required' => true, ], 'list' => [ 'label' => __( 'Select List', 'wpforms-lite' ), 'type' => 'select', 'required' => true, 'placeholder' => __( '--- Select Mailing List ---', 'wpforms-lite' ), ], 'custom_fields' => [ 'label' => __( 'Custom Fields', 'wpforms-lite' ), 'type' => 'custom-fields', 'required' => false, ], ], 'unsubscribe' => [ 'email' => [ 'label' => __( 'Email', 'wpforms-lite' ), 'type' => 'select', 'map' => 'email', 'required' => true, 'placeholder' => __( '--- Select Form Field ---', 'wpforms-lite' ), ], 'opt_out_reason' => [ 'label' => __( 'Reason', 'wpforms-lite' ), 'type' => 'select', 'required' => false, 'placeholder' => __( '--- Select Form Field ---', 'wpforms-lite' ), ], ], 'delete' => [ 'email' => [ 'label' => __( 'Email', 'wpforms-lite' ), 'type' => 'select', 'map' => 'email', 'required' => true, ], ], ], 'connections' => isset( $this->form_data['providers'][ $this->core->slug ] ) ? array_reverse( $this->form_data['providers'][ $this->core->slug ], true ) : [], 'conditionals' => [], ]; foreach ( $data['connections'] as $connection ) { if ( empty( $connection['id'] ) ) { continue; } // This will either return an empty placeholder or complete set of rules, as a DOM. $data['conditionals'][ $connection['id'] ] = wpforms()->is_pro() ? wpforms_conditional_logic()->builder_block( [ 'form' => $this->form_data, 'type' => 'panel', 'parent' => 'providers', 'panel' => $this->core->slug, 'subsection' => $connection['id'], ], false ) : ''; } return array_merge( $data, $this->get_accounts_data() ); } /** * Get accounts data. * * @since 1.9.3 * * @return array */ private function get_accounts_data(): array { $accounts = wpforms_get_providers_options( $this->core->slug ); $data = [ 'accounts' => $accounts, 'custom_fields' => [], 'lists' => [], ]; if ( empty( $accounts ) ) { return $data; } $predefined_custom_fields = ConstantContact::get_predefined_custom_fields(); foreach ( $accounts as $account_id => $account ) { try { $api = new Api( $account ); $data['lists'][ $account_id ] = $api->get_contact_list(); $data['custom_fields'][ $account_id ] = array_merge( $predefined_custom_fields, $api->get_custom_fields( 'label', 'custom_field_id' ) ); } catch ( Exception $e ) { continue; } } return $data; } /** * Builder custom templates. * * @since 1.9.3 */ public function builder_custom_templates() { $templates = [ 'connection', 'error', 'select-field', ]; foreach ( $templates as $template ) { $template_name = ucwords( str_replace( '-', ' ', $template ) ); $script_id = 'tmpl-wpforms-' . esc_attr( $this->core->slug ) . '-builder-content-connection'; if ( $template !== 'connection' ) { $script_id .= '-' . $template; } ?> <!-- Single Constant Contact connection block: <?php echo esc_attr( $template_name ); ?>. --> <script type="text/html" id="<?php echo esc_attr( $script_id ); ?>"> <?php // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo wpforms_render( 'integrations/constant-contact-v3/builder/' . $template, [ 'slug' => $this->core->slug, ], true ); ?> </script> <?php } } /** * Default content for the provider settings panel in the form builder. * * @since 1.9.3 * * @param string $content Default content. * * @return string * @noinspection HtmlUnknownTarget */ public function builder_settings_default_content( string $content ): string { ob_start(); ?> <p> <a href="<?php echo esc_url( Auth::get_auth_url() ); ?>" class="wpforms-btn wpforms-btn-md wpforms-btn-orange wpforms-builder-constant-contact-v3-provider-sign-up" target="_blank" rel="noopener noreferrer"> <?php esc_html_e( 'Try Constant Contact for Free', 'wpforms-lite' ); ?> </a> </p> <p> <?php printf( '<a href="%1$s" target="_blank" rel="noopener noreferrer" class="secondary-text">%2$s</a>', esc_url( admin_url( 'admin.php?page=wpforms-page&view=constant-contact' ) ), esc_html__( 'Learn more about the power of email marketing.', 'wpforms-lite' ) ); ?> </p> <?php return $content . ob_get_clean(); } } Integrations/ConstantContact/V3/Settings/FieldMapping.php 0000644 00000017015 15174710275 0017474 0 ustar 00 <?php namespace WPForms\Integrations\ConstantContact\V3\Settings; use WPForms\Integrations\ConstantContact\V3\ConstantContact; /** * Class FieldMapping. * * @since 1.9.3 */ class FieldMapping { /** * Connection data. * * @since 1.9.3 * * @var array */ private $connection; /** * Submitted fields. * * @since 1.9.3 * * @var array */ private $fields; /** * Constructor. * * @since 1.9.3 * * @param array $connection Connection data. * @param array $fields Fields data. */ public function __construct( array $connection, array $fields ) { $this->connection = $connection; $this->fields = $fields; } /** * Get a list ID. * * @since 1.9.3 * * @return string */ public function get_list_id(): string { return $this->connection['list'] ?? ''; } /** * Get field value. * * @since 1.9.3 * * @param string $connection_key Connection key. */ public function get_field( string $connection_key ): string { if ( ! isset( $this->connection[ $connection_key ], $this->fields[ $this->connection[ $connection_key ] ]['value'] ) ) { return ''; } $limit = $connection_key === 'opt_out_reason' ? 255 : 50; return $this->trim_value( (string) $this->fields[ $this->connection[ $connection_key ] ]['value'], $limit ); } /** * Get field value from connection custom fields. * * @since 1.9.3 * * @param string $connection_key Connection key. * * @return string */ public function get_meta_field( string $connection_key ): string { $field_id_full = $this->get_field_meta_id( $connection_key ); $limit = $connection_key === 'phone' ? 25 : 50; return $this->trim_value( $this->get_field_value( $field_id_full ), $limit ); } /** * Get field value by ID. * * @since 1.9.3 * * @param string $field_id Field ID. Can be integer or string in the {field_id}.{subfield} format. * * @return string */ private function get_field_value( string $field_id ): string { $field_parts = explode( '.', $field_id ); $field_id = $field_parts[0]; $field_key = $field_parts[1] ?? 'value'; if ( $field_key === 'full' ) { $field_key = 'value'; } return $this->fields[ $field_id ][ $field_key ] ?? ''; } /** * Get connection custom fields. * * @since 1.9.3 * * @return array */ private function get_connection_custom_fields(): array { if ( empty( $this->connection['fields_meta'] ) ) { return []; } $predefined_custom_fields = ConstantContact::get_predefined_custom_fields(); $fields_meta = []; foreach ( $this->connection['fields_meta'] as $field ) { if ( ! isset( $field['name'], $field['field_id'] ) ) { continue; } if ( in_array( $field['name'], $predefined_custom_fields, true ) ) { continue; } $fields_meta[ $field['name'] ] = $field['field_id']; } return $fields_meta; } /** * Get a list of CC custom fields. * * @since 1.9.3 * * @param array $custom_fields_formats Constant Contact custom fields formats. * * @return array */ public function get_custom_fields( array $custom_fields_formats ): array { $fields_meta = $this->get_connection_custom_fields(); $custom_fields = []; foreach ( $fields_meta as $custom_field_id => $field_id ) { $field_format = $custom_fields_formats[ $custom_field_id ] ?? 'string'; $value = $this->get_custom_field_value( (string) $field_id, $field_format ); if ( wpforms_is_empty_string( $value ) ) { continue; } $custom_fields[] = [ 'custom_field_id' => $custom_field_id, 'value' => $this->trim_value( $value, 255 ), ]; } return $custom_fields; } /** * Get a custom field value. * * @since 1.9.3 * * @param string $field_id Field ID. * @param string $field_format Constant Contact custom field format. * * @return string */ private function get_custom_field_value( string $field_id, string $field_format ): string { if ( $field_format !== 'date' ) { return $this->trim_value( $this->get_field_value( $field_id ), 255 ); } $field = $this->fields[ $field_id ] ?? []; // Only Date / Time field is allowed to be sent as a date custom field format. if ( empty( $field['unix'] ) ) { return ''; } return (string) gmdate( 'm/d/Y', $field['unix'] ); } /** * Get street address from connection data. * * @since 1.9.3 * * @return array */ public function get_street_address(): array { $field_id = $this->get_field_meta_id( 'address' ); if ( empty( $field_id ) || empty( $this->fields[ $field_id ] ) ) { return []; } $address_fields = $this->build_address_fields( $this->fields[ $field_id ] ); return $this->is_valid_address( $address_fields ) ? $address_fields : []; } /** * Get meta field ID. * * @since 1.9.3 * * @param string $connection_key Connection key. * * @return string */ private function get_field_meta_id( string $connection_key ): string { $fields = wp_list_pluck( $this->connection['fields_meta'], 'field_id', 'name' ); return $fields[ $connection_key ] ?? ''; } /** * Get address kind. * * @since 1.9.3 * * @param array $address Address data. * * @return string */ private function get_address_kind( array $address ): string { $default_kind = 'other'; /** * Kind of address to be saved in the Constant Contact account. * * Possible values are 'other', 'home', 'work'. * * @since 1.9.3 * * @param array $default_kind Default kind of address, possible values are 'other', 'home', 'work'. * @param array $address Address data. * @param FieldMapping $field_mapping Instance of the FieldMapping class. * * @return string Default value is 'other'. */ $kind = apply_filters( 'wpforms_integrations_constant_contact_v3_settings_field_mapping_get_address_kind', $default_kind, $address, $this ); if ( in_array( $kind, [ $default_kind, 'home', 'work' ], true ) ) { return $kind; } return $default_kind; } /** * Get address street. * * @since 1.9.3 * * @param array $address Address data. * * @return string */ private function get_address_street( array $address ): string { $street = $address['address1'] ?? ''; return ! empty( $address['address2'] ) ? $street . ' ' . $address['address2'] : $street; } /** * Build address fields. * * @since 1.9.3 * * @param array $address Address data. * * @return array */ private function build_address_fields( array $address ): array { return [ 'kind' => $this->get_address_kind( $address ), 'street' => $this->trim_value( $this->get_address_street( $address ), 255 ), 'city' => $this->trim_value( $address['city'] ?? '' ), 'state' => $this->trim_value( $address['state'] ?? '' ), 'postal_code' => $this->trim_value( $address['postal'] ?? '' ), 'country' => $this->trim_value( $address['country'] ?? '' ), ]; } /** * Check if the address is valid. * * @since 1.9.3 * * @param array $address_fields Address fields. * * @return bool */ private function is_valid_address( array $address_fields ): bool { $filtered = array_filter( $address_fields ); return count( $filtered ) > 1; } /** * Trim value to the specified length. * * @see https://v3.developer.constantcontact.com/api_reference/index.html#!/Contacts/createOrUpdateContact * * @since 1.9.3 * * @param string $value Value to trim. * @param int $length Length to trim to. * * @return string */ private function trim_value( string $value, int $length = 50 ): string { return wp_html_excerpt( $value, $length ); } } Integrations/ConstantContact/V3/Settings/PageIntegrations.php 0000644 00000005310 15174710275 0020373 0 ustar 00 <?php namespace WPForms\Integrations\ConstantContact\V3\Settings; use WPForms\Providers\Provider\Settings\PageIntegrations as PageIntegrationsAbstract; use WPForms\Providers\Provider\Core; /** * Class PageIntegrations. * * @since 1.9.3 */ class PageIntegrations extends PageIntegrationsAbstract { /** * Constructor. * * @since 1.9.3 * * @param Core $core Provider core class. */ public function __construct( Core $core ) { parent::__construct( $core ); $this->hooks(); } /** * Hooks. * * @since 1.9.3 */ private function hooks() { add_action( 'wpforms_providers_provider_settings_page_integrations_display_connected_account_item_before', [ $this, 'display_re_auth' ], 10, 2 ); } /** * Display reauthorization notice. * * @since 1.9.3 * * @param string $account_id Account ID. * @param array $account Account data. * * @noinspection PhpMissingParamTypeInspection * @noinspection PhpUnusedParameterInspection */ public function display_re_auth( $account_id, $account ) { if ( empty( $account['accounts'] ) || empty( $account['email'] ) ) { return; } ?> <div class="wpforms-alert wpforms-alert-danger wpforms-alert-dismissible"> <div class="wpforms-alert-message"> <p> <?php esc_html_e( 'Your Constant Contact account connection has expired. Please reconnect your account.', 'wpforms-lite' ); ?> </p> </div> <div class="wpforms-alert-buttons wpforms-alert-buttons-constant-contact-v3"> <a class="wpforms-btn wpforms-btn-md wpforms-btn-light-grey wpforms-constant-contact-v3-auth" href="#" data-login-hint="<?php echo esc_attr( $account['email'] ); ?>"> <i class="fa fa-repeat"></i> <?php esc_html_e( 'Reconnect Account', 'wpforms-lite' ); ?> </a> </div> </div> <?php } /** * Display Constants Contact V3 integrations tab. * * @since 1.9.3 * * @noinspection HtmlUnknownTarget */ protected function display_add_new() { ?> <p> <a class="wpforms-btn wpforms-btn-md wpforms-btn-light-grey wpforms-constant-contact-v3-auth" href="#"> <i class="fa fa-plus"></i> <?php esc_html_e( 'Add New Account', 'wpforms-lite' ); ?> </a> </p> <p> <?php printf( '<a href="%1$s" target="_blank" rel="noopener noreferrer" class="secondary-text">%2$s</a>', // @todo: confirm the link. // @see: https://github.com/awesomemotive/wpforms-plugin/issues/12504 esc_url( wpforms_utm_link( 'https://wpforms.com/docs/how-to-connect-constant-contact-with-wpforms/', 'Settings - Integration', 'ConstantContact V3 Documentation' ) ), esc_html__( 'Click here for documentation on connecting WPForms with Constant Contact.', 'wpforms-lite' ) ); ?> </p> <?php } } Integrations/ConstantContact/V3/Process.php 0000644 00000015143 15174710275 0014753 0 ustar 00 <?php namespace WPForms\Integrations\ConstantContact\V3; use Exception; use RuntimeException; use WPForms\Tasks\Meta; use WPForms\Integrations\ConstantContact\V3\Api\Api; use WPForms\Providers\Provider\Process as ProcessAbstract; use WPForms\Integrations\ConstantContact\V3\Settings\FieldMapping; /** * Class Process. * * @since 1.9.3 */ class Process extends ProcessAbstract { /** * Async task name. * * @since 1.9.3 */ const TASK_NAME = 'wpforms_constant_contact_v3_process'; /** * Connection data. * * @since 1.9.3 * * @var array */ private $connection; /** * API client. * * @since 1.9.3 * * @var Api */ private $api; /** * Process constructor. * * @since 1.9.3 * * @param Core $core Core instance of the provider class. */ public function __construct( Core $core ) { parent::__construct( $core ); $this->hooks(); } /** * Register hooks. * * @since 1.9.3 */ public function hooks() { add_action( self::TASK_NAME, [ $this, 'task_async_action_trigger' ] ); } /** * Process the form. * * @since 1.9.3 * * @param array $fields Submitted fields. * @param array $entry Saved entry data. * @param array $form_data Form data and settings. * @param int $entry_id Saved entry ID. */ public function process( $fields, $entry, $form_data, $entry_id ) { if ( empty( $form_data['providers'][ $this->core->slug ] ) ) { return; } $this->fields = $fields; $this->entry = $entry; $this->form_data = $form_data; $this->entry_id = $entry_id; foreach ( $this->form_data['providers'][ $this->core->slug ] as $connection ) { $this->connection = $connection; if ( ! $this->process_conditionals( $this->fields, $this->form_data, $connection ) ) { $this->log_errors( sprintf( 'The Constant Contact connection %s was not processed due to conditional logic.', $connection['name'] ?? '' ) ); continue; } if ( empty( $this->connection['action'] ) ) { continue; } $this->create_connection_async_task(); } } /** * Create an async task for a specific connection. * * @since 1.9.3 */ private function create_connection_async_task() { $tasks = wpforms()->obj( 'tasks' ); if ( ! $tasks ) { return; } $tasks ->create( self::TASK_NAME )->async() ->params( $this->connection, $this->fields, $this->form_data, $this->entry_id ) ->register(); } /** * Process the addon async tasks. * * @since 1.9.3 * * @param int|mixed $meta_id Task meta ID. */ public function task_async_action_trigger( $meta_id ) { $meta = $this->get_task_meta( (int) $meta_id ); // We expect a certain type and number of params. if ( count( $meta ) !== 4 ) { return; } // We expect a certain metadata structure for this task. list( $this->connection, $this->fields, $this->form_data, $this->entry_id ) = $meta; try { $this->process_action(); } catch ( Exception $e ) { $this->log_errors( $e->getMessage() ); } } /** * Processes single action. * * @since 1.9.3 * * @throws Exception If something went wrong. * * @uses Api::unsubscribe_contact() * @uses Api::delete_contact() * @uses Api::subscribe_contact() */ private function process_action() { $this->api = $this->get_api_client(); $contact_data = $this->prepare_contact_data(); $api_method = $this->connection['action'] . '_contact'; if ( ! method_exists( $this->api, $api_method ) ) { return; } $response = $this->api->$api_method( $contact_data ); /** * Fire when request was sent successfully or not. * * @since 1.9.3 * * @param array $response Response data. * @param array $connection Connection data. * @param array $args Additional arguments. */ do_action( 'wpforms_integrations_constant_contact_v3_process_completed', $response, $this->connection, [ 'form_data' => $this->form_data, 'fields' => $this->fields, 'entry' => $this->entry, ] ); } /** * Prepare contact data. * * @since 1.9.3 * * @return array */ private function prepare_contact_data(): array { $field_mapping = new FieldMapping( $this->connection, $this->fields ); if ( $this->connection['action'] === 'subscribe' ) { return array_filter( [ 'email_address' => $field_mapping->get_field( 'email' ), 'first_name' => $field_mapping->get_meta_field( 'first_name' ), 'last_name' => $field_mapping->get_meta_field( 'last_name' ), 'job_title' => $field_mapping->get_meta_field( 'job_title' ), 'company_name' => $field_mapping->get_meta_field( 'company_name' ), 'phone_number' => $field_mapping->get_meta_field( 'phone' ), 'street_address' => $field_mapping->get_street_address(), 'list_memberships' => [ $field_mapping->get_list_id() ], 'custom_fields' => $field_mapping->get_custom_fields( $this->api->get_custom_fields( 'type', 'custom_field_id' ) ), ] ); } if ( $this->connection['action'] === 'unsubscribe' ) { return [ 'email_address' => $field_mapping->get_field( 'email' ), 'opt_out_reason' => $field_mapping->get_field( 'opt_out_reason' ), ]; } return [ 'email_address' => $field_mapping->get_field( 'email' ), ]; } /** * Get task meta data. * * @since 1.9.3 * * @param int $meta_id Task meta ID. * * @return array */ private function get_task_meta( int $meta_id ): array { $task_meta = new Meta(); $meta = $task_meta->get( $meta_id ); // We should actually receive something. if ( empty( $meta ) || empty( $meta->data ) ) { return []; } return (array) $meta->data; } /** * Get the API client based on connection and provider options. * * @since 1.9.3 * * @return Api * * @throws RuntimeException If account ID is missing or account doesn't exist. */ private function get_api_client(): Api { if ( empty( $this->connection['account_id'] ) ) { throw new RuntimeException( 'Account ID is missing in connection.' ); } $provider_settings = wpforms_get_providers_options( $this->core->slug ); return new Api( $provider_settings[ $this->connection['account_id'] ] ?? [] ); } /** * Log an API-related error with all the data. * * @since 1.9.3 * * @param string $error_message Error message. */ private function log_errors( string $error_message ) { wpforms_log( 'Submission Constant Contact failed (#' . $this->entry_id . ')', [ 'message' => $error_message, 'connection' => $this->connection, ], [ 'type' => [ 'provider', 'error' ], 'parent' => $this->entry_id, 'form_id' => $this->form_data['id'], ] ); } } Integrations/WooCommerce/Notifications.php 0000644 00000012405 15174710275 0014766 0 ustar 00 <?php namespace WPForms\Integrations\WooCommerce; use WPForms\Integrations\IntegrationInterface; /** * Class Notifications for WooCommerce integration. * * @since 1.8.9 */ class Notifications implements IntegrationInterface { /** * Assets handle. * * @since 1.8.9 * * @var string Handle. */ const HANDLE = 'wpforms-woocommerce-notifications'; /** * Option name to store the dismissed state. * * @since 1.8.9 * * @var string Option name. */ const OPTION_NAME = 'wpforms_woocommerce_notifications_dismissed'; /** * Indicate if current integration is allowed to load. * * @since 1.8.9 * * @return bool */ public function allow_load() { // Check if WooCommerce is not installed and active. if ( ! class_exists( 'woocommerce' ) ) { return false; } // Do not show the notification if it was dismissed before. if ( get_option( self::OPTION_NAME ) ) { return false; } // Allow to load when the notification is being dismissed via AJAX. // phpcs:ignore WordPress.Security.NonceVerification.Missing if ( wpforms_is_admin_ajax() && isset( $_POST['action'] ) && $_POST['action'] === 'wpforms_woocommerce_dismiss' ) { return true; } // Load only on an WooCommerce Settings > Emails page. if ( ! $this->is_woocommerce_email_settings_page() ) { return false; } // Do not show the notification if any SMTP plugin is active. return ! $this->has_smtp_plugin(); } /** * Load integration. * * @since 1.8.9 */ public function load() { $this->hooks(); } /** * Register hooks. * * @since 1.8.9 */ private function hooks() { add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ], 20 ); add_action( 'woocommerce_admin_field_email_notification' , [ $this, 'add_notification' ] ); add_action( 'wp_ajax_wpforms_woocommerce_dismiss', [ $this, 'dismiss' ] ); } /** * Enqueue assets. * * @since 1.8.9 */ public function enqueue_assets() { $min = wpforms_get_min_suffix(); wp_enqueue_style( self::HANDLE, WPFORMS_PLUGIN_URL . "/assets/css/integrations/woocommerce/notifications{$min}.css", [], WPFORMS_VERSION ); wp_enqueue_script( self::HANDLE, WPFORMS_PLUGIN_URL . "/assets/js/integrations/woocommerce/notifications{$min}.js", [ 'jquery' ], WPFORMS_VERSION, true ); wp_localize_script( self::HANDLE, 'wpforms_woocommerce_notifications', [ 'ajax_url' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( self::HANDLE ), ] ); } /** * Add notification. * * @since 1.8.9 */ public function add_notification() { ?> <div class='wpforms-woocommerce-notification'> <div class='wpforms-woocommerce-notification-content'> <h2> <?php esc_html_e( 'Make Sure Important Emails Reach Your Customers', 'wpforms-lite' ); ?> </h2> <p> <?php esc_html_e( 'Solve common email deliverability issues for good.', 'wpforms-lite' ); ?> </p> <a href="<?php echo esc_url( admin_url( 'admin.php?page=wpforms-smtp&source=woocommerce' ) ); ?>" class='button button-primary'> <?php esc_html_e( 'Get WP Mail SMTP', 'wpforms-lite' ); ?> </a> </div> <div class='wpforms-woocommerce-notification-image'></div> <i class='dashicons dashicons-no-alt' id='wpforms-woocommerce-close' title="<?php esc_attr_e( 'Close the notification', 'wpforms-lite' ); ?>"></i> </div> <?php } /** * Dismiss notification. * * @since 1.8.9 */ public function dismiss() { // Run a security check. check_ajax_referer( self::HANDLE, 'nonce' ); // Check for permissions. if ( ! current_user_can( 'install_plugins' ) ) { wp_send_json_error(); } update_option( self::OPTION_NAME, true ); wp_send_json_success(); } /** * Check if the current page is WooCommerce Settings > Emails page. * * @since 1.8.9 * * @return bool */ private function is_woocommerce_email_settings_page(): bool { // phpcs:ignore WordPress.Security.NonceVerification.Recommended return isset( $_GET['page'], $_GET['tab'] ) && $_GET['page'] === 'wc-settings' && $_GET['tab'] === 'email'; } /** * Check if the site has any active SMTP plugins. * * @since 1.8.9 * * @return bool */ private function has_smtp_plugin(): bool { $smtp_plugins = [ 'wp-mail-smtp-pro/wp_mail_smtp.php', 'wp-mail-smtp/wp_mail_smtp.php', 'easy-wp-smtp/easy-wp-smtp.php', 'smtp-settings-for-gravity-forms/smtp-settings-gravity-forms.php', 'post-smtp/postman-smtp.php', 'fluent-smtp/fluent-smtp.php', 'gosmtp/gosmtp.php', 'smtp-mailer/main.php', 'wp-smtp/wp-smtp.php', 'gmail-smtp/main.php', 'simple-smtp/wp-simple-smtp.php', 'bws-smtp/bws-smtp.php', 'wp-mail-smtp-mailer/wp-mail-smtp-mailer.php', 'welcome-email-editor/sb_welcome_email_editor.php', 'bit-smtp/bit_smtp.php', 'sar-friendly-smtp/sar-friendly-smtp.php', 'smtp-mailer/main.php', 'yaysmtp/yay-smtp.php', 'smtp2go/smtp2go-wordpress-plugin.php', 'mailersend-official-smtp-integration/mailersend-wordpress.php', 'cf7-smtp/cf7-smtp.php', 'smtp-mail/index.php', 'mailpoet/mailpoet.php', ]; foreach ( $smtp_plugins as $plugin ) { // Check if plugin is active or installed. if ( is_plugin_active( $plugin ) || file_exists( WP_PLUGIN_DIR . '/' . dirname( $plugin ) ) ) { return true; } } return false; } } Integrations/Elementor/Elementor.php 0000644 00000032604 15174710275 0013625 0 ustar 00 <?php // phpcs:disable Generic.Commenting.DocComment.MissingShort /** @noinspection PhpUndefinedNamespaceInspection */ /** @noinspection PhpUndefinedClassInspection */ // phpcs:enable Generic.Commenting.DocComment.MissingShort namespace WPForms\Integrations\Elementor; use Elementor\Controls_Manager; use Elementor\Plugin as ElementorPlugin; use WPForms\Admin\Education\StringsTrait; use WPForms\Frontend\CSSVars; use WPForms\Integrations\IntegrationInterface; use WPForms\Lite\Integrations\Elementor\ThemesData; /** * Improve Elementor Compatibility. * * @since 1.6.0 */ class Elementor implements IntegrationInterface { use StringsTrait; /** * Rest API class instance. * * @since 1.9.6 * * @var RestApi */ protected $rest_api_obj; /** * ThemesData class instance. * * @since 1.9.6 * * @var ThemesData */ protected $themes_data_obj; /** * Indicates if the current integration is allowed to load. * * @since 1.6.0 * * @return bool */ public function allow_load() { return (bool) did_action( 'elementor/loaded' ); } /** * Load an integration. * * @since 1.6.0 */ public function load() { $this->themes_data_obj = new ThemesData(); $this->hooks(); } /** * Integration hooks. * * @since 1.6.0 * * @noinspection PhpUndefinedConstantInspection */ protected function hooks() { // Skip if Elementor is not available. if ( ! class_exists( '\Elementor\Plugin' ) ) { return; } add_action( 'elementor/preview/init', [ $this, 'init' ] ); add_action( 'elementor/frontend/after_enqueue_scripts', [ $this, 'preview_assets' ] ); add_action( 'elementor/frontend/after_enqueue_scripts', [ $this, 'frontend_assets' ] ); add_action( 'elementor/editor/after_enqueue_styles', [ $this, 'editor_assets' ] ); add_action( 'elementor/controls/register', [ $this, 'register_controls' ] ); version_compare( ELEMENTOR_VERSION, '3.5.0', '>=' ) ? add_action( 'elementor/widgets/register', [ $this, 'register_widget' ] ) : add_action( 'elementor/widgets/widgets_registered', [ $this, 'register_widget' ] ); add_action( 'wp_ajax_wpforms_admin_get_form_selector_options', [ $this, 'ajax_get_form_selector_options' ] ); add_filter( 'wpforms_integrations_gutenberg_form_selector_allow_render', [ $this, 'disable_gutenberg_block_render' ] ); add_filter( 'wpforms_forms_anti_spam_v3_is_honeypot_enabled', [ $this, 'filter_is_honeypot_enabled' ] ); add_action( 'rest_api_init', [ $this, 'init_rest' ] ); } /** * Initialize rest API. * * @since 1.9.6 */ public function init_rest(): void { if ( ! $this->rest_api_obj ) { $this->rest_api_obj = new RestApi( $this, $this->themes_data_obj ); } } /** * Register Elementor controls. * * @since 1.9.6 * * @param Controls_Manager $controls_manager Elementor controls manager. */ public function register_controls( Controls_Manager $controls_manager ): void { require_once WPFORMS_PLUGIN_DIR . 'src/Integrations/Elementor/Controls/WPFormsThemes.php'; $controls_manager->register( new Controls\WPFormsThemes() ); } /** * Init the main logic. * * @since 1.6.0 */ public function init(): void { // phpcs:ignore WPForms.PHP.HooksMethod.InvalidPlaceForAddingHooks /** * Allow developers to determine whether the compatibility layer should be applied. * We do this check here because we want this filter to be available for theme developers too. * * @since 1.6.0 * * @param bool $use_compat Use compatibility. */ $use_compat = (bool) apply_filters( 'wpforms_apply_elementor_preview_compat', true ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName if ( $use_compat !== true ) { return; } // Load WPForms assets globally on the Elementor Preview panel only. add_filter( 'wpforms_global_assets', '__return_true' ); // Hide CAPTCHA badge on Elementor Preview panel. add_filter( 'wpforms_frontend_recaptcha_disable', '__return_true' ); } /** * Load assets in the preview panel. * * @since 1.6.2 */ public function preview_assets() { if ( ! ElementorPlugin::$instance->preview->is_preview_mode() ) { return; } $min = wpforms_get_min_suffix(); // jQuery.Confirm Reloaded. wp_enqueue_style( 'jquery-confirm', WPFORMS_PLUGIN_URL . 'assets/lib/jquery.confirm/jquery-confirm.min.css', null, '1.0.0' ); wp_enqueue_script( 'jquery-confirm', WPFORMS_PLUGIN_URL . 'assets/lib/jquery.confirm/jquery-confirm.min.js', [ 'jquery' ], '1.0.0', false ); wp_enqueue_style( 'wpforms-font-awesome', WPFORMS_PLUGIN_URL . 'assets/lib/font-awesome/css/all.min.css', null, '7.0.1' ); // FontAwesome v4 compatibility shims. wp_enqueue_style( 'wpforms-font-awesome-v4-shim', WPFORMS_PLUGIN_URL . 'assets/lib/font-awesome/css/v4-shims.min.css', null, '4.7.0' ); wp_enqueue_style( 'wpforms-integrations', WPFORMS_PLUGIN_URL . "assets/css/admin-integrations{$min}.css", null, WPFORMS_VERSION ); wp_enqueue_script( 'wpforms-elementor', WPFORMS_PLUGIN_URL . "assets/js/integrations/elementor/editor{$min}.js", [ 'elementor-frontend', 'jquery', 'wp-util', 'wpforms', 'jquery-confirm', 'wp-api-fetch' ], WPFORMS_VERSION, true ); if ( $this->is_modern_widget() ) { wp_enqueue_script( 'wpforms-generic-utils', WPFORMS_PLUGIN_URL . "assets/js/share/utils{$min}.js", [ 'jquery' ], WPFORMS_VERSION, true ); wp_enqueue_script( 'wpforms-elementor-modern', WPFORMS_PLUGIN_URL . "assets/js/integrations/elementor/editor-modern{$min}.js", [ 'wpforms-elementor', 'wpforms-generic-utils' ], WPFORMS_VERSION, true ); } wp_enqueue_script( 'wpforms-elementor-themes', WPFORMS_PLUGIN_URL . "assets/js/integrations/elementor/themes{$min}.js", [ 'wpforms-elementor-modern' ], WPFORMS_VERSION, true ); // Define strings for JS. $strings = [ 'heads_up' => esc_html__( 'Heads up!', 'wpforms-lite' ), 'cancel' => esc_html__( 'Cancel', 'wpforms-lite' ), 'copy_paste_error' => esc_html__( 'There was an error parsing your JSON code. Please check your code and try again.', 'wpforms-lite' ), 'button_background' => esc_html__( 'Button Background', 'wpforms-lite' ), 'button_text' => esc_html__( 'Button Text', 'wpforms-lite' ), 'field_label' => esc_html__( 'Field Label', 'wpforms-lite' ), 'field_sublabel' => esc_html__( 'Field Sublabel', 'wpforms-lite' ), 'field_border' => esc_html__( 'Field Border', 'wpforms-lite' ), 'theme_delete_title' => esc_html__( 'Delete Form Theme', 'wpforms-lite' ), // Translators: %1$s: Theme name. 'theme_delete_confirm' => esc_html__( 'Are you sure you want to delete the %1$s theme?', 'wpforms-lite' ), 'theme_delete_cant_undone' => esc_html__( 'This cannot be undone.', 'wpforms-lite' ), 'theme_delete_yes' => esc_html__( 'Yes, Delete', 'wpforms-lite' ), 'theme_copy' => esc_html__( 'Copy', 'wpforms-lite' ), 'theme_custom' => esc_html__( 'Custom Theme', 'wpforms-lite' ), 'theme_noname' => esc_html__( 'Noname Theme', 'wpforms-lite' ), 'form_themes' => esc_html__( 'Themes', 'wpforms-lite' ), 'themes_error' => esc_html__( 'Error loading themes. Please try again later.', 'wpforms-lite' ), 'upgrade_button' => esc_html__( 'Upgrade to Pro', 'wpforms-lite' ), 'license_message' => esc_html__( 'To access the %name%, please enter and activate your WPForms license key in the plugin settings.', 'wpforms-lite' ), 'license_button' => esc_html__( 'Enter License Key', 'wpforms-lite' ), 'license_url' => esc_url( admin_url( 'admin.php?page=wpforms-settings' ) ), 'pro_sections' => [ 'background' => esc_html__( 'Background Styles', 'wpforms-lite' ), 'container' => esc_html__( 'Container Styles', 'wpforms-lite' ), ], ]; /** * Filter the strings passed to the Elementor editor script. * * @since 1.9.6 * * @param array $strings Array of strings to be filtered. */ $strings = apply_filters( 'wpforms_integrations_elementor_editor_strings', $strings ); /** * Filter the variables passed to an Elementor editor script. * * @since 1.9.6 * * @param array $vars Array of variables to be filtered. */ $vars = apply_filters( 'wpforms_integrations_elementor_editor_vars', [ 'ajax_url' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'wpforms-elementor-integration' ), 'edit_form_url' => admin_url( 'admin.php?page=wpforms-builder&view=fields&form_id=' ), 'add_form_url' => admin_url( 'admin.php?page=wpforms-builder&view=setup' ), 'css_url' => WPFORMS_PLUGIN_URL . "assets/css/admin-integrations{$min}.css", 'debug' => wpforms_debug(), 'isPro' => wpforms()->is_pro(), 'isAdmin' => current_user_can( 'manage_options' ), 'is_modern_markup' => wpforms_get_render_engine() === 'modern', 'is_full_styling' => (int) wpforms_setting( 'disable-css', '1' ) === 1, 'route_namespace' => RestApi::ROUTE_NAMESPACE, 'strings' => $strings, 'sizes' => [ 'field-size' => CSSVars::FIELD_SIZE, 'label-size' => CSSVars::LABEL_SIZE, 'button-size' => CSSVars::BUTTON_SIZE, 'container-shadow-size' => CSSVars::CONTAINER_SHADOW_SIZE, ], ] ); wp_localize_script( 'wpforms-elementor', 'wpformsElementorVars', $vars ); } /** * Load an integration assets on the frontend. * * @since 1.6.2 */ public function frontend_assets(): void { if ( ElementorPlugin::$instance->preview->is_preview_mode() ) { return; } $min = wpforms_get_min_suffix(); wp_register_script( 'wpforms-elementor', WPFORMS_PLUGIN_URL . "assets/js/integrations/elementor/frontend{$min}.js", [ 'elementor-frontend', 'jquery', 'wp-util', 'wpforms' ], WPFORMS_VERSION, true ); wp_localize_script( 'wpforms-elementor', 'wpformsElementorVars', [ 'captcha_provider' => wpforms_setting( 'captcha-provider', 'recaptcha' ), 'recaptcha_type' => wpforms_setting( 'recaptcha-type', 'v2' ), ] ); } /** * Load assets in the elementor document. * * @since 1.6.2 */ public function editor_assets() { if ( empty( $_GET['action'] ) || $_GET['action'] !== 'elementor' ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended return; } $min = wpforms_get_min_suffix(); wp_enqueue_style( 'wpforms-integrations', WPFORMS_PLUGIN_URL . "assets/css/admin-integrations{$min}.css", null, WPFORMS_VERSION ); // Choices.js. wp_enqueue_script( 'choicesjs', WPFORMS_PLUGIN_URL . 'assets/lib/choices.min.js', [], '10.2.0', false ); wp_enqueue_script( 'wpforms-admin-education-core', WPFORMS_PLUGIN_URL . "assets/js/admin/education/core{$min}.js", [ 'jquery' ], WPFORMS_VERSION, true ); wp_enqueue_script( 'wpforms-elementor-modern', WPFORMS_PLUGIN_URL . "assets/js/integrations/elementor/editor-context{$min}.js", [ 'jquery' ], WPFORMS_VERSION, true ); wp_localize_script( 'wpforms-admin-education-core', 'wpforms_education', $this->get_js_strings() ); } /** * Register WPForms Widget. * * @since 1.6.2 * @since 1.7.6 Added support for the new registration method since 3.5.0. * @since 1.8.3 Added a condition for selecting the required widget instance. * * @noinspection PhpUndefinedConstantInspection */ public function register_widget(): void { $widget_instance = $this->is_modern_widget() ? new WidgetModern() : new Widget(); version_compare( ELEMENTOR_VERSION, '3.5.0', '>=' ) ? ElementorPlugin::instance()->widgets_manager->register( $widget_instance ) : ElementorPlugin::instance()->widgets_manager->register_widget_type( $widget_instance ); } /** * Get form selector options. * * @since 1.6.2 */ public function ajax_get_form_selector_options(): void { check_ajax_referer( 'wpforms-elementor-integration', 'nonce' ); wp_send_json_success( ( new Widget() )->get_form_selector_options() ); } /** * Detect modern Widget. * * @since 1.8.3 */ protected function is_modern_widget(): bool { return wpforms_get_render_engine() === 'modern' && (int) wpforms_setting( 'disable-css', '1' ) === 1; } /** * Disable the block render for pages built in Elementor. * * @since 1.8.8 * * @param bool|mixed $allow_render Whether to allow the block render. * * @return bool Whether to disable the block render. */ public function disable_gutenberg_block_render( $allow_render ): bool { $allow_render = (bool) $allow_render; $document = ElementorPlugin::$instance->documents->get( get_the_ID() ); if ( $document && $document->is_built_with_elementor() ) { return false; } return $allow_render; } /** * Disable honeypot on the preview panel. * * @since 1.9.0 * * @param bool|mixed $is_enabled True if the honeypot is enabled, false otherwise. * * @return bool Whether to disable the honeypot. */ public function filter_is_honeypot_enabled( $is_enabled ): bool { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $action = sanitize_key( $_REQUEST['action'] ?? '' ); if ( in_array( $action, [ 'elementor', 'elementor_ajax' ], true ) || ElementorPlugin::$instance->preview->is_preview_mode() ) { return false; } return (bool) $is_enabled; } } Integrations/Elementor/ThemesData.php 0000644 00000012014 15174710275 0013703 0 ustar 00 <?php namespace WPForms\Integrations\Elementor; use WPForms\Helpers\File; /** * ThemesData class for Elementor Modern widget. * * @since 1.9.6 */ abstract class ThemesData { /** * Custom themes JSON file path. * * Relative to `wp-content/uploads/wpforms` directory. * * @since 1.9.6 * * @var string */ private const THEMES_CUSTOM_JSON_PATH = 'themes/elementor/themes-custom.json'; /** * WPForms themes JSON file path for the lite version. * * Relative to the WPForms plugin directory. * * @since 1.9.6 * * @var string */ protected const THEMES_WPFORMS_JSON_PATH_LITE = 'assets/lite/js/integrations/elementor/themes.json'; /** * Custom themes file path. * * @since 1.9.6 * * @var string */ private $custom_themes_file_path; /** * WPForms themes data. * * @since 1.9.6 * * @var array */ protected $wpforms_themes; /** * Custom themes data. * * @since 1.9.6 * * @var array */ private $custom_themes; /** * Return WPForms themes. * * @since 1.9.6 * * @return array */ public function get_wpforms_themes(): array { if ( $this->wpforms_themes !== null ) { return $this->wpforms_themes; } $themes_json = File::get_contents( WPFORMS_PLUGIN_DIR . static::THEMES_WPFORMS_JSON_PATH ) ?? '{}'; $themes = json_decode( $themes_json, true ); $this->wpforms_themes = ! empty( $themes ) ? $themes : []; return $this->wpforms_themes; } /** * Return custom themes. * * @since 1.9.6 * * @return array */ public function get_custom_themes(): array { if ( $this->custom_themes !== null ) { return $this->custom_themes; } $themes_json = File::get_contents( $this->get_custom_themes_file_path() ) ?? '{}'; $themes = json_decode( $themes_json, true ); $this->custom_themes = ! empty( $themes ) ? $themes : []; return $this->custom_themes; } /** * Return theme data. * * @since 1.9.6 * * @param string $slug Theme slug. * * @return array|null */ public function get_theme( string $slug ): ?array { $wpforms = $this->get_wpforms_themes(); if ( ! empty( $wpforms[ $slug ] ) ) { return $wpforms[ $slug ]; } $custom = $this->get_custom_themes(); if ( ! empty( $custom[ $slug ] ) ) { return $custom[ $slug ]; } return null; } /** * Get custom themes JSON file path. * * @since 1.9.6 * * @return string|bool File path OR false in the case of permissions error. */ public function get_custom_themes_file_path() { // Caching the file path in the class property. if ( $this->custom_themes_file_path !== null ) { return $this->custom_themes_file_path; } // Determine a custom themes file path. $upload_dir = wpforms_upload_dir(); $upload_path = ! empty( $upload_dir['path'] ) ? $upload_dir['path'] : WP_CONTENT_DIR . 'uploads/wpforms/'; $upload_path = trailingslashit( wp_normalize_path( $upload_path ) ); $file_path = $upload_path . self::THEMES_CUSTOM_JSON_PATH; $dirname = dirname( $file_path ); // If the directory doesn't exist, create it. Also, check for permissions. if ( ! wp_mkdir_p( $dirname ) ) { $file_path = false; } $this->custom_themes_file_path = $file_path; return $file_path; } /** * Sanitize custom themes data. * * @since 1.9.6 * * @param array $custom_themes Custom themes data. * * @return array */ private function sanitize_custom_themes_data( array $custom_themes ): array { $wpforms = $this->get_wpforms_themes(); $sanitized_themes = []; // Get the default theme settings. // If there are no default settings, use an empty array. This should never happen, but just in case. $default_theme = $wpforms['default'] ?? []; $default_theme['settings'] = $default_theme['settings'] ?? []; foreach ( $custom_themes as $slug => $theme ) { $slug = sanitize_key( $slug ); $sanitized_themes[ $slug ]['name'] = sanitize_text_field( $theme['name'] ?? 'Copy of ' . $default_theme['name'] ); // Fill in missed settings keys with default values. $settings = wp_parse_args( $theme['settings'] ?? [], $default_theme['settings'] ); // Make sure we will save only settings that are present in the default theme. $settings = array_intersect_key( $settings, $default_theme['settings'] ); // Sanitize settings. $sanitized_themes[ $slug ]['settings'] = array_map( 'sanitize_text_field', $settings ); } return $sanitized_themes; } /** * Update custom themes data. * * @since 1.9.6 * * @param array $custom_themes Custom themes data. * * @return bool */ public function update_custom_themes_file( array $custom_themes ): bool { // Sanitize custom themes data to be saved. $sanitized_themes = $this->sanitize_custom_themes_data( $custom_themes ); // Determine a custom themes file path. $themes_file = $this->get_custom_themes_file_path(); $json_data = ! empty( $sanitized_themes ) ? wp_json_encode( $sanitized_themes ) : '{}'; // Save custom themes data and return the result. return File::put_contents( $themes_file, $json_data ); } } Integrations/Elementor/WidgetModern.php 0000644 00000057773 15174710275 0014301 0 ustar 00 <?php // phpcs:disable Generic.Commenting.DocComment.MissingShort /** @noinspection PhpUndefinedNamespaceInspection */ /** @noinspection PhpUndefinedClassInspection */ /** @noinspection PhpUndefinedMethodInspection */ // phpcs:enable Generic.Commenting.DocComment.MissingShort namespace WPForms\Integrations\Elementor; use Elementor\Plugin; use Elementor\Controls_Manager; use Exception; use WPForms\Frontend\CSSVars; /** * WPForms modern widget for Elementor page builder. * * @since 1.8.3 */ class WidgetModern extends Widget { /** * Size options for widget settings. * * @since 1.8.3 * * @var array */ protected $size_options; /** * Border type options for widget settings. * * @since 1.9.6 * * @var array */ private $border_options; /** * Instance of CSSVars class. * * @since 1.8.3 * * @var CSSVars */ protected $css_vars_obj; /** * Widget constructor. * * @since 1.8.3 * * @param array $data Widget data. * @param array $args Widget arguments. * * @throws Exception If arguments are missing when initializing a full widget. * @noinspection PhpMissingParamTypeInspection */ public function __construct( $data = [], $args = null ) { parent::__construct( $data, $args ); $this->load(); } /** * Load widget. * * @since 1.8.3 */ private function load(): void { $this->size_options = [ 'small' => esc_html__( 'Small', 'wpforms-lite' ), 'medium' => esc_html__( 'Medium', 'wpforms-lite' ), 'large' => esc_html__( 'Large', 'wpforms-lite' ), ]; $this->border_options = [ 'none' => esc_html__( 'None', 'wpforms-lite' ), 'solid' => esc_html__( 'Solid', 'wpforms-lite' ), 'dashed' => esc_html__( 'Dashed', 'wpforms-lite' ), 'dotted' => esc_html__( 'Dotted', 'wpforms-lite' ), ]; $this->css_vars_obj = wpforms()->obj( 'css_vars' ); } /** * Register widget controls. * * Adds different input fields to allow the user to change and customize the widget settings. * * @since 1.8.3 */ protected function register_controls() { $this->content_controls(); $this->style_controls(); } /** * Register widget controls for the Style section. * * Adds different input fields into the "Style" section to allow the user to change and customize the widget style * settings. * * @since 1.8.3 */ private function style_controls(): void { $this->add_theme_style_controls(); if ( $this->is_admin() ) { $this->add_field_style_controls(); $this->add_label_style_controls(); $this->add_button_style_controls(); $this->add_container_style_controls(); $this->add_background_style_controls(); $this->add_other_style_controls(); } $this->add_advanced_style_controls(); } /** * Register widget controls for the Theme Style section. * * @since 1.9.6 */ private function add_theme_style_controls(): void { $this->start_controls_section( 'themes', [ 'label' => esc_html__( 'Themes', 'wpforms-lite' ), 'tab' => Controls_Manager::TAB_STYLE, ] ); $this->add_control( 'lead_forms_notice', [ 'show_label' => false, 'type' => Controls_Manager::RAW_HTML, 'raw' => sprintf( '<strong>%s</strong>%s', esc_html__( 'Form Styles are disabled because Lead Form Mode is turned on.', 'wpforms-lite' ), esc_html__( 'To change the styling for this form, open it in the form builder and edit the options in the Lead Forms settings.', 'wpforms-lite' ) ), 'classes' => 'wpforms-elementor-lead-forms-notice', 'content_classes' => 'elementor-panel-alert elementor-panel-alert-warning', ] ); $this->add_control( 'wpformsTheme', [ 'type' => 'wpforms_themes', 'default' => 'default', ] ); if ( $this->is_admin() ) { $this->add_control( 'isCustomTheme', [ 'type' => Controls_Manager::HIDDEN, ] ); $this->add_control( 'isMigrated', [ 'type' => Controls_Manager::HIDDEN, 'default' => 'false', ] ); $this->add_control( 'customThemeName', [ 'type' => Controls_Manager::TEXT, 'label' => esc_html__( 'Theme Name', 'wpforms-lite' ), 'ai' => [ 'active' => false, ], 'condition' => [ 'isCustomTheme!' => '', ], ] ); $this->add_control( 'deleteThemeButton', [ 'type' => Controls_Manager::BUTTON, 'event' => 'WPFormsDeleteThemeButtonClick', 'button_type' => 'danger', 'text' => esc_html__( 'DELETE THEME', 'wpforms-lite' ), 'condition' => [ 'isCustomTheme!' => '', ], ] ); } $this->end_controls_section(); } /** * Register widget controls for the Field Style section. * * Adds controls to the "Field Styles" section of the Widget Style settings. * * @since 1.8.3 */ protected function add_field_style_controls(): void { $this->start_controls_section( 'field_styles', [ 'label' => esc_html__( 'Field Styles', 'wpforms-lite' ), 'tab' => Controls_Manager::TAB_STYLE, ] ); $this->add_control( 'fieldSize', [ 'label' => esc_html__( 'Size', 'wpforms-lite' ), 'type' => Controls_Manager::SELECT, 'options' => $this->size_options, 'default' => 'medium', ] ); $this->add_control( 'fieldBorderStyle', [ 'label' => esc_html__( 'Border', 'wpforms-lite' ), 'type' => Controls_Manager::SELECT, 'options' => $this->border_options, 'default' => 'solid', ] ); $this->add_control( 'fieldBorderSize', [ 'label' => esc_html__( 'Border Size (px)', 'wpforms-lite' ), 'type' => Controls_Manager::NUMBER, 'default' => '1', 'min' => '0', 'condition' => [ 'fieldBorderStyle!' => 'none', ], ] ); $this->add_control( 'fieldBorderRadius', [ 'label' => esc_html__( 'Border Radius (px)', 'wpforms-lite' ), 'type' => Controls_Manager::NUMBER, 'min' => '0', 'default' => '3', ] ); $this->add_control( 'fieldBackgroundColor', [ 'label' => esc_html__( 'Background', 'wpforms-lite' ), 'type' => Controls_Manager::COLOR, 'default' => CSSVars::ROOT_VARS['field-background-color'], ] ); $this->add_control( 'fieldBorderColor', [ 'label' => esc_html__( 'Border', 'wpforms-lite' ), 'type' => Controls_Manager::COLOR, 'alpha' => true, 'default' => CSSVars::ROOT_VARS['field-border-color'], ] ); $this->add_control( 'fieldTextColor', [ 'label' => esc_html__( 'Text', 'wpforms-lite' ), 'type' => Controls_Manager::COLOR, 'alpha' => true, 'default' => CSSVars::ROOT_VARS['field-text-color'], ] ); $this->add_control( 'fieldMenuColor', [ 'type' => Controls_Manager::HIDDEN, 'default' => CSSVars::ROOT_VARS['field-menu-color'], ] ); $this->end_controls_section(); } /** * Register widget controls for the Label Style section. * * Adds controls to the "Label Styles" section of the Widget Style settings. * * @since 1.8.3 */ private function add_label_style_controls(): void { $this->start_controls_section( 'label_styles', [ 'label' => esc_html__( 'Label Styles', 'wpforms-lite' ), 'tab' => Controls_Manager::TAB_STYLE, ] ); $this->add_control( 'labelSize', [ 'label' => esc_html__( 'Size', 'wpforms-lite' ), 'type' => Controls_Manager::SELECT, 'options' => $this->size_options, 'default' => 'medium', ] ); $this->add_control( 'labelColor', [ 'label' => esc_html__( 'Label', 'wpforms-lite' ), 'type' => Controls_Manager::COLOR, 'alpha' => true, 'default' => CSSVars::ROOT_VARS['label-color'], ] ); $this->add_control( 'labelSublabelColor', [ 'label' => esc_html__( 'Sublabel & Hint', 'wpforms-lite' ), 'type' => Controls_Manager::COLOR, 'alpha' => true, 'default' => CSSVars::ROOT_VARS['label-sublabel-color'], ] ); $this->add_control( 'labelErrorColor', [ 'label' => esc_html__( 'Error', 'wpforms-lite' ), 'type' => Controls_Manager::COLOR, 'default' => CSSVars::ROOT_VARS['label-error-color'], ] ); $this->end_controls_section(); } /** * Register widget controls for the "Button Style" section. * * Adds controls to the "Button Styles" section of the Widget Style settings. * * @since 1.8.3 */ private function add_button_style_controls(): void { $this->start_controls_section( 'button_styles', [ 'label' => esc_html__( 'Button Styles', 'wpforms-lite' ), 'tab' => Controls_Manager::TAB_STYLE, ] ); $this->add_control( 'buttonSize', [ 'label' => esc_html__( 'Size', 'wpforms-lite' ), 'type' => Controls_Manager::SELECT, 'options' => $this->size_options, 'default' => 'medium', ] ); $this->add_control( 'buttonBorderStyle', [ 'label' => esc_html__( 'Border', 'wpforms-lite' ), 'type' => Controls_Manager::SELECT, 'options' => $this->border_options, 'default' => CSSVars::ROOT_VARS['button-border-style'], ] ); $this->add_control( 'buttonBorderSize', [ 'label' => esc_html__( 'Border Size (px)', 'wpforms-lite' ), 'type' => Controls_Manager::NUMBER, 'default' => CSSVars::ROOT_VARS['button-border-size'], 'min' => '0', 'condition' => [ 'buttonBorderStyle!' => 'none', ], ] ); $this->add_control( 'buttonBorderRadius', [ 'label' => esc_html__( 'Border Radius (px)', 'wpforms-lite' ), 'type' => Controls_Manager::NUMBER, 'min' => '0', 'default' => '3', ] ); $this->add_control( 'buttonBackgroundColor', [ 'label' => esc_html__( 'Background', 'wpforms-lite' ), 'type' => Controls_Manager::COLOR, 'default' => CSSVars::ROOT_VARS['button-background-color'], ] ); $this->add_control( 'buttonBorderColor', [ 'label' => esc_html__( 'Border', 'wpforms-lite' ), 'type' => Controls_Manager::COLOR, 'default' => CSSVars::ROOT_VARS['button-border-color'], 'condition' => [ 'buttonBorderStyle!' => 'none', ], ] ); $this->add_control( 'buttonTextColor', [ 'label' => esc_html__( 'Text', 'wpforms-lite' ), 'type' => Controls_Manager::COLOR, 'default' => CSSVars::ROOT_VARS['button-text-color'], ] ); $this->end_controls_section(); } /** * Register widget controls for the "Container Style" section. * * @since 1.9.6 */ private function add_container_style_controls(): void { $this->start_controls_section( 'container_styles', [ 'label' => esc_html__( 'Container Styles', 'wpforms-lite' ), 'tab' => Controls_Manager::TAB_STYLE, ] ); $this->add_control( 'containerPadding', [ 'label' => esc_html__( 'Padding (px)', 'wpforms-lite' ), 'type' => Controls_Manager::NUMBER, 'default' => CSSVars::ROOT_VARS['container-padding'], 'min' => '0', ] ); $this->add_control( 'containerBorderStyle', [ 'label' => esc_html__( 'Border', 'wpforms-lite' ), 'type' => Controls_Manager::SELECT, 'options' => $this->border_options, 'default' => CSSVars::ROOT_VARS['container-border-style'], ] ); $this->add_control( 'containerBorderWidth', [ 'label' => esc_html__( 'Border Size (px)', 'wpforms-lite' ), 'type' => Controls_Manager::NUMBER, 'default' => CSSVars::ROOT_VARS['container-border-width'], 'min' => '0', 'condition' => [ 'containerBorderStyle!' => 'none', ], ] ); $this->add_control( 'containerBorderRadius', [ 'label' => esc_html__( 'Border Radius (px)', 'wpforms-lite' ), 'type' => Controls_Manager::NUMBER, 'min' => '0', 'default' => '3', ] ); $this->add_control( 'containerShadowSize', [ 'label' => esc_html__( 'Shadow', 'wpforms-lite' ), 'type' => Controls_Manager::SELECT, 'options' => [ 'none' => esc_html__( 'None', 'wpforms-lite' ), 'small' => esc_html__( 'Small', 'wpforms-lite' ), 'medium' => esc_html__( 'Medium', 'wpforms-lite' ), 'large' => esc_html__( 'Large', 'wpforms-lite' ), ], 'default' => CSSVars::CONTAINER_SHADOW_SIZE['none']['box-shadow'], ] ); $this->add_control( 'containerBorderColor', [ 'label' => esc_html__( 'Border', 'wpforms-lite' ), 'type' => Controls_Manager::COLOR, 'default' => CSSVars::ROOT_VARS['container-border-color'], 'condition' => [ 'containerBorderStyle!' => 'none', ], ] ); $this->end_controls_section(); } /** * Register widget controls for the "Background Style" section. * * @since 1.9.6 */ private function add_background_style_controls(): void { $this->start_controls_section( 'background_styles', [ 'label' => esc_html__( 'Background Styles', 'wpforms-lite' ), 'tab' => Controls_Manager::TAB_STYLE, ] ); $this->add_control( 'backgroundImage', [ 'label' => esc_html__( 'Image', 'wpforms-lite' ), 'type' => Controls_Manager::SELECT, 'options' => [ 'none' => esc_html__( 'None', 'wpforms-lite' ), 'library' => esc_html__( 'Media Library', 'wpforms-lite' ), 'stock' => esc_html__( 'Stock Photo', 'wpforms-lite' ), ], 'default' => 'none', ] ); $this->add_control( 'backgroundPosition', [ 'label' => esc_html__( 'Position', 'wpforms-lite' ), 'type' => Controls_Manager::SELECT, 'options' => [ 'top left' => esc_html__( 'Top Left', 'wpforms-lite' ), 'top center' => esc_html__( 'Top Center', 'wpforms-lite' ), 'top right' => esc_html__( 'Top Right', 'wpforms-lite' ), 'center left' => esc_html__( 'Center Left', 'wpforms-lite' ), 'center center' => esc_html__( 'Center Center', 'wpforms-lite' ), 'center right' => esc_html__( 'Center Right', 'wpforms-lite' ), 'bottom left' => esc_html__( 'Bottom Left', 'wpforms-lite' ), 'bottom center' => esc_html__( 'Bottom Center', 'wpforms-lite' ), 'bottom right' => esc_html__( 'Bottom Right', 'wpforms-lite' ), ], 'default' => CSSVars::ROOT_VARS['background-position'], 'condition' => [ 'backgroundImage!' => 'none', ], ] ); $this->add_control( 'backgroundRepeat', [ 'label' => esc_html__( 'Repeat', 'wpforms-lite' ), 'type' => Controls_Manager::SELECT, 'options' => [ 'no-repeat' => esc_html__( 'No Repeat', 'wpforms-lite' ), 'repeat' => esc_html__( 'Tile', 'wpforms-lite' ), 'repeat-x' => esc_html__( 'Repeat X', 'wpforms-lite' ), 'repeat-y' => esc_html__( 'Repeat Y', 'wpforms-lite' ), ], 'default' => CSSVars::ROOT_VARS['background-repeat'], 'condition' => [ 'backgroundImage!' => 'none', ], ] ); $this->add_control( 'backgroundSize', [ 'label' => esc_html__( 'Size', 'wpforms-lite' ), 'type' => Controls_Manager::SELECT, 'options' => [ 'dimensions' => esc_html__( 'Dimensions', 'wpforms-lite' ), 'cover' => esc_html__( 'Cover', 'wpforms-lite' ), ], 'default' => CSSVars::ROOT_VARS['background-size'], 'condition' => [ 'backgroundImage!' => 'none', ], ] ); $this->add_control( 'backgroundSizeMode', [ 'type' => Controls_Manager::HIDDEN, 'default' => CSSVars::ROOT_VARS['background-size'], ] ); $this->add_control( 'backgroundWidth', [ 'label' => esc_html__( 'Width (px)', 'wpforms-lite' ), 'type' => Controls_Manager::NUMBER, 'default' => '100', 'min' => '0', 'condition' => [ 'backgroundImage!' => 'none', 'backgroundSize' => 'dimensions', ], ] ); $this->add_control( 'backgroundHeight', [ 'label' => esc_html__( 'Height (px)', 'wpforms-lite' ), 'type' => Controls_Manager::NUMBER, 'default' => '100', 'min' => '0', 'condition' => [ 'backgroundImage!' => 'none', 'backgroundSize' => 'dimensions', ], ] ); $this->add_control( 'backgroundUrl', [ 'label' => esc_html__( 'Choose Image', 'wpforms-lite' ), 'type' => Controls_Manager::MEDIA, 'default' => [ 'url' => CSSVars::ROOT_VARS['background-url'], ], 'ai' => [ 'active' => false, ], 'separator' => 'after', 'condition' => [ 'backgroundImage!' => 'none', ], ] ); $this->add_control( 'backgroundColor', [ 'label' => esc_html__( 'Background', 'wpforms-lite' ), 'type' => Controls_Manager::COLOR, 'default' => CSSVars::ROOT_VARS['background-color'], ] ); $this->end_controls_section(); } /** * Register widget controls for the "Other Styles" section. * * @since 1.9.6 */ private function add_other_style_controls(): void { $this->start_controls_section( 'other_styles', [ 'label' => esc_html__( 'Other Styles', 'wpforms-lite' ), 'tab' => Controls_Manager::TAB_STYLE, ] ); $this->add_control( 'pageBreakColor', [ 'label' => esc_html__( 'Page Break', 'wpforms-lite' ), 'type' => Controls_Manager::COLOR, 'default' => CSSVars::ROOT_VARS['page-break-color'], ] ); $this->end_controls_section(); } /** * Register widget controls for the "Advanced" section. * * Adds controls to the "Button Styles" section of the Widget Style settings. * * @since 1.8.3 */ private function add_advanced_style_controls(): void { $this->start_controls_section( 'advanced', [ 'label' => esc_html__( 'Advanced', 'wpforms-lite' ), 'tab' => Controls_Manager::TAB_STYLE, ] ); $this->add_control( 'className', [ 'label' => esc_html__( 'Additional Classes', 'wpforms-lite' ), 'type' => Controls_Manager::TEXT, 'description' => esc_html__( 'Separate multiple classes with spaces.', 'wpforms-lite' ), 'ai' => [ 'active' => false, ], 'prefix_class' => '', // Prevents re-rendering of the widget. ] ); if ( $this->is_admin() ) { $this->add_control( 'ACDivider', [ 'type' => Controls_Manager::DIVIDER, ] ); $this->add_control( 'copyPasteJsonValue', [ 'label' => esc_html__( 'Copy / Paste Style Settings', 'wpforms-lite' ), 'type' => Controls_Manager::TEXTAREA, 'description' => esc_html__( 'If you\'ve copied style settings from another form, you can paste them here to add the same styling to this form. Any current style settings will be overwritten.', 'wpforms-lite' ), 'ai' => [ 'active' => false, ], ] ); $this->add_control( 'CPDivider', [ 'type' => Controls_Manager::DIVIDER, ] ); } $this->end_controls_section(); } /** * Render widget output on the frontend. * * @since 1.8.3 */ protected function render_frontend() { if ( empty( $this->css_vars_obj ) ) { return; } $widget_id = $this->get_id(); $attr = $this->get_settings_for_display(); $css_vars = $this->css_vars_obj->get_customized_css_vars( $attr ); $custom_classes = ! empty( $attr['className'] ) ? trim( $attr['className'] ) : ''; if ( ! empty( $css_vars ) ) { $style_id = 'wpforms-css-vars-elementor-widget-' . $widget_id; /** * Filter the CSS selector for output CSS variables for styling the form in Elementor widget. * * @since 1.8.3 * * @param string $selector The CSS selector for output CSS variables for styling the Elementor Widget. * @param array $attr Attributes passed by Elementor Widget. * @param array $css_vars CSS variables data. */ $vars_selector = apply_filters( 'wpforms_integrations_elementor_widget_modern_output_css_vars_selector', ".elementor-widget-wpforms.elementor-element-{$widget_id}", $attr, $css_vars ); $this->css_vars_obj->output_selector_vars( $vars_selector, $css_vars, $style_id ); } // Add custom classes. if ( $custom_classes ) { $this->add_render_attribute( '_wrapper', [ 'class' => [ $custom_classes, ], ] ); } // Render selected form. $this->render_form(); } /** * Get settings for display. * * @since 1.8.3 * * @param string|null $setting_key Optional. The key of the requested setting. Default is null. * * @return mixed The settings. */ public function get_settings_for_display( $setting_key = null ) { $settings = parent::get_settings_for_display( $setting_key ); if ( ! empty( $setting_key ) ) { return $settings; } $settings = $this->remove_empty_settings( $settings ); $settings = $this->apply_dimension_settings( $settings ); $settings = $this->apply_complex_settings( $settings ); if ( isset( $settings['__globals__'] ) ) { $settings = $this->check_global_styles( $settings ); } return $settings; } /** * Remove empty settings. * * @since 1.9.6 * * @param mixed $settings Widget settings. * * @return mixed Updated settings. */ private function remove_empty_settings( $settings ) { if ( ! is_array( $settings ) ) { return $settings; } return array_filter( $settings, static function ( $value ) { return ! empty( $value ); } ); } /** * Apply complex settings values. * * @since 1.9.6 * * @param mixed $settings Widget settings. * * @return mixed Updated settings. */ private function apply_complex_settings( $settings ) { if ( isset( $settings['backgroundUrl'] ) && is_array( $settings['backgroundUrl'] ) ) { $image_url = $settings['backgroundUrl']['url'] ?? ''; $settings['backgroundUrl'] = 'url( ' . $image_url . ' )'; } if ( isset( $settings['backgroundSize'] ) && $settings['backgroundSize'] === 'dimensions' ) { $bg_width = $settings['backgroundWidth'] ?? CSSVars::ROOT_VARS['background-width']; $bg_height = $settings['backgroundHeight'] ?? CSSVars::ROOT_VARS['background-height']; $settings['backgroundSize'] = "{$bg_width} {$bg_height}"; } return $settings; } /** * Apply dimension settings with pixel units. * * @since 1.9.6 * * @param mixed $settings Widget settings. * * @return mixed Updated settings with dimension values. */ private function apply_dimension_settings( $settings ) { $dimension_properties = [ 'fieldBorderRadius' => 'field-border-radius', 'fieldBorderSize' => 'field-border-size', 'buttonBorderRadius' => 'button-border-radius', 'buttonBorderSize' => 'button-border-size', 'containerPadding' => 'container-padding', 'containerBorderWidth' => 'container-border-width', 'containerBorderRadius' => 'container-border-radius', 'backgroundWidth' => 'background-width', 'backgroundHeight' => 'background-height', ]; foreach ( $dimension_properties as $property => $root_var ) { if ( ! isset( $settings[ $property ] ) ) { $settings[ $property ] = CSSVars::ROOT_VARS[ $root_var ]; continue; } $value = (string) $settings[ $property ]; if ( $value !== '' && substr( $value, -2 ) !== 'px' ) { $settings[ $property ] = $value . 'px'; } } return $settings; } /** * Check if global styles are used in colors controls and update its values with the real ones. * * @since 1.8.3 * * @param mixed $settings Widget settings. * * @return mixed Updated settings. */ private function check_global_styles( $settings ) { $global_settings = $settings['__globals__'] ?? []; $kit = Plugin::$instance->kits_manager->get_active_kit_for_frontend(); $system_colors = $kit->get_settings_for_display( 'system_colors' ); $custom_colors = $kit->get_settings_for_display( 'custom_colors' ); $global_colors = array_merge( $system_colors, $custom_colors ); foreach ( $global_settings as $key => $value ) { if ( empty( $value ) ) { continue; } $color_id = str_replace( 'globals/colors?id=', '', $value ); foreach ( $global_colors as $color ) { if ( $color['_id'] === $color_id ) { $settings[ $key ] = $color['color']; } } } return $settings; } /** * Check if the user is an admin. * * @since 1.9.6 * * @return bool True if the user is an admin, false otherwise. */ private function is_admin(): bool { return current_user_can( 'manage_options' ); } } Integrations/Elementor/Controls/WPFormsThemes.php 0000644 00000002122 15174710275 0016171 0 ustar 00 <?php // phpcs:disable Generic.Commenting.DocComment.MissingShort /** @noinspection PhpUndefinedNamespaceInspection */ /** @noinspection PhpUndefinedClassInspection */ // phpcs:enable Generic.Commenting.DocComment.MissingShort namespace WPForms\Integrations\Elementor\Controls; use Elementor\Base_Data_Control; /** * Custom WPForms Themes control for Elementor editor. * * @since 1.9.6 */ class WPFormsThemes extends Base_Data_Control { /** * Get the control type. * * @since 1.9.6 * * @return string Control type. */ public function get_type() { return 'wpforms_themes'; } /** * Get the control's default settings. * * @since 1.9.6 * * @return array Control default settings. */ protected function get_default_settings() { return [ 'label_block' => true, ]; } /** * Render control output in the editor. * * @since 1.9.6 */ public function content_template() { ?> <div class="elementor-control-field"> <div class="elementor-control-input-wrapper"> <div class="wpforms-elementor-themes-control"></div> </div> </div> <?php } } Integrations/Elementor/RestApi.php 0000644 00000007442 15174710275 0013244 0 ustar 00 <?php namespace WPForms\Integrations\Elementor; use WP_Error; use WP_REST_Request; use WP_REST_Response; // phpcs:ignore WPForms.PHP.UseStatement.UnusedUseStatement use WPForms\Frontend\CSSVars; /** * Rest API for Elementor Modern widget. * * @since 1.9.6 */ class RestApi { /** * Route prefix. * * @since 1.9.6 * * @var string */ public const ROUTE_NAMESPACE = '/wpforms/v1/'; /** * ThemesData class instance. * * @since 1.9.6 * * @var CSSVars */ private $themes_data; /** * Initialize class. * * @since 1.9.6 * * @param Widget|mixed $widget_obj Widget object. * @param ThemesData|mixed $themes_data ThemesData object. */ public function __construct( $widget_obj, $themes_data ) { if ( ! $widget_obj || ! $themes_data || ! wpforms_is_wpforms_rest() ) { return; } $this->themes_data = $themes_data; $this->hooks(); } /** * Hooks. * * @since 1.9.6 */ private function hooks(): void { add_action( 'rest_api_init', [ $this, 'register_api_routes' ], 20 ); } /** * Register API routes for Elementor Modern widget. * * @since 1.9.6 */ public function register_api_routes() { /** * Register routes with WordPress. * * @see https://developer.wordpress.org/reference/functions/register_rest_route/ */ register_rest_route( self::ROUTE_NAMESPACE, '/elementor/themes/', [ 'methods' => 'GET', 'callback' => [ $this, 'get_themes' ], 'permission_callback' => [ $this, 'permissions_check' ], ] ); register_rest_route( self::ROUTE_NAMESPACE, '/elementor/themes/custom/', [ 'methods' => 'POST', 'callback' => [ $this, 'save_themes' ], 'permission_callback' => [ $this, 'admin_permissions_check' ], ] ); } /** * Check if a user has permission to access private data. * * @since 1.9.6 * * @return true|WP_Error True if a user has permission. */ public function permissions_check() { // Restrict endpoint to only users who have the edit_posts capability. if ( ! current_user_can( 'edit_posts' ) ) { return new WP_Error( 'rest_forbidden', esc_html__( 'This route is private.', 'wpforms-lite' ), [ 'status' => 401 ] ); } return true; } /** * Check if a user has admin permissions. * * @since 1.9.6 * * @return true|WP_Error True if a user has permission. */ public function admin_permissions_check() { // Restrict endpoint to only users who have the manage_options capability. if ( ! current_user_can( 'manage_options' ) ) { return new WP_Error( 'rest_forbidden', esc_html__( 'This route is accessible only to administrators.', 'wpforms-lite' ), [ 'status' => 401 ] ); } return true; } /** * Return themes as a protected WP_REST_Response object. * * @since 1.9.6 * * @return WP_Error|WP_REST_Response */ public function get_themes() { $custom_themes = $this->themes_data->get_custom_themes(); $wpforms_themes = $this->themes_data->get_wpforms_themes(); return rest_ensure_response( [ 'custom' => ! empty( $custom_themes ) ? $custom_themes : null, 'wpforms' => ! empty( $wpforms_themes ) ? $wpforms_themes : null, ] ); } /** * Save custom themes. * * @since 1.9.6 * * @param WP_REST_Request $request Request object. * * @return WP_Error|WP_REST_Response */ public function save_themes( WP_REST_Request $request ) { $custom_themes = (array) ( $request->get_param( 'customThemes' ) ?? [] ); // Save custom themes data and return REST response. $result = $this->themes_data->update_custom_themes_file( $custom_themes ); if ( ! $result ) { return rest_ensure_response( [ 'result' => false, 'error' => esc_html__( 'Can\'t save theme data.', 'wpforms-lite' ), ] ); } return rest_ensure_response( [ 'result' => true ] ); } } Integrations/Elementor/Widget.php 0000644 00000023702 15174710275 0013115 0 ustar 00 <?php // phpcs:disable Generic.Commenting.DocComment.MissingShort /** @noinspection PhpUndefinedNamespaceInspection */ /** @noinspection PhpUndefinedClassInspection */ // phpcs:enable Generic.Commenting.DocComment.MissingShort namespace WPForms\Integrations\Elementor; use Elementor\Plugin; use Elementor\Widget_Base; use Elementor\Controls_Manager; /** * WPForms widget for Elementor page builder. * * @since 1.6.2 */ class Widget extends Widget_Base { /** * Script dependencies. * * @since 1.9.1 * * @return array */ public function get_script_depends(): array { return [ 'wpforms-elementor' ]; } /** * Get widget name. * * Retrieve shortcode widget name. * * @since 1.6.2 * * @return string Widget name. */ public function get_name() { return 'wpforms'; } /** * Get widget title. * * Retrieve shortcode widget title. * * @since 1.6.2 * * @return string Widget title. */ public function get_title() { return __( 'WPForms', 'wpforms-lite' ); } /** * Get widget icon. * * Retrieve shortcode widget icon. * * @since 1.6.2 * * @return string Widget icon. */ public function get_icon() { return 'icon-wpforms'; } /** * Get widget keywords. * * Retrieve the list of keywords the widget belongs to. * * @since 1.6.2 * * @return array Widget keywords. */ public function get_keywords() { return [ 'form', 'forms', 'wpforms', 'contact form', 'sullie', 'the dude', ]; } /** * Get widget categories. * * @since 1.6.2 * * @return array Widget categories. */ public function get_categories() { return [ 'basic', ]; } /** * Register widget controls. * * Adds different input fields to allow the user to change and customize the widget settings. * * @since 1.6.2 */ protected function register_controls() { // phpcs:ignore PSR2.Methods.MethodDeclaration.Underscore $this->content_controls(); } /** * Register content tab controls. * * @since 1.6.2 * * @noinspection PhpUndefinedMethodInspection * @noinspection HtmlUnknownTarget */ protected function content_controls() { $this->start_controls_section( 'section_form', [ 'label' => esc_html__( 'Form', 'wpforms-lite' ), 'tab' => Controls_Manager::TAB_CONTENT, ] ); $forms = $this->get_forms(); if ( empty( $forms ) ) { $this->add_control( 'add_form_notice', [ 'show_label' => false, 'type' => Controls_Manager::RAW_HTML, 'raw' => wp_kses( __( '<b>You haven\'t created a form yet.</b><br> What are you waiting for?', 'wpforms-lite' ), [ 'b' => [], 'br' => [], ] ), 'content_classes' => 'elementor-panel-alert elementor-panel-alert-info wpforms-elementor-no-forms-notice', ] ); } $this->add_control( 'form_id', [ 'label' => esc_html__( 'Form', 'wpforms-lite' ), 'type' => Controls_Manager::SELECT, 'label_block' => true, 'options' => $forms, 'default' => '0', ] ); $this->add_control( 'edit_form', [ 'show_label' => false, 'type' => Controls_Manager::RAW_HTML, 'raw' => wp_kses( /* translators: %s - WPForms documentation link. */ __( 'Need to make changes? <a href="#">Edit the selected form.</a>', 'wpforms-lite' ), [ 'a' => [] ] ), 'condition' => [ 'form_id!' => '0', ], ] ); $this->add_control( 'test_form_notice', [ 'show_label' => false, 'type' => Controls_Manager::RAW_HTML, 'raw' => sprintf( wp_kses( /* translators: %s - WPForms documentation link. */ __( '<b>Heads up!</b> Don\'t forget to test your form. <a href="%s" target="_blank" rel="noopener noreferrer">Check out our complete guide!</a>', 'wpforms-lite' ), [ 'b' => [], 'br' => [], 'a' => [ 'href' => [], 'rel' => [], 'target' => [], ], ] ), 'https://wpforms.com/docs/how-to-properly-test-your-wordpress-forms-before-launching-checklist/' ), 'condition' => [ 'form_id!' => '0', ], 'content_classes' => 'elementor-panel-alert elementor-panel-alert-info', ] ); $this->add_control( 'add_form_btn', [ 'show_label' => false, 'label_block' => false, 'type' => Controls_Manager::BUTTON, 'button_type' => 'default', 'separator' => 'before', 'text' => '<b>+</b>' . esc_html__( 'New form', 'wpforms-lite' ), 'event' => 'elementorWPFormsAddFormBtnClick', ] ); $this->add_legacy_styles_notice(); $this->end_controls_section(); $this->start_controls_section( 'section_display', [ 'label' => esc_html__( 'Display Options', 'wpforms-lite' ), 'tab' => Controls_Manager::TAB_CONTENT, 'condition' => [ 'form_id!' => '0', ], ] ); $this->add_control( 'display_form_name', [ 'label' => esc_html__( 'Form Name', 'wpforms-lite' ), 'type' => Controls_Manager::SWITCHER, 'label_on' => esc_html__( 'Show', 'wpforms-lite' ), 'label_off' => esc_html__( 'Hide', 'wpforms-lite' ), 'return_value' => 'yes', 'condition' => [ 'form_id!' => '0', ], ] ); $this->add_control( 'display_form_description', [ 'label' => esc_html__( 'Form Description', 'wpforms-lite' ), 'type' => Controls_Manager::SWITCHER, 'label_on' => esc_html__( 'Show', 'wpforms-lite' ), 'label_off' => esc_html__( 'Hide', 'wpforms-lite' ), 'separator' => 'after', 'return_value' => 'yes', 'condition' => [ 'form_id!' => '0', ], ] ); $this->end_controls_section(); } /** * Add legacy styles notice. * * @since 1.9.6 * * @noinspection PhpUndefinedMethodInspection * @noinspection HtmlUnknownTarget */ private function add_legacy_styles_notice() { $is_modern = wpforms_get_render_engine() === 'modern'; $is_full_styles = (int) wpforms_setting( 'disable-css', '1' ) === 1; if ( ! $is_modern || ! $is_full_styles ) { $notice_text = ! $is_modern ? __( 'Upgrade your forms to use our modern markup and unlock extensive style controls.', 'wpforms-lite' ) : __( 'Update your forms to use base and form theme styling and unlock extensive style controls.', 'wpforms-lite' ); $this->add_control( 'legacy_styling_notice', [ 'show_label' => false, 'type' => Controls_Manager::RAW_HTML, 'raw' => sprintf( wp_kses( /* translators: %s - WPForms documentation link. */ __( '<b>Want to customize your form styles without editing CSS?</b> <p>%1$s</p> <a href="%2$s" target="_blank" rel="noopener noreferrer">Learn more</a>', 'wpforms-lite' ), [ 'b' => [], 'p' => [], 'a' => [ 'href' => [], 'rel' => [], 'target' => [], ], ] ), $notice_text, wpforms_utm_link( 'https://wpforms.com/docs/styling-your-forms/', 'Elementor Widget Settings', 'Form Styles Documentation' ) ), 'content_classes' => 'elementor-panel-alert elementor-panel-alert-warning wpforms-legacy-styles-notice', ] ); } } /** * Render widget output. * * @since 1.6.2 */ protected function render() { if ( Plugin::$instance->editor->is_edit_mode() ) { $this->render_edit_mode(); } else { $this->render_frontend(); } } /** * Render widget output in edit mode. * * @since 1.6.3.1 * * @noinspection PhpPossiblePolymorphicInvocationInspection */ protected function render_edit_mode() { $form_id = $this->get_settings_for_display( 'form_id' ); // Popup markup template. echo wpforms_render( 'integrations/elementor/popup' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped if ( count( $this->get_forms() ) < 2 ) { // No forms block. echo wpforms_render( 'integrations/elementor/no-forms' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped return; } if ( empty( $form_id ) ) { // Render form selector. echo wpforms_render( // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped 'integrations/elementor/form-selector', [ 'forms' => $this->get_form_selector_options(), ], true ); return; } // Finally, render selected form. $this->render_frontend(); } /** * Render widget output on the frontend. * * @since 1.6.3.1 */ protected function render_frontend() { // Render selected form. $this->render_form(); } /** * Render widget as plain content. * * @since 1.6.2 */ public function render_plain_content() { $this->render_form(); } /** * Render a form. * * @since 1.8.3 * * @noinspection PhpPossiblePolymorphicInvocationInspection */ public function render_form() { wpforms_display( $this->get_settings_for_display( 'form_id' ), $this->get_settings_for_display( 'display_form_name' ) === 'yes', $this->get_settings_for_display( 'display_form_description' ) === 'yes' ); } /** * Get form list. * * @since 1.6.2 * * @returns array Array of forms. */ public function get_forms() { static $forms_list = []; if ( empty( $forms_list ) ) { $forms_obj = wpforms()->obj( 'form' ); $forms = $forms_obj ? $forms_obj->get() : null; if ( ! empty( $forms ) ) { $forms_list[0] = esc_html__( 'Select a form', 'wpforms-lite' ); foreach ( $forms as $form ) { $forms_list[ $form->ID ] = mb_strlen( $form->post_title ) > 100 ? mb_substr( $form->post_title, 0, 97 ) . '...' : $form->post_title; } } } return $forms_list; } /** * Get form selector options. * * @since 1.6.2 * * @returns string Rendered options for the select tag. */ public function get_form_selector_options() { $forms = $this->get_forms(); $options = ''; foreach ( $forms as $form_id => $form ) { $options .= sprintf( '<option value="%d">%s</option>', (int) $form_id, esc_html( $form ) ); } return $options; } } Integrations/IntegrationInterface.php 0000644 00000000642 15174710275 0014042 0 ustar 00 <?php namespace WPForms\Integrations; /** * Interface IntegrationInterface defines required methods for integrations to work properly. * * @since 1.4.8 */ interface IntegrationInterface { /** * Indicate if current integration is allowed to load. * * @since 1.4.8 * * @return bool */ public function allow_load(); /** * Load an integration. * * @since 1.4.8 */ public function load(); } Integrations/WPMailSMTP/Notifications.php 0000644 00000012640 15174710275 0014405 0 ustar 00 <?php namespace WPForms\Integrations\WPMailSMTP; use WPMailSMTP\Options; use WPForms\Integrations\IntegrationInterface; /** * WP Mail SMTP hints inside form builder notifications. * * @since 1.4.8 */ class Notifications implements IntegrationInterface { /** * WP Mail SMTP options. * * @since 1.4.8 * * @var Options */ public $options; /** * Indicate if current integration is allowed to load. * * @since 1.4.8 * * @return bool */ public function allow_load() { return wpforms_is_admin_page( 'builder' ) && function_exists( 'wp_mail_smtp' ); } /** * Load an integration. * * @since 1.4.8 */ public function load() { $this->options = new Options(); $this->hooks(); } /** * Integration filters. * * @since 1.4.8 */ protected function hooks() { add_filter( 'wpforms_builder_notifications_from_name_after', [ $this, 'from_name_after' ] ); add_filter( 'wpforms_builder_notifications_from_email_after', [ $this, 'from_email_after' ] ); add_filter( 'wpforms_builder_notifications_sender_name_settings', [ $this, 'change_from_name_settings' ], 10, 3 ); add_filter( 'wpforms_builder_notifications_sender_address_settings', [ $this, 'change_from_email_settings' ], 10, 3 ); add_action( 'wpforms_form_settings_notifications_single_after', [ $this, 'add_hidden_from_name_field' ], 10, 2 ); add_action( 'wpforms_form_settings_notifications_single_after', [ $this, 'add_hidden_from_email_field' ], 10, 2 ); } /** * Redefine From Name settings with data from WP Mail SMTP. * * @since 1.7.6 * * @param array $args Field settings. * @param array $form_data Form data. * @param int $id Notification ID. * * @return array */ public function change_from_name_settings( $args, $form_data, $id ) { if ( ! $this->options->get( 'mail', 'from_name_force' ) ) { return $args; } $args['value'] = $this->options->get( 'mail', 'from_name' ); unset( $args['smarttags'] ); return $args; } /** * Redefine From Email settings with data from WP Mail SMTP. * * @since 1.7.6 * * @param array $args Field settings. * @param array $form_data Form data. * @param int $id Notification ID. * * @return array */ public function change_from_email_settings( $args, $form_data, $id ) { if ( ! $this->options->get( 'mail', 'from_email_force' ) ) { return $args; } $args['value'] = $this->options->get( 'mail', 'from_email' ); unset( $args['smarttags'] ); return $args; } /** * Add hidden From Name field to overwrite value from WP Mail SMTP. * * @since 1.7.6 * * @param array $settings Form settings. * @param int $id Notification id. */ public function add_hidden_from_name_field( $settings, $id ) { if ( empty( $settings->form_data['settings']['notifications'][ $id ]['sender_name'] ) || ! $this->options->get( 'mail', 'from_name_force' ) ) { return; } wpforms_panel_field( 'text', 'notifications', 'sender_name', $settings->form_data, '', [ 'parent' => 'settings', 'subsection' => $id, 'readonly' => true, 'class' => 'wpforms-hidden', 'value' => $settings->form_data['settings']['notifications'][ $id ]['sender_name'], ] ); } /** * Add hidden From Email field to overwrite value from WP Mail SMTP. * * @since 1.7.6 * * @param array $settings Form settings. * @param int $id Notification id. */ public function add_hidden_from_email_field( $settings, $id ) { if ( empty( $settings->form_data['settings']['notifications'][ $id ]['sender_address'] ) || ! $this->options->get( 'mail', 'from_email_force' ) ) { return; } wpforms_panel_field( 'text', 'notifications', 'sender_address', $settings->form_data, '', [ 'parent' => 'settings', 'subsection' => $id, 'readonly' => true, 'class' => 'wpforms-hidden', 'value' => $settings->form_data['settings']['notifications'][ $id ]['sender_address'], ] ); } /** * Display hint if WP Mail SMTP is forcing from name. * * @since 1.4.8 * * @param string $after Text displayed after setting. * * @return string */ public function from_name_after( $after ) { if ( ! $this->options->get( 'mail', 'from_name_force' ) ) { return $after; } return sprintf( wp_kses( /* translators: %s - URL WP Mail SMTP settings. */ __( 'This setting is disabled because you have the "Force From Name" setting enabled in the <a href="%s" target="_blank">WP Mail SMTP</a> plugin.', 'wpforms-lite' ), [ 'a' => [ 'href' => [], 'target' => [], ], ] ), esc_url( admin_url( 'options-general.php?page=wp-mail-smtp#wp-mail-smtp-setting-row-from_name' ) ) ); } /** * Display hint if WP Mail SMTP is forcing from email. * * @since 1.4.8 * * @param string $after Text displayed after setting. * * @return string */ public function from_email_after( $after ) { if ( ! $this->options->get( 'mail', 'from_email_force' ) ) { return $after; } return sprintf( wp_kses( /* translators: %s - URL WP Mail SMTP settings. */ __( 'This setting is disabled because you have the "Force From Email" setting enabled in the <a href="%s" target="_blank">WP Mail SMTP</a> plugin.', 'wpforms-lite' ), [ 'a' => [ 'href' => [], 'target' => [], ], ] ), esc_url( admin_url( 'options-general.php?page=wp-mail-smtp#wp-mail-smtp-setting-row-from_email' ) ) ); } } Integrations/Stripe/Api/Common.php 0000644 00000026331 15174710275 0013150 0 ustar 00 <?php // phpcs:ignoreFile WPForms.PHP.BackSlash.RemoveBackslash namespace WPForms\Integrations\Stripe\Api; use WPForms\Vendor\Stripe\Customer; use WPForms\Vendor\Stripe\Plan; use WPForms\Vendor\Stripe\Stripe; use WPForms\Vendor\Stripe\Subscription; use WPForms\Integrations\Stripe\Helpers; /** * Common methods for every Stripe API implementation. * * @since 1.8.2 */ abstract class Common { /** * API configuration. * * @since 1.8.2 * * @var array */ protected $config; /** * Stripe customer object. * * @since 1.8.2 * * @var Customer */ protected $customer; /** * Stripe subscription object. * * @since 1.8.2 * * @var Subscription */ protected $subscription; /** * API error message. * * @since 1.8.2 * * @var string */ protected $error; /** * API exception. * * @since 1.8.2 * * @var \Exception */ protected $exception; /** * Get class variable value or its key. * * @since 1.8.2 * * @param string $field Name of the variable to retrieve. * @param string $key Name of the key to retrieve. * * @return mixed */ protected function get_var( $field, $key = '' ) { $var = isset( $this->{$field} ) ? $this->{$field} : null; if ( ! $key ) { return $var; } if ( is_object( $var ) ) { return isset( $var->{$key} ) ? $var->{$key} : null; } if ( is_array( $var ) ) { return isset( $var[ $key ] ) ? $var[ $key ] : null; } return $var; } /** * Get API configuration array or its key. * * @since 1.8.2 * * @param string $key Name of the key to retrieve. * * @return mixed */ public function get_config( $key = '' ) { return $this->get_var( 'config', $key ); } /** * Get saved Stripe customer object or its key. * * @since 1.8.2 * * @param string $key Name of the key to retrieve. * * @return mixed */ public function get_customer( $key = '' ) { return $this->get_var( 'customer', $key ); } /** * Get saved Stripe subscription object or its key. * * @since 1.8.2 * * @param string $key Name of the key to retrieve. * * @return mixed */ public function get_subscription( $key = '' ) { return $this->get_var( 'subscription', $key ); } /** * Get API error message. * * @since 1.8.2 * * @return string */ public function get_error() { return $this->get_var( 'error' ); } /** * Get API exception. * * @since 1.8.2 * * @return \Exception */ public function get_exception() { return $this->get_var( 'exception' ); } /** * Initial Stripe app configuration. * * @since 1.8.2 */ public function setup_stripe() { Stripe::setAppInfo( 'WPForms acct_17Xt6qIdtRxnENqV', WPFORMS_VERSION, 'https://wpforms.com/addons/stripe-addon/', 'pp_partner_Dw7IkUZbIlCrtq' ); } /** * Set a customer object. * Check if a customer exists in Stripe, if not creates one. * * @since 1.8.2 * @since 1.8.6 Added customer name argument and allow empty email. * @since 1.8.8 Added customer billing address argument. * @since 1.9.6 Added customer phone and metadata arguments. * * @param string $email Email to fetch an existing customer. * @param string $name Customer name. * @param array $address Customer billing address. * @param string $phone Customer phone number. * @param array $metadata Customer metadata. */ protected function set_customer( string $email = '', string $name = '', array $address = [], string $phone = '', array $metadata = [] ) { if ( ! $email && ! $name && ! $phone ) { return; } $args = []; if ( $name ) { $args['name'] = $name; } if ( $address ) { $args['address'] = $address; } if ( $phone ) { $args['phone'] = $phone; } if ( $metadata ) { $args['metadata'] = $metadata; } // Create a customer with name only if email is empty. if ( ! $email ) { try { $customer = Customer::create( $args, Helpers::get_auth_opts() ); } catch ( \Exception $e ) { $customer = null; } if ( ! isset( $customer->id ) ) { return; } $this->customer = $customer; return; } // Retrieve a customer by email. try { $customers = Customer::all( [ 'email' => $email ], Helpers::get_auth_opts() ); } catch ( \Exception $e ) { $customers = null; } // Determine whether the customer name/address needs to be updated. if ( isset( $customers->data[0]->id ) ) { $this->customer = $customers->data[0]; $needUpdateName = ! empty( $name ) && $name !== $this->customer->name; $needUpdatePhone = ! empty( $phone ) && $phone !== $this->customer->phone; $needUpdateAddress = false; if ( ! $needUpdateName ) { $existingAddress = isset( $this->customer->address ) && method_exists( $this->customer->address, 'toArray' ) ? $this->customer->address->toArray() : []; $needUpdateAddress = ! empty( array_diff_assoc( $address, $existingAddress ) ); } // Update customer name/address/phone. if ( $needUpdateName || $needUpdateAddress || $needUpdatePhone ) { try { $this->customer = Customer::update( $this->customer->id, $args, Helpers::get_auth_opts() ); } catch ( \Exception $e ) { wpforms_log( 'Stripe: Unable to update customer information.', $e->getMessage(), [ 'type' => [ 'payment', 'error' ], ] ); } } return; } // Create a customer with email. try { $args['email'] = $email; $customer = Customer::create( $args, Helpers::get_auth_opts() ); } catch ( \Exception $e ) { $customer = null; } if ( ! isset( $customer->id ) ) { return; } $this->customer = $customer; } /** * Set an error message from a Stripe API exception. * * @since 1.8.2 * * @param \Exception|\WPForms\Vendor\Stripe\Exception\ApiErrorException $e Stripe API exception to process. */ protected function set_error_from_exception( $e ) { /** * WPForms set Stripe error from exception. * * @since 1.8.2 * * @param \Exception|\WPForms\Vendor\Stripe\Exception\ApiErrorException $e Stripe API exception to process. */ do_action( 'wpformsstripe_api_common_set_error_from_exception', $e ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName if ( is_a( $e, '\WPForms\Vendor\Stripe\Exception\CardException' ) ) { $body = $e->getJsonBody(); $this->error = $body['error']['message']; return; } $errors = [ '\WPForms\Vendor\Stripe\Exception\RateLimitException' => esc_html__( 'Too many requests made to the API too quickly.', 'wpforms-lite' ), '\WPForms\Vendor\Stripe\Exception\InvalidRequestException' => esc_html__( 'Invalid parameters were supplied to Stripe API.', 'wpforms-lite' ), '\WPForms\Vendor\Stripe\Exception\AuthenticationException' => esc_html__( 'Authentication with Stripe API failed.', 'wpforms-lite' ), '\WPForms\Vendor\Stripe\Exception\ApiConnectionException' => esc_html__( 'Network communication with Stripe failed.', 'wpforms-lite' ), '\WPForms\Vendor\Stripe\Exception\ApiErrorException' => esc_html__( 'Unable to process Stripe payment.', 'wpforms-lite' ), '\Exception' => esc_html__( 'Unable to process payment.', 'wpforms-lite' ), ]; foreach ( $errors as $error_type => $error_message ) { if ( is_a( $e, $error_type ) ) { $this->error = $error_message; return; } } } /** * Set an exception from a Stripe API exception. * * @since 1.8.2 * * @param \Exception $e Stripe API exception to process. */ protected function set_exception( $e ) { $this->exception = $e; } /** * Handle Stripe API exception. * * @since 1.8.2 * * @param \Exception $e Stripe API exception to process. */ protected function handle_exception( $e ) { $this->set_exception( $e ); $this->set_error_from_exception( $e ); } /** * Get data for every subscription period. * * @since 1.8.2 * * @return array */ protected function get_subscription_period_data() { return [ 'daily' => [ 'name' => 'daily', 'interval' => 'day', 'count' => 1, 'desc' => esc_html__( 'Daily', 'wpforms-lite' ), ], 'weekly' => [ 'name' => 'weekly', 'interval' => 'week', 'count' => 1, 'desc' => esc_html__( 'Weekly', 'wpforms-lite' ), ], 'monthly' => [ 'name' => 'monthly', 'interval' => 'month', 'count' => 1, 'desc' => esc_html__( 'Monthly', 'wpforms-lite' ), ], 'quarterly' => [ 'name' => 'quarterly', 'interval' => 'month', 'count' => 3, 'desc' => esc_html__( 'Quarterly', 'wpforms-lite' ), ], 'semiyearly' => [ 'name' => 'semiyearly', 'interval' => 'month', 'count' => 6, 'desc' => esc_html__( 'Semi-Yearly', 'wpforms-lite' ), ], 'yearly' => [ 'name' => 'yearly', 'interval' => 'year', 'count' => 1, 'desc' => esc_html__( 'Yearly', 'wpforms-lite' ), ], ]; } /** * Create Stripe plan. * * @since 1.8.2 * * @param string $id ID of a plan to create. * @param array $period Subscription period data. * @param array $args Additional arguments. * * @return Plan|null */ protected function create_plan( $id, $period, $args ) { $name = sprintf( '%s (%s %s)', ! empty( $args['settings']['name'] ) ? $args['settings']['name'] : $args['form_title'], $args['amount'], $period['desc'] ); /** * Allow to filter Stripe subscription plan name. * * @since 1.8.8 * * @param string $name Plan name. * @param array $period Subscription period data. * @param array $args Additional arguments. */ $name = (string) apply_filters( 'wpforms_integrations_stripe_api_common_create_plan_name', $name, $period, $args ); $plan_args = [ 'amount' => $args['amount'], 'interval' => $period['interval'], 'interval_count' => $period['count'], 'product' => [ 'name' => sanitize_text_field( $name ), ], 'nickname' => sanitize_text_field( $name ), 'currency' => strtolower( wpforms_get_currency() ), 'id' => $id, 'metadata' => [ 'form_name' => sanitize_text_field( $args['form_title'] ), 'form_id' => $args['form_id'], ], ]; try { $plan = Plan::create( $plan_args, Helpers::get_auth_opts() ); } catch ( \Exception $e ) { $plan = null; } return $plan; } /** * Get Stripe plan ID. * Check if a plan exists in Stripe, if not creates one. * * @since 1.8.2 * * @param array $args Arguments needed for getting a valid plan ID. * * @return string */ protected function get_plan_id( $args ) { $period_data = $this->get_subscription_period_data(); $period = array_key_exists( $args['settings']['period'], $period_data ) ? $period_data[ $args['settings']['period'] ] : $period_data['yearly']; if ( ! empty( $args['settings']['name'] ) ) { $slug = preg_replace( '/[^a-z0-9\-]/', '', strtolower( str_replace( ' ', '-', $args['settings']['name'] ) ) ); } else { $slug = 'form' . $args['form_id']; } $plan_id = sprintf( '%s_%s_%s', $slug, $args['amount'], $period['name'] ); try { $plan = Plan::retrieve( $plan_id, Helpers::get_auth_opts() ); } catch ( \Exception $e ) { $plan = $this->create_plan( $plan_id, $period, $args ); } return isset( $plan->id ) ? $plan->id : ''; } } Integrations/Stripe/Api/WebhookRoute.php 0000644 00000020505 15174710275 0014332 0 ustar 00 <?php namespace WPForms\Integrations\Stripe\Api; use Exception; use WPForms\Integrations\Stripe\Api\Webhooks\Exceptions\AmountMismatchException; use WPForms\Vendor\Stripe\Webhook; use RuntimeException; use BadMethodCallException; use WPForms\Vendor\Stripe\Event as StripeEvent; use WPForms\Vendor\Stripe\Exception\SignatureVerificationException; use WPForms\Integrations\Stripe\Helpers; use WPForms\Integrations\Stripe\WebhooksHealthCheck; /** * Webhooks Rest Route handler. * * @since 1.8.4 */ class WebhookRoute extends Common { /** * Event type. * * @since 1.8.4 * * @var string */ private $event_type = 'unknown'; /** * Payload. * * @since 1.8.4 * * @var array */ private $payload = []; /** * Response. * * @since 1.8.4 * * @var string */ private $response = ''; /** * Response code. * * @since 1.8.4 * * @var int */ private $response_code = 200; /** * Initialize. * * @since 1.8.4 */ public function init() { $this->hooks(); } /** * Register hooks. * * @since 1.8.4 */ private function hooks() { if ( $this->is_rest_verification() ) { add_action( 'rest_api_init', [ $this, 'register_rest_routes' ] ); return; } // Do not serve regular page when it seems Stripe Webhooks are still sending requests to disabled CURL endpoint. // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( isset( $_GET[ Helpers::get_webhook_endpoint_data()['fallback'] ] ) && ( ! Helpers::is_webhook_enabled() || Helpers::is_rest_api_set() ) ) { add_action( 'wp', [ $this, 'dispatch_with_error_500' ] ); return; } if ( ! Helpers::is_webhook_enabled() || ! Helpers::is_webhook_configured() ) { return; } if ( Helpers::is_rest_api_set() ) { add_action( 'rest_api_init', [ $this, 'register_rest_routes' ] ); } else { add_action( 'wp', [ $this, 'dispatch_with_url_param' ] ); } } /** * Register webhook REST route. * * @since 1.8.4 */ public function register_rest_routes() { $methods = [ 'POST' ]; if ( $this->is_rest_verification() ) { $methods[] = 'GET'; } register_rest_route( Helpers::get_webhook_endpoint_data()['namespace'], '/' . Helpers::get_webhook_endpoint_data()['route'], [ 'methods' => $methods, 'callback' => [ $this, 'dispatch_stripe_webhooks_payload' ], 'show_in_index' => false, 'permission_callback' => '__return_true', ] ); } /** * Dispatch Stripe webhooks payload for the url param. * * @since 1.8.4 */ public function dispatch_with_url_param() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( ! isset( $_GET[ Helpers::get_webhook_endpoint_data()['fallback'] ] ) ) { return; } $this->dispatch_stripe_webhooks_payload(); } /** * Dispatch Stripe webhooks payload for the url param with error 500. * * Runs when url param is not configured or webhooks are not enabled at all. * * @since 1.8.4 */ public function dispatch_with_error_500() { $this->response = esc_html__( 'It seems to be request to Stripe PHP Listener method handler but the site is not configured to use it.', 'wpforms-lite' ); $this->response_code = 500; $this->respond(); } /** * Dispatch Stripe webhooks payload. * * @since 1.8.4 */ public function dispatch_stripe_webhooks_payload() { if ( $this->is_rest_verification() ) { wp_send_json_success(); } try { $this->payload = file_get_contents( 'php://input' ); $event = Webhook::constructEvent( $this->payload, $this->get_webhook_signature(), $this->get_webhook_signing_secret() ); // Update webhooks site health status. WebhooksHealthCheck::save_status( WebhooksHealthCheck::ENDPOINT_OPTION, WebhooksHealthCheck::STATUS_OK ); WebhooksHealthCheck::save_status( WebhooksHealthCheck::SIGNATURE_OPTION,WebhooksHealthCheck::STATUS_OK ); $this->event_type = $event->type; $this->response = 'WPForms Stripe: ' . $this->event_type . ' event received.'; $processed = $this->process_event( $event ); $this->response_code = $processed ? 200 : 202; $this->respond(); } catch ( AmountMismatchException $e ) { $this->response_code = 202; $this->response = $e->getMessage(); $this->respond(); } catch ( SignatureVerificationException $e ) { WebhooksHealthCheck::save_status( WebhooksHealthCheck::SIGNATURE_OPTION, WebhooksHealthCheck::STATUS_ERROR ); $this->response_code = 500; $this->response = $e->getMessage(); $this->respond(); } catch ( Exception $e ) { $this->handle_exception( $e ); $this->response = $e->getMessage(); $this->response_code = $e instanceof BadMethodCallException ? 501 : 500; $this->respond(); } } /** * Get webhook stripe signature. * * @since 1.8.4 * * @throws RuntimeException When Stripe signature is not set. * * @return string */ private function get_webhook_signature() { if ( ! isset( $_SERVER['HTTP_STRIPE_SIGNATURE'] ) ) { throw new RuntimeException( 'Stripe signature is not set.' ); } return $_SERVER['HTTP_STRIPE_SIGNATURE']; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash } /** * Get webhook signing secret. * * @since 1.8.4 * * @throws RuntimeException When webhook signing secret is not set. * * @return string */ private function get_webhook_signing_secret() { $secret = wpforms_setting( 'stripe-webhooks-secret-' . Helpers::get_stripe_mode() ); if ( empty( $secret ) ) { throw new RuntimeException( 'Webhook signing secret is not set.' ); } return $secret; } /** * Process Stripe event. * * @since 1.8.4 * * @param StripeEvent $event Stripe event. * * @return bool True if event has handling class, false otherwise. */ private function process_event( StripeEvent $event ) { $webhooks = self::get_event_whitelist(); // Event can't be handled. if ( ! isset( $webhooks[ $event->type ] ) || ! class_exists( $webhooks[ $event->type ] ) ) { return false; } $handler = new $webhooks[ $event->type ](); $handler->setup( $event ); return $handler->handle(); } /** * Get event whitelist. * * @since 1.8.4 * * @return array */ private static function get_event_whitelist() { return [ 'charge.refunded' => Webhooks\ChargeRefunded::class, 'charge.refund.updated' => Webhooks\ChargeRefundUpdated::class, 'invoice.payment_succeeded' => Webhooks\InvoicePaymentSucceeded::class, 'invoice.created' => Webhooks\InvoiceCreated::class, 'charge.succeeded' => Webhooks\ChargeSucceeded::class, 'customer.subscription.created' => Webhooks\CustomerSubscriptionCreated::class, 'customer.subscription.updated' => Webhooks\CustomerSubscriptionUpdated::class, 'customer.subscription.deleted' => Webhooks\CustomerSubscriptionDeleted::class, ]; } /** * Check if rest verification is requested. * * @since 1.8.4 * * @return bool */ private function is_rest_verification() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended return isset( $_GET['verify'] ) && $_GET['verify'] === '1'; } /** * Respond to the request. * * @since 1.8.4 */ private function respond() { $this->log_webhook(); wp_die( esc_html( $this->response ), '', (int) $this->response_code ); } /** * Log webhook request. * * @since 1.8.4 */ private function log_webhook() { // log only if WP_DEBUG_LOG and WPFORMS_WEBHOOKS_DEBUG are set to true. if ( ! defined( 'WPFORMS_WEBHOOKS_DEBUG' ) || ! WPFORMS_WEBHOOKS_DEBUG || ! defined( 'WP_DEBUG_LOG' ) || ! WP_DEBUG_LOG ) { return; } // If it is set to explictly display logs on output, return: this would make response to Stripe malformed. if ( defined( 'WP_DEBUG_DISPLAY' ) && WP_DEBUG_DISPLAY ) { return; } $webhook_log = maybe_serialize( [ 'event_type' => $this->event_type, 'response_code' => $this->response_code, 'response' => $this->response, 'payload' => $this->payload, ] ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log error_log( $webhook_log ); } /** * Get webhooks events list. * * @since 1.8.4 * * @return array */ public static function get_webhooks_events_list() { return array_keys( self::get_event_whitelist() ); } } Integrations/Stripe/Api/DomainManager.php 0000644 00000010655 15174710275 0014424 0 ustar 00 <?php namespace WPForms\Integrations\Stripe\Api; use Exception; use WPForms\Helpers\File; use WPForms\Vendor\Stripe\PaymentMethodDomain; use WPForms\Integrations\Stripe\DomainHealthCheck; use WPForms\Integrations\Stripe\Helpers; /** * Domain Manager. * * @since 1.8.6 */ class DomainManager { /** * Domain status option name. * * @since 1.8.6 */ const STATUS_OPTION = 'wpforms_stripe_domain_status'; /** * Active status. * * @since 1.8.6 */ const STATUS_ACTIVE = 'active'; /** * Inactive status. * * @since 1.8.6 */ const STATUS_INACTIVE = 'inactive'; /** * Validate domain. * * @since 1.8.6 * * @return bool */ public function validate() { if ( $this->is_exists_and_valid() ) { $this->set_status( self::STATUS_ACTIVE ); return true; } ( new DomainHealthCheck() )->maybe_schedule_task(); if ( ! $this->maybe_create_domain_association_file() || ! $this->register() ) { $this->set_status( self::STATUS_INACTIVE ); return false; } $this->set_status( self::STATUS_ACTIVE ); return true; } /** * Set status. * * @since 1.8.6 * * @param string $status Status. */ private function set_status( $status ) { update_option( self::STATUS_OPTION, $status ); } /** * Determine whether domain is active. * * @since 1.8.6 * * @return bool */ public function is_domain_active() { return get_option( self::STATUS_OPTION, self::STATUS_ACTIVE ) === self::STATUS_ACTIVE; } /** * Register domain. * * @since 1.8.6 * * @return bool */ private function register() { try { $domain = PaymentMethodDomain::create( [ 'domain_name' => $this->get_site_domain(), ], Helpers::get_auth_opts() ); } catch ( Exception $e ) { wpforms_log( 'Stripe: Unable to create a domain.', $e->getMessage(), [ 'type' => [ 'payment', 'error' ], ] ); return false; } return $this->is_apple_pay_valid( $domain ); } /** * Check if domain already exists and valid. * * @since 1.8.6 * * @return bool */ private function is_exists_and_valid() { try { $all_domains = PaymentMethodDomain::all( [ 'limit' => 100, ], Helpers::get_auth_opts() ); } catch ( Exception $e ) { wpforms_log( 'Stripe: Unable to get list of domains.', $e->getMessage(), [ 'type' => [ 'payment', 'error' ], ] ); return false; } if ( empty( $all_domains ) || ! isset( $all_domains->data ) ) { return false; } $site_domain = $this->get_site_domain(); foreach ( $all_domains->data as $domain ) { if ( $domain->domain_name !== $site_domain ) { continue; } if ( ! $this->is_apple_pay_valid( $domain ) ) { continue; } return true; } return false; } /** * Verify if Apple Pay active and valid. * * @since 1.8.6 * * @param object $domain Stripe domain object. * * @return bool */ private function is_apple_pay_valid( $domain ) { return isset( $domain->apple_pay ) && $domain->apple_pay->status === 'active'; } /** * Get site domain. * * @since 1.8.6 * * @return string */ private function get_site_domain() { $site_url_parts = wp_parse_url( site_url() ); return $site_url_parts['host']; } /** * Maybe create domain association file. * * @since 1.8.6 * * @return bool */ private function maybe_create_domain_association_file() { $wp_filesystem = File::get_filesystem(); if ( is_null( $wp_filesystem ) ) { return false; } $association_dir = $wp_filesystem->abspath() . '.well-known'; $file_name = 'apple-developer-merchantid-domain-association'; $association_file = $association_dir . '/' . $file_name; // Return early if file already exists. if ( $wp_filesystem->exists( $association_file ) ) { return true; } if ( ! $wp_filesystem->mkdir( $association_dir, 0755 ) ) { $this->log_error( 'Stripe: Unable to create domain association folder in site root.' ); return false; } if ( ! $wp_filesystem->copy( WPFORMS_PLUGIN_DIR . 'src/Integrations/Stripe/' . $file_name, $association_file, true ) ) { $this->log_error( 'Stripe: Unable to copy domain association file to domain .well-known directory.' ); return false; } return true; } /** * Log error message. * * @since 1.8.6 * * @param string $error Error message. */ private function log_error( $error ) { wpforms_log( $error, '', [ 'type' => [ 'payment', 'error' ], ] ); } } Integrations/Stripe/Api/PaymentIntents.php 0000644 00000054274 15174710275 0014711 0 ustar 00 <?php namespace WPForms\Integrations\Stripe\Api; use WPForms\Vendor\Stripe\Mandate; use WPForms\Vendor\Stripe\SetupIntent; use WPForms\Vendor\Stripe\PaymentIntent; use WPForms\Vendor\Stripe\Stripe; use WPForms\Vendor\Stripe\Subscription; use WPForms\Vendor\Stripe\Refund; use WPForms\Vendor\Stripe\Exception\ApiErrorException; use WPForms\Integrations\Stripe\Fields\StripeCreditCard; use WPForms\Integrations\Stripe\Fields\PaymentElementCreditCard; use WPForms\Integrations\Stripe\Helpers; use WPForms\Helpers\Crypto; use Exception; use WPForms\Vendor\Stripe\Charge; use WPForms\Vendor\Stripe\CountrySpec; /** * Stripe PaymentIntents API. * * @since 1.8.2 */ class PaymentIntents extends Common implements ApiInterface { /** * Stripe PaymentMethod id received from Elements. * * @since 1.8.2 * * @var string */ protected $payment_method_id; /** * Stripe PaymentIntent id received from Elements. * * @since 1.8.2 * * @var string */ protected $payment_intent_id; /** * Stripe PaymentIntent object. * * @since 1.8.2 * * @var PaymentIntent */ protected $intent; /** * API config data. * * @since 1.8.2 * * @var array */ protected $config; /** * Initialize. * * @since 1.8.2 * * @return PaymentIntents */ public function init() { $this->set_config(); $this->load_card_field(); $this->hooks(); return $this; } /** * Register hooks. * * @since 1.8.2 */ private function hooks() { add_filter( 'wpforms_process_bypass_captcha', [ $this, 'bypass_captcha_on_3dsecure_submit' ], 10, 3 ); } /** * Load Credit Card Field Class. * * @since 1.8.2 */ private function load_card_field() { if ( Helpers::is_payment_element_enabled() ) { new PaymentElementCreditCard(); return; } new StripeCreditCard(); } /** * Set API configuration. * * @since 1.8.2 */ public function set_config() { $localize_script = [ 'element_locale' => $this->filter_config_element_locale(), ]; $this->config = [ 'remote_js_url' => 'https://js.stripe.com/v3/', 'field_slug' => 'stripe-credit-card', 'localize_script' => $localize_script, ]; if ( Helpers::is_payment_element_enabled() ) { $this->set_payment_element_config(); return; } $this->set_card_element_config(); } /** * Set API configuration for Payment Element. * * @since 1.8.2 */ private function set_payment_element_config() { $min = wpforms_get_min_suffix(); /** * This filter allows to overwrite a Payment element appearance object. * * @since 1.8.5 * * @link https://stripe.com/docs/elements/appearance-api * * @param array $appearance Appearance object. */ $element_style = (array) apply_filters( 'wpforms_integrations_stripe_api_payment_intents_set_element_appearance', [] ); $this->config['localize_script']['element_appearance'] = $element_style; $this->config['local_js_url'] = WPFORMS_PLUGIN_URL . "assets/js/integrations/stripe/wpforms-stripe-payment-element{$min}.js"; $this->config['local_css_url'] = WPFORMS_PLUGIN_URL . "assets/css/integrations/stripe/wpforms-stripe{$min}.css"; } /** * Set API configuration for Card Element. * * @since 1.8.2 */ private function set_card_element_config() { /** * This filter allows to overwrite a Style object, which consists of CSS properties nested under objects. * * @since 1.8.2 * * @link https://stripe.com/docs/js/appendix/style * * @param array $styles Style object. */ $element_style = (array) apply_filters( 'wpforms_stripe_api_payment_intents_set_config_element_style', [] ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName $this->config['localize_script']['element_style'] = $element_style; $this->config['localize_script']['element_classes'] = [ 'base' => 'wpforms-stripe-element', 'complete' => 'wpforms-stripe-element-complete', 'empty' => 'wpforms-stripe-element-empty', 'focus' => 'wpforms-stripe-element-focus', 'invalid' => 'wpforms-stripe-element-invalid', 'webkitAutofill' => 'wpforms-stripe-element-webkit-autofill', ]; $min = wpforms_get_min_suffix(); $this->config['local_js_url'] = WPFORMS_PLUGIN_URL . "assets/js/integrations/stripe/wpforms-stripe-elements{$min}.js"; } /** * Get stripe locale. * * @since 1.8.2 * * @return string */ public function filter_config_element_locale() { /** * WPForms Stripe Api payment intent element locale. * * @since 1.8.2 * * @param string $locale Element locale. */ $locale = apply_filters( 'wpforms_stripe_api_payment_intents_filter_config_element_locale', '' ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName // Stripe Elements makes its own locale validation, but we add a general sanity check. return strlen( $locale ) === 2 ? esc_html( $locale ) : 'auto'; } /** * Initial Stripe app configuration. * * @since 1.8.2 */ public function setup_stripe() { parent::setup_stripe(); Stripe::setApiVersion( '2019-05-16' ); } /** * Set payment tokens from a submitted form data. * * @since 1.8.2 * * @param array $entry Copy of original $_POST. */ public function set_payment_tokens( $entry ) { if ( ! empty( $entry['payment_method_id'] ) && empty( $entry['payment_intent_id'] ) ) { $this->payment_method_id = $entry['payment_method_id']; } if ( ! empty( $entry['payment_intent_id'] ) ) { $this->payment_intent_id = $entry['payment_intent_id']; } if ( empty( $this->payment_method_id ) && empty( $this->payment_intent_id ) ) { $this->error = esc_html__( 'Stripe payment stopped, missing both PaymentMethod and PaymentIntent ids.', 'wpforms-lite' ); } } /** * Retrieve PaymentIntent object from Stripe. * * @since 1.8.2 * @since 1.8.7 Changed method visibility. * * @param string $id PaymentIntent id. * @param array $args Additional arguments (e.g. 'expand'). * * @throws ApiErrorException If the request fails. * * @return PaymentIntent|null */ public function retrieve_payment_intent( $id, $args = [] ) { try { $defaults = [ 'id' => $id ]; if ( isset( $args['mode'] ) ) { $auth_opts = [ 'api_key' => Helpers::get_stripe_key( 'secret', $args['mode'] ) ]; unset( $args['mode'] ); } $args = wp_parse_args( $args, $defaults ); return PaymentIntent::retrieve( $args, $auth_opts ?? Helpers::get_auth_opts() ); } catch ( Exception $e ) { $this->handle_exception( $e ); } return null; } /** * Process single payment. * * @since 1.8.2 * * @param array $args Single payment arguments. * * @throws ApiErrorException If the request fails. */ public function process_single( $args ) { if ( $this->payment_method_id ) { // Normal flow. $this->charge_single( $args ); } elseif ( $this->payment_intent_id ) { // 3D Secure flow. $this->finalize_single(); } } /** * Refund a payment. * * @since 1.8.4 * @since 1.8.8.2 $args param was added. * * @param string $payment_intent_id PaymentIntent id. * @param array $args Additional arguments (e.g. 'mode', 'metadata', 'reason' ). * * @return bool */ public function refund_payment( string $payment_intent_id, array $args = [] ): bool { try { $intent = $this->retrieve_payment_intent( $payment_intent_id ); if ( ! $intent ) { return false; } $defaults = [ 'payment_intent' => $payment_intent_id, ]; if ( isset( $args['mode'] ) ) { $auth_opts = [ 'api_key' => Helpers::get_stripe_key( 'secret', $args['mode'] ) ]; unset( $args['mode'] ); } $args = wp_parse_args( $args, $defaults ); $refund = Refund::create( $args, $auth_opts ?? Helpers::get_auth_opts() ); if ( ! $refund ) { return false; } } catch ( Exception $e ) { $this->handle_exception( $e ); return false; } return true; } /** * Get a charge. * * @since 1.8.4 * * @param string $charge_id Charge id. * * @return Charge|bool */ public function get_charge( $charge_id ) { try { $charge = Charge::retrieve( $charge_id, Helpers::get_auth_opts() ); if ( ! $charge ) { return false; } } catch ( Exception $e ) { $this->handle_exception( $e ); return false; } return $charge; } /** * Cancel a subscription. * * @since 1.8.4 * * @param string $subscription_id Subscription id. * * @return bool */ public function cancel_subscription( $subscription_id ) { try { $subscription = Subscription::retrieve( $subscription_id, Helpers::get_auth_opts() ); if ( ! $subscription ) { return false; } Subscription::update( $subscription_id, [ 'metadata' => array_merge( $subscription->metadata->values(), [ 'canceled_by' => 'wpforms_dashboard', ] ), ], Helpers::get_auth_opts() ); $subscription->cancel(); } catch ( Exception $e ) { $this->handle_exception( $e ); return false; } return true; } /** * Request a single payment charge to be made by Stripe. * * @since 1.8.2 * * @param array $args Single payment arguments. */ protected function charge_single( $args ) { // phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh if ( empty( $this->payment_method_id ) ) { $this->error = esc_html__( 'Stripe payment stopped, missing PaymentMethod id.', 'wpforms-lite' ); return; } $defaults = [ 'payment_method' => $this->payment_method_id, 'confirm' => true, 'automatic_payment_methods' => [ 'enabled' => true, 'allow_redirects' => 'never', ], ]; $args = wp_parse_args( $args, $defaults ); try { if ( isset( $args['customer_email'] ) || isset( $args['customer_name'] ) || isset( $args['customer_phone'] ) ) { $this->set_customer( $args['customer_email'] ?? '', $args['customer_name'] ?? '', $args['customer_address'] ?? [], $args['customer_phone'] ?? '', $args['customer_metadata'] ?? [] ); $args['customer'] = $this->get_customer( 'id' ); } unset( $args['customer_email'], $args['customer_name'], $args['customer_address'], $args['customer_phone'], $args['customer_metadata'] ); $this->intent = PaymentIntent::create( $args, Helpers::get_auth_opts() ); if ( ! in_array( $this->intent->status, [ 'succeeded', 'requires_action', 'requires_confirmation' ], true ) ) { $this->error = esc_html__( 'Stripe payment stopped. Invalid PaymentIntent status.', 'wpforms-lite' ); return; } if ( $this->intent->status === 'succeeded' ) { return; } $this->set_bypass_captcha_3dsecure_token( $args ); if ( $this->intent->status === 'requires_confirmation' ) { $this->request_confirm_payment_ajax( $this->intent ); } $this->request_3dsecure_ajax( $this->intent ); } catch ( Exception $e ) { $this->handle_exception( $e ); } } /** * Finalize single payment after 3D Secure authorization is finished successfully. * * @since 1.8.2 * * @throws ApiErrorException If the request fails. */ protected function finalize_single() { // Saving payment info is important for a future form entry meta update. $this->intent = $this->retrieve_payment_intent( $this->payment_intent_id, [ 'expand' => [ 'customer' ] ] ); if ( $this->intent->status !== 'succeeded' ) { // This error is unlikely to happen because the same check is done on a frontend. $this->error = esc_html__( 'Stripe payment was not confirmed. Please check your Stripe dashboard.', 'wpforms-lite' ); return; } // Saving customer and subscription info is important for a future form meta update. $this->customer = $this->intent->customer; } /** * Process subscription. * * @since 1.8.2 * * @param array $args Subscription arguments. * * @throws ApiErrorException If the request fails. */ public function process_subscription( $args ) { if ( $this->payment_method_id ) { // Normal flow. $this->charge_subscription( $args ); } elseif ( $this->payment_intent_id ) { // 3D Secure flow. $this->finalize_subscription(); } } /** * Request a subscription charge to be made by Stripe. * * @since 1.8.2 * * @param array $args Subscription payment arguments. */ protected function charge_subscription( $args ) { // phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh if ( empty( $this->payment_method_id ) ) { $this->error = esc_html__( 'Stripe subscription stopped, missing PaymentMethod id.', 'wpforms-lite' ); return; } $sub_args = [ 'items' => [ [ 'plan' => $this->get_plan_id( $args ), ], ], 'metadata' => [ 'form_name' => $args['form_title'], 'form_id' => $args['form_id'], 'cycles' => $args['cycles'] ?? null, ], 'expand' => [ 'latest_invoice.payment_intent' ], ]; if ( isset( $args['application_fee_percent'] ) ) { $sub_args['application_fee_percent'] = $args['application_fee_percent']; } if ( isset( $args['description'] ) ) { $sub_args['description'] = $args['description']; } try { $this->set_customer( $args['email'], $args['customer_name'] ?? '', $args['customer_address'] ?? [], $args['customer_phone'] ?? '', $args['customer_metadata'] ?? [] ); $sub_args['customer'] = $this->get_customer( 'id' ); $sub_args['payment_behavior'] = 'default_incomplete'; $sub_args['off_session'] = true; $sub_args['payment_settings'] = [ 'save_default_payment_method' => 'on_subscription', ]; if ( Helpers::is_link_supported() ) { $sub_args['payment_settings']['payment_method_types'] = [ 'card', 'link' ]; } // Create the subscription. $this->subscription = Subscription::create( $sub_args, Helpers::get_auth_opts() ); $this->intent = $this->subscription->latest_invoice->payment_intent; if ( ! $this->intent || ! in_array( $this->intent->status, [ 'succeeded', 'requires_action', 'requires_confirmation', 'requires_payment_method' ], true ) ) { $this->error = esc_html__( 'Stripe subscription stopped. invalid PaymentIntent status.', 'wpforms-lite' ); return; } if ( $this->intent->status === 'succeeded' ) { return; } $this->set_bypass_captcha_3dsecure_token( $args ); if ( in_array( $this->intent->status , [ 'requires_confirmation', 'requires_payment_method' ], true ) ) { $this->request_confirm_payment_ajax( $this->intent ); } $this->request_3dsecure_ajax( $this->intent ); } catch ( Exception $e ) { $this->handle_exception( $e ); } } /** * Finalize a subscription after 3D Secure authorization is finished successfully. * * @since 1.8.2 * * @throws ApiErrorException If the request fails. */ protected function finalize_subscription() { // Saving payment info is important for a future form entry meta update. $this->intent = $this->retrieve_payment_intent( $this->payment_intent_id, [ 'expand' => [ 'invoice.subscription', 'customer' ] ] ); if ( $this->intent->status !== 'succeeded' ) { // This error is unlikely to happen because the same check is done on a frontend. $this->error = esc_html__( 'Stripe subscription was not confirmed. Please check your Stripe dashboard.', 'wpforms-lite' ); return; } // Saving customer and subscription info is important for a future form meta update. $this->customer = $this->intent->customer; $this->subscription = $this->intent->invoice->subscription; } /** * Get saved Stripe PaymentIntent object or its key. * * @since 1.8.2 * * @param string $key Name of the key to retrieve. * * @return mixed */ public function get_payment( $key = '' ) { return $this->get_var( 'intent', $key ); } /** * Get details from a saved Charge object. * * @since 1.8.2 * * @param string|array $keys Key or an array of keys to retrieve. * * @return array */ public function get_charge_details( $keys ) { $charge = isset( $this->intent->charges->data[0] ) ? $this->intent->charges->data[0] : null; if ( empty( $charge ) || empty( $keys ) ) { return []; } if ( is_string( $keys ) ) { $keys = [ $keys ]; } $result = []; foreach ( $keys as $key ) { if ( isset( $charge->payment_method_details->card, $charge->payment_method_details->card->{$key} ) ) { $result[ $key ] = sanitize_text_field( $charge->payment_method_details->card->{$key} ); continue; } if ( isset( $charge->payment_method_details->{$key} ) ) { $result[ $key ] = sanitize_text_field( $charge->payment_method_details->{$key} ); continue; } if ( isset( $charge->billing_details->{$key} ) ) { $result[ $key ] = sanitize_text_field( $charge->billing_details->{$key} ); } } return $result; } /** * Request a frontend 3D Secure authorization from a user. * * @since 1.8.2 * * @param PaymentIntent $intent PaymentIntent to authorize. */ protected function request_3dsecure_ajax( $intent ) { if ( ! isset( $intent->status, $intent->next_action->type ) ) { return; } if ( $intent->status !== 'requires_action' || $intent->next_action->type !== 'use_stripe_sdk' ) { return; } wp_send_json_success( [ 'action_required' => true, 'payment_intent_client_secret' => $intent->client_secret, 'payment_method_id' => $this->payment_method_id, ] ); } /** * Request a frontend payment confirmation from a user. * * @since 1.8.2 * * @param PaymentIntent $intent PaymentIntent to authorize. */ protected function request_confirm_payment_ajax( $intent ) { wp_send_json_success( [ 'action_required' => true, 'payment_intent_client_secret' => $intent->client_secret, 'payment_method_id' => $this->payment_method_id, ] ); } /** * Set an encrypted token as a PaymentIntent metadata item. * * @since 1.8.2 * @since 1.9.6 Added $args parameter. * * @param array $args Additional arguments. * * @throws ApiErrorException In case payment intent save wasn't successful. */ private function set_bypass_captcha_3dsecure_token( array $args = [] ) { $form_data = wpforms()->obj( 'process' )->form_data; // Set token only if captcha is enabled for the form. if ( empty( $form_data['settings']['recaptcha'] ) ) { return; } $this->intent->metadata['captcha_3dsecure_token'] = Crypto::encrypt( $this->intent->id ); $this->intent->metadata['spam_reason'] = $args['metadata']['spam_reason'] ?? null; $this->intent->update( $this->intent->id, $this->intent->serializeParameters(), Helpers::get_auth_opts() ); } /** * Bypass CAPTCHA check on successful 3dSecure check. * * @since 1.8.2 * * @param bool $is_bypassed True if CAPTCHA is bypassed. * @param array $entry Form entry data. * @param array $form_data Form data and settings. * * @return bool * * @throws ApiErrorException In case payment intent save wasn't successful. */ public function bypass_captcha_on_3dsecure_submit( $is_bypassed, $entry, $form_data ) { // Firstly, run checks that may prevent bypassing: // 1) Sanity check to prevent possible tinkering with captcha on non-payment forms. // 2) All Captcha providers are enabled by the same setting. if ( ! Helpers::is_payments_enabled( $form_data ) || empty( $form_data['settings']['recaptcha'] ) || empty( $entry['payment_intent_id'] ) ) { return $is_bypassed; } // This is executed before payment processing kicks in and fills `$this->intent`. // PaymentIntent intent has to be retrieved from Stripe instead of getting it from `$this->intent`. $intent = $this->retrieve_payment_intent( $entry['payment_intent_id'] ); if ( empty( $intent->status ) || $intent->status !== 'succeeded' ) { return $is_bypassed; } $token = ! empty( $intent->metadata['captcha_3dsecure_token'] ) ? $intent->metadata['captcha_3dsecure_token'] : ''; if ( Crypto::decrypt( $token ) !== $intent->id ) { return $is_bypassed; } // Cleanup the token to prevent its repeated usage and declutter the metadata. $intent->metadata['captcha_3dsecure_token'] = null; $intent->update( $intent->id, $intent->serializeParameters(), Helpers::get_auth_opts() ); if ( isset( $intent->metadata['spam_reason'] ) ) { return $is_bypassed; } return true; } /** * Retrieve Mandate object from Stripe. * * @since 1.8.7 * * @param string $id Mandate id. * @param array $args Additional arguments. * * @throws ApiErrorException If the request fails. * * @return Mandate|null */ public function retrieve_mandate( string $id, array $args = [] ) { try { $defaults = [ 'id' => $id ]; if ( isset( $args['mode'] ) ) { $auth_opts = [ 'api_key' => Helpers::get_stripe_key( 'secret', $args['mode'] ) ]; unset( $args['mode'] ); } $args = wp_parse_args( $args, $defaults ); return Mandate::retrieve( $args, $auth_opts ?? Helpers::get_auth_opts() ); } catch ( Exception $e ) { wpforms_log( 'Stripe: Unable to get Mandate.', $e->getMessage(), [ 'type' => [ 'payment', 'error' ], ] ); } return null; } /** * Create Stripe Setup Intent. * * @since 1.8.7 * * @param array $intent_data Intent data. * @param array $args Additional arguments. * * @throws ApiErrorException If the request fails. * * @return SetupIntent|null */ public function create_setup_intent( array $intent_data, array $args ) { try { if ( isset( $args['mode'] ) ) { $auth_opts = [ 'api_key' => Helpers::get_stripe_key( 'secret', $args['mode'] ) ]; } return SetupIntent::create( $intent_data, $auth_opts ?? Helpers::get_auth_opts() ); } catch ( Exception $e ) { wpforms_log( 'Stripe: Unable to create Setup Intent.', $e->getMessage(), [ 'type' => [ 'payment', 'error' ], ] ); } return null; } /** * Get Country Specs. * * @since 1.9.1 * * @param string $country Country code. * @param array $args Additional arguments. * * @throws ApiErrorException If the request fails. * * @return CountrySpec|null */ public function get_country_specs( string $country, array $args = [] ) { try { if ( isset( $args['mode'] ) ) { $auth_opts = [ 'api_key' => Helpers::get_stripe_key( 'secret', $args['mode'] ) ]; } return CountrySpec::retrieve( $country, $auth_opts ?? Helpers::get_auth_opts() ); } catch ( Exception $e ) { wpforms_log( 'Stripe: Unable to get Country specs.', $e->getMessage(), [ 'type' => [ 'payment', 'error' ], ] ); } return null; } } Integrations/Stripe/Api/ApiInterface.php 0000644 00000004177 15174710275 0014256 0 ustar 00 <?php namespace WPForms\Integrations\Stripe\Api; use Exception; /** * Payment API interface. * * @since 1.8.2 */ interface ApiInterface { /** * API class initialization. * * @since 1.8.2 */ public function init(); /** * Set API configuration. * * @since 1.8.2 */ public function set_config(); /** * Initial Stripe app configuration. * * @since 1.8.2 */ public function setup_stripe(); /** * Set payment tokens from a submitted form data. * * @since 1.8.2 * * @param array $entry Copy of original $_POST. */ public function set_payment_tokens( $entry ); /** * Process single payment. * * @since 1.8.2 * * @param array $args Single payment arguments. */ public function process_single( $args ); /** * Process subscription. * * @since 1.8.2 * * @param array $args Subscription arguments. */ public function process_subscription( $args ); /** * Get API configuration array or its key. * * @since 1.8.2 * * @param string $key Name of the key to retrieve. * * @return mixed */ public function get_config( $key = '' ); /** * Get saved Stripe payment object or its key. * * @since 1.8.2 * * @param string $key Name of the key to retrieve. * * @return mixed */ public function get_payment( $key = '' ); /** * Get saved Stripe customer object or its key. * * @since 1.8.2 * * @param string $key Name of the key to retrieve. * * @return mixed */ public function get_customer( $key = '' ); /** * Get saved Stripe subscription object or its key. * * @since 1.8.2 * * @param string $key Name of the key to retrieve. * * @return mixed */ public function get_subscription( $key = '' ); /** * Get details from a saved Charge object. * * @since 1.8.2 * * @param string|array $keys Key or an array of keys to retrieve. * * @return array */ public function get_charge_details( $keys ); /** * Get API error message. * * @since 1.8.2 * * @return string */ public function get_error(); /** * Get API exception. * * @since 1.8.2 * * @return Exception */ public function get_exception(); } Integrations/Stripe/Api/WebhooksManager.php 0000644 00000013125 15174710275 0014771 0 ustar 00 <?php namespace WPForms\Integrations\Stripe\Api; use Exception; use WPForms\Vendor\Stripe\WebhookEndpoint; use WPForms\Integrations\Stripe\Helpers; use WPForms\Integrations\Stripe\WebhooksHealthCheck; /** * Webhooks Manager. * * @since 1.8.4 */ class WebhooksManager { /** * API version. * * @since 1.8.4 * * @var string */ const STRIPE_API_VERSION = '2023-08-16'; /** * Determine whether a webhook endpoint is valid. * * @since 1.8.4 * * @return bool */ public function is_valid(): bool { $webhook_id = $this->get_id(); // It's not valid if either endpoint ID or secret is empty. if ( empty( $webhook_id ) || empty( $this->get_secret() ) ) { return false; } $webhook = $this->get( $webhook_id ); if ( ! $webhook || $webhook->status !== 'enabled' || $webhook->url !== Helpers::get_webhook_url() || ! empty( array_diff( WebhookRoute::get_webhooks_events_list(), $webhook->enabled_events ) ) // Has unconfigured events. ) { return false; } return true; } /** * Retrieve a webhook endpoint ID. * * @since 1.8.4 * * @return string */ public function get_id() { return wpforms_setting( 'stripe-webhooks-id-' . Helpers::get_stripe_mode(), '' ); } /** * Retrieve a webhook endpoint secret. * * @since 1.8.4 * * @return string */ private function get_secret() { return wpforms_setting( 'stripe-webhooks-secret-' . Helpers::get_stripe_mode(), '' ); } /** * Retrieve a webhook endpoint object. * * @since 1.8.4 * * @param string $webhook_id Endpoint ID. * * @return WebhookEndpoint|null */ private function get( $webhook_id ) { try { $webhook = WebhookEndpoint::retrieve( $webhook_id, Helpers::get_auth_opts() ); } catch ( Exception $e ) { return null; } return $webhook; } /** * Update a webhook endpoint. * * @since 1.8.4 * * @param string $id Endpoint ID. * @param array $params Params. * * @return WebhookEndpoint|null */ public function update( $id, $params ) { try { $webhook = WebhookEndpoint::update( $id, $params, Helpers::get_auth_opts() ); } catch ( Exception $e ) { return null; } return $webhook; } /** * Connect webhook endpoint. * * Remove existing endpoints and create a new one. * * @since 1.8.4 * * @return bool */ public function connect(): bool { // Prevent duplication of endpoints. if ( $this->is_valid() ) { return true; } // Clean up existing endpoints. $this->cleanup(); // Always create a new because you can't get a secret for existing. $webhook = $this->create(); // Register AS task. ( new WebhooksHealthCheck() )->maybe_schedule_task(); // Store endpoint ID and secret. if ( $webhook ) { $this->save_settings( $webhook ); return true; } return false; } /** * Cleanup endpoints. * * @since 1.8.4 * * @return bool */ private function cleanup() { try { $webhooks = $this->get_all(); if ( ! $webhooks ) { return false; } $valid_urls = $this->get_valid_urls(); foreach ( $webhooks as $wh ) { if ( in_array( $wh->url, $valid_urls, true ) ) { $wh->delete(); } } } catch ( Exception $e ) { return false; } return true; } /** * Retrieve possible endpoint URLs. * * @since 1.8.4 * * @return array */ private function get_valid_urls() { $urls = [ Helpers::get_webhook_url_for_rest(), Helpers::get_webhook_url_for_curl(), ]; if ( defined( 'WPFORMS_STRIPE_WHURL' ) ) { $urls[] = WPFORMS_STRIPE_WHURL; } return $urls; } /** * Create a webhook endpoint. * * @since 1.8.4 * * @return WebhookEndpoint|bool */ private function create() { try { $webhook = WebhookEndpoint::create( [ 'url' => Helpers::get_webhook_url(), 'enabled_events' => WebhookRoute::get_webhooks_events_list(), 'connect' => false, 'api_version' => self::STRIPE_API_VERSION, 'description' => sprintf( 'WPForms endpoint (%1$s mode)', Helpers::get_stripe_mode() ), ], Helpers::get_auth_opts() ); } catch ( Exception $e ) { return false; } return $webhook; } /** * Get list of all webhook endpoints. * * @since 1.8.4 * * @return array */ private function get_all(): array { try { $webhooks = WebhookEndpoint::all( [], Helpers::get_auth_opts() ); } catch ( Exception $e ) { return []; } return isset( $webhooks->data ) ? (array) $webhooks->data : []; } /** * Save webhook settings. * * @since 1.8.4 * * @param WebhookEndpoint $webhook Webhook endpoint. */ private function save_settings( $webhook ) { $mode = Helpers::get_stripe_mode(); $settings = (array) get_option( 'wpforms_settings', [] ); // Save webhooks endpoint ID. $settings[ 'stripe-webhooks-id-' . $mode ] = sanitize_text_field( $webhook->id ); // Store webhooks endpoint secret, but it is not defined on ::update() call. if ( ! empty( $webhook->secret ) ) { $settings[ 'stripe-webhooks-secret-' . $mode ] = sanitize_text_field( $webhook->secret ); WebhooksHealthCheck::save_status( WebhooksHealthCheck::SIGNATURE_OPTION, WebhooksHealthCheck::STATUS_OK ); } WebhooksHealthCheck::save_status( WebhooksHealthCheck::ENDPOINT_OPTION, WebhooksHealthCheck::STATUS_OK ); // Enable webhooks setting shouldn't be rewritten. if ( ! isset( $settings['stripe-webhooks-enabled'] ) ) { $settings['stripe-webhooks-enabled'] = true; } update_option( 'wpforms_settings', $settings ); } /** * Disconnect webhook endpoints. * * @since 1.9.8 * * @return void */ public function disconnect(): void { $this->cleanup(); } } Integrations/Stripe/Api/Webhooks/InvoicePaymentSucceeded.php 0000644 00000004662 15174710275 0020243 0 ustar 00 <?php namespace WPForms\Integrations\Stripe\Api\Webhooks; use RuntimeException; use Stripe\Exception\ApiErrorException; use WPForms\Db\Payments\Queries; use WPForms\Integrations\Stripe\Helpers; use WPForms\Vendor\Stripe\PaymentIntent; /** * Webhook invoice.payment_succeeded class. * * @since 1.8.4 */ class InvoicePaymentSucceeded extends Base { /** * Handle invoice.payment_succeeded webhook for subscription_cycle billing reason (payment renewal). * * @since 1.8.4 * * @throws RuntimeException If subscription not found or not updated. * * @return bool */ public function handle() { if ( ! isset( $this->data->object->billing_reason ) || $this->data->object->billing_reason !== 'subscription_cycle' ) { return false; // Webhook handler for Invoice.PaymentSucceeded with reason subscription_cycle not implemented yet. } if ( $this->data->object->paid !== true ) { return false; // Subscription not paid, so we are not going to proceed with update. } $db_renewal = ( new Queries() )->get_renewal_by_invoice_id( $this->data->object->id ); if ( is_null( $db_renewal ) ) { return false; // Newest renewal not found. } $currency = strtoupper( $this->data->object->currency ); $amount = $this->data->object->amount_paid / wpforms_get_currency_multiplier( $currency ); wpforms()->obj( 'payment' )->update( $db_renewal->id, [ 'total_amount' => $amount, 'subtotal_amount' => $amount, 'status' => 'completed', 'transaction_id' => $this->data->object->payment_intent, ] ); $this->copy_meta_from_payment_intent( $db_renewal->id ); wpforms()->obj( 'payment_meta' )->add_log( $db_renewal->id, sprintf( 'Stripe renewal was successfully paid. (Payment Intent ID: %1$s)', $this->data->object->payment_intent ) ); return true; } /** * Copy meta from payment intent. * * @since 1.8.4 * * @param int $renewal_id Renewal ID. * * @noinspection PhpMissingParamTypeInspection */ private function copy_meta_from_payment_intent( $renewal_id ) { try { $payment_intent = PaymentIntent::retrieve( $this->data->object->payment_intent, Helpers::get_auth_opts() ); } catch ( ApiErrorException $e ) { $payment_intent = null; } if ( ! isset( $payment_intent->charges->data[0]->payment_method_details ) ) { return; } $this->update_payment_method_details( $renewal_id, $payment_intent->charges->data[0]->payment_method_details ); } } Integrations/Stripe/Api/Webhooks/CustomerSubscriptionUpdated.php 0000644 00000002615 15174710275 0021215 0 ustar 00 <?php namespace WPForms\Integrations\Stripe\Api\Webhooks; use RuntimeException; use WPForms\Db\Payments\ValueValidator; /** * Webhook customer.subscription.updated class. * * @since 1.8.4 */ class CustomerSubscriptionUpdated extends Base { /** * Handle the Webhook's data. * * @since 1.8.4 * * @throws RuntimeException If payment not found or not updated. * * @return bool */ public function handle() { $this->delay(); if ( ( isset( $this->data->object->metadata->canceled_by ) && $this->data->object->metadata->canceled_by === 'wpforms_dashboard' ) || ! ValueValidator::is_valid( $this->data->object->status, 'status' ) ) { return false; } $payment = wpforms()->obj( 'payment' )->get_by( 'subscription_id', $this->data->object->id ); if ( ! $payment ) { return false; } if ( $payment->subscription_status === $this->data->object->status || ( ! empty( $this->data->previous_attributes->status ) && $this->data->previous_attributes->status !== $payment->subscription_status ) ) { return true; } if ( ! wpforms()->obj( 'payment' )->update( $payment->id, [ 'subscription_status' => $this->data->object->status ] ) ) { throw new RuntimeException( 'Payment not updated' ); } wpforms()->obj( 'payment_meta' )->add_log( $payment->id, sprintf( 'Stripe subscription was set to %1$s.', $this->data->object->status ) ); return true; } } Integrations/Stripe/Api/Webhooks/InvoiceCreated.php 0000644 00000010743 15174710275 0016365 0 ustar 00 <?php namespace WPForms\Integrations\Stripe\Api\Webhooks; use RuntimeException; use Exception; use WPForms\Vendor\Stripe\Invoice; use WPForms\Integrations\Stripe\Helpers; use WPForms\Db\Payments\Queries; /** * Webhook invoice.created class. * * @since 1.8.4 */ class InvoiceCreated extends Base { /** * Handle invoice.created webhook. * * @since 1.8.4 * * @throws RuntimeException If original subscription not found or not updated. * * @return bool */ public function handle() { if ( ! isset( $this->data->object->billing_reason ) || $this->data->object->billing_reason !== 'subscription_cycle' ) { return false; // Webhook handler for Invoice.Create supports only billing_reason = subscription_cycle. } $original_subscription = ( new Queries() )->get_subscription( $this->data->object->subscription ); if ( is_null( $original_subscription ) ) { return false; // Original subscription not found. } $renewal = ( new Queries() )->get_renewal_by_invoice_id( $this->data->object->id ); if ( ! is_null( $renewal ) ) { return false; // Renewal already exists. } $renewal_id = $this->insert_renewal( $original_subscription ); if ( ! $renewal_id ) { throw new RuntimeException( 'Subscription renewal not saved in database' ); } $this->insert_renewal_meta( $renewal_id, $original_subscription ); wpforms()->obj( 'payment_meta' )->add_log( $renewal_id, sprintf( 'Stripe renewal was created (Invoice ID: %1$s).', $this->data->object->id ) ); $this->finalize_invoice(); return true; } /** * Insert renewal. * * @since 1.8.4 * * @param object $original_subscription Original subscription. * * @return int|false */ private function insert_renewal( $original_subscription ) { $currency = strtoupper( $this->data->object->currency ); $amount = $this->data->object->amount_due / wpforms_get_currency_multiplier( $currency ); return wpforms()->obj( 'payment' )->add( [ 'mode' => $original_subscription->mode, 'form_id' => isset( $original_subscription->form_id ) ? $original_subscription->form_id : 0, 'entry_id' => isset( $original_subscription->entry_id ) ? $original_subscription->entry_id : 0, 'status' => 'pending', 'type' => 'renewal', 'gateway' => 'stripe', 'title' => $original_subscription->title, 'subtotal_amount' => $amount, 'total_amount' => $amount, 'currency' => $currency, 'transaction_id' => '', 'subscription_id' => $original_subscription->subscription_id, 'customer_id' => $original_subscription->customer_id, 'date_created_gmt' => gmdate( 'Y-m-d H:i:s', $this->data->object->lines->data[0]->period->start ), 'date_updated_gmt' => gmdate( 'Y-m-d H:i:s' ), ] ); } /** * Insert renewal meta. * * @since 1.8.4 * * @param int $renewal_id Renewal ID. * @param object $original_subscription Original subscription. */ private function insert_renewal_meta( $renewal_id, $original_subscription ) { $meta = $this->copy_meta_from_db( $original_subscription->id ); $meta['invoice_id'] = $this->data->object->id; $meta['customer_email'] = isset( $this->data->object->customer_email ) ? $this->data->object->customer_email : ''; wpforms()->obj( 'payment_meta' )->bulk_add( $renewal_id, $meta ); } /** * Copy meta from original subscription. * * @since 1.8.4 * * @param int $original_subscription_id Original subscription ID. * * @return array */ private function copy_meta_from_db( $original_subscription_id ) { $all_meta = wpforms()->obj( 'payment_meta' )->get_all( $original_subscription_id ); $db_meta_keys = [ 'fields', 'subscription_period', 'coupon_value', 'coupon_info', 'coupon_id', ]; $meta = []; foreach ( $db_meta_keys as $key ) { if ( isset( $all_meta[ $key ]->value ) ) { $meta[ $key ] = $all_meta[ $key ]->value; } } return $meta; } /** * Finalize invoice. * * @since 1.8.4 * * @throws RuntimeException If invoice not finalized. */ private function finalize_invoice() { try { $invoice = new Invoice(); $invoice = $invoice->retrieve( $this->data->object->id, Helpers::get_auth_opts() ); if ( empty( $invoice->finalized_at ) ) { $invoice->finalizeInvoice(); } } catch ( Exception $e ) { // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped throw new RuntimeException( esc_html( $e->getMessage() ) ); } } } Integrations/Stripe/Api/Webhooks/ChargeRefunded.php 0000644 00000006335 15174710275 0016351 0 ustar 00 <?php namespace WPForms\Integrations\Stripe\Api\Webhooks; use RuntimeException; use WPForms\Db\Payments\UpdateHelpers; use WPForms\Integrations\Stripe\Api\PaymentIntents; use WPForms\Integrations\Stripe\Api\Webhooks\Exceptions\AmountMismatchException; /** * Webhook charge.refunded class. * * @since 1.8.4 */ class ChargeRefunded extends Base { /** * Decimals amount. * * @since 1.8.4 * * @var int */ private $decimals_amount; /** * Handle the Webhook's data. * * Save refunded amount in the payment meta with key refunded_amount. * Update payment status to 'partrefund' or 'refunded' if refunded amount is equal to total amount. * * @since 1.8.4 * * @throws RuntimeException If payment not updated. * * @return bool */ public function handle() { $this->set_payment(); if ( ! $this->db_payment ) { return false; } $currency = strtoupper( $this->data->object->currency ); $this->decimals_amount = wpforms_get_currency_multiplier( $currency ); $charge = ( new PaymentIntents() )->get_charge( $this->data->object->id ); if ( isset( $charge->refunds->data[0]->metadata->refunded_by ) && $charge->refunds->data[0]->metadata->refunded_by === 'wpforms_dashboard' ) { return false; } $event_previous_refunded_amount = isset( $this->data->previous_attributes->amount_refunded ) ? $this->data->previous_attributes->amount_refunded : 0; if ( $this->get_refunded_amount() !== $event_previous_refunded_amount ) { throw new AmountMismatchException( 'Refund amount mismatch detected. Possible reasons: duplicate webhook processing or webhooks received out of order.' ); } // We need to format amount since it doesn't contain decimals, e.g. 525 instead of 5.25. $refunded_amount = $this->data->object->amount_refunded / $this->decimals_amount; $last_refund_amount = $this->get_last_refund_amount() / $this->decimals_amount; $last_refund_formatted = wpforms_format_amount( $last_refund_amount, true, $currency ); $log = sprintf( 'Stripe payment refunded from the Stripe dashboard. Refunded amount: %1$s.', $last_refund_formatted ); if ( ! UpdateHelpers::refund_payment( $this->db_payment, $refunded_amount, $log ) ) { throw new RuntimeException( 'Payment not updated' ); } return true; } /** * Get refunded amount from the database. * * @since 1.9.3 * * @return int The refunded amount from the database, in cents. */ private function get_refunded_amount() { $refunded_amount = wpforms()->obj( 'payment_meta' )->get_last_by( 'refunded_amount', $this->db_payment->id ); if ( ! $refunded_amount ) { return 0; } return (int) ( $refunded_amount->meta_value * $this->decimals_amount ); } /** * Get last refund amount. * * @since 1.8.4 * * @return int Last refund amount in cents. */ private function get_last_refund_amount() { if ( isset( $this->data->object->refunds->data[0]->amount ) ) { return $this->data->object->refunds->data[0]->amount; } if ( isset( $this->data->previous_attributes->amount_refunded ) ) { return $this->data->object->amount_refunded - $this->data->previous_attributes->amount_refunded; } return $this->data->object->amount_refunded - $this->get_refunded_amount(); } } Integrations/Stripe/Api/Webhooks/CustomerSubscriptionDeleted.php 0000644 00000001642 15174710275 0021174 0 ustar 00 <?php namespace WPForms\Integrations\Stripe\Api\Webhooks; use RuntimeException; use WPForms\Db\Payments\UpdateHelpers; /** * Webhook customer.subscription.deleted class. * * @since 1.8.4 */ class CustomerSubscriptionDeleted extends Base { /** * Handle the Webhook's data. * * @since 1.8.4 * * @throws RuntimeException If payment not found or not updated. * * @return bool */ public function handle() { $payment = wpforms()->obj( 'payment' )->get_by( 'subscription_id', $this->data->object->id ); if ( ! $payment ) { return false; } if ( isset( $this->data->object->metadata->canceled_by ) && $this->data->object->metadata->canceled_by === 'wpforms_dashboard' ) { return false; } if ( ! UpdateHelpers::cancel_subscription( $payment->id, 'Stripe subscription cancelled from the Stripe dashboard.' ) ) { throw new RuntimeException( 'Payment not updated' ); } return true; } } Integrations/Stripe/Api/Webhooks/ChargeRefundUpdated.php 0000644 00000012031 15174710275 0017335 0 ustar 00 <?php namespace WPForms\Integrations\Stripe\Api\Webhooks; use RuntimeException; use WPForms\Integrations\Stripe\Api\PaymentIntents; use WPForms\Integrations\Stripe\Api\Webhooks\Exceptions\AmountMismatchException; /** * Webhook charge.refund.updated class. * Currently, this class processes only events where the refund status is 'canceled'. * * @since 1.9.2 */ class ChargeRefundUpdated extends Base { /** * Handle the Webhook's data. * * Update refunded amount in the payment meta with key refunded_amount. * Update payment status to 'partrefund' or 'completed' if refunded amount is 0. * * @since 1.9.2 * * @throws RuntimeException If payment not found or not updated. * * @return bool */ public function handle() { $this->set_payment(); if ( ! $this->db_payment ) { return false; } // Proceed only if the refund status is 'canceled'. if ( $this->data->object->status !== 'canceled' ) { return false; } $charge = ( new PaymentIntents() )->get_charge( $this->data->object->charge ); if ( ! isset( $charge->amount_refunded ) ) { return false; } $db_refunded_amount = $this->get_refunded_amount(); $currency = strtoupper( $this->data->object->currency ); $decimals_amount = wpforms_get_currency_multiplier( $currency ); // We need to format amount since it doesn't contain decimals, e.g. 525 instead of 5.25. $refunded_amount = $charge->amount_refunded / $decimals_amount; $canceled_refund_amount = $this->data->object->amount / $decimals_amount; // Prevent duplicate webhook processing. if ( ! $this->is_valid_refund_amount( $refunded_amount, $db_refunded_amount, $canceled_refund_amount ) ) { throw new AmountMismatchException( 'Refund amount mismatch detected. Possible reasons: duplicate webhook processing or webhooks received out of order.' ); } $status = $this->is_full_refund( $canceled_refund_amount, $db_refunded_amount ) ? 'completed' : 'partrefund'; $this->update_payment_status( $status ); $this->update_payment_meta( $refunded_amount ); $this->add_refund_cancel_log( $canceled_refund_amount, $currency ); return true; } /** * Validate the refund amount to prevent duplicate webhook processing. * * @since 1.9.2 * * @param float $refunded_amount Refunded amount. * @param float $db_refunded_amount Refunded amount from the database. * @param float $canceled_refund_amount Canceled refund amount. * * @return bool */ private function is_valid_refund_amount( float $refunded_amount, float $db_refunded_amount, float $canceled_refund_amount ): bool { return $refunded_amount === ( $db_refunded_amount - $canceled_refund_amount ); } /** * Check if this is a full refund. * * @since 1.9.2 * * @param float $canceled_refund_amount Canceled refund amount. * @param float $db_refunded_amount Refunded amount from the database. * * @return bool */ private function is_full_refund( float $canceled_refund_amount, float $db_refunded_amount ): bool { return $canceled_refund_amount >= $db_refunded_amount; } /** * Update the payment status. * * @since 1.9.2 * * @param string $status Available values: 'completed', 'partrefund'. * * @throws RuntimeException If payment status not updated. */ private function update_payment_status( string $status ) { if ( ! in_array( $status, [ 'completed', 'partrefund' ], true ) ) { throw new RuntimeException( 'Payment not updated' ); } $updated_payment = wpforms()->obj( 'payment' )->update( $this->db_payment->id, [ 'status' => $status, ] ); if ( ! $updated_payment ) { throw new RuntimeException( 'Payment not updated' ); } } /** * Update the refunded amount meta. * * @since 1.9.2 * * @param float $refunded_amount Refunded amount. * * @throws RuntimeException If payment meta not updated. */ private function update_payment_meta( float $refunded_amount ) { $updated_payment_meta = wpforms()->obj( 'payment_meta' )->update_or_add( $this->db_payment->id, 'refunded_amount', $refunded_amount ); if ( ! $updated_payment_meta ) { throw new RuntimeException( 'Payment meta not updated' ); } } /** * Add a log entry for the canceled refund. * * @since 1.9.2 * * @param float $canceled_refund_amount Canceled refund amount. * @param string $currency Currency code. */ private function add_refund_cancel_log( float $canceled_refund_amount, string $currency ) { $formatted_amount = wpforms_format_amount( $canceled_refund_amount, true, $currency ); wpforms()->obj( 'payment_meta' )->add_log( $this->db_payment->id, sprintf( 'Stripe refund cancelled from the Stripe dashboard. Cancelled refund amount: %1$s.', $formatted_amount ) ); } /** * Get refunded amount from the database. * * @since 1.9.2 * * @return float */ private function get_refunded_amount(): float { $refunded_amount = wpforms()->obj( 'payment_meta' )->get_last_by( 'refunded_amount', $this->db_payment->id ); return $refunded_amount ? $refunded_amount->meta_value : 0; } } Integrations/Stripe/Api/Webhooks/CustomerSubscriptionCreated.php 0000644 00000001517 15174710275 0021176 0 ustar 00 <?php namespace WPForms\Integrations\Stripe\Api\Webhooks; use RuntimeException; /** * Webhook customer.subscription.created class. * * @since 1.8.4 */ class CustomerSubscriptionCreated extends Base { /** * Handle the Webhook's data. * * @since 1.8.4 * * @throws RuntimeException If payment not found or not updated. * * @return bool */ public function handle() { $this->delay(); $payment = wpforms()->obj( 'payment' )->get_by( 'subscription_id', $this->data->object->id ); if ( ! $payment ) { return false; } if ( ! wpforms()->obj( 'payment' )->update( $payment->id, [ 'subscription_status' => 'active' ] ) ) { throw new RuntimeException( 'Payment not updated' ); } wpforms()->obj( 'payment_meta' )->add_log( $payment->id, 'Stripe subscription was set to active.' ); return true; } } Integrations/Stripe/Api/Webhooks/ChargeSucceeded.php 0000644 00000004100 15174710275 0016465 0 ustar 00 <?php namespace WPForms\Integrations\Stripe\Api\Webhooks; use WPForms\Db\Payments\Queries; use WPForms\Integrations\Stripe\Helpers; use RuntimeException; /** * Webhook charge.succeeded class. * * @since 1.8.4 */ class ChargeSucceeded extends Base { /** * Handle the Webhook's data. * * @since 1.8.4 * * @throws RuntimeException If payment not found or not updated. * * @return bool */ public function handle() { $this->delay(); $this->set_payment(); if ( ! $this->db_payment ) { // Handle a case when charge.succeeded was sent before invoice.payment_succeeded to update a payment method details. if ( ! empty( $this->data->object->invoice ) ) { $db_renewal = ( new Queries() )->get_renewal_by_invoice_id( $this->data->object->invoice ); if ( is_null( $db_renewal ) || empty( $this->data->object->payment_method_details ) ) { return false; } $this->update_payment_method_details( $db_renewal->id, $this->data->object->payment_method_details ); } return false; } // Update payment method details to keep them up to date. if ( ! empty( $this->data->object->payment_method_details ) ) { $this->update_payment_method_details( $this->db_payment->id, $this->data->object->payment_method_details ); } if ( $this->db_payment->status !== 'processed' ) { return false; } $currency = strtoupper( $this->data->object->currency ); $db_amount = wpforms_format_amount( $this->db_payment->total_amount ); $amount = wpforms_format_amount( $this->data->object->amount_captured / wpforms_get_currency_multiplier( $currency ) ); if ( $amount !== $db_amount || ! $this->data->object->paid ) { return false; } $updated_payment = wpforms()->obj( 'payment' )->update( $this->db_payment->id, [ 'status' => 'completed', 'date_updated_gmt' => gmdate( 'Y-m-d H:i:s' ), ] ); if ( ! $updated_payment ) { throw new RuntimeException( 'Payment not updated' ); } wpforms()->obj( 'payment_meta' )->add_log( $this->db_payment->id, 'Stripe payment was completed.' ); return true; } } Integrations/Stripe/Api/Webhooks/Base.php 0000644 00000007211 15174710275 0014347 0 ustar 00 <?php namespace WPForms\Integrations\Stripe\Api\Webhooks; use WPForms\Vendor\Stripe\Event as StripeEvent; use WPForms\Integrations\Stripe\Helpers; /** * Webhook base class. * * @since 1.8.4 */ abstract class Base { /** * Event data from Stripe object. * * @since 1.8.4 * * @var object */ protected $data; /** * Payment object. * * @since 1.8.4 * * @var object */ protected $db_payment; /** * Webhook setup. * * @since 1.8.4 * * @param StripeEvent $event Stripe event. */ public function setup( StripeEvent $event ) { $this->data = $event->data; $this->hooks(); } /** * Register hooks. * * @since 1.8.4 */ private function hooks() { add_filter( 'wpforms_current_user_can', '__return_true' ); } /** * Handle the Webhook's data. * * @since 1.8.4 * * return bool */ abstract public function handle(); /** * Set payment object. * * Set payment object from database. If payment not registered yet in DB, throw exception. * * @since 1.8.4 */ protected function set_payment() { // Determine whether a legacy API version bundled into the addon is still used. // When it's dropped from the addon, this line can be safely removed. $is_legacy_api = Helpers::is_pro() && absint( wpforms_setting( 'stripe-api-version' ) ) === 2; if ( $is_legacy_api && ! isset( $this->data->object->id ) ) { return; // Payment id not found. } if ( ! $is_legacy_api && ! isset( $this->data->object->payment_intent ) ) { return; // Payment intent not found. } $transaction_id = $is_legacy_api ? $this->data->object->id : $this->data->object->payment_intent; $this->db_payment = wpforms()->obj( 'payment' )->get_by( 'transaction_id', $transaction_id ); } /** * Delay webhook handling. * * Stripe sends some webhooks before payment is saved in our database. * Sometimes it is required to wait until form submission has ended and payment is saved in the database. * * @since 1.8.4 */ protected function delay() { sleep( 5 ); } /** * Check if previous statuses are matched. * * If webhook payload contains previous payment status and it's not matching with the status in the database, return false. * * @since 1.8.4 * * @depecated 1.9.3 * * @return bool */ protected function is_previous_statuses_matched(): bool { _deprecated_function( __METHOD__, '1.9.3 of the WPForms plugin' ); $db_stripe = [ 'partrefund' => 'refunded', 'refunded' => 'refunded', 'completed' => 'succeeded', 'pending' => 'processing', ]; if ( isset( $this->data->previous_attributes->status ) && in_array( $this->data->previous_attributes->status, $db_stripe, true ) && $db_stripe[ $this->db_payment->status ] !== $this->data->previous_attributes->status ) { return false; } return true; } /** * Update payment method details. * * @since 1.8.7 * * @param int $payment_id Payment ID. * @param array $details Charge details. * * @noinspection PhpMissingParamTypeInspection */ protected function update_payment_method_details( $payment_id, $details ) { $meta['method_type'] = ! empty( $details->type ) ? sanitize_text_field( $details->type ) : ''; if ( ! empty( $details->card->last4 ) ) { $meta['method_type'] = $meta['method_type'] ?? 'card'; $meta['credit_card_last4'] = $details->card->last4; $meta['credit_card_method'] = $details->card->brand; $meta['credit_card_expires'] = $details->card->exp_month . '/' . $details->card->exp_year; } $payment_meta_obj = wpforms()->obj( 'payment_meta' ); if ( ! $payment_meta_obj ) { return; } $payment_meta_obj->bulk_add( $payment_id, $meta ); } } Integrations/Stripe/Api/Webhooks/Exceptions/AmountMismatchException.php 0000644 00000000620 15174710275 0022423 0 ustar 00 <?php namespace WPForms\Integrations\Stripe\Api\Webhooks\Exceptions; use Exception; /** * Class AmountMismatchException. * * @since 1.9.7 */ class AmountMismatchException extends Exception { /** * AmountMismatchException constructor. * * @since 1.9.7 * * @param string $message Message. */ public function __construct( $message ) { parent::__construct( $message, 202 ); } } Integrations/Stripe/Helpers.php 0000644 00000026062 15174710275 0012612 0 ustar 00 <?php namespace WPForms\Integrations\Stripe; /** * Stripe related helper methods. * * @since 1.8.2 */ class Helpers { /** * Stripe connection modes. * * @since 1.8.2 */ const CONNECTION_MODES = [ 'live', 'test' ]; /** * Get field slug. * * @since 1.8.2 * * @return string */ public static function get_field_slug() { return self::is_pro() ? wpforms_stripe()->api->get_config( 'field_slug' ) : 'stripe-credit-card'; } /** * Determine whether the Stripe field is in the form. * * @since 1.8.2 * * @param array $forms Form data (e.g. forms on a current page). * @param bool $multiple Must be 'true' if $forms contain multiple forms. * * @return bool */ public static function has_stripe_field( $forms, $multiple = false ) { $slug = self::get_field_slug(); if ( empty( $slug ) ) { return false; } return wpforms_has_field_type( $slug, $forms, $multiple ) !== false; } /** * Determine whether the Stripe is enabled in forms used on the page. * * @since 1.8.2 * * @param array $forms Form data (e.g. forms on a current page). * * @return bool */ public static function has_stripe_enabled( $forms ) { foreach ( $forms as $form ) { if ( self::is_payments_enabled( $form ) ) { return true; } } return false; } /** * Determine whether Stripe keys are configured on the Payments settings page. * * @since 1.8.2 * * @param string $mode Stripe mode to check the keys for. * * @return bool */ public static function has_stripe_keys( $mode = '' ) { $mode = self::validate_stripe_mode( $mode ); return wpforms_setting( "stripe-{$mode}-secret-key" ) && wpforms_setting( "stripe-{$mode}-publishable-key" ); } /** * Validate Stripe mode name to ensure it's either 'live' or 'test'. * If given mode is invalid, fetches current Stripe mode. * * @since 1.8.2 * * @param string $mode Stripe mode to validate. * * @return string */ public static function validate_stripe_mode( $mode ) { if ( empty( $mode ) || ! in_array( $mode, self::CONNECTION_MODES, true ) ) { return self::get_stripe_mode(); } return $mode; } /** * Get Stripe mode from the WPForms settings. * * @since 1.8.2 * * @return string */ public static function get_stripe_mode() { return wpforms_setting( 'stripe-test-mode' ) ? 'test' : 'live'; } /** * Get Stripe key from the WPForms settings. * * @since 1.8.2 * * @param string $type Key type (e.g. 'publishable' or 'secret'). * @param string $mode Stripe mode (e.g. 'live' or 'test'). * * @return string */ public static function get_stripe_key( $type, $mode = '' ) { $mode = self::validate_stripe_mode( $mode ); if ( ! in_array( $type, [ 'publishable', 'secret' ], true ) ) { return ''; } $key = wpforms_setting( "stripe-{$mode}-{$type}-key" ); if ( ! empty( $key ) && is_string( $key ) ) { return sanitize_text_field( $key ); } return ''; } /** * Set Stripe key from the WPForms settings. * * @since 1.8.2 * * @param string $value Key string to set. * @param string $type Key type (e.g. 'publishable' or 'secret'). * @param string $mode Stripe mode (e.g. 'live' or 'test'). * * @return bool */ public static function set_stripe_key( $value, $type, $mode = '' ) { $mode = self::validate_stripe_mode( $mode ); if ( ! in_array( $type, [ 'publishable', 'secret' ], true ) ) { return false; } $key = "stripe-{$mode}-{$type}-key"; $settings = (array) get_option( 'wpforms_settings', [] ); $settings[ $key ] = sanitize_text_field( $value ); return wpforms_update_settings( $settings ); } /** * Determine whether a license key is active. * * @since 1.8.2 * * @return bool */ public static function is_license_active() { $license = (array) get_option( 'wpforms_license', [] ); return ! empty( wpforms_get_license_key() ) && empty( $license['is_expired'] ) && empty( $license['is_disabled'] ) && empty( $license['is_invalid'] ); } /** * Determine whether a license type is allowed. * * @since 1.8.2 * * @return bool */ public static function is_allowed_license_type() { return in_array( wpforms_get_license_type(), [ 'pro', 'elite', 'agency', 'ultimate' ], true ); } /** * Determine whether a license is ok. * * @since 1.8.2 * * @return bool */ public static function is_license_ok() { return self::is_license_active() && self::is_allowed_license_type(); } /** * Determine whether the addon is activated. * * @since 1.8.2 * @since 1.9.5 Added a fallback for legacy versions of the Stripe addon. * * @return bool */ public static function is_addon_active(): bool { // Legacy versions of the Stripe addon do not support the Requirements core feature. if ( defined( 'WPFORMS_STRIPE_VERSION' ) && version_compare( WPFORMS_STRIPE_VERSION, '3.0.1', '<=' ) ) { return function_exists( 'wpforms_stripe' ); } return wpforms_is_addon_initialized( 'stripe' ); } /** * Determine whether the addon is activated and appropriate license is set. * * @since 1.8.2 * * @return bool */ public static function is_pro() { return self::is_addon_active() && self::is_allowed_license_type(); } /** * Get authorization options used for every Stripe transaction as recommended in Stripe official docs. * * @link https://stripe.com/docs/connect/authentication#api-keys * * @since 1.8.2 * * @return array */ public static function get_auth_opts() { return [ 'api_key' => self::get_stripe_key( 'secret' ) ]; } /** * Determine whether the Payment element mode is enabled. * * @since 1.8.2 * * @return bool */ public static function is_payment_element_enabled() { return wpforms_setting( 'stripe-card-mode' ) === 'payment'; } /** * Determine whether the application fee is supported. * * @since 1.8.2 * * @return bool */ public static function is_application_fee_supported() { return ! in_array( self::get_account_country(), [ 'br', 'in', 'mx' ], true ); } /** * Get Stripe webhook endpoint data. * * @since 1.8.4 * * @return array */ public static function get_webhook_endpoint_data() { return [ 'namespace' => 'wpforms', 'route' => 'stripe/webhooks', 'fallback' => 'wpforms_stripe_webhooks', ]; } /** * Get webhook URL for REST API. * * @since 1.8.4 * * @return string */ public static function get_webhook_url_for_rest() { $path = implode( '/', [ self::get_webhook_endpoint_data()['namespace'], self::get_webhook_endpoint_data()['route'], ] ); return rest_url( $path ); } /** * Get webhook URL for cURL fallback. * * @since 1.8.4 * * @return string */ public static function get_webhook_url_for_curl() { return add_query_arg( self::get_webhook_endpoint_data()['fallback'], '1', site_url() ); } /** * Determine if webhook ID and secret is set in WPForms settings. * * @since 1.8.4 * * @return bool */ public static function is_webhook_configured() { $mode = self::get_stripe_mode(); return wpforms_setting( 'stripe-webhooks-id-' . $mode ) && wpforms_setting( 'stripe-webhooks-secret-' . $mode ); } /** * Determine if webhooks are enabled in WPForms settings. * * @since 1.8.4 * * @return bool */ public static function is_webhook_enabled() { return wpforms_setting( 'stripe-webhooks-enabled' ); } /** * Determine if REST API is set in WPForms settings. * * @since 1.8.4 * * @return bool */ public static function is_rest_api_set() { return wpforms_setting( 'stripe-webhooks-communication', 'rest' ) === 'rest'; } /** * Get decimals amount. * * @since 1.8.4 * @deprecated 1.9.5 * * @param string $currency Currency. * * @return int */ public static function get_decimals_amount( $currency = '' ) { _deprecated_function( __METHOD__, '1.9.5 of the WPForms plugin', 'wpforms_get_currency_multiplier()' ); return wpforms_get_currency_multiplier( $currency ); } /** * Get Stripe webhook endpoint URL. * * If the constant WPFORMS_STRIPE_WHURL is defined, it will be used as the webhook URL. * * @since 1.8.4 * * @return string */ public static function get_webhook_url() { if ( defined( 'WPFORMS_STRIPE_WHURL' ) ) { return WPFORMS_STRIPE_WHURL; } if ( self::is_rest_api_set() ) { return self::get_webhook_url_for_rest(); } return self::get_webhook_url_for_curl(); } /** * Is Stripe payment enabled for the form. * * @since 1.8.4 * * @param array $form_data Form data. * * @return bool */ public static function is_payments_enabled( $form_data ) { return self::is_modern_settings_enabled( $form_data ) || ! empty( $form_data['payments']['stripe']['enable'] ); } /** * Is Stripe modern payment enabled for the form. * * @since 1.8.4 * * @param array $form_data Form data. * * @return bool */ public static function is_modern_settings_enabled( $form_data ) { return ! empty( $form_data['payments']['stripe']['enable_one_time'] ) || ! empty( $form_data['payments']['stripe']['enable_recurring'] ); } /** * Detect if form supports multiple subscription plans. * * @since 1.8.4 * * @param array $form_data Form data. * * @return bool */ public static function is_form_supports_multiple_recurring_plans( $form_data ) { return ! isset( $form_data['payments']['stripe'] ) || empty( $form_data['payments']['stripe']['recurring']['enable'] ); } /** * Determine if legacy payment settings should be displayed. * * @since 1.8.4 * * @param array $form_data Form data. * * @return bool */ public static function is_legacy_payment_settings( $form_data ) { $has_legacy_settings = ! self::is_form_supports_multiple_recurring_plans( $form_data ); // Return early if form has legacy payment settings. if ( $has_legacy_settings ) { return true; } $addon_compat = ( new StripeAddonCompatibility() )->init(); // Return early if Stripe Pro addon doesn't support modern settings (multiple plans). if ( $addon_compat && ! $addon_compat->is_supported_modern_settings() ) { return true; } return false; } /** * Determine whether the Link is supported. * * @link https://docs.stripe.com/payments/payment-methods/integration-options#payment-method-availability * * @since 1.8.8 * * @return bool */ public static function is_link_supported(): bool { return ! in_array( self::get_account_country(), [ 'br', 'in', 'id', 'th' ], true ); } /** * Get the maximum number of cycles for recurring plans. * * @since 1.9.8 * * @return int */ public static function recurring_plan_cycles_max(): int { /** * Filters the maximum number of cycles for recurring plans. * * @since 1.9.8 * * @param int $max Maximum number of cycles. */ return (int) apply_filters( 'wpforms_stripe_recurring_plan_cycles_max', 100 ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName } /** * Get account country. * * @since 1.8.8 * * @return string */ private static function get_account_country(): string { $mode = self::get_stripe_mode(); return get_option( "wpforms_stripe_{$mode}_account_country", '' ); } } Integrations/Stripe/RateLimit.php 0000644 00000021276 15174710275 0013104 0 ustar 00 <?php namespace WPForms\Integrations\Stripe; /** * Stripe error rate limiting. * * @since 1.8.2 */ final class RateLimit { /** * Allowed number of attempts. * * @since 1.8.2 * * @var int */ private $allowed_attempts; /** * Rate Limit block expiration time. * * @since 1.8.2 * * @var int */ private $expires_in; /** * Perform certain things on class init. * * @since 1.8.2 */ public function init() { // phpcs:disable WPForms.PHP.ValidateHooks.InvalidHookName /** * This filter allow to modify Stripe rate limit attempts count. * * @since 1.8.2 * * @param int $count Attempts count. */ $this->allowed_attempts = (int) apply_filters( 'wpforms_stripe_rate_limit_allowed_attempts', 3 ); /** * This filter allow to modify Stripe rate limit expiration time. * * @since 1.8.2 * * @param int $expires_in Expiration time. */ $this->expires_in = (int) apply_filters( 'wpforms_stripe_rate_limit_expires_in', HOUR_IN_SECONDS * 6 ); // phpcs:enable WPForms.PHP.ValidateHooks.InvalidHookName } /** * Check if rate limit is under threshold and passes. * * @since 1.8.2 * * @return bool */ public function is_ok() { $entry = $this->get_entry(); if ( empty( $entry['attempts'] ) ) { return true; } if ( $entry['attempts'] < $this->allowed_attempts ) { return true; } $this->increment_attempts( $entry ); return false; } /** * Increment the number of attempts for a specific IP address. * * @since 1.8.2 * * @param array $entry Rate limit entry data. * * @return bool */ public function increment_attempts( $entry = [] ) { if ( empty( $entry ) ) { $entry = $this->get_entry(); } $entry['attempts'] = (int) $entry['attempts'] + 1; return $this->update_entry( $entry['attempts'] ); } /** * Get rate limit entry id based on IP address. * * @since 1.8.2 * * @return string */ private function get_entry_id() { return 'wpforms_stripe_attempt_' . wp_hash( wpforms_get_ip() ); } /** * Get rate limit entry attempts and expiration data. * * @since 1.8.2 * * @return array */ private function get_entry() { $storage = $this->get_storage_type(); $entry_id = $this->get_entry_id(); if ( $storage === 'file' ) { return $this->get_file_entry( $entry_id ); } if ( $storage === 'transient' ) { return $this->get_transient_entry( $entry_id ); } return [ 'attempts' => false, 'expiration' => false, ]; } /** * Update rate limit entry attempts and expiration data. * * @since 1.8.2 * * @param int $attempts Number of attempts to set. * * @return bool */ private function update_entry( $attempts ) { $storage = $this->get_storage_type(); $entry_id = $this->get_entry_id(); if ( $storage === 'file' ) { return $this->update_file_entry( $entry_id, $attempts ); } if ( $storage === 'transient' ) { return $this->update_transient_entry( $entry_id, $attempts ); } return false; } /** * Get a storage type where rate limit entries are saved. * * @since 1.8.2 * * @return string */ private function get_storage_type() { $file = $this->get_file_path(); if ( empty( $file ) ) { return 'transient'; } if ( ! file_exists( $file ) ) { $this->create_file( $file ); } // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_is_writable if ( ! is_writable( $file ) ) { return 'transient'; } return 'file'; } /** * Get file path to store the rate limit entries in. * * @since 1.8.2 * * @return string */ private function get_file_path() { if ( function_exists( 'wpforms_upload_dir' ) ) { $upload_dir = wpforms_upload_dir(); } if ( isset( $upload_dir['path'] ) ) { $upload_dir['path'] = trailingslashit( $upload_dir['path'] ) . 'stripe'; } $file_name = wp_hash( site_url() ) . '-rate-limiting.log'; return isset( $upload_dir['path'] ) ? wp_normalize_path( trailingslashit( $upload_dir['path'] ) . $file_name ) : ''; } /** * Create index.html file in the specified directory if it doesn't exist. * * @since 1.8.2 * * @return bool True if file exists or was successfully created, false on failure. */ private function create_index_html_file() { $file = $this->get_file_path(); if ( empty( $file ) ) { return false; } $index_file = wp_normalize_path( trailingslashit( dirname( $file ) ) . 'index.html' ); // Do nothing if index.html exists in the directory. if ( file_exists( $index_file ) ) { return true; } // Create empty index.html. // phpcs:ignore WordPress.WP.AlternativeFunctions return file_put_contents( $index_file, '' ) !== false; } /** * Create a file path to store the rate limit entries in. * * @since 1.8.2 * * @param string $file File path. * * @return bool */ private function create_file( $file ) { if ( ! wp_mkdir_p( dirname( $file ) ) ) { return false; } if ( ! $this->create_index_html_file() ) { return false; } // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents if ( file_put_contents( $file, '' ) === false ) { return false; } // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_chmod chmod( $file, 0664 ); return true; } /** * Read full contents of a rate limit entries file. * * @since 1.8.2 * * @return array */ private function read_whole_file() { $file = $this->get_file_path(); if ( empty( $file ) ) { return []; } // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents $contents = file_get_contents( $file ); $contents = json_decode( $contents, true ); return is_array( $contents ) ? $contents : []; } /** * Write full contents to a rate limit entries file. * * @since 1.8.2 * * @param array $contents Array of all rate limit entries. * * @return bool */ private function write_whole_file( $contents ) { if ( ! is_array( $contents ) ) { return false; } $file = $this->get_file_path(); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_is_writable if ( ! is_writable( $file ) ) { return false; } // phpcs:ignore WordPress.WP.AlternativeFunctions return (bool) file_put_contents( $file, wp_json_encode( $contents ) ); } /** * Filter out the expired rate limit entries from a file. * * @since 1.8.2 * * @param array $contents Array of all rate limit entries. * @param string $entry_id Rate limit entry id. * * @return array */ private function filter_expired_file_entry( $contents, $entry_id ) { $expiration = isset( $contents[ $entry_id ]['expiration'] ) ? (int) $contents[ $entry_id ]['expiration'] : false; if ( empty( $expiration ) ) { return $contents; } if ( $expiration >= time() ) { return $contents; } unset( $contents[ $entry_id ] ); $this->write_whole_file( $contents ); return $contents; } /** * Get rate limit entry attempts and expiration data from a file. * * @since 1.8.2 * * @param string $entry_id Rate limit entry id. * * @return array */ private function get_file_entry( $entry_id ) { $contents = $this->read_whole_file(); $contents = $this->filter_expired_file_entry( $contents, $entry_id ); return [ 'attempts' => isset( $contents[ $entry_id ]['attempts'] ) ? $contents[ $entry_id ]['attempts'] : false, 'expiration' => isset( $contents[ $entry_id ]['expiration'] ) ? $contents[ $entry_id ]['expiration'] : false, ]; } /** * Update rate limit entry attempts and expiration data in a file. * * @since 1.8.2 * * @param string $entry_id Rate limit entry id. * @param int $attempts Number of attempts to set. * * @return bool */ private function update_file_entry( $entry_id, $attempts ) { if ( ! $this->create_index_html_file() ) { return false; } $contents = $this->read_whole_file(); $contents[ $entry_id ] = [ 'attempts' => $attempts, 'expiration' => time() + $this->expires_in, ]; return $this->write_whole_file( $contents ); } /** * Get rate limit entry attempts and expiration data from a transient. * * @since 1.8.2 * * @param string $entry_id Rate limit entry id. * * @return array */ private function get_transient_entry( $entry_id ) { return [ 'attempts' => get_transient( $entry_id ), 'expiration' => get_option( '_transient_timeout_' . $entry_id ), ]; } /** * Update rate limit entry attempts and expiration data in a transient. * * @since 1.8.2 * * @param string $entry_id Rate limit entry id. * @param int $attempts Number of attempts to set. * * @return bool */ private function update_transient_entry( $entry_id, $attempts ) { return set_transient( $entry_id, $attempts, $this->expires_in ); } } Integrations/Stripe/StripeAddonCompatibility.php 0000644 00000003532 15174710275 0016153 0 ustar 00 <?php namespace WPForms\Integrations\Stripe; /** * Compatibility with the Stripe addon. * * @since 1.8.2 */ class StripeAddonCompatibility { /** * Minimum compatible version of the Stripe addon. * * @since 1.8.2 * * @var string */ const MIN_COMPAT_VERSION = '3.0.0'; /** * Minimum modern settings compatible version of the Stripe addon. * * @since 1.8.4 * * @var string */ const MIN_MODERN_SETTINGS_VERSION = '3.1.0'; /** * Initialization. * * @since 1.8.4 * * @return StripeAddonCompatibility|null */ public function init() { return Helpers::is_pro() ? $this : null; } /** * Register hooks. * * @since 1.8.2 */ public function hooks() { // Warn the user about the fact that the not supported addon has been installed. add_action( 'admin_notices', [ $this, 'display_legacy_addon_notice' ] ); } /** * Check if the supported Stripe addon is active. * * @since 1.8.2 * * @return bool */ public function is_supported_version() { return defined( 'WPFORMS_STRIPE_VERSION' ) && version_compare( WPFORMS_STRIPE_VERSION, self::MIN_COMPAT_VERSION, '>=' ); } /** * Check if the supported Stripe addon is active for modern builder settings. * * @since 1.8.4 * * @return bool */ public function is_supported_modern_settings() { return defined( 'WPFORMS_STRIPE_VERSION' ) && version_compare( WPFORMS_STRIPE_VERSION, self::MIN_MODERN_SETTINGS_VERSION, '>=' ); } /** * Display wp-admin notification saying user first have to update addon to the latest version. * * @since 1.8.2 */ public function display_legacy_addon_notice() { echo '<div class="notice notice-error"><p>'; esc_html_e( 'The WPForms Stripe addon is out of date. To avoid payment processing issues, please upgrade the Stripe addon to the latest version.', 'wpforms-lite' ); echo '</p></div>'; } } Integrations/Stripe/WebhooksHealthCheck.php 0000644 00000016416 15174710275 0015057 0 ustar 00 <?php namespace WPForms\Integrations\Stripe; use WPForms\Admin\Notice; use WPForms\Integrations\Stripe\Api\WebhooksManager; /** * Webhooks Health Check class. * * @since 1.8.4 */ class WebhooksHealthCheck { /** * Endpoint status option name. * * @since 1.8.4 */ const ENDPOINT_OPTION = 'wpforms_stripe_webhooks_endpoint_status'; /** * Signature status option name. * * @since 1.8.4 */ const SIGNATURE_OPTION = 'wpforms_stripe_webhooks_signature_status'; /** * Signature verified key. * * @since 1.8.4 */ const STATUS_OK = 'ok'; /** * Signature error key. * * @since 1.8.4 */ const STATUS_ERROR = 'error'; /** * AS task name. * * @since 1.8.4 */ const ACTION = 'wpforms_stripe_webhooks_health_check'; /** * Admin notice ID. * * @since 1.8.4 */ const NOTICE_ID = 'wpforms_stripe_webhooks_site_health'; /** * Webhooks manager. * * @since 1.8.4 * * @var WebhooksManager */ private $webhooks_manager; /** * Initialization. * * @since 1.8.4 */ public function init() { $this->webhooks_manager = new WebhooksManager(); $this->hooks(); } /** * Register hooks. * * @since 1.8.4 */ private function hooks() { add_action( 'admin_notices', [ $this, 'admin_notice' ] ); add_action( self::ACTION, [ $this, 'process_webhooks_status_action' ] ); add_action( 'action_scheduler/migration_complete', [ $this, 'maybe_schedule_task' ] ); add_action( 'wpforms_settings_updated', [ $this, 'maybe_webhook_settings_is_updated' ], 10, 3 ); } /** * Schedule webhooks health check. * * @since 1.8.4 */ public function maybe_schedule_task() { /** * Allow customers to disable webhooks health check task. * * @since 1.8.4 * * @param bool $cancel True if task needs to be canceled. */ $is_canceled = (bool) apply_filters( 'wpforms_integrations_stripe_webhooks_health_check_cancel', false ); $tasks = wpforms()->obj( 'tasks' ); // Bail early in some instances. if ( $is_canceled || ! Helpers::has_stripe_keys() || $tasks->is_scheduled( self::ACTION ) ) { return; } /** * Filters the webhooks health check interval. * * @since 1.8.4 * * @param int $interval Interval in seconds. */ $interval = (int) apply_filters( 'wpforms_integrations_stripe_webhooks_health_check_interval', HOUR_IN_SECONDS ); $tasks->create( self::ACTION ) ->recurring( time(), $interval ) ->register(); } /** * Process webhooks status. * * @since 1.8.4 */ public function process_webhooks_status_action() { // Bail out if user unchecked option to enable webhooks. if ( ! Helpers::is_webhook_enabled() ) { return; } $last_payment = $this->get_last_stripe_payment(); // Bail out if there is no Stripe payment, // and remove options for reason to avoid any edge cases. if ( ! $last_payment ) { delete_option( self::SIGNATURE_OPTION ); delete_option( self::ENDPOINT_OPTION ); return; } // Signing secret is expired, try to reconnect. if ( ( get_option( self::SIGNATURE_OPTION, self::STATUS_OK ) !== self::STATUS_OK ) && ! $this->webhooks_manager->connect() ) { return; } // If a last Stripe payment has processed status and webhooks are not valid, // most likely there is issue with webhooks. if ( $last_payment['status'] === 'processed' && time() > strtotime( $last_payment['date_created_gmt'] ) + 15 * MINUTE_IN_SECONDS && ! $this->webhooks_manager->is_valid() ) { self::save_status( self::ENDPOINT_OPTION, self::STATUS_ERROR ); return; } self::save_status( self::ENDPOINT_OPTION, self::STATUS_OK ); } /** * Determine whether there is Stripe payment. * * @since 1.8.4 * * @return array */ private function get_last_stripe_payment(): array { $payment = wpforms()->obj( 'payment' )->get_payments( [ 'gateway' => 'stripe', 'mode' => 'any', 'number' => 1, ] ); return ! empty( $payment[0] ) ? $payment[0] : []; } /** * Display notice about issues with webhooks. * * @since 1.8.4 */ public function admin_notice() { // Bail out if Stripe account is not connected. if ( ! Helpers::has_stripe_keys() ) { return; } // Bail out if webhooks is not enabled. if ( ! Helpers::is_webhook_enabled() ) { return; } // Bail out if webhooks is configured and active. if ( Helpers::is_webhook_configured() && $this->is_webhooks_active() ) { return; } // Bail out if there are no Stripe payments. if ( ! $this->get_last_stripe_payment() ) { return; } $notice = sprintf( wp_kses( /* translators: %s - WPForms.com URL for Stripe webhooks documentation. */ __( 'Heads up! Looks like you have a problem with your webhooks configuration. Please check and confirm that you\'ve configured the WPForms webhooks in your Stripe account. This notice will disappear automatically when a new Stripe request comes in. See our <a href="%1$s" rel="nofollow noopener" target="_blank">documentation</a> for more information.', 'wpforms-lite' ), [ 'a' => [ 'href' => [], 'target' => [], 'rel' => [], ], ] ), esc_url( wpforms_utm_link( 'https://wpforms.com/docs/setting-up-stripe-webhooks/', 'Admin', 'Stripe Webhooks not active' ) ) ); Notice::error( $notice, [ 'dismiss' => true, 'slug' => self::NOTICE_ID, ] ); } /** * Maybe perform updating of endpoint URL or register AS task in certain cases. * * @since 1.8.4 * * @param array $settings An array of plugin settings. * @param bool $updated Whether an option was updated or not. * @param array $old_settings An old array of plugin settings. */ public function maybe_webhook_settings_is_updated( $settings, $updated, $old_settings ) { // Bail out early if Webhooks is not enabled. if ( empty( $settings['stripe-webhooks-enabled'] ) ) { return; } // Bail out early if it's not Settings > Payments admin page. if ( ! isset( $_POST['nonce'] ) || ! wpforms_is_admin_page( 'settings', 'payments' ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'wpforms-settings-nonce' ) ) { return; } // If Webhooks Method is changed, we have to update an endpoint's URL. if ( ! empty( $settings['stripe-webhooks-communication'] ) && ! empty( $old_settings['stripe-webhooks-communication'] ) && $settings['stripe-webhooks-communication'] !== $old_settings['stripe-webhooks-communication'] ) { $this->webhooks_manager->update( $this->webhooks_manager->get_id(), [ 'url' => Helpers::get_webhook_url(), 'disabled' => false, ] ); return; } $this->maybe_schedule_task(); } /** * Determine whether webhooks is active. * * @since 1.8.4 * * @return bool */ public function is_webhooks_active() { if ( get_option( self::ENDPOINT_OPTION, self::STATUS_OK ) !== self::STATUS_OK ) { return false; } if ( get_option( self::SIGNATURE_OPTION, self::STATUS_OK ) !== self::STATUS_OK ) { return false; } return true; } /** * Save webhooks status. * * @since 1.8.4 * * @param string $option Option name. * @param string $value Status value. */ public static function save_status( $option, $value ) { if ( ! in_array( $value, [ self::STATUS_OK, self::STATUS_ERROR ], true ) ) { return; } update_option( $option, $value ); } } Integrations/Stripe/Fields/PaymentElementCreditCard.php 0000644 00000027412 15174710275 0017272 0 ustar 00 <?php namespace WPForms\Integrations\Stripe\Fields; use WPForms_Field; use WPForms\Integrations\Stripe\Fields\Traits\CreditCard; /** * Stripe Payment element credit card field. * * @since 1.8.2 */ class PaymentElementCreditCard extends WPForms_Field { use CreditCard; /** * Field preview CVC icon SVG code. * * @since 1.8.2 */ const FIELD_PREVIEW_CVC_ICON_SVG = '<svg width="24" height="24" viewBox="0 0 24 24"><path opacity=".2" fill-rule="evenodd" clip-rule="evenodd" d="M15.337 4A5.493 5.493 0 0013 8.5c0 1.33.472 2.55 1.257 3.5H4a1 1 0 00-1 1v1a1 1 0 001 1h16a1 1 0 001-1v-.6a5.526 5.526 0 002-1.737V18a2 2 0 01-2 2H3a2 2 0 01-2-2V6a2 2 0 012-2h12.337zm6.707.293c.239.202.46.424.662.663a2.01 2.01 0 00-.662-.663z"></path><path opacity=".4" fill-rule="evenodd" clip-rule="evenodd" d="M13.6 6a5.477 5.477 0 00-.578 3H1V6h12.6z"></path><path fill-rule="evenodd" clip-rule="evenodd" d="M18.5 14a5.5 5.5 0 110-11 5.5 5.5 0 010 11zm-2.184-7.779h-.621l-1.516.77v.786l1.202-.628v3.63h.943V6.22h-.008zm1.807.629c.448 0 .762.251.762.613 0 .393-.37.668-.904.668h-.235v.668h.283c.565 0 .95.282.95.691 0 .393-.377.66-.911.66-.393 0-.786-.126-1.194-.37v.786c.44.189.88.291 1.312.291 1.029 0 1.736-.526 1.736-1.288 0-.535-.33-.967-.88-1.14.472-.157.778-.573.778-1.045 0-.738-.652-1.241-1.595-1.241a3.143 3.143 0 00-1.234.267v.77c.378-.212.763-.33 1.132-.33zm3.394 1.713c.574 0 .974.338.974.778 0 .463-.4.785-.974.785-.346 0-.707-.11-1.076-.337v.809c.385.173.778.26 1.163.26.204 0 .392-.032.573-.08a4.313 4.313 0 00.644-2.262l-.015-.33a1.807 1.807 0 00-.967-.252 3 3 0 00-.448.032V6.944h1.132a4.423 4.423 0 00-.362-.723h-1.587v2.475a3.9 3.9 0 01.943-.133z"></path></svg>'; /** * Define additional field properties. * * @since 1.8.2 * * @param array $properties Field properties. * @param array $field Field settings. * @param array $form_data Form data and settings. * * @return array */ public function field_properties( $properties, $field, $form_data ) { // Save form data for future usage in the class. $this->form_data = $form_data; unset( $properties['label']['attr']['for'] ); return $properties; } /** * Advanced section field options. * * @since 1.8.2 * * @param array $field Field settings. */ protected function advanced_options( $field ) { // Link Email field map. $output = $this->field_element( 'label', $field, [ 'slug' => 'link_email', 'value' => esc_html__( 'Link Email', 'wpforms-lite' ), 'tooltip' => esc_html__( 'Select an Email field to autofill your customers’ payment information using Link.', 'wpforms-lite' ), ], false ); $output .= $this->field_element( 'select', $field, [ 'slug' => 'link_email', 'value' => ! empty( $field['link_email'] ) ? esc_attr( $field['link_email'] ) : '', 'options' => $this->get_email_field_options(), 'class' => 'wpforms-field-map-select', 'data' => [ 'field-map-allowed' => 'email', 'field-map-placeholder' => esc_attr__( 'Stripe Credit Card Email', 'wpforms-lite' ), ], ], false ); $this->field_element( 'row', $field, [ 'slug' => 'link_email', 'content' => $output, ] ); $output = $this->field_element( 'label', $field, [ 'slug' => 'sublabel_position', 'value' => esc_html__( 'Sublabel Position', 'wpforms-lite' ), ], false ); $output .= $this->field_element( 'select', $field, [ 'slug' => 'sublabel_position', 'value' => ! empty( $field['sublabel_position'] ) ? esc_attr( $field['sublabel_position'] ) : '', 'options' => [ 'above' => esc_html__( 'Above', 'wpforms-lite' ), 'floating' => esc_html__( 'Floating', 'wpforms-lite' ), ], ], false ); $this->field_element( 'row', $field, [ 'slug' => 'sublabel_position', 'content' => $output, ] ); } /** * Array of available form email fields. * * @since 1.8.2 * * @return array */ private function get_email_field_options() { $fields = [ '' => esc_html__( 'Stripe Credit Card Email', 'wpforms-lite' ) ]; $email_options = wpforms_get_form_fields( $this->form_data, [ 'email' ] ); if ( empty( $email_options ) ) { return $fields; } foreach ( $email_options as $id => $email_option ) { $fields[ $id ] = ! empty( $email_option['label'] ) ? esc_attr( $email_option['label'] ) : sprintf( /* translators: %d - field ID. */ esc_html__( 'Field #%d', 'wpforms-lite' ), absint( $id ) ); } return $fields; } /** * Field preview inside the builder. * * @since 1.8.2 * * @param array $field Field settings. */ public function field_preview( $field ) { // Label. $this->field_preview_option( 'label', $field ); $sublabels = $this->get_sublabels(); $sublabel_position = ! empty( $field['sublabel_position'] ) ? $field['sublabel_position'] : 'above'; $hide_link_email = ! empty( $field['link_email'] ) ? 'wpforms-hidden' : ''; ?> <div class="format-selected wpforms-stripe-payment-element <?php echo esc_attr( $sublabel_position ); ?>"> <div class="wpforms-field-row wpforms-stripe-link-email <?php echo esc_attr( $hide_link_email ); ?>"> <?php $this->input_preview( $sublabels['email'] ); ?> </div> <div class="wpforms-field-row"> <?php $this->input_preview( $sublabels['number'] ); ?> <div class="wpforms-stripe-cardnumber-pics"></div> </div> <div class="wpforms-field-row"> <div class="wpforms-one-half"> <?php $this->input_preview( $sublabels['exp'] ); ?> </div> <div class="wpforms-one-half last wpforms-stripe-cvc"> <?php $this->input_preview( $sublabels['cvv'] ); ?> <?php echo self::FIELD_PREVIEW_CVC_ICON_SVG; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> </div> </div> <div class="wpforms-field-row"> <label class="wpforms-sub-label"><?php echo esc_attr( $sublabels['country'] ); ?></label> <?php $this->get_country_dropdown_preview( $field ); ?> </div> </div> <?php // Description. $this->field_preview_option( 'description', $field ); } /** * Input preview html output. * * @since 1.8.2 * * @param string $label Label text. */ private function input_preview( $label ) { echo '<label class="wpforms-sub-label">' . esc_html( $label ) . '</label>'; echo '<input type="text" placeholder="' . esc_attr( $label ) . '" readonly>'; } /** * Get Sublabels strings. * * @since 1.8.2 * * @return array */ private function get_sublabels() { return [ 'email' => __( 'Email', 'wpforms-lite' ), 'number' => __( 'Card Number', 'wpforms-lite' ), 'exp' => __( 'Expiration', 'wpforms-lite' ), 'cvv' => __( 'CVC', 'wpforms-lite' ), 'country' => __( 'Country', 'wpforms-lite' ), ]; } /** * Get Country dropdown preview. * * @since 1.8.2 * * @param array $field Field settings. */ private function get_country_dropdown_preview( $field ) { $display_label = ! empty( $field['sublabel_position'] ) && $field['sublabel_position'] === 'above'; $sublabels = $this->get_sublabels(); echo '<select readonly>'; echo '<option value="empty" ' . selected( $display_label, true, false ) . '></option>'; echo '<option value="country" ' . selected( ! $display_label, true, false ) . '>'; echo esc_attr( $sublabels['country'] ); echo '</option>'; echo '</select>'; } /** * Block editor field preview. * * @since 1.8.2 * * @param array $field Field settings. */ private function block_editor_field_display( $field ) { $hide_sub_label = ! empty( $field['sublabel_hide'] ); $sublabel_position = ! empty( $field['sublabel_position'] ) ? $field['sublabel_position'] : 'above'; $field_class = 'wpforms-field-row wpforms-field-row-responsive wpforms-field-' . sanitize_html_class( $field['size'] ); $no_columns_class = 'wpforms-field-row wpforms-no-columns wpforms-field-' . sanitize_html_class( $field['size'] ); $sublabels = $this->get_sublabels(); ?> <div class="format-selected wpforms-stripe-payment-element"> <?php if ( empty( $field['link_email'] ) ) : ?> <div class="<?php echo esc_attr( $no_columns_class ); ?> "> <?php $this->block_editor_input_preview( $sublabels['email'], $sublabel_position, $hide_sub_label ); ?> </div> <?php endif; ?> <div class="<?php echo esc_attr( $no_columns_class ); ?>"> <?php $this->block_editor_input_preview( $sublabels['number'], $sublabel_position, $hide_sub_label, '1234 1234 1234 1234' ); ?> <div class="wpforms-stripe-payment-element-cardnumber-preview"></div> </div> <div class="<?php echo esc_attr( $field_class ); ?>"> <div class="wpforms-field-row-block wpforms-one-half wpforms-first"> <?php $this->block_editor_input_preview( $sublabels['exp'], $sublabel_position, $hide_sub_label, 'MM / YY' ); ?> </div> <div class="wpforms-field-row-block wpforms-one-half wpforms-stripe-payment-element-cvc-preview"> <?php $this->block_editor_input_preview( $sublabels['cvv'], $sublabel_position, $hide_sub_label, 'CVC' ); ?> <?php echo self::FIELD_PREVIEW_CVC_ICON_SVG; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> </div> </div> <div class="<?php echo esc_attr( $no_columns_class ); ?>"> <?php if ( $sublabel_position === 'above' && ! $hide_sub_label ) : ?> <label class="wpforms-field-sublabel before"><?php echo esc_attr( $sublabels['country'] ); ?></label> <?php endif; ?> <?php $this->get_country_dropdown_preview( $field ); ?> </div> </div> <?php } /** * Get block editor input preview html. * * @since 1.8.2 * * @param string $label Label text. * @param string $position Label Position. * @param bool $hide Hide label. * @param string $placeholder Placeholder text. */ private function block_editor_input_preview( $label, $position, $hide, $placeholder = '' ) { if ( $hide ) { echo '<input type="text" readonly placeholder="' . esc_attr( $placeholder ) . '">'; return; } if ( $position === 'above' ) { echo '<label class="wpforms-field-sublabel before">' . esc_html( $label ) . '</label><input type="text" readonly placeholder="' . esc_attr( $placeholder ) . '">'; return; } echo '<input type="text" readonly placeholder="' . esc_attr( $label ) . '">'; } /** * Field display on the form front-end. * * @since 1.8.2 * * @param array $field Field data and settings. * @param array $deprecated Deprecated field attributes. Use field properties. * @param array $form_data Form data and settings. */ public function field_display( $field, $deprecated, $form_data ) { if ( $this->field_display_errors( $form_data ) ) { return; } if ( wpforms_is_editor_page() ) { $this->block_editor_field_display( $field ); return; } $form_id = absint( $form_data['id'] ); $hide_sub_label = ! empty( $field['sublabel_hide'] ) ? 'wpforms-sublabel-hide' : ''; $sublabel_position = ! empty( $field['sublabel_position'] ) ? $field['sublabel_position'] : 'above'; $link_email = ! empty( $field['link_email'] ) ? $field['link_email'] : ''; // Row wrapper. echo '<div class="wpforms-field-row wpforms-no-columns wpforms-field-' . sanitize_html_class( $field['size'] ) . ' ' . sanitize_html_class( $hide_sub_label ) . '" data-sublabel-position="' . esc_attr( $sublabel_position ) . '" data-link-email="' . esc_attr( $link_email ) . '" data-required="' . (int) ! empty( $field['required'] ) . '">'; if ( ! $link_email ) { echo '<div id="wpforms-field-stripe-link-element-' . absint( $form_id ) . '"></div>'; } echo '<div id="wpforms-field-stripe-payment-element-' . absint( $form_id ) . '"></div>'; echo '<input type="text" class="wpforms-stripe-credit-card-hidden-input" name="wpforms[stripe-credit-card-hidden-input-' . absint( $form_data['id'] ) . ']" disabled style="display: none;">'; echo '</div>'; } } Integrations/Stripe/Fields/StripeCreditCard.php 0000644 00000022636 15174710275 0015614 0 ustar 00 <?php namespace WPForms\Integrations\Stripe\Fields; use WPForms_Field; use WPForms\Integrations\Stripe\Fields\Traits\CreditCard; /** * Stripe credit card field. * * @since 1.8.2 */ class StripeCreditCard extends WPForms_Field { use CreditCard; /** * Field preview card icon SVG code. * * @since 1.8.2 */ const FIELD_PREVIEW_CARD_ICON_SVG = '<svg viewBox="0 0 32 21"><g transform="translate(0 2)"><path d="M26.58 19H2.42A2.4 2.4 0 0 1 0 16.62V2.38A2.4 2.4 0 0 1 2.42 0h24.16A2.4 2.4 0 0 1 29 2.38v14.25A2.4 2.4 0 0 1 26.58 19zM10 5.83c0-.46-.35-.83-.78-.83H3.78c-.43 0-.78.37-.78.83v3.34c0 .46.35.83.78.83h5.44c.43 0 .78-.37.78-.83V5.83z" opacity=".2"></path><path d="M25 15h-3c-.65 0-1-.3-1-1s.35-1 1-1h3c.65 0 1 .3 1 1s-.35 1-1 1zm-6 0h-3c-.65 0-1-.3-1-1s.35-1 1-1h3c.65 0 1 .3 1 1s-.35 1-1 1zm-6 0h-3c-.65 0-1-.3-1-1s.35-1 1-1h3c.65 0 1 .3 1 1s-.35 1-1 1zm-6 0H4c-.65 0-1-.3-1-1s.35-1 1-1h3c.65 0 1 .3 1 1s-.35 1-1 1z" opacity=".3"></path></g></svg>'; /** * Define additional field properties. * * @since 1.8.2 * * @param array $properties Field properties. * @param array $field Field settings. * @param array $form_data Form data and settings. * * @return array */ public function field_properties( $properties, $field, $form_data ) { unset( $properties['inputs']['primary'], $properties['label']['attr']['for'] ); $form_id = absint( $form_data['id'] ); $field_id = absint( $field['id'] ); $props = [ 'inputs' => [ 'number' => [ 'attr' => [ 'name' => '', 'value' => '', ], 'block' => [ 'wpforms-field-stripe-credit-card-number', ], 'class' => [ 'wpforms-field-stripe-credit-card-cardnumber', ], 'data' => [], 'id' => "wpforms-{$form_id}-field_{$field_id}", 'required' => ! empty( $field['required'] ) ? 'required' : '', 'sublabel' => [ 'hidden' => ! empty( $field['sublabel_hide'] ), 'value' => esc_html__( 'Card', 'wpforms-lite' ), 'position' => 'after', ], ], 'name' => [ 'attr' => [ 'name' => 'wpforms[stripe-credit-card-cardname]', 'value' => '', 'placeholder' => ! empty( $field['cardname_placeholder'] ) ? $field['cardname_placeholder'] : '', ], 'block' => [ 'wpforms-field-stripe-credit-card-name', ], 'class' => [ 'wpforms-field-stripe-credit-card-cardname', ], 'data' => [], 'id' => "wpforms-{$form_id}-field_{$field_id}-cardname", 'required' => ! empty( $field['required'] ) ? 'required' : '', 'sublabel' => [ 'hidden' => ! empty( $field['sublabel_hide'] ), 'value' => esc_html__( 'Name on Card', 'wpforms-lite' ), 'position' => 'after', ], ], ], ]; $properties = array_merge_recursive( $properties, $props ); // If this field is required we need to make some adjustments. if ( ! empty( $field['required'] ) ) { // Add required class if needed (for multi-page validation). $properties['inputs']['number']['class'][] = 'wpforms-field-required'; $properties['inputs']['name']['class'][] = 'wpforms-field-required'; } return $properties; } /** * Advanced section field options. * * @since 1.8.2 * * @param array $field Field settings. */ protected function advanced_options( $field ) { // Card Name. $cardname_placeholder = ! empty( $field['cardname_placeholder'] ) ? $field['cardname_placeholder'] : ''; printf( '<div class="wpforms-clear wpforms-field-option-row wpforms-field-option-row-cardname" id="wpforms-field-option-row-%d-cardname" data-subfield="cardname" data-field-id="%d">', absint( $field['id'] ), absint( $field['id'] ) ); $this->field_element( 'label', $field, [ 'slug' => 'cardname_placeholder', 'value' => esc_html__( 'Name on Card Placeholder Text', 'wpforms-lite' ), ] ); echo '<div class="placeholder">'; printf( '<input type="text" class="placeholder-update" id="wpforms-field-option-%d-cardname_placeholder" name="fields[%d][cardname_placeholder]" value="%s" data-field-id="%d" data-subfield="stripe-credit-card-cardname">', absint( $field['id'] ), absint( $field['id'] ), esc_attr( $cardname_placeholder ), absint( $field['id'] ) ); echo '</div>'; echo '</div>'; // Custom CSS classes. $this->field_option( 'css', $field ); } /** * Field preview inside the builder. * * @since 1.8.2 * * @param array $field Field settings. */ public function field_preview( $field ) { // Define data. $card_placeholder = esc_html__( 'Card number', 'wpforms-lite' ); $name_placeholder = ! empty( $field['cardname_placeholder'] ) ? $field['cardname_placeholder'] : ''; // Label. $this->field_preview_option( 'label', $field ); ?> <div class="format-selected format-selected-full"> <div class="wpforms-field-row"> <input type="text" readonly> <div class="wpforms-field-preview-wrap"> <div class="wpforms-field-stripe-credit-card-number-placeholder-preview"> <?php echo self::FIELD_PREVIEW_CARD_ICON_SVG; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> <span><?php echo esc_attr( $card_placeholder ); ?></span> </div> <div class="wpforms-field-stripe-credit-card-number-expcvc-preview">MM / YY CVC</div> </div> <label class="wpforms-sub-label"><?php esc_html_e( 'Card', 'wpforms-lite' ); ?></label> </div> <div class="wpforms-field-row"> <div class="wpforms-stripe-credit-card-cardname"> <input type="text" placeholder="<?php echo esc_attr( $name_placeholder ); ?>" readonly> <label class="wpforms-sub-label"><?php esc_html_e( 'Name on Card', 'wpforms-lite' ); ?></label> </div> </div> </div> <?php // Description. $this->field_preview_option( 'description', $field ); } /** * Field display on the form front-end. * * @since 1.8.2 * * @param array $field Field data and settings. * @param array $deprecated Deprecated field attributes. Use field properties. * @param array $form_data Form data and settings. */ public function field_display( $field, $deprecated, $form_data ) { if ( $this->field_display_errors( $form_data ) ) { return; } if ( wpforms_is_editor_page() ) { $this->block_editor_field_display( $field ); return; } // Define data. $number = ! empty( $field['properties']['inputs']['number'] ) ? $field['properties']['inputs']['number'] : []; $name = ! empty( $field['properties']['inputs']['name'] ) ? $field['properties']['inputs']['name'] : []; // Row wrapper. echo '<div class="wpforms-field-row wpforms-field-' . sanitize_html_class( $field['size'] ) . '">'; echo '<div ' . wpforms_html_attributes( false, $number['block'] ) . '>'; $this->field_display_sublabel( 'number', 'before', $field ); printf( '<div %s data-required="%s"><!-- a Stripe Element will be inserted here. --></div>', wpforms_html_attributes( $number['id'], $number['class'], $number['data'], $number['attr'] ), esc_html( $number['required'] ) ); // Hidden input is needed for styling and validation. echo '<input type="text" class="wpforms-stripe-credit-card-hidden-input" name="wpforms[stripe-credit-card-hidden-input-' . absint( $form_data['id'] ) . ']" disabled style="display: none;">'; $this->field_display_sublabel( 'number', 'after', $field ); $this->field_display_error( 'number', $field ); echo '</div>'; echo '</div>'; // Row wrapper. echo '<div class="wpforms-field-row wpforms-field-' . sanitize_html_class( $field['size'] ) . '">'; // Name. echo '<div ' . wpforms_html_attributes( false, $name['block'] ) . '>'; $this->field_display_sublabel( 'name', 'before', $field ); printf( '<input type="text" %s %s>', wpforms_html_attributes( $name['id'], $name['class'], $name['data'], $name['attr'] ), esc_html( $name['required'] ) ); $this->field_display_sublabel( 'name', 'after', $field ); $this->field_display_error( 'name', $field ); echo '</div>'; echo '</div>'; } /** * Block editor field preview. * * @since 1.8.2 * * @param array $field Field settings. */ private function block_editor_field_display( $field ) { $field_class = 'wpforms-field-row wpforms-no-columns wpforms-field-' . sanitize_html_class( $field['size'] ); $card_placeholder = esc_html__( 'Card number', 'wpforms-lite' ); $name_placeholder = ! empty( $field['properties']['inputs']['name']['attr']['placeholder'] ) ? $field['properties']['inputs']['name']['attr']['placeholder'] : ''; ?> <div class="<?php echo esc_attr( $field_class ); ?> "> <?php $this->field_display_sublabel( 'number', 'before', $field ); ?> <input type="text" class="wpforms-field-stripe-credit-card-number-preview" readonly placeholder=""> <div class="wpforms-field-stripe-credit-card-number-placeholder-preview"> <?php echo self::FIELD_PREVIEW_CARD_ICON_SVG; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> <span><?php echo esc_attr( $card_placeholder ); ?></span> </div> <div class="wpforms-field-stripe-credit-card-number-expcvc-preview">MM / YY CVC</div> <?php $this->field_display_sublabel( 'number', 'after', $field ); ?> </div> <div class="<?php echo esc_attr( $field_class ); ?>"> <?php $this->field_display_sublabel( 'name', 'before', $field ); ?> <input type="text" readonly placeholder="<?php echo esc_attr( $name_placeholder ); ?>"> <?php $this->field_display_sublabel( 'name', 'after', $field ); ?> </div> <?php } } Integrations/Stripe/Fields/Traits/CreditCard.php 0000644 00000024700 15174710275 0015665 0 ustar 00 <?php namespace WPForms\Integrations\Stripe\Fields\Traits; use WPForms\Integrations\Stripe\Helpers; /** * Stripe credit card field. * * @since 1.8.2 */ trait CreditCard { /** * Primary class constructor. * * @since 1.8.2 */ public function init() { // Define field type information. $this->name = esc_html__( 'Stripe Credit Card', 'wpforms-lite' ); $this->keywords = esc_html__( 'store, ecommerce, credit card, pay, payment, debit card', 'wpforms-lite' ); $this->type = 'stripe-credit-card'; $this->icon = 'fa-credit-card'; $this->order = 90; $this->group = 'payment'; // Define additional field properties. add_filter( 'wpforms_field_properties_stripe-credit-card', [ $this, 'field_properties' ], 5, 3 ); add_filter( 'wpforms_builder_fields_options', [ $this, 'pre_fields_options' ] ); // Set field to the required by default. add_filter( 'wpforms_field_new_required', [ $this, 'default_required' ], 10, 2 ); add_action( 'wpforms_builder_enqueues', [ $this, 'builder_enqueues' ] ); add_filter( 'wpforms_builder_strings', [ $this, 'builder_js_strings' ], 10, 2 ); add_filter( 'wpforms_builder_field_button_attributes', [ $this, 'field_button_attributes' ], 10, 3 ); add_filter( 'wpforms_pro_fields_entry_preview_is_field_support_preview_stripe-credit-card_field', [ $this, 'entry_preview_availability' ], 10, 4 ); add_filter( 'wpforms_field_new_display_duplicate_button', [ $this, 'field_display_duplicate_button' ], 10, 2 ); add_filter( 'wpforms_field_preview_display_duplicate_button', [ $this, 'field_display_duplicate_button' ], 10, 2 ); add_filter( 'wpforms_field_display_sublabel_skip_for', [ $this, 'skip_sublabel_for_attribute' ], 10, 3 ); } /** * Define if "Duplicate" button has to be displayed on field preview in a Form Builder. * * @since 1.8.5 * * @param bool $display Display switch. * @param array $field Field settings. * * @return bool */ public function field_display_duplicate_button( $display, $field ): bool { return Helpers::get_field_slug() === $field['type'] ? false : $display; } /** * Pre Builder Field Options. * * @since 1.8.2 * * @param array $form Current form post data. */ public function pre_fields_options( $form ): void { if ( ! isset( $form->post_content ) ) { $this->form_data = []; return; } $this->form_data = $form ? wpforms_decode( $form->post_content ) : []; if ( ! is_array( $this->form_data ) ) { $this->form_data = []; } } /** * Field options panel inside the builder. * * @since 1.8.2 * * @param array $field Field settings. */ public function field_options( $field ) { $this->field_option( 'basic-options', $field, [ 'markup' => 'open' ] ); // Label. $this->field_option( 'label', $field ); // Description. $this->field_option( 'description', $field ); // Required toggle. $this->field_option( 'required', $field ); $this->field_option( 'basic-options', $field, [ 'markup' => 'close' ] ); $this->field_option( 'advanced-options', $field, [ 'markup' => 'open' ] ); // Size. $this->field_option( 'size', $field ); $this->advanced_options( $field ); // Hide Label. $this->field_option( 'label_hide', $field ); // Hide sub labels. $this->field_option( 'sublabel_hide', $field ); $this->field_option( 'advanced-options', $field, [ 'markup' => 'close' ] ); } /** * Disallow dynamic population. * * @since 1.8.2 * * @param array $properties Field properties. * @param array $field Current field specific data. * * @return bool */ public function is_dynamic_population_allowed( $properties, $field ): bool { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed return false; } /** * Disallow fallback population. * * @since 1.8.2 * * @param array $properties Field properties. * @param array $field Current field specific data. * * @return bool */ public function is_fallback_population_allowed( $properties, $field ): bool { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed return false; } /** * Default is required. * * @since 1.8.2 * * @param bool $required Required status, true is required. * @param array $field Field settings. * * @return bool */ public function default_required( $required, $field ): bool { return $field['type'] === $this->type ? true : $required; } /** * Enqueue assets for the builder. * * @since 1.8.2 * * @param string $view Current view. * * @noinspection PhpMissingParamTypeInspection * @noinspection PhpUnusedParameterInspection */ public function builder_enqueues( $view ): void { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found $min = wpforms_get_min_suffix(); wp_enqueue_style( 'wpforms-builder-stripe-card-field', WPFORMS_PLUGIN_URL . "assets/css/integrations/stripe/builder-stripe{$min}.css", [], WPFORMS_VERSION ); wp_enqueue_script( 'wpforms-builder-stripe-card-field', WPFORMS_PLUGIN_URL . "assets/js/integrations/stripe/admin-builder-stripe-card-field{$min}.js", [ 'jquery', 'wpforms-builder' ], WPFORMS_VERSION, true ); } /** * Add our localized strings to be available in the form builder. * * @since 1.8.2 * * @param array $strings Form builder JS strings. * @param array $form Form data. * * @return array * @noinspection PhpMissingParamTypeInspection * @noinspection PhpUnusedParameterInspection */ public function builder_js_strings( $strings, $form ): array { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed $strings['stripe_ajax_required'] = wp_kses( __( '<p>AJAX form submissions are required when using the Stripe Credit Card field.</p><p>To proceed, please go to <strong>Settings » General » Advanced</strong> and check <strong>Enable AJAX form submission</strong>.</p>', 'wpforms-lite' ), [ 'p' => [], 'strong' => [], ] ); $strings['stripe_keys_required'] = wp_kses( __( '<p>Stripe account connection is required when using the Stripe Credit Card field.</p><p>To proceed, please go to <strong>WPForms Settings » Payments » Stripe</strong> and press <strong>Connect with Stripe</strong> button.</p>', 'wpforms-lite' ), [ 'p' => [], 'strong' => [], ] ); $strings['payments_enabled_required'] = wp_kses( __( '<p>Stripe Payments must be enabled when using the Stripe Credit Card field.</p><p>To proceed, please go to <strong>Payments » Stripe</strong> and check <strong>Enable Stripe payments</strong>.</p>', 'wpforms-lite' ), [ 'p' => [], 'strong' => [], ] ); return $strings; } /** * Define additional "Add Field" button attributes. * * @since 1.8.2 * * @param array $attributes Button attributes. * @param array $field Field settings. * @param array $form_data Form data and settings. * * @return array */ public function field_button_attributes( $attributes, $field, $form_data ): array { if ( Helpers::get_field_slug() !== $field['type'] ) { return $attributes; } if ( Helpers::has_stripe_field( $form_data ) ) { $attributes['atts']['disabled'] = 'true'; return $attributes; } if ( ! Helpers::has_stripe_keys() ) { $attributes['class'][] = 'warning-modal'; $attributes['class'][] = 'stripe-keys-required'; } return $attributes; } /** * Currently validation happens on the front end. We do not do * generic server-side validation because we do not allow the card * details to POST to the server. * * @since 1.8.2 * * @param int $field_id Field ID. * @param array $field_submit Submitted field value (raw data). * @param array $form_data Form data and settings. */ public function validate( $field_id, $field_submit, $form_data ): void { } /** * Format field. * * @since 1.8.2 * * @param int $field_id Field ID. * @param array $field_submit Submitted field value. * @param array $form_data Form data and settings. */ public function format( $field_id, $field_submit, $form_data ): void { // Define data. $name = ! empty( $form_data['fields'][ $field_id ]['label'] ) ? $form_data['fields'][ $field_id ]['label'] : ''; // Set final field details. wpforms()->obj( 'process' )->fields[ $field_id ] = [ 'name' => sanitize_text_field( $name ), 'value' => '', 'id' => absint( $field_id ), 'type' => $this->type, ]; } /** * The field value availability for the entry preview field. * * @since 1.8.2 * * @param bool $is_supported The field availability. * @param string $value The submitted Credit Card detail. * @param array $field Field data. * @param array $form_data Form data. * * @return bool * @noinspection PhpMissingParamTypeInspection * @noinspection PhpUnusedParameterInspection */ public function entry_preview_availability( $is_supported, $value, $field, $form_data ): bool { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed return ! empty( $value ) && $value !== '-'; } /** * Maybe display errors before the field. * * @since 1.8.2 * * @param array $form_data Form data and settings. * * @return bool */ private function field_display_errors( $form_data ): bool { // Display warning for non SSL pages. if ( ! is_ssl() ) { echo '<div class="wpforms-cc-warning wpforms-error-alert">'; esc_html_e( 'This page is insecure. Credit Card field should be used for testing purposes only.', 'wpforms-lite' ); echo '</div>'; } if ( ! Helpers::has_stripe_keys() ) { echo '<div class="wpforms-cc-warning wpforms-error-alert">'; esc_html_e( 'Credit Card field is disabled, Stripe keys are missing.', 'wpforms-lite' ); echo '</div>'; return true; } if ( ! Helpers::has_stripe_enabled( [ $form_data ] ) ) { echo '<div class="wpforms-cc-warning wpforms-error-alert">'; esc_html_e( 'Credit Card field is disabled, Stripe payments are not enabled in the form settings.', 'wpforms-lite' ); echo '</div>'; return true; } return false; } /** * Do not add the `for` attribute to certain sublabels. * * @since 1.8.9 * * @param bool $skip Whether to skip the `for` attribute. * @param string $key Input key. * @param array $field Field data and settings. * * @return bool */ public function skip_sublabel_for_attribute( $skip, $key, $field ): bool { if ( $field['type'] !== $this->type ) { return $skip; } if ( $key === 'number' ) { return true; } return $skip; } } Integrations/Stripe/apple-developer-merchantid-domain-association 0000644 00000021632 15174710275 0021377 0 ustar 00 7B227073704964223A2239373943394538343346343131343044463144313834343232393232313734313034353044314339464446394437384337313531303944334643463542433731222C2276657273696F6E223A312C22637265617465644F6E223A313536363233343735303036312C227369676E6174757265223A22333038303036303932613836343838366637306430313037303261303830333038303032303130313331306633303064303630393630383634383031363530333034303230313035303033303830303630393261383634383836663730643031303730313030303061303830333038323033653333303832303338386130303330323031303230323038346333303431343935313964353433363330306130363038326138363438636533643034303330323330376133313265333032633036303335353034303330633235343137303730366336353230343137303730366336393633363137343639366636653230343936653734363536373732363137343639366636653230343334313230326432303437333333313236333032343036303335353034306230633164343137303730366336353230343336353732373436393636363936333631373436393666366532303431373537343638366637323639373437393331313333303131303630333535303430613063306134313730373036633635323034393665363332653331306233303039303630333535303430363133303235353533333031653137306433313339333033353331333833303331333333323335333735613137306433323334333033353331333633303331333333323335333735613330356633313235333032333036303335353034303330633163363536333633326437333664373032643632373236663662363537323264373336393637366535663535343333343264353035323466343433313134333031323036303335353034306230633062363934663533323035333739373337343635366437333331313333303131303630333535303430613063306134313730373036633635323034393665363332653331306233303039303630333535303430363133303235353533333035393330313330363037326138363438636533643032303130363038326138363438636533643033303130373033343230303034633231353737656465626436633762323231386636386464373039306131323138646337623062643666326332383364383436303935643934616634613534313162383334323065643831316633343037653833333331663163353463336637656233323230643662616435643465666634393238393839336537633066313361333832303231313330383230323064333030633036303335353164313330313031666630343032333030303330316630363033353531643233303431383330313638303134323366323439633434663933653465663237653663346636323836633366613262626664326534623330343530363038326230363031303530353037303130313034333933303337333033353036303832623036303130353035303733303031383632393638373437343730336132663266366636333733373032653631373037303663363532653633366636643266366636333733373033303334326436313730373036633635363136393633363133333330333233303832303131643036303335353164323030343832303131343330383230313130333038323031306330363039326138363438383666373633363430353031333038316665333038316333303630383262303630313035303530373032303233303831623630633831623335323635366336393631366536333635323036663665323037343638363937333230363336353732373436393636363936333631373436353230363237393230363136653739323037303631373237343739323036313733373337353664363537333230363136333633363537303734363136653633363532303666363632303734363836353230373436383635366532303631373037303663363936333631363236633635323037333734363136653634363137323634323037343635373236643733323036313665363432303633366636653634363937343639366636653733323036663636323037353733363532633230363336353732373436393636363936333631373436353230373036663663363936333739323036313665363432303633363537323734363936363639363336313734363936663665323037303732363136333734363936333635323037333734363137343635366436353665373437333265333033363036303832623036303130353035303730323031313632613638373437343730336132663266373737373737326536313730373036633635326536333666366432663633363537323734363936363639363336313734363536313735373436383666373236393734373932663330333430363033353531643166303432643330326233303239613032376130323538363233363837343734373033613266326636333732366332653631373037303663363532653633366636643266363137303730366336353631363936333631333332653633373236633330316430363033353531643065303431363034313439343537646236666435373438313836383938393736326637653537383530376537396235383234333030653036303335353164306630313031666630343034303330323037383033303066303630393261383634383836663736333634303631643034303230353030333030613036303832613836343863653364303430333032303334393030333034363032323130306265303935373166653731653165373335623535653561666163623463373266656234343566333031383532323263373235313030326236316562643666353530323231303064313862333530613564643664643665623137343630333562313165623263653837636661336536616636636264383338303839306463383263646461613633333038323032656533303832303237356130303330323031303230323038343936643266626633613938646139373330306130363038326138363438636533643034303330323330363733313162333031393036303335353034303330633132343137303730366336353230353236663666373432303433343132303264323034373333333132363330323430363033353530343062306331643431373037303663363532303433363537323734363936363639363336313734363936663665323034313735373436383666373236393734373933313133333031313036303335353034306130633061343137303730366336353230343936653633326533313062333030393036303335353034303631333032353535333330316531373064333133343330333533303336333233333334333633333330356131373064333233393330333533303336333233333334333633333330356133303761333132653330326330363033353530343033306332353431373037303663363532303431373037303663363936333631373436393666366532303439366537343635363737323631373436393666366532303433343132303264323034373333333132363330323430363033353530343062306331643431373037303663363532303433363537323734363936363639363336313734363936663665323034313735373436383666373236393734373933313133333031313036303335353034306130633061343137303730366336353230343936653633326533313062333030393036303335353034303631333032353535333330353933303133303630373261383634386365336430323031303630383261383634386365336430333031303730333432303030346630313731313834313964373634383564353161356532353831303737366538383061326566646537626165346465303864666334623933653133333536643536363562333561653232643039373736306432323465376262613038666437363137636538386362373662623636373062656338653832393834666635343435613338316637333038316634333034363036303832623036303130353035303730313031303433613330333833303336303630383262303630313035303530373330303138363261363837343734373033613266326636663633373337303265363137303730366336353265363336663664326636663633373337303330333432643631373037303663363537323666366637343633363136373333333031643036303335353164306530343136303431343233663234396334346639336534656632376536633466363238366333666132626266643265346233303066303630333535316431333031303166663034303533303033303130316666333031663036303335353164323330343138333031363830313462626230646561313538333338383961613438613939646562656264656261666461636232346162333033373036303335353164316630343330333032653330326361303261613032383836323636383734373437303361326632663633373236633265363137303730366336353265363336663664326636313730373036633635373236663666373436333631363733333265363337323663333030653036303335353164306630313031666630343034303330323031303633303130303630613261383634383836663736333634303630323065303430323035303033303061303630383261383634386365336430343033303230333637303033303634303233303361636637323833353131363939623138366662333563333536636136326266663431376564643930663735346461323865626566313963383135653432623738396638393866373962353939663938643534313064386639646539633266653032333033323264643534343231623061333035373736633564663333383362393036376664313737633263323136643936346663363732363938323132366635346638376137643162393963623962303938393231363130363939306630393932316430303030333138323031386233303832303138373032303130313330383138363330376133313265333032633036303335353034303330633235343137303730366336353230343137303730366336393633363137343639366636653230343936653734363536373732363137343639366636653230343334313230326432303437333333313236333032343036303335353034306230633164343137303730366336353230343336353732373436393636363936333631373436393666366532303431373537343638366637323639373437393331313333303131303630333535303430613063306134313730373036633635323034393665363332653331306233303039303630333535303430363133303235353533303230383463333034313439353139643534333633303064303630393630383634383031363530333034303230313035303061303831393533303138303630393261383634383836663730643031303930333331306230363039326138363438383666373064303130373031333031633036303932613836343838366637306430313039303533313066313730643331333933303338333133393331333733313332333333303561333032613036303932613836343838366637306430313039333433313164333031623330306430363039363038363438303136353033303430323031303530306131306130363038326138363438636533643034303330323330326630363039326138363438383666373064303130393034333132323034323062303731303365313430613462386231376262613230316130336163643036396234653431366232613263383066383661383338313435633239373566633131333030613036303832613836343863653364303430333032303434363330343430323230343639306264636637626461663833636466343934396534633035313039656463663334373665303564373261313264376335666538633033303033343464663032323032363764353863393365626233353031333836363062353730373938613064643731313734316262353864626436613138363633353038353431656565393035303030303030303030303030227D Integrations/Stripe/DomainHealthCheck.php 0000644 00000006167 15174710275 0014507 0 ustar 00 <?php namespace WPForms\Integrations\Stripe; use WPForms\Admin\Notice; use WPForms\Integrations\Stripe\Api\DomainManager; /** * Domain Health Check class. * * @since 1.8.6 */ class DomainHealthCheck { /** * AS task name. * * @since 1.8.6 */ const ACTION = 'wpforms_stripe_domain_health_check'; /** * Admin notice ID. * * @since 1.8.6 */ const NOTICE_ID = 'wpforms_stripe_domain_site_health'; /** * Domain manager. * * @since 1.8.6 * * @var DomainManager */ private $domain_manager; /** * Initialization. * * @since 1.8.6 */ public function init() { $this->domain_manager = new DomainManager(); $this->hooks(); } /** * Register hooks. * * @since 1.8.6 */ private function hooks() { add_action( 'admin_notices', [ $this, 'admin_notice' ] ); add_action( self::ACTION, [ $this, 'process_domain_status_action' ] ); } /** * Schedule domain health check. * * @since 1.8.6 */ public function maybe_schedule_task() { /** * Allow customers to disable domain health check task. * * @since 1.8.6 * * @param bool $cancel True if task needs to be canceled. */ $is_canceled = (bool) apply_filters( 'wpforms_integrations_stripe_domain_health_check_cancel', false ); $tasks = wpforms()->obj( 'tasks' ); // Bail early in some instances. if ( $is_canceled || ! Helpers::has_stripe_keys() || $tasks->is_scheduled( self::ACTION ) ) { return; } /** * Filters the domain health check interval. * * @since 1.8.6 * * @param int $interval Interval in seconds. */ $interval = (int) apply_filters( 'wpforms_integrations_stripe_domain_health_check_interval', DAY_IN_SECONDS ); $tasks->create( self::ACTION ) ->recurring( time(), $interval ) ->register(); } /** * Process domain status. * * @since 1.8.6 */ public function process_domain_status_action() { // Bail out if Stripe account is not connected. if ( ! Helpers::has_stripe_keys() ) { return; } $this->domain_manager->validate(); } /** * Display notice about issues with domain. * * @since 1.8.6 */ public function admin_notice() { // Only load if we are actually on the settings page. if ( ! wpforms_is_admin_page( 'settings' ) ) { return; } // Bail out if Stripe account is not connected. if ( ! Helpers::has_stripe_keys() ) { return; } if ( $this->domain_manager->is_domain_active() ) { return; } $notice = sprintf( wp_kses( /* translators: %1$s - Stripe.com URL for domains registration documentation. */ __( 'Heads up! It looks like there\'s a problem with your domain verification, and Stripe Apple Pay may stop working. If this notice does not disappear in a day, <a href="%1$s" rel="nofollow noopener" target="_blank">please register it manually.</a>' , 'wpforms-lite' ), [ 'a' => [ 'href' => [], 'target' => [], 'rel' => [], ], ] ), esc_url( 'https://stripe.com/docs/payments/payment-methods/pmd-registration?platform=dashboard#register-your-domain' ) ); Notice::error( $notice, [ 'dismiss' => true, 'slug' => self::NOTICE_ID, ] ); } } Integrations/Stripe/Frontend.php 0000644 00000013057 15174710275 0012767 0 ustar 00 <?php namespace WPForms\Integrations\Stripe; use Elementor\Plugin; /** * Stripe form frontend related functionality. * * @since 1.8.2 */ class Frontend { /** * Handle name for wp_register_styles handle. * * @since 1.8.2 * * @var string */ const HANDLE = 'wpforms-stripe'; /** * Api interface. * * @since 1.8.2 * * @var Api\ApiInterface */ private $api; /** * Initialize. * * @since 1.8.2 * * @param Api\ApiInterface $api Api interface. */ public function init( $api ) { $this->api = $api; $this->hooks(); } /** * Hooks. * * @since 1.8.2 */ private function hooks() { add_action( 'wpforms_frontend_container_class', [ $this, 'form_container_class' ], 10, 2 ); add_action( 'wpforms_wp_footer', [ $this, 'enqueues' ] ); add_action( 'enqueue_block_editor_assets', [ $this, 'enqueue_assets' ] ); add_action( 'elementor/frontend/after_enqueue_styles', [ $this, 'elementor_enqueues' ] ); add_action( 'enqueue_block_assets', [ $this, 'enqueue_block_assets' ] ); add_filter( 'register_block_type_args', [ $this, 'register_block_type_args' ], 20, 2 ); if ( wpforms_is_divi_editor() ) { add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_divi_styles' ], 12 ); } } /** * Add class to form container if Stripe is enabled. * * @since 1.8.2 * * @param array $class Array of form classes. * @param array $form_data Form data of current form. * * @return array */ public function form_container_class( $class, $form_data ) { if ( ! Helpers::has_stripe_field( $form_data ) ) { return $class; } if ( ! Helpers::has_stripe_keys() ) { return $class; } if ( Helpers::is_payments_enabled( $form_data ) ) { $class[] = 'wpforms-stripe'; } return $class; } /** * Enqueue assets in the frontend if Stripe is in use on the page. * * @since 1.8.2 * * @param array $forms Form data of forms on current page. */ public function enqueues( $forms ) { if ( ! Helpers::has_stripe_enabled( $forms ) || ! Helpers::has_stripe_field( $forms, true ) ) { return; } $this->enqueue_assets(); } /** * Enqueue block editor assets. * * @since 1.8.6 */ public function enqueue_block_assets() { if ( ! is_admin() ) { return; } $this->enqueue_styles(); } /** * Enqueue assets on the frontend. * * @since 1.8.2 */ public function enqueue_assets() { $min = wpforms_get_min_suffix(); if ( ! Helpers::has_stripe_keys() ) { return; } $config = $this->api->get_config(); $in_footer = ! wpforms_is_frontend_js_header_force_load(); wp_enqueue_script( 'wpforms-generic-utils', WPFORMS_PLUGIN_URL . "assets/js/share/utils{$min}.js", [ 'jquery' ], WPFORMS_VERSION, $in_footer ); wp_enqueue_script( 'stripe-js', $config['remote_js_url'], [ 'jquery' ], null, // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion $in_footer ); wp_enqueue_script( self::HANDLE, $config['local_js_url'], [ 'jquery', 'stripe-js', 'wpforms-generic-utils' ], WPFORMS_VERSION, $in_footer ); wp_localize_script( self::HANDLE, 'wpforms_stripe', [ 'publishable_key' => Helpers::get_stripe_key( 'publishable' ), 'data' => $config['localize_script'], 'i18n' => [ 'empty_details' => esc_html__( 'Please fill out payment details to continue.', 'wpforms-lite' ), 'element_load_error' => esc_html__( 'Payment Element failed to load. Stripe API responded with the message:', 'wpforms-lite' ), 'token_already_used' => esc_html__( 'The security token has expired. Please resubmit the form.', 'wpforms-lite' ), ], 'styles_enabled' => (int) wpforms_setting( 'disable-css', '1' ) !== 3, ] ); $this->enqueue_styles(); } /** * Set editor style for block type editor. * * @since 1.8.2 * * @param array $args Array of arguments for registering a block type. * @param string $block_type Block type name including namespace. */ public function register_block_type_args( $args, $block_type ) { if ( $block_type !== 'wpforms/form-selector' || ! is_admin() ) { return $args; } $config = $this->api->get_config(); if ( ! isset( $config['local_css_url'] ) ) { return $args; } wp_register_style( 'wpforms-stripe', $config['local_css_url'], [ $args['editor_style'] ], WPFORMS_VERSION ); $args['editor_style'] = self::HANDLE; return $args; } /** * Enqueue styles for Elementor preview. * * @since 1.8.4.1 * * @noinspection PhpUndefinedFieldInspection */ public function elementor_enqueues() { if ( ! class_exists( Plugin::class ) || empty( Plugin::instance()->preview ) || ! Plugin::instance()->preview->is_preview_mode() ) { return; } $this->enqueue_styles(); } /** * Enqueue styles. * * @since 1.8.4.1 * @since 1.9.4 Become public for the action callback. */ public function enqueue_styles(): void { if ( (int) wpforms_setting( 'disable-css', '1' ) === 3 ) { return; } $min = wpforms_get_min_suffix(); wp_enqueue_style( self::HANDLE, WPFORMS_PLUGIN_URL . "assets/css/integrations/stripe/wpforms-stripe{$min}.css", [], WPFORMS_VERSION ); } /** * Enqueue Stripe integration styles for Divi Builder. * * @since 1.9.9 */ public function enqueue_divi_styles(): void { if ( (int) wpforms_setting( 'disable-css', '1' ) === 3 ) { return; } $min = wpforms_get_min_suffix(); wp_enqueue_style( self::HANDLE, WPFORMS_PLUGIN_URL . "assets/css/integrations/stripe/divi/wpforms-stripe{$min}.css", [], WPFORMS_VERSION ); } } Integrations/Stripe/Stripe.php 0000644 00000003366 15174710275 0012460 0 ustar 00 <?php namespace WPForms\Integrations\Stripe; use WPForms\Integrations\IntegrationInterface; /** * Integration of the Stripe payment gateway. * * @since 1.8.2 */ final class Stripe implements IntegrationInterface { /** * Determine if the integration is allowed to load. * * @since 1.8.2 * * @return bool */ public function allow_load() { // Determine whether the Stripe addon version is compatible with the WPForms plugin version. $addon_compat = ( new StripeAddonCompatibility() )->init(); if ( $addon_compat && ! $addon_compat->is_supported_version() ) { $addon_compat->hooks(); return false; } /** * Whether the integration is allowed to load. * * @since 1.8.2 * * @param bool $is_allowed Integration loading state. */ return (bool) apply_filters( 'wpforms_integrations_stripe_allow_load', true ); } /** * Load the integration. * * @since 1.8.2 */ public function load() { ( new Api\WebhookRoute() )->init(); if ( wpforms_is_admin_page( 'builder' ) ) { ( new Admin\Builder\Enqueues() )->init(); } $api = new Api\PaymentIntents(); ( new WebhooksHealthCheck() )->init(); ( new DomainHealthCheck() )->init(); ( new Admin\Payments\SingleActionsHandler() )->init( $api ); // Bail early for paid users with active Stripe addon. if ( Helpers::is_pro() ) { return; } // It must be run only for the integration bundled into the core plugin. $api->init(); ( new Process() )->init( $api ); ( new Frontend() )->init( $api ); if ( wpforms_is_admin_page( 'settings', 'payments' ) ) { ( new Admin\Settings() )->init(); } if ( wpforms_is_admin_page( 'builder' ) ) { ( new Admin\Builder\Settings() )->init(); ( new Admin\Builder\Notifications() )->init(); } } } Integrations/Stripe/Admin/Settings.php 0000644 00000027326 15174710275 0014044 0 ustar 00 <?php namespace WPForms\Integrations\Stripe\Admin; use WPForms\Integrations\Stripe\Api\PaymentIntents; use WPForms\Integrations\Stripe\Helpers; use WPForms\Admin\Notice; /** * Stripe "Settings" section methods. * * @since 1.8.2 */ class Settings { /** * Stripe Connect. * * @since 1.8.2 * * @var Connect */ protected $connect; /** * Stripe Webhook Settings. * * @since 1.8.4 * * @var WebhookSettings */ protected $webhook_settings; /** * Initialize class. * * @since 1.8.2 */ public function init() { $this->connect = ( new Connect() )->init(); $this->webhook_settings = ( new WebhookSettings() )->init(); $this->hooks(); } /** * Register hooks. * * @since 1.8.2 */ private function hooks() { add_action( 'wpforms_settings_init', [ $this, 'connection_is_missing_notice' ] ); add_action( 'wpforms_settings_init', [ $this, 'not_supported_currency_notice' ] ); add_action( 'wpforms_settings_enqueue', [ $this, 'enqueue_assets' ] ); add_filter( 'wpforms_settings_defaults', [ $this, 'register_settings_fields' ], 6 ); } /** * Stripe is not connected for the current payment mode notice. * * @since 1.8.2 */ public function connection_is_missing_notice() { if ( ! Helpers::is_pro() || Helpers::has_stripe_keys() ) { return; } $account = $this->connect->get_connected_account(); if ( ! empty( $account->id ) ) { return; } Notice::warning( esc_html__( 'Stripe is not connected for your current payment mode. Please press the "Connect with Stripe" button to complete this setup.', 'wpforms-lite' ) ); } /** * Selected currency is not supported for connected account. * * @since 1.8.2 */ public function not_supported_currency_notice() { if ( ! Helpers::has_stripe_keys() ) { return; } $account = $this->connect->get_connected_account(); if ( is_null( $account ) ) { return; } $selected_currency = strtolower( wpforms_get_currency() ); if ( $selected_currency === $account->default_currency ) { return; } $country_specs = ( new PaymentIntents() )->get_country_specs( $account->country ); if ( ! $country_specs || in_array( $selected_currency, $country_specs->supported_payment_currencies, true ) ) { return; } Notice::error( sprintf( wp_kses( /* translators: %1$s - Selected currency on the WPForms Settings admin page. */ __( '<strong>Payments Cannot Be Processed</strong><br>The currency you have set (%1$s) is not supported by Stripe. Please choose a different currency.', 'wpforms-lite' ), [ 'strong' => [], 'br' => [], ] ), esc_html( wpforms_get_currency() ) ) ); } /** * Enqueue "Settings" scripts and styles. * * @since 1.8.2 */ public function enqueue_assets() { $min = wpforms_get_min_suffix(); wp_enqueue_style( 'wpforms-admin-settings-stripe', WPFORMS_PLUGIN_URL . "assets/css/integrations/stripe/admin-settings-stripe{$min}.css", [], WPFORMS_VERSION ); wp_enqueue_script( 'wpforms-admin-settings-stripe', WPFORMS_PLUGIN_URL . "assets/js/integrations/stripe/admin-settings-stripe{$min}.js", [ 'jquery', 'wpforms-admin-utils' ], WPFORMS_VERSION, true ); $admin_settings_stripe_l10n = [ 'mode_update' => wp_kses( __( '<p>Switching test/live modes requires Stripe account reconnection.</p><p>Press the <em>"Connect with Stripe"</em> button after saving the settings to reconnect.</p>', 'wpforms-lite' ), [ 'p' => [], 'em' => [], ] ), 'webhook_urls' => [ 'rest' => Helpers::get_webhook_url_for_rest(), 'curl' => Helpers::get_webhook_url_for_curl(), ], ]; wp_localize_script( 'wpforms-admin-settings-stripe', 'wpforms_admin_settings_stripe', $admin_settings_stripe_l10n ); } /** * Register "Stripe" settings fields. * * @since 1.8.2 * * @param array $settings Admin area settings list. * * @return array */ public function register_settings_fields( $settings ) { // Bail early, in case "Payments" settings is not registered. if ( ! isset( $settings['payments'] ) ) { return $settings; } $stripe_settings = [ 'stripe-heading' => [ 'id' => 'stripe-heading', 'content' => $this->get_heading_content(), 'type' => 'content', 'no_label' => true, 'class' => [ 'section-heading' ], ], 'stripe-connection-status' => [ 'id' => 'stripe-connection-status', 'name' => esc_html__( 'Connection Status', 'wpforms-lite' ), 'content' => $this->get_connection_status_content(), 'type' => 'content', ], 'stripe-test-mode' => [ 'id' => 'stripe-test-mode', 'name' => esc_html__( 'Test Mode', 'wpforms-lite' ), 'type' => 'toggle', 'status' => true, 'desc' => sprintf( wp_kses( /* translators: %s - WPForms.com URL for Stripe payments with more details. */ __( 'Prevent Stripe from processing live transactions. Please see <a href="%s" target="_blank" rel="noopener noreferrer">our documentation on Stripe test payments</a> for full details.', 'wpforms-lite' ), [ 'a' => [ 'href' => [], 'target' => [], 'rel' => [], 'class' => [], ], ] ), esc_url( wpforms_utm_link( 'https://wpforms.com/docs/how-to-test-stripe-payments-on-your-site/', 'Settings - Payments', 'Stripe Test Payments Documentation' ) ) ), ], ]; $stripe_settings = $this->webhook_settings->settings( $stripe_settings ); $this->maybe_set_card_mode(); // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( ! empty( $_GET['stripe_card_mode'] ) || ! Helpers::is_payment_element_enabled() ) { $stripe_settings['stripe-card-mode'] = [ 'id' => 'stripe-card-mode', 'name' => esc_html__( 'Credit Card Field Mode', 'wpforms-lite' ), 'type' => 'radio', 'default' => 'payment', 'desc_after' => $this->get_credit_card_field_desc_after(), 'options' => [ 'card' => esc_html__( 'Card Element', 'wpforms-lite' ), 'payment' => esc_html__( 'Payment Element', 'wpforms-lite' ), ], ]; } $settings['payments'] = array_merge( $settings['payments'], $stripe_settings ); return $settings; } /** * Maybe set card mode setting. * * @since 1.8.2 */ private function maybe_set_card_mode() { // Bail out if a card mode is already set. if ( wpforms_setting( 'stripe-card-mode' ) ) { return; } $settings = (array) get_option( 'wpforms_settings', [] ); $settings['stripe-card-mode'] = Helpers::has_stripe_keys() ? 'card' : 'payment'; update_option( 'wpforms_settings', $settings ); } /** * Section header content. * * @since 1.8.2 * * @return string */ private function get_heading_content() { return '<h4>' . esc_html__( 'Stripe', 'wpforms-lite' ) . '</h4>' . '<p>' . sprintf( wp_kses( /* translators: %s - WPForms.com Stripe documentation article URL. */ __( 'Easily collect credit card payments with Stripe. For getting started and more information, see our <a href="%s" target="_blank" rel="noopener noreferrer">Stripe documentation</a>.', 'wpforms-lite' ), [ 'a' => [ 'href' => [], 'target' => [], 'rel' => [], ], ] ), esc_url( wpforms_utm_link( 'https://wpforms.com/docs/how-to-install-and-use-the-stripe-addon-with-wpforms/', 'Settings - Payments', 'Stripe Documentation' ) ) ) . '</p>' . Notices::get_fee_notice(); } /** * Connection Status setting content. * * @since 1.8.2 * * @return string */ protected function get_connection_status_content() { $output = ''; $current_mode = Helpers::get_stripe_mode(); foreach ( Helpers::CONNECTION_MODES as $mode ) { $class_names = [ 'wpforms-stripe-connection-status', "wpforms-stripe-connection-status-{$mode}", $current_mode !== $mode ? 'wpforms-hide' : '', ]; $account = $this->connect->get_connected_account( $mode ); $output .= sprintf( '<div %s>', wpforms_html_attributes( '', $class_names ) ); if ( empty( $account->id ) ) { $output .= $this->get_disconnected_status_content( $mode ); } else { $output .= $this->get_connected_status_content( $mode ); } $output .= '</div>'; } return $output; } /** * Connected Status setting content. * * @since 1.8.2 * * @param string $mode Stripe mode (e.g. 'live' or 'test'). * * @return string */ private function get_connected_status_content( string $mode = '' ): string { $output = ''; $account_name = $this->connect->get_connected_account_name( $mode ); $connect_url = $this->connect->get_connect_with_stripe_url( $mode ); $disconnect_url = $this->connect->get_disconnect_stripe_url( $mode ); $connected_status = sprintf( wp_kses( /* translators: %1$s - Stripe account name connected, %2$s - Stripe mode connected (live or test). */ __( 'Connected to Stripe as <em>%1$s</em> in <strong>%2$s Mode</strong>.', 'wpforms-lite' ), [ 'strong' => [], 'em' => [], ] ), esc_html( $account_name ), ucwords( $mode ? $mode : Helpers::get_stripe_mode() ) ); $output .= sprintf( '<div class="wpforms-connected"><p>%s</p></div>', $connected_status ); $output .= '<p>' . sprintf( wp_kses( /* translators: %s - Stripe connect URL. */ __( '<a href="%s" class="wpforms-btn wpforms-btn-md wpforms-btn-light-grey" style="margin-right: 10px;">Switch Accounts</a>', 'wpforms-lite' ), [ 'a' => [ 'href' => [], 'class' => [], 'style' => [], ], ] ), esc_url( $connect_url ) ); $output .= sprintf( wp_kses( /* translators: %s - Stripe disconnect URL. */ __( '<a href="%s" class="wpforms-btn wpforms-btn-md wpforms-btn-light-grey">Disconnect</a>', 'wpforms-lite' ), [ 'a' => [ 'href' => [], 'class' => [], ], ] ), esc_url( $disconnect_url ) ) . '</p>'; return $output; } /** * Disconnected Status setting content. * * @since 1.8.2 * * @param string $mode Stripe mode (e.g. 'live' or 'test'). * * @return string */ protected function get_disconnected_status_content( $mode = '' ) { $connect_url = $this->connect->get_connect_with_stripe_url( $mode ); $connect_button = sprintf( wp_kses( /* translators: %s - WPForms.com Stripe documentation article URL. */ __( 'Securely connect to Stripe with just a few clicks to begin accepting payments! <a href="%s" target="_blank" rel="noopener noreferrer" class="wpforms-learn-more">Learn More</a>', 'wpforms-lite' ), [ 'a' => [ 'href' => [], 'target' => [], 'rel' => [], 'class' => [], ], ] ), esc_url( wpforms_utm_link( 'https://wpforms.com/docs/how-to-install-and-use-the-stripe-addon-with-wpforms/#connect-stripe', 'Settings - Payments', 'Stripe Documentation' ) ) ); return sprintf( '<div class="wpforms-connect"><a href="%s" class="wpforms-stripe-connect-button" title="%s"></a><p>%s</p></div>', esc_url( $connect_url ), esc_attr__( 'Connect with Stripe', 'wpforms-lite' ), $connect_button ); } /** * Credit Card mode description. * * @since 1.8.2 * * @return string */ private function get_credit_card_field_desc_after() { return '<p class="desc">' . sprintf( wp_kses( /* translators: %s - WPForms.com Stripe documentation article URL. */ __( 'Please see <a href="%s" target="_blank" rel="noopener noreferrer">our documentation on Stripe Credit Card field modes for full details</a>.', 'wpforms-lite' ), [ 'a' => [ 'href' => [], 'target' => [], 'rel' => [], ], ] ), esc_url( wpforms_utm_link( 'https://wpforms.com/docs/how-to-install-and-use-the-stripe-addon-with-wpforms/#field-modes', 'Settings - Payments', 'Stripe Field Modes' ) ) ) . '</p>'; } } Integrations/Stripe/Admin/WebhookSettings.php 0000644 00000022455 15174710275 0015361 0 ustar 00 <?php namespace WPForms\Integrations\Stripe\Admin; use WPForms\Integrations\Stripe\Helpers; use WPForms\Integrations\Stripe\WebhooksHealthCheck; /** * Stripe "Webhook Settings" section methods. * * @since 1.8.4 */ class WebhookSettings { /** * Initialization. * * @since 1.8.4 * * @return WebhookSettings */ public function init() { return $this; } /** * Register "Stripe webhooks" settings fields. * * @since 1.8.4 * * @param array $settings Admin area settings list. * * @return array */ public function settings( $settings ) { $this->maybe_set_default_settings(); // Do not display it as long as Stripe account is not connected. if ( ! Helpers::has_stripe_keys() ) { return $settings; } $settings['stripe-webhooks-enabled'] = [ 'id' => 'stripe-webhooks-enabled', 'name' => esc_html__( 'Enable Webhooks', 'wpforms-lite' ), 'type' => 'toggle', 'status' => true, 'default' => true, 'desc' => sprintf( wp_kses( /* translators: %s - WPForms.com URL for Stripe webhooks documentation. */ __( 'Stripe uses webhooks to notify WPForms when an event has occurred in your Stripe account. Please see <a href="%s" target="_blank" rel="noopener noreferrer">our documentation on Stripe webhooks</a> for full details.', 'wpforms-lite' ), [ 'a' => [ 'href' => [], 'target' => [], 'rel' => [], ], ] ), esc_url( wpforms_utm_link( 'https://wpforms.com/docs/setting-up-stripe-webhooks/', 'Stripe Settings', 'Enable webhooks' ) ) ), ]; // Bail out if $_GET parameter is not passed or webhooks is configured and active. if ( ! isset( $_GET['webhooks_settings'] ) && // phpcs:ignore WordPress.Security.NonceVerification.Recommended Helpers::is_webhook_configured() && ( new WebhooksHealthCheck() )->is_webhooks_active() ) { return $settings; } $settings['stripe-webhooks-communication'] = [ 'id' => 'stripe-webhooks-communication', 'name' => esc_html__( 'Webhooks Method', 'wpforms-lite' ), 'type' => 'radio', 'default' => wpforms_setting( 'stripe-webhooks-communication', 'rest' ), 'options' => [ 'rest' => esc_html__( 'REST API (recommended)', 'wpforms-lite' ), 'curl' => esc_html__( 'PHP listener', 'wpforms-lite' ), ], 'desc' => sprintf( wp_kses( /* translators: %s - WPForms.com URL for Stripe webhooks documentation. */ __( 'Choose the method of communication between Stripe and WPForms. If REST API support is disabled for WordPress, use PHP listener. <a href="%s" rel="nofollow noopener" target="_blank">Learn more</a>.', 'wpforms-lite' ), [ 'a' => [ 'href' => [], 'target' => [], 'rel' => [], ], ] ), esc_url( wpforms_utm_link( 'https://wpforms.com/docs/setting-up-stripe-webhooks/', 'Stripe Settings', 'Webhook Listener' ) ) ), 'class' => $this->get_html_classes(), ]; $settings['stripe-webhooks-endpoint-set'] = [ 'id' => 'stripe-webhooks-endpoint-set', 'name' => esc_html__( 'Webhooks Endpoint', 'wpforms-lite' ), 'url' => Helpers::get_webhook_url(), 'type' => 'webhook_endpoint', 'desc' => sprintf( wp_kses( /* translators: %s - Stripe Webhooks Settings url. */ __( 'Ensure an endpoint with the above URL is present in the <a href="%s" target="_blank" rel="noopener noreferrer">Stripe webhook settings</a>.', 'wpforms-lite' ), [ 'a' => [ 'href' => [], 'target' => [], 'rel' => [], ], ] ), Helpers::get_stripe_mode() === 'test' ? 'https://dashboard.stripe.com/test/workbench/webhooks/create' : 'https://dashboard.stripe.com/workbench/webhooks/create' ), 'class' => $this->get_html_classes(), ]; $settings['stripe-webhooks-id-test'] = [ 'id' => 'stripe-webhooks-id-test', 'name' => esc_html__( 'Webhooks Test ID', 'wpforms-lite' ), 'type' => 'text', 'desc' => $this->get_webhooks_id_desc( 'test' ), 'class' => $this->get_html_classes(), ]; $settings['stripe-webhooks-secret-test'] = [ 'id' => 'stripe-webhooks-secret-test', 'name' => esc_html__( 'Webhooks Test Secret', 'wpforms-lite' ), 'type' => 'password', 'desc' => $this->get_webhooks_secret_desc( 'test' ), 'class' => $this->get_html_classes(), ]; $settings['stripe-webhooks-id-live'] = [ 'id' => 'stripe-webhooks-id-live', 'name' => esc_html__( 'Webhooks Live ID', 'wpforms-lite' ), 'type' => 'text', 'desc' => $this->get_webhooks_id_desc( 'live' ), 'class' => $this->get_html_classes(), ]; $settings['stripe-webhooks-secret-live'] = [ 'id' => 'stripe-webhooks-secret-live', 'name' => esc_html__( 'Webhooks Live Secret', 'wpforms-lite' ), 'type' => 'password', 'desc' => $this->get_webhooks_secret_desc( 'live' ), 'class' => $this->get_html_classes(), ]; return $settings; } /** * Show the link to the documentation about the Webhooks ID. * * @since 1.8.4 * * @param string $mode Stripe mode (e.g. 'live' or 'test'). * * @return string */ private function get_webhooks_id_desc( $mode ) { $modes = [ 'live' => __( 'Live Mode Endpoint ID', 'wpforms-lite' ), 'test' => __( 'Test Mode Endpoint ID', 'wpforms-lite' ), ]; return sprintf( wp_kses( /* translators: %1$s - Live Mode Endpoint ID or Test Mode Endpoint ID. %2$s - WPForms.com Stripe documentation article URL. */ __( 'Retrieve your %1$s from your <a href="%2$s" target="_blank" rel="noopener noreferrer">Stripe webhook settings</a>. Select the endpoint, then click Copy button.', 'wpforms-lite' ), [ 'a' => [ 'href' => [], 'target' => [], 'rel' => [], ], ] ), $modes[ $mode ], $mode === 'test' ? 'https://dashboard.stripe.com/test/workbench/webhooks' : 'https://dashboard.stripe.com/workbench/webhooks' ); } /** * Show the link to the documentation about the Webhooks Secret. * * @since 1.8.4 * * @param string $mode Stripe mode (e.g. 'live' or 'test'). * * @return string */ private function get_webhooks_secret_desc( $mode ) { $modes = [ 'live' => __( 'Live Mode Signing Secret', 'wpforms-lite' ), 'test' => __( 'Test Mode Signing Secret', 'wpforms-lite' ), ]; return sprintf( wp_kses( /* translators: %1$s - Live Mode Signing Secret or Test Mode Signing Secret. %2$s - WPForms.com Stripe documentation article URL. */ __( 'Retrieve your %1$s from your <a href="%2$s" target="_blank" rel="noopener noreferrer">Stripe webhook settings</a>. Select the endpoint, then click Reveal.', 'wpforms-lite' ), [ 'a' => [ 'href' => [], 'target' => [], 'rel' => [], ], ] ), $modes[ $mode ], $mode === 'test' ? 'https://dashboard.stripe.com/test/workbench/webhooks' : 'https://dashboard.stripe.com/workbench/webhooks' ); } /** * Get HTML classes for the Webhooks section. * * @since 1.8.4 * * @return array */ private function get_html_classes() { $classes = [ 'wpforms-settings-stripe-webhooks' ]; // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( isset( $_GET['webhooks_settings'] ) ) { return $classes; } if ( ! wpforms_setting( 'stripe-webhooks-enabled' ) ) { $classes[] = 'wpforms-hide'; } return $classes; } /** * Maybe set default settings. * * @since 1.8.4 */ private function maybe_set_default_settings() { $settings = (array) get_option( 'wpforms_settings', [] ); $is_updated = false; // Enable Stripe webhooks by default if account is connected. // phpcs:ignore WPForms.PHP.BackSlash.UseShortSyntax if ( ! isset( $settings['stripe-webhooks-enabled'] ) && Helpers::has_stripe_keys() ) { $settings['stripe-webhooks-enabled'] = true; $is_updated = true; } // Set a default communication method. if ( ! isset( $settings['stripe-webhooks-communication'] ) ) { $settings['stripe-webhooks-communication'] = $this->is_rest_api_enabled() ? 'rest' : 'curl'; $is_updated = true; } // Save settings only if something is changed. if ( $is_updated ) { update_option( 'wpforms_settings', $settings ); } } /** * Check if REST API is enabled. * * Testing configured webhook endpoint with non-authorised request. * Based on UsageTracking::is_rest_api_enabled(). * * @since 1.8.4 * * @return bool */ private function is_rest_api_enabled() { // phpcs:disable WPForms.PHP.ValidateHooks.InvalidHookName /** This filter is documented in wp-includes/class-wp-http-streams.php */ $sslverify = apply_filters( 'https_local_ssl_verify', false ); // phpcs:enable WPForms.PHP.ValidateHooks.InvalidHookName $url = add_query_arg( [ 'verify' => 1 ], Helpers::get_webhook_url_for_rest() ); $response = wp_remote_get( $url, [ 'timeout' => 10, 'cookies' => [], 'sslverify' => $sslverify, 'headers' => [ 'Cache-Control' => 'no-cache', ], ] ); // When testing the REST API, an error was encountered, leave early. if ( is_wp_error( $response ) ) { return false; } // When testing the REST API, an unexpected result was returned, leave early. if ( wp_remote_retrieve_response_code( $response ) !== 200 ) { return false; } // The REST API did not behave correctly, leave early. if ( ! wpforms_is_json( wp_remote_retrieve_body( $response ) ) ) { return false; } // We are all set. Confirm the connection. return true; } } Integrations/Stripe/Admin/Builder/Settings.php 0000644 00000016545 15174710275 0015433 0 ustar 00 <?php namespace WPForms\Integrations\Stripe\Admin\Builder; use WPForms\Integrations\Stripe\Helpers; /** * Settings panel for Stripe in the Builder. * * @since 1.8.2 */ class Settings { use Traits\ContentTrait; /** * Slug of the integration. * * @since 1.8.2 * * @var string */ private $slug = 'stripe'; /** * Name of the integration. * * @since 1.8.2 * * @var string */ private $name = 'Stripe'; /** * Marker means the payment integration is recommended. * * @since 1.8.2 * * @var bool */ private $recommended = true; /** * Icon URL. * * @since 1.8.2 * * @var string */ private $icon = ''; /** * Form data. * * @since 1.8.2 * * @var array $form_data */ private $form_data = []; /** * Initialize. * * @since 1.8.2 */ public function init() { $this->icon = WPFORMS_PLUGIN_URL . 'assets/images/addon-icon-stripe.png'; $this->form_data = $this->get_form_data(); $this->hooks(); } /** * Register hooks. * * @since 1.8.2 */ private function hooks() { add_filter( 'wpforms_payments_available', [ $this, 'register_payment' ] ); add_action( 'wpforms_payments_panel_content', [ $this, 'builder_output' ], 0 ); add_action( 'wpforms_payments_panel_sidebar', [ $this, 'builder_sidebar' ], 0 ); add_filter( 'wpforms_admin_education_addons_item_base_display_single_addon_hide', [ $this, 'should_hide_educational_menu_item' ], 10, 2 ); } /** * Register the payment gateway. * * @since 1.8.2 * * @param array $payments_available List of available payment gateways. * * @return array */ public function register_payment( $payments_available ) { $payments_available[ $this->slug ] = $this->name; return $payments_available; } /** * Output the gateway menu item. * * @since 1.8.2 */ public function builder_sidebar() { // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo wpforms_render( 'builder/payment/sidebar', [ 'configured' => $this->is_payments_enabled() ? 'configured' : '', 'slug' => $this->slug, 'icon' => $this->icon, 'name' => $this->name, 'recommended' => $this->recommended, ], true ); } /** * Output the gateway settings. * * @since 1.8.2 */ public function builder_output() { ?> <div class="wpforms-panel-content-section wpforms-panel-content-section-<?php echo esc_attr( $this->slug ); ?>" id="<?php echo esc_attr( $this->slug ); ?>-provider" data-provider="<?php echo esc_attr( $this->slug ); ?>" data-provider-name="<?php echo esc_attr( $this->name ); ?>"> <div class="wpforms-panel-content-section-title"> <?php echo esc_html( $this->name ); ?> </div> <div class="wpforms-payment-settings wpforms-clear"> <?php $this->builder_content(); ?> </div> </div> <?php } /** * Check if it is going to be displayed Stripe educational menu item and hide it. * * @since 1.8.2 * * @param bool $hide Whether to hide the menu item. * @param array $addon Addon data. * * @return bool */ public function should_hide_educational_menu_item( $hide, $addon ) { return isset( $addon['clear_slug'] ) && $this->slug === $addon['clear_slug'] ? true : $hide; } /** * Check if payments enabled. * * @since 1.8.2 * * @return bool */ private function is_payments_enabled(): bool { return ! empty( $this->form_data['payments'][ $this->slug ]['enable'] ) || ! empty( $this->form_data['payments'][ $this->slug ]['enable_one_time'] ) || ! empty( $this->form_data['payments'][ $this->slug ]['enable_recurring'] ); } /** * Get form data. * * @since 1.8.2 * * @return array */ private function get_form_data() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $form_id = isset( $_GET['form_id'] ) ? absint( $_GET['form_id'] ) : 0; if ( ! $form_id ) { return []; } $form_data = wpforms()->obj( 'form' )->get( $form_id, [ 'content_only' => true, ] ); return is_array( $form_data ) ? $form_data : []; } /** * Get single payments conditional logic for the Stripe settings panel. * * @since 1.8.2 * * @return string */ private function single_payments_conditional_logic_section() { return $this->get_conditional_logic_toggle(); } /** * Get recurring payments conditional logic for the Stripe settings panel. * * @since 1.8.2 * @since 1.8.4 Added Plan ID parameter. * * @param string $plan_id Plan ID. * * @return string */ private function recurring_payments_conditional_logic_section( $plan_id ) { return $this->get_conditional_logic_toggle( true ); } /** * Get education toggle for the conditional logic. * * @since 1.8.2 * * @param bool $is_recurring Is recurring section. * * @return string */ private function get_conditional_logic_toggle( $is_recurring = false ) { return wpforms_panel_field( 'toggle', 'stripe', 'conditional_logic', $this->maybe_reset_conditional_logic( $this->form_data ), esc_html__( 'Enable Conditional Logic', 'wpforms-lite' ), [ 'input_class' => 'education-modal', 'parent' => 'payments', 'subsection' => $is_recurring ? 'recurring' : '', 'pro_badge' => ! Helpers::is_allowed_license_type(), 'data' => $this->get_conditional_logic_section_data(), 'attrs' => [ 'disabled' => 'disabled', ], ], false ); } /** * Get conditional logic section data. * * @since 1.8.2 * * @return array */ private function get_conditional_logic_section_data() { $addon = wpforms()->obj( 'addons' )->get_addon( 'stripe' ); if ( empty( $addon ) || empty( $addon['action'] ) || empty( $addon['status'] ) || ( $addon['status'] === 'active' && $addon['action'] !== 'upgrade' ) ) { return []; } if ( $addon['plugin_allow'] && $addon['action'] === 'install' ) { return [ 'action' => 'install', 'message' => esc_html__( 'The Stripe Pro addon is required to enable conditional logic for payments. Would you like to install and activate it?', 'wpforms-lite' ), 'url' => $addon['url'], 'nonce' => wp_create_nonce( 'wpforms-admin' ), 'license' => 'pro', ]; } if ( $addon['plugin_allow'] && $addon['action'] === 'activate' ) { return [ 'action' => 'activate', 'message' => esc_html__( 'The Stripe Pro addon is required to enable conditional logic for payments. Would you like to activate it?', 'wpforms-lite' ), 'path' => $addon['path'], 'nonce' => wp_create_nonce( 'wpforms-admin' ), ]; } return [ 'action' => 'upgrade', 'name' => esc_html__( 'Smart Conditional Logic', 'wpforms-lite' ), 'utm-content' => 'Builder Stripe Conditional Logic', 'licence' => 'pro', ]; } /** * Maybe reset conditional logic. * * If Stripe Pro is disabled, reset conditional logic for Stripe settings. * * @since 1.8.2.2 * * @param array $form_data Form data. * * @return array */ private function maybe_reset_conditional_logic( $form_data ) { if ( Helpers::is_pro() ) { return $form_data; } if ( ! isset( $form_data['payments']['stripe']['conditional_logic'] ) && ! isset( $form_data['payments']['stripe']['recurring']['conditional_logic'] ) ) { return $form_data; } unset( $form_data['payments']['stripe']['conditional_logic'], $form_data['payments']['stripe']['recurring']['conditional_logic'] ); return $form_data; } } Integrations/Stripe/Admin/Builder/Notifications.php 0000644 00000006326 15174710275 0016440 0 ustar 00 <?php namespace WPForms\Integrations\Stripe\Admin\Builder; use WPForms\Integrations\Stripe\Helpers; /** * Stripe Form Builder notifications-related functionality. * * @since 1.9.5 */ class Notifications { /** * Initialize. * * @since 1.9.5 */ public function init() { $this->hooks(); } /** * Register hooks. * * @since 1.9.5 */ private function hooks() { $hook_name = wpforms()->is_pro() ? 'wpforms_form_settings_notifications_single_after' : 'wpforms_lite_form_settings_notifications_block_content_after'; add_action( $hook_name, [ $this, 'notification_settings' ], 5, 2 ); } /** * Add checkbox to form notification settings. * * @since 1.9.5 * * @param object|mixed $settings Current confirmation settings. * @param int $id Subsection ID. */ public function notification_settings( $settings, int $id ) { if ( empty( $settings->form_data ) ) { return; } wpforms_panel_field( 'toggle', 'notifications', 'stripe', $settings->form_data, esc_html__( 'Enable for Stripe completed payments', 'wpforms-lite' ), $this->get_notification_settings_data( $settings->form_data, $id ) ); } /** * Get notification settings data based on the license type. * * @since 1.9.5 * * @param array $form_data Form settings data. * @param int $id Subsection ID. * * @return array */ private function get_notification_settings_data( array $form_data, int $id ): array { return [ 'parent' => 'settings', 'class' => ! Helpers::is_payments_enabled( $form_data ) ? 'wpforms-hidden' : '', 'subsection' => $id, 'value' => 0, 'input_class' => 'education-modal', 'pro_badge' => ! Helpers::is_allowed_license_type(), 'data' => $this->get_notification_section_data(), 'attrs' => [ 'disabled' => 'disabled' ], ]; } /** * Get notification section data. * * @since 1.9.5 * * @return array */ private function get_notification_section_data(): array { $addon = wpforms()->obj( 'addons' )->get_addon( 'stripe' ); if ( empty( $addon ) || empty( $addon['action'] ) || empty( $addon['status'] ) || ( $addon['status'] === 'active' && $addon['action'] !== 'upgrade' ) ) { return []; } if ( $addon['plugin_allow'] && $addon['action'] === 'install' ) { return [ 'action' => 'install', 'message' => esc_html__( 'The Stripe Pro addon is required to enable notification for completed payments. Would you like to install and activate it?', 'wpforms-lite' ), 'url' => $addon['url'], 'nonce' => wp_create_nonce( 'wpforms-admin' ), 'license' => 'pro', ]; } if ( $addon['plugin_allow'] && $addon['action'] === 'activate' ) { return [ 'action' => 'activate', 'message' => esc_html__( 'The Stripe Pro addon is required to enable notification for completed payments. Would you like to activate it?', 'wpforms-lite' ), 'path' => $addon['path'], 'nonce' => wp_create_nonce( 'wpforms-admin' ), ]; } return [ 'action' => 'upgrade', 'name' => esc_html__( 'Notification for Stripe Completed Payments', 'wpforms-lite' ), 'utm-content' => 'Builder Stripe Completed Payments', 'license' => 'pro', ]; } } Integrations/Stripe/Admin/Builder/Enqueues.php 0000644 00000007242 15174710275 0015417 0 ustar 00 <?php namespace WPForms\Integrations\Stripe\Admin\Builder; use WPForms\Integrations\Stripe\Helpers; /** * Script enqueues for the Stripe Builder settings panel. * * @since 1.8.2 */ class Enqueues { /** * Initialize. * * @since 1.8.2 */ public function init() { $this->hooks(); } /** * Hooks. * * @since 1.8.2 */ private function hooks() { add_filter( 'wpforms_builder_strings', [ $this, 'javascript_strings' ], 10, 2 ); add_action( 'wpforms_builder_enqueues', [ $this, 'enqueues' ] ); } /** * Add our localized strings to be available in the form builder. * * @since 1.8.2 * * @param array $strings Form builder JS strings. * @param array $form Form data and settings. * * @return array */ public function javascript_strings( $strings, $form = [] ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed $strings = (array) $strings; $strings['stripe_recurring_heading'] = esc_html__( 'Missing Required Fields', 'wpforms-lite' ); $strings['stripe_recurring_email'] = esc_html__( 'When recurring subscription payments are enabled, the Customer Email is required.', 'wpforms-lite' ); $strings['stripe_required_one_time_fields'] = esc_html__( 'In order to complete your form\'s Stripe One-Time Payments, please check that all required (*) fields have been filled out.', 'wpforms-lite' ); $strings['stripe_required_recurring_fields'] = esc_html__( 'In order to complete your form\'s Stripe Recurring Subscription Payments, please check that all required (*) fields have been filled out.', 'wpforms-lite' ); $strings['stripe_required_both_fields'] = esc_html__( 'In order to complete your form\'s Stripe One-Time Payments and Recurring Subscription Payments, please check that all required (*) fields have been filled out.', 'wpforms-lite' ); $strings['stripe_recurring_settings'] = wp_kses( __( 'Please go to the <a href="#" class="wpforms-stripe-settings-redirect">Stripe payment settings</a> and fill out the required field(s).', 'wpforms-lite' ), [ 'a' => [ 'href' => [], 'class' => [], ], ] ); return $strings; } /** * Enqueue assets for the builder. * * @since 1.8.2 * * @param string|null $view Current view. */ public function enqueues( $view = null ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found $min = wpforms_get_min_suffix(); if ( Helpers::has_stripe_keys() ) { wp_enqueue_style( 'wpforms-builder-stripe-common', WPFORMS_PLUGIN_URL . "assets/css/integrations/stripe/builder-stripe-common{$min}.css", [], WPFORMS_VERSION ); } wp_enqueue_script( 'wpforms-builder-stripe', WPFORMS_PLUGIN_URL . "assets/js/integrations/stripe/admin-builder-stripe{$min}.js", [ 'conditions' ], WPFORMS_VERSION, false ); wp_enqueue_script( 'wpforms-builder-modern-stripe', WPFORMS_PLUGIN_URL . "assets/js/integrations/stripe/admin-builder-modern-stripe{$min}.js", [], WPFORMS_VERSION, false ); /** * Allow to filter builder stripe script data. * * @since 1.8.2 * @since 1.9.5 Added the `field_slug` key. * * @param array $data Script data. */ $script_data = (array) apply_filters( 'wpforms_integrations_stripe_admin_builder_enqueues_data', [ 'field_slug' => Helpers::get_field_slug(), 'field_slugs' => [ 'stripe-credit-card' ], 'is_pro' => Helpers::is_pro(), 'cycles_max' => Helpers::recurring_plan_cycles_max(), 'i18n' => [ 'cycles_default' => esc_html__( 'Unlimited', 'wpforms-lite' ), ], ] ); wp_localize_script( 'wpforms-builder-stripe', 'wpforms_builder_stripe', $script_data ); } } Integrations/Stripe/Admin/Builder/Traits/ContentTrait.php 0000644 00000053224 15174710275 0017512 0 ustar 00 <?php namespace WPForms\Integrations\Stripe\Admin\Builder\Traits; use WPForms\Integrations\Stripe\Helpers; use WPForms\Integrations\Stripe\Admin\Notices; /** * Payment builder settings content trait. * * @since 1.8.2 */ trait ContentTrait { /** * Display content inside the panel content area. * * @since 1.8.2 */ public function builder_content() { if ( $this->builder_alerts() ) { return; } $hide_class = ! Helpers::has_stripe_field( $this->form_data ) ? 'wpforms-hidden' : ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo Notices::get_fee_notice( $hide_class ); if ( Helpers::is_legacy_payment_settings( $this->form_data ) ) { $this->legacy_builder_content(); return; } $this->maybe_convert_legacy_settings(); echo '<div id="wpforms-panel-content-section-payment-stripe" class="' . esc_attr( $hide_class ) . '">'; if ( ! Helpers::is_pro() ) { $this->builder_content_one_time(); $this->builder_content_recurring(); } else { parent::builder_content(); } echo '</div>'; } /** * Convert legacy settings if they exist. * * @since 1.8.4 */ private function maybe_convert_legacy_settings() { // Enable one-time payments if they were active. if ( ! empty( $this->form_data['payments']['stripe']['enable'] ) ) { unset( $this->form_data['payments']['stripe']['enable'] ); $this->form_data['payments']['stripe']['enable_one_time'] = 1; } // Convert subscription settings if they exist and disabled to new default plan. if ( empty( $this->form_data['payments']['stripe']['recurring'] ) || ! empty( $this->form_data['payments']['stripe']['enable_recurring'] ) ) { return; } $stripe_recurring_settings = $this->form_data['payments']['stripe']['recurring']; unset( $this->form_data['payments']['stripe']['recurring'] ); if ( ! empty( $stripe_recurring_settings['enable'] ) || array_filter( $stripe_recurring_settings, 'is_array' ) === $stripe_recurring_settings ) { return; } // Preserve all settings (name, period, email, and CL). $this->form_data['payments']['stripe']['recurring'][] = $stripe_recurring_settings; } /** * Display legacy content inside the panel content area. * * @since 1.8.4 */ private function legacy_builder_content() { $this->enable_payments_toggle(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo $this->content_section_body(); } /** * Builder content for one time payments. * * @since 1.8.4 */ private function builder_content_one_time() { ?> <div class="wpforms-panel-content-section-payment"> <h2 class="wpforms-panel-content-section-payment-subtitle"> <?php esc_html_e( 'One-Time Payments', 'wpforms-lite' ); ?> </h2> <?php wpforms_panel_field( 'toggle', $this->slug, 'enable_one_time', $this->form_data, esc_html__( 'Enable one-time payments', 'wpforms-lite' ), [ 'parent' => 'payments', 'default' => '0', 'tooltip' => esc_html__( 'Allow your customers to one-time pay via the form.', 'wpforms-lite' ), 'class' => 'wpforms-panel-content-section-payment-toggle wpforms-panel-content-section-payment-toggle-one-time', ] ); ?> <div class="wpforms-panel-content-section-payment-one-time wpforms-panel-content-section-payment-toggled-body"> <?php echo $this->get_builder_content_one_time_content(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> </div> </div> <?php } /** * Builder content for recurring payments. * * @since 1.8.4 */ private function builder_content_recurring() { ?> <div class="wpforms-panel-content-section-payment"> <h2 class="wpforms-panel-content-section-payment-subtitle"> <?php esc_html_e( 'Recurring Payments ', 'wpforms-lite' ); ?> </h2> <?php $this->add_plan_education(); wpforms_panel_field( 'toggle', $this->slug, 'enable_recurring', $this->form_data, esc_html__( 'Enable recurring subscription payments', 'wpforms-lite' ), [ 'parent' => 'payments', 'default' => '0', 'tooltip' => esc_html__( 'Allow your customer to pay recurringly via the form.', 'wpforms-lite' ), 'class' => 'wpforms-panel-content-section-payment-toggle wpforms-panel-content-section-payment-toggle-recurring', ] ); ?> <div class="wpforms-panel-content-section-payment-recurring wpforms-panel-content-section-payment-toggled-body"> <?php if ( empty( $this->form_data['payments'][ $this->slug ]['recurring'] ) ) { $this->form_data['payments'][ $this->slug ]['recurring'][] = []; } foreach ( $this->form_data['payments'][ $this->slug ]['recurring'] as $plan_id => $plan_settings ) { // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo wpforms_render( 'builder/payment/recurring/item', [ 'plan_id' => $plan_id, 'content' => $this->get_builder_content_recurring_payment_content( $plan_id ), ], true ); // Limit plans if Stripe addon is NOT active. break; } ?> </div> </div> <?php } /** * Add new plan education modals. * * @since 1.8.4 */ private function add_plan_education() { $label = __( 'Add New Plan', 'wpforms-lite' ); if ( Helpers::is_allowed_license_type() ) { $addon = wpforms()->obj( 'addons' )->get_addon( 'wpforms-stripe' ); if ( empty( $addon ) ) { return; } echo '<a href="#" class="wpforms-panel-content-section-payment-button wpforms-panel-content-section-payment-button-add-plan education-modal" data-action="' . esc_attr( $addon['action'] ) . '" data-path="' . esc_attr( $addon['path'] ) . '" data-slug="' . esc_attr( $addon['slug'] ) . '" data-url="' . esc_url( $addon['url'] ) . '" data-nonce="' . esc_attr( wp_create_nonce( 'wpforms-admin' ) ) . '" data-name="' . esc_attr__( 'Stripe Pro', 'wpforms-lite' ) . '" >' . esc_html( $label ) . '</a>'; return; } echo '<a href="#" class="wpforms-panel-content-section-payment-button wpforms-panel-content-section-payment-button-add-plan education-modal" data-action="upgrade" data-name="' . esc_attr__( 'Multiple Subscriptions', 'wpforms-lite' ) . '" >' . esc_html( $label ) . '</a>'; } /** * Display alert if Stripe keys are not set. * * @since 1.8.2 * * @return bool */ private function builder_alerts() { if ( Helpers::has_stripe_keys() ) { if ( Helpers::is_legacy_payment_settings( $this->form_data ) ) { Notices::prompt_new_interface(); } $this->stripe_credit_card_alert(); return false; } $this->alert_content( __( 'Heads up! Stripe payments can\'t be enabled yet.', 'wpforms-lite' ), sprintf( wp_kses( /* translators: %1$s - admin area Payments settings page URL. */ __( 'First, please connect to your Stripe account on the <a href="%1$s" class="secondary-text">WPForms Settings</a> page.', 'wpforms-lite' ), [ 'a' => [ 'href' => [], 'class' => [], ], ] ), esc_url( admin_url( 'admin.php?page=wpforms-settings&view=payments' ) ) ) ); return true; } /** * Display alert if Stripe Credit Card field is not added to the form. * * @since 1.8.2 */ private function stripe_credit_card_alert() { $hide_class = Helpers::has_stripe_field( $this->form_data ) ? 'wpforms-hidden' : ''; ?> <div id="wpforms-stripe-credit-card-alert" class="wpforms-alert wpforms-alert-info <?php echo esc_attr( $hide_class ); ?>"> <?php $this->alert_content( '', esc_html__( 'To use Stripe, first add the Stripe payment field to your form.', 'wpforms-lite' ) ); ?> </div> <?php } /** * Display toggle to enable Stripe payments. * * @since 1.8.2 */ private function enable_payments_toggle() { wpforms_panel_field( 'toggle', 'stripe', 'enable', $this->form_data, esc_html__( 'Enable Stripe payments', 'wpforms-lite' ), [ 'parent' => 'payments', 'default' => '0', ] ); } /** * Display content inside the panel content section. * * @since 1.8.4 * * @return string Stripe settings builder content section. */ private function content_section_body() { $content = '<div class="wpforms-panel-content-section-stripe-body">'; $content .= $this->get_builder_content_one_time_content(); $content .= sprintf( '<h2>%1$s</h2>', esc_html__( 'Subscriptions', 'wpforms-lite' ) ); $content .= wpforms_panel_field( 'toggle', 'stripe', 'enable', $this->form_data, esc_html__( 'Enable recurring subscription payments', 'wpforms-lite' ), [ 'parent' => 'payments', 'subsection' => 'recurring', 'default' => '0', ], false ); $content .= $this->get_builder_content_recurring_payment_content( '' ); $content .= '</div>'; return $content; } /** * Get content inside the one time payment area. * * @since 1.8.4 * * @return string */ protected function get_builder_content_one_time_content() { $content = wpforms_panel_field( 'text', $this->slug, 'payment_description', $this->form_data, esc_html__( 'Payment Description', 'wpforms-lite' ), [ 'parent' => 'payments', 'tooltip' => esc_html__( 'Enter your payment description. Eg: Donation for the soccer team. Only used for standard one-time payments.', 'wpforms-lite' ), ], false ); $content .= wpforms_panel_field( 'select', $this->slug, 'receipt_email', $this->form_data, esc_html__( 'Stripe Payment Receipt', 'wpforms-lite' ), [ 'parent' => 'payments', 'field_map' => [ 'email' ], 'placeholder' => esc_html__( '--- Select Email ---', 'wpforms-lite' ), 'tooltip' => esc_html__( 'If you would like to have Stripe send a receipt after payment, select the email field to use. This is optional but recommended. Only used for standard one-time payments.', 'wpforms-lite' ), ], false ); $content .= wpforms_panel_field( 'select', $this->slug, 'customer_email', $this->form_data, esc_html__( 'Customer Email', 'wpforms-lite' ), [ 'parent' => 'payments', 'field_map' => [ 'email' ], 'placeholder' => esc_html__( '--- Select Email ---', 'wpforms-lite' ), 'tooltip' => esc_html__( 'Select the field that contains the customer\'s email address. This is optional but recommended.', 'wpforms-lite' ), ], false ); $content .= $this->get_customer_name_panel_field(); $content .= $this->get_customer_phone_field(); $content .= $this->get_address_panel_fields(); $content .= $this->get_custom_metadata_table(); $content .= $this->single_payments_conditional_logic_section(); return $content; } /** * Get content inside the recurring payment area. * * @since 1.8.4 * * @param string $plan_id Plan id. * * @return string */ protected function get_builder_content_recurring_payment_content( $plan_id ) { $content = wpforms_panel_field( 'text', $this->slug, 'name', $this->form_data, esc_html__( 'Plan Name', 'wpforms-lite' ), [ 'parent' => 'payments', 'subsection' => 'recurring', 'index' => $plan_id, 'tooltip' => esc_html__( 'Enter the subscription name. Eg: Email Newsletter. Subscription period and price are automatically appended. If left empty the form name will be used.', 'wpforms-lite' ), 'class' => 'wpforms-panel-content-section-payment-plan-name', ], false ); $content .= wpforms_panel_field( 'select', $this->slug, 'period', $this->form_data, esc_html__( 'Recurring Period', 'wpforms-lite' ), [ 'parent' => 'payments', 'subsection' => 'recurring', 'index' => $plan_id, 'default' => 'yearly', 'options' => [ 'daily' => esc_html__( 'Daily', 'wpforms-lite' ), 'weekly' => esc_html__( 'Weekly', 'wpforms-lite' ), 'monthly' => esc_html__( 'Monthly', 'wpforms-lite' ), 'quarterly' => esc_html__( 'Quarterly', 'wpforms-lite' ), 'semiyearly' => esc_html__( 'Semi-Yearly', 'wpforms-lite' ), 'yearly' => esc_html__( 'Yearly', 'wpforms-lite' ), ], 'tooltip' => esc_html__( 'How often you would like the charge to recur.', 'wpforms-lite' ), 'class' => 'wpforms-panel-content-section-payment-plan-period', ], false ); $max_cycles = $this->get_recurring_max_cycles( $plan_id ); $range_cycles = range( 1, $max_cycles ); $content .= wpforms_panel_field( 'select', $this->slug, 'cycles', $this->form_data, esc_html__( 'Recurring Cycles', 'wpforms-lite' ), [ 'parent' => 'payments', 'subsection' => 'recurring', 'index' => $plan_id, 'default' => 'unlimited', 'options' => [ 'unlimited' => esc_html__( 'Unlimited', 'wpforms-lite' ) ] + array_combine( $range_cycles, $range_cycles ), 'tooltip' => esc_html__( 'How many times you want the payment to repeat. Stripe supports up to 100 recurrences or a maximum duration of 20 years, whichever comes first.', 'wpforms-lite' ), 'class' => 'wpforms-panel-content-section-payment-plan-cycles', ], false ); $is_empty_email = isset( $this->form_data['payments'][ $this->slug ]['recurring'][ $plan_id ]['email'] ) && empty( $this->form_data['payments'][ $this->slug ]['recurring'][ $plan_id ]['email'] ); $content .= wpforms_panel_field( 'select', $this->slug, 'email', $this->form_data, esc_html__( 'Customer Email', 'wpforms-lite' ), [ 'parent' => 'payments', 'subsection' => 'recurring', 'index' => $plan_id, 'input_class' => $is_empty_email ? 'wpforms-required-field-error' : '', 'field_map' => [ 'email' ], 'placeholder' => esc_html__( '--- Select Email ---', 'wpforms-lite' ), 'tooltip' => esc_html__( "Select the field that contains the customer's email address. This field is required.", 'wpforms-lite' ), ], false ); $content .= $this->get_customer_name_panel_field( $plan_id ); $content .= $this->get_customer_phone_field( $plan_id ); $content .= $this->get_address_panel_fields( $plan_id ); $content .= $this->get_custom_metadata_table( $plan_id ); $content .= $this->recurring_payments_conditional_logic_section( $plan_id ); return $content; } /** * Alert icon. * * @since 1.8.4 */ private function alert_icon() { printf( '<img src="%1$s" class="wpforms-builder-payment-settings-alert-icon" alt="%2$s">', esc_url( WPFORMS_PLUGIN_URL . 'assets/images/addon-icon-stripe.png' ), esc_attr__( 'Connect WPForms to Stripe.', 'wpforms-lite' ) ); } /** * Learn more link. * * @since 1.8.4 * * @return string */ private function learn_more_link() { return sprintf( '<a href="%1$s" target="_blank" rel="noopener noreferrer" class="secondary-text">%2$s</a>', esc_url( wpforms_utm_link( 'https://wpforms.com/docs/how-to-install-and-use-the-stripe-addon-with-wpforms/', 'builder-payments', 'Stripe Documentation' ) ), esc_html__( 'Learn more about our Stripe integration.', 'wpforms-lite' ) ); } /** * Get Customer name panel field. * * @since 1.8.6 * * @param string|null $plan_id Plan ID. * * @return string */ private function get_customer_name_panel_field( $plan_id = null ) { $args = [ 'parent' => 'payments', 'field_map' => [ 'name' ], 'placeholder' => esc_html__( '--- Select Name ---', 'wpforms-lite' ), 'tooltip' => esc_html__( 'Select the field that contains the customer\'s name. This is optional but recommended.', 'wpforms-lite' ), ]; if ( ! is_null( $plan_id ) ) { $args['subsection'] = 'recurring'; $args['index'] = $plan_id; } return wpforms_panel_field( 'select', $this->slug, 'customer_name', $this->form_data, esc_html__( 'Customer Name', 'wpforms-lite' ), $args, false ); } /** * Get address panel fields. * * @since 1.8.8 * * @param string|null $plan_id Plan ID. * * @return string */ private function get_address_panel_fields( $plan_id = null ): string { $args = [ 'parent' => 'payments', 'field_map' => [ 'address' ], 'placeholder' => esc_html__( '--- Select Address ---', 'wpforms-lite' ), ]; $is_subscription = ! is_null( $plan_id ); if ( $is_subscription ) { $args['subsection'] = 'recurring'; $args['index'] = $plan_id; } $is_pro = wpforms()->is_pro(); if ( ! $is_pro ) { $args['pro_badge'] = true; $args['data'] = [ 'action' => 'upgrade', 'name' => esc_html__( 'Customer Address', 'wpforms-lite' ), 'utm-content' => 'Builder Stripe Address Field', 'licence' => 'pro', ]; $args['input_class'] = 'education-modal'; $args['readonly'] = true; } else { $args['tooltip'] = esc_html__( 'Select the field that contains the customer\'s address. This is optional but required for some regions.', 'wpforms-lite' ); } $output = wpforms_panel_field( 'select', $this->slug, 'customer_address', $this->form_data, esc_html__( 'Customer Address', 'wpforms-lite' ), $args, false ); if ( $is_subscription ) { return $output; } if ( ! $is_pro ) { $args['data']['name'] = esc_html__( 'Shipping Address', 'wpforms-lite' ); } else { $args['tooltip'] = esc_html__( 'Select the field that contains the shipping address. This is optional but required for some regions.', 'wpforms-lite' ); } $output .= wpforms_panel_field( 'select', $this->slug, 'shipping_address', $this->form_data, esc_html__( 'Shipping Address', 'wpforms-lite' ), $args, false ); return $output; } /** * Get the Customer phone panel field. * * @since 1.9.6 * * @param string|null $plan_id Plan ID. * * @return string */ private function get_customer_phone_field( ?string $plan_id = null ): string { $args = [ 'parent' => 'payments', 'field_map' => [ 'phone' ], 'placeholder' => esc_html__( '--- Select Phone ---', 'wpforms-lite' ), ]; if ( ! is_null( $plan_id ) ) { $args['subsection'] = 'recurring'; $args['index'] = $plan_id; } $is_pro = wpforms()->is_pro(); if ( ! $is_pro ) { $args['pro_badge'] = true; $args['data'] = [ 'action' => 'upgrade', 'name' => esc_html__( 'Customer Phone', 'wpforms-lite' ), 'utm-content' => 'Builder Stripe Phone Field', 'licence' => 'pro', ]; $args['input_class'] = 'education-modal'; $args['readonly'] = true; } else { $args['tooltip'] = esc_html__( 'Select the field that contains the customer\'s phone. This is optional but recommended.', 'wpforms-lite' ); } return (string) wpforms_panel_field( 'select', $this->slug, 'customer_phone', $this->form_data, esc_html__( 'Customer Phone', 'wpforms-lite' ), $args, false ); } /** * Get custom meta table html. * * @since 1.9.6 * * @param string|null $plan_id Plan ID. * * @return string */ private function get_custom_metadata_table( $plan_id = null ): string { $subsection = ! is_null( $plan_id ) ? 'recurring_custom_metadata_' . $plan_id : 'custom_metadata'; $custom_metadata = $this->form_data['payments'][ $this->slug ][ $subsection ] ?? [ [] ]; /** * Filter the allowed fields for custom metadata. * * @since 1.9.6 * * @param array $allowed_fields Allowed fields. */ $allowed_fields = (array) apply_filters( 'wpforms_stripe_custom_metadata_allowed_fields', $this->get_allowed_meta_value_fields() ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped return wpforms_render( 'integrations/stripe/builder/custom-metadata', [ 'custom_metadata' => $custom_metadata, 'subsection' => $subsection, 'slug' => $this->slug, 'form_data' => $this->form_data, 'fields' => $allowed_fields, ], true ); } /** * Get allowed meta value fields. * * @since 1.9.6 * * @return array */ private function get_allowed_meta_value_fields(): array { $fields = [ 'text', 'textarea', 'checkbox', 'radio', 'select', 'number', 'name', 'email', 'number-slider', 'payment-checkbox', 'payment-multiple', 'payment-select', 'payment-single', 'payment-total', ]; if ( ! wpforms()->is_pro() ) { return $fields; } return array_merge( $fields, [ 'address', 'date-time', 'hidden', 'phone', 'rating', ] ); } /** * Get recurring max cycles value. * * @param string $plan_id Selected plan id. * * @since 1.9.8 * * @return int */ private function get_recurring_max_cycles( string $plan_id ): int { // The API limit is 20 years. if ( ! isset( $this->form_data['payments'][ $this->slug ]['recurring'][ $plan_id ]['period'] ) || $this->form_data['payments'][ $this->slug ]['recurring'][ $plan_id ]['period'] === 'yearly' ) { return 20; } // 20 years is 40 semi-years. if ( $this->form_data['payments'][ $this->slug ]['recurring'][ $plan_id ]['period'] === 'semiyearly' ) { return 40; } // 20 years is 80 quarters. if ( $this->form_data['payments'][ $this->slug ]['recurring'][ $plan_id ]['period'] === 'quarterly' ) { return 80; } return Helpers::recurring_plan_cycles_max(); } /** * Display alert content. * * @since 1.9.9 * * @param string $title Alert title. * @param string $message Alert message. */ private function alert_content( string $title, string $message ): void { ?> <?php $this->alert_icon(); ?> <div class="wpforms-builder-payment-settings-default-content"> <?php if ( ! empty( $title ) ) : ?> <p class="wpforms-builder-payment-settings-error-title"> <?php echo esc_html( $title ); ?> </p> <?php endif; ?> <p> <?php echo $message; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> </p> <p class="wpforms-builder-payment-settings-learn-more"> <?php echo $this->learn_more_link(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> </p> </div> <?php } } Integrations/Stripe/Admin/Payments/SingleActionsHandler.php 0000644 00000013434 15174710275 0020077 0 ustar 00 <?php namespace WPForms\Integrations\Stripe\Admin\Payments; use WPForms\Integrations\Stripe\Api\PaymentIntents; use WPForms\Db\Payments\UpdateHelpers; use WPForms\Integrations\Stripe\Helpers; /** * Things related to Stripe functionality on single payment screen. * * @since 1.8.4 */ class SingleActionsHandler { /** * Gateway name. * * @since 1.8.4 * * @var string */ const GATEWAY = 'stripe'; /** * PaymentIntents API. * * @since 1.8.4 * * @var PaymentIntents */ private $payment_intents; /** * Initialize. * * @since 1.8.4 * * @param PaymentIntents $payment_intents PaymentIntents API. * * @return $this */ public function init( $payment_intents ) { $this->payment_intents = $payment_intents; $this->hooks(); return $this; } /** * Register hooks. * * @since 1.8.4 */ private function hooks() { if ( wpforms_is_admin_ajax() ) { add_action( 'wp_ajax_wpforms_stripe_payments_refund', [ $this, 'ajax_single_payment_refund' ] ); add_action( 'wp_ajax_wpforms_stripe_payments_cancel', [ $this, 'ajax_single_payment_cancel' ] ); return; } add_filter( 'wpforms_admin_strings', [ $this, 'admin_strings' ] ); } /** * Add admin strings related to payments. * * @since 1.8.4 * * @param array $admin_strings Admin strings. * * @return array */ public function admin_strings( $admin_strings ) { $admin_strings['single_payment_button_handlers'][] = self::GATEWAY; return $admin_strings; } /** * Refund a single payment. * * Handler for ajax request with action "wpforms_payments_refund". * * @since 1.8.4 */ public function ajax_single_payment_refund() { if ( ! isset( $_POST['payment_id'] ) ) { wp_send_json_error( [ 'message' => esc_html__( 'Missing payment ID.', 'wpforms-lite' ) ] ); } if ( ! wpforms_current_user_can( wpforms_get_capability_manage_options() ) ) { wp_send_json_error( [ 'message' => esc_html__( 'You are not allowed to perform this action.', 'wpforms-lite' ) ] ); } $this->check_payment_collection_type(); check_ajax_referer( 'wpforms-admin', 'nonce' ); $payment_id = (int) $_POST['payment_id']; $payment_db = wpforms()->obj( 'payment' )->get( $payment_id ); if ( empty( $payment_db ) ) { wp_send_json_error( [ 'message' => esc_html__( 'Payment not found in the database.', 'wpforms-lite' ) ] ); } $args = [ 'metadata' => [ 'refunded_by' => 'wpforms_dashboard', ], 'reason' => 'requested_by_customer', ]; $refund = $this->payment_intents->refund_payment( $payment_db->transaction_id, $args ); if ( ! $refund ) { wp_send_json_error( [ 'message' => esc_html__( 'Refund failed.', 'wpforms-lite' ) ] ); } if ( $payment_db->status === 'partrefund' ) { $already_refunded = wpforms()->obj( 'payment_meta' )->get_single( $payment_db->id, 'refunded_amount' ); $amount_to_log = $payment_db->total_amount - $already_refunded; } else { $amount_to_log = $payment_db->total_amount; } $log = sprintf( 'Stripe payment refunded from the WPForms plugin interface. Refunded amount: %1$s.', wpforms_format_amount( wpforms_sanitize_amount( $amount_to_log ), true ) ); if ( UpdateHelpers::refund_payment( $payment_db, $payment_db->total_amount, $log ) ) { wp_send_json_success( [ 'message' => esc_html__( 'Refund successful.', 'wpforms-lite' ) ] ); } wp_send_json_error( [ 'message' => esc_html__( 'Saving refund in the database failed.', 'wpforms-lite' ) ] ); } /** * Cancel subscription. * * Handler for ajax request with action "wpforms_payments_cancel_subscription". * * @since 1.8.4 */ public function ajax_single_payment_cancel() { if ( ! isset( $_POST['payment_id'] ) ) { wp_send_json_error( [ 'message' => esc_html__( 'Payment ID not provided.', 'wpforms-lite' ) ] ); } if ( ! wpforms_current_user_can( wpforms_get_capability_manage_options() ) ) { wp_send_json_error( [ 'message' => esc_html__( 'You are not allowed to perform this action.', 'wpforms-lite' ) ] ); } $this->check_payment_collection_type(); check_ajax_referer( 'wpforms-admin', 'nonce' ); $payment_id = (int) $_POST['payment_id']; $payment_db = wpforms()->obj( 'payment' )->get( $payment_id ); if ( empty( $payment_db ) ) { wp_send_json_error( [ 'message' => esc_html__( 'Subscription not found in the database.', 'wpforms-lite' ) ] ); } $cancel = $this->payment_intents->cancel_subscription( $payment_db->subscription_id ); if ( ! $cancel ) { wp_send_json_error( [ 'message' => esc_html__( 'Subscription cancellation failed.', 'wpforms-lite' ) ] ); } if ( UpdateHelpers::cancel_subscription( $payment_db->id, 'Stripe subscription cancelled from the WPForms plugin interface.' ) ) { wp_send_json_success( [ 'message' => esc_html__( 'Subscription cancelled.', 'wpforms-lite' ) ] ); } wp_send_json_error( [ 'message' => esc_html__( 'Updating subscription in the database failed.', 'wpforms-lite' ) ] ); } /** * Check the current payment collection type. * If the deprecated type is still used, then warn users about it. * * When it's dropped from the addon, this method can be safely removed. * * @since 1.8.4 */ private function check_payment_collection_type() { if ( ! Helpers::is_pro() || absint( wpforms_setting( 'stripe-api-version' ) ) !== 2 ) { return; } $message = sprintf( wp_kses( /* translators: %s - Payments settings page URL. */ __( "The used Stripe payment collection type doesn't support this action.<br><br> Please <a href='%s'>update your payment collection type</a> to continue processing payments successfully.", 'wpforms-lite' ), [ 'br' => [], 'a' => [ 'href' => [], ], ] ), esc_url( admin_url( 'admin.php?page=wpforms-settings&view=payments#wpforms-setting-row-stripe-api-version' ) ) ); wp_send_json_error( [ 'modal_msg' => $message ] ); } } Integrations/Stripe/Admin/Notices.php 0000644 00000012325 15174710275 0013641 0 ustar 00 <?php namespace WPForms\Integrations\Stripe\Admin; use WPForms\Integrations\Stripe\Helpers; use WPForms\Integrations\Stripe\StripeAddonCompatibility; /** * Stripe related admin notices. * * @since 1.8.2 */ class Notices { /** * Get a notice if a license is insufficient not to be charged a fee. * * @since 1.8.2 * * @param string $classes Additional notice classes. * * @return string */ public static function get_fee_notice( $classes = '' ) { if ( ! Helpers::is_application_fee_supported() ) { return ''; } $is_allowed_license = Helpers::is_allowed_license_type(); $is_active_license = Helpers::is_license_active(); $notice = ''; if ( $is_allowed_license && $is_active_license ) { return $notice; } if ( ! $is_allowed_license ) { $notice = self::get_non_pro_license_level_notice(); } elseif ( ! $is_active_license ) { $notice = self::get_non_active_license_notice(); } if ( wpforms_is_admin_page( 'builder' ) ) { return sprintf( '<p class="wpforms-stripe-notice-info wpforms-alert wpforms-alert-info ' . wpforms_sanitize_classes( $classes ) . '">%s</p>', $notice ); } return sprintf( '<div class="wpforms-stripe-notice-info ' . wpforms_sanitize_classes( $classes ) . '"><p>%s</p></div>', $notice ); } /** * Get a fee notice for a non-active license. * * If the license is NOT set/activated, show the notice to activate it. * Otherwise, show the notice to renew it. * * @since 1.8.2 * * @return string */ private static function get_non_active_license_notice() { $setting_page_url = add_query_arg( [ 'page' => 'wpforms-settings', 'view' => 'general', ], admin_url( 'admin.php' ) ); // The license is not set/activated at all. if ( empty( wpforms_get_license_key() ) ) { return sprintf( wp_kses( /* translators: %s - general admin settings page URL. */ __( '<strong>Pay-as-you-go Pricing</strong><br>3%% fee per-transaction + Stripe fees. <a href="%s">Activate your license</a> to remove additional fees and unlock powerful features.', 'wpforms-lite' ), [ 'strong' => [], 'br' => [], 'a' => [ 'href' => [], 'target' => [], ], ] ), esc_url( $setting_page_url ) ); } return sprintf( wp_kses( /* translators: %s - general admin settings page URL. */ __( '<strong>Pay-as-you-go Pricing</strong><br> 3%% fee per-transaction + Stripe fees. <a href="%s">Renew your license</a> to remove additional fees and unlock powerful features.', 'wpforms-lite' ), [ 'strong' => [], 'br' => [], 'a' => [ 'href' => [], 'target' => [], ], ] ), esc_url( $setting_page_url ) ); } /** * Get a fee notice for license levels below the `pro`. * * Show the notice to upgrade to Pro. * * @since 1.8.2 * * @return string */ private static function get_non_pro_license_level_notice() { $utm_content = 'Stripe Pro - Remove Fees'; $utm_medium = wpforms_is_admin_page( 'builder' ) ? 'Payment Settings' : 'Settings - Payments'; $upgrade_link = wpforms()->is_pro() ? wpforms_admin_upgrade_link( $utm_medium, $utm_content ) : wpforms_utm_link( 'https://wpforms.com/lite-upgrade/', $utm_medium, $utm_content ); return sprintf( wp_kses( /* translators: %s - WPForms.com Upgrade page URL. */ __( '<strong>Pay-as-you-go Pricing</strong><br> 3%% fee per-transaction + Stripe fees. <a href="%s" target="_blank">Upgrade to Pro</a> to remove additional fees and unlock powerful features.', 'wpforms-lite' ), [ 'strong' => [], 'br' => [], 'a' => [ 'href' => [], 'target' => [], ], ] ), esc_url( $upgrade_link ) ); } /** * Display alert about new interface. * * @since 1.8.4 */ public static function prompt_new_interface() { $dismissed = get_user_meta( get_current_user_id(), 'wpforms_dismissed', true ); // Check if not dismissed. if ( ! empty( $dismissed['edu-wpforms-stripe-legacy-interface'] ) ) { return; } $addon_compat = ( new StripeAddonCompatibility() )->init(); if ( $addon_compat && ! $addon_compat->is_supported_modern_settings() ) { $message = __( 'A new and improved Stripe interface is available with new Stripe Pro addon.', 'wpforms-lite' ); } else { $message = __( 'A new and improved Stripe interface is available when you create new forms.', 'wpforms-lite' ); } ?> <div id="wpforms-stripe-new-interface-alert" class="wpforms-alert wpforms-alert-warning wpforms-alert-dismissible wpforms-dismiss-container"> <div class="wpforms-alert-message"> <p> <?php echo esc_html( $message ); ?> <?php printf( '<a href="%1$s" target="_blank" rel="noopener noreferrer">%2$s</a>', esc_url( wpforms_utm_link( 'https://wpforms.com/introducing-wpforms-1-8-4-new-stripe-payment-tools/#stripe-conditional-logic', 'Builder Settings', 'Stripe New Payments Interface' ) ), esc_html__( 'What\'s new?', 'wpforms-lite' ) ); ?> </p> </div> <div class="wpforms-alert-buttons"> <button type="button" class="wpforms-dismiss-button" title="<?php esc_attr_e( 'Dismiss this message.', 'wpforms-lite' ); ?>" data-section="wpforms-stripe-legacy-interface"></button> </div> </div> <?php } } Integrations/Stripe/Admin/Connect.php 0000644 00000031377 15174710275 0013636 0 ustar 00 <?php namespace WPForms\Integrations\Stripe\Admin; use WPForms\Integrations\Stripe\Api\DomainManager; use WPForms\Integrations\Stripe\Api\WebhooksManager; use WPForms\Integrations\Stripe\Helpers; use WPForms\Integrations\Stripe\WebhooksHealthCheck; use WPForms\Vendor\Stripe\Account; /** * Stripe Connect functionality. * * @since 1.8.2 */ class Connect { /** * WPForms Stripe OAuth URL. * * @since 1.8.2 */ const WPFORMS_URL = 'https://wpforms.com/oauth/stripe-connect'; /** * Stripe live/test account objects. * * @since 1.8.2 * * @var array */ protected $accounts = []; /** * Webhooks manager. * * @since 1.8.4 * * @var WebhooksManager */ private $webhooks_manager; /** * Domain manager. * * @since 1.8.6 * * @var DomainManager */ private $domain_manager; /** * Initialize. * * @since 1.8.2 * * @return Connect */ public function init() { $this->webhooks_manager = new WebhooksManager(); $this->domain_manager = new DomainManager(); $this->hooks(); return $this; } /** * Hooks. * * @since 1.8.2 */ private function hooks() { add_action( 'admin_init', [ $this, 'handle_oauth_handshake' ] ); } /** * Handle Stripe Connect OAuth handshake and save Stripe keys. * * @since 1.8.2 */ public function handle_oauth_handshake(): void { // Handle disconnect requests. if ( $this->is_valid_disconnect_request() ) { $this->handle_disconnect(); return; } if ( ! $this->is_valid_handshake_request() ) { return; } $state = isset( $_GET['state'] ) ? sanitize_text_field( wp_unslash( $_GET['state'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( empty( $state ) ) { return; } $credentials = $this->fetch_stripe_credentials( $state ); $required_keys = [ 'stripe_user_id', 'stripe_publishable_key', 'access_token', 'refresh_token', 'live_mode' ]; if ( 0 !== count( array_diff( $required_keys, array_keys( $credentials ) ) ) ) { return; } $mode = empty( $credentials['live_mode'] ) ? 'test' : 'live'; $this->set_connected_user_id( $credentials['stripe_user_id'], $mode ); $this->set_current_mode( $mode ); // In case of switching accounts existing account data needs to be cleared. unset( $this->accounts[ $mode ] ); Helpers::set_stripe_key( $credentials['stripe_publishable_key'], 'publishable', $mode ); Helpers::set_stripe_key( $credentials['access_token'], 'secret', $mode ); $this->update_account_meta( $credentials['stripe_user_id'], $mode ); $this->set_connected_account_country( $mode ); $this->webhooks_manager->connect(); $this->domain_manager->validate(); $settings_url = $this->get_payments_settings_url( false ); wp_safe_redirect( $settings_url ); exit; } /** * Validates if the current handshake request is valid. * * @since 1.9.5 */ private function is_valid_handshake_request(): bool { if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( sanitize_key( $_GET['_wpnonce'] ), 'wpforms_stripe_connect' ) ) { return false; } if ( ! isset( $_GET['stripe_connect'] ) || $_GET['stripe_connect'] !== 'complete' ) { return false; } if ( ! wpforms_current_user_can() ) { return false; } return true; } /** * Validates if the current disconnect request is valid. * * @since 1.9.8 * * @return bool */ private function is_valid_disconnect_request(): bool { if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( sanitize_key( $_GET['_wpnonce'] ), 'wpforms_stripe_disconnect' ) ) { return false; } if ( ! wpforms_current_user_can() ) { return false; } return true; } /** * Fetch Stripe credentials from https://wpforms.com. * * @since 1.8.2 * * @param string $state Anonymous autogenerated ID to safely fetch Stripe credentials. * * @return array */ protected function fetch_stripe_credentials( $state ) { $response = wp_remote_post( self::WPFORMS_URL, [ 'body' => [ 'action' => 'credentials', 'state' => $state, ], ] ); if ( is_wp_error( $response ) ) { return []; } $body = wpforms_json_decode( wp_remote_retrieve_body( $response ), true ); return is_array( $body ) ? $body : []; } /** * Fetch Stripe Account from Stripe. * * @since 1.8.2 * * @param string $mode Stripe mode (e.g. 'live' or 'test'). * * @return null|Account */ protected function fetch_stripe_account( $mode = '' ) { $api_key = Helpers::get_stripe_key( 'secret', $mode ); if ( ! $api_key ) { return null; } try { $account = Account::retrieve( null, sanitize_text_field( $api_key ) ); } catch ( \Exception $e ) { $account = null; } return $account; } /** * Update connected account meta. * * @since 1.8.2 * * @param string $account_id Account ID. * @param string $mode Stripe mode (e.g. 'live' or 'test'). */ public function update_account_meta( $account_id = '', $mode = '' ) { if ( ! $mode ) { $mode = Helpers::get_stripe_mode(); } // Stripe API has limited update method for live accounts only. if ( $mode !== 'live' ) { return; } if ( ! $account_id ) { $account_id = $this->get_connected_user_id( $mode ); } // Return early if no connected account. if ( ! $account_id ) { return; } $licence_type = wpforms_get_license_type(); $metadata = [ 'wpforms_stripe' => Helpers::is_addon_active() ? 'addon' : 'core', 'wpforms_type' => wpforms()->is_pro() ? 'pro' : 'lite', 'wpforms_license' => $licence_type ? $licence_type : 'lite', ]; try { Account::update( $account_id, [ 'metadata' => $metadata ], Helpers::get_auth_opts() ); } catch ( \Exception $e ) { wpforms_log( 'Unable to update connected Stripe account meta.', $e->getMessage(), [ 'type' => [ 'payment', 'error' ], ] ); } } /** * Generate random alphanumeric token string. * Token length is always 32 chars. * * @since 1.8.2 * * @return string */ public function generate_random_token() { $random = false; if ( function_exists( 'openssl_random_pseudo_bytes' ) ) { $strong_result = false; // This has been added as argument #2 ($strong_result) cannot be passed by reference. $random = openssl_random_pseudo_bytes( 16, $strong_result ); } if ( $random === false ) { return md5( wp_rand() ); } return bin2hex( $random ); } /** * Set fetched Stripe Account for caching purposes. * * @since 1.8.2 * * @param string $mode Stripe mode (e.g. 'live' or 'test'). */ protected function set_connected_account( $mode = '' ) { $user_id = $this->get_connected_user_id( $mode ); if ( ! $user_id ) { return; } $account = $this->fetch_stripe_account( $mode ); $this->accounts[ $mode ] = null; if ( ! isset( $account->id ) || $account->id !== $user_id ) { return; } $this->accounts[ $mode ] = $account; } /** * Get cached Stripe Account or fetch it from Stripe. * * @since 1.8.2 * * @param string $mode Stripe mode (e.g. 'live' or 'test'). * * @return null|Account */ public function get_connected_account( $mode = '' ) { $mode = Helpers::validate_stripe_mode( $mode ); if ( empty( $this->accounts ) || ( is_array( $this->accounts ) && ! array_key_exists( $mode, $this->accounts ) ) ) { $this->set_connected_account( $mode ); } if ( ! empty( $this->accounts[ $mode ] ) ) { return $this->accounts[ $mode ]; } return null; } /** * Save connected user id to an option. * * @since 1.8.2 * * @param string $user_id User id to set. * @param string $mode Stripe mode (e.g. 'live' or 'test'). * * @return bool */ protected function set_connected_user_id( $user_id, $mode = '' ) { $mode = Helpers::validate_stripe_mode( $mode ); return update_option( "wpforms_stripe_{$mode}_connect_user_id", sanitize_text_field( $user_id ) ); } /** * Get saved Stripe Connect user id from DB. * * @since 1.8.2 * * @param string $mode Stripe mode (e.g. 'live' or 'test'). * * @return string */ public function get_connected_user_id( $mode = '' ) { $mode = Helpers::validate_stripe_mode( $mode ); $user_id = get_option( "wpforms_stripe_{$mode}_connect_user_id", '' ); /** * User ID associated with the Stripe account. * * @since 1.8.2 * * @param string $user_id User ID. */ return (string) apply_filters( 'wpforms_stripe_admin_connect_get_connected_user_id', $user_id ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName } /** * Get Stripe Account name. * * @since 1.8.2 * * @param string $mode Stripe mode (e.g. 'live' or 'test'). * * @return string */ public function get_connected_account_name( $mode = '' ) { $account = $this->get_connected_account( $mode ); if ( isset( $account->display_name ) ) { return $account->display_name; } if ( isset( $account->settings, $account->settings->dashboard->display_name ) ) { return $account->settings->dashboard->display_name; } return ''; } /** * Set Stripe mode. * * @since 1.8.4 * * @param string $mode Stripe mode (e.g. 'live' or 'test'). */ private function set_current_mode( $mode ) { $key = 'stripe-test-mode'; $settings = (array) get_option( 'wpforms_settings', [] ); $settings[ $key ] = $mode === 'test'; update_option( 'wpforms_settings', $settings ); } /** * Set Stripe Account country. * * @since 1.8.2 * * @param string $mode Stripe mode (e.g. 'live' or 'test'). */ private function set_connected_account_country( $mode = '' ) { $account = $this->get_connected_account( $mode ); if ( ! isset( $account->country ) ) { return; } update_option( "wpforms_stripe_{$mode}_account_country", strtolower( $account->country ) ); } /** * Get Stripe Connect button URL. * * @since 1.8.2 * * @param string $mode Stripe mode (e.g. 'live' or 'test'). * * @return string */ public function get_connect_with_stripe_url( $mode = '' ) { $mode = Helpers::validate_stripe_mode( $mode ); $settings_url = $this->get_payments_settings_url(); return add_query_arg( [ 'action' => 'init', 'live_mode' => absint( $mode === 'live' ), 'state' => $this->generate_random_token(), 'site_url' => rawurlencode( $settings_url ), ], self::WPFORMS_URL ); } /** * Get "Payments" settings page URL. * * @since 1.8.2 * @since 1.9.8 Added `$include_nonce` and `$action` parameters to allow more flexible settings. * * @param bool $include_nonce Whether to include nonce in the URL. * @param string $action Action to be used for nonce verification. * * @return string */ private function get_payments_settings_url( bool $include_nonce = true, string $action = 'wpforms_stripe_connect' ): string { $args = [ 'page' => 'wpforms-settings', 'view' => 'payments', ]; if ( $include_nonce ) { $args['_wpnonce'] = wp_create_nonce( $action ); } return add_query_arg( $args, admin_url( 'admin.php' ) ); } /** * Get Stripe disconnect URL. * * @since 1.9.8 * * @param string $mode Stripe mode (e.g. 'live' or 'test'). * * @return string */ public function get_disconnect_stripe_url( string $mode ): string { $mode = Helpers::validate_stripe_mode( $mode ); $action = 'wpforms_stripe_disconnect'; return add_query_arg( [ 'action' => $action, 'live_mode' => absint( $mode === 'live' ), ], $this->get_payments_settings_url( true, $action ) ); } /** * Disconnect from Stripe. * * @since 1.9.8 */ private function handle_disconnect(): void { $mode = Helpers::get_stripe_mode(); $response = wp_remote_post( self::WPFORMS_URL, [ 'body' => [ 'action' => 'deauthorize', 'secret_key' => Helpers::get_stripe_key( 'secret', $mode ), 'stripe_user_id' => $this->get_connected_user_id( $mode ), 'live_mode' => absint( $mode === 'live' ), ], 'timeout' => 15, // Handle case with slow connection. ] ); if ( is_wp_error( $response ) || wp_remote_retrieve_response_code( $response ) !== 200 ) { return; } // Cleanup saved options. $this->disconnect_cleanup(); // Disconnect webhooks. $this->webhooks_manager->disconnect(); wp_safe_redirect( $this->get_payments_settings_url( false ) ); exit; } /** * Cleanup after disconnecting from Stripe. * * @since 1.9.8 */ private function disconnect_cleanup(): void { $mode = Helpers::get_stripe_mode(); // Clean keys. Helpers::set_stripe_key( '', 'publishable', $mode ); Helpers::set_stripe_key( '', 'secret', $mode ); // Clean user ID and other saved options. delete_option( "wpforms_stripe_{$mode}_connect_user_id" ); delete_option( "wpforms_stripe_{$mode}_account_country" ); delete_option( DomainManager::STATUS_OPTION ); delete_option( WebhooksHealthCheck::ENDPOINT_OPTION ); // Clean cached account. unset( $this->accounts[ $mode ] ); } } Integrations/Stripe/Process.php 0000644 00000110151 15174710275 0012617 0 ustar 00 <?php namespace WPForms\Integrations\Stripe; use Stripe\Exception\ApiErrorException; use WPForms\Helpers\Transient; use WPForms\Vendor\Stripe\SubscriptionSchedule; /** * Stripe payment processing. * * @since 1.8.2 */ class Process { /** * Payment amount. * * @since 1.8.2 * * @var string */ public $amount = ''; /** * Form ID. * * @since 1.8.2 * * @var int */ public $form_id = 0; /** * Form Stripe payment settings. * * @since 1.8.2 * * @var array */ public $settings = []; /** * Sanitized submitted field values and data. * * @since 1.8.2 * * @var array */ public $fields = []; /** * Form data and settings. * * @since 1.8.2 * * @var array */ public $form_data = []; /** * Rate Limit object. * * @since 1.8.2 * * @var RateLimit */ private $rate_limit; /** * Api interface. * * @since 1.8.2 * * @var Api\ApiInterface */ protected $api; /** * Whether the payment has been processed. * * @since 1.8.3 * * @var bool */ protected $is_payment_processed = false; /** * Save matched subscription settings. * * @since 1.8.4 * * @var array */ private $subscription_settings = []; /** * Save the matched plan id. * * @since 1.9.6 * * @var string|null */ private $plan_id = null; /** * Initialize. * * @since 1.8.2 * * @param Api\ApiInterface $api Api interface. */ public function init( $api ) { $this->api = $api; $this->hooks(); } /** * Hooks. * * @since 1.8.2 */ private function hooks() { add_action( 'wpforms_process', [ $this, 'process_entry' ], 10, 3 ); add_action( 'wpforms_process_payment_saved', [ $this, 'process_payment_saved' ], 10, 3 ); add_action( 'wpformsstripe_api_common_set_error_from_exception', [ $this, 'process_card_error' ] ); add_filter( 'wpforms_forms_submission_prepare_payment_data', [ $this, 'prepare_payment_data' ] ); add_filter( 'wpforms_forms_submission_prepare_payment_meta', [ $this, 'prepare_payment_meta' ], 10, 3 ); add_action( 'wpforms_process_entry_saved', [ $this, 'process_entry_data' ], 10, 4 ); } /** * Check if a payment exists with an entry, if so validate and process. * * @since 1.8.2 * * @param array $fields Final/sanitized submitted field data. * @param array $entry Copy of original $_POST. * @param array $form_data Form data and settings. */ public function process_entry( $fields, $entry, $form_data ) { // Check if payment method exists and is enabled. if ( ! Helpers::has_stripe_enabled( [ $form_data ] ) ) { return; } $this->form_id = (int) $form_data['id']; $this->fields = $fields; $this->form_data = $form_data; $this->settings = $form_data['payments']['stripe']; $this->amount = wpforms_get_total_payment( $this->fields ); $this->rate_limit = new RateLimit(); $this->rate_limit->init(); if ( $this->is_process_entry_error() ) { return; } if ( $this->is_submitted_payment_data_corrupted( $entry ) ) { return; } $this->api->set_payment_tokens( $entry ); $error = $this->get_entry_errors(); // Before proceeding, check if any basic errors were detected. if ( $error ) { $this->log_error( $error ); $this->display_error( $error ); return; } $this->process_payment(); } /** * Bypass captcha if payment has been processed. * * @since 1.8.3 * @deprecated 1.9.6 * * @param bool $bypass_captcha Whether to bypass captcha. * * @return bool */ public function bypass_captcha( $bypass_captcha ) { _deprecated_function( __METHOD__, '1.9.6 of the WPForms plugin' ); if ( $bypass_captcha ) { return $bypass_captcha; } return $this->is_payment_processed; } /** * Check on process entry errors. * * @since 1.8.2 * * @return bool */ protected function is_process_entry_error() { // Check for processing errors. if ( ! empty( wpforms()->obj( 'process' )->errors[ $this->form_id ] ) || ! $this->is_card_field_visibility_ok() ) { return true; } // Check rate limit. if ( ! $this->is_rate_limit_ok() ) { wpforms()->obj( 'process' )->errors[ $this->form_id ]['footer'] = esc_html__( 'Unable to process payment, please try again later.', 'wpforms-lite' ); return true; } return false; } /** * Add meta for a successful payment. * * @since 1.8.2 * * @param array $payment_meta Payment meta. * @param array $fields Final/sanitized submitted field data. * @param array $form_data Form data and settings. * * @noinspection PhpMissingParamTypeInspection * @noinspection PhpUnusedParameterInspection */ public function prepare_payment_meta( $payment_meta, $fields, $form_data ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed $payment = $this->api->get_payment(); if ( empty( $payment->id ) ) { return $payment_meta; } $charge_details = $this->api->get_charge_details( [ 'type', 'name', 'last4', 'brand', 'exp_month', 'exp_year' ] ); $payment_meta['method_type'] = $this->get_payment_type( $charge_details ); $payment_meta['customer_name'] = $this->get_customer_name(); $payment_meta['customer_email'] = $this->get_customer_email(); $subscription = $this->api->get_subscription(); if ( ! empty( $subscription->id ) ) { $payment_meta['subscription_period'] = sanitize_text_field( $this->subscription_settings['period'] ); } if ( ! empty( $charge_details['brand'] ) ) { $payment_meta['credit_card_method'] = $charge_details['brand']; } if ( ! empty( $charge_details['name'] ) ) { $payment_meta['credit_card_name'] = $charge_details['name']; } if ( ! empty( $charge_details['last4'] ) ) { $payment_meta['credit_card_last4'] = $charge_details['last4']; } if ( ! empty( $charge_details['exp_month'] ) && ! empty( $charge_details['exp_year'] ) ) { $payment_meta['credit_card_expires'] = sprintf( '%s/%s', $charge_details['exp_month'], $charge_details['exp_year'] ); } $log = [ 'value' => $payment->object === 'payment_intent' ? sprintf( 'Stripe payment intent created. (Payment Intent ID: %s)', $payment->id ) : 'Stripe payment was created.', 'date' => gmdate( 'Y-m-d H:i:s' ), ]; $payment_meta['log'] = wp_json_encode( $log ); return $payment_meta; } /** * Get payment method type. * * @since 1.8.2.1 * * @param array $charge_details Get details from a saved Charge object. * * @return string */ private function get_payment_type( $charge_details ) { if ( empty( $charge_details['last4'] ) ) { return 'link'; } if ( ! empty( $charge_details['type'] ) ) { return sanitize_text_field( $charge_details['type'] ); } return 'card'; } /** * Add payment info for successful payment. * * @since 1.8.2 * * @param int $payment_id Payment ID. * @param array $fields Final/sanitized submitted field data. * @param array $form_data Form data and settings. */ public function process_payment_saved( $payment_id, $fields, $form_data ) { $payment = $this->api->get_payment(); if ( empty( $payment->id ) ) { return; } $payment_url = add_query_arg( [ 'page' => 'wpforms-payments', 'view' => 'payment', 'payment_id' => $payment_id, ], admin_url( 'admin.php' ) ); // Update the Stripe charge metadata to include the Payment ID. $payment->metadata['payment_id'] = $payment_id; $payment->metadata['payment_url'] = esc_url_raw( $payment_url ); // Clean up spam reason in case it was set before. $payment->metadata['spam_reason'] = null; $custom_metadata = $this->get_mapped_custom_metadata( 'payment' ); array_walk( $custom_metadata, static function ( &$value, $key ) use ( $payment ) { $payment->metadata[ $key ] = $value; } ); /** * Allow to add additional payment metadata to the Stripe payment. * * @since 1.8.2.2 * * @param array $additional_meta Additional metadata. * @param int $payment_id Payment ID. * @param array $fields Final/sanitized submitted field data. * @param array $form_data Form data and settings. */ $additional_meta = (array) apply_filters( 'wpforms_integrations_stripe_process_additional_metadata', [], $payment_id, $fields, $form_data ); array_walk( $additional_meta, static function ( $meta, $key ) use ( &$payment ) { $payment->metadata[ $key ] = $meta; } ); $payment->update( $payment->id, $payment->serializeParameters(), Helpers::get_auth_opts() ); $subscription = $this->api->get_subscription(); // Update the Stripe subscription metadata to include the Payment ID. if ( ! empty( $subscription->id ) ) { $subscription->metadata['payment_id'] = $payment_id; $subscription->metadata['payment_url'] = esc_url_raw( $payment_url ); $this->maybe_set_subscription_schedule( $subscription ); // Clean up cycles value. $subscription->metadata['cycles'] = null; $subscription->update( $subscription->id, $subscription->serializeParameters(), Helpers::get_auth_opts() ); } wpforms()->obj( 'payment_meta' )->add_log( $payment_id, sprintf( 'Stripe charge processed. (Charge ID: %1$s)', isset( $payment->latest_charge ) ? $payment->latest_charge : $payment->id ) ); /** * Fire when processing is complete. * * @since 1.8.2 * * @param array $fields Final/sanitized submitted field data. * @param array $form_data Form data and settings. * @param int $payment_id Payment ID. * @param mixed $payment Stripe payment object. * @param mixed $subscription Stripe subscription object. * @param mixed $customer Stripe customer object. */ do_action( 'wpforms_stripe_process_complete', $fields, $form_data, $payment_id, $payment, $subscription, $this->api->get_customer() ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName } /** * Get mapped custom metadata. * * @since 1.9.6 * * @param string $type Object type ( e.g 'customer', 'payment' ). * * @return array */ private function get_mapped_custom_metadata( string $type ): array { $settings_key = ! is_null( $this->plan_id ) ? 'recurring_custom_metadata_' . $this->plan_id : 'custom_metadata'; if ( empty( $this->form_data['payments']['stripe'][ $settings_key ] ) ) { return []; } $metadata = []; foreach ( $this->form_data['payments']['stripe'][ $settings_key ] as $data ) { // Skip if the field type not set or the meta-key is empty. if ( $data['object_type'] !== $type || empty( $data['meta_key'] ) ) { continue; } // Skip if either the key or value is empty. if ( ! $data['meta_key'] || ! $data['meta_value'] ) { continue; } $field_id = $data['meta_value']; if ( ! isset( $this->fields[ $field_id ]['value'] ) || wpforms_is_empty_string( $this->fields[ $field_id ]['value'] ) ) { continue; } // Add quantity for the field value. if ( wpforms_payment_has_quantity( $this->fields[ $field_id ], $this->form_data ) ) { $field_value = wpforms_payment_format_quantity( $this->fields[ $field_id ] ); } else { $field_value = $this->fields[ $field_id ]['value']; } // Key length limited to 40 characters long by Stripe API. $key = wp_html_excerpt( sanitize_text_field( $data['meta_key'] ), 40 ); // Check whether the meta-key is empty once again after sanitization. if ( empty( $key ) ) { continue; } // Value length limited to 500 characters long by Stripe API. $metadata[ $key ] = wp_html_excerpt( wpforms_decode_string( $field_value ), 500 ); } return $metadata; } /** * Add details to payment data. * * @since 1.8.2 * * @param array $payment_data Payment data args. * * @return array */ public function prepare_payment_data( $payment_data ) { $payment = $this->api->get_payment(); if ( empty( $payment->id ) ) { return $payment_data; } $customer = $this->api->get_customer(); $subscription = $this->api->get_subscription(); $payment_data['status'] = 'processed'; $payment_data['gateway'] = 'stripe'; $payment_data['mode'] = Helpers::get_stripe_mode(); $payment_data['transaction_id'] = sanitize_text_field( $payment->id ); $payment_data['customer_id'] = ! empty( $customer->id ) ? sanitize_text_field( $customer->id ) : ''; $payment_data['title'] = $this->get_payment_title(); if ( ! empty( $subscription->id ) ) { $payment_data['subscription_id'] = sanitize_text_field( $subscription->id ); $payment_data['subscription_status'] = 'not-synced'; } return $payment_data; } /** * Get Payment title. * * @since 1.8.2 * * @return string Payment title. */ private function get_payment_title() { $customer_name = $this->get_customer_name(); if ( $customer_name ) { return $customer_name; } $customer_email = $this->get_customer_email(); if ( $customer_email ) { return $customer_email; } return ''; } /** * Get Customer name. * * @since 1.8.2 * * @return string Customer name. */ private function get_customer_name() { $customer_name = $this->api->get_customer( 'name' ); if ( $customer_name ) { return $customer_name; } $charge_details = $this->api->get_charge_details( [ 'name' ] ); if ( ! empty( $charge_details['name'] ) ) { return $charge_details['name']; } return ''; } /** * Get Customer email. * * @since 1.8.2 * * @return string Customer email. */ private function get_customer_email() { $customer_email = $this->api->get_customer( 'email' ); if ( $customer_email ) { return $customer_email; } $charge_details = $this->api->get_charge_details( [ 'email' ] ); if ( ! empty( $charge_details['email'] ) ) { return $charge_details['email']; } // phpcs:disable WordPress.Security.NonceVerification.Missing if ( isset( $_POST['wpforms']['payment_link_email'] ) ) { return sanitize_email( wp_unslash( $_POST['wpforms']['payment_link_email'] ) ); } // phpcs:enable WordPress.Security.NonceVerification.Missing return ''; } /** * Logic that helps decide if we should send completed payments notifications. * * @since 1.8.2 * * @deprecated 1.9.5 * * @param bool $process Whether to process or not. * @param array $fields Form fields. * @param array $form_data Form data. * @param int $notification_id Notification ID. * * @return bool */ public function process_email( $process, $fields, $form_data, $notification_id ) { _deprecated_function( __METHOD__, '1.9.5 of the WPForms plugin', 'WPFormsStripe\Process::process_email()' ); if ( ! $process ) { return false; } if ( ! Helpers::has_stripe_enabled( [ $form_data ] ) ) { return $process; } if ( empty( $form_data['settings']['notifications'][ $notification_id ]['stripe'] ) ) { return $process; } if ( empty( $this->api->get_payment() ) ) { return false; } if ( ! $this->is_payment_processed ) { return false; } return empty( $this->api->get_error() ); } /** * Update entry details for a successful payment. * * @since 1.8.2 * * @param array $fields Final/sanitized submitted field data. * @param array $entry Copy of original $_POST. * @param array $form_data Form data and settings. * @param string $entry_id Entry ID. */ public function process_entry_data( $fields, $entry, $form_data, $entry_id ) { $payment = $this->api->get_payment(); if ( empty( $payment->id ) || empty( $entry_id ) ) { return; } wpforms()->obj( 'entry' )->update( $entry_id, [ 'type' => 'payment', ], '', '', [ 'cap' => false ] ); } /** * Get general errors before payment processing. * * @since 1.8.2 * * @return string */ protected function get_entry_errors() { // Check for Stripe payment tokens (card token or payment id). $error = $this->api->get_error(); // Check for Stripe keys. if ( ! Helpers::has_stripe_keys() ) { return esc_html__( 'Stripe payment stopped, missing keys.', 'wpforms-lite' ); } // Check that, despite how the form is configured, the form and // entry actually contain payment fields, otherwise no need to proceed. if ( ! wpforms_has_payment( 'form', $this->form_data ) || ! wpforms_has_payment( 'entry', $this->fields ) ) { return esc_html__( 'Stripe payment stopped, missing payment fields.', 'wpforms-lite' ); } // Check total charge amount. if ( empty( $this->amount ) || wpforms_sanitize_amount( 0 ) === $this->amount ) { return esc_html__( 'Stripe payment stopped, invalid/empty amount.', 'wpforms-lite' ); } if ( 50 > ( $this->amount * 100 ) ) { return esc_html__( 'Stripe payment stopped, amount less than minimum charge required.', 'wpforms-lite' ); } return $error; } /** * Process a payment. * * @since 1.8.2 */ public function process_payment() { if ( $this->is_api_errors() ) { return; } // Proceed to executing the purchase. if ( ! empty( $this->settings['enable_recurring'] ) || ! empty( $this->settings['recurring']['enable'] ) ) { $this->process_payment_subscription(); return; } $this->process_payment_single(); } /** * Process a subscription payment for forms with old payments interface. * * @since 1.8.4 */ protected function process_legacy_payment_subscription() { if ( ! $this->is_recurring_settings_ok( $this->settings['recurring'] ) ) { return; } $args = $this->get_base_subscription_args(); $args['settings'] = $this->settings['recurring']; $args['email'] = sanitize_email( $this->fields[ $args['settings']['email'] ]['value'] ); $args['customer_name'] = ! empty( $args['settings']['customer_name'] ) ? sanitize_text_field( $this->fields[ $args['settings']['customer_name'] ]['value'] ) : ''; $args['customer_phone'] = ! empty( $args['settings']['customer_phone'] ) ? sanitize_text_field( $this->fields[ $args['settings']['customer_phone'] ]['value'] ) : ''; // Customer address. if ( wpforms()->is_pro() && isset( $args['settings']['customer_address'] ) && $args['settings']['customer_address'] !== '' ) { $args['customer_address'] = $this->map_address_field( $this->fields[ $args['settings']['customer_address'] ], $args['settings']['customer_address'] ); } // Set plan id to get correct mapped meta. $this->plan_id = ''; // Customer custom metadata. $args['customer_metadata'] = $this->get_mapped_custom_metadata( 'customer' ); $this->process_subscription( $args ); // Set payment processing flag. $this->is_payment_processed = true; } /** * Process a single payment. * * @since 1.8.2 */ public function process_payment_single() { $amount_decimals = wpforms_get_currency_multiplier(); // Define the basic payment details. $args = [ 'amount' => $this->amount * $amount_decimals, 'currency' => strtolower( wpforms_get_currency() ), 'metadata' => [ 'form_name' => sanitize_text_field( $this->form_data['settings']['form_title'] ), 'form_id' => $this->form_id, ], ]; if ( ! Helpers::is_license_ok() && Helpers::is_application_fee_supported() ) { $args['application_fee_amount'] = (int) ( round( $this->amount * 0.03, 2 ) * $amount_decimals ); } // Store spam reason if exists. if ( isset( $this->form_data['spam_reason'] ) ) { $args['metadata']['spam_reason'] = $this->form_data['spam_reason']; } // Payment description. if ( ! empty( $this->settings['payment_description'] ) ) { $args['description'] = html_entity_decode( $this->settings['payment_description'], ENT_COMPAT, 'UTF-8' ); } // Receipt email. if ( isset( $this->settings['receipt_email'] ) && $this->settings['receipt_email'] !== '' && ! empty( $this->fields[ $this->settings['receipt_email'] ]['value'] ) ) { $args['receipt_email'] = sanitize_email( $this->fields[ $this->settings['receipt_email'] ]['value'] ); } // Customer email. if ( isset( $this->settings['customer_email'] ) && $this->settings['customer_email'] !== '' && ! empty( $this->fields[ $this->settings['customer_email'] ]['value'] ) ) { $args['customer_email'] = sanitize_email( $this->fields[ $this->settings['customer_email'] ]['value'] ); } // Customer name. if ( isset( $this->settings['customer_name'] ) && $this->settings['customer_name'] !== '' && ! empty( $this->fields[ $this->settings['customer_name'] ]['value'] ) ) { $args['customer_name'] = sanitize_text_field( $this->fields[ $this->settings['customer_name'] ]['value'] ); } // Customer phone. if ( isset( $this->settings['customer_phone'] ) && $this->settings['customer_phone'] !== '' && ! empty( $this->fields[ $this->settings['customer_phone'] ]['value'] ) ) { $args['customer_phone'] = sanitize_text_field( $this->fields[ $this->settings['customer_phone'] ]['value'] ); } // Customer custom metadata. $args['customer_metadata'] = $this->get_mapped_custom_metadata( 'customer' ); $args = $this->payment_single_map_address( $args ); $this->api->process_single( $args ); // Set payment processing flag. $this->is_payment_processed = true; $this->update_credit_card_field_value(); $this->process_api_error( 'single' ); } /** * Map address field for single payment. * * @since 1.9.0 * * @param array $args Payment arguments. * * @return array */ private function payment_single_map_address( array $args ): array { if ( ! wpforms()->is_pro() ) { return $args; } // Customer address. if ( isset( $this->settings['customer_address'] ) && $this->settings['customer_address'] !== '' ) { $args['customer_address'] = $this->map_address_field( $this->fields[ $this->settings['customer_address'] ], $this->settings['customer_address'] ); } // Shipping address. if ( isset( $this->settings['shipping_address'] ) && $this->settings['shipping_address'] !== '' ) { $args['shipping']['name'] = $args['customer_name'] ?? ''; $args['shipping']['address'] = $this->map_address_field( $this->fields[ $this->settings['shipping_address'] ], $this->settings['shipping_address'] ); } return $args; } /** * Process a subscription payment. * * @since 1.8.2 */ public function process_payment_subscription() { if ( Helpers::is_legacy_payment_settings( $this->form_data ) ) { $this->process_legacy_payment_subscription(); return; } $args = $this->get_base_subscription_args(); foreach ( $this->settings['recurring'] as $key => $recurring ) { if ( ! $this->is_subscription_plan_valid( $recurring ) ) { continue; } $this->plan_id = $key; $args['email'] = sanitize_email( $this->fields[ $recurring['email'] ]['value'] ); $args['settings'] = $recurring; $args['description'] = sanitize_text_field( $recurring['name'] ); // Customer name. if ( isset( $recurring['customer_name'] ) && $recurring['customer_name'] !== '' && ! empty( $this->fields[ $recurring['customer_name'] ]['value'] ) ) { $args['customer_name'] = sanitize_text_field( $this->fields[ $recurring['customer_name'] ]['value'] ); } // Customer phone. if ( isset( $recurring['customer_phone'] ) && $recurring['customer_phone'] !== '' && ! empty( $this->fields[ $recurring['customer_phone'] ]['value'] ) ) { $args['customer_phone'] = sanitize_text_field( $this->fields[ $recurring['customer_phone'] ]['value'] ); } // Customer address. if ( wpforms()->is_pro() && isset( $recurring['customer_address'] ) && $recurring['customer_address'] !== '' ) { $args['customer_address'] = $this->map_address_field( $this->fields[ $recurring['customer_address'] ], $recurring['customer_address'] ); } // Customer custom metadata. $args['customer_metadata'] = $this->get_mapped_custom_metadata( 'customer' ); // Validate the number of cycle to process. if ( ! empty( $recurring['cycles'] ) && ( $recurring['cycles'] === 'undefined' || ( is_numeric( $recurring['cycles'] ) && $recurring['cycles'] > 0 ) ) ) { $args['cycles'] = sanitize_text_field( $recurring['cycles'] ); } $this->process_subscription( $args ); return; } if ( ! empty( $this->settings['enable_one_time'] ) ) { $this->process_payment_single(); return; } $this->log_error( esc_html__( 'Stripe Subscription payment stopped, validation error.', 'wpforms-lite' ), $this->fields, 'conditional_logic' ); } /** * Validate plan before process. * * @since 1.8.4 * * @param array $plan Plan settings. * * @return bool */ protected function is_subscription_plan_valid( $plan ) { return ! empty( $plan['email'] ) && $this->is_recurring_settings_ok( $plan ); } /** * Update the credit card field value to contain basic details. * * @since 1.8.2 */ public function update_credit_card_field_value() { foreach ( $this->fields as $field_id => $field ) { if ( empty( $field['type'] ) || $this->api->get_config( 'field_slug' ) !== $field['type'] ) { continue; } $details = $this->api->get_charge_details( [ 'name', 'last4', 'brand' ] ); if ( ! empty( $details['last4'] ) ) { $details['last4'] = 'xxxx xxxx xxxx ' . $details['last4']; } if ( ! empty( $details['brand'] ) ) { $details['brand'] = ucfirst( $details['brand'] ); } $details = is_array( $details ) && ! empty( $details ) ? implode( "\n", array_filter( $details ) ) : '-'; /** * This filter allows to overwrite a Style Credit Card value in saved entry. * * @since 1.8.2 * * @param array $details Card details. * @param object $payment Stripe payment objects. */ wpforms()->obj( 'process' )->fields[ $field_id ]['value'] = apply_filters( 'wpforms_stripe_creditcard_value', $details, $this->api->get_payment() ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName } } /** * Check if there is at least one visible (not hidden by conditional logic) card field in the form. * * @since 1.8.2 */ protected function is_card_field_visibility_ok() { // If the form contains no fields with conditional logic the card field is visible by default. if ( empty( $this->form_data['conditional_fields'] ) ) { return true; } foreach ( $this->fields as $field ) { if ( empty( $field['type'] ) || $this->api->get_config( 'field_slug' ) !== $field['type'] ) { continue; } // if the field is NOT in array of conditional fields, it's visible. if ( ! in_array( $field['id'], $this->form_data['conditional_fields'], true ) ) { return true; } // if the field IS in array of conditional fields and marked as visible, it's visible. if ( ! empty( $field['visible'] ) ) { return true; } } return false; } /** * Log payment error. * * @since 1.8.2 * * @param string $title Error title. * @param string $message Error message. * @param string $level Error level to add to 'payment' error level. */ protected function log_error( $title, $message = '', $level = 'error' ) { if ( $message instanceof ApiErrorException ) { $body = $message->getJsonBody(); $message = isset( $body['error']['message'] ) ? $body['error'] : $message->getMessage(); } wpforms_log( $title, $message, [ 'type' => [ 'payment', $level ], 'form_id' => $this->form_id, ] ); } /** * Collect errors from API and turn it into form errors. * * @since 1.8.2 * * @param string $type Payment time (e.g. 'single' or 'subscription'). */ protected function process_api_error( $type ) { $message = $this->api->get_error(); if ( empty( $message ) ) { return; } $message = sprintf( /* translators: %s - error message. */ esc_html__( 'Payment Error: %s', 'wpforms-lite' ), $message ); $this->display_error( $message ); if ( $type === 'subscription' ) { $title = esc_html__( 'Stripe subscription payment stopped by error', 'wpforms-lite' ); } else { $title = esc_html__( 'Stripe payment stopped by error', 'wpforms-lite' ); } $this->log_error( $title, $this->api->get_exception() ); } /** * Display form error. * * @since 1.8.2 * * @param string $error Error to display. */ private function display_error( $error ) { if ( ! $error ) { return; } $field_slug = $this->api->get_config( 'field_slug' ); // Check if the form contains a required credit card. If it does // and there was an error, return the error to the user and prevent // the form from being submitted. This should not occur under normal // circumstances. foreach ( $this->form_data['fields'] as $field ) { if ( empty( $field['type'] ) || $field_slug !== $field['type'] ) { continue; } if ( ! empty( $field['required'] ) ) { wpforms()->obj( 'process' )->errors[ $this->form_id ]['footer'] = $error; return; } } } /** * Process card error from Stripe API exception and adds rate limit tracking. * * @since 1.8.2 * * @param ApiErrorException $e Stripe API exception to process. */ public function process_card_error( $e ) { if ( Helpers::get_stripe_mode() === 'test' ) { return; } if ( ! is_a( $e, '\WPForms\Vendor\Stripe\Exception\CardException' ) ) { return; } /** * Allow to filter Stripe process card error. * * @since 1.8.2 * * @param bool $flag True if any error. */ if ( ! apply_filters( 'wpforms_stripe_process_process_card_error', true ) ) { // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName return; } $this->rate_limit->increment_attempts(); } /** * Check if rate limit is under threshold and passes. * * @since 1.8.2 */ protected function is_rate_limit_ok() { return $this->rate_limit->is_ok(); } /** * Check if any API errors occurs. * * @since 1.8.4 * * @return bool */ protected function is_api_errors() { $this->api->setup_stripe(); $error = $this->api->get_error(); if ( $error ) { $this->process_api_error( 'general' ); return true; } return false; } /** * Check if recurring settings is configured correctly. * * @since 1.8.4 * * @param {array} $settings Settings data. * * @return bool */ protected function is_recurring_settings_ok( $settings ) { $error = ''; // Check subscription settings are provided. if ( empty( $settings['period'] ) || empty( $settings['email'] ) ) { $error = esc_html__( 'Stripe subscription payment stopped, missing form settings.', 'wpforms-lite' ); } // Check for required customer email. if ( ! $error && empty( $this->fields[ $settings['email'] ]['value'] ) ) { $error = esc_html__( 'Stripe subscription payment stopped, customer email not found.', 'wpforms-lite' ); } // Before proceeding, check if any basic errors were detected. if ( $error ) { $this->log_error( $error ); $this->display_error( $error ); return false; } return true; } /** * Process subscription API call. * * @since 1.8.4 * * @param array $args Prepared subscription arguments. */ protected function process_subscription( $args ) { $this->subscription_settings = $args['settings']; if ( ! Helpers::is_license_ok() && Helpers::is_application_fee_supported() ) { $args['application_fee_percent'] = 3; } // Store spam reason if exists. if ( isset( $this->form_data['spam_reason'] ) ) { $args['metadata']['spam_reason'] = $this->form_data['spam_reason']; } $this->api->process_subscription( $args ); // Set payment processing flag. $this->is_payment_processed = true; // Update the credit card field value to contain basic details. $this->update_credit_card_field_value(); $this->process_api_error( 'subscription' ); } /** * Get base subscription arguments. * * @since 1.8.4 * * @return array */ protected function get_base_subscription_args() { return [ 'form_id' => $this->form_id, 'form_title' => sanitize_text_field( $this->form_data['settings']['form_title'] ), 'amount' => $this->amount * wpforms_get_currency_multiplier(), ]; } /** * Map WPForms Address field to Stripe format. * * @since 1.8.8 * * @param array $submitted_data Submitted address data. * @param string $field_id Address field ID. * * @return array */ private function map_address_field( array $submitted_data, string $field_id ): array { $line = sanitize_text_field( $submitted_data['address1'] ); $country = ''; if ( isset( $submitted_data['address2'] ) ) { $line .= ' ' . sanitize_text_field( $submitted_data['address2'] ); } if ( isset( $submitted_data['country'] ) ) { $country = sanitize_text_field( $submitted_data['country'] ); } elseif ( $this->form_data['fields'][ $field_id ]['scheme'] !== 'international' ) { $country = 'US'; } return [ 'line1' => $line, 'state' => isset( $submitted_data['state'] ) ? sanitize_text_field( $submitted_data['state'] ) : '', 'city' => sanitize_text_field( $submitted_data['city'] ), 'postal_code' => sanitize_text_field( $submitted_data['postal'] ), 'country' => $country, ]; } /** * Check the submitted payment data whether it was corrupted. * If so, refund a payment / cancel subscription. * * @since 1.8.8.2 * * @param array $entry Submitted entry data. * * @return bool */ private function is_submitted_payment_data_corrupted( array $entry ): bool { // Bail early if there are no payment intents. if ( empty( $entry['payment_intent_id'] ) ) { return false; } // Get stored corrupted payment intents if exist. $corrupted_intents = (array) Transient::get( 'corrupted-stripe-intents' ); // We must prevent a processing if payment intent was identified as corrupted. // Also if the transaction ID exists in DB (transaction ID is unique value). if ( in_array( $entry['payment_intent_id'], $corrupted_intents, true ) || wpforms()->obj( 'payment' )->get_by( 'transaction_id', $entry['payment_intent_id'] ) ) { wpforms()->obj( 'process' )->errors[ $this->form_id ]['footer'] = esc_html__( 'Secondary form submission was declined.', 'wpforms-lite' ); return true; } $intent = $this->api->retrieve_payment_intent( $entry['payment_intent_id'], [ 'expand' => [ 'invoice.subscription' ], ] ); // Round to the nearest whole number because $this->amount can contain a number close to, // but slightly under it, due to how it is stored in the memory. $submitted_amount = round( $this->amount * wpforms_get_currency_multiplier() ); // Prevent form submission if a mismatch of the payment amount is detected. if ( ! empty( $intent ) && (int) $submitted_amount !== (int) $intent->amount ) { wpforms()->obj( 'process' )->errors[ $this->form_id ]['footer'] = esc_html__( 'Irregular activity detected. Your submission has been declined and payment refunded.', 'wpforms-lite' ); $args = [ 'reason' => 'fraudulent', ]; // We can't cancel a payment because it's already paid. // So we can perform a refund only. $this->api->refund_payment( $entry['payment_intent_id'], $args ); // Cancel subscription if exists. if ( ! empty( $intent->invoice->subscription ) ) { $this->api->cancel_subscription( $intent->invoice->subscription->id ); } // This payment indent is identified as corrupted. // Store it in order to prevent re-using it (form re-submitting). if ( ! in_array( $entry['payment_intent_id'], $corrupted_intents, true ) ) { $corrupted_intents[] = $entry['payment_intent_id']; Transient::set( 'corrupted-stripe-intents', $corrupted_intents, WEEK_IN_SECONDS ); } return true; } return false; } /** * Maybe create a subscription schedule if the cycles was set. * * @since 1.9.8 * * @param object $subscription Stripe subscription object. */ private function maybe_set_subscription_schedule( object $subscription ): void { if ( empty( $subscription->metadata['cycles'] ) || $subscription->metadata['cycles'] === 'unlimited' || (int) $subscription->metadata['cycles'] < 1 || empty( $subscription->items->data ) ) { return; } try { $schedule = SubscriptionSchedule::create( [ 'from_subscription' => $subscription->id, ], Helpers::get_auth_opts() ); $subscription_item = $subscription->items->data[0]; $schedule::update( $schedule->id, [ 'end_behavior' => 'cancel', 'phases' => [ [ 'start_date' => $subscription_item->current_period_start, 'items' => [ [ 'plan' => $subscription_item->plan->id, ], ], 'iterations' => $subscription->metadata['cycles'], ], ], ], Helpers::get_auth_opts() ); } catch ( \Exception $e ) { wpforms_log( 'Stripe: Unable to create Subscription Schedule.', $e->getMessage(), [ 'type' => [ 'payment', 'error' ], ] ); } } } Integrations/Loader.php 0000644 00000005500 15174710275 0011142 0 ustar 00 <?php namespace WPForms\Integrations; /** * Class Loader gives ability to track/load all integrations. * * @since 1.4.8 */ class Loader { /** * Get the instance of a class and store it in itself. * * @since 1.4.8 */ public static function get_instance() { static $instance; if ( ! $instance ) { $instance = new Loader(); } return $instance; } /** * Loader constructor. * * @since 1.4.8 */ public function __construct() { $core_class_names = [ 'SMTP\Notifications', 'LiteConnect\LiteConnect', 'Divi\Divi', 'Elementor\Elementor', 'WPCode\WPCode', 'WPCode\RegisterLibrary', 'Gutenberg\FormSelector', 'WPMailSMTP\Notifications', 'WPorg\Translations', 'Stripe\Stripe', 'UncannyAutomator\UncannyAutomator', 'UsageTracking\UsageTracking', 'DefaultThemes\DefaultThemes', 'Translations\Translations', 'DefaultContent\DefaultContent', 'PopupMaker\PopupMaker', 'WooCommerce\Notifications', 'AI\AI', 'ConstantContact\V3\ConstantContact', 'Square\Square', 'MotoPress\MotoPress', 'Abilities\Abilities', ]; /** * Filter available integrations. * * @since 1.7.0 * * @param array $core_class_names Array of core class names. */ $class_names = (array) apply_filters( 'wpforms_integrations_available', $core_class_names ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName foreach ( $class_names as $class_name ) { $integration = $this->register_class( $class_name ); wpforms()->register_instance( $class_name, $integration ); if ( ! empty( $integration ) ) { $this->load_integration( $integration ); } } } /** * Load an integration. * * @param IntegrationInterface $integration Instance of an integration class. * * @since 1.4.8 */ protected function load_integration( IntegrationInterface $integration ) { if ( $integration->allow_load() ) { $integration->load(); } } /** * Register a new class. * * @since 1.5.6 * * @param string $class_name Class name to register. * * @return IntegrationInterface Instance of class. */ public function register_class( $class_name ) { $class_name = sanitize_text_field( $class_name ); // Load Lite class if exists. if ( class_exists( 'WPForms\Lite\Integrations\\' . $class_name ) && ! wpforms()->is_pro() ) { $class_name = 'WPForms\Lite\Integrations\\' . $class_name; return new $class_name(); } // Load Pro class if exists. if ( class_exists( 'WPForms\Pro\Integrations\\' . $class_name ) && wpforms()->is_pro() ) { $class_name = 'WPForms\Pro\Integrations\\' . $class_name; return new $class_name(); } // Load general class if neither Pro nor Lite class exists. if ( class_exists( __NAMESPACE__ . '\\' . $class_name ) ) { $class_name = __NAMESPACE__ . '\\' . $class_name; return new $class_name(); } } } Integrations/Abilities/Abilities.php 0000644 00000031040 15174710275 0013544 0 ustar 00 <?php namespace WPForms\Integrations\Abilities; use WP_Error; use WP_Post; use WPForms\Integrations\IntegrationInterface; /** * WordPress Abilities API Integration for WPForms. * * Provides a standardized interface for AI assistants and automation tools * to discover and interact with WPForms functionality. * * @since 1.9.9 */ abstract class Abilities implements IntegrationInterface { /** * Ability namespace for WPForms abilities. * * @since 1.9.9 * * @var string */ protected const ABILITY_NAMESPACE = 'wpforms'; /** * Category slug for WPForms abilities. * * @since 1.9.9 * * @var string */ protected const CATEGORY_SLUG = 'wpforms-forms'; /** * Indicate if the current integration is allowed to load. * * @since 1.9.9 * * @return bool */ public function allow_load(): bool { // Only load if the Abilities API is available (WordPress 6.9+). return function_exists( 'wp_register_ability' ); } /** * Load the integration. * * @since 1.9.9 */ public function load(): void { $this->hooks(); } /** * Register hooks. * * @since 1.9.9 */ protected function hooks(): void { add_action( 'wp_abilities_api_categories_init', [ $this, 'register_category' ] ); add_action( 'wp_abilities_api_init', [ $this, 'register_abilities' ] ); } /** * Register the WPForms ability category. * * @since 1.9.9 */ public function register_category(): void { wp_register_ability_category( self::CATEGORY_SLUG, [ 'label' => __( 'WPForms', 'wpforms-lite' ), 'description' => __( 'Abilities for interacting with WPForms forms and entries.', 'wpforms-lite' ), ] ); } /** * Register WPForms abilities. * * @since 1.9.9 */ abstract public function register_abilities(); /** * Register common abilities shared between Lite and Pro. * * @since 1.9.9 */ protected function register_common_abilities(): void { $this->register_list_forms_ability(); $this->register_get_form_ability(); } /** * Register the list_forms ability. * * @since 1.9.9 */ protected function register_list_forms_ability(): void { wp_register_ability( self::ABILITY_NAMESPACE . '/list-forms', [ 'label' => __( 'List Forms', 'wpforms-lite' ), 'description' => __( 'List all available WPForms forms with their metadata.', 'wpforms-lite' ), 'category' => self::CATEGORY_SLUG, 'execute_callback' => [ $this, 'ability_list_forms' ], 'permission_callback' => [ $this, 'check_view_forms_permission' ], 'input_schema' => [ 'type' => 'object', 'properties' => [ 'status' => [ 'description' => __( 'Filter forms by status.', 'wpforms-lite' ), 'type' => 'string', 'enum' => [ 'publish', 'draft', 'trash' ], 'default' => 'publish', ], 'limit' => [ 'description' => __( 'Maximum number of forms to return.', 'wpforms-lite' ), 'type' => 'integer', 'minimum' => 1, 'maximum' => 100, 'default' => 20, ], 'offset' => [ 'description' => __( 'Number of forms to skip.', 'wpforms-lite' ), 'type' => 'integer', 'minimum' => 0, 'default' => 0, ], ], ], 'output_schema' => [ 'type' => 'object', 'properties' => [ 'forms' => [ 'type' => 'array', 'description' => __( 'Array of form objects.', 'wpforms-lite' ), 'items' => [ 'type' => 'object', 'properties' => [ 'id' => [ 'type' => 'integer' ], 'title' => [ 'type' => 'string' ], 'status' => [ 'type' => 'string' ], 'created' => [ 'type' => 'string' ], 'modified' => [ 'type' => 'string' ], ], ], ], 'total' => [ 'type' => 'integer', 'description' => __( 'Total number of forms returned.', 'wpforms-lite' ), ], ], ], 'meta' => [ 'annotations' => [ 'readonly' => true, 'destructive' => false, 'idempotent' => true, ], 'show_in_rest' => true, 'mcp' => [ 'public' => true, ], ], ] ); } /** * Register the get_form ability. * * @since 1.9.9 */ protected function register_get_form_ability(): void { wp_register_ability( self::ABILITY_NAMESPACE . '/get-form', [ 'label' => __( 'Get Form', 'wpforms-lite' ), 'description' => __( 'Get detailed information about a specific WPForms form including its fields.', 'wpforms-lite' ), 'category' => self::CATEGORY_SLUG, 'execute_callback' => [ $this, 'ability_get_form' ], 'permission_callback' => [ $this, 'check_view_single_form_permission' ], 'input_schema' => [ 'type' => 'object', 'properties' => [ 'form_id' => [ 'description' => __( 'The ID of the form to retrieve.', 'wpforms-lite' ), 'type' => 'integer', 'minimum' => 1, ], 'include_fields' => [ 'description' => __( 'Whether to include field configuration.', 'wpforms-lite' ), 'type' => 'boolean', 'default' => true, ], ], 'required' => [ 'form_id' ], ], 'output_schema' => [ 'type' => 'object', 'properties' => [ 'id' => [ 'type' => 'integer' ], 'title' => [ 'type' => 'string' ], 'status' => [ 'type' => 'string' ], 'settings' => [ 'type' => 'object' ], 'fields' => [ 'type' => 'array' ], ], ], 'meta' => [ 'annotations' => [ 'readonly' => true, 'destructive' => false, 'idempotent' => true, ], 'show_in_rest' => true, 'mcp' => [ 'public' => true, ], ], ] ); } /** * Permission callback: Check if the user can view forms. * * @since 1.9.9 * * @return bool|WP_Error */ public function check_view_forms_permission() { if ( ! wpforms_current_user_can( 'view_forms' ) ) { return new WP_Error( 'wpforms_forbidden', __( 'You do not have permission to view forms.', 'wpforms-lite' ), [ 'status' => 403 ] ); } return true; } /** * Permission callback: Check if the user can view a specific form. * * @since 1.9.9 * * @param mixed $input Input data containing form_id. * * @return bool|WP_Error */ public function check_view_single_form_permission( $input = null ) { $input = $this->normalize_input( $input ); $form_id = absint( $input['form_id'] ?? 0 ); if ( ! $form_id || ! wpforms_current_user_can( 'view_form_single', $form_id ) ) { return new WP_Error( 'wpforms_forbidden', __( 'You do not have permission to view this form.', 'wpforms-lite' ), [ 'status' => 403 ] ); } return true; } /** * Ability callback: List forms. * * @since 1.9.9 * * @param mixed $input Input data. * * @return array */ public function ability_list_forms( $input = null ): array { $args = $this->normalize_input( $input ); $form_handler = $this->get_form_handler(); if ( is_wp_error( $form_handler ) ) { return [ 'forms' => [], 'total' => 0, ]; } $limit = absint( $args['limit'] ?? 20 ); $offset = absint( $args['offset'] ?? 0 ); $status = sanitize_text_field( $args['status'] ?? 'publish' ); // Get total count efficiently using the cached WordPress function. $counts = wp_count_posts( 'wpforms' ); $total = $counts->{$status} ?? 0; // Get paginated forms with proper WordPress pagination. $query_args = [ 'post_status' => $status, 'posts_per_page' => $limit, 'offset' => $offset, 'nopaging' => false, // Override default to enable pagination. 'order' => 'DESC', 'orderby' => 'date', ]; $forms = $form_handler->get( '', $query_args ); if ( empty( $forms ) ) { return [ 'forms' => [], 'total' => $total, ]; } $result = []; foreach ( $forms as $form ) { $result[] = $this->format_form_summary( $form ); } return [ 'forms' => $result, 'total' => $total, ]; } /** * Ability callback: Get single form. * * @since 1.9.9 * * @param mixed $input Input data. * * @return array|WP_Error */ public function ability_get_form( $input = null ) { $args = $this->normalize_input( $input ); $form_id = absint( $args['form_id'] ?? 0 ); if ( empty( $form_id ) ) { return new WP_Error( 'wpforms_invalid_form_id', __( 'Invalid form ID.', 'wpforms-lite' ), [ 'status' => 400 ] ); } $form_handler = $this->get_form_handler(); if ( is_wp_error( $form_handler ) ) { return $form_handler; } $form = $form_handler->get( $form_id ); if ( empty( $form ) ) { return new WP_Error( 'wpforms_form_not_found', __( 'Form not found.', 'wpforms-lite' ), [ 'status' => 404 ] ); } $include_fields = wp_validate_boolean( $args['include_fields'] ?? true ); return $this->format_form_detail( $form, $include_fields ); } /** * Normalize input data to array format. * * @since 1.9.9 * * @param mixed $input Input data (can be the array, object, or null). * * @return array */ protected function normalize_input( $input ): array { if ( is_array( $input ) ) { return $input; } if ( is_object( $input ) ) { return (array) $input; } return []; } /** * Get the form handler and validate it. * * @since 1.9.9 * * @return object|WP_Error Form handler object or WP_Error on failure. */ protected function get_form_handler() { $form_handler = wpforms()->obj( 'form' ); if ( ! $form_handler ) { return new WP_Error( 'wpforms_form_handler_error', __( 'Form handler not available.', 'wpforms-lite' ), [ 'status' => 500 ] ); } return $form_handler; } /** * Format form data for summary listing. * * @since 1.9.9 * * @param WP_Post $form Form the `post` object. * * @return array */ protected function format_form_summary( WP_Post $form ): array { return [ 'id' => $form->ID, 'title' => $form->post_title, 'status' => $form->post_status, 'created' => $form->post_date, 'modified' => $form->post_modified, 'author' => absint( $form->post_author ), ]; } /** * Format form data for the detailed view. * * @since 1.9.9 * * @param WP_Post $form Form `post` object. * @param bool $include_fields Whether to include fields. * * @return array */ protected function format_form_detail( WP_Post $form, bool $include_fields = true ): array { $form_handler = $this->get_form_handler(); $form_data = ! is_wp_error( $form_handler ) ? $form_handler->get( $form->ID, [ 'content_only' => true ] ) : []; // Ensure form_data is an array. if ( ! is_array( $form_data ) ) { $form_data = []; } $result = [ 'id' => $form->ID, 'title' => $form->post_title, 'status' => $form->post_status, 'created' => $form->post_date, 'modified' => $form->post_modified, 'author' => absint( $form->post_author ), 'settings' => $this->get_safe_settings( $form_data ), ]; if ( $include_fields && ! empty( $form_data['fields'] ) ) { $result['fields'] = $this->format_fields( $form_data['fields'] ); } return $result; } /** * Get safe settings (without sensitive data). * * @since 1.9.9 * * @param array $form_data Form data. * * @return array */ protected function get_safe_settings( array $form_data ): array { $settings = $form_data['settings'] ?? []; // Return only safe, non-sensitive settings. return [ 'form_title' => $settings['form_title'] ?? '', 'form_desc' => $settings['form_desc'] ?? '', 'submit_text' => $settings['submit_text'] ?? __( 'Submit', 'wpforms-lite' ), 'ajax_submit' => ! empty( $settings['ajax_submit'] ), 'honeypot' => ! empty( $settings['honeypot'] ), 'antispam' => ! empty( $settings['antispam'] ), ]; } /** * Format fields for output. * * @since 1.9.9 * * @param array $fields Form fields. * * @return array */ protected function format_fields( array $fields ): array { $result = []; foreach ( $fields as $field_id => $field ) { $result[] = [ 'id' => absint( $field_id ), 'type' => sanitize_text_field( $field['type'] ?? '' ), 'label' => sanitize_text_field( $field['label'] ?? '' ), 'description' => sanitize_text_field( $field['description'] ?? '' ), 'required' => ! empty( $field['required'] ), 'size' => sanitize_text_field( $field['size'] ?? 'medium' ), ]; } return $result; } } Integrations/Square/AddonCompatibility.php 0000644 00000002562 15174710275 0014760 0 ustar 00 <?php namespace WPForms\Integrations\Square; /** * Compatibility with the Square addon. * * @since 1.9.5 */ class AddonCompatibility { /** * Minimum compatible version of the Square addon. * * @since 1.9.5 * * @var string */ private const MIN_COMPAT_VERSION = '2.0.0'; /** * Initialization. * * @since 1.9.5 * * @return AddonCompatibility|null */ public function init() { return Helpers::is_pro() ? $this : null; } /** * Register hooks. * * @since 1.9.5 */ public function hooks() { // Warn the user about the fact that the not supported addon has been installed. add_action( 'admin_notices', [ $this, 'display_legacy_addon_notice' ] ); } /** * Check if the supported Square addon is active. * * @since 1.9.5 * * @return bool */ public function is_supported_version(): bool { return defined( 'WPFORMS_SQUARE_VERSION' ) && version_compare( WPFORMS_SQUARE_VERSION, self::MIN_COMPAT_VERSION, '>=' ); } /** * Display wp-admin notification saying user first have to update addon to the latest version. * * @since 1.9.5 */ public function display_legacy_addon_notice() { echo '<div class="notice notice-error"><p>'; esc_html_e( 'The WPForms Square addon is out of date. To avoid payment processing issues, please upgrade the Square addon to the latest version.', 'wpforms-lite' ); echo '</p></div>'; } } Integrations/Square/Connection.php 0000644 00000024164 15174710275 0013302 0 ustar 00 <?php namespace WPForms\Integrations\Square; use WPForms\Vendor\Square\Environment; use WPForms\Helpers\Crypto; /** * Connection class. * * @since 1.9.5 */ class Connection { /** * Valid connection status. * * @since 1.9.5 */ const STATUS_VALID = 'valid'; /** * Invalid connection status. * * @since 1.9.5 */ const STATUS_INVALID = 'invalid'; /** * Determine if a connection for production mode. * * @since 1.9.5 * * @var bool */ private $live_mode; /** * Access token. * * @since 1.9.5 * * @var string */ private $access_token; /** * Refresh token. * * @since 1.9.5 * * @var string */ private $refresh_token; /** * Square-issued ID of an application. * * @since 1.9.5 * * @var string */ private $client_id; /** * Square-issued ID of the merchant. * * @since 1.9.5 * * @var string */ private $merchant_id; /** * Currency associated with a merchant account. * * @since 1.9.5 * * @var string */ private $currency; /** * Connection status. * * @since 1.9.5 * * @var string */ private $status; /** * Date when tokens should be renewed. * * @since 1.9.5 * * @var int */ private $renew_at; /** * Determine if connection tokens are encrypted. * * @since 1.9.5 * * @var bool */ private $encrypted; /** * Determine if scopes were updated. * * @since 1.9.5 * * @var int */ private $scopes_updated = 0; /** * Connection constructor. * * @since 1.9.5 * * @param array $data Connection data. * @param bool $encrypted Optional. Default true. Use false when connection tokens were not encrypted. */ public function __construct( array $data, bool $encrypted = true ) { $data = (array) $data; if ( ! empty( $data['access_token'] ) ) { $this->access_token = $data['access_token']; } if ( ! empty( $data['refresh_token'] ) ) { $this->refresh_token = $data['refresh_token']; } if ( ! empty( $data['client_id'] ) ) { $this->client_id = $data['client_id']; } if ( ! empty( $data['merchant_id'] ) ) { $this->merchant_id = $data['merchant_id']; } if ( ! empty( $data['scopes_updated'] ) ) { $this->scopes_updated = $data['scopes_updated']; } $this->set_status( empty( $data['status'] ) ? self::STATUS_VALID : $data['status'] ); $this->currency = empty( $data['currency'] ) ? '' : strtoupper( (string) $data['currency'] ); $this->renew_at = empty( $data['renew_at'] ) ? time() : (int) $data['renew_at']; $this->live_mode = ! empty( $data['live_mode'] ); $this->encrypted = (bool) $encrypted; } /** * Retrieve a connection instance if it exists. * * @since 1.9.5 * * @param string $mode Square mode. * @param bool $encrypted Optional. Default true. Use false when connection tokens were not encrypted. * * @return Connection|null */ public static function get( string $mode = '', bool $encrypted = true ) { $mode = Helpers::validate_mode( $mode ); $connections = (array) get_option( 'wpforms_square_connections', [] ); if ( empty( $connections[ $mode ] ) ) { return null; } return new self( (array) $connections[ $mode ], $encrypted ); } /** * Save connection data into DB. * * @since 1.9.5 */ public function save() { $connections = (array) get_option( 'wpforms_square_connections', [] ); $connections[ $this->get_mode() ] = $this->get_data(); update_option( 'wpforms_square_connections', $connections ); } /** * Delete connection data from DB. * * @since 1.9.5 */ public function delete() { $connections = (array) get_option( 'wpforms_square_connections', [] ); unset( $connections[ $this->get_mode() ] ); empty( $connections ) ? delete_option( 'wpforms_square_connections' ) : update_option( 'wpforms_square_connections', $connections ); } /** * Revoke tokens from DB. * * @since 1.9.5 */ public function revoke_tokens() { $connections = (array) get_option( 'wpforms_square_connections', [] ); $mode = $this->get_mode(); $connections[ $mode ] = $this->get_data(); $connections[ $mode ]['access_token'] = ''; $connections[ $mode ]['refresh_token'] = ''; update_option( 'wpforms_square_connections', $connections ); } /** * Retrieve true if a connection for production mode. * * @since 1.9.5 * * @return bool */ public function get_live_mode(): bool { return $this->live_mode; } /** * Retrieve a connection mode. * * @since 1.9.5 * * @return string */ public function get_mode(): string { return $this->live_mode ? Environment::PRODUCTION : Environment::SANDBOX; } /** * Retrieve an un-encrypted access token. * * @since 1.9.5 * * @return string */ public function get_access_token(): string { return $this->encrypted ? Crypto::decrypt( $this->access_token ) : $this->access_token; } /** * Retrieve an un-encrypted refresh token. * * @since 1.9.5 * * @return string */ public function get_refresh_token(): string { return $this->encrypted ? Crypto::decrypt( $this->refresh_token ) : $this->refresh_token; } /** * Retrieve a client ID. * * @since 1.9.5 * * @return string */ public function get_client_id(): string { return $this->client_id; } /** * Retrieve an ID of the authorized merchant. * * @since 1.9.5 * * @return string */ public function get_merchant_id(): string { return $this->merchant_id; } /** * Retrieve a currency code of the authorized merchant. * * @since 1.9.5 * * @return string */ public function get_currency(): string { return $this->currency; } /** * Set a currency code. * * @since 1.9.5 * * @param string $code Currency code. * * @return Connection */ public function set_currency( string $code ) { $this->currency = strtoupper( $code ); return $this; } /** * Retrieve a connection status. * * @since 1.9.5 * * @return string */ public function get_status(): string { return $this->status; } /** * Set a connection status if it valid. * * @since 1.9.5 * * @param string $status The connection status. * * @return Connection */ public function set_status( string $status ) { if ( in_array( $status, $this->get_statuses(), true ) ) { $this->status = $status; } return $this; } /** * Retrieve a renewal timestamp. * * @since 1.9.5 * * @return int */ public function get_renew_at(): int { return $this->renew_at; } /** * Set/update a renewal timestamp. * * @since 1.9.5 * * @return Connection */ public function set_renew_at() { // Tokens must automatically renew every 7 days or less. $this->renew_at = time() + wp_rand( 5, 8 ) * DAY_IN_SECONDS; return $this; } /** * Retrieve a scopes updated timestamp. * * @since 1.9.5 * * @return int */ public function get_scopes_updated(): int { return $this->scopes_updated; } /** * Set/update a scopes updated timestamp. * * @since 1.9.5 * * @return Connection */ public function set_scopes_updated() { $this->scopes_updated = time(); return $this; } /** * Encrypt tokens, if it needed. * * @since 1.9.5 * * @return Connection */ public function encrypt_tokens() { // Bail if tokens have already encrypted. if ( $this->encrypted ) { return $this; } // Bail if tokens are not passed. if ( empty( $this->access_token ) || empty( $this->refresh_token ) ) { return $this; } // Prepare encrypted tokens. $encrypted_access_token = Crypto::encrypt( $this->access_token ); $encrypted_refresh_token = Crypto::encrypt( $this->refresh_token ); // Bail if encrypted tokens are invalid. if ( empty( $encrypted_access_token ) || empty( $encrypted_refresh_token ) ) { return $this; } $this->encrypted = true; $this->access_token = $encrypted_access_token; $this->refresh_token = $encrypted_refresh_token; return $this; } /** * Retrieve available statuses. * * @since 1.9.5 * * @return array */ private function get_statuses(): array { return [ self::STATUS_VALID, self::STATUS_INVALID ]; } /** * Retrieve a connection in array format, simply like `toArray` method. * * @since 1.9.5 * * @return array */ private function get_data(): array { return [ 'live_mode' => $this->live_mode, 'access_token' => $this->access_token, 'refresh_token' => $this->refresh_token, 'client_id' => $this->client_id, 'merchant_id' => $this->merchant_id, 'currency' => $this->currency, 'status' => $this->status, 'renew_at' => $this->renew_at, 'scopes_updated' => $this->scopes_updated, ]; } /** * Determine whether connection tokens is encrypted. * * @since 1.9.5 * * @return bool */ private function is_encrypted(): bool { return $this->encrypted; } /** * Determine whether a connection is configured fully. * * @since 1.9.5 * * @return bool */ public function is_configured(): bool { return ! empty( $this->get_access_token() ) && ! empty( $this->get_refresh_token() ) && ! empty( $this->client_id ) && ! empty( $this->merchant_id ); } /** * Determine whether a connection is expired. * * @since 1.9.5 * * @return bool */ public function is_expired(): bool { return ( $this->renew_at - time() ) < HOUR_IN_SECONDS; } /** * Determine whether a connection currency is matched with WPForms currency. * * @since 1.9.5 * * @return bool */ public function is_currency_matched(): bool { return $this->currency === strtoupper( wpforms_get_currency() ); } /** * Determine whether a connection is valid. * * @since 1.9.5 * * @return bool */ public function is_valid(): bool { return $this->get_status() === self::STATUS_VALID; } /** * Determine whether a connection is ready for save. * * @since 1.9.5 * * @return bool */ public function is_saveable(): bool { return $this->is_configured() && ! $this->is_expired() && $this->is_encrypted(); } /** * Determine whether a connection is ready for use. * * @since 1.9.5 * * @return bool */ public function is_usable(): bool { return $this->is_configured() && $this->is_valid() && $this->is_currency_matched(); } } Integrations/Square/Api/WebhookRoute.php 0000644 00000017253 15174710275 0014332 0 ustar 00 <?php namespace WPForms\Integrations\Square\Api; use Exception; use RuntimeException; use BadMethodCallException; use WPForms\Integrations\Square\Helpers; use WPForms\Integrations\Square\WebhooksHealthCheck; /** * Webhooks Rest Route handler. * * @since 1.9.5 */ class WebhookRoute { /** * Event type. * * @since 1.9.5 * * @var string */ private $event_type = 'unknown'; /** * Payload. * * @since 1.9.5 * * @var array */ private $payload = []; /** * Response. * * @since 1.9.5 * * @var string */ private $response = ''; /** * Response code. * * @since 1.9.5 * * @var int */ private $response_code = 200; /** * Initialize. * * @since 1.9.5 */ public function init() { $this->hooks(); } /** * Register hooks. * * @since 1.9.5 */ private function hooks() { if ( $this->is_rest_verification() ) { add_action( 'rest_api_init', [ $this, 'register_rest_routes' ] ); return; } // Do not serve regular page when it seems Square Webhooks are still sending requests to disabled CURL endpoint. // phpcs:disable WordPress.Security.NonceVerification.Recommended if ( isset( $_GET[ Helpers::get_webhook_endpoint_data()['fallback'] ] ) && ( ! Helpers::is_webhook_enabled() || Helpers::is_rest_api_set() ) ) { add_action( 'wp', [ $this, 'dispatch_with_error_500' ] ); return; } // Check if Square is configured. if ( ! Helpers::is_square_configured() ) { return; } // phpcs:enable WordPress.Security.NonceVerification.Recommended if ( ! Helpers::is_webhook_enabled() || ! Helpers::is_webhook_configured() ) { return; } if ( Helpers::is_rest_api_set() ) { add_action( 'rest_api_init', [ $this, 'register_rest_routes' ] ); return; } add_action( 'wp', [ $this, 'dispatch_with_url_param' ] ); } /** * Register webhook REST route. * * @since 1.9.5 */ public function register_rest_routes() { $methods = [ 'POST' ]; if ( $this->is_rest_verification() ) { $methods[] = 'GET'; } register_rest_route( Helpers::get_webhook_endpoint_data()['namespace'], '/' . Helpers::get_webhook_endpoint_data()['route'], [ 'methods' => $methods, 'callback' => [ $this, 'dispatch_square_webhooks_payload' ], 'show_in_index' => false, 'permission_callback' => '__return_true', ] ); } /** * Dispatch Square webhooks payload for the url param. * * @since 1.9.5 */ public function dispatch_with_url_param() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( ! isset( $_GET[ Helpers::get_webhook_endpoint_data()['fallback'] ] ) ) { return; } $this->dispatch_square_webhooks_payload(); } /** * Dispatch Square webhooks payload for the url param with error 500. * * Runs when url param is not configured or webhooks are not enabled at all. * * @since 1.9.5 */ public function dispatch_with_error_500() { $this->response = esc_html__( 'It seems to be request to Square PHP Listener method handler but the site is not configured to use it.', 'wpforms-lite' ); $this->response_code = 500; $this->respond(); } /** * Dispatch Square webhooks' payload. * * @since 1.9.5 * * @throws RuntimeException When Square signature is not set. */ public function dispatch_square_webhooks_payload() { if ( $this->is_rest_verification() ) { wp_send_json_success(); } try { // Get raw payload and signature. $this->payload = file_get_contents( 'php://input' ); // Construct event. $event = WebhookEvent::construct_event( $this->payload, $this->get_webhook_signature(), $this->get_webhook_signing_secret() ); $event_whitelist = self::get_webhooks_events_list(); if ( ! in_array( $event->type, $event_whitelist, true ) ) { throw new RuntimeException( 'Square event type is not whitelisted.' ); } // Update webhook site health status. WebhooksHealthCheck::save_status( WebhooksHealthCheck::ENDPOINT_OPTION, WebhooksHealthCheck::STATUS_OK ); $this->event_type = $event->type; $this->response = 'WPForms Square: ' . $this->event_type . ' event received.'; $processed = $this->process_event( $event ); $this->response_code = $processed ? 200 : 202; $this->respond(); } catch ( Exception $e ) { $this->response = $e->getMessage(); $this->response_code = $e instanceof BadMethodCallException ? 501 : 500; $this->respond(); } } /** * Get webhook signature. * * @since 1.9.5 * * @throws RuntimeException When Square signature is not set. * * @return string */ private function get_webhook_signature(): string { if ( ! isset( $_SERVER['HTTP_X_SQUARE_HMACSHA256_SIGNATURE'] ) ) { throw new RuntimeException( 'Square signature is not set.' ); } return sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_SQUARE_HMACSHA256_SIGNATURE'] ) ); } /** * Get webhook signing secret. * * @since 1.9.5 * * @throws RuntimeException When webhook signing secret is not set. * * @return string */ private function get_webhook_signing_secret(): string { $secret = wpforms_setting( 'square-webhooks-secret-' . Helpers::get_mode() ); if ( empty( $secret ) ) { throw new RuntimeException( 'Webhook signing secret is not set.' ); } return $secret; } /** * Process Square event. * * @since 1.9.5 * * @param object $event Square event. * * @return bool True if event has handling class, false otherwise. */ private function process_event( $event ): bool { $webhooks = self::get_event_whitelist(); // Event can't be handled. if ( ! isset( $webhooks[ $event->type ] ) || ! class_exists( $webhooks[ $event->type ] ) ) { return false; } $handler = new $webhooks[ $event->type ](); $handler->setup( $event ); return $handler->handle(); } /** * Get event allowlist. * * @since 1.9.5 * * @return array */ private static function get_event_whitelist(): array { return [ 'refund.updated' => Webhooks\RefundUpdated::class, 'payment.updated' => Webhooks\PaymentUpdated::class, 'payment.created' => Webhooks\PaymentCreated::class, 'subscription.created' => Webhooks\SubscriptionCreated::class, 'subscription.updated' => Webhooks\SubscriptionUpdated::class, ]; } /** * Check if rest verification is requested. * * @since 1.9.5 * * @return bool */ private function is_rest_verification(): bool { // phpcs:ignore WordPress.Security.NonceVerification.Recommended return isset( $_GET['verify'] ) && $_GET['verify'] === '1'; } /** * Respond to the request. * * @since 1.9.5 */ private function respond() { $this->log_webhook(); wp_die( esc_html( $this->response ), '', (int) $this->response_code ); } /** * Log webhook request. * * @since 1.9.5 */ private function log_webhook() { // log only if WP_DEBUG_LOG and WPFORMS_WEBHOOKS_DEBUG are set to true. if ( ! defined( 'WPFORMS_WEBHOOKS_DEBUG' ) || ! WPFORMS_WEBHOOKS_DEBUG || ! defined( 'WP_DEBUG_LOG' ) || ! WP_DEBUG_LOG ) { return; } // If it is set to explicitly display logs on output, return: this would make response to Square malformed. if ( defined( 'WP_DEBUG_DISPLAY' ) && WP_DEBUG_DISPLAY ) { return; } $webhook_log = maybe_serialize( [ 'event_type' => $this->event_type, 'response_code' => $this->response_code, 'response' => $this->response, 'payload' => $this->payload, ] ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log error_log( $webhook_log ); } /** * Get a webhook events list. * * @since 1.9.5 * * @return array */ public static function get_webhooks_events_list(): array { return array_keys( self::get_event_whitelist() ); } } Integrations/Square/Api/WebhooksManager.php 0000644 00000013307 15174710275 0014765 0 ustar 00 <?php namespace WPForms\Integrations\Square\Api; use Exception; use WPForms\Vendor\Square\SquareClient; use WPForms\Integrations\Square\Helpers; use WPForms\Integrations\Square\WebhooksHealthCheck; use WPForms\Vendor\Square\Models\WebhookSubscription; use WPForms\Vendor\Square\Models\CreateWebhookSubscriptionRequest; use WPForms\Vendor\Square\Models\UpdateWebhookSubscriptionRequest; /** * Webhooks Manager. * * @since 1.9.5 */ class WebhooksManager { /** * Square client. * * @since 1.9.5 * * @var SquareClient */ private $client; /** * Create webhook endpoint. * Retrieve the existing one when the endpoint already exists. * * @since 1.9.5 */ public function connect() { // Security and permissions check. if ( ! check_ajax_referer( 'wpforms-admin', 'nonce', false ) || ! wpforms_current_user_can() ) { wp_send_json_error( [ 'message ' => esc_html__( 'You are not allowed to perform this action', 'wpforms-lite' ) ] ); } $personal_access_token = ! empty( $_POST['token'] ) ? sanitize_text_field( wp_unslash( $_POST['token'] ) ) : ''; if ( empty( $personal_access_token ) ) { wp_send_json_error( [ 'message' => esc_html__( 'Personal access token is required.', 'wpforms-lite' ) ] ); } $webhook = $this->create( $personal_access_token ); // Register AS task. ( new WebhooksHealthCheck() )->maybe_schedule_task(); // Store endpoint ID and secret. if ( ! empty( $webhook ) ) { $this->save_settings( $webhook ); wp_send_json_success( [ 'message' => esc_html__( 'Webhook created successfully!', 'wpforms-lite' ) ] ); } wp_send_json_error( [ 'message' => esc_html__( 'Failed to create webhook.', 'wpforms-lite' ) ] ); } /** * Create or update a webhook endpoint. * * @since 1.9.5 * * @param string $personal_access_token Personal access token. * * @return array Endpoint ID and secret. */ private function create( string $personal_access_token ): array { $this->client = new SquareClient( [ 'accessToken' => $personal_access_token, 'environment' => Helpers::get_mode(), ] ); // Check if the webhook already exists. $existing_webhook = $this->webhook_exists(); // Prepare a webhook subscription object. $webhook_subscription = new WebhookSubscription(); $webhook_subscription->setName( sprintf( 'WPForms endpoint (%1$s mode)', Helpers::get_mode() ) ); $webhook_subscription->setNotificationUrl( Helpers::get_webhook_url() ); $webhook_subscription->setEventTypes( WebhookRoute::get_webhooks_events_list() ); $webhooks_api = $this->client->getWebhookSubscriptionsApi(); if ( $existing_webhook ) { try { // Create an update request and set the subscription payload. $request = new UpdateWebhookSubscriptionRequest(); $request->setSubscription( $webhook_subscription ); // Update the existing webhook subscription. $response = $webhooks_api->updateWebhookSubscription( $existing_webhook['id'], $request ); if ( $response->isSuccess() ) { $subscription = $response->getResult()->getSubscription(); return [ 'id' => $subscription->getId(), 'signature_key' => $existing_webhook['signature_key'] ?? '', // getSignatureKey() isn't available in the update response, fall back to the existing webhook's signature key. ]; } // If the update fails, return the existing webhook details. return $existing_webhook; } catch ( Exception $e ) { return $existing_webhook; } } // Create a new webhook subscription if none exists. $request = new CreateWebhookSubscriptionRequest( $webhook_subscription ); $request->setIdempotencyKey( uniqid() ); try { // Create the webhook subscription. $response = $webhooks_api->createWebhookSubscription( $request ); if ( $response->isSuccess() ) { $subscription = $response->getResult()->getSubscription(); return [ 'id' => $subscription->getId(), 'signature_key' => $subscription->getSignatureKey(), ]; } return []; } catch ( Exception $e ) { return []; } } /** * Check if webhook already exists. * * @since 1.9.5 * * @return array */ private function webhook_exists(): array { try { $response = $this->client->getWebhookSubscriptionsApi()->listWebhookSubscriptions(); if ( ! $response->isSuccess() || empty( $response->getResult()->getSubscriptions() ) ) { return []; } foreach ( $response->getResult()->getSubscriptions() as $subscription ) { if ( $subscription->getNotificationUrl() !== Helpers::get_webhook_url() ) { continue; } $signature = $this->client->getWebhookSubscriptionsApi()->retrieveWebhookSubscription( $subscription->getId() ); return $signature->isSuccess() ? [ 'id' => $signature->getResult()->getSubscription()->getId(), 'signature_key' => $signature->getResult()->getSubscription()->getSignatureKey(), ] : []; } } catch ( Exception $e ) { return []; } return []; } /** * Save webhook settings. * * @since 1.9.5 * * @param array $webhook Webhook endpoint. */ private function save_settings( array $webhook ) { $mode = Helpers::get_mode(); $settings = (array) get_option( 'wpforms_settings', [] ); // Save webhooks endpoint ID and secret. $settings[ 'square-webhooks-id-' . $mode ] = sanitize_text_field( $webhook['id'] ); $settings[ 'square-webhooks-secret-' . $mode ] = sanitize_text_field( $webhook['signature_key'] ); WebhooksHealthCheck::save_status( WebhooksHealthCheck::ENDPOINT_OPTION, WebhooksHealthCheck::STATUS_OK ); // Enable webhooks setting shouldn't be rewritten. if ( empty( $settings['square-webhooks-enabled'] ) ) { $settings['square-webhooks-enabled'] = true; } update_option( 'wpforms_settings', $settings ); } } Integrations/Square/Api/Api.php 0000644 00000127454 15174710275 0012433 0 ustar 00 <?php namespace WPForms\Integrations\Square\Api; use stdClass; use WPForms\Integrations\Square\Helpers; use WPForms\Vendor\Square\Models\Invoice; use WPForms\Vendor\Square\Exceptions\ApiException; use WPForms\Vendor\Square\Http\ApiResponse; use WPForms\Vendor\Square\Models\Address; use WPForms\Vendor\Square\Models\Card; use WPForms\Vendor\Square\Models\CatalogObject; use WPForms\Vendor\Square\Models\CatalogObjectType; use WPForms\Vendor\Square\Models\CatalogQuery; use WPForms\Vendor\Square\Models\CatalogQueryExact; use WPForms\Vendor\Square\Models\CatalogSubscriptionPlan; use WPForms\Vendor\Square\Models\UpdateSubscriptionResponse; use WPForms\Vendor\Square\Models\CatalogSubscriptionPlanVariation; use WPForms\Vendor\Square\Models\CreateCardRequest; use WPForms\Vendor\Square\Models\CreateCustomerRequest; use WPForms\Vendor\Square\Models\CreateOrderRequest; use WPForms\Vendor\Square\Models\CreatePaymentRequest; use WPForms\Vendor\Square\Models\CreatePaymentResponse; use WPForms\Vendor\Square\Models\CreateSubscriptionRequest; use WPForms\Vendor\Square\Models\CreateSubscriptionResponse; use WPForms\Vendor\Square\Models\Money; use WPForms\Vendor\Square\Models\Order; use WPForms\Vendor\Square\Models\OrderLineItem; use WPForms\Vendor\Square\Models\OrderLineItemDiscount; use WPForms\Vendor\Square\Models\OrderSource; use WPForms\Vendor\Square\Models\OrderState; use WPForms\Vendor\Square\Models\Payment; use WPForms\Vendor\Square\Models\RefundPaymentRequest; use WPForms\Vendor\Square\Models\SearchCatalogObjectsRequest; use WPForms\Vendor\Square\Models\Subscription; use WPForms\Vendor\Square\Models\SubscriptionPhase; use WPForms\Vendor\Square\Models\SubscriptionPricing; use WPForms\Vendor\Square\Models\SubscriptionSource; use WPForms\Vendor\Square\Models\UpdateOrderRequest; use WPForms\Vendor\Square\Models\UpdateSubscriptionRequest; use WPForms\Vendor\Square\Models\UpsertCatalogObjectRequest; use WPForms\Vendor\Square\Models\Customer; use WPForms\Vendor\Square\SquareClient; use WPForms\Integrations\Square\Connection; use WPForms\Integrations\Square\Square; /** * WPForms Square API class. * * @since 1.9.5 */ class Api { /** * Square API client instance. * * @since 1.9.5 * * @var SquareClient */ private $client; /** * Payment token (card nonce) generated by the Web Payments SDK. * * @since 1.9.5 * * @var string */ private $source_id; /** * Last API call response. * * @since 1.9.5 * * @var ApiResponse */ private $response; /** * Last API call exception. * * @since 1.9.5 * * @var ApiException */ private $exception; /** * API errors. * * @since 1.9.5 * * @var array */ private $errors; /** * Constructs the main Square API wrapper class. * * @since 1.9.5 * * @param Connection $connection Connection object. */ public function __construct( $connection ) { $this->client = new SquareClient( [ 'accessToken' => $connection->get_access_token(), 'environment' => $connection->get_mode(), ] ); } /** * Set tokens from a submitted form data. * * @since 1.9.5 * * @param array $entry Copy of original $_POST. */ public function set_payment_tokens( array $entry ) { if ( ! empty( $entry['square']['source_id'] ) ) { $this->source_id = $entry['square']['source_id']; } } /** * Check if OAuth connection is present and ready to use. * * @since 1.9.5 */ private function check_connection() { $connection = Connection::get(); if ( ! $connection || ! $connection->is_configured() ) { $this->errors[] = esc_html__( 'Square account connection is missing.', 'wpforms-lite' ); return; } if ( ! $connection->is_valid() ) { $this->errors[] = esc_html__( 'Square account connection is invalid.', 'wpforms-lite' ); return; } if ( ! $connection->is_currency_matched() ) { $this->errors[] = esc_html__( 'The currency associated with the payment is not valid for the provided business location.', 'wpforms-lite' ); } } /** * Check if single payment tokens are present. * * @since 1.9.5 */ private function check_payment_tokens() { if ( empty( $this->source_id ) ) { $this->errors[] = esc_html__( 'Square payment stopped, missing card tokens.', 'wpforms-lite' ); } } /** * Check if all required general arguments are present. * * @since 1.9.5 * * @param array $args Arguments to check. */ private function check_required_args_general( array $args ) { if ( empty( $args['location_id'] ) ) { $this->errors[] = esc_html__( 'Missing location ID.', 'wpforms-lite' ); } if ( empty( $args['currency'] ) ) { $this->errors[] = esc_html__( 'Missing currency.', 'wpforms-lite' ); } if ( empty( $args['amount'] ) && ! is_numeric( $args['amount'] ) ) { $this->errors[] = esc_html__( 'Missing amount.', 'wpforms-lite' ); } } /** * Check if all required single payment arguments are present. * * @since 1.9.5 * * @param array $args Arguments to check. */ private function check_required_args_single( array $args ) { if ( empty( $args['order_items'] ) ) { $this->errors[] = esc_html__( 'Missing order/payment items.', 'wpforms-lite' ); } } /** * Process single transaction. * * @since 1.9.5 * * @param array $args Payment arguments. */ public function process_single_transaction( array $args ) { $this->check_connection(); $this->check_payment_tokens(); $this->check_required_args_general( $args ); $this->check_required_args_single( $args ); if ( $this->errors ) { return; } $result = $this->perform_single_transaction( $args ); /** * Fire when a single transaction is performed. * * @since 1.9.5 * * @param array $result Single transaction result. * @param array $args Payment arguments. * @param Api $api Api class instance. */ do_action( 'wpforms_square_api_process_single_transaction_after', $result, $args, $this ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName } /** * Process subscription transaction. * * @since 1.9.5 * * @param array $args Payment arguments. */ public function process_subscription_transaction( array $args ) { $this->check_connection(); $this->check_payment_tokens(); $this->check_required_args_general( $args ); $this->check_required_args_subscription( $args ); if ( $this->errors ) { return; } $this->perform_subscription_transaction( $args ); } /** * Refund payment. * * @since 1.9.5 * * @param string $payment_id Payment ID. * @param array $args Payment data. */ public function refund_payment( string $payment_id, array $args ): bool { try { $request = new RefundPaymentRequest( $this->get_idempotency_key(), new Money() ); $request->setPaymentId( $payment_id ); $request->setReason( $args['reason'] ); $request->getAmountMoney()->setAmount( $args['amount'] ); $request->getAmountMoney()->setCurrency( $args['currency'] ); $this->response = $this->client->getRefundsApi()->refundPayment( $request ); if ( ! $this->response->isSuccess() ) { return false; } } catch ( ApiException $e ) { $this->exception = $e; return false; } return true; } /** * Update subscription. * * @since 1.9.5 * * @param array $args Subscription arguments. */ public function update_subscription( array $args ) { $subscription = $this->retrieve_subscription( $args['id'] ); if ( ! $subscription instanceof Subscription ) { return; } $request = $this->get_update_subscription_request_object( $subscription, $args ); $this->send_update_subscription_request( $args['id'], $request ); } /** * Cancel subscription. * * @since 1.9.5 * * @param string $subscription_id Subscription id. * * @return bool */ public function cancel_subscription( string $subscription_id ): bool { try { $this->response = $this->client->getSubscriptionsApi()->cancelSubscription( $subscription_id ); if ( ! $this->response->isSuccess() ) { return false; } } catch ( ApiException $e ) { $this->exception = $e; return false; } return true; } /** * Retrieve a Card object. * * @since 1.9.5 * * @param Subscription $subscription Subscription object. * * @return Card|null */ public function get_subscription_card( $subscription ) { if ( ! $subscription instanceof Subscription ) { return null; } $card_id = $subscription->getCardId(); if ( empty( $card_id ) ) { return null; } // Get a customer. $card = $this->send_retrieve_card_request( $card_id ); if ( ! $card instanceof Card ) { return null; } return $card; } /** * Send a retrieve subscription request to API. * * @since 1.9.5 * * @param string $id The ID of the subscription to retrieve. * * @return Subscription|null */ public function retrieve_subscription( string $id ) { try { $response = $this->client->getSubscriptionsApi()->retrieveSubscription( $id ); if ( ! $response->isSuccess() ) { return null; } return $response->getResult()->getSubscription(); } catch ( ApiException $e ) { return null; } } /** * Retrieve a Card object. * * @since 1.9.5 * * @param Subscription $subscription Subscription object. * * @return Invoice|null */ public function get_latest_subscription_invoice( $subscription ) { if ( ! $subscription instanceof Subscription ) { return null; } try { $invoices = $subscription->getInvoiceIds(); if ( empty( $invoices ) ) { return null; } // Get a customer. $response = $this->client->getInvoicesApi()->getInvoice( reset( $invoices ) ); // Get the latest invoice by using the first ID in the array as the subscription's invoice IDs are sorted by date in ascending order. if ( ! $response->isSuccess() ) { return null; } return $response->getResult()->getInvoice(); } catch ( ApiException $e ) { return null; } } /** * Perform a single transaction. * * @since 1.9.5 * * @param array $args Payment arguments. * * @return array */ private function perform_single_transaction( array $args ): array { $result = []; // Create an order. $order_request = $this->prepare_create_order_request( $args ); $order = $this->send_create_order_request( $order_request ); if ( ! $order instanceof Order ) { return $result; } $result['order'] = $order; // Create a payment. $payment_request = $this->prepare_create_payment_request( $order, $args ); $payment = $this->send_create_payment_request( $payment_request ); // In this case we should cancel an order. if ( ! $payment instanceof Payment ) { $update_order_request = $this->prepare_update_order_request( $order ); $updated_order = $this->send_update_order_request( $order->getId(), $update_order_request ); return $updated_order instanceof Order ? [ 'order' => $updated_order ] : $result; } $result['payment'] = $payment; return $result; } /** * Prepare a create order request object for sending to API. * * @since 1.9.5 * * @param array $args Single payment arguments. * * @return CreateOrderRequest */ private function prepare_create_order_request( array $args ): CreateOrderRequest { $request = $this->get_create_order_request_object( $args ); $request = $this->create_order_request_set_line_items( $request, $args ); $request = $this->create_order_request_set_discounts( $request, $args ); /** * Filter a create order request object. * * @since 1.9.5 * * @param CreateOrderRequest $request Create order request object. * @param array $args Payment arguments. * @param Api $api Api class instance. */ return apply_filters( 'wpforms_square_api_prepare_create_order_request', $request, $args, $this ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName } /** * Prepare an update order request object for sending to API. * * @since 1.9.5 * * @param Order $order Order object. * * @return UpdateOrderRequest */ private function prepare_update_order_request( $order ): UpdateOrderRequest { $request = $this->get_update_order_request_object( $order ); /** * Filter an update order request object. * * @since 1.9.5 * * @param UpdateOrderRequest $request Update order request object. * @param Order $order Order object. * @param Api $api Api class instance. */ return apply_filters( 'wpforms_square_api_prepare_update_order_request', $request, $order, $this ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName } /** * Prepare a create payment request object for sending to API. * * @since 1.9.5 * * @param Order $order Order object. * @param array $args Payment arguments. * * @return CreatePaymentRequest */ private function prepare_create_payment_request( $order, array $args ): CreatePaymentRequest { $request = $this->get_create_payment_request_object(); $request = $this->create_payment_request_set_order( $request, $order ); if ( ! empty( $args['billing'] ) ) { $address = $this->get_address_object( $args['billing'] ); $request->setBillingAddress( $address ); } if ( ! empty( $args['buyer_email'] ) ) { $request->setBuyerEmailAddress( $args['buyer_email'] ); } if ( ! empty( $args['note'] ) ) { $request->setNote( $args['note'] ); } /** * Filter a create payment request object. * * @since 1.9.5 * * @param CreateOrderRequest $request Create payment request object. * @param array $args Payment arguments. * @param Api $api Api class instance. */ return apply_filters( 'wpforms_square_api_prepare_create_payment_request', $request, $args, $this ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName } /** * Retrieve a create order request object. * * @since 1.9.5 * * @param array $args Payment arguments. * * @return CreateOrderRequest */ private function get_create_order_request_object( array $args ): CreateOrderRequest { $request = new CreateOrderRequest(); $request->setIdempotencyKey( $this->get_idempotency_key() ); $request->setOrder( new Order( $args['location_id'] ) ); $request->getOrder()->setSource( new OrderSource() ); $request->getOrder()->getSource()->setName( Square::APP_NAME ); return $request; } /** * Retrieve an update order request object. * * @since 1.9.5 * * @param Order $order Order object. * * @return UpdateOrderRequest */ private function get_update_order_request_object( $order ): UpdateOrderRequest { $request = new UpdateOrderRequest(); $request->setIdempotencyKey( $this->get_idempotency_key() ); $request->setOrder( $order ); $request->getOrder()->setState( OrderState::CANCELED ); return $request; } /** * Retrieve a create payment request object. * * @since 1.9.5 * * @return CreatePaymentRequest */ private function get_create_payment_request_object(): CreatePaymentRequest { $request = new CreatePaymentRequest( $this->source_id, $this->get_idempotency_key() ); $request->setAmountMoney( new Money() ); return $request; } /** * Send a create order request object to API. * * @since 1.9.5 * * @param CreateOrderRequest $request Create order request object. * * @return Order|null */ private function send_create_order_request( $request ) { try { $this->response = $this->client->getOrdersApi()->createOrder( $request ); if ( ! $this->response->isSuccess() ) { $this->errors[] = esc_html__( 'Square fail: order was not created.', 'wpforms-lite' ); return null; } return $this->response->getResult()->getOrder(); } catch ( ApiException $e ) { $this->exception = $e; $this->errors[] = esc_html__( 'Square fail: order was not created.', 'wpforms-lite' ); return null; } } /** * Send an update order request object to API. * * @since 1.9.5 * * @param string $order_id The ID of the order to update. * @param UpdateOrderRequest $request Update order request object. * * @return Order|null */ private function send_update_order_request( string $order_id, $request ) { try { $response = $this->client->getOrdersApi()->updateOrder( $order_id, $request ); if ( ! $response->isSuccess() ) { return null; } return $response->getResult()->getOrder(); } catch ( ApiException $e ) { return null; } } /** * Send a create payment request to API. * * @since 1.9.5 * * @param CreatePaymentRequest $request Create payment request object. * * @return Payment|null */ private function send_create_payment_request( $request ) { try { $this->response = $this->client->getPaymentsApi()->createPayment( $request ); if ( ! $this->response->isSuccess() ) { $this->errors[] = esc_html__( 'Square fail: payment was not processed.', 'wpforms-lite' ); $this->add_response_errors_message(); return null; } return $this->response->getResult()->getPayment(); } catch ( ApiException $e ) { $this->exception = $e; $this->errors[] = esc_html__( 'Square fail: payment was not processed.', 'wpforms-lite' ); $this->add_response_errors_message( 'Exception' ); return null; } } /** * Set line items to a create order request object. * * @since 1.9.5 * * @param CreateOrderRequest $request Request to create an order. * @param array $args Payment arguments. * * @return CreateOrderRequest */ private function create_order_request_set_line_items( $request, array $args ): CreateOrderRequest { $line_items = []; foreach ( (array) $args['order_items'] as $item ) { // Order item without variations. if ( empty( $item['variations'] ) ) { $line_items[] = $this->get_order_line_item_object( $item, $args ); continue; } // Order item with variations (e.g. Small, Medium and Large). foreach ( (array) $item['variations'] as $variation ) { $order_line_item = $this->get_order_line_item_object( $variation, $args ); $order_line_item->setVariationName( $variation['variation_name'] ); $line_items[] = $order_line_item; } } $request->getOrder()->setLineItems( $line_items ); return $request; } /** * Set discounts to a create order request object. * * @since 1.9.5 * * @param CreateOrderRequest $request Request to create an order. * @param array $args Payment arguments. * * @return CreateOrderRequest */ private function create_order_request_set_discounts( $request, array $args ): CreateOrderRequest { $discounts = []; if ( empty( $args['discounts'] ) ) { return $request; } foreach ( (array) $args['discounts'] as $discount ) { $discounts[] = $this->get_order_discount_object( $discount, $args ); } if ( ! empty( $discounts ) ) { $request->getOrder()->setDiscounts( $discounts ); } return $request; } /** * Set order to a create payment request object. * * @since 1.9.5 * * @param CreatePaymentRequest $request Create payment request object. * @param Order $order Order object. * * @return CreatePaymentRequest */ private function create_payment_request_set_order( $request, $order ): CreatePaymentRequest { $request->setOrderId( $order->getId() ); $request->setLocationId( $order->getLocationId() ); $amount = $order->getTotalMoney()->getAmount(); $currency = $order->getTotalMoney()->getCurrency(); $request->getAmountMoney()->setAmount( $amount ); $request->getAmountMoney()->setCurrency( $currency ); if ( ! Helpers::is_license_ok() && Helpers::is_application_fee_supported( $currency ) ) { $request->setAppFeeMoney( new Money() ); $request->getAppFeeMoney()->setAmount( (int) ( round( $amount * 0.03 ) ) ); $request->getAppFeeMoney()->setCurrency( $currency ); } return $request; } /** * Retrieve a single order line item object. * * @since 1.9.5 * * @param array $item Order item data. * @param array $args Payment arguments. * * @return OrderLineItem */ private function get_order_line_item_object( array $item, array $args ): OrderLineItem { $order_line_items = new OrderLineItem( $item['quantity'] ); $order_line_items->setName( $item['name'] ); $order_line_items->setBasePriceMoney( new Money() ); // Round to the nearest whole number because $item['amount'] can contain a number close to, // but slightly under it, due to how it is stored in the memory. $order_line_items->getBasePriceMoney()->setAmount( round( $item['amount'] ) ); $order_line_items->getBasePriceMoney()->setCurrency( $args['currency'] ); return $order_line_items; } /** * Retrieve a single order discount object. * * @since 1.9.5 * * @param array $discount Discount data. * @param array $args Payment arguments. * * @return OrderLineItemDiscount */ private function get_order_discount_object( array $discount, array $args ): OrderLineItemDiscount { $order_discounts = new OrderLineItemDiscount(); $order_discounts->setName( $discount['name'] ); $order_discounts->setAmountMoney( new Money() ); $order_discounts->getAmountMoney()->setAmount( $discount['amount'] ); $order_discounts->getAmountMoney()->setCurrency( $args['currency'] ); return $order_discounts; } /** * Prepare and retrieve an address object. * * @since 1.9.5 * * @param array $data Address data. * * @return Address */ private function get_address_object( array $data ): Address { $address = new Address(); // The empty country value may occur API errors. if ( ! empty( $data['address']['country'] ) ) { $address->setAddressLine1( $data['address']['address1'] ); $address->setAddressLine2( $data['address']['address2'] ); $address->setLocality( $data['address']['city'] ); $address->setAdministrativeDistrictLevel1( $data['address']['state'] ); $address->setPostalCode( $data['address']['postal'] ); $address->setCountry( $data['address']['country'] ); } if ( ! empty( $data['first_name'] ) ) { $address->setFirstName( $data['first_name'] ); } if ( ! empty( $data['last_name'] ) ) { $address->setLastName( $data['last_name'] ); } return $address; } /** * Retrieve information of all locations of a business. * * @since 1.9.5 * * @return array|Location|null */ public function get_locations() { try { $this->response = $this->client->getLocationsApi()->listLocations(); if ( ! $this->response->isSuccess() ) { return null; } return $this->response->getResult()->getLocations(); } catch ( ApiException $e ) { $this->exception = $e; return null; } } /** * Retrieve a Merchant object with details. * * @since 1.9.5 * * @param string $id The Merchant ID. * * @return Merchant|null */ public function get_merchant( string $id ) { try { $this->response = $this->client->getMerchantsApi()->retrieveMerchant( $id ); if ( ! $this->response->isSuccess() ) { return null; } return $this->response->getResult()->getMerchant(); } catch ( ApiException $e ) { $this->exception = $e; return null; } } /** * Retrieve an idempotency key. * * @since 1.9.5 * * @link https://developer.squareup.com/docs/working-with-apis/idempotency * * @return string */ private function get_idempotency_key(): string { return uniqid( '', false ); } /** * Retrieve API errors. * * @since 1.9.5 * * @return array|null */ public function get_errors() { return $this->errors; } /** * Retrieve last API call errors if are exist. * * @since 1.9.5 * * @return array */ public function get_response_errors(): array { if ( $this->response instanceof ApiResponse && ! $this->response->isSuccess() ) { $errors = []; foreach ( (array) $this->response->getErrors() as $error ) { $errors[] = $error->jsonSerialize(); } return $errors; } if ( $this->exception instanceof ApiException ) { return [ 'code' => $this->exception->getCode(), 'message' => $this->exception->getMessage(), ]; } return []; } /** * Retrieve last API call response resource. * * @since 1.9.5 * * @return array */ public function get_response_resource(): array { if ( ! $this->response instanceof ApiResponse ) { return []; } $result = $this->response->getResult(); if ( $result instanceof CreatePaymentResponse ) { return [ 'payment' => $this->response->getResult()->getPayment() ]; } if ( $result instanceof CreateSubscriptionResponse || $result instanceof UpdateSubscriptionResponse ) { return [ 'subscription' => $this->response->getResult()->getSubscription() ]; } return []; } /** * Retrieve last API call response and display error messages. * * @since 1.14.0 * * @param string $type Type of error message. */ private function add_response_errors_message( string $type = 'API' ) { $errors = $this->get_response_errors(); if ( empty( $errors ) ) { return; } $key = ( $type === 'Exception' ) ? 'message' : 'detail'; foreach ( $errors as $error ) { $message = $error[ $key ] ?? ''; if ( empty( $error['code'] ) || empty( $message ) ) { return; } $this->errors[] = esc_html( $type ) . ': (' . esc_html( $error['code'] ) . ') ' . esc_html( $message ); } } /** * Perform a subscription transaction. * * @since 1.9.5 * * @param array $args Payment arguments. */ private function perform_subscription_transaction( array $args ) { // Create a customer. $customer_request = $this->prepare_create_customer_request( $args ); $customer = $this->send_create_customer_request( $customer_request ); if ( ! $customer instanceof Customer ) { return; } // Create a customer card. $card_request = $this->prepare_create_customer_card_request( $customer->getId(), $args ); $card = $this->send_create_customer_card_request( $card_request ); if ( ! $card instanceof Card ) { return; } // Get a subscription plan. $plan = $this->get_plan( $args ); if ( ! $plan instanceof CatalogObject ) { return; } // Get a subscription plan variation. $plan_variation = $this->get_plan_variation( $args, $plan ); if ( ! $plan_variation instanceof CatalogObject ) { return; } // Create a subscription. $subscription_request = $this->prepare_create_subscription_request( $plan_variation->getId(), $customer->getId(), $card->getId(), $args ); $this->send_create_subscription_request( $subscription_request ); } /** * Check if all required subscription arguments are present. * * @since 1.9.5 * * @param array $args Arguments to check. */ private function check_required_args_subscription( array $args ) { if ( empty( $args['subscription']['plan_name'] ) ) { $this->errors[] = esc_html__( 'Missing subscription plan name.', 'wpforms-lite' ); } if ( empty( $args['subscription']['plan_variation_name'] ) ) { $this->errors[] = esc_html__( 'Missing subscription plan variation name.', 'wpforms-lite' ); } if ( empty( $args['subscription']['phase_cadence'] ) ) { $this->errors[] = esc_html__( 'Missing subscription cadence.', 'wpforms-lite' ); } if ( empty( $args['subscription']['customer']['first_name'] ) && empty( $args['subscription']['customer']['last_name'] ) ) { $this->errors[] = esc_html__( 'Missing customer name.', 'wpforms-lite' ); } if ( empty( $args['subscription']['customer']['email'] ) ) { $this->errors[] = esc_html__( 'Missing customer email.', 'wpforms-lite' ); } } /** * Prepare a create customer request object for sending to API. * * @since 1.9.5 * * @param array $args Payment arguments. * * @return CreateCustomerRequest */ private function prepare_create_customer_request( array $args ): CreateCustomerRequest { $request = $this->get_create_customer_request_object(); $request->setEmailAddress( $args['subscription']['customer']['email'] ); if ( ! empty( $args['subscription']['customer']['first_name'] ) ) { $request->setGivenName( $args['subscription']['customer']['first_name'] ); } if ( ! empty( $args['subscription']['customer']['last_name'] ) ) { $request->setFamilyName( $args['subscription']['customer']['last_name'] ); } if ( ! empty( $args['subscription']['customer']['address'] ) ) { $address = $this->get_address_object( $args['subscription']['customer'] ); $request->setAddress( $address ); } return $request; } /** * Prepare a create customer card request object for sending to API. * * @since 1.9.5 * * @param string $customer_id Customer ID. * @param array $args Payment arguments. * * @return CreateCardRequest */ private function prepare_create_customer_card_request( string $customer_id, array $args ): CreateCardRequest { $request = new CreateCardRequest( $this->get_idempotency_key(), $this->source_id, new Card() ); $request->getCard()->setCustomerId( $customer_id ); if ( ! empty( $args['subscription']['customer']['address'] ) ) { $address = $this->get_address_object( $args['subscription']['customer'] ); // For subscriptions: make sure that a postal code is not empty. // Otherwise, API error "The postal code doesn't match the one used for card nonce creation" is occur here. if ( ! empty( $address->getPostalCode() ) ) { $request->getCard()->setBillingAddress( $address ); } } if ( ! empty( $args['subscription']['card_name'] ) ) { $request->getCard()->setCardholderName( $args['subscription']['card_name'] ); } return $request; } /** * Prepare a create subscription request object for sending to API. * * @since 1.9.5 * * @param string $plan_id Plan ID. * @param string $customer_id Customer ID. * @param string $card_id Customer ID. * @param array $args Payment arguments. * * @return CreateSubscriptionRequest */ private function prepare_create_subscription_request( string $plan_id, string $customer_id, string $card_id, array $args ): CreateSubscriptionRequest { $request = $this->get_create_subscription_request_object( $plan_id, $customer_id, $args ); $request->setCardId( $card_id ); $request->setSource( new SubscriptionSource() ); $request->getSource()->setName( Square::APP_NAME ); return $request; } /** * Retrieve a create customer request object. * * @since 1.9.5 * * @return CreateCustomerRequest */ private function get_create_customer_request_object(): CreateCustomerRequest { $request = new CreateCustomerRequest(); $request->setIdempotencyKey( $this->get_idempotency_key() ); return $request; } /** * Retrieve a search catalog request object. * * @since 1.9.5 * * @param string $type Object type. * @param string $name Object name. * * @return SearchCatalogObjectsRequest */ private function get_search_catalog_request_object( string $type, string $name ): SearchCatalogObjectsRequest { $request = new SearchCatalogObjectsRequest(); $request->setObjectTypes( [ $type ] ); $request->setLimit( 1 ); $request->setQuery( new CatalogQuery() ); $request->getQuery()->setExactQuery( new CatalogQueryExact( 'name', $name ) ); return $request; } /** * Retrieve a create plan request object. * * @since 1.9.5 * * @param array $args Payment arguments. * * @return UpsertCatalogObjectRequest */ private function get_create_plan_request_object( array $args ): UpsertCatalogObjectRequest { $request = new UpsertCatalogObjectRequest( $this->get_idempotency_key(), new CatalogObject( CatalogObjectType::SUBSCRIPTION_PLAN, '#plan' ) ); $plan_data = new CatalogSubscriptionPlan( $args['subscription']['plan_name'] ); $plan_data->setAllItems( true ); $request->getObject()->setSubscriptionPlanData( $plan_data ); return $request; } /** * Get subscription plan variation. * * @since 1.9.5 * * @param array $args Payment arguments. * @param CatalogObject $plan Plan object. * * @return CatalogObject|null */ private function get_plan_variation( array $args, $plan ) { // Search a subscription plan. $search_plan_request = $this->get_search_catalog_request_object( CatalogObjectType::SUBSCRIPTION_PLAN_VARIATION, $args['subscription']['plan_name'] ); $plan_variation = $this->send_search_catalog_request( $search_plan_request ); // Create a subscription plan if it's not exists. if ( $plan_variation !== null ) { return $plan_variation; } $plan_variations_request = $this->get_create_plan_variations_request_object( $args, $plan ); $plan_variation = $this->send_create_plan_variations_request( $plan_variations_request ); if ( ! $plan_variation instanceof CatalogObject ) { return null; } return $plan_variation; } /** * Retrieve a create plan variation request object. * * @since 1.9.5 * * @param array $args Payment arguments. * @param CatalogObject $plan Plan object. * * @return UpsertCatalogObjectRequest */ private function get_create_plan_variations_request_object( array $args, $plan ): UpsertCatalogObjectRequest { $request = new UpsertCatalogObjectRequest( $this->get_idempotency_key(), new CatalogObject( CatalogObjectType::SUBSCRIPTION_PLAN_VARIATION, '#plan_variation' ) ); $phase = new SubscriptionPhase( $args['subscription']['phase_cadence']['value'] ); $phase->setPricing( new SubscriptionPricing() ); $phase->getPricing()->setType( 'STATIC' ); $phase->getPricing()->setPriceMoney( new Money() ); $phase->getPricing()->getPriceMoney()->setAmount( $args['amount'] ); $phase->getPricing()->getPriceMoney()->setCurrency( $args['currency'] ); $request->getObject()->setSubscriptionPlanVariationData( new CatalogSubscriptionPlanVariation( $args['subscription']['plan_variation_name'], [ $phase ] ) ); $request->getObject()->getSubscriptionPlanVariationData()->setSubscriptionPlanId( $plan->getId() ); return $request; } /** * Retrieve a create subscription request object. * * @since 1.9.5 * * @param string $plan_id Plan ID. * @param string $customer_id Customer ID. * @param array $args Payment arguments. * * @return CreateSubscriptionRequest */ private function get_create_subscription_request_object( string $plan_id, string $customer_id, array $args ): CreateSubscriptionRequest { $request = new CreateSubscriptionRequest( $args['location_id'], $customer_id ); $request->setIdempotencyKey( $this->get_idempotency_key() ); $request->setPlanVariationId( $plan_id ); return $request; } /** * Send a create customer request to API. * * @since 1.9.5 * * @param CreateCustomerRequest $request Create customer request object. * * @return Customer|null */ private function send_create_customer_request( $request ) { try { $this->response = $this->client->getCustomersApi()->createCustomer( $request ); if ( ! $this->response->isSuccess() ) { $this->errors[] = esc_html__( 'Square fail: customer was not created.', 'wpforms-lite' ); return null; } return $this->response->getResult()->getCustomer(); } catch ( ApiException $e ) { $this->exception = $e; $this->errors[] = esc_html__( 'Square fail: customer was not created.', 'wpforms-lite' ); return null; } } /** * Send a retrieve card request to API. * * @since 1.9.5 * * @param string $id The ID of the customer to retrieve. * * @return Card|null */ private function send_retrieve_card_request( string $id ) { try { $response = $this->client->getCardsApi()->retrieveCard( $id ); if ( ! $response->isSuccess() ) { return null; } return $response->getResult()->getCard(); } catch ( ApiException $e ) { return null; } } /** * Send a create customer card request to API. * * @since 1.9.5 * * @param CreateCardRequest $request Create card request object. * * @return Card|null */ private function send_create_customer_card_request( $request ) { try { $this->response = $this->client->getCardsApi()->createCard( $request ); if ( ! $this->response->isSuccess() ) { $this->errors[] = esc_html__( 'Square fail: customer card was not created.', 'wpforms-lite' ); return null; } return $this->response->getResult()->getCard(); } catch ( ApiException $e ) { $this->exception = $e; $this->errors[] = esc_html__( 'Square fail: customer card was not created.', 'wpforms-lite' ); return null; } } /** * Send a search catalog request to API. * * @since 1.9.5 * * @param SearchCatalogObjectsRequest $request Search subscription plan request object. * * @return CatalogObject|null */ private function send_search_catalog_request( $request ) { try { $this->response = $this->client->getCatalogApi()->searchCatalogObjects( $request ); if ( ! $this->response->isSuccess() ) { return null; } $objects = $this->response->getResult()->getObjects(); if ( ! is_array( $objects ) || empty( $objects[0] ) ) { return null; } return $objects[0]; } catch ( ApiException $e ) { $this->exception = $e; return null; } } /** * Get subscription plan. * * @since 1.9.5 * * @param array $args Payment arguments. * * @return CatalogObject|null */ private function get_plan( array $args ) { // Search a subscription plan. $search_plan_request = $this->get_search_catalog_request_object( CatalogObjectType::SUBSCRIPTION_PLAN, $args['subscription']['plan_variation_name'] ); $plan = $this->send_search_catalog_request( $search_plan_request ); // Create a subscription plan if it's not exists. if ( $plan === null ) { $create_plan_request = $this->get_create_plan_request_object( $args ); $plan = $this->send_create_plan_request( $create_plan_request ); } if ( ! $plan instanceof CatalogObject ) { return null; } return $plan; } /** * Send a create subscription plan request to API. * * @since 1.9.5 * * @param UpsertCatalogObjectRequest $request Create subscription plan request object. * * @return CatalogObject|null */ private function send_create_plan_request( $request ) { try { $this->response = $this->client->getCatalogApi()->upsertCatalogObject( $request ); if ( ! $this->response->isSuccess() ) { $this->errors[] = esc_html__( 'Square fail: subscription plan was not created.', 'wpforms-lite' ); return null; } return $this->response->getResult()->getCatalogObject(); } catch ( ApiException $e ) { $this->exception = $e; $this->errors[] = esc_html__( 'Square fail: subscription plan was not created.', 'wpforms-lite' ); return null; } } /** * Send a create subscription plan variations request to API. * * @since 1.9.5 * * @param UpsertCatalogObjectRequest $request Create subscription plan request object. * * @return CatalogObject|null */ private function send_create_plan_variations_request( $request ) { try { $this->response = $this->client->getCatalogApi()->upsertCatalogObject( $request ); if ( ! $this->response->isSuccess() ) { $this->errors[] = esc_html__( 'Square fail: subscription plan variation was not created.', 'wpforms-lite' ); return null; } return $this->response->getResult()->getCatalogObject(); } catch ( ApiException $e ) { $this->exception = $e; $this->errors[] = esc_html__( 'Square fail: subscription plan variation was not created.', 'wpforms-lite' ); return null; } } /** * Send a create subscription request to API. * * @since 1.9.5 * * @param CreateSubscriptionRequest $request Create subscription request object. * * @return Subscription|null */ private function send_create_subscription_request( $request ) { try { $this->response = $this->client->getSubscriptionsApi()->createSubscription( $request ); if ( ! $this->response->isSuccess() ) { $this->errors[] = esc_html__( 'Square fail: something went wrong during subscription process.', 'wpforms-lite' ); return null; } return $this->response->getResult()->getSubscription(); } catch ( ApiException $e ) { $this->exception = $e; $this->errors[] = esc_html__( 'Square fail: something went wrong during subscription process.', 'wpforms-lite' ); return null; } } /** * Retrieve a update subscription request object. * * @since 1.9.5 * * @param Subscription $subscription Subscription object. * @param array $args Subscription arguments. * * @return UpdateSubscriptionRequest|null */ private function get_update_subscription_request_object( $subscription, array $args ) { if ( ! $subscription instanceof Subscription ) { return null; } $subscription->setSource( new SubscriptionSource() ); $subscription->getSource()->setName( Square::APP_NAME . ' Payment #' . $args['payment_id'] ); $request = new UpdateSubscriptionRequest(); $request->setSubscription( $subscription ); return $request; } /** * Send a create subscription request to API. * * @since 1.9.5 * * @param string $subscription_id Subscription id. * @param UpdateSubscriptionRequest $request Update subscription request object. * * @return Subscription|null */ private function send_update_subscription_request( string $subscription_id, $request ) { try { $this->response = $this->client->getSubscriptionsApi()->updateSubscription( $subscription_id, $request ); if ( ! $this->response->isSuccess() ) { $this->errors[] = esc_html__( 'Square fail: something went wrong during subscription update.', 'wpforms-lite' ); return null; } return $this->response->getResult()->getSubscription(); } catch ( ApiException $e ) { $this->exception = $e; $this->errors[] = esc_html__( 'Square fail: something went wrong during subscription update.', 'wpforms-lite' ); return null; } } /** * Retrieve card details from specific transaction in object format. * * @since 1.9.5 * * @param string $transaction_id The ID of the order to retrieve card details from. * * @return stdClass|void */ public function get_card_details_from_transaction_id( string $transaction_id ) { $response = $this->client->getPaymentsApi()->getPayment( $transaction_id ); if ( ! $response->isSuccess() ) { return; } $payment = $response->getResult()->getPayment(); $card_details = $payment->getCardDetails(); if ( ! $card_details ) { return; } $card = $card_details->getCard(); if ( ! $card ) { return; } // Create a temporary object to mimic Square's payment_method_details structure. $details = new stdClass(); $details->source_type = 'card'; $details->card_details = new stdClass(); $details->card_details->card = new stdClass(); $details->card_details->card->last_4 = $card->getLast4(); $details->card_details->card->card_brand = $card->getCardBrand(); $details->card_details->card->exp_month = $card->getExpMonth(); $details->card_details->card->exp_year = $card->getExpYear(); return $details; } /** * Retrieve the latest invoice transaction_id. * * @since 1.9.5 * * @param Invoice $invoice Invoice object. * * @return string */ public function get_latest_invoice_transaction_id( $invoice ): string { $order_id = $invoice->getOrderId(); if ( ! $order_id ) { return ''; } $order_response = $this->client->getOrdersApi()->retrieveOrder( $order_id ); if ( ! $order_response->isSuccess() ) { return ''; } $order = $order_response->getResult()->getOrder(); $tenders = $order->getTenders(); if ( empty( $tenders ) ) { return ''; } // Assuming the last tender represents the final transaction. return end( $tenders )->getPaymentId(); } /** * Retrieve the invoice by order ID. * * @since 1.9.5 * * @param string $order_id Order ID. * * @return Invoice|null */ public function get_invoice_by_order_id( string $order_id ) { $invoices_api = $this->client->getInvoicesApi(); try { $response = $invoices_api->listInvoices( Helpers::get_location_id() ); if ( ! $response->isSuccess() ) { return null; } $invoices = $response->getResult()->getInvoices(); if ( empty( $invoices ) ) { return null; } foreach ( $invoices as $invoice ) { if ( $invoice->getOrderId() === $order_id ) { return $invoice; } } } catch ( ApiException $e ) { return null; } return null; } } Integrations/Square/Api/Webhooks/SubscriptionCreated.php 0000644 00000001511 15174710275 0017440 0 ustar 00 <?php namespace WPForms\Integrations\Square\Api\Webhooks; use RuntimeException; /** * Webhook subscription.created class. * * @since 1.9.5 */ class SubscriptionCreated extends Base { /** * Set subscription status to active. * * @since 1.9.5 * * @return bool * * @throws RuntimeException If payment isn't found or not updated. */ public function handle(): bool { $payment = wpforms()->obj( 'payment' )->get_by( 'subscription_id', $this->data->object->subscription->id ); if ( ! $payment ) { return false; } if ( ! wpforms()->obj( 'payment' )->update( $payment->id, [ 'subscription_status' => 'active' ] ) ) { throw new RuntimeException( 'Payment not updated' ); } wpforms()->obj( 'payment_meta' )->add_log( $payment->id, 'Square subscription was set to active.' ); return true; } } Integrations/Square/Api/Webhooks/SubscriptionUpdated.php 0000644 00000002732 15174710275 0017465 0 ustar 00 <?php namespace WPForms\Integrations\Square\Api\Webhooks; use RuntimeException; use WPForms\Db\Payments\UpdateHelpers; /** * Webhook subscription.updated class. * * @since 1.9.5 */ class SubscriptionUpdated extends Base { /** * Update the subscription status. * * @since 1.9.5 * * @return bool * * @throws RuntimeException If payment isn't found or not updated. */ public function handle(): bool { $payment = wpforms()->obj( 'payment' )->get_by( 'subscription_id', $this->data->object->subscription->id ); if ( ! $payment ) { return false; } // Track canceled subscriptions. if ( isset( $this->data->object->subscription->canceled_date ) ) { if ( ! UpdateHelpers::cancel_subscription( $payment->id, 'Square subscription cancelled from the Square dashboard.' ) ) { throw new RuntimeException( 'Subscription cancellation was not updated.' ); } return true; } $status = strtolower( $this->data->object->subscription->status ); // Return true if the subscription status is the same as the status in the webhook data. if ( $payment->subscription_status === $status ) { return true; } // Update subscription status. if ( ! wpforms()->obj( 'payment' )->update( $payment->id, [ 'subscription_status' => $status ] ) ) { throw new RuntimeException( 'Payment not updated' ); } wpforms()->obj( 'payment_meta' )->add_log( $payment->id, sprintf( 'Square subscription was set to %1$s.', $status ) ); return true; } } Integrations/Square/Api/Webhooks/RefundUpdated.php 0000644 00000005337 15174710275 0016230 0 ustar 00 <?php namespace WPForms\Integrations\Square\Api\Webhooks; use RuntimeException; use WPForms\Db\Payments\UpdateHelpers; /** * Webhook refund.updated class. * * @since 1.9.5 */ class RefundUpdated extends Base { /** * Handle the Webhook's data. * * Save refunded amount in the payment meta with key refunded_amount. * Update payment status to 'partrefund' or 'refunded' if refunded amount is equal to the total amount. * * @since 1.9.5 * * @throws RuntimeException If payment isn't updated. * * @return bool */ public function handle(): bool { $this->set_payment(); if ( $this->db_payment === null ) { throw new RuntimeException( 'Refund Update Event: Payment has not been found and set.' ); } // Perform refund only if it's allowed. if ( ! $this->is_refund_allowed() ) { return false; } $currency = strtoupper( $this->data->object->refund->amount_money->currency ); $decimals_amount = wpforms_get_currency_multiplier( $currency ); // We need to format the amount since it doesn't contain decimals, e.g., 525 instead of 5.25. $refunded_amount = ( $decimals_amount !== 0 ) ? ( $this->data->object->refund->amount_money->amount / $decimals_amount ) : 0; $refunded_amount_formatted = wpforms_format_amount( $refunded_amount, true, $currency ); $log = sprintf( 'Square payment refunded from the Square dashboard. Refunded amount: %1$s.', $refunded_amount_formatted ); $total_amount_refund = wpforms()->obj( 'payment_meta' )->get_single( $this->db_payment->id, 'total_refunded_amount' ); if ( empty( $total_amount_refund ) ) { $total_amount_refund = $refunded_amount; } if ( ! UpdateHelpers::refund_payment( $this->db_payment, $total_amount_refund, $log ) ) { /* translators: %s - transaction id. */ $log = sprintf( __( 'Payment for transaction %s was not updated.', 'wpforms-lite' ), $this->db_payment->transaction_id ); throw new RuntimeException( esc_html( $log ) ); } return true; } /** * Check if refund is allowed. * * @since 1.9.5 * * @return bool */ private function is_refund_allowed(): bool { if ( ! $this->db_payment ) { return false; } // Do not track uncompleted refunds. if ( $this->data->object->refund->status !== 'COMPLETED' ) { return false; } // Do not track refunds that were not requested by the customer. if ( ! empty( $this->data->object->refund->reason ) && $this->data->object->refund->reason !== 'Requested by customer' ) { return false; } // Square sends two webhooks for a refund with the same COMPLETED statuses, // but the final is the one with the fee included. if ( ! isset( $this->data->object->refund->processing_fee ) ) { return false; } return true; } } Integrations/Square/Api/Webhooks/PaymentUpdated.php 0000644 00000011631 15174710275 0016414 0 ustar 00 <?php namespace WPForms\Integrations\Square\Api\Webhooks; use RuntimeException; use WPForms\Db\Payments\Queries; /** * Webhook payment.updated class. * Set the status to 'completed' if payment is paid. * * @since 1.9.5 */ class PaymentUpdated extends Base { /** * Invoice object. * * @since 1.9.5 * * @var Invoice|null */ private $invoice; /** * Handle the Webhook's data. * * @since 1.9.5 * * @throws RuntimeException If payment isn't found or not updated. * * @return bool */ public function handle(): bool { $order_id = $this->data->object->payment->order_id ?? ''; $this->invoice = $this->api->get_invoice_by_order_id( $order_id ); $subscription_id = $this->invoice ? $this->invoice->getSubscriptionId() : ''; // If a subscription ID exists, process the subscription-specific logic. if ( $subscription_id ) { $this->update_subscription_payment( $subscription_id ); } $this->set_payment(); if ( $this->db_payment === null ) { throw new RuntimeException( 'Payment Update Event: Payment has not been found and set.' ); } // Update payment method details to keep them up to date. if ( isset( $this->data->object->payment ) && $this->data->object->payment !== null ) { $this->update_payment_method_details( $this->db_payment->id, $this->data->object->payment ); } // Update total refunded amount if set. if ( ! empty( $this->data->object->payment->refunded_money ) ) { $this->update_total_refund( $this->db_payment->id, $this->data->object->payment->refunded_money ); } if ( $this->db_payment->status !== 'processed' || $this->data->object->payment->status !== 'COMPLETED' ) { return false; } $currency = strtoupper( $this->data->object->payment->total_money->currency ); $db_amount = wpforms_format_amount( $this->db_payment->total_amount ); $amount = wpforms_format_amount( $this->data->object->payment->total_money->amount / wpforms_get_currency_multiplier( $currency ) ); if ( $amount !== $db_amount ) { return false; } $updated_payment = wpforms()->obj( 'payment' )->update( $this->db_payment->id, [ 'status' => 'completed', 'date_updated_gmt' => gmdate( 'Y-m-d H:i:s' ), ] ); if ( ! $updated_payment ) { throw new RuntimeException( 'Payment not updated' ); } wpforms()->obj( 'payment_meta' )->add_log( $this->db_payment->id, 'Square payment was completed.' ); return true; } /** * Update subscription payment. * * @since 1.9.5 * * @param string $subscription_id Subscription ID. * * @return bool */ private function update_subscription_payment( string $subscription_id ): bool { // If this is the first invoice in the subscription, do not create a renewal. if ( $this->is_initial_invoice_for_subscription( $subscription_id ) ) { return false; } // Retrieve the renewal record from the database. $db_renewal = ( new Queries() )->get_renewal_by_invoice_id( $this->invoice->getId() ); if ( is_null( $db_renewal ) ) { return false; // The newest renewal not found. } // Check if the renewal payment is already completed. if ( $db_renewal->status === 'completed' ) { return true; } // Retrieve the payment requests from the invoice. $payment_requests = $this->invoice->getPaymentRequests(); if ( empty( $payment_requests ) ) { return false; } // Use the first payment request to get the final paid amount. $total_completed = $payment_requests[0]->getTotalCompletedAmountMoney(); $currency = strtoupper( $total_completed->getCurrency() ); $amount = $total_completed->getAmount() / wpforms_get_currency_multiplier( $currency ); // Retrieve the transaction ID using the subscription ID. $transaction_id = $this->get_latest_subscription_transaction_id( $subscription_id ); if ( empty( $transaction_id ) ) { $transaction_id = ''; } // Update the renewal payment with the final amount and transaction ID. wpforms()->obj( 'payment' )->update( $db_renewal->id, [ 'total_amount' => $amount, 'subtotal_amount' => $amount, 'status' => 'completed', 'transaction_id' => $transaction_id, ] ); // Copy additional meta data from the transaction details. $this->copy_meta_from_transaction_details( (int) $db_renewal->id, $transaction_id ); wpforms()->obj( 'payment_meta' )->add_log( $db_renewal->id, sprintf( 'Square renewal was successfully paid. (Payment ID: %1$s)', $transaction_id ) ); return true; } /** * Copy meta from transaction. * * @since 1.9.5 * * @param int $renewal_id Renewal ID. * @param string $transaction_id Transaction ID. */ private function copy_meta_from_transaction_details( int $renewal_id, string $transaction_id ) { $card_details = $this->api->get_card_details_from_transaction_id( $transaction_id ); if ( ! $card_details ) { return; } $this->update_payment_method_details( $renewal_id, $card_details ); } } Integrations/Square/Api/Webhooks/PaymentCreated.php 0000644 00000013071 15174710275 0016375 0 ustar 00 <?php namespace WPForms\Integrations\Square\Api\Webhooks; use RuntimeException; use WPForms\Db\Payments\Queries; /** * Webhook payment.created class. * * @since 1.9.5 */ class PaymentCreated extends Base { /** * Invoice object. * * @since 1.9.5 * * @var Invoice|null */ private $invoice; /** * Set the transaction ID. * Create renewal payment. * * @since 1.9.5 * * @return bool * * @throws RuntimeException If subscription ID or order ID is missing. */ public function handle(): bool { $order_id = $this->data->object->payment->order_id ?? ''; $this->invoice = $this->api->get_invoice_by_order_id( $order_id ); // Ensure the invoice was retrieved. if ( ! $this->invoice ) { throw new RuntimeException( 'Invoice not found for order ID: ' . esc_html( $order_id ) ); } $subscription_id = $this->invoice->getSubscriptionId(); if ( ! $subscription_id ) { throw new RuntimeException( 'Missing subscription ID in payment.created event.' ); } $original_subscription = ( new Queries() )->get_subscription( $subscription_id ); if ( is_null( $original_subscription ) ) { return false; // Original subscription not found. } $payment = wpforms()->obj( 'payment' )->get_by( 'subscription_id', $subscription_id ); if ( ! $payment ) { return false; } $this->set_transaction_id( $payment ); // If this is the first invoice in the subscription, we don't want to create a renewal. if ( $this->is_initial_invoice_for_subscription( $subscription_id ) ) { return false; } $renewal = ( new Queries() )->get_renewal_by_invoice_id( $this->invoice->getId() ); if ( ! is_null( $renewal ) ) { return false; // Renewal already exists. } $renewal_id = $this->insert_renewal( $original_subscription ); if ( ! $renewal_id ) { throw new RuntimeException( 'Subscription renewal not saved in database' ); } $this->insert_renewal_meta( $renewal_id, $original_subscription ); wpforms()->obj( 'payment_meta' )->add_log( $renewal_id, sprintf( 'Square renewal was created (Invoice ID: %1$s).', $this->invoice->getId() ) ); return true; } /** * Insert renewal. * * @since 1.9.5 * * @param object $original_subscription Original subscription. * * @return int|false */ private function insert_renewal( $original_subscription ) { // Retrieve payment requests from the invoice. $payment_requests = $this->invoice->getPaymentRequests(); if ( empty( $payment_requests ) ) { return false; } // Use the first payment request. $first_payment_request = $payment_requests[0]; $computed_amount_money = $first_payment_request->getComputedAmountMoney(); $currency = strtoupper( $computed_amount_money->getCurrency() ); $amount = $computed_amount_money->getAmount() / wpforms_get_currency_multiplier( $currency ); return wpforms()->obj( 'payment' )->add( [ 'mode' => $original_subscription->mode, 'form_id' => $original_subscription->form_id ?? 0, 'entry_id' => $original_subscription->entry_id ?? 0, 'status' => 'pending', 'type' => 'renewal', 'gateway' => 'square', 'title' => $original_subscription->title, 'subtotal_amount' => $amount, 'total_amount' => $amount, 'currency' => $currency, 'transaction_id' => '', 'subscription_id' => $original_subscription->subscription_id, 'customer_id' => $original_subscription->customer_id, 'date_created_gmt' => gmdate( 'Y-m-d H:i:s', strtotime( $this->invoice->getCreatedAt() ) ), 'date_updated_gmt' => gmdate( 'Y-m-d H:i:s' ), ] ); } /** * Insert renewal meta. * * @since 1.9.5 * * @param int $renewal_id Renewal ID. * @param object $original_subscription Original subscription. */ private function insert_renewal_meta( int $renewal_id, $original_subscription ) { $meta = $this->copy_meta_from_db( $original_subscription->id ); $meta['invoice_id'] = $this->invoice->getId(); $meta['customer_email'] = $this->invoice->getPrimaryRecipient()->getEmailAddress() ?? ''; wpforms()->obj( 'payment_meta' )->bulk_add( $renewal_id, $meta ); } /** * Copy meta from the original subscription. * * @since 1.9.5 * * @param int $original_subscription_id Original subscription ID. * * @return array */ private function copy_meta_from_db( int $original_subscription_id ): array { $all_meta = wpforms()->obj( 'payment_meta' )->get_all( $original_subscription_id ); $db_meta_keys = [ 'fields', 'subscription_period', 'coupon_value', 'coupon_info', 'coupon_id', ]; $meta = []; foreach ( $db_meta_keys as $key ) { if ( isset( $all_meta[ $key ]->value ) ) { $meta[ $key ] = $all_meta[ $key ]->value; } } return $meta; } /** * Set the transaction ID for the initial payment. * * @since 1.9.5 * * @param object $payment Payment object. * * @return bool * * @throws RuntimeException If subscription ID or order ID is missing. */ private function set_transaction_id( $payment ): bool { $subscription_id = $this->invoice->getSubscriptionId(); $transaction_id = $this->get_latest_subscription_transaction_id( $subscription_id ); if ( ! $transaction_id ) { return false; } wpforms()->obj( 'payment' )->update( $payment->id, [ 'transaction_id' => $transaction_id, ] ); wpforms()->obj( 'payment_meta' )->add_log( $payment->id, sprintf( 'Square subscription was created. (Invoice ID: %s)', $this->invoice->getId() ) ); return true; } } Integrations/Square/Api/Webhooks/Base.php 0000644 00000010564 15174710275 0014346 0 ustar 00 <?php namespace WPForms\Integrations\Square\Api\Webhooks; use RuntimeException; use WPForms\Integrations\Square\Api\Api; use WPForms\Integrations\Square\Connection; /** * Webhook base class. * * @since 1.9.5 */ abstract class Base { /** * Event type. * * @since 1.9.5 * * @var string */ protected $type; /** * Event data from a Square object. * * @since 1.9.5 * * @var object */ protected $data; /** * Payment object. * * @since 1.9.5 * * @var object */ protected $db_payment; /** * Main class that communicates with the Square API. * * @since 1.9.5 * * @var Api */ protected $api; /** * Webhook setup. * * @since 1.9.5 * * @param object $event Webhook event object. * * @throws RuntimeException When Square connection is not available. */ public function setup( $event ) { $this->data = $event->data; $this->type = $event->type; if ( ! Connection::get() ) { throw new RuntimeException( 'Square connection is not available.' ); } $this->api = new Api( Connection::get() ); $this->hooks(); } /** * Register hooks. * * @since 1.9.5 */ private function hooks() { add_filter( 'wpforms_current_user_can', '__return_true' ); } /** * Handle the Webhook's data. * * @since 1.9.5 * * return bool */ abstract public function handle(); /** * Set payment object. * * Set payment object from a database. If payment is not registered yet in DB, throw exception. * * @since 1.9.5 */ protected function set_payment() { $transaction_id = $this->data->object->payment->id ?? ''; if ( $this->type === 'refund.updated' ) { $transaction_id = $this->data->object->refund->payment_id; } $this->db_payment = wpforms()->obj( 'payment' )->get_by( 'transaction_id', $transaction_id ); } /** * Update payment method details. * * @since 1.9.5 * * @param int $payment_id Payment ID. * @param object $details Charge details. */ protected function update_payment_method_details( int $payment_id, $details ) { $meta['method_type'] = ! empty( $details->source_type ) ? sanitize_text_field( $details->source_type ) : ''; if ( ! empty( $details->card_details->card->last_4 ) ) { $meta['credit_card_last4'] = $details->card_details->card->last_4; $meta['credit_card_method'] = $details->card_details->card->card_brand; $meta['credit_card_expires'] = $details->card_details->card->exp_month . '/' . $details->card_details->card->exp_year; } $payment_meta_obj = wpforms()->obj( 'payment_meta' ); if ( ! $payment_meta_obj ) { return; } $payment_meta_obj->bulk_add( $payment_id, $meta ); } /** * Update total refunded amount. * * @since 1.9.5 * * @param int $payment_id Payment ID. * @param object $refund_details Refund details. */ protected function update_total_refund( int $payment_id, $refund_details ) { $decimals_amount = wpforms_get_currency_multiplier( $refund_details->currency ); $total_refunded_amount = ( $decimals_amount !== 0 ) ? ( $refund_details->amount / $decimals_amount ) : 0; if ( ! $total_refunded_amount ) { return; } wpforms()->obj( 'payment_meta' )->update_or_add( $payment_id, 'total_refunded_amount', $total_refunded_amount ); } /** * Get latest transaction ID from subscription. * * @since 1.9.5 * * @param string $subscription_id Subscription ID. * * @return string */ protected function get_latest_subscription_transaction_id( string $subscription_id ): string { $subscription = $this->api->retrieve_subscription( $subscription_id ); if ( ! $subscription ) { return ''; } $invoice = $this->api->get_latest_subscription_invoice( $subscription ); if ( ! $invoice ) { return ''; } $transaction_id = $this->api->get_latest_invoice_transaction_id( $invoice ); if ( ! $transaction_id ) { return ''; } return $transaction_id; } /** * Check if the invoice is initial for the subscription. * * @since 1.9.5 * * @param string $subscription_id Subscription ID. * * @return bool */ protected function is_initial_invoice_for_subscription( string $subscription_id ): bool { $subscription = $this->api->retrieve_subscription( $subscription_id ); if ( ! $subscription ) { return false; } $invoices = $subscription->getInvoiceIds(); if ( empty( $invoices ) ) { return false; } return count( $invoices ) <= 1; } } Integrations/Square/Api/WebhookEvent.php 0000644 00000002633 15174710275 0014311 0 ustar 00 <?php namespace WPForms\Integrations\Square\Api; use RuntimeException; use WPForms\Integrations\Square\Helpers; use WPForms\Vendor\Square\Utils\WebhooksHelper; /** * Webhook event handler. * * @since 1.9.5 */ class WebhookEvent { /** * Construct and validate the Square webhook event. * * @since 1.9.5 * * @param string $payload The raw JSON payload from Square. * @param string $signature The Square webhook signature from headers. * @param string $webhook_secret The webhook signing secret from Square Developer Dashboard. * * @return object The decoded event data. * * @throws RuntimeException If the webhook payload structure is invalid. */ public static function construct_event( string $payload, string $signature, string $webhook_secret ) { // Validate the webhook signature. if ( ! WebhooksHelper::isValidWebhookEventSignature( $payload, $signature, $webhook_secret, Helpers::get_webhook_url() ) ) { throw new RuntimeException( 'Invalid webhook signature. Possible unauthorized request.' ); } // Decode JSON payload. $event = json_decode( $payload, false ); // Check for JSON decoding errors. if ( json_last_error() !== JSON_ERROR_NONE ) { throw new RuntimeException( 'Invalid JSON payload' ); } if ( ! $event || ! isset( $event->type, $event->data ) ) { throw new RuntimeException( 'Invalid webhook payload structure.' ); } return $event; } } Integrations/Square/Helpers.php 0000644 00000027504 15174710275 0012606 0 ustar 00 <?php namespace WPForms\Integrations\Square; use WPForms\Vendor\Square\Environment; use WPForms\Vendor\Square\Models\SubscriptionCadence; use WPForms\Helpers\Transient; /** * Square related helper methods. * * @since 1.9.5 */ class Helpers { /** * Determine whether the addon is activated and appropriate license is set. * * @since 1.9.5 * * @return bool */ public static function is_pro(): bool { return self::is_addon_active() && self::is_allowed_license_type(); } /** * Determine whether the addon is activated. * * @since 1.9.5 * * @return bool */ public static function is_addon_active(): bool { return wpforms_is_addon_initialized( 'square' ); } /** * Determine whether a license is ok. * * @since 1.9.5 * * @return bool */ public static function is_license_ok(): bool { return self::is_license_active() && self::is_allowed_license_type(); } /** * Determine whether a license type is allowed. * * @since 1.9.5 * * @return bool */ public static function is_allowed_license_type(): bool { return in_array( wpforms_get_license_type(), [ 'pro', 'elite', 'agency', 'ultimate' ], true ); } /** * Determine whether a license key is active. * * @since 1.9.5 * * @return bool */ public static function is_license_active(): bool { $license = (array) get_option( 'wpforms_license', [] ); return ! empty( wpforms_get_license_key() ) && empty( $license['is_expired'] ) && empty( $license['is_disabled'] ) && empty( $license['is_invalid'] ); } /** * Determine if Square single payment is enabled for the form. * * @since 1.9.5 * * @param array $form_data Form data and settings. * * @return bool */ private static function is_square_single_enabled( array $form_data ): bool { return ! empty( $form_data['payments']['square']['enable'] ) || ! empty( $form_data['payments']['square']['enable_one_time'] ); } /** * Determine if Square recurring payment is enabled for the form. * * @since 1.9.5 * * @param array $form_data Form data and settings. * * @return bool */ public static function is_square_recurring_enabled( array $form_data ): bool { return ! empty( $form_data['payments']['square']['enable_recurring'] ); } /** * Determine if Square payment enabled for the form. * * @since 1.9.5 * * @param array $form_data Form data. * * @return bool */ public static function is_payments_enabled( array $form_data ): bool { return self::is_square_single_enabled( $form_data ) || self::is_square_recurring_enabled( $form_data ); } /** * Determine if Square is in use on the page. * * @since 1.9.5 * * @param array $forms Forms data (e.g. forms on a current page). * * @return bool */ public static function has_square_enabled( array $forms ): bool { foreach ( $forms as $form_data ) { if ( self::is_payments_enabled( $form_data ) ) { return true; } } return false; } /** * Determine if Square field is in the form. * * @since 1.9.5 * * @param array $forms Form data (e.g. forms on a current page). * @param bool $multiple Must be 'true' if $forms contain multiple forms. * * @return bool */ public static function has_square_field( array $forms, bool $multiple = false ): bool { return wpforms_has_field_type( 'square', $forms, $multiple ); } /** * Determine whether Square is in sandbox mode. * * @since 1.9.5 * * @return bool */ public static function is_sandbox_mode(): bool { return self::get_mode() === Environment::SANDBOX; } /** * Determine whether Square is in production mode. * * @since 1.9.5 * * @return bool */ public static function is_production_mode(): bool { return self::get_mode() === Environment::PRODUCTION; } /** * Retrieve Square mode from WPForms settings. * * @since 1.9.5 * * @return string */ public static function get_mode(): string { return wpforms_setting( 'square-sandbox-mode' ) ? Environment::SANDBOX : Environment::PRODUCTION; } /** * Set/update Square mode from WPForms settings. * * @since 1.9.5 * * @param string $mode Square mode that will be set. * * @return bool */ public static function set_mode( string $mode ): bool { $key = 'square-sandbox-mode'; $settings = (array) get_option( 'wpforms_settings', [] ); $settings[ $key ] = $mode === Environment::SANDBOX; return update_option( 'wpforms_settings', $settings ); } /** * Retrieve Square Business Location ID from WPForms settings. * * @since 1.9.5 * * @param string $mode Square mode. * * @return string */ public static function get_location_id( string $mode = '' ): string { $mode = self::validate_mode( $mode ); return wpforms_setting( 'square-location-id-' . $mode, '' ); } /** * Set/update Square Business Location ID from WPForms settings. * * @since 1.9.5 * * @param string $id The location ID. * @param string $mode Square mode. * * @return bool */ public static function set_locataion_id( string $id, string $mode ): bool { $mode = self::validate_mode( $mode ); $key = 'square-location-id-' . $mode; $settings = (array) get_option( 'wpforms_settings', [] ); $settings[ $key ] = $id; return update_option( 'wpforms_settings', $settings ); } /** * Delete transients by mode. * * @since 1.9.5 * * @param string $mode Square mode. */ public static function detete_transients( string $mode ) { Transient::delete( 'wpforms_square_account_' . $mode ); Transient::delete( 'wpforms_square_active_locations_' . $mode ); } /** * Retrieve Square available modes. * * @since 1.9.5 * * @return array */ public static function get_available_modes(): array { return [ Environment::SANDBOX, Environment::PRODUCTION ]; } /** * Validate Square mode to ensure it's either 'production' or 'sandbox'. * If given mode is invalid, fetches current Square mode. * * @since 1.9.5 * * @param string $mode Square mode to validate. * * @return string */ public static function validate_mode( string $mode ): string { return in_array( $mode, self::get_available_modes(), true ) ? $mode : self::get_mode(); } /** * Retrieve the WPForms > Payments settings page URL. * * @since 1.9.5 * * @return string */ public static function get_settings_page_url(): string { return add_query_arg( [ 'page' => 'wpforms-settings', 'view' => 'payments', ], admin_url( 'admin.php' ) ); } /** * The `array_key_first` polyfill. * * @since 1.9.5 * * @param array $arr Input array. * * @return mixed|null */ public static function array_key_first( array $arr ) { if ( function_exists( 'array_key_first' ) ) { return array_key_first( $arr ); } foreach ( $arr as $key => $unused ) { return $key; } return null; } /** * Determine if webhook ID and secret are set in WPForms settings. * * @since 1.9.5 * * @return bool */ public static function is_webhook_configured(): bool { $mode = self::get_mode(); return wpforms_setting( 'square-webhooks-id-' . $mode ) && wpforms_setting( 'square-webhooks-secret-' . $mode ); } /** * Determine if Square is configured and valid. * * @since 1.9.5 * * @return bool */ public static function is_square_configured(): bool { $connection = Connection::get(); // Check if connection is configured and valid. return ! ( ! $connection || ! $connection->is_configured() || ! $connection->is_valid() ); } /** * Get webhook URL for REST API. * * @since 1.9.5 * * @return string */ public static function get_webhook_url_for_rest(): string { $path = implode( '/', [ self::get_webhook_endpoint_data()['namespace'], self::get_webhook_endpoint_data()['route'], ] ); return rest_url( $path ); } /** * Reset Square webhooks settings. * * @since 1.9.5 * * @param bool $reset_enable Optional. Whether to reset the webhook enabled status. Default is false. * * @return bool */ public static function reset_webhook_configuration( bool $reset_enable = false ): bool { $settings = (array) get_option( 'wpforms_settings', [] ); $mode = self::get_mode(); if ( $reset_enable ) { $settings['square-webhooks-enabled'] = false; // Switch off webhooks. } $settings[ 'square-webhooks-id-' . $mode ] = ''; $settings[ 'square-webhooks-secret-' . $mode ] = ''; return update_option( 'wpforms_settings', $settings ); } /** * Determine the billing cadences of a Subscription. * * @since 1.9.5 * * @return array */ public static function get_subscription_cadences(): array { /** * Filter the available billing cadences of a Subscription. * * @since 1.9.5 * * @param array $cadences Subscription billing cadences. */ return (array) apply_filters( 'wpforms_integrations_square_helpers_get_subscription_cadences', [ 'daily' => [ 'slug' => 'daily', 'name' => esc_html__( 'Daily', 'wpforms-lite' ), 'value' => SubscriptionCadence::DAILY, ], 'weekly' => [ 'slug' => 'weekly', 'name' => esc_html__( 'Weekly', 'wpforms-lite' ), 'value' => SubscriptionCadence::WEEKLY, ], 'monthly' => [ 'slug' => 'monthly', 'name' => esc_html__( 'Monthly', 'wpforms-lite' ), 'value' => SubscriptionCadence::MONTHLY, ], 'quarterly' => [ 'slug' => 'quarterly', 'name' => esc_html__( 'Quarterly', 'wpforms-lite' ), 'value' => SubscriptionCadence::QUARTERLY, ], 'semiyearly' => [ 'slug' => 'semiyearly', 'name' => esc_html__( 'Semi-Yearly', 'wpforms-lite' ), 'value' => SubscriptionCadence::EVERY_SIX_MONTHS, ], 'yearly' => [ 'slug' => 'yearly', 'name' => esc_html__( 'Yearly', 'wpforms-lite' ), 'value' => SubscriptionCadence::ANNUAL, ], ] ); } /** * Return a formatted amount required by Square API. * * @since 1.9.5 * * @param string $amount Price amount. * * @return float|int */ public static function format_amount( string $amount ) { return wpforms_sanitize_amount( $amount ) * wpforms_get_currency_multiplier(); } /** * Get Square webhook endpoint data. * * @since 1.9.5 * * @return array */ public static function get_webhook_endpoint_data(): array { return [ 'namespace' => 'wpforms', 'route' => 'square/webhooks', 'fallback' => 'wpforms_square_webhooks', ]; } /** * Get Square webhook endpoint URL. * * If the constant WPFORMS_SQUARE_WHURL is defined, it will be used as the webhook URL. * * @since 1.9.5 * * @return string */ public static function get_webhook_url(): string { if ( defined( 'WPFORMS_SQUARE_WHURL' ) ) { return WPFORMS_SQUARE_WHURL; } if ( self::is_rest_api_set() ) { return self::get_webhook_url_for_rest(); } return self::get_webhook_url_for_curl(); } /** * Determine if the REST API is set in WPForms settings. * * @since 1.9.5 * * @return bool */ public static function is_rest_api_set(): bool { return wpforms_setting( 'square-webhooks-communication', 'rest' ) === 'rest'; } /** * Get webhook URL for cURL fallback. * * @since 1.9.5 * * @return string */ public static function get_webhook_url_for_curl(): string { return add_query_arg( self::get_webhook_endpoint_data()['fallback'], '1', site_url() ); } /** * Determine if webhooks are enabled in WPForms settings. * * @since 1.9.5 * * @return bool */ public static function is_webhook_enabled(): bool { return wpforms_setting( 'square-webhooks-enabled' ); } /** * Determine whether the application fee is supported. * * @since 1.9.5 * * @param string $currency Currency. * * @return bool */ public static function is_application_fee_supported( string $currency = '' ): bool { $currency = ! $currency ? wpforms_get_currency() : $currency; return strtoupper( $currency ) === 'USD'; } } Integrations/Square/Square.php 0000644 00000010440 15174710275 0012433 0 ustar 00 <?php namespace WPForms\Integrations\Square; use WPForms\Integrations\IntegrationInterface; /** * Integration of the Square payment gateway. * * @since 1.9.5 */ final class Square implements IntegrationInterface { /** * Square application name. * * @since 1.9.5 */ public const APP_NAME = 'WPForms'; /** * Determine if the integration is allowed to load. * * @since 1.9.5 * * @return bool */ public function allow_load(): bool { // Determine whether the Square addon version is compatible with the WPForms plugin version. $addon_compat = ( new AddonCompatibility() )->init(); // Do not load integration if unsupported version of the Square addon is active. if ( $addon_compat && ! $addon_compat->is_supported_version() ) { $addon_compat->hooks(); return false; } // Determine whether the cURL extension is enabled. $curl_compat = ( new CurlCompatibility() )->init(); // Do not load integration if curl is not enabled. if ( $curl_compat ) { $curl_compat->hooks(); return false; } /** * Whether the integration is allowed to load. * * @since 1.9.5 * * @param bool $is_allowed Integration loading state. */ return (bool) apply_filters( 'wpforms_integrations_square_allow_load', true ); } /** * Load the integration. * * @since 1.9.5 */ public function load() { $this->load_admin_entries(); $this->load_settings(); $this->load_connect(); $this->load_field(); $this->load_frontend(); $this->load_integrations(); $this->load_payments_actions(); $this->load_builder(); $this->load_webhooks(); $this->load_tasks(); // Bail early for paid users with active Square addon. if ( Helpers::is_pro() ) { return; } $this->load_builder_settings(); $this->load_processing(); } /** * Load admin entries functionality. * * @since 1.9.5 */ private function load_admin_entries() { if ( wpforms_is_admin_page( 'entries' ) ) { ( new Admin\Entries() )->init(); } } /** * Load Square settings. * * @since 1.9.5 */ private function load_settings() { if ( wpforms_is_admin_page( 'settings', 'payments' ) ) { ( new Admin\Settings() )->init(); ( new Admin\Notices() )->init(); } } /** * Load connect handler. * * @since 1.9.5 */ private function load_connect() { ( new Admin\Connect() )->init(); } /** * Load Square field. * * @since 1.9.5 */ private function load_field() { // phpcs:disable WordPress.Security.NonceVerification $is_elementor = ( ! empty( $_POST['action'] ) && $_POST['action'] === 'elementor_ajax' ) || ( ! empty( $_GET['action'] ) && $_GET['action'] === 'elementor' ); // phpcs:enable WordPress.Security.NonceVerification if ( $is_elementor || ! is_admin() || wp_doing_ajax() || wpforms_is_admin_page( 'builder' ) ) { ( new Fields\Square() )->init(); } } /** * Load builder functionality. * * @since 1.9.5 */ private function load_builder() { if ( wpforms_is_admin_page( 'builder' ) ) { ( new Admin\Builder\Enqueues() )->init(); } } /** * Load builder settings functionality. * * @since 1.9.5 */ private function load_builder_settings() { if ( wpforms_is_admin_page( 'builder' ) ) { ( new Admin\Builder\Settings() )->init(); ( new Admin\Builder\Notifications() )->init(); } } /** * Load payments actions. * * @since 1.9.5 */ private function load_payments_actions() { if ( ! Connection::get() ) { return; } ( new Admin\Payments\SingleActionsHandler() )->init(); } /** * Load frontend functionality. * * @since 1.9.5 */ private function load_frontend() { if ( ! is_admin() ) { ( new Frontend() )->init(); } } /** * Load payment form processing. * * @since 1.9.5 */ private function load_processing() { if ( ! is_admin() || wpforms_is_frontend_ajax() ) { ( new Process() )->init(); } } /** * Load integrations. * * @since 1.9.5 */ private function load_integrations() { ( new Integrations\Loader() )->init(); } /** * Load webhooks. * * @since 1.9.5 */ private function load_webhooks() { ( new Api\WebhookRoute() )->init(); ( new WebhooksHealthCheck() )->init(); } /** * Load tasks. * * @since 1.9.5 */ private function load_tasks() { if ( ! Connection::get() ) { return; } ( new Tasks() )->init(); } } Integrations/Square/WebhooksHealthCheck.php 0000644 00000012220 15174710275 0015036 0 ustar 00 <?php namespace WPForms\Integrations\Square; use WPForms\Admin\Notice; /** * Webhooks Health Check class. * * @since 1.9.5 */ class WebhooksHealthCheck { /** * Endpoint status option name. * * @since 1.9.5 */ public const ENDPOINT_OPTION = 'wpforms_square_webhooks_endpoint_status'; /** * Signature verified key. * * @since 1.9.5 */ public const STATUS_OK = 'ok'; /** * Signature error key. * * @since 1.9.5 */ private const STATUS_ERROR = 'error'; /** * AS task name. * * @since 1.9.5 */ private const ACTION = 'wpforms_square_webhooks_health_check'; /** * Admin notice ID. * * @since 1.9.5 */ private const NOTICE_ID = 'wpforms_square_webhooks_site_health'; /** * Initialization. * * @since 1.9.5 */ public function init() { $this->hooks(); } /** * Register hooks. * * @since 1.9.5 */ private function hooks() { add_action( 'admin_notices', [ $this, 'admin_notice' ] ); add_action( self::ACTION, [ $this, 'process_webhooks_status_action' ] ); add_action( 'action_scheduler/migration_complete', [ $this, 'maybe_schedule_task' ] ); } /** * Schedule webhook health check. * * @since 1.9.5 */ public function maybe_schedule_task() { /** * Allow customers to disable a webhook health check task. * * @since 1.9.5 * * @param bool $cancel True if a task needs to be canceled. */ $is_canceled = (bool) apply_filters( 'wpforms_integrations_square_webhooks_health_check_cancel', false ); $tasks = wpforms()->obj( 'tasks' ); // Bail early in some instances. if ( $is_canceled || $tasks === null || ! Helpers::is_square_configured() || $tasks->is_scheduled( self::ACTION ) ) { return; } /** * Filters the webhook health check interval. * * @since 1.9.5 * * @param int $interval Interval in seconds. */ $interval = (int) apply_filters( 'wpforms_integrations_square_webhooks_health_check_interval', HOUR_IN_SECONDS ); $tasks->create( self::ACTION ) ->recurring( time(), $interval ) ->register(); } /** * Process webhook status. * * @since 1.9.5 */ public function process_webhooks_status_action() { // Bail out if user unchecked option to enable webhooks. if ( ! Helpers::is_webhook_enabled() ) { return; } $last_payment = $this->get_last_square_payment(); // Bail out if there is no Square payment, // and remove options for reason to avoid any edge cases. if ( ! $last_payment ) { delete_option( self::ENDPOINT_OPTION ); return; } // If a last Square payment has processed status and webhooks are not valid, // most likely there is an issue with webhooks. if ( $last_payment['status'] === 'processed' && time() > ( strtotime( $last_payment['date_created_gmt'] ) + ( 15 * MINUTE_IN_SECONDS ) ) ) { Helpers::reset_webhook_configuration(); self::save_status( self::ENDPOINT_OPTION, self::STATUS_ERROR ); return; } self::save_status( self::ENDPOINT_OPTION, self::STATUS_OK ); } /** * Determine whether there is Square payment. * * @since 1.9.5 * * @return array */ private function get_last_square_payment(): array { $payment = wpforms()->obj( 'payment' )->get_payments( [ 'gateway' => 'square', 'mode' => 'any', 'number' => 1, ] ); return ! empty( $payment[0] ) ? $payment[0] : []; } /** * Display notice about issues with webhooks. * * @since 1.9.5 */ public function admin_notice() { // Bail out if a Square account is not connected. if ( ! Helpers::is_square_configured() ) { return; } // Bail out if webhooks are not enabled. if ( ! Helpers::is_webhook_enabled() ) { return; } // Bail out if webhooks are configured and active. if ( Helpers::is_webhook_configured() ) { return; } // Show notice only in case if ENDPOINT_OPTION has error status. if ( get_option( self::ENDPOINT_OPTION, self::STATUS_OK ) === self::STATUS_OK ) { return; } // Bail out if there are no Square payments. if ( ! $this->get_last_square_payment() ) { return; } $notice = sprintf( wp_kses( /* translators: %s - WPForms.com URL for Square webhooks documentation. */ __( 'Looks like you have a problem with your webhooks configuration. Please check and confirm that you\'ve configured the WPForms webhooks in your Square account. This notice will disappear automatically when a new Square request comes in. See our <a href="%1$s" rel="nofollow noopener" target="_blank">documentation</a> for more information.', 'wpforms-lite' ), [ 'a' => [ 'href' => [], 'target' => [], 'rel' => [], ], ] ), esc_url( wpforms_utm_link( 'https://wpforms.com/docs/setting-up-square-webhooks/', 'Admin', 'Square Webhooks not active' ) ) ); Notice::error( $notice, [ 'dismiss' => true, 'slug' => self::NOTICE_ID, ] ); } /** * Save webhooks status. * * @since 1.9.5 * * @param string $option Option name. * @param string $value Status value. */ public static function save_status( string $option, string $value ) { if ( ! in_array( $value, [ self::STATUS_OK, self::STATUS_ERROR ], true ) ) { return; } update_option( $option, $value ); } } Integrations/Square/Tasks.php 0000644 00000001306 15174710275 0012261 0 ustar 00 <?php namespace WPForms\Integrations\Square; use WPForms\Tasks\Actions\SquareSubscriptionTransactionIDTask; /** * Register tasks. * * @since 1.9.5 */ class Tasks { /** * Initialize. * * @since 1.9.5 */ public function init() { $this->hooks(); } /** * Frontend hooks. * * @since 1.9.5 */ private function hooks() { add_filter( 'wpforms_tasks_get_tasks', [ $this, 'register' ] ); } /** * Add class to registered tasks array. * * @since 1.9.5 * * @param array $tasks Array of tasks. * * @return array */ public function register( $tasks ): array { $tasks = (array) $tasks; $tasks[] = SquareSubscriptionTransactionIDTask::class; return $tasks; } } Integrations/Square/Fields/Square.php 0000644 00000041000 15174710275 0013635 0 ustar 00 <?php namespace WPForms\Integrations\Square\Fields; use WPForms_Field; use WPForms\Integrations\Square\Connection; use WPForms\Integrations\Square\Helpers; /** * Square credit card field. * * @since 1.9.5 */ class Square extends WPForms_Field { /** * Primary class constructor. * * @since 1.9.5 */ public function init() { // Define field type information. $this->name = esc_html__( 'Square', 'wpforms-lite' ); $this->keywords = esc_html__( 'store, ecommerce, credit card, pay, payment, debit card', 'wpforms-lite' ); $this->type = 'square'; $this->icon = 'fa-credit-card'; $this->order = 92; $this->group = 'payment'; $this->hooks(); } /** * Field specific hooks. * * @since 1.14.0 * * @return void */ private function hooks() { add_filter( 'wpforms_field_properties_square', [ $this, 'field_properties' ], 5, 3 ); add_filter( 'wpforms_field_new_required', [ $this, 'default_required' ], 10, 2 ); add_filter( 'wpforms_builder_field_button_attributes', [ $this, 'field_button_atts' ], 10, 3 ); add_filter( 'wpforms_field_new_display_duplicate_button', [ $this, 'field_display_duplicate_button' ], 10, 2 ); add_filter( 'wpforms_field_preview_display_duplicate_button', [ $this, 'field_display_duplicate_button' ], 10, 2 ); add_filter( 'wpforms_pro_fields_entry_preview_is_field_support_preview_square_field', [ $this, 'entry_preview_availability' ], 10, 4 ); add_filter( 'wpforms_field_display_sublabel_skip_for', [ $this, 'skip_sublabel_for_attribute' ], 10, 3 ); } /** * Define additional field properties. * * @since 1.9.5 * * @param array $properties Field properties. * @param array $field Field settings. * @param array $form_data Form data and settings. * * @return array */ public function field_properties( $properties, array $field, array $form_data ): array { $properties = (array) $properties; unset( $properties['label']['attr']['for'] ); $form_id = absint( $form_data['id'] ); $field_id = absint( $field['id'] ); $props = [ 'inputs' => [ 'number' => [ 'attr' => [ 'name' => '', 'value' => '', ], 'block' => [ 'wpforms-field-square-number', ], 'class' => [ 'wpforms-field-square-cardnumber', ], 'data' => [], 'id' => "wpforms-{$form_id}-field_{$field_id}", 'required' => ! empty( $field['required'] ) ? 'required' : '', 'sublabel' => [ 'hidden' => ! empty( $field['sublabel_hide'] ), 'value' => esc_html__( 'Card', 'wpforms-lite' ), 'position' => 'after', ], ], 'name' => [ 'attr' => [ 'name' => "wpforms[fields][{$field_id}][cardname]", 'placeholder' => ! empty( $field['cardname_placeholder'] ) ? $field['cardname_placeholder'] : '', ], 'block' => [ 'wpforms-field-square-name', ], 'class' => [ 'wpforms-field-square-cardname', ], 'data' => [], 'id' => "wpforms-{$form_id}-field_{$field_id}-cardname", 'required' => ! empty( $field['required'] ) ? 'required' : '', 'sublabel' => [ 'hidden' => ! empty( $field['sublabel_hide'] ), 'value' => esc_html__( 'Name on Card', 'wpforms-lite' ), 'position' => 'after', ], ], ], ]; $properties = array_merge_recursive( $properties, $props ); // If this field is required, we need to make some adjustments. if ( ! empty( $field['required'] ) ) { // Add required class if needed (for multipage validation). $properties['inputs']['number']['class'][] = 'wpforms-field-required'; $properties['inputs']['name']['class'][] = 'wpforms-field-required'; } return $properties; } /** * Default to the required. * * @since 1.9.5 * * @param bool $required Required status, true is required. * @param array $field Field settings. * * @return bool */ public function default_required( $required, array $field ): bool { return $this->type === $field['type'] ? true : (bool) $required; } /** * Define additional "Add Field" button attributes. * * @since 1.9.5 * * @param array $atts Add Field button attributes. * @param array $field Field settings. * @param array $form_data Form data and settings. * * @return array */ public function field_button_atts( $atts, array $field, array $form_data ): array { $atts = (array) $atts; if ( $field['type'] !== $this->type ) { return $atts; } if ( Helpers::has_square_field( $form_data ) ) { $atts['atts']['disabled'] = 'true'; $atts['class'][] = 'wpforms-add-fields-button-disabled'; return $atts; } if ( ! Connection::get() ) { $atts['class'][] = 'warning-modal'; $atts['class'][] = 'square-connection-required'; } return $atts; } /** * Disallow field preview "Duplicate" button. * * @since 1.9.5 * * @param bool $display Display switch. * @param array $field Field settings. * * @return bool */ public function field_display_duplicate_button( $display, array $field ): bool { return $field['type'] === $this->type ? false : (bool) $display; } /** * The field value availability for the Entry Preview field. * * @since 1.9.5 * * @param bool $is_supported The field availability. * @param string|array $value The submitted Credit Card detail. * @param array $field Field data. * @param array $form_data Form data. * * @return bool * @noinspection PhpUnusedParameterInspection */ public function entry_preview_availability( $is_supported, $value, array $field, array $form_data ): bool { return ! empty( $value ); } /** * Disallow dynamic population. * * @since 1.9.5 * * @param array $properties Field properties. * @param array $field Current field specific data. * * @return bool */ public function is_dynamic_population_allowed( $properties, $field ): bool { return false; } /** * Disallow fallback population. * * @since 1.9.5 * * @param array $properties Field properties. * @param array $field Current field specific data. * * @return bool */ public function is_fallback_population_allowed( $properties, $field ): bool { return false; } /** * Field options panel inside the builder. * * @since 1.9.5 * * @param array $field Field settings. */ public function field_options( $field ) { /* * Basic field options. */ // Options open markup. $args = [ 'markup' => 'open', ]; $this->field_option( 'basic-options', $field, $args ); // Label. $this->field_option( 'label', $field ); // Description. $this->field_option( 'description', $field ); // Required toggle. $this->field_option( 'required', $field ); // Options close markup. $args = [ 'markup' => 'close', ]; $this->field_option( 'basic-options', $field, $args ); /* * Advanced field options. */ // Options open markup. $args = [ 'markup' => 'open', ]; $this->field_option( 'advanced-options', $field, $args ); // Size. $this->field_option( 'size', $field ); // Card Name. $cardname_placeholder = ! empty( $field['cardname_placeholder'] ) ? esc_attr( $field['cardname_placeholder'] ) : ''; $cardname_field = sprintf( '<div class="placeholder"><input type="text" class="placeholder-update" id="wpforms-field-option-%1$d-cardname_placeholder" name="fields[%1$d][cardname_placeholder]" value="%2$s" data-field-id="%1$d" data-subfield="square-cardname"></div>', absint( $field['id'] ), esc_html( $cardname_placeholder ) ); printf( '<div class="wpforms-clear wpforms-field-option-row wpforms-field-option-row-cardname" id="wpforms-field-option-row-%1$d-cardname" data-subfield="cardname" data-field-id="%1$d">', absint( $field['id'] ) ); $this->field_element( 'label', $field, [ 'slug' => 'cardname_placeholder', 'value' => esc_html__( 'Name on Card Placeholder Text', 'wpforms-lite' ), ] ); echo $cardname_field; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo '</div>'; // Custom CSS classes. $this->field_option( 'css', $field ); // Hide Label. $this->field_option( 'label_hide', $field ); // Hide sublabels. $this->field_option( 'sublabel_hide', $field ); // Options close markup. $args = [ 'markup' => 'close', ]; $this->field_option( 'advanced-options', $field, $args ); } /** * Field preview inside the builder. * * @since 1.9.5 * * @param array $field Field settings. */ public function field_preview( $field ) { // Label. $this->field_preview_option( 'label', $field ); // Placeholder. $this->field_preview_placeholder( $field ); // Description. $this->field_preview_option( 'description', $field ); } /** * Field display on the form front-end. * * @since 1.9.5 * * @param array $field Field data and settings. * @param array $deprecated Deprecated field attributes. Use field properties. * @param array $form_data Form data and settings. * * @noinspection HtmlUnknownAttribute */ public function field_display( $field, $deprecated, $form_data ) { if ( wpforms_is_editor_page() ) { $this->field_preview_placeholder( $field ); return; } // Define data. $number = ! empty( $field['properties']['inputs']['number'] ) ? $field['properties']['inputs']['number'] : []; $name = ! empty( $field['properties']['inputs']['name'] ) ? $field['properties']['inputs']['name'] : []; // Display warning for non SSL pages. if ( ! is_ssl() ) { echo '<div class="wpforms-cc-warning wpforms-error-alert">'; esc_html_e( 'This page is insecure. Credit Card field should be used for testing purposes only.', 'wpforms-lite' ); echo '</div>'; } $connection = Connection::get(); if ( ! $connection ) { echo '<div class="wpforms-cc-warning wpforms-error-alert">'; esc_html_e( 'Credit Card field is disabled, Square account connection is missing.', 'wpforms-lite' ); echo '</div>'; return; } if ( ! $connection->is_usable() ) { echo '<div class="wpforms-cc-warning wpforms-error-alert">'; esc_html_e( 'Credit Card field is disabled, Square account connection is invalid. Please, contact to the site administrator.', 'wpforms-lite' ); echo '</div>'; return; } if ( ! Helpers::is_payments_enabled( $form_data ) ) { echo '<div class="wpforms-cc-warning wpforms-error-alert">'; esc_html_e( 'Credit Card field is disabled, Square payments are not enabled in the form settings.', 'wpforms-lite' ); echo '</div>'; return; } if ( $connection->is_expired() && wpforms_current_user_can() ) { echo '<div class="wpforms-cc-warning wpforms-error-alert">'; esc_html_e( 'Heads up! Square account connection is expired. Tokens must be refreshed.', 'wpforms-lite' ); echo '</div>'; } // Row wrapper. echo '<div class="wpforms-field-row wpforms-field-' . sanitize_html_class( $field['size'] ) . '">'; echo '<div ' . wpforms_html_attributes( false, $number['block'] ) . '>'; $this->field_display_sublabel( 'number', 'before', $field ); printf( '<div %s data-required="%s"><!-- Square credit card will be inserted here. --></div>', wpforms_html_attributes( $number['id'], $number['class'], $number['data'], $number['attr'] ), esc_attr( $number['required'] ) ); // Hidden input is needed for validation on the frontend and as a substitute in Block Editor previews. printf( '<input type="text" class="wpforms-square-credit-card-hidden-input" name="wpforms[square-credit-card-hidden-input-%1$d]" id="wpforms-square-credit-card-hidden-input-%1$d" %2$s>', (int) $form_data['id'], wpforms_is_editor_page() ? '' : 'style="display: none;" disabled' ); $this->field_display_sublabel( 'number', 'after', $field ); $this->field_display_error( 'number', $field ); echo '</div>'; echo '</div>'; // Row wrapper. echo '<div class="wpforms-field-row wpforms-field-' . sanitize_html_class( $field['size'] ) . '">'; // Name. echo '<div ' . wpforms_html_attributes( false, $name['block'] ) . '>'; $this->field_display_sublabel( 'name', 'before', $field ); printf( '<input type="text" %s %s>', wpforms_html_attributes( $name['id'], $name['class'], $name['data'], $name['attr'] ), esc_attr( $name['required'] ) ); $this->field_display_sublabel( 'name', 'after', $field ); $this->field_display_error( 'name', $field ); echo '</div>'; echo '</div>'; } /** * Currently validation happens on the front end. We do not do * generic server-side validation because we do not allow the card * details to POST to the server. * * @since 1.9.5 * * @param int $field_id Field ID. * @param array $field_submit Submitted field value. * @param array $form_data Form data and settings. */ public function validate( $field_id, $field_submit, $form_data ) {} /** * Format field. * * @since 1.9.5 * * @param int $field_id Field ID. * @param array $field_submit Submitted field value. * @param array $form_data Form data and settings. */ public function format( $field_id, $field_submit, $form_data ) { // Define data. $field_name = ! empty( $form_data['fields'][ $field_id ]['label'] ) ? $form_data['fields'][ $field_id ]['label'] : ''; $card_name = ! empty( $field_submit['cardname'] ) ? $field_submit['cardname'] : ''; // Set final field details. wpforms()->obj( 'process' )->fields[ $field_id ] = [ 'name' => sanitize_text_field( $field_name ), 'cardname' => sanitize_text_field( $card_name ), 'value' => '', 'id' => absint( $field_id ), 'type' => $this->type, ]; } /** * Card field placeholder. * * @since 1.9.5 * * @param array $field Current field specific data. */ private function field_preview_placeholder( array $field ) { // Define data. $name_placeholder = ! empty( $field['cardname_placeholder'] ) ? esc_attr( $field['cardname_placeholder'] ) : ''; $size = ! empty( $field['size'] ) ? sprintf( 'wpforms-field-%s', sanitize_html_class( $field['size'] ) ) : ''; $hide_sub_label = ! empty( $field['sublabel_hide'] ); ?> <div class="format-selected format-selected-full"> <div class="wpforms-field-row"> <div class="wpforms-square-cardnumber"> <div class="wpforms-square-cardnumber-wrapper <?php echo esc_attr( $size ); ?>"> <div class="card-number"> <div class="card-icon"> <svg width="36" height="24"> <linearGradient id="a" x1="18" x2="18" y1="54.1" y2="-16.7" gradientTransform="matrix(1 0 0 -1 0 26)" gradientUnits="userSpaceOnUse"> <stop offset="0" stop-color="#626364"/> <stop offset="1" stop-color="#414447"/> </linearGradient> <path fill="url(#a)" d="M4 0h28a4 4 0 0 1 4 4v16a4 4 0 0 1-4 4H4a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4z"/> <path fill="#fff" fill-opacity=".3" d="M7 12h22c.6 0 1 .4 1 1s-.4 1-1 1H7c-.6 0-1-.4-1-1s.4-1 1-1zm-.5 5h6c.3 0 .5.2.5.5s-.2.5-.5.5h-6c-.3 0-.5-.2-.5-.5s.2-.5.5-.5z"/> </svg> </div> <input type="text" placeholder="<?php esc_html_e( 'Card number', 'wpforms-lite' ); ?>" disabled> </div> <div class="card-data"> <input type="text" class="exp-input-wrapper" placeholder="<?php esc_html_e( 'MM/YY', 'wpforms-lite' ); ?>" disabled> <input type="text" class="cvv-input-wrapper" placeholder="<?php esc_html_e( 'CVV', 'wpforms-lite' ); ?>" disabled> </div> </div> <label class="wpforms-sub-label wpforms-field-sublabel <?php echo $hide_sub_label ? 'wpforms-sublabel-hide' : ''; ?>"><?php esc_html_e( 'Card', 'wpforms-lite' ); ?></label> </div> </div> <div class="wpforms-field-row"> <div class="wpforms-square-cardname"> <input type="text" class="<?php echo esc_attr( $size ); ?>" placeholder="<?php echo esc_attr( $name_placeholder ); ?>" disabled> <label class="wpforms-sub-label wpforms-field-sublabel <?php echo $hide_sub_label ? 'wpforms-sublabel-hide' : ''; ?>"><?php esc_html_e( 'Name on Card', 'wpforms-lite' ); ?></label> </div> </div> </div> <?php } /** * Do not add the `for` attribute to certain sublabels. * * @since 1.9.5 * * @param bool $skip Whether to skip the `for` attribute. * @param string $key Input key. * @param array $field Field data and settings. * * @return bool */ public function skip_sublabel_for_attribute( $skip, string $key, array $field ): bool { $skip = (bool) $skip; if ( $field['type'] !== $this->type ) { return $skip; } if ( in_array( $key, [ 'name', 'number' ], true ) ) { return true; } return $skip; } } Integrations/Square/Frontend.php 0000644 00000011332 15174710275 0012753 0 ustar 00 <?php namespace WPForms\Integrations\Square; /** * Square form frontend related functionality. * * @since 1.9.5 */ class Frontend { /** * Initialize. * * @since 1.9.5 */ public function init() { $this->hooks(); return $this; } /** * Frontend hooks. * * @since 1.9.5 */ private function hooks() { add_action( 'wpforms_frontend_container_class', [ $this, 'form_container_class' ], 10, 2 ); add_action( 'wpforms_wp_footer', [ $this, 'enqueues' ] ); } /** * Add class to form container if Square is enabled. * * @since 1.9.5 * * @param array $classes Array of form classes. * @param array $form_data Form data of current form. * * @return array */ public function form_container_class( $classes, array $form_data ): array { $classes = (array) $classes; if ( ! Connection::get() ) { return $classes; } if ( ! Helpers::has_square_field( $form_data ) || ! Helpers::is_payments_enabled( $form_data ) ) { return $classes; } if ( Helpers::is_square_recurring_enabled( $form_data ) ) { $classes[] = 'wpforms-square-is-recurring'; } $classes[] = 'wpforms-square'; return $classes; } /** * Enqueue assets in the frontend if Square is in use on the page. * * @since 1.9.5 * * @param array $forms Form data of forms on current page. */ public function enqueues( $forms ) { $connection = Connection::get(); if ( ! $connection || ! $connection->is_usable() ) { return; } $forms = (array) $forms; if ( ! Helpers::has_square_field( $forms, true ) ) { return; } if ( ! Helpers::has_square_enabled( $forms ) ) { return; } $min = wpforms_get_min_suffix(); // Include styles if the "Include Form Styling > No Styles" is not set. if ( wpforms_setting( 'disable-css', '1' ) !== '3' ) { wp_enqueue_style( 'wpforms-square', WPFORMS_PLUGIN_URL . "assets/css/integrations/square/wpforms-square{$min}.css", [], WPFORMS_VERSION ); } // phpcs:disable WordPress.WP.EnqueuedResourceParameters.MissingVersion wp_enqueue_script( 'square-web-payments-sdk', Helpers::is_sandbox_mode() ? 'https://sandbox.web.squarecdn.com/v1/square.js' : 'https://web.squarecdn.com/v1/square.js', [], null, true ); // phpcs:enable WordPress.WP.EnqueuedResourceParameters.MissingVersion wp_enqueue_script( 'wpforms-square', WPFORMS_PLUGIN_URL . "assets/js/integrations/square/wpforms-square{$min}.js", [ 'jquery', 'square-web-payments-sdk' ], WPFORMS_VERSION, true ); /** * This filter allows to set a card configuration and styles. * * @since 1.9.5 * * @link https://developer.squareup.com/reference/sdks/web/payments/card-payments#Card.configure.options * * @param array $card_config Configuration and style options. * @param array $forms Form data of forms on current page. */ $card_config = (array) apply_filters( 'wpforms_square_frontend_enqueues_card_config', [], $forms ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName wp_localize_script( 'wpforms-square', 'wpforms_square', [ 'client_id' => $connection->get_client_id(), 'location_id' => Helpers::get_location_id(), 'card_config' => $card_config, 'billing_details' => $this->get_mapped_contact_fields( $forms ), 'i18n' => [ 'missing_sdk_script' => esc_html__( 'Square.js failed to load properly.', 'wpforms-lite' ), 'general_error' => esc_html__( 'An unexpected Square SDK error has occurred.', 'wpforms-lite' ), 'missing_creds' => esc_html__( 'Client ID and/or Location ID is incorrect.', 'wpforms-lite' ), 'card_init_error' => esc_html__( 'Initializing Card failed.', 'wpforms-lite' ), 'token_process_fail' => esc_html__( 'Tokenization of the payment card failed.', 'wpforms-lite' ), 'token_status_error' => esc_html__( 'Tokenization failed with status:', 'wpforms-lite' ), 'buyer_verify_error' => esc_html__( 'The verification was not successful. An issue occurred while verifying the buyer.', 'wpforms-lite' ), 'empty_details' => esc_html__( 'Please fill out payment details to continue.', 'wpforms-lite' ), ], ] ); } /** * Map provided billing details with forms on the page. * * @since 1.9.5 * * @param array $forms Form data of forms on current page. * * @return array */ public function get_mapped_contact_fields( array $forms ): array { return array_map( function ( $form_data ) { return [ 'buyer_email' => $form_data['payments']['square']['buyer_email'] ?? '', 'billing_address' => $form_data['payments']['square']['billing_address'] ?? '', 'billing_name' => $form_data['payments']['square']['billing_name'] ?? '', ]; }, $forms ); } } Integrations/Square/CurlCompatibility.php 0000644 00000002157 15174710275 0014640 0 ustar 00 <?php namespace WPForms\Integrations\Square; /** * Compatibility with cURL extension. * Square SDK requires cURL to be enabled to work correctly. * * @since 1.9.5 */ class CurlCompatibility { /** * Initialization. * * @since 1.9.5 * * @return CurlCompatibility|null */ public function init(): ?CurlCompatibility { return ! $this->is_curl_loaded() ? $this : null; } /** * Register hooks. * * @since 1.9.5 */ public function hooks() { // Warn the user about the fact that cURL is not loaded. add_action( 'admin_notices', [ $this, 'display_curl_missing_notice' ] ); } /** * Check if cURL is loaded. * * @since 1.9.5 * * @return bool */ private function is_curl_loaded(): bool { return extension_loaded( 'curl' ); } /** * Display wp-admin notification saying user to enable cURL extension for the Square payments. * * @since 1.9.5 */ public function display_curl_missing_notice() { echo '<div class="notice notice-error"><p>'; esc_html_e( 'The WPForms Square payments require cURL to be enabled to work correctly.', 'wpforms-lite' ); echo '</p></div>'; } } Integrations/Square/Admin/Entries.php 0000644 00000001517 15174710275 0013641 0 ustar 00 <?php namespace WPForms\Integrations\Square\Admin; use WPForms\Integrations\Square\Helpers; /** * Square admin entries. * * @since 1.9.5 */ class Entries { /** * Init the class. * * @since 1.9.5 */ public function init() { $this->hooks(); return $this; } /** * Entries hooks. * * @since 1.9.5 */ private function hooks() { add_filter( 'wpforms_has_payment_gateway', [ $this, 'has_payment_gateway' ], 10, 2 ); } /** * Make Square payment gateway work on the admin entries page. * * @since 1.9.5 * * @param bool $result Initial value. * @param array $form_data Form data and settings. * * @return bool */ public function has_payment_gateway( $result, array $form_data ): bool { if ( Helpers::is_payments_enabled( $form_data ) ) { return true; } return (bool) $result; } } Integrations/Square/Admin/Settings.php 0000644 00000040215 15174710275 0014026 0 ustar 00 <?php namespace WPForms\Integrations\Square\Admin; use WPForms\Vendor\Square\Environment; use WPForms\Integrations\Square\Connection; use WPForms\Integrations\Square\Helpers; /** * Square addon settings. * * @since 1.9.5 */ class Settings { /** * Determine if Square account is connected. * * @since 1.9.5 * * @var bool */ private $is_connected; /** * Square Connect. * * @since 1.9.5 * * @var Connect */ protected $connect; /** * Square Webhook Settings. * * @since 1.9.5 * * @var WebhookSettings */ protected $webhook_settings; /** * Initialize. * * @since 1.9.5 * * @return Settings */ public function init() { $this->connect = ( new Connect() )->init(); $this->webhook_settings = ( new WebhookSettings() )->init(); $this->hooks(); return $this; } /** * Settings hooks. * * @since 1.9.5 */ private function hooks() { add_action( 'wpforms_settings_enqueue', [ $this, 'enqueue_assets' ] ); add_filter( 'wpforms_admin_strings', [ $this, 'javascript_strings' ] ); add_filter( 'wpforms_settings_defaults', [ $this, 'register' ], 12 ); add_action( 'wpforms_settings_updated', [ $this, 'reset_transients' ] ); } /** * Enqueue Settings assets. * * @since 1.9.5 */ public function enqueue_assets() { $min = wpforms_get_min_suffix(); wp_enqueue_style( 'wpforms-admin-settings-square', WPFORMS_PLUGIN_URL . "assets/css/integrations/square/admin-settings-square{$min}.css", [], WPFORMS_VERSION ); wp_enqueue_script( 'wpforms-admin-settings-square', WPFORMS_PLUGIN_URL . "assets/js/integrations/square/admin/settings-square{$min}.js", [ 'jquery', 'wpforms-admin-utils' ], WPFORMS_VERSION, true ); } /** * Localize needed strings. * * @since 1.9.5 * * @param array $strings JS strings. * * @return array */ public function javascript_strings( $strings ): array { $strings = (array) $strings; $strings['square'] = [ 'mode_update' => wp_kses( __( '<p>Switching sandbox/production modes requires Square account reconnection.</p><p>Press the <em>"Connect with Square"</em> button after saving the settings to reconnect.</p>', 'wpforms-lite' ), [ 'p' => [], 'em' => [], ] ), 'refresh_error' => esc_html__( 'Something went wrong while performing a refresh tokens request.', 'wpforms-lite' ), 'webhook_create_title' => esc_html__( 'Personal Access Token', 'wpforms-lite' ), 'webhook_create_description' => sprintf( wp_kses( /* translators: %s - the Square developer dashboard URL. */ __( '<p>To receive events, create a webhook route by providing your Personal Access Token, which you can find after registering an app on the <a href="%1$s" target="_blank">Square Developer Dashboard</a>. You can also set it up manually in the Advanced section.</p><p>See <a href="%2$s" target="_blank">our documentation</a> for details.</p>', 'wpforms-lite' ), [ 'a' => [ 'href' => [], 'target' => [], ], 'p' => [], ] ), esc_url( WebhookSettings::SQUARE_APPS_URL ), esc_url( wpforms_utm_link( 'https://wpforms.com/docs/setting-up-square-webhooks/', 'Settings - Payments', 'Square Webhooks Documentation Modal' ) ) ), 'webhook_token_placeholder' => esc_html__( 'Personal Access Token', 'wpforms-lite' ), 'token_is_required' => esc_html__( 'Personal Access Token is required to proceed.', 'wpforms-lite' ), 'webhook_urls' => [ 'rest' => Helpers::get_webhook_url_for_rest(), 'curl' => Helpers::get_webhook_url_for_curl(), ], ]; return $strings; } /** * Register Settings fields. * * @since 1.9.5 * * @param array $settings Array of current form settings. * * @return array */ public function register( $settings ): array { $settings = (array) $settings; $settings['payments']['square-heading'] = [ 'id' => 'square-heading', 'content' => $this->get_heading_content(), 'type' => 'content', 'no_label' => true, 'class' => [ 'section-heading' ], ]; foreach ( Helpers::get_available_modes() as $mode ) { $mode = sanitize_key( $mode ); $settings['payments'][ 'square-connection-status-' . $mode ] = [ 'id' => 'square-connection-status-' . $mode, 'name' => esc_html__( 'Connection Status', 'wpforms-lite' ), 'content' => $this->get_connection_status_content( $mode ), 'type' => 'content', 'is_hidden' => Helpers::get_mode() !== $mode, ]; if ( $this->is_connected ) { $is_location_set = ! empty( Helpers::get_location_id( $mode ) ); $settings['payments'][ 'square-location-id-' . $mode ] = [ 'id' => 'square-location-id-' . $mode, 'class' => $is_location_set ? '' : 'location-error', 'name' => esc_html__( 'Business Location', 'wpforms-lite' ), 'desc' => esc_html__( 'Only active locations that support credit card processing in Square can be chosen.', 'wpforms-lite' ), 'type' => 'select', 'choicesjs' => true, 'options' => $this->get_location_options( $mode ), 'is_hidden' => Helpers::get_mode() !== $mode, ]; $settings['payments'][ 'square-location-status-' . $mode ] = [ 'id' => 'square-location-status-' . $mode, 'content' => $is_location_set ? '' : $this->get_location_content_error(), 'type' => 'content', 'is_hidden' => Helpers::get_mode() !== $mode, ]; } } $settings['payments']['square-sandbox-mode'] = [ 'id' => 'square-sandbox-mode', 'name' => esc_html__( 'Test Mode', 'wpforms-lite' ), 'desc' => sprintf( wp_kses( /* translators: %s - WPForms.com URL for Square payment with more details. */ __( 'Prevent Square from processing live transactions. <a href="%s" target="_blank" rel="noopener noreferrer" class="wpforms-learn-more">Learn More</a>', 'wpforms-lite' ), [ 'a' => [ 'href' => [], 'target' => [], 'rel' => [], 'class' => [], ], ] ), esc_url( wpforms_utm_link( 'https://wpforms.com/docs/how-to-test-square-payments-on-your-site/', 'Settings - Payments', 'Square Test Documentation' ) ) ), 'type' => 'toggle', 'status' => true, ]; $webhooks_settings = $this->webhook_settings->settings( $settings ); return array_merge( $settings, $webhooks_settings ); } /** * Reset transients on settings save. * * @since 1.9.5 */ public function reset_transients() { array_map( 'WPForms\Integrations\Square\Helpers::detete_transients', Helpers::get_available_modes() ); } /** * Retrieve a section header content. * * @since 1.9.5 * * @return string */ private function get_heading_content(): string { return '<h4>' . esc_html__( 'Square', 'wpforms-lite' ) . '</h4><p>' . sprintf( wp_kses( /* translators: %s - WPForms.com Square documentation article URL. */ __( 'Easily collect credit card payments with Square. For getting started and more information, see our <a href="%s" target="_blank" rel="noopener noreferrer">Square documentation</a>.', 'wpforms-lite' ), [ 'a' => [ 'href' => [], 'target' => [], 'rel' => [], ], ] ), esc_url( wpforms_utm_link( 'https://wpforms.com/docs/how-to-install-and-use-the-square-addon-with-wpforms/', 'Settings - Payments', 'Square Documentation' ) ) ) . '</p>' . Notices::get_fee_notice(); } /** * Retrieve a Connection Status setting content. * * @since 1.9.5 * * @param string $mode Square mode. * * @return string */ private function get_connection_status_content( string $mode ): string { $this->is_connected = false; $connection = Connection::get( $mode ); if ( ! $connection ) { return $this->get_disconnected_status_content( $mode ); } $content = $this->get_disabled_status_content( $connection ); if ( ! empty( $content ) ) { return $content; } $this->is_connected = true; return $this->get_enabled_status_content( $connection ); } /** * Retrieve a location content error. * * @since 1.9.5 * * @return string */ private function get_location_content_error(): string { return '<div class="wpforms-notice notice-error"><p>' . $this->get_error_icon() . esc_html__( 'Business Location is required to process Square payments.', 'wpforms-lite' ) . '</p></div>'; } /** * Retrieve setting content when a connection is disabled. * * @since 1.9.5 * * @param Connection $connection Connection data. * * @return string */ private function get_disabled_status_content( Connection $connection ): string { if ( ! $connection->is_configured() ) { return $this->get_missing_status_content( $connection->get_mode() ); } if ( ! $connection->is_valid() ) { return $this->get_invalid_status_content( $connection->get_mode() ); } return ''; } /** * Retrieve setting content when a connection is enabled. * * @since 1.9.5 * * @param Connection $connection Connection data. * * @return string */ private function get_enabled_status_content( Connection $connection ): string { if ( $connection->is_expired() ) { return $this->get_expired_status_content( $connection->get_mode() ); } if ( ! $connection->is_currency_matched() ) { return $this->get_currency_mismatch_status_content( $connection->get_mode() ); } return '<div class="wpforms-square-connected"><span class="wpforms-success-icon"></span>' . $this->get_connected_status_content( $connection->get_mode() ) . $this->get_disconnect_button( $connection->get_mode() ) . '</div>'; } /** * Retrieve a Connected Status setting content. * * @since 1.9.5 * * @param string $mode Square mode. * * @return string */ private function get_connected_status_content( string $mode ): string { $account_data = $this->connect->get_connected_account( $mode ); $account_name = empty( $account_data['business_name'] ) ? '' : $account_data['business_name']; if ( empty( $account_name ) ) { return sprintf( wp_kses( /* translators: %s - Square mode. */ __( 'Connected to Square in <strong>%s</strong> mode.', 'wpforms-lite' ), [ 'strong' => [], ] ), $mode === Environment::SANDBOX ? esc_html__( 'Sandbox', 'wpforms-lite' ) : esc_html__( 'Production', 'wpforms-lite' ) ); } return sprintf( wp_kses( /* translators: %1$s - Square connected account name; %2$s - Square mode. */ __( 'Connected to Square as <em>%1$s</em> in <strong>%2$s</strong> mode.', 'wpforms-lite' ), [ 'strong' => [], 'em' => [], ] ), esc_html( $account_name ), $mode === Environment::SANDBOX ? esc_html__( 'Sandbox', 'wpforms-lite' ) : esc_html__( 'Production', 'wpforms-lite' ) ); } /** * Retrieve a Disconnected Status setting content. * * @since 1.9.5 * * @param string $mode Square mode. * * @return string */ private function get_disconnected_status_content( string $mode ): string { return $this->get_connect_button( $mode, false ) . '<p class="desc">' . sprintf( wp_kses( /* translators: %s - WPForms.com Square documentation article URL. */ __( 'Securely connect to Square with just a few clicks to begin accepting payments! <a href="%s" target="_blank" rel="noopener noreferrer" class="wpforms-learn-more">Learn More</a>', 'wpforms-lite' ), [ 'a' => [ 'href' => [], 'target' => [], 'rel' => [], 'class' => [], ], ] ), esc_url( wpforms_utm_link( 'https://wpforms.com/docs/how-to-install-and-use-the-square-addon-with-wpforms/#connect-square', 'Settings - Payments', 'Square Learn More' ) ) ) . '</p>'; } /** * Retrieve a connection is missing status content. * * @since 1.9.5 * * @param string $mode Square mode. * * @return string */ private function get_missing_status_content( string $mode ): string { return '<div class="wpforms-square-connected">' . $this->get_error_icon() . esc_html__( 'Your connection is missing required data. You must reconnect your Square account.', 'wpforms-lite' ) . $this->get_disconnect_button( $mode ) . '</div>'; } /** * Retrieve a connection invalid status content. * * @since 1.9.5 * * @param string $mode Square mode. * * @return string */ private function get_invalid_status_content( string $mode ): string { return '<div class="wpforms-square-connected">' . $this->get_error_icon() . $this->get_connected_status_content( $mode ) . '<p>' . esc_html__( 'It appears your connection may be invalid. You must refresh tokens or reconnect your account.', 'wpforms-lite' ) . '</p>' . '<p>' . $this->get_refresh_button( $mode ) . $this->get_disconnect_button( $mode, false ) . '</p></div>'; } /** * Retrieve a connection expired status content. * * @since 1.9.5 * * @param string $mode Square mode. * * @return string */ private function get_expired_status_content( string $mode ): string { return '<div class="wpforms-square-connected">' . $this->get_error_icon() . $this->get_connected_status_content( $mode ) . '<p>' . esc_html__( 'Your connection is expired. You must refresh tokens or reconnect your account.', 'wpforms-lite' ) . '</p>' . '<p>' . $this->get_refresh_button( $mode ) . $this->get_disconnect_button( $mode, false ) . '</p></div>'; } /** * Retrieve a currency mismatch status content. * * @since 1.9.5 * * @param string $mode Square mode. * * @return string */ private function get_currency_mismatch_status_content( string $mode ): string { return '<div class="wpforms-square-connected">' . $this->get_error_icon() . $this->get_connected_status_content( $mode ) . '<span class="wpforms-notice notice-error"><p>' . esc_html__( 'WPForms currency and Business Location currency are not matched.', 'wpforms-lite' ) . '</p></span></div>' . $this->get_disconnect_button( $mode ); } /** * Retrieve the Connect button. * * @since 1.9.5 * * @param string $mode Square mode. * @param bool $wrap Optional. Wrap a button HTML element or not. * * @return string */ private function get_connect_button( string $mode, bool $wrap = true ): string { $button = sprintf( '<a class="wpforms-btn wpforms-btn-md wpforms-btn-light-grey" href="%1$s" title="%2$s">%3$s</a>', esc_url( $this->connect->get_connect_url( $mode ) ), esc_attr__( 'Connect Square account', 'wpforms-lite' ), esc_html__( 'Connect with Square', 'wpforms-lite' ) ); return $wrap ? '<p>' . $button . '</p>' : $button; } /** * Retrieve the Disconnect button. * * @since 1.9.5 * * @param string $mode Square mode. * @param bool $wrap Optional. Wrap a button HTML element or not. * * @return string */ private function get_disconnect_button( string $mode, bool $wrap = true ): string { $button = sprintf( '<a class="wpforms-btn wpforms-btn-md wpforms-btn-light-grey" href="%1$s" title="%2$s">%3$s</a>', esc_url( $this->connect->get_disconnect_url( $mode ) ), esc_attr__( 'Disconnect Square account', 'wpforms-lite' ), esc_html__( 'Disconnect', 'wpforms-lite' ) ); return $wrap ? '<p>' . $button . '</p>' : $button; } /** * Retrieve the Refresh tokens button. * * @since 1.9.5 * * @param string $mode Square mode. * * @return string */ private function get_refresh_button( string $mode ): string { return sprintf( '<button class="wpforms-btn wpforms-btn-md wpforms-btn-light-grey wpforms-square-refresh-btn" type="button" data-mode="%1$s" data-url="%2$s" title="%3$s">%4$s</button>', esc_attr( $mode ), esc_url( Helpers::get_settings_page_url() ), esc_attr__( 'Refresh connection tokens', 'wpforms-lite' ), esc_html__( 'Refresh tokens', 'wpforms-lite' ) ); } /** * Retrieve the Error icon emoji. * * @since 1.9.5 * * @return string */ private function get_error_icon(): string { return '<span class="wpforms-error-icon"></span>'; } /** * Retrieve Business Location options. * * @since 1.9.5 * * @param string $mode Square mode. * * @return array */ private function get_location_options( string $mode ): array { $locations = $this->connect->get_connected_locations( $mode ); return ! empty( $locations ) ? array_column( $locations, 'name', 'id' ) : [ '' => esc_html__( 'No locations were found', 'wpforms-lite' ) ]; } } Integrations/Square/Admin/WebhookSettings.php 0000644 00000031171 15174710275 0015346 0 ustar 00 <?php namespace WPForms\Integrations\Square\Admin; use WPForms\Integrations\Square\Helpers; /** * Square "Webhook Settings" section methods. * * @since 1.9.5 */ class WebhookSettings { /** * Square Apps URL. * * @since 1.9.5 */ public const SQUARE_APPS_URL = 'https://developer.squareup.com/apps'; /** * Initialization. * * @since 1.9.5 * * @return WebhookSettings */ public function init(): WebhookSettings { return $this; } /** * Register "Square webhooks" settings fields. * * @since 1.9.5 * * @param array $settings Admin area settings list. * * @return array */ public function settings( $settings ): array { $settings = (array) $settings; $this->maybe_set_default_settings(); // Do not display it as long as a Square account is not connected. if ( ! Helpers::is_square_configured() ) { return $settings; } $settings['payments']['square-webhooks-enabled'] = [ 'id' => 'square-webhooks-enabled', 'name' => esc_html__( 'Enable Webhooks', 'wpforms-lite' ), 'type' => 'toggle', 'status' => true, 'default' => true, 'desc' => sprintf( wp_kses( /* translators: %s - WPForms.com URL for Square webhooks documentation. */ __( 'Square uses webhooks to notify WPForms when an event has occurred in your Square account. Please see <a href="%s" target="_blank" rel="noopener noreferrer">our documentation on Square webhooks</a> for full details.', 'wpforms-lite' ), [ 'a' => [ 'href' => [], 'target' => [], 'rel' => [], ], ] ), esc_url( wpforms_utm_link( 'https://wpforms.com/docs/setting-up-square-webhooks/', 'Settings - Payments', 'Square Webhooks Documentation' ) ) ), ]; $mode = Helpers::get_mode(); $settings['payments'][ 'square-webhooks-connect-status-' . $mode ] = [ 'id' => 'square-webhooks-connect-status-' . $mode, 'name' => '', 'content' => $this->get_webhook_connection_notice(), 'type' => 'content', 'class' => Helpers::is_webhook_enabled() && ! Helpers::is_webhook_configured() ? '' : 'wpforms-hide', ]; $settings['payments']['square-webhooks-connect'] = [ 'id' => 'square-webhooks-connect', 'name' => '', 'content' => $this->get_connect_button(), 'type' => 'content', 'class' => ! Helpers::is_webhook_enabled() ? 'wpforms-hide' : '', ]; // Bail out if $_GET parameter is not passed. // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( ! isset( $_GET['webhooks_settings'] ) ) { return $settings; } $settings['payments']['square-webhooks-communication'] = [ 'id' => 'square-webhooks-communication', 'name' => esc_html__( 'Webhooks Method', 'wpforms-lite' ), 'type' => 'radio', 'default' => wpforms_setting( 'square-webhooks-communication', 'rest' ), 'options' => [ 'rest' => esc_html__( 'REST API (recommended)', 'wpforms-lite' ), 'curl' => esc_html__( 'PHP listener', 'wpforms-lite' ), ], 'desc' => sprintf( wp_kses( /* translators: %s - WPForms.com URL for Square webhooks documentation. */ __( 'Choose the method of communication between Square and WPForms. If REST API support is disabled for WordPress, use PHP listener. <a href="%s" rel="nofollow noopener" target="_blank">Learn more</a>.', 'wpforms-lite' ), [ 'a' => [ 'href' => [], 'target' => [], 'rel' => [], ], ] ), esc_url( wpforms_utm_link( 'https://wpforms.com/docs/setting-up-square-webhooks/', 'Settings - Payments', 'Square Webhooks Documentation' ) ) ), 'class' => $this->get_html_classes(), ]; $settings['payments']['square-webhooks-communication-status'] = [ 'id' => 'square-webhooks-communication-status', 'name' => '', 'content' => $this->display_webhooks_communication_notice(), 'type' => 'content', 'class' => 'wpforms-hide', ]; $settings['payments']['square-webhooks-endpoint-set'] = [ 'id' => 'square-webhooks-endpoint-set', 'name' => esc_html__( 'Webhooks Endpoint', 'wpforms-lite' ), 'url' => Helpers::get_webhook_url(), 'type' => 'webhook_endpoint', 'provider' => 'square', 'desc' => sprintf( wp_kses( /* translators: %s - Square Dashboard Webhooks Settings URL. */ __( 'Ensure an endpoint with the above URL is present in the <a href="%s" target="_blank" rel="noopener noreferrer">Square webhook settings</a>.', 'wpforms-lite' ), [ 'a' => [ 'href' => [], 'target' => [], 'rel' => [], ], ] ), esc_url( self::SQUARE_APPS_URL ) ), 'class' => $this->get_html_classes(), ]; $settings['payments']['square-webhooks-id-sandbox'] = [ 'id' => 'square-webhooks-id-sandbox', 'name' => esc_html__( 'Webhooks Test ID', 'wpforms-lite' ), 'type' => 'text', 'desc' => $this->get_webhooks_id_desc( 'sandbox' ), 'class' => $this->get_html_classes(), ]; $settings['payments']['square-webhooks-secret-sandbox'] = [ 'id' => 'square-webhooks-secret-sandbox', 'name' => esc_html__( 'Webhooks Test Signature Key', 'wpforms-lite' ), 'type' => 'password', 'desc' => $this->get_webhooks_secret_desc( 'sandbox' ), 'class' => $this->get_html_classes(), ]; $settings['payments']['square-webhooks-id-live'] = [ 'id' => 'square-webhooks-id-live', 'name' => esc_html__( 'Webhooks Live ID', 'wpforms-lite' ), 'type' => 'text', 'desc' => $this->get_webhooks_id_desc( 'live' ), 'class' => $this->get_html_classes(), ]; $settings['payments']['square-webhooks-secret-live'] = [ 'id' => 'square-webhooks-secret-live', 'name' => esc_html__( 'Webhooks Live Signature Key', 'wpforms-lite' ), 'type' => 'password', 'desc' => $this->get_webhooks_secret_desc( 'live' ), 'class' => $this->get_html_classes(), ]; return $settings; } /** * Get the Webhook connection notice. * * @since 1.9.5 * * @return string */ private function get_webhook_connection_notice(): string { if ( ! Helpers::is_webhook_enabled() ) { return ''; } if ( Helpers::is_webhook_configured() ) { return ''; } return sprintf( '<div class="wpforms-notice notice-error"><p><span class="wpforms-error-icon"></span>%1$s</p></div>', esc_html__( 'Webhooks are enabled, but not yet connected.', 'wpforms-lite' ) ); } /** * Display the Webhooks communication notice. The notice is displayed when a user tries to change communication method manually * and needs to be informed that the Webhook URL should be updated after that accordingly. * * @since 1.9.5 * * @return string */ private function display_webhooks_communication_notice(): string { return sprintf( '<div class="wpforms-notice notice-warning"><p>%1$s</p></div>', esc_html__( 'Make sure that Webhooks Endpoint is updated inside the Square app after Webhooks Method switch.', 'wpforms-lite' ) ); } /** * Show the link to the documentation about the Webhooks ID. * * @since 1.9.5 * * @param string $mode Square mode (e.g. 'live' or 'sandbox'). * * @return string */ private function get_webhooks_id_desc( string $mode ): string { $modes = [ 'live' => __( 'Live Mode Endpoint Subscription ID', 'wpforms-lite' ), 'sandbox' => __( 'Test Mode Endpoint Subscription ID', 'wpforms-lite' ), ]; return sprintf( wp_kses( /* translators: %1$s - Live Mode Endpoint ID or Test Mode Endpoint ID. %2$s - Square Dashboard Webhooks Settings URL. */ __( 'Retrieve your %1$s from your <a href="%2$s" target="_blank" rel="noopener noreferrer">Square webhook settings</a>. Select the endpoint, then click Copy button.', 'wpforms-lite' ), [ 'a' => [ 'href' => [], 'target' => [], 'rel' => [], ], ] ), $modes[ $mode ], esc_url( self::SQUARE_APPS_URL ) ); } /** * Show the link to the documentation about the Webhook Signature Key. * * @since 1.9.5 * * @param string $mode Square mode (e.g. 'live' or 'sandbox'). * * @return string */ private function get_webhooks_secret_desc( string $mode ): string { $modes = [ 'live' => __( 'Live Mode Signature Key', 'wpforms-lite' ), 'sandbox' => __( 'Test Mode Signature Key', 'wpforms-lite' ), ]; return sprintf( wp_kses( /* translators: %1$s - Live Mode Signing Secret or Test Mode Signing Secret. %2$s - Square Dashboard Webhooks Settings URL. */ __( 'Retrieve your %1$s from your <a href="%2$s" target="_blank" rel="noopener noreferrer">Square webhook settings</a>. Select the endpoint, then click Reveal.', 'wpforms-lite' ), [ 'a' => [ 'href' => [], 'target' => [], 'rel' => [], ], ] ), $modes[ $mode ], esc_url( self::SQUARE_APPS_URL ) ); } /** * Get HTML classes for the Webhooks section. * * @since 1.9.5 * * @return array */ private function get_html_classes(): array { $classes = [ 'wpforms-settings-square-webhooks' ]; // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( isset( $_GET['webhooks_settings'] ) ) { return $classes; } if ( ! wpforms_setting( 'square-webhooks-enabled' ) ) { $classes[] = 'wpforms-hide'; } return $classes; } /** * Maybe set default settings. * * @since 1.9.5 */ private function maybe_set_default_settings() { $settings = (array) get_option( 'wpforms_settings', [] ); $is_updated = false; // Enable Square webhooks by default if an account is connected. if ( ! isset( $settings['square-webhooks-enabled'] ) && Helpers::is_square_configured() ) { $settings['square-webhooks-enabled'] = true; $is_updated = true; } // Set a default communication method. if ( ! isset( $settings['square-webhooks-communication'] ) ) { $settings['square-webhooks-communication'] = $this->is_rest_api_enabled() ? 'rest' : 'curl'; $is_updated = true; } // Save settings only if something is changed. if ( $is_updated ) { update_option( 'wpforms_settings', $settings ); } } /** * Check if REST API is enabled. * * Testing configured webhook endpoint with non-authorised request. * Based on UsageTracking::is_rest_api_enabled(). * * @since 1.9.5 * * @return bool */ private function is_rest_api_enabled(): bool { // phpcs:disable WPForms.PHP.ValidateHooks.InvalidHookName /** This filter is documented in wp-includes/class-wp-http-streams.php */ $sslverify = apply_filters( 'https_local_ssl_verify', false ); // phpcs:enable WPForms.PHP.ValidateHooks.InvalidHookName $url = add_query_arg( [ 'verify' => 1 ], Helpers::get_webhook_url_for_rest() ); $response = wp_remote_get( $url, [ 'timeout' => 10, 'cookies' => [], 'sslverify' => $sslverify, 'headers' => [ 'Cache-Control' => 'no-cache', ], ] ); // When testing the REST API, an error was encountered, leave early. if ( is_wp_error( $response ) ) { return false; } // When testing the REST API, an unexpected result was returned, leave early. if ( wp_remote_retrieve_response_code( $response ) !== 200 ) { return false; } // The REST API did not behave correctly, leave early. if ( ! wpforms_is_json( wp_remote_retrieve_body( $response ) ) ) { return false; } // We are all set. Confirm the connection. return true; } /** * Get the "Connect Webhooks" button. * * @since 1.9.5 * * @return string */ private function get_connect_button(): string { $is_connected = Helpers::is_webhook_enabled() && Helpers::is_webhook_configured(); if ( $is_connected ) { return sprintf( '<div><span class="%s"></span>%s</div>', esc_attr( 'wpforms-success-icon' ), esc_html__( 'Webhooks are connected and active.', 'wpforms-lite' ) ); } $button = sprintf( '<button class="wpforms-btn wpforms-btn-md wpforms-btn-blue" type="button" id="wpforms-setting-square-webhooks-connect" title="%1$s">%2$s</button>', esc_attr__( 'Press here to see the further instructions.', 'wpforms-lite' ), esc_html__( 'Connect Webhooks', 'wpforms-lite' ) ); $description = sprintf( '<p class="desc">%s</p>', wp_kses( /* translators: %s - WPForms.com URL for Square webhooks documentation. */ __( 'To start using webhooks, please register a webhook route inside our application. You can do this by pressing the button above or setting the credentials manually. Please see <a href="%1$s" target="_blank">our documentation on Square webhooks</a> for full details.', 'wpforms-lite' ), [ 'a' => [ 'href' => [], 'target' => [], ], ] ) ); $description = sprintf( $description, esc_url( wpforms_utm_link( 'https://wpforms.com/docs/setting-up-square-webhooks/', 'Settings - Payments', 'Square Webhooks Documentation' ) ) ); return $button . $description; } } Integrations/Square/Admin/Builder/Settings.php 0000644 00000015070 15174710275 0015415 0 ustar 00 <?php namespace WPForms\Integrations\Square\Admin\Builder; use WPForms\Integrations\Square\Helpers; /** * Square Form Builder related functionality. * * @since 1.9.5 */ class Settings { use Traits\Content; /** * Slug of the integration. * * @since 1.9.5 * * @var string */ private $slug = 'square'; /** * Name of the integration. * * @since 1.9.5 * * @var string */ private $name = 'Square'; /** * Icon URL. * * @since 1.9.5 * * @var string */ private $icon = WPFORMS_PLUGIN_URL . 'assets/images/addon-icon-square.png'; /** * Form data. * * @since 1.9.5 * * @var array $form_data */ private $form_data = []; /** * Initialize. * * @since 1.9.5 */ public function init() { $this->form_data = $this->get_form_data(); $this->hooks(); } /** * Builder hooks. * * @since 1.9.5 */ private function hooks() { add_filter( 'wpforms_payments_available', [ $this, 'register_payment' ] ); add_action( 'wpforms_payments_panel_content', [ $this, 'builder_output' ], 1 ); add_action( 'wpforms_payments_panel_sidebar', [ $this, 'builder_sidebar' ], 1 ); add_filter( 'wpforms_admin_education_addons_item_base_display_single_addon_hide', [ $this, 'should_hide_educational_menu_item' ], 10, 2 ); } /** * Register the payment gateway. * * @since 1.9.5 * * @param array $payments_available List of available payment gateways. * * @return array */ public function register_payment( $payments_available ): array { $payments_available = (array) $payments_available; $payments_available[ $this->slug ] = $this->name; return $payments_available; } /** * Output the gateway menu item. * * @since 1.9.5 */ public function builder_sidebar() { // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo wpforms_render( 'builder/payment/sidebar', [ 'configured' => $this->is_payments_enabled() ? 'configured' : '', 'slug' => $this->slug, 'icon' => $this->icon, 'name' => $this->name, 'recommended' => true, ], true ); } /** * Output the gateway settings. * * @since 1.9.5 */ public function builder_output() { ?> <div class="wpforms-panel-content-section wpforms-panel-content-section-<?php echo esc_attr( $this->slug ); ?>" id="<?php echo esc_attr( $this->slug ); ?>-provider" data-provider="<?php echo esc_attr( $this->slug ); ?>" data-provider-name="<?php echo esc_attr( $this->name ); ?>"> <div class="wpforms-panel-content-section-title"> <?php echo esc_html( $this->name ); ?> </div> <div class="wpforms-payment-settings wpforms-clear"> <?php $this->builder_content(); ?> </div> </div> <?php } /** * Check if it is going to be displayed Square educational menu item and hide it. * * @since 1.9.5 * * @param bool $hide Whether to hide the menu item. * @param array $addon Addon data. * * @return bool */ public function should_hide_educational_menu_item( $hide, array $addon ): bool { return isset( $addon['clear_slug'] ) && $this->slug === $addon['clear_slug'] ? true : (bool) $hide; } /** * Get form data. * * @since 1.9.5 * * @return array */ private function get_form_data(): array { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $form_id = isset( $_GET['form_id'] ) ? absint( $_GET['form_id'] ) : 0; if ( ! $form_id ) { return []; } $form_data = wpforms()->obj( 'form' )->get( $form_id, [ 'content_only' => true, ] ); return is_array( $form_data ) ? $form_data : []; } /** * Check if payments enabled. * * @since 1.9.5 * * @return bool */ private function is_payments_enabled(): bool { return ! empty( $this->form_data['payments'][ $this->slug ]['enable'] ) || ! empty( $this->form_data['payments'][ $this->slug ]['enable_one_time'] ); } /** * Get single payments conditional logic for the Square settings panel. * * @since 1.9.5 * * @return string */ private function single_payments_conditional_logic_section(): string { return $this->get_conditional_logic_toggle(); } /** * Get education toggle for the conditional logic. * * @since 1.9.5 * * @param bool $is_recurring Is recurring section. * * @return string */ private function get_conditional_logic_toggle( bool $is_recurring = false ): string { return wpforms_panel_field( 'toggle', $this->slug, 'conditional_logic', $this->form_data, esc_html__( 'Enable Conditional Logic', 'wpforms-lite' ), [ 'value' => 0, 'input_class' => 'education-modal', 'parent' => 'payments', 'subsection' => $is_recurring ? 'recurring' : '', 'pro_badge' => ! Helpers::is_allowed_license_type(), 'data' => $this->get_conditional_logic_section_data(), 'attrs' => [ 'disabled' => 'disabled', ], ], false ); } /** * Get conditional logic section data. * * @since 1.9.5 * * @return array */ private function get_conditional_logic_section_data(): array { $addon = wpforms()->obj( 'addons' )->get_addon( $this->slug ); if ( empty( $addon ) || empty( $addon['action'] ) || empty( $addon['status'] ) || ( $addon['status'] === 'active' && $addon['action'] !== 'upgrade' ) ) { return []; } if ( $addon['plugin_allow'] && $addon['action'] === 'install' ) { return [ 'action' => 'install', 'message' => esc_html__( 'The Square Pro addon is required to enable conditional logic for payments. Would you like to install and activate it?', 'wpforms-lite' ), 'url' => $addon['url'], 'nonce' => wp_create_nonce( 'wpforms-admin' ), 'license' => 'pro', ]; } if ( $addon['plugin_allow'] && $addon['action'] === 'activate' ) { return [ 'action' => 'activate', 'message' => esc_html__( 'The Square Pro addon is required to enable conditional logic for payments. Would you like to activate it?', 'wpforms-lite' ), 'path' => $addon['path'], 'nonce' => wp_create_nonce( 'wpforms-admin' ), ]; } return [ 'action' => 'upgrade', 'name' => esc_html__( 'Smart Conditional Logic', 'wpforms-lite' ), 'utm-content' => 'Builder Square Conditional Logic', 'licence' => 'pro', ]; } /** * Get recurring payments conditional logic for the Square settings panel. * * @since 1.9.5 * * @param string $plan_id Plan ID. * * @return string */ private function recurring_payments_conditional_logic_section( string $plan_id ): string { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found return $this->get_conditional_logic_toggle( true ); } } Integrations/Square/Admin/Builder/Notifications.php 0000644 00000006326 15174710275 0016432 0 ustar 00 <?php namespace WPForms\Integrations\Square\Admin\Builder; use WPForms\Integrations\Square\Helpers; /** * Square Form Builder notifications-related functionality. * * @since 1.9.5 */ class Notifications { /** * Initialize. * * @since 1.9.5 */ public function init() { $this->hooks(); } /** * Register hooks. * * @since 1.9.5 */ private function hooks() { $hook_name = wpforms()->is_pro() ? 'wpforms_form_settings_notifications_single_after' : 'wpforms_lite_form_settings_notifications_block_content_after'; add_action( $hook_name, [ $this, 'notification_settings' ], 5, 2 ); } /** * Add checkbox to form notification settings. * * @since 1.9.5 * * @param object|mixed $settings Current confirmation settings. * @param int $id Subsection ID. */ public function notification_settings( $settings, int $id ) { if ( empty( $settings->form_data ) ) { return; } wpforms_panel_field( 'toggle', 'notifications', 'square', $settings->form_data, esc_html__( 'Enable for Square completed payments', 'wpforms-lite' ), $this->get_notification_settings_data( $settings->form_data, $id ) ); } /** * Get notification settings data based on the license type. * * @since 1.9.5 * * @param array $form_data Form settings data. * @param int $id Subsection ID. * * @return array */ private function get_notification_settings_data( array $form_data, int $id ): array { return [ 'parent' => 'settings', 'class' => ! Helpers::is_payments_enabled( $form_data ) ? 'wpforms-hidden' : '', 'subsection' => $id, 'value' => 0, 'input_class' => 'education-modal', 'pro_badge' => ! Helpers::is_allowed_license_type(), 'data' => $this->get_notification_section_data(), 'attrs' => [ 'disabled' => 'disabled' ], ]; } /** * Get notification section data. * * @since 1.9.5 * * @return array */ private function get_notification_section_data(): array { $addon = wpforms()->obj( 'addons' )->get_addon( 'square' ); if ( empty( $addon ) || empty( $addon['action'] ) || empty( $addon['status'] ) || ( $addon['status'] === 'active' && $addon['action'] !== 'upgrade' ) ) { return []; } if ( $addon['plugin_allow'] && $addon['action'] === 'install' ) { return [ 'action' => 'install', 'message' => esc_html__( 'The Square Pro addon is required to enable notification for completed payments. Would you like to install and activate it?', 'wpforms-lite' ), 'url' => $addon['url'], 'nonce' => wp_create_nonce( 'wpforms-admin' ), 'license' => 'pro', ]; } if ( $addon['plugin_allow'] && $addon['action'] === 'activate' ) { return [ 'action' => 'activate', 'message' => esc_html__( 'The Square Pro addon is required to enable notification for completed payments. Would you like to activate it?', 'wpforms-lite' ), 'path' => $addon['path'], 'nonce' => wp_create_nonce( 'wpforms-admin' ), ]; } return [ 'action' => 'upgrade', 'name' => esc_html__( 'Notification for Square Completed Payments', 'wpforms-lite' ), 'utm-content' => 'Builder Square Completed Payments', 'license' => 'pro', ]; } } Integrations/Square/Admin/Builder/Enqueues.php 0000644 00000006513 15174710275 0015411 0 ustar 00 <?php namespace WPForms\Integrations\Square\Admin\Builder; use WPForms\Integrations\Square\Helpers; /** * Script enqueues for the Square Builder settings panel. * * @since 1.9.5 */ class Enqueues { /** * Initialize. * * @since 1.9.5 */ public function init() { $this->hooks(); } /** * Builder hooks. * * @since 1.9.5 */ private function hooks() { add_action( 'wpforms_builder_enqueues', [ $this, 'enqueues' ] ); add_filter( 'wpforms_builder_strings', [ $this, 'javascript_strings' ] ); } /** * Enqueue assets for the builder. * * @since 1.9.5 */ public function enqueues() { $min = wpforms_get_min_suffix(); wp_enqueue_script( 'wpforms-builder-square', WPFORMS_PLUGIN_URL . "assets/js/integrations/square/admin/builder-square{$min}.js", [ 'wpforms-builder' ], WPFORMS_VERSION, true ); wp_enqueue_style( 'wpforms-square-placeholder', WPFORMS_PLUGIN_URL . "assets/css/integrations/square/wpforms-square-card-placeholder{$min}.css", [], WPFORMS_VERSION ); /** * Currently, we would like to limit number of the Square Credit Card fields * and allows only one field per form cause there is technical issue with the Web Payments SDK. */ wp_add_inline_style( 'wpforms-builder', '.wpforms-add-fields-group .wpforms-add-fields-button-disabled:hover { background-color: #036aab; cursor: no-drop; }' ); } /** * Add localized strings to be available in the form builder. * * @since 1.9.5 * * @param array $strings Form builder JS strings. * * @return array */ public function javascript_strings( $strings ): array { $strings = (array) $strings; $strings['square_connection_required'] = wp_kses( __( '<p>Square account connection is required when using the Square field.</p><p>To proceed, please go to <strong>WPForms Settings » Payments » Square</strong> and press <strong>Connect with Square</strong> button.</p>', 'wpforms-lite' ), [ 'p' => [], 'strong' => [], ] ); $strings['square_payments_enabled_required'] = wp_kses( __( '<p>Square Payments must be enabled when using the Square field.</p><p>To proceed, please go to <strong>Payments » Square</strong> and check <strong>Enable Square Payments</strong>.</p>', 'wpforms-lite' ), [ 'p' => [], 'strong' => [], ] ); $strings['square_ajax_required'] = wp_kses( __( '<p>AJAX form submissions are required when using the Square field.</p><p>To proceed, please go to <strong>Settings » General » Advanced</strong> and check <strong>Enable AJAX form submission</strong>.</p>', 'wpforms-lite' ), [ 'p' => [], 'strong' => [], ] ); $strings['square_recurring_payments_fields_heading'] = esc_html__( 'Missing Required Fields', 'wpforms-lite' ); $strings['square_recurring_payments_fields_required'] = esc_html__( 'When recurring subscription payments are enabled, the Customer Email and Customer Name are required.', 'wpforms-lite' ); $strings['square_recurring_payments_fields_settings'] = wp_kses( __( 'Please go to the <a href="#" class="wpforms-square-settings-redirect">Square payment settings</a> and select a Customer Email and Customer Name.', 'wpforms-lite' ), [ 'a' => [ 'href' => [], 'class' => [], ], ] ); $strings['square_is_pro'] = Helpers::is_pro(); return $strings; } } Integrations/Square/Admin/Builder/Traits/Content.php 0000644 00000036745 15174710275 0016511 0 ustar 00 <?php namespace WPForms\Integrations\Square\Admin\Builder\Traits; use WPForms\Integrations\Square\Admin\Notices; use WPForms\Integrations\Square\Connection; use WPForms\Integrations\Square\Helpers; /** * Payment builder settings content trait. * * @since 1.9.5 */ trait Content { /** * Display content inside the panel content area. * * @since 1.9.5 */ public function builder_content() { if ( $this->builder_alerts() ) { return; } $hide_class = ! Helpers::has_square_field( $this->form_data ) ? 'wpforms-hidden' : ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo Notices::get_fee_notice( $hide_class ); $this->maybe_convert_legacy_settings(); echo '<div id="wpforms-panel-content-section-payment-square" class="' . esc_attr( $hide_class ) . '">'; if ( ! Helpers::is_pro() ) { $this->builder_content_one_time(); $this->builder_content_recurring(); } else { parent::builder_content(); } echo '</div>'; } /** * Convert legacy settings if they exist. * * @since 1.9.5 */ private function maybe_convert_legacy_settings() { if ( empty( $this->form_data['payments']['square']['enable'] ) ) { return; } // Enable one-time payments if they were active. unset( $this->form_data['payments']['square']['enable'] ); $this->form_data['payments']['square']['enable_one_time'] = 1; } /** * Get content inside the one time payment area. * * @since 1.9.5 * * @return string */ protected function get_builder_content_one_time_content(): string { $content = wpforms_panel_field( 'text', 'square', 'payment_description', $this->form_data, esc_html__( 'Payment Description', 'wpforms-lite' ), [ 'parent' => 'payments', 'tooltip' => esc_html__( 'Enter your payment description. Eg: Donation for the soccer team.', 'wpforms-lite' ), ], false ); $content .= wpforms_panel_field( 'select', 'square', 'buyer_email', $this->form_data, esc_html__( 'Buyer Email', 'wpforms-lite' ), [ 'parent' => 'payments', 'field_map' => [ 'email' ], 'placeholder' => esc_html__( '--- Select Buyer Email ---', 'wpforms-lite' ), 'tooltip' => esc_html__( "Select the field that contains the buyer's email address. This field is optional.", 'wpforms-lite' ), ], false ); $content .= wpforms_panel_field( 'select', 'square', 'billing_name', $this->form_data, esc_html__( 'Billing Name', 'wpforms-lite' ), [ 'parent' => 'payments', 'field_map' => [ 'name' ], 'placeholder' => esc_html__( '--- Select Billing Name ---', 'wpforms-lite' ), 'tooltip' => esc_html__( "Select the field that contains the billing's name. This field is optional.", 'wpforms-lite' ), ], false ); $content .= $this->get_address_panel_fields(); $content .= $this->single_payments_conditional_logic_section(); return $content; } /** * Get content inside the recurring payment area. * * @since 1.9.5 * * @param string $plan_id Plan id. * * @return string */ protected function get_builder_content_recurring_payment_content( $plan_id ): string { $content = wpforms_panel_field( 'text', $this->slug, 'name', $this->form_data, esc_html__( 'Plan Name', 'wpforms-lite' ), [ 'parent' => 'payments', 'subsection' => 'recurring', 'index' => $plan_id, 'tooltip' => esc_html__( 'Enter the subscription name. Eg: Email Newsletter. Subscription period and price are automatically appended. If left empty the form name will be used.', 'wpforms-lite' ), 'class' => 'wpforms-panel-content-section-payment-plan-name', ], false ); $content .= wpforms_panel_field( 'select', $this->slug, 'phase_cadence', $this->form_data, esc_html__( 'Phase Cadence', 'wpforms-lite' ), [ 'parent' => 'payments', 'subsection' => 'recurring', 'index' => $plan_id, 'default' => 'yearly', 'options' => wp_list_pluck( Helpers::get_subscription_cadences(), 'name', 'slug' ), 'tooltip' => esc_html__( 'How often you would like the charge to recur.', 'wpforms-lite' ), ], false ); $is_empty_email = isset( $this->form_data['payments'][ $this->slug ]['recurring'][ $plan_id ]['customer_email'] ) && empty( $this->form_data['payments'][ $this->slug ]['recurring'][ $plan_id ]['customer_email'] ); $content .= wpforms_panel_field( 'select', $this->slug, 'customer_email', $this->form_data, esc_html__( 'Customer Email', 'wpforms-lite' ), [ 'parent' => 'payments', 'subsection' => 'recurring', 'index' => $plan_id, 'input_class' => $is_empty_email ? 'wpforms-required-field-error' : '', 'field_map' => [ 'email' ], 'placeholder' => esc_html__( '--- Select Email ---', 'wpforms-lite' ), 'tooltip' => esc_html__( "Select the field that contains the customer's email address. This field is required.", 'wpforms-lite' ), ], false ); $is_empty_name = isset( $this->form_data['payments'][ $this->slug ]['recurring'][ $plan_id ]['customer_name'] ) && empty( $this->form_data['payments'][ $this->slug ]['recurring'][ $plan_id ]['customer_name'] ); $content .= wpforms_panel_field( 'select', $this->slug, 'customer_name', $this->form_data, esc_html__( 'Customer Name', 'wpforms-lite' ), [ 'parent' => 'payments', 'subsection' => 'recurring', 'index' => $plan_id, 'input_class' => $is_empty_name ? 'wpforms-required-field-error' : '', 'field_map' => [ 'name' ], 'placeholder' => esc_html__( '--- Select Name ---', 'wpforms-lite' ), 'tooltip' => esc_html__( "Select the field that contains the customer's name. This field is required.", 'wpforms-lite' ), ], false ); $content .= $this->get_address_panel_fields( $plan_id ); $content .= $this->recurring_payments_conditional_logic_section( $plan_id ); return $content; } /** * Display Single payment content inside the panel content area. * * @since 1.9.5 */ private function builder_content_one_time() { ?> <div class="wpforms-panel-content-section-payment"> <h2 class="wpforms-panel-content-section-payment-subtitle"> <?php esc_html_e( 'One-Time Payments', 'wpforms-lite' ); ?> </h2> <?php wpforms_panel_field( 'toggle', $this->slug, 'enable_one_time', $this->form_data, esc_html__( 'Enable one-time payments', 'wpforms-lite' ), [ 'parent' => 'payments', 'default' => '0', 'tooltip' => esc_html__( 'Allow your customers to one-time pay via the form.', 'wpforms-lite' ), 'class' => 'wpforms-panel-content-section-payment-toggle wpforms-panel-content-section-payment-toggle-one-time', ] ); ?> <div class="wpforms-panel-content-section-payment-one-time wpforms-panel-content-section-payment-toggled-body"> <?php echo $this->get_builder_content_one_time_content(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> </div> </div> <?php } /** * Builder content for recurring payments. * * @since 1.9.5 */ private function builder_content_recurring() { ?> <div class="wpforms-panel-content-section-payment"> <h2 class="wpforms-panel-content-section-payment-subtitle"> <?php esc_html_e( 'Recurring Payments ', 'wpforms-lite' ); ?> </h2> <?php $this->add_plan_education(); wpforms_panel_field( 'toggle', $this->slug, 'enable_recurring', $this->form_data, esc_html__( 'Enable recurring subscription payments', 'wpforms-lite' ), [ 'parent' => 'payments', 'default' => '0', 'tooltip' => esc_html__( 'Allow your customer to pay recurringly via the form.', 'wpforms-lite' ), 'class' => 'wpforms-panel-content-section-payment-toggle wpforms-panel-content-section-payment-toggle-recurring', ] ); ?> <div class="wpforms-panel-content-section-payment-recurring wpforms-panel-content-section-payment-toggled-body"> <?php if ( empty( $this->form_data['payments'][ $this->slug ]['recurring'] ) ) { $this->form_data['payments'][ $this->slug ]['recurring'][] = []; } foreach ( $this->form_data['payments'][ $this->slug ]['recurring'] as $plan_id => $plan_settings ) { // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo wpforms_render( 'builder/payment/recurring/item', [ 'plan_id' => $plan_id, 'content' => $this->get_builder_content_recurring_payment_content( $plan_id ), ], true ); break; } ?> </div> </div> <?php } /** * Add new plan education modals. * * @since 1.9.5 */ private function add_plan_education() { $label = __( 'Add New Plan', 'wpforms-lite' ); if ( ! Helpers::is_allowed_license_type() ) { echo '<a href="#" class="wpforms-panel-content-section-payment-button wpforms-panel-content-section-payment-button-add-plan education-modal" data-action="upgrade" data-name="' . esc_attr__( 'Multiple Subscriptions', 'wpforms-lite' ) . '" >' . esc_html( $label ) . '</a>'; return; } $addon = wpforms()->obj( 'addons' )->get_addon( 'wpforms-square' ); if ( empty( $addon ) ) { return; } echo '<a href="#" class="wpforms-panel-content-section-payment-button wpforms-panel-content-section-payment-button-add-plan education-modal" data-action="' . esc_attr( $addon['action'] ) . '" data-path="' . esc_attr( $addon['path'] ) . '" data-slug="' . esc_attr( $addon['slug'] ) . '" data-url="' . esc_url( $addon['url'] ) . '" data-nonce="' . esc_attr( wp_create_nonce( 'wpforms-admin' ) ) . '" data-name="' . esc_attr__( 'Square Pro', 'wpforms-lite' ) . '" >' . esc_html( $label ) . '</a>'; } /** * Get address panel fields. * * @since 1.9.5 * * @param string|null $plan_id Plan ID. * * @return string */ private function get_address_panel_fields( $plan_id = null ): string { $args = [ 'parent' => 'payments', 'field_map' => [ 'address' ], ]; $is_pro = wpforms()->is_pro(); if ( ! $is_pro ) { $args['pro_badge'] = true; $args['data'] = [ 'action' => 'upgrade', 'name' => esc_html__( 'Customer Address', 'wpforms-lite' ), 'utm-content' => 'Builder Square Address Field', 'licence' => 'pro', ]; $args['input_class'] = 'education-modal'; $args['readonly'] = true; } else { $args['tooltip'] = esc_html__( 'Select the field that contains the customer\'s Address. This field is optional.', 'wpforms-lite' ); } // Check if subscription. if ( ! is_null( $plan_id ) ) { $args['placeholder'] = esc_html__( '--- Select Address ---', 'wpforms-lite' ); $args['subsection'] = 'recurring'; $args['index'] = $plan_id; return wpforms_panel_field( 'select', $this->slug, 'customer_address', $this->form_data, esc_html__( 'Customer Address', 'wpforms-lite' ), $args, false ); } if ( ! $is_pro ) { $args['data']['name'] = esc_html__( 'Billing Address', 'wpforms-lite' ); } else { $args['tooltip'] = esc_html__( 'Select the field that contains the billing\'s address. This field is optional.', 'wpforms-lite' ); } $args['placeholder'] = esc_html__( '--- Select Billing Address ---', 'wpforms-lite' ); return wpforms_panel_field( 'select', $this->slug, 'billing_address', $this->form_data, esc_html__( 'Billing Address', 'wpforms-lite' ), $args, false ); } /** * Check if connection exists and ready to use. * * @since 1.9.5 * * @return bool */ private function builder_alerts(): bool { $connection = Connection::get(); if ( ! $connection ) { $this->alert_content( __( 'Heads up! Square payments can\'t be enabled yet.', 'wpforms-lite' ), sprintf( wp_kses( /* translators: %s - Admin area Payments settings page URL. */ __( "First, please connect to your Square account on the <a href='%s'>WPForms Settings</a> page.", 'wpforms-lite' ), [ 'a' => [ 'href' => [], ], ] ), esc_url( Helpers::get_settings_page_url() . '#wpforms-setting-row-square-heading' ) ) ); return true; } if ( ! $connection->is_usable() ) { $this->alert_content( __( 'Square payments can\'t be processed because there\'s a problem with the account connection.', 'wpforms-lite' ), sprintf( wp_kses( /* translators: %s - the WPForms Payments settings page URL. */ __( "First, please resolve the connection issue on the <a href='%2\$s'>Payment Settings</a> page.", 'wpforms-lite' ), [ 'a' => [ 'href' => [], ], ] ), Helpers::is_sandbox_mode() ? esc_html__( 'Sandbox', 'wpforms-lite' ) : esc_html__( 'Production', 'wpforms-lite' ), esc_url( Helpers::get_settings_page_url() . '#wpforms-setting-row-square-heading' ) ) ); return true; } if ( $connection->is_expired() ) { $this->alert_content( __( 'Heads up! Square account connection is expired.', 'wpforms-lite' ), sprintf( wp_kses( /* translators: %s - the WPForms Payments settings page URL. */ __( "Tokens must be refreshed. Please refresh them on the <a href='%2\$s'>WPForms Settings</a> page.", 'wpforms-lite' ), [ 'a' => [ 'href' => [], ], ] ), Helpers::is_sandbox_mode() ? esc_html__( 'Sandbox', 'wpforms-lite' ) : esc_html__( 'Production', 'wpforms-lite' ), esc_url( Helpers::get_settings_page_url() . '#wpforms-setting-row-square-heading' ) ) ); return true; } $this->credit_card_alert(); return false; } /** * Display alert content. * * @since 1.9.5 * * @param string $title Alert title. * @param string $message Alert message. */ private function alert_content( string $title, string $message ) { ?> <?php $this->alert_icon(); ?> <div class="wpforms-builder-payment-settings-default-content"> <?php if ( ! empty( $title ) ) : ?> <p class="wpforms-builder-payment-settings-error-title"> <?php echo esc_html( $title ); ?> </p> <?php endif; ?> <p> <?php echo $message; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> </p> <p class="wpforms-builder-payment-settings-learn-more"> <?php echo $this->learn_more_link(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> </p> </div> <?php } /** * Display alert if Square Credit Card field is not added to the form. * * @since 1.9.5 */ private function credit_card_alert() { $hide_class = Helpers::has_square_field( $this->form_data ) ? 'wpforms-hidden' : ''; ?> <div id="wpforms-<?php echo esc_attr( $this->slug ); ?>-credit-card-alert" class="wpforms-alert wpforms-alert-info <?php echo esc_attr( $hide_class ); ?>"> <?php $this->alert_content( '', esc_html__( 'To use Square, first add the Square payment field to your form.', 'wpforms-lite' ) ); ?> </div> <?php } /** * Alert icon. * * @since 1.9.5 */ private function alert_icon() { printf( '<img src="%1$s" class="wpforms-builder-payment-settings-alert-icon" alt="%2$s">', esc_url( $this->icon ), esc_attr__( 'Connect WPForms to Square.', 'wpforms-lite' ) ); } /** * Learn more link. * * @since 1.9.5 * * @return string */ private function learn_more_link(): string { return sprintf( '<a href="%1$s" target="_blank" rel="noopener noreferrer" class="secondary-text">%2$s</a>', esc_url( wpforms_utm_link( 'https://wpforms.com/docs/how-to-install-and-use-the-square-addon-with-wpforms/', 'builder-payments', 'Square Documentation' ) ), esc_html__( 'Learn more about our Square integration.', 'wpforms-lite' ) ); } } Integrations/Square/Admin/Payments/SingleActionsHandler.php 0000644 00000010226 15174710275 0020065 0 ustar 00 <?php namespace WPForms\Integrations\Square\Admin\Payments; use WPForms\Db\Payments\UpdateHelpers; use WPForms\Integrations\Square\Api\Api; use WPForms\Integrations\Square\Connection; use WPForms\Integrations\Square\Helpers; /** * Things related to Square functionality on single payment screen. * * @since 1.9.5 */ class SingleActionsHandler { /** * Main class that communicates with the Square API. * * @since 1.9.5 * * @var Api */ private $api; /** * Initialize. * * @since 1.9.5 */ public function init() { // Set an API instance. $this->api = new Api( Connection::get() ); $this->hooks(); } /** * Register hooks. * * @since 1.9.5 */ private function hooks() { if ( wpforms_is_admin_ajax() ) { add_action( 'wp_ajax_wpforms_square_payments_refund', [ $this, 'ajax_payment_refund' ] ); add_action( 'wp_ajax_wpforms_square_payments_cancel', [ $this, 'ajax_payments_cancel' ] ); return; } add_filter( 'wpforms_admin_strings', [ $this, 'admin_strings' ] ); } /** * Add admin strings related to payments. * * @since 1.9.5 * * @param array $admin_strings Admin strings. * * @return array */ public function admin_strings( $admin_strings ): array { $admin_strings = (array) $admin_strings; $admin_strings['single_payment_button_handlers'][] = 'square'; return $admin_strings; } /** * Refund a single payment. * * Handler for ajax request with action "wpforms_payments_refund". * * @since 1.9.5 */ public function ajax_payment_refund() { $payment_db = $this->get_db_payment(); $amount_to_refund = $payment_db->total_amount; if ( $payment_db->status === 'partrefund' ) { $already_refunded = wpforms()->obj( 'payment_meta' )->get_single( $payment_db->id, 'refunded_amount' ); $amount_to_refund = $payment_db->total_amount - $already_refunded; } $args = [ 'amount' => Helpers::format_amount( $amount_to_refund ), 'currency' => $payment_db->currency, 'reason' => 'Requested by customer', ]; $refund = $this->api->refund_payment( $payment_db->transaction_id, $args ); if ( ! $refund ) { wp_send_json_error( [ 'message' => esc_html__( 'Refund failed.', 'wpforms-lite' ) ] ); } $log = sprintf( 'Square payment refunded from the WPForms plugin interface. Refunded amount: %1$s.', wpforms_format_amount( wpforms_sanitize_amount( $amount_to_refund ), true ) ); if ( UpdateHelpers::refund_payment( $payment_db, $payment_db->total_amount, $log ) ) { wp_send_json_success( [ 'message' => esc_html__( 'Refund successful.', 'wpforms-lite' ) ] ); } wp_send_json_error( [ 'message' => esc_html__( 'Saving refund in the database failed.', 'wpforms-lite' ) ] ); } /** * Cancel subscription. * * Handler for ajax request with action "wpforms_payments_cancel_subscription". * * @since 1.9.5 */ public function ajax_payments_cancel() { $payment_db = $this->get_db_payment(); $cancel = $this->api->cancel_subscription( $payment_db->subscription_id ); if ( ! $cancel ) { wp_send_json_error( [ 'message' => esc_html__( 'Subscription cancellation failed.', 'wpforms-lite' ) ] ); } if ( UpdateHelpers::cancel_subscription( $payment_db->id, 'Square subscription cancelled from the WPForms plugin interface.' ) ) { wp_send_json_success( [ 'message' => esc_html__( 'Subscription cancelled.', 'wpforms-lite' ) ] ); } wp_send_json_error( [ 'message' => esc_html__( 'Updating subscription in the database failed.', 'wpforms-lite' ) ] ); } /** * Retrieve the payment from the database. * * @since 1.9.5 * * @return object|null */ private function get_db_payment() { if ( ! isset( $_POST['payment_id'] ) || ! wpforms_current_user_can( wpforms_get_capability_manage_options() ) ) { wp_send_json_error( [ 'message' => esc_html__( 'You are not allowed to perform this action.', 'wpforms-lite' ) ] ); } check_ajax_referer( 'wpforms-admin', 'nonce' ); $payment_id = (int) $_POST['payment_id']; $payment_db = wpforms()->obj( 'payment' )->get( $payment_id ); if ( empty( $payment_db ) ) { wp_send_json_error( [ 'message' => esc_html__( 'Payment not found in the database.', 'wpforms-lite' ) ] ); } return $payment_db; } } Integrations/Square/Admin/Notices.php 0000644 00000016062 15174710275 0013635 0 ustar 00 <?php namespace WPForms\Integrations\Square\Admin; use WPForms\Admin\Notice; use WPForms\Integrations\Square\Helpers; use WPForms\Integrations\Square\Connection; use WPForms\Integrations\Square\WebhooksHealthCheck; /** * Square related admin notices. * * @since 1.9.5 */ class Notices { /** * Initialize. * * @since 1.9.5 * * @return Notices */ public function init() { $this->hooks(); return $this; } /** * Register hooks. * * @since 1.9.5 */ private function hooks() { add_action( 'wpforms_settings_init', [ $this, 'display_notice' ] ); } /** * Display admin error notice if something wrong with the Square settings. * * @since 1.9.5 */ public function display_notice() { $connection = Connection::get(); if ( ! $connection ) { return; } $this->maybe_display_notice( $connection ); } /** * Maybe display admin notices in the settings area. * * @since 1.9.5 * * @param Connection $connection Connection data. */ private function maybe_display_notice( Connection $connection ) { $all_notices = array_filter( [ $this->maybe_get_notice( $connection ), $this->maybe_get_webhook_notice(), ] ); if ( empty( $all_notices ) ) { return; } // Notice header. $message = sprintf( '<strong>%s</strong>', esc_html__( 'There Are Some Problems With Your Square Connection', 'wpforms-lite' ) ); foreach ( $all_notices as $notice ) { $message .= '<br/>' . $notice; } // Display the notice. Notice::error( $message ); } /** * Maybe get admin error notice if a connection exists, but is not ready to use. * * @since 1.9.5 * * @param Connection $connection Connection object. * * @return string Notice. */ private function maybe_get_notice( Connection $connection ): string { if ( ! $connection->is_configured() ) { return esc_html__( 'Square account connection is missing required data. You must reconnect your Square account.', 'wpforms-lite' ); } if ( ! $connection->is_valid() ) { return esc_html__( 'Square account connection is invalid. You must reconnect your Square account.', 'wpforms-lite' ); } if ( $connection->is_expired() ) { return esc_html__( 'Square account connection is expired. Tokens must be refreshed.', 'wpforms-lite' ); } if ( empty( Helpers::get_location_id() ) ) { return esc_html__( 'Business Location is required to process Square payments.', 'wpforms-lite' ); } if ( $connection->is_currency_matched() ) { return ''; } return sprintf( /* translators: %1$s - Selected currency on the WPForms Settings admin page; %2$s - Currency of a business location. */ esc_html__( 'The currency you have set (%1$s) does not match the currency of your Square business location (%2$s). Please choose a different business location or update your WPForms currency to %2$s.', 'wpforms-lite' ), esc_html( wpforms_get_currency() ), esc_html( $connection->get_currency() ) ); } /** * Maybe get webhook notice if connection is not set. * * @since 1.9.5 * * @return string */ private function maybe_get_webhook_notice(): string { // Bail out if webhooks are not enabled. if ( ! Helpers::is_webhook_enabled() ) { return ''; } // Bail out if webhooks are configured and active. if ( Helpers::is_webhook_configured() ) { return ''; } // If ENDPOINT_OPTION is set, it says that webhooks were configured previously. We have another notice for this case. if ( get_option( WebhooksHealthCheck::ENDPOINT_OPTION ) ) { return ''; } return esc_html__( 'Webhooks are enabled, but not yet connected.', 'wpforms-lite' ); } /** * Get a notice if a license is insufficient not to be charged a fee. * * @since 1.9.5 * * @param string $classes Additional notice classes. * * @return string */ public static function get_fee_notice( string $classes = '' ): string { if ( ! Helpers::is_application_fee_supported() ) { return ''; } $is_allowed_license = Helpers::is_allowed_license_type(); $is_active_license = Helpers::is_license_active(); $notice = ''; if ( $is_allowed_license && $is_active_license ) { return $notice; } if ( ! $is_allowed_license ) { $notice = self::get_non_pro_license_level_notice(); } elseif ( ! $is_active_license ) { $notice = self::get_non_active_license_notice(); } if ( wpforms_is_admin_page( 'builder' ) ) { return sprintf( '<p class="wpforms-square-notice-info wpforms-alert wpforms-alert-info ' . wpforms_sanitize_classes( $classes ) . '">%s</p>', $notice ); } return sprintf( '<div class="wpforms-square-notice-info ' . wpforms_sanitize_classes( $classes ) . '"><p>%s</p></div>', $notice ); } /** * Get a fee notice for a non-active license. * * If the license is NOT set/activated, show the notice to activate it. * Otherwise, show the notice to renew it. * * @since 1.9.5 * * @return string */ private static function get_non_active_license_notice(): string { $setting_page_url = add_query_arg( [ 'page' => 'wpforms-settings', 'view' => 'general', ], admin_url( 'admin.php' ) ); // The license is not set/activated at all. if ( empty( wpforms_get_license_key() ) ) { return sprintf( wp_kses( /* translators: %s - general admin settings page URL. */ __( '<strong>Pay-as-you-go Pricing</strong><br>3%% fee per-transaction + Square fees. <a href="%s">Activate your license</a> to remove additional fees and unlock powerful features.', 'wpforms-lite' ), [ 'strong' => [], 'br' => [], 'a' => [ 'href' => [], 'target' => [], ], ] ), esc_url( $setting_page_url ) ); } return sprintf( wp_kses( /* translators: %s - general admin settings page URL. */ __( '<strong>Pay-as-you-go Pricing</strong><br> 3%% fee per-transaction + Square fees. <a href="%s">Renew your license</a> to remove additional fees and unlock powerful features.', 'wpforms-lite' ), [ 'strong' => [], 'br' => [], 'a' => [ 'href' => [], 'target' => [], ], ] ), esc_url( $setting_page_url ) ); } /** * Get a fee notice for license levels below the `pro`. * * Show the notice to upgrade to Pro. * * @since 1.9.5 * * @return string */ private static function get_non_pro_license_level_notice(): string { $utm_content = 'Square Pro - Remove Fees'; $utm_medium = wpforms_is_admin_page( 'builder' ) ? 'Payment Settings' : 'Settings - Payments'; $upgrade_link = wpforms()->is_pro() ? wpforms_admin_upgrade_link( $utm_medium, $utm_content ) : wpforms_utm_link( 'https://wpforms.com/lite-upgrade/', $utm_medium, $utm_content ); return sprintf( wp_kses( /* translators: %s - WPForms.com Upgrade page URL. */ __( '<strong>Pay-as-you-go Pricing</strong><br> 3%% fee per-transaction + Square fees. <a href="%s" target="_blank">Upgrade to Pro</a> to remove additional fees and unlock powerful features.', 'wpforms-lite' ), [ 'strong' => [], 'br' => [], 'a' => [ 'href' => [], 'target' => [], ], ] ), esc_url( $upgrade_link ) ); } } Integrations/Square/Admin/Connect.php 0000644 00000046754 15174710275 0013635 0 ustar 00 <?php namespace WPForms\Integrations\Square\Admin; use WPForms\Integrations\Square\Api\Api; use WPForms\Vendor\Square\Models\Location; use WPForms\Vendor\Square\Models\LocationCapability; use WPForms\Vendor\Square\Models\LocationStatus; use WPForms\Vendor\Square\Environment; use WP_Error; use WPForms\Admin\Notice; use WPForms\Helpers\Transient; use WPForms\Tasks\Tasks; use WPForms\Integrations\Square\Connection; use WPForms\Integrations\Square\Helpers; use WPForms\Integrations\Square\Api\WebhooksManager; /** * Square Connect functionality. * * @since 1.9.5 */ class Connect { /** * WPForms website URL. * * @since 1.9.5 */ private const WPFORMS_URL = 'https://wpforms.com'; /** * Webhooks manager. * * @since 1.9.5 * * @var WebhooksManager */ private $webhooks_manager; /** * Initialize. * * @since 1.9.5 * * @return Connect */ public function init() { $this->webhooks_manager = new WebhooksManager(); $this->hooks(); return $this; } /** * Connect hooks. * * @since 1.9.5 */ private function hooks() { add_action( 'admin_init', [ $this, 'handle_actions' ] ); add_action( 'wpforms_square_refresh_connection', [ $this, 'refresh_connection_schedule' ] ); add_action( 'wp_ajax_wpforms_square_refresh_connection', [ $this, 'refresh_connection_manual' ] ); add_action( 'wp_ajax_wpforms_square_create_webhook', [ $this->webhooks_manager, 'connect' ] ); } /** * Handle actions. * * @since 1.9.5 */ public function handle_actions() { if ( ! wpforms_current_user_can() || wp_doing_ajax() ) { return; } $this->validate_scopes(); if ( isset( $_GET['_wpnonce'] ) && wp_verify_nonce( sanitize_key( $_GET['_wpnonce'] ), 'wpforms_square_disconnect' ) ) { $this->handle_disconnect(); return; } $this->schedule_refresh(); if ( ! empty( $_GET['state'] ) && isset( $_GET['square_connect'] ) && $_GET['square_connect'] === 'complete' ) { $this->handle_connected(); } } /** * Validate connection scopes. * * @since 1.9.5 */ private function validate_scopes() { if ( Helpers::is_license_ok() ) { return; } $connection = Connection::get(); if ( ! $connection || ! $connection->is_configured() ) { return; } // Bail early if currency is not supported for applying a fee. if ( ! Helpers::is_application_fee_supported() ) { return; } if ( $connection->get_scopes_updated() ) { return; } // Revoke tokens if the license is not valid and scopes are missing. $connection->revoke_tokens(); } /** * Handle a successful connection. * * @since 1.9.5 */ private function handle_connected() { $state = sanitize_text_field( wp_unslash( $_GET['state'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotValidated if ( empty( $state ) ) { return; } $connection_raw = $this->fetch_new_connection( $state ); $connection = $this->maybe_save_connection( $connection_raw ); if ( ! $connection ) { return; } $mode = $connection->get_mode(); // Sync the Square settings mode with a connection mode. Helpers::set_mode( $mode ); $this->prepare_locations( $mode ); $redirect_url = Helpers::get_settings_page_url(); if ( ! $connection->is_usable() ) { $redirect_url .= '#wpforms-setting-row-square-heading'; } wp_safe_redirect( $redirect_url ); exit; } /** * Handle disconnection. * * @since 1.9.5 */ private function handle_disconnect() { $live_mode = isset( $_GET['live_mode'] ) ? absint( $_GET['live_mode'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended $mode = $live_mode ? Environment::PRODUCTION : Environment::SANDBOX; $connection = Connection::get( $mode ); if ( $connection ) { $connection->delete(); } if ( Helpers::is_production_mode() ) { $this->unschedule_refresh(); } if ( Helpers::is_webhook_enabled() ) { Helpers::reset_webhook_configuration( true ); } Helpers::set_locataion_id( '', $mode ); Helpers::detete_transients( $mode ); wp_safe_redirect( Helpers::get_settings_page_url() ); exit; } /** * Handle refresh connection triggered by AS task. * * @since 1.9.5 */ public function refresh_connection_schedule() { // Don't run refresh tokens for Sandbox connection. if ( Helpers::is_sandbox_mode() ) { return; } $connection = Connection::get(); // Check connection and cancel AS task if connection is not exists, broken OR already invalid. if ( ! $connection || ! $connection->is_configured() || ! $connection->is_valid() ) { $this->unschedule_refresh(); return; } // If connection is not expired, we'll just fetch active locations. if ( ! $connection->is_expired() ) { $this->prepare_locations( $connection->get_mode() ); return; } // If connection is expired, try to refresh tokens. $connection = $this->try_refresh_connection( $connection ); if ( is_wp_error( $connection ) ) { return; } // If connection is invalid, we'll cancel AS task. if ( $connection && ! $connection->is_valid() ) { $this->unschedule_refresh(); return; } // Make sure and check connection tokens through fetching active locations. $this->prepare_locations( $connection->get_mode() ); } /** * Handle refresh connection triggered manually. * * @since 1.9.5 */ public function refresh_connection_manual() { // Security and permissions check. if ( ! check_ajax_referer( 'wpforms-admin', 'nonce', false ) || ! wpforms_current_user_can() ) { wp_send_json_error( esc_html__( 'You are not allowed to perform this action', 'wpforms-lite' ) ); } $error_general = esc_html__( 'Something went wrong while performing a refresh tokens request', 'wpforms-lite' ); // Required data check. if ( empty( $_POST['mode'] ) ) { wp_send_json_error( $error_general ); } $mode = sanitize_key( $_POST['mode'] ); $connection = Connection::get( $mode ); // Connection check. if ( ! $connection || ! $connection->is_configured() ) { wp_send_json_error( $error_general ); } // Try to refresh connection. $connection = $this->try_refresh_connection( $connection ); if ( is_wp_error( $connection ) ) { $error_specific = $connection->get_error_message(); $error_message = empty( $error_specific ) ? $error_general : $error_general . ': ' . $error_specific; wp_send_json_error( $error_message ); } $this->prepare_locations( $mode ); wp_send_json_success(); } /** * Try to refresh connection. * * @since 1.9.5 * * @param Connection $connection Connection object. * * @return Connection|WP_Error */ private function try_refresh_connection( $connection ) { $response = $this->fetch_refresh_connection( $connection->get_refresh_token(), $connection->get_mode() ); if ( is_wp_error( $response ) ) { if ( $response->get_error_code() === 'refresh_connection_fail' && $connection->is_valid() ) { $connection ->set_status( Connection::STATUS_INVALID ) ->save(); } return $response; } $refreshed_connection = $this->maybe_save_connection( $response, true ); return $refreshed_connection ?? new WP_Error(); } /** * Schedule the connection refresh. * * @since 1.9.5 */ private function schedule_refresh() { /** * Allow modifying a condition check where the AS task will be registered. * * @since 1.9.5 * * @param int $interval The refresh interval. */ if ( (bool) apply_filters( 'wpforms_square_admin_connect_schedule_refresh_prevent_task_registration', ! wpforms_is_admin_page() ) ) { // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName return; } $tasks = wpforms()->obj( 'tasks' ); if ( is_null( $tasks ) ) { return; } if ( $tasks->is_scheduled( 'wpforms_square_refresh_connection' ) !== false ) { return; } // Register AS task only if a Production connection exists. if ( ! Connection::get( Environment::PRODUCTION ) ) { return; } /** * Filter the frequency with which the OAuth connection should be refreshed. * * @since 1.9.5 * * @param int $interval The refresh interval. */ $interval = (int) apply_filters( 'wpforms_square_admin_connect_schedule_refresh_interval', 12 * HOUR_IN_SECONDS ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName $tasks->create( 'wpforms_square_refresh_connection' ) ->recurring( time() + $interval, $interval ) ->register(); } /** * Unschedule the connection refresh. * * @since 1.9.5 */ private function unschedule_refresh() { // Exit if AS function does not exist. if ( ! function_exists( 'as_unschedule_all_actions' ) ) { return; } as_unschedule_all_actions( 'wpforms_square_refresh_connection', [ 'tasks_meta_id' => null ], Tasks::GROUP ); } /** * Check connection raw data and save it if everything is OK. * * @since 1.9.5 * * @param array $raw Connection raw data. * @param bool $silent Optional. Whether to prevent showing admin notices. Default false. * * @return Connection|null */ private function maybe_save_connection( array $raw, bool $silent = false ) { $connection = new Connection( $raw, false ); // Bail if a connection doesn't have required data. if ( ! $connection->is_configured() ) { $silent ? wpforms_log( 'Square error', 'We could not connect to Square. No tokens were given.', [ 'type' => [ 'payment', 'error' ], ] ) : Notice::error( esc_html__( 'Square Error: We could not connect to Square. No tokens were given.', 'wpforms-lite' ) ); return null; } // Prepare connection for save. $connection ->set_renew_at() ->set_scopes_updated() ->encrypt_tokens(); // Bail if a connection is not ready for save. if ( ! $connection->is_saveable() ) { $silent ? wpforms_log( 'Square error', 'We could not save an account connection safely. Please, try again later.', [ 'type' => [ 'payment', 'error' ], ] ) : Notice::error( esc_html__( 'Square Error: We could not save an account connection safely. Please, try again later.', 'wpforms-lite' ) ); return null; } $connection->save(); return $connection; } /** * Prepare Square business locations. * * @since 1.9.5 * * @param string $mode Square mode. * * @return array */ private function prepare_locations( string $mode ): array { $locations = $this->fetch_locations( $mode ); if ( $locations === null ) { $this->reset_location( $mode ); Transient::delete( 'wpforms_square_active_locations_' . $mode ); return []; } $locations = $this->active_locations_filter( $locations ); if ( empty( $locations ) ) { $this->reset_location( $mode ); Transient::set( 'wpforms_square_active_locations_' . $mode, [] ); return []; } $this->set_location( $locations, $mode ); Transient::set( 'wpforms_square_active_locations_' . $mode, $locations ); return $locations; } /** * Fetch Square business locations. * * @since 1.9.5 * * @param string $mode Square mode. * * @return array|null */ private function fetch_locations( string $mode ) { $connection = Connection::get( $mode ); if ( ! $connection ) { return null; } $api = new Api( $connection ); $locations = $api->get_locations(); if ( ! $locations ) { $connection ->set_status( Connection::STATUS_INVALID ) ->save(); return null; } return is_array( $locations ) ? $locations : [ $locations ]; } /** * Fetch Square seller account from Square. * * @since 1.9.5 * * @param string $mode Square mode. * * @return array|null */ private function fetch_account( string $mode ) { $connection = Connection::get( $mode ); if ( ! $connection ) { return null; } $api = new Api( $connection ); $merchant = $api->get_merchant( $connection->get_merchant_id() ); if ( ! $merchant ) { return null; } return $merchant->jsonSerialize(); } /** * Fetch new connection credentials. * * @since 1.9.5 * * @param string $state Unique ID to safely fetch connection data. * * @return array */ private function fetch_new_connection( string $state ): array { $connection = []; $response = wp_remote_post( $this->get_server_url() . '/oauth/square-connect', [ 'body' => [ 'action' => 'credentials', 'state' => $state, ], 'timeout' => 30, ] ); if ( ! is_wp_error( $response ) && wp_remote_retrieve_response_code( $response ) === 200 ) { $body = json_decode( wp_remote_retrieve_body( $response ), true ); $connection = is_array( $body ) ? $body : []; } return $connection; } /** * Fetch refresh connection credentials. * * @since 1.9.5 * * @param string $token The refresh token. * @param string $mode Square mode. * * @return array|WP_Error */ private function fetch_refresh_connection( string $token, string $mode ) { $response = wp_remote_post( $this->get_server_url() . '/oauth/square-connect', [ 'body' => [ 'action' => 'refresh', 'live_mode' => absint( $mode === Environment::PRODUCTION ), 'token' => $token, ], 'timeout' => 30, ] ); if ( wp_remote_retrieve_response_code( $response ) !== 200 ) { return new WP_Error(); } $body = json_decode( wp_remote_retrieve_body( $response ), true ); if ( ! is_array( $body ) ) { return new WP_Error(); } if ( ! empty( $body['success'] ) ) { return $body; } $error_message = empty( $body['message'] ) ? '' : wp_kses_post( $body['message'] ); return new WP_Error( 'refresh_connection_fail', $error_message ); } /** * Retrieve active business locations with processing capability. * * @since 1.9.5 * * @param array $locations Locations. * * @return array */ private function active_locations_filter( array $locations ): array { $active_locations = []; if ( empty( $locations ) ) { return $active_locations; } foreach ( $locations as $location ) { if ( ! $location instanceof Location || $location->getStatus() !== LocationStatus::ACTIVE || ! is_array( $location->getCapabilities() ) || ! in_array( LocationCapability::CREDIT_CARD_PROCESSING, $location->getCapabilities(), true ) ) { continue; } $location_id = $location->getId(); $active_locations[ $location_id ] = [ 'id' => $location_id, 'name' => $location->getName(), 'currency' => $location->getCurrency(), ]; } return $active_locations; } /** * Set/update location things: ID and currency. * * @since 1.9.5 * * @param array $locations Active locations. * @param string $mode Square mode. */ private function set_location( array $locations, string $mode ) { $connection = Connection::get( $mode ); $stored_location_id = Helpers::get_location_id( $mode ); // Location ID was not set previously or saved ID is not available now. if ( empty( $stored_location_id ) || ! isset( $locations[ $stored_location_id ] ) ) { $stored_location_id = Helpers::array_key_first( $locations ); // Set a new location ID. Helpers::set_locataion_id( $stored_location_id, $mode ); } // Set location currency for connection. // In this case, we can make sure that location currency is matched with WPForms currency. if ( $connection !== null ) { $connection->set_currency( $locations[ $stored_location_id ]['currency'] )->save(); } } /** * Reset location ID and currency if no locations are received. * * @since 1.9.5 * * @param string $mode Square mode. */ private function reset_location( string $mode ) { Helpers::set_locataion_id( '', $mode ); $connection = Connection::get( $mode ); if ( ! $connection ) { return; } $connection->set_currency( '' )->save(); } /** * Get cached business locations or fetch it from Square. * * @since 1.9.5 * * @param string $mode Square mode. * * @return array */ public function get_connected_locations( string $mode ): array { $mode = Helpers::validate_mode( $mode ); $locations = Transient::get( 'wpforms_square_active_locations_' . $mode ); if ( empty( $locations ) ) { $locations = $this->prepare_locations( $mode ); } return $locations; } /** * Get cached Square seller account or fetch it from Square. * * @since 1.9.5 * * @param string $mode Square mode. * * @return array|null */ public function get_connected_account( string $mode ) { $mode = Helpers::validate_mode( $mode ); $account = Transient::get( 'wpforms_square_account_' . $mode ); if ( empty( $account['id'] ) ) { $account_id = $this->get_connected_account_id( $mode ); if ( ! $account_id ) { return null; } $account = $this->fetch_account( $mode ); if ( empty( $account['id'] ) || $account['id'] !== $account_id ) { return null; } Transient::set( 'wpforms_square_account_' . $mode, $account ); } return $account; } /** * Retrieve saved Square seller account ID from DB. * * @since 1.9.5 * * @param string $mode Square mode. * * @return string */ public function get_connected_account_id( string $mode ): string { $connection = Connection::get( $mode ); $account_id = $connection ? $connection->get_merchant_id() : ''; /** * Filter the connected account ID. * * @since 1.9.5 * * @param string $account_id Square account ID. * @param string $mode Square mode. */ return (string) apply_filters( 'wpforms_square_admin_connect_get_connected_account_id', $account_id, $mode ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName } /** * Retrieve the connect URL. * * @since 1.9.5 * * @param string $mode Square mode. * * @return string */ public function get_connect_url( string $mode ): string { $mode = Helpers::validate_mode( $mode ); return add_query_arg( [ 'action' => 'init', 'live_mode' => absint( $mode === Environment::PRODUCTION ), 'state' => uniqid( '', true ), 'site_url' => rawurlencode( Helpers::get_settings_page_url() ), 'scopes' => implode( ' ', $this->get_scopes() ), ], $this->get_server_url() . '/oauth/square-connect' ); } /** * Retrieve the disconnect URL. * * @since 1.9.5 * * @param string $mode Square mode. * * @return string */ public function get_disconnect_url( string $mode ): string { $mode = Helpers::validate_mode( $mode ); $action = 'wpforms_square_disconnect'; $url = add_query_arg( [ 'action' => $action, 'live_mode' => absint( $mode === Environment::PRODUCTION ), ], Helpers::get_settings_page_url() ); return wp_nonce_url( $url, $action ); } /** * Retrieve a connect server URL. * * @since 1.9.5 * * @return string */ public function get_server_url(): string { if ( defined( 'WPFORMS_SQUARE_LOCAL_CONNECT_SERVER' ) && WPFORMS_SQUARE_LOCAL_CONNECT_SERVER ) { return home_url(); } return self::WPFORMS_URL; } /** * Retrieve the connection scopes (permissions). * * @since 1.9.5 * * @return array */ public function get_scopes(): array { /** * Filter the connection scopes. * * @since 1.9.5 * * @param array $scopes The connection scopes. */ return (array) apply_filters( // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName 'wpforms_square_admin_connect_get_scopes', [ 'MERCHANT_PROFILE_READ', 'PAYMENTS_READ', 'PAYMENTS_WRITE', 'ORDERS_READ', 'ORDERS_WRITE', 'CUSTOMERS_READ', 'CUSTOMERS_WRITE', 'SUBSCRIPTIONS_READ', 'SUBSCRIPTIONS_WRITE', 'ITEMS_READ', 'ITEMS_WRITE', 'INVOICES_WRITE', 'INVOICES_READ', 'PAYMENTS_WRITE_ADDITIONAL_RECIPIENTS', ] ); } } Integrations/Square/Integrations/Divi.php 0000644 00000004116 15174710275 0014537 0 ustar 00 <?php namespace WPForms\Integrations\Square\Integrations; /** * Integration with Divi. * * @since 1.9.5 */ class Divi implements IntegrationInterface { /** * Indicate whether current integration is allowed to load. * * @since 1.9.5 * * @return bool */ public function allow_load(): bool { return wpforms_is_divi_active(); } /** * Register hooks. * * @since 1.9.5 */ public function hooks() { add_action( 'wpforms_frontend_css', [ $this, 'frontend_styles' ], 12 ); if ( $this->is_editor_page() ) { add_action( 'wp_enqueue_scripts', [ $this, 'editor_styles' ], 12 ); } } /** * Determine whether editor page is loaded. * * @since 1.9.5 * * @return bool */ public function is_editor_page(): bool { return wpforms_is_divi_editor(); } /** * Load editor styles. * * @since 1.9.5 */ public function editor_styles() { // Do not include styles if the "Include Form Styling > No Styles" is set. if ( wpforms_setting( 'disable-css', '1' ) === '3' ) { return; } $min = wpforms_get_min_suffix(); wp_enqueue_style( 'wpforms-square-divi-integration-card-placeholder', WPFORMS_PLUGIN_URL . "assets/css/integrations/square/divi/wpforms-square-card-placeholder{$min}.css", [], WPFORMS_VERSION ); } /** * Load frontend styles. * * @since 1.9.5 */ public function frontend_styles() { if ( ! $this->is_divi_plugin_loaded() ) { return; } // Do not include styles if the "Include Form Styling > No Styles" is set. if ( wpforms_setting( 'disable-css', '1' ) === '3' ) { return; } $min = wpforms_get_min_suffix(); wp_enqueue_style( 'wpforms-square-divi-integration-frontend', WPFORMS_PLUGIN_URL . "assets/css/integrations/square/divi/wpforms-square{$min}.css", [], WPFORMS_VERSION ); } /** * Determine whether the Divi Builder plugin is loaded. * * @since 1.9.5 * * @return bool */ private function is_divi_plugin_loaded(): bool { if ( ! is_singular() ) { return false; } return function_exists( 'et_is_builder_plugin_active' ) && et_is_builder_plugin_active(); } } Integrations/Square/Integrations/Elementor.php 0000644 00000003237 15174710275 0015601 0 ustar 00 <?php namespace WPForms\Integrations\Square\Integrations; use Elementor\Plugin as ElementorPlugin; /** * Integration with Elementor. * * @since 1.9.5 */ class Elementor implements IntegrationInterface { /** * Indicate whether current integration is allowed to load. * * @since 1.9.5 * * @return bool */ public function allow_load(): bool { return (bool) did_action( 'elementor/loaded' ); } /** * Register hooks. * * @since 1.9.5 */ public function hooks() { add_action( 'elementor/frontend/after_enqueue_scripts', [ $this, 'enqueue_editor_assets' ] ); } /** * Determine whether editor page is loaded. * * @since 1.9.5 * * @return bool */ public function is_editor_page(): bool { // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Missing return ( ! empty( $_POST['action'] ) && $_POST['action'] === 'elementor_ajax' ) || ( ! empty( $_GET['action'] ) && $_GET['action'] === 'elementor' ); } /** * Load editor assets. * * @since 1.9.5 */ public function enqueue_editor_assets() { if ( ! class_exists( ElementorPlugin::class ) || empty( ElementorPlugin::instance()->preview ) || ! ElementorPlugin::instance()->preview->is_preview_mode() ) { return; } // Do not include styles if the "Include Form Styling > No Styles" is set. if ( wpforms_setting( 'disable-css', '1' ) === '3' ) { return; } $min = wpforms_get_min_suffix(); wp_enqueue_style( 'wpforms-square-elementor-integration-card-placeholder', WPFORMS_PLUGIN_URL . "assets/css/integrations/square/wpforms-square-card-placeholder{$min}.css", [], WPFORMS_VERSION ); } } Integrations/Square/Integrations/IntegrationInterface.php 0000644 00000001063 15174710275 0017746 0 ustar 00 <?php namespace WPForms\Integrations\Square\Integrations; /** * Interface defines required methods for integrations to work properly. * * @since 1.9.5 */ interface IntegrationInterface { /** * Indicate whether current integration is allowed to load. * * @since 1.9.5 * * @return bool */ public function allow_load(): bool; /** * Register hooks. * * @since 1.9.5 */ public function hooks(); /** * Determine whether editor page is loaded. * * @since 1.9.5 * * @return bool */ public function is_editor_page(): bool; } Integrations/Square/Integrations/Loader.php 0000644 00000002145 15174710275 0015052 0 ustar 00 <?php namespace WPForms\Integrations\Square\Integrations; /** * Integrations loader. * * @since 1.9.5 */ class Loader { /** * Loaded integrations. * * @since 1.9.5 * * @var array */ private $integrations = []; /** * Classes to register. * * @since 1.9.5 */ private const CLASSES = [ 'Divi', 'Elementor', 'BlockEditor', ]; /** * Init. * * @since 1.9.5 */ public function init() { foreach ( self::CLASSES as $class_name ) { $this->load_integration( $class_name ); } } /** * Load an integration. * * @since 1.9.5 * * @param string $class_name Class name to register. */ private function load_integration( string $class_name ) { if ( isset( $this->integrations[ $class_name ] ) ) { return; } $full_class_name = 'WPForms\Integrations\Square\Integrations\\' . sanitize_text_field( $class_name ); $integration = class_exists( $full_class_name ) ? new $full_class_name() : null; if ( $integration === null || ! $integration->allow_load() ) { return; } $integration->hooks(); $this->integrations[ $class_name ] = $integration; } } Integrations/Square/Integrations/BlockEditor.php 0000644 00000005455 15174710275 0016054 0 ustar 00 <?php namespace WPForms\Integrations\Square\Integrations; use WPForms\Integrations\Square\Connection; use WPForms\Integrations\Square\Helpers; /** * Integration with Block Editor. * * @since 1.9.5 */ class BlockEditor implements IntegrationInterface { /** * Handle name for wp_register_styles handle. * * @since 1.9.5 * * @var string */ const HANDLE = 'wpforms-square-card-placeholder'; /** * Indicate whether current integration is allowed to load. * * @since 1.9.5 * * @return bool */ public function allow_load(): bool { return true; } /** * Register hooks. * * @since 1.9.5 */ public function hooks() { // Field styles for Gutenberg. add_action( 'enqueue_block_editor_assets', [ $this, 'enqueue_assets' ] ); // Set editor style for block type editor. Must run at 20 in add-ons. add_filter( 'register_block_type_args', [ $this, 'block_editor_assets' ], 20, 2 ); } /** * Determine whether editor page is loaded. * * @since 1.9.5 * * @return bool */ public function is_editor_page(): bool { // phpcs:ignore WordPress.Security.NonceVerification.Recommended return defined( 'REST_REQUEST' ) && REST_REQUEST && ! empty( $_REQUEST['context'] ) && $_REQUEST['context'] === 'edit'; } /** * Enqueue assets. * * @since 1.9.5 */ public function enqueue_assets() { $this->enqueue_css(); } /** * Enqueue css. * * @since 1.9.5 */ private function enqueue_css() { // Do not include styles if the "Include Form Styling > No Styles" is set. if ( wpforms_setting( 'disable-css', '1' ) === '3' ) { return; } $min = wpforms_get_min_suffix(); wp_enqueue_style( 'wpforms-square', WPFORMS_PLUGIN_URL . "assets/css/integrations/square/wpforms-square{$min}.css", [], WPFORMS_VERSION ); wp_enqueue_style( self::HANDLE, WPFORMS_PLUGIN_URL . "assets/css/integrations/square/wpforms-square-card-placeholder{$min}.css", [], WPFORMS_VERSION ); } /** * Set editor style for block type editor. * * @since 1.9.5 * * @param array $args Array of arguments for registering a block type. * @param string $block_type Block type name including namespace. * * @return array */ public function block_editor_assets( $args, string $block_type ): array { $args = (array) $args; if ( $block_type !== 'wpforms/form-selector' || ! is_admin() ) { return $args; } // Do not include styles if the "Include Form Styling > No Styles" is set. if ( wpforms_setting( 'disable-css', '1' ) === '3' ) { return $args; } $min = wpforms_get_min_suffix(); wp_register_style( self::HANDLE, WPFORMS_PLUGIN_URL . "assets/css/integrations/square/wpforms-square-card-placeholder{$min}.css", [ $args['editor_style'] ], WPFORMS_VERSION ); $args['editor_style'] = self::HANDLE; return $args; } } Integrations/Square/Process.php 0000644 00000076312 15174710275 0012623 0 ustar 00 <?php namespace WPForms\Integrations\Square; use WP_Post; use WPForms\Integrations\Square\Api\Api; use WPForms\Vendor\Square\Models\Card; use WPForms\Vendor\Square\Models\ErrorCode; /** * Square payment processing. * * @since 1.9.5 */ class Process { /** * Form ID. * * @since 1.9.5 * * @var int */ public $form_id = 0; /** * Sanitized submitted field values and data. * * @since 1.9.5 * * @var array */ public $fields = []; /** * Form submission raw data ($_POST). * * @since 1.9.5 * * @var array */ public $entry = []; /** * Form data and settings. * * @since 1.9.5 * * @var array */ public $form_data = []; /** * Square payment settings. * * @since 1.9.5 * * @var array */ public $settings = []; /** * Square credit card field. * * @since 1.9.5 * * @var array */ public $cc_field = []; /** * Payment amount. * * @since 1.9.5 * * @var string */ public $amount = ''; /** * Payment currency. * * @since 1.9.5 * * @var string */ public $currency = ''; /** * Connection data. * * @since 1.9.5 * * @var Connection */ public $connection; /** * Main class that communicates with the Square API. * * @since 1.9.5 * * @var Api */ protected $api; /** * Processing errors. * * @since 1.9.5 * * @var array */ protected $errors; /** * Save matched subscription settings. * * @since 1.9.5 * * @var array */ private $subscription_settings = []; /** * Whether the payment has been processed. * * @since 1.9.5 * * @var bool */ protected $is_payment_processed = false; /** * Initialize. * * @since 1.9.5 */ public function init(): Process { $this->hooks(); return $this; } /** * Hooks. * * @since 1.9.5 */ protected function hooks() { add_action( 'wpforms_process', [ $this, 'process_entry' ], 10, 3 ); add_action( 'wpforms_process_entry_saved', [ $this, 'update_entry_meta' ], 10, 4 ); add_filter( 'wpforms_forms_submission_prepare_payment_data', [ $this, 'prepare_payment_data' ], 10, 3 ); add_filter( 'wpforms_forms_submission_prepare_payment_meta', [ $this, 'prepare_payment_meta' ], 10, 3 ); add_action( 'wpforms_process_payment_saved', [ $this, 'process_payment_saved' ], 10, 3 ); } /** * Check if a payment exists with an entry, if so validate and process. * * @since 1.9.5 * * @param array $fields Final/sanitized submitted fields data. * @param array $entry Copy of original $_POST. * @param array $form_data Form data and settings. */ public function process_entry( $fields, array $entry, array $form_data ) { $fields = (array) $fields; // Check if payment method exists and is enabled. if ( ! Helpers::is_payments_enabled( $form_data ) ) { return; } $this->fields = $fields; $this->entry = $entry; $this->form_data = $form_data; $this->form_id = isset( $form_data['id'] ) ? (int) $form_data['id'] : 0; $this->settings = $form_data['payments']['square']; $this->cc_field = $this->get_credit_card_field(); $this->currency = $this->get_currency(); $this->amount = $this->get_amount(); $this->connection = Connection::get(); // Before proceeding, check if any basic errors were detected. if ( ! $this->is_form_processed() ) { $this->display_errors(); return; } // Set an API instance. $this->api = new Api( $this->connection ); // Set tokens provided by Web Payments SDK. $this->api->set_payment_tokens( $entry ); // Proceed to executing the purchase. $this->process_payment(); // Update the card field value to contain basic details. $this->update_credit_card_field_value(); } /** * Bypass captcha if payment has been processed. * * @since 1.9.5 * @deprecated 1.9.6 * * @param bool $bypass_captcha Whether to bypass captcha. * * @return bool */ public function bypass_captcha( $bypass_captcha ): bool { _deprecated_function( __METHOD__, '1.9.6 of the WPForms plugin' ); if ( (bool) $bypass_captcha ) { return true; } return $this->is_payment_processed; } /** * Check if form has errors before payment processing. * * @since 1.9.5 * * @return bool */ private function is_form_processed(): bool { // Bail in case there are form processing errors. if ( ! empty( wpforms()->obj( 'process' )->errors[ $this->form_id ] ) ) { return false; } if ( ! $this->is_card_field_visibility_ok() ) { return false; } return $this->is_form_ok(); } /** * Check form settings, fields, etc. * * @since 1.9.5 * * @return bool */ private function is_form_ok(): bool { // Check for Square connection. if ( ! $this->is_connection_ok() ) { $error_title = esc_html__( 'Square payment stopped, account connection is missing.', 'wpforms-lite' ); $this->errors[] = $error_title; $this->log_errors( $error_title ); return false; } // Check total charge amount. // Square has different minimum amount limits by country. if ( ! $this->is_amount_ok() ) { $error_title = esc_html__( 'Square payment stopped, amount is smaller than the allowed minimum amount for a payment.', 'wpforms-lite' ); $this->errors[] = $error_title; $this->log_errors( $error_title, [ 'amount' => $this->amount, 'currency' => $this->currency, ] ); return false; } // Check that, despite how the form is configured, the form and // entry actually contain payment fields, otherwise no need to proceed. if ( empty( $this->cc_field ) || ! wpforms_has_payment( 'form', $this->fields ) ) { $error_title = esc_html__( 'Square payment stopped, missing payment fields.', 'wpforms-lite' ); $this->errors[] = $error_title; $this->log_errors( $error_title ); return false; } return true; } /** * Check if the Square credit card field in the form is visible (not hidden by conditional logic). * * @since 1.9.5 * * @return bool */ private function is_card_field_visibility_ok(): bool { // If the form doesn't contain the credit card field. if ( empty( $this->cc_field ) ) { return false; } // If the form contains no fields with conditional logic, the credit card field is visible by default. if ( empty( $this->form_data['conditional_fields'] ) ) { return true; } // If the credit card field is NOT in array of conditional fields, it's visible. if ( ! in_array( $this->cc_field['id'], $this->form_data['conditional_fields'], true ) ) { return true; } // If the credit card field IS in array of conditional fields and marked as visible, it's visible. if ( ! empty( $this->cc_field['visible'] ) ) { return true; } return false; } /** * Check if connection exists, configured and valid. * * @since 1.9.5 * * @return bool */ private function is_connection_ok(): bool { return $this->connection !== null && $this->connection->is_usable(); } /** * Check if an amount is greater than the minimum amount. * * @since 1.9.5 * * @link https://developer.squareup.com/docs/build-basics/working-with-monetary-amounts#monetary-amount-limits * * @return bool */ private function is_amount_ok(): bool { $amount = Helpers::format_amount( $this->amount ); if ( $amount < 1 && in_array( $this->currency, [ 'USD', 'CAD' ], true ) ) { return false; } if ( $amount < 100 && in_array( $this->currency, [ 'EUR', 'GBP', 'AUD', 'JPY' ], true ) ) { return false; } return true; } /** * Process a payment. * * @since 1.9.5 */ private function process_payment() { if ( ! empty( $this->settings['enable_recurring'] ) ) { $this->process_payment_subscription(); return; } $this->process_payment_single(); } /** * Process a single payment. * * @since 1.9.5 */ protected function process_payment_single() { $args = $this->get_payment_args_single(); $this->api->process_single_transaction( $args ); // Set payment processing flag. $this->is_payment_processed = true; $this->process_api_errors( 'single' ); } /** * Process a subscription payment. * * @since 1.9.5 */ private function process_payment_subscription() { $args = $this->get_payment_args_general(); foreach ( $this->settings['recurring'] as $recurring ) { if ( ! $this->is_subscription_plan_valid( $recurring ) ) { continue; } // Put subscription arguments into its own key. $args['subscription'] = $this->get_payment_args_subscription( $recurring ); $this->subscription_settings = $args['subscription']; $this->api->process_subscription_transaction( $args ); // Set payment processing flag. $this->is_payment_processed = true; $this->process_api_errors( 'subscription' ); return; } if ( ! empty( $this->settings['enable_one_time'] ) ) { $this->process_payment_single(); return; } $this->log_errors( esc_html__( 'Square Subscription payment stopped, validation error.', 'wpforms-lite' ), $this->fields, 'conditional_logic' ); } /** * Retrieve subscription payment args. * * @since 1.9.5 * * @param array $plan Plan settings. * * @return array */ private function get_payment_args_subscription( array $plan ): array { $args_sub['customer']['email'] = sanitize_email( $this->fields[ $plan['customer_email'] ]['value'] ); $args_sub['customer']['first_name'] = sanitize_text_field( $this->fields[ $plan['customer_name'] ]['first'] ); $args_sub['customer']['last_name'] = sanitize_text_field( $this->fields[ $plan['customer_name'] ]['last'] ); // If a Name field has the `Simple` format. if ( empty( $args_sub['customer']['first_name'] ) && empty( $args_sub['customer']['last_name'] ) && ! empty( $this->fields[ $plan['customer_name'] ]['value'] ) ) { $args_sub['customer']['last_name'] = sanitize_text_field( $this->fields[ $plan['customer_name'] ]['value'] ); } // Customer address. if ( isset( $plan['customer_address'] ) && $plan['customer_address'] !== '' && wpforms()->is_pro() ) { $args_sub['customer']['address'] = $this->fields[ $plan['customer_address'] ]; } $cadences_list = Helpers::get_subscription_cadences(); $phase_cadence = $cadences_list[ $plan['phase_cadence'] ] ?? $cadences_list['yearly']; // Subscription cadence. $args_sub['phase_cadence'] = $phase_cadence; $plan_name = $this->get_form_name(); $plan_name .= empty( $plan['name'] ) ? '' : ': ' . $plan['name']; $args_sub['plan_name'] = sprintf( '%s (%s)', $plan_name, $phase_cadence['name'] ); $args_sub['plan_variation_name'] = sprintf( '%s (%s %s %s)', $plan['name'], $this->amount, $this->currency, $phase_cadence['name'] ); // Card holder. $args_sub['card_name'] = empty( $this->fields[ $this->cc_field['id'] ]['cardname'] ) ? '' : sanitize_text_field( $this->fields[ $this->cc_field['id'] ]['cardname'] ); /** * Filter subscription payment arguments. * * @since 1.9.5 * * @param array $args The subscription payment arguments. * @param Process $process The Process instance. */ return (array) apply_filters( 'wpforms_integrations_square_process_get_payment_args_subscription', $args_sub, $this ); } /** * Validate plan before process. * * @since 1.9.5 * * @param array $plan Plan settings. * * @return bool */ protected function is_subscription_plan_valid( array $plan ): bool { return ! empty( $plan['customer_email'] ) && $this->is_recurring_settings_ok( $plan ); } /** * Check if recurring settings is configured correctly. * * @since 1.9.5 * * @param array $settings Settings data. * * @return bool */ protected function is_recurring_settings_ok( array $settings ): bool { $error = ''; // Check subscription settings are provided. if ( empty( $settings['phase_cadence'] ) || empty( $settings['customer_email'] ) || empty( $settings['customer_name'] ) ) { $error = esc_html__( 'Square subscription payment stopped, missing form settings.', 'wpforms-lite' ); } // Check for required customer email. if ( ! $error && empty( $this->fields[ $settings['customer_email'] ]['value'] ) ) { $error = esc_html__( 'Square subscription payment stopped, customer email not found.', 'wpforms-lite' ); } // Check for required customer name. if ( ! $error && empty( $this->fields[ $settings['customer_name'] ]['value'] ) ) { $error = esc_html__( 'Square subscription payment stopped, customer name not found.', 'wpforms-lite' ); } // Before proceeding, check if any basic errors were detected. if ( $error ) { $this->log_errors( $error, $settings ); return false; } return true; } /** * Retrieve single payment args. * * @since 1.9.5 * * @return array */ private function get_payment_args_single(): array { $args = $this->get_payment_args_general(); $customer_name = $this->get_customer_name(); $customer_email = $this->get_customer_email(); // Billing Name. if ( isset( $customer_name['first_name'] ) ) { $args['billing']['first_name'] = sanitize_text_field( $customer_name['first_name'] ); } if ( isset( $customer_name['last_name'] ) ) { $args['billing']['last_name'] = sanitize_text_field( $customer_name['last_name'] ); } // Billing Address. if ( ! empty( $this->fields[ $this->settings['billing_address'] ] ) ) { $args['billing']['address'] = $this->fields[ $this->settings['billing_address'] ]; } // Buyer Email. if ( ! empty( $customer_email ) ) { $args['buyer_email'] = sanitize_email( $customer_email ); } // Payment description. $description = empty( $this->settings['payment_description'] ) ? $this->get_form_name() : html_entity_decode( $this->settings['payment_description'], ENT_COMPAT, 'UTF-8' ); // The maximum length for the Square notes field is 500 characters. $args['note'] = wp_html_excerpt( Square::APP_NAME . ': ' . $description, 500 ); // Order items. $args['order_items'] = $this->get_order_items(); /** * Filter single payment arguments. * * @param array $args The single payment arguments. * @param Process $process The Process instance. * *@since 1.9.5 */ return (array) apply_filters( 'wpforms_square_process_get_payment_args_single', $args, $this ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName } /** * Retrieve arguments for any type of payment. * * @since 1.9.5 * * @return array */ private function get_payment_args_general(): array { /** * Filter arguments for any type of payment. * * @since 1.9.5 * * @param array $args The general payment arguments. * @param Process $process The Process instance. */ return (array) apply_filters( // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName 'wpforms_square_process_get_payment_args_general', [ 'amount' => Helpers::format_amount( $this->amount ), 'currency' => $this->currency, 'location_id' => Helpers::get_location_id(), ], $this ); } /** * Retrieve a payment currency. * * @since 1.9.5 * * @return string */ private function get_currency(): string { return strtoupper( wpforms_get_currency() ); } /** * Retrieve a payment amount. * * @since 1.9.5 * * @return string */ private function get_amount(): string { $amount = wpforms_get_total_payment( $this->fields ); return $amount === false ? wpforms_sanitize_amount( 0 ) : $amount; } /** * Retrieve order items. * * @since 1.9.5 * * @return array */ private function get_order_items(): array { /** * Filter order items types. * * @since 1.9.5 * * @param array $types The order items types. */ $types = (array) apply_filters( 'wpforms_square_process_get_order_items_types', wpforms_payment_fields() ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName $items = []; foreach ( $this->fields as $field_id => $field ) { if ( empty( $field['type'] ) || ! in_array( $field['type'], $types, true ) ) { continue; } // Skip payment field that is not filled in. if ( ! isset( $this->entry['fields'][ $field_id ] ) || wpforms_is_empty_string( $this->entry['fields'][ $field_id ] ) ) { continue; } $items[] = $this->prepare_order_line_item( $field ); } return $items; } /** * Retrieve a Form Name. * * @since 1.9.5 * * @return string */ private function get_form_name(): string { if ( ! empty( $this->form_data['settings']['form_title'] ) ) { return sanitize_text_field( $this->form_data['settings']['form_title'] ); } $fallback = sprintf( /* translators: %d - Form ID. */ esc_html__( 'Form #%d', 'wpforms-lite' ), $this->form_id ); $form_obj = wpforms()->obj( 'form' ); if ( ! $form_obj ) { return $fallback; } $form = $form_obj->get( $this->form_id ); return $form instanceof WP_Post ? $form->post_title : $fallback; } /** * Retrieve a Square credit card field. * * @since 1.9.5 * * @return array */ private function get_credit_card_field(): array { if ( ! is_array( $this->fields ) ) { return []; } foreach ( $this->fields as $field ) { if ( ! empty( $field['type'] ) && $field['type'] === 'square' ) { return $field; } } return []; } /** * Prepare order line item. * * @since 1.9.5 * * @param array $field Field data. * * @return array */ private function prepare_order_line_item( array $field ): array { $field_id = absint( $field['id'] ); $quantity = isset( $field['quantity'] ) ? (int) $field['quantity'] : 1; $name = empty( $field['name'] ) ? sprintf( /* translators: %d - Field ID. */ esc_html__( 'Field #%d', 'wpforms-lite' ), $field_id ) : $field['name']; $item = [ 'name' => $name, 'quantity' => $quantity, ]; if ( empty( $field['value_raw'] ) ) { $item['amount'] = Helpers::format_amount( $field['amount_raw'] ); return $item; } return $this->prepare_order_line_item_variations( $item, $field, $field_id ); } /** * Prepare order line item variations. * * @since 1.9.5 * * @param array $item Item data. * @param array $field Field data. * @param int $field_id Field ID. * * @return array */ private function prepare_order_line_item_variations( array $item, array $field, int $field_id ): array { $values = explode( ',', $field['value_raw'] ); foreach ( $values as $value ) { if ( empty( $this->form_data['fields'][ $field_id ]['choices'][ $value ] ) ) { continue; } $choice = $this->form_data['fields'][ $field_id ]['choices'][ $value ]; $item['variations'][] = [ 'name' => $item['name'], 'quantity' => $item['quantity'], 'variation_name' => empty( $choice['label'] ) ? sprintf( /* translators: %d - Choice ID. */ esc_html__( 'Choice %d', 'wpforms-lite' ), absint( $value ) ) : $choice['label'], 'amount' => Helpers::format_amount( $choice['value'] ), ]; } return $item; } /** * Display form errors. * * @since 1.9.5 * * @param array $errors Errors to display. */ private function display_errors( array $errors = [] ) { if ( ! $errors ) { $errors = $this->errors; } if ( ! $errors || ! is_array( $errors ) ) { return; } // Check if the form contains a required credit card. If it does // and there was an error, return the error to the user and prevent // the form from being submitted. This should not occur under normal // circumstances. if ( empty( $this->cc_field ) || empty( $this->form_data['fields'][ $this->cc_field['id'] ] ) ) { return; } if ( ! empty( $this->form_data['fields'][ $this->cc_field['id'] ]['required'] ) ) { wpforms()->obj( 'process' )->errors[ $this->form_id ]['footer'] = implode( '<br>', $errors ); } } /** * Collect errors from API and turn it into form errors. * * @since 1.9.5 * * @param string $type Payment type (e.g. 'single'). */ private function process_api_errors( string $type ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found $errors = $this->api->get_errors(); if ( empty( $errors ) || ! is_array( $errors ) ) { return; } $this->display_errors( $errors ); if ( $type === 'subscription' ) { $title = esc_html__( 'Square subscription payment stopped', 'wpforms-lite' ); } else { $title = esc_html__( 'Square payment stopped', 'wpforms-lite' ); } $_errors = $this->api->get_response_errors(); if ( ! empty( $_errors ) ) { $this->process_api_errors_codes( $_errors ); $errors[] = $_errors; } // Log transaction specific errors. $this->log_errors( $title, $errors ); } /** * Check specific error codes. * * @since 1.9.5 * * @param array $errors The last API call errors. */ private function process_api_errors_codes( array $errors ) { $codes = $this->get_oauth_error_codes(); foreach ( $errors as $error ) { if ( empty( $error['code'] ) || ! in_array( $error['code'], $codes, true ) ) { continue; } // If the error indicates that access token is bad, set a connection as invalid. $this->connection ->set_status( Connection::STATUS_INVALID ) ->save(); } } /** * Retrieve OAuth-related errors. * * @since 1.9.5 * * @link https://developer.squareup.com/docs/oauth-api/best-practices#ensure-api-calls-made-with-oauth-tokens-handle-token-based-errors-appropriately * * @return array */ private function get_oauth_error_codes(): array { return [ ErrorCode::ACCESS_TOKEN_EXPIRED, ErrorCode::ACCESS_TOKEN_REVOKED, ErrorCode::UNAUTHORIZED ]; } /** * Log payment errors. * * @since 1.9.5 * * @param string $title Error title. * @param array|string $messages Error messages. * @param string $level Error level to add to 'payment' error level. */ protected function log_errors( string $title, $messages = [], string $level = 'error' ) { wpforms_log( $title, $messages, [ 'type' => [ 'payment', $level ], 'form_id' => $this->form_id, ] ); } /** * Update the credit card field value to contain basic details. * * @since 1.9.5 */ private function update_credit_card_field_value() { if ( $this->errors || ! $this->api ) { return; } // Get a card. $card = $this->get_card(); if ( empty( $card ) ) { return; } $details = [ 'brand' => $card->getCardBrand(), 'last4' => $card->getLast4(), 'holder' => $this->get_card_holder( $card ), ]; $details = implode( "\n", array_filter( $details ) ); /** * Filter a credit card field value by card details. * * @since 1.9.5 * * @param string $details Card details. * @param Process $process Process object. */ $details = apply_filters( // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName 'wpforms_square_process_update_credit_card_field_value', $details, $this ); wpforms()->obj( 'process' )->fields[ $this->cc_field['id'] ]['value'] = $details; } /** * Get card object. * * @since 1.9.5 * * @return Card|array */ private function get_card() { $resource = $this->api->get_response_resource(); if ( empty( $resource ) ) { return []; } $type = Helpers::array_key_first( $resource ); return $type === 'subscription' ? $this->api->get_subscription_card( $resource[ $type ] ) : $resource[ $type ]->getCardDetails()->getCard(); } /** * Update entry details and add meta for a successful payment. * * @since 1.9.5 * * @param array $fields Final/sanitized submitted field data. * @param array $entry Copy of original $_POST. * @param array $form_data Form data and settings. * @param string $entry_id Entry ID. */ public function update_entry_meta( $fields, $entry, $form_data, $entry_id ) { if ( empty( $entry_id ) || $this->errors || ! $this->api ) { return; } $resource = $this->api->get_response_resource(); if ( empty( $resource ) ) { return; } wpforms()->obj( 'entry' )->update( $entry_id, [ 'type' => 'payment', ], '', '', [ 'cap' => false ] ); /** * Fire when entry details and add meta was successfully updated. * * @since 1.9.5 * * @param array $fields Final/sanitized submitted field data. * @param array $form_data Form data and settings. * @param string $entry_id Entry ID. * @param array $resource Response resource data. * @param Process $process Process class instance. */ do_action( 'wpforms_square_process_update_entry_meta', $fields, $form_data, $entry_id, $resource, $this ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName } /** * Add details to payment data. * * @since 1.9.5 * * @param array $payment_data Payment data args. * @param array $fields Final/sanitized submitted field data. * @param array $form_data Form data and settings. * * @return array */ public function prepare_payment_data( $payment_data, array $fields, array $form_data ): array { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed $payment_data = (array) $payment_data; // If there are errors or API is not initialized, return the original payment data. if ( $this->errors || ! $this->api ) { return $payment_data; } $resource = $this->api->get_response_resource(); // If the resource is empty, return the original payment meta. if ( empty( $resource ) ) { return $payment_data; } $type = Helpers::array_key_first( $resource ); $payment = $resource[ $type ]; $is_subscription = $type === 'subscription'; $payment_data['status'] = 'processed'; $payment_data['gateway'] = 'square'; $payment_data['mode'] = Helpers::is_sandbox_mode() ? 'test' : 'live'; $payment_data['customer_id'] = sanitize_text_field( $payment->getCustomerId() ); $payment_data['title'] = $this->get_payment_title( $payment ); if ( $is_subscription ) { $payment_data['subscription_id'] = sanitize_text_field( $payment->getId() ); $payment_data['subscription_status'] = 'not-synced'; return $payment_data; } $payment_data['transaction_id'] = sanitize_text_field( $payment->getId() ); return $payment_data; } /** * Get Payment title. * * @since 1.9.5 * * @param object $payment Payment object. * * @return string Payment title. */ private function get_payment_title( $payment ): string { // Look for the cardholder name. $card = $this->get_card(); $customer_name = $card ? $this->get_card_holder( $card ) : ''; if ( $customer_name ) { return sanitize_text_field( $customer_name ); } $customer_name = $this->get_customer_name(); if ( $customer_name ) { return sanitize_text_field( implode( ' ', array_values( $customer_name ) ) ); } $customer_email = $this->get_customer_email(); if ( $customer_email ) { return sanitize_email( $customer_email ); } return ''; } /** * Add payment meta for a successful one-time or subscription payment. * * @since 1.9.5 * * @param array $payment_meta Payment meta. * @param array $fields Final/sanitized submitted field data. * @param array $form_data Form data and settings. * * @return array */ public function prepare_payment_meta( $payment_meta, array $fields, array $form_data ): array { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed $payment_meta = (array) $payment_meta; // If there are errors or API is not initialized, return the original payment meta. if ( $this->errors || ! $this->api ) { return $payment_meta; } $resource = $this->api->get_response_resource(); // If the resource is empty, return the original payment meta. if ( empty( $resource ) ) { return $payment_meta; } $type = Helpers::array_key_first( $resource ); $credit_card_details = $this->get_card(); $is_subscription = $type === 'subscription'; if ( $is_subscription ) { $payment_meta['subscription_period'] = $this->subscription_settings['phase_cadence']['slug']; } $payment_meta['method_type'] = 'card'; if ( ! empty( $credit_card_details ) ) { $payment_meta['credit_card_last4'] = $credit_card_details->getLast4(); $payment_meta['credit_card_expires'] = $credit_card_details->getExpMonth() . '/' . $credit_card_details->getExpYear(); $payment_meta['credit_card_method'] = strtolower( $credit_card_details->getCardBrand() ); $payment_meta['credit_card_name'] = $this->get_card_holder( $credit_card_details ); } // Add a log indicating that the charge was successful. $payment_meta['log'] = $this->format_payment_log( 'Square payment was created.' ); return $payment_meta; } /** * Add payment info for successful payment. * * @since 1.9.5 * * @param string $payment_id Payment ID. * @param array $fields Final/sanitized submitted field data. * @param array $form_data Form data and settings. */ public function process_payment_saved( $payment_id, array $fields, array $form_data ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed $payment_id = (string) $payment_id; // If there are errors or API is not initialized, return the original payment meta. if ( $this->errors || ! $this->api ) { return; } $resource = $this->api->get_response_resource(); // If the resource is empty, return the original payment meta. if ( empty( $resource ) ) { return; } $type = Helpers::array_key_first( $resource ); if ( $type === 'subscription' ) { $this->api->update_subscription( [ 'id' => $resource[ $type ]->getId(), 'payment_id' => $payment_id, ] ); return; } wpforms()->obj( 'payment_meta' )->add_log( $payment_id, sprintf( 'Square payment was processed. (Receipt ID: %s)', $resource[ $type ]->getReceiptNumber() ) ); } /** * Return payment log value. * * @since 1.9.5 * * @param string $value Log value. * * @return string */ private function format_payment_log( string $value ): string { return wp_json_encode( [ 'value' => sanitize_text_field( $value ), 'date' => gmdate( 'Y-m-d H:i:s' ), ] ); } /** * Get Customer name. * * @since 1.9.5 * * @return array */ private function get_customer_name(): array { $customer_name = []; // Billing first name. if ( ! empty( $this->fields[ $this->settings['billing_name'] ]['first'] ) ) { $customer_name['first_name'] = $this->fields[ $this->settings['billing_name'] ]['first']; } // Billing last name. if ( ! empty( $this->fields[ $this->settings['billing_name'] ]['last'] ) ) { $customer_name['last_name'] = $this->fields[ $this->settings['billing_name'] ]['last']; } // If a Name field has the `Simple` format. if ( empty( $customer_name['first_name'] ) && empty( $customer_name['last_name'] ) && ! empty( $this->fields[ $this->settings['billing_name'] ]['value'] ) ) { $customer_name['first_name'] = $this->fields[ $this->settings['billing_name'] ]['value']; } return $customer_name; } /** * Get Customer email. * * @since 1.9.5 * * @return string */ private function get_customer_email(): string { return ! empty( $this->fields[ $this->settings['buyer_email'] ]['value'] ) ? $this->fields[ $this->settings['buyer_email'] ]['value'] : ''; } /** * Retrieve a Cardholder Name. * * @since 1.9.5 * * @param Card $card Card object. * * @return string */ private function get_card_holder( $card ): string { $holder = ''; if ( $card instanceof Card ) { $holder = $card->getCardholderName(); } if ( empty( $holder ) && isset( $this->cc_field['cardname'] ) ) { $holder = $this->cc_field['cardname']; } return $holder; } } Integrations/DefaultContent/DefaultContent.php 0000644 00000003041 15174710275 0015570 0 ustar 00 <?php namespace WPForms\Integrations\DefaultContent; use WPForms\Integrations\IntegrationInterface; /** * Class DefaultContent. * * @since 1.7.2 */ class DefaultContent implements IntegrationInterface { /** * Indicate if current integration is allowed to load. * * @since 1.7.2 * * @return bool */ public function allow_load() { global $pagenow; return get_option( 'fresh_site' ) && $pagenow === 'customize.php'; } /** * Load an integration. * * @since 1.7.2 */ public function load() { add_filter( 'get_theme_starter_content', [ $this, 'modify_starter_content' ], 1000, 2 ); } /** * Append education text to Contact page content. * * @since 1.7.2 * * @param array $content Array of starter content. * @param array $config Array of theme-specific starter content configuration. * * @return array */ public function modify_starter_content( $content, $config ) { if ( ! isset( $content['posts']['contact'] ) ) { return $content; } $content['posts']['contact']['post_content'] .= sprintf( "<!-- wp:paragraph -->\n<p>%s</p>\n<!-- /wp:paragraph -->", wp_kses( sprintf( /* translators: %s - forms overview page URL. */ _x( 'Create your <a href="%s" target="_blank" rel="noopener noreferrer">contact form</a> with WPForms in minutes.', 'Theme starter content', 'wpforms-lite' ), esc_url( admin_url( 'admin.php?page=wpforms-overview' ) ) ), [ 'a' => [ 'href' => [], 'rel' => [], 'target' => [], ], ] ) ); return $content; } } Integrations/DefaultThemes/DefaultThemes.php 0000644 00000017655 15174710275 0015236 0 ustar 00 <?php namespace WPForms\Integrations\DefaultThemes; use WPForms\Integrations\IntegrationInterface; /** * Class DefaultThemes. * * @since 1.6.6 */ class DefaultThemes implements IntegrationInterface { /** * Twenty Twenty theme name. * * @since 1.6.6 */ const TT = 'twentytwenty'; /** * Twenty Twenty-One theme name. * * @since 1.6.6 */ const TT1 = 'twentytwentyone'; /** * OceanWP theme name. * * @since 1.9.1 */ const OCEANWP = 'oceanwp'; /** * Current theme name. * * @since 1.6.6 * * @var string */ private $current_theme; /** * Determine if WordPress default theme is used. * * @since 1.6.6 * * @return string */ private function get_current_default_theme(): string { $allow_themes = [ self::TT, self::TT1, self::OCEANWP ]; $theme_name = get_template(); return in_array( $theme_name, $allow_themes, true ) ? $theme_name : ''; } /** * Indicate if current integration is allowed to load. * * @since 1.6.6 * * @return bool */ public function allow_load() { $this->current_theme = $this->get_current_default_theme(); return ! empty( $this->current_theme ); } /** * Load an integration. * * @since 1.6.6 */ public function load() { if ( $this->current_theme === self::TT ) { $this->tt_hooks(); return; } if ( $this->current_theme === self::TT1 ) { $this->tt1_hooks(); return; } if ( $this->current_theme === self::OCEANWP ) { $this->ocean_hooks(); } } /** * Hooks for the Twenty Twenty theme. * * @since 1.6.6 */ private function tt_hooks() { // phpcs:ignore WPForms.PHP.HooksMethod.InvalidPlaceForAddingHooks add_action( 'wp_enqueue_scripts', [ $this, 'tt_iframe_fix' ], 11 ); add_action( 'wpforms_frontend_css', [ $this, 'tt_dropdown_fix' ] ); } /** * Hooks for the Twenty Twenty-One theme. * * @since 1.6.6 */ private function tt1_hooks() { // phpcs:ignore WPForms.PHP.HooksMethod.InvalidPlaceForAddingHooks if ( wpforms_get_render_engine() === 'modern' ) { return; } $form_styling = wpforms_setting( 'disable-css', '1' ); if ( $form_styling === '1' ) { add_action( 'wp_enqueue_scripts', [ $this, 'tt1_multiple_fields_fix' ], 11 ); add_action( 'wp_enqueue_scripts', [ $this, 'tt1_dropdown_fix' ], 11 ); } if ( $form_styling === '2' ) { add_action( 'wp_enqueue_scripts', [ $this, 'tt1_base_style_fix' ], 11 ); } } /** * Hooks for the OceanWP theme. * * @since 1.9.1 */ private function ocean_hooks() { // phpcs:ignore WPForms.PHP.HooksMethod.InvalidPlaceForAddingHooks add_action( 'wp_enqueue_scripts', [ $this, 'ocean_button_hover' ], 100 ); } /** * Apply button hover fix for OceanWP theme. * * @since 1.9.1 */ public function ocean_button_hover() { // Only full styles are supported. if ( (int) wpforms_setting( 'disable-css', 1 ) !== 1 ) { return; } $styles = wpforms_get_render_engine() === 'modern' ? /** @lang CSS */ 'body div.wpforms-container-full .wpforms-form input[type=submit]:hover, body div.wpforms-container-full .wpforms-form input[type=submit]:active, body div.wpforms-container-full .wpforms-form button[type=submit]:hover, body div.wpforms-container-full .wpforms-form button[type=submit]:active, body div.wpforms-container-full .wpforms-form .wpforms-page-button:hover, body div.wpforms-container-full .wpforms-form .wpforms-page-button:active, body .wp-core-ui div.wpforms-container-full .wpforms-form input[type=submit]:hover, body .wp-core-ui div.wpforms-container-full .wpforms-form input[type=submit]:active, body .wp-core-ui div.wpforms-container-full .wpforms-form button[type=submit]:hover, body .wp-core-ui div.wpforms-container-full .wpforms-form button[type=submit]:active, body .wp-core-ui div.wpforms-container-full .wpforms-form .wpforms-page-button:hover, body .wp-core-ui div.wpforms-container-full .wpforms-form .wpforms-page-button:active { background: linear-gradient(0deg, rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0.2)), var(--wpforms-button-background-color-alt, var(--wpforms-button-background-color)) !important; }' : /** @lang CSS */ 'div.wpforms-container-full .wpforms-form input[type=submit]:hover, div.wpforms-container-full .wpforms-form input[type=submit]:focus, div.wpforms-container-full .wpforms-form input[type=submit]:active, div.wpforms-container-full .wpforms-form button[type=submit]:hover, div.wpforms-container-full .wpforms-form button[type=submit]:focus, div.wpforms-container-full .wpforms-form button[type=submit]:active, div.wpforms-container-full .wpforms-form .wpforms-page-button:hover, div.wpforms-container-full .wpforms-form .wpforms-page-button:active, div.wpforms-container-full .wpforms-form .wpforms-page-button:focus { border: none; }'; wp_add_inline_style( 'oceanwp-style', $styles ); } /** * Apply fix for Checkboxes and Radio fields in the Twenty Twenty-One theme. * * @since 1.6.6 */ public function tt1_multiple_fields_fix() { wp_add_inline_style( 'twenty-twenty-one-style', /** @lang CSS */ '@supports (-webkit-appearance: none) or (-moz-appearance: none) { div.wpforms-container-full .wpforms-form input[type=checkbox] { -webkit-appearance: checkbox; -moz-appearance: checkbox; } div.wpforms-container-full .wpforms-form input[type=radio] { -webkit-appearance: radio; -moz-appearance: radio; } div.wpforms-container-full .wpforms-form input[type=checkbox]:after, div.wpforms-container-full .wpforms-form input[type=radio]:after { content: none; } }' ); } /** * Apply fix for Dropdown field arrow, when it disappeared from select in the Twenty Twenty-One theme. * * @since 1.6.8 */ public function tt1_dropdown_fix() { wp_add_inline_style( 'twenty-twenty-one-style', /** @lang CSS */ 'div.wpforms-container-full form.wpforms-form select { background-image: url("data:image/svg+xml;utf8,<svg xmlns=\'http://www.w3.org/2000/svg\' width=\'10\' height=\'10\' fill=\'%2328303d\'><polygon points=\'0,0 10,0 5,5\'/></svg>"); background-repeat: no-repeat; background-position: right var(--form--spacing-unit) top 60%; padding-right: calc(var(--form--spacing-unit) * 2.5); }' ); } /** * Apply fix for Checkboxes and Radio fields width in the Twenty Twenty-One theme, when the user uses only base styles. * * @since 1.6.8 */ public function tt1_base_style_fix() { wp_add_inline_style( 'twenty-twenty-one-style', /** @lang CSS */ '.wpforms-container .wpforms-field input[type=checkbox], .wpforms-container .wpforms-field input[type=radio] { width: 25px; height: 25px; } .wpforms-container .wpforms-field input[type=checkbox] + label, .wpforms-container .wpforms-field input[type=radio] + label { vertical-align: top; }' ); } /** * Apply resize fix for iframe HTML element, when the next page was clicked in the Twenty Twenty theme. * * @since 1.6.6 */ public function tt_iframe_fix() { wp_add_inline_script( 'twentytwenty-js', /** @lang JavaScript */ 'window.addEventListener( "load", function() { if ( typeof jQuery === "undefined" ) { return; } jQuery( document ).on( "wpformsPageChange wpformsShowConditionalsField", function() { if ( typeof twentytwenty === "undefined" || typeof twentytwenty.intrinsicRatioVideos === "undefined" || typeof twentytwenty.intrinsicRatioVideos.makeFit === "undefined" ) { return; } twentytwenty.intrinsicRatioVideos.makeFit(); } ); jQuery( document ).on( "wpformsRichTextEditorInit", function( e, editor ) { jQuery( editor.container ).find( "iframe" ).addClass( "intrinsic-ignore" ); } ); } );' ); } /** * Apply fix for the dropdown list in Twenty Twenty theme. * * @since 1.7.3 */ public function tt_dropdown_fix() { static $fixed = false; if ( $fixed ) { return; } ?> <style> #site-content { overflow: visible !important; } </style> <?php $fixed = true; } } Integrations/Gutenberg/ThemesData.php 0000644 00000011714 15174710275 0013701 0 ustar 00 <?php namespace WPForms\Integrations\Gutenberg; use WPForms\Helpers\File; /** * Rest API for Gutenberg block. * * @since 1.8.8 */ abstract class ThemesData { /** * Custom themes JSON file path. * * Relative to `wp-content/uploads/wpforms` directory. * * @since 1.8.8 * * @var string */ const THEMES_CUSTOM_JSON_PATH = 'themes/themes-custom.json'; /** * WPForms themes JSON file path for lite version. * * Relative to WPForms plugin directory. * * @since 1.8.8 * * @var string */ const THEMES_WPFORMS_JSON_PATH_LITE = 'assets/lite/js/integrations/gutenberg/themes.json'; /** * Custom themes file path. * * @since 1.8.8 * * @var string */ private $custom_themes_file_path; /** * WPForms themes data. * * @since 1.8.8 * * @var array */ protected $wpforms_themes; /** * Custom themes data. * * @since 1.8.8 * * @var array */ private $custom_themes; /** * Return WPForms themes. * * @since 1.8.8 * * @return array */ public function get_wpforms_themes(): array { if ( $this->wpforms_themes !== null ) { return $this->wpforms_themes; } $themes_json = File::get_contents( WPFORMS_PLUGIN_DIR . static::THEMES_WPFORMS_JSON_PATH ) ?? '{}'; $themes = json_decode( $themes_json, true ); $this->wpforms_themes = ! empty( $themes ) ? $themes : []; return $this->wpforms_themes; } /** * Return custom themes. * * @since 1.8.8 * * @return array */ public function get_custom_themes(): array { if ( $this->custom_themes !== null ) { return $this->custom_themes; } $themes_json = File::get_contents( $this->get_custom_themes_file_path() ) ?? '{}'; $themes = json_decode( $themes_json, true ); $this->custom_themes = ! empty( $themes ) ? $themes : []; return $this->custom_themes; } /** * Return theme data. * * @since 1.8.8 * * @param string $slug Theme slug. * * @return array|null */ public function get_theme( string $slug ) { $wpforms = $this->get_wpforms_themes(); if ( ! empty( $wpforms[ $slug ] ) ) { return $wpforms[ $slug ]; } $custom = $this->get_custom_themes(); if ( ! empty( $custom[ $slug ] ) ) { return $custom[ $slug ]; } return null; } /** * Get custom themes json file path. * * @since 1.8.8 * * @return string|bool File path OR false in the case of permissions error. */ public function get_custom_themes_file_path() { // Caching the file path in the class property. if ( $this->custom_themes_file_path !== null ) { return $this->custom_themes_file_path; } // Determine custom themes file path. $upload_dir = wpforms_upload_dir(); $upload_path = ! empty( $upload_dir['path'] ) ? $upload_dir['path'] : WP_CONTENT_DIR . 'uploads/wpforms/'; $upload_path = trailingslashit( wp_normalize_path( $upload_path ) ); $file_path = $upload_path . self::THEMES_CUSTOM_JSON_PATH; $dirname = dirname( $file_path ); // If the directory doesn't exist, create it. Also, check for permissions. if ( ! wp_mkdir_p( $dirname ) ) { $file_path = false; } $this->custom_themes_file_path = $file_path; return $file_path; } /** * Sanitize custom themes data. * * @since 1.8.8 * * @param array $custom_themes Custom themes data. * * @return array */ private function sanitize_custom_themes_data( array $custom_themes ): array { $wpforms = $this->get_wpforms_themes(); $sanitized_themes = []; // Get the default theme settings. // If there are no default settings, use an empty array. This should never happen, but just in case. $default_theme = $wpforms['default'] ?? []; $default_theme['settings'] = $default_theme['settings'] ?? []; foreach ( $custom_themes as $slug => $theme ) { $slug = sanitize_key( $slug ); $sanitized_themes[ $slug ]['name'] = sanitize_text_field( $theme['name'] ?? 'Copy of ' . $default_theme['name'] ); // Fill in missed settings keys with default values. $settings = wp_parse_args( $theme['settings'] ?? [], $default_theme['settings'] ); // Make sure we will save only settings that are present in the default theme. $settings = array_intersect_key( $settings, $default_theme['settings'] ); // Sanitize settings. $sanitized_themes[ $slug ]['settings'] = array_map( 'sanitize_text_field', $settings ); } return $sanitized_themes; } /** * Update custom themes data. * * @since 1.8.8 * * @param array $custom_themes Custom themes data. * * @return bool */ public function update_custom_themes_file( array $custom_themes ): bool { // Sanitize custom themes data to be saved. $sanitized_themes = $this->sanitize_custom_themes_data( $custom_themes ); // Determine custom themes file path. $themes_file = $this->get_custom_themes_file_path(); $json_data = ! empty( $sanitized_themes ) ? wp_json_encode( $sanitized_themes ) : '{}'; // Save custom themes data and return the result. return File::put_contents( $themes_file, $json_data ); } } Integrations/Gutenberg/FormSelector.php 0000644 00000106307 15174710275 0014271 0 ustar 00 <?php namespace WPForms\Integrations\Gutenberg; use WPForms\Frontend\CSSVars; use WPForms\Integrations\IntegrationInterface; use WPForms\Admin\Education\StringsTrait; /** * Form Selector Gutenberg block with a live preview. * * @since 1.4.8 */ abstract class FormSelector implements IntegrationInterface { use StringsTrait; /** * Default attributes. * * @since 1.8.1 * * @var array */ private const DEFAULT_ATTRIBUTES = [ 'formId' => '', 'displayTitle' => false, 'displayDesc' => false, 'theme' => '', 'themeName' => '', 'fieldSize' => 'medium', 'backgroundImage' => CSSVars::ROOT_VARS['background-image'], 'backgroundPosition' => CSSVars::ROOT_VARS['background-position'], 'backgroundRepeat' => CSSVars::ROOT_VARS['background-repeat'], 'backgroundSizeMode' => CSSVars::ROOT_VARS['background-size'], 'backgroundSize' => CSSVars::ROOT_VARS['background-size'], 'backgroundWidth' => CSSVars::ROOT_VARS['background-width'], 'backgroundHeight' => CSSVars::ROOT_VARS['background-height'], 'backgroundUrl' => CSSVars::ROOT_VARS['background-url'], 'backgroundColor' => CSSVars::ROOT_VARS['background-color'], 'fieldBorderRadius' => CSSVars::ROOT_VARS['field-border-radius'], 'fieldBorderStyle' => CSSVars::ROOT_VARS['field-border-style'], 'fieldBorderSize' => CSSVars::ROOT_VARS['field-border-size'], 'fieldBackgroundColor' => CSSVars::ROOT_VARS['field-background-color'], 'fieldBorderColor' => CSSVars::ROOT_VARS['field-border-color'], 'fieldTextColor' => CSSVars::ROOT_VARS['field-text-color'], 'fieldMenuColor' => CSSVars::ROOT_VARS['field-menu-color'], 'labelSize' => 'medium', 'labelColor' => CSSVars::ROOT_VARS['label-color'], 'labelSublabelColor' => CSSVars::ROOT_VARS['label-sublabel-color'], 'labelErrorColor' => CSSVars::ROOT_VARS['label-error-color'], 'buttonSize' => 'medium', 'buttonBorderStyle' => CSSVars::ROOT_VARS['button-border-style'], 'buttonBorderSize' => CSSVars::ROOT_VARS['button-border-size'], 'buttonBorderRadius' => CSSVars::ROOT_VARS['button-border-radius'], 'buttonBackgroundColor' => CSSVars::ROOT_VARS['button-background-color'], 'buttonTextColor' => CSSVars::ROOT_VARS['button-text-color'], 'buttonBorderColor' => CSSVars::ROOT_VARS['button-border-color'], 'pageBreakColor' => CSSVars::ROOT_VARS['page-break-color'], 'containerPadding' => CSSVars::ROOT_VARS['container-padding'], 'containerBorderStyle' => CSSVars::ROOT_VARS['container-border-style'], 'containerBorderWidth' => CSSVars::ROOT_VARS['container-border-width'], 'containerBorderColor' => CSSVars::ROOT_VARS['container-border-color'], 'containerBorderRadius' => CSSVars::ROOT_VARS['container-border-radius'], 'containerShadowSize' => CSSVars::CONTAINER_SHADOW_SIZE['none']['box-shadow'], 'customCss' => '', 'copyPasteJsonValue' => '', ]; /** * Rest API class instance. * * @since 1.8.8 * * @var RestApi */ protected $rest_api_obj; /** * Rest API class instance. * * @since 1.8.8 * * @var ThemesData */ protected $themes_data_obj; /** * Render engine. * * @since 1.8.1 * * @var string */ protected $render_engine; /** * Disabled CSS setting. * * @since 1.8.1 * * @var integer */ protected $disable_css_setting; /** * Instance of CSSVars class. * * @since 1.8.1 * * @var CSSVars */ private $css_vars_obj; /** * Callbacks registered for wpforms_frontend_container_class filter. * * @since 1.7.5 * * @var array */ private $callbacks = []; /** * Currently displayed form ID. * * @since 1.8.8 * * @var string|int */ private $current_form_id = 0; /** * Indicate if the current integration is allowed to load. * * @since 1.4.8 * * @return bool */ public function allow_load(): bool { return function_exists( 'register_block_type' ); } /** * Load an integration. * * @since 1.4.8 */ public function load() { $this->render_engine = wpforms_get_render_engine(); $this->disable_css_setting = (int) wpforms_setting( 'disable-css', '1' ); $this->css_vars_obj = wpforms()->obj( 'css_vars' ); wpforms()->register_instance( 'formselector_themes_data', $this->themes_data_obj ); $this->hooks(); } /** * Integration hooks. * * @since 1.4.8 */ protected function hooks() { add_action( 'init', [ $this, 'register_block' ] ); add_action( 'enqueue_block_editor_assets', [ $this, 'enqueue_block_editor_assets' ] ); add_action( 'wpforms_frontend_output_container_after', [ $this, 'replace_wpforms_frontend_container_class_filter' ] ); add_filter( 'wpforms_frontend_form_action', [ $this, 'form_action_filter' ], 10, 2 ); add_filter( 'wpforms_forms_anti_spam_v3_is_honeypot_enabled', [ $this, 'filter_is_honeypot_enabled' ] ); add_filter( 'wpforms_field_richtext_display_editor_is_media_enabled', [ $this, 'disable_richtext_media' ], 10, 2 ); } /** * Disable honeypot in Gutenberg/Block editor. * * @since 1.9.0 * * @param bool|mixed $is_enabled True if the honeypot is enabled, false otherwise. * * @return bool Whether to disable the honeypot. */ public function filter_is_honeypot_enabled( $is_enabled ): bool { if ( wpforms_is_wpforms_rest() ) { return false; } return (bool) $is_enabled; } /** * Replace the filter registered for wpforms_frontend_container_class. * * @since 1.7.5 * * @param array $form_data Form data. * * @return void */ public function replace_wpforms_frontend_container_class_filter( array $form_data ): void { // phpcs:ignore WPForms.PHP.HooksMethod.InvalidPlaceForAddingHooks if ( empty( $this->callbacks[ $form_data['id'] ] ) ) { return; } $callback = array_shift( $this->callbacks[ $form_data['id'] ] ); remove_filter( 'wpforms_frontend_container_class', $callback ); if ( ! empty( $this->callbacks[ $form_data['id'] ] ) ) { add_filter( 'wpforms_frontend_container_class', reset( $this->callbacks[ $form_data['id'] ] ), 10, 2 ); } } /** * Register WPForms Gutenberg block on the backend. * * @since 1.4.8 */ public function register_block(): void { $type_string = [ 'type' => 'string' ]; $type_boolean = [ 'type' => 'boolean' ]; $attributes = [ 'clientId' => $type_string, 'formId' => $type_string, 'displayTitle' => $type_boolean, 'displayDesc' => $type_boolean, 'className' => $type_string, 'theme' => $type_string, 'themeName' => $type_string, 'fieldSize' => $type_string, 'fieldBorderRadius' => $type_string, 'fieldBorderStyle' => $type_string, 'fieldBorderSize' => $type_string, 'fieldBackgroundColor' => $type_string, 'fieldBorderColor' => $type_string, 'fieldTextColor' => $type_string, 'fieldMenuColor' => $type_string, 'labelSize' => $type_string, 'labelColor' => $type_string, 'labelSublabelColor' => $type_string, 'labelErrorColor' => $type_string, 'buttonSize' => $type_string, 'buttonBorderStyle' => $type_string, 'buttonBorderSize' => $type_string, 'buttonBorderRadius' => $type_string, 'buttonBackgroundColor' => $type_string, 'buttonBorderColor' => $type_string, 'buttonTextColor' => $type_string, 'pageBreakColor' => $type_string, 'backgroundImage' => $type_string, 'backgroundPosition' => $type_string, 'backgroundRepeat' => $type_string, 'backgroundSizeMode' => $type_string, 'backgroundSize' => $type_string, 'backgroundWidth' => $type_string, 'backgroundHeight' => $type_string, 'backgroundUrl' => $type_string, 'backgroundColor' => $type_string, 'containerPadding' => $type_string, 'containerBorderStyle' => $type_string, 'containerBorderWidth' => $type_string, 'containerBorderColor' => $type_string, 'containerBorderRadius' => $type_string, 'containerShadowSize' => $type_string, 'customCss' => $type_string, 'copyPasteJsonValue' => $type_string, ]; $this->register_styles(); /** * Modify WPForms block attributes. * * @since 1.5.8.2 * * @param array $attributes Attributes. */ $attributes = apply_filters( 'wpforms_gutenberg_form_selector_attributes', $attributes ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName register_block_type( 'wpforms/form-selector', [ 'api_version' => $this->get_block_api_version(), 'attributes' => $attributes, 'style' => 'wpforms-gutenberg-form-selector', 'editor_style' => 'wpforms-integrations', 'render_callback' => [ $this, 'get_form_html' ], ] ); } /** * Register WPForms Gutenberg block styles. * * @since 1.7.4.2 */ protected function register_styles() { if ( ! is_admin() ) { return; } $min = wpforms_get_min_suffix(); wp_register_style( 'wpforms-integrations', WPFORMS_PLUGIN_URL . "assets/css/admin-integrations{$min}.css", [ 'dashicons' ], WPFORMS_VERSION ); if ( $this->disable_css_setting === 3 ) { return; } $css_file = $this->disable_css_setting === 2 ? 'base' : 'full'; $handle = 'wpforms-gutenberg-form-selector'; wp_register_style( $handle, WPFORMS_PLUGIN_URL . "assets/css/frontend/{$this->render_engine}/wpforms-{$css_file}{$min}.css", [ 'wp-edit-blocks', 'wpforms-integrations' ], WPFORMS_VERSION ); // Add root CSS variables for the Modern Markup mode for full styles. if ( empty( $this->css_vars_obj ) || $this->render_engine !== 'modern' || $css_file !== 'full' ) { return; } wp_add_inline_style( $handle, $this->css_vars_obj->get_root_vars_css() ); } /** * Load WPForms Gutenberg block scripts. * * @since 1.4.8 */ public function enqueue_block_editor_assets() { $min = wpforms_get_min_suffix(); wp_enqueue_style( 'wpforms-integrations' ); wp_set_script_translations( 'wpforms-gutenberg-form-selector', 'wpforms-lite' ); // jQuery.Confirm Reloaded. wp_enqueue_style( 'jquery-confirm', WPFORMS_PLUGIN_URL . 'assets/lib/jquery.confirm/jquery-confirm.min.css', null, '1.0.0' ); wp_enqueue_script( 'jquery-confirm', WPFORMS_PLUGIN_URL . 'assets/lib/jquery.confirm/jquery-confirm.min.js', [ 'jquery' ], '1.0.0', false ); // Support for the legacy form selector. // It is located in the common namespace. if ( $this->is_legacy_block() ) { wp_enqueue_script( 'wpforms-gutenberg-form-selector', WPFORMS_PLUGIN_URL . "assets/js/integrations/gutenberg/formselector-legacy.es5{$min}.js", [ 'wp-blocks', 'wp-i18n', 'wp-element', 'jquery' ], WPFORMS_VERSION, true ); return; } if ( $this->render_engine === 'modern' ) { wp_enqueue_script( 'wpforms-modern', WPFORMS_PLUGIN_URL . "assets/js/frontend/wpforms-modern{$min}.js", [ 'wpforms-gutenberg-form-selector' ], WPFORMS_VERSION, true ); } wp_enqueue_script( 'wpforms-admin-education-core', WPFORMS_PLUGIN_URL . "assets/js/admin/education/core{$min}.js", [ 'jquery', 'jquery-confirm' ], WPFORMS_VERSION, true ); wp_localize_script( 'wpforms-admin-education-core', 'wpforms_education', $this->get_js_strings() ); } /** * Whether the block is legacy. * * @since 1.8.8 */ protected function is_legacy_block() { return version_compare( $GLOBALS['wp_version'], '6.0', '<' ); } /** * Get localize data. * * @since 1.8.1 * * @return array */ public function get_localize_data(): array { $strings = [ 'title' => esc_html__( 'WPForms', 'wpforms-lite' ), 'description' => esc_html__( 'Select and display one of your forms.', 'wpforms-lite' ), 'form_keywords' => [ esc_html__( 'form', 'wpforms-lite' ), esc_html__( 'contact', 'wpforms-lite' ), esc_html__( 'survey', 'wpforms-lite' ), ], 'form_select' => esc_html__( 'Select a Form', 'wpforms-lite' ), 'form_settings' => esc_html__( 'Form Settings', 'wpforms-lite' ), 'form_edit' => esc_html__( 'Edit Form', 'wpforms-lite' ), 'form_entries' => esc_html__( 'View Entries', 'wpforms-lite' ), 'themes' => esc_html__( 'Themes', 'wpforms-lite' ), 'theme_name' => esc_html__( 'Theme Name', 'wpforms-lite' ), 'theme_delete' => esc_html__( 'Delete Theme', 'wpforms-lite' ), 'theme_delete_title' => esc_html__( 'Delete Form Theme', 'wpforms-lite' ), // Translators: %1$s: Theme name. 'theme_delete_confirm' => esc_html__( 'Are you sure you want to delete the %1$s theme?', 'wpforms-lite' ), 'theme_delete_cant_undone' => esc_html__( 'This cannot be undone.', 'wpforms-lite' ), 'theme_delete_yes' => esc_html__( 'Yes, Delete', 'wpforms-lite' ), 'theme_copy' => esc_html__( 'Copy', 'wpforms-lite' ), 'theme_custom' => esc_html__( 'Custom Theme', 'wpforms-lite' ), 'theme_noname' => esc_html__( 'Noname Theme', 'wpforms-lite' ), 'field_styles' => esc_html__( 'Field Styles', 'wpforms-lite' ), 'field_label' => esc_html__( 'Field Label', 'wpforms-lite' ), 'field_sublabel' => esc_html__( 'Field Sublabel', 'wpforms-lite' ), 'field_border' => esc_html__( 'Field Border', 'wpforms-lite' ), 'label_styles' => esc_html__( 'Label Styles', 'wpforms-lite' ), 'button_background' => esc_html__( 'Button Background', 'wpforms-lite' ), 'button_text' => esc_html__( 'Button Text', 'wpforms-lite' ), 'button_styles' => esc_html__( 'Button Styles', 'wpforms-lite' ), 'container_styles' => esc_html__( 'Container Styles', 'wpforms-lite' ), 'background_styles' => esc_html__( 'Background Styles', 'wpforms-lite' ), 'remove_image' => esc_html__( 'Remove Image', 'wpforms-lite' ), 'position' => esc_html__( 'Position', 'wpforms-lite' ), 'top_left' => esc_html__( 'Top Left', 'wpforms-lite' ), 'top_center' => esc_html__( 'Top Center', 'wpforms-lite' ), 'top_right' => esc_html__( 'Top Right', 'wpforms-lite' ), 'center_left' => esc_html__( 'Center Left', 'wpforms-lite' ), 'center_center' => esc_html__( 'Center Center', 'wpforms-lite' ), 'center_right' => esc_html__( 'Center Right', 'wpforms-lite' ), 'bottom_left' => esc_html__( 'Bottom Left', 'wpforms-lite' ), 'bottom_center' => esc_html__( 'Bottom Center', 'wpforms-lite' ), 'bottom_right' => esc_html__( 'Bottom Right', 'wpforms-lite' ), 'repeat' => esc_html__( 'Repeat', 'wpforms-lite' ), 'no_repeat' => esc_html__( 'No Repeat', 'wpforms-lite' ), 'repeat_x' => esc_html__( 'Repeat Horizontal', 'wpforms-lite' ), 'repeat_y' => esc_html__( 'Repeat Vertical', 'wpforms-lite' ), 'tile' => esc_html__( 'Tile', 'wpforms-lite' ), 'cover' => esc_html__( 'Cover', 'wpforms-lite' ), 'dimensions' => esc_html__( 'Dimensions', 'wpforms-lite' ), 'width' => esc_html__( 'Width', 'wpforms-lite' ), 'height' => esc_html__( 'Height', 'wpforms-lite' ), 'button_color_notice' => esc_html__( 'Also used for other fields like Multiple Choice, Checkboxes, Rating, and NPS Survey.', 'wpforms-lite' ), 'advanced' => esc_html__( 'Advanced', 'wpforms-lite' ), 'additional_css_classes' => esc_html__( 'Additional CSS Classes', 'wpforms-lite' ), 'form_selected' => esc_html__( 'Form', 'wpforms-lite' ), 'show_title' => esc_html__( 'Show Title', 'wpforms-lite' ), 'show_description' => esc_html__( 'Show Description', 'wpforms-lite' ), 'panel_notice_head' => esc_html__( 'Heads up!', 'wpforms-lite' ), 'panel_notice_text' => esc_html__( 'Do not forget to test your form.', 'wpforms-lite' ), 'panel_notice_link' => esc_url( wpforms_utm_link( 'https://wpforms.com/docs/how-to-properly-test-your-wordpress-forms-before-launching-checklist/', 'gutenberg' ) ), 'panel_notice_link_text' => esc_html__( 'Check out our complete guide!', 'wpforms-lite' ), 'update_wp_notice_head' => esc_html__( 'Want to customize your form styles without editing CSS?', 'wpforms-lite' ), 'update_wp_notice_text' => esc_html__( 'Update WordPress to the latest version to use our modern markup and unlock the controls below.', 'wpforms-lite' ), 'update_wp_notice_link' => esc_url( wpforms_utm_link( 'https://wpforms.com/docs/styling-your-forms/', 'Block Settings', 'Form Styles Documentation' ) ), 'learn_more' => esc_html__( 'Learn more', 'wpforms-lite' ), 'use_modern_notice_head' => esc_html__( 'Want to customize your form styles without editing CSS?', 'wpforms-lite' ), 'use_modern_notice_text' => esc_html__( 'Enable modern markup in your WPForms settings to unlock the controls below.', 'wpforms-lite' ), 'use_modern_notice_link' => esc_url( wpforms_utm_link( 'https://wpforms.com/docs/styling-your-forms/', 'Block Settings', 'Form Styles Documentation' ) ), 'lead_forms_panel_notice_head' => esc_html__( 'Form Styles are disabled because Lead Form Mode is turned on.', 'wpforms-lite' ), 'lead_forms_panel_notice_text' => esc_html__( 'To change the styling for this form, open it in the form builder and edit the options in the Lead Forms settings.', 'wpforms-lite' ), 'size' => esc_html__( 'Size', 'wpforms-lite' ), 'padding' => esc_html__( 'Padding', 'wpforms-lite' ), 'background' => esc_html__( 'Background', 'wpforms-lite' ), 'border' => esc_html__( 'Border', 'wpforms-lite' ), 'text' => esc_html__( 'Text', 'wpforms-lite' ), 'menu' => esc_html__( 'Menu', 'wpforms-lite' ), 'image' => esc_html__( 'Image', 'wpforms-lite' ), 'media_library' => esc_html__( 'Media Library', 'wpforms-lite' ), 'choose_image' => esc_html__( 'Choose Image', 'wpforms-lite' ), 'stock_photo' => esc_html__( 'Stock Photo', 'wpforms-lite' ), 'border_radius' => esc_html__( 'Border Radius', 'wpforms-lite' ), 'border_size' => esc_html__( 'Border Size', 'wpforms-lite' ), 'border_style' => esc_html__( 'Border Style', 'wpforms-lite' ), 'none' => esc_html__( 'None', 'wpforms-lite' ), 'solid' => esc_html__( 'Solid', 'wpforms-lite' ), 'dashed' => esc_html__( 'Dashed', 'wpforms-lite' ), 'dotted' => esc_html__( 'Dotted', 'wpforms-lite' ), 'double' => esc_html__( 'Double', 'wpforms-lite' ), 'shadow_size' => esc_html__( 'Shadow', 'wpforms-lite' ), 'border_width' => esc_html__( 'Border Size', 'wpforms-lite' ), 'border_color' => esc_html__( 'Border', 'wpforms-lite' ), 'colors' => esc_html__( 'Colors', 'wpforms-lite' ), 'label' => esc_html__( 'Label', 'wpforms-lite' ), 'sublabel_hints' => esc_html__( 'Sublabel & Hint', 'wpforms-lite' ), 'error_message' => esc_html__( 'Error Message', 'wpforms-lite' ), 'small' => esc_html__( 'Small', 'wpforms-lite' ), 'medium' => esc_html__( 'Medium', 'wpforms-lite' ), 'large' => esc_html__( 'Large', 'wpforms-lite' ), 'btn_yes' => esc_html__( 'Yes', 'wpforms-lite' ), 'btn_no' => esc_html__( 'No', 'wpforms-lite' ), 'copy_paste_settings' => esc_html__( 'Copy / Paste Style Settings', 'wpforms-lite' ), 'copy_paste_error' => esc_html__( 'There was an error parsing your JSON code. Please check your code and try again.', 'wpforms-lite' ), 'copy_paste_notice' => esc_html__( 'If you\'ve copied style settings from another form, you can paste them here to add the same styling to this form. Any current style settings will be overwritten.', 'wpforms-lite' ), 'custom_css' => esc_html__( 'Custom CSS', 'wpforms-lite' ), 'custom_css_notice' => esc_html__( 'Further customize the look of this form without having to edit theme files.', 'wpforms-lite' ), // Translators: %1$s: Opening strong tag, %2$s: Closing strong tag. 'wpforms_empty_info' => sprintf( esc_html__( 'You can use %1$sWPForms%2$s to build contact forms, surveys, payment forms, and more with just a few clicks.', 'wpforms-lite' ), '<strong>','</strong>' ), // Translators: %1$s: Opening anchor tag, %2$s: Closing anchor tag. 'wpforms_empty_help' => sprintf( esc_html__( 'Need some help? Check out our %1$scomprehensive guide.%2$s', 'wpforms-lite' ), '<a target="_blank" href="' . esc_url( wpforms_utm_link( 'https://wpforms.com/docs/creating-first-form/', 'gutenberg', 'Create Your First Form Documentation' ) ) . '">','</a>' ), 'other_styles' => esc_html__( 'Other Styles', 'wpforms-lite' ), 'page_break' => esc_html__( 'Page Break', 'wpforms-lite' ), 'rating' => esc_html__( 'Rating', 'wpforms-lite' ), 'heads_up' => esc_html__( 'Heads Up!', 'wpforms-lite' ), 'form_not_available_message' => esc_html__( 'It looks like the form you had selected is in the Trash or has been permanently deleted.', 'wpforms-lite' ), ]; return [ 'logo_url' => WPFORMS_PLUGIN_URL . 'assets/images/wpforms-logo.svg', 'block_preview_url' => WPFORMS_PLUGIN_URL . 'assets/images/integrations/gutenberg/block-preview.png', 'block_empty_url' => WPFORMS_PLUGIN_URL . 'assets/images/empty-states/no-forms.svg', 'route_namespace' => RestApi::ROUTE_NAMESPACE, 'wpnonce' => wp_create_nonce( 'wpforms-gutenberg-form-selector' ), 'urls' => [ 'form_url' => admin_url( 'admin.php?page=wpforms-builder&view=fields&form_id={ID}' ), 'entries_url' => admin_url( 'admin.php?view=list&page=wpforms-entries&form_id={ID}' ), ], 'forms' => $this->get_form_list(), 'strings' => $strings, 'isAdmin' => current_user_can( 'manage_options' ), 'isPro' => wpforms()->is_pro(), 'defaults' => self::DEFAULT_ATTRIBUTES, 'is_modern_markup' => $this->render_engine === 'modern', 'is_full_styling' => $this->disable_css_setting === 1, 'wpforms_guide' => esc_url( wpforms_utm_link( 'https://wpforms.com/docs/creating-first-form/', 'gutenberg', 'Create Your First Form Documentation' ) ), 'get_started_url' => esc_url( admin_url( 'admin.php?page=wpforms-builder' ) ), 'sizes' => [ 'field-size' => CSSVars::FIELD_SIZE, 'label-size' => CSSVars::LABEL_SIZE, 'button-size' => CSSVars::BUTTON_SIZE, 'container-shadow-size' => CSSVars::CONTAINER_SHADOW_SIZE, ], ]; } /** * Get the form list. * * @since 1.8.8 * * @return array * @noinspection NullPointerExceptionInspection */ public function get_form_list(): array { $forms = wpforms()->obj( 'form' )->get( '', [ 'order' => 'DESC' ] ); if ( empty( $forms ) ) { return []; } return array_map( static function ( $form ) { $form->post_title = htmlspecialchars_decode( $form->post_title, ENT_QUOTES ); $max_length = 47; $form->post_title = trim( mb_substr( trim( $form->post_title ), 0, $max_length ) ); $form->post_title = mb_strlen( $form->post_title ) === $max_length ? $form->post_title . '…' : $form->post_title; return $form; }, $forms ); } /** * Filter form action. * * @since 1.8.8 * * @param string|mixed $action Form action. * @param array|mixed $form_data Form data. * * @return string * @noinspection PhpUnusedParameterInspection */ public function form_action_filter( $action, $form_data ): string { if ( $this->is_gb_editor() ) { // Remove inappropriate form action URL that contains all the block attributes. $action = ''; } return (string) $action; } /** * Get form HTML to display in a WPForms Gutenberg block. * * @since 1.4.8 * * @param array|mixed $attr Attributes passed by WPForms Gutenberg block. * * @return string */ public function get_form_html( $attr ): string { $attr = (array) $attr; $id = ! empty( $attr['formId'] ) ? absint( $attr['formId'] ) : 0; $this->current_form_id = $id; if ( empty( $id ) ) { return ''; } if ( $this->is_gb_editor() ) { $this->disable_fields_in_gb_editor(); } $title = ! empty( $attr['displayTitle'] ); $desc = ! empty( $attr['displayDesc'] ); $this->add_class_callback( $id, $attr ); // Maybe override block attributes with the theme settings. $attr = $this->maybe_override_block_attributes( $attr ); // Get block content. $content = $this->get_content( $id, $title, $desc, $attr ); // phpcs:disable WPForms.PHP.ValidateHooks.InvalidHookName /** * Filter Gutenberg block content. * * @since 1.5.8.2 * * @param string $content Block content. * @param int $id Form id. */ return (string) apply_filters( 'wpforms_gutenberg_block_form_content', $content, $id ); // phpcs:enable WPForms.PHP.ValidateHooks.InvalidHookName } /** * Maybe override block attributes. * * This method is used to override block attributes with the theme settings. * * @since 1.8.8 * * @param array $attr Attributes passed by WPForms Gutenberg block. * * @return array */ private function maybe_override_block_attributes( array $attr ): array { $theme_slug = (string) ( $attr['theme'] ?? '' ); // Previously added blocks (FS 1.0) don't have the themeName attribute. // To preserve existing styling of such old blocks, we shouldn't override attributes. if ( ! isset( $attr['themeName'] ) || ( empty( $attr['themeName'] ) && $theme_slug === 'default' ) ) { return $attr; } if ( $theme_slug === '' ) { $theme_slug = $this->get_theme_slug( $attr ); } $theme_data = $this->themes_data_obj->get_theme( $theme_slug ); // Theme doesn't exist, let's return. if ( ! $theme_data ) { return $attr; } // Override block attributes with the theme settings. return array_merge( $attr, $theme_data['settings'] ); } /** * Get the theme slug. * * @since 1.9.7 * * @param array $attr Attributes passed by WPForms Gutenberg block. * * @return string */ private function get_theme_slug( array $attr ): string { $form_handler = wpforms()->obj( 'form' ); if ( ! $form_handler ) { return 'default'; } $form_id = (int) $attr['formId']; $form_data = $form_handler->get( $form_id, [ 'content_only' => true ] ); if ( empty( $form_data['settings']['themes']['wpformsTheme'] ) ) { return 'default'; } return $form_data['settings']['themes']['wpformsTheme']; } /** * Add class callback. * * @since 1.8.1 * * @param int $id Form id. * @param array $attr Form attributes. * * @return void */ private function add_class_callback( int $id, array $attr ): void { // phpcs:ignore WPForms.PHP.HooksMethod.InvalidPlaceForAddingHooks $class_callback = static function ( $classes, $form_data ) use ( $id, $attr ) { if ( (int) $form_data['id'] !== $id ) { return $classes; } $cls = []; // Add custom class to form container. if ( ! empty( $attr['className'] ) ) { $cls = array_map( 'esc_attr', explode( ' ', $attr['className'] ) ); } // Add classes to identify that the form displays inside the block. $cls[] = 'wpforms-block'; if ( ! empty( $attr['clientId'] ) ) { $cls[] = 'wpforms-block-' . $attr['clientId']; } return array_unique( array_merge( $classes, $cls ) ); }; if ( empty( $this->callbacks[ $id ] ) ) { add_filter( 'wpforms_frontend_container_class', $class_callback, 10, 2 ); } $this->callbacks[ $id ][] = $class_callback; } /** * Get content. * * @since 1.8.1 * * @param int $id Form id. * @param bool $title Form title is not empty. * @param bool $desc Form desc is not empty. * @param array $attr Form attributes. * * @return string * @noinspection JSUnresolvedReference */ private function get_content( int $id, bool $title, bool $desc, array $attr ): string { /** * Filter allow render block content flag. * * @since 1.8.8 * * @param bool $allow_render Allow render flag. Defaults to `true`. */ $allow_render = (bool) apply_filters( 'wpforms_integrations_gutenberg_form_selector_allow_render', true ); if ( ! $allow_render ) { return ''; } ob_start(); // phpcs:disable WPForms.PHP.ValidateHooks.InvalidHookName /** * Fires before Gutenberg block output. * * @since 1.5.8.2 */ do_action( 'wpforms_gutenberg_block_before' ); /** * Filter block title display flag. * * @since 1.5.8.2 * * @param bool $title Title display flag. * @param int $id Form id. */ $title = (bool) apply_filters( 'wpforms_gutenberg_block_form_title', $title, $id ); /** * Filter block description display flag. * * @since 1.5.8.2 * * @param bool $desc Description display flag. * @param int $id Form id. */ $desc = (bool) apply_filters( 'wpforms_gutenberg_block_form_desc', $desc, $id ); $this->output_css_vars( $attr ); $this->output_custom_css( $attr ); wpforms_display( $id, $title, $desc ); /** * Fires after Gutenberg block output. * * @since 1.5.8.2 */ do_action( 'wpforms_gutenberg_block_after' ); // phpcs:enable WPForms.PHP.ValidateHooks.InvalidHookName $content = (string) ob_get_clean(); if ( ! $this->is_gb_editor() ) { return $content; } if ( empty( $content ) ) { return '<div class="components-placeholder"><div class="components-placeholder__label"></div>' . '<div class="components-placeholder__fieldset">' . esc_html__( 'The form cannot be displayed.', 'wpforms-lite' ) . '</div></div>'; } /** * Unfortunately, the inline 'script' tag cannot be executed in the GB editor. * This is the hacky way to trigger custom event on form loaded in the Block Editor / GB / FSE. */ // phpcs:disable WordPress.PHP.DevelopmentFunctions.error_log_var_export $content .= sprintf( // language=JavaScript '<img src="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==" onLoad=" window.top.dispatchEvent( new CustomEvent( \'wpformsFormSelectorFormLoaded\', { detail: { formId: %1$s, title: %2$s, desc: %3$s, block: this.closest( \'.wp-block\' ) } } ) ); " class="wpforms-pix-trigger" alt="">', absint( $id ), var_export( $title, true ), var_export( $desc, true ) ); // phpcs:enable WordPress.PHP.DevelopmentFunctions.error_log_var_export return $content; } /** * Checking if is Gutenberg REST API call. * * @since 1.5.7 * * @return bool True if is Gutenberg REST API call. */ public function is_gb_editor(): bool { // TODO: Find a better way to check if is GB editor API call. // phpcs:ignore WordPress.Security.NonceVerification.Recommended return defined( 'REST_REQUEST' ) && REST_REQUEST && ! empty( $_REQUEST['context'] ) && $_REQUEST['context'] === 'edit'; } /** * Disable form fields if called from the Gutenberg editor. * * @since 1.7.5 * * @return void */ private function disable_fields_in_gb_editor(): void { // phpcs:ignore WPForms.PHP.HooksMethod.InvalidPlaceForAddingHooks add_filter( 'wpforms_frontend_container_class', static function ( $classes ) { $classes[] = 'wpforms-gutenberg-form-selector'; return $classes; } ); add_action( 'wpforms_frontend_output', static function () { echo '<fieldset disabled>'; }, 3 ); add_action( 'wpforms_frontend_output', static function () { echo '</fieldset>'; }, 30 ); } /** * Output CSS variables for the particular form. * * @since 1.8.1 * * @param array $attr Attributes passed by WPForms Gutenberg block. */ private function output_css_vars( array $attr ): void { if ( empty( $this->css_vars_obj ) || ! method_exists( $this->css_vars_obj, 'get_vars' ) ) { return; } if ( $this->render_engine === 'classic' || $this->disable_css_setting !== 1 ) { return; } $css_vars = $this->css_vars_obj->get_customized_css_vars( $attr ); if ( empty( $css_vars ) ) { return; } $style_id = "#wpforms-css-vars-{$attr['formId']}-block-{$attr['clientId']}"; /** * Filter the CSS selector for output CSS variables for styling the GB block form. * * @since 1.8.1 * * @param string $selector The CSS selector for output CSS variables for styling the GB block form. * @param array $attr Attributes passed by WPForms Gutenberg block. * @param array $css_vars CSS variables data. */ $vars_selector = apply_filters( 'wpforms_integrations_gutenberg_form_selector_output_css_vars_selector', "#wpforms-{$attr['formId']}.wpforms-block-{$attr['clientId']}", $attr, $css_vars ); $style_id = rtrim( $style_id, '-' ); $vars_selector = rtrim( $vars_selector, '-' ); $this->css_vars_obj->output_selector_vars( $vars_selector, $css_vars, $style_id, $this->current_form_id ); } /** * Output custom CSS styles. * * @since 1.8.8 * * @param array $attr Attributes passed by WPForms Gutenberg block. */ private function output_custom_css( array $attr ): void { if ( wpforms_get_render_engine() === 'classic' ) { return; } $custom_css = trim( $attr['customCss'] ?? '' ); if ( empty( $custom_css ) ) { return; } $style_id = "#wpforms-custom-css-{$attr['formId']}-block-{$attr['clientId']}"; printf( '<style id="%1$s"> %2$s </style>', sanitize_key( $style_id ), wp_strip_all_tags( $custom_css ) // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ); } /** * Disable loading media for the richtext editor for edit action to prevent script conflicts. * * @since 1.9.1 * * @param bool|mixed $media_enabled Whether to enable media. * @param array $field Field data. * * @return bool * @noinspection PhpUnusedParameterInspection */ public function disable_richtext_media( $media_enabled, array $field ): bool { // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( ! empty( $_REQUEST['action'] ) && $_REQUEST['action'] === 'edit' && is_admin() ) { return false; } return (bool) $media_enabled; } /** * Get block API version based on WP core version. * * @since 1.9.3 * * @return int Block API version. */ private function get_block_api_version(): int { if ( $this->is_legacy_block() ) { return 1; } return version_compare( $GLOBALS['wp_version'], '6.3', '<' ) ? 2 : 3; } } Integrations/Gutenberg/RestApi.php 0000644 00000012541 15174710275 0013230 0 ustar 00 <?php namespace WPForms\Integrations\Gutenberg; use WP_Error; use WP_REST_Request; // phpcs:ignore WPForms.PHP.UseStatement.UnusedUseStatement use WP_REST_Response; // phpcs:ignore WPForms.PHP.UseStatement.UnusedUseStatement /** * Rest API for Gutenberg block. * * @since 1.8.8 */ class RestApi { /** * Route prefix. * * @since 1.8.8 * * @var string */ const ROUTE_NAMESPACE = '/wpforms/v1/'; /** * FormSelector class instance. * * @since 1.8.8 * * @var FormSelector */ private $form_selector_obj; /** * ThemesData class instance. * * @since 1.8.8 * * @var ThemesData */ private $themes_data_obj; /** * Initialize class. * * @since 1.8.8 * * @param FormSelector|mixed $form_selector_obj FormSelector object. * @param ThemesData|mixed $themes_data_obj ThemesData object. */ public function __construct( $form_selector_obj, $themes_data_obj ) { if ( ! $form_selector_obj || ! $themes_data_obj || ! wpforms_is_wpforms_rest() ) { return; } $this->form_selector_obj = $form_selector_obj; $this->themes_data_obj = $themes_data_obj; $this->hooks(); } /** * Hooks. * * @since 1.8.8 */ private function hooks() { add_action( 'rest_api_init', [ $this, 'register_api_routes' ], 20 ); } /** * Register API routes for Gutenberg block. * * @since 1.8.8 */ public function register_api_routes() { /** * Register routes with WordPress. * * @see https://developer.wordpress.org/reference/functions/register_rest_route/ */ register_rest_route( self::ROUTE_NAMESPACE, '/forms/', [ 'methods' => 'GET', 'callback' => [ $this, 'get_forms' ], 'permission_callback' => [ $this, 'forms_permissions_check' ], ] ); register_rest_route( self::ROUTE_NAMESPACE, '/themes/', [ 'methods' => 'GET', 'callback' => [ $this, 'get_themes' ], 'permission_callback' => [ $this, 'permissions_check' ], ] ); register_rest_route( self::ROUTE_NAMESPACE, '/themes/custom/', [ 'methods' => 'POST', 'callback' => [ $this, 'save_themes' ], 'permission_callback' => [ $this, 'admin_permissions_check' ], ] ); } /** * Check if a user has permission to access private data. * * @since 1.8.8 * * @see https://developer.wordpress.org/rest-api/extending-the-rest-api/routes-and-endpoints/#permissions-callback * * @return true|WP_Error True if a user has permission. */ public function permissions_check() { // Restrict endpoint to only users who have the edit_posts capability. if ( ! current_user_can( 'edit_posts' ) ) { return new WP_Error( 'rest_forbidden', esc_html__( 'This route is private.', 'wpforms-lite' ), [ 'status' => 401 ] ); } return true; } /** * Check if a user has permission to access forms data. * * @since 1.9.9.4 * * @return true|WP_Error True if a user has permission. */ public function forms_permissions_check() { // Restrict endpoint to only users who have WPForms capabilities. if ( ! wpforms_current_user_can() ) { return new WP_Error( 'rest_forbidden', esc_html__( 'This route is private.', 'wpforms-lite' ), [ 'status' => 401 ] ); } return true; } /** * Check if a user has admin permissions. * * @since 1.9.2.3 * * @return true|WP_Error True if a user has permission. */ public function admin_permissions_check() { // Restrict endpoint to only users who have the manage_options capability. if ( ! current_user_can( 'manage_options' ) ) { return new WP_Error( 'rest_forbidden', esc_html__( 'This route is accessible only to administrators.', 'wpforms-lite' ), [ 'status' => 401 ] ); } return true; } /** * Return form list protected WP_REST_Response object. * * @since 1.8.8 * * @return WP_Error|WP_REST_Response */ public function get_forms() { return rest_ensure_response( $this->form_selector_obj->get_form_list() ); } /** * Return themes as protected WP_REST_Response object. * * @since 1.8.8 * * @return WP_Error|WP_REST_Response */ public function get_themes() { $custom_themes = $this->themes_data_obj->get_custom_themes(); $wpforms_themes = $this->themes_data_obj->get_wpforms_themes(); return rest_ensure_response( [ 'custom' => ! empty( $custom_themes ) ? $custom_themes : null, 'wpforms' => ! empty( $wpforms_themes ) ? $wpforms_themes : null, ] ); } /** * Save custom themes. * * @since 1.8.8 * * @param WP_REST_Request $request Request object. * * @return WP_Error|WP_REST_Response */ public function save_themes( WP_REST_Request $request ) { // Determine custom themes file path. $themes_file = $this->themes_data_obj->get_custom_themes_file_path(); // In the case of error. if ( ! $themes_file ) { return rest_ensure_response( [ 'result' => false, 'error' => esc_html__( 'Can\'t create themes storage file.', 'wpforms-lite' ), ] ); } $custom_themes = (array) ( $request->get_param( 'customThemes' ) ?? [] ); // Save custom themes data and return REST response. $result = $this->themes_data_obj->update_custom_themes_file( $custom_themes ); if ( ! $result ) { return rest_ensure_response( [ 'result' => false, 'error' => esc_html__( 'Can\'t save theme data.', 'wpforms-lite' ), ] ); } return rest_ensure_response( [ 'result' => true ] ); } } Integrations/SMTP/Notifications.php 0000644 00000023171 15174710275 0013334 0 ustar 00 <?php namespace WPForms\Integrations\SMTP; use WPForms\Integrations\IntegrationInterface; /** * Notifications class. * * @since 1.7.6 */ class Notifications implements IntegrationInterface { /** * Determine if the class is allowed to load. * * @since 1.7.6 * * @return bool */ public function allow_load() { return wpforms_is_admin_page( 'builder' ) || wpforms_is_admin_ajax(); } /** * Load the class. * * @since 1.7.6 */ public function load() { $this->hooks(); } /** * Hooks. * * @since 1.7.6 */ private function hooks() { add_filter( 'wpforms_builder_notifications_sender_address_settings', [ $this, 'change_from_email_settings' ], PHP_INT_MIN, 3 ); add_filter( 'wpforms_builder_notifications_sender_name_settings', [ $this, 'change_from_name_settings' ], PHP_INT_MIN, 3 ); add_action( 'wp_ajax_wpforms_builder_notification_from_email_validate', [ $this, 'notification_from_email_validate' ] ); add_filter( 'wpforms_builder_strings', [ $this, 'form_builder_strings' ], 10, 2 ); } /** * Validate email. * * @since 1.8.1 */ public function notification_from_email_validate() { check_ajax_referer( 'wpforms-builder', 'nonce' ); // Before checking if $_POST['email'] is valid email, we need to check if smart tag is used and return its value. $email = ! empty( $_POST['email'] ) ? sanitize_text_field( wp_unslash( $_POST['email'] ) ) : ''; $email = $email ? sanitize_email( wpforms_process_smart_tags( $email, [], [], '', 'smtp-notification-validation' ) ) : ''; if ( ! is_email( $email ) ) { wp_send_json_error( sprintf( '<div class="wpforms-alert wpforms-alert-warning wpforms-alert-warning-wide">%s</div>', __( 'Please enter a valid email address. Your notifications won\'t be sent if the field is not filled in correctly.', 'wpforms-lite' ) ) ); } if ( ! $this->email_domain_matches_site_domain( $email ) ) { wp_send_json_error( $this->get_warning_message() ); } wp_send_json_success(); } /** * Append additional strings for form builder. * * @since 1.8.1 * * @param array $strings List of strings. * @param object $form Current form object. * * @return array */ public function form_builder_strings( $strings, $form ) { $strings['empty_email_address'] = esc_html__( 'Please enter a valid email address. Your notifications won\'t be sent if the field is not filled in correctly.', 'wpforms-lite' ); $strings['allow_only_one_email'] = esc_html__( 'Notifications can only use 1 From Email. Please do not enter multiple addresses.', 'wpforms-lite' ); $strings['allow_only_email_fields'] = esc_html__( 'This smart tag does not point to an Email field in your form.', 'wpforms-lite' ); return $strings; } /** * Add warning message when email doesn't match site domain. * * @since 1.7.6 * * @param array $args Field settings. * @param array $form_data Form data. * @param int $id Notification ID. * * @return array */ public function change_from_email_settings( $args, $form_data, $id ) { // phpcs:disable WPForms.PHP.ValidateHooks.InvalidHookName /** This filter is documented in lite/wpforms-lite.php */ $from_email_after = apply_filters( 'wpforms_builder_notifications_from_email_after', '', $form_data, $id ); // phpcs:enable WPForms.PHP.ValidateHooks.InvalidHookName if ( ! empty( $from_email_after ) ) { $default = [ 'readonly' => true, 'after' => '<div class="wpforms-alert wpforms-alert-warning">' . $from_email_after . '</div>', 'input_class' => 'wpforms-disabled', 'class' => 'from-email wpforms-panel-field-warning', ]; } else { $default = [ 'class' => 'from-email js-wpforms-from-email-validation', 'tooltip' => esc_html__( 'Notifications can only use 1 From Email. Please do not enter multiple addresses.', 'wpforms-lite' ), ]; } $args = wp_parse_args( $args, $default ); return $args; } /** * Add warning message when name empty. * * @since 1.8.4 * * @param array $args Field settings. * @param array $form_data Form data. * @param int $id Notification ID. * * @return array */ public function change_from_name_settings( $args, $form_data, $id ) { // phpcs:disable WPForms.PHP.ValidateHooks.InvalidHookName /** This filter is documented in lite/wpforms-lite.php */ $from_name_after = apply_filters( 'wpforms_builder_notifications_from_name_after', '', $form_data, $id ); // phpcs:enable WPForms.PHP.ValidateHooks.InvalidHookName if ( ! empty( $from_name_after ) ) { $default = [ 'readonly' => true, 'after' => '<div class="wpforms-alert wpforms-alert-warning">' . $from_name_after . '</div>', 'input_class' => 'wpforms-disabled', 'class' => 'from-name wpforms-panel-field-warning', ]; } else { $default = [ 'class' => 'from-name', ]; } return wp_parse_args( $args, $default ); } /** * Get warning message. * * @since 1.8.1 * * @return string */ private function get_warning_message() { $site_domain = wp_parse_url( get_bloginfo( 'wpurl' ) )['host']; $email_does_not_match_text = sprintf( /* translators: %1$s - WordPress site domain. */ __( 'The current \'From Email\' address does not match your website domain name (%1$s). This can cause your notification emails to be blocked or marked as spam.', 'wpforms-lite' ), esc_html( $site_domain ) ); $install_wp_mail_smtp_text = ''; // If WP Mail SMTP is not active, show a message to install it. if ( ! is_plugin_active( 'wp-mail-smtp-pro/wp_mail_smtp.php' ) && ! is_plugin_active( 'wp-mail-smtp/wp_mail_smtp.php' ) ) { $install_wp_mail_smtp_text .= sprintf( wp_kses( /* translators: %1$s - WP Mail SMTP install page URL. */ __( 'We strongly recommend that you install the free <a href="%1$s" target="_blank">WP Mail SMTP</a> plugin! The Setup Wizard makes it easy to fix your emails.', 'wpforms-lite' ), [ 'a' => [ 'href' => [], 'target' => [], ], ] ), esc_url( admin_url( 'admin.php?page=wpforms-smtp' ) ) ); } $address_match_text = sprintf( /* translators: %1$s - WordPress site domain. */ __( 'Alternately, try using a From Address that matches your website domain (admin@%1$s).', 'wpforms-lite' ), esc_html( $site_domain ) ); $fix_email_delivery_text = sprintf( wp_kses( /* translators: %1$s - fixing email delivery issues doc URL. */ __( 'Please check out our <a href="%1$s" target="_blank" rel="noopener noreferrer">doc on fixing email delivery issues</a> for more details.', 'wpforms-lite' ), [ 'a' => [ 'href' => [], 'target' => [], 'rel' => [], ], ] ), esc_url( wpforms_utm_link( 'https://wpforms.com/docs/how-to-fix-wordpress-contact-form-not-sending-email-with-smtp/', 'Builder Notifications', 'Delivery Issues Documentation' ) ) ); return sprintf( '<div class="wpforms-alert wpforms-alert-warning wpforms-alert-warning-wide"> <p>%1$s</p> <p>%2$s</p> <p>%3$s</p> <p>%4$s</p> </div>', $email_does_not_match_text, $install_wp_mail_smtp_text, $address_match_text, $fix_email_delivery_text ); } /** * Check if the domain name in an email address matches the WordPress site domain. * * @since 1.7.6 * * @param string $email The email address to check against the WordPress site domain. * * @return bool */ private function email_domain_matches_site_domain( $email ) { // Process smart tags if they are used as a value. $email = wpforms_process_smart_tags( $email, [], [], '', 'smtp-notification-validation' ); // Skip processing when email is empty or does not set. // e.g. {field_id="3"} which we don't have at the moment. if ( empty( $email ) ) { return true; } $email_domain = substr( strrchr( $email, '@' ), 1 ); $site_domain = wp_parse_url( get_bloginfo( 'wpurl' ) )['host']; // Check if From email domain ends with site domain. return ! empty( $email_domain ) && preg_match( "/\b{$email_domain}$/", $site_domain ) === 1; } /** * Check if the site has any active SMTP plugins. * * @since 1.7.6 * * @return bool */ private function has_active_smtp_plugin() { // List of plugins from \WPMailSMTP\Conflicts. $smtp_plugin_list = [ 'branda-white-labeling/ultimate-branding.php', 'bws-smtp/bws-smtp.php', 'cimy-swift-smtp/cimy_swift_smtp.php', 'disable-emails/disable-emails.php', 'easy-wp-smtp/easy-wp-smtp.php', 'fluent-smtp/fluent-smtp.php', 'gmail-smtp/main.php', 'mailgun/mailgun.php', 'my-smtp-wp/my-smtp-wp.php', 'post-smtp/postman-smtp.php', 'postman-smtp/postman-smtp.php', 'postmark-approved-wordpress-plugin/postmark.php', 'sar-friendly-smtp/sar-friendly-smtp.php', 'sendgrid-email-delivery-simplified/wpsendgrid.php', 'smtp-mail/index.php', 'smtp-mailer/main.php', 'sparkpost/wordpress-sparkpost.php', 'turbosmtp/turbo-smtp-plugin.php', 'woocommerce-sendinblue-newsletter-subscription/woocommerce-sendinblue.php', 'wp-amazon-ses-smtp/wp-amazon-ses.php', 'wp-easy-smtp/wp-easy-smtp.php', 'wp-gmail-smtp/wp-gmail-smtp.php', 'wp-html-mail/wp-html-mail.php', 'wp-mail-bank/wp-mail-bank.php', 'wp-mail-booster/wp-mail-booster.php', 'wp-mail-smtp-mailer/wp-mail-smtp-mailer.php', 'wp-mail-smtp-pro/wp_mail_smtp.php', 'wp-mail-smtp/wp_mail_smtp.php', 'wp-mailgun-smtp/wp-mailgun-smtp.php', 'wp-offload-ses/wp-offload-ses.php', 'wp-sendgrid-smtp/wp-sendgrid-smtp.php', 'wp-ses/wp-ses.php', 'wp-smtp/wp-smtp.php', 'wp-yahoo-smtp/wp-yahoo-smtp.php', ]; foreach ( $smtp_plugin_list as $smtp_plugin ) { if ( is_plugin_active( $smtp_plugin ) ) { return true; } } return false; } } Integrations/MotoPress/MotoPress.php 0000644 00000002351 15174710275 0013623 0 ustar 00 <?php namespace WPForms\Integrations\MotoPress; use WPForms\Integrations\IntegrationInterface; /** * Improve MotoPress compatibility. * * @since 1.9.9 */ class MotoPress implements IntegrationInterface { /** * Indicate if the current integration is allowed to load. * * @since 1.9.9 * * @return bool */ public function allow_load(): bool { return $this->is_motopress_active() && $this->is_motopress_editor(); } /** * Load an integration. * * @since 1.9.9 */ public function load() { $this->hooks(); } /** * Hooks. * * @since 1.9.9 */ private function hooks(): void { // Disable Anti Spam v3 honeypot. add_filter( 'wpforms_forms_anti_spam_v3_is_honeypot_enabled', '__return_false' ); } /** * Determine if a current page is opened in the MotorPress editor. * * @since 1.9.9 * * @return bool */ private function is_motopress_editor(): bool { return ! empty( $_GET['motopress-ce'] ) || ! empty( $_GET['mpce-edit'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended } /** * Determine if the MotoPress plugin is active. * * @since 1.9.9 * * @return bool */ private function is_motopress_active(): bool { return function_exists( 'mpceSettings' ); } } Integrations/WPorg/Translations.php 0000644 00000007205 15174710275 0013457 0 ustar 00 <?php namespace WPForms\Integrations\WPorg; use Language_Pack_Upgrader; use Automatic_Upgrader_Skin; use WPForms\Integrations\IntegrationInterface; /** * Load translations from WordPress.org for the Lite version. * * @since 1.6.9 */ class Translations implements IntegrationInterface { /** * Full wp.org API URL for the plugin. * * @since 1.6.9 */ const API_URL = 'https://api.wordpress.org/plugins/update-check/1.1/'; /** * Indicate if current integration is allowed to load. * * @since 1.6.9 * * @return bool */ public function allow_load() { if ( ! is_admin() ) { return false; } // For WordPress versions 4.9.0-4.9.4 this file must be included before the current_user_can() check. require_once ABSPATH . 'wp-admin/includes/template.php'; if ( ! current_user_can( 'install_languages' ) ) { return false; } require_once ABSPATH . 'wp-admin/includes/file.php'; require_once ABSPATH . 'wp-admin/includes/translation-install.php'; return wp_can_install_language_pack(); } /** * Load an integration. * * @since 1.6.9 */ public function load() { // Download translations for all addons when language for the site has been changed. add_action( 'update_option_WPLANG', [ $this, 'download_translations' ] ); } /** * Get translation packages from the wp.org API. * * @since 1.6.9 * * @return array */ private function get_translation_packages() { $plugin_data = get_plugin_data( WPFORMS_PLUGIN_FILE ); $plugin_data['Name'] = 'WPForms Lite'; $plugin_data['Version'] = '9999.0'; $request = wp_remote_post( self::API_URL, [ 'body' => [ 'plugins' => wp_json_encode( [ 'plugins' => [ 'wpforms-lite/wpforms.php' => $plugin_data, ], 'active' => [], ] ), 'locale' => wp_json_encode( get_available_languages() ), ], ] ); $code = wp_remote_retrieve_response_code( $request ); $body = wp_remote_retrieve_body( $request ); if ( $code !== 200 || $body === 'error' || is_wp_error( $body ) ) { return []; } $body = json_decode( $body, true ); return ! empty( $body['translations'] ) ? $body['translations'] : []; } /** * Download translations for all available languages. * * @since 1.6.9 */ public function download_translations() { $translations = $this->get_translation_packages(); if ( empty( $translations ) ) { return; } $skin = new Automatic_Upgrader_Skin(); $upgrader = new Language_Pack_Upgrader( $skin ); foreach ( $translations as $language ) { // Sometimes a language can be passed as array. $this->download_package( (object) $language, $upgrader, $skin ); } } /** * Download translation for the language. * * @since 1.6.9 * * @param object $language Language package. * @param Language_Pack_Upgrader $upgrader The instance of the core class used for updating/installing language packs (translations). * @param Automatic_Upgrader_Skin $skin Upgrader Skin for Automatic WordPress Upgrades. */ private function download_package( $language, Language_Pack_Upgrader $upgrader, Automatic_Upgrader_Skin $skin ) { if ( ! property_exists( $language, 'package' ) || empty( $language->package ) ) { return; } $skin->language_update = $language; $upgrader->run( [ 'package' => $language->package, 'destination' => WP_LANG_DIR . '/plugins', 'abort_if_destination_exists' => false, 'is_multi' => true, 'hook_extra' => [ 'language_update_type' => $language->type, 'language_update' => $language, ], ] ); } } Integrations/LiteConnect/RefreshAccessTokenTask.php 0000644 00000002415 15174710275 0016511 0 ustar 00 <?php namespace WPForms\Integrations\LiteConnect; /** * Class RefreshAccessTokenTask. * * @since 1.7.4 */ class RefreshAccessTokenTask extends Integration { /** * Task name. * * @since 1.7.4 * * @var string */ const LITE_CONNECT_TASK = 'wpforms_lite_connect_refresh_access_token'; /** * RefreshAccessTokenTask constructor. * * @since 1.7.4 */ public function __construct() { parent::__construct(); $this->hooks(); } /** * Initialize the hooks. * * @since 1.7.4 */ private function hooks() { // Process the tasks as needed. add_action( self::LITE_CONNECT_TASK, [ $this, 'process' ] ); } /** * Creates a task to refresh the Lite Connect access token via Action Scheduler. * * @since 1.7.4 */ public function create() { $action_id = wpforms()->obj( 'tasks' ) ->create( self::LITE_CONNECT_TASK ) ->once( time() + 6 * DAY_IN_SECONDS ) ->register(); if ( $action_id === null ) { wpforms_log( 'Lite Connect: error creating the AS task', [ 'task' => self::LITE_CONNECT_TASK, ], [ 'type' => [ 'error' ] ] ); } } /** * Process the task to regenerate the access token. * * @since 1.7.4 */ public function process() { $this->get_access_token( $this->get_site_key(), true ); } } Integrations/LiteConnect/API.php 0000644 00000024442 15174710275 0012562 0 ustar 00 <?php namespace WPForms\Integrations\LiteConnect; use WPForms\Helpers\Transient; /** * Class API. * * @since 1.7.4 */ class API { /** * Option name. * * @since 1.7.4 * * @var string */ const LITE_CONNECT_OPTION = 'wpforms_lite_connect'; /** * Staging option name. * * @since 1.7.4 * * @var string */ const STAGING_LITE_CONNECT_OPTION = 'wpforms_lite_connect_staging'; /** * Lite Connect API URL. * * @since 1.7.4 * * @var string */ const API_URL = 'https://wpformsliteconnect.com'; /** * Lite Connect staging API URL. * * @since 1.7.4 * * @var string */ const STAGING_API_URL = 'https://staging.wpformsliteconnect.com'; /** * Lite Connect generate_site_key() lock transient name. * * @since 1.7.4 * * @var string */ const LITE_CONNECT_SITE_KEY_LOCK = 'lite_connect_site_key_lock'; /** * Lite Connect generate_access_token() lock transient name. * * @since 1.7.4 */ const LITE_CONNECT_ACCESS_TOKEN_LOCK = 'lite_connect_access_token_lock'; /** * Lite Connect create_not_logged_in_nonce() action. * * @since 1.7.4 * * @var string */ const KEY_NONCE_ACTION = 'lite_connect_key_action'; /** * Max number of attempts for generate_site_key(). * * @since 1.7.5 * * @var integer */ const MAX_GENERATE_KEY_ATTEMPTS = 20; /** * Generate key attempt counter. * * @since 1.7.5 * * @var string */ const GENERATE_KEY_ATTEMPT_COUNTER_OPTION = 'wpforms_lite_connect_generate_key_attempt_counter'; /** * Lite Connect API URL. * * @since 1.7.4 * * @var string */ protected $api_url; /** * The site domain. * * @since 1.7.4 * * @var string */ protected $domain; /** * The site ID. * * @since 1.7.4 * * @var string */ protected $site_id; /** * API constructor. * * @since 1.7.4 */ public function __construct() { // Get the domain name. // Strip protocol `http(s)://` and `www.` from the site URL. $this->domain = preg_replace( '/(https?:\/\/)?(www\.)?(.*)\/?/', '$3', home_url() ); $this->api_url = self::API_URL; if ( defined( 'WPFORMS_LITE_CONNECT_STAGING' ) && WPFORMS_LITE_CONNECT_STAGING ) { $this->api_url = self::STAGING_API_URL; } $this->set_site_id(); } /** * Generate the site key. * * @since 1.7.4 * * @return false */ protected function generate_site_key() { if ( $this->is_max_generate_key_attempts_reached() ) { return false; } if ( Transient::get( self::LITE_CONNECT_SITE_KEY_LOCK ) ) { return false; } Transient::set( self::LITE_CONNECT_SITE_KEY_LOCK, true, MINUTE_IN_SECONDS ); $admin_email = Integration::get_enabled_email(); $user = get_user_by( 'email', $admin_email ); $data = [ 'domain' => $this->domain, 'admin_email' => $admin_email, 'first_name' => ! empty( $user->first_name ) ? $user->first_name : '', 'last_name' => ! empty( $user->last_name ) ? $user->last_name : '', 'nonce' => $this->create_not_logged_in_nonce(), 'callback' => add_query_arg( [ LiteConnect::AUTH_KEY_ARG => '' ], trailingslashit( home_url() ) ), ]; $response = $this->request( '/auth/key', $data ); if ( $response !== false ) { Transient::delete( self::LITE_CONNECT_SITE_KEY_LOCK ); } $this->update_generate_key_attempts_count(); // At this point, we do not have the site key. // It will be sent to us in the 'wpforms/auth/key/nonce' callback. return false; } /** * Generate the access token. * * @since 1.7.4 * * @param string $site_key The site key. * * @return false|string */ protected function generate_access_token( $site_key ) { // Verify if an access token is already being generated. if ( Transient::get( self::LITE_CONNECT_ACCESS_TOKEN_LOCK ) ) { return false; } // Set a lock to avoid multiple requests to generate the access token. Transient::set( self::LITE_CONNECT_ACCESS_TOKEN_LOCK, true, MINUTE_IN_SECONDS ); $response = $this->request( '/auth/access_token', [ 'domain' => $this->domain, 'site_id' => $this->site_id, 'wp_version' => get_bloginfo( 'version' ), ], [ 'X-WPForms-Lite-Connect-Site-Key' => $site_key, ] ); if ( $response && strpos( $response, '{"error":' ) === false ) { // Delete lock. Transient::delete( self::LITE_CONNECT_ACCESS_TOKEN_LOCK ); } return $response; } /** * Add an entry to the Lite Connect API. * * @since 1.7.4 * * @param string $access_token The access token. * @param int $form_id The form ID. * @param string $entry_data The entry data. * * @return false|string */ public function add_form_entry( $access_token, $form_id, $entry_data ) { return $this->request( '/storage/entries', [ 'site_id' => $this->site_id, 'form_id' => $form_id, 'data' => $entry_data, ], [ 'X-WPForms-Lite-Connect-Access-Token' => $access_token, ] ); } /** * Send a request to the Lite Connect API. * * @since 1.7.4 * * @param string $uri The request's URI. * @param array $body The request's body. * @param array $headers The HTTP headers. * * @return false|string */ protected function request( $uri, $body, $headers = [] ) { $url = $this->api_url . $uri; $user_agent = 'WPForms/' . WPFORMS_VERSION . '; ' . home_url(); /** * Allow to filter Lite Connect request timeout. * * @since 1.8.8 * * @param int $timeout Timeout value in seconds. */ $timeout = (int) apply_filters( 'wpforms_integrations_lite_connect_api_request_timeout', 60 ); $response = wp_remote_post( $url, [ 'method' => 'POST', 'timeout' => $timeout, 'headers' => $headers, 'body' => $body, 'user-agent' => $user_agent, ] ); if ( is_wp_error( $response ) || ( isset( $response['response']['code'] ) && (int) $response['response']['code'] !== 200 ) ) { if ( ! is_wp_error( $response ) ) { unset( $response['headers'], $response['http_response'], $response['cookies'], $response['filename'] ); } $args = [ 'type' => [ 'error' ], ]; if ( isset( $body['form_id'] ) ) { $args['form_id'] = $body['form_id']; } wpforms_log( 'Lite Connect: remote API request error', [ 'response' => $response, 'request' => [ 'url' => $url, 'body' => $this->prepare_log_data( $body ), 'headers' => $this->prepare_log_data( $headers ), 'user-agent' => $user_agent, ], ], $args ); } if ( is_wp_error( $response ) ) { return false; } return wp_remote_retrieve_body( $response ); } /** * Prepare data for logging. * * @since 1.7.4 * * @param mixed $data Data to log. * * @return mixed */ private function prepare_log_data( $data ) { $asterisks = '***'; if ( ! empty( $data['X-WPForms-Lite-Connect-Access-Token'] ) ) { $data['X-WPForms-Lite-Connect-Access-Token'] = $asterisks; } if ( ! empty( $data['X-WPForms-Lite-Connect-Site-Key'] ) ) { $data['X-WPForms-Lite-Connect-Site-Key'] = $asterisks; } if ( ! empty( $data['nonce'] ) ) { $data['nonce'] = $asterisks; } return $data; } /** * Get debug setting. * * @since 1.7.4 * * @param string $name Setting name. * * @return false|mixed */ protected function get_debug_setting( $name ) { // To be defined in wp-config.php. if ( ! defined( 'WPFORMS_DEBUG_LITE_CONNECT' ) || ! is_array( WPFORMS_DEBUG_LITE_CONNECT ) ) { return false; } return ! empty( WPFORMS_DEBUG_LITE_CONNECT[ $name ] ) ? WPFORMS_DEBUG_LITE_CONNECT[ $name ] : false; } /** * Create not logged in nonce. * We need it, because callback from the server to the wpforms/auth/key/nonce will be processed as not logged in. * * @since 1.7.4 * * @return string */ private function create_not_logged_in_nonce() { $user = wp_get_current_user(); $user_id = $user ? $user->ID : 0; wp_set_current_user( 0 ); $saved_cookie = $_COOKIE; $_COOKIE = []; $nonce = wp_create_nonce( self::KEY_NONCE_ACTION ); $_COOKIE = $saved_cookie; wp_set_current_user( $user_id ); return $nonce; } /** * Set site ID. * * @since 1.7.4 * * @return void */ private function set_site_id() { // At first, try to use the site ID from the wp-config.php file. $debug_site_id = $this->get_debug_setting( 'id' ); if ( $debug_site_id !== false ) { $this->site_id = $debug_site_id; return; } // Otherwise, use the site ID generated and saved as setting. $site = wpforms_setting( 'site', false, Integration::get_option_name() ); if ( ! isset( $site['id'] ) ) { return; } $this->site_id = $site['id']; } /** * Check that we have not reached the max number of attempts to get keys from API using generate_keys(). * * @since 1.7.5 * * @return bool */ private function is_max_generate_key_attempts_reached() { $attempts_count = get_option( self::GENERATE_KEY_ATTEMPT_COUNTER_OPTION, 0 ); return $attempts_count >= self::MAX_GENERATE_KEY_ATTEMPTS; } /** * Update count of the attempts to get keys from API using generate_keys(). * It allows us to prevent sending requests to the API server infinitely. * * @since 1.7.5 */ private function update_generate_key_attempts_count() { global $wpdb; $counter = get_option( self::GENERATE_KEY_ATTEMPT_COUNTER_OPTION, 0 ); if ( $counter >= self::MAX_GENERATE_KEY_ATTEMPTS - 1 ) { // Disable Lite Connect. $wpforms_settings = get_option( 'wpforms_settings', [] ); $wpforms_settings[ LiteConnect::SETTINGS_SLUG ] = 0; update_option( 'wpforms_settings', $wpforms_settings ); } // Store actual attempt counter value to the option. // We need here an atomic operation to avoid race conditions with getting site key via callback. // phpcs:disable WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $wpdb->query( $wpdb->prepare( "INSERT INTO $wpdb->options (option_name, option_value, autoload) VALUES ( %s, 1, 'no' ) ON DUPLICATE KEY UPDATE option_value = option_value + 1", self::GENERATE_KEY_ATTEMPT_COUNTER_OPTION ) ); // phpcs:enable WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize wp_cache_delete( self::GENERATE_KEY_ATTEMPT_COUNTER_OPTION, 'options' ); } } Integrations/LiteConnect/LiteConnect.php 0000644 00000013151 15174710275 0014353 0 ustar 00 <?php namespace WPForms\Integrations\LiteConnect; use WPForms\Integrations\IntegrationInterface; /** * Class LiteConnect. * * @since 1.7.4 */ abstract class LiteConnect implements IntegrationInterface { /** * The slug that will be used to save the option of Lite Connect. * * @since 1.7.4 * * @var string */ const SETTINGS_SLUG = 'lite-connect-enabled'; /** * The $_GET argument to trigger the auth key endpoint. * * @since 1.7.4.1 * * @var string */ const AUTH_KEY_ARG = 'wpforms-liteconnect-auth-key'; /** * Indicate if current integration is allowed to load. * * @since 1.7.4 * * @return bool */ public function allow_load() { return self::is_allowed(); } /** * Whether Lite Connect is allowed. * * @since 1.7.4 * * @return bool */ public static function is_allowed() { // Disable Lite Connect integration for local hosts. $allowed = ! self::is_local_not_debug() && self::is_production(); // phpcs:disable WPForms.PHP.ValidateHooks.InvalidHookName /** * Determine whether Lite Connect integration is allowed to load. * * @since 1.7.4 * * @param bool $is_allowed Is LiteConnect allowed? Value by default: true. */ return (bool) apply_filters( 'wpforms_integrations_lite_connect_is_allowed', $allowed ); // phpcs:enable WPForms.PHP.ValidateHooks.InvalidHookName } /** * Whether Lite Connect is enabled. * * @since 1.7.4 * * @return bool */ public static function is_enabled() { // phpcs:disable WPForms.PHP.ValidateHooks.InvalidHookName /** * Determine whether LiteConnect is enabled on the WPForms > Settings admin page. * * @since 1.7.4 * * @param bool $is_enabled Is LiteConnect enabled on WPForms > Settings page? */ return (bool) apply_filters( 'wpforms_integrations_lite_connect_is_enabled', wpforms_setting( self::SETTINGS_SLUG ) ); // phpcs:enable WPForms.PHP.ValidateHooks.InvalidHookName } /** * Load an integration. * * @since 1.7.4 */ public function load() { $this->endpoints(); } /** * Whether Lite Connect is running locally and not in the debug mode. * * @since 1.7.4 * * @return bool */ private static function is_local_not_debug() { return ! defined( 'WPFORMS_DEBUG_LITE_CONNECT' ) && self::is_localhost(); } /** * Whether Lite Connect is running locally. * * @since 1.7.4 * * @return bool */ private static function is_localhost() { // Check for local TLDs. if ( ! empty( $_SERVER['HTTP_HOST'] ) ) { $host = sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ) ); $local_tlds = [ '.local', '.invalid', '.example', '.test', ]; foreach ( $local_tlds as $tld ) { if ( preg_match( '/' . $tld . '$/', $host ) ) { return true; } } } // Return false if IP and TLD are not local. return false; } /** * Whether Lite Connect is running on production website. * * @since 1.7.6 * * @return bool */ private static function is_production() { return wp_get_environment_type() === 'production'; } /** * Provide responses to endpoint requests. * * @since 1.7.4 */ private function endpoints() { // We check nonce in the endpoint_key(). // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( ! isset( $_GET[ self::AUTH_KEY_ARG ] ) ) { return; } $this->endpoint_key(); } /** * Process endpoint for callback on generate_site_key(). * * @since 1.7.4 */ private function endpoint_key() { $json = file_get_contents( 'php://input' ); $response = json_decode( $json, true ); if ( ! $response ) { $this->endpoint_die( 'Lite Connect: No response' ); } if ( isset( $response['error'] ) ) { $this->endpoint_die( 'Lite Connect: unable to add the site to system', $response ); } if ( ! isset( $response['key'], $response['id'], $response['nonce'] ) ) { $this->endpoint_die( 'Lite Connect: unknown communication error', $response ); } if ( ! wp_verify_nonce( $response['nonce'], API::KEY_NONCE_ACTION ) ) { $this->endpoint_die( 'Lite Connect: nonce verification failed', $response ); } unset( $response['nonce'] ); $settings = get_option( Integration::get_option_name(), [] ); $settings['site'] = $response; update_option( API::GENERATE_KEY_ATTEMPT_COUNTER_OPTION, 0 ); update_option( Integration::get_option_name(), $settings ); exit(); } /** * Finish the endpoint execution with wp_die(). * * @since 1.7.4 * * @param string $title Log message title. * @param array $response Response. * * @noinspection ForgottenDebugOutputInspection */ private function endpoint_die( $title = '', $response = [] ) { // phpcs:ignore WPForms.PHP.HooksMethod.InvalidPlaceForAddingHooks $this->log( $title, $response ); // We call wp_die too early, before the query is run. // So, we should remove some filters to avoid having PHP notices in error log. remove_filter( 'wp_robots', 'wp_robots_noindex_embeds' ); remove_filter( 'wp_robots', 'wp_robots_noindex_search' ); wp_die( esc_html__( 'This is the Lite Connect endpoint page.', 'wpforms-lite' ), 'Lite Connect endpoint', 400 ); } /** * Log message. * * @since 1.7.4 * * @param string $title Log message title. * @param array $response Response. */ private function log( $title = '', $response = [] ) { if ( ! $title ) { return; } wpforms_log( $title, [ 'response' => $response, 'request' => [ 'domain' => isset( $response['domain'] ) ? $response['domain'] : '', 'admin_email' => Integration::get_enabled_email(), ], ], [ 'type' => [ 'error' ] ] ); } } Integrations/LiteConnect/Integration.php 0000644 00000026244 15174710275 0014436 0 ustar 00 <?php namespace WPForms\Integrations\LiteConnect; use WPForms\Admin\Notice; use WPForms\Helpers\Transient; use WPForms\Tasks\Tasks; /** * Class Integration. * * Base integration between Lite Connect API and WPForms. * * @since 1.7.4 */ class Integration extends API { /** * Authentication data. * * @since 1.7.4 * * @var array */ protected $auth = []; /** * Option name to store the total count of Lite Connect entries. * * @since 1.7.4 * * @var string */ const LITE_CONNECT_ENTRIES_COUNT_OPTION = 'wpforms_lite_connect_entries_count'; /** * Post meta name to store the total count of Lite Connect form entries. * * @since 1.7.9 * * @var string */ const LITE_CONNECT_FORM_ENTRIES_COUNT_META = 'wpforms_lite_connect_form_entries_count'; /** * Integration constructor. * * @since 1.7.4 */ public function __construct() { static $updated; parent::__construct(); $this->hooks(); // Update the site key and access token. if ( ! $updated && ( is_admin() && ! wp_doing_ajax() ) && ( ( wpforms()->is_pro() && self::get_enabled_since() ) || LiteConnect::is_enabled() ) ) { $this->maybe_update_access_token(); $this->update_keys(); $updated = true; } } /** * Hooks. * * @since 1.7.5 */ private function hooks() { add_action( 'admin_init', [ $this, 'max_attempts_notice' ], 10 ); } /** * Update the site key and access token if they do not exist. * * @since 1.7.4 */ public function update_keys() { if ( isset( $this->auth['site_key'], $this->auth['access_token'] ) ) { return; } $site_key = $this->get_site_key(); $this->auth = [ 'site_key' => $site_key, 'access_token' => $this->get_access_token( $site_key ), ]; } /** * Get the site key. * * @since 1.7.4 * * @return string|false|array The site key, or false on error. */ protected function get_site_key() { // At first, try to get the site key from the wp-config.php file. $debug_site_key = $this->get_debug_setting( 'key' ); if ( $debug_site_key !== false ) { return $debug_site_key; } // If site key already exists, then we won't need to regenerate it. $curr_key = wpforms_setting( 'site', false, self::get_option_name() ); if ( ! empty( $curr_key['key'] ) ) { return $curr_key['key']; } // Generate the site key. return $this->generate_site_key(); } /** * Get the access token. * * @since 1.7.4 * * @param string|array $site_key The site key. * @param bool $force True to force generate a new access token. * * @return string|false|void The access token, or false on error. */ protected function get_access_token( $site_key, $force = false ) { if ( ! $site_key ) { return false; } $curr_token = wpforms_setting( 'access_token', false, self::get_option_name() ); // It won't regenerate the access token if $force is false, and the current token is not expired. if ( $force === false && isset( $curr_token['expires_at'] ) && (int) $curr_token['expires_at'] - time() > 0 ) { return $curr_token['access_token']; } // Generate the access token. $response = $this->generate_access_token( $site_key ); if ( $response ) { $response = json_decode( $response, true ); if ( isset( $response['access_token'] ) ) { $settings = get_option( self::get_option_name(), [] ); $settings['access_token'] = $response; update_option( self::get_option_name(), $settings ); // Create task to refresh access token in 6 days. $this->refresh_access_token_task(); return $response['access_token']; } wpforms_log( 'Lite Connect: unable to generate access token', [ 'response' => $response, 'request' => [ 'domain' => $this->domain, 'site_id' => $this->site_id, 'wp_version' => get_bloginfo( 'version' ), ], ], [ 'type' => [ 'error' ] ] ); } return false; } /** * Create a task to refresh the access token. * * @since 1.7.4 */ private function refresh_access_token_task() { $tasks = wpforms()->obj( 'tasks' ); if ( $tasks instanceof Tasks && ! $tasks->is_scheduled( RefreshAccessTokenTask::LITE_CONNECT_TASK ) ) { ( new RefreshAccessTokenTask() )->create(); } } /** * Get the name for the Lite Connect's option. * * @since 1.7.4 * * @return string */ public static function get_option_name() { if ( self::is_staging() ) { return API::STAGING_LITE_CONNECT_OPTION; } return API::LITE_CONNECT_OPTION; } /** * Get the Lite Connect entries count. * * @since 1.7.4 * * @return int The entries count. */ public static function get_entries_count() { return (int) get_option( self::LITE_CONNECT_ENTRIES_COUNT_OPTION, 0 ); } /** * Get the Lite Connect form entries count. * * @since 1.7.9 * * @param int $form_id The form ID. * * @return int The form entries count. */ public static function get_form_entries_count( $form_id ) { return (int) get_post_meta( $form_id, self::LITE_CONNECT_FORM_ENTRIES_COUNT_META, true ); } /** * Get the Lite Connect new entries count (since previous import). * * @since 1.7.4 * * @return int The new entries count. */ public static function get_new_entries_count() { // Get current total entries count. $count = self::get_entries_count(); // Reduces the entries that were already imported previously from the count. $import = wpforms_setting( 'import', false, self::get_option_name() ); $prev_count = 0; if ( isset( $import['previous_import_count'] ) ) { $prev_count = (int) $import['previous_import_count']; } if ( isset( $import['previous_failed_count'] ) ) { $prev_count += (int) $import['previous_failed_count']; } return $count < $prev_count ? 0 : $count - $prev_count; } /** * Maybe restart the import flag (for when the user re-upgrades to pro). * * @since 1.7.4 */ public static function maybe_restart_import_flag() { $settings = get_option( self::get_option_name(), [] ); if ( empty( $settings ) ) { return; } $status = isset( $settings['import']['status'] ) ? $settings['import']['status'] : false; if ( $status === 'done' ) { $previous_imported_entries = Transient::get( 'lite_connect_imported_entries' ); $settings['import']['previous_import_count'] = is_array( $previous_imported_entries ) ? count( $previous_imported_entries ) : 0; $previous_failed_entries = Transient::get( 'lite_connect_failed_entries' ); $settings['import']['previous_failed_count'] = is_array( $previous_failed_entries ) ? count( $previous_failed_entries ) : 0; } self::maybe_set_entries_count(); // Reset import status to be able to restart import process. unset( $settings['import']['status'], $settings['import']['user_notified'] ); update_option( self::get_option_name(), $settings ); if ( Transient::get( 'lite_connect_error' ) !== false ) { Transient::delete( 'lite_connect_error' ); } } /** * Get the Lite Connect enabled since timestamp. * * @since 1.7.4 * * @return bool|int */ public static function get_enabled_since() { return wpforms_setting( LiteConnect::SETTINGS_SLUG . '-since' ); } /** * Get the Email of the user who enabled Lite Connect. * * @since 1.7.4 * * @return bool|string */ public static function get_enabled_email() { return wpforms_setting( LiteConnect::SETTINGS_SLUG . '-email' ); } /** * Normalize Lite Connect entries counter when their value is wrong. * * @since 1.7.4 */ public static function maybe_set_entries_count() { $settings = get_option( self::get_option_name(), [] ); if ( empty( $settings ) ) { return; } $previous_import_count = isset( $settings['import']['previous_import_count'] ) ? (int) $settings['import']['previous_import_count'] : 0; $previous_failed_count = isset( $settings['import']['previous_failed_count'] ) ? (int) $settings['import']['previous_failed_count'] : 0; $previous_import_count += $previous_failed_count; // When the entries counter was manually deleted from options OR it was modified by another process, // we are setting the counter to the value of the previous imported entries. // In this way, the next form submission will increase counter properly, and user will see value of the backed up entries. // Obviously, this solution is not perfect, but we don't have another source of the total entries count. if ( $previous_import_count > self::get_entries_count() ) { update_option( self::LITE_CONNECT_ENTRIES_COUNT_OPTION, $previous_import_count ); } } /** * Show the Lite Connect notice about the max attempts to generate the API key. * * @since 1.7.5 */ public function max_attempts_notice() { $attempts_count = get_option( self::GENERATE_KEY_ATTEMPT_COUNTER_OPTION, 0 ); $notice_text = sprintf( wp_kses( /* translators: %s - WPForms documentation link. */ __( 'Your form entries can’t be backed up because WPForms can’t connect to the backup server. If you’d like to back up your entries, find out how to <a href="%s" target="_blank" rel="noopener noreferrer">fix entry backup issues</a>.', 'wpforms-lite' ), [ 'a' => [ 'href' => [], 'target' => [], 'rel' => [], ], ] ), wpforms_utm_link( 'https://wpforms.com/docs/how-to-use-lite-connect-for-wpforms/#backup-issues', 'Admin Notice' ) ); if ( $attempts_count >= self::MAX_GENERATE_KEY_ATTEMPTS ) { Notice::warning( $notice_text, [ 'dismiss' => Notice::DISMISS_GLOBAL, 'slug' => 'max_attempts', ] ); } } /** * Maybe update access token. * * @since 1.7.6 */ public function maybe_update_access_token() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $action = isset( $_GET['wpforms_lite_connect_action'] ) ? sanitize_key( $_GET['wpforms_lite_connect_action'] ) : ''; if ( ! isset( $_GET['_wpnonce'] ) || $action !== 'update-access-token' || ! current_user_can( 'manage_options' ) || ! wp_verify_nonce( sanitize_key( $_GET['_wpnonce'] ), 'wpforms_lite_connect_action' ) ) { return; } $this->get_access_token( $this->get_site_key(), true ); } /** * Determine if Lite Connect staging is used. * * @since 1.9.1 * * @return bool */ private static function is_staging(): bool { return defined( 'WPFORMS_LITE_CONNECT_STAGING' ) && WPFORMS_LITE_CONNECT_STAGING; } /** * Get the site credentials. * * @since 1.9.1 * * @return array */ public static function get_site_credentials(): array { $settings = (array) get_option( self::get_option_name(), [] ); if ( ! empty( $settings['site']['id'] ) && ! empty( $settings['access_token']['access_token'] ) ) { return [ 'site_id' => $settings['site']['id'], 'access_token' => $settings['access_token']['access_token'], ]; } $instance = ( new self() ); // Try to get the site id from the wp-config.php file. $debug_site_id = $instance->get_debug_setting( 'id' ); if ( empty( $debug_site_id ) ) { return []; } $access_token = $instance->get_access_token( $instance->get_site_key() ); if ( ! $access_token ) { return []; } return [ 'site_id' => $debug_site_id, 'access_token' => $access_token, 'is_production' => ! self::is_staging(), ]; } } Integrations/UsageTracking/SendUsageTask.php 0000644 00000004605 15174710275 0015171 0 ustar 00 <?php namespace WPForms\Integrations\UsageTracking; use WPForms\Tasks\Task; /** * Class SendUsageTask. * * @since 1.6.1 */ class SendUsageTask extends Task { /** * Action name for this task. * * @since 1.6.1 */ const ACTION = 'wpforms_send_usage_data'; /** * Server URL to send requests to. * * @since 1.6.1 */ const TRACK_URL = 'https://wpformsusage.com/v1/track'; /** * Option name to store the timestamp of the last run. * * @since 1.6.3 */ const LAST_RUN = 'wpforms_send_usage_last_run'; /** * Class constructor. * * @since 1.6.1 */ public function __construct() { parent::__construct( self::ACTION ); $this->init(); } /** * Initialize the task with all the proper checks. * * @since 1.6.1 */ public function init() { // Register the action handler. $this->hooks(); $tasks = wpforms()->obj( 'tasks' ); // Add new if none exists. if ( $tasks->is_scheduled( self::ACTION ) !== false ) { return; } $this->recurring( $this->generate_start_date(), WEEK_IN_SECONDS )->register(); } /** * Add hooks. * * @since 1.7.3 */ private function hooks() { add_action( self::ACTION, [ $this, 'process' ] ); } /** * Randomly pick a timestamp * which is not more than 1 week in the future * starting from next sunday. * * @since 1.6.1 * * @return int */ private function generate_start_date() { $tracking = []; $tracking['days'] = wp_rand( 0, 6 ) * DAY_IN_SECONDS; $tracking['hours'] = wp_rand( 0, 23 ) * HOUR_IN_SECONDS; $tracking['minutes'] = wp_rand( 0, 59 ) * MINUTE_IN_SECONDS; $tracking['seconds'] = wp_rand( 0, 59 ); return strtotime( 'next sunday' ) + array_sum( $tracking ); } /** * Send the actual data in a POST request. * * @since 1.6.1 */ public function process() { $last_run = get_option( self::LAST_RUN ); // Make sure we do not run it more than once a day. if ( $last_run !== false && ( time() - $last_run ) < DAY_IN_SECONDS ) { return; } // Send data to the usage tracking API. $ut = new UsageTracking(); wp_remote_post( self::TRACK_URL, [ 'timeout' => 5, 'redirection' => 5, 'httpversion' => '1.1', 'blocking' => true, 'body' => $ut->get_data(), 'user-agent' => $ut->get_user_agent(), ] ); // Update the last run option to the current timestamp. update_option( self::LAST_RUN, time() ); } } Integrations/UsageTracking/UsageTracking.php 0000644 00000066754 15174710275 0015234 0 ustar 00 <?php namespace WPForms\Integrations\UsageTracking; use WPForms\Admin\Builder\Templates; use WPForms\Integrations\AI\Helpers as AIHelpers; use WPForms\Integrations\IntegrationInterface; use WPForms\Integrations\LiteConnect\Integration; /** * Usage Tracker functionality to understand what's going on client's sites. * * @since 1.6.1 */ class UsageTracking implements IntegrationInterface { /** * The slug that will be used to save the option of Usage Tracker. * * @since 1.6.1 */ const SETTINGS_SLUG = 'usage-tracking-enabled'; /** * Indicate if current integration is allowed to load. * * @since 1.6.1 * * @return bool */ public function allow_load(): bool { /** * Whether the Usage Tracking code is allowed to be loaded. * * @since 1.6.1 * * @param bool $var Boolean value. */ return (bool) apply_filters( 'wpforms_usagetracking_is_allowed', true ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName } /** * Whether Usage Tracking is enabled. * * @since 1.6.1 * * @return bool */ public function is_enabled(): bool { /** * Whether the Usage Tracking is enabled. * * @since 1.6.1 * * @param bool $var Boolean value taken from the DB. */ return (bool) apply_filters( 'wpforms_integrations_usagetracking_is_enabled', wpforms_setting( self::SETTINGS_SLUG ) ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName } /** * Load an integration. * * @since 1.6.1 */ public function load() { // phpcs:ignore WPForms.PHP.HooksMethod.InvalidPlaceForAddingHooks add_filter( 'wpforms_settings_defaults', [ $this, 'settings_misc_option' ], 4 ); // Deregister the action if option is disabled. add_action( 'wpforms_settings_updated', function () { if ( ! $this->is_enabled() ) { ( new SendUsageTask() )->cancel(); } } ); // Register the action handler only if enabled. if ( $this->is_enabled() ) { add_filter( 'wpforms_tasks_get_tasks', static function ( $tasks ) { $tasks[] = SendUsageTask::class; return $tasks; } ); } } /** * Add "Allow Usage Tracking" to WPForms settings. * * @since 1.6.1 * * @param array $settings WPForms settings. * * @return array */ public function settings_misc_option( $settings ) { $settings['misc'][ self::SETTINGS_SLUG ] = [ 'id' => self::SETTINGS_SLUG, 'name' => esc_html__( 'Allow Usage Tracking', 'wpforms-lite' ), 'desc' => esc_html__( 'By allowing us to track usage data, we can better help you, as we will know which WordPress configurations, themes, and plugins we should test.', 'wpforms-lite' ), 'type' => 'toggle', 'status' => true, ]; return $settings; } /** * Get the User Agent string that will be sent to the API. * * @since 1.6.1 * * @return string */ public function get_user_agent(): string { return 'WPForms/' . WPFORMS_VERSION . '; ' . get_bloginfo( 'url' ); } /** * Get data for sending to the server. * * @since 1.6.1 * * @return array * @noinspection PhpUndefinedConstantInspection * @noinspection PhpUndefinedFunctionInspection */ public function get_data(): array { global $wpdb; $theme_data = wp_get_theme(); $activated_dates = get_option( 'wpforms_activated', [] ); $first_form_date = get_option( 'wpforms_forms_first_created' ); $forms = $this->get_all_forms(); $forms_total = count( $forms ); $form_templates_total = count( $this->get_all_forms( 'wpforms-template' ) ); $entries_total = $this->get_entries_total(); $form_fields_count = $this->get_form_fields_count( $forms ); $data = [ // Generic data (environment). 'url' => home_url(), 'php_version' => PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION, 'wp_version' => get_bloginfo( 'version' ), 'mysql_version' => $wpdb->db_version(), 'server_version' => isset( $_SERVER['SERVER_SOFTWARE'] ) ? sanitize_text_field( wp_unslash( $_SERVER['SERVER_SOFTWARE'] ) ) : '', 'is_ssl' => is_ssl(), 'is_multisite' => is_multisite(), 'is_network_activated' => $this->is_active_for_network(), 'is_wpcom' => defined( 'IS_WPCOM' ) && IS_WPCOM, 'is_wpcom_vip' => ( defined( 'WPCOM_IS_VIP_ENV' ) && WPCOM_IS_VIP_ENV ) || ( function_exists( 'wpcom_is_vip' ) && wpcom_is_vip() ), 'is_wp_cache' => defined( 'WP_CACHE' ) && WP_CACHE, 'is_wp_rest_api_enabled' => $this->is_rest_api_enabled(), 'is_user_logged_in' => is_user_logged_in(), 'sites_count' => $this->get_sites_total(), 'active_plugins' => $this->get_active_plugins(), 'theme_name' => $theme_data->get( 'Name' ), 'theme_version' => $theme_data->get( 'Version' ), 'locale' => get_locale(), 'timezone_offset' => wp_timezone_string(), // WPForms-specific data. 'wpforms_version' => WPFORMS_VERSION, 'wpforms_license_key' => wpforms_get_license_key(), 'wpforms_license_type' => $this->get_license_type(), 'wpforms_license_status' => $this->get_license_status(), 'wpforms_is_pro' => wpforms()->is_pro(), 'wpforms_entries_avg' => $this->get_entries_avg( $forms_total, $entries_total ), 'wpforms_entries_total' => $entries_total, 'wpforms_entries_last_7days' => $this->get_entries_total( '7days' ), 'wpforms_entries_last_30days' => $this->get_entries_total( '30days' ), 'wpforms_forms_total' => $forms_total, 'wpforms_form_fields_count' => $form_fields_count, 'wpforms_form_templates_total' => $form_templates_total, 'wpforms_form_antispam_stat' => $this->get_form_antispam_stat( $forms ), 'wpforms_challenge_stats' => get_option( 'wpforms_challenge', [] ), 'wpforms_lite_installed_date' => $this->get_installed( $activated_dates, 'lite' ), 'wpforms_pro_installed_date' => $this->get_installed( $activated_dates, 'pro' ), 'wpforms_builder_opened_date' => (int) get_option( 'wpforms_builder_opened_date', 0 ), 'wpforms_settings' => $this->get_settings( $forms ), 'wpforms_integration_active' => $this->get_forms_integrations( $forms ), 'wpforms_payments_active' => $this->get_payments_active( $forms ), 'wpforms_product_quantities' => [ 'payment-single' => $this->count_fields_with_setting( $forms, 'payment-single', 'enable_quantity' ), 'payment-select' => $this->count_fields_with_setting( $forms, 'payment-select', 'enable_quantity' ), ], 'wpforms_order_summaries' => $this->count_fields_with_setting( $forms, 'payment-total', 'summary' ), 'wpforms_multiple_confirmations' => count( $this->get_forms_with_multiple_confirmations( $forms ) ), 'wpforms_multiple_notifications' => count( $this->get_forms_with_multiple_notifications( $forms ) ), 'wpforms_ajax_form_submissions' => count( $this->get_ajax_form_submissions( $forms ) ), 'wpforms_notification_count' => wpforms()->obj( 'notifications' )->get_count(), 'wpforms_stats' => $this->get_additional_stats(), 'wpforms_ai' => AIHelpers::is_used(), 'wpforms_ai_killswitch' => AIHelpers::is_disabled(), 'wpforms_disabled_entries_count' => count( $this->get_forms_with_disabled_entries( $forms ) ), ]; $data = $this->add_promotion_plugin_data( $data ); if ( ! empty( $first_form_date ) ) { $data['wpforms_forms_first_created'] = $first_form_date; } if ( $data['is_multisite'] ) { $data['url_primary'] = network_site_url(); } return $data; } /** * Adds promotional plugin data to the provided array. * * @since 1.9.8.6 * * @param array $data An array of existing data. * * @return array Modified data array with promotional plugin information added, if applicable. */ private function add_promotion_plugin_data( array $data ): array { $plugins = [ 'wpconsent', 'sugar-calendar', 'duplicator', 'uncannyautomator', ]; foreach ( $plugins as $plugin ) { $source = (string) get_option( $plugin . '_source', '' ); $date = (int) get_option( $plugin . '_date', 0 ); if ( $date && strpos( $source, 'WPForms' ) !== false ) { $data[ 'wpforms_' . $plugin . '_date' ] = $date; } } return $data; } /** * Get the license type. * * @since 1.6.1 * @since 1.7.2 Clarified the license type. * @since 1.7.9 Return only the license type, not the status. * * @return string */ private function get_license_type(): string { return wpforms()->is_pro() ? wpforms_get_license_type() : 'lite'; } /** * Get the license status. * * @since 1.7.9 * * @return string */ private function get_license_status(): string { if ( ! wpforms()->is_pro() ) { return 'lite'; } $license_type = wpforms_get_license_type(); $license_key = wpforms_get_license_key(); if ( ! $license_type ) { return empty( $license_key ) ? 'no license' : 'not verified'; } if ( wpforms_setting( 'is_expired', false, 'wpforms_license' ) ) { return 'expired'; } if ( wpforms_setting( 'is_disabled', false, 'wpforms_license' ) ) { return 'disabled'; } if ( wpforms_setting( 'is_invalid', false, 'wpforms_license' ) ) { return 'invalid'; } // The correct type is returned in get_license_type(), so we "collapse" them here to a single value. if ( in_array( $license_type, [ 'basic', 'plus', 'pro', 'elite', 'ultimate', 'agency' ], true ) ) { $license_type = 'correct'; } return $license_type; } /** * Get all settings, except those with sensitive data. * * @since 1.6.1 * @since 1.9.3 Added $forms parameter. * * @param array $forms List of forms. * * @return array */ private function get_settings( array $forms ): array { // Remove keys with exact names that we don't need. $settings = array_diff_key( get_option( 'wpforms_settings', [] ), array_flip( [ 'stripe-test-secret-key', 'stripe-test-publishable-key', 'stripe-live-secret-key', 'stripe-live-publishable-key', 'stripe-webhooks-secret-test', 'stripe-webhooks-secret-live', 'stripe-webhooks-id-test', 'stripe-webhooks-id-live', 'square-webhooks-id-sandbox', 'square-webhooks-id-live', 'square-webhooks-secret-sandbox', 'square-webhooks-secret-live', 'authorize_net-test-api-login-id', 'authorize_net-test-transaction-key', 'authorize_net-live-api-login-id', 'authorize_net-live-transaction-key', 'square-location-id-sandbox', 'square-location-id-production', 'geolocation-google-places-api-key', 'geolocation-algolia-places-application-id', 'geolocation-algolia-places-search-only-api-key', 'geolocation-mapbox-search-access-token', 'recaptcha-site-key', 'recaptcha-secret-key', 'recaptcha-fail-msg', 'hcaptcha-site-key', 'hcaptcha-secret-key', 'hcaptcha-fail-msg', 'turnstile-site-key', 'turnstile-secret-key', 'turnstile-fail-msg', 'pdf-ninja-api_key', ] ) ); $data = []; // Remove keys with a vague names that we don't need. foreach ( $settings as $key => $value ) { if ( strpos( $key, 'validation-' ) !== false ) { continue; } $data[ $key ] = $value; } $lite_connect_data = get_option( Integration::get_option_name() ); // If lite connect has been restored, set lite connect data. if ( isset( $lite_connect_data['import']['status'] ) && $lite_connect_data['import']['status'] === 'done' ) { $data['lite_connect'] = [ 'restore_date' => $lite_connect_data['import']['ended_at'], 'restored_entry_count' => Integration::get_entries_count(), ]; } // Add Dropbox Delete Local Files setting usage count. $data['dropbox_delete_local_files_setting_count'] = $this->get_dropbox_delete_local_files_setting_count( $forms ); // Add favorite templates to the settings array. return array_merge( $data, $this->get_favorite_templates() ); } /** * Get the count of forms with Delete Local Files active option for Dropbox. * * @since 1.9.3 * * @param array $forms List of forms. * * @return int */ private function get_dropbox_delete_local_files_setting_count( array $forms ): int { $delete_local_files_count = 0; foreach ( $forms as $form ) { // Check if the Dropbox integration is configured in the form. if ( empty( $form->post_content['providers']['dropbox'] ) ) { continue; } // Delete Local Files option is applied for all connections if applied, // so it's enough to check the first connection only. $connection = current( $form->post_content['providers']['dropbox'] ); if ( ! $connection || ! isset( $connection['delete_local_files'] ) ) { continue; } ++$delete_local_files_count; } return $delete_local_files_count; } /** * Get the list of active plugins. * * @since 1.6.1 * * @return array */ private function get_active_plugins(): array { if ( ! function_exists( 'get_plugins' ) ) { include ABSPATH . '/wp-admin/includes/plugin.php'; } $active = is_multisite() ? array_merge( get_option( 'active_plugins', [] ), array_flip( get_site_option( 'active_sitewide_plugins', [] ) ) ) : get_option( 'active_plugins', [] ); $plugins = array_intersect_key( get_plugins(), array_flip( $active ) ); return array_map( static function ( $plugin ) { if ( isset( $plugin['Version'] ) ) { return $plugin['Version']; } return 'Not Set'; }, $plugins ); } /** * Installed date. * * @since 1.6.1 * * @param array $activated_dates Input array with dates. * @param string $key Input key what you want to get. * * @return mixed */ private function get_installed( array $activated_dates, string $key ) { if ( ! empty( $activated_dates[ $key ] ) ) { return $activated_dates[ $key ]; } return false; } /** * Number of forms with some integrations active. * * @since 1.6.1 * * @param array $forms List of forms. * * @return array List of forms with active integrations count. */ private function get_forms_integrations( array $forms ): array { $integrations = array_map( static function ( $form ) { if ( empty( $form->post_content['providers'] ) ) { return false; } $active_integrations = []; foreach ( $form->post_content['providers'] as $provider_slug => $connections ) { if ( ! empty( $connections ) ) { $active_integrations[] = $provider_slug; } } return $active_integrations; }, $forms ); $integrations = array_filter( $integrations ); if ( count( $integrations ) > 0 ) { $integrations = call_user_func_array( 'array_merge', array_values( $integrations ) ); } return array_count_values( $integrations ); } /** * Number of forms with active payments. * * @since 1.6.1 * * @param array $forms Input forms list. * * @return array List of forms with active payments count. */ private function get_payments_active( array $forms ): array { $payments = array_map( static function ( $form ) { if ( empty( $form->post_content['payments'] ) ) { return false; } $enabled = []; foreach ( $form->post_content['payments'] as $key => $value ) { if ( ! empty( $value['enable'] ) ) { $enabled[] = $key; } } return empty( $enabled ) ? false : $enabled; }, $forms ); $payments = array_filter( $payments ); if ( count( $payments ) > 0 ) { $payments = call_user_func_array( 'array_merge', array_values( $payments ) ); } return array_count_values( $payments ); } /** * Forms with multiple notifications. * * @since 1.6.1 * * @param array $forms List of forms to check. * * @return array List of forms with multiple notifications. */ private function get_forms_with_multiple_notifications( array $forms ): array { return array_filter( $forms, static function ( $form ) { return ! empty( $form->post_content['settings']['notifications'] ) && count( $form->post_content['settings']['notifications'] ) > 1; } ); } /** * Forms with multiple confirmations. * * @since 1.6.1 * * @param array $forms List of forms to check. * * @return array List of forms with multiple confirmations. */ private function get_forms_with_multiple_confirmations( array $forms ): array { return array_filter( $forms, static function ( $form ) { return ! empty( $form->post_content['settings']['confirmations'] ) && count( $form->post_content['settings']['confirmations'] ) > 1; } ); } /** * Forms with ajax submission option enabled. * * @since 1.6.1 * * @param array $forms All forms. * * @return array */ private function get_ajax_form_submissions( array $forms ): array { return array_filter( $forms, static function ( $form ) { return ! empty( $form->post_content['settings']['ajax_submit'] ); } ); } /** * Retrieve forms with disabled entries. * * @since 1.9.8 * * @param array $forms List of forms. * * @return array. */ private function get_forms_with_disabled_entries( array $forms ): array { return array_filter( $forms, static function ( $form ) { return ! empty( $form->post_content['settings']['disable_entries'] ); } ); } /** * Total number of sites. * * @since 1.6.1 * * @return int */ private function get_sites_total(): int { return function_exists( 'get_blog_count' ) ? (int) get_blog_count() : 1; } /** * Total number of entries. * * @since 1.6.1 * * @param string $period Which period should be counted? Possible values: 7days, 30days. * Everything else will mean "all" entries. * * @return int */ private function get_entries_total( string $period = 'all' ): int { if ( ! wpforms()->is_pro() ) { return $this->get_entries_total_lite( $period ); } $args = []; // Limit results to only forms, excluding form templates. $form_ids = wp_list_pluck( $this->get_all_forms(), 'ID' ); if ( ! empty( $form_ids ) ) { $args['form_id'] = $form_ids; } switch ( $period ) { case '7days': $args = [ 'date' => [ gmdate( 'Y-m-d', strtotime( '-7 days' ) ), gmdate( 'Y-m-d' ), ], ]; break; case '30days': $args = [ 'date' => [ gmdate( 'Y-m-d', strtotime( '-30 days' ) ), gmdate( 'Y-m-d' ), ], ]; break; } $entry_obj = wpforms()->obj( 'entry' ); return $entry_obj ? $entry_obj->get_entries( $args, true ) : 0; } /** * Total number of entries in Lite. * * @since 1.9.0 * * @param string $period Which period should be counted? Possible values: 7days, 30days. * Everything else will mean "all" entries. * * @return int */ private function get_entries_total_lite( string $period = 'all' ): int { if ( $period === '7days' || $period === '30days' ) { return 0; } global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $count = $wpdb->get_var( "SELECT SUM(meta_value) FROM $wpdb->postmeta WHERE meta_key = 'wpforms_entries_count';" ); return (int) $count; } /** * Forms field occurrences. * * @since 1.7.9 * * @param array $forms List of forms. * * @return array List of field occurrences in all forms created. */ private function get_form_fields_count( array $forms ): array { // Bail early, in case there are no forms created yet! if ( empty( $forms ) ) { return []; } $fields = array_map( static function ( $form ) { return $form->post_content['fields'] ?? []; }, $forms ); $fields_flatten = array_merge( [], ...$fields ); $field_types = array_column( $fields_flatten, 'type' ); return array_count_values( $field_types ); } /** * Determines whether the plugin is active for the entire network. * * This is a copy of the WP core is_plugin_active_for_network() function. * * @since 1.8.2 * * @return bool */ private function is_active_for_network(): bool { // Bail early, in case we are not in multisite. if ( ! is_multisite() ) { return false; } // Get all active plugins. $plugins = get_site_option( 'active_sitewide_plugins' ); // Bail early, in case the plugin is active for the entire network. if ( isset( $plugins[ plugin_basename( WPFORMS_PLUGIN_FILE ) ] ) ) { return true; } return false; } /** * Average entries count. * * @since 1.6.1 * * @param int $forms Total forms count. * @param int $entries Total entries count. * * @return int */ private function get_entries_avg( int $forms, int $entries ): int { return $forms ? round( $entries / $forms ) : 0; } /** * Get all forms. * * @since 1.6.1 * @since 1.8.9 Added post_type parameter. * * @param string|string[] $post_type Allow to sort result by post_type. By default, it's 'wpforms'. * * @return array */ private function get_all_forms( $post_type = 'wpforms' ): array { $forms = wpforms()->obj( 'form' )->get( '', [ 'post_type' => $post_type ] ); if ( ! is_array( $forms ) ) { return []; } return array_map( static function ( $form ) { $form->post_content = wpforms_decode( $form->post_content ); return $form; }, $forms ); } /** * Get the favorite templates. * * @since 1.7.7 * * @return array */ private function get_favorite_templates(): array { $settings = []; $templates = (array) get_option( Templates::FAVORITE_TEMPLATES_OPTION, [] ); foreach ( $templates as $user_templates ) { foreach ( $user_templates as $template => $v ) { $name = 'fav_templates_' . str_replace( '-', '_', $template ); $settings[ $name ] = empty( $settings[ $name ] ) ? 1 : ++$settings[ $name ]; } } return $settings; } /** * Test if the REST API is accessible. * * The REST API might be inaccessible due to various security measures, * or it might be completely disabled by a plugin. * * @since 1.8.2.2 * * @return bool */ private function is_rest_api_enabled(): bool { // phpcs:disable WPForms.PHP.ValidateHooks.InvalidHookName /** This filter is documented in wp-includes/class-wp-http-streams.php */ $sslverify = apply_filters( 'https_local_ssl_verify', false ); // phpcs:enable WPForms.PHP.ValidateHooks.InvalidHookName $url = rest_url( 'wp/v2/types/post' ); $response = wp_remote_get( $url, [ 'timeout' => 10, 'cookies' => is_user_logged_in() ? wp_unslash( $_COOKIE ) : [], 'sslverify' => $sslverify, 'headers' => [ 'Cache-Control' => 'no-cache', 'X-WP-Nonce' => wp_create_nonce( 'wp_rest' ), ], ] ); // When testing the REST API, an error was encountered, leave early. if ( is_wp_error( $response ) ) { return false; } // When testing the REST API, an unexpected result was returned, leave early. if ( wp_remote_retrieve_response_code( $response ) !== 200 ) { return false; } // The REST API did not behave correctly, leave early. if ( ! wpforms_is_json( wp_remote_retrieve_body( $response ) ) ) { return false; } // We are all set. Confirm the connection. return true; } /** * Retrieves additional statistics. * * @since 1.8.8 * * @return array */ private function get_additional_stats(): array { // Initialize an empty array to store the statistics. $stats = []; return $this->get_admin_pointer_stats( $stats ); } /** * Retrieves statistics for admin pointers. * This function retrieves statistics for admin pointers based on their engagement or dismissal status. * * Note: Pointers can only be engaged (interacted with) or dismissed. * * - If the value is 1 or true, it means the pointer is shown and interacted with (engaged). * - If the value is 0 or false, it means the pointer is dismissed. * - If there is no pointer ID in the stats, it means the user hasn't seen the pointer yet. * * @since 1.8.8 * * @param array $stats An array containing existing statistics. * * @return array */ private function get_admin_pointer_stats( array $stats ): array { $pointers = get_option( 'wpforms_pointers', [] ); // If there are no pointers, return empty statistics. if ( empty( $pointers ) ) { return $stats; } // Pointers can only be interacted with or dismissed. // If there are engagement pointers, process them. if ( isset( $pointers['engagement'] ) ) { foreach ( $pointers['engagement'] as $pointer ) { $stats[ sanitize_key( $pointer ) ] = true; } } // If there are dismiss pointers, process them. if ( isset( $pointers['dismiss'] ) ) { foreach ( $pointers['dismiss'] as $pointer ) { $stats[ sanitize_key( $pointer ) ] = false; } } return $stats; } /** * Retrieves form anti-spam settings statistic. * * @since 1.9.0 * * @param array $forms List of forms and their settings. * * @return array */ private function get_form_antispam_stat( array $forms ): array { // phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh $stat = [ 'antispam' => 0, 'antispam_v3' => 0, 'akismet' => 0, 'store_spam_entries' => 0, 'time_limit' => 0, 'country_filter' => 0, 'keyword_filter' => 0, 'captcha' => 0, ]; foreach ( $forms as $form ) { $settings = $form->post_content['settings'] ?? []; // Skip forms with disabled anti-spam settings. if ( empty( $settings['antispam'] ) && empty( $settings['antispam_v3'] ) ) { continue; } // Increment the counters for each form with enabled anti-spam settings. $stat['antispam'] += ! empty( $settings['antispam'] ) ? 1 : 0; // Classic anti-spam enabled. $stat['antispam_v3'] += ! empty( $settings['antispam_v3'] ) ? 1 : 0; // Modern anti-spam enabled. $anti_spam = $settings['anti_spam'] ?? []; // Increment the counter for each enabled anti-spam feature. $stat['akismet'] += ! empty( $anti_spam['akismet'] ) ? 1 : 0; $stat['store_spam_entries'] += ! empty( $settings['store_spam_entries'] ) ? 1 : 0; $stat['time_limit'] += ! empty( $anti_spam['time_limit']['enable'] ) ? 1 : 0; $stat['country_filter'] += ! empty( $anti_spam['country_filter']['enable'] ) ? 1 : 0; $stat['keyword_filter'] += ! empty( $anti_spam['keyword_filter']['enable'] ) ? 1 : 0; $stat['captcha'] += ! empty( $settings['recaptcha'] ) ? 1 : 0; } // Count the list of keywords for the keyword filter. $keyword_filter = wpforms()->obj( 'antispam_keyword_filter' ); $keywords = method_exists( $keyword_filter, 'get_keywords' ) ? $keyword_filter->get_keywords() : []; $stat['keywords'] = count( $keywords ); return $stat; } /** * Count how many field have a specific setting enabled. * * @since 1.9.0.3 * * @param array $forms Published forms. * @param string $field_type Field type. * @param string $field_setting Field setting. * * @return int */ private function count_fields_with_setting( array $forms, string $field_type, string $field_setting ): int { $counter = 0; // Bail early, in case there are no forms. if ( empty( $forms ) ) { return $counter; } // Go through all forms. foreach ( $forms as $form ) { $fields = $form->post_content['fields'] ?? []; if ( empty( $fields ) ) { continue; } // Go through all fields on the form. foreach ( $fields as $field ) { if ( ! empty( $field['type'] ) && $field['type'] === $field_type && ! empty( $field[ $field_setting ] ) ) { ++$counter; } } } return $counter; } } Integrations/AI/AI.php 0000644 00000003306 15174710275 0010520 0 ustar 00 <?php // phpcs:disable Generic.Commenting.DocComment.MissingShort /** @noinspection PhpIllegalPsrClassPathInspection */ /** @noinspection AutoloadingIssuesInspection */ // phpcs:enable Generic.Commenting.DocComment.MissingShort namespace WPForms\Integrations\AI; use WPForms\Integrations\IntegrationInterface; use WPForms\Integrations\AI\Admin\Ajax\Choices as ChoicesAjax; use WPForms\Integrations\AI\Admin\Ajax\Forms as FormsAjax; use WPForms\Integrations\AI\Admin\Builder\Enqueues; use WPForms\Integrations\AI\Admin\Builder\FieldOption; use WPForms\Integrations\AI\Admin\Builder\Forms as FormsEnqueues; use WPForms\Integrations\AI\Admin\Pages\Templates as TemplatesPage; /** * Integration of the AI features. * * @since 1.9.1 */ class AI implements IntegrationInterface { /** * Determine whether the integration is allowed to load. * * @since 1.9.1 * * @return bool */ public function allow_load(): bool { // Always load the Settings class to register the toggle. if ( wpforms_is_admin_page( 'settings', 'misc' ) ) { ( new Admin\Settings() )->init(); } return ! Helpers::is_disabled(); } /** * Load the integration classes. * * @since 1.9.1 */ public function load() { if ( wpforms_is_admin_page( 'builder' ) ) { ( new Enqueues() )->init(); ( new FieldOption() )->init(); ( new FormsEnqueues() )->init(); } if ( wpforms_is_admin_page( 'templates' ) ) { ( new TemplatesPage() )->init(); } if ( wpforms_is_admin_ajax() ) { $this->load_ajax_classes(); } } /** * Load AJAX classes. * * @since 1.9.1 */ protected function load_ajax_classes() { ( new FieldOption() )->init(); ( new ChoicesAjax() )->init(); ( new FormsAjax() )->init(); } } Integrations/AI/API/Choices.php 0000644 00000002466 15174710275 0012223 0 ustar 00 <?php namespace WPForms\Integrations\AI\API; use WPForms\Integrations\AI\Helpers; /** * Choices class. * * @since 1.9.1 */ class Choices extends API { /** * Get choices from the API. * * @since 1.9.1 * * @param string $prompt Prompt to get choices for. * @param string $session_id Session ID. * * @return array */ public function choices( string $prompt, string $session_id = '' ): array { $args = [ 'userPrompt' => $this->prepare_prompt( $prompt ), 'limit' => $this->get_limit(), ]; if ( ! empty( $session_id ) ) { $args['sessionId'] = $session_id; } $endpoint = '/ai-choices'; $response = $this->request->post( $endpoint, $args ); if ( $response->has_errors() ) { $error_data = $response->get_error_data(); Helpers::log_error( $response->get_log_message( $error_data ), $endpoint, $args ); return $error_data; } $result = $response->get_body(); // Limit the number of choices. // In some cases, the API may return more choices than requested. $choices = array_slice( $result['choices'], 0, $this->get_limit() ); // Remove numeration from choices. $choices = array_map( static function ( $choice ) { return preg_replace( '/^\d+\.\s+/', '', $choice ); }, $choices ); $result['choices'] = $choices; return $result; } } Integrations/AI/API/Forms.php 0000644 00000026415 15174710275 0011734 0 ustar 00 <?php namespace WPForms\Integrations\AI\API; use WPForms\Integrations\AI\Admin\Ajax\Forms as FormsAjax; use WPForms\Integrations\AI\Helpers; use WPForms\Forms\Fields\Pagebreak\Field as PagebreakField; /** * Form API class. * * @since 1.9.2 */ class Forms extends API { /** * API endpoint. * * @since 1.9.2 */ private const ENDPOINT = '/ai-forms'; /** * Get form from the API. * * @since 1.9.2 * * @param string $prompt Prompt to get the form. * @param string $session_id Session ID. * * @return array * @noinspection PhpUndefinedConstantInspection */ public function form( string $prompt, string $session_id = '' ): array { $args = [ 'userPrompt' => $this->prepare_prompt( $prompt ), 'limit' => $this->get_limit(), ]; if ( ! empty( $session_id ) ) { $args['sessionId'] = $session_id; } // Flag requests from Lite plugin. $args['lite'] = ! wpforms()->is_pro(); // Add available addons to the request arguments. $args['addons'] = $this->get_available_addons(); // Add GDPR setting to the request arguments. $args['gdpr'] = wpforms_setting( 'gdpr' ); // Add a Page break field support. $args['pagebreak'] = true; // Add prompt debug info support. $args['debug'] = defined( 'WPFORMS_AI_DEBUG' ) && WPFORMS_AI_DEBUG; $response = $this->request->post( self::ENDPOINT, $args ); if ( $response->has_errors() ) { $error_data = $response->get_error_data(); Helpers::log_error( $response->get_log_message( $error_data ), self::ENDPOINT, $args ); return $error_data; } return $this->normalize_form_data( $response->get_body() ); } /** * Get available addons. * * @since 1.9.2 * * @return array */ private function get_available_addons(): array { // Since starting from 1.9.4, we display unavailable addon fields in Lite and all Pro licenses, // we need to return all required addons to let AI generate addon fields. return FormsAjax::FORM_GENERATOR_REQUIRED_ADDONS; } /** * Normalize form data. * * @since 1.9.2 * * @param array $form_data Form data. * * @return array */ private function normalize_form_data( array $form_data ): array { // Recursively normalize form data. $form_data = $this->normalize_form_data_recursive( $form_data ); // Fix fields data. $form_data = $this->fix_fields_data( $form_data ); // Notifications and confirmations arrays should be indexed from 1. if ( ! empty( $form_data['settings']['notifications'] ) ) { $form_data['settings']['notifications'] = array_combine( range( 1, count( $form_data['settings']['notifications'] ) ), array_values( $form_data['settings']['notifications'] ) ); } if ( ! empty( $form_data['settings']['confirmations'] ) ) { $form_data['settings']['confirmations'] = array_combine( range( 1, count( $form_data['settings']['confirmations'] ) ), array_values( $form_data['settings']['confirmations'] ) ); } $form_data['form_title'] = empty( $form_data['form_title'] ) ? esc_html__( 'Untitled Form', 'wpforms-lite' ) : $form_data['form_title']; $form_data['settings']['form_title'] = $form_data['form_title']; return $form_data; } /** * Normalize form data recursive. * * @since 1.9.2 * * @param array $form_data Form data. * * @return array */ private function normalize_form_data_recursive( array $form_data ): array { foreach ( $form_data as $key => $value ) { if ( is_array( $value ) ) { $form_data[ $key ] = $this->normalize_form_data_recursive( $value ); } // Convert `false` and `true` values to '0' and '1'. $form_data[ $key ] = $form_data[ $key ] === false ? '0' : $form_data[ $key ]; $form_data[ $key ] = $form_data[ $key ] === true ? '1' : $form_data[ $key ]; // Remove null values. if ( $form_data[ $key ] === null ) { unset( $form_data[ $key ] ); } } return $form_data; } /** * Fix fields' data. * * @since 1.9.2 * * @param array $form_data Form data. * * @return array */ private function fix_fields_data( array $form_data ): array { $updated_fields_data = []; $page_breaks = []; // Fix array keys. The key should be identical to `id`. foreach ( $form_data['fields'] as $field_data ) { $updated_fields_data[ (string) $field_data['id'] ] = $field_data; } $form_data['fields'] = $updated_fields_data; // Fix choice values and choices array indexes. foreach ( $form_data['fields'] as $id => $field_data ) { $field_data = $this->fix_field_defaults( $field_data ); $form_data['fields'][ $id ] = $this->fix_choices( $field_data ); } // Fix conditional logic rules and detect page breaks. foreach ( $form_data['fields'] as $id => $field_data ) { $form_data['fields'][ $id ] = $this->fix_field_cl( $field_data, $form_data ); if ( $field_data['type'] === 'pagebreak' ) { $page_breaks[] = $id; } } // Fix page breaks. if ( ! empty( $page_breaks ) ) { $form_data = $this->fix_page_breaks( $form_data, $page_breaks ); } return $form_data; } /** * Fix field's conditional logic rules. * * @since 1.9.2 * * @param array $field Field data. * @param array $form_data Form data. * * @return array */ private function fix_field_cl( array $field, array $form_data ): array { if ( empty( $field['conditionals'] ) || empty( $field['conditional_logic'] ) ) { return $field; } // Loop groups. foreach ( $field['conditionals'] as $group_key => $group ) { // Loop rules. foreach ( $group as $rule_key => $rule ) { $choices = $form_data['fields'][ $rule['field'] ]['choices'] ?? []; // We only need to update rules for choice-based fields. if ( empty( $choices ) ) { continue; } // AI uses choice value, but we should use the index of the choice in the `choices` array. $field['conditionals'][ $group_key ][ $rule_key ]['value'] = $this->get_choice_index( $choices, $rule['value'] ); // Continue if the operator is supported by the choice-based field. if ( in_array( $rule['operator'], [ '==', '!=', 'e', '!e' ], true ) ) { continue; } // Fix `operator` value for choice-based fields. $rule['operator'] = in_array( $rule['operator'], [ 'c', '^', '>', '<' ], true ) ? '==' : $rule['operator']; $rule['operator'] = in_array( $rule['operator'], [ '!c', '~' ], true ) ? '!=' : $rule['operator']; $field['conditionals'][ $group_key ][ $rule_key ]['operator'] = $rule['operator']; } } return $field; } /** * Find choice index in the `choices` array. * * @since 1.9.2 * * @param array $choices Choices data. * @param string $value Value to find in choices. * * @return string|null */ private function get_choice_index( array $choices, string $value ) { $index = array_search( $value, array_column( $choices, 'value' ), true ); if ( $index === false ) { $index = array_search( $value, array_column( $choices, 'label' ), true ); } $choices_keys = array_keys( $choices ); return $index === false ? null : $choices_keys[ $index ]; } /** * Add missing default attributes to the field. * * @since 1.9.4 * * @param array $field Field data. * * @return array */ private function fix_field_defaults( array $field ): array { /** * Allow the default field settings to be filtered. * * @since 1.0.8 * * @param array $field Default field settings. */ $field = (array) apply_filters( 'wpforms_field_new_default', $field ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName // Set the defaults for certain fields. if ( $field['type'] === 'content' ) { $field['label'] = empty( $field['label'] ) ? esc_html__( 'Content', 'wpforms-lite' ) : $field['label']; } if ( $field['type'] === 'richtext' ) { $field['default_value'] = ''; } return $field; } /** * Fix choices. * * Remove unnecessary values from choices. * * @since 1.9.2 * * @param array $field Field data. * * @return array */ private function fix_choices( array $field ): array { if ( empty( $field['choices'] ) ) { return $field; } // Remove values from choices for non-payment fields. if ( ! in_array( $field['type'], [ 'payment-multiple', 'payment-checkbox', 'payment-select' ], true ) ) { // Remove values from choices. foreach ( $field['choices'] as $i => $choice ) { $field['choices'][ $i ]['value'] = ''; } } $updated_choices = []; // Update array keys to start from 1. foreach ( $field['choices'] as $i => $choice ) { $updated_choices[ $i + 1 ] = $choice; } $field['choices'] = $updated_choices; return $field; } /** * Fix page breaks. * * Add top and bottom page breaks to the form, set `nav_align` for all page breaks. * * @since 1.9.3 * * @param array $form_data Form data. * @param array $page_breaks Page break IDs. * * @return array */ private function fix_page_breaks( array $form_data, array $page_breaks ): array { // phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh reset( $form_data['fields'] ); $max_field_id = max( array_keys( $form_data['fields'] ) ); // Update or add the top page break. $first_field_id = key( $form_data['fields'] ); // If the first field is a page break, use its ID, otherwise create a new one. if ( $form_data['fields'][ $first_field_id ]['type'] === 'pagebreak_top' ) { $top_id = $first_field_id; } else { $top_id = '0'; $form_data['fields'] = [ $top_id => [] ] + $form_data['fields']; } $form_data['fields'][ $top_id ] = [ 'type' => 'pagebreak', 'id' => $top_id, 'position' => 'top', 'indicator' => 'progress', 'indicator_color' => PagebreakField::get_default_indicator_color(), 'title' => $form_data['fields'][ $top_id ]['title'] ?? '', 'nav_align' => $form_data['fields'][ $top_id ]['nav_align'] ?? 'left', ]; // Remove the Previous button from the first normal pagebreak. $form_data['fields'][ $page_breaks[0] ]['prev'] = ''; $form_data['fields'][ $page_breaks[0] ]['prev_toggle'] = ''; end( $form_data['fields'] ); // Update or add the bottom page break. // If the last field is a page break, use its ID, otherwise create a new one. $last_field_id = key( $form_data['fields'] ); $last_field = $form_data['fields'][ key( $form_data['fields'] ) ]; $bottom_id = $last_field['type'] === 'pagebreak_bottom' ? $last_field_id : (string) ++$max_field_id; $form_data['fields'][ $bottom_id ] = [ 'type' => 'pagebreak', 'id' => $bottom_id, 'position' => 'bottom', 'title' => '', 'prev' => $form_data['fields'][ $bottom_id ]['prev'] ?? '', 'prev_toggle' => $form_data['fields'][ $bottom_id ]['prev_toggle'] ?? '', ]; // Remove the Previous button from the bottom pagebreak. if ( empty( $form_data['fields'][ $bottom_id ]['prev_toggle'] ) ) { unset( $form_data['fields'][ $bottom_id ]['prev'], $form_data['fields'][ $bottom_id ]['prev_toggle'] ); } // Prevent wrong pagebreaks. foreach ( $form_data['fields'] as $d => $field ) { $field['type'] = $field['type'] === 'pagebreak_top' ? 'pagebreak' : $field['type']; $field['type'] = $field['type'] === 'pagebreak_bottom' ? 'pagebreak' : $field['type']; $form_data['fields'][ $d ] = $field; } return $form_data; } } Integrations/AI/API/Http/Response.php 0000644 00000004740 15174710275 0013360 0 ustar 00 <?php namespace WPForms\Integrations\AI\API\Http; // phpcs:ignore WPForms.PHP.UseStatement.UnusedUseStatement use WP_Error; /** * Response class. * * @since 1.9.1 */ class Response { /** * Response. * * @since 1.9.1 * * @var array */ protected $response; /** * Response constructor. * * @since 1.9.1 * * @param array|WP_Error $response Response. */ public function __construct( $response ) { $this->response = $response; } /** * Retrieve only the body from the raw response. * * @since 1.9.1 * * @return array The body of the response. */ public function get_body(): array { $body = wp_remote_retrieve_body( $this->response ); if ( empty( $body ) ) { return []; } return json_decode( $body, true ) ?? []; } /** * Get error data. * * @since 1.9.1 * * @return array */ public function get_error_data(): array { $code = $this->get_response_code(); return [ 'error' => $this->get_response_message(), 'code' => empty( $code ) ? 'wp_error' : $code, ]; } /** * Retrieve only the response message from the raw response. * * @since 1.9.1 * * @return string The response error. */ public function get_response_message(): string { if ( is_wp_error( $this->response ) ) { if ( $this->response->get_error_code() === 'http_request_failed' ) { return __( 'There appears to be a network error.', 'wpforms-lite' ); } return $this->response->get_error_message(); } $body = $this->get_body(); return $body['error_message'] ?? wp_remote_retrieve_response_message( $this->response ); } /** * Get the error log message. * * @since 1.9.2 * * @param array $error_data Error data. * * @return string The error log message. */ public function get_log_message( array $error_data ): string { return sprintf( /* translators: %1$s - error code, %2$s - error message. */ __( 'API response: %1$s %2$s', 'wpforms-lite' ), $error_data['code'], $error_data['error'] ); } /** * Retrieve only the response code from the raw response. * * @since 1.9.1 * * @return int The response code as an integer. */ private function get_response_code(): int { return absint( wp_remote_retrieve_response_code( $this->response ) ); } /** * Whether we received errors in the response. * * @since 1.9.1 * * @return bool True if response has errors. */ public function has_errors(): bool { $code = $this->get_response_code(); return $code < 200 || $code > 299; } } Integrations/AI/API/Http/Request.php 0000644 00000006446 15174710275 0013217 0 ustar 00 <?php namespace WPForms\Integrations\AI\API\Http; use WPForms\Integrations\AI\Helpers; use WPForms\Integrations\LiteConnect\LiteConnect; use WPForms\Integrations\LiteConnect\Integration; /** * Request class. * * @since 1.9.1 */ class Request { /** * API URL. * * @since 1.9.1 */ private const URL = 'https://wpformsapi.com/api/v1'; /** * Request timeout. * * @since 1.9.1 */ private const TIMEOUT = 60; /** * Send a POST request. * * @since 1.9.1 * * @param string $endpoint Endpoint to request. * @param array $args Request arguments. * * @return Response Response from the API. */ public function post( string $endpoint, array $args = [] ): Response { return $this->request( 'POST', $endpoint, $args ); } /** * Make a request to the API. * * @since 1.9.1 * * @param string $method Request method. * @param string $endpoint Endpoint to request. * @param array $args Arguments to send. * * @return Response Response from the API. * @noinspection PhpSameParameterValueInspection */ private function request( string $method, string $endpoint, array $args ): Response { // Once mark AI features as used when making a first request. Helpers::set_ai_used(); // Add domain to the request. $args['domain'] = preg_replace( '/(https?:\/\/)?(www\.)?(.*)\/?/', '$3', home_url() ); $args = $this->maybe_add_lite_connect_credentials( $args ); $options = [ 'method' => $method, 'headers' => $this->get_headers(), 'timeout' => $this->get_timeout(), 'body' => wp_json_encode( $args ), ]; $url = $this->get_request_url( $endpoint ); return new Response( wp_safe_remote_request( $url, $options ) ); } /** * Get AI API request URL. * * @since 1.9.3 * * @param string $endpoint Endpoint to request. * * @return string */ private function get_request_url( string $endpoint ): string { /** * Filter AI API request URL. * * @since 1.9.3 * * @param string $url API request URL. * @param string $endpoint Endpoint to request. */ return (string) apply_filters( 'wpforms_integrations_aiapi_http_request_url', self::URL . $endpoint, $endpoint ); } /** * Maybe add Lite Connect credentials to the request. * * @since 1.9.1 * * @param array $args Arguments to send. * * @return array */ private function maybe_add_lite_connect_credentials( array $args ): array { if ( wpforms()->is_pro() ) { return $args; } if ( ! LiteConnect::is_allowed() || ! LiteConnect::is_enabled() ) { return $args; } return array_merge( $args, Integration::get_site_credentials() ); } /** * Retrieve request headers. * * @since 1.9.1 * * @return array */ private function get_headers(): array { $headers = [ 'Content-Type' => 'application/json', ]; if ( wpforms()->is_pro() ) { $headers['x-wpforms-licensekey'] = wpforms_get_license_key(); } return $headers; } /** * Retrieve request timeout. * * @since 1.9.1 * * @return int */ private function get_timeout(): int { /** * Filter the API request timeout. * * @since 1.9.1 * * @param int $timeout Request timeout. */ return (int) apply_filters( 'wpforms_integrations_ai_api_http_request_timeout', self::TIMEOUT ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName } } Integrations/AI/API/API.php 0000644 00000004175 15174710275 0011256 0 ustar 00 <?php namespace WPForms\Integrations\AI\API; use WPForms\Integrations\AI\API\Http\Request; use WPForms\Integrations\AI\Helpers; /** * API class. * * @since 1.9.1 */ class API { /** * API limit. * * @since 1.9.1 */ const LIMIT = 100; /** * API limit max. * * @since 1.9.1 */ const LIMIT_MAX = 1000; /** * Request instance. * * @since 1.9.1 * * @var Request */ protected $request; /** * Initialize the API. * * @since 1.9.1 */ public function init() { $this->request = new Request(); } /** * Rate the response. * * @since 1.9.1 * * @param bool $helpful Whether the response was helpful. * @param string $response_id Response ID to rate. * * @return array */ public function rate( bool $helpful, string $response_id ): array { $args = [ 'helpful' => $helpful, 'responseId' => $response_id, ]; $endpoint = '/rate-response'; $response = $this->request->post( $endpoint, $args ); if ( $response->has_errors() ) { $error_data = $response->get_error_data(); Helpers::log_error( $response->get_log_message( $error_data ), $endpoint, $args ); return $error_data; } return $response->get_body(); } /** * Get the limit for the API request. * Returns limit set by the filter or the default limit. * The limit is capped at LIMIT_MAX. * * @since 1.9.1 * * @return int */ protected function get_limit(): int { return min( /** * Filter the limit for the API request. * * @since 1.9.1 * * @param int $limit Limit for the API request. */ (int) apply_filters( 'wpforms_integrations_ai_api_get_limit', self::LIMIT ), // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName self::LIMIT_MAX ); } /** * Prepare the prompt. * * @since 1.9.1 * * @param string $prompt Prompt text. * * @return string */ protected function prepare_prompt( string $prompt ): string { // Remove any HTML tags. $prompt = wp_strip_all_tags( $prompt ); // Remove any extra spaces. $prompt = preg_replace( '/\s+/', ' ', $prompt ); // Remove any extra characters. return trim( $prompt, ' .,!?;:' ); } } Integrations/AI/Helpers.php 0000644 00000005574 15174710275 0011642 0 ustar 00 <?php namespace WPForms\Integrations\AI; /** * AI features related helper methods. * * @since 1.9.1 */ class Helpers { /** * Key for a state whether integration is disabled on the Settings > Misc admin page. * * @since 1.9.1 */ public const DISABLE_KEY = 'ai-feature-disabled'; /** * Key for a state whether integration is used (or has been used). * There is no UI/UX for it, and it's used for internal purposes. * * @since 1.9.1 */ private const USE_KEY = 'ai-feature-used'; /** * Determine whether integration is disabled. * * @since 1.9.1 * * @return bool */ public static function is_disabled(): bool { return self::is_disabled_by_rule() || wpforms_setting( self::DISABLE_KEY ); } /** * Determine whether integration is used. * * @since 1.9.1 * * @return bool */ public static function is_used(): bool { return (bool) wpforms_setting( self::USE_KEY ); } /** * Mark integration as used. * * @since 1.9.1 */ public static function set_ai_used() { if ( self::is_used() ) { return; } $settings = (array) get_option( 'wpforms_settings', [] ); $settings[ self::USE_KEY ] = true; update_option( 'wpforms_settings', $settings ); } /** * Determine whether integration is disabled through constant or filter. * * @since 1.9.1 * * @return bool * @noinspection PhpUndefinedConstantInspection */ public static function is_disabled_by_rule(): bool { $is_disabled = defined( 'WPFORMS_DISABLE_AI_FEATURES' ) && WPFORMS_DISABLE_AI_FEATURES; /** * Allow modifying whether AI integration is disabled in WPForms. * * @since 1.9.1 * * @param bool $is_disabled True if AI integration is disabled. Default is false. */ return (bool) apply_filters( 'wpforms_disable_ai_features', $is_disabled ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName } /** * Log an error record. * * @since 1.9.1 * * @param string $message Error message. * @param string $endpoint Endpoint. * @param array $args Arguments. */ public static function log_error( string $message, string $endpoint, array $args ) { wpforms_log( 'AI Integration Error', [ 'error' => $message, 'endpoint' => $endpoint, 'args' => $args, ], [ 'type' => [ 'ai', 'error' ], ] ); } /** * Get the license type. * * @since 1.9.4 * * @return string */ public static function get_license_type(): string { $license = (array) get_option( 'wpforms_license', [] ); return $license['type'] ?? ''; } /** * Determine whether a license key is active. * * @since 1.9.4 * * @return bool */ public static function is_license_active(): bool { $license = (array) get_option( 'wpforms_license', [] ); return ! empty( wpforms_get_license_key() ) && empty( $license['is_expired'] ) && empty( $license['is_disabled'] ) && empty( $license['is_invalid'] ); } } Integrations/AI/Admin/Pages/Templates.php 0000644 00000005442 15174710275 0014257 0 ustar 00 <?php namespace WPForms\Integrations\AI\Admin\Pages; use WPForms\Integrations\LiteConnect\LiteConnect; /** * Enqueue assets on the Form Templates admin page. * * @since 1.9.2 */ class Templates { /** * Initialize. * * @since 1.9.2 */ public function init(): void { $this->hooks(); } /** * Register hooks. * * @since 1.9.2 */ private function hooks(): void { add_action( 'admin_enqueue_scripts', [ $this, 'enqueues' ] ); add_action( 'wpforms_admin_form_templates_list_before', [ $this, 'output_card' ] ); } /** * Enqueue styles and scripts. * * @since 1.9.2 */ public function enqueues(): void { $min = wpforms_get_min_suffix(); wp_enqueue_style( 'wpforms-ai-forms-admin', WPFORMS_PLUGIN_URL . "assets/css/integrations/ai/form-templates-page$min.css", [], WPFORMS_VERSION ); } /** * Output Generate with AI card. * * @since 1.9.3 * * @noinspection HtmlUnknownTarget * @noinspection HtmlUnknownAttribute */ public function output_card(): void { $button_class = 'wpforms-template-generate'; $button_attr = ''; // In Lite, we should disable the button in the case Lite Connect is not allowed. if ( ! LiteConnect::is_allowed() && ! wpforms()->is_pro() ) { $button_class .= ' wpforms-inactive wpforms-help-tooltip'; $button_attr = sprintf( 'data-tooltip-position="top" title="%1$s"', esc_html__( 'WPForms AI is not available on local sites.', 'wpforms-lite' ) ); } printf( '<div class="wpforms-template" id="wpforms-template-generate"> <div class="wpforms-template-thumbnail"> <div class="wpforms-template-thumbnail-placeholder"> <img src="%1$s" alt="%2$s" loading="lazy"> </div> </div> <div class="wpforms-template-name-wrap"> <h3 class="wpforms-template-name categories has-access favorite slug subcategories fields" data-categories="all,new" data-subcategories="" data-fields="" data-has-access="1" data-favorite="" data-slug="generate"> %2$s </h3> <span class="wpforms-badge wpforms-badge-sm wpforms-badge-inline wpforms-badge-purple wpforms-badge-rounded">%3$s</span> </div> <p class="wpforms-template-desc"> %4$s </p> <div class="wpforms-template-buttons"> <a href="#" class="%5$s wpforms-btn wpforms-btn-md wpforms-btn-purple-dark" %6$s> %7$s </a> </div> </div>', esc_url( WPFORMS_PLUGIN_URL ) . 'assets/images/integrations/ai/ai-feature-icon.svg', esc_html__( 'Generate With AI', 'wpforms-lite' ), esc_html__( 'NEW!', 'wpforms-lite' ), esc_html__( 'Write simple prompts to create complex forms catered to your specific needs.', 'wpforms-lite' ), esc_attr( $button_class ), $button_attr, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped esc_html__( 'Generate Form', 'wpforms-lite' ) ); } } Integrations/AI/Admin/Settings.php 0000644 00000002730 15174710275 0013057 0 ustar 00 <?php namespace WPForms\Integrations\AI\Admin; use WPForms\Integrations\AI\Helpers; /** * AI Settings class. * * @since 1.9.1 */ class Settings { /** * Initialize. * * @since 1.9.1 */ public function init() { $this->hooks(); } /** * Register hooks. * * @since 1.9.1 */ private function hooks() { add_filter( 'wpforms_settings_defaults', [ $this, 'register_settings' ] ); } /** * Add toggle to the Settings > Misc admin page. * * @since 1.9.1 * * @param array|mixed $settings WPForms settings. * * @return array */ public function register_settings( $settings ): array { $settings = (array) $settings; $ai_settings = [ 'id' => Helpers::DISABLE_KEY, 'name' => esc_html__( 'Hide AI Features', 'wpforms-lite' ), 'desc' => esc_html__( 'Hide everything related to AI in WPForms.', 'wpforms-lite' ), 'type' => 'toggle', 'status' => true, 'value' => Helpers::is_disabled(), 'disabled' => Helpers::is_disabled_by_rule(), ]; if ( $ai_settings['disabled'] ) { $ai_settings['disabled_desc'] = wp_kses( __( '<strong>AI features were hidden by filter or constant.</strong>', 'wpforms-lite' ), // phpcs:ignore WordPress.WP.I18n.NoHtmlWrappedStrings [ 'strong' => [], ] ); } // Add after the "Hide Admin Bar Menu" toggle. $settings['misc'] = wpforms_array_insert( $settings['misc'], [ Helpers::DISABLE_KEY => $ai_settings ], 'hide-admin-bar' ); return $settings; } } Integrations/AI/Admin/Ajax/Choices.php 0000644 00000002260 15174710275 0013515 0 ustar 00 <?php namespace WPForms\Integrations\AI\Admin\Ajax; use WPForms\Integrations\AI\API\Choices as ChoicesAPI; /** * Choices class. * * @since 1.9.1 */ class Choices extends Base { /** * API Choices instance. * * @since 1.9.1 * * @var ChoicesAPI */ protected $api_choices; /** * Initialize. * * @since 1.9.1 */ public function init() { parent::init(); $this->api_choices = new ChoicesAPI(); $this->api_choices->init(); $this->hooks(); } /** * Register hooks. * * @since 1.9.1 */ private function hooks() { add_action( 'wp_ajax_wpforms_get_ai_choices', [ $this, 'get_choices' ] ); } /** * Get choices. * * @since 1.9.1 */ public function get_choices() { if ( ! $this->validate_nonce() ) { wp_send_json_error( [ 'error' => esc_html__( 'Your session expired. Please reload the builder.', 'wpforms-lite' ) ] ); } $prompt = $this->get_post_data( 'prompt' ); if ( $this->is_empty_prompt( $prompt ) ) { wp_send_json_success( [ 'choices' => [] ] ); } $session_id = $this->get_post_data( 'session_id' ); $choices = $this->api_choices->choices( $prompt, $session_id ); wp_send_json_success( $choices ); } } Integrations/AI/Admin/Ajax/Forms.php 0000644 00000030242 15174710275 0013227 0 ustar 00 <?php namespace WPForms\Integrations\AI\Admin\Ajax; use WPForms\Integrations\AI\API\Forms as FormsAPI; use WPForms_Template_Blank; /** * Forms AJAX class. * * @since 1.9.2 */ class Forms extends Base { /** * The addons required for the AI form generator. * * @since 1.9.2 * * @var array */ public const FORM_GENERATOR_REQUIRED_ADDONS = [ 'surveys-polls', 'signatures', 'coupons', 'calculations', 'quiz' ]; /** * The addon fields. * * @since 1.9.4 * * @var array */ public const FORM_GENERATOR_ADDON_FIELDS = [ 'likert_scale' => 'surveys-polls', 'net_promoter_score' => 'surveys-polls', 'signature' => 'signatures', 'payment-coupon' => 'coupons', ]; /** * Forms API instance. * * @since 1.9.2 * * @var FormsAPI */ private $forms_api; /** * Initialize. * * @since 1.9.2 */ public function init() { parent::init(); $this->forms_api = new FormsAPI(); $this->forms_api->init(); $this->hooks(); } /** * Register hooks. * * @since 1.9.2 */ private function hooks(): void { add_action( 'wp_ajax_wpforms_get_ai_form', [ $this, 'get_form' ] ); add_action( 'wp_ajax_wpforms_get_ai_form_field_preview', [ $this, 'get_field_preview' ] ); add_action( 'wp_ajax_wpforms_use_ai_form', [ $this, 'use_form' ] ); add_action( 'wp_ajax_wpforms_dismiss_ai_form', [ $this, 'dismiss' ] ); } /** * "Get form" AJAX callback. * * @since 1.9.2 */ public function get_form(): void { if ( ! $this->validate_nonce() ) { wp_send_json_error( [ 'error' => esc_html__( 'Your session expired. Please reload the builder.', 'wpforms-lite' ) ] ); } $prompt = $this->get_post_data( 'prompt' ); if ( empty( $prompt ) && $prompt !== '0' ) { wp_send_json_error( [ 'error' => esc_html__( 'Empty prompt.', 'wpforms-lite' ) ] ); } $session_id = $this->get_post_data( 'session_id' ); $form = $this->forms_api->form( $prompt, $session_id ); $form['fieldsOrder'] = array_keys( $form['fields'] ?? [] ); /** * Filters the form data before outputting it. * * @since 1.9.7 * * @param array $form Form data. * @param string $session_id Session ID. */ $form = apply_filters( 'wpforms_integrations_ai_admin_ajax_forms_get_form_before_send', $form, $session_id ); wp_send_json_success( $form ); } /** * Get form field preview. * * @since 1.9.2 */ public function get_field_preview(): void { if ( ! $this->validate_nonce() ) { wp_send_json_error( [ 'error' => esc_html__( 'Your session expired. Please reload the builder.', 'wpforms-lite' ) ] ); } $field = $this->prepare_field_data( $this->get_post_data( 'field', 'array' ) ); if ( empty( $field ) ) { wp_send_json_error( [ 'error' => esc_html__( 'Empty field data.', 'wpforms-lite' ) ] ); } $field_type = $field['type'] ?? ''; // Check if the field type is available. if ( has_action( "wpforms_display_field_{$field_type}" ) ) { ob_start(); // Generate field preview. /** This action is documented in includes/admin/builder/panels/class-fields.php. */ do_action( "wpforms_builder_fields_previews_{$field_type}", $field ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName $preview = ob_get_clean(); } wp_send_json_success( $preview ?? '' ); } /** * Prepare the form fields data. * * @since 1.9.2 * * @param array $form_data Form data. * * @return array */ private function prepare_fields_data( array $form_data ): array { $fields_data = $form_data['fields'] ?? []; $fields_order = $form_data['fieldsOrder'] ?? []; $fields = []; foreach ( $fields_order as $id ) { $fields[ $id ] = $this->prepare_field_data( $fields_data[ $id ] ); } return $fields; } /** * Prepare the field data. * * @since 1.9.2 * * @param array $field_data Field data. * * @return array */ private function prepare_field_data( array $field_data ): array { $field_type = $field_data['type'] ?? ''; if ( $field_type === 'content' ) { $field_data['content'] = htmlspecialchars_decode( $field_data['content'] ?? '' ); } if ( $field_type === 'html' ) { $field_data['code'] = htmlspecialchars_decode( $field_data['code'] ?? '' ); } $field_data['description'] = htmlspecialchars_decode( $field_data['description'] ?? '' ); if ( ! empty( $field_data['conditionals'] ) ) { $field_data['conditionals'] = $this->prepare_field_cl( $field_data ); } return $field_data; } /** * Prepare the form settings. * * @since 1.9.2 * * @param array $form_data Form data. * * @return array */ private function prepare_form_settings( array $form_data ): array { // Prepare the notifications. if ( isset( $form_data['settings']['notifications']['1'] ) ) { $form_data['settings']['notifications']['1']['subject'] = sprintf( /* translators: %s - form name. */ esc_html__( 'New Entry: %s', 'wpforms-lite' ), esc_html( $form_data['form_title'] ) ); } // Quiz settings. $form_data = $this->prepare_quiz_settings( $form_data ); return $form_data['settings']; } /** * Prepare the field conditional logic. * * @since 1.9.2 * * @param array $field Field data. * * @return array */ private function prepare_field_cl( array $field ): array { if ( empty( $field['conditionals'] ) ) { return []; } // Loop groups. foreach ( $field['conditionals'] as $group_key => $group ) { // Loop rules. foreach ( $group as $rule_key => $rule ) { // Fix `operator` value for choice-based fields. $rule['operator'] = htmlspecialchars_decode( $rule['operator'] ); $rule['value'] = htmlspecialchars_decode( $rule['value'] ); $field['conditionals'][ $group_key ][ $rule_key ] = $rule; } } return $field['conditionals']; } /** * Use form checks and prepare data. * * @since 1.9.2 * * @return array Form ID and the generated form data. */ private function use_form_check_data(): array { if ( ! $this->validate_nonce() ) { wp_send_json_error( [ 'error' => esc_html__( 'Your session expired. Please reload the builder.', 'wpforms-lite' ) ] ); } $form_id = $this->get_post_data( 'formId', 'int' ); $form_data = $this->get_post_data( 'formData', 'array' ); if ( empty( $form_data ) ) { wp_send_json_error( [ 'error' => esc_html__( 'Empty form data.', 'wpforms-lite' ) ] ); } if ( empty( $form_id ) && ! wpforms_current_user_can( 'create_forms' ) ) { wp_send_json_error( [ 'error' => esc_html__( 'Sorry, you are not allowed to create new forms.', 'wpforms-lite' ) ] ); } if ( ! empty( $form_id ) && ! wpforms_current_user_can( 'edit_form_single', $form_id ) ) { wp_send_json_error( [ 'error' => esc_html__( 'Sorry, you are not allowed to edit this form.', 'wpforms-lite' ) ] ); } $form_obj = wpforms()->obj( 'form' ); if ( ! $form_obj ) { wp_send_json_error( [ 'error' => esc_html__( 'Form database object not found.', 'wpforms-lite' ) ] ); } $form_post = ! empty( $form_id ) ? $form_obj->get( $form_id ) : null; if ( ( empty( $form_post ) && ! empty( $form_id ) ) || ( ! empty( $form_post->post_status ) && $form_post->post_status === 'trash' ) ) { wp_send_json_error( [ 'error' => esc_html__( 'It looks like the form you are trying to access is no longer available.', 'wpforms-lite' ) ] ); } $session_id = $this->get_post_data( 'sessionId' ); $response_history = $this->get_post_data( 'responseHistory', 'array' ); $chat_html = $this->get_post_data( 'chatHtml', 'string' ); return [ $form_id, $form_data, $session_id, $response_history, $chat_html, $form_obj ]; } /** * Use form. * * @since 1.9.2 */ public function use_form(): void { [ $form_id, $form_data, $session_id, $response_history, $chat_html, $form_obj ] = $this->use_form_check_data(); // Save the chat history in the user mata data. $user_meta = [ 'chatHtml' => $chat_html, 'responseHistory' => $response_history, ]; update_user_meta( get_current_user_id(), 'wpforms_builder_ai_form_chat_' . $session_id, $user_meta ); // Prepare the new form data. $form_data['fields'] = $this->prepare_fields_data( $form_data ); $form_data['settings'] = $this->prepare_form_settings( $form_data ); $form_data['field_id'] = count( $form_data['fields'] ) + 1; $meta = []; $meta['template'] = 'generate'; $meta['sessionId'] = $form_data['sessionId']; $meta['responseId'] = $form_data['responseId']; // Unset unrelated data. unset( $form_data['fieldsOrder'], $form_data['explanation'], $form_data['sessionId'], $form_data['responseId'], $form_data['processingData'] ); // Add a new form if it is a new form. if ( empty( $form_id ) ) { $form_id = $form_obj->add( $form_data['form_title'] ); } // Check if the form was created. if ( empty( $form_id ) ) { wp_send_json_error( [ 'error' => esc_html__( 'Form could not be created.', 'wpforms-lite' ) ] ); } // Get the blank template form data. $blank_form_data = WPForms_Template_Blank::get_data(); // Merge the blank form data with the new form data. // In this way, we can keep the default settings of the blank form. $form_data = array_replace_recursive( $blank_form_data, $form_data ); // Update the form ID. $form_data['id'] = $form_id; // Update the form. $form_obj->update( $form_id, $form_data, [ 'skip_revision' => 1 ] ); $form_obj->update_meta( $form_id, 'template', $meta['template'], [ 'skip_revision' => 1 ] ); $form_obj->update_meta( $form_id, 'sessionId', $meta['sessionId'], [ 'skip_revision' => 1 ] ); $form_obj->update_meta( $form_id, 'responseId', $meta['responseId'], [ 'skip_revision' => 1 ] ); // Result. wp_send_json_success( [ 'id' => $form_id, 'redirect' => add_query_arg( [ 'view' => 'fields', 'form_id' => $form_id, 'session' => $session_id, ], admin_url( 'admin.php?page=wpforms-builder' ) ), ] ); } /** * Ajax handler for dismissing. * * @since 1.9.2 */ public function dismiss(): void { if ( ! $this->validate_nonce() ) { wp_send_json_error( [ 'error' => esc_html__( 'Your session expired. Please reload the builder.', 'wpforms-lite' ) ] ); } // Identifier of the dismissible element. $element = $this->get_post_data( 'element', 'string' ); // Dismiss or de-dismiss. $dismiss = $this->get_post_data( 'dismiss', 'bool' ); if ( empty( $element ) ) { wp_send_json_error( [ 'error' => esc_html__( 'Please specify an element.', 'wpforms-lite' ) ] ); } // Check for permissions. if ( ! wpforms_current_user_can( 'edit_forms' ) ) { wp_send_json_error( [ 'error' => esc_html__( 'Sorry, you are not allowed to dismiss.', 'wpforms-lite' ) ] ); } $user_id = get_current_user_id(); $dismissed = get_user_meta( $user_id, 'wpforms_dismissed', true ); if ( empty( $dismissed ) ) { $dismissed = []; } if ( $dismiss ) { $dismissed[ 'ai-forms-' . $element ] = time(); } else { unset( $dismissed[ 'ai-forms-' . $element ] ); } update_user_meta( $user_id, 'wpforms_dismissed', $dismissed ); wp_send_json_success(); } /** * Prepare the quiz settings. * * @since 1.9.9 * * @param array $form_data Form data. * * @return array */ private function prepare_quiz_settings( array $form_data ): array { if ( empty( $form_data['settings']['quiz']['enabled'] ) ) { return $form_data; } $outcomes = (array) ( $form_data['settings']['quiz']['outcomes'] ?? [] ); $default_outcome = [ 'graded_message' => '', 'personality_message' => '', 'weighted_message' => '', 'conditionals' => [], ]; // Decode the outcome messages. foreach ( $outcomes as $key => $outcome ) { $outcome = wp_parse_args( $outcome, $default_outcome ); $outcome['graded_message'] = htmlspecialchars_decode( $outcome['graded_message'] ); $outcome['personality_message'] = htmlspecialchars_decode( $outcome['personality_message'] ); $outcome['weighted_message'] = htmlspecialchars_decode( $outcome['weighted_message'] ); $outcome['conditionals'] = $this->prepare_field_cl( $outcome ); $outcomes[ $key ] = $outcome; } $form_data['settings']['quiz']['outcomes'] = $outcomes; return $form_data; } } Integrations/AI/Admin/Ajax/Base.php 0000644 00000005343 15174710275 0013017 0 ustar 00 <?php namespace WPForms\Integrations\AI\Admin\Ajax; use WPForms\Integrations\AI\API\API; /** * Base class. * * @since 1.9.1 */ abstract class Base { /** * API instance. * * @since 1.9.1 * * @var API */ protected $api; /** * Initialize. * * @since 1.9.1 */ public function init() { $this->api = new API(); $this->api->init(); $this->hooks(); } /** * Register hooks. * * @since 1.9.1 */ private function hooks(): void { add_action( 'wp_ajax_wpforms_rate_ai_response', [ $this, 'rate_response' ] ); } /** * Rate choices response. * * @since 1.9.1 */ public function rate_response(): void { if ( ! $this->validate_nonce() ) { wp_send_json_error(); } $helpful = $this->get_post_data( 'helpful', 'bool' ); $response_id = $this->get_post_data( 'response_id' ); $response = $this->api->rate( $helpful, $response_id ); wp_send_json_success( $response ); } /** * Validate nonce. * * @since 1.9.1 * * @return bool|int */ protected function validate_nonce() { return check_ajax_referer( 'wpforms-ai-nonce', 'nonce', false ); } /** * Get the post's data by key. * * @since 1.9.1 * * @param string $key Key to get data for. * @param string $type Type of data to get. * * @return mixed */ protected function get_post_data( string $key, string $type = 'text' ) { switch ( $type ) { case 'int': $value = filter_input( INPUT_POST, $key, FILTER_SANITIZE_NUMBER_INT ) ?? 0; break; case 'array': $value = filter_input( INPUT_POST, $key, FILTER_SANITIZE_FULL_SPECIAL_CHARS, FILTER_REQUIRE_ARRAY ) ?? []; break; case 'bool': $value = filter_input( INPUT_POST, $key, FILTER_VALIDATE_BOOLEAN ) ?? false; break; case 'json': $value = json_decode( filter_input( INPUT_POST, $key ), true ); break; default: // We should use this alternative to FILTER_SANITIZE_FULL_SPECIAL_CHARS filter, // because htmlspecialchars() function does double encoding of special characters, // which is necessary to properly handle the encoded HTML in chat questions. $value = htmlspecialchars( filter_input( INPUT_POST, $key ) ?? '' ); break; } return $value; } /** * Determine whether a given prompt is empty. * * It must contain a minimum of one character. * * @since 1.9.1 * * @param string $prompt The prompt to check. * * @return bool True if the prompt is empty. */ protected function is_empty_prompt( string $prompt ): bool { $special_chars = [ '@', '!', '#', '$', '%', '^', '&', '*', '(', ')', '-', '+', '=', '{', '}', '[', ']', '|', '\\', ':', ';', '"', "'", '<', '>', ',', '.', '?', '/' ]; $prompt = str_replace( $special_chars, '', $prompt ); return empty( $prompt ); } } Integrations/AI/Admin/Builder/Forms.php 0000644 00000045156 15174710275 0013744 0 ustar 00 <?php // phpcs:disable Generic.Commenting.DocComment.MissingShort /** @noinspection PhpIllegalPsrClassPathInspection */ /** @noinspection AutoloadingIssuesInspection */ // phpcs:enable Generic.Commenting.DocComment.MissingShort namespace WPForms\Integrations\AI\Admin\Builder; use WP_Post; use WPForms\Integrations\AI\Admin\Ajax\Forms as FormsAjax; use WPForms\Integrations\AI\Helpers; use WPForms\Integrations\LiteConnect\LiteConnect; /** * Enqueue assets on the Form Builder screen in Pro. * * @since 1.9.2 * @since 1.9.4 Moved to the Lite plugin namespace. */ class Forms { /** * Initialize. * * @since 1.9.2 */ public function init(): void { $this->hooks(); } /** * Register hooks. * * @since 1.9.2 */ private function hooks(): void { add_action( 'wpforms_builder_enqueues', [ $this, 'enqueues' ] ); add_filter( 'wpforms_integrations_ai_admin_builder_enqueues_localize_chat_strings', [ $this, 'add_localize_chat_data' ] ); add_filter( 'wpforms_builder_template_active', [ $this, 'template_active' ], 10, 2 ); } /** * Enqueue styles and scripts. * * @since 1.9.2 * * @param string|null $view Current view (panel). * * @noinspection PhpMissingParamTypeInspection * @noinspection PhpUnusedParameterInspection */ public function enqueues( $view ): void { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found $this->enqueue_styles(); $this->enqueue_scripts(); } /** * Enqueue styles. * * @since 1.9.2 */ private function enqueue_styles(): void { $min = wpforms_get_min_suffix(); wp_enqueue_style( 'wpforms-ai-forms', WPFORMS_PLUGIN_URL . "assets/css/integrations/ai/ai-forms{$min}.css", [], WPFORMS_VERSION ); } /** * Enqueue scripts. * * @since 1.9.2 */ private function enqueue_scripts(): void { $min = wpforms_get_min_suffix(); wp_enqueue_script( 'wpforms-ai-form-generator', WPFORMS_PLUGIN_URL . "assets/js/integrations/ai/form-generator/form-generator{$min}.js", [], WPFORMS_VERSION, true ); wp_localize_script( 'wpforms-ai-form-generator', 'wpforms_ai_form_generator', $this->get_localize_form_generator_data() ); } /** * Set an active form template. * * @since 1.9.2 * * @param array|mixed $details Details. * @param WP_Post|false $form Form data. * * @return array */ public function template_active( $details, $form ): array { $details = (array) $details; if ( empty( $form ) ) { return []; } $form_data = wpforms_decode( $form->post_content ); if ( empty( $form_data['meta']['template'] ) || $form_data['meta']['template'] !== 'generate' ) { return $details; } return [ 'name' => esc_html__( 'Generate With AI', 'wpforms-lite' ), 'slug' => 'generate', 'description' => '', 'includes' => '', 'icon' => '', 'modal' => '', 'modal_display' => false, ]; } /** * Get form generator localize data. * * @since 1.9.2 * * @return array * @noinspection HtmlUnknownTarget */ private function get_localize_form_generator_data(): array { $min = wpforms_get_min_suffix(); $addons_data = $this->get_required_addons_data(); $modules_path = './modules/'; return [ 'nonce' => wp_create_nonce( 'wpforms-ai-nonce' ), 'adminNonce' => wp_create_nonce( 'wpforms-admin' ), 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 'addonsData' => $addons_data, 'addonsAction' => $this->get_required_addons_action( $addons_data ), 'addonFields' => FormsAjax::FORM_GENERATOR_ADDON_FIELDS, 'dismissed' => $this->get_dismissed_elements(), 'isPro' => wpforms()->is_pro(), 'isLicenseActive' => Helpers::is_license_active(), 'licenseType' => Helpers::get_license_type(), 'liteConnectEnabled' => LiteConnect::is_enabled(), 'liteConnectAllowed' => LiteConnect::is_allowed(), 'modules' => [ 'main' => $modules_path . "main{$min}.js?ver=" . WPFORMS_VERSION, 'preview' => $modules_path . "preview{$min}.js?ver=" . WPFORMS_VERSION, 'modals' => $modules_path . "modals{$min}.js?ver=" . WPFORMS_VERSION, ], 'templateCard' => [ 'imageSrc' => WPFORMS_PLUGIN_URL . 'assets/images/integrations/ai/ai-feature-icon.svg', 'name' => esc_html__( 'Generate With AI', 'wpforms-lite' ), 'desc' => esc_html__( 'Write simple prompts to create complex forms catered to your specific needs.', 'wpforms-lite' ), 'buttonTextInit' => esc_html__( 'Generate Form', 'wpforms-lite' ), 'buttonTextContinue' => esc_html__( 'Continue Generating', 'wpforms-lite' ), 'new' => esc_html__( 'NEW!', 'wpforms-lite' ), 'liteConnectNotAllowed' => esc_html__( 'WPForms AI is not available on local sites.', 'wpforms-lite' ), ], 'panel' => [ 'backToTemplates' => esc_html__( 'Back to Templates', 'wpforms-lite' ), 'emptyStateTitle' => esc_html__( 'Build Your Form Fast With the Help of AI', 'wpforms-lite' ), 'emptyStateDesc' => esc_html__( 'Not sure where to begin? Use our Generative AI tool to get started or take your pick from our wide variety of fields and start building out your form!', 'wpforms-lite' ), 'submitButton' => esc_html__( 'Submit', 'wpforms-lite' ), 'tooltipTitle' => esc_html__( 'This is just a preview of your form.', 'wpforms-lite' ), 'tooltipText' => esc_html__( 'Click "Use This Form" to start editing.', 'wpforms-lite' ), ], 'addons' => [ 'installTitle' => esc_html__( 'Before We Proceed', 'wpforms-lite' ), 'installContent' => esc_html__( 'In order to build the best forms possible, we need to install some addons. Would you like to install the recommended addons?', 'wpforms-lite' ), 'activateContent' => esc_html__( 'In order to build the best forms possible, we need to activate some addons. Would you like to activate the recommended addons?', 'wpforms-lite' ), 'installConfirmButton' => esc_html__( 'Yes, Install', 'wpforms-lite' ), 'activateConfirmButton' => esc_html__( 'Yes, Activate', 'wpforms-lite' ), 'cancelButton' => esc_html__( 'No, Thanks', 'wpforms-lite' ), 'dontShow' => esc_html__( 'Don\'t show this again', 'wpforms-lite' ), 'okay' => esc_html__( 'Okay', 'wpforms-lite' ), 'installing' => esc_html__( 'Installing...', 'wpforms-lite' ), 'activating' => esc_html__( 'Activating...', 'wpforms-lite' ), 'addonsInstalledTitle' => esc_html__( 'Addons Installed', 'wpforms-lite' ), 'addonsActivatedTitle' => esc_html__( 'Addons Activated', 'wpforms-lite' ), 'addonsInstalledContent' => esc_html__( 'You’re all set. We’re going to reload the builder and you can start building your form.', 'wpforms-lite' ), 'addonsInstallErrorTitle' => esc_html__( 'Addons Installation Error', 'wpforms-lite' ), 'addonsActivateErrorTitle' => esc_html__( 'Addons Activation Error', 'wpforms-lite' ), 'addonsInstallError' => esc_html__( 'Can\'t install or activate the required addons.', 'wpforms-lite' ), 'addonsInstallErrorNetwork' => esc_html__( 'There appears to be a network error.', 'wpforms-lite' ), 'dismissErrorTitle' => esc_html__( 'Error', 'wpforms-lite' ), 'dismissError' => esc_html__( 'Can\'t dismiss the modal window.', 'wpforms-lite' ), 'addon' => esc_html__( 'Addon', 'wpforms-lite' ), 'and' => esc_html__( 'and', 'wpforms-lite' ), 'addonInstalledTitle' => esc_html__( 'Addon Installed', 'wpforms-lite' ), 'addonActivatedTitle' => esc_html__( 'Addon Activated', 'wpforms-lite' ), 'addonInstalledContent' => esc_html__( 'You’re all set. We’re going to continue building your form.', 'wpforms-lite' ), ], 'quiz' => [ 'modalTitle' => esc_html__( 'Quiz Detected', 'wpforms-lite' ), 'modalContent' => sprintf( wp_kses( /* translators: %1$s - Quiz addon doc link. */ __( 'It looks like you\'re trying to create a quiz. Would you like to activate the <a href="%1$s" target="_blank" rel="noopener noreferrer">Quiz Addon</a> and easily create graded, personality, and weighted quizzes?', 'wpforms-lite' ), [ 'a' => [ 'href' => [], 'rel' => [], 'target' => [], ], ] ), // @TODO: Confirm the URL. esc_url( wpforms_utm_link( 'https://wpforms.com/docs/quiz-addon/', 'builder-modal', 'Quiz Addon Documentation' ) ) ), ], 'previewNotice' => [ 'title' => esc_html__( 'This Form Would Be Even Better With Fields From', 'wpforms-lite' ), 'msgUpgrade' => wp_kses( /* translators: %1$s - Upgrade to Pro link attributes. */ __( '<a href="#">Upgrade to Pro</a> and gain access to all fields and create the best possible forms.', 'wpforms-lite' ), [ 'a' => [ 'href' => [], ], ] ), 'btnUpgrade' => esc_html__( 'Upgrade to Pro', 'wpforms-lite' ), 'addons' => esc_html__( 'Addons', 'wpforms-lite' ), 'dismiss' => esc_html__( 'Dismiss this notice', 'wpforms-lite' ), ], 'misc' => [ 'warningExistingForm' => esc_html__( 'You’re about to overwrite your existing form. This will delete all fields and reset external connections. Are you sure you want to continue?', 'wpforms-lite' ), 'frozenChallengeTooltip' => esc_html__( 'The challenge will continue once AI form generation is complete', 'wpforms-lite' ), ], ]; } /** * Add chat element localize data. * * @since 1.9.2 * * @param array $strings Strings. * * @return array * @noinspection PhpMissingParamTypeInspection * @noinspection HtmlUnknownTarget */ public function add_localize_chat_data( $strings ): array { $for_lite = wpforms()->is_pro() ? '' : ' for Lite'; $strings['forms'] = [ 'title' => esc_html__( 'Generate a Form', 'wpforms-lite' ), 'description' => esc_html__( 'Describe the form you would like to create or use one of the example prompts below to get started.', 'wpforms-lite' ), 'descrEndDot' => '', 'learnMore' => esc_html__( 'Learn More About WPForms AI', 'wpforms-lite' ), 'learnMoreUrl' => wpforms_utm_link( 'https://wpforms.com/features/wpforms-ai/', 'Builder - Settings', 'Learn more - AI Forms' . $for_lite ), 'inactiveAnswerTitle' => esc_html__( 'Go back to this version of the form', 'wpforms-lite' ), 'useForm' => esc_html__( 'Use This Form', 'wpforms-lite' ), 'placeholder' => esc_html__( 'What would you like to create?', 'wpforms-lite' ), 'waiting' => esc_html__( 'Just a minute...', 'wpforms-lite' ), 'errors' => [ 'default' => esc_html__( 'An error occurred while generating form.', 'wpforms-lite' ), 'rate_limit' => esc_html__( 'Sorry, you\'ve reached your daily limit for generating forms.', 'wpforms-lite' ), ], 'footer' => [ esc_html__( 'What do you think of the form I created for you? If you’re happy with it, you can use this form. Otherwise, make changes by entering additional prompts.', 'wpforms-lite' ), esc_html__( 'How’s that? Are you ready to use this form?', 'wpforms-lite' ), esc_html__( 'Does this look good? Are you ready to implement this form?', 'wpforms-lite' ), esc_html__( 'Is this what you had in mind? Are you satisfied with the results?', 'wpforms-lite' ), esc_html__( 'Happy with the form? Ready to move forward?', 'wpforms-lite' ), esc_html__( 'Is this form a good fit for your needs? Can we proceed?', 'wpforms-lite' ), esc_html__( 'Are you pleased with the outcome? Ready to use this form?', 'wpforms-lite' ), esc_html__( 'Does this form meet your expectations? Can we move on to the next step?', 'wpforms-lite' ), esc_html__( 'Is this form what you were envisioning? Are you ready to use it?', 'wpforms-lite' ), esc_html__( 'Satisfied with the form? Let\'s use it!', 'wpforms-lite' ), esc_html__( 'Does this form align with your goals? Are you ready to implement it?', 'wpforms-lite' ), esc_html__( 'Happy with the results? Let\'s put this form to work!', 'wpforms-lite' ), ], 'reasons' => [ 'default' => sprintf( wp_kses( /* translators: %1$s - Reload link class. */ __( '<a href="#" class="%1$s">Reload this window</a> and try again.', 'wpforms-lite' ), [ 'a' => [ 'href' => [], 'class' => [], ], ] ), 'wpforms-ai-chat-reload-link' ), 'rate_limit' => sprintf( wp_kses( /* translators: %s - WPForms contact support link. */ __( 'You may only generate forms 50 times per day. If you believe this is an error, <a href="%s" target="_blank" rel="noopener noreferrer">please contact WPForms support</a>.', 'wpforms-lite' ), [ 'a' => [ 'href' => [], 'target' => [], 'rel' => [], ], ] ), wpforms_utm_link( 'https://wpforms.com/account/support/', 'AI Feature' ) ), ], 'samplePrompts' => [ [ 'icon' => 'wpforms-ai-chat-sample-restaurant', 'title' => esc_html__( 'Restaurant customer satisfaction survey', 'wpforms-lite' ), ], [ 'icon' => 'wpforms-ai-chat-sample-ticket', 'title' => esc_html__( 'Online event registration', 'wpforms-lite' ), ], [ 'icon' => 'wpforms-ai-chat-sample-design', 'title' => esc_html__( 'Job application for a web designer', 'wpforms-lite' ), ], [ 'icon' => 'wpforms-ai-chat-sample-stop', 'title' => esc_html__( 'Cancellation survey for a subscription', 'wpforms-lite' ), ], [ 'icon' => 'wpforms-ai-chat-sample-pizza', 'title' => esc_html__( 'Takeout order for a pizza store', 'wpforms-lite' ), ], [ 'icon' => 'wpforms-ai-chat-sample-market', 'title' => esc_html__( 'Market vendor application', 'wpforms-lite' ), ], [ 'icon' => 'wpforms-ai-chat-sample-quiz-capitals', 'title' => esc_html__( 'How well do you know world capitals?', 'wpforms-lite' ), 'prompt' => esc_html__( 'Create a graded quiz on the topic of "How well do you know world capitals?" with 10 questions and 3 answers each. Randomize the choices. Collect the user\'s name and email address. Create 4 outcomes set to Graded Quiz type with appropriate text for each grade and utilize the available smart tags. The graded outcomes should be if Quiz Grade is A, if Quiz Grade is B, if Quiz Grade is C, if Quiz Grade is D, and if Quiz Grade is F.', 'wpforms-lite' ), ], [ 'icon' => 'wpforms-ai-chat-sample-quiz-learn', 'title' => esc_html__( 'What is your ideal learning style?', 'wpforms-lite' ), 'prompt' => esc_html__( 'Create a personality quiz on the topic of "What is your ideal learning style?" with 10 questions and 4 answers each. The personalities are Visual, Auditory, Reading/Writing, and Kinesthetic. Collect the user\'s name and email address. Create 4 outcomes set to Personality Quiz type with neutral text and utilize the available smart tags.', 'wpforms-lite' ), ], [ 'icon' => 'wpforms-ai-chat-sample-quiz-business', 'title' => esc_html__( 'How prepared are you to start a business?', 'wpforms-lite' ), 'prompt' => esc_html__( 'Create a weighted quiz on the topic of "How prepared are you to start a business?" with 10 questions and 3 answers each. Collect the user\'s name and email address. Create 3 outcomes set to Weighted Quiz type for greater than 74%, less than 75% and greater than 49%, and less than 50%.', 'wpforms-lite' ), ], ], ]; $user_id = get_current_user_id(); // Get the chat session stored in user meta. // phpcs:disable WordPress.Security.NonceVerification.Recommended if ( ! empty( $_GET['session'] ) ) { $session_id = sanitize_text_field( wp_unslash( $_GET['session'] ) ); $meta = get_user_meta( $user_id, 'wpforms_builder_ai_form_chat_' . $session_id, true ); } // phpcs:enable WordPress.Security.NonceVerification.Recommended // If we have the meta-data, add it to the strings. if ( ! empty( $meta ) ) { // Remove user meta after using it. delete_user_meta( $user_id, 'wpforms_builder_ai_form_chat_' . ( $session_id ?? '' ) ); $strings['forms']['chatHtml'] = $meta['chatHtml']; $strings['forms']['responseHistory'] = $meta['responseHistory']; } return $strings; } /** * Get required addons' data. * * @since 1.9.2 * * @return array */ private function get_required_addons_data(): array { // The addon installation procedure has floating issues in PHP < 7.4. // It's better to skip the installation in this case to avoid addon installation errors. if ( PHP_VERSION_ID < 70400 ) { return []; } $addons_obj = wpforms()->obj( 'addons' ); if ( ! $addons_obj ) { return []; } $data = []; // Get the URLs for the required addons. foreach ( FormsAjax::FORM_GENERATOR_REQUIRED_ADDONS as $slug ) { $addon = $addons_obj->get_addon( $slug ); $data[ $slug ] = $this->get_required_addon_data( $addon ); } return array_filter( $data ); } /** * Get required addon data. * * @since 1.9.9 * * @param array|mixed $addon Addon data. * * @return array|null */ private function get_required_addon_data( $addon ): ?array { if ( empty( $addon ) || // Exceptional case when `addons.json` is not loaded. // This means that addon is already installed and active. ( isset( $addon['status'] ) && $addon['status'] === 'active' ) || // This means that addon is not available in the current license. // We should skip in this case as it is impossible to install or activate the addon. ( isset( $addon['action'] ) && $addon['action'] === 'upgrade' ) ) { return null; } return [ 'url' => $addon['url'] ?? '', 'path' => $addon['path'] ?? '', ]; } /** * Get required addons action. * * @since 1.9.2 * * @param array $addons_data Addons data. * * @return string */ private function get_required_addons_action( array $addons_data ): string { if ( empty( $addons_data ) ) { return ''; } foreach ( $addons_data as $data ) { if ( ! empty( $data['url'] ) ) { return 'install'; } } return 'activate'; } /** * Get dismissed elements data. * * @since 1.9.2 * * @return array */ private function get_dismissed_elements(): array { $user_id = get_current_user_id(); // Dismissed elements. $dismissed = get_user_meta( $user_id, 'wpforms_dismissed', true ); return [ 'installAddons' => ! empty( $dismissed['edu-ai-forms-install-addons-modal'] ), 'previewNotice' => ! empty( $dismissed['edu-ai-forms-preview-addons-notice'] ), ]; } } Integrations/AI/Admin/Builder/FieldOption.php 0000644 00000006570 15174710275 0015067 0 ustar 00 <?php namespace WPForms\Integrations\AI\Admin\Builder; use WPForms\Integrations\LiteConnect\LiteConnect; /** * AI Field Option class. * * @since 1.9.1 */ class FieldOption { /** * Initialize. * * @since 1.9.1 */ public function init() { $this->hooks(); } /** * Register hooks. * * @since 1.9.1 */ private function hooks() { add_action( 'wpforms_field_option_ai_modal_button', [ $this, 'add_option' ], 10, 4 ); } /** * Add AI Modal button to the field options. * * @since 1.9.1 * * @param string|mixed $output HTML output. * @param array $field Field settings. * @param array $args Additional arguments. * @param object $wpforms_field WPForms_Field object. * * @return string * @noinspection PhpUnusedParameterInspection */ public function add_option( $output, array $field, array $args, $wpforms_field ): string { $type = $args['type'] ?? 'default'; $data = [ 'field-id' => $field['id'], ]; $classes = [ 'wpforms-btn-purple', 'wpforms-ai-modal-button', 'wpforms-ai-' . $type . '-button', empty( $field['dynamic_choices'] ) ? '' : 'wpforms-hidden', ]; $attrs = []; [ $classes, $data, $attrs ] = $this->maybe_disable_button( $classes, $data, $attrs ); $button = $wpforms_field->field_element( 'button', $field, [ 'slug' => 'ai_modal_button', 'value' => $args['value'] ?? esc_html__( 'Open AI Modal', 'wpforms-lite' ), 'class' => wpforms_sanitize_classes( $classes ), 'data' => $data, 'attrs' => $attrs, ], false ); return (string) $wpforms_field->field_element( 'row', $field, [ 'slug' => 'ai_modal_button', 'content' => $button, ], false ); } /** * Maybe disable the button and show modal. * * @since 1.9.1 * * @param array $classes Classes list. * @param array $data Data arguments list. * @param array $attrs Attributes list. * * @return array */ private function maybe_disable_button( array $classes, array $data, array $attrs ): array { $is_pro = wpforms()->is_pro(); // Pro, license is not active. if ( $is_pro && ! $this->is_license_active() ) { $classes[] = 'education-modal'; $classes[] = 'wpforms-prevent-default'; $data['action'] = 'license'; $data['field-name'] = 'AI Choices'; $data['utm-content'] = 'AI Choices'; return [ $classes, $data, $attrs ]; } // Lite, LC is not enabled. if ( ! $is_pro && ! LiteConnect::is_enabled() && LiteConnect::is_allowed() ) { $classes[] = 'enable-lite-connect-modal'; $classes[] = 'wpforms-prevent-default'; } // Lite, LC is not configured or not allowed. if ( ! $is_pro && ! LiteConnect::is_allowed() ) { $classes[] = 'wpforms-prevent-default'; $classes[] = 'wpforms-inactive'; $classes[] = 'wpforms-help-tooltip'; $attrs['title'] = esc_html__( 'WPForms AI is not available on local sites.', 'wpforms-lite' ); $data['tooltip-position'] = 'top'; } return [ $classes, $data, $attrs ]; } /** * Determine whether a license key is active. * * @since 1.9.1 * * @return bool */ private function is_license_active(): bool { $license = (array) get_option( 'wpforms_license', [] ); return ! empty( wpforms_get_license_key() ) && empty( $license['is_expired'] ) && empty( $license['is_disabled'] ) && empty( $license['is_invalid'] ); } } Integrations/AI/Admin/Builder/Enqueues.php 0000644 00000020624 15174710275 0014441 0 ustar 00 <?php // phpcs:disable Generic.Commenting.DocComment.MissingShort /** @noinspection PhpIllegalPsrClassPathInspection */ /** @noinspection AutoloadingIssuesInspection */ // phpcs:enable Generic.Commenting.DocComment.MissingShort namespace WPForms\Integrations\AI\Admin\Builder; /** * Enqueue assets on the Form Builder screen. * * @since 1.9.1 */ class Enqueues { /** * Initialize. * * @since 1.9.1 */ public function init(): void { $this->hooks(); } /** * Register hooks. * * @since 1.9.1 */ private function hooks(): void { add_action( 'wpforms_builder_enqueues', [ $this, 'enqueues' ] ); } /** * Enqueue styles and scripts. * * @since 1.9.1 * * @param string|null $view Current view (panel). * * @noinspection PhpMissingParamTypeInspection * @noinspection PhpUnusedParameterInspection */ public function enqueues( $view ): void { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found $this->enqueue_styles(); $this->enqueue_scripts(); } /** * Enqueue styles. * * @since 1.9.1 */ private function enqueue_styles(): void { $min = wpforms_get_min_suffix(); wp_enqueue_style( 'wpforms-ai-modal', WPFORMS_PLUGIN_URL . "assets/css/integrations/ai/modal{$min}.css", [], WPFORMS_VERSION ); wp_enqueue_style( 'wpforms-ai-chat-element', WPFORMS_PLUGIN_URL . "assets/css/integrations/ai/chat-element{$min}.css", [], WPFORMS_VERSION ); } /** * Enqueue scripts. * * @since 1.9.1 */ private function enqueue_scripts(): void { $min = wpforms_get_min_suffix(); wp_enqueue_script( 'wpforms-ai-dock', WPFORMS_PLUGIN_URL . "assets/js/integrations/ai/chat-element/wpforms-ai-dock{$min}.js", [], WPFORMS_VERSION, false ); wp_enqueue_script( 'wpforms-ai-modal', WPFORMS_PLUGIN_URL . "assets/js/integrations/ai/choices/wpforms-ai-modal{$min}.js", [ 'wpforms-ai-dock' ], WPFORMS_VERSION, false ); wp_enqueue_script( 'wpforms-ai-chat-element', WPFORMS_PLUGIN_URL . "assets/js/integrations/ai/chat-element/wpforms-ai-chat-element{$min}.js", [], WPFORMS_VERSION, false ); wp_localize_script( 'wpforms-ai-chat-element', 'wpforms_ai_chat_element', $this->get_localize_chat_data() ); } /** * Get chat localize data. * * @since 1.9.1 * * @return array */ private function get_localize_chat_data(): array { $min = wpforms_get_min_suffix(); $strings = [ 'ajaxurl' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'wpforms-ai-nonce' ), 'min' => wpforms_get_min_suffix(), 'dislike' => esc_html__( 'Bad response', 'wpforms-lite' ), 'refresh' => esc_html__( 'Clear chat history', 'wpforms-lite' ), 'btnYes' => esc_html__( 'Yes, Continue', 'wpforms-lite' ), 'btnCancel' => esc_html__( 'Cancel', 'wpforms-lite' ), 'confirm' => [ 'refreshTitle' => esc_html__( 'Clear Chat History', 'wpforms-lite' ), 'refreshMessage' => esc_html__( 'Are you sure you want to clear the AI chat history and start over?', 'wpforms-lite' ), ], 'errors' => [ 'default' => esc_html__( 'An error occurred.', 'wpforms-lite' ), 'network' => esc_html__( 'There appears to be a network error.', 'wpforms-lite' ), 'empty' => esc_html__( 'I\'m not sure what to do with that.', 'wpforms-lite' ), ], 'warnings' => [ 'prohibited_code' => esc_html__( 'Prohibited code has been removed.', 'wpforms-lite' ), ], 'reasons' => [ 'default' => esc_html__( 'Please try again.', 'wpforms-lite' ), 'empty' => esc_html__( 'Please try a different prompt. You might need to be more descriptive.', 'wpforms-lite' ), 'prohibited_code' => esc_html__( 'Only basic styling tags are permitted. All other code deemed unsafe has been removed.', 'wpforms-lite' ), ], 'choices' => $this->get_choices_chat_data(), 'actions' => [], // Additional actions for js/integrations/ai/modules/api.js. 'pinChat' => is_rtl() ? esc_html__( 'Dock to the Left', 'wpforms-lite' ) : esc_html__( 'Dock to the Right', 'wpforms-lite' ), 'unpinChat' => esc_html__( 'Open in Popup', 'wpforms-lite' ), 'close' => esc_html__( 'Close', 'wpforms-lite' ), ]; /** * Allows loading additional modules from other addons. * See wpforms-calculations/src/Admin/Builder.php as example. * Used in js/integrations/ai/wpforms-ai-chat-element.js. */ $strings['modules'] = [ [ 'name' => 'api', 'path' => "./modules/api{$min}.js", ], [ 'name' => 'text', 'path' => "./modules/helpers-text{$min}.js", ], [ 'name' => 'choices', 'path' => "./modules/helpers-choices{$min}.js", ], [ 'name' => 'forms', 'path' => "./modules/helpers-forms{$min}.js", ], ]; /** * Filters the AI chat localize strings. * * @since 1.9.2 * * @param array $strings Localize strings. */ return apply_filters( 'wpforms_integrations_ai_admin_builder_enqueues_localize_chat_strings', $strings ); } /** * Get choices chat data. * * @since 1.9.1 * * @return array * @noinspection HtmlUnknownTarget * @noinspection PackedHashtableOptimizationInspection */ private function get_choices_chat_data(): array { return [ 'title' => esc_html__( 'Generate Choices', 'wpforms-lite' ), 'description' => esc_html__( 'Describe the choices you would like to create or use one of the examples below to get started.', 'wpforms-lite' ), 'descrEndDot' => '.', 'footer' => wp_kses( __( '<strong>What do you think of these choices?</strong> If you’re happy with them, you can insert these choices, or make changes by entering additional prompts.', 'wpforms-lite' ), // phpcs:ignore WordPress.WP.I18n.NoHtmlWrappedStrings [ 'strong' => [], ] ), 'learnMore' => esc_html__( 'Learn More About WPForms AI', 'wpforms-lite' ), 'warning' => esc_html__( 'It looks like you have some existing choices in this field. If you generate new choices, your existing choices will be overwritten. You can simply close this window if you’d like to keep your existing choices.', 'wpforms-lite' ), 'placeholder' => esc_html__( 'What would you like to create?', 'wpforms-lite' ), 'waiting' => esc_html__( 'Just a minute...', 'wpforms-lite' ), 'insert' => esc_html__( 'Insert Choices', 'wpforms-lite' ), 'learnMoreUrl' => wpforms_utm_link( 'https://wpforms.com/features/wpforms-ai/', 'Builder - Settings', 'Learn more - AI Choices modal' ), 'errors' => [ 'default' => esc_html__( 'An error occurred while generating choices.', 'wpforms-lite' ), 'rate_limit' => esc_html__( 'Sorry, you\'ve reached your daily limit for generating choices.', 'wpforms-lite' ), ], 'reasons' => [ 'rate_limit' => sprintf( wp_kses( /* translators: %s - WPForms contact support link. */ __( 'You may only generate choices 50 times per day. If you believe this is an error, <a href="%s" target="_blank" rel="noopener noreferrer">please contact WPForms support</a>.', 'wpforms-lite' ), [ 'a' => [ 'href' => [], 'target' => [], 'rel' => [], ], ] ), wpforms_utm_link( 'https://wpforms.com/account/support/', 'AI Feature' ) ), ], 'warnings' => [ 'prohibited_code' => esc_html__( 'Prohibited code has been removed from your choices.', 'wpforms-lite' ), ], 'samplePrompts' => [ [ 'icon' => 'wpforms-ai-chat-flag', 'title' => esc_html__( 'american public holidays with dates in brackets', 'wpforms-lite' ), ], [ 'icon' => 'wpforms-ai-chat-clover', 'title' => esc_html__( 'provinces of canada ordered by population', 'wpforms-lite' ), ], [ 'icon' => 'wpforms-ai-chat-thumbs-up', 'title' => esc_html__( 'top 5 social networks in europe', 'wpforms-lite' ), ], [ 'icon' => 'wpforms-ai-chat-globe', 'title' => esc_html__( 'top 10 most spoken languages in the world', 'wpforms-lite' ), ], [ 'icon' => 'wpforms-ai-chat-palm', 'title' => esc_html__( 'top 20 most popular tropical travel destinations', 'wpforms-lite' ), ], [ 'icon' => 'wpforms-ai-chat-shop', 'title' => esc_html__( '30 household item categories for a marketplace', 'wpforms-lite' ), ], ], 'defaults' => [ '1' => esc_html__( 'First Choice', 'wpforms-lite' ), '2' => esc_html__( 'Second Choice', 'wpforms-lite' ), '3' => esc_html__( 'Third Choice', 'wpforms-lite' ), ], ]; } } Integrations/WPCode/WPCode.php 0000644 00000011552 15174710275 0012202 0 ustar 00 <?php namespace WPForms\Integrations\WPCode; use WPForms\Integrations\IntegrationInterface; /** * Route class for the API. * * @since 1.8.5 */ class WPCode implements IntegrationInterface { /** * WPCode lite download URL. * * @since 1.8.5 * * @var string */ public $lite_download_url = 'https://downloads.wordpress.org/plugin/insert-headers-and-footers.zip'; /** * Lite plugin slug. * * @since 1.8.5 * * @var string */ public $lite_plugin_slug = 'insert-headers-and-footers/ihaf.php'; /** * WPCode lite download URL. * * @since 1.8.5 * * @var string */ public $pro_plugin_slug = 'wpcode-premium/wpcode.php'; /** * Determine if the class is allowed to load. * * @since 1.8.5 * * @return bool * @noinspection PhpMissingReturnTypeInspection * @noinspection ReturnTypeCanBeDeclaredInspection */ public function allow_load() { return wpforms_is_admin_page( 'tools', 'wpcode' ); } /** * Load the class. * * @since 1.8.5 */ public function load() { $this->hooks(); } /** * Hooks. * * @since 1.8.5 */ private function hooks() { if ( ! is_admin() || wp_doing_ajax() || wp_doing_cron() ) { return; } add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_scripts' ], 20 ); } /** * Load the WPCode snippets for our desired username or return an empty array if not available. * * @since 1.8.5 * * @return array The snippets. */ public function load_wpforms_snippets(): array { $snippets = $this->get_placeholder_snippets(); if ( function_exists( 'wpcode_get_library_snippets_by_username' ) ) { $snippets = wpcode_get_library_snippets_by_username( 'wpforms' ); } // Sort by installed. uasort( $snippets, function ( $a, $b ) { return ( $b['installed'] <=> $a['installed'] ); } ); return $snippets; } /** * Checks if the plugin is installed, either the lite or premium version. * * @since 1.8.5 * * @return bool True if the plugin is installed. */ public function is_plugin_installed(): bool { return $this->is_pro_installed() || $this->is_lite_installed(); } /** * Is the pro plugin installed. * * @since 1.8.5 * * @return bool True if the pro plugin is installed. */ public function is_pro_installed(): bool { return array_key_exists( $this->pro_plugin_slug, get_plugins() ); } /** * Is the lite plugin installed. * * @since 1.8.5 * * @return bool True if the lite plugin is installed. */ public function is_lite_installed(): bool { return array_key_exists( $this->lite_plugin_slug, get_plugins() ); } /** * Basic check if the plugin is active by looking for the main function. * * @since 1.8.5 * * @return bool True if the plugin is active. */ public function is_plugin_active(): bool { return function_exists( 'wpcode' ); } /** * Get plugin version. * * @since 1.8.5 * * @return string */ public function plugin_version(): string { if ( $this->is_pro_installed() ) { return get_plugins()[ $this->pro_plugin_slug ]['Version']; } if ( $this->is_lite_installed() ) { return get_plugins()[ $this->lite_plugin_slug ]['Version']; } return ''; } /** * Get placeholder snippets if the WPCode snippets are not available. * * @since 1.8.5 * * @return array The placeholder snippets. */ private function get_placeholder_snippets(): array { $snippet_titles = [ 'Add Field Values for Dropdown, Checkboxes, and Multiple Choice', 'Allow Date Range Selection in Date Picker', 'Allow Multiple Dates Selection in Date Picker', 'Change CSV Export Delimiter', 'Change Position of v2 Invisible reCAPTCHA Badge', 'Change Sublabels for the Email Field', 'Create Additional Schemes for the Address Field', 'Change Sublabels for the Stripe Credit Card Field', 'Change the Submit Button Color', 'Defer the reCAPTCHA Script', 'Disable Enter Key in WPForms', 'Disable the Email Address Suggestion', ]; $placeholder_snippets = []; foreach ( $snippet_titles as $snippet_title ) { // Add placeholder install link so we show a button. $placeholder_snippets[] = [ 'title' => $snippet_title, 'install' => 'https://library.wpcode.com/', 'installed' => false, 'note' => 'Placeholder code snippet short description text.', ]; } return $placeholder_snippets; } /** * Enqueue assets. * * @since 1.8.5 */ public function enqueue_scripts() { $min = wpforms_get_min_suffix(); wp_enqueue_script( 'listjs', WPFORMS_PLUGIN_URL . 'assets/lib/list.min.js', [ 'jquery' ], '1.5.0', false ); wp_enqueue_script( 'wpforms-wpcode', WPFORMS_PLUGIN_URL . "assets/js/integrations/wpcode/wpcode{$min}.js", [ 'jquery', 'listjs' ], WPFORMS_VERSION, true ); wp_localize_script( 'wpforms-wpcode', 'wpformsWpcodeVars', [ 'installing_text' => __( 'Installing', 'wpforms-lite' ), ] ); } } Integrations/WPCode/RegisterLibrary.php 0000644 00000002062 15174710275 0014166 0 ustar 00 <?php namespace WPForms\Integrations\WPCode; use WPForms\Integrations\IntegrationInterface; /** * Register the WPCode library username. * * @since 1.8.5 */ class RegisterLibrary implements IntegrationInterface { /** * Determine if the class is allowed to load. * * @since 1.8.5 * * @return bool * @noinspection PhpMissingReturnTypeInspection * @noinspection ReturnTypeCanBeDeclaredInspection */ public function allow_load() { return is_admin(); } /** * Load the class. * * @since 1.8.5 */ public function load() { $this->hooks(); } /** * Hooks. * * @since 1.8.5 */ private function hooks() { add_action( 'plugins_loaded', [ $this, 'wpforms_register_wpcode_username' ], 20 ); } /** * Register a WPCode Library username so that it's loaded in the library inside the WPCode plugin. * * @since 1.8.5 */ public function wpforms_register_wpcode_username() { if ( ! function_exists( 'wpcode_register_library_username' ) ) { return; } wpcode_register_library_username( 'wpforms', 'WPForms' ); } } Integrations/UncannyAutomator/UncannyAutomator.php 0000644 00000012065 15174710275 0016556 0 ustar 00 <?php namespace WPForms\Integrations\UncannyAutomator; use WPForms\Integrations\IntegrationInterface; /** * UncannyAutomator class. * * @since 1.7.0 */ class UncannyAutomator implements IntegrationInterface { /** * Custom priority for a provider, that will affect loading/placement order. * * @since 1.7.0 * * @var int */ const PRIORITY = 15; /** * Unique provider slug. * * @since 1.7.0 * * @var string */ const SLUG = 'uncanny-automator'; /** * Translatable provider name. * * @since 1.7.0 * * @var string */ private $name; /** * Custom provider icon (logo). * * @since 1.7.0 * * @var string */ private $icon; /** * Indicate if current integration is allowed to load. * * @since 1.7.0 * * @return bool */ public function allow_load() { return ! function_exists( 'Automator' ); } /** * Load the integration. * * @since 1.7.0 */ public function load() { $this->name = esc_html__( 'Uncanny Automator', 'wpforms-lite' ); $this->icon = WPFORMS_PLUGIN_URL . 'assets/images/icon-provider-uncanny-automator.png'; $this->hooks(); } /** * Register all hooks. * * @since 1.7.0 */ private function hooks() { add_action( 'wpforms_providers_panel_sidebar', [ $this, 'display_sidebar' ], self::PRIORITY ); add_action( 'wpforms_providers_panel_content', [ $this, 'display_content' ], self::PRIORITY ); add_filter( 'automator_on_activate_redirect_to_dashboard', '__return_false' ); add_action( 'wpforms_plugin_activated', [ $this, 'update_source' ] ); } /** * Display content inside the panel sidebar area. * * @since 1.7.0 */ public function display_sidebar() { printf( '<a href="#" class="wpforms-panel-sidebar-section icon wpforms-panel-sidebar-section-%1$s" data-section="%1$s"> <img src="%2$s" alt="%4$s">%3$s<i class="fa fa-angle-right wpforms-toggle-arrow"></i> </a>', esc_attr( self::SLUG ), esc_url( $this->icon ), esc_html( $this->name ), esc_attr( $this->name ) ); } /** * Display content inside the panel area. * * @since 1.7.0 */ public function display_content() { $plugins = get_plugins(); $is_installed = ! empty( $plugins[ sprintf( '%1$s/%1$s.php', self::SLUG ) ] ); $button_label = $is_installed ? esc_html__( 'Activate Now', 'wpforms-lite' ) : esc_html__( 'Install Now', 'wpforms-lite' ); $learn_more_url = esc_url( add_query_arg( [ 'utm_source' => 'wpforms', 'utm_medium' => 'form_marketing', 'utm_content' => 'learn_more_btn_before_install', 'utm_r' => 150, ], 'https://automatorplugin.com/wpforms-automation/' ) ); ?> <div class="wpforms-panel-content-section wpforms-builder-provider wpforms-panel-content-section-<?php echo esc_attr( self::SLUG ); ?>" id="<?php echo esc_attr( self::SLUG ); ?>-provider" data-provider="<?php echo esc_attr( self::SLUG ); ?>"> <div class="wpforms-builder-provider-title wpforms-panel-content-section-title"> <?php echo esc_html( $this->name ); ?> <?php printf( '<button class="wpforms-builder-provider-title-add education-modal" data-name="%1$s" data-slug="%2$s" data-action="%3$s" data-path="%2$s/%2$s.php" data-type="plugin" data-url="https://downloads.wordpress.org/plugin/%2$s.zip" data-nonce="%4$s" data-hide-on-success="true">%5$s</button>', esc_attr( sprintf( /* translators: %s - plugin name. */ __( '%s plugin', 'wpforms-lite' ), $this->name ) ), esc_attr( self::SLUG ), $is_installed ? 'activate' : 'install', esc_attr( wp_create_nonce( 'wpforms-admin' ) ), esc_html( $button_label ) ); ?> </div> <div class="wpforms-builder-provider-connections-default"> <img src="<?php echo esc_url( $this->icon ); ?>" alt="<?php echo esc_attr( $this->name ); ?>"> <div class="wpforms-builder-provider-settings-default-content"> <h2><?php esc_html_e( 'Put Your WordPress Site on Autopilot', 'wpforms-lite' ); ?></h2> <p><?php esc_html_e( 'Build powerful automations that control what happens on form submission. Connect your forms to Google Sheets, Zoom, social media, membership plugins, elearning platforms, and more with Uncanny Automator.', 'wpforms-lite' ); ?></p> <p> <a href="<?php echo esc_url( $learn_more_url ); ?>" class="wpforms-btn wpforms-btn-md wpforms-btn-orange" target="_blank" rel="noopener noreferrer"> <?php esc_html_e( 'Learn More', 'wpforms-lite' ); ?> </a> </p> </div> </div> <div class="wpforms-builder-provider-body"> <div class="wpforms-provider-connections-wrap wpforms-clear"> <div class="wpforms-builder-provider-connections"></div> </div> </div> </div> <?php } /** * Update source. * * @since 1.7.0 * * @param string $plugin_base Path to the plugin file relative to the plugins' directory. */ public function update_source( $plugin_base ) { if ( sprintf( '%1$s/%1$s.php', self::SLUG ) !== $plugin_base ) { return; } update_option( 'uncannyautomator_source', 'wpforms' ); } } Integrations/SolidCentral/SolidCentral.php 0000644 00000001474 15174710275 0014710 0 ustar 00 <?php namespace WPForms\Integrations\SolidCentral; /** * Class SolidCentral. * * @since 1.9.2 */ class SolidCentral { /** * Do not allow SolidCentral to set WP_ADMIN to true. * * @since 1.9.2 * * @return void */ public function init() { if ( ! defined( 'ITHEMES_SYNC_SKIP_SET_IS_ADMIN_TO_TRUE' ) ) { // phpcs:ignore WPForms.Comments.PHPDocDefine.MissPHPDoc, WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound define( 'ITHEMES_SYNC_SKIP_SET_IS_ADMIN_TO_TRUE', true ); return; } if ( ! defined( 'WP_ADMIN' ) ) { // phpcs:ignore WPForms.Comments.PHPDocDefine.MissPHPDoc, WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound define( 'WP_ADMIN', false ); } // phpcs:enable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound } } litespeed-wp-plugin/lscwp_versions_v2 0000644 00000001537 15174710665 0014076 0 ustar 00 allowed { 6.3.0.1 6.2.0.1 6.1 6.0.0.1 5.7.0.1 5.6 5.5.1 5.4 5.3.3 5.2.1 4.6 3.6.4 2.9.9.2 1.9.1.1 } short { 6.3.0.1 6.3 6.2.0.1 6.2 6.1 6.0.0.1 6.0 5.x 4.x 3.x 2.x 1.x } old { 6.3.0.1 6.3 6.2.0.1 6.2 6.1 6.0.0.1 6.0 5.7.0.1 5.7 5.6 5.5.1 5.5 5.4 5.3.3 5.3.2 5.3.1 5.3 5.2.1 5.2 5.1 5.0.1 5.0.0.1 5.0 4.6 4.5.0.1 4.5 4.4.7 4.4.6 4.4.5 4.4.4 4.4.3 4.4.2 4.4.1 4.4 4.3 4.2 4.1 4.0 3.6.4 3.6.3 3.6.2 3.6.1 3.6 3.5.2 3.5.1 3.5.0.2 3.5.0.1 3.5 3.4.2 3.4.1 3.4 3.3.1 3.3 3.2.4 3.2.3.2 3.2.3.1 3.2.3 3.2.2 3.2.1 3.2 3.1 3.0.9 3.0.8.6 3.0.8.5 3.0.8.4 3.0.8.3 3.0.8.2 3.0.8.1 3.0.8 3.0.4 3.0.3 3.0.2 3.0.1 3.0 2.9.9.2 2.9.9.1 2.9.9 2.9.8.7 2.9.8.6 2.9.8.5 2.9.8.4 2.9.8.3 2.9.8.2 2.9.8.1 2.9.8 2.9.7.2 2.9.7.1 2.9.7 2.9.6 2.9.5 2.9.4.1 2.9.3 2.9.2 2.9.1 2.9 2.8.1 2.7.3 2.6.4.1 2.4.4 2.3.1 2.2.7 2.1.2 2.0 1.9.1.1 1.8.3 1.7.2 1.6.7 1.5 1.4 1.3.1.1 1.2.3.1 1.1.6 1.0.15 } class-jetpack-crm-data.php 0000644 00000004545 15174711622 0011505 0 ustar 00 <?php /** * Compatibility functions for the Jetpack CRM plugin. * * @since 9.0.0 * * @package automattic/jetpack */ namespace Automattic\Jetpack; use WP_Error; /** * Provides Jetpack CRM plugin data. */ class Jetpack_CRM_Data { const JETPACK_CRM_PLUGIN_SLUG = 'zero-bs-crm/ZeroBSCRM.php'; /** * Provides Jetpack CRM plugin data for use in the Contact Form block sidebar menu. * * @return array An array containing the Jetpack CRM plugin data. */ public function get_crm_data() { $plugins = Plugins_Installer::get_plugins(); // Set default values. $response = array( 'crm_installed' => false, 'crm_active' => false, 'crm_version' => null, 'jp_form_ext_enabled' => null, 'can_install_crm' => false, 'can_activate_crm' => false, 'can_activate_extension' => false, ); if ( isset( $plugins[ self::JETPACK_CRM_PLUGIN_SLUG ] ) ) { $response['crm_installed'] = true; $crm_data = $plugins[ self::JETPACK_CRM_PLUGIN_SLUG ]; $response['crm_active'] = $crm_data['active']; $response['crm_version'] = $crm_data['Version']; if ( $response['crm_active'] ) { if ( function_exists( 'zeroBSCRM_isExtensionInstalled' ) ) { $response['jp_form_ext_enabled'] = zeroBSCRM_isExtensionInstalled( 'jetpackforms' ); } } } $response['can_install_crm'] = $response['crm_installed'] ? false : current_user_can( 'install_plugins' ); $response['can_activate_crm'] = $response['crm_active'] ? false : current_user_can( 'activate_plugins' ); if ( $response['crm_active'] && function_exists( 'zeroBSCRM_extension_install_jetpackforms' ) ) { // phpcs:ignore WordPress.WP.Capabilities.Unknown $response['can_activate_extension'] = current_user_can( 'admin_zerobs_manage_options' ); } return $response; } /** * Activates Jetpack CRM's Jetpack Forms extension. This is used by a button in the Jetpack Contact Form * sidebar menu. * * @return true|WP_Error Returns true if activation is success, else returns a WP_Error object. */ public function activate_crm_jetpackforms_extension() { if ( function_exists( 'zeroBSCRM_extension_install_jetpackforms' ) ) { return zeroBSCRM_extension_install_jetpackforms(); } return new WP_Error( 'jp_forms_extension_activation_failed', esc_html__( 'The Jetpack Forms extension could not be activated.', 'jetpack' ) ); } } class-jetpack-script-data.php 0000644 00000002057 15174711622 0012224 0 ustar 00 <?php /** * Jetpack_Script_Data. * * Adds Jetpack-plugin-specific data to the consolidated JetpackScriptData object. * * @package automattic/jetpack */ namespace Automattic\Jetpack\Plugin; /** * Jetpack_Script_Data class. */ class Jetpack_Script_Data { /** * Configure script data. */ public static function configure() { add_filter( 'jetpack_admin_js_script_data', array( __CLASS__, 'set_admin_script_data' ), 10, 1 ); } /** * Add Jetpack-plugin-specific data to the consolidated JetpackScriptData object. * * @since 15.6 * * @param array $data The script data. * @return array */ public static function set_admin_script_data( $data ) { /** * Whether to show the Jetpack branding in editor panels (e.g., SEO, AI Assistant). * * @since 15.6 * * @param bool $show Whether to show the Jetpack editor panel branding. Defaults to true. */ $data['jetpack'] = array( 'flags' => array( 'showJetpackBranding' => (bool) apply_filters( 'jetpack_show_editor_panel_branding', true ), ), ); return $data; } } class-deprecate.php 0000644 00000015703 15174711622 0010330 0 ustar 00 <?php /** * Place to properly deprecate Jetpack features. * * @package automattic/jetpack */ namespace Automattic\Jetpack\Plugin; use Automattic\Jetpack\Assets; use Automattic\Jetpack\Redirect; use Automattic\Jetpack\Status\Host; /** * Place to properly deprecate Jetpack features. */ class Deprecate { /** * The singleton instance. * * @var Deprecate */ private static $instance; /** * An array of notices to display. * * @var array */ private $notices = array(); /** * Initialize the class. */ private function __construct() { // Modify the notices array to include the notices you want to display. // For more information, see /docs/deprecating-features.md. $this->notices = array( 'my-admin' => array( 'title' => __( "Retired feature: Jetpack's XYZ Feature", 'jetpack' ), 'message' => __( 'This feature is being retired and will be removed effective November, 2024. Please use the Classic Theme Helper plugin instead.', 'jetpack' ), 'link' => array( 'label' => __( 'Learn more', 'jetpack' ), 'url' => 'jetpack-support-xyz', ), 'show' => false, // 'show' is not required, but setting it to false will ensure that the notice will not be displayed. 'hide_in_woa' => true, // 'hide_in_woa' is not required, but setting it to true will ensure that the notice will not be displayed in the WoA admin (none will display in Simple regardless). ), ); $this->set_notices(); if ( $this->has_notices() ) { // We only want the notice to appear on the main WP Admin dashboard, which hooking into load-index.php will allow. add_action( 'load-index.php', function () { add_action( 'admin_notices', array( $this, 'render_admin_notices' ) ); } ); add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_scripts' ) ); add_filter( 'my_jetpack_red_bubble_notification_slugs', array( $this, 'add_my_jetpack_red_bubbles' ) ); } } /** * Create/get the singleton instance. * * @return static */ public static function instance() { if ( null === static::$instance ) { static::$instance = new static(); } return static::$instance; } /** * Enqueue the scripts. * * @return void */ public function enqueue_admin_scripts() { if ( ! $this->has_notices() ) { return; } if ( ! wp_script_is( 'jetpack-deprecate', 'registered' ) ) { wp_register_script( 'jetpack-deprecate', Assets::get_file_url_for_environment( '_inc/build/deprecate.min.js', '_inc/deprecate.js' ), array(), JETPACK__VERSION, true ); } wp_enqueue_script( 'jetpack-deprecate' ); wp_add_inline_script( 'jetpack-deprecate', 'window.noticeInfo = ' . wp_json_encode( $this->notices, JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ) . ';', 'before' ); } /** * Ensure the notices variable is properly formatted and includes the required suffix and show value. * * @return void */ private function set_notices() { $notices = array(); $required_id_suffix = '-deprecate-feature'; $host = new Host(); foreach ( $this->notices as $id => $notice ) { if ( $host->is_woa_site() && isset( $notice['hide_in_woa'] ) && true === $notice['hide_in_woa'] ) { continue; } if ( isset( $notice['show'] ) && false === $notice['show'] ) { continue; } if ( empty( $notice['title'] ) || empty( $notice['message'] ) || empty( $notice['link']['url'] ) ) { continue; } if ( empty( $notice['link']['label'] ) ) { $notice['link']['label'] = __( 'Learn more', 'jetpack' ); } if ( strpos( $id, $required_id_suffix ) === false ) { $id .= $required_id_suffix; } $notices[ $id ] = $notice; } $this->notices = $notices; } /** * Render deprecation notices for relevant features. * * @return void */ public function render_admin_notices() { foreach ( $this->notices as $id => $notice ) { if ( $this->show_feature_notice( $id ) ) { $support_url = Redirect::get_url( $notice['link']['url'] ); $this->render_notice( $id, '<div class="jetpack-deprecation-notice-container">' . '<div class="jetpack-deprecation-notice-svg">' . '<svg class="jetpack-deprecation-notice-icon gridicon gridicons-info-outline needs-offset" height="20" width="20" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" color="#000000">' . '<g><path d="M13 9h-2V7h2v2zm0 2h-2v6h2v-6zm-1-7c-4.41 0-8 3.59-8 8s3.59 8 8 8 8-3.59 8-8-3.59-8-8-8m0-2c5.523 0 10 4.477 10 10s-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2z"></path></g>' . '</svg>' . '</div>' . '<div class="jetpack-deprecation-notice-text">' . '<p class="jetpack-deprection-notice-title">' . esc_html( $notice['title'] ) . '</p>' . '<p>' . esc_html( $notice['message'] ) . '</p>' . '<a href="' . $support_url . '" target="_blank" class="jetpack-deprecation-notice-link"> ' . esc_html( $notice['link']['label'] ) . '</a>' . '<svg class="gridicons-external" height="14" width="14" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 20">' . '<g><path d="M19 13v6c0 1.105-.895 2-2 2H5c-1.105 0-2-.895-2-2V7c0-1.105.895-2 2-2h6v2H5v12h12v-6h2zM13 3v2h4.586l-7.793 7.793 1.414 1.414L19 6.414V11h2V3h-8z"></path></g>' . '</svg>' . '</div>' . '</div>' ); } } } /** * Add the deprecation notices to My Jetpack. * * @param array $slugs Already added bubbles. * * @return array */ public function add_my_jetpack_red_bubbles( $slugs ) { foreach ( $this->notices as $id => $notice ) { if ( $this->show_feature_notice( $id ) ) { $slugs[ $id ] = array( 'data' => array( 'text' => $notice['message'], 'title' => $notice['title'], 'link' => array( 'label' => esc_html( $notice['link']['label'] ), 'url' => Redirect::get_url( $notice['link']['url'] ), ), 'id' => $id, ), ); } } return $slugs; } /** * Render the notice. * * @param string $id The notice ID. * @param string $text The notice text. * * @return void */ private function render_notice( $id, $text ) { printf( '<div id="%1$s" class="notice notice-warning is-dismissible jetpack-deprecate-dismissible" style="border-left-color: #000000;">%2$s</div>', esc_html( $id ), $text // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Output already escaped in render_admin_notices ); } /** * Check if there are any notices to be displayed, so we wouldn't load unnecessary JS and run excessive hooks. * * @return bool */ private function has_notices() { foreach ( $this->notices as $id => $notice ) { if ( $this->show_feature_notice( $id ) ) { return true; } } return false; } /** * Check if the feature notice should be shown, based on the existence of the cookie. * * @param string $id The notice ID. * * @return bool */ private function show_feature_notice( $id ) { return empty( $_COOKIE['jetpack_deprecate_dismissed'][ $id ] ); } } class-tracking.php 0000644 00000015767 15174711622 0010210 0 ustar 00 <?php /** * Tracks class. * * @package automattic/jetpack */ namespace Automattic\Jetpack\Plugin; use Automattic\Jetpack\Connection\Manager as Connection_Manager; use Automattic\Jetpack\Tracking as Tracks; use IXR_Error; use WP_Error; use WP_User; /** * Tracks class. */ class Tracking { /** * Tracking object. * * @var Tracks * * @access private */ private $tracking; /** * Prevents the Tracking from being initialized more than once. * * @var bool */ private static $initialized = false; /** * Initialization function. */ public function init() { if ( static::$initialized ) { return; } static::$initialized = true; $this->tracking = new Tracks( 'jetpack' ); // For tracking stuff via js/ajax. add_action( 'admin_enqueue_scripts', array( $this->tracking, 'enqueue_tracks_scripts' ) ); add_action( 'jetpack_activate_module', array( $this, 'jetpack_activate_module' ), 1, 1 ); add_action( 'jetpack_deactivate_module', array( $this, 'jetpack_deactivate_module' ), 1, 1 ); add_action( 'jetpack_user_authorized', array( $this, 'jetpack_user_authorized' ) ); // Tracking XMLRPC server events. add_action( 'jetpack_xmlrpc_server_event', array( $this, 'jetpack_xmlrpc_server_event' ), 10, 4 ); // Track that we've begun verifying the previously generated secret. add_action( 'jetpack_verify_secrets_begin', array( $this, 'jetpack_verify_secrets_begin' ), 10, 2 ); add_action( 'jetpack_verify_secrets_success', array( $this, 'jetpack_verify_secrets_success' ), 10, 2 ); add_action( 'jetpack_verify_secrets_fail', array( $this, 'jetpack_verify_secrets_fail' ), 10, 3 ); add_action( 'jetpack_verify_api_authorization_request_error_double_encode', array( $this, 'jetpack_verify_api_authorization_request_error_double_encode' ) ); add_action( 'jetpack_connection_register_fail', array( $this, 'jetpack_connection_register_fail' ), 10, 2 ); add_action( 'jetpack_connection_register_success', array( $this, 'jetpack_connection_register_success' ) ); } /** * Track that a specific module has been activated. * * @access public * * @param string $module Module slug. */ public function jetpack_activate_module( $module ) { $this->tracking->record_user_event( 'module_activated', array( 'module' => $module ) ); } /** * Track that a specific module has been deactivated. * * @access public * * @param string $module Module slug. */ public function jetpack_deactivate_module( $module ) { $this->tracking->record_user_event( 'module_deactivated', array( 'module' => $module ) ); } /** * Track that the user has successfully received an auth token. * * @access public */ public function jetpack_user_authorized() { $user_id = get_current_user_id(); $anon_id = get_user_meta( $user_id, 'jetpack_tracks_anon_id', true ); if ( $anon_id ) { $this->tracking->record_user_event( '_aliasUser', array( 'anonId' => $anon_id ) ); delete_user_meta( $user_id, 'jetpack_tracks_anon_id' ); if ( ! headers_sent() ) { setcookie( 'tk_ai', 'expired', time() - 1000, COOKIEPATH, COOKIE_DOMAIN, is_ssl(), false ); // phpcs:ignore Jetpack.Functions.SetCookie -- Want this accessible. } } $connection_manager = new Connection_Manager(); $wpcom_user_data = $connection_manager->get_connected_user_data( $user_id ); if ( isset( $wpcom_user_data['ID'] ) ) { update_user_meta( $user_id, 'jetpack_tracks_wpcom_id', $wpcom_user_data['ID'] ); } $this->tracking->record_user_event( 'wpa_user_linked', array() ); } /** * Track that we've begun verifying the secrets. * * @access public * * @param string $action Type of secret (one of 'register', 'authorize', 'publicize'). * @param WP_User $user The user object. */ public function jetpack_verify_secrets_begin( $action, $user ) { $this->tracking->record_user_event( "jpc_verify_{$action}_begin", array(), $user ); } /** * Track that we've succeeded in verifying the secrets. * * @access public * * @param string $action Type of secret (one of 'register', 'authorize', 'publicize'). * @param WP_User $user The user object. */ public function jetpack_verify_secrets_success( $action, $user ) { $this->tracking->record_user_event( "jpc_verify_{$action}_success", array(), $user ); } /** * Track that we've failed verifying the secrets. * * @access public * * @param string $action Type of secret (one of 'register', 'authorize', 'publicize'). * @param WP_User $user The user object. * @param WP_Error $error Error object. */ public function jetpack_verify_secrets_fail( $action, $user, $error ) { $this->tracking->record_user_event( "jpc_verify_{$action}_fail", array( 'error_code' => $error->get_error_code(), 'error_message' => $error->get_error_message(), ), $user ); } /** * Track a failed login attempt. * * @deprecated 13.9 Method is not longer in use. */ public function wp_login_failed() { _deprecated_function( __METHOD__, '13.9' ); } /** * Track a connection failure at the registration step. * * @access public * * @param string|int $error The error code. * @param WP_Error $registered The error object. */ public function jetpack_connection_register_fail( $error, $registered ) { $this->tracking->record_user_event( 'jpc_register_fail', array( 'error_code' => $error, 'error_message' => $registered->get_error_message(), ) ); } /** * Track that the registration step of the connection has been successful. * * @access public * * @param string $from The 'from' GET parameter. */ public function jetpack_connection_register_success( $from ) { $this->tracking->record_user_event( 'jpc_register_success', array( 'from' => $from, ) ); } /** * Handles the jetpack_xmlrpc_server_event action that combines several types of events that * happen during request serving. * * @param String $action the action name, i.e., 'remote_authorize'. * @param String $stage the execution stage, can be 'begin', 'success', 'error', etc. * @param array|WP_Error|IXR_Error $parameters (optional) extra parameters to be passed to the tracked action. * @param WP_User $user (optional) the acting user. */ public function jetpack_xmlrpc_server_event( $action, $stage, $parameters = array(), $user = null ) { if ( is_wp_error( $parameters ) ) { $parameters = array( 'error_code' => $parameters->get_error_code(), 'error_message' => $parameters->get_error_message(), ); } elseif ( is_a( $parameters, IXR_Error::class ) ) { $parameters = array( 'error_code' => $parameters->code, 'error_message' => $parameters->message, ); } $this->tracking->record_user_event( 'jpc_' . $action . '_' . $stage, $parameters, $user ); } /** * Track that the site is incorrectly double-encoding redirects from http to https. * * @access public */ public function jetpack_verify_api_authorization_request_error_double_encode() { $this->tracking->record_user_event( 'error_double_encode' ); } } class-jetpack-modules-overrides.php 0000644 00000007132 15174711622 0013460 0 ustar 00 <?php /** * Special cases for overriding modules. * * @package automattic/jetpack */ /** * Provides methods for dealing with module overrides. * * @since 5.9.0 */ class Jetpack_Modules_Overrides { /** * Used to cache module overrides so that we minimize how many times we apply the * option_jetpack_active_modules filter. * * @var null|array */ private $overrides = null; /** * Clears the $overrides member used for caching. * * Since get_overrides() can be passed a falsey value to skip caching, this is probably * most useful for clearing cache between tests. * * @return void */ public function clear_cache() { $this->overrides = null; } /** * Returns true if there is a filter on the jetpack_active_modules option. * * @return bool Whether there is a filter on the jetpack_active_modules option. */ public function do_overrides_exist() { return ( has_filter( 'option_jetpack_active_modules' ) || has_filter( 'jetpack_active_modules' ) ); } /** * Gets the override for a given module. * * @param string $module_slug The module's slug. * @param boolean $use_cache Whether or not cached overrides should be used. * * @return bool|string False if no override for module. 'active' or 'inactive' if there is an override. */ public function get_module_override( $module_slug, $use_cache = true ) { $overrides = $this->get_overrides( $use_cache ); if ( ! isset( $overrides[ $module_slug ] ) ) { return false; } return $overrides[ $module_slug ]; } /** * Returns an array of module overrides where the key is the module slug and the value * is true if the module is forced on and false if the module is forced off. * * @param bool $use_cache Whether or not cached overrides should be used. * * @return array The array of module overrides. */ public function get_overrides( $use_cache = true ) { if ( $use_cache && $this->overrides !== null ) { return $this->overrides; } if ( ! $this->do_overrides_exist() ) { return array(); } $available_modules = Jetpack::get_available_modules(); /** * First, let's get all modules that have been forced on. */ /** This filter is documented in wp-includes/option.php */ $filtered = apply_filters( 'option_jetpack_active_modules', array() ); /** This filter is documented in class.jetpack.php */ $filtered = apply_filters( 'jetpack_active_modules', $filtered ); $forced_on = array_diff( $filtered, array() ); /** * Second, let's get all modules forced off. */ /** This filter is documented in wp-includes/option.php */ $filtered = apply_filters( 'option_jetpack_active_modules', $available_modules ); /** This filter is documented in class.jetpack.php */ $filtered = apply_filters( 'jetpack_active_modules', $filtered ); $forced_off = array_diff( $available_modules, $filtered ); /** * Last, build the return value. */ $return_value = array(); foreach ( $forced_on as $on ) { $return_value[ $on ] = 'active'; } foreach ( $forced_off as $off ) { $return_value[ $off ] = 'inactive'; } $this->overrides = $return_value; return $return_value; } /** * A reference to an instance of this class. * * @var Jetpack_Modules_Overrides */ private static $instance = null; /** * Returns the singleton instance of Jetpack_Modules_Overrides * * @return Jetpack_Modules_Overrides */ public static function instance() { if ( self::$instance === null ) { self::$instance = new Jetpack_Modules_Overrides(); } return self::$instance; } /** * Private construct to enforce singleton. */ private function __construct() { } }
| ver. 1.6 |
Github
|
.
| PHP 8.3.30 | Генерация страницы: 0.07 |
proxy
|
phpinfo
|
Настройка