diff --git a/.env.dist b/.env.dist new file mode 100644 index 0000000..132d497 --- /dev/null +++ b/.env.dist @@ -0,0 +1,8 @@ +WP_URL="http://test.pods.dev" +WP_ROOT_FOLDER="/tmp/wordpress" +DB_NAME=wordpress +DB_USER=wordpress +DB_HOST=localhost +DB_PASSWORD=password +DB_TABLE_PREFIX=wp_ +WP_DOMAIN="test.pods.dev" diff --git a/.env.docker b/.env.docker new file mode 100644 index 0000000..e474bb8 --- /dev/null +++ b/.env.docker @@ -0,0 +1,9 @@ +WP_URL="http://test.dev" +WP_ROOT_FOLDER="/app/public" +DB_NAME=wordpress_test +DB_USER=root +DB_HOST=localhost +DB_PASSWORD=root +DB_TABLE_PREFIX=wptests_ +WP_DOMAIN="test.dev" +TEST_REBUILD_DATA=true diff --git a/.gitattributes b/.gitattributes index 5e79f83..f737362 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,9 +1,16 @@ # Auto detect text files and perform LF normalization * text=auto + +# Ignore other files +/.env.* export-ignore +/codeception.* export-ignore +/composer.* export-ignore +/docs export-ignore +/phpcs.xml export-ignore +/README.md export-ignore /tests export-ignore -/sources export-ignore -kill-travis.php export-ignore -/assets export-ignore +/TESTS.md export-ignore +/vendor export-ignore # Custom for Visual Studio *.cs diff=csharp diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f53c496 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +vendor/ +composer.lock + +# Codeception stuff +codeception.yml +!tests/_data/dump.sql +!tests/_data/test-data.sql +tests/_output/* +tests/*.suite.yml +tests/*.pem +tests/*.cert +tests/*.key diff --git a/TESTS.md b/TESTS.md new file mode 100644 index 0000000..6f73f8e --- /dev/null +++ b/TESTS.md @@ -0,0 +1,19 @@ +# Testing + +These tests utilize [Codeception](http://codeception.com/docs) and [WP Browser](https://github.com/lucatume/wp-browser). + +## Installation + +* `composer install` +* Create a new file `codeception.yml` with the env file you want to use: + +```yaml +params: + - .env.docker +``` + +## Running tests + +```shell +./bin/codecept run integration +``` diff --git a/codeception.dist.yml b/codeception.dist.yml new file mode 100644 index 0000000..fd656cc --- /dev/null +++ b/codeception.dist.yml @@ -0,0 +1,16 @@ +actor: Tester +paths: + tests: tests + log: tests/_output + data: tests/_data + support: tests/_support + envs: tests/_envs +settings: + bootstrap: _bootstrap.php + colors: true + memory_limit: 2048M +extensions: + enabled: + - Codeception\Extension\RunFailed +params: + - .env.dist diff --git a/codeception.example.yml b/codeception.example.yml new file mode 100644 index 0000000..6a72c44 --- /dev/null +++ b/codeception.example.yml @@ -0,0 +1,2 @@ +params: + - .env.docker diff --git a/composer.json b/composer.json index 772748a..02e8570 100644 --- a/composer.json +++ b/composer.json @@ -1,34 +1,50 @@ { - "name" : "pods-framework/pods-gravity-forms", - "description" : "Pods Gravity Forms Add-On", - "type" : "wordpress-plugin", - "keywords" : [ "pods", "wordpress", "gravityforms", "gravity-forms" ], - "homepage" : "https://github.com/pods-framework/pods-gravity-forms", - "license" : "GPL-2.0+", - "authors" : [ - { - "name" : "Pods Framework Team", - "email" : "contact@pods.io", - "homepage" : "http://pods.io/" - }, - { - "name" : "Scott Kingsley Clark", - "email" : "scott@pods.io", - "homepage" : "http://scottkclark.com/", - "role" : "Lead Developer" - }, - { - "name" : "Naomi C. Bush | gravity+", - "email" : "naomi@gravityplus.pro", - "homepage" : "https://gravityplus.pro" - } - ], - "support" : { - "issues" : "https://github.com/pods-framework/pods-gravity-forms/issues", - "source" : "https://github.com/pods-framework/pods-gravity-forms" - }, - "require" : { - "composer/installers" : "~1.0", - "php" : ">=5.3" - } + "name": "pods-framework/pods-gravity-forms", + "description": "Pods Gravity Forms Add-On", + "type": "wordpress-plugin", + "keywords": [ + "pods", + "wordpress", + "gravityforms", + "gravity-forms" + ], + "homepage": "https://github.com/pods-framework/pods-gravity-forms", + "license": "GPL-2.0+", + "authors": [ + { + "name": "Pods Framework Team", + "email": "contact@pods.io", + "homepage": "https://pods.io/" + }, + { + "name": "Scott Kingsley Clark", + "email": "scott@pods.io", + "homepage": "https://www.scottkclark.com/", + "role": "Lead Developer" + }, + { + "name": "Naomi C. Bush | gravity+", + "email": "naomi@gravityplus.pro", + "homepage": "https://gravityplus.pro", + "role": "Contributing Developer" + } + ], + "support": { + "issues": "https://github.com/pods-framework/pods-gravity-forms/issues", + "source": "https://github.com/pods-framework/pods-gravity-forms" + }, + "autoload": { + "psr-4": { + "Pods\\GF\\": "tests/integration/Pods/GF/", + "Pods\\GF\\Tests\\": "tests/_support/" + } + }, + "require": { + "composer/installers": "~1.0", + "php": ">=5.3" + }, + "require-dev": { + "lucatume/wp-browser": "^2.0", + "vlucas/phpdotenv": "^2.4" + } } \ No newline at end of file diff --git a/includes/Markdown.php b/includes/Markdown.php index 99e1b4a..05a22da 100644 --- a/includes/Markdown.php +++ b/includes/Markdown.php @@ -1,263 +1,417 @@ -# -# Original Markdown -# Copyright (c) 2004-2006 John Gruber -# -# +/** + * ID: markdown-syntax + * + * Name: Markdown Syntax + * + * Description: Integration with Markdown (http://michelf.com/projects/php-markdown/); Adds an option to enable + * Markdown syntax for Paragraph text fields. + * + * Version: 1.0 + * + * Category: Field Types + * + * @package Pods\Components + * @subpackage Markdown + */ + +if ( ! function_exists( 'Markdown' ) ) : + // + // Markdown - A text-to-HTML conversion tool for web writers + // + // PHP Markdown + // Copyright (c) 2004-2013 Michel Fortin + // + // + // Original Markdown + // Copyright (c) 2004-2006 John Gruber + // + // + define( 'MARKDOWN_VERSION', '1.0.2' ); + // 29 Nov 2013 + // + // Global default settings: + // + // Change to ">" for HTML output + @define( 'MARKDOWN_EMPTY_ELEMENT_SUFFIX', ' />' ); + + // Define the width of a tab for code blocks. + @define( 'MARKDOWN_TAB_WIDTH', 4 ); + + // + // WordPress settings: + // + // Change to false to remove Markdown from posts and/or comments. + @define( 'MARKDOWN_WP_POSTS', true ); + @define( 'MARKDOWN_WP_COMMENTS', true ); + + // Standard Function Interface ### + @define( 'MARKDOWN_PARSER_CLASS', 'Markdown_Parser' ); + + /** + * @param $text + * + * @return mixed + */ + function Markdown( $text ) { + + // + // Initialize the parser and return the result of its transform method. + // + // Setup static parser variable. + static $parser; + if ( ! isset( $parser ) ) { + $parser_class = MARKDOWN_PARSER_CLASS; + $parser = new $parser_class(); + } + // Transform text using parser. + return $parser->transform( $text ); + } -define( 'MARKDOWN_VERSION', "1.0.1o" ); # Sun 8 Jan 2012 + // WordPress Plugin Interface ### + if ( isset( $wp_version ) ) { + // More details about how it works here: + // + // Post content and excerpts + // - Remove WordPress paragraph generator. + // - Run Markdown on excerpt, then remove all tags. + // - Add paragraph tag around the excerpt, but remove it for the excerpt rss. + if ( MARKDOWN_WP_POSTS ) { + remove_filter( 'the_content', 'wpautop' ); + remove_filter( 'the_content_rss', 'wpautop' ); + remove_filter( 'the_excerpt', 'wpautop' ); + add_filter( 'the_content', 'Markdown', 6 ); + add_filter( 'the_content_rss', 'Markdown', 6 ); + add_filter( 'get_the_excerpt', 'Markdown', 6 ); + add_filter( 'get_the_excerpt', 'trim', 7 ); + add_filter( 'the_excerpt', 'mdwp_add_p' ); + add_filter( 'the_excerpt_rss', 'mdwp_strip_p' ); + + remove_filter( 'content_save_pre', 'balanceTags', 50 ); + remove_filter( 'excerpt_save_pre', 'balanceTags', 50 ); + add_filter( 'the_content', 'balanceTags', 50 ); + add_filter( 'get_the_excerpt', 'balanceTags', 9 ); + } + // Comments + // - Remove WordPress paragraph generator. + // - Remove WordPress auto-link generator. + // - Scramble important tags before passing them to the kses filter. + // - Run Markdown on excerpt then remove paragraph tags. + if ( MARKDOWN_WP_COMMENTS ) { + remove_filter( 'comment_text', 'wpautop', 30 ); + remove_filter( 'comment_text', 'make_clickable' ); + add_filter( 'pre_comment_content', 'Markdown', 6 ); + add_filter( 'pre_comment_content', 'mdwp_hide_tags', 8 ); + add_filter( 'pre_comment_content', 'mdwp_show_tags', 12 ); + add_filter( 'get_comment_text', 'Markdown', 6 ); + add_filter( 'get_comment_excerpt', 'Markdown', 6 ); + add_filter( 'get_comment_excerpt', 'mdwp_strip_p', 7 ); + + global $mdwp_hidden_tags, $mdwp_placeholders; + $mdwp_hidden_tags = explode( ' ', '

 
  • ' ); + $mdwp_placeholders = explode( ' ', str_rot13( 'pEj07ZbbBZ U1kqgh4w4p pre2zmeN6K QTi31t9pre ol0MP1jzJR ' . 'ML5IjmbRol ulANi1NsGY J7zRLJqPul liA8ctl16T K9nhooUHli' ) ); + } -# -# Global default settings: -# + /** + * @param $text + * + * @return mixed|string + */ + function mdwp_add_p( $text ) { -# Change to ">" for HTML output -@define( 'MARKDOWN_EMPTY_ELEMENT_SUFFIX', " />"); + if ( ! preg_match( '{^$|^<(p|ul|ol|dl|pre|blockquote)>}i', $text ) ) { + $text = '

    ' . $text . '

    '; + $text = preg_replace( '{\n{2,}}', "

    \n\n

    ", $text ); + } -# Define the width of a tab for code blocks. -@define( 'MARKDOWN_TAB_WIDTH', 4 ); + return $text; + } + /** + * @param $t + * + * @return mixed + */ + function mdwp_strip_p( $t ) { -# -# WordPress settings: -# + return preg_replace( '{}i', '', $t ); + } -# Change to false to remove Markdown from posts and/or comments. -@define( 'MARKDOWN_WP_POSTS', false ); -@define( 'MARKDOWN_WP_COMMENTS', false ); + /** + * @param $text + * + * @return mixed + */ + function mdwp_hide_tags( $text ) { + global $mdwp_hidden_tags, $mdwp_placeholders; + return str_replace( $mdwp_hidden_tags, $mdwp_placeholders, $text ); + } -### Standard Function Interface ### + /** + * @param $text + * + * @return mixed + */ + function mdwp_show_tags( $text ) { -@define( 'MARKDOWN_PARSER_CLASS', 'Markdown_Parser' ); + global $mdwp_hidden_tags, $mdwp_placeholders; -function Markdown($text) { -# -# Initialize the parser and return the result of its transform method. -# - # Setup static parser variable. - static $parser; - if (!isset($parser)) { - $parser_class = MARKDOWN_PARSER_CLASS; - $parser = new $parser_class; + return str_replace( $mdwp_placeholders, $mdwp_hidden_tags, $text ); + } + }//end if + + // bBlog Plugin Info ### + /** + * @return array + */ + function identify_modifier_markdown() { + + return array( + 'name' => 'markdown', + 'type' => 'modifier', + 'nicename' => 'Markdown', + 'description' => 'A text-to-HTML conversion tool for web writers', + 'authors' => 'Michel Fortin and John Gruber', + 'licence' => 'BSD-like', + 'version' => MARKDOWN_VERSION, + 'help' => 'Markdown syntax allows you to write using an easy-to-read, easy-to-write plain text format. Based on the original Perl version by John Gruber. More...', + ); } - # Transform text using parser. - return $parser->transform($text); -} + // Smarty Modifier Interface ### + /** + * @param $text + * + * @return mixed + */ + function smarty_modifier_markdown( $text ) { + return Markdown( $text ); + } -### bBlog Plugin Info ### - -function identify_modifier_markdown() { - return array( - 'name' => 'markdown', - 'type' => 'modifier', - 'nicename' => 'Markdown', - 'description' => 'A text-to-HTML conversion tool for web writers', - 'authors' => 'Michel Fortin and John Gruber', - 'licence' => 'BSD-like', - 'version' => MARKDOWN_VERSION, - 'help' => 'Markdown syntax allows you to write using an easy-to-read, easy-to-write plain text format. Based on the original Perl version by John Gruber. More...' - ); -} + // Textile Compatibility Mode ### + // Rename this file to "classTextile.php" and it can replace Textile everywhere. + if ( strcasecmp( substr( __FILE__, - 16 ), 'classTextile.php' ) == 0 ) { + // Try to include PHP SmartyPants. Should be in the same directory. + @include_once 'smartypants.php'; + + // Fake Textile class. It calls Markdown instead. + + /** + * Class Textile + */ + class Textile { + + /** + * @param $text + * @param string $lite + * @param string $encode + * + * @return mixed + */ + public function TextileThis( $text, $lite = '', $encode = '' ) { + + if ( $lite == '' && $encode == '' ) { + $text = Markdown( $text ); + } + if ( function_exists( 'SmartyPants' ) ) { + $text = SmartyPants( $text ); + } + return $text; + } -### Smarty Modifier Interface ### + // Fake restricted version: restrictions are not supported for now. -function smarty_modifier_markdown($text) { - return Markdown($text); -} + /** + * @param $text + * @param string $lite + * @param string $noimage + * + * @return mixed + */ + public function TextileRestricted( $text, $lite = '', $noimage = '' ) { + return $this->TextileThis( $text, $lite ); + } -### Textile Compatibility Mode ### + // Workaround to ensure compatibility with TextPattern 4.0.3. -# Rename this file to "classTextile.php" and it can replace Textile everywhere. + /** + * @param $text + * + * @return mixed + */ + public function blockLite( $text ) { -if (strcasecmp(substr(__FILE__, -16), "classTextile.php") == 0) { - # Try to include PHP SmartyPants. Should be in the same directory. - @include_once 'smartypants.php'; - # Fake Textile class. It calls Markdown instead. - /** - * @package Pods\Components - * @subpackage Markdown - */ - class Textile { - function TextileThis($text, $lite='', $encode='') { - if ($lite == '' && $encode == '') $text = Markdown($text); - if (function_exists('SmartyPants')) $text = SmartyPants($text); - return $text; - } - # Fake restricted version: restrictions are not supported for now. - function TextileRestricted($text, $lite='', $noimage='') { - return $this->TextileThis($text, $lite); + return $text; + } } - # Workaround to ensure compatibility with TextPattern 4.0.3. - function blockLite($text) { return $text; } - } -} + }//end if + // + // Markdown Parser Class + // + /** + * Class Markdown_Parser + */ + class Markdown_Parser { -# -# Markdown Parser Class -# -/** -* @package Pods\Components -* @subpackage Markdown -*/ + // Configuration Variables ### + // Change to ">" for HTML output. + public $empty_element_suffix = MARKDOWN_EMPTY_ELEMENT_SUFFIX; + public $tab_width = MARKDOWN_TAB_WIDTH; -class Markdown_Parser { + // Change to `true` to disallow markup or entities. + public $no_markup = false; + public $no_entities = false; - # Regex to match balanced [brackets]. - # Needed to insert a maximum bracked depth while converting to PHP. - var $nested_brackets_depth = 6; - var $nested_brackets_re; + // Predefined urls and titles for reference links and images. + public $predef_urls = array(); + public $predef_titles = array(); - var $nested_url_parenthesis_depth = 4; - var $nested_url_parenthesis_re; + // Parser Implementation ### + // Regex to match balanced [brackets]. + // Needed to insert a maximum bracked depth while converting to PHP. + public $nested_brackets_depth = 6; + public $nested_brackets_re; - # Table of hash values for escaped characters: - var $escape_chars = '\`*_{}[]()>#+-.!'; - var $escape_chars_re; + public $nested_url_parenthesis_depth = 4; + public $nested_url_parenthesis_re; - # Change to ">" for HTML output. - var $empty_element_suffix = MARKDOWN_EMPTY_ELEMENT_SUFFIX; - var $tab_width = MARKDOWN_TAB_WIDTH; + // Table of hash values for escaped characters: + public $escape_chars = '\`*_{}[]()>#+-.!'; + public $escape_chars_re; - # Change to `true` to disallow markup or entities. - var $no_markup = false; - var $no_entities = false; + /** + * Markdown_Parser constructor. + */ + public function __construct() { - # Predefined urls and titles for reference links and images. - var $predef_urls = array(); - var $predef_titles = array(); + // + // Constructor function. Initialize appropriate member variables. + // + $this->_initDetab(); + $this->prepareItalicsAndBold(); + $this->nested_brackets_re = str_repeat( '(?>[^\[\]]+|\[', $this->nested_brackets_depth ) . str_repeat( '\])*', $this->nested_brackets_depth ); - function Markdown_Parser() { - # - # Constructor function. Initialize appropriate member variables. - # - $this->_initDetab(); - $this->prepareItalicsAndBold(); + $this->nested_url_parenthesis_re = str_repeat( '(?>[^()\s]+|\(', $this->nested_url_parenthesis_depth ) . str_repeat( '(?>\)))*', $this->nested_url_parenthesis_depth ); - $this->nested_brackets_re = - str_repeat('(?>[^\[\]]+|\[', $this->nested_brackets_depth). - str_repeat('\])*', $this->nested_brackets_depth); + $this->escape_chars_re = '[' . preg_quote( $this->escape_chars ) . ']'; - $this->nested_url_parenthesis_re = - str_repeat('(?>[^()\s]+|\(', $this->nested_url_parenthesis_depth). - str_repeat('(?>\)))*', $this->nested_url_parenthesis_depth); + // Sort document, block, and span gamut in ascendent priority order. + asort( $this->document_gamut ); + asort( $this->block_gamut ); + asort( $this->span_gamut ); + } - $this->escape_chars_re = '['.preg_quote($this->escape_chars).']'; + // Internal hashes used during transformation. + public $urls = array(); + public $titles = array(); + public $html_hashes = array(); - # Sort document, block, and span gamut in ascendent priority order. - asort($this->document_gamut); - asort($this->block_gamut); - asort($this->span_gamut); - } + // Status flag to avoid invalid nesting. + public $in_anchor = false; + public function setup() { - # Internal hashes used during transformation. - var $urls = array(); - var $titles = array(); - var $html_hashes = array(); + // + // Called before the transformation process starts to setup parser + // states. + // + // Clear global hashes. + $this->urls = $this->predef_urls; + $this->titles = $this->predef_titles; + $this->html_hashes = array(); - # Status flag to avoid invalid nesting. - var $in_anchor = false; + $this->in_anchor = false; + } + public function teardown() { - function setup() { - # - # Called before the transformation process starts to setup parser - # states. - # - # Clear global hashes. - $this->urls = $this->predef_urls; - $this->titles = $this->predef_titles; - $this->html_hashes = array(); + // + // Called after the transformation process to clear any variable + // which may be taking up memory unnecessarly. + // + $this->urls = array(); + $this->titles = array(); + $this->html_hashes = array(); + } - $in_anchor = false; - } + /** + * @param $text + * + * @return string + */ + public function transform( $text ) { - function teardown() { - # - # Called after the transformation process to clear any variable - # which may be taking up memory unnecessarly. - # - $this->urls = array(); - $this->titles = array(); - $this->html_hashes = array(); - } + // + // Main function. Performs some preprocessing on the input text + // and pass it through the document gamut. + // + $this->setup(); + // Remove UTF-8 BOM and marker character in input, if present. + $text = preg_replace( '{^\xEF\xBB\xBF|\x1A}', '', $text ); - function transform($text) { - # - # Main function. Performs some preprocessing on the input text - # and pass it through the document gamut. - # - $this->setup(); + // Standardize line endings: + // DOS to Unix and Mac to Unix + $text = preg_replace( '{\r\n?}', "\n", $text ); - # Remove UTF-8 BOM and marker character in input, if present. - $text = preg_replace('{^\xEF\xBB\xBF|\x1A}', '', $text); + // Make sure $text ends with a couple of newlines: + $text .= "\n\n"; - # Standardize line endings: - # DOS to Unix and Mac to Unix - $text = preg_replace('{\r\n?}', "\n", $text); + // Convert all tabs to spaces. + $text = $this->detab( $text ); - # Make sure $text ends with a couple of newlines: - $text .= "\n\n"; + // Turn block-level HTML blocks into hash entries + $text = $this->hashHTMLBlocks( $text ); - # Convert all tabs to spaces. - $text = $this->detab($text); + // Strip any lines consisting only of spaces and tabs. + // This makes subsequent regexen easier to write, because we can + // match consecutive blank lines with /\n+/ instead of something + // contorted like /[ ]*\n+/ . + $text = preg_replace( '/^[ ]+$/m', '', $text ); - # Turn block-level HTML blocks into hash entries - $text = $this->hashHTMLBlocks($text); + // Run document gamut methods. + foreach ( $this->document_gamut as $method => $priority ) { + $text = $this->$method( $text ); + } - # Strip any lines consisting only of spaces and tabs. - # This makes subsequent regexen easier to write, because we can - # match consecutive blank lines with /\n+/ instead of something - # contorted like /[ ]*\n+/ . - $text = preg_replace('/^[ ]+$/m', '', $text); + $this->teardown(); - # Run document gamut methods. - foreach ($this->document_gamut as $method => $priority) { - $text = $this->$method($text); + return $text . "\n"; } - $this->teardown(); - - return $text . "\n"; - } - - var $document_gamut = array( - # Strip link definitions, store in hashes. - "stripLinkDefinitions" => 20, + public $document_gamut = array( + // Strip link definitions, store in hashes. + 'stripLinkDefinitions' => 20, - "runBasicBlockGamut" => 30, + 'runBasicBlockGamut' => 30, ); - - function stripLinkDefinitions($text) { - # - # Strips link definitions from text, stores the URLs and titles in - # hash references. - # - $less_than_tab = $this->tab_width - 1; - - # Link defs are in the form: ^[id]: url "optional title" - $text = preg_replace_callback('{ - ^[ ]{0,'.$less_than_tab.'}\[(.+)\][ ]?: # id = $1 + /** + * @param $text + * + * @return mixed + */ + public function stripLinkDefinitions( $text ) { + + // + // Strips link definitions from text, stores the URLs and titles in + // hash references. + // + $less_than_tab = $this->tab_width - 1; + + // Link defs are in the form: ^[id]: url "optional title" + $text = preg_replace_callback( '{ + ^[ ]{0,' . $less_than_tab . '}\[(.+)\][ ]?: # id = $1 [ ]* \n? # maybe *one* newline [ ]* @@ -277,45 +431,59 @@ function stripLinkDefinitions($text) { [ ]* )? # title is optional (?:\n+|\Z) - }xm', - array(&$this, '_stripLinkDefinitions_callback'), - $text); - return $text; - } - function _stripLinkDefinitions_callback($matches) { - $link_id = strtolower($matches[1]); - $url = $matches[2] == '' ? $matches[3] : $matches[2]; - $this->urls[$link_id] = $url; - $this->titles[$link_id] =& $matches[4]; - return ''; # String that will replace the block - } + }xm', array( &$this, '_stripLinkDefinitions_callback' ), $text ); + + return $text; + } + /** + * @param $matches + * + * @return string + */ + public function _stripLinkDefinitions_callback( $matches ) { - function hashHTMLBlocks($text) { - if ($this->no_markup) return $text; - - $less_than_tab = $this->tab_width - 1; - - # Hashify HTML blocks: - # We only want to do this for block-level HTML tags, such as headers, - # lists, and tables. That's because we still want to wrap

    s around - # "paragraphs" that are wrapped in non-block-level tags, such as anchors, - # phrase emphasis, and spans. The list of tags we're looking for is - # hard-coded: - # - # * List "a" is made of tags which can be both inline or block-level. - # These will be treated block-level when the start tag is alone on - # its line, otherwise they're not matched here and will be taken as - # inline later. - # * List "b" is made of tags which are always block-level; - # - $block_tags_a_re = 'ins|del'; - $block_tags_b_re = 'p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|address|'. - 'script|noscript|form|fieldset|iframe|math'; - - # Regular expression for the content of a block tag. - $nested_tags_level = 4; - $attr = ' + $link_id = strtolower( $matches[1] ); + $url = $matches[2] == '' ? $matches[3] : $matches[2]; + $this->urls[ $link_id ] = $url; + $this->titles[ $link_id ] =& $matches[4]; + + return ''; + // String that will replace the block + } + + /** + * @param $text + * + * @return mixed + */ + public function hashHTMLBlocks( $text ) { + + if ( $this->no_markup ) { + return $text; + } + + $less_than_tab = $this->tab_width - 1; + + // Hashify HTML blocks: + // We only want to do this for block-level HTML tags, such as headers, + // lists, and tables. That's because we still want to wrap

    s around + // "paragraphs" that are wrapped in non-block-level tags, such as anchors, + // phrase emphasis, and spans. The list of tags we're looking for is + // hard-coded: + // + // * List "a" is made of tags which can be both inline or block-level. + // These will be treated block-level when the start tag is alone on + // its line, otherwise they're not matched here and will be taken as + // inline later. + // * List "b" is made of tags which are always block-level; + // + $block_tags_a_re = 'ins|del'; + $block_tags_b_re = 'p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|address|' . 'script|noscript|form|fieldset|iframe|math|svg|' . 'article|section|nav|aside|hgroup|header|footer|' . 'figure'; + + // Regular expression for the content of a block tag. + $nested_tags_level = 4; + $attr = ' (?> # optional tag attributes \s # starts with whitespace (?> @@ -329,40 +497,38 @@ function hashHTMLBlocks($text) { )* )? '; - $content = - str_repeat(' + $content = str_repeat( ' (?> [^<]+ # content without tag | <\2 # nested opening tag - '.$attr.' # attributes + ' . $attr . ' # attributes (?> /> | - >', $nested_tags_level). # end of opening tag - '.*?'. # last level nested tag content - str_repeat(' + >', $nested_tags_level ) . // end of opening tag + '.*?' . // last level nested tag content + str_repeat( ' # closing nested tag ) | <(?!/\2\s*> # other tags with a different name ) - )*', - $nested_tags_level); - $content2 = str_replace('\2', '\3', $content); - - # First, look for nested blocks, e.g.: - #

    - #
    - # tags for inner block must be indented. - #
    - #
    - # - # The outermost tags must start at the left margin for this to match, and - # the inner nested divs must be indented. - # We need to do this before the next, more liberal match, because the next - # match will start at the first `
    ` and stop at the first `
    `. - $text = preg_replace_callback('{(?> + )*', $nested_tags_level ); + $content2 = str_replace( '\2', '\3', $content ); + + // First, look for nested blocks, e.g.: + //
    + //
    + // tags for inner block must be indented. + //
    + //
    + // + // The outermost tags must start at the left margin for this to match, and + // the inner nested divs must be indented. + // We need to do this before the next, more liberal match, because the next + // match will start at the first `
    ` and stop at the first `
    `. + $text = preg_replace_callback( '{(?> (?> (?<=\n\n) # Starting after a blank line | # or @@ -373,20 +539,20 @@ function hashHTMLBlocks($text) { # Match from `\n` to `\n`, handling nested tags # in between. - [ ]{0,'.$less_than_tab.'} - <('.$block_tags_b_re.')# start tag = $2 - '.$attr.'> # attributes followed by > and \n - '.$content.' # content, support nesting + [ ]{0,' . $less_than_tab . '} + <(' . $block_tags_b_re . ')# start tag = $2 + ' . $attr . '> # attributes followed by > and \n + ' . $content . ' # content, support nesting # the matching end tag [ ]* # trailing spaces/tabs (?=\n+|\Z) # followed by a newline or end of document | # Special version for tags of group a. - [ ]{0,'.$less_than_tab.'} - <('.$block_tags_a_re.')# start tag = $3 - '.$attr.'>[ ]*\n # attributes followed by > - '.$content2.' # content, support nesting + [ ]{0,' . $less_than_tab . '} + <(' . $block_tags_a_re . ')# start tag = $3 + ' . $attr . '>[ ]*\n # attributes followed by > + ' . $content2 . ' # content, support nesting # the matching end tag [ ]* # trailing spaces/tabs (?=\n+|\Z) # followed by a newline or end of document @@ -394,16 +560,16 @@ function hashHTMLBlocks($text) { | # Special case just for
    . It was easier to make a special # case than to make the other regex more complicated. - [ ]{0,'.$less_than_tab.'} + [ ]{0,' . $less_than_tab . '} <(hr) # start tag = $2 - '.$attr.' # attributes + ' . $attr . ' # attributes /?> # the matching end tag [ ]* (?=\n{2,}|\Z) # followed by a blank line or end of document | # Special case for standalone HTML comments: - [ ]{0,'.$less_than_tab.'} + [ ]{0,' . $less_than_tab . '} (?s: ) @@ -412,7 +578,7 @@ function hashHTMLBlocks($text) { | # PHP and ASP-style processor instructions (hashBlock($text); - return "\n\n$key\n\n"; - } + )}Sxmi', array( &$this, '_hashHTMLBlocks_callback' ), $text ); + return $text; + } - function hashPart($text, $boundary = 'X') { - # - # Called whenever a tag must be hashed when a function insert an atomic - # element in the text stream. Passing $text to through this function gives - # a unique text-token which will be reverted back when calling unhash. - # - # The $boundary argument specify what character should be used to surround - # the token. By convension, "B" is used for block elements that needs not - # to be wrapped into paragraph tags at the end, ":" is used for elements - # that are word separators and "X" is used in the general case. - # - # Swap back any tag hash found in $text so we do not have to `unhash` - # multiple times at the end. - $text = $this->unhash($text); - - # Then hash the block. - static $i = 0; - $key = "$boundary\x1A" . ++$i . $boundary; - $this->html_hashes[$key] = $text; - return $key; # String that will replace the tag. - } + /** + * @param $matches + * + * @return string + */ + public function _hashHTMLBlocks_callback( $matches ) { + $text = $matches[1]; + $key = $this->hashBlock( $text ); - function hashBlock($text) { - # - # Shortcut function for hashPart with block-level boundaries. - # - return $this->hashPart($text, 'B'); - } + return "\n\n$key\n\n"; + } + /** + * @param $text + * @param string $boundary + * + * @return string + */ + public function hashPart( $text, $boundary = 'X' ) { + + // + // Called whenever a tag must be hashed when a function insert an atomic + // element in the text stream. Passing $text to through this function gives + // a unique text-token which will be reverted back when calling unhash. + // + // The $boundary argument specify what character should be used to surround + // the token. By convension, "B" is used for block elements that needs not + // to be wrapped into paragraph tags at the end, ":" is used for elements + // that are word separators and "X" is used in the general case. + // + // Swap back any tag hash found in $text so we do not have to `unhash` + // multiple times at the end. + $text = $this->unhash( $text ); + + // Then hash the block. + static $i = 0; + $key = "$boundary\x1A" . ++ $i . $boundary; + $this->html_hashes[ $key ] = $text; + + return $key; + // String that will replace the tag. + } - var $block_gamut = array( - # - # These are all the transformations that form block-level - # tags like paragraphs, headers, and list items. - # - "doHeaders" => 10, - "doHorizontalRules" => 20, + /** + * @param $text + * + * @return string + */ + public function hashBlock( $text ) { + + // + // Shortcut function for hashPart with block-level boundaries. + // + return $this->hashPart( $text, 'B' ); + } - "doLists" => 40, - "doCodeBlocks" => 50, - "doBlockQuotes" => 60, + public $block_gamut = array( + // + // These are all the transformations that form block-level + // tags like paragraphs, headers, and list items. + // + 'doHeaders' => 10, + 'doHorizontalRules' => 20, + + 'doLists' => 40, + 'doCodeBlocks' => 50, + 'doBlockQuotes' => 60, ); - function runBlockGamut($text) { - # - # Run block gamut tranformations. - # - # We need to escape raw HTML in Markdown source before doing anything - # else. This need to be done for each block, and not only at the - # begining in the Markdown function since hashed blocks can be part of - # list items and could have been indented. Indented blocks would have - # been seen as a code block in a previous pass of hashHTMLBlocks. - $text = $this->hashHTMLBlocks($text); - - return $this->runBasicBlockGamut($text); - } - - function runBasicBlockGamut($text) { - # - # Run block gamut tranformations, without hashing HTML blocks. This is - # useful when HTML blocks are known to be already hashed, like in the first - # whole-document pass. - # - foreach ($this->block_gamut as $method => $priority) { - $text = $this->$method($text); + /** + * @param $text + * + * @return string + */ + public function runBlockGamut( $text ) { + + // + // Run block gamut tranformations. + // + // We need to escape raw HTML in Markdown source before doing anything + // else. This need to be done for each block, and not only at the + // begining in the Markdown function since hashed blocks can be part of + // list items and could have been indented. Indented blocks would have + // been seen as a code block in a previous pass of hashHTMLBlocks. + $text = $this->hashHTMLBlocks( $text ); + + return $this->runBasicBlockGamut( $text ); } - # Finally form paragraph and restore hashed blocks. - $text = $this->formParagraphs($text); + /** + * @param $text + * + * @return string + */ + public function runBasicBlockGamut( $text ) { + + // + // Run block gamut tranformations, without hashing HTML blocks. This is + // useful when HTML blocks are known to be already hashed, like in the first + // whole-document pass. + // + foreach ( $this->block_gamut as $method => $priority ) { + $text = $this->$method( $text ); + } - return $text; - } + // Finally form paragraph and restore hashed blocks. + $text = $this->formParagraphs( $text ); + return $text; + } - function doHorizontalRules($text) { - # Do Horizontal Rules: - return preg_replace( - '{ + /** + * @param $text + * + * @return mixed + */ + public function doHorizontalRules( $text ) { + + // Do Horizontal Rules: + return preg_replace( '{ ^[ ]{0,3} # Leading space ([-*_]) # $1: First marker (?> # Repeated marker group @@ -522,72 +722,93 @@ function doHorizontalRules($text) { ){2,} # Group repeated at least twice [ ]* # Tailing spaces $ # End of line. - }mx', - "\n".$this->hashBlock("empty_element_suffix")."\n", - $text); - } - + }mx', "\n" . $this->hashBlock( "empty_element_suffix" ) . "\n", $text ); + } - var $span_gamut = array( - # - # These are all the transformations that occur *within* block-level - # tags like paragraphs, headers, and list items. - # - # Process character escapes, code spans, and inline HTML - # in one shot. - "parseSpan" => -30, - - # Process anchor and image tags. Images must come first, - # because ![foo][f] looks like an anchor. - "doImages" => 10, - "doAnchors" => 20, - - # Make links out of things like `` - # Must come after doAnchors, because you can use < and > - # delimiters in inline links like [this](). - "doAutoLinks" => 30, - "encodeAmpsAndAngles" => 40, - - "doItalicsAndBold" => 50, - "doHardBreaks" => 60, + public $span_gamut = array( + // + // These are all the transformations that occur *within* block-level + // tags like paragraphs, headers, and list items. + // + // Process character escapes, code spans, and inline HTML + // in one shot. + 'parseSpan' => - 30, + + // Process anchor and image tags. Images must come first, + // because ![foo][f] looks like an anchor. + 'doImages' => 10, + 'doAnchors' => 20, + + // Make links out of things like `` + // Must come after doAnchors, because you can use < and > + // delimiters in inline links like [this](). + 'doAutoLinks' => 30, + 'encodeAmpsAndAngles' => 40, + + 'doItalicsAndBold' => 50, + 'doHardBreaks' => 60, ); - function runSpanGamut($text) { - # - # Run span gamut tranformations. - # - foreach ($this->span_gamut as $method => $priority) { - $text = $this->$method($text); + /** + * @param $text + * + * @return mixed + */ + public function runSpanGamut( $text ) { + + // + // Run span gamut tranformations. + // + foreach ( $this->span_gamut as $method => $priority ) { + $text = $this->$method( $text ); + } + + return $text; } - return $text; - } + /** + * @param $text + * + * @return mixed + */ + public function doHardBreaks( $text ) { + // Do hard breaks: + return preg_replace_callback( '/ {2,}\n/', array( &$this, '_doHardBreaks_callback' ), $text ); + } - function doHardBreaks($text) { - # Do hard breaks: - return preg_replace_callback('/ {2,}\n/', - array(&$this, '_doHardBreaks_callback'), $text); - } - function _doHardBreaks_callback($matches) { - return $this->hashPart("empty_element_suffix\n"); - } + /** + * @param $matches + * + * @return string + */ + public function _doHardBreaks_callback( $matches ) { + return $this->hashPart( "empty_element_suffix\n" ); + } - function doAnchors($text) { - # - # Turn Markdown link shortcuts into XHTML tags. - # - if ($this->in_anchor) return $text; - $this->in_anchor = true; + /** + * @param $text + * + * @return mixed + */ + public function doAnchors( $text ) { + + // + // Turn Markdown link shortcuts into XHTML tags. + // + if ( $this->in_anchor ) { + return $text; + } + $this->in_anchor = true; - # - # First, handle reference-style links: [link text] [id] - # - $text = preg_replace_callback('{ + // + // First, handle reference-style links: [link text] [id] + // + $text = preg_replace_callback( '{ ( # wrap whole match in $1 \[ - ('.$this->nested_brackets_re.') # link text = $2 + (' . $this->nested_brackets_re . ') # link text = $2 \] [ ]? # one optional space @@ -597,23 +818,22 @@ function doAnchors($text) { (.*?) # id = $3 \] ) - }xs', - array(&$this, '_doAnchors_reference_callback'), $text); + }xs', array( &$this, '_doAnchors_reference_callback' ), $text ); - # - # Next, inline-style links: [link text](url "optional title") - # - $text = preg_replace_callback('{ + // + // Next, inline-style links: [link text](url "optional title") + // + $text = preg_replace_callback( '{ ( # wrap whole match in $1 \[ - ('.$this->nested_brackets_re.') # link text = $2 + (' . $this->nested_brackets_re . ') # link text = $2 \] \( # literal paren [ \n]* (?: <(.+?)> # href = $3 | - ('.$this->nested_url_parenthesis_re.') # href = $4 + (' . $this->nested_url_parenthesis_re . ') # href = $4 ) [ \n]* ( # $5 @@ -624,92 +844,110 @@ function doAnchors($text) { )? # title is optional \) ) - }xs', - array(&$this, '_doAnchors_inline_callback'), $text); - - # - # Last, handle reference-style shortcuts: [link text] - # These must come last in case you've also got [link text][1] - # or [link text](/foo) - # - $text = preg_replace_callback('{ + }xs', array( &$this, '_doAnchors_inline_callback' ), $text ); + + // + // Last, handle reference-style shortcuts: [link text] + // These must come last in case you've also got [link text][1] + // or [link text](/foo) + // + $text = preg_replace_callback( '{ ( # wrap whole match in $1 \[ ([^\[\]]+) # link text = $2; can\'t contain [ or ] \] ) - }xs', - array(&$this, '_doAnchors_reference_callback'), $text); + }xs', array( &$this, '_doAnchors_reference_callback' ), $text ); - $this->in_anchor = false; - return $text; - } - function _doAnchors_reference_callback($matches) { - $whole_match = $matches[1]; - $link_text = $matches[2]; - $link_id =& $matches[3]; - - if ($link_id == "") { - # for shortcut links like [this][] or [this]. - $link_id = $link_text; + $this->in_anchor = false; + + return $text; } - # lower-case and turn embedded newlines into spaces - $link_id = strtolower($link_id); - $link_id = preg_replace('{[ ]?\n}', ' ', $link_id); + /** + * @param $matches + * + * @return string + */ + public function _doAnchors_reference_callback( $matches ) { - if (isset($this->urls[$link_id])) { - $url = $this->urls[$link_id]; - $url = $this->encodeAttribute($url); + $whole_match = $matches[1]; + $link_text = $matches[2]; + $link_id =& $matches[3]; - $result = "titles[$link_id] ) ) { - $title = $this->titles[$link_id]; - $title = $this->encodeAttribute($title); - $result .= " title=\"$title\""; + if ( $link_id == '' ) { + // for shortcut links like [this][] or [this]. + $link_id = $link_text; } - $link_text = $this->runSpanGamut($link_text); - $result .= ">$link_text"; - $result = $this->hashPart($result); - } - else { - $result = $whole_match; - } - return $result; - } - function _doAnchors_inline_callback($matches) { - $whole_match = $matches[1]; - $link_text = $this->runSpanGamut($matches[2]); - $url = $matches[3] == '' ? $matches[4] : $matches[3]; - $title =& $matches[7]; - - $url = $this->encodeAttribute($url); - - $result = "encodeAttribute($title); - $result .= " title=\"$title\""; + // lower-case and turn embedded newlines into spaces + $link_id = strtolower( $link_id ); + $link_id = preg_replace( '{[ ]?\n}', ' ', $link_id ); + + if ( isset( $this->urls[ $link_id ] ) ) { + $url = $this->urls[ $link_id ]; + $url = $this->encodeAttribute( $url ); + + $result = "titles[ $link_id ] ) ) { + $title = $this->titles[ $link_id ]; + $title = $this->encodeAttribute( $title ); + $result .= " title=\"$title\""; + } + + $link_text = $this->runSpanGamut( $link_text ); + $result .= ">$link_text"; + $result = $this->hashPart( $result ); + } else { + $result = $whole_match; + } + + return $result; } - $link_text = $this->runSpanGamut($link_text); - $result .= ">$link_text"; + /** + * @param $matches + * + * @return string + */ + public function _doAnchors_inline_callback( $matches ) { - return $this->hashPart($result); - } + $whole_match = $matches[1]; + $link_text = $this->runSpanGamut( $matches[2] ); + $url = $matches[3] == '' ? $matches[4] : $matches[3]; + $title =& $matches[7]; + + $url = $this->encodeAttribute( $url ); + + $result = "encodeAttribute( $title ); + $result .= " title=\"$title\""; + } + $link_text = $this->runSpanGamut( $link_text ); + $result .= ">$link_text"; + + return $this->hashPart( $result ); + } - function doImages($text) { - # - # Turn Markdown image shortcuts into tags. - # - # - # First, handle reference-style labeled images: ![alt text][id] - # - $text = preg_replace_callback('{ + /** + * @param $text + * + * @return mixed + */ + public function doImages( $text ) { + + // + // Turn Markdown image shortcuts into tags. + // + // + // First, handle reference-style labeled images: ![alt text][id] + // + $text = preg_replace_callback( '{ ( # wrap whole match in $1 !\[ - ('.$this->nested_brackets_re.') # alt text = $2 + (' . $this->nested_brackets_re . ') # alt text = $2 \] [ ]? # one optional space @@ -720,17 +958,16 @@ function doImages($text) { \] ) - }xs', - array(&$this, '_doImages_reference_callback'), $text); - - # - # Next, handle inline images: ![alt text](url "optional title") - # Don't forget: encode * and _ - # - $text = preg_replace_callback('{ + }xs', array( &$this, '_doImages_reference_callback' ), $text ); + + // + // Next, handle inline images: ![alt text](url "optional title") + // Don't forget: encode * and _ + // + $text = preg_replace_callback( '{ ( # wrap whole match in $1 !\[ - ('.$this->nested_brackets_re.') # alt text = $2 + (' . $this->nested_brackets_re . ') # alt text = $2 \] \s? # One optional whitespace character \( # literal paren @@ -738,7 +975,7 @@ function doImages($text) { (?: <(\S*)> # src url = $3 | - ('.$this->nested_url_parenthesis_re.') # src url = $4 + (' . $this->nested_url_parenthesis_re . ') # src url = $4 ) [ \n]* ( # $5 @@ -749,127 +986,169 @@ function doImages($text) { )? # title is optional \) ) - }xs', - array(&$this, '_doImages_inline_callback'), $text); - - return $text; - } - function _doImages_reference_callback($matches) { - $whole_match = $matches[1]; - $alt_text = $matches[2]; - $link_id = strtolower($matches[3]); + }xs', array( &$this, '_doImages_inline_callback' ), $text ); - if ($link_id == "") { - $link_id = strtolower($alt_text); # for shortcut links like ![this][]. + return $text; } - $alt_text = $this->encodeAttribute($alt_text); - if (isset($this->urls[$link_id])) { - $url = $this->encodeAttribute($this->urls[$link_id]); - $result = "\"$alt_text\"";titles[$link_id])) { - $title = $this->titles[$link_id]; - $title = $this->encodeAttribute($title); - $result .= " title=\"$title\""; + /** + * @param $matches + * + * @return string + */ + public function _doImages_reference_callback( $matches ) { + + $whole_match = $matches[1]; + $alt_text = $matches[2]; + $link_id = strtolower( $matches[3] ); + + if ( $link_id == '' ) { + $link_id = strtolower( $alt_text ); + // for shortcut links like ![this][]. } - $result .= $this->empty_element_suffix; - $result = $this->hashPart($result); - } - else { - # If there's no such link ID, leave intact: - $result = $whole_match; - } - return $result; - } - function _doImages_inline_callback($matches) { - $whole_match = $matches[1]; - $alt_text = $matches[2]; - $url = $matches[3] == '' ? $matches[4] : $matches[3]; - $title =& $matches[7]; - - $alt_text = $this->encodeAttribute($alt_text); - $url = $this->encodeAttribute($url); - $result = "\"$alt_text\"";encodeAttribute($title); - $result .= " title=\"$title\""; # $title already quoted + $alt_text = $this->encodeAttribute( $alt_text ); + if ( isset( $this->urls[ $link_id ] ) ) { + $url = $this->encodeAttribute( $this->urls[ $link_id ] ); + $result = "\"$alt_text\"";titles[ $link_id ] ) ) { + $title = $this->titles[ $link_id ]; + $title = $this->encodeAttribute( $title ); + $result .= " title=\"$title\""; + } + $result .= $this->empty_element_suffix; + $result = $this->hashPart( $result ); + } else { + // If there's no such link ID, leave intact: + $result = $whole_match; + } + + return $result; } - $result .= $this->empty_element_suffix; - return $this->hashPart($result); - } + /** + * @param $matches + * + * @return string + */ + public function _doImages_inline_callback( $matches ) { + + $whole_match = $matches[1]; + $alt_text = $matches[2]; + $url = $matches[3] == '' ? $matches[4] : $matches[3]; + $title =& $matches[7]; + + $alt_text = $this->encodeAttribute( $alt_text ); + $url = $this->encodeAttribute( $url ); + $result = "\"$alt_text\"";encodeAttribute( $title ); + $result .= " title=\"$title\""; + // $title already quoted + } + $result .= $this->empty_element_suffix; + return $this->hashPart( $result ); + } - function doHeaders($text) { - # Setext-style headers: - # Header 1 - # ======== - # - # Header 2 - # -------- - # - $text = preg_replace_callback('{ ^(.+?)[ ]*\n(=+|-+)[ ]*\n+ }mx', - array(&$this, '_doHeaders_callback_setext'), $text); - - # atx-style headers: - # # Header 1 - # ## Header 2 - # ## Header 2 with closing hashes ## - # ... - # ###### Header 6 - # - $text = preg_replace_callback('{ + /** + * @param $text + * + * @return mixed + */ + public function doHeaders( $text ) { + + // Setext-style headers: + // Header 1 + // ======== + // + // Header 2 + // -------- + // + $text = preg_replace_callback( '{ ^(.+?)[ ]*\n(=+|-+)[ ]*\n+ }mx', array( + &$this, + '_doHeaders_callback_setext', + ), $text ); + + // atx-style headers: + // Header 1 + // Header 2 + // Header 2 with closing hashes ## + // ... + // Header 6 + // + $text = preg_replace_callback( '{ ^(\#{1,6}) # $1 = string of #\'s [ ]* (.+?) # $2 = Header text [ ]* \#* # optional closing #\'s (not counted) \n+ - }xm', - array(&$this, '_doHeaders_callback_atx'), $text); + }xm', array( &$this, '_doHeaders_callback_atx' ), $text ); - return $text; - } - function _doHeaders_callback_setext($matches) { - # Terrible hack to check we haven't found an empty list item. - if ($matches[2] == '-' && preg_match('{^-(?: |$)}', $matches[1])) - return $matches[0]; - - $level = $matches[2]{0} == '=' ? 1 : 2; - $block = "".$this->runSpanGamut($matches[1]).""; - return "\n" . $this->hashBlock($block) . "\n\n"; - } - function _doHeaders_callback_atx($matches) { - $level = strlen($matches[1]); - $block = "".$this->runSpanGamut($matches[2]).""; - return "\n" . $this->hashBlock($block) . "\n\n"; - } + return $text; + } + /** + * @param $matches + * + * @return string + */ + public function _doHeaders_callback_setext( $matches ) { - function doLists($text) { - # - # Form HTML ordered (numbered) and unordered (bulleted) lists. - # - $less_than_tab = $this->tab_width - 1; + // Terrible hack to check we haven't found an empty list item. + if ( $matches[2] == '-' && preg_match( '{^-(?: |$)}', $matches[1] ) ) { + return $matches[0]; + } - # Re-usable patterns to match list item bullets and number markers: - $marker_ul_re = '[*+-]'; - $marker_ol_re = '\d+[\.]'; - $marker_any_re = "(?:$marker_ul_re|$marker_ol_re)"; + $level = $matches[2]{0} == '=' ? 1 : 2; + $block = "" . $this->runSpanGamut( $matches[1] ) . ""; - $markers_relist = array( - $marker_ul_re => $marker_ol_re, - $marker_ol_re => $marker_ul_re, + return "\n" . $this->hashBlock( $block ) . "\n\n"; + } + + /** + * @param $matches + * + * @return string + */ + public function _doHeaders_callback_atx( $matches ) { + + $level = strlen( $matches[1] ); + $block = "" . $this->runSpanGamut( $matches[2] ) . ""; + + return "\n" . $this->hashBlock( $block ) . "\n\n"; + } + + /** + * @param $text + * + * @return mixed + */ + public function doLists( $text ) { + + // + // Form HTML ordered (numbered) and unordered (bulleted) lists. + // + $less_than_tab = $this->tab_width - 1; + + // Re-usable patterns to match list item bullets and number markers: + $marker_ul_re = '[*+-]'; + $marker_ol_re = '\d+[\.]'; + $marker_any_re = "(?:$marker_ul_re|$marker_ol_re)"; + + $markers_relist = array( + $marker_ul_re => $marker_ol_re, + $marker_ol_re => $marker_ul_re, ); - foreach ($markers_relist as $marker_re => $other_marker_re) { - # Re-usable pattern to match any entirel ul or ol list: - $whole_list_re = ' + foreach ( $markers_relist as $marker_re => $other_marker_re ) { + // Re-usable pattern to match any entirel ul or ol list: + $whole_list_re = ' ( # $1 = whole list ( # $2 - ([ ]{0,'.$less_than_tab.'}) # $3 = number of spaces - ('.$marker_re.') # $4 = first list item marker + ([ ]{0,' . $less_than_tab . '}) # $3 = number of spaces + (' . $marker_re . ') # $4 = first list item marker [ ]+ ) (?s:.+?) @@ -880,335 +1159,381 @@ function doLists($text) { (?=\S) (?! # Negative lookahead for another list item marker [ ]* - '.$marker_re.'[ ]+ + ' . $marker_re . '[ ]+ ) | (?= # Lookahead for another kind of list \n \3 # Must have the same indentation - '.$other_marker_re.'[ ]+ + ' . $other_marker_re . '[ ]+ ) ) ) - '; // mx - - # We use a different prefix before nested lists than top-level lists. - # See extended comment in _ProcessListItems(). - - if ($this->list_level) { - $text = preg_replace_callback('{ + '; + // mx + // We use a different prefix before nested lists than top-level lists. + // See extended comment in _ProcessListItems(). + if ( $this->list_level ) { + $text = preg_replace_callback( '{ ^ - '.$whole_list_re.' - }mx', - array(&$this, '_doLists_callback'), $text); - } - else { - $text = preg_replace_callback('{ + ' . $whole_list_re . ' + }mx', array( &$this, '_doLists_callback' ), $text ); + } else { + $text = preg_replace_callback( '{ (?:(?<=\n)\n|\A\n?) # Must eat the newline - '.$whole_list_re.' - }mx', - array(&$this, '_doLists_callback'), $text); - } + ' . $whole_list_re . ' + }mx', array( &$this, '_doLists_callback' ), $text ); + } + }//end foreach + + return $text; } - return $text; - } - function _doLists_callback($matches) { - # Re-usable patterns to match list item bullets and number markers: - $marker_ul_re = '[*+-]'; - $marker_ol_re = '\d+[\.]'; - $marker_any_re = "(?:$marker_ul_re|$marker_ol_re)"; + /** + * @param $matches + * + * @return string + */ + public function _doLists_callback( $matches ) { - $list = $matches[1]; - $list_type = preg_match("/$marker_ul_re/", $matches[4]) ? "ul" : "ol"; + // Re-usable patterns to match list item bullets and number markers: + $marker_ul_re = '[*+-]'; + $marker_ol_re = '\d+[\.]'; + $marker_any_re = "(?:$marker_ul_re|$marker_ol_re)"; - $marker_any_re = ( $list_type == "ul" ? $marker_ul_re : $marker_ol_re ); + $list = $matches[1]; + $list_type = preg_match( "/$marker_ul_re/", $matches[4] ) ? 'ul' : 'ol'; - $list .= "\n"; - $result = $this->processListItems($list, $marker_any_re); + $marker_any_re = ( $list_type == 'ul' ? $marker_ul_re : $marker_ol_re ); - $result = $this->hashBlock("<$list_type>\n" . $result . ""); - return "\n". $result ."\n\n"; - } + $list .= "\n"; + $result = $this->processListItems( $list, $marker_any_re ); + + $result = $this->hashBlock( "<$list_type>\n" . $result . "" ); + + return "\n" . $result . "\n\n"; + } - var $list_level = 0; - - function processListItems($list_str, $marker_any_re) { - # - # Process the contents of a single ordered or unordered list, splitting it - # into individual list items. - # - # The $this->list_level global keeps track of when we're inside a list. - # Each time we enter a list, we increment it; when we leave a list, - # we decrement. If it's zero, we're not in a list anymore. - # - # We do this because when we're not inside a list, we want to treat - # something like this: - # - # I recommend upgrading to version - # 8. Oops, now this line is treated - # as a sub-list. - # - # As a single paragraph, despite the fact that the second line starts - # with a digit-period-space sequence. - # - # Whereas when we're inside a list (or sub-list), that line will be - # treated as the start of a sub-list. What a kludge, huh? This is - # an aspect of Markdown's syntax that's hard to parse perfectly - # without resorting to mind-reading. Perhaps the solution is to - # change the syntax rules such that sub-lists must start with a - # starting cardinal number; e.g. "1." or "a.". - - $this->list_level++; - - # trim trailing blank lines: - $list_str = preg_replace("/\n{2,}\\z/", "\n", $list_str); - - $list_str = preg_replace_callback('{ + public $list_level = 0; + + /** + * @param $list_str + * @param $marker_any_re + * + * @return mixed + */ + public function processListItems( $list_str, $marker_any_re ) { + + // + // Process the contents of a single ordered or unordered list, splitting it + // into individual list items. + // + // The $this->list_level global keeps track of when we're inside a list. + // Each time we enter a list, we increment it; when we leave a list, + // we decrement. If it's zero, we're not in a list anymore. + // + // We do this because when we're not inside a list, we want to treat + // something like this: + // + // I recommend upgrading to version + // 8. Oops, now this line is treated + // as a sub-list. + // + // As a single paragraph, despite the fact that the second line starts + // with a digit-period-space sequence. + // + // Whereas when we're inside a list (or sub-list), that line will be + // treated as the start of a sub-list. What a kludge, huh? This is + // an aspect of Markdown's syntax that's hard to parse perfectly + // without resorting to mind-reading. Perhaps the solution is to + // change the syntax rules such that sub-lists must start with a + // starting cardinal number; e.g. "1." or "a.". + $this->list_level ++; + + // trim trailing blank lines: + $list_str = preg_replace( "/\n{2,}\\z/", "\n", $list_str ); + + $list_str = preg_replace_callback( '{ (\n)? # leading line = $1 (^[ ]*) # leading whitespace = $2 - ('.$marker_any_re.' # list marker and space = $3 + (' . $marker_any_re . ' # list marker and space = $3 (?:[ ]+|(?=\n)) # space only required if item is not empty ) ((?s:.*?)) # list item text = $4 (?:(\n+(?=\n))|\n) # tailing blank line = $5 - (?= \n* (\z | \2 ('.$marker_any_re.') (?:[ ]+|(?=\n)))) - }xm', - array(&$this, '_processListItems_callback'), $list_str); + (?= \n* (\z | \2 (' . $marker_any_re . ') (?:[ ]+|(?=\n)))) + }xm', array( &$this, '_processListItems_callback' ), $list_str ); - $this->list_level--; - return $list_str; - } - function _processListItems_callback($matches) { - $item = $matches[4]; - $leading_line =& $matches[1]; - $leading_space =& $matches[2]; - $marker_space = $matches[3]; - $tailing_blank_line =& $matches[5]; - - if ($leading_line || $tailing_blank_line || - preg_match('/\n{2,}/', $item)) - { - # Replace marker with the appropriate whitespace indentation - $item = $leading_space . str_repeat(' ', strlen($marker_space)) . $item; - $item = $this->runBlockGamut($this->outdent($item)."\n"); - } - else { - # Recursion for sub-lists: - $item = $this->doLists($this->outdent($item)); - $item = preg_replace('/\n+$/', '', $item); - $item = $this->runSpanGamut($item); + $this->list_level --; + + return $list_str; } - return "
  • " . $item . "
  • \n"; - } + /** + * @param $matches + * + * @return string + */ + public function _processListItems_callback( $matches ) { + + $item = $matches[4]; + $leading_line =& $matches[1]; + $leading_space =& $matches[2]; + $marker_space = $matches[3]; + $tailing_blank_line =& $matches[5]; + + if ( $leading_line || $tailing_blank_line || preg_match( '/\n{2,}/', $item ) ) { + // Replace marker with the appropriate whitespace indentation + $item = $leading_space . str_repeat( ' ', strlen( $marker_space ) ) . $item; + $item = $this->runBlockGamut( $this->outdent( $item ) . "\n" ); + } else { + // Recursion for sub-lists: + $item = $this->doLists( $this->outdent( $item ) ); + $item = preg_replace( '/\n+$/', '', $item ); + $item = $this->runSpanGamut( $item ); + } + return '
  • ' . $item . "
  • \n"; + } - function doCodeBlocks($text) { - # - # Process Markdown `
    ` blocks.
    -	#
    -		$text = preg_replace_callback('{
    +		/**
    +		 * @param $text
    +		 *
    +		 * @return mixed
    +		 */
    +		public function doCodeBlocks( $text ) {
    +
    +			//
    +			// Process Markdown `
    ` blocks.
    +			//
    +			$text = preg_replace_callback( '{
     				(?:\n\n|\A\n?)
     				(	            # $1 = the code block -- one or more lines, starting with a space/tab
     				  (?>
    -					[ ]{'.$this->tab_width.'}  # Lines must start with a tab or a tab-width of spaces
    +					[ ]{' . $this->tab_width . '}  # Lines must start with a tab or a tab-width of spaces
     					.*\n+
     				  )+
     				)
    -				((?=^[ ]{0,'.$this->tab_width.'}\S)|\Z)	# Lookahead for non-space at line-start, or end of doc
    -			}xm',
    -			array(&$this, '_doCodeBlocks_callback'), $text);
    +				((?=^[ ]{0,' . $this->tab_width . '}\S)|\Z)	# Lookahead for non-space at line-start, or end of doc
    +			}xm', array( &$this, '_doCodeBlocks_callback' ), $text );
     
    -		return $text;
    -	}
    -	function _doCodeBlocks_callback($matches) {
    -		$codeblock = $matches[1];
    +			return $text;
    +		}
     
    -		$codeblock = $this->outdent($codeblock);
    -		$codeblock = htmlspecialchars($codeblock, ENT_NOQUOTES);
    +		/**
    +		 * @param $matches
    +		 *
    +		 * @return string
    +		 */
    +		public function _doCodeBlocks_callback( $matches ) {
     
    -		# trim leading newlines and trailing newlines
    -		$codeblock = preg_replace('/\A\n+|\n+\z/', '', $codeblock);
    +			$codeblock = $matches[1];
     
    -		$codeblock = "
    $codeblock\n
    "; - return "\n\n".$this->hashBlock($codeblock)."\n\n"; - } + $codeblock = $this->outdent( $codeblock ); + $codeblock = htmlspecialchars( $codeblock, ENT_NOQUOTES ); + // trim leading newlines and trailing newlines + $codeblock = preg_replace( '/\A\n+|\n+\z/', '', $codeblock ); - function makeCodeSpan($code) { - # - # Create a code span markup for $code. Called from handleSpanToken. - # - $code = htmlspecialchars(trim($code), ENT_NOQUOTES); - return $this->hashPart("$code"); - } + $codeblock = "
    $codeblock\n
    "; + + return "\n\n" . $this->hashBlock( $codeblock ) . "\n\n"; + } + + /** + * @param $code + * + * @return string + */ + public function makeCodeSpan( $code ) { + // + // Create a code span markup for $code. Called from handleSpanToken. + // + $code = htmlspecialchars( trim( $code ), ENT_NOQUOTES ); - var $em_relist = array( - '' => '(?:(? '(?<=\S|^)(? '(?<=\S|^)(?hashPart( "$code" ); + } + + public $em_relist = array( + '' => '(?:(? '(?<=\S|^)(? '(?<=\S|^)(? '(?:(? '(?<=\S|^)(? '(?<=\S|^)(? '(?:(? '(?<=\S|^)(? '(?<=\S|^)(? '(?:(? '(?<=\S|^)(? '(?<=\S|^)(? '(?:(? '(?<=\S|^)(? '(?<=\S|^)(?em_relist as $em => $em_re) { - foreach ($this->strong_relist as $strong => $strong_re) { - # Construct list of allowed token expressions. - $token_relist = array(); - if (isset($this->em_strong_relist["$em$strong"])) { - $token_relist[] = $this->em_strong_relist["$em$strong"]; - } - $token_relist[] = $em_re; - $token_relist[] = $strong_re; - - # Construct master expression from list. - $token_re = '{('. implode('|', $token_relist) .')}'; - $this->em_strong_prepared_relist["$em$strong"] = $token_re; - } - } - } + public $em_strong_prepared_relist; + + public function prepareItalicsAndBold() { + + // + // Prepare regular expressions for searching emphasis tokens in any + // context. + // + foreach ( $this->em_relist as $em => $em_re ) { + foreach ( $this->strong_relist as $strong => $strong_re ) { + // Construct list of allowed token expressions. + $token_relist = array(); + if ( isset( $this->em_strong_relist["$em$strong"] ) ) { + $token_relist[] = $this->em_strong_relist["$em$strong"]; + } + $token_relist[] = $em_re; + $token_relist[] = $strong_re; - function doItalicsAndBold($text) { - $token_stack = array(''); - $text_stack = array(''); - $em = ''; - $strong = ''; - $tree_char_em = false; - - while (1) { - # - # Get prepared regular expression for seraching emphasis tokens - # in current context. - # - $token_re = $this->em_strong_prepared_relist["$em$strong"]; - - # - # Each loop iteration search for the next emphasis token. - # Each token is then passed to handleSpanToken. - # - $parts = preg_split($token_re, $text, 2, PREG_SPLIT_DELIM_CAPTURE); - $text_stack[0] .= $parts[0]; - $token =& $parts[1]; - $text =& $parts[2]; - - if (empty($token)) { - # Reached end of text span: empty stack without emitting. - # any more emphasis. - while ($token_stack[0]) { - $text_stack[1] .= array_shift($token_stack); - $text_stack[0] .= array_shift($text_stack); + // Construct master expression from list. + $token_re = '{(' . implode( '|', $token_relist ) . ')}'; + $this->em_strong_prepared_relist["$em$strong"] = $token_re; } - break; } + } - $token_len = strlen($token); - if ($tree_char_em) { - # Reached closing marker while inside a three-char emphasis. - if ($token_len == 3) { - # Three-char closing marker, close em and strong. - array_shift($token_stack); - $span = array_shift($text_stack); - $span = $this->runSpanGamut($span); - $span = "$span"; - $text_stack[0] .= $this->hashPart($span); - $em = ''; - $strong = ''; - } else { - # Other closing marker: close one em or strong and - # change current token state to match the other - $token_stack[0] = str_repeat($token{0}, 3-$token_len); - $tag = $token_len == 2 ? "strong" : "em"; - $span = $text_stack[0]; - $span = $this->runSpanGamut($span); - $span = "<$tag>$span"; - $text_stack[0] = $this->hashPart($span); - $$tag = ''; # $$tag stands for $em or $strong - } - $tree_char_em = false; - } else if ($token_len == 3) { - if ($em) { - # Reached closing marker for both em and strong. - # Closing strong marker: - for ($i = 0; $i < 2; ++$i) { - $shifted_token = array_shift($token_stack); - $tag = strlen($shifted_token) == 2 ? "strong" : "em"; - $span = array_shift($text_stack); - $span = $this->runSpanGamut($span); - $span = "<$tag>$span"; - $text_stack[0] .= $this->hashPart($span); - $$tag = ''; # $$tag stands for $em or $strong + /** + * @param $text + * + * @return string + */ + public function doItalicsAndBold( $text ) { + + $token_stack = array( '' ); + $text_stack = array( '' ); + $em = ''; + $strong = ''; + $tree_char_em = false; + + while ( 1 ) { + // + // Get prepared regular expression for seraching emphasis tokens + // in current context. + // + $token_re = $this->em_strong_prepared_relist["$em$strong"]; + + // + // Each loop iteration search for the next emphasis token. + // Each token is then passed to handleSpanToken. + // + $parts = preg_split( $token_re, $text, 2, PREG_SPLIT_DELIM_CAPTURE ); + $text_stack[0] .= $parts[0]; + $token =& $parts[1]; + $text =& $parts[2]; + + if ( empty( $token ) ) { + // Reached end of text span: empty stack without emitting. + // any more emphasis. + while ( $token_stack[0] ) { + $text_stack[1] .= array_shift( $token_stack ); + $text_stack[0] .= array_shift( $text_stack ); } - } else { - # Reached opening three-char emphasis marker. Push on token - # stack; will be handled by the special condition above. - $em = $token{0}; - $strong = "$em$em"; - array_unshift($token_stack, $token); - array_unshift($text_stack, ''); - $tree_char_em = true; + break; } - } else if ($token_len == 2) { - if ($strong) { - # Unwind any dangling emphasis marker: - if (strlen($token_stack[0]) == 1) { - $text_stack[1] .= array_shift($token_stack); - $text_stack[0] .= array_shift($text_stack); + + $token_len = strlen( $token ); + if ( $tree_char_em ) { + // Reached closing marker while inside a three-char emphasis. + if ( $token_len == 3 ) { + // Three-char closing marker, close em and strong. + array_shift( $token_stack ); + $span = array_shift( $text_stack ); + $span = $this->runSpanGamut( $span ); + $span = "$span"; + $text_stack[0] .= $this->hashPart( $span ); + $em = ''; + $strong = ''; + } else { + // Other closing marker: close one em or strong and + // change current token state to match the other + $token_stack[0] = str_repeat( $token{0}, 3 - $token_len ); + $tag = $token_len == 2 ? 'strong' : 'em'; + $span = $text_stack[0]; + $span = $this->runSpanGamut( $span ); + $span = "<$tag>$span"; + $text_stack[0] = $this->hashPart( $span ); + $$tag = ''; + // $$tag stands for $em or $strong + }//end if + $tree_char_em = false; + } elseif ( $token_len == 3 ) { + if ( $em ) { + // Reached closing marker for both em and strong. + // Closing strong marker: + for ( $i = 0; $i < 2; ++ $i ) { + $shifted_token = array_shift( $token_stack ); + $tag = strlen( $shifted_token ) == 2 ? 'strong' : 'em'; + $span = array_shift( $text_stack ); + $span = $this->runSpanGamut( $span ); + $span = "<$tag>$span"; + $text_stack[0] .= $this->hashPart( $span ); + $$tag = ''; + // $$tag stands for $em or $strong + } + } else { + // Reached opening three-char emphasis marker. Push on token + // stack; will be handled by the special condition above. + $em = $token{0}; + $strong = "$em$em"; + array_unshift( $token_stack, $token ); + array_unshift( $text_stack, '' ); + $tree_char_em = true; + }//end if + } elseif ( $token_len == 2 ) { + if ( $strong ) { + // Unwind any dangling emphasis marker: + if ( strlen( $token_stack[0] ) == 1 ) { + $text_stack[1] .= array_shift( $token_stack ); + $text_stack[0] .= array_shift( $text_stack ); + } + // Closing strong marker: + array_shift( $token_stack ); + $span = array_shift( $text_stack ); + $span = $this->runSpanGamut( $span ); + $span = "$span"; + $text_stack[0] .= $this->hashPart( $span ); + $strong = ''; + } else { + array_unshift( $token_stack, $token ); + array_unshift( $text_stack, '' ); + $strong = $token; } - # Closing strong marker: - array_shift($token_stack); - $span = array_shift($text_stack); - $span = $this->runSpanGamut($span); - $span = "$span"; - $text_stack[0] .= $this->hashPart($span); - $strong = ''; } else { - array_unshift($token_stack, $token); - array_unshift($text_stack, ''); - $strong = $token; - } - } else { - # Here $token_len == 1 - if ($em) { - if (strlen($token_stack[0]) == 1) { - # Closing emphasis marker: - array_shift($token_stack); - $span = array_shift($text_stack); - $span = $this->runSpanGamut($span); - $span = "$span"; - $text_stack[0] .= $this->hashPart($span); - $em = ''; + // Here $token_len == 1 + if ( $em ) { + if ( strlen( $token_stack[0] ) == 1 ) { + // Closing emphasis marker: + array_shift( $token_stack ); + $span = array_shift( $text_stack ); + $span = $this->runSpanGamut( $span ); + $span = "$span"; + $text_stack[0] .= $this->hashPart( $span ); + $em = ''; + } else { + $text_stack[0] .= $token; + } } else { - $text_stack[0] .= $token; + array_unshift( $token_stack, $token ); + array_unshift( $text_stack, '' ); + $em = $token; } - } else { - array_unshift($token_stack, $token); - array_unshift($text_stack, ''); - $em = $token; - } - } + }//end if + }//end while + + return $text_stack[0]; } - return $text_stack[0]; - } + /** + * @param $text + * + * @return mixed + */ + public function doBlockQuotes( $text ) { - function doBlockQuotes($text) { - $text = preg_replace_callback('/ + $text = preg_replace_callback( '/ ( # Wrap whole match in $1 (?> ^[ ]*>[ ]? # ">" at the start of a line @@ -1217,140 +1542,174 @@ function doBlockQuotes($text) { \n* # blanks )+ ) - /xm', - array(&$this, '_doBlockQuotes_callback'), $text); - - return $text; - } - function _doBlockQuotes_callback($matches) { - $bq = $matches[1]; - # trim one level of quoting - trim whitespace-only lines - $bq = preg_replace('/^[ ]*>[ ]?|^[ ]+$/m', '', $bq); - $bq = $this->runBlockGamut($bq); # recurse - - $bq = preg_replace('/^/m', " ", $bq); - # These leading spaces cause problem with
     content,
    -		# so we need to fix that:
    -		$bq = preg_replace_callback('{(\s*
    .+?
    )}sx', - array(&$this, '_doBlockQuotes_callback2'), $bq); - - return "\n". $this->hashBlock("
    \n$bq\n
    ")."\n\n"; - } - function _doBlockQuotes_callback2($matches) { - $pre = $matches[1]; - $pre = preg_replace('/^ /m', '', $pre); - return $pre; - } + /xm', array( &$this, '_doBlockQuotes_callback' ), $text ); + return $text; + } - function formParagraphs($text) { - # - # Params: - # $text - string to process with html

    tags - # - # Strip leading and trailing lines: - $text = preg_replace('/\A\n+|\n+\z/', '', $text); - - $grafs = preg_split('/\n{2,}/', $text, -1, PREG_SPLIT_NO_EMPTY); - - # - # Wrap

    tags and unhashify HTML blocks - # - foreach ($grafs as $key => $value) { - if (!preg_match('/^B\x1A[0-9]+B$/', $value)) { - # Is a paragraph. - $value = $this->runSpanGamut($value); - $value = preg_replace('/^([ ]*)/', "

    ", $value); - $value .= "

    "; - $grafs[$key] = $this->unhash($value); - } - else { - # Is a block. - # Modify elements of @grafs in-place... - $graf = $value; - $block = $this->html_hashes[$graf]; - $graf = $block; -// if (preg_match('{ -// \A -// ( # $1 =
    tag -//
    ]* -// \b -// markdown\s*=\s* ([\'"]) # $2 = attr quote char -// 1 -// \2 -// [^>]* -// > -// ) -// ( # $3 = contents -// .* -// ) -// (
    ) # $4 = closing tag -// \z -// }xs', $block, $matches)) -// { -// list(, $div_open, , $div_content, $div_close) = $matches; -// -// # We can't call Markdown(), because that resets the hash; -// # that initialization code should be pulled into its own sub, though. -// $div_content = $this->hashHTMLBlocks($div_content); -// -// # Run document gamut methods on the content. -// foreach ($this->document_gamut as $method => $priority) { -// $div_content = $this->$method($div_content); -// } -// -// $div_open = preg_replace( -// '{\smarkdown\s*=\s*([\'"]).+?\1}', '', $div_open); -// -// $graf = $div_open . "\n" . $div_content . "\n" . $div_close; -// } - $grafs[$key] = $graf; - } + /** + * @param $matches + * + * @return string + */ + public function _doBlockQuotes_callback( $matches ) { + + $bq = $matches[1]; + // trim one level of quoting - trim whitespace-only lines + $bq = preg_replace( '/^[ ]*>[ ]?|^[ ]+$/m', '', $bq ); + $bq = $this->runBlockGamut( $bq ); + // recurse + $bq = preg_replace( '/^/m', ' ', $bq ); + // These leading spaces cause problem with
     content,
    +			// so we need to fix that:
    +			$bq = preg_replace_callback( '{(\s*
    .+?
    )}sx', array( &$this, '_doBlockQuotes_callback2' ), $bq ); + + return "\n" . $this->hashBlock( "
    \n$bq\n
    " ) . "\n\n"; } - return implode("\n\n", $grafs); - } + /** + * @param $matches + * + * @return mixed + */ + public function _doBlockQuotes_callback2( $matches ) { + $pre = $matches[1]; + $pre = preg_replace( '/^ /m', '', $pre ); - function encodeAttribute($text) { - # - # Encode text for a double-quoted HTML attribute. This function - # is *not* suitable for attributes enclosed in single quotes. - # - $text = $this->encodeAmpsAndAngles($text); - $text = str_replace('"', '"', $text); - return $text; - } + return $pre; + } + + /** + * @param $text + * + * @return string + */ + public function formParagraphs( $text ) { + + // + // Params: + // $text - string to process with html

    tags + // + // Strip leading and trailing lines: + $text = preg_replace( '/\A\n+|\n+\z/', '', $text ); + + $grafs = preg_split( '/\n{2,}/', $text, - 1, PREG_SPLIT_NO_EMPTY ); + + // + // Wrap

    tags and unhashify HTML blocks + // + foreach ( $grafs as $key => $value ) { + if ( ! preg_match( '/^B\x1A[0-9]+B$/', $value ) ) { + // Is a paragraph. + $value = $this->runSpanGamut( $value ); + $value = preg_replace( '/^([ ]*)/', '

    ', $value ); + $value .= '

    '; + $grafs[ $key ] = $this->unhash( $value ); + } else { + // Is a block. + // Modify elements of @grafs in-place... + $graf = $value; + $block = $this->html_hashes[ $graf ]; + $graf = $block; + // if (preg_match('{ + // \A + // ( # $1 =
    tag + //
    ]* + // \b + // markdown\s*=\s* ([\'"]) # $2 = attr quote char + // 1 + // \2 + // [^>]* + // > + // ) + // ( # $3 = contents + // .* + // ) + // (
    ) # $4 = closing tag + // \z + // }xs', $block, $matches)) + // { + // list(, $div_open, , $div_content, $div_close) = $matches; + // + // # We can't call Markdown(), because that resets the hash; + // # that initialization code should be pulled into its own sub, though. + // $div_content = $this->hashHTMLBlocks($div_content); + // + // # Run document gamut methods on the content. + // foreach ($this->document_gamut as $method => $priority) { + // $div_content = $this->$method($div_content); + // } + // + // $div_open = preg_replace( + // '{\smarkdown\s*=\s*([\'"]).+?\1}', '', $div_open); + // + // $graf = $div_open . "\n" . $div_content . "\n" . $div_close; + // } + $grafs[ $key ] = $graf; + }//end if + }//end foreach + + return implode( "\n\n", $grafs ); + } + /** + * @param $text + * + * @return mixed + */ + public function encodeAttribute( $text ) { - function encodeAmpsAndAngles($text) { - # - # Smart processing for ampersands and angle brackets that need to - # be encoded. Valid character entities are left alone unless the - # no-entities mode is set. - # - if ($this->no_entities) { - $text = str_replace('&', '&', $text); - } else { - # Ampersand-encoding based entirely on Nat Irons's Amputator - # MT plugin: - $text = preg_replace('/&(?!#?[xX]?(?:[0-9a-fA-F]+|\w+);)/', - '&', $text);; + // + // Encode text for a double-quoted HTML attribute. This function + // is *not* suitable for attributes enclosed in single quotes. + // + $text = $this->encodeAmpsAndAngles( $text ); + $text = str_replace( '"', '"', $text ); + + return $text; } - # Encode remaining <'s - $text = str_replace('<', '<', $text); - return $text; - } + /** + * @param $text + * + * @return mixed + */ + public function encodeAmpsAndAngles( $text ) { + + // + // Smart processing for ampersands and angle brackets that need to + // be encoded. Valid character entities are left alone unless the + // no-entities mode is set. + // + if ( $this->no_entities ) { + $text = str_replace( '&', '&', $text ); + } else { + // Ampersand-encoding based entirely on Nat Irons's Amputator + // MT plugin: + $text = preg_replace( '/&(?!#?[xX]?(?:[0-9a-fA-F]+|\w+);)/', '&', $text ); + } + // Encode remaining <'s + $text = str_replace( '<', '<', $text ); + + return $text; + } + /** + * @param $text + * + * @return mixed + */ + public function doAutoLinks( $text ) { - function doAutoLinks($text) { - $text = preg_replace_callback('{<((https?|ftp|dict):[^\'">\s]+)>}i', - array(&$this, '_doAutoLinks_url_callback'), $text); + $text = preg_replace_callback( '{<((https?|ftp|dict):[^\'">\s]+)>}i', array( + &$this, + '_doAutoLinks_url_callback', + ), $text ); - # Email addresses: - $text = preg_replace_callback('{ + // Email addresses: + $text = preg_replace_callback( '{ < (?:mailto:)? ( @@ -1367,295 +1726,384 @@ function doAutoLinks($text) { ) ) > - }xi', - array(&$this, '_doAutoLinks_email_callback'), $text); + }xi', array( &$this, '_doAutoLinks_email_callback' ), $text ); + $text = preg_replace_callback( '{<(tel:([^\'">\s]+))>}i', array( + &$this, + '_doAutoLinks_tel_callback', + ), $text ); - return $text; - } - function _doAutoLinks_url_callback($matches) { - $url = $this->encodeAttribute($matches[1]); - $link = "$url"; - return $this->hashPart($link); - } - function _doAutoLinks_email_callback($matches) { - $address = $matches[1]; - $link = $this->encodeEmailAddress($address); - return $this->hashPart($link); - } + return $text; + } + /** + * @param $matches + * + * @return string + */ + public function _doAutoLinks_tel_callback( $matches ) { - function encodeEmailAddress($addr) { - # - # Input: an email address, e.g. "foo@example.com" - # - # Output: the email address as a mailto link, with each character - # of the address encoded as either a decimal or hex entity, in - # the hopes of foiling most address harvesting spam bots. E.g.: - # - #

    foo@exampl - # e.com

    - # - # Based by a filter by Matthew Wickline, posted to BBEdit-Talk. - # With some optimizations by Milian Wolff. - # - $addr = "mailto:" . $addr; - $chars = preg_split('/(? $char) { - $ord = ord($char); - # Ignore non-ascii chars. - if ($ord < 128) { - $r = ($seed * (1 + $key)) % 100; # Pseudo-random function. - # roughly 10% raw, 45% hex, 45% dec - # '@' *must* be encoded. I insist. - if ($r > 90 && $char != '@') /* do nothing */; - else if ($r < 45) $chars[$key] = '&#x'.dechex($ord).';'; - else $chars[$key] = '&#'.$ord.';'; - } + $url = $this->encodeAttribute( $matches[1] ); + $tel = $this->encodeAttribute( $matches[2] ); + $link = "$tel"; + + return $this->hashPart( $link ); } - $addr = implode('', $chars); - $text = implode('', array_slice($chars, 7)); # text without `mailto:` - $addr = "$text"; + /** + * @param $matches + * + * @return string + */ + public function _doAutoLinks_url_callback( $matches ) { - return $addr; - } + $url = $this->encodeAttribute( $matches[1] ); + $link = "$url"; + + return $this->hashPart( $link ); + } + + /** + * @param $matches + * + * @return string + */ + public function _doAutoLinks_email_callback( $matches ) { + + $address = $matches[1]; + $link = $this->encodeEmailAddress( $address ); + + return $this->hashPart( $link ); + } + + /** + * @param $addr + * + * @return string + */ + public function encodeEmailAddress( $addr ) { + + // + // Input: an email address, e.g. "foo@example.com" + // + // Output: the email address as a mailto link, with each character + // of the address encoded as either a decimal or hex entity, in + // the hopes of foiling most address harvesting spam bots. E.g.: + // + //

    foo@exampl + // e.com

    + // + // Based by a filter by Matthew Wickline, posted to BBEdit-Talk. + // With some optimizations by Milian Wolff. + // + $addr = 'mailto:' . $addr; + $chars = preg_split( '/(? $char ) { + $ord = ord( $char ); + // Ignore non-ascii chars. + if ( $ord < 128 ) { + $r = ( $seed * ( 1 + $key ) ) % 100; + // Pseudo-random function. + // roughly 10% raw, 45% hex, 45% dec + // '@' *must* be encoded. I insist. + if ( $r > 90 && $char != '@' ) { /* do nothing */ + } elseif ( $r < 45 ) { + $chars[ $key ] = '&#x' . dechex( $ord ) . ';'; + } else { + $chars[ $key ] = '&#' . $ord . ';'; + } + } + } + + $addr = implode( '', $chars ); + $text = implode( '', array_slice( $chars, 7 ) ); + // text without `mailto:` + $addr = "$text"; + + return $addr; + } + /** + * @param $str + * + * @return string + */ + public function parseSpan( $str ) { - function parseSpan($str) { - # - # Take the string $str and parse it into tokens, hashing embeded HTML, - # escaped characters and handling code spans. - # - $output = ''; + // + // Take the string $str and parse it into tokens, hashing embeded HTML, + // escaped characters and handling code spans. + // + $output = ''; - $span_re = '{ + $span_re = '{ ( - \\\\'.$this->escape_chars_re.' + \\\\' . $this->escape_chars_re . ' | (?no_markup ? '' : ' + ' . ( $this->no_markup ? '' : ' | # comment | <\?.*?\?> | <%.*?%> # processing instruction | - <[/!$]?[-a-zA-Z0-9:_]+ # regular tags + <[!$]?[-a-zA-Z0-9:_]+ # regular tags (?> \s (?>[^"\'>]+|"[^"]*"|\'[^\']*\')* )? > - ').' + | + <[-a-zA-Z0-9:_]+\s*/> # xml-style empty tag + | + # closing tag + ' ) . ' ) }xs'; - while (1) { - # - # Each loop iteration seach for either the next tag, the next - # openning code span marker, or the next escaped character. - # Each token is then passed to handleSpanToken. - # - $parts = preg_split($span_re, $str, 2, PREG_SPLIT_DELIM_CAPTURE); - - # Create token from text preceding tag. - if ($parts[0] != "") { - $output .= $parts[0]; - } + while ( 1 ) { + // + // Each loop iteration seach for either the next tag, the next + // openning code span marker, or the next escaped character. + // Each token is then passed to handleSpanToken. + // + $parts = preg_split( $span_re, $str, 2, PREG_SPLIT_DELIM_CAPTURE ); + + // Create token from text preceding tag. + if ( $parts[0] != '' ) { + $output .= $parts[0]; + } - # Check if we reach the end. - if (isset($parts[1])) { - $output .= $this->handleSpanToken($parts[1], $parts[2]); - $str = $parts[2]; - } - else { - break; - } + // Check if we reach the end. + if ( isset( $parts[1] ) ) { + $output .= $this->handleSpanToken( $parts[1], $parts[2] ); + $str = $parts[2]; + } else { + break; + } + }//end while + + return $output; } - return $output; - } + /** + * @param $token + * @param $str + * + * @return string + */ + public function handleSpanToken( $token, &$str ) { + + // + // Handle $token provided by parseSpan by determining its nature and + // returning the corresponding value that should replace it. + // + switch ( $token{0} ) { + case '\\': + return $this->hashPart( '&#' . ord( $token{1} ) . ';' ); + case '`': + // Search for end marker in remaining text. + if ( preg_match( '/^(.*?[^`])' . preg_quote( $token ) . '(?!`)(.*)$/sm', $str, $matches ) ) { + $str = $matches[2]; + $codespan = $this->makeCodeSpan( $matches[1] ); + + return $this->hashPart( $codespan ); + } + return $token; + // return as text since no ending marker found. + default: + return $this->hashPart( $token ); + } + } - function handleSpanToken($token, &$str) { - # - # Handle $token provided by parseSpan by determining its nature and - # returning the corresponding value that should replace it. - # - switch ($token{0}) { - case "\\": - return $this->hashPart("&#". ord($token{1}). ";"); - case "`": - # Search for end marker in remaining text. - if (preg_match('/^(.*?[^`])'.preg_quote($token).'(?!`)(.*)$/sm', - $str, $matches)) - { - $str = $matches[2]; - $codespan = $this->makeCodeSpan($matches[1]); - return $this->hashPart($codespan); - } - return $token; // return as text since no ending marker found. - default: - return $this->hashPart($token); + /** + * @param $text + * + * @return mixed + */ + public function outdent( $text ) { + + // + // Remove one level of line-leading tabs or spaces + // + return preg_replace( '/^(\t|[ ]{1,' . $this->tab_width . '})/m', '', $text ); } - } - function outdent($text) { - # - # Remove one level of line-leading tabs or spaces - # - return preg_replace('/^(\t|[ ]{1,'.$this->tab_width.'})/m', '', $text); - } + // String length function for detab. `_initDetab` will create a function to + // hanlde UTF-8 if the default function does not exist. + public $utf8_strlen = 'mb_strlen'; + /** + * @param $text + * + * @return mixed + */ + public function detab( $text ) { - # String length function for detab. `_initDetab` will create a function to - # hanlde UTF-8 if the default function does not exist. - var $utf8_strlen = 'mb_strlen'; + // + // Replace tabs with the appropriate amount of space. + // + // For each line we separate the line in blocks delemited by + // tab characters. Then we reconstruct every line by adding the + // appropriate number of space between each blocks. + $text = preg_replace_callback( '/^.*\t.*$/m', array( &$this, '_detab_callback' ), $text ); - function detab($text) { - # - # Replace tabs with the appropriate amount of space. - # - # For each line we separate the line in blocks delemited by - # tab characters. Then we reconstruct every line by adding the - # appropriate number of space between each blocks. + return $text; + } - $text = preg_replace_callback('/^.*\t.*$/m', - array(&$this, '_detab_callback'), $text); + /** + * @param $matches + * + * @return string + */ + public function _detab_callback( $matches ) { + + $line = $matches[0]; + $strlen = $this->utf8_strlen; + // strlen function for UTF-8. + // Split in blocks. + $blocks = explode( "\t", $line ); + // Add each blocks to the line. + $line = $blocks[0]; + unset( $blocks[0] ); + // Do not add first block twice. + foreach ( $blocks as $block ) { + // Calculate amount of space, insert spaces, insert block. + $amount = $this->tab_width - $strlen( $line, 'UTF-8' ) % $this->tab_width; + $line .= str_repeat( ' ', $amount ) . $block; + } - return $text; - } - function _detab_callback($matches) { - $line = $matches[0]; - $strlen = $this->utf8_strlen; # strlen function for UTF-8. - - # Split in blocks. - $blocks = explode("\t", $line); - # Add each blocks to the line. - $line = $blocks[0]; - unset($blocks[0]); # Do not add first block twice. - foreach ($blocks as $block) { - # Calculate amount of space, insert spaces, insert block. - $amount = $this->tab_width - - $strlen($line, 'UTF-8') % $this->tab_width; - $line .= str_repeat(" ", $amount) . $block; + return $line; } - return $line; - } - function _initDetab() { - # - # Check for the availability of the function in the `utf8_strlen` property - # (initially `mb_strlen`). If the function is not available, create a - # function that will loosely count the number of UTF-8 characters with a - # regular expression. - # - if (function_exists($this->utf8_strlen)) return; - $this->utf8_strlen = create_function('$text', 'return preg_match_all( + + public function _initDetab() { + + // + // Check for the availability of the function in the `utf8_strlen` property + // (initially `mb_strlen`). If the function is not available, create a + // function that will loosely count the number of UTF-8 characters with a + // regular expression. + // + if ( function_exists( $this->utf8_strlen ) ) { + return; + } + $this->utf8_strlen = create_function( '$text', 'return preg_match_all( "/[\\\\x00-\\\\xBF]|[\\\\xC0-\\\\xFF][\\\\x80-\\\\xBF]*/", - $text, $m);'); - } + $text, $m);' ); + } + /** + * @param $text + * + * @return mixed + */ + public function unhash( $text ) { + + // + // Swap back in all the tags hashed by _HashHTMLBlocks. + // + return preg_replace_callback( '/(.)\x1A[0-9]+\1/', array( &$this, '_unhash_callback' ), $text ); + } - function unhash($text) { - # - # Swap back in all the tags hashed by _HashHTMLBlocks. - # - return preg_replace_callback('/(.)\x1A[0-9]+\1/', - array(&$this, '_unhash_callback'), $text); - } - function _unhash_callback($matches) { - return $this->html_hashes[$matches[0]]; - } + /** + * @param $matches + * + * @return mixed + */ + public function _unhash_callback( $matches ) { -} + return $this->html_hashes[ $matches[0] ]; + } -/* + } -PHP Markdown -============ + /* + PHP Markdown + ============ -Description ------------ + Description + ----------- -This is a PHP translation of the original Markdown formatter written in -Perl by John Gruber. + This is a PHP translation of the original Markdown formatter written in + Perl by John Gruber. -Markdown is a text-to-HTML filter; it translates an easy-to-read / -easy-to-write structured text format into HTML. Markdown's text format -is most similar to that of plain text email, and supports features such -as headers, *emphasis*, code blocks, blockquotes, and links. + Markdown is a text-to-HTML filter; it translates an easy-to-read / + easy-to-write structured text format into HTML. Markdown's text format + is mostly similar to that of plain text email, and supports features such + as headers, *emphasis*, code blocks, blockquotes, and links. -Markdown's syntax is designed not as a generic markup language, but -specifically to serve as a front-end to (X)HTML. You can use span-level -HTML tags anywhere in a Markdown document, and you can use block level -HTML tags (like
    and as well). + Markdown's syntax is designed not as a generic markup language, but + specifically to serve as a front-end to (X)HTML. You can use span-level + HTML tags anywhere in a Markdown document, and you can use block level + HTML tags (like
    and
    as well). -For more information about Markdown's syntax, see: + For more information about Markdown's syntax, see: - + -Bugs ----- + Bugs + ---- -To file bug reports please send email to: + To file bug reports please send email to: - + -Please include with your report: (1) the example input; (2) the output you -expected; (3) the output Markdown actually produced. + Please include with your report: (1) the example input; (2) the output you + expected; (3) the output Markdown actually produced. -Version History ---------------- + Version History + --------------- -See the readme file for detailed release notes for this version. + See the readme file for detailed release notes for this version. -Copyright and License ---------------------- + Copyright and License + --------------------- -PHP Markdown -Copyright (c) 2004-2009 Michel Fortin - -All rights reserved. + PHP Markdown + Copyright (c) 2004-2013 Michel Fortin + + All rights reserved. -Based on Markdown -Copyright (c) 2003-2006 John Gruber - -All rights reserved. + Based on Markdown + Copyright (c) 2003-2006 John Gruber + + All rights reserved. -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: + 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 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. + * 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 "Markdown" nor the names of its contributors may - be used to endorse or promote products derived from this software - without specific prior written permission. + * Neither the name "Markdown" 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 owner -or 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. + 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 owner + or 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. -*/ + */ endif; diff --git a/includes/Pods_GF.php b/includes/Pods_GF.php index e869767..cebe254 100644 --- a/includes/Pods_GF.php +++ b/includes/Pods_GF.php @@ -151,7 +151,7 @@ public static function get_instance( $pod, $form_id, $options ) { * @param int $form_id GF Form ID * @param array $options Form options for integration */ - private function __construct ( $pod, $form_id ) { + private function __construct( $pod, $form_id ) { // Pod object if ( is_object( $pod ) ) { @@ -201,16 +201,24 @@ public function setup_options( $options ) { } if ( ! pods_v( 'admin', $options, 0 ) && ( is_admin() && RGForms::is_gravity_page() ) ) { + if ( ! has_action( 'gform_post_update_entry_' . $form_id, array( $this, '_gf_post_update_entry' ) ) ) { + add_action( 'gform_post_update_entry_' . $form_id, array( $this, '_gf_post_update_entry' ), 10, 2 ); + add_action( 'gform_after_update_entry_' . $form_id, array( $this, '_gf_after_update_entry' ), 10, 3 ); + } + return; } if ( ! has_filter( 'gform_pre_render_' . $form_id, array( $this, '_gf_pre_render' ) ) ) { add_filter( 'gform_pre_render_' . $form_id, array( $this, '_gf_pre_render' ), 10, 2 ); + add_filter( 'gform_pre_validation_' . $form_id, array( $this, '_gf_pre_render' ), 10, 1 ); add_filter( 'gform_get_form_filter_' . $form_id, array( $this, '_gf_get_form_filter' ), 10, 2 ); add_filter( 'gform_pre_submission_filter_' . $form_id, array( $this, '_gf_pre_submission_filter' ), 9, 1 ); add_action( 'gform_after_submission_' . $form_id, array( $this, '_gf_after_submission' ), 10, 2 ); + add_action( 'gform_post_update_entry_' . $form_id, array( $this, '_gf_post_update_entry' ), 10, 2 ); + add_action( 'gform_after_update_entry_' . $form_id, array( $this, '_gf_after_update_entry' ), 10, 3 ); // Hook into validation add_filter( 'gform_validation_' . $form_id, array( $this, '_gf_validation' ), 11, 1 ); @@ -264,7 +272,7 @@ public function setup_options( $options ) { * * @return bool Whether the conditions were met */ - public static function conditions ( $conditions, $form_id ) { + public static function conditions( $conditions, $form_id ) { $relation = 'AND'; @@ -319,7 +327,7 @@ public static function conditions ( $conditions, $form_id ) { * * @return bool Whether the condition was met */ - public static function condition ( $condition, $value, $field = null ) { + public static function condition( $condition, $value, $field = null ) { $field_value = pods_v( 'value', $condition ); $field_check = pods_v( 'check', $condition, 'value' ); @@ -487,7 +495,7 @@ public static function condition ( $condition, $value, $field = null ) { * * @return bool Whether the length was valid */ - public static function condition_validate_length ( $condition, $value ) { + public static function condition_validate_length( $condition, $value ) { $valid = false; @@ -568,7 +576,7 @@ public static function condition_validate_length ( $condition, $value ) { * * @return bool Whether the value was valid */ - public static function condition_validate_value ( $condition, $value ) { + public static function condition_validate_value( $condition, $value ) { $valid = false; @@ -673,7 +681,7 @@ public static function condition_validate_value ( $condition, $value ) { * @param int $form_id GF Form ID * @param bool $keep_files To keep or delete files when deleting GF entries */ - public static function auto_delete ( $form_id = null, $keep_files = null ) { + public static function auto_delete( $form_id = null, $keep_files = null ) { if ( null !== $keep_files ) { self::$keep_files[ $form_id ] = (boolean) $keep_files; @@ -681,7 +689,7 @@ public static function auto_delete ( $form_id = null, $keep_files = null ) { $form = ( ! empty( $form_id ) ? '_' . (int) $form_id : '' ); - add_action( 'gform_post_submission' . $form, array( get_class(), 'delete_entry' ), 20, 1 ); + add_action( 'gform_post_submission' . $form, array( get_class(), 'gf_delete_entry' ), 20, 1 ); } @@ -694,7 +702,7 @@ public static function auto_delete ( $form_id = null, $keep_files = null ) { * @param int $field_id GF Field ID * @param array $options Dynamic select options */ - public static function dynamic_select ( $form_id, $field_id, $options ) { + public static function dynamic_select( $form_id, $field_id, $options ) { self::$dynamic_selects[] = array_merge( array( 'form' => $form_id, @@ -715,7 +723,7 @@ public static function dynamic_select ( $form_id, $field_id, $options ) { $class = get_class(); if ( ! has_filter( 'gform_pre_render_' . $form_id, array( $class, 'gf_dynamic_select' ) ) ) { - add_filter( 'gform_pre_render_' . $form_id, array( $class, 'gf_dynamic_select' ), 10, 2 ); + add_filter( 'gform_pre_render_' . $form_id, array( $class, 'gf_dynamic_select' ), 10, 1 ); } } @@ -728,7 +736,7 @@ public static function dynamic_select ( $form_id, $field_id, $options ) { * * @return array Choices array */ - public static function build_choices ( $values, $current_value = '', $default = '' ) { + public static function build_choices( $values, $current_value = '', $default = '' ) { $choices = array(); @@ -744,10 +752,13 @@ public static function build_choices ( $values, $current_value = '', $default = if ( is_array( $label ) ) { $choices[] = $label; } else { + + $isSelected = ( (string) $value === $current_value ) || ( is_array( $current_value ) && in_array( $value, $current_value ) ); + $choices[] = array( 'text' => $label, 'value' => $value, - 'isSelected' => ( (string) $value === (string) $current_value ) + 'isSelected' => $isSelected, ); } } @@ -765,7 +776,7 @@ public static function build_choices ( $values, $current_value = '', $default = * * @return array Selected choice array */ - public static function get_selected_choice ( $choices, $current_value = '', $default = '' ) { + public static function get_selected_choice( $choices, $current_value = '', $default = '' ) { if ( null === $current_value || '' === $current_value ) { $current_value = ''; @@ -785,7 +796,7 @@ public static function get_selected_choice ( $choices, $current_value = '', $def ); } - if ( 1 == pods_v( 'isSelected', $choice ) || '' === $current_value || ( ! isset( $choice['isSelected'] ) && (string) $choice['value'] === (string) $current_value ) ) { + if ( 1 === (int) pods_v( 'isSelected', $choice ) || '' === $current_value || ( ! isset( $choice['isSelected'] ) && (string) $choice['value'] === (string) $current_value ) ) { $selected = $choice; $selected['isSelected'] = true; @@ -807,7 +818,7 @@ public static function get_selected_choice ( $choices, $current_value = '', $def * @param int $id Pod item ID * @param array $fields Field mapping to prepopulate from */ - public static function prepopulate ( $form_id, $pod, $id, $fields ) { + public static function prepopulate( $form_id, $pod, $id, $fields ) { self::$prepopulate = array( 'form' => $form_id, @@ -819,7 +830,7 @@ public static function prepopulate ( $form_id, $pod, $id, $fields ) { $class = get_class(); if ( ! has_filter( 'gform_pre_render_' . $form_id, array( $class, 'gf_prepopulate' ) ) ) { - add_filter( 'gform_pre_render_' . $form_id, array( $class, 'gf_prepopulate' ), 10, 2 ); + add_filter( 'gform_pre_render_' . $form_id, array( $class, 'gf_prepopulate' ), 10, 1 ); } } @@ -829,7 +840,7 @@ public static function prepopulate ( $form_id, $pod, $id, $fields ) { * @param int|array $form_id * @param int|array $field_id */ - public static function prepopulate_override_value ( $form_id, $field_id ) { + public static function prepopulate_override_value( $form_id, $field_id ) { if ( is_array( $form_id ) ) { $form_id = $form_id['id']; @@ -855,7 +866,7 @@ public static function prepopulate_override_value ( $form_id, $field_id ) { * * @return mixed */ - public static function gf_prepopulate_value ( $post_value_override, $value_override ) { + public static function gf_prepopulate_value( $post_value_override, $value_override ) { if ( null === $post_value_override ) { $post_value_override = $value_override; @@ -871,9 +882,9 @@ public static function gf_prepopulate_value ( $post_value_override, $value_overr * @param int $form_id GF Form ID * @param array $options Secondary Submits options */ - public static function secondary_submits ( $form_id, $options = array() ) { + public static function secondary_submits( $form_id, $options = array() ) { - self::$secondary_submits[$form_id] = array( + self::$secondary_submits[ $form_id ] = array( 'imageUrl' => null, 'text' => 'Alt Submit', 'action' => 'alt', @@ -882,7 +893,7 @@ public static function secondary_submits ( $form_id, $options = array() ) { ); if ( is_array( $options ) ) { - self::$secondary_submits[$form_id] = $options; + self::$secondary_submits[ $form_id ] = $options; } if ( ! has_filter( 'gform_submit_button_' . $form_id, array( 'Pods_GF', 'gf_secondary_submit_button' ) ) ) { @@ -909,7 +920,7 @@ public static function secondary_submits ( $form_id, $options = array() ) { * * @return string Button HTML */ - public static function gf_secondary_submit_button ( $button_input, $form ) { + public static function gf_secondary_submit_button( $button_input, $form ) { $secondary_submits = pods_v( $form['id'], self::$secondary_submits, array(), true ); @@ -990,7 +1001,7 @@ public static function gf_secondary_submit_button ( $button_input, $form ) { * @param int $form_id GF Form ID * @param array $options Save for Later options */ - public static function save_for_later ( $form_id, $options = array() ) { + public static function save_for_later( $form_id, $options = array() ) { self::$save_for_later[$form_id] = array( 'redirect' => null, @@ -1022,7 +1033,7 @@ public static function save_for_later ( $form_id, $options = array() ) { * * @return array $form GF Form array */ - public static function gf_save_for_later_load ( $form, $ajax ) { + public static function gf_save_for_later_load( $form, $ajax ) { $save_for_later = pods_v( $form['id'], self::$save_for_later, array(), true ); @@ -1046,7 +1057,7 @@ public static function gf_save_for_later_load ( $form, $ajax ) { * * @return array|bool */ - public static function gf_save_for_later_data ( $form_id ) { + public static function gf_save_for_later_data( $form_id ) { $postdata = array(); @@ -1086,7 +1097,7 @@ public static function gf_save_for_later_data ( $form_id ) { * * @return string Button HTML */ - public static function gf_save_for_later_button ( $button_input, $form ) { + public static function gf_save_for_later_button( $button_input, $form ) { $save_for_later = pods_v( $form['id'], self::$save_for_later, array(), true ); @@ -1131,7 +1142,7 @@ public static function gf_save_for_later_button ( $button_input, $form ) { /** * AJAX Handler for Save for Later */ - public static function gf_save_for_later_ajax () { + public static function gf_save_for_later_ajax() { global $user_ID; @@ -1187,7 +1198,7 @@ public static function gf_save_for_later_ajax () { * @param array $entry GF Entry array * @param array $form GF Form array */ - public static function gf_save_for_later_clear ( $entry, $form, $force = false ) { + public static function gf_save_for_later_clear( $entry, $form, $force = false ) { global $user_ID; @@ -1222,7 +1233,7 @@ public static function gf_save_for_later_clear ( $entry, $form, $force = false ) * @param int $form_id GF Form ID * @param array $options Save for Later options */ - public static function remember ( $form_id, $options = array() ) { + public static function remember( $form_id, $options = array() ) { self::$remember[$form_id] = array( 'fields' => null @@ -1251,7 +1262,7 @@ public static function remember ( $form_id, $options = array() ) { * * @return array $form GF Form array */ - public static function gf_remember_load ( $form, $ajax ) { + public static function gf_remember_load( $form, $ajax ) { global $user_ID; @@ -1301,7 +1312,7 @@ public static function gf_remember_load ( $form, $ajax ) { * @param array $entry GF Entry array * @param array $form GF Form array */ - public static function gf_remember_save ( $entry, $form ) { + public static function gf_remember_save( $entry, $form ) { global $user_ID; @@ -1348,11 +1359,14 @@ public static function gf_remember_save ( $entry, $form ) { /** * Map GF form fields to Pods fields * - * @param array $form GF Form array + * @param array $form GF Form array + * @param array $entry GF Entry array * * @return int Pod item ID */ - public function _gf_to_pods_handler( $form ) { + public function _gf_to_pods_handler( $form, $entry = array() ) { + + $form = $this->setup_form( $form ); $id = 0; @@ -1366,6 +1380,18 @@ public function _gf_to_pods_handler( $form ) { $save_action = 'add'; + if ( empty( $id ) && ! empty( $entry['id'] ) && ( ! empty( $this->options['update_pod_item'] ) || apply_filters( 'pods_gf_to_pods_update_pod_items', false ) ) ) { + $item_id = (int) gform_get_meta( $entry['id'], '_pods_item_id' ); + $item_pod = gform_get_meta( $entry['id'], '_pods_item_pod' ); + + if ( $item_id && $item_pod && is_object( $this->pod ) && $item_pod === $this->pod->pod ) { + // Only use the item ID if it exists. + if ( $this->pod->fetch( $item_id ) ) { + $id = $item_id; + } + } + } + if ( ! empty( $id ) ) { $save_action = 'save'; } @@ -1374,21 +1400,24 @@ public function _gf_to_pods_handler( $form ) { $save_action = $this->options['save_action']; } - if ( empty( $id ) || ! in_array( $save_action, array( 'add', 'save', 'bypass' ) ) ) { - $save_action = 'add'; + if ( 'bypass' === $save_action ) { + return $id; } - if ( 'bypass' == $save_action ) { - return $id; + if ( empty( $id ) || ! in_array( $save_action, array( 'add', 'save' ), true ) ) { + $save_action = 'add'; } - $data = self::gf_to_pods( $form, $this->options ); + // Set pod_item_id for use later. + $this->options['pod_item_id'] = $id; + + $data = self::gf_to_pods( $form, $this->options, $this->pod, $entry ); $args = array( $data // Data ); - if ( 'save' == $save_action ) { + if ( 'save' === $save_action ) { $args[1] = null; // Value $args[2] = $id; // ID } @@ -1404,25 +1433,27 @@ public function _gf_to_pods_handler( $form ) { unset( $data[ $this->pod->data->field_id ] ); } - if ( 'post_type' == $this->pod->pod_data['type'] ) { - if ( ! empty( $form['postStatus'] ) && empty( $args[0]['post_status'] ) ) { - $args[0]['post_status'] = $form['postStatus']; - } + if ( empty( $id ) ) { + if ( 'post_type' === $this->pod->pod_data['type'] ) { + if ( ! empty( $form['postStatus'] ) && empty( $args[0]['post_status'] ) ) { + $args[0]['post_status'] = $form['postStatus']; + } - if ( empty( $args[0]['post_author'] ) ) { - if ( ! empty( $form['useCurrentUserAsAuthor'] ) && is_user_logged_in() ) { - $args[0]['post_author'] = get_current_user_id(); - } elseif ( ! empty( $form['postAuthor'] ) ) { - $args[0]['post_author'] = $form['postAuthor']; + if ( empty( $args[0]['post_author'] ) ) { + if ( ! empty( $form['useCurrentUserAsAuthor'] ) && is_user_logged_in() ) { + $args[0]['post_author'] = get_current_user_id(); + } elseif ( ! empty( $form['postAuthor'] ) ) { + $args[0]['post_author'] = $form['postAuthor']; + } } - } - if ( ! empty( $form['postCategory'] ) && empty( $args[0]['category'] ) && ! empty( $this->pod->pod_data['object_fields']['category'] ) ) { - $args[0]['category'] = $form['postCategory']; - } + if ( ! empty( $form['postCategory'] ) && empty( $args[0]['category'] ) && ! empty( $this->pod->pod_data['object_fields']['category'] ) ) { + $args[0]['category'] = $form['postCategory']; + } - if ( ! empty( $form['postFormat'] ) && empty( $args[0]['post_format'] ) && ! empty( $this->pod->pod_data['object_fields']['post_format'] ) ) { - $args[0]['post_format'] = $form['postFormat']; + if ( ! empty( $form['postFormat'] ) && empty( $args[0]['post_format'] ) && ! empty( $this->pod->pod_data['object_fields']['post_format'] ) ) { + $args[0]['post_format'] = $form['postFormat']; + } } } @@ -1442,8 +1473,23 @@ public function _gf_to_pods_handler( $form ) { $this->id = $id = apply_filters( 'pods_gf_to_pod_' . $save_action, $id, $this->pod, $data, $this ); } + // Set pod_item_id for use later. + $this->options['pod_item_id'] = $id; + self::$gf_to_pods_id[ $this->form_id ] = $id; + if ( $this->pod && ! empty( $this->pod->pod_data ) ) { + self::$gf_to_pods_id[ $this->form_id . '_permalink' ] = $this->pod->field( 'detail_url' ); + } + + if ( ! empty( $entry['id'] ) ) { + gform_update_meta( $entry['id'], '_pods_item_id', $id ); + + if ( is_object( $this->pod ) ) { + gform_update_meta( $entry['id'], '_pods_item_pod', $this->pod->pod ); + } + } + do_action( 'pods_gf_to_pods', $this->pod, $save_action, $data, $id, $this ); return $id; @@ -1453,12 +1499,14 @@ public function _gf_to_pods_handler( $form ) { /** * Map GF form fields to Pods fields * - * @param array $form GF Form array - * @param array $options Form config + * @param array $form GF Form array + * @param array $options Form config + * @param Pods|array $pod Pod object or entry array + * @param array $entry GF Entry array * * @return array Data array for saving */ - public static function gf_to_pods ( $form, $options ) { + public static function gf_to_pods( $form, $options, $pod = array(), $entry = array() ) { $data = array(); @@ -1473,10 +1521,50 @@ public static function gf_to_pods ( $form, $options ) { $gf_fields = array(); + if ( ! empty( $entry ) ) { + $entry['form_title'] = $form['title']; + } + + $extra_gf_fields = array( + 'id', + 'date_created', + 'ip', + 'source_url', + 'form_title', + 'transaction_id', + 'payment_amount', + 'payment_date', + 'payment_status', + ); + + $basic_gf_field_data = array( + 'id' => '', + 'label' => '', + 'type' => 'text', + 'isRequired' => false, + 'visibility' => 'visible', + 'formId' => $form['id'], + ); + + foreach ( $extra_gf_fields as $extra_gf_field ) { + $basic_gf_field_data['id'] = $extra_gf_field; + $basic_gf_field_data['label'] = $extra_gf_field; + + $gf_fields[ $extra_gf_field ] = GF_Fields::create( $basic_gf_field_data ); + } + foreach ( $fields as $gf_field ) { $gf_fields[ (string) $gf_field->id ] = $gf_field; } + if ( empty( $entry ) ) { + if ( ! empty( $options['entry'] ) ) { + $entry = $options['entry']; + } else { + $entry = GFFormsModel::get_current_lead(); + } + } + foreach ( $options['fields'] as $field => $field_options ) { $field = (string) $field; @@ -1499,12 +1587,18 @@ public static function gf_to_pods ( $form, $options ) { // Get GF field object $gf_field = null; + $field_full = null; + /** * @var $gf_field GF_Field */ if ( isset( $gf_fields[ (string) $field ] ) ) { + $field_full = $field; + $gf_field = $gf_fields[ (string) $field ]; } elseif ( false !== strpos( $field, '.' ) ) { + $field_full = $field; + $gf_field_expanded = explode( '.', $field ); if ( isset( $gf_fields[ (string) $gf_field_expanded[0] ] ) ) { @@ -1515,13 +1609,23 @@ public static function gf_to_pods ( $form, $options ) { // GF input field $value = null; + $field_data = array(); + + if ( is_object( $pod ) && ! empty( $pod->pod_data ) ) { + $field_data = $pod->fields( $field_options['field'] ); + } + if ( $gf_field ) { $gf_params = array( - 'gf_field' => $gf_field, - 'field' => $field, - 'form' => $form, - 'options' => $options, - 'handle_files' => true, + 'gf_field' => $gf_field, + 'gf_field_options' => $field_options, + 'field' => $field_full, + 'field_options' => $field_data, + 'pod' => $pod, + 'form' => $form, + 'entry' => $entry, + 'options' => $options, + 'handle_files' => true, ); $value = self::get_gf_field_value( $value, $gf_params ); @@ -1530,17 +1634,31 @@ public static function gf_to_pods ( $form, $options ) { // Manual value override if ( null !== $field_options['value'] ) { $value = $field_options['value']; + + if ( is_string( $value ) && ! empty( $field_options['gf_merge_tags'] ) ) { + $value = GFCommon::replace_variables( $value, $form, $entry ); + } } $value = apply_filters( 'pods_gf_to_pods_value_' . $form['id'] . '_' . $field, $value, $field, $field_options, $form, $gf_field, $data, $options ); $value = apply_filters( 'pods_gf_to_pods_value_' . $form['id'], $value, $field, $field_options, $form, $gf_field, $data, $options ); $value = apply_filters( 'pods_gf_to_pods_value', $value, $field, $field_options, $form, $gf_field, $data, $options ); + // If a file is not set, check if we are editing an item. + if ( null === $value && in_array( $gf_field->type, array( 'fileupload', 'post_image' ), true ) ) { + // If we are editing an item, don't attempt to save. + if ( is_object( $pod ) && $pod->id ) { + continue; + } + } + // Set data if ( null !== $value ) { // @todo Support simple repeatable fields in Pods 2.9 - if ( $gf_field && 'list' == $gf_field->type && is_array( $value ) ) { - $value = json_encode( $value ); + if ( $gf_field && 'list' === $gf_field->type && is_array( $value ) ) { + if ( empty( $pod ) || empty( $gf_params['field_options']['type'] ) || 'pick' !== $gf_params['field_options']['type'] ) { + $value = json_encode( $value ); + } } $data[ $field_options['field'] ] = $value; @@ -1562,7 +1680,7 @@ public static function gf_to_pods ( $form, $options ) { * * @return bool Whether the notifications were sent */ - public static function gf_notifications ( $entry, $form, $options ) { + public static function gf_notifications( $entry, $form, $options ) { if ( ! isset( $options['notifications'] ) || empty( $options['notifications'] ) ) { return false; @@ -1737,12 +1855,11 @@ public static function gf_delete_entry( $entry ) { * Set dynamic option values for GF Form fields * * @param array $form GF Form array - * @param bool $ajax Whether the form was submitted using AJAX * @param array|null $dynamic_selects The Dynamic select options to use * * @return array $form GF Form array */ - public static function gf_dynamic_select ( $form, $ajax, $dynamic_selects = null ) { + public static function gf_dynamic_select( $form, $dynamic_selects = null ) { if ( null === $dynamic_selects ) { $dynamic_selects = self::$dynamic_selects; @@ -1779,6 +1896,7 @@ public static function gf_dynamic_select ( $form, $ajax, $dynamic_selects = null 'default' => null, // override default selected value 'options' => null, // set to an array for a basic custom options list + 'select_text' => null, // set to the text to use for the empty option 'pod' => null, // set to a pod to use 'field_text' => null, // set to the field to show for text (option label) @@ -1797,11 +1915,28 @@ public static function gf_dynamic_select ( $form, $ajax, $dynamic_selects = null } $field_key = $field_keys[$field]; + $field_obj = $form['fields'][$field_key]; $choices = false; + $field_options = array(); + if ( is_array( $dynamic_select['options'] ) && ! empty( $dynamic_select['options'] ) ) { - $choices = self::build_choices( $dynamic_select['options'] ); + $current_value = ''; + + if ( ! empty( $_POST[ 'input_' . $field ] ) ) { + $current_value = $_POST[ 'input_' . $field ]; + } elseif ( ! empty( $_GET[ 'pods_gf_field_' . $field ] ) ) { + $current_value = $_GET[ 'pods_gf_field_' . $field ]; + } + + $default_value = ''; + + if ( null !== $dynamic_select['default'] ) { + $default_value = $dynamic_select['default']; + } + + $choices = self::build_choices( $dynamic_select['options'], $current_value, $default_value ); } elseif ( ! empty( $dynamic_select['pod'] ) ) { if ( ! is_object( $dynamic_select['pod'] ) ) { @@ -1856,6 +1991,42 @@ public static function gf_dynamic_select ( $form, $ajax, $dynamic_selects = null } } + // Additional handling for showing an empty choice for fields that are not required. + if ( empty( $field_obj->isRequired ) && empty( $field_obj->placeholder ) ) { + if ( 'radio' === $field_obj->type || ( 'entry' !== rgget( 'view' ) && 'select' === $field_obj->type ) ) { + $needs_empty = true; + + // Check if we have an empty option already. + foreach ( $choices as $choice ) { + if ( '' === $choice['value'] ) { + $needs_empty = false; + + break; + } + } + + if ( $needs_empty ) { + if ( ! empty( $dynamic_select['select_text'] ) ) { + $empty_text = $dynamic_select['select_text']; + } else { + $empty_text = __( 'Select One', 'pods-gravity-forms' ); + + if ( 'select' === $field_obj->type ) { + $empty_text = sprintf( '-- %s --', $empty_text ); + } + } + + $empty_choice = array( + 'text' => $empty_text, + 'value' => '', + ); + + // Add empty choice to front of choices list. + array_unshift( $choices, $empty_choice ); + } + } + } + $choices = apply_filters( 'pods_gf_dynamic_choices_' . $form['id'] . '_' . $field, $choices, $dynamic_select, $field, $form, $dynamic_selects ); $choices = apply_filters( 'pods_gf_dynamic_choices_' . $form['id'], $choices, $dynamic_select, $field, $form, $dynamic_selects ); $choices = apply_filters( 'pods_gf_dynamic_choices', $choices, $dynamic_select, $field, $form, $dynamic_selects ); @@ -1865,18 +2036,37 @@ public static function gf_dynamic_select ( $form, $ajax, $dynamic_selects = null } if ( null !== $dynamic_select['default'] ) { - $form['fields'][$field_key]['defaultValue'] = $dynamic_select['default']; + $field_obj->defaultValue = $dynamic_select['default']; + } + + // Remove extra empty choices. + $empty_choice_found = false; + + foreach ( $choices as $k => $choice ) { + if ( '' === $choice['value'] ) { + if ( $empty_choice_found ) { + unset( $choices[ $k ] ); + + continue; + } + + $empty_choice_found = true; + } } - $form['fields'][$field_key]['choices'] = $choices; + $choices = array_values( $choices ); + + $field_obj->choices = $choices; // Additional handling for checkboxes - if ( 'checkbox' == $form['fields'][$field_key]['type'] ) { + if ( 'checkbox' === $field_obj->type ) { $inputs = array(); - $input_id = 1; + $input_id = 0; foreach ( $choices as $choice ) { + $input_id ++; + // Workaround for GF bug with multiples of 10 (so that 5.1 doesn't conflict with 5.10) if ( 0 == $input_id % 10 ) { $input_id ++; @@ -1889,8 +2079,15 @@ public static function gf_dynamic_select ( $form, $ajax, $dynamic_selects = null ); } - $form['fields'][$field_key]['inputs'] = $inputs; + $field_obj->inputs = $inputs; + + if ( is_admin() && 'gf_edit_forms' === pods_v( 'page' ) && 'settings' === pods_v( 'view' ) && 'pods-gravity-forms' === pods_v( 'subview' ) ) { + $field_obj->choices = array(); + $field_obj->inputs = array(); + } } + + $form['fields'][$field_key] = $field_obj; } return $form; @@ -1901,12 +2098,11 @@ public static function gf_dynamic_select ( $form, $ajax, $dynamic_selects = null * Prepopulate a GF Form * * @param array $form GF Form array - * @param bool $ajax Whether the form was submitted using AJAX * @param array|null $prepopulate The prepopulate array * * @return array $form GF Form array */ - public static function gf_prepopulate ( $form, $ajax, $prepopulate = null ) { + public static function gf_prepopulate( $form, $prepopulate = null ) { if ( null === $prepopulate ) { $prepopulate = self::$prepopulate; @@ -2008,6 +2204,19 @@ public static function gf_prepopulate ( $form, $ajax, $prepopulate = null ) { $field = (string) $field_options['gf_field']; } + $full_field = $field; + + $field_expanded = array( + $field, + '', + ); + + if ( false !== strpos( $field, '.' ) ) { + $field_expanded = explode( '.', $field ); + + $field = $field_expanded[0]; + } + // No GF field set if ( empty( $field ) || ! isset( $field_keys[$field] ) ) { continue; @@ -2018,31 +2227,32 @@ public static function gf_prepopulate ( $form, $ajax, $prepopulate = null ) { continue; } - $field_key = $field_keys[$field]; + $field_key = $field_keys[ $field ]; + + $gf_field = $form['fields'][ $field_key ]; // Allow for value to be overridden by existing prepopulation or callback $value_override = $field_options['value']; - if ( null === $value_override && isset( $form['fields'][$field_key]['allowsPrepopulate'] ) && $form['fields'][$field_key]['allowsPrepopulate'] ) { + if ( null === $value_override && isset( $gf_field->allowsPrepopulate ) && $gf_field->allowsPrepopulate ) { // @todo handling for field types that have different $_POST input names - if ( 'checkbox' == $form['fields'][$field_key]['type'] ) { - if ( isset( $_GET[$form['fields'][$field_key]['name']] ) ) { - $value_override = $_GET[$form['fields'][$field_key]['name']]; + if ( 'checkbox' === $gf_field->type ) { + if ( isset( $_GET[ $gf_field->name ] ) ) { + $value_override = $_GET[ $gf_field->name ]; - foreach ( $form['fields'][$field_key]['choices'] as $k => $choice ) { - $form['fields'][$field_key]['choices'][$k]['isSelected'] = false; + foreach ( $gf_field->choices as $k => $choice ) { + $gf_field->choices[ $k ]['isSelected'] = false; if ( ( ! is_array( $value_override ) && $choice['value'] == $value_override ) || ( is_array( $value_override ) && in_array( $choice['value'], $value_override ) ) ) { - $form['fields'][$field_key]['choices'][$k]['isSelected'] = true; + $gf_field->choices[ $k ]['isSelected'] = true; break; } } } - } - elseif ( isset( $form['fields'][$field_key]['inputName'] ) && isset( $_GET[$form['fields'][$field_key]['inputName']] ) ) { - $value_override = $_GET[$form['fields'][$field_key]['inputName']]; + } elseif ( isset( $gf_field->inputName ) && isset( $_GET[ $gf_field->inputName ] ) ) { + $value_override = $_GET[ $gf_field->inputName ]; } } @@ -2051,61 +2261,40 @@ public static function gf_prepopulate ( $form, $ajax, $prepopulate = null ) { if ( null === $value_override ) { $value = $value_override; + $pod_field_type = ''; + if ( is_object( $pod ) ) { - $value_override = $pod->field( $field_options['field'] ); + $pod_field_type = $pod->fields( $field_options['field'], 'type' ); } - elseif ( ! empty( $pod ) ) { - if ( isset( $pod[$field_options['field']] ) ) { - $value_override = maybe_unserialize( $pod[$field_options['field']] ); - - if ( ! empty( $value_override ) ) { - if ( 'list' == $form['fields'][$field_key]['type'] ) { - $list = $value_override; - - $value_override = array(); - - foreach ( $list as $list_row ) { - $value_override = array_merge( $value_override, array_values( $list_row ) ); - } - } - elseif ( 'checkbox' == $form['fields'][$field_key]['type'] ) { - $values = $value_override; - $value_override = array(); - - foreach ( $form['fields'][$field_key]['choices'] as $k => $choice ) { - $form['fields'][$field_key]['choices'][$k]['isSelected'] = false; - - if ( ( ! is_array( $values ) && $choice['value'] == $values ) || ( is_array( $values ) && in_array( $choice['value'], $values ) ) ) { - $form['fields'][$field_key]['choices'][$k]['isSelected'] = true; - - $value_override['input_' . $choice['id']] = $choice['value']; - } - } + if ( is_array( $pod ) && isset( $pod[ $field_options['field'] ] ) ) { + $value_override = $pod[ $field_options['field'] ]; + } - $autopopulate = false; - } - } - } - elseif ( null !== $field_key && 'checkbox' == $form['fields'][$field_key]['type'] ) { + if ( $pod_field_type ) { + if ( is_object( $pod ) ) { + $value_override = $pod->field( $field_options['field'], array( 'output' => 'ids' ) ); + } elseif ( is_array( $pod ) && null !== $field_key && 'checkbox' === $gf_field->type ) { $value_override = array(); $items = 0; $counter = 1; - $total_choices = count( $form['fields'][$field_key]['choices'] ); + $total_choices = count( $gf_field->choices ); while ( $items < $total_choices ) { - if ( isset( $pod[$field_options['field'] . '.' . $counter] ) ) { + $field_key_counter = $field_options['field'] . '.' . $counter; + + if ( isset( $pod[ $field_key_counter ] ) ) { $choice_counter = 1; - foreach ( $form['fields'][$field_key]['choices'] as $k => $choice ) { - $form['fields'][$field_key]['choices'][$k]['isSelected'] = false; + foreach ( $gf_field->choices as $k => $choice ) { + $gf_field->choices[ $k ]['isSelected'] = false; - if ( $choice['value'] == $pod[$field_options['field'] . '.' . $counter] || $counter == $choice_counter ) { - $form['fields'][$field_key]['choices'][$k]['isSelected'] = true; + if ( (string) $choice['value'] === (string) $pod[ $field_key_counter ] || $counter == $choice_counter ) { + $gf_field->choices[ $k ]['isSelected'] = true; - $value_override['input_' . pods_v( 'id', $choice, $field_options['field'] . '.1', true )] = $choice['value']; + $value_override[ 'input_' . pods_v( 'id', $choice, $field_options['field'] . '.1', true ) ] = $choice['value']; } $choice_counter ++; @@ -2127,19 +2316,140 @@ public static function gf_prepopulate ( $form, $ajax, $prepopulate = null ) { $autopopulate = false; } + + $date_time_types = array( + 'date', + 'datetime', + ); + + $empty_values = array( + '0000-00-00', + '0000-00-00 00:00:00', + ); + + if ( in_array( $pod_field_type, $date_time_types, true ) && in_array( $value_override, $empty_values, true ) ) { + $value_override = ''; + } elseif ( ! empty( $value_override ) ) { + if ( 'list' === $gf_field->type ) { + if ( is_string( $value_override ) ) { + $list = @json_decode( $value_override, true ); + + if ( ! $list || ! is_array( $list ) ) { + $list = maybe_unserialize( $value_override ); + } + + $value_override = array(); + + if ( $list && is_array( $list ) ) { + $list = array_map( 'array_values', $list ); + $list = call_user_func_array( 'array_merge', $list ); + + $value_override = $list; + } + } elseif ( 'pick' === $pod_field_type ) { + $related_pod = false; + + if ( is_object( $pod ) ) { + $related_pod = $pod->field( $field_options['field'], array( 'output' => 'find' ) ); + } + + $value_override = array(); + + if ( $related_pod && is_a( $related_pod, 'Pods' ) && is_a( $gf_field, 'GF_Field_List' ) ) { + $columns = wp_list_pluck( $gf_field->choices, 'text' ); + + $columns = self::match_pod_fields_to_list_columns( $related_pod, $columns, $form, $gf_field ); + + while ( $related_pod->fetch() ) { + foreach ( $columns as $column ) { + $column_value = $related_pod->field( $column, array( 'output' => 'ids' ) ); + + $value_override[] = $column_value; + } + } + } + } + } elseif ( 'time' === $gf_field->type && ! empty( $value_override ) ) { + $format = empty( $gf_field->timeFormat ) ? '12' : esc_attr( $gf_field->timeFormat ); + + if ( '12' === $format && preg_match( '/^(\d{1,2}):(\d{1,2})/', $value_override, $matches ) && 12 < (int) $matches[1] ) { + $hour = (int) $matches[1]; + $hour -= 12; + + if ( $hour < 10 ) { + $hour = '0' . $hour; + } + + $value_override = sprintf( '%s:%s pm', $hour, $matches[2] ); + } + } elseif ( 'address' === $gf_field->type ) { + // @todo Figure out what to do for address values + } elseif ( 'name' === $gf_field->type ) { + // @todo Figure out what to do for name values + } elseif ( 'chainedselect' === $gf_field->type ) { + // @todo Figure out what to do for chained select values + } elseif ( 'checkbox' === $gf_field->type ) { + $values = $value_override; + + $value_override = array(); + + $choice_id = 1; + + foreach ( $gf_field->choices as $k => $choice ) { + $gf_field->choices[$k]['isSelected'] = false; + + $is_selected = false; + + if ( 'boolean' === $pod_field_type && 1 === (int) $values && ! empty( $choice['value'] ) ) { + $is_selected = true; + } elseif ( ( ! is_array( $values ) && (string) $choice['value'] === (string) $values ) + || ( is_array( $values ) && in_array( $choice['value'], $values ) ) ) { + $is_selected = true; + } + + if ( $is_selected ) { + $gf_field->choices[$k]['isSelected'] = true; + + if ( ! empty( $choice['id'] ) ) { + $choice_id = $choice['id']; + } + + $value_override[ 'input_' . $choice_id ] = $choice['value']; + } + + $choice_id ++; + } + + $autopopulate = false; + } + } } } if ( null !== $field_key ) { - $form['fields'][$field_key]['allowsPrepopulate'] = $autopopulate; - $form['fields'][$field_key]['inputName'] = 'pods_gf_field_' . $field; + $gf_field->allowsPrepopulate = $autopopulate; + $gf_field->inputName = 'pods_gf_field_' . $field; } + $form['fields'][ $field_key ] = $gf_field; + $value_override = apply_filters( 'pods_gf_pre_populate_value_' . $form['id'] . '_' . $field, $value_override, $field, $field_options, $form, $prepopulate, $pod ); $value_override = apply_filters( 'pods_gf_pre_populate_value_' . $form['id'], $value_override, $field, $field_options, $form, $prepopulate, $pod ); $value_override = apply_filters( 'pods_gf_pre_populate_value', $value_override, $field, $field_options, $form, $prepopulate, $pod ); if ( null !== $value_override ) { + if ( is_array( $value_override ) && 'list' === $gf_field->type ) { + $choices = $gf_field->choices; + + $value_override_chunked = array_chunk( $value_override, count( $choices ) ); + + foreach ( $value_override_chunked as $k => $v ) { + $value_override_chunked[ $k ] = implode( '|', $v ); + } + + $value_override = implode( ',', $value_override_chunked ); + } + $_GET['pods_gf_field_' . $field] = pods_slash( $value_override ); } @@ -2149,6 +2459,10 @@ public static function gf_prepopulate ( $form, $ajax, $prepopulate = null ) { $post_value_override = apply_filters( 'pods_gf_field_value', $post_value_override, $value_override, $field, $field_options, $form, $prepopulate, $pod ); if ( null !== $post_value_override ) { + if ( is_array( $post_value_override ) && 'list' === $gf_field->type ) { + $post_value_override = maybe_serialize( $post_value_override ); + } + $_POST['input_' . $field] = pods_slash( $post_value_override ); } } @@ -2163,7 +2477,7 @@ public static function gf_prepopulate ( $form, $ajax, $prepopulate = null ) { * @param int $form_id GF Form ID * @param array $options Confirmation options */ - public static function confirmation ( $form_id, $options = array() ) { + public static function confirmation( $form_id, $options = array() ) { self::$confirmation[$form_id] = $options; @@ -2179,7 +2493,7 @@ public static function confirmation ( $form_id, $options = array() ) { * @param array $form GF Form array * @param array $confirmation Confirmation array */ - public static function gf_confirmation ( $confirmation, $form, $lead, $ajax = false, $return_confirmation = false ) { + public static function gf_confirmation( $confirmation, $form, $lead, $ajax = false, $return_confirmation = false ) { $gf_confirmation = pods_v( $form['id'], self::$confirmation, array(), true ); @@ -2245,12 +2559,18 @@ public static function gf_confirmation ( $confirmation, $form, $lead, $ajax = fa } else { $gf_to_pods_id = 0; + $gf_to_pods_permalink = ''; if ( ! empty( self::$gf_to_pods_id[ $form['id'] ] ) ) { $gf_to_pods_id = self::$gf_to_pods_id[ $form['id'] ]; } + if ( ! empty( self::$gf_to_pods_id[ $form['id'] . '_permalink' ] ) ) { + $gf_to_pods_permalink = self::$gf_to_pods_id[ $form['id'] . '_permalink' ]; + } + $confirmation['url'] = str_replace( '{@gf_to_pods_id}', $gf_to_pods_id, $confirmation['url'] ); + $confirmation['url'] = str_replace( '{@gf_to_pods_permalink}', $gf_to_pods_permalink, $confirmation['url'] ); $url = trim( GFCommon::replace_variables( trim( $confirmation['url'] ), $form, $lead, false, true ) ); $url_info = parse_url( $url ); @@ -2295,12 +2615,18 @@ public static function gf_confirmation ( $confirmation, $form, $lead, $ajax = fa } } elseif ( ! empty( $confirmation['redirect'] ) ) { $gf_to_pods_id = 0; + $gf_to_pods_permalink = ''; if ( ! empty( self::$gf_to_pods_id[ $form['id'] ] ) ) { $gf_to_pods_id = self::$gf_to_pods_id[ $form['id'] ]; } + if ( ! empty( self::$gf_to_pods_id[ $form['id'] . '_permalink' ] ) ) { + $gf_to_pods_permalink = self::$gf_to_pods_id[ $form['id'] . '_permalink' ]; + } + $confirmation['redirect'] = str_replace( '{@gf_to_pods_id}', $gf_to_pods_id, $confirmation['redirect'] ); + $confirmation['redirect'] = str_replace( '{@gf_to_pods_permalink}', $gf_to_pods_permalink, $confirmation['redirect'] ); } } @@ -2344,7 +2670,7 @@ public static function gf_get_js_redirect_confirmation( $url, $ajax ) { * * @return array $form GF Form array */ - public static function gf_markdown ( $form, $ajax, $markdown = null ) { + public static function gf_markdown( $form, $ajax, $markdown = null ) { if ( isset( self::$actioned[$form['id']] ) && in_array( __FUNCTION__, self::$actioned[$form['id']] ) ) { return $form; @@ -2414,7 +2740,7 @@ public static function gf_markdown ( $form, $ajax, $markdown = null ) { * @param int $id Pod item ID * @param array $fields Field mapping to prepopulate from */ - public static function read_only ( $form_id, $fields = true, $exclude_fields = array() ) { + public static function read_only( $form_id, $fields = true, $exclude_fields = array() ) { self::$read_only = array( 'form' => $form_id, @@ -2425,7 +2751,7 @@ public static function read_only ( $form_id, $fields = true, $exclude_fields = a $class = get_class(); if ( ! has_filter( 'gform_pre_render_' . $form_id, array( $class, 'gf_read_only' ) ) ) { - add_filter( 'gform_pre_render_' . $form_id, array( $class, 'gf_read_only' ), 10, 2 ); + add_filter( 'gform_pre_render_' . $form_id, array( $class, 'gf_read_only' ), 10, 1 ); add_filter( 'gform_pre_submission_filter_' . $form_id, array( $class, 'gf_read_only_pre_submission' ), 10, 1 ); } @@ -2435,12 +2761,11 @@ public static function read_only ( $form_id, $fields = true, $exclude_fields = a * Enable Read Only for fields * * @param array $form GF Form array - * @param bool $ajax Whether the form was submitted using AJAX * @param array $read_only Read Only options * * @return array $form GF Form array */ - public static function gf_read_only ( $form, $ajax, $read_only = null ) { + public static function gf_read_only( $form, $read_only = null ) { if ( null === $read_only ) { $read_only = self::$read_only; @@ -2503,7 +2828,7 @@ public static function gf_read_only ( $form, $ajax, $read_only = null ) { $form['fields'][$field_keys[$field]]['isRequired'] = false; if ( 'list' == GFFormsModel::get_input_type( $gf_field ) ) { - $columns = ( is_array( $gf_field['choices'] ) ? $gf_field['choices'] : array( array() ) ); + $columns = ( is_array( $gf_field->choices ) ? $gf_field->choices : array( array() ) ); $col_number = 1; @@ -2556,7 +2881,7 @@ public static function gf_read_only ( $form, $ajax, $read_only = null ) { * * @return string Input HTML override */ - public static function gf_field_input_read_only ( $input_html, $field, $value, $lead_id, $form_id ) { + public static function gf_field_input_read_only( $input_html, $field, $value, $lead_id, $form_id ) { if ( ! isset( self::$actioned[$form_id] ) ) { self::$actioned[$form_id] = array(); @@ -2730,7 +3055,7 @@ public static function gf_field_input_read_only ( $input_html, $field, $value, $ * * @return string Input HTML override */ - public static function gf_field_column_read_only ( $input, $input_info, $field, $text, $value, $form_id ) { + public static function gf_field_column_read_only( $input, $input_info, $field, $text, $value, $form_id ) { $read_only = self::$read_only; @@ -2786,7 +3111,7 @@ public static function gf_field_column_read_only ( $input, $input_info, $field, * * @param array $form GF Form array */ - public static function gf_read_only_pre_submission ( $form ) { + public static function gf_read_only_pre_submission( $form ) { if ( isset( self::$actioned[$form['id']] ) && in_array( __FUNCTION__, self::$actioned[$form['id']] ) ) { return $form; @@ -2826,86 +3151,14 @@ public static function gf_read_only_pre_submission ( $form ) { * * @return array $form GF Form array */ - public function _gf_pre_render ( $form, $ajax ) { + public function _gf_pre_render( $form, $ajax = false ) { + + $form = $this->setup_form( $form ); if ( empty( $this->options ) ) { return $form; } - // Add Dynamic Selects - if ( isset( $this->options['dynamic_select'] ) && ! empty( $this->options['dynamic_select'] ) ) { - $form = self::gf_dynamic_select( $form, $ajax, $this->options['dynamic_select'] ); - } - - // Prepopulate values - if ( isset( $this->options['prepopulate'] ) && ! empty( $this->options['prepopulate'] ) ) { - $prepopulate = array( - 'pod' => $this->pod, - 'id' => pods_v( 'save_id', $this->options, $this->get_current_id(), true ), - 'fields' => $this->options['fields'] - ); - - if ( is_array( $this->options['prepopulate'] ) ) { - $prepopulate = array_merge( $prepopulate, $this->options['prepopulate'] ); - } - - $form = self::gf_prepopulate( $form, $ajax, $prepopulate ); - } - - // Read Only handling - if ( isset( $this->options['read_only'] ) && ! empty( $this->options['read_only'] ) ) { - $read_only = array( - 'form' => $form['id'], - - 'fields' => $this->options['read_only'], - 'exclude_fields' => array() - ); - - if ( is_array( $this->options['read_only'] ) && ( isset( $this->options['read_only']['fields'] ) || isset( $this->options['read_only']['exclude_fields'] ) ) ) { - $read_only = array_merge( $read_only, $this->options['read_only'] ); - } - - $form = self::gf_read_only( $form, $ajax, $read_only ); - } - - // Markdown Syntax for HTML - if ( isset( $this->options['markdown'] ) && ! empty( $this->options['markdown'] ) ) { - $form = self::gf_markdown( $form, $ajax, $this->options['markdown'] ); - } - - // Submit Button customization - if ( isset( $this->options['submit_button'] ) && ! empty( $this->options['submit_button'] ) ) { - if ( is_array( $this->options['submit_button'] ) ) { - if ( isset( $this->options['submit_button']['imageUrl'] ) ) { - $this->options['submit_button']['type'] = 'imageUrl'; - } - elseif ( isset( $this->options['submit_button']['text'] ) ) { - $this->options['submit_button']['type'] = 'text'; - } - - $button = $this->options['submit_button']; - } - elseif ( ( false !== strpos( $this->options['submit_button'], '://' ) && strpos( $this->options['submit_button'], '://' ) < 6 ) || 0 === strpos( $this->options['submit_button'], '/' ) ) { - $button = array( - 'imageUrl' => $this->options['submit_button'], - 'type' => 'imageUrl' - ); - } - else { - $button = array( - 'text' => $this->options['submit_button'], - 'type' => 'text' - ); - } - - $form['button'] = $button; - } - - // Secondary Submit actions - if ( isset( $this->options['secondary_submits'] ) && ! empty( $this->options['secondary_submits'] ) ) { - self::secondary_submits( $form['id'], $this->options['secondary_submits'] ); - } - return $form; } @@ -2918,7 +3171,7 @@ public function _gf_pre_render ( $form, $ajax ) { * * @return string Form HTML */ - public function _gf_get_form_filter ( $form_string, $form ) { + public function _gf_get_form_filter( $form_string, $form ) { if ( isset( self::$actioned[$form['id']] ) && in_array( __FUNCTION__, self::$actioned[$form['id']] ) ) { return $form_string; @@ -2943,31 +3196,84 @@ public function _gf_get_form_filter ( $form_string, $form ) { } /** - * Get GF field value + * Match pod fields to List field columns. * - * @param mixed $value - * @param array $params + * @param Pods $pod Pods object. + * @param array $columns List of column names from a List field type. + * @param array $form Form data. + * @param GF_Field $gf_field Field data. * - * @return mixed + * @return array */ - public static function get_gf_field_value( $value, $params ) { + public static function match_pod_fields_to_list_columns( $pod, $columns, $form, $gf_field ) { - $params = array_merge( - array( - 'gf_field' => null, - 'field' => null, - 'form' => null, - 'options' => null, - 'handle_files' => false, - ), - $params - ); + $related_fields = $pod->fields(); + + if ( $related_fields ) { + $related_fields = wp_list_pluck( $related_fields, 'label', 'name' ); + + foreach ( $columns as $k => $column ) { + $field_found = array_search( $column, $related_fields, true ); + + if ( $field_found ) { + $columns[ $k ] = $field_found; + } + } + } + + /** + * Filter list field columns for relationship field mapping purposes. + * + * @param array $columns List field columns. + * @param array $form GF form data. + * @param GF_Field $gf_field GF field data. + * @param Pods $pod Pods object. + * + * @since 1.4 + */ + $columns = (array) gf_apply_filters( array( 'pods_gf_field_columns_mapping', $form['id'], $gf_field->id ), $columns, $form, $gf_field, $pod ); + + return $columns; + + } + + /** + * Get GF field value + * + * @param mixed $value + * @param array $params + * + * @return mixed + */ + public static function get_gf_field_value( $value, $params ) { - $gf_field = $params['gf_field']; - $full_field = $params['field']; - $form = $params['form']; - $options = $params['options']; - $handle_files = $params['handle_files']; + static $cached_field_value = array(); + + $params = array_merge( array( + 'gf_field' => null, + 'gf_field_options' => array(), + 'field' => null, + 'field_options' => array(), + 'pod' => null, + 'form' => null, + 'entry' => null, + 'options' => array(), + 'handle_files' => false, + ), $params ); + + $gf_field = $params['gf_field']; + $gf_field_options = $params['gf_field_options']; + $full_field = $params['field']; + $field_options = $params['field_options']; + $pod = $params['pod']; + $form = $params['form']; + $entry = $params['entry']; + $options = $params['options']; + $handle_files = $params['handle_files']; + + if ( empty( $entry ) && ! empty( $options['entry'] ) ) { + $entry = $options['entry']; + } if ( is_array( $gf_field ) ) { /** @@ -2994,18 +3300,69 @@ public static function get_gf_field_value( $value, $params ) { $full_field = $gf_field->id; } + $cache_key = $form['id'] . ':' . $full_field; + + if ( ! empty( $field_options['id'] ) ) { + $cache_key .= ':' . $field_options['id']; + } + + if ( isset( $cached_field_value[ $cache_key ] ) ) { + return $cached_field_value[ $cache_key ]['value']; + } + if ( null === $value ) { - if ( ! empty( $options['entry'] ) && isset( $options['entry'][ $full_field ] ) ) { - $value = rgar( $options['entry'], $full_field ); + if ( ! empty( $entry ) && isset( $entry[ $full_field ] ) ) { + $value = rgar( $entry, $full_field ); } else { + $forced_is_submit = false; + + $tmp_post = $_POST; + + if ( empty( $_POST[ 'is_submit_' . $form['id'] ] ) ) { + // We need to force is_submit_{$form_id} on. + $forced_is_submit = true; + + $_POST[ 'is_submit_' . $form['id'] ] = true; + + if ( ! empty( $gf_field->inputs ) && is_array( $gf_field->inputs ) ) { + // Handle multi input fields. + foreach ( $entry as $entry_field => $entry_value ) { + if ( 0 === strpos( $entry_field, $gf_field->id . '.' ) ) { + $new_entry_field = str_replace( '.', '_', $entry_field ); + + $_POST[ 'input_' . $entry_field ] = $entry_value; + $_POST[ 'input_' . $new_entry_field ] = $entry_value; + } + } + } + } + $value = GFFormsModel::get_field_value( $gf_field ); - if ( is_array( $value ) && ! empty( $gf_field->inputs ) && isset( $value[ $full_field ] ) ) { - $value = $value[ $full_field ]; + if ( $forced_is_submit ) { + unset( $_POST[ 'is_submit_' . $form['id'] ] ); } + + $_POST = $tmp_post; } } + if ( is_array( $value ) && ! empty( $gf_field->inputs ) && isset( $value[ $full_field ] ) ) { + $value = $value[ $full_field ]; + } + + if ( 'multiselect' === $gf_field->type && ! is_array( $value ) && ! empty( $value ) && 0 === strpos( $value, '[' ) ) { + $check_value = json_decode( $value ); + + if ( is_array( $check_value ) ) { + $value = $check_value; + } + } + + if ( 'list' === $gf_field->type && ! is_array( $value ) && ! empty( $value ) ) { + $value = maybe_unserialize( $value ); + } + if ( in_array( $gf_field->type, array( 'post_category', 'post_title', 'post_content', 'post_excerpt', 'post_tags', 'post_custom_field', 'post_image' ) ) ) { // Block new post being created in GF add_filter( 'gform_disable_post_creation_' . $form['id'], '__return_true' ); @@ -3025,6 +3382,8 @@ public static function get_gf_field_value( $value, $params ) { if ( in_array( $gf_field->type, array( 'name' ), true ) && is_array( $value ) ) { $value = implode( ' ', array_filter( $value ) ); + } elseif ( in_array( $gf_field->type, array( 'email' ), true ) && is_array( $value ) ) { + $value = current( $value ); } elseif ( in_array( $gf_field->type, array( 'checkbox', 'post_category', 'post_tags' ), true ) && is_array( $value ) ) { foreach ( $value as $k => $v ) { if ( '' === $v ) { @@ -3043,10 +3402,208 @@ public static function get_gf_field_value( $value, $params ) { } } } - } elseif ( in_array( $gf_field->type, array( 'address' ), true ) && is_array( $value ) ) { - $value = implode( ', ', array_filter( $value ) ); - } elseif ( in_array( $gf_field->type, array( 'time' ), true ) && is_array( $value ) ) { - $value = sprintf( '%d:%d %s', $value[0], $value[1], $value[2] ); + + if ( 'boolean' === pods_v( 'type', $field_options ) ) { + if ( ! empty( $value ) ) { + $value = 1; + } else { + $value = 0; + } + } + } elseif ( in_array( $gf_field->type, array( 'address' ), true ) ) { + if ( is_array( $value ) ) { + $value = implode( ', ', array_filter( $value ) ); + } elseif ( 'pick' === pods_v( 'type', $field_options ) ) { + $pick_object = pods_v( 'pick_object', $field_options ); + + if ( 'country' === $pick_object ) { + $value_check = $gf_field->get_country_code( $value ); + + if ( $value_check ) { + $value = $value_check; + } + } elseif ( 'us_state' === $pick_object ) { + $value = $gf_field->get_us_state_code( $value ); + + if ( $value_check ) { + $value = $value_check; + } + } elseif ( 'ca_province' === $pick_object ) { + $provinces = $pod->fields( $field_options['name'], 'data' ); + + if ( $provinces ) { + $provinces = array_map( 'strtoupper', $provinces ); + + $value_check = array_search( strtoupper( $value ), $provinces, true ); + + if ( false !== $value_check ) { + $value = $value_check; + } + } + } + } + } elseif ( in_array( $gf_field->type, array( 'date' ), true ) ) { + $format = empty( $gf_field->dateFormat ) ? 'mdy' : esc_attr( $gf_field->dateFormat ); + $value = GFcommon::parse_date( $value, $format ); + + if ( ! empty( $value ) ) { + $value = array_map( 'absint', $value ); + + if ( $value['month'] < 10 ) { + $value['month'] = '0' . $value['month']; + } + + if ( $value['day'] < 10 ) { + $value['day'] = '0' . $value['day']; + } + + // Format as: Y-m-d + $value = sprintf( '%s-%s-%s', $value['year'], $value['month'], $value['day'] ); + } else { + $value = ''; + } + } elseif ( in_array( $gf_field->type, array( 'time' ), true ) ) { + $format = empty( $gf_field->timeFormat ) ? '12' : esc_attr( $gf_field->timeFormat ); + + if ( ! is_array( $value ) ) { + if ( preg_match( '/^(\d{1,2}):(\d{1,2}) (\w{2})$/', $value, $matches ) ) { + $value = array( + $matches[1], + $matches[2], + $matches[3], + ); + + $format = '12'; + } elseif ( preg_match( '/^(\d{1,2}):(\d{1,2})$/', $value, $matches ) ) { + $value = array( + $matches[1], + $matches[2], + ); + + $format = '24'; + } + } + + if ( is_array( $value ) ) { + // Enforce max value using min(). + $value[0] = min( absint( $value[0] ), 23 ); + $value[1] = min( absint( $value[1] ), 59 ); + + // Handle am/pm conversion. + if ( '12' === $format ) { + $ampm = strtolower( $value[2] ); + + if ( 'pm' === $ampm ) { + $value[0] += 12; + } + + if ( 24 === $value[0] || ( 'am' === $ampm && 12 === $value[0] ) ) { + $value[0] = 0; + } + } + + if ( $value[0] < 10 ) { + $value[0] = '0' . $value[0]; + } + + if ( $value[1] < 10 ) { + $value[1] = '0' . $value[1]; + } + + // Format as: H:i + $value = sprintf( '%s:%s:00', $value[0], $value[1] ); + } + } elseif ( in_array( $gf_field->type, array( 'list' ), true ) && is_array( $value ) && ! empty( $value ) ) { + $columns = array_keys( current( $value ) ); + + if ( $columns ) { + $related_obj = false; + + // Attempt to detect the field names from related pod field labels + if ( is_object( $pod ) && ! empty( $field_options['type'] ) && 'pick' === $field_options['type'] ) { + $object_type = $field_options['pick_object']; + $object = $field_options['pick_val']; + + if ( 'table' === $object_type ) { + $pick_val = pods_v( 'pick_table', $field_options['options'], $object, true ); + } + + if ( 'pod' === $object_type ) { + $related_obj = pods( $object, null, false ); + } else { + $table = $pod->api->get_table_info( $object_type, $object, null, null, $field_options ); + + if ( ! empty( $table['pod'] ) ) { + $related_obj = pods( $table['pod']['name'], null, false ); + } + } + } + + if ( $related_obj ) { + $columns = self::match_pod_fields_to_list_columns( $related_obj, $columns, $form, $gf_field ); + + $related_id_field = $related_obj->data->field_id; + + $related_ids = array(); + + // Handle insert/update of relationship data. + foreach ( $value as $k => $row ) { + $row = array_combine( $columns, $row ); + + /** + * Filter list field row for relationship field saving purposes. + * + * @param array $row List field row. + * @param array $columns List field columns. + * @param int $form_id GF form data. + * @param GF_Field $gf_field GF field data. + * @param array $options Pods GF options. + * @param Pods|false $related_obj Related Pod object. + * + * @since 1.4 + */ + $row = (array) gf_apply_filters( array( 'pods_gf_field_column_row', $form['id'], $gf_field->id ), $row, $columns, $form, $gf_field, $options, $related_obj ); + + $related_id = 0; + + if ( isset( $row[ $related_id_field ] ) ) { + $related_id = $row[ $related_id_field ]; + + unset( $row[ $related_id_field ] ); + } + + if ( ! empty( $related_id ) ) { + $related_obj->save( $row, null, $related_id ); + } else { + $row_has_values = array_filter( $row ); + + if ( ! $row_has_values ) { + continue; + } + + $related_id = $related_obj->add( $row ); + } + + if ( $related_id ) { + $related_ids[] = $related_id; + } + } + + $single_multi = pods_v( $field_options['type'] . '_format_type', $field_options['options'], 'single' ); + + if ( 'single' === $single_multi ) { + if ( $related_ids ) { + $related_ids = current( $related_ids ); + } else { + $related_ids = 0; + } + } + + $value = $related_ids; + } + } else { + $value = null; + } } elseif ( $handle_files && in_array( $gf_field->type, array( 'fileupload', 'post_image' ), true ) ) { $value = null; @@ -3058,8 +3615,8 @@ public static function get_gf_field_value( $value, $params ) { // Form already submitted if ( ! empty( $options['gf_to_pods_priority'] ) && 'submission' === $options['gf_to_pods_priority'] ) { if ( empty( $attachments ) ) { - if ( ! empty( $options['entry'] ) ) { - $file_value = rgar( $options['entry'], $gf_field->id ); + if ( ! empty( $entry ) ) { + $file_value = rgar( $entry, $gf_field->id ); $file_value = trim( $file_value, '|' ); if ( ! empty( $file_value ) ) { @@ -3239,8 +3796,13 @@ public static function get_gf_field_value( $value, $params ) { continue; } + $attachment = explode( '|:|', $attachment ); + $attachment = $attachment[0]; + if ( is_string( $attachment ) ) { $attachment_id = pods_attachment_import( $attachment ); + } else { + $attachment_id = absint( $attachment ); } if ( 0 < $attachment_id ) { @@ -3258,6 +3820,14 @@ public static function get_gf_field_value( $value, $params ) { } } + if ( is_string( $value ) && ! empty( $gf_field_options['gf_merge_tags'] ) ) { + $value = GFCommon::replace_variables( $value, $form, $entry ); + } + + $cached_field_value[ $cache_key ] = array( + 'value' => $value, + ); + return $value; } @@ -3272,7 +3842,7 @@ public static function get_gf_field_value( $value, $params ) { * * @return array GF validation result */ - public function _gf_field_validation ( $validation_result, $value, $form, $field ) { + public function _gf_field_validation( $validation_result, $value, $form, $field ) { if ( ! $validation_result['is_valid'] ) { return $validation_result; @@ -3347,14 +3917,26 @@ public function _gf_field_validation ( $validation_result, $value, $form, $field $pods_api = pods_api(); $gf_params = array( - 'gf_field' => $field, - 'field' => $field_full, - 'form' => $form, - 'options' => $this->options, + 'gf_field' => $field, + 'gf_field_options' => $field_options, + 'field' => $field_full, + 'field_options' => $field_data, + 'pod' => $this->pod, + 'form' => $form, + 'entry' => GFFormsModel::get_current_lead(), + 'options' => $this->options, ); $gf_value = self::get_gf_field_value( $value, $gf_params ); + // If a file is not set, check if we are editing an item. + if ( null === $gf_value && in_array( $field->type, array( 'fileupload', 'post_image' ), true ) ) { + // If we are editing an item, return normal result, don't attempt to save. + if ( is_object( $this->pod ) && $this->pod->id ) { + return $validation_result; + } + } + $validate = $pods_api->handle_field_validation( $gf_value, $field_options['field'], $this->pod->pod_data['object_fields'], $this->pod->pod_data['fields'], $this->pod, null ); } } @@ -3396,7 +3978,7 @@ public function _gf_field_validation ( $validation_result, $value, $form, $field * * @return array GF Validation result */ - public function _gf_validation ( $validation_result ) { + public function _gf_validation( $validation_result ) { if ( ! $validation_result['is_valid'] ) { return $validation_result; @@ -3442,7 +4024,7 @@ public function _gf_validation ( $validation_result ) { * * @return null */ - public function _gf_validation_message ( $validation_message, $form ) { + public function _gf_validation_message( $validation_message, $form ) { if ( isset( self::$actioned[$form['id']] ) && in_array( __FUNCTION__, self::$actioned[$form['id']] ) ) { return $validation_message; @@ -3493,17 +4075,29 @@ public function _gf_entry_pre_save_id( $lead_id, $form ) { } /** - * Action handler for Gravity Forms: gform_pre_submission_filter_{$form_id} + * Setup form object based on features available. * - * @param array $form GF Form array + * @param array $form Form object. + * + * @return array Form object. */ - public function _gf_pre_submission_filter ( $form ) { + public function setup_form( $form ) { + + static $setup = array(); + + if ( isset( $setup[ $form['id'] ] ) ) { + return $setup[ $form['id'] ]; + } // Add Dynamic Selects if ( isset( $this->options['dynamic_select'] ) && ! empty( $this->options['dynamic_select'] ) ) { - $form = self::gf_dynamic_select( $form, false, $this->options['dynamic_select'] ); + foreach ( $this->options['dynamic_select'] as $field_id => $dynamic_select ) { + self::dynamic_select( $form['id'], $field_id, $dynamic_select ); + } } + $form = self::gf_dynamic_select( $form ); + // Read Only handling if ( isset( $this->options['read_only'] ) && ! empty( $this->options['read_only'] ) ) { $read_only = array( @@ -3517,10 +4111,81 @@ public function _gf_pre_submission_filter ( $form ) { $read_only = array_merge( $read_only, $this->options['read_only'] ); } - $form = self::gf_read_only( $form, false, $read_only ); + self::read_only( $form['id'], $read_only['fields'], $read_only['exclude_fields'] ); } - return $form; + $form = self::gf_read_only( $form ); + + // Prepopulate values + if ( isset( $this->options['prepopulate'] ) && ! empty( $this->options['prepopulate'] ) ) { + $prepopulate = array( + 'pod' => $this->pod, + 'id' => pods_v( 'save_id', $this->options, $this->get_current_id(), true ), + 'fields' => $this->options['fields'] + ); + + if ( is_array( $this->options['prepopulate'] ) ) { + $prepopulate = array_merge( $prepopulate, $this->options['prepopulate'] ); + } + + self::prepopulate( $form['id'], $prepopulate['pod'], $prepopulate['id'], $prepopulate['fields'] ); + } + + $form = self::gf_prepopulate( $form ); + + // Markdown Syntax for HTML + if ( isset( $this->options['markdown'] ) && ! empty( $this->options['markdown'] ) ) { + $form = self::gf_markdown( $form, $ajax, $this->options['markdown'] ); + } + + // Submit Button customization + if ( isset( $this->options['submit_button'] ) && ! empty( $this->options['submit_button'] ) ) { + if ( is_array( $this->options['submit_button'] ) ) { + if ( isset( $this->options['submit_button']['imageUrl'] ) ) { + $this->options['submit_button']['type'] = 'imageUrl'; + } + elseif ( isset( $this->options['submit_button']['text'] ) ) { + $this->options['submit_button']['type'] = 'text'; + } + + $button = $this->options['submit_button']; + } + elseif ( ( false !== strpos( $this->options['submit_button'], '://' ) && strpos( $this->options['submit_button'], '://' ) < 6 ) || 0 === strpos( $this->options['submit_button'], '/' ) ) { + $button = array( + 'imageUrl' => $this->options['submit_button'], + 'type' => 'imageUrl' + ); + } + else { + $button = array( + 'text' => $this->options['submit_button'], + 'type' => 'text' + ); + } + + $form['button'] = $button; + } + + // Secondary Submit actions + if ( isset( $this->options['secondary_submits'] ) && ! empty( $this->options['secondary_submits'] ) ) { + self::secondary_submits( $form['id'], $this->options['secondary_submits'] ); + } + + // Save form object for later use. + $setup[ $form['id'] ] = $form; + + return $setup[ $form['id'] ]; + + } + + /** + * Action handler for Gravity Forms: gform_pre_submission_filter_{$form_id} + * + * @param array $form GF Form array + */ + public function _gf_pre_submission_filter( $form ) { + + return $this->setup_form( $form ); } @@ -3530,7 +4195,7 @@ public function _gf_pre_submission_filter ( $form ) { * @param array $lead GF Entry array * @param array $form GF Form array */ - public function _gf_entry_post_save ( $lead, $form ) { + public function _gf_entry_post_save( $lead, $form ) { global $wpdb; @@ -3599,120 +4264,212 @@ public function _gf_entry_post_save ( $lead, $form ) { * @param array $entry GF Entry array * @param array $form GF Form array */ - public function _gf_after_submission ( $entry, $form ) { + public function _gf_after_submission( $entry, $form ) { if ( empty( $this->gf_validation_message ) ) { - remove_action( 'gform_post_submission_' . $form['id'], array( $this, '_gf_after_submission' ), 10 ); - - if ( isset( self::$actioned[$form['id']] ) && in_array( __FUNCTION__, self::$actioned[$form['id']] ) ) { - return $entry; - } - elseif ( ! isset( self::$actioned[$form['id']] ) ) { - self::$actioned[$form['id']] = array(); - } + return $entry; + } - self::$actioned[$form['id']][] = __FUNCTION__; + remove_action( 'gform_post_submission_' . $form['id'], array( $this, '_gf_after_submission' ), 10 ); - if ( is_array( $this->pod ) ) { - $this->id = $entry['id']; - } + if ( isset( self::$actioned[$form['id']] ) && in_array( __FUNCTION__, self::$actioned[$form['id']] ) ) { + return $entry; + } + elseif ( ! isset( self::$actioned[$form['id']] ) ) { + self::$actioned[$form['id']] = array(); + } - $this->entry_id = $entry['id']; + self::$actioned[$form['id']][] = __FUNCTION__; - if ( empty( $this->options ) ) { - return $entry; - } + if ( is_array( $this->pod ) ) { + $this->id = $entry['id']; + } - // Alternative gf_to_pods handling - if ( ! empty( $this->options['gf_to_pods_priority'] ) && 'submission' == $this->options['gf_to_pods_priority'] ) { - try { - $this->options['entry'] = $entry; + $this->entry_id = $entry['id']; - $this->_gf_to_pods_handler( $form ); - } - catch ( Exception $e ) { - // @todo Log something to the form entry - } - } + if ( empty( $this->options ) ) { + return $entry; + } - // Send notifications - self::gf_notifications( $entry, $form, $this->options ); + // Alternative gf_to_pods handling + if ( ! empty( $this->options['gf_to_pods_priority'] ) && 'submission' == $this->options['gf_to_pods_priority'] ) { + try { + $this->options['entry'] = $entry; - if ( pods_v( 'auto_delete', $this->options, false ) ) { - self::gf_delete_entry( $entry ); + $this->_gf_to_pods_handler( $form, $entry ); } + catch ( Exception $e ) { + // @todo Log something to the form entry + } + } - do_action( 'pods_gf_after_submission_' . $form['id'], $entry, $form ); - do_action( 'pods_gf_after_submission', $entry, $form ); + // Send notifications + self::gf_notifications( $entry, $form, $this->options ); - // Redirect after - if ( pods_v( 'redirect_after', $this->options, false ) ) { - // Handle secondary submits and redirect to next ID - $secondary_submits = (array) pods_v( 'secondary_submits', $this->options, array() ); + if ( pods_v( 'auto_delete', $this->options, false ) ) { + self::gf_delete_entry( $entry ); + } - if ( ! empty( $secondary_submits ) ) { - if ( isset( $secondary_submits['action'] ) ) { - $secondary_submits = array( $secondary_submits ); - } + do_action( 'pods_gf_after_submission_' . $form['id'], $entry, $form ); + do_action( 'pods_gf_after_submission', $entry, $form ); - $defaults = array( - 'imageUrl' => null, - 'text' => 'Alt Submit', - 'action' => 'alt', - 'value' => 1, - 'value_from_ui' => '' - ); + // Redirect after + if ( pods_v( 'redirect_after', $this->options, false ) ) { + // Handle secondary submits and redirect to next ID + $secondary_submits = (array) pods_v( 'secondary_submits', $this->options, array() ); - foreach ( $secondary_submits as $secondary_submit ) { - $secondary_submit = array_merge( $defaults, $secondary_submit ); + if ( ! empty( $secondary_submits ) ) { + if ( isset( $secondary_submits['action'] ) ) { + $secondary_submits = array( $secondary_submits ); + } - // Not set - if ( ! isset( $_POST['pods_gf_ui_action_' . $secondary_submit['action']] ) ) { - continue; - } - // No value - elseif ( empty( $_POST['pods_gf_ui_action_' . $secondary_submit['action']] ) ) { - break; - } - // Not auto handling - elseif ( ! in_array( $secondary_submit['value_from_ui'], array( 'next_id', 'prev_id' ) ) ) { - break; - } + $defaults = array( + 'imageUrl' => null, + 'text' => 'Alt Submit', + 'action' => 'alt', + 'value' => 1, + 'value_from_ui' => '' + ); - pods_redirect( add_query_arg( array( 'id' => (int) $_POST['pods_gf_ui_action_' . $secondary_submit['action']] ) ) ); + foreach ( $secondary_submits as $secondary_submit ) { + $secondary_submit = array_merge( $defaults, $secondary_submit ); + // Not set + if ( ! isset( $_POST['pods_gf_ui_action_' . $secondary_submit['action']] ) ) { + continue; + } + // No value + elseif ( empty( $_POST['pods_gf_ui_action_' . $secondary_submit['action']] ) ) { break; } + // Not auto handling + elseif ( ! in_array( $secondary_submit['value_from_ui'], array( 'next_id', 'prev_id' ) ) ) { + break; + } + + pods_redirect( add_query_arg( array( 'id' => (int) $_POST['pods_gf_ui_action_' . $secondary_submit['action']] ) ) ); + + break; } + } - $confirmation = self::gf_confirmation( $form['confirmation'], $form, $entry, false, true ); + $confirmation = self::gf_confirmation( $form['confirmation'], $form, $entry, false, true ); + + if ( ! is_array( $confirmation ) || 'redirect' != $confirmation['type'] || ( ! isset( $confirmation['url'] ) && ! isset( $confirmation['redirect'] ) ) ) { + pods_redirect( pods_var_update( array( 'action' => 'edit', 'id' => $this->get_current_id() ) ) ); + } + else { + $url = false; - if ( ! is_array( $confirmation ) || 'redirect' != $confirmation['type'] || ( ! isset( $confirmation['url'] ) && ! isset( $confirmation['redirect'] ) ) ) { - pods_redirect( pods_var_update( array( 'action' => 'edit', 'id' => $this->get_current_id() ) ) ); + if ( isset( $confirmation['url'] ) ) { + $url = $confirmation['url']; } - else { - $url = false; + elseif ( isset( $confirmation['redirect'] ) ) { + $url = $confirmation['redirect']; + } + + if ( $url ) { + $gf_to_pods_id = 0; + $gf_to_pods_permalink = ''; - if ( isset( $confirmation['url'] ) ) { - $url = $confirmation['url']; + if ( ! empty( self::$gf_to_pods_id[ $form['id'] ] ) ) { + $gf_to_pods_id = self::$gf_to_pods_id[ $form['id'] ]; } - elseif ( isset( $confirmation['redirect'] ) ) { - $url = $confirmation['redirect']; + + if ( ! empty( self::$gf_to_pods_id[ $form['id'] . '_permalink' ] ) ) { + $gf_to_pods_permalink = self::$gf_to_pods_id[ $form['id'] . '_permalink' ]; } - if ( $url ) { - $gf_to_pods_id = 0; + $url = str_replace( '{@gf_to_pods_id}', $gf_to_pods_id, $url ); + $url = str_replace( '{@gf_to_pods_permalink}', $gf_to_pods_permalink, $url ); - if ( ! empty( self::$gf_to_pods_id[ $form['id'] ] ) ) { - $gf_to_pods_id = self::$gf_to_pods_id[ $form['id'] ]; - } + pods_redirect( $url ); + } + } + } - $url = str_replace( '{@gf_to_pods_id}', $gf_to_pods_id, $url ); + return $entry; - pods_redirect( $url ); - } - } + } + + /** + * Action handler for Gravity Forms: gform_post_update_entry_{$form_id} + * + * @param array $entry GF Entry array + * @param array $original_entry Original GF Entry array + */ + public function _gf_post_update_entry( $entry, $original_entry ) { + + if ( empty( $this->options['update_pod_item'] ) ) { + return $entry; + } + + $form_id = $entry['form_id']; + + $form = GFAPI::get_form( $form_id ); + + if ( $form && empty( $this->gf_validation_message ) ) { + if ( isset( self::$actioned[ $form['id'] ] ) && in_array( __FUNCTION__, self::$actioned[ $form['id'] ] ) ) { + return $entry; + } elseif ( ! isset( self::$actioned[ $form['id'] ] ) ) { + self::$actioned[ $form['id'] ] = array(); + } + + self::$actioned[$form['id']][] = __FUNCTION__; + + return $this->_gf_after_update_entry( $form, $entry, $original_entry ); + } + + } + + /** + * Action handler for Gravity Forms: gform_after_update_entry_{$form_id} + * + * @param array $form GF Form array + * @param array $entry GF Entry array + * @param array $original_entry Original GF Entry array + */ + public function _gf_after_update_entry( $form, $entry, $original_entry ) { + + if ( empty( $this->options['update_pod_item'] ) ) { + return $entry; + } + + if ( ! is_array( $entry ) ) { + $entry = GFAPI::get_entry( $entry ); + } + + if ( empty( $this->gf_validation_message ) ) { + if ( isset( self::$actioned[$form['id']] ) && in_array( __FUNCTION__, self::$actioned[$form['id']] ) ) { + return $entry; + } + elseif ( ! isset( self::$actioned[$form['id']] ) ) { + self::$actioned[$form['id']] = array(); } + + self::$actioned[$form['id']][] = __FUNCTION__; + + if ( is_array( $this->pod ) ) { + $this->id = $entry['id']; + } + + $this->entry_id = $entry['id']; + + if ( empty( $this->options ) ) { + return $entry; + } + + try { + $this->options['entry'] = $entry; + + $this->_gf_to_pods_handler( $form, $entry ); + } + catch ( Exception $e ) { + // @todo Log something to the form entry + } + + do_action( 'pods_gf_after_update_entry_' . $form['id'], $entry, $form ); + do_action( 'pods_gf_after_update_entry', $entry, $form ); } return $entry; @@ -3752,7 +4509,7 @@ public function get_current_id() { * * @return null|mixed Value of the variable key or default value */ - public static function v ( $name, $var, $default = null, $strict = false ) { + public static function v( $name, $var, $default = null, $strict = false ) { $value = $default; diff --git a/includes/Pods_GF_Addon.php b/includes/Pods_GF_Addon.php index 2ab409f..9a4822c 100755 --- a/includes/Pods_GF_Addon.php +++ b/includes/Pods_GF_Addon.php @@ -1,20 +1,58 @@ true, ); - $selected_pod = $this->get_setting( 'pod' ); - $pod_fields = array(); - $pod_type = ''; - - $custom_name = '_gaddon_setting_pod_fields_custom_override_%s'; - $custom_value_name = 'pod_fields_custom_override_%s'; - - $after_select = ' -
    - - -
    - '; - - $after_select = sprintf( - $after_select, - esc_attr( $custom_name ), - esc_html__( 'Override value', 'pods-gravity-forms' ), - esc_attr( $custom_name ), - esc_attr__( 'Enter text here', 'pods-gravity-forms' ), - esc_attr( $custom_name ) - ); + $selected_pod = $this->get_setting( 'pod' ); + $enable_current_post = (int) $this->get_setting( 'enable_current_post' ); + $enable_current_user = (int) $this->get_setting( 'enable_current_user' ); + + $posted_settings = $this->get_posted_settings(); + + if ( isset( $posted_settings['enable_current_post'] ) ) { + $enable_current_post = (int) $posted_settings['enable_current_post']; + } + + if ( isset( $posted_settings['enable_current_user'] ) ) { + $enable_current_user = (int) $posted_settings['enable_current_user']; + } + + $pod_fields = array(); + $pod_type = ''; if ( ! empty( $selected_pod ) ) { $pod_object = $pods_api->load_pod( array( 'name' => $selected_pod ) ); @@ -208,7 +242,21 @@ public function feed_settings_fields() { continue; } - $wp_object_fields[] = array( + if ( in_array( $pod_type, array( 'post_type', 'media' ), true ) ) { + if ( in_array( $name, array( 'post_title', 'post_content' ), true ) ) { + $field['options']['required'] = 1; + } + } elseif ( 'taxonomy' === $pod_type ) { + if ( 'name' === $name ) { + $field['options']['required'] = 1; + } + } elseif ( 'user' === $pod_type ) { + if ( 'user_login' === $name ) { + $field['options']['required'] = 1; + } + } + + $wp_object_fields[ $name ] = array( 'needs_process' => true, 'name' => $name, 'field' => $field, @@ -217,7 +265,7 @@ public function feed_settings_fields() { } if ( 'post_type' === $pod_type ) { - $wp_object_fields[] = array( + $wp_object_fields['_thumbnail_id'] = array( 'name' => '_thumbnail_id', 'label' => __( 'Featured Image', 'pods-gravity-forms' ), ); @@ -228,11 +276,16 @@ public function feed_settings_fields() { 'label' => __( 'WP Object Fields', 'pods-gravity-forms' ), 'type' => 'field_map', 'dependency' => 'pod', - 'field_map' => $wp_object_fields, + 'field_map' => array_values( $wp_object_fields ), ); - $settings = array( - 'title' => __( 'Pods Feed Settings', 'pods-gravity-forms' ), + $settings = array(); + + /////////////////// + // Pod feed mapping + /////////////////// + $settings['pod_mapping'] = array( + 'title' => __( 'Pod Feed Mapping', 'pods-gravity-forms' ), 'fields' => array( $feed_field_name, $feed_field_pod, @@ -241,52 +294,49 @@ public function feed_settings_fields() { ); if ( ! empty( $feed_field_wp_object_fields['field_map'] ) ) { - $settings['fields'][] = $feed_field_wp_object_fields; + $settings['pod_mapping']['fields'][] = $feed_field_wp_object_fields; } + $blacklisted_keys = array(); + // Build field mapping data arrays - foreach ( $settings['fields'] as $k => $field_set ) { + foreach ( $settings['pod_mapping']['fields'] as $k => $field_set ) { if ( empty( $field_set['field_map'] ) ) { continue; } foreach ( $field_set['field_map'] as $kf => $field_map ) { + $blacklisted_keys[] = $field_map['name']; + if ( ! empty( $field_map['needs_process'] ) ) { $name = $field_map['name']; $field = $field_map['field']; - $field_value = $this->get_setting( $name ); - $custom_field_value = $this->get_setting( sprintf( $custom_value_name, $name ) ); - $field_required = false; if ( isset( $field['options']['required'] ) && 1 === (int) $field['options']['required'] ) { $field_required = true; - } - - $container_class = ' hidden'; - if ( '_pods_custom' === $field_value ) { - $container_class = ''; + if ( isset( $wp_object_fields[ $name ] ) ) { + if ( in_array( $pod_type, array( 'post_type', 'media' ), true ) && 1 === $enable_current_post ) { + $field_required = false; + } elseif ( 'user' === $pod_type && 1 === $enable_current_user ) { + $field_required = false; + } + } } $field_map = array( 'name' => $name, 'label' => $field['label'], 'required' => $field_required, - 'after_select' => sprintf( - $after_select, - esc_attr( $container_class ), - esc_attr( $name ), - esc_attr( $name ), - esc_attr( $custom_field_value ), - esc_attr( $name ) - ), ); - foreach ( $gf_fields as $gf_field ) { - if ( $field['label'] === $gf_field['label'] ) { - $field_map['default_value'] = $gf_field['id']; + if ( 0 === (int) pods_v( 'fid' ) ) { + foreach ( $gf_fields as $gf_field ) { + if ( strtolower( $field['label'] ) === strtolower( $gf_field['label'] ) ) { + $field_map['default_value'] = $gf_field['id']; + } } } } @@ -298,24 +348,59 @@ public function feed_settings_fields() { esc_html( $field_map['name'] ) ); - $settings['fields'][ $k ]['field_map'][ $kf ] = $field_map; + $settings['pod_mapping']['fields'][ $k ]['field_map'][ $kf ] = $field_map; } } - $settings['fields'][] = array( - 'name' => 'delete_entry', - 'label' => __( 'Delete Gravity Form Entry on submission', 'pods-gravity-forms' ), + /////////////////// + // Custom fields + /////////////////// + if ( in_array( $pod_type, array( 'post_type', 'taxonomy', 'user', 'media', 'comment' ), true ) ) { + $settings['custom_fields'] = array( + 'title' => esc_html__( 'Custom Fields', 'pods-gravity-forms' ), + 'fields' => array( + array( + 'name' => 'custom_fields', + 'label' => esc_html__( 'Custom Fields', 'pods-gravity-forms' ), + 'type' => 'generic_map', + 'key_field' => array( + 'choices' => $this->get_meta_field_map( $selected_pod, $pod_type, $blacklisted_keys ), + 'placeholder' => esc_html__( 'Custom Field Name', 'pods-gravity-forms' ), + 'title' => esc_html__( 'Name', 'pods-gravity-forms' ), + ), + 'value_field' => array( + 'choices' => 'form_fields', + 'custom_value' => false, + 'merge_tags' => true, + 'placeholder' => esc_html__( 'Custom Field Value', 'pods-gravity-forms' ), + ), + ), + ), + ); + } + + /////////////////// + // Advanced + /////////////////// + $settings['advanced'] = array( + 'title' => __( 'Advanced', 'pods-gravity-forms' ), + 'fields' => array(), + ); + + $settings['advanced']['fields'][] = array( + 'name' => 'update_pod_item', + 'label' => __( 'Support entry updates', 'pods-gravity-forms' ), 'type' => 'checkbox', 'choices' => array( array( 'value' => 1, - 'label' => __( 'Delete entry after processing', 'pods-gravity-forms' ), - 'name' => 'delete_entry', + 'label' => __( 'Update pod item if the entry is updated', 'pods-gravity-forms' ), + 'name' => 'update_pod_item', ), ), ); - $settings['fields'][] = array( + $settings['advanced']['fields'][] = array( 'name' => 'enable_markdown', 'label' => __( 'Enable Markdown', 'pods-gravity-forms' ), 'type' => 'checkbox', @@ -329,7 +414,7 @@ public function feed_settings_fields() { ); if ( 'user' === $pod_type ) { - $settings['fields'][] = array( + $settings['advanced']['fields'][] = array( 'name' => 'enable_current_user', 'label' => __( 'Enable editing with this form using logged in user', 'pods-gravity-forms' ), 'type' => 'checkbox', @@ -342,7 +427,7 @@ public function feed_settings_fields() { ), ); - $settings['fields'][] = array( + $settings['advanced']['fields'][] = array( 'name' => 'enable_prepopulate', 'label' => __( 'Enable populating field values for this form using logged in user', 'pods-gravity-forms' ), 'type' => 'checkbox', @@ -354,8 +439,8 @@ public function feed_settings_fields() { ), ), ); - } elseif ( 'post_type' === $pod_type ) { - $settings['fields'][] = array( + } elseif ( in_array( $pod_type, array( 'post_type', 'media' ), true ) ) { + $settings['advanced']['fields'][] = array( 'name' => 'enable_current_post', 'label' => __( 'Enable editing with this form using current post', 'pods-gravity-forms' ), 'type' => 'checkbox', @@ -368,7 +453,7 @@ public function feed_settings_fields() { ), ); - $settings['fields'][] = array( + $settings['advanced']['fields'][] = array( 'name' => 'enable_prepopulate', 'label' => __( 'Enable populating field values for this form using current post', 'pods-gravity-forms' ), 'type' => 'checkbox', @@ -382,35 +467,289 @@ public function feed_settings_fields() { ); } + $settings['advanced']['fields'][] = array( + 'name' => 'delete_entry', + 'label' => __( 'Delete Gravity Form Entry on submission', 'pods-gravity-forms' ), + 'type' => 'checkbox', + 'choices' => array( + array( + 'value' => 1, + 'label' => __( 'Delete entry after processing', 'pods-gravity-forms' ), + 'name' => 'delete_entry', + ), + ), + ); + $addon_slug = $this->get_slug(); add_filter( "gform_{$addon_slug}_field_map_choices", array( $this, 'add_field_map_choices' ) ); - $setting_fields = array( - $settings, + $settings['advanced']['fields'][] = array( + 'name' => 'feed_condition', + 'label' => __( 'Conditional Logic', 'pods-gravity-forms' ), + 'checkbox_label' => __( 'Enable', 'pods-gravity-forms' ), + 'type' => 'feed_condition', ); - return $setting_fields; + return $settings; + + } + + /** + * Prepare fields for meta field mapping. + * + * Props go to GF Post Creation add-on for the initial source of this method. + * + * @param string $pod_name Current pod name + * @param string $pod_type Current pod type + * @param string[] $blacklisted_keys Meta keys to exclude + * + * @uses GFFormsModel::get_custom_field_names() + * + * @return array + */ + public function get_meta_field_map( $pod_name = '', $pod_type = '', $blacklisted_keys = array() ) { + + // Setup meta fields array. + $meta_fields = array( + array( + 'label' => esc_html__( 'Select a Custom Field Name', 'pods-gravity-forms' ), + 'value' => '', + ), + ); + + /////////////////// + // Custom fields + /////////////////// + + // Get most used post meta keys + $meta_keys = $this->get_custom_field_names( $pod_name, $pod_type ); + + // If no meta keys exist, return an empty array. + if ( empty( $meta_keys ) ) { + return array(); + } + + // Add post meta keys to the meta fields array. + foreach ( $meta_keys as $meta_key ) { + $meta_fields[] = array( + 'label' => $meta_key, + 'value' => $meta_key, + ); + } + + /////////////////// + // Custom key + /////////////////// + $meta_fields[] = array( + 'label' => esc_html__( 'Add New Custom Field Name', 'pods-gravity-forms' ), + 'value' => 'gf_custom', + ); + + return $meta_fields; + + } + + /** + * Get most common custom field names from DB. + * + * @param string $pod_name Current pod name + * @param string $pod_type Current pod type + * @param string[] $blacklisted_keys Meta keys to exclude + * + * @return string[] + */ + public function get_custom_field_names( $pod_name = '', $pod_type = '', $blacklisted_keys = array() ) { + + global $wpdb; + + $object_table = $wpdb->posts; + $meta_table = $wpdb->postmeta; + $id_col = 'ID'; + $meta_id_col = 'post_id'; + $blacklist_where = " + AND `object`.`post_type` LIKE '_pods_%' + "; + + if ( 'taxonomy' === $pod_type ) { + $object_table = $wpdb->terms; + $meta_table = $wpdb->termmeta; + $id_col = 'term_id'; + $meta_id_col = 'term_id'; + $blacklist_where = ''; + } elseif ( 'user' === $pod_type ) { + $object_table = $wpdb->users; + $meta_table = $wpdb->usermeta; + $id_col = 'ID'; + $meta_id_col = 'user_id'; + $blacklist_where = ''; + } elseif ( 'comment' === $pod_type ) { + $object_table = $wpdb->users; + $meta_table = $wpdb->usermeta; + $id_col = 'comment_ID'; + $meta_id_col = 'comment_id'; + $blacklist_where = ''; + } + + $where = ''; + + $pods_blacklist_keys = array(); + + if ( $blacklist_where ) { + $sql = " + SELECT `meta`.`meta_key` + FROM `{$meta_table}` AS `meta` + LEFT JOIN `{$object_table}` AS `object` ON `object`.`{$id_col}` = `meta`.`{$meta_id_col}` + WHERE + `meta`.`meta_key` NOT LIKE '\_%' {$blacklist_where} + GROUP BY `meta`.`meta_key` + "; + + $pods_blacklist_keys = $wpdb->get_col( $sql ); + } + + $pods_blacklist_keys = array_merge( $pods_blacklist_keys, $blacklisted_keys ); + $pods_blacklist_keys = array_unique( $pods_blacklist_keys ); + $pods_blacklist_keys = array_filter( $pods_blacklist_keys ); + + if ( $pods_blacklist_keys ) { + $placeholders = array_fill( 0, count( $pods_blacklist_keys ), '%s' ); + + $where = " + AND `meta`.`meta_key` NOT IN ( " . implode( ", ", $placeholders ) . " ) + "; + + $where = $wpdb->prepare( $where, $pods_blacklist_keys ); + } + + $sql = " + SELECT `meta`.`meta_key`, COUNT(*) AS `total_count` + FROM `{$meta_table}` AS `meta` + WHERE + `meta`.`meta_key` NOT LIKE '\_%' {$where} + GROUP BY `meta`.`meta_key` + ORDER BY `total_count` DESC + LIMIT 50 + "; + + $meta_keys = $wpdb->get_col( $sql ); + + if ( $meta_keys ) { + natcasesort( $meta_keys ); + } + + return $meta_keys; } + /** + * @param $choices + * + * @return array + */ public function add_field_map_choices( $choices ) { - $choices[] = array( - 'value' => '_pods_custom', - 'label' => __( 'Custom override value', 'pods-gravity-forms' ), + // Remove first choice + array_shift( $choices ); + + $choices = array_merge( + array( + // Add first choice back + array( + 'value' => '', + 'label' => __( 'Select a Field', 'gravityforms' ), // Use gravtiyforms text domain here + ), + // Make custom override first option + array( + 'value' => 'gf_custom', + 'label' => __( 'Custom override value', 'pods-gravity-forms' ), + ), + ), + $choices, + array( + array( + 'value' => 'transaction_id', + 'label' => 'Transaction ID', + ), + array( + 'value' => 'payment_amount', + 'label' => 'Payment Amount', + ), + array( + 'value' => 'payment_date', + 'label' => 'Payment Date', + ), + array( + 'value' => 'payment_status', + 'label' => 'Payment Status', + ), + ) ); + foreach ( $choices as $k => $choice ) { + if ( '_pods_item_id' === $choice['value'] ) { + unset( $choices[ $k ] ); + + $choices = array_values( $choices ); + + break; + } + } + return $choices; } + /*** + * Renders and initializes a drop down field based on the $field array + * + * @param array $field - Field array containing the configuration options of this field + * @param bool $echo = true - true to echo the output to the screen, false to simply return the contents as a string + * + * @return string The HTML for the field + */ + public function settings_select( $field, $echo = true ) { + + $has_gf_custom = false; + + if ( ! empty( $field['choices'] ) ) { + foreach ( $field['choices'] as $choice ) { + if ( isset( $choice['value'] ) && 'gf_custom' === $choice['value'] ) { + $has_gf_custom = true; + + break; + } + } + } + + // Select has no custom choice or we already took over the first select. + if ( empty( $field['choices'] ) || ! $has_gf_custom || ! empty( $field['_pods_custom_select'] ) ) { + return parent::settings_select( $field, $echo ); + } + + // Already doing custom select. + if ( ! empty( $field['type'] ) && 'generic_map' === $field['type'] ) { + return parent::settings_select( $field, $echo ); + } + + $field['_pods_custom_select'] = true; + + return parent::settings_select_custom( $field, $echo ); + + } + + /** + * @return string|void + */ public function field_map_title() { return __( 'Pod Field', 'pods-gravity-forms' ); } + /** + * @return array + */ public function feed_list_columns() { return array( @@ -420,12 +759,20 @@ public function feed_list_columns() { } + /** + * @param $feed + * + * @return string + */ public function get_column_value_pod( $feed ) { return '' . $feed['meta']['pod'] . ''; } + /** + * + */ public function init_admin() { parent::init_admin(); @@ -436,6 +783,10 @@ public function init_admin() { } + /** + * @param $position + * @param $form_id + */ public function populate_related_items_settings( $position, $form_id ) { if ( -1 === $position ) { @@ -454,6 +805,9 @@ public function populate_related_items_settings( $position, $form_id ) { } + /** + * + */ public function populate_related_items_editor_script() { ?> @@ -471,6 +825,11 @@ public function populate_related_items_editor_script() { } + /** + * @param $tooltips + * + * @return mixed + */ public function populate_related_items_tooltip( $tooltips ) { $tooltips['form_populate_related_items_value'] = sprintf( '
    %s
    %s', __( 'Populate Related Items from Pods', 'pods-gravity-forms' ), __( 'Check this box to populate the related items from Pods instead of keeping the list up-to-date manually.' ) ); @@ -479,36 +838,209 @@ public function populate_related_items_tooltip( $tooltips ) { } + /** + * + */ public function init() { parent::init(); if ( $this->is_gravityforms_supported() ) { - add_filter( 'gform_pre_render', array( $this, '_gf_pre_render' ) ); + // Handle normal forms. + add_filter( 'gform_pre_render', array( $this, '_gf_pre_render' ), 9, 2 ); + add_filter( 'gform_admin_pre_render', array( $this, '_gf_pre_render' ), 9, 1 ); add_filter( 'gform_pre_process', array( $this, '_gf_pre_process' ) ); + + // Handle merge tags + add_filter( 'gform_custom_merge_tags', array( $this, '_gf_custom_merge_tags' ), 10, 2 ); + add_filter( 'gform_merge_tag_data', array( $this, '_gf_add_merge_tags' ), 10, 3 ); + add_filter( 'gform_replace_merge_tags', array( $this, '_gf_replace_merge_tags' ), 10, 2 ); + + // Handle entry detail edits. + add_action( 'gform_pre_entry_detail', array( $this, '_gf_pre_entry_detail' ), 10, 2 ); + add_action( 'check_admin_referer', array( $this, '_check_admin_referer' ), 10, 2 ); + add_action( 'gform_entry_detail_content_before', array( $this, '_gf_entry_detail_content_before' ), 10, 2 ); + + // Handle entry updates. + add_action( 'gform_post_update_entry', array( $this, '_gf_post_update_entry' ), 9, 2 ); + add_action( 'gform_after_update_entry', array( $this, '_gf_after_update_entry' ), 9, 3 ); + + // Handle Payment Add-on callbacks. + add_action( 'gform_action_pre_payment_callback', array( $this, '_gf_action_pre_payment_callback' ), 10, 2 ); + } + + } + + /** + * Processes feed action. + * + * @since Unknown + * @access public + * + * @param array $feed The Feed Object currently being processed. + * @param array $entry The Entry Object currently being processed. + * @param array $form The Form Object currently being processed. + * + * @return array|null Returns a modified entry object or null. + */ + public function process_feed( $feed, $entry, $form ) { + + if ( empty( $this->pods_gf[ $feed['id'] ] ) ) { + return null; + } + + $form = $this->_gf_pre_render( $form, $entry ); + + /** @var Pods_GF $pods_gf */ + $pods_gf = $this->pods_gf[ $feed['id'] ]; + + try { + $pods_gf->options['entry'] = $entry; + + $id = $pods_gf->_gf_to_pods_handler( $form, $entry ); + + // Set post_id if we have it. + if ( 'post_type' === $pods_gf->pod->pod_data['type'] ) { + $entry['post_id'] = $id; + + return $entry; + } + } + catch ( Exception $e ) { + // @todo Log something to the form entry + } + + return null; + + } + + /** + * Action handler for Gravity Forms: gform_action_pre_payment_callback. + * + * @param array $action Action data being saved. + * @param array $entry GF Entry array. + */ + public function _gf_action_pre_payment_callback( $action, $entry ) { + + $form = GFAPI::get_form( $entry['form_id'] ); + + $this->_gf_pre_process( $form ); + + } + + /** + * Action handler for Gravity Forms: gform_post_update_entry. + * + * @param array $entry GF Entry array + * @param array $original_entry Original GF Entry array + */ + public function _gf_post_update_entry( $entry, $original_entry ) { + + $form = GFAPI::get_form( $entry['form_id'] ); + + $this->_gf_pre_process( $form ); + + } + + /** + * Action handler for Gravity Forms: gform_after_update_entry. + * + * @param array $form GF Form array + * @param array $entry GF Entry array + * @param array $original_entry Original GF Entry array + */ + public function _gf_after_update_entry( $form, $entry, $original_entry ) { + + $this->_gf_pre_process( $form ); + + } + + /** + * Hook into action to setup Pods GF add-on hooks before form entry edit form is shown. + * + * @param array|object $form GF form data. + * @param array|object $entry GF entry data. + */ + public function _gf_pre_entry_detail( $form, $entry ) { + + // Remove other hooks for workarounds we don't need if this hook now exists. Not in GF when this was written. + remove_action( 'check_admin_referer', array( $this, '_check_admin_referer' ) ); + remove_action( 'gform_entry_detail_content_before', array( $this, '_gf_entry_detail_content_before' ) ); + + $this->_gf_pre_render( $form, $entry, true ); + + } + + /** + * Hook into check_admin_referer to setup Pods GF add-on hooks before updating form entry. + * + * @param string $action The nonce action. + * @param false|int $result False if the nonce is invalid, 1 if the nonce is valid and generated between + * 0-12 hours ago, 2 if the nonce is valid and generated between 12-24 hours ago. + */ + public function _check_admin_referer( $action, $result ) { + + // So hacky, we need GF to add a better hook than this. + if ( $result && 'gforms_save_entry' === $action ) { + $form = GFEntryDetail::get_current_form(); + + $this->_gf_pre_process( $form ); } } - public function _gf_pre_render( $form ) { + /** + * Hook into action to setup Pods GF add-on hooks before form entry edit form is shown. + * + * @param array|object $form GF form data. + * @param array|object $entry GF entry data. + */ + public function _gf_entry_detail_content_before( $form, $entry ) { + + $this->_gf_pre_render( $form, $entry, true ); + + } + + /** + * @param $form + * + * @return mixed + */ + public function _gf_pre_render( $form, $entry = null, $admin_edit = false ) { + + static $setup = array(); + + if ( ! empty( $setup[ $form['id'] ] ) ) { + return $setup[ $form['id'] ]; + } $feeds = $this->get_feeds( $form['id'] ); if ( empty( $feeds ) ) { - return $form; + $setup[ $form['id'] ] = $form; + + return $setup[ $form['id'] ]; } $pod_fields = array(); $pod_name = ''; $feed = null; + if ( empty( $entry ) ) { + $entry = GFFormsModel::get_current_lead(); + } + foreach ( $feeds as $feed ) { if ( 1 !== (int) $feed['is_active'] ) { continue; } - $pod_fields = $this->get_field_map_fields_with_custom_values( $feed, 'pod_fields' ); - $object_fields = $this->get_field_map_fields_with_custom_values( $feed, 'wp_object_fields' ); + if ( $admin_edit && empty( $feed['meta']['update_pod_item'] ) ) { + continue; + } + + $pod_fields = self::get_field_map_fields_with_custom_values( $feed, 'pod_fields' ); + $object_fields = self::get_field_map_fields_with_custom_values( $feed, 'wp_object_fields' ); $pod_fields = array_merge( $pod_fields, $object_fields ); @@ -521,9 +1053,13 @@ public function _gf_pre_render( $form ) { $pod_obj = pods( $pod_name, null, false ); if ( empty( $pod_obj ) || ! $pod_obj->valid() ) { - return $form; + $setup[ $form['id'] ] = $form; + + return $setup[ $form['id'] ]; } + $dynamic_selects = array(); + /** * @var GF_Field $gf_field */ @@ -541,167 +1077,323 @@ public function _gf_pre_render( $form ) { continue; } - $data = $pod_obj->fields( $pod_field, 'data' ); + $pod_field_options = $pod_obj->fields( $pod_field ); + + // Override limit for autocomplete + $object_params = array( + 'limit' => -1, + ); + + $data = PodsForm::field_method( $pod_field_options['type'], 'get_field_data', $pod_field_options, array(), $object_params ); if ( empty( $data ) ) { continue; } + if ( isset( $data[''] ) ) { + unset( $data[''] ); + } + + $select_text = pods_v( $pod_field_options['type'] . '_select_text', $pod_field_options['options'], __( '-- Select One --', 'pods' ), true ); + $options = array( 'options' => $data, ); - Pods_GF::dynamic_select( $form['id'], (string) $gf_field->id, $options ); + if ( $select_text ) { + $options['select_text'] = $select_text; + } + + $dynamic_selects[ $gf_field->id ] = $options; } } - // Support other options like prepopulating etc - $this->_gf_pre_process( $form ); + if ( ! empty( $dynamic_selects ) ) { + $form = Pods_GF::gf_dynamic_select( $form, $dynamic_selects ); + } } - return $form; + $form = $this->_gf_pre_process( $form ); + + $setup[ $form['id'] ] = $form; + + return $setup[ $form['id'] ]; } + /** + * @param $form + * + * @return mixed + */ public function _gf_pre_process( $form ) { static $setup = array(); if ( ! empty( $setup[ $form['id'] ] ) ) { - return $form; + return $setup[ $form['id'] ]; } $feeds = $this->get_feeds( $form['id'] ); + $entry = GFFormsModel::get_current_lead(); + if ( ! empty( $feeds ) ) { foreach ( $feeds as $feed ) { - if ( 1 !== (int) $feed['is_active'] ) { + if ( 1 !== (int) $feed['is_active'] && $this->is_feed_condition_met( $feed, $form, $entry ) ) { continue; } - // Block new post being created in GF - add_filter( 'gform_disable_post_creation_' . $form['id'], '__return_true' ); + $this->setup_pods_gf( $form, $feed ); + } + } - $pod_fields = $this->get_field_map_fields_with_custom_values( $feed, 'pod_fields' ); - $object_fields = $this->get_field_map_fields_with_custom_values( $feed, 'wp_object_fields' ); + $setup[ $form['id'] ] = $form; - $fields = array_merge( $pod_fields, $object_fields ); + return $setup[ $form['id'] ]; - $options = array( - // array ( 'gf_field_id' => 'pod_field_name' ) - 'fields' => $fields, - 'auto_delete' => (int) pods_v( 'delete_entry', $feed['meta'], 0 ), - 'markdown' => (int) pods_v( 'enable_markdown', $feed['meta'], 0 ), - 'gf_to_pods_priority' => 'submission', - ); + } - // Setup pod object - $pod = pods( $feed['meta']['pod'] ); + /** + * @param array $form Form object. + * @param array $feed Feed object. + * + * @return boolean + */ + public function setup_pods_gf( $form, $feed ) { - $edit_id = 0; - $prepopulate = false; - $prepopulate_id = 0; + // Block new post being created in GF + add_filter( 'gform_disable_post_creation_' . $form['id'], '__return_true' ); - if ( 'user' === $pod->pod_data['type'] && is_user_logged_in() ) { - // Support user data editing - if ( 1 === (int) pods_v( 'enable_current_user', $feed['meta'], 0 ) ) { - $edit_id = get_current_user_id(); - } + $pod_fields = self::get_field_map_fields_with_custom_values( $feed, 'pod_fields' ); + $object_fields = self::get_field_map_fields_with_custom_values( $feed, 'wp_object_fields' ); + $custom_fields = self::get_field_map_custom_fields_with_custom_values( $feed ); - // Support prepopulating - if ( 1 === (int) pods_v( 'enable_prepopulate', $feed['meta'], 0 ) ) { - $prepopulate = true; + $fields = array_merge( $pod_fields, $object_fields, $custom_fields ); - $prepopulate_id = get_current_user_id(); - } - } elseif ( 'post_type' === $pod->pod_data['type'] && is_singular( $pod->pod ) ) { - // Support post data editing - if ( 1 === (int) pods_v( 'enable_current_post', $feed['meta'], 0 ) ) { - $edit_id = get_the_ID(); - } + $options = array( + // array ( 'gf_field_id' => 'pod_field_name' ) + 'fields' => $fields, + 'update_pod_item' => (int) pods_v( 'update_pod_item', $feed['meta'], 0 ), + 'markdown' => (int) pods_v( 'enable_markdown', $feed['meta'], 0 ), + 'auto_delete' => (int) pods_v( 'delete_entry', $feed['meta'], 0 ), + 'gf_to_pods_priority' => 'submission', + ); - // Support prepopulating - if ( 1 === (int) pods_v( 'enable_prepopulate', $feed['meta'], 0 ) ) { - $prepopulate = true; + // Setup pod object + $pod = pods( $feed['meta']['pod'] ); - $prepopulate_id = get_the_ID(); - } - } + $edit_id = 0; + $prepopulate = false; + $prepopulate_id = 0; - /** - * Allow filtering of which item ID to use when editing (default none, always add new items) - * - * @param int $edit_id Edit ID - * @param string $pod_name Pod name - * @param int $form_id GF Form ID - * @param array $feed GF Form feed array - * @param array $form GF Form array - * @param array $options Pods GF options - * @param Pods $pod Pods object - */ - $edit_id = (int) apply_filters( 'pods_gf_addon_edit_id', $edit_id, $feed['meta']['pod'], $form['id'], $feed, $form, $options, $pod ); - - /** - * Allow filtering of whether to prepopulate form fields (default none) - * - * @param bool $prepopulate Whether to prepopulate or not - * @param string $pod_name Pod name - * @param int $form_id GF Form ID - * @param array $feed GF Form feed array - * @param array $form GF Form array - * @param array $options Pods GF options - * @param Pods $pod Pods object - */ - $prepopulate = (boolean) apply_filters( 'pods_gf_addon_prepopulate', $prepopulate, $feed['meta']['pod'], $form['id'], $feed, $form, $options, $pod ); - - if ( empty( $edit_id ) && $prepopulate ) { - /** - * Allow filtering of which item ID to use when prepopulating form fields (default is same as Edit ID) - * - * @param int $prepopulate_id ID to use when prepopulating - * @param string $pod_name Pod name - * @param int $form_id GF Form ID - * @param array $feed GF Form feed array - * @param array $form GF Form array - * @param array $options Pods GF options - * @param Pods $pod Pods object - */ - $prepopulate_id = (int) apply_filters( 'pods_gf_addon_prepopulate_id', $prepopulate_id, $feed['meta']['pod'], $form['id'], $feed, $form, $options, $pod ); - } + if ( 'user' === $pod->pod_data['type'] && is_user_logged_in() ) { + // Support user data editing + if ( 1 === (int) pods_v( 'enable_current_user', $feed['meta'], 0 ) ) { + $edit_id = get_current_user_id(); + } - if ( 0 < $edit_id ) { - $options['edit'] = true; + // Support prepopulating + if ( 1 === (int) pods_v( 'enable_prepopulate', $feed['meta'], 0 ) ) { + $prepopulate = true; - if ( $prepopulate ) { - $options['prepopulate'] = true; - } + $prepopulate_id = get_current_user_id(); + } + } elseif ( in_array( $pod->pod_data['type'], array( 'post_type', 'media' ), true ) && is_singular( $pod->pod ) ) { + // Support post data editing + if ( 1 === (int) pods_v( 'enable_current_post', $feed['meta'], 0 ) ) { + $edit_id = get_the_ID(); + } - $pod->fetch( $edit_id ); - } elseif ( $prepopulate && 0 < $prepopulate_id ) { - $options['prepopulate'] = true; + // Support prepopulating + if ( 1 === (int) pods_v( 'enable_prepopulate', $feed['meta'], 0 ) ) { + $prepopulate = true; - $pod->fetch( $prepopulate_id ); - } + $prepopulate_id = get_the_ID(); + } + } - /** - * Allow filtering of Pods GF options to set custom settings apart from Pods GF add-on options - * - * @param array $options Pods GF options - * @param string $pod_name Pod name - * @param int $form_id GF Form ID - * @param array $feed GF Form feed array - * @param array $form GF Form array - * @param Pods $pod Pods object - */ - $options = apply_filters( 'pods_gf_addon_options', $options, $feed['meta']['pod'], $form['id'], $feed, $form, $pod ); - - pods_gf( $pod, $form['id'], $options ); - - $setup[ $form['id'] ] = true; + /** + * Allow filtering of which item ID to use when editing (default none, always add new items) + * + * @param int $edit_id Edit ID + * @param string $pod_name Pod name + * @param int $form_id GF Form ID + * @param array $feed GF Form feed array + * @param array $form GF Form array + * @param array $options Pods GF options + * @param Pods $pod Pods object + */ + $edit_id = (int) apply_filters( 'pods_gf_addon_edit_id', $edit_id, $feed['meta']['pod'], $form['id'], $feed, $form, $options, $pod ); + + /** + * Allow filtering of whether to prepopulate form fields (default none) + * + * @param bool $prepopulate Whether to prepopulate or not + * @param string $pod_name Pod name + * @param int $form_id GF Form ID + * @param array $feed GF Form feed array + * @param array $form GF Form array + * @param array $options Pods GF options + * @param Pods $pod Pods object + */ + $prepopulate = (boolean) apply_filters( 'pods_gf_addon_prepopulate', $prepopulate, $feed['meta']['pod'], $form['id'], $feed, $form, $options, $pod ); + + if ( empty( $edit_id ) && $prepopulate ) { + /** + * Allow filtering of which item ID to use when prepopulating form fields (default is same as Edit ID) + * + * @param int $prepopulate_id ID to use when prepopulating + * @param string $pod_name Pod name + * @param int $form_id GF Form ID + * @param array $feed GF Form feed array + * @param array $form GF Form array + * @param array $options Pods GF options + * @param Pods $pod Pods object + */ + $prepopulate_id = (int) apply_filters( 'pods_gf_addon_prepopulate_id', $prepopulate_id, $feed['meta']['pod'], $form['id'], $feed, $form, $options, $pod ); + } + + if ( 0 < $edit_id ) { + $options['edit'] = true; + + if ( $prepopulate ) { + $options['prepopulate'] = true; } + + $pod->fetch( $edit_id ); + } elseif ( $prepopulate && 0 < $prepopulate_id ) { + $options['prepopulate'] = true; + + $pod->fetch( $prepopulate_id ); + } + + /** + * Allow filtering of Pods GF options to set custom settings apart from Pods GF add-on options + * + * @param array $options Pods GF options + * @param string $pod_name Pod name + * @param int $form_id GF Form ID + * @param array $feed GF Form feed array + * @param array $form GF Form array + * @param Pods $pod Pods object + */ + $options = apply_filters( 'pods_gf_addon_options', $options, $feed['meta']['pod'], $form['id'], $feed, $form, $pod ); + + $this->pods_gf[ $feed['id'] ] = pods_gf( $pod, $form['id'], $options ); + + $setup[ $form['id'] ] = true; + + return true; + + } + + /** + * Add custom merge tags for Pods GF. + * + * @param array $merge_tags Merge tags. + * @param int $form_id Form ID. + * + * @return array Merge tags, + */ + public function _gf_custom_merge_tags( $merge_tags, $form_id ) { + + $merge_tags[] = array( + 'tag' => '{pods:id}', + 'label' => esc_html__( 'Pods GF Item ID', 'pods-gravity-forms' ), + ); + + $merge_tags[] = array( + 'tag' => '{pods:permalink}', + 'label' => esc_html__( 'Pods GF Item Permalink', 'pods-gravity-forms' ), + ); + + return $merge_tags; + + } + + /** + * Add custom merge tags for Pods GF. + * + * @param array $data Merge tag data. + * @param string $text Content to replace custom merge tags in. + * @param false|array $form GF form object. + * + * @return array Merge tag data. + */ + public function _gf_add_merge_tags( $data, $text, $form ) { + + if ( empty( $form ) || empty( $form['id'] ) ) { + return $data; + } + + $form_id = $form['id']; + + $id = 0; + $permalink = ''; + + if ( ! empty( Pods_GF::$gf_to_pods_id[ $form_id ] ) ) { + $id = Pods_GF::$gf_to_pods_id[ $form_id ]; + } + + if ( ! empty( Pods_GF::$gf_to_pods_id[ $form_id . '_permalink' ] ) ) { + $permalink = Pods_GF::$gf_to_pods_id[ $form_id . '_permalink' ]; } - return $form; + $data['pods'] = array( + 'id' => $id, + 'permalink' => $permalink, + ); + + return $data; + + } + + /** + * Replace custom merge tags in content for Pods GF. + * + * @param string $content Content to replace custom merge tags in. + * @param false|array $form GF form object. + * + * @return string Content with custom merge tags replaced. + */ + public function _gf_replace_merge_tags( $content, $form ) { + + if ( empty( $form ) || empty( $form['id'] ) ) { + return $content; + } + + $form_id = $form['id']; + + $id = 0; + $permalink = ''; + + if ( ! empty( Pods_GF::$gf_to_pods_id[ $form_id ] ) ) { + $id = Pods_GF::$gf_to_pods_id[ $form_id ]; + } + + if ( ! empty( Pods_GF::$gf_to_pods_id[ $form_id . '_permalink' ] ) ) { + $permalink = Pods_GF::$gf_to_pods_id[ $form_id . '_permalink' ]; + } + + // For backcompat purposes. + $id_merge_tags = array( + '{pods:id}', + '{gf_to_pods_id}', + '{@gf_to_pods_id}', + ); + + // For backcompat purposes. + $permalink_merge_tags = array( + '{pods:permalink}', + '{gf_to_pods_permalink}', + '{@gf_to_pods_permalink}', + ); + + $content = str_replace( $id_merge_tags, $id, $content ); + $content = str_replace( $permalink_merge_tags, $permalink, $content ); + + return $content; } @@ -718,70 +1410,163 @@ public function _gf_pre_process( $form ) { */ public static function get_field_map_fields_with_custom_values( $feed, $field_name ) { - $prefix = $field_name . '_'; - $custom_prefix = $prefix . 'override_custom_'; + $prefix = $field_name . '_'; + + $old_custom_prefix = $prefix . 'override_custom_'; $fields = array(); - foreach ( $feed['meta'] as $config_field_name => $value ) { + $skip = array(); + + foreach ( $feed['meta'] as $config_field_name => $config ) { $config_field_name = (string) $config_field_name; + $gf_field_custom = sprintf( '%s_custom', $config_field_name ); - if ( 0 === strpos( $config_field_name, $custom_prefix ) ) { - // Skip override values + if ( in_array( $config_field_name, $skip, true ) ) { continue; - } elseif ( 0 === strpos( $config_field_name, $prefix ) ) { - // Get field name - $field_name = substr( $config_field_name, strlen( $prefix ) ); + } - // Mapping value is the GF field ID - $gf_field = trim( $value ); + if ( 0 === strpos( $config_field_name, $old_custom_prefix ) ) { + // Skip override values (old way) + continue; + } - // Mapping value - $mapping_value = array( - 'gf_field' => $gf_field, - 'field' => $field_name, - ); + if ( 0 !== strpos( $config_field_name, $prefix ) && $field_name !== $config_field_name ) { + continue; + } - // Support override value settings - if ( '_pods_custom' === $gf_field ) { - $gf_field = sprintf( '_pods_gf_custom_%s', $field_name ); + // Get field name + $field_name = substr( $config_field_name, strlen( $prefix ) ); - $mapping_value = array( - 'gf_field' => $gf_field, - 'field' => $field_name, - 'value' => '', - ); + // Mapping value is the GF field ID + $gf_field = trim( $config ); + + // Mapping value + $mapping_value = array( + 'gf_field' => $gf_field, + 'field' => $field_name, + ); - if ( ! empty( $feed['meta'][ $custom_prefix ] ) ) { - $value = trim( $feed['meta'][ $custom_prefix ] ); + if ( 'gf_custom' === $gf_field ) { + // Support override value settings (new way) + $gf_field = sprintf( '_pods_gf_custom_%s', $field_name ); - if ( ! empty( $value ) ) { - $mapping_value['value'] = $value; - } + $mapping_value['gf_field'] = $gf_field; + $mapping_value['value'] = ''; + + if ( ! empty( $feed['meta'][ $gf_field_custom ] ) ) { + $value = trim( $feed['meta'][ $gf_field_custom ] ); + + if ( ! empty( $value ) ) { + $mapping_value['value'] = $value; + + $mapping_value['gf_merge_tags'] = true; } } + } elseif ( '_pods_custom' === $gf_field ) { + // Support override value settings (old way) + $gf_field = sprintf( '_pods_gf_custom_%s', $field_name ); - if ( ! empty( $gf_field ) ) { - $fields[] = $mapping_value; + $mapping_value['gf_field'] = $gf_field; + $mapping_value['value'] = ''; + + if ( ! empty( $feed['meta'][ $old_custom_prefix ] ) ) { + $value = trim( $feed['meta'][ $old_custom_prefix ] ); + + if ( ! empty( $value ) ) { + $mapping_value['value'] = $value; + + $mapping_value['gf_merge_tags'] = true; + } } } + + $skip[] = $gf_field_custom; + + if ( ! empty( $gf_field ) ) { + $fields[] = $mapping_value; + } } return $fields; } + /** + * Get field map field values with support for custom override values. + * + * This differs from get_field_map_fields() in that it returns an array + * that is already formatted for Pods GF field mapping usage. + * + * @param array $feed + * + * @return array + */ + public static function get_field_map_custom_fields_with_custom_values( $feed ) { + + if ( empty( $feed['meta']['custom_fields'] ) ) { + return array(); + } + + $configs = $feed['meta']['custom_fields']; + + $fields = array(); + + foreach ( $configs as $config ) { + $config = array_map( 'trim', $config ); + + $gf_field = $config['value']; + $field_name = $config['key']; + + if ( in_array( $field_name, array( 'gf_custom', '' ), true ) ) { + $field_name = $config['custom_key']; + } + + // Mapping value + $mapping_value = array( + 'gf_field' => $gf_field, + 'field' => $field_name, + ); + + if ( in_array( $gf_field, array( 'gf_custom', '' ), true ) && ! empty( $config['custom_value'] ) ) { + $mapping_value['gf_field'] = sprintf( '_pods_gf_custom_%s', $field_name ); + $mapping_value['value'] = $config['custom_value']; + $mapping_value['gf_merge_tags'] = true; + } + + if ( '' === $mapping_value['gf_field'] || '' === $mapping_value['field'] ) { + continue; + } + + $fields[] = $mapping_value; + } + + return $fields; + + } + + /** + * @param array $entry_meta + * @param int $form_id + * + * @return array + */ public function get_entry_meta( $entry_meta, $form_id ) { if ( $this->has_feed( $form_id ) ) { - $entry_meta['pod_id'] = array( - 'label' => 'Pod ID', + $entry_meta['_pods_item_id'] = array( + 'label' => 'Pod Item ID', 'is_numeric' => true, 'is_default_column' => true, 'update_entry_meta_callback' => array( $this, 'update_entry_meta_pod_id' ), 'filter' => array( - 'operators' => array( 'is', 'isnot', '>', '<' ) - ) + 'operators' => array( + 'is', + 'isnot', + '>', + '<', + ), + ), ); } @@ -789,6 +1574,13 @@ public function get_entry_meta( $entry_meta, $form_id ) { } + /** + * @param $key + * @param $entry + * @param $form + * + * @return int + */ public function update_entry_meta_pod_id( $key, $entry, $form ) { if ( ! empty( Pods_GF::$gf_to_pods_id[ $form['id'] ] ) ) { diff --git a/includes/Pods_GF_CLI.php b/includes/Pods_GF_CLI.php new file mode 100644 index 0000000..cbedbd2 --- /dev/null +++ b/includes/Pods_GF_CLI.php @@ -0,0 +1,128 @@ + + * : The Gravity Form ID. + * + * [--feed=] + * : The Gravity Form Pods Feed ID. + * + * ## EXAMPLES + * + * wp pods-gf sync --form=123 --feed=2 + * + * @param $args + * @param $assoc_args + * + * @throws \WP_CLI\ExitException + */ + public function sync( $args, $assoc_args ) { + + add_filter( 'pods_gf_to_pods_update_pod_items', '__return_true' ); + + $form_id = 0; + + if ( ! empty( $assoc_args['form'] ) ) { + $form_id = absint( $assoc_args['form'] ); + } + + if ( empty( $form_id ) ) { + \WP_CLI::error( esc_html__( 'Form ID is required.', 'pods-gravity-forms' ) ); + } + + $feed_id = 0; + + if ( ! empty( $assoc_args['feed'] ) ) { + $feed_id = absint( $assoc_args['feed'] ); + } + + // Get form. + $form = \GFAPI::get_form( $form_id ); + + if ( empty( $form ) || is_wp_error( $form ) ) { + \WP_CLI::error( esc_html__( 'Form not found.', 'pods-gravity-forms' ) ); + } + + $active_only = true; + + if ( 0 < $feed_id ) { + $active_only = false; + } + + // Get feed. + $feeds = \GFAPI::get_feeds( $feed_id, $form_id, 'pods-gravity-forms', $active_only ); + + if ( empty( $feeds ) || is_wp_error( $feeds ) ) { + \WP_CLI::error( esc_html__( 'Feed not found.', 'pods-gravity-forms' ) ); + } + + // Use first feed. + $feed = reset( $feeds ); + + $feed_id = $feed['id']; + + if ( empty( $feed_id ) ) { + \WP_CLI::error( esc_html__( 'Invalid feed.', 'pods-gravity-forms' ) ); + } + + /** @var Pods_GF_Addon $pods_gf_addon */ + $pods_gf_addon = Pods_GF_Addon::get_instance(); + + $pods_gf_addon->setup_pods_gf( $form, $feed ); + + $pods_gf_addon->pods_gf[ $feed_id ]->options['update_pod_item'] = 1; + + $total_entries = 0; + + $search_criteria = array( + 'status' => 'active', + ); + + $paging = array( + 'offset' => 0, + 'page_size' => 50, + ); + + $entries = \GFAPI::get_entries( $form_id, $search_criteria, null, $paging, $total_entries ); + + /** @var \cli\progress\Bar $progress_bar */ + /* translators: Total entries number is used in this message. */ + $progress_bar = \WP_CLI\Utils\make_progress_bar( sprintf( esc_html_x( 'Syncing %s entries', 'Sync status message for WP-CLI feed sync using total entries count', 'pods-gravity-forms' ), number_format_i18n( $total_entries ) ), $total_entries ); + + $entries_counter = 0; + + // Loop through all pages of entries and process feeds. + do { + // Loop through entries and process feed. + foreach ( $entries as $entry ) { + $pods_gf_addon->process_feed( $feed, $entry, $form ); + + Pods_GF::$actioned = []; + + $progress_bar->tick(); + + $entries_counter++; + } + + $paging['offset'] = $entries_counter; + + $entries = \GFAPI::get_entries( $form_id, $search_criteria, null, $paging, $total_entries ); + } while ( $entries ); + + $progress_bar->finish(); + + /* translators: Feed ID is used in this message. */ + \WP_CLI::success( sprintf( esc_html_x( 'Form entries synced to Pods using feed %d.', 'Success message for WP-CLI feed sync using Feed ID', 'pods-gravity-forms' ), $feed_id ) ); + + } + +} diff --git a/includes/Pods_GF_UI.php b/includes/Pods_GF_UI.php index e6b0056..e5d6181 100755 --- a/includes/Pods_GF_UI.php +++ b/includes/Pods_GF_UI.php @@ -404,22 +404,22 @@ private function setup_ui() { foreach ( $this->actions as $action => $options ) { $this->actions[ $action ] = $options = array_merge( $defaults, $options ); - if ( !empty( $options[ 'heading' ] ) ) { + if ( !empty( $options[ 'heading' ] ) && empty( $this->ui[ 'heading' ][ $action ] ) ) { $this->ui[ 'heading' ][ $action ] = $options[ 'heading' ]; } - if ( !empty( $options[ 'header' ] ) ) { + if ( !empty( $options[ 'header' ] ) && empty( $this->ui[ 'header' ][ $action ] ) ) { $this->ui[ 'header' ][ $action ] = $options[ 'header' ]; } if ( !empty( $options[ 'label' ] ) ) { $this->ui[ 'label' ][ $action ] = $options[ 'label' ]; - if ( !isset( $this->ui[ 'heading' ][ $action ] ) || empty( $this->ui[ 'heading' ][ $action ] ) ) { + if ( empty( $this->ui[ 'heading' ][ $action ] ) ) { $this->ui[ 'heading' ][ $action ] = $options[ 'label' ]; } - if ( !isset( $this->ui[ 'header' ][ $action ] ) || empty( $this->ui[ 'header' ][ $action ] ) ) { + if ( empty( $this->ui[ 'header' ][ $action ] ) ) { $this->ui[ 'header' ][ $action ] = $options[ 'label' ]; } @@ -818,6 +818,10 @@ public function access_constraints( $constraints ) { public function _action_add( $obj ) { self::$pods_ui =& $obj; + + if ( $obj->restricted( $obj->action ) ) { + return false; + } ?>
    icon ) { ?> style="background-position:0 0;background-size:100%;background-image:url(icon; ?>);">
    @@ -825,7 +829,7 @@ public function _action_add( $obj ) { header[ 'add' ]; - if ( !in_array( 'manage', $obj->actions_disabled ) && !in_array( 'manage', $obj->actions_hidden ) ) { + if ( !in_array( 'manage', $obj->actions_disabled ) && !in_array( 'manage', $obj->actions_hidden ) && ! $obj->restricted( 'manage' ) ) { $link = pods_var_update( array( 'action' . $obj->num => 'manage', 'id' . $obj->num => '' ), PodsUI::$allowed, $obj->exclusion() ); if ( !empty( $obj->action_links[ 'manage' ] ) ) { @@ -890,6 +894,10 @@ public function _action_edit( $duplicate, $obj = null ) { return; } + + if ( $obj->restricted( $obj->action, $obj->row ) ) { + return $obj->error( sprintf( __( 'Error: You do not have access to this %s.', 'pods' ), $obj->item ) ); + } ?>
    icon ) { ?> style="background-position:0 0;background-size:100%;background-image:url(icon; ?>);">
    @@ -897,7 +905,7 @@ public function _action_edit( $duplicate, $obj = null ) { do_template( $duplicate ? $obj->header[ 'duplicate' ] : $obj->header[ $obj->action ] ); - if ( !in_array( 'manage', $obj->actions_disabled ) && !in_array( 'manage', $obj->actions_hidden ) ) { + if ( !in_array( 'manage', $obj->actions_disabled ) && !in_array( 'manage', $obj->actions_hidden ) && ! $obj->restricted( 'manage' ) ) { $link = pods_var_update( array( 'action' . $obj->num => 'manage', 'id' . $obj->num => '' ), PodsUI::$allowed, $obj->exclusion() ); if ( !empty( $obj->action_links[ 'manage' ] ) ) { @@ -952,6 +960,10 @@ public function _action_view( $obj = null ) { return; } + + if ( $obj->restricted( $obj->action, $obj->row ) ) { + return false; + } ?>
    icon ) { ?> style="background-position:0 0;background-size:100%;background-image:url(icon; ?>);">
    @@ -959,7 +971,7 @@ public function _action_view( $obj = null ) { do_template( $obj->header[ $obj->action ] ); - if ( !in_array( 'manage', $obj->actions_disabled ) && !in_array( 'manage', $obj->actions_hidden ) ) { + if ( !in_array( 'manage', $obj->actions_disabled ) && !in_array( 'manage', $obj->actions_hidden ) && ! $obj->restricted( 'manage' ) ) { $link = pods_var_update( array( 'action' . $obj->num => 'manage', 'id' . $obj->num => '' ), PodsUI::$allowed, $obj->exclusion() ); if ( !empty( $obj->action_links[ 'manage' ] ) ) { @@ -1003,6 +1015,10 @@ public function _action_delete( $id, $obj = null ) { self::$pods_ui =& $obj; + if ( $obj->restricted( $obj->action, $obj->row ) ) { + return false; + } + if ( is_object( $this->pod ) ) { return false; // continue as normal } @@ -1034,6 +1050,10 @@ public function _action_delete( $id, $obj = null ) { public function _action_custom( $obj ) { self::$pods_ui =& $obj; + + if ( $obj->restricted( $obj->action, $obj->row ) ) { + return false; + } ?>
    icon ) { ?> style="background-position:0 0;background-size:100%;background-image:url(icon; ?>);">
    @@ -1041,7 +1061,7 @@ public function _action_custom( $obj ) { header[ $this->action ]; - if ( !in_array( 'manage', $obj->actions_disabled ) && !in_array( 'manage', $obj->actions_hidden ) ) { + if ( !in_array( 'manage', $obj->actions_disabled ) && !in_array( 'manage', $obj->actions_hidden ) && ! $obj->restricted( 'manage' ) ) { $link = pods_var_update( array( 'action' . $obj->num => 'manage', 'id' . $obj->num => '' ), PodsUI::$allowed, $obj->exclusion() ); if ( !empty( $obj->action_links[ 'manage' ] ) ) { diff --git a/pods-gravity-forms.php b/pods-gravity-forms.php index dfbd88d..d05b045 100755 --- a/pods-gravity-forms.php +++ b/pods-gravity-forms.php @@ -1,16 +1,15 @@

    %s

    ', - __( 'Pods Gravity Forms requires that the Pods and Gravity Forms core plugins be installed and activated.', 'pods-gravity-forms' ) - ); + echo sprintf( '

    %s

    ', esc_html__( 'Pods Gravity Forms requires that the Pods and Gravity Forms core plugins be installed and activated.', 'pods-gravity-forms' ) ); } } - /** * Add Advanced Related Objects * @@ -122,6 +131,7 @@ function pods_gf_add_related_objects() { ); } + add_action( 'pods_form_ui_field_pick_related_objects_other', 'pods_gf_add_related_objects' ); /** @@ -148,11 +158,7 @@ function pods_gf_add_related_objects_forms( $name = null, $value = null, $option $form_title = $form->title; if ( 1 !== (int) $form->is_active ) { - $form_title = sprintf( - '%s (%s)', - $form_title, - __( 'inactive', 'pods-gravity-forms' ) - ); + $form_title = sprintf( '%s (%s)', $form_title, __( 'inactive', 'pods-gravity-forms' ) ); } $data[ $form->id ] = $form_title; @@ -160,4 +166,4 @@ function pods_gf_add_related_objects_forms( $name = null, $value = null, $option return apply_filters( 'pods_form_ui_field_pick_' . __FUNCTION__, $data, $name, $value, $options, $pod, $id ); -} \ No newline at end of file +} diff --git a/readme.txt b/readme.txt index a0c60ae..ca6414d 100755 --- a/readme.txt +++ b/readme.txt @@ -2,9 +2,9 @@ Contributors: sc0ttkclark, jimtrue, naomicbush, gravityplus Donate link: https://pods.io/friends-of-pods/ Tags: pods, gravity forms, form mapping -Requires at least: 4.0 -Tested up to: 4.8 -Stable tag: 1.3 +Requires at least: 4.6 +Tested up to: 4.9.8 +Stable tag: 1.4 License: GPLv2 or later License URI: http://www.gnu.org/licenses/gpl-2.0.html @@ -20,6 +20,86 @@ Please report bugs or request featured on [GitHub](https://github.com/pods-frame Special thanks to Rocketgenius for their sponsorship support and to Naomi C. Bush for her help in the initial add-on UI work. += WP-CLI Command for Syncing Entries = + +This add-on provides the ability to sync entries from a Form Submission and Entry Edit screen. To bulk sync all entries even prior to setting up a Pods Gravity Form Feed, you can run a WP-CLI command. + +**Example 1: Sync all entries for Form 123 first active Pod feed** + +`wp pods-gf sync --form=123` + +**Example 2: Sync all entries for Form 123 using a specific feed (even if it is inactive)** + +`wp pods-gf sync --form=123 --feed=2` + += Mapping GF List Fields to a Pods Relationship field = + +You can map a GF List field to a Relationship field related to another Pod. Using the below examples you can customize how the automatic mapping works. By default, the list columns will map to the pod fields with the same labels. + +**Example 1: Customize what columns map to which Related Pod fields for Form ID 1, Field ID 2** + +Customizing a list field row can be done by using the `pods_gf_field_columns_mapping` filter, which has Form ID and Field ID variations (`pods_gf_field_columns_mapping_{form_id}` and `pods_gf_field_columns_mapping_{form_id}_{field_id}`). + +` +add_filter( 'pods_gf_field_columns_mapping_1_2', 'my_columns_mapping', 10, 4 ); + +/** + * Filter list columns mapping for related pod fields. + * + * @param array $columns List field columns. + * @param array $form GF form. + * @param GF_Field $gf_field GF field data. + * @param Pods $pod Pods object. + * + * @return array + */ +function my_columns_mapping( $columns, $form, $gf_field, $related_obj ) { + + $columns[0] = 'first_field'; + $columns[1] = 'second_field'; + $columns[2] = 'third_field'; + + return $columns; + +} +` + +**Example 2: Customize a List row for Form ID 1, Field ID 2** + +Customizing a list field row can be done by using the `pods_gf_field_column_row` filter, which has Form ID and Field ID variations (`pods_gf_field_column_row_{form_id}` and `pods_gf_field_column_row_{form_id}_{field_id}`). + +` +add_filter( 'pods_gf_field_column_row_1_2', 'my_column_row_override', 10, 6 ); + +/** + * Filter list field row for relationship field saving purposes. + * + * @param array $row List field row. + * @param array $columns List field columns. + * @param array $form GF form. + * @param GF_Field $gf_field GF field data. + * @param array $options Pods GF options. + * @param Pods|false $related_obj Related Pod object. + * + * @return array + */ +function my_column_row_override( $row, $columns, $form, $gf_field, $options, $related_obj ) { + + // Update certain row fields based on the value of specific column. + if ( ! empty( $row['user_relationship_field'] ) ) { + $user = get_userdata( (int) $row['user'] ); + + // Set the post_title to match the User display name. + if ( $user && ! is_wp_error( $user ) ) { + $row['post_title'] = $user->display_name; + } + } + + return $row; + +} +` + == Screenshots == 1. In the Pods Admin, create your Pods and Pod Fields: Pods Admin -> Add New @@ -34,6 +114,39 @@ Special thanks to Rocketgenius for their sponsorship support and to Naomi C. Bus == Changelog == += 1.4 - October 16th, 2018 = + +* Support: Added support for Gravity Forms 2.3 database tables changes (You may see a warning on the Edit Pod screen but this is a false positive because we cache a list of all tables to transients and it triggers the warning solved by removing those old "rg" tables) +* Changed: Backwards compatibility issue -- You can now more easily set custom override values, however the old style was not able to be brought over -- you'll want to update your feeds when possible, the old values will not show up and you'll have to select the custom override value option once more, then fill it in +* Changed: Backwards compatibility issue -- Now requiring WordPress 4.6+ +* Feature: When editing entries in the admin area, changes now sync to the associated Pod item (except trash/deletes) +* Feature: New Bulk Entry Syncing to Pods WP-CLI command `wp pods-gf sync --form=123` or you can specify which feed (even if it is not active) with `wp pods-gf sync --form=123 --feed=2` +* Feature: Support for List field mapping to a Pod field which ends up serializing the value, but can be prepopulated back into the Gravity Form +* Feature: List field mapping to relationship fields related to another Pod (list columns map to individual fields in the related Pod) with new filters `pods_gf_field_columns_mapping` and `pods_gf_field_column_row` +* Feature: Support for Chained Select field mapping to a Pod field +* Feature: New Custom fields section added for Pods that support meta (Posts, Terms, Users, Media, and Comments), you can set additional custom fields including ability to set custom values there too +* Feature: Ability to set conditional processing per feed, based on specific values submitted +* Added: Whenever you create a new feed, mapping will automatically be associated between a Gravity Form field and a Pod field if the labels match +* Added: Custom override values now support GF merge tags by default (no insert UI yet) like `{form_id}` and any other merge tag +* Added: Required WP Object Fields in mapping are no longer required if you choose to 'Enable editing with this form using ____' option for Post/Media or User pod types +* Added: Support for E-mail field mappings with 'Confirm E-mail' enabled +* Added: Support for Date fields with multiple inputs (date dropdown / text fields) +* Added: Smarter requirement handling for WP object fields based on object type (only require what the WP insert API requires) +* Added: New mapping fields are now available for more Entry and Payment fields +* Added: New merge tags `{pods.id}` and `{pods.permalink}` are available for usage and in the merge tag selection dropdowns +* Improved: Added headings to each group of feed options so they are easier to work with +* Improved: Address field mapping for Country, State, and CA Provinces now convert properly to their Pods counterparts +* Updated: PHP Markdown library updated to 1.0.2 +* Fixed: Issues with using 'bypass' as a save action +* Fixed: Dynamic select options should set the current value (as posted in form) properly +* Fixed: Date/time fields shouldn't auto populate with empty dates such as 0000-00-00 anymore +* Fixed: Additional attachment processing fixes +* Fixed: Lots of Pods GF UI issues resolved +* Fixed: Removed Autocomplete limit (was 30) that was being enforced, now all data from related field will show +* Fixed: Dynamic mapping value checking to support arrays of values +* Fixed: Lots of Prepopulating fixes +* Fixed: Now supports multi page form validation and prepopulating + = 1.3 - June 2nd, 2017 = * Added: When creating new feeds mapping will automatically be detected based on matching field labels diff --git a/tests/_bootstrap.php b/tests/_bootstrap.php new file mode 100644 index 0000000..358203e --- /dev/null +++ b/tests/_bootstrap.php @@ -0,0 +1,7 @@ +esc_like( $wpdb->prefix . 'rg_' ) . '%', + $wpdb->esc_like( $wpdb->prefix . 'gf_' ) . '%', + $wpdb->esc_like( $wpdb->prefix . 'pods' ) . '%', + $wpdb->esc_like( $wpdb->prefix . 'post' ) . '%', + $wpdb->esc_like( $wpdb->prefix . 'term' ) . '%', + ]; + + $tables = $wpdb->get_col( $wpdb->prepare( $sql, $prepare ) ); + + foreach ( $tables as $table ) { + $wpdb->query( "TRUNCATE `{$table}`" ); + } + + pods_init()->reset(); + } + + if ( $rebuild_data ) { + wp_set_current_user( 1 ); + + // do setup + self::setup_forms(); + + echo "\nData rebuilt, you can now export the updated SQL to tests/_data/test-data.sql\n"; + die(); + } + + } + + /** + * Helper for after running each test. + */ + public static function after_test() { + + /** + * @var $wpdb \wpdb + */ + global $wpdb; + + $wpdb->show_errors( true ); + $wpdb->suppress_errors( false ); + + // Clear sessions/cookies + \WP_User_Meta_Session_Tokens::drop_sessions(); + + wp_set_current_user( 0 ); + + $_SESSION = []; + $_COOKIE = []; + + // Clear caches + global $wp_object_cache; + + $wpdb->queries = []; + + if ( is_object( $wp_object_cache ) ) { + $wp_object_cache->group_ops = []; + $wp_object_cache->stats = []; + $wp_object_cache->memcache_debug = []; + + // Make sure this is a public property, before trying to clear it + try { + $cache_property = new \ReflectionProperty( $wp_object_cache, 'cache' ); + if ( $cache_property->isPublic() ) { + $wp_object_cache->cache = []; + } + unset( $cache_property ); + } catch ( \ReflectionException $e ) { + } + + /* + * In the case where we're not using an external object cache, we need to call flush on the default + * WordPress object cache class to clear the values from the cache property + */ + if ( ! wp_using_ext_object_cache() ) { + wp_cache_flush(); + } + + if ( is_callable( $wp_object_cache, '__remoteset' ) ) { + call_user_func( [ $wp_object_cache, '__remoteset' ] ); // important + } + } else { + wp_cache_flush(); + } + + } + + /** + * Get mapping config. + * + * @return array + */ + public static function get_mapping_config() { + + $mapping_config = array( + array( + 'gf_type' => 'text', + 'pod_type' => 'text', + ), + array( + 'gf_type' => 'text', + 'pod_type' => 'password', + ), + array( + 'gf_type' => 'text', + 'pod_type' => 'website', + ), + array( + 'gf_type' => 'text', + 'pod_type' => 'phone', + ), + array( + 'gf_type' => 'text', + 'pod_type' => 'email', + ), + array( + 'gf_type' => 'text', + 'pod_type' => 'paragraph', + ), + array( + 'gf_type' => 'text', + 'pod_type' => 'wysiwyg', + ), + array( + 'gf_type' => 'text', + 'pod_type' => 'code', + ), + array( + 'gf_type' => 'text', + 'pod_type' => 'datetime', + ), + array( + 'gf_type' => 'text', + 'pod_type' => 'date', + ), + array( + 'gf_type' => 'text', + 'pod_type' => 'time', + ), + array( + 'gf_type' => 'text', + 'pod_type' => 'number', + ), + array( + 'gf_type' => 'text', + 'pod_type' => 'currency', + ), + array( + 'gf_type' => 'text', + 'pod_type' => 'oembed', + ), + array( + 'gf_type' => 'text', + 'pod_type' => 'boolean', + ), + array( + 'gf_type' => 'text', + 'pod_type' => 'color', + ), + array( + 'gf_type' => 'text + enablePasswordInput=1', + 'pod_type' => 'text', + ), + array( + 'gf_type' => 'text + enablePasswordInput=1', + 'pod_type' => 'password', + ), + array( + 'gf_type' => 'textarea', + 'pod_type' => 'text', + ), + array( + 'gf_type' => 'textarea', + 'pod_type' => 'paragraph', + ), + array( + 'gf_type' => 'textarea', + 'pod_type' => 'wysiwyg', + ), + array( + 'gf_type' => 'textarea', + 'pod_type' => 'code', + ), + array( + 'gf_type' => 'textarea + useRichTextEditor=1', + 'pod_type' => 'text', + ), + array( + 'gf_type' => 'textarea + useRichTextEditor=1', + 'pod_type' => 'paragraph', + ), + array( + 'gf_type' => 'textarea + useRichTextEditor=1', + 'pod_type' => 'wysiwyg', + ), + array( + 'gf_type' => 'textarea + useRichTextEditor=1', + 'pod_type' => 'code', + ), + array( + 'gf_type' => 'select', + 'pod_type' => 'text', + ), + array( + 'gf_type' => 'select', + 'pod_type' => 'website', + ), + array( + 'gf_type' => 'select', + 'pod_type' => 'phone', + ), + array( + 'gf_type' => 'select', + 'pod_type' => 'email', + ), + array( + 'gf_type' => 'select', + 'pod_type' => 'paragraph', + ), + array( + 'gf_type' => 'select', + 'pod_type' => 'wysiwyg', + ), + array( + 'gf_type' => 'select', + 'pod_type' => 'code', + ), + array( + 'gf_type' => 'select', + 'pod_type' => 'datetime', + ), + array( + 'gf_type' => 'select', + 'pod_type' => 'date', + ), + array( + 'gf_type' => 'select', + 'pod_type' => 'time', + ), + array( + 'gf_type' => 'select', + 'pod_type' => 'number', + ), + array( + 'gf_type' => 'select', + 'pod_type' => 'currency', + ), + array( + 'gf_type' => 'select', + 'pod_type' => 'oembed', + ), + array( + 'gf_type' => 'select', + 'pod_type' => 'pick', + ), + array( + 'gf_type' => 'select', + 'pod_type' => 'boolean', + ), + array( + 'gf_type' => 'select', + 'pod_type' => 'color', + ), + array( + 'gf_type' => 'multiselect', + 'pod_type' => 'pick', + ), + array( + 'gf_type' => 'number', + 'pod_type' => 'text', + ), + array( + 'gf_type' => 'number', + 'pod_type' => 'paragraph', + ), + array( + 'gf_type' => 'number', + 'pod_type' => 'wysiwyg', + ), + array( + 'gf_type' => 'number', + 'pod_type' => 'code', + ), + array( + 'gf_type' => 'number', + 'pod_type' => 'number', + ), + array( + 'gf_type' => 'number', + 'pod_type' => 'currency', + ), + array( + 'gf_type' => 'number', + 'pod_type' => 'pick', + ), + array( + 'gf_type' => 'checkbox', + 'pod_type' => 'pick', + ), + array( + 'gf_type' => 'radio', + 'pod_type' => 'text', + ), + array( + 'gf_type' => 'radio', + 'pod_type' => 'website', + ), + array( + 'gf_type' => 'radio', + 'pod_type' => 'phone', + ), + array( + 'gf_type' => 'radio', + 'pod_type' => 'email', + ), + array( + 'gf_type' => 'radio', + 'pod_type' => 'paragraph', + ), + array( + 'gf_type' => 'radio', + 'pod_type' => 'wysiwyg', + ), + array( + 'gf_type' => 'radio', + 'pod_type' => 'code', + ), + array( + 'gf_type' => 'radio', + 'pod_type' => 'datetime', + ), + array( + 'gf_type' => 'radio', + 'pod_type' => 'date', + ), + array( + 'gf_type' => 'radio', + 'pod_type' => 'time', + ), + array( + 'gf_type' => 'radio', + 'pod_type' => 'number', + ), + array( + 'gf_type' => 'radio', + 'pod_type' => 'currency', + ), + array( + 'gf_type' => 'radio', + 'pod_type' => 'oembed', + ), + array( + 'gf_type' => 'radio', + 'pod_type' => 'pick', + ), + array( + 'gf_type' => 'radio', + 'pod_type' => 'boolean', + ), + array( + 'gf_type' => 'radio', + 'pod_type' => 'color', + ), + array( + 'gf_type' => 'hidden', + 'pod_type' => 'text', + ), + array( + 'gf_type' => 'hidden', + 'pod_type' => 'password', + ), + array( + 'gf_type' => 'hidden', + 'pod_type' => 'website', + ), + array( + 'gf_type' => 'hidden', + 'pod_type' => 'phone', + ), + array( + 'gf_type' => 'hidden', + 'pod_type' => 'email', + ), + array( + 'gf_type' => 'hidden', + 'pod_type' => 'paragraph', + ), + array( + 'gf_type' => 'hidden', + 'pod_type' => 'wysiwyg', + ), + array( + 'gf_type' => 'hidden', + 'pod_type' => 'code', + ), + array( + 'gf_type' => 'hidden', + 'pod_type' => 'datetime', + ), + array( + 'gf_type' => 'hidden', + 'pod_type' => 'date', + ), + array( + 'gf_type' => 'hidden', + 'pod_type' => 'time', + ), + array( + 'gf_type' => 'hidden', + 'pod_type' => 'number', + ), + array( + 'gf_type' => 'hidden', + 'pod_type' => 'currency', + ), + array( + 'gf_type' => 'hidden', + 'pod_type' => 'oembed', + ), + array( + 'gf_type' => 'hidden', + 'pod_type' => 'pick', + ), + array( + 'gf_type' => 'hidden', + 'pod_type' => 'boolean', + ), + array( + 'gf_type' => 'hidden', + 'pod_type' => 'color', + ), + array( + 'gf_type' => 'name', + 'pod_type' => 'text', + ), + array( + 'gf_type' => 'name', + 'pod_type' => 'paragraph', + ), + array( + 'gf_type' => 'name', + 'pod_type' => 'wysiwyg', + ), + array( + 'gf_type' => 'name', + 'pod_type' => 'code', + ), + array( + 'gf_type' => 'date + dateType=datefield', + 'pod_type' => 'text', + ), + array( + 'gf_type' => 'date + dateType=datefield', + 'pod_type' => 'paragraph', + ), + array( + 'gf_type' => 'date + dateType=datefield', + 'pod_type' => 'wysiwyg', + ), + array( + 'gf_type' => 'date + dateType=datefield', + 'pod_type' => 'code', + ), + array( + 'gf_type' => 'date + dateType=datefield', + 'pod_type' => 'date', + ), + array( + 'gf_type' => 'date + dateType=datepicker', + 'pod_type' => 'text', + ), + array( + 'gf_type' => 'date + dateType=datepicker', + 'pod_type' => 'paragraph', + ), + array( + 'gf_type' => 'date + dateType=datepicker', + 'pod_type' => 'wysiwyg', + ), + array( + 'gf_type' => 'date + dateType=datepicker', + 'pod_type' => 'code', + ), + array( + 'gf_type' => 'date + dateType=datepicker', + 'pod_type' => 'date', + ), + array( + 'gf_type' => 'date + dateType=datedropdown', + 'pod_type' => 'text', + ), + array( + 'gf_type' => 'date + dateType=datedropdown', + 'pod_type' => 'paragraph', + ), + array( + 'gf_type' => 'date + dateType=datedropdown', + 'pod_type' => 'wysiwyg', + ), + array( + 'gf_type' => 'date + dateType=datedropdown', + 'pod_type' => 'code', + ), + array( + 'gf_type' => 'date + dateType=datedropdown', + 'pod_type' => 'date', + ), + array( + 'gf_type' => 'time + timeFormat=12', + 'pod_type' => 'text', + ), + array( + 'gf_type' => 'time + timeFormat=12', + 'pod_type' => 'paragraph', + ), + array( + 'gf_type' => 'time + timeFormat=12', + 'pod_type' => 'wysiwyg', + ), + array( + 'gf_type' => 'time + timeFormat=12', + 'pod_type' => 'code', + ), + array( + 'gf_type' => 'time + timeFormat=12', + 'pod_type' => 'time', + ), + array( + 'gf_type' => 'time + timeFormat=24', + 'pod_type' => 'text', + ), + array( + 'gf_type' => 'time + timeFormat=24', + 'pod_type' => 'paragraph', + ), + array( + 'gf_type' => 'time + timeFormat=24', + 'pod_type' => 'wysiwyg', + ), + array( + 'gf_type' => 'time + timeFormat=24', + 'pod_type' => 'code', + ), + array( + 'gf_type' => 'time + timeFormat=24', + 'pod_type' => 'time', + ), + array( + 'gf_type' => 'phone', + 'pod_type' => 'text', + ), + array( + 'gf_type' => 'phone', + 'pod_type' => 'phone', + ), + array( + 'gf_type' => 'phone', + 'pod_type' => 'paragraph', + ), + array( + 'gf_type' => 'phone', + 'pod_type' => 'wysiwyg', + ), + array( + 'gf_type' => 'phone', + 'pod_type' => 'code', + ), + array( + 'gf_type' => 'address', + 'pod_type' => 'text', + ), + array( + 'gf_type' => 'address', + 'pod_type' => 'paragraph', + ), + array( + 'gf_type' => 'address', + 'pod_type' => 'wysiwyg', + ), + array( + 'gf_type' => 'address', + 'pod_type' => 'code', + ), + array( + 'gf_type' => 'website', + 'pod_type' => 'text', + ), + array( + 'gf_type' => 'website', + 'pod_type' => 'website', + ), + array( + 'gf_type' => 'website', + 'pod_type' => 'paragraph', + ), + array( + 'gf_type' => 'website', + 'pod_type' => 'wysiwyg', + ), + array( + 'gf_type' => 'website', + 'pod_type' => 'code', + ), + array( + 'gf_type' => 'email', + 'pod_type' => 'text', + ), + array( + 'gf_type' => 'email', + 'pod_type' => 'email', + ), + array( + 'gf_type' => 'email', + 'pod_type' => 'paragraph', + ), + array( + 'gf_type' => 'email', + 'pod_type' => 'wysiwyg', + ), + array( + 'gf_type' => 'email', + 'pod_type' => 'code', + ), + array( + 'gf_type' => 'fileupload', + 'pod_type' => 'file', + ), + array( + 'gf_type' => 'list', + 'pod_type' => 'text', + ), + array( + 'gf_type' => 'list', + 'pod_type' => 'code', + ), + array( + 'gf_type' => 'list', + 'pod_type' => 'pick', + ), + array( + 'gf_type' => 'postcreation_post_title', + 'pod_type' => 'text', + ), + array( + 'gf_type' => 'postcreation_post_title', + 'pod_type' => 'paragraph', + ), + array( + 'gf_type' => 'postcreation_post_title', + 'pod_type' => 'wysiwyg', + ), + array( + 'gf_type' => 'postcreation_post_title', + 'pod_type' => 'code', + ), + array( + 'gf_type' => 'postcreation_post_content', + 'pod_type' => 'text', + ), + array( + 'gf_type' => 'postcreation_post_content', + 'pod_type' => 'paragraph', + ), + array( + 'gf_type' => 'postcreation_post_content', + 'pod_type' => 'wysiwyg', + ), + array( + 'gf_type' => 'postcreation_post_content', + 'pod_type' => 'code', + ), + array( + 'gf_type' => 'postcreation_post_excerpt', + 'pod_type' => 'text', + ), + array( + 'gf_type' => 'postcreation_post_excerpt', + 'pod_type' => 'paragraph', + ), + array( + 'gf_type' => 'postcreation_post_excerpt', + 'pod_type' => 'wysiwyg', + ), + array( + 'gf_type' => 'postcreation_post_excerpt', + 'pod_type' => 'code', + ), + array( + 'gf_type' => 'product', + 'pod_type' => 'text', + ), + array( + 'gf_type' => 'product', + 'pod_type' => 'paragraph', + ), + array( + 'gf_type' => 'product', + 'pod_type' => 'wysiwyg', + ), + array( + 'gf_type' => 'product', + 'pod_type' => 'code', + ), + array( + 'gf_type' => 'quantity', + 'pod_type' => 'text', + ), + array( + 'gf_type' => 'quantity', + 'pod_type' => 'paragraph', + ), + array( + 'gf_type' => 'quantity', + 'pod_type' => 'wysiwyg', + ), + array( + 'gf_type' => 'quantity', + 'pod_type' => 'code', + ), + array( + 'gf_type' => 'quantity', + 'pod_type' => 'number', + ), + array( + 'gf_type' => 'quantity', + 'pod_type' => 'currency', + ), + array( + 'gf_type' => 'option', + 'pod_type' => 'text', + ), + array( + 'gf_type' => 'option', + 'pod_type' => 'paragraph', + ), + array( + 'gf_type' => 'option', + 'pod_type' => 'wysiwyg', + ), + array( + 'gf_type' => 'option', + 'pod_type' => 'code', + ), + array( + 'gf_type' => 'shipping', + 'pod_type' => 'text', + ), + array( + 'gf_type' => 'shipping', + 'pod_type' => 'paragraph', + ), + array( + 'gf_type' => 'shipping', + 'pod_type' => 'wysiwyg', + ), + array( + 'gf_type' => 'shipping', + 'pod_type' => 'code', + ), + array( + 'gf_type' => 'shipping', + 'pod_type' => 'number', + ), + array( + 'gf_type' => 'shipping', + 'pod_type' => 'currency', + ), + array( + 'gf_type' => 'total', + 'pod_type' => 'text', + ), + array( + 'gf_type' => 'total', + 'pod_type' => 'paragraph', + ), + array( + 'gf_type' => 'total', + 'pod_type' => 'wysiwyg', + ), + array( + 'gf_type' => 'total', + 'pod_type' => 'code', + ), + array( + 'gf_type' => 'total', + 'pod_type' => 'number', + ), + array( + 'gf_type' => 'total', + 'pod_type' => 'currency', + ), + array( + 'gf_type' => 'entry_id', + 'pod_type' => 'text', + ), + array( + 'gf_type' => 'entry_id', + 'pod_type' => 'paragraph', + ), + array( + 'gf_type' => 'entry_id', + 'pod_type' => 'wysiwyg', + ), + array( + 'gf_type' => 'entry_id', + 'pod_type' => 'code', + ), + array( + 'gf_type' => 'entry_id', + 'pod_type' => 'number', + ), + array( + 'gf_type' => 'entry_date', + 'pod_type' => 'text', + ), + array( + 'gf_type' => 'entry_date', + 'pod_type' => 'paragraph', + ), + array( + 'gf_type' => 'entry_date', + 'pod_type' => 'wysiwyg', + ), + array( + 'gf_type' => 'entry_date', + 'pod_type' => 'code', + ), + array( + 'gf_type' => 'entry_date', + 'pod_type' => 'datetime', + ), + array( + 'gf_type' => 'entry_date', + 'pod_type' => 'date', + ), + array( + 'gf_type' => 'user_agent', + 'pod_type' => 'text', + ), + array( + 'gf_type' => 'user_agent', + 'pod_type' => 'paragraph', + ), + array( + 'gf_type' => 'user_agent', + 'pod_type' => 'wysiwyg', + ), + array( + 'gf_type' => 'user_agent', + 'pod_type' => 'code', + ), + array( + 'gf_type' => 'ip', + 'pod_type' => 'text', + ), + array( + 'gf_type' => 'ip', + 'pod_type' => 'paragraph', + ), + array( + 'gf_type' => 'ip', + 'pod_type' => 'wysiwyg', + ), + array( + 'gf_type' => 'ip', + 'pod_type' => 'code', + ), + array( + 'gf_type' => 'source_url', + 'pod_type' => 'text', + ), + array( + 'gf_type' => 'source_url', + 'pod_type' => 'website', + ), + array( + 'gf_type' => 'source_url', + 'pod_type' => 'paragraph', + ), + array( + 'gf_type' => 'source_url', + 'pod_type' => 'wysiwyg', + ), + array( + 'gf_type' => 'source_url', + 'pod_type' => 'code', + ), + array( + 'gf_type' => 'transaction_id', + 'pod_type' => 'text', + ), + array( + 'gf_type' => 'transaction_id', + 'pod_type' => 'paragraph', + ), + array( + 'gf_type' => 'transaction_id', + 'pod_type' => 'wysiwyg', + ), + array( + 'gf_type' => 'transaction_id', + 'pod_type' => 'code', + ), + array( + 'gf_type' => 'transaction_id', + 'pod_type' => 'number', + ), + array( + 'gf_type' => 'payment_amount', + 'pod_type' => 'text', + ), + array( + 'gf_type' => 'payment_amount', + 'pod_type' => 'paragraph', + ), + array( + 'gf_type' => 'payment_amount', + 'pod_type' => 'wysiwyg', + ), + array( + 'gf_type' => 'payment_amount', + 'pod_type' => 'code', + ), + array( + 'gf_type' => 'payment_amount', + 'pod_type' => 'number', + ), + array( + 'gf_type' => 'payment_amount', + 'pod_type' => 'currency', + ), + array( + 'gf_type' => 'payment_date', + 'pod_type' => 'text', + ), + array( + 'gf_type' => 'payment_date', + 'pod_type' => 'paragraph', + ), + array( + 'gf_type' => 'payment_date', + 'pod_type' => 'wysiwyg', + ), + array( + 'gf_type' => 'payment_date', + 'pod_type' => 'code', + ), + array( + 'gf_type' => 'payment_date', + 'pod_type' => 'datetime', + ), + array( + 'gf_type' => 'payment_date', + 'pod_type' => 'date', + ), + array( + 'gf_type' => 'payment_status', + 'pod_type' => 'text', + ), + array( + 'gf_type' => 'payment_status', + 'pod_type' => 'paragraph', + ), + array( + 'gf_type' => 'payment_status', + 'pod_type' => 'wysiwyg', + ), + array( + 'gf_type' => 'payment_status', + 'pod_type' => 'code', + ), + array( + 'gf_type' => 'payment_status', + 'pod_type' => 'pick', + ), + ); + + return $mapping_config; + + } + + /** + * Setup test forms. + */ + public static function setup_forms() { + + $mapping_config = self::get_mapping_config(); + + $pods = array(); + + $form_meta = array( + 'title' => 'Test form', + ); + + \GFAPI::add_form( $form_meta ); + + $gf_fields = array(); + $pod_fields = array(); + + foreach ( $mapping_config as $config ) { + + } + + } + + /** + * Short-circuits calls to the `wp_remote_post` and `wp_remote_get` functions and sets expectations on their call + * arguments. + * + * @param WPTestCase $test_case Test case. + * @param null|int $expected_times The number of times the `wp_remote_post` function is expected to be called; + * passing `null` here means the function will be simply short-circuited. + * @param array $expected_urls An array of expected URLs for each call; the first one will be matched to the + * first, the second one to the second call and so on; empty values will not be + * matched. + * @param mixed $return The response return value; defaults to `false`. + */ + public static function mock_wp_remote_call( $test_case, $expected_times = null, array $expected_urls = [], $return = false ) { + add_filter( 'pre_http_request', function ( $block, $r, $url ) use ( $expected_times, $expected_urls, $return ) { + static $times; + $times = null === $times ? 1 : ++ $times; + + if ( ! empty( $expected_urls[ $times - 1 ] ) ) { + $expected_url = $expected_urls[ $times - 1 ]; + if ( $expected_url !== $url ) { + $message = "Expected '{$expected_url}' at call {$times}; got '{$url}'"; + throw new \Exception( $message ); + } + } + + if ( 0 < $expected_times ) { + if ( $times === $expected_times ) { + throw new MockedCall(); + } + } + + return $return; + }, 1, 3 ); + + if ( null !== $expected_times ) { + $test_case->expectException( MockedCall::class ); + } + } + +} diff --git a/tests/_support/_generated/.gitignore b/tests/_support/_generated/.gitignore new file mode 100644 index 0000000..c96a04f --- /dev/null +++ b/tests/_support/_generated/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/tests/_support/travis/build.sh b/tests/_support/travis/build.sh new file mode 100755 index 0000000..6ffeb69 --- /dev/null +++ b/tests/_support/travis/build.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash +# Created from the .travis.yml file +# Last update 2018-08-20 +# Update this file when/if updating the .travis.yml file removing the lines that would run the tests: the script part. +# Used in Travis CI debug simply by calling it like this from the $TRAVIS_BUILD_DIR folder +# cd $TRAVIS_BUILD_DIR && sh ./tests/travis/build.sh + +cd $TRAVIS_BUILD_DIR + +export wpDbName="wordpress" wpLoaderDbName="wordpress" wpDbPrefix="wp_" wpAdminUsername="admin" wpAdminPassword="admin" wpRootFolder="/tmp/wordpress" + +# set up DB +mysql -e "CREATE DATABASE IF NOT EXISTS $wpDbName;" -uroot +mysql -e "CREATE DATABASE IF NOT EXISTS $wpLoaderDbName;" -uroot + +if [ ! -d "$wpRootFolder" ]; then + # set up folders + mkdir -p $HOME/tools $wpRootFolder + + # install wp-cli + wget https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar -P /tmp/tools/ + chmod +x /tmp/tools/wp-cli.phar && mv /tmp/tools/wp-cli.phar /tmp/tools/wp + export PATH=$PATH:/tmp/tools:vendor/bin + + # install Apache and WordPress setup scripts + git clone https://github.com/lucatume/travis-apache-setup.git /tmp/tools/travis-apache-setup + chmod +x /tmp/tools/travis-apache-setup/apache-setup.sh + chmod +x /tmp/tools/travis-apache-setup/wp-install.sh + ln -s /tmp/tools/travis-apache-setup/apache-setup.sh /tmp/tools/apache-setup + ln -s /tmp/tools/travis-apache-setup/wp-install.sh /tmp/tools/wp-install + + # download and install WordPress + if [ $wpMultisite = "1" ]; then + wp-install --dir="$wpRootFolder" --dbname="$wpDbName" --dbuser="root" --dbpass="" --dbprefix="$wpDbPrefix" --multisite --subdomains --domain="$wpUrl" --title="Test" --admin_user="$wpAdminUsername" --admin_password="$wpAdminPassword" --admin_email="admin@$wpUrl" --theme="twentysixteen" --empty + fi + + if [ $wpMultisite = "0" ]; then + wp-install --dir="$wpRootFolder" --dbname="$wpDbName" --dbuser="root" --dbpass="" --dbprefix="$wpDbPrefix" --domain="$wpUrl" --title="Test" --admin_user="$wpAdminUsername" --admin_password="$wpAdminPassword" --admin_email="admin@$wpUrl" --theme="twentysixteen" --empty + fi +fi + +if [ ! -d "$wpRootFolder/wp-content/$pluginsFolder" ]; then + # make the folder if it needs to be made + mkdir $wpRootFolder/wp-content/$pluginsFolder +fi + +# move the plugin into WordPress folder +mv $TRAVIS_BUILD_DIR/wp-content/$pluginsFolder/$pluginSlug $wpRootFolder/wp-content/$pluginsFolder/$pluginSlug + +# set up Apache virtual host +sudo env "PATH=$PATH" apache-setup --host="127.0.0.1" --url="$wpUrl" --dir="$wpRootFolder" + +# get back to the plugin dir +cd $wpRootFolder/wp-content/$pluginsFolder/$pluginSlug + +# update composer +composer update --prefer-dist + +if [ $pluginsFolder = "plugins" ]; then + # activate the plugin in WordPress now that we have all dependencies + wp plugin activate $pluginSlug --path=$wpRootFolder +fi + +# flush rewrite rules +printf "apache_modules:\n\t- mod_rewrite" > $wpRootFolder/wp-cli.yml +wp rewrite structure '/%postname%/' --hard --path=$wpRootFolder diff --git a/tests/config.php b/tests/config.php new file mode 100644 index 0000000..b3d9bbc --- /dev/null +++ b/tests/config.php @@ -0,0 +1 @@ +assertTrue( true ); + + } + +} diff --git a/tests/integration/_bootstrap.php b/tests/integration/_bootstrap.php new file mode 100644 index 0000000..a4abe2d --- /dev/null +++ b/tests/integration/_bootstrap.php @@ -0,0 +1,2 @@ +