| Server IP : 178.212.43.201 / Your IP : 10.100.0.33 Web Server : Apache/2.4.37 (Oracle Linux Server) OpenSSL/1.1.1k System : Linux spa0007.srv.paxillus.pl 5.4.17-2136.355.3.1.el8uek.x86_64 #3 SMP Sat May 9 17:11:55 PDT 2026 x86_64 User : apache ( 48) PHP Version : 7.4.33 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : ON Directory : /var/www/html/kontur_dev/wp-content/plugins/page-generator-pro/includes/admin/ |
Upload File : |
<?php
/**
* Content Group Generation Class
*
* @package Page_Generator_Pro
* @author WP Zinc
*/
/**
* Handles generating content from Content Groups (Pages, Posts
* and Custom Post Types)
*
* @package Page_Generator_Pro
* @author WP Zinc
* @version 1.0.0
*/
class Page_Generator_Pro_Generate {
/**
* Holds the base object.
*
* @since 1.9.8
*
* @var object
*/
public $base;
/**
* Holds an array comprising of every keyword detected in the Group.
* Each Keyword holds an array comprising of every single Term for that Keyword.
*
* @since 1.9.8
*
* @var array
*/
public $keywords = array();
/**
* Holds an array comprising of every keyword detected in the Group.
* Each Keyword holds the nth Term that will be used to replace the Keyword.
*
* @since 1.9.8
*
* @var array
*/
public $keywords_terms = array();
/**
* Holds an array comprising of every required keyword detected in the Group.
*
* @since 4.0.4
*
* @var array
*/
public $required_keywords = array();
/**
* Holds an array comprising of every required keyword detected in the Group,
* including columns and modifiers.
*
* @since 4.0.4
*
* @var array
*/
public $required_keywords_full = array();
/**
* Holds the array of keywords to replace e.g. {city}
*
* @since 1.3.1
*
* @var array
*/
public $searches = array();
/**
* Holds the array of keyword values to replace e.g. Birmingham
*
* @since 1.3.1
*
* @var array
*/
public $replacements = array();
/**
* Holds a flag to denote if one or more $replacements are an array
* If they're an array, it's because the :random_different transformation
* is used, and so we have to perform a slower search/replace method.
*
* @since 2.7.2
*
* @var bool
*/
public $replacements_contain_array = false;
/**
* Holds a flag to denote whether Page Generator Pro shortcodes
* should be processed on the main Post Content
*
* @since 1.9.5
*
* @var bool
*/
public $process_shortcodes_on_post_content = false;
/**
* Constructor.
*
* @since 1.9.3
*
* @param object $base Base Plugin Class.
*/
public function __construct( $base ) {
// Store base class.
$this->base = $base;
// Delete Geodata on Post Deletion.
add_action( 'delete_post', array( $this, 'delete_latitude_longitude_by_post_id' ) );
}
/**
* Runs the main generate process for the given Content or Term Group across
* CLI or CRON.
*
* The browser method (i.e. AJAX) uses the generate_content() function directly.
*
* @since 3.1.3
*
* @param int $group_id Group ID.
* @param string $type Group Type (content|term).
* @param int $resume_index Resume Index to start at.
* @param int $number_of_posts Number of Posts/Terms to Generate.
* @param bool $test_mode Test Mode.
* @param string $system System (browser|server|cli).
* @return WP_Error|bool
*/
public function generate( $group_id, $type = 'content', $resume_index = 0, $number_of_posts = 0, $test_mode = false, $system = 'browser' ) {
$this->add_to_debug_log(
sprintf(
/* translators: Group ID */
__( 'Group ID #%s: Started', 'page-generator-pro' ),
$group_id
),
$system
);
// Get Groups or Groups Term Instance.
$groups = ( ( $type === 'term' ) ? $this->base->get_class( 'groups_terms' ) : $this->base->get_class( 'groups' ) );
// If this Group has a request to cancel generation, silently clear the status, system and cancel
// flags before performing further checks on whether we should generate.
if ( $groups->cancel_generation_requested( $group_id ) ) {
$this->add_to_debug_log(
sprintf(
/* translators: Group ID */
__( 'Group ID #%s: Generation cancelled by User', 'page-generator-pro' ),
$group_id
),
$system
);
$groups->stop_generation( $group_id );
}
// If the group is already generating, bail.
if ( $groups->is_generating( $group_id ) ) {
$error = new WP_Error(
'page_generator_pro_generate',
sprintf(
/* translators: %1$s: Group ID, %2$s: System (browser, cron or cli) */
__( 'Group ID #%1$s: Generation is already running via %2$s', 'page-generator-pro' ),
$group_id,
$groups->get_system( $group_id )
)
);
$this->add_to_debug_log( $error->get_error_message(), $system, true );
$groups->stop_generation( $group_id );
return $error;
}
// Get group.
$group = $groups->get_settings( $group_id, false, true );
// Bail if an error occured.
if ( is_wp_error( $group ) ) {
$this->add_to_debug_log( $group->get_error_message(), $system, true );
$groups->stop_generation( $group_id );
return $group;
}
if ( $type === 'term' ) {
/**
* Runs any actions before Generate Content has started.
*
* @since 3.0.7
*
* @param int $group_id Group ID.
* @param bool $test_mode Test Mode.
* @param string $system System.
*/
do_action( 'page_generator_pro_generate_terms_before', $group_id, $test_mode, $system );
} else {
/**
* Runs any actions before Generate Content has started.
*
* @since 3.0.7
*
* @param int $group_id Group ID.
* @param bool $test_mode Test Mode.
* @param string $system System.
*/
do_action( 'page_generator_pro_generate_content_before', $group_id, $test_mode, $system );
}
// Replace the Group's Number of Posts and Resume Index now, if specified in the call to this function.
// If they're not specified, the Group's existing Number of Posts and Resume Index settings will be used.
if ( $number_of_posts > 0 ) {
$group['numberOfPosts'] = absint( $number_of_posts );
}
if ( $resume_index > 0 ) {
$group['resumeIndex'] = absint( $resume_index );
}
// Calculate how many pages could be generated.
$number_of_pages_to_generate = $this->get_max_number_of_pages( $group );
if ( is_wp_error( $number_of_pages_to_generate ) ) {
$error = new WP_Error(
'page_generator_pro_generate',
sprintf(
/* translators: %1$s: Group ID, %2$s: Return message */
__( 'Group ID #%1$s: %2$s', 'page-generator-pro' ),
$group_id,
$number_of_pages_to_generate->get_error_message()
)
);
$this->add_to_debug_log( $error->get_error_message(), $system, true );
$groups->stop_generation( $group_id );
return $error;
}
// If no limit specified, set one now.
if ( empty( $group['numberOfPosts'] ) ) {
if ( $group['method'] === 'random' ) {
$group['numberOfPosts'] = 10;
} else {
$group['numberOfPosts'] = $number_of_pages_to_generate;
}
$this->add_to_debug_log(
sprintf(
/* translators: %1$s: Group ID, %2$s: Number of Posts */
__( 'Group ID #%1$s: Setting Number of Posts = %2$s, as no limit specified in Group.', 'page-generator-pro' ),
$group_id,
$group['numberOfPosts']
),
$system
);
}
// If the requested Number of Posts exceeds the Number of Pages that could be generated,
// set Number of Posts to match the Number of Pages that could be generated.
if ( $group['numberOfPosts'] > $number_of_pages_to_generate ) {
$group['numberOfPosts'] = $number_of_pages_to_generate;
$this->add_to_debug_log(
sprintf(
/* translators: %1$s: Group ID, %2$s: Number of Posts */
__( 'Group ID #%1$s: Restricting Number of Posts = %2$s, as limit specified in Group exceeded the number of possible Pages that could be generated.', 'page-generator-pro' ),
$group_id,
$number_of_pages_to_generate
),
$system
);
}
// Add Plugin Settings.
$group['stop_on_error'] = (int) $this->base->get_class( 'settings' )->get_setting( $this->base->plugin->name . '-generate', 'stop_on_error', 0 );
$group['stop_on_error_pause'] = (int) $this->base->get_class( 'settings' )->get_setting( $this->base->plugin->name . '-generate', 'stop_on_error_pause', 5 );
// Set last generated post date and time based on the Group's settings (i.e. date/time of now,
// specific date/time or a random date/time).
$last_generated_post_date_time = $this->post_date( $group );
// Set a flag to denote that this Group is generating content.
$this->add_to_debug_log(
sprintf(
/* translators: %1$s: Group ID, %2$s: System (browser,cron,cli) */
__( 'Group ID #%1$s: Setting Group Status = Generating via %2$s.', 'page-generator-pro' ),
$group_id,
$system
),
$system
);
$groups->start_generation( $group_id, 'generating', $system );
// Get first admin user.
$users = get_users(
array(
'role' => 'administrator',
)
);
$user_id = $users[0]->ID;
// Run a loop to generate each page.
for ( $i = $group['resumeIndex']; $i < ( $group['numberOfPosts'] + $group['resumeIndex'] ); $i++ ) {
// Set a sensible timeout to minimise timeouts for this generation iteration.
// This is higher since the OpenAI Dynamic Element was introduced in 4.1.3,
// as a Content Group might have multiple OpenAI Dynamic Elements specified,
// each taking a minute or two.
set_time_limit( MINUTE_IN_SECONDS * 5 );
// Clear cache every ~ 100 Pages/Terms.
if ( $i % 100 === 0 ) {
wp_cache_flush();
}
// If cancel generation was requested, exit now.
if ( $groups->cancel_generation_requested( $group_id ) ) {
$groups->stop_generation( $group_id );
$error = new WP_Error( 'page_generator_pro_generate', 'Group ID #' . $group_id . ': Generation cancelled by User' );
$this->add_to_debug_log( $error->get_error_message(), $system, true );
return $error;
}
// For Generate via Server, set current User to an Admin, so that the unfiltered_html capability is enabled,
// allowing generation to perform the same way as if run through the browser or CLI
// (i.e. iframes are permitted).
if ( $system === 'cron' ) {
if ( ! function_exists( 'wp_set_current_user' ) ) {
include_once ABSPATH . 'wp-includes/pluggable.php';
}
wp_set_current_user( $user_id );
}
// Run.
switch ( $type ) {
case 'term':
$result = $this->generate_term( $group_id, $i, $test_mode, $system );
break;
default:
$result = $this->generate_content( $group_id, $i, $test_mode, $system, $last_generated_post_date_time );
break;
}
// For Generate via Server, set current User to nothing, so that the unfiltered_html capability is disabled.
if ( $system === 'cron' ) {
wp_set_current_user( 0 );
}
// If an error occured, output it and then stop or continue, depending on the Stop on Error setting.
if ( is_wp_error( $result ) ) {
// Build error message.
$message = sprintf(
/* translators: %1$s: Group ID, %2$s: Index Number, %3$s: Total Number, %4$s: Result message */
__( 'Group ID #%1$s: %2$s/%3$s: %4$s', 'page-generator-pro' ),
$group_id,
( $i + 1 ),
$group['numberOfPosts'],
$result->get_error_message()
);
switch ( $group['stop_on_error'] ) {
// Stop.
case 1:
// Log error.
$this->add_to_debug_log( $message, $system, true );
// Stop Generation.
$groups->stop_generation( $group_id );
// Exit.
return new WP_Error( 'page_generator_pro_generate', $message );
// Continue, attempting to regenerate the Term again.
case 0:
// Decrement index so we regenerate this Term again.
--$i;
// Append error message.
$message .= sprintf(
/* translators: Number of seconds */
__( '. Waiting %s seconds before attempting to regenerate this item.', 'page-generator-pro' ),
$group['stop_on_error_pause']
);
break;
// Continue, skipping the failed Term.
case -1:
// Append error message.
$message .= sprintf(
/* translators: Number of seconds */
__( '. Waiting %s seconds before generating the next item.', 'page-generator-pro' ),
$group['stop_on_error_pause']
);
break;
}
// Log error.
$this->add_to_debug_log( $message, $system, true );
// Pause for the required number of seconds before resuming.
sleep( $group['stop_on_error_pause'] );
// Continue execution.
continue;
}
// Update last generated post date and time, which will be used on the next iteration.
$last_generated_post_date_time = $result['last_generated_post_date_time'];
// Build message and output.
$message = array(
sprintf(
/* translators: %1$s: Group ID, %2$s: Index Number, %3$s: Total Number, %4$s: Result message, %5$s: Permalink / URL, %6$s: Time taken to generate, in seconds, %7$s: Memory Usage, in MB, %8$s: Peak Memory Usage, in MB */
__( 'Group ID #%1$s: %2$s/%3$s: %4$s. Permalink: %5$s. Time: %6$s seconds. Memory Usage / Peak: %7$s/%8$sMB', 'page-generator-pro' ),
$group_id,
( $i + 1 ),
( $group['numberOfPosts'] + $group['resumeIndex'] ),
$result['message'],
$result['url'],
$result['duration'],
$result['memory_usage'],
$result['memory_peak_usage']
),
);
foreach ( $result['keywords_terms'] as $keyword => $term ) {
// Strip HTML tags for log output.
$term = wp_strip_all_tags( $term );
$message[] = '{' . $keyword . '}: ' . ( strlen( $term ) > 50 ? substr( $term, 0, 50 ) . '...' : $term );
}
$message[] = '--';
// Output log.
$this->add_to_debug_log( implode( "\n", $message ), $system );
}
// Stop generation.
$groups->stop_generation( $group_id );
if ( $type === 'term' ) {
/**
* Runs any actions after Generate Terms has finished.
*
* @since 3.0.7
*
* @param int $group_id Group ID.
* @param bool $test_mode Test Mode.
* @param string $system System.
*/
do_action( 'page_generator_pro_generate_terms_after', $group_id, $test_mode, $system );
} else {
/**
* Runs any actions after Generate Content has finished.
*
* @since 3.0.7
*
* @param int $group_id Group ID.
* @param bool $test_mode Test Mode.
* @param string $system System.
*/
do_action( 'page_generator_pro_generate_content_after', $group_id, $test_mode, $system );
}
$this->add_to_debug_log(
sprintf(
/* translators: Group ID */
__( 'Group ID #%s: Finished', 'page-generator-pro' ),
$group_id
),
$system
);
return true;
}
/**
* Calculates the maximum number of items that will be generated based
* on the settings.
*
* @since 1.1.5
*
* @param array $settings Group Settings (either a Content or Term Group).
* @return WP_Error|int
*/
public function get_max_number_of_pages( $settings ) {
// Remove some settings that we don't want to be spun/have keywords replaced on,
// as they're for a subsection i.e. Comment Generation.
unset( $settings['comments_generate'] );
// Build a class array of required keywords that need replacing with data.
$required_keywords = $this->find_keywords_in_settings( $settings );
// Bail if no keywords were found.
if ( count( $required_keywords['required_keywords'] ) === 0 ) {
return 0;
}
// Update Keywords that don't use a local source now.
$result = $this->base->get_class( 'keywords' )->refresh_terms( $required_keywords['required_keywords'] );
// If an error occured refreshing Keywords, bail.
if ( is_wp_error( $result ) ) {
return $result;
}
// Get the terms for each required keyword.
$this->keywords = $this->get_keywords_terms_columns_delimiters( $required_keywords['required_keywords'] );
// Bail if no keywords were found.
if ( empty( $this->keywords['terms'] ) ) {
return 0;
}
// Depending on the generation method chosen, for each keyword, define the term
// that will replace it.
switch ( $settings['method'] ) {
/**
* All
* Random
* - Generates all possible term combinations across keywords
*/
case 'all':
case 'random':
$total = 1;
foreach ( $this->keywords['terms'] as $keyword => $terms ) {
$total = ( $total * count( $terms ) );
}
return $total;
/**
* Sequential
* - Generates term combinations across keywords matched by index
*/
case 'sequential':
$total = 0;
foreach ( $this->keywords['terms'] as $keyword => $terms ) {
if ( count( $terms ) > 0 && ( count( $terms ) < $total || $total === 0 ) ) {
$total = count( $terms );
}
}
return $total;
}
return 0;
}
/**
* Generates a Page, Post or Custom Post Type for the given Group and Index
*
* @since 1.6.1
*
* @param int $group_id Group ID.
* @param int $index Keyword Index.
* @param bool $test_mode Test Mode.
* @param string $system System (browser|cron|cli).
* @param bool|string $last_generated_post_date_time Last Generated Post's Date and Time.
* @return WP_Error|array
*/
public function generate_content( $group_id, $index = 0, $test_mode = false, $system = 'browser', $last_generated_post_date_time = false ) {
// Performance debugging.
$start = ( function_exists( 'hrtime' ) ? hrtime( true ) : microtime( true ) );
// Define the Group ID and Index as globals, so it can be picked up by our shortcodes when they're processed.
global $page_generator_pro_group_id, $page_generator_pro_index;
$page_generator_pro_group_id = $group_id;
$page_generator_pro_index = $index;
// If test mode is enabled, set the debug constant.
if ( $test_mode && ! defined( 'PAGE_GENERATOR_PRO_DEBUG' ) ) {
define( 'PAGE_GENERATOR_PRO_DEBUG', true );
}
// Get group settings.
$settings = $this->base->get_class( 'groups' )->get_settings( $group_id, false, true );
// Bail if an error occured.
if ( is_wp_error( $settings ) ) {
return $settings;
}
/**
* Modify the Group's settings prior to starting the generation routine.
*
* Changes made only affect this item in the generation set, and are not persistent or saved.
*
* For Gutenberg and Page Builders with Blocks / Elements registered by this Plugin, this
* is a good time to convert JSON encoded strings into arrays/objects that can be iterated
* for better Keyword/Spintax detection.
*
* @since 3.7.8
*
* @param array $settings Group Settings.
* @param int $group_id Group ID.
* @param int $index Keyword Index.
* @param bool $test_mode Test Mode.
*/
$settings = apply_filters( 'page_generator_pro_generate_content_settings_before', $settings, $group_id, $index, $test_mode );
// Remove some settings that we don't want to be spun/have keywords replaced on,
// as they're for a subsection i.e. Comment Generation.
$original_settings = $settings;
unset( $settings['comments_generate'] );
// If this Group has a request to cancel generation, exit.
if ( ! $test_mode ) {
if ( $this->base->get_class( 'groups' )->cancel_generation_requested( $group_id ) ) {
// Stop Generation.
$this->base->get_class( 'groups' )->stop_generation( $group_id );
// Return error.
return $this->generate_error_return(
new WP_Error( 'generation_error', __( 'A request to cancel generation was made by the User. Exiting...', 'page-generator-pro' ) ),
$group_id,
0,
$settings['type'],
$test_mode,
$system,
false
);
}
}
// If the Group is not published, generation might fail in Gutenberg stating that no keywords could be found
// in the Content. Change its status to published.
if ( ! in_array( get_post_status( $group_id ), array_keys( $this->base->get_class( 'groups' )->get_group_statuses() ), true ) ) {
$result = wp_update_post(
array(
'ID' => $group_id,
'post_status' => 'publish',
),
true
);
if ( is_wp_error( $result ) ) {
// Return error.
return $this->generate_error_return(
$result,
$group_id,
0,
$settings['type'],
$test_mode,
$system,
false
);
}
}
// Validate group.
$validated = $this->base->get_class( 'groups' )->validate( $group_id );
if ( is_wp_error( $validated ) ) {
return $this->generate_error_return(
$validated,
$group_id,
0,
$settings['type'],
$test_mode,
$system,
false
);
}
/**
* Run any actions before an individual Page, Post or Custom Post Type is generated
*
* @since 2.4.1
*
* @param int $group_id Group ID.
* @param array $settings Group Settings.
* @param int $index Keyword Index.
* @param bool $test_mode Test Mode
*/
do_action( 'page_generator_pro_generate_content_started', $group_id, $settings, $index, $test_mode );
// Build a class array of required keywords that need replacing with data.
$required_keywords = $this->find_keywords_in_settings( $settings );
if ( count( $required_keywords['required_keywords'] ) === 0 ) {
return $this->generate_error_return(
new WP_Error( 'keyword_error', __( 'No keywords were specified in the Group.', 'page-generator-pro' ) ),
$group_id,
0,
$settings['type'],
$test_mode,
$system,
false
);
}
// If we're in Test Mode, update Keywords that don't use a local source now.
if ( $test_mode ) {
$result = $this->base->get_class( 'keywords' )->refresh_terms( $required_keywords['required_keywords'] );
// If an error occured refreshing Keywords, bail.
if ( is_wp_error( $result ) ) {
return $this->generate_error_return(
$result,
$group_id,
0,
$settings['type'],
$test_mode,
$system,
false
);
}
}
// Build a keywords array comprising of terms, columns and delimiters for each of the required keywords.
$this->keywords = $this->get_keywords_terms_columns_delimiters( $required_keywords['required_keywords'] );
if ( count( $this->keywords['terms'] ) === 0 ) {
return $this->generate_error_return(
new WP_Error( 'keyword_error', __( 'Keywords were specified in the Group, but no keywords exist in either the Keywords section of the Plugin or as a Taxonomy.', 'page-generator-pro' ) ),
$group_id,
0,
$settings['type'],
$test_mode,
$system,
false
);
}
// Build array of keyword --> term key/value pairs to use for this generation.
$keywords_terms = $this->get_keywords_terms( $settings['method'], (int) $index );
if ( is_wp_error( $keywords_terms ) ) {
return $this->generate_error_return(
$keywords_terms,
$group_id,
0,
$settings['type'],
$test_mode,
$system,
false
);
}
// Rotate Author.
if ( isset( $settings['rotateAuthors'] ) ) {
$authors = $this->base->get_class( 'common' )->get_authors();
$user_index = wp_rand( 0, ( count( $authors ) - 1 ) );
}
// Define whether we'll process shortcodes on the Post Content
// Some Page Builders will mean we won't do this, such as Elementor, which don't use
// the Post Content for output.
$this->process_shortcodes_on_post_content = $this->should_process_shortcodes_on_post_content( $settings );
// Remove all shortcode processors, so we don't process any shortcodes. This ensures page builders, galleries etc
// will work as their shortcodes will be processed when the generated page is viewed.
remove_all_shortcodes();
// Add Page Generator Pro's shortcodes, so they're processed now (true = we want to register shortcodes that need processing into HTML).
$this->base->get_class( 'shortcode' )->add_shortcodes( true );
// Iterate through each detected Keyword to build a full $this->searches and $this->replacements arrays.
$this->build_search_replace_arrays( $required_keywords['required_keywords_full'], $keywords_terms );
// Determine if a Latitude and Longitude exist (either a Keyword or actual value).
$has_lat_lng = false;
if ( ! empty( $settings['latitude'] ) && ! empty( $settings['longitude'] ) ) {
$has_lat_lng = true;
}
// Iterate through each keyword and term key/value pair.
$settings = $this->replace_keywords( $settings );
// Define Post Name / Slug.
// If no Permalink exists, use the Post Title.
if ( ! empty( $settings['permalink'] ) ) {
$post_name = sanitize_title( $settings['permalink'] );
} else {
$post_name = sanitize_title( $settings['title'] );
}
// Determine the Post Parent.
$post_parent = $this->get_post_parent( $group_id, $settings );
if ( is_wp_error( $post_parent ) ) {
return $this->generate_error_return(
$post_parent,
$group_id,
0,
$settings['type'],
$test_mode,
$system,
$keywords_terms
);
}
// Depending on the Ovewrite setting, check if an existing Post exists.
switch ( $settings['overwrite'] ) {
/**
* No, skip if existing Page generated by this Group
*/
case 'skip_if_exists':
// Find existing Post by Permalink generated by this Group.
$existing_post_id = $this->post_exists( $group_id, $settings['type'], $post_parent, $post_name );
// Bail if a Post is found, as we're skipping Generation.
if ( $existing_post_id > 0 ) {
return $this->generate_return(
$group_id,
$existing_post_id,
$settings['type'],
false,
sprintf(
/* translators: Post Type or Taxonomy Name */
__( 'Skipped, as %s with Permalink already generated by this Group', 'page-generator-pro' ),
$settings['type']
),
$start,
$test_mode,
$system,
$keywords_terms
);
}
break;
/**
* No, skip if existing Page exists
*/
case 'skip_if_exists_any':
// Find existing Post by Permalink, regardless of Group.
$existing_post_id = $this->post_exists( 0, $settings['type'], $post_parent, $post_name );
// Bail if a Post is found, as we're skipping Generation.
if ( $existing_post_id > 0 ) {
return $this->generate_return(
$group_id,
$existing_post_id,
$settings['type'],
false,
sprintf(
/* translators: Post Type or Taxonomy Name */
__( 'Skipped, as %s with Permalink already exists in WordPress', 'page-generator-pro' ),
$settings['type']
),
$start,
$test_mode,
$system,
$keywords_terms
);
}
break;
/**
* Yes, if existing Page generated by this Group
* Yes, if existing Page generated by this Group, preserving original Publish date
*/
case 'overwrite':
case 'overwrite_preseve_date':
// Try to find existing post.
$existing_post_id = $this->post_exists( $group_id, $settings['type'], $post_parent, $post_name );
// If no existing post found, this will not be an overwrite option, so break.
if ( ! $existing_post_id ) {
break;
}
// This will overwrite an existing post, so remove settings from the Group that are NOT to be overwritten now.
$settings = $this->remove_content_from_settings( $settings );
break;
/**
* Yes, if existing Page exists
* Yes, if existing Page exists, preserving original Publish date
*/
case 'overwrite_any':
case 'overwrite_any_preseve_date':
// Try to find existing post.
$existing_post_id = $this->post_exists( 0, $settings['type'], $post_parent, $post_name );
// If no existing post found, this will not be an overwrite option, so break.
if ( ! $existing_post_id ) {
break;
}
// This will overwrite an existing post, so remove settings from the Group that are NOT to be overwritten now.
$settings = $this->remove_content_from_settings( $settings );
break;
}
/**
* Modify the Group's settings prior to parsing shortcodes and building the Post Arguments
* to use for generating a single Page, Post or Custom Post Type.
*
* Changes made only affect this item in the generation set, and are not persistent or saved.
*
* For Gutenberg and Page Builders with Blocks / Elements registered by this Plugin, this
* is a good time to convert them to a Shortcode Block / Element / Text
*
* @since 2.6.0
*
* @param array $settings Group Settings.
* @param int $group_id Group ID.
* @param int $index Keyword Index.
* @param bool $test_mode Test Mode.
*/
$settings = apply_filters( 'page_generator_pro_generate_content_settings', $settings, $group_id, $index, $test_mode );
// Process Shortcodes.
// Blocks above that have been converted into Shortcodes will now be processed.
array_walk_recursive( $settings, array( $this, 'process_shortcodes_in_array' ) );
// Build Post args.
$post_args = array(
'post_type' => $settings['type'],
'post_title' => $settings['title'],
'post_content' => $settings['content'],
'post_status' => ( $test_mode ? 'draft' : $settings['status'] ),
'post_author' => ( ( isset( $settings['rotateAuthors'] ) && $settings['rotateAuthors'] == 1 && isset( $authors ) && isset( $user_index ) ) ? $authors[ $user_index ]->ID : $settings['author'] ), // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual
'comment_status' => ( ( isset( $settings['comments'] ) && $settings['comments'] == 1 ) ? 'open' : 'closed' ), // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual
'ping_status' => ( ( isset( $settings['trackbacks'] ) && $settings['trackbacks'] == 1 ) ? 'open' : 'closed' ), // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual
'post_parent' => $post_parent,
'post_name' => $post_name,
'post_date' => $this->post_date( $settings, $last_generated_post_date_time ),
);
// Define Post Excerpt, if the Post Type supports it.
if ( post_type_supports( $settings['type'], 'excerpt' ) ) {
$post_args['post_excerpt'] = $settings['excerpt'];
}
/**
* Filters arguments used for creating or updating a Post when running
* content generation.
*
* @since 1.6.1
*
* @param array $post_args wp_insert_post() / wp_update_post() compatible arguments.
* @param array $settings Content Group Settings.
*/
$post_args = apply_filters( 'page_generator_pro_generate_post_args', $post_args, $settings );
/**
* Run any actions immediately before an individual Page, Post or Custom Post Type is generated.
*
* @since 2.4.1
*
* @param int $group_id Group ID.
* @param array $settings Group Settings.
* @param int $index Keyword Index.
* @param bool $test_mode Test Mode.
*/
do_action( 'page_generator_pro_generate_content_before_insert_update_post', $group_id, $settings, $index, $test_mode );
// Create or Update a Post.
switch ( $settings['overwrite'] ) {
/**
* Overwrite
*/
case 'overwrite':
// If a Post was found, update it.
if ( isset( $existing_post_id ) && $existing_post_id > 0 ) {
// Define the Post ID to update.
$post_args['ID'] = $existing_post_id;
// Remove Post Args that we're not overwriting.
$post_args = $this->restrict_post_args_by_overwrite_sections( array_keys( $settings['overwrite_sections'] ), $post_args );
// Delete Attachments assigned to the existing Post ID created by this Group if we're overwriting the Content.
if ( array_key_exists( 'post_content', $settings['overwrite_sections'] ) ) {
$this->delete_attachments_by_post_ids( array( $existing_post_id ), $group_id );
}
// Delete Featured Image assigned to the existing Post ID created by this Group if we're overwriting the Featured Image.
if ( array_key_exists( 'featured_image', $settings['overwrite_sections'] ) ) {
$this->delete_featured_image_by_post_ids( array( $existing_post_id ), $group_id );
}
// Update Page, Post or CPT.
$post_id = wp_update_post( $post_args, true );
// Define return message.
$log = sprintf(
/* translators: Post Type Name */
__( 'Updated, as %s with Permalink already generated by this Group', 'page-generator-pro' ),
$settings['type']
);
} else {
// Create Page, Post or CPT.
$post_id = wp_insert_post( $post_args, true );
// Define return message.
$log = sprintf(
/* translators: Post Type Name */
__( 'Created, as %s with Permalink has not yet been generated by this Group', 'page-generator-pro' ),
$settings['type']
);
}
break;
/**
* Overwrite Any
*/
case 'overwrite_any':
// If a Post was found, update it.
if ( isset( $existing_post_id ) && $existing_post_id > 0 ) {
// Define the Post ID to update.
$post_args['ID'] = $existing_post_id;
// Remove Post Args that we're not overwriting.
$post_args = $this->restrict_post_args_by_overwrite_sections( array_keys( $settings['overwrite_sections'] ), $post_args );
// Delete Attachments assigned to the existing Post ID created by this Group if we're overwriting the Content.
if ( array_key_exists( 'post_content', $settings['overwrite_sections'] ) ) {
$this->delete_attachments_by_post_ids( array( $existing_post_id ), $group_id );
}
// Delete Featured Image assigned to the existing Post ID created by this Group if we're overwriting the Featured Image.
if ( array_key_exists( 'featured_image', $settings['overwrite_sections'] ) ) {
$this->delete_featured_image_by_post_ids( array( $existing_post_id ), $group_id );
}
// Update Page, Post or CPT.
$post_id = wp_update_post( $post_args, true );
// Define return message.
$log = sprintf(
/* translators: Post Type or Taxonomy Name */
__( 'Updated, as %s with Permalink already exists in WordPress', 'page-generator-pro' ),
$settings['type']
);
} else {
// Create Page, Post or CPT.
$post_id = wp_insert_post( $post_args, true );
// Define return message.
$log = sprintf(
/* translators: Post Type or Taxonomy Name */
__( 'Created, as %s with Permalink does not exist in WordPress', 'page-generator-pro' ),
$settings['type']
);
}
break;
/**
* Don't Overwrite
*/
default:
// Create Page, Post or CPT.
$post_id = wp_insert_post( $post_args, true );
// Define return message.
$log = sprintf(
__( 'Created', 'page-generator-pro' ),
$settings['type']
);
break;
}
// Check Post creation / update worked.
if ( is_wp_error( $post_id ) ) {
// Fetch error codes when trying to insert / update the Post.
$error_codes = $post_id->get_error_codes();
// Ignore invalid_page_template errors. wp_update_post() adds the existing page_template
// parameter to $post_args before passing onto wp_insert_post(); however the template
// might belong to a Page Builder Template that has / will not register the template with
// the active Theme.
// We manually assign _wp_page_template later on in this process, so we can safely ignore
// this error.
if ( count( $error_codes ) === 1 && $error_codes[0] === 'invalid_page_template' ) {
// The Post ID will be the existing Post ID we just updated.
$post_id = ( isset( $existing_post_id ) ? $existing_post_id : 0 );
} else {
// UTF-8 encode the Title, Excerpt and Content.
$post_args['post_title'] = mb_convert_encoding( $post_args['post_title'], 'UTF-8', mb_list_encodings() );
$post_args['post_content'] = mb_convert_encoding( $post_args['post_content'], 'UTF-8', mb_list_encodings() );
if ( post_type_supports( $settings['type'], 'excerpt' ) ) {
$post_args['post_excerpt'] = mb_convert_encoding( $post_args['post_excerpt'], 'UTF-8', mb_list_encodings() );
}
// Try again.
if ( isset( $post_args['ID'] ) ) {
// Remove Post Args that we're not overwriting.
$post_args = $this->restrict_post_args_by_overwrite_sections( array_keys( $settings['overwrite_sections'] ), $post_args );
// Update Page, Post or CPT.
$post_id = wp_update_post( $post_args, true );
} else {
// Create Page, Post or CPT.
$post_id = wp_insert_post( $post_args, true );
}
// If Post creation / update still didn't work, bail.
if ( is_wp_error( $post_id ) ) {
$post_id->add_data( $post_args, $post_id->get_error_code() );
return $this->generate_error_return(
$post_id,
$group_id,
0,
$settings['type'],
$test_mode,
$system,
$keywords_terms
);
}
}
}
/**
* Run any actions immediately after an individual Page, Post or Custom Post Type is generated, but before
* its Page Template, Featured Image, Custom Fields, Post Meta, Geodata or Taxonomy Terms have been assigned.
*
* @since 2.4.1
*
* @param int $post_id Post ID.
* @param int $group_id Group ID.
* @param array $settings Group Settings.
* @param int $index Keyword Index.
* @param bool $test_mode Test Mode.
*/
do_action( 'page_generator_pro_generate_content_after_insert_update_post', $post_id, $group_id, $settings, $index, $test_mode );
// Store this Group ID and Index in the Post's meta, so we can edit/delete the generated Post(s) in the future.
update_post_meta( $post_id, '_page_generator_pro_group', $group_id );
update_post_meta( $post_id, '_page_generator_pro_index', $index );
// Assign Attachments that may have been created by shortcode processing to the generated Post.
// We do this here as shortcodes are processed before a Post is generated, therefore any Attachments
// created won't have a Post ID.
$result = $this->assign_attachments_to_post_id( $post_id, $group_id, $index );
if ( is_wp_error( $result ) && defined( 'PAGE_GENERATOR_PRO_DEBUG' ) && PAGE_GENERATOR_PRO_DEBUG === true ) {
return $this->generate_error_return(
$result,
$group_id,
$post_id,
$settings['type'],
$test_mode,
$system,
$keywords_terms
);
}
// Store Page Template.
$this->set_page_template( $post_id, $group_id, $settings, $post_args );
// Store Custom Fields as Post Meta on the Generated Post.
$this->set_custom_fields( $post_id, $group_id, $settings, $post_args, $keywords_terms );
// Store Header and Footer Code on the Generated Post.
$this->set_header_footer_code( $post_id, $group_id, $settings, $post_args, $keywords_terms );
// Store Post Meta (ACF, Yoast, Page Builder data etc) on the Generated Post.
$this->set_post_meta( $post_id, $group_id, $settings, $post_args );
// Store Latitude and Longitude.
if ( $has_lat_lng ) {
$result = $this->latitude_longitude( $post_id, $group_id, $settings );
if ( is_wp_error( $result ) && defined( 'PAGE_GENERATOR_PRO_DEBUG' ) && PAGE_GENERATOR_PRO_DEBUG === true ) {
return $this->generate_error_return(
$result,
$group_id,
$post_id,
$settings['type'],
$test_mode,
$system,
$keywords_terms
);
}
}
// Assign Generated Post to Menu, if required and we're not in Test Mode.
if ( ! $test_mode ) {
$result = $this->set_menu( $post_id, $settings, $post_args );
if ( is_wp_error( $result ) && defined( 'PAGE_GENERATOR_PRO_DEBUG' ) && PAGE_GENERATOR_PRO_DEBUG === true ) {
return $this->generate_error_return(
$result,
$group_id,
$post_id,
$settings['type'],
$test_mode,
$system,
$keywords_terms
);
}
}
// Assign Taxonomy Terms to the Generated Post.
$result = $this->assign_taxonomy_terms_to_post( $post_id, $settings, $post_args );
if ( is_wp_error( $result ) && defined( 'PAGE_GENERATOR_PRO_DEBUG' ) && PAGE_GENERATOR_PRO_DEBUG === true ) {
return $this->generate_error_return(
$result,
$group_id,
$post_id,
$settings['type'],
$test_mode,
$system,
$keywords_terms
);
}
// Featured Image.
// We do this last so that Page Builders e.g. Divi don't overwrite the _thumbnail_id after Custom Field / Post Meta copying.
$image_id = $this->featured_image( $post_id, $group_id, $index, $settings, $post_args );
if ( is_wp_error( $image_id ) && defined( 'PAGE_GENERATOR_PRO_DEBUG' ) && PAGE_GENERATOR_PRO_DEBUG === true ) {
return $this->generate_error_return(
$image_id,
$group_id,
$post_id,
$settings['type'],
$test_mode,
$system,
$keywords_terms
);
}
// Generate Comments.
$result = $this->generate_comments( $post_id, $group_id, $index, $original_settings, $post_args );
if ( is_wp_error( $result ) && defined( 'PAGE_GENERATOR_PRO_DEBUG' ) && PAGE_GENERATOR_PRO_DEBUG === true ) {
return $this->generate_error_return(
$result,
$group_id,
$post_id,
$settings['type'],
$test_mode,
$system,
$keywords_terms
);
}
// Request that the user review the Plugin, if we're not in Test Mode. Notification displayed later,
// can be called multiple times and won't re-display the notification if dismissed.
if ( ! $test_mode && ! $this->base->licensing->has_feature( 'whitelabelling' ) ) {
$this->base->dashboard->request_review();
}
// Store current index as the last index generated for this Group, if we're not in test mode.
if ( ! $test_mode ) {
$this->base->get_class( 'groups' )->update_last_index_generated( $group_id, $index );
}
/**
* Run any actions after an individual Page, Post or Custom Post Type is generated
* successfully.
*
* @since 2.4.1
*
* @param int $post_id Generated Post ID.
* @param int $group_id Group ID.
* @param array $settings Group Settings.
* @param int $index Keyword Index.
* @param bool $test_mode Test Mode.
* @param array $post_args wp_insert_post() / wp_update_post() compatible arguments.
*/
do_action( 'page_generator_pro_generate_content_finished', $post_id, $group_id, $settings, $index, $test_mode, $post_args );
// Get Generated Post's Date.
// We don't use $post_args['post_date'] as it might not be set if overwriting dates are disabled.
$post_date = get_the_date( 'Y-m-d H:i:s', $post_id );
// Return success data.
return $this->generate_return( $group_id, $post_id, $settings['type'], true, $log, $start, $test_mode, $system, $keywords_terms, $post_date );
}
/**
* Generates a Taxonomy Term for the given Group and Index
*
* @since 1.0.0
*
* @param int $group_id Group ID.
* @param int $index Keyword Index.
* @param bool $test_mode Test Mode.
* @param string $system System (browser|cron|cli).
* @return WP_Error|array
*/
public function generate_term( $group_id, $index, $test_mode = false, $system = 'browser' ) {
// Performance debugging.
$start = ( function_exists( 'hrtime' ) ? hrtime( true ) : microtime( true ) );
// If test mode is enabled, set the debug constant.
if ( $test_mode && ! defined( 'PAGE_GENERATOR_PRO_DEBUG' ) ) {
define( 'PAGE_GENERATOR_PRO_DEBUG', true );
}
// If this Group has a request to cancel generation, exit.
if ( ! $test_mode ) {
if ( $this->base->get_class( 'groups_terms' )->cancel_generation_requested( $group_id ) ) {
$this->base->get_class( 'groups_terms' )->stop_generation( $group_id );
return new WP_Error( 'generation_error', __( 'A request to cancel generation was made by the User. Exiting...', 'page-generator-pro' ) );
}
}
// Get group settings.
$settings = $this->base->get_class( 'groups_terms' )->get_settings( $group_id, false, true );
// Bail if an error occured.
if ( is_wp_error( $settings ) ) {
return $settings;
}
/**
* Run any actions before an individual Term is generated successfully.
*
* @since 2.4.1
*
* @param int $group_id Group ID.
* @param array $settings Group Settings.
* @param int $index Keyword Index.
* @param bool $test_mode Test Mode.
*/
do_action( 'page_generator_pro_generate_term_started', $group_id, $settings, $index, $test_mode );
// Validate group.
$validated = $this->base->get_class( 'groups_terms' )->validate( $group_id );
if ( is_wp_error( $validated ) ) {
return $validated;
}
// Build a class array of required keywords that need replacing with data.
$required_keywords = $this->find_keywords_in_settings( $settings );
if ( count( $required_keywords['required_keywords'] ) === 0 ) {
return new WP_Error( 'page_generator_pro_generate_generate_term_keyword_error', __( 'No keywords were specified in the Group.', 'page-generator-pro' ) );
}
// Build a keywords array comprising of terms, columns and delimiters for each of the required keywords.
$this->keywords = $this->get_keywords_terms_columns_delimiters( $required_keywords['required_keywords'] );
if ( count( $this->keywords['terms'] ) === 0 ) {
return new WP_Error( 'page_generator_pro_generate_generate_term_keyword_error', __( 'Keywords were specified in the Group, but no keywords exist in either the Keywords section of the Plugin or as a Taxonomy.', 'page-generator-pro' ) );
}
// Build array of keyword --> term key/value pairs to use for this generation.
$keywords_terms = $this->get_keywords_terms( $settings['method'], (int) $index );
if ( is_wp_error( $keywords_terms ) ) {
return $keywords_terms;
}
// Iterate through each detected Keyword to build a full $this->searches and $this->replacements arrays.
$this->build_search_replace_arrays( $required_keywords['required_keywords_full'], $keywords_terms );
// Iterate through each keyword and term key/value pair.
$settings = $this->replace_keywords( $settings );
// Build Term args.
$term_args = array(
'description' => $settings['excerpt'],
);
// Define Slug.
// If no Permalink exists, use the Title.
if ( ! empty( $settings['permalink'] ) ) {
$term_args['slug'] = sanitize_title( $settings['permalink'] );
}
// If the taxonomy is hierarhical, and a parent term has been specified, determine its ID,
// creating terms if necessary.
$term_args['parent'] = 0;
if ( is_taxonomy_hierarchical( $settings['taxonomy'] ) && ! empty( $settings['parent_term'] ) ) {
$term_args['parent'] = $this->get_term_path_id( $settings['taxonomy'], $settings['parent_term'] );
}
/**
* Filters arguments used for creating or updating a Term when running
* content generation.
*
* @since 1.6.1
*
* @param array $term_args wp_insert_term() / wp_update_term() compatible arguments.
* @param array $settings Content Group Settings.
*/
$term_args = apply_filters( 'page_generator_pro_generate_term_args', $term_args, $settings );
// Depending on the Overwrite setting, check if an existing Term exists.
switch ( $settings['overwrite'] ) {
/**
* No, skip if existing Term generated by this Group
*/
case 'skip_if_exists':
// Find existing Term by Permalink generated by this Group.
$existing_term_id = $this->term_exists( $group_id, $settings['taxonomy'], $term_args['parent'], $settings['title'] );
// Bail if a Term is found, as we're skipping Generation.
if ( $existing_term_id > 0 ) {
return $this->generate_return(
$group_id,
$existing_term_id,
$settings['taxonomy'],
false,
sprintf(
/* translators: Post Type or Taxonomy Name */
__( 'Skipped, as %s with Permalink already generated by this Group', 'page-generator-pro' ),
$settings['taxonomy']
),
$start,
$test_mode,
$system,
$keywords_terms
);
}
break;
/**
* No, skip if existing Term exists
*/
case 'skip_if_exists_any':
// Find existing Post by Permalink, regardless of Group.
$existing_term_id = $this->term_exists( 0, $settings['taxonomy'], $term_args['parent'], $settings['title'] );
// Bail if a Post is found, as we're skipping Generation.
if ( $existing_term_id > 0 ) {
return $this->generate_return(
$group_id,
$existing_term_id,
$settings['taxonomy'],
false,
sprintf(
/* translators: Post Type or Taxonomy Name */
__( 'Skipped, as %s with Permalink already exists in WordPress', 'page-generator-pro' ),
$settings['taxonomy']
),
$start,
$test_mode,
$system,
$keywords_terms
);
}
break;
/**
* Yes, if existing Term generated by this Group
*/
case 'overwrite':
// Try to find existing term.
$existing_term_id = $this->term_exists( $group_id, $settings['taxonomy'], $term_args['parent'], $settings['title'] );
break;
/**
* Yes, if existing Term exists
*/
case 'overwrite_any':
// Try to find existing post.
$existing_term_id = $this->term_exists( 0, $settings['taxonomy'], $term_args['parent'], $settings['title'] );
break;
}
/**
* Run any actions immediately before an individual Term is generated.
*
* @since 2.4.1
*
* @param int $group_id Group ID.
* @param array $settings Group Settings.
* @param int $index Keyword Index.
* @param bool $test_mode Test Mode.
*/
do_action( 'page_generator_pro_generate_term_before_insert_update_term', $group_id, $settings, $index, $test_mode );
// Create or Update a Term.
switch ( $settings['overwrite'] ) {
/**
* Overwrite
*/
case 'overwrite':
// If a Term was found, update it.
if ( isset( $existing_term_id ) && $existing_term_id > 0 ) {
// Update Term.
$term = wp_update_term( $existing_term_id, $settings['taxonomy'], $term_args );
// Define return message.
$log = __( 'Updated, as Term with Permalink already generated by this Group', 'page-generator-pro' );
} else {
// Create Term.
$term = wp_insert_term( $settings['title'], $settings['taxonomy'], $term_args );
// Define return message.
$log = __( 'Created, as Term with Permalink has not yet been generated by this Group', 'page-generator-pro' );
}
break;
/**
* Overwrite Any
*/
case 'overwrite_any':
// If a Term was found, update it.
if ( isset( $existing_term_id ) && $existing_term_id > 0 ) {
// Update Term.
$term = wp_update_term( $existing_term_id, $settings['taxonomy'], $term_args );
// Define return message.
$log = sprintf(
/* translators: Post Type or Taxonomy Name */
__( 'Updated, as %s with Permalink already exists in WordPress', 'page-generator-pro' ),
$settings['taxonomy']
);
} else {
// Create Term.
$term = wp_insert_term( $settings['title'], $settings['taxonomy'], $term_args );
// Define return message.
$log = sprintf(
/* translators: Post Type or Taxonomy Name */
__( 'Created, as %s with Permalink does not exist in WordPress', 'page-generator-pro' ),
$settings['taxonomy']
);
}
break;
/**
* Don't Overwrite
*/
default:
// If the Term already exists in this Taxonomy, just return it.
// This prevents calling wp_insert_term(), which would WP_Error when the Taxonomy Term already exists.
$existing_term_id = $this->term_exists( 0, $settings['taxonomy'], $term_args['parent'], $settings['title'] );
if ( $existing_term_id > 0 ) {
return $this->generate_return(
$group_id,
$existing_term_id,
$settings['taxonomy'],
false,
sprintf(
/* translators: Post Type or Taxonomy Name */
__( 'Skipped, as %s with Permalink already exists in WordPress', 'page-generator-pro' ),
$settings['taxonomy']
),
$start,
$test_mode,
$system,
$keywords_terms
);
} else {
// Create Term.
$term = wp_insert_term( $settings['title'], $settings['taxonomy'], $term_args );
// Define return message.
$log = sprintf(
/* translators: Taxonomy name */
__( 'Created', 'page-generator-pro' ),
$settings['taxonomy']
);
}
break;
}
// Check Term creation / update worked.
if ( is_wp_error( $term ) ) {
$term->add_data( $term_args, $term->get_error_code() );
return $term;
}
/**
* Run any actions immediately after an individual Taxonomy Term is generated, but before
* its Custom Fields or Term Meta have been assigned.
*
* @since 2.6.3
*
* @param array $term Generated Term.
* @param int $group_id Group ID.
* @param array $settings Group Settings.
* @param int $index Keyword Index.
* @param bool $test_mode Test Mode.
*/
do_action( 'page_generator_pro_generate_term_after_insert_update_term', $term, $group_id, $settings, $index, $test_mode );
// Store this Group ID and Index in the Term's meta, so we can edit/delete the generated Term(s) in the future.
update_term_meta( $term['term_id'], '_page_generator_pro_group', $group_id );
update_term_meta( $term['term_id'], '_page_generator_pro_index', $index );
// Store Term Meta (ACF, Yoast, Page Builder data etc) on the Generated Term.
$this->set_term_meta( $term['term_id'], $group_id, $settings, $term_args );
// Request that the user review the Plugin, if we're not in Test Mode. Notification displayed later,
// can be called multiple times and won't re-display the notification if dismissed.
if ( ! $test_mode && ! $this->base->licensing->has_feature( 'whitelabelling' ) ) {
$this->base->dashboard->request_review();
}
// Store current index as the last index generated for this Group, if we're not in test mode.
if ( ! $test_mode ) {
$this->base->get_class( 'groups_terms' )->update_last_index_generated( $group_id, $index );
}
/**
* Run any actions after an individual Term is generated successfully.
*
* @since 2.4.1
*
* @param array $term Generated Term.
* @param int $group_id Group ID.
* @param array $settings Group Settings.
* @param int $index Keyword Index.
* @param bool $test_mode Test Mode.
*/
do_action( 'page_generator_pro_generate_term_finished', $term, $group_id, $settings, $index, $test_mode );
// Return the URL and keyword / term replacements used.
return $this->generate_return( $group_id, $term['term_id'], $settings['taxonomy'], true, $log, $start, $test_mode, $system, $keywords_terms );
}
/**
* Resets the Search and Replacement class arrays
*
* @since 3.0.4
*/
private function reset_search_replace_arrays() {
// Reset search and replacement arrays.
$this->searches = array();
$this->replacements = array();
}
/**
* For all Keyword tags found in the Group, builds search and replacement class arrays for later use
* when recursively iterating through a Group's settings to replace the Keyword tags with their Term counterparts
*
* @since 2.6.1
*
* @param array $required_keywords_full Required Keywords, Full.
* @param array $keywords_terms Keywords / Terms Key/Value Pairs.
*/
public function build_search_replace_arrays( $required_keywords_full, $keywords_terms ) {
// Reset search and replacement arrays.
$this->reset_search_replace_arrays();
foreach ( $required_keywords_full as $keyword => $keywords_with_modifiers ) {
// Build search and replacement arrays for this Keyword.
foreach ( $keywords_with_modifiers as $keyword_with_modifiers ) {
// If the Keyword isn't truly a Keyword in the database, don't do anything.
if ( ! isset( $keywords_terms[ $keyword ] ) ) {
continue;
}
// Cast keyword as a string so numeric keywords don't break search/replace.
$this->build_search_replace_arrays_for_keyword( $keyword_with_modifiers, (string) $keyword, $keywords_terms[ $keyword ] );
}
}
}
/**
* Appends the search and replace arrays for the given Keyword (column name, nth term, transformations) and its applicable Term.
*
* @since 2.6.1
*
* @param string $keyword_with_modifiers Keyword with Modifiers (search, e.g. keyword(column):3:uppercase_all:url.
* @param string $keyword Keyword without Modifiers (e.g. keyword).
* @param string $term Term (replacement).
*/
public function build_search_replace_arrays_for_keyword( $keyword_with_modifiers, $keyword, $term ) {
// If the Keyword with Modifiers matches the Keyword, we have no modifiers
// Just return the term.
if ( $keyword_with_modifiers == $keyword ) { // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual
$this->searches[] = '{' . $keyword_with_modifiers . '}';
$this->replacements[] = $term;
return;
}
// Fetch an array of transformations that might exist and need applying to the Term.
$keyword_transformations = false;
if ( strpos( $keyword_with_modifiers, ':' ) !== false ) {
$keyword_transformations = explode( ':', $keyword_with_modifiers );
}
// If Keyword Transformation(s) exist, and one Transformation is numeric, this is an nth term specifier
// Fetch the Keyword's nth Term now as the Term to use.
if ( $keyword_transformations ) {
foreach ( $keyword_transformations as $keyword_transformation ) {
if ( ! is_numeric( $keyword_transformation ) ) {
continue;
}
// Keyword Transformation is an nth term specifier.
if ( isset( $this->keywords['terms'][ $keyword ][ $keyword_transformation - 1 ] ) ) {
$term = $this->keywords['terms'][ $keyword ][ $keyword_transformation - 1 ];
}
}
}
// If the Keyword contains a column, fetch the Keyword Term's Column value now.
$column = false;
$keyword_column_start_bracket_position = strpos( $keyword_with_modifiers, '(' );
$keyword_column_end_bracket_position = strpos( $keyword_with_modifiers, ')', $keyword_column_start_bracket_position );
if ( $keyword_column_start_bracket_position !== false && $keyword_column_end_bracket_position !== false ) {
// Extract Column Name.
$column = substr( $keyword_with_modifiers, ( $keyword_column_start_bracket_position + 1 ), ( $keyword_column_end_bracket_position - $keyword_column_start_bracket_position - 1 ) );
// Split the Term into Columns.
$term_parts = str_getcsv( $term, $this->keywords['delimiters'][ $keyword ] );
// Fetch the Column Index.
$column_index = array_search( $column, $this->keywords['columns'][ $keyword ] ); // phpcs:ignore WordPress.PHP.StrictInArray
// Fetch the Term.
if ( $column_index !== false && isset( $term_parts[ $column_index ] ) ) {
$term = trim( $term_parts[ $column_index ] );
}
}
// If Keyword Transformation(s) exist, transform the Term using each Transformation in the order
// they're listed.
if ( $keyword_transformations ) {
foreach ( $keyword_transformations as $keyword_transformation ) {
// Keyword Transformation is an nth term specifier; skip as we dealt with this earlier.
if ( is_numeric( $keyword_transformation ) ) {
continue;
}
$term = $this->apply_keyword_transformation( $keyword_transformation, $term, $keyword, $column );
}
}
// Add Keyword and Term to Search and Replace arrays.
$this->searches[] = '{' . $keyword_with_modifiers . '}';
$this->replacements[] = $term;
// If $term is an array, set a flag.
if ( is_array( $term ) ) {
$this->replacements_contain_array = true;
}
}
/**
* Applies the given keyword transformation to the given string (term)
*
* @since 2.2.3
*
* @param string $keyword_transformation Keyword Transformation.
* @param string $term Term.
* @param string $keyword Keyword.
* @param bool|string $column Keyword Column.
* @return string|array Transformed Term
*/
private function apply_keyword_transformation( $keyword_transformation, $term, $keyword, $column = false ) {
$arguments = false;
// Split out the Keyword Transformation's arguments, if any exist.
preg_match_all( '/\[([^\]]*)\]/', $keyword_transformation, $matches );
// If arguments exist, remove them from the Keyword Transformation so we get e.g.
// nearby[city_latitude,city_longitude,3,distance] --> nearby.
if ( ! empty( $matches[0] ) ) {
$keyword_transformation = str_replace( $matches[0][0], '', $keyword_transformation );
$arguments = explode( ',', $matches[1][0] );
}
switch ( $keyword_transformation ) {
/**
* Uppercase
*/
case 'uppercase_all':
// Use i18n compatible method if available.
if ( function_exists( 'mb_convert_case' ) ) {
return mb_convert_case( $term, MB_CASE_UPPER );
}
// Fallback to basic version which doesn't support i18n.
return strtoupper( $term );
/**
* Lowercase
*/
case 'lowercase_all':
// Use i18n compatible method if available.
if ( function_exists( 'mb_convert_case' ) ) {
return mb_convert_case( $term, MB_CASE_LOWER );
}
// Fallback to basic version which doesn't support i18n.
return strtolower( $term );
/**
* Upperchase first character
*/
case 'uppercase_first_character':
// Use i18n compatible method if available.
if ( function_exists( 'mb_strtoupper' ) ) {
return mb_strtoupper( mb_substr( $term, 0, 1 ) ) . mb_substr( $term, 1 );
}
// Fallback to basic version which doesn't support i18n.
return ucfirst( $term );
/**
* Uppercase first character of each word
*/
case 'uppercase_first_character_words':
// Use i18n compatible method if available.
if ( function_exists( 'mb_convert_case' ) ) {
return mb_convert_case( $term, MB_CASE_TITLE );
}
// Fallback to basic version which doesn't support i18n.
return ucwords( $term );
/**
* First Word
*/
case 'first_word':
$term_parts = explode( ' ', $term );
return $term_parts[0];
/**
* Last Word
*/
case 'last_word':
$term_parts = explode( ' ', $term );
return $term_parts[ count( $term_parts ) - 1 ];
/**
* URL
*/
case 'url':
return sanitize_title( $term );
/**
* URL, Underscore
*/
case 'url_underscore':
return str_replace( '-', '_', sanitize_title( $term ) );
/**
* Number to Words
*/
case 'number_to_words':
// Strip commas.
$term = str_replace( ',', '', $term );
// Bail if Term isn't numeric.
if ( ! is_numeric( $term ) ) {
return $term;
}
// Bail if PHP version is older than 7.4.
if ( phpversion() < '7.4' ) {
return $term;
}
// Extract arguments.
$args = array(
'lang' => ( isset( $arguments[0] ) ? $arguments[0] : get_locale() ),
);
// Some language codes are case sensitive and become lowercased, so we need to correct them
// for the library to work.
switch ( $args['lang'] ) {
case 'fr_be':
$args['lang'] = 'fr_BE';
break;
case 'pt_br':
$args['lang'] = 'pt_BR';
break;
}
// Convert to integer.
$term = (int) $term;
// Setup class.
$number_to_words = new NumberToWords\NumberToWords();
// Get number transformer for the language, falling back to English
// if the given language code is not supported.
try {
$transformer = $number_to_words->getNumberTransformer( $args['lang'] );
} catch ( NumberToWords\Exception\InvalidArgumentException $e ) {
$transformer = $number_to_words->getNumberTransformer( 'en' );
}
// Convert to words.
return $transformer->toWords( $term );
/**
* Currency to Words
*/
case 'currency_to_words':
// Strip commas.
$term = str_replace( ',', '', $term );
// Bail if Term isn't numeric.
if ( ! is_numeric( $term ) ) {
return $term;
}
// Bail if PHP version is older than 7.4.
if ( phpversion() < '7.4' ) {
return $term;
}
// Extract arguments.
$args = array(
'lang' => ( isset( $arguments[0] ) ? $arguments[0] : get_locale() ),
'currency' => ( isset( $arguments[1] ) ? $arguments[1] : 'USD' ),
);
// Some language codes are case sensitive and become lowercased, so we need to correct them
// for the library to work.
switch ( $args['lang'] ) {
case 'fr_be':
$args['lang'] = 'fr_BE';
break;
case 'pt_br':
$args['lang'] = 'pt_BR';
break;
}
// Convert to integer, multiplying by 100 to convert to e.g. cents.
$term = (int) ( $term * 100 );
// Setup class.
$number_to_words = new NumberToWords\NumberToWords();
// Get number transformer for the language, falling back to English
// if the given language code is not supported.
try {
$transformer = $number_to_words->getCurrencyTransformer( $args['lang'] );
} catch ( NumberToWords\Exception\InvalidArgumentException $e ) {
$transformer = $number_to_words->getCurrencyTransformer( 'en' );
}
// Convert to words.
return $transformer->toWords( $term, $args['currency'] );
/**
* All, comma separated
*/
case 'all':
if ( $column ) {
$terms = array();
foreach ( $this->keywords['terms'][ $keyword ] as $term ) {
// Split the term.
$term_parts = str_getcsv( $term, $this->keywords['delimiters'][ $keyword ] );
// Fetch the column index.
$column_index = array_search( $column, $this->keywords['columns'][ $keyword ] ); // phpcs:ignore WordPress.PHP.StrictInArray
// Skip if no column index could be found.
if ( $column_index === false ) {
continue;
}
$terms[] = ( isset( $term_parts[ $column_index ] ) ? trim( $term_parts[ $column_index ] ) : '' );
}
// Remove duplicates.
$terms = array_values( array_unique( $terms ) );
// Return All Column Terms for the Keyword.
return implode( ', ', $terms );
}
// Return all Terms for the Keyword.
return implode( ', ', $this->keywords['terms'][ $keyword ] );
/**
* Random
*/
case 'random':
if ( $column ) {
$terms = array();
foreach ( $this->keywords['terms'][ $keyword ] as $term ) {
// Split the term.
$term_parts = str_getcsv( $term, $this->keywords['delimiters'][ $keyword ] );
// Fetch the column index.
$column_index = array_search( $column, $this->keywords['columns'][ $keyword ] ); // phpcs:ignore WordPress.PHP.StrictInArray
// Skip if no column index could be found.
if ( $column_index === false ) {
continue;
}
$terms[] = ( isset( $term_parts[ $column_index ] ) ? trim( $term_parts[ $column_index ] ) : '' );
}
// Remove duplicates.
$terms = array_values( array_unique( $terms ) );
} else {
$terms = $this->keywords['terms'][ $keyword ];
}
// If an argument is specified, pick the given number of Terms at random
// and return them as a comma separated list.
if ( is_array( $arguments ) && is_numeric( $arguments[0] ) ) {
$random_terms = array();
for ( $i = 0; $i < $arguments[0]; $i++ ) {
$random_terms[] = $terms[ wp_rand( 0, ( count( $terms ) - 1 ) ) ];
}
return implode( ', ', $random_terms );
}
// Return blank string if Terms array is empty.
if ( ! count( $terms ) ) {
return '';
}
// Return single random Term.
return $terms[ wp_rand( 0, ( count( $terms ) - 1 ) ) ];
/**
* Random, Different
* - Returns an array, so generation can pick a Term at random each time
*/
case 'random_different':
if ( $column ) {
$terms = array();
foreach ( $this->keywords['terms'][ $keyword ] as $term ) {
// Split the term.
$term_parts = str_getcsv( $term, $this->keywords['delimiters'][ $keyword ] );
// Fetch the column index.
$column_index = array_search( $column, $this->keywords['columns'][ $keyword ] ); // phpcs:ignore WordPress.PHP.StrictInArray
// Skip if no column index could be found.
if ( $column_index === false ) {
continue;
}
$terms[] = ( isset( $term_parts[ $column_index ] ) ? trim( $term_parts[ $column_index ] ) : '' );
}
// Remove duplicates.
$terms = array_values( array_unique( $terms ) );
// Return All Column Terms for the Keyword.
return $terms;
}
// Return all Terms for the Keyword.
return $this->keywords['terms'][ $keyword ];
/**
* Random Subset
* - Returns e.g. three,four from a Term / Term Column of one,two,three,four,five
*/
case 'random_subset':
// Just return the Term if it doesn't contain a delimiter.
if ( strpos( $term, ',' ) === false ) {
return $term;
}
// Build array of Terms.
$terms = explode( ',', $term );
// Fetch arguments.
$args = array(
'min' => absint( ( isset( $arguments[0] ) ? $arguments[0] : 1 ) ), // Minimum number of subsets to return.
'max' => absint( ( isset( $arguments[1] ) ? $arguments[1] : 0 ) ), // Maximum number of subsets to return.
);
// Bail if min is zero.
if ( ! $args['min'] ) {
return $term;
}
// Determine the number of subsets to pick.
$subsets = ( ! $args['max'] ? $args['min'] : wp_rand( $args['min'], $args['max'] ) );
// Fetch the number of subsets from the array of Terms.
$terms_keys = array_rand( $terms, $subsets );
// Shuffle the order of the keys to produce a random variation.
if ( count( $terms_keys ) > 1 ) {
shuffle( $terms_keys );
}
// Build array of subsets.
$random_term_subsets = array();
foreach ( $terms_keys as $index ) {
$random_term_subsets[] = $terms[ $index ];
}
// Implode.
$term = implode( ', ', $random_term_subsets );
// Return.
return $term;
/**
* Nearby
*/
case 'nearby':
$args = array(
( isset( $arguments[0] ) ? $arguments[0] : 'city_latitude' ),
( isset( $arguments[1] ) ? $arguments[1] : 'city_longitude' ),
( isset( $arguments[2] ) ? $arguments[2] : 3 ),
( isset( $arguments[3] ) ? $arguments[3] : false ),
);
return $this->apply_nearby_keyword_transformation(
$term,
$keyword,
$column,
$args[0],
$args[1],
$args[2],
$args[3]
);
/**
* Other Transformations
*/
default:
/**
* Filter to perform non-standard keyword transformation.
*
* @since 1.7.8
*
* @param string $term Term.
* @param string $transformation Keyword Transformation.
* @param string $keyword Keyword.
* @param string|bool $column Keyword Column.
*/
$term = apply_filters( 'page_generator_pro_generate_generate_content_apply_keyword_transformation', $term, $keyword_transformation, $keyword, $column );
return $term;
}
}
/**
* Applies the 'Nearby' keyword transformation to the given string (term)
*
* @since 3.3.1
*
* @param string $term Term.
* @param string $keyword Keyword.
* @param bool|string $column Keyword Column.
* @param string $latitude_column Latitude Column in Keyword.
* @param string $longitude_column Longitude Column in Keyword.
* @param int $radius Radius.
* @param bool $order_by_distance Order By Distance.
* @return string Transformed Term
*/
private function apply_nearby_keyword_transformation( $term, $keyword, $column, $latitude_column, $longitude_column, $radius = 3, $order_by_distance = false ) {
// Bail if no Column.
if ( ! $column ) {
return '';
}
// Get Latitude and Longitude of current Term.
foreach ( $this->keywords['terms'][ $keyword ] as $row => $keyword_term ) {
// Split the term.
$term_parts = str_getcsv( $keyword_term, $this->keywords['delimiters'][ $keyword ] );
// Fetch the column index.
$column_index = array_search( $column, $this->keywords['columns'][ $keyword ] ); // phpcs:ignore WordPress.PHP.StrictInArray
// Skip if no column index could be found.
if ( $column_index === false ) {
continue;
}
// Skip if this isn't the current Term.
if ( trim( $term_parts[ $column_index ] ) != $term ) { // phpcs:ignore Universal.Operators.StrictComparisons.LooseNotEqual
continue;
}
// If here, we found the current Term in the Keyword data.
// Fetch the column indicies for latitude and longitude.
$column_index_latitude = array_search( $latitude_column, $this->keywords['columns'][ $keyword ] ); // phpcs:ignore WordPress.PHP.StrictInArray
$column_index_longitude = array_search( $longitude_column, $this->keywords['columns'][ $keyword ] ); // phpcs:ignore WordPress.PHP.StrictInArray
// Skip if no latitude and longitude columns exist.
if ( $column_index_latitude === false || $column_index_longitude === false ) {
return '';
}
// Get Term's Latitude and Longitude, and break.
$term_latitude = (float) trim( $term_parts[ $column_index_latitude ] );
$term_longitude = (float) trim( $term_parts[ $column_index_longitude ] );
break;
}
// If no Latitude and Longitude found, bail.
if ( ! isset( $term_latitude ) || ! isset( $term_longitude ) ) {
return '';
}
// Iterate through Keyword Terms again, adding to the Terms array for rows that fall within the radius of the current Term's
// latitude and longitude .
$terms = array();
foreach ( $this->keywords['terms'][ $keyword ] as $row => $keyword_term ) {
// Split the term to fetch the latitude and longitude.
$term_parts = str_getcsv( $keyword_term, $this->keywords['delimiters'][ $keyword ] );
$value = trim( $term_parts[ $column_index ] );
$latitude = (float) ( isset( $column_index_latitude ) ? trim( $term_parts[ $column_index_latitude ] ) : 0 );
$longitude = (float) ( isset( $column_index_longitude ) ? trim( $term_parts[ $column_index_longitude ] ) : 0 );
// Skip if value is the Term.
if ( $value == $term ) { // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual
continue;
}
// Add Term if falls within radius.
$distance = 3959 *
acos(
cos( 0.0174532925 * $term_latitude ) *
cos( 0.0174532925 * $latitude ) *
cos( ( 0.0174532925 * $longitude ) - ( 0.0174532925 * $term_longitude ) ) +
sin( 0.0174532925 * $term_latitude ) *
sin( 0.0174532925 * $latitude )
);
if ( $distance > $radius ) {
continue;
}
$terms[] = array(
'value' => $value,
'distance' => $distance,
);
}
// If no Terms, bail.
if ( ! count( $terms ) ) {
return '';
}
// Sort by distance if required.
if ( $order_by_distance ) {
usort(
$terms,
function ( $a, $b ) {
return $a['distance'] <=> $b['distance'];
}
);
}
// Return comma separated list.
$terms_list = array();
foreach ( $terms as $term ) {
$terms_list[] = $term['value'];
}
return implode( ', ', $terms_list );
}
/**
* Helper method to iterate through each keyword's tags, including any modifiers,
* building search and replacement arrays before recursively iterating through the supplied settings,
* replacing the keywords and their transformations with the terms.
*
* @since 1.9.8
*
* @param array $settings Group Settings.
* @return array Group Settings
*/
public function replace_keywords( $settings ) {
// Iterate through Group Settings, replacing $this->searches (Keywords) with $this->replacements (Terms)
// as well as performing spintax and shortcode processing.
array_walk_recursive( $settings, array( $this, 'replace_keywords_in_array' ) );
// Return.
return $settings;
}
/**
* Returns an array comprising of all keywords and their term replacements,
* including keywords with column names in the format keyword_column.
*
* Does not include transformations or nth terms
*
* Used to store basic keyword/term data in the generated Page's Post Meta
* if Store Keywords is enabled
*
* @since 2.2.8
*
* @param array $keywords_terms Keyword / Term Key/Value Pairs.
* @return bool|array Keyword / Term Key/Value Pairs
*/
private function get_keywords_terms_array_with_columns( $keywords_terms ) {
$store_keywords = array();
foreach ( $keywords_terms as $keyword => $term ) {
// Add keyword/term pair.
$store_keywords[ $keyword ] = $term;
// If no columns exist for this Keyword, continue.
if ( ! isset( $this->keywords['columns'] ) ) {
continue;
}
if ( ! isset( $this->keywords['columns'][ $keyword ] ) ) {
continue;
}
foreach ( $this->keywords['columns'][ $keyword ] as $column ) {
// Skip if column name is empty.
if ( empty( $column ) ) {
continue;
}
// Split the term.
$term_parts = str_getcsv( $term, $this->keywords['delimiters'][ $keyword ] );
// Fetch the column index.
$column_index = array_search( $column, $this->keywords['columns'][ $keyword ] ); // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict
// Skip if no column index could be found.
if ( $column_index === false ) {
continue;
}
// Add to the search and replace arrays.
$store_keywords[ $keyword . '_' . $column ] = ( isset( $term_parts[ $column_index ] ) ? trim( $term_parts[ $column_index ] ) : '' );
}
}
// Bail if no keywords.
if ( count( $store_keywords ) === 0 ) {
return false;
}
return $store_keywords;
}
/**
* A faster method for fetching all keyword combinations for PHP 5.5+
*
* @since 1.5.1
*
* @param array $input Multidimensional array of Keyword Names (keys) => Terms (values).
* @return \Page_Generator_Pro_Cartesian_Product Cartesian Product
*/
private function generate_all_combinations( $input ) {
// Load class.
require_once $this->base->plugin->folder . '/includes/admin/cartesian-product.php';
// Return.
return new Page_Generator_Pro_Cartesian_Product( $input );
}
/**
* Recursively goes through the settings array, finding any {keywords}
* specified, to build up an array of keywords we need to fetch.
*
* @since 1.0.0
*
* @param array $settings Settings.
* @return array Required Keywords
*/
public function find_keywords_in_settings( $settings ) {
// Reset required keywords array.
$this->required_keywords = array();
$this->required_keywords_full = array();
// Recursively walk through all settings to find all keywords.
array_walk_recursive( $settings, array( $this, 'find_keywords_in_string' ) );
// Build results.
$results = array(
'required_keywords' => $this->required_keywords, // Keywords only.
'required_keywords_full' => $this->required_keywords_full, // Includes columns and modifiers.
);
// Reset required keywords array.
$this->required_keywords = array();
$this->required_keywords_full = array();
return $results;
}
/**
* For the given array of keywords, only returns keywords with terms, column names and delimiters
* where each keywords have terms.
*
* @since 1.6.5
*
* @param array $required_keywords Required Keywords.
* @return array Keywords with Terms, Columns and Delimiters
*/
public function get_keywords_terms_columns_delimiters( $required_keywords ) {
// Define blank array for keywords with terms and keywords with columns.
$results = array(
'terms' => array(),
'columns' => array(),
'delimiters' => array(),
);
foreach ( $required_keywords as $key => $keyword ) {
// Get terms for this keyword.
// If this keyword starts with 'taxonomy_', try to fetch the terms for the Taxonomy.
if ( strpos( $keyword, 'taxonomy_' ) !== false && strpos( $keyword, 'taxonomy_' ) === 0 ) {
$result = get_terms(
array(
'taxonomy' => str_replace( 'taxonomy_', '', $keyword ),
'hide_empty' => false,
'fields' => 'names',
'update_term_meta_cache' => false,
)
);
// Skip if no results.
if ( ! is_array( $result ) ) {
continue;
}
if ( count( $result ) === 0 ) {
continue;
}
$results['terms'][ $keyword ] = $result;
} else {
$result = $this->base->get_class( 'keywords' )->get_by( 'keyword', $keyword );
// Skip if no results.
if ( ! is_array( $result ) ) {
continue;
}
if ( count( $result ) === 0 ) {
continue;
}
$results['terms'][ $keyword ] = $result['dataArr'];
// Ensure column names are lowercase so array_search() works against our lowercase keyword + column.
$results['columns'][ $keyword ] = array();
foreach ( $result['columnsArr'] as $column ) {
$results['columns'][ $keyword ][] = strtolower( $column );
}
$results['delimiters'][ $keyword ] = $result['delimiter'];
}
}
// Return results.
return $results;
}
/**
* Returns an array of keyword and term key / value pairs.
*
* @since 1.0.0
*
* @param string $method Generation Method.
* @param int $index Generation Index.
* @return WP_Error|array
*/
private function get_keywords_terms( $method, $index ) {
$keywords_terms = array();
switch ( $method ) {
/**
* All
* - Generates all possible term combinations across keywords
*/
case 'all':
// Use our Cartesian Product class, which implements a Generator
// to allow iteration of data without needing to build an array in memory.
// See: http://php.net/manual/en/language.generators.overview.php.
$combinations = $this->generate_all_combinations( $this->keywords['terms'] );
// If the current index exceeds the total number of combinations, we've exhausted all
// options and don't want to generate any more Pages (otherwise we end up with duplicates).
if ( $index > ( $combinations->count() - 1 ) ) {
// If the combinations count is a negative number, we exceeded the floating point for an integer
// Tell the user to upgrade PHP and/or reduce the number of keyword terms.
if ( $combinations->count() < 0 ) {
$message = __( 'The total possible number of unique keyword term combinations exceeds the maximum number value that can be stored by your version of PHP. Please consider upgrading to a 64 bit PHP 7.0+ build and/or reducing the number of keyword terms that you are using.', 'page-generator-pro' );
} else {
$message = __( 'All possible keyword term combinations have been generated. Generating more Pages/Posts would result in duplicate content.', 'page-generator-pro' );
}
return new WP_Error( 'page_generator_pro_generate_content_keywords_exhausted', $message );
}
// Iterate through the combinations until we reach the one matching the index.
foreach ( $combinations as $c_index => $combination ) {
// Skip if not the index we want.
if ( $c_index !== $index ) {
continue;
}
// Define the keyword => term key/value pairs to use based on the current index.
$keywords_terms = $combination;
break;
}
break;
/**
* Sequential
* - Generates term combinations across keywords matched by index
*/
case 'sequential':
foreach ( $this->keywords['terms'] as $keyword => $terms ) {
// Use modulo to get the term index for this keyword.
$term_index = ( $index % count( $terms ) );
// Build the keyword => term key/value pairs.
$keywords_terms[ $keyword ] = $terms[ $term_index ];
}
break;
/**
* Random
* - Gets a random term for each keyword
*/
case 'random':
foreach ( $this->keywords['terms'] as $keyword => $terms ) {
// If only one term exists, use that.
if ( count( $terms ) === 1 ) {
$term_index = 0;
} else {
$term_index = wp_rand( 0, ( count( $terms ) - 1 ) );
}
// Build the keyword => term key/value pairs.
$keywords_terms[ $keyword ] = $terms[ $term_index ];
}
break;
/**
* Invalid method
*/
default:
return new WP_Error( 'page_generator_pro_generate_get_keywords_terms_invalid_method', __( 'The method given is invalid.', 'page-generator-pro' ) );
}
// Cleanup the terms.
foreach ( $keywords_terms as $key => $term ) {
$keywords_terms[ $key ] = trim( html_entity_decode( $term ) );
}
/**
* Returns an array of keyword and term key / value pairs, before any
* search or replacement arrays are built.
*
* @since 2.7.5
*
* @param array $keywords_terms Keywords and Terms for this Page Generation.
* @param string $method Generation Method.
* @param int $index Generation Index.
*/
$keywords_terms = apply_filters( 'page_generator_pro_generate_get_keywords_terms', $keywords_terms, $method, $index );
// Return.
return $keywords_terms;
}
/**
* Performs a search on the given string to find any {keywords}
*
* @since 1.2.0
*
* @param object|string $content Array Value (string to search).
* @param string $key Array Key.
*/
private function find_keywords_in_string( $content, $key ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter
// If $content is an object, iterate this call.
if ( is_object( $content ) ) {
return array_walk_recursive( $content, array( $this, 'find_keywords_in_string' ) );
}
// Bail if content is null.
if ( is_null( $content ) ) { // @phpstan-ignore-line
return;
}
/**
* Get Keywords in this string. Covers:
* - Alphanumeric and accented keyword names, with hyphens and underscores
* - Alphanumeric and accented keyword column names, with hyphens and underscores
* - Keyword modifiers
* - Keyword modifier arguments
*
* For example:
* {keyword}
* {keyword_ü}
* {keyword-keyword}
* {keyword_keyword}
* {keyword:modifier}
* {keyword(column)}, {keyword(column_name)}, {keyword(column-name)}
* {keyword(column_name):modifier}, {keyword(column-name):modifier...:modifier}
* {keyword(column_name):modifier[args]}
* {keyword(column_name):modifier[arg1,argN]}
*
* Previous method "|{(.+?)}|" would include spintax and fail to extract keywords
* within JSON e.g. Gutenberg Block JSON strings that contain a Keyword.
*/
preg_match_all( '/{([\p{L}0-9_\-:,()\\[\\]]+?)}/u', $content, $matches );
// Bail if no matches are found.
if ( ! is_array( $matches ) ) {
return;
}
if ( count( $matches[1] ) === 0 ) {
return;
}
// Iterate through matches, adding them to the required keywords array.
foreach ( $matches[1] as $m_key => $keyword ) {
$this->add_keyword_to_required_keywords( $keyword );
}
}
/**
* Adds the given keyword to the required keywords array, if it doesn't already exist
*
* @since 2.8.8
*
* @param string $keyword Possible Keyword.
*/
private function add_keyword_to_required_keywords( $keyword ) {
// If a keyword is within spintax at the start of the string (e.g. {{service}|{service2}} ),
// we get an additional leading curly brace for some reason. Remove it.
$keyword = str_replace( '{', '', $keyword );
$keyword = str_replace( '}', '', $keyword );
// Lowercase keyword, to avoid duplicates e.g. {City} and {city}.
$keyword = strtolower( $keyword );
// Fetch just the Keyword Name.
$keyword_name = $this->extract_keyword_name_from_keyword( $keyword );
// Fetch Keyword Names.
$keywords_names = $this->base->get_class( 'keywords' )->get_keywords_names();
// Bail if no Keyword Names are specified.
if ( ! $keywords_names ) {
return;
}
// If the Keyword Name is a spin that's just text (i.e it's not actually a Keyword), skip it.
if ( ! in_array( $keyword_name, $keywords_names, true ) ) {
return;
}
// If the Keyword Name is not in our required_keywords array, add it now.
if ( ! in_array( $keyword_name, $this->required_keywords, true ) ) {
$this->required_keywords[ $keyword_name ] = $keyword_name;
}
// If the Keyword (Full) is not in our required_keywords_full array, add it now.
if ( ! isset( $this->required_keywords_full[ $keyword_name ] ) ) {
$this->required_keywords_full[ $keyword_name ] = array();
}
if ( ! in_array( $keyword, $this->required_keywords_full[ $keyword_name ], true ) ) {
$this->required_keywords_full[ $keyword_name ][] = $keyword;
}
}
/**
* Returns just the keyword name, excluding any columns, nth terms and transformations
*
* @since 2.6.1
*
* @param string $keyword Keyword.
* @return string Keyword Name excluding any columns, nth terms and transformations
*/
private function extract_keyword_name_from_keyword( $keyword ) {
if ( strpos( $keyword, ':' ) !== false ) {
$keyword_parts = explode( ':', $keyword );
$keyword = trim( $keyword_parts[0] );
}
$keyword = preg_replace( '/\(.*?\)/', '', $keyword );
return $keyword;
}
/**
* Callback for array_walk_recursive, which finds $this->searches, replacing with
* $this->replacements in $item.
*
* Also performs spintax.
*
* @since 1.3.1
*
* @param array|object|string $item Item.
* @param string $key Key.
*/
private function replace_keywords_in_array( &$item, $key ) {
// If the settings key's value is an array, walk through it recursively to search/replace
// Otherwise do a standard search/replace on the string.
if ( is_array( $item ) ) {
// Array.
array_walk_recursive( $item, array( $this, 'replace_keywords_in_array' ) );
} elseif ( is_object( $item ) ) {
// Object.
array_walk_recursive( $item, array( $this, 'replace_keywords_in_array' ) );
} elseif ( is_string( $item ) ) {
// If here, we have a string.
// Perform keyword replacement, spintax and shortcode processing now.
// If replacements contain an array, we're using the :random_different Keyword Transformation
// and therefore need to perform a slower search/replace to iterate through every occurance of
// the same transformation.
if ( $this->replacements_contain_array ) {
foreach ( $this->searches as $index => $search ) {
// Standard search/replace.
if ( ! is_array( $this->replacements[ $index ] ) ) {
$item = str_ireplace( $search, $this->replacements[ $index ], $item );
continue;
}
// Pluck a value at random from the array of replacement Terms for the given search, doing this
// every time we find the Keyword, so we get truly random Terms each time in a single string.
$pos = stripos( $item, $search );
while ( $pos !== false ) {
$item = substr_replace( $item, $this->replacements[ $index ][ wp_rand( 0, ( count( $this->replacements[ $index ] ) - 1 ) ) ], $pos, strlen( $search ) );
// Search for next occurrence of this Keyword .
$pos = stripos( $item, $search, $pos + 1 );
}
}
} else {
// Replace all searches with all replacements.
$item = str_ireplace( $this->searches, $this->replacements, $item );
}
// Process Spintax.
$result = $this->base->get_class( 'spintax' )->process( $item );
if ( is_wp_error( $result ) ) {
if ( defined( 'PAGE_GENERATOR_PRO_DEBUG' ) && PAGE_GENERATOR_PRO_DEBUG ) {
// Store the error in the item.
$item = $result->get_error_message();
return;
}
}
// Spintax OK - assign to item.
$item = $result;
// Process Block Spinning.
$result = $this->base->get_class( 'block_spin' )->process( $item );
if ( is_wp_error( $result ) ) {
if ( defined( 'PAGE_GENERATOR_PRO_DEBUG' ) && PAGE_GENERATOR_PRO_DEBUG ) {
// Store the error in the item.
$item = $result->get_error_message();
return;
}
}
// Block Spinning OK - assign to item.
$item = $result;
// Process Conditional Output.
$result = $this->base->get_class( 'conditional_output' )->process( $item );
if ( is_wp_error( $result ) ) {
if ( defined( 'PAGE_GENERATOR_PRO_DEBUG' ) && PAGE_GENERATOR_PRO_DEBUG ) {
// Store the error in the item.
$item = $result->get_error_message();
return;
}
}
// Conditional Output OK - assign to item.
$item = $result;
/**
* Perform any other keyword replacements or string processing.
*
* @since 1.9.8
*
* @param string $item Group Setting String (this can be Post Meta, Custom Fields, Permalink, Title, Content etc).
* @param string $key Group Setting Key.
*/
$item = apply_filters( 'page_generator_pro_generate_replace_keywords_in_array', $item, $key );
}
}
/**
* Determines if a Post already exists that was generated by the given Group ID for the
* given Post Type, Parent and Slug
*
* @since 2.1.8
*
* @param int $group_id Group ID (0 = any Group).
* @param string $post_type Generated Post Type.
* @param int $post_parent Post Parent (0 = none).
* @param string $post_name Post Name.
* @return bool|int
*/
private function post_exists( $group_id, $post_type, $post_parent, $post_name ) {
// Fetch valid Post Statuses that can be used when generating content.
$statuses = array_keys( $this->base->get_class( 'common' )->get_post_statuses() );
// Build query arguments.
$args = array(
'post_type' => $post_type,
'post_status' => $statuses,
'post_parent' => $post_parent,
'post_name__in' => array( $post_name ),
// For performance, just return the Post ID and don't update meta or term caches.
'fields' => 'ids',
'cache_results' => false,
'update_post_meta_cache' => false,
'update_post_term_cache' => false,
);
// If the Group ID isn't zero, add the Group clause to the query.
if ( $group_id > 0 ) {
$args['meta_query'] = array(
array(
'key' => '_page_generator_pro_group',
'value' => absint( $group_id ),
),
);
}
// Try to find existing post.
$existing_post = new WP_Query( $args );
if ( count( $existing_post->posts ) === 0 ) {
return false;
}
// Return existing Post's ID.
return $existing_post->posts[0];
}
/**
* Determines if a Term already exists that was generated by the given Group ID for the
* given Taxonomy, Parent and Title.
*
* @since 2.4.8
*
* @param int $group_id Group ID (0 = any Group).
* @param string $taxonomy Generated Term's Taxonomy.
* @param int $term_parent Term Parent (0 = none).
* @param string $term_name Term Name.
* @return bool|int
*/
private function term_exists( $group_id, $taxonomy, $term_parent, $term_name ) {
// Build query arguments.
$args = array(
'taxonomy' => array( $taxonomy ),
'name' => array( $term_name ),
'hide_empty' => false,
// For performance, just return the Post ID and don't update meta or term caches.
'fields' => 'ids',
'update_term_meta_cache' => false,
);
// If a Parent Term ID exists, restrict the above query to only check for existing Terms generated
// that belong to the Parent.
if ( $term_parent > 0 ) {
$args['child_of'] = absint( $term_parent );
}
// If the Group ID isn't zero, add the Group clause to the query.
if ( $group_id > 0 ) {
$args['meta_query'] = array(
array(
'key' => '_page_generator_pro_group',
'value' => absint( $group_id ),
),
);
}
// Try to find existing term.
$existing_terms = new WP_Term_Query( $args );
// Bail if no existing terms exist.
if ( is_null( $existing_terms->terms ) ) { // @phpstan-ignore-line
return false;
}
// Return existing Term's ID.
return $existing_terms->terms[0];
}
/**
* Removes Content from the Group Settings if it is not selected for overwriting.
*
* Page Builders hook here to remove their metadata used for the Post Content.
*
* @since 3.3.8
*
* @param array $settings Group Settings.
* @return array Group Settings
*/
private function remove_content_from_settings( $settings ) {
// Determine if we are overwriting the content.
$overwrite = ( ! array_key_exists( 'post_content', $settings['overwrite_sections'] ) ? false : true );
// If overwriting is enabled, no need to exclude the content.
if ( $overwrite ) {
return $settings;
}
// Exclude content from the settings, as it is not needed because it is not going to overwrite the existing generated post's content.
$settings['content'] = '';
// Return settings if no Post Meta exists that we need to check.
if ( ! isset( $settings['post_meta'] ) ) {
return $settings;
}
// Define Post Meta Keys to remove from this Content Group's Settings.
$ignored_keys = array();
/**
* Defines Post Content related Meta Keys to remove from the Content Group's Settings, so that they are not processed
* and not copied to the existing Generated Page, as the Content Group's Overwrite Settings disable overwriting of content
*
* @since 3.3.8
*
* @param array $ignored_keys Ignored Keys (preg_match() compatible regex expressions are supported).
* @param array $settings Group Settings.
*/
$ignored_keys = apply_filters( 'page_generator_pro_generate_remove_content_from_settings_ignored_keys', $ignored_keys, $settings );
// If no Post Meta Keys need to be ignored, return.
if ( ! count( $ignored_keys ) ) {
return $settings;
}
// Iterate through Post Meta.
foreach ( $settings['post_meta'] as $meta_key => $meta_value ) {
// Remove ignored keys.
if ( in_array( $meta_key, $ignored_keys, true ) ) {
unset( $settings['post_meta'][ $meta_key ] );
continue;
}
// Iterate through the ignored keys using preg_match(), so we can support
// regular expressions.
foreach ( $ignored_keys as $ignored_key ) {
// Don't evaluate if not a regular expression.
if ( strpos( $ignored_key, '/' ) === false ) {
continue;
}
// Don't copy this Meta Key/Value if it's set to be ignored.
if ( preg_match( $ignored_key, $meta_key ) ) {
unset( $settings['post_meta'][ $meta_key ] );
continue 2;
}
}
}
// Return Group Settings, which will now have no Post Content / Page Builder Meta.
return $settings;
}
/**
* Callback for array_walk_recursive, which processes shortcodes.
*
* @since 1.9.7
*
* @param array|object|string $item Item.
* @param string $key Key.
*/
private function process_shortcodes_in_array( &$item, $key ) {
// If the settings key's value is an array, walk through it recursively to search/replace
// Otherwise do a standard search/replace on the string.
if ( is_array( $item ) ) {
// Array.
array_walk_recursive( $item, array( $this, 'process_shortcodes_in_array' ) );
} elseif ( is_object( $item ) ) {
// Object.
array_walk_recursive( $item, array( $this, 'process_shortcodes_in_array' ) );
} elseif ( is_string( $item ) ) {
// If here, we have a string.
// Perform shortcode processing.
// Some Page Builders don't use the main Post Content for output, and instead use their own post meta to build the output.
// Therefore, processing shortcodes on the Post Content would duplicate effort.
switch ( $key ) {
case 'content':
if ( $this->process_shortcodes_on_post_content ) {
$item = do_shortcode( $item );
}
break;
default:
$item = do_shortcode( $item );
break;
}
/**
* Filter to allow registering and processing shortcodes on a string.
*
* @since 1.9.8
*
* @param string $item Group Setting String (this can be Post Meta, Custom Fields, Permalink, Title, Content etc).
* @param string $key Group Setting Key.
*/
$item = apply_filters( 'page_generator_pro_generate_process_shortcodes_in_array', $item, $key );
}
}
/**
* Assigns any Attachments to the given Post ID that have the specified Group ID and Index
*
* @since 2.4.1
*
* @param int $post_id Generated Post ID.
* @param int $group_id Group ID.
* @param int $index Generation Index.
* @return WP_Error|bool
*/
private function assign_attachments_to_post_id( $post_id, $group_id, $index ) {
// Build query.
$args = array(
'post_type' => 'attachment',
'post_status' => 'any',
'posts_per_page' => -1,
'meta_query' => array(
array(
'key' => '_page_generator_pro_group',
'value' => absint( $group_id ),
),
array(
'key' => '_page_generator_pro_index',
'value' => absint( $index ),
),
),
'fields' => 'ids',
);
// Get all Attachments belonging to the given Group ID and Index.
$attachments = new WP_Query( $args );
// If no Attachments found, return false, as there's nothing to assign.
if ( count( $attachments->posts ) === 0 ) {
return false;
}
// For each Attachment, assign it to the Post.
foreach ( $attachments->posts as $attachment_id ) {
$result = wp_update_post(
array(
'ID' => $attachment_id,
'post_parent' => $post_id,
),
true
);
if ( is_wp_error( $result ) ) {
return $result;
}
}
// Done.
return true;
}
/**
* Returns a wp_insert_post() and wp_update_post() compatible date and time to publish/schedule the generated post,
* based on the Group's settings.
*
* @since 3.1.8
*
* @param array $settings Group Settings.
* @param bool|string $last_generated_post_date_time Last Generated Post's Date and Time.
*/
public function post_date( $settings, $last_generated_post_date_time = false ) {
// Bail if the Group doesn't have a date option.
if ( ! isset( $settings['date_option'] ) ) {
return false;
}
// Define the Post Date.
switch ( $settings['date_option'] ) {
/**
* Specific Date
*/
case 'specific':
// Define date.
$date = date_create( str_replace( 'T', ' ', $settings['date_specific'] ) );
// Just return the date and time if Status isn't Scheduled.
if ( $settings['status'] !== 'future' ) {
return date_format( $date, 'Y-m-d H:i:s' );
}
// If here, Status = Scheduled.
// Increment the specific date by the schedule hours and unit.
if ( $last_generated_post_date_time ) {
$date = date_create( $last_generated_post_date_time );
$date = date_add( $date, date_interval_create_from_date_string( '+' . $settings['schedule'] . ' ' . $settings['scheduleUnit'] ) );
}
break;
/**
* Random
*/
case 'random':
// Define date based on random settings.
$min = strtotime( $settings['date_min'] );
$max = strtotime( $settings['date_max'] );
$date = date_create( date_i18n( 'Y-m-d H:i:s', wp_rand( $min, $max ) ) );
break;
/**
* Now
*/
case 'now':
default:
// Define date as now.
$date = date_create( date_i18n( 'Y-m-d H:i:s' ) );
// Just return the date and time if Status isn't Scheduled.
if ( $settings['status'] !== 'future' ) {
return date_format( $date, 'Y-m-d H:i:s' );
}
// If here, Status = Scheduled.
// Increment the current date by the schedule hours and unit.
if ( $last_generated_post_date_time ) {
$date = date_create( $last_generated_post_date_time );
$date = date_add( $date, date_interval_create_from_date_string( '+' . $settings['schedule'] . ' ' . $settings['scheduleUnit'] ) );
}
break;
}
return date_format( $date, 'Y-m-d H:i:s' );
}
/**
* Defines the Featured Image for the given generated Post ID, if
* the Group Settings specify a Featured Image and (if overwriting)
* the Featured Image should be overwritten
*
* @since 2.3.5
*
* @param int $post_id Generated Post ID.
* @param int $group_id Group ID.
* @param int $index Generation Index.
* @param array $settings Group Settings.
* @param array $post_args wp_insert_post() / wp_update_post() arguments.
* @return WP_Error|bool|int
*/
private function featured_image( $post_id, $group_id, $index, $settings, $post_args ) {
// Bail if the target Post Type doesn't support a Featured Image.
if ( ! $this->base->get_class( 'common' )->post_type_supports( $settings['type'], 'thumbnail' ) ) {
return false;
}
// Bail if no Featured Image source defined.
if ( empty( $settings['featured_image_source'] ) ) {
return false;
}
// Bail if we're overwriting an existing Post and don't want to overwrite the Featured Image.
if ( isset( $post_args['ID'] ) && ! array_key_exists( 'featured_image', $settings['overwrite_sections'] ) ) {
return false;
}
switch ( $settings['featured_image_source'] ) {
/**
* Media Library ID
*/
case 'id':
// Build Featured Image Search Arguments.
$search_args = array(
'title' => ( ! empty( $settings['featured_image_media_library_title'] ) ? $settings['featured_image_media_library_title'] : false ),
'caption' => ( ! empty( $settings['featured_image_media_library_caption'] ) ? $settings['featured_image_media_library_caption'] : false ),
'alt' => ( ! empty( $settings['featured_image_media_library_alt'] ) ? $settings['featured_image_media_library_alt'] : false ),
'description' => ( ! empty( $settings['featured_image_media_library_description'] ) ? $settings['featured_image_media_library_description'] : false ),
'operator' => ( ! empty( $settings['featured_image_media_library_operator'] ) ? $settings['featured_image_media_library_operator'] : 'AND' ),
'ids' => ( ! empty( $settings['featured_image_media_library_ids'] ) ? $settings['featured_image_media_library_ids'] : false ),
'min_id' => ( ! empty( $settings['featured_image_media_library_min_id'] ) ? $settings['featured_image_media_library_min_id'] : false ),
'max_id' => ( ! empty( $settings['featured_image_media_library_max_id'] ) ? $settings['featured_image_media_library_max_id'] : false ),
);
// Get Image ID.
$image_id = $this->base->get_class( 'media_library' )->get_random_image_id( $search_args );
// Bail if no Image ID was defined.
if ( ! isset( $image_id ) ) {
return false;
}
if ( ! $image_id ) {
return false;
}
// Return the error if a WP_Error.
if ( is_wp_error( $image_id ) ) {
return $image_id;
}
// If we're copying the image to a new Media Library attachment, do this now.
if ( $settings['featured_image_copy'] ) {
// Get image.
$image = wp_get_attachment_image_src( $image_id, 'full' );
if ( ! $image ) {
return new WP_Error( 'page_generator_pro_generate_featured_image', __( 'Featured Image: Could not get Image ID\'s source', 'page-generator-pro' ) );
}
// Copy to new image.
$image_id = $this->base->get_class( 'import' )->import_remote_image(
$image[0],
$post_id,
$group_id,
$index,
$settings['featured_image_filename'],
$settings['featured_image_title'],
$settings['featured_image_caption'],
$settings['featured_image_alt'],
$settings['featured_image_description']
);
}
break;
/**
* Image URL
*/
case 'url':
// Bail if no Featured Image URL specified.
if ( empty( $settings['featured_image'] ) ) {
return new WP_Error( 'page_generator_pro_generate', __( 'Featured Image: No URL was specified.', 'page-generator-pro' ) );
}
// Import Image into the Media Library.
$image_id = $this->base->get_class( 'import' )->import_remote_image(
$settings['featured_image'],
$post_id,
$group_id,
$index,
$settings['featured_image_filename'],
$settings['featured_image_title'],
$settings['featured_image_caption'],
$settings['featured_image_alt'],
$settings['featured_image_description']
);
break;
/**
* Creative Commons
*/
case 'creative_commons':
// Bail if no Featured Image Term specified.
if ( empty( $settings['featured_image'] ) ) {
return new WP_Error( 'page_generator_pro_generate', __( 'Featured Image: Creative Commons: No Term was specified.', 'page-generator-pro' ) );
}
// Adjust image orientation setting to be API compatible.
switch ( $settings['featured_image_orientation'] ) {
case 'portrait':
$settings['featured_image_orientation'] = 'tall';
break;
case 'landscape':
$settings['featured_image_orientation'] = 'wide';
break;
}
// Define the number of images to return to then choose one at random from.
$per_page = 20;
// Run query to fetch total number of pages of results that are available.
$page_count = $this->base->get_class( 'creative_commons' )->page_count(
$settings['featured_image'],
$settings['featured_image_orientation'],
false,
false,
$per_page
);
// Bail if an error occured.
if ( is_wp_error( $page_count ) ) {
return $page_count;
}
// Pick a page index at random from the resultset.
if ( $page_count === 1 ) {
$page_index = 1;
} else {
$page_index = wp_rand( 1, $page_count );
}
// Run images query.
$images = $this->base->get_class( 'creative_commons' )->photos_search(
$settings['featured_image'],
$settings['featured_image_orientation'],
false,
false,
$per_page,
$page_index
);
// Bail if an error occured.
if ( is_wp_error( $images ) ) {
return $images;
}
// Pick an image at random from the resultset.
if ( count( $images ) === 1 ) {
$image_index = 0;
} else {
$image_index = wp_rand( 0, ( count( $images ) - 1 ) );
}
// Import Image into the Media Library.
$image_id = $this->base->get_class( 'import' )->import_remote_image(
$images[ $image_index ]['url'],
$post_id,
$group_id,
$index,
$settings['featured_image_filename'],
$settings['featured_image_title'],
$settings['featured_image_caption'],
$settings['featured_image_alt'],
$settings['featured_image_description']
);
break;
/**
* OpenAI
*/
case 'openai':
// Bail if no Featured Image Term specified.
if ( empty( $settings['featured_image'] ) ) {
return new WP_Error( 'page_generator_pro_generate_featured_image', __( 'Featured Image: OpenAI: No Term was specified.', 'page-generator-pro' ) );
}
// Bail if OpenAI is not configured.
if ( empty( $this->base->get_class( 'settings' )->get_setting( $this->base->plugin->name . '-research', 'openai_api_key' ) ) ) {
return new WP_Error( 'page_generator_pro_generate_featured_image', __( 'Featured Image: OpenAI: No API Key is defined at Settings > Integrations.', 'page-generator-pro' ) );
}
// Send request to OpenAI.
$result = $this->base->get_class( 'openai' )->create_image(
$settings['featured_image'],
$settings['featured_image_openai_style'],
$settings['featured_image_openai_rewrite']
);
// Bail if an error occured.
if ( is_wp_error( $result ) ) {
return $result;
}
// Import Image into the Media Library.
$image_id = $this->base->get_class( 'import' )->import_remote_image(
$result->data[0]->url,
$post_id,
$group_id,
$index,
$settings['featured_image_filename'],
$settings['featured_image_title'],
$settings['featured_image_caption'],
$settings['featured_image_alt'],
$settings['featured_image_description']
);
break;
/**
* Pexels
*/
case 'pexels':
// Bail if no Featured Image Term specified.
if ( empty( $settings['featured_image'] ) ) {
return new WP_Error( 'page_generator_pro_generate_featured_image', __( 'Featured Image: Pexels: No Term was specified.', 'page-generator-pro' ) );
}
// If a Pexels API Key has been specified, use it instead of the class default.
$api_key = $this->base->get_class( 'settings' )->get_setting( $this->base->plugin->name . '-integrations', 'pexels_api_key' );
if ( ! empty( $api_key ) ) {
$this->base->get_class( 'pexels' )->set_api_key( $api_key );
}
// Define the number of images to return to then choose one at random from.
$per_page = 80;
// Run query to fetch total number of pages of results that are available.
$page_count = $this->base->get_class( 'pexels' )->page_count( $settings['featured_image'], 'original', $settings['featured_image_orientation'], $per_page );
// Bail if an error occured.
if ( is_wp_error( $page_count ) ) {
return $page_count;
}
// Pick a page index at random from the resultset.
if ( $page_count === 1 ) {
$page_index = 1;
} else {
$page_index = wp_rand( 1, $page_count );
}
// Run images query.
$images = $this->base->get_class( 'pexels' )->photos_search( $settings['featured_image'], 'original', $settings['featured_image_orientation'], $per_page, $page_index );
// Bail if an error occured.
if ( is_wp_error( $images ) ) {
return $images;
}
// Pick an image at random from the resultset.
if ( count( $images ) === 1 ) {
$image_index = 0;
} else {
$image_index = wp_rand( 0, ( count( $images ) - 1 ) );
}
// Import Image into the Media Library.
$image_id = $this->base->get_class( 'import' )->import_remote_image(
$images[ $image_index ]['url'],
$post_id,
$group_id,
$index,
$settings['featured_image_filename'],
$settings['featured_image_title'],
$settings['featured_image_caption'],
$settings['featured_image_alt'],
$settings['featured_image_description']
);
break;
/**
* Pixabay
*/
case 'pixabay':
// Bail if no Featured Image Term specified.
if ( empty( $settings['featured_image'] ) ) {
return new WP_Error( 'page_generator_pro_generate', __( 'Featured Image: Pixabay: No Term was specified.', 'page-generator-pro' ) );
}
// If a Pixabay API Key has been specified, use it instead of the class default.
$api_key = $this->base->get_class( 'settings' )->get_setting( $this->base->plugin->name . '-integrations', 'pixabay_api_key' );
if ( ! empty( $api_key ) ) {
$this->base->get_class( 'pixabay' )->set_api_key( $api_key );
}
// Define the number of images to return to then choose one at random from.
$per_page = 150;
// Run query to fetch total number of pages of results that are available.
$page_count = $this->base->get_class( 'pixabay' )->page_count(
$settings['featured_image'],
$settings['featured_image_pixabay_language'],
$settings['featured_image_pixabay_image_type'],
$settings['featured_image_orientation'],
$settings['featured_image_pixabay_image_category'],
0,
0,
$settings['featured_image_pixabay_image_color'],
false,
$per_page
);
// Bail if an error occured.
if ( is_wp_error( $page_count ) ) {
return $page_count;
}
// Pick a page index at random from the resultset.
if ( $page_count === 1 ) {
$page_index = 1;
} else {
$page_index = wp_rand( 1, $page_count );
}
// Run images query.
$images = $this->base->get_class( 'pixabay' )->photos_search(
$settings['featured_image'],
$settings['featured_image_pixabay_language'],
$settings['featured_image_pixabay_image_type'],
$settings['featured_image_orientation'],
$settings['featured_image_pixabay_image_category'],
0,
0,
$settings['featured_image_pixabay_image_color'],
false,
$per_page,
$page_index
);
// Bail if an error occured.
if ( is_wp_error( $images ) ) {
return $images;
}
// Pick an image at random from the resultset.
if ( count( $images ) === 1 ) {
$image_index = 0;
} else {
$image_index = wp_rand( 0, ( count( $images ) - 1 ) );
}
// Import Image into the Media Library.
$image_id = $this->base->get_class( 'import' )->import_remote_image(
$images[ $image_index ]['url'],
$post_id,
$group_id,
$index,
$settings['featured_image_filename'],
$settings['featured_image_title'],
$settings['featured_image_caption'],
$settings['featured_image_alt'],
$settings['featured_image_description']
);
break;
/**
* Wikipedia
*/
case 'wikipedia':
// Bail if no Featured Image Term specified.
if ( empty( $settings['featured_image'] ) ) {
return new WP_Error( 'page_generator_pro_generate', __( 'Featured Image: Wikipedia: No Term was specified.', 'page-generator-pro' ) );
}
// Run images query.
$images = $this->base->get_class( 'wikipedia' )->get_images( $settings['featured_image'] );
// Bail if an error occured.
if ( is_wp_error( $images ) ) {
return $images;
}
// Define image index.
if ( $settings['featured_image_wikipedia_use_first_image'] || count( $images ) === 1 ) {
// Either 'Use First Image' is enabled or only one image exists in the resultset.
$image_index = 0;
} else {
// Pick an image at random.
$image_index = wp_rand( 0, ( count( $images ) - 1 ) );
}
// Import Image into the Media Library.
$image_id = $this->base->get_class( 'import' )->import_remote_image(
$images[ $image_index ]['url'],
$post_id,
$group_id,
$index,
$settings['featured_image_filename'],
$settings['featured_image_title'],
$settings['featured_image_caption'],
$settings['featured_image_alt'],
$settings['featured_image_description']
);
break;
/**
* Third Party Integrations
*/
default:
$image_id = false;
/**
* Defines the Featured Image for the given generated Post ID, if
* the Group Settings specify a Featured Image and (if overwriting)
* the Featured Image should be overwritten
*
* @since 2.9.3
*
* @param bool|int $image_id Image ID.
* @param int $post_id Generated Post ID.
* @param int $group_id Group ID.
* @param int $index Generation Index.
* @param array $settings Group Settings.
* @param array $post_args wp_insert_post() / wp_update_post() arguments.
* @return WP_Error|bool|int
*/
$image_id = apply_filters( 'page_generator_pro_generate_featured_image_' . $settings['featured_image_source'], false, $post_id, $group_id, $index, $settings, $post_args );
break;
}
// Return the error if a WP_Error.
if ( is_wp_error( $image_id ) ) {
return $image_id;
}
// Bail if no Image ID was defined.
if ( ! isset( $image_id ) ) {
return false;
}
if ( ! $image_id ) {
return false;
}
// Update Featured Image.
update_post_meta( $post_id, '_thumbnail_id', $image_id );
// EXIF Data for Featured Image.
$exif = $this->base->get_class( 'exif' )->write(
$image_id,
$settings['featured_image_exif_description'],
$settings['featured_image_exif_comments'],
$settings['featured_image_exif_latitude'],
$settings['featured_image_exif_longitude']
);
if ( is_wp_error( $exif ) ) {
return $exif;
}
// Return Featured Image ID.
return $image_id;
}
/**
* Gets the Generated Post Parent for the given Group ID and its settings.
*
* If the Group has a Parent Group specified, will only return the Post Parent
* if it was generated by the Parent Group
*
* @since 3.0.9
*
* @param int $group_id Group ID.
* @param array $settings Group Settings.
* @return WP_Error|int
*/
private function get_post_parent( $group_id, $settings ) {
// Assume there isn't a Post Parent.
$post_parent = 0;
// If Post Parent isn't set, bail.
if ( ! isset( $settings['pageParent'] ) ) {
return 0;
}
if ( ! isset( $settings['pageParent'][ $settings['type'] ] ) ) {
return 0;
}
if ( empty( $settings['pageParent'][ $settings['type'] ] ) ) {
return 0;
}
// Get Post Parent.
$post_parent = $settings['pageParent'][ $settings['type'] ];
// If the Post Parent isn't a number, convert to a Post ID.
if ( ! is_numeric( $post_parent ) ) {
// Convert to a slug, retaining forwardslashes.
// This also converts special accented characters to non-accented versions.
$post_parent = $this->base->get_class( 'common' )->sanitize_slug( $post_parent );
// Find the Post ID based on the given name.
$parent_page = get_page_by_path( $post_parent, OBJECT, $settings['type'] );
// Throw an error, as we require a Post Parent but couldn't find one.
if ( ! $parent_page ) {
return new WP_Error(
'page_generator_pro_generate_get_post_parent',
sprintf(
/* translators: slug */
__( 'Could not find a parent page with the slug <code>%1$s</code> (<code>%2$s</code>). Make sure this %3$s is generated or manually created first', 'page-generator-pro' ),
$post_parent,
get_bloginfo( 'url' ) . '/' . $post_parent,
$settings['type']
)
);
}
// Fetch the ID.
$post_parent = $parent_page->ID;
}
// Check the post parent exists.
if ( ! get_post( $post_parent ) ) {
return new WP_Error(
'page_generator_pro_generate_get_post_parent',
sprintf(
/* translators: Post ID */
__( 'Could not find a parent page with ID %s', 'page-generator-pro' ),
$post_parent
)
);
}
// If no Parent Group specified, just return the Post Parent now.
$group_parent_id = wp_get_post_parent_id( $group_id );
if ( ! $group_parent_id ) {
return $post_parent;
}
// As the Group Parent is specified, don't return the Post Parent if it was not generated by the Parent Group.
if ( ! $this->base->get_class( 'groups' )->is_generated_by_group( $post_parent, $group_parent_id ) ) {
return new WP_Error(
'page_generator_pro_generate_get_post_parent',
sprintf(
/* translators: %1$s: Translated word of 'ID' or 'Slug', %2$s: Post Parent ID or Slug, %3$s: Group ID */
__( 'Parent page with ID %1$s exists, but the parent page was not generated by the specified Parent Group ID %2$s. Either remove the Parent Group setting, or generate the Parent Page through the Parent Group first.', 'page-generator-pro' ),
$post_parent,
$group_id
)
);
}
// Parent Page was generated by the Parent Group, so return the Parent Page ID.
return $post_parent;
}
/**
* Copies Attributes > Template to the Generated Post ID, honoring
* the Overwrite setting.
*
* @since 2.9.0
*
* @param int $post_id Generated Post ID.
* @param int $group_id Group ID.
* @param array $settings Group Settings.
* @param array $post_args wp_insert_post() / wp_update_post() arguments.
* @return bool Updated Page Template on Generated Post ID
*/
private function set_page_template( $post_id, $group_id, $settings, $post_args ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter
// Bail if the target Post Type doesn't support templates.
if ( ! $this->base->get_class( 'common' )->post_type_supports( $settings['type'], 'templates' ) ) {
return false;
}
// Bail if we're overwriting an existing Post and don't want to overwrite the Template.
if ( isset( $post_args['ID'] ) && ! array_key_exists( 'attributes', $settings['overwrite_sections'] ) ) {
return false;
}
// Backward compat for Free.
if ( ! empty( $settings['pageTemplate'] ) && ! is_array( $settings['pageTemplate'] ) ) {
update_post_meta( $post_id, '_wp_page_template', $settings['pageTemplate'] );
}
if ( ! empty( $settings['pageTemplate'][ $settings['type'] ] ) ) {
update_post_meta( $post_id, '_wp_page_template', $settings['pageTemplate'][ $settings['type'] ] );
}
/**
* Action to perform any further steps with the Content Group's Page Template
* after the Page Template has been copied from the Content Group to the Generated Content.
*
* @since 2.9.7
*
* @param int $post_id Generated Page ID.
* @param array $settings Group Settings.
* @param array $post_args wp_insert_post() / wp_update_post() arguments.
*/
do_action( 'page_generator_pro_generate_set_page_template', $post_id, $settings, $post_args );
return true;
}
/**
* Copies Custom Fields to the Generated Post ID, if
* the Group Settings specify Custom Field data and (if overwriting)
* whether the Custom Fields data should be overwritten.
*
* @since 2.3.5
*
* @param int $post_id Generated Post ID.
* @param int $group_id Group ID.
* @param array $settings Group Settings.
* @param array $post_args wp_insert_post() / wp_update_post() arguments.
* @param array $keywords_terms Keywords / Terms Key / Value array.
* @return bool Updated Custom Fields on Generated Post ID
*/
private function set_custom_fields( $post_id, $group_id, $settings, $post_args, $keywords_terms ) {
// Bail if we're overwriting an existing Post and don't want to overwrite the Custom Fields.
if ( isset( $post_args['ID'] ) && ! array_key_exists( 'custom_fields', $settings['overwrite_sections'] ) ) {
return false;
}
// Custom Fields.
// delete_post_meta() and add_post_meta() are used instead of update_post_meta() so that
// multiple Custom Fields with the same Meta Key are all copied over, instead of the last one.
if ( isset( $settings['meta'] ) && ! empty( $settings['meta'] ) ) {
// Delete existing Custom Fields.
foreach ( $settings['meta']['key'] as $meta_index => $meta_key ) {
delete_post_meta( $post_id, $meta_key );
}
// Add Custom Fields.
foreach ( $settings['meta']['key'] as $meta_index => $meta_key ) {
add_post_meta( $post_id, $meta_key, $settings['meta']['value'][ $meta_index ] );
}
}
// Store Keywords.
if ( $settings['store_keywords'] ) {
$store_keywords = $this->get_keywords_terms_array_with_columns( $keywords_terms );
if ( $store_keywords ) {
foreach ( $store_keywords as $meta_key => $meta_value ) {
update_post_meta( $post_id, $meta_key, $meta_value );
}
}
}
/**
* Action to perform any further steps with the Content Group's Custom Fields,
* after all Custom Fields has been copied from the Content Group to the Generated Content.
*
* @since 2.9.7
*
* @param int $post_id Generated Page ID.
* @param int $group_id Group ID.
* @param array $meta Group Custom Fields.
* @param array $settings Group Settings.
* @param array $post_args wp_insert_post() / wp_update_post() arguments.
* @param array $keywords_terms Keywords / Terms Key / Value array.
*/
do_action( 'page_generator_pro_generate_set_custom_fields', $post_id, $group_id, $settings['meta'], $settings, $post_args, $keywords_terms );
return true;
}
/**
* Stores Header and Footer code in the Generated Post ID as metadata, if
* the Group Settings specify Header and/or Footer data and (if overwriting)
* whether the Header and Footer data should be overwritten.
*
* @since 4.3.2
*
* @param int $post_id Generated Post ID.
* @param int $group_id Group ID.
* @param array $settings Group Settings.
* @param array $post_args wp_insert_post() / wp_update_post() arguments.
* @param array $keywords_terms Keywords / Terms Key / Value array.
* @return bool Updated Custom Fields on Generated Post ID
*/
private function set_header_footer_code( $post_id, $group_id, $settings, $post_args, $keywords_terms ) {
// Bail if we're overwriting an existing Post and don't want to overwrite the Header and Footer Code.
if ( isset( $post_args['ID'] ) && ! array_key_exists( 'header_footer_code', $settings['overwrite_sections'] ) ) {
return false;
}
// Delete existing Header and Footer Code.
delete_post_meta( $post_id, '_page_generator_pro_header_code' );
delete_post_meta( $post_id, '_page_generator_pro_footer_code' );
// Add Header Code if defined.
if ( array_key_exists( 'header_code', $settings ) && ! empty( $settings['header_code'] ) ) {
add_post_meta( $post_id, '_page_generator_pro_header_code', $settings['header_code'] );
}
// Add Footer Code if defined.
if ( array_key_exists( 'footer_code', $settings ) && ! empty( $settings['footer_code'] ) ) {
add_post_meta( $post_id, '_page_generator_pro_footer_code', $settings['footer_code'] );
}
/**
* Action to perform any further steps with the Content Group's Header and Footer Code,
* after Header and Footer code has been copied from the Content Group to the Generated Content.
*
* @since 4.3.2
*
* @param int $post_id Generated Page ID.
* @param int $group_id Group ID.
* @param array $settings Group Settings.
* @param array $post_args wp_insert_post() / wp_update_post() arguments.
* @param array $keywords_terms Keywords / Terms Key / Value array.
*/
do_action( 'page_generator_pro_generate_set_header_footer_code', $post_id, $group_id, $settings, $post_args, $keywords_terms );
return true;
}
/**
* Copies the Content Group's Post Meta to the Generated Post ID,
* including Page Builder / ACF data.
*
* @since 2.3.5
*
* @param int $post_id Generated Post ID.
* @param int $group_id Group ID.
* @param array $settings Group Settings.
* @param array $post_args wp_insert_post() / wp_update_post() arguments.
* @return bool Updated Post Meta on Generated Post ID
*/
private function set_post_meta( $post_id, $group_id, $settings, $post_args ) {
// Bail if no Post Meta to copy to the generated Post.
if ( ! isset( $settings['post_meta'] ) ) {
return false;
}
// Define the metadata to ignore.
$ignored_keys = array(
'_wp_page_template',
);
/**
* Defines Post Meta Keys in a Content Group to ignore and not copy to generated Posts / Groups.
*
* @since 2.6.1
*
* @param array $ignored_keys Ignored Keys (preg_match() compatible regex expressions are supported).
* @param int $post_id Generated Post ID.
* @param array $settings Group Settings.
* @param array $post_args wp_insert_post() / wp_update_post() arguments.
*/
$ignored_keys = apply_filters( 'page_generator_pro_generate_set_post_meta_ignored_keys', $ignored_keys, $post_id, $settings, $post_args );
// Iterate through Post Meta.
foreach ( $settings['post_meta'] as $meta_key => $meta_value ) {
// Skip ignored keys.
if ( in_array( $meta_key, $ignored_keys, true ) ) {
continue;
}
// Iterate through the ignored keys using preg_match(), so we can support
// regular expressions.
foreach ( $ignored_keys as $ignored_key ) {
// Don't evaluate if not a regular expression.
if ( strpos( $ignored_key, '/' ) === false ) {
continue;
}
// Don't copy this Meta Key/Value if it's set to be ignored.
if ( preg_match( $ignored_key, $meta_key ) ) {
continue 2;
}
}
/**
* Filters the Group Metadata for the given Key and Value, immediately before it's
* saved to the Generated Page.
*
* @since 2.6.1
*
* @param array|string|int|bool $meta_value Meta Value.
* @param int $post_id Generated Post ID.
* @param int $group_id Group ID.
* @param array $settings Group Settings.
* @param array $post_args wp_insert_post() / wp_update_post() arguments.
*/
$meta_value = apply_filters( 'page_generator_pro_generate_set_post_meta_' . $meta_key, $meta_value, $post_id, $group_id, $settings, $post_args );
// Update Generated Page's Meta Value.
update_post_meta( $post_id, $meta_key, $meta_value );
}
/**
* Action to perform any further steps with the Content Group's Post Meta,
* after all Post Meta has been copied from the Content Group to the Generated Content.
*
* @since 2.9.7
*
* @param int $post_id Generated Page ID.
* @param int $group_id Group ID.
* @param array $post_meta Group Post Meta.
* @param array $settings Group Settings.
* @param array $post_args wp_insert_post() / wp_update_post() arguments.
*/
do_action( 'page_generator_pro_generate_set_post_meta', $post_id, $group_id, $settings['post_meta'], $settings, $post_args );
return true;
}
/**
* Copies the Term Group's Term Meta to the Generated Term ID,
* including Yoast / ACF data.
*
* @since 2.6.3
*
* @param int $term_id Generated Term ID.
* @param int $group_id Group ID.
* @param array $settings Group Settings.
* @param array $term_args wp_insert_term() / wp_update_term() arguments.
* @return bool Updated Term Meta on Generated Term ID
*/
private function set_term_meta( $term_id, $group_id, $settings, $term_args ) {
// Bail if no Term Meta to copy to the generated Term.
if ( ! isset( $settings['term_meta'] ) ) {
return false;
}
// Define the metadata to ignore.
$ignored_keys = array();
/**
* Defines Term Meta Keys in a Term Group to ignore and not copy to generated Terms / Groups.
*
* @since 2.6.3
*
* @param array $ignored_keys Ignored Keys (preg_match() compatible regex expressions are supported).
* @param int $term_id Generated Term ID.
* @param array $settings Group Settings.
* @param array $term_args wp_insert_term() / wp_update_term() arguments.
*/
$ignored_keys = apply_filters( 'page_generator_pro_generate_set_term_meta_ignored_keys', $ignored_keys, $term_id, $settings, $term_args );
// Iterate through Term Meta.
foreach ( $settings['term_meta'] as $meta_key => $meta_value ) {
// Skip ignored keys.
if ( in_array( $meta_key, $ignored_keys, true ) ) {
continue;
}
// Iterate through the ignored keys using preg_match(), so we can support
// regular expressions.
foreach ( $ignored_keys as $ignored_key ) {
// Don't evaluate if not a regular expression.
if ( strpos( $ignored_key, '/' ) === false ) {
continue;
}
// Don't copy this Meta Key/Value if it's set to be ignored.
if ( preg_match( $ignored_key, $meta_key ) ) {
continue 2;
}
}
/**
* Filters the Group Metadata for the given Key and Value, immediately before it's
* saved to the Generated Term.
*
* @since 2.6.3
*
* @param array|string|int|object $value Meta Value.
*/
$meta_value = apply_filters( 'page_generator_pro_generate_set_term_meta_' . $meta_key, $meta_value );
// Update Generated Term's Meta Value.
update_term_meta( $term_id, $meta_key, $meta_value );
}
/**
* Action to perform any further steps with the Term Group's Post Meta,
* after all Post Meta has been copied from the Term Group to the Generated Term.
*
* @since 2.9.7
*
* @param int $term_id Generated Term ID.
* @param int $group_id Group ID.
* @param array $term_meta Group Term Meta.
* @param array $settings Group Settings.
* @param array $term_args wp_insert_term() / wp_update_term() arguments.
*/
do_action( 'page_generator_pro_generate_set_term_meta', $term_id, $group_id, $settings['term_meta'], $settings, $term_args );
return true;
}
/**
* Assigns the Generated Post ID to a Menu, if defined in the Group Settings.
*
* If the Generated Post ID is already an item in the Menu, replaces it.
*
* @since 2.7.1
*
* @param int $post_id Generated Post ID.
* @param array $settings Group Settings.
* @param array $post_args wp_insert_post() / wp_update_post() arguments.
* @return bool|WP_Error|int
*/
private function set_menu( $post_id, $settings, $post_args ) {
// Bail if no Menu is specified.
if ( ! isset( $settings['menu'] ) || ! $settings['menu'] ) {
return false;
}
// Bail if we're overwriting an existing Post and don't want to overwrite the Custom Fields.
if ( isset( $post_args['ID'] ) && ! array_key_exists( 'menu', $settings['overwrite_sections'] ) ) {
return false;
}
// Build menu arguments.
$args = array(
'menu-item-object-id' => $post_id,
'menu-item-object' => $settings['type'],
'menu-item-type' => 'post_type',
'menu-item-status' => 'publish',
);
// If a title exists, use it instead.
if ( isset( $settings['menu_title'] ) && ! empty( $settings['menu_title'] ) ) {
$args['menu-item-title'] = $settings['menu_title'];
}
// If a parent is specified, try to find the parent menu item.
if ( isset( $settings['menu_parent'] ) && ! empty( $settings['menu_parent'] ) ) {
// If the parent is numeric, use it as the parent ID.
if ( is_numeric( $settings['menu_parent'] ) ) {
$args['menu-item-parent-id'] = $settings['menu_parent'];
} else {
$parent_menu_item_id = $this->get_menu_item_by_title( $settings['menu_parent'], $settings['menu'] );
if ( $parent_menu_item_id ) {
$args['menu-item-parent-id'] = $parent_menu_item_id;
}
}
}
// Update (or create) Menu Item.
return wp_update_nav_menu_item(
$settings['menu'],
$this->get_menu_item_by_post_id( $post_id, $settings['menu'] ),
$args
);
}
/**
* Gets the given Post ID's Menu Item ID, if it exists in the given Menu
*
* @since 2.7.1
*
* @param int $post_id Generated Post ID.
* @param int $menu_id Menu ID.
* @param int $menu_item_parent Restrict search by Menu Item Parent.
* @return bool|int Existing Menu Item ID | false
*/
private function get_menu_item_by_post_id( $post_id, $menu_id, $menu_item_parent = 0 ) {
$menu_items = wp_get_nav_menu_items( $menu_id );
if ( empty( $menu_items ) ) {
return false;
}
foreach ( $menu_items as $menu_item ) {
// Skip this menu item if it doesn't match the Post ID.
if ( (int) $menu_item->object_id !== $post_id ) {
continue;
}
// Skip this menu item if it doesn't match the Menu Parent ID.
if ( $menu_item_parent && (int) $menu_item->menu_item_parent !== $menu_item_parent ) {
continue;
}
// Matching Menu Item found.
return $menu_item->db_id;
}
// If here, this Post doesn't exist in the Menu.
return false;
}
/**
* Returns a Menu Item ID by its Title, if it exists in the given Menu
*
* @since 2.7.5
*
* @param string $title Menu Title.
* @param int $menu_id Menu ID.
* @param int $menu_item_parent Restrict search by Menu Item Parent.
* @return bool|int Existing Menu Item ID | false
*/
private function get_menu_item_by_title( $title, $menu_id, $menu_item_parent = 0 ) {
$menu_items = wp_get_nav_menu_items( $menu_id );
if ( empty( $menu_items ) ) {
return false;
}
foreach ( $menu_items as $menu_item ) {
// Skip this menu item if it doesn't match the Title.
if ( $menu_item->title !== $title ) {
continue;
}
// Skip this menu item if it doesn't match the Menu Parent ID.
if ( $menu_item_parent && $menu_item->menu_item_parent !== $menu_item_parent ) {
continue;
}
// Matching Menu Item found.
return $menu_item->db_id;
}
// If here, this Post doesn't exist in the Menu.
return false;
}
/**
* Stores the Latitude and Longitude in the Geo table against the generated Post ID and Group ID
*
* If the settings are missing a latitude and longitude, or they're not numbers, something
* went wrong.
*
* @since 2.3.6
*
* @param int $post_id Generated Post ID.
* @param int $group_id Group ID.
* @param array $settings Group Settings.
* @return WP_Error|bool
*/
private function latitude_longitude( $post_id, $group_id, $settings ) {
// Bail if we don't have a latitude or longitude.
if ( empty( $settings['latitude'] ) && empty( $settings['longitude'] ) ) {
return new WP_Error(
'page_generator_pro_generate_latitude_longitude_error',
__( 'The specified latitude and longitude values are blank. Check they exist in the Keyword.', 'page-generator-pro' )
);
}
if ( ! $this->base->get_class( 'geo' )->is_latitude( $settings['latitude'] ) ) {
return new WP_Error(
'page_generator_pro_generate_latitude_longitude_error',
sprintf(
/* translators: Latitude Value */
__( 'The specified latitude value %s is not a valid latitude. Correct this in the Keyword or Group.', 'page-generator-pro' ),
$settings['latitude']
)
);
}
if ( ! $this->base->get_class( 'geo' )->is_longitude( $settings['longitude'] ) ) {
return new WP_Error(
'page_generator_pro_generate_latitude_longitude_error',
sprintf(
/* translators: Longitude Value */
__( 'The specified longitude value %s is not a valid longitude. Correct this in the Keyword or Group.', 'page-generator-pro' ),
$settings['longitude']
)
);
}
// Insert / Update Latitude and Longitude against this Post ID and Group ID.
return $this->base->get_class( 'geo' )->update(
$post_id,
$group_id,
$settings['latitude'],
$settings['longitude']
);
}
/**
* Assigns Taxonomy Terms to the Generated Post ID
*
* @since 1.9.5
*
* @param int $post_id Generated Post ID.
* @param array $settings Group Settings.
* @param array $post_args wp_insert_post() / wp_update_post() arguments.
* @return bool|WP_Error false: No Terms assigned due to Group Settings.
* true: Terms assigned.
* WP_Error: Attempted to assign Terms but an error occured.
*/
private function assign_taxonomy_terms_to_post( $post_id, $settings, $post_args ) {
// Bail if the target Post Type doesn't support Taxonomies.
if ( ! $this->base->get_class( 'common' )->post_type_supports( $settings['type'], 'taxonomies' ) ) {
return false;
}
// Bail if we're overwriting an existing Post and don't want to overwrite Taxonomy Terms.
if ( isset( $post_args['ID'] ) && ! array_key_exists( 'taxonomies', $settings['overwrite_sections'] ) ) {
return false;
}
// Define Taxonomy Terms and Post Type from Settings.
$taxonomy_terms = $settings['tax'];
$post_type = $settings['type'];
// Get Post Type Taxonomies.
$taxonomies = $this->base->get_class( 'common' )->get_post_type_taxonomies( $post_type );
// Bail if no Taxonomies exist.
if ( count( $taxonomies ) === 0 ) {
return true;
}
// Iterate through Taxonomies.
foreach ( $taxonomies as $taxonomy ) {
// Cleanup from last iteration.
unset( $terms );
// Bail if no Terms exist for this Taxonomy.
if ( ! isset( $taxonomy_terms[ $taxonomy->name ] ) ) {
continue;
}
if ( empty( $taxonomy_terms[ $taxonomy->name ] ) ) {
continue;
}
if ( is_array( $taxonomy_terms[ $taxonomy->name ] ) && count( $taxonomy_terms[ $taxonomy->name ] ) === 0 ) { // @phpstan-ignore-line
continue;
}
// Build Terms, depending on whether the Taxonomy is hierarchical or not.
switch ( $taxonomy->hierarchical ) {
case true:
foreach ( $taxonomy_terms[ $taxonomy->name ] as $tax_id => $terms_string ) {
// If Tax ID is not zero, the Term already exists in the Taxonomy.
// Just add it to the Terms array.
if ( $tax_id != 0 ) { // phpcs:ignore Universal.Operators.StrictComparisons.LooseNotEqual
$terms[] = (int) $tax_id;
continue;
}
// Convert string to array.
$terms_arr = str_getcsv( $terms_string, ',' );
foreach ( $terms_arr as $new_term ) {
// If the term is null, ignore it.
if ( is_null( $new_term ) ) {
continue;
}
// Remove leading or trailing whitespace.
$new_term = trim( $new_term );
// If the term is blank, ignore it.
if ( empty( $new_term ) ) {
continue;
}
// Check if this named term already exists in the taxonomy.
$result = term_exists( $new_term, $taxonomy->name );
if ( $result !== 0 && $result !== null ) {
$terms[] = (int) $result['term_id'];
continue;
}
// Term does not exist in the taxonomy - create it.
$result = wp_insert_term( $new_term, $taxonomy->name );
// Skip if something went wrong.
if ( is_wp_error( $result ) ) {
return $result;
}
// Add to term IDs.
$terms[] = (int) $result['term_id'];
}
}
break;
case false:
default:
$terms = $taxonomy_terms[ $taxonomy->name ];
break;
}
// If terms are not set or empty for this Taxonomy, continue.
if ( ! isset( $terms ) || empty( $terms ) ) {
continue;
}
// Assign Terms to Post.
$result = wp_set_post_terms( $post_id, $terms, $taxonomy->name, false );
// Bail if an error occured.
if ( is_wp_error( $result ) ) {
return $result;
}
}
// Terms assigned to Post successfully.
return true;
}
/**
* Checks the given Group settings to see whether we need to process shortcodes on
* the main Post Content.
*
* @since 1.9.5
*
* @param array $settings Group Settings.
* @return bool Process Shortcodes
*/
private function should_process_shortcodes_on_post_content( $settings ) {
// Assume that we will process shortcodes on the Post Content.
$process = true;
/**
* Flag whether the given Group should process shortcodes on the main Post Content
* (i.e. $post->post_content).
*
* @since 2.6.1
*
* @param bool $process Process Shortcodes on Post Content.
* @param array $settings Group Settings.
* @return bool Process Shortcodes on Post Content
*/
$process = apply_filters( 'page_generator_pro_generate_should_process_shortcodes_on_post_content', $process, $settings );
// Return result.
return $process;
}
/**
* Generates Comments for the given generated Post ID, based on
* the Content Group's Comments settings
*
* @since 2.8.8
*
* @param int $post_id Generated Post ID.
* @param int $group_id Group ID.
* @param int $index Generation Index.
* @param array $settings Group Settings.
* @param array $post_args wp_insert_post() / wp_update_post() arguments.
* @return WP_Error|bool
*/
private function generate_comments( $post_id, $group_id, $index, $settings, $post_args ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter
// Bail if the target Post Type doesn't support Comments.
if ( ! $this->base->get_class( 'common' )->post_type_supports( $settings['type'], 'comments' ) ) {
return false;
}
// Bail if Generating Comments isn't enabled.
if ( ! isset( $settings['comments_generate'] ) ) {
return false;
}
if ( ! isset( $settings['comments_generate']['enabled'] ) ) {
return false;
}
if ( ! $settings['comments_generate']['enabled'] ) {
return false;
}
// Bail if we're overwriting an existing Post and don't want to overwrite Comments.
if ( isset( $post_args['ID'] ) && ! array_key_exists( 'comments', $settings['overwrite_sections'] ) ) {
return false;
}
// Build a class array of required keywords that need replacing with data.
$required_keywords = $this->find_keywords_in_settings( $settings['comments_generate'] );
if ( count( $required_keywords['required_keywords'] ) > 0 ) {
$this->keywords = $this->get_keywords_terms_columns_delimiters( $required_keywords['required_keywords'] );
}
// Determine the number of comments to generate.
$limit = absint( $settings['comments_generate']['limit'] );
if ( ! $limit ) {
$limit = wp_rand( 0, 10 );
}
// Delete Comments.
$result = $this->delete_comments( $group_id, $post_id );
if ( is_wp_error( $result ) ) {
return $result;
}
// Reset search and replace arrays before we build them for each Comment.
$this->reset_search_replace_arrays();
// Generate Comments.
for ( $comments_index = 0; $comments_index < $limit; $comments_index++ ) {
// If no Keywords exist in the Generate Comments section of the Content Group, don't build search/replace arrays.
if ( count( $this->keywords['terms'] ) ) {
$this->build_search_replace_arrays(
$required_keywords['required_keywords_full'],
$this->get_keywords_terms( $settings['method'], (int) $comments_index )
);
}
// Replace Keywords and process Spintax.
$comments_settings = $this->replace_keywords( $settings['comments_generate'] );
// Generate Comment.
$result = $this->generate_comment( $post_id, $group_id, $comments_index, $comments_settings );
if ( is_wp_error( $result ) ) {
return $result;
}
}
// Done.
return true;
}
/**
* Generates a single Comment for the given generated Post ID, based on
* the Content Group's Comments settings
*
* @since 2.8.8
*
* @param int $post_id Generated Post ID.
* @param int $group_id Group ID.
* @param int $comments_index Comment Generation Index.
* @param array $settings Group Settings.
* @return WP_Error|int
*/
private function generate_comment( $post_id, $group_id, $comments_index, $settings ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter
// Define the Comment Date.
switch ( $settings['date_option'] ) {
/**
* Now
*/
case 'now':
$comment_date = date_i18n( 'Y-m-d H:i:s' );
break;
/**
* Specific Date
*/
case 'specific':
$comment_date = $settings['date_specific'];
break;
/**
* Random
*/
case 'random':
default:
$min = strtotime( $settings['date_min'] );
$max = strtotime( $settings['date_max'] );
$comment_date = date_i18n( 'Y-m-d H:i:s', wp_rand( $min, $max ) );
break;
}
// Build array of comment arguments.
$comment_args = array(
'comment_author' => $settings['firstname'] . ' ' . $settings['surname'],
'comment_content' => $settings['comment'],
'comment_date' => $comment_date,
'comment_date_gmt' => $comment_date,
'comment_post_ID' => $post_id,
'comment_meta' => array(
'_page_generator_pro_group' => $group_id,
),
);
/**
* Filters arguments used for creating a Comment when running
* content generation for a specific Group ID and Post ID
*
* @since 2.8.8
*
* @param array $comment_args wp_insert_comment() compatible arguments.
* @param array $settings Content Group's Comment Settings.
*/
$comment_args = apply_filters( 'page_generator_pro_generate_comment_args', $comment_args, $settings );
// Insert comment.
$comment_id = wp_insert_comment( $comment_args );
// If false, something went wrong.
if ( ! $comment_id ) {
return new WP_Error(
'page_generator_pro_generate_comment_error',
sprintf(
/* translators: Post ID */
__( 'Unable to generate comment for Post ID %s', 'page-generator-pro' ),
$post_id
)
);
}
return $comment_id;
}
/**
* Get Term ID by the given Term Path by recursing through each Term in the Term Path,
* finding its Term object in WordPress as a child of the previous Term in the Term Path.
*
* Where a Term doesn't exist, it's created.
*
* @since 3.5.5
*
* @param string $taxonomy Taxonomy.
* @param string $term_path Term Path (parent|parent/child|parent/child/grandchild etc).
* @return int $term_id Term ID
*/
private function get_term_path_id( $taxonomy, $term_path ) {
// Recurse through each Term in the Term Path, finding its Term object in WordPress
// as a child of the previous Term in the Term Path.
// Where a Term doesn't exist, it's created.
$term_path_id = 0;
foreach ( explode( '/', $term_path ) as $term ) {
// Build query args.
$args = array(
'taxonomy' => $taxonomy,
'name' => $term,
'parent' => $term_path_id,
'hide_empty' => false,
// For performance, just return the Post ID and don't update meta or term caches.
'fields' => 'ids',
'update_term_meta_cache' => false,
);
// Query to see if this Term exists.
$existing_terms = new WP_Term_Query( $args );
// If no Term found, create it.
if ( is_null( $existing_terms->terms ) ) { // @phpstan-ignore-line
$new_term = wp_insert_term(
$term,
$taxonomy,
array(
'parent' => $term_path_id,
)
);
// Store this Term as the Term Path ID.
$term_path_id = $new_term['term_id'];
// Move onto the next Term in the Term Path.
continue;
}
// If here, Term exists. Assign its ID to $term_path_id.
$term_path_id = $existing_terms->terms[0];
}
// Term Path ID is the 'final' Term in the path, and the Term ID we want.
return $term_path_id;
}
/**
* Main function to trash previously generated Contents
* for the given Group ID
*
* @since 1.2.3
*
* @param int $group_id Group ID.
* @param int $limit Number of Generated Posts to delete (-1 = all).
* @param bool|array $exclude_post_ids Exclude Post IDs from deletion.
* @return WP_Error|bool
*/
public function trash_content( $group_id, $limit = -1, $exclude_post_ids = false ) {
// Get all Post IDs generated by this Group.
$post_ids = $this->get_generated_content_post_ids( $group_id, $limit, $exclude_post_ids );
// Bail if an error occured.
if ( is_wp_error( $post_ids ) ) {
return $post_ids;
}
// Delete Posts by their IDs.
foreach ( $post_ids as $post_id ) {
$result = wp_trash_post( $post_id );
if ( ! $result ) {
return new WP_Error(
'page_generator_pro_generate_trash_content',
sprintf(
/* translators: Post ID */
__( 'Unable to trash generated content with ID = %s', 'page-generator-pro' ),
$post_id
)
);
}
}
/**
* Run any actions after all generated content for a given Content Group has been trashd.
*
* @since 3.4.2
*
* @param int $group_id Group ID.
* @param array $post_ids Generated Post IDs that were deleted.
*/
do_action( 'page_generator_pro_generate_trash_content_finished', $group_id, $post_ids );
// Done.
return true;
}
/**
* Main function to delete previously generated Contents
* for the given Group ID
*
* @since 1.2.3
*
* @param int $group_id Group ID.
* @param int $limit Number of Generated Posts to delete (-1 = all).
* @param bool|array $exclude_post_ids Exclude Post IDs from deletion.
* @return WP_Error|bool
*/
public function delete_content( $group_id, $limit = -1, $exclude_post_ids = false ) {
// Get all Post IDs generated by this Group.
$post_ids = $this->get_generated_content_post_ids( $group_id, $limit, $exclude_post_ids );
// Bail if an error occured.
if ( is_wp_error( $post_ids ) ) {
return $post_ids;
}
// Delete Attachments.
$this->delete_attachments_by_post_ids( $post_ids, $group_id );
$this->delete_featured_image_by_post_ids( $post_ids, $group_id );
// Delete Posts.
foreach ( $post_ids as $post_id ) {
$result = wp_delete_post( $post_id, true );
if ( ! $result ) {
return new WP_Error(
'page_generator_pro_generate_delete_content',
sprintf(
/* translators: Post ID */
__( 'Unable to delete generated content with ID = %s', 'page-generator-pro' ),
$post_id
)
);
}
}
/**
* Run any actions after all generated content for a given Content Group has been deleted.
*
* @since 3.4.2
*
* @param int $group_id Group ID.
* @param array $post_ids Generated Post IDs that were deleted.
*/
do_action( 'page_generator_pro_generate_delete_content_finished', $group_id, $post_ids );
return true;
}
/**
* Main function to delete previously generated Comments
* for the given Group ID and Post ID
*
* @since 2.8.8
*
* @param int $group_id Group ID.
* @param int $post_id Post ID.
* @return WP_Error|bool
*/
public function delete_comments( $group_id, $post_id ) {
// Get all Comment IDs generated by this Group.
$comment_ids = $this->get_generated_comment_ids( $group_id, $post_id );
// Bail if no comments exist.
if ( ! $comment_ids ) {
return false;
}
// Delete Comments.
foreach ( $comment_ids as $comment_id ) {
$result = wp_delete_comment( $comment_id, true );
if ( ! $result ) {
return new WP_Error(
'page_generator_pro_generate_delete_comments',
sprintf(
/* translators: Comment ID */
__( 'Unable to trash generated comment with ID = %s', 'page-generator-pro' ),
$comment_id
)
);
}
}
/**
* Run any actions after all generated comments for a given Content Group and Generated Post has been deleted.
*
* @since 3.4.2
*
* @param int $group_id Group ID.
* @param int $post_id Generated Post ID.
* @param array $comment_ids Generated Comment IDs that were deleted.
*/
do_action( 'page_generator_pro_generate_delete_comments_finished', $group_id, $post_id, $comment_ids );
return true;
}
/**
* Deletes latitude and longitude from the Geo table when a Post is deleted.
*
* Trashed Posts are unaffected.
*
* @since 2.3.6
*
* @param int $post_id Post ID.
*/
public function delete_latitude_longitude_by_post_id( $post_id ) {
$this->base->get_class( 'geo' )->delete( $post_id );
}
/**
* Returns all Post IDs generated by the given Group ID
*
* @since 1.9.1
*
* @param int $group_id Group ID.
* @param int $limit Number of Post IDs to return (-1 = no limit).
* @param bool|array $exclude_post_ids Exclude Post IDs.
* @return WP_Error|array
*/
public function get_generated_content_post_ids( $group_id, $limit = -1, $exclude_post_ids = false ) {
// Fetch valid Post Statuses that can be used when generating content.
$statuses = array_keys( $this->base->get_class( 'common' )->get_post_statuses() );
$params = array(
'post_type' => 'any',
'post_status' => $statuses,
'posts_per_page' => $limit,
'meta_query' => array(
array(
'key' => '_page_generator_pro_group',
'value' => absint( $group_id ),
),
),
'fields' => 'ids',
'update_post_meta_cache' => false,
'update_post_term_cache' => false,
);
// Add excluded Post IDs, if defined.
if ( is_array( $exclude_post_ids ) ) {
// Cast to integers.
foreach ( $exclude_post_ids as $index => $post_id ) {
$exclude_post_ids[ $index ] = absint( $post_id );
}
// Add to query parameters.
$params['post__not_in'] = $exclude_post_ids;
}
// Get all Posts.
$posts = new WP_Query( $params );
// If no Posts found, return an error.
if ( ! $posts->posts || count( $posts->posts ) === 0 ) { // @phpstan-ignore-line
return new WP_Error( 'page_generator_pro_generate_get_generated_content_post_ids', __( 'No content has been generated by this group.', 'page-generator-pro' ) );
}
// Return Post IDs.
return $posts->posts;
}
/**
* Returns all Term IDs generated by the given Group ID
*
* @since 3.0.3
*
* @param int $group_id Group ID.
* @param int $limit Number of Term IDs to return.
* @param bool|array $exclude_term_ids Exclude Term IDs.
* @return WP_Error|array
*/
public function get_generated_term_ids( $group_id, $limit = 999999, $exclude_term_ids = false ) {
// Get Settings.
$settings = $this->base->get_class( 'groups_terms' )->get_settings( $group_id, false );
// Bail if an error occured.
if ( is_wp_error( $settings ) ) {
return $settings;
}
// Build parameters.
$params = array(
'taxonomy' => $settings['taxonomy'],
'number' => $limit,
'meta_query' => array(
array(
'key' => '_page_generator_pro_group',
'value' => absint( $group_id ),
),
),
'hide_empty' => false,
// For performance, just return the Post ID and don't update meta or term caches.
'fields' => 'ids',
'update_term_meta_cache' => false,
);
// Add excluded Term IDs, if defined.
if ( is_array( $exclude_term_ids ) ) {
// Cast to integers.
foreach ( $exclude_term_ids as $index => $term_id ) {
$exclude_term_ids[ $index ] = absint( $term_id );
}
// Add to query parameters.
$params['exclude'] = $exclude_term_ids;
}
// Get all Terms.
$terms = new WP_Term_Query( $params );
// If no Terms found, return false, as there's nothing to delete.
if ( is_null( $terms->terms ) ) { // @phpstan-ignore-line
return new WP_Error(
'page_generator_pro_generate_get_generated_term_ids',
__( 'No Terms have been generated by this group, so there are no Terms to delete.', 'page-generator-pro' )
);
}
// Return Term IDs.
return $terms->terms;
}
/**
* Returns all Comment IDs generated by the given Group ID and Post ID
*
* @since 2.8.8
*
* @param int $group_id Group ID.
* @param int $post_id Post ID.
* @return bool|array
*/
public function get_generated_comment_ids( $group_id, $post_id ) {
// Get all Comments.
$comments = new WP_Comment_Query(
array(
'post_id' => $post_id,
'meta_query' => array(
array(
'key' => '_page_generator_pro_group',
'value' => absint( $group_id ),
),
),
'fields' => 'ids',
)
);
// If no Comments found, bail.
if ( ! $comments->comments || count( $comments->comments ) === 0 ) { // @phpstan-ignore-line
return false;
}
// Return Comment IDs.
return $comments->comments;
}
/**
* Gets attachment IDs for the given Post IDs and Content Group ID
*
* @since 2.7.9
*
* @param array $post_ids Post IDs.
* @param int $group_id Group ID.
* @return bool|array
*/
private function get_attachment_ids_by_post_ids( $post_ids, $group_id ) {
$results = new WP_Query(
array(
'post_type' => 'attachment',
'post_status' => 'any',
'posts_per_page' => -1,
'post_parent__in' => $post_ids,
'meta_query' => array(
array(
'key' => '_page_generator_pro_group',
'value' => absint( $group_id ),
),
),
'fields' => 'ids',
)
);
// If no Attachments found, return false.
if ( count( $results->posts ) === 0 ) {
return false;
}
// Return.
return $results->posts;
}
/**
* Gets all featured image IDs for the given Post IDs.
*
* @since 2.7.9
*
* @param array $post_ids Post IDs.
* @param int $group_id Group ID.
* @return bool|array
*/
private function get_featured_image_attachment_ids_by_post_ids( $post_ids, $group_id ) {
$featured_image_ids = array();
foreach ( $post_ids as $post_id ) {
$image_id = absint( get_post_meta( $post_id, '_thumbnail_id', true ) );
// Skip if empty.
if ( ! $image_id ) {
continue;
}
// Check that the image is assigned to the Content Group.
// This prevents Featured Images assigned to generated Pages where copy = no from being deleted.
$featured_image_group_id = absint( get_post_meta( $image_id, '_page_generator_pro_group', true ) );
// Skip if empty.
if ( ! $featured_image_group_id ) {
continue;
}
// Skip if the image is assigned to a different Content Group.
if ( $featured_image_group_id !== $group_id ) {
continue;
}
// Add to array.
$featured_image_ids[] = $image_id;
}
// If no images found, return false.
if ( ! count( $featured_image_ids ) ) {
return false;
}
return $featured_image_ids;
}
/**
* Deletes Attachments assigned to the given Post IDs and Group ID, excluding the Featured Image
*
* @since 2.4.1
*
* @param array $post_ids Post IDs.
* @param int $group_id Group ID.
* @return bool Attachments were deleted (false = no attachments to delete)
*/
private function delete_attachments_by_post_ids( $post_ids, $group_id ) {
// Get all Attachments belonging to the given Post IDs and Group.
$attachment_ids = $this->get_attachment_ids_by_post_ids( $post_ids, $group_id );
// If no Attachments found, return false, as there's nothing to delete.
if ( ! $attachment_ids ) {
return false;
}
// Get all Attachments that are Featured Images for the given Post IDs.
$featured_images_ids = $this->get_featured_image_attachment_ids_by_post_ids( $post_ids, $group_id );
// Delete attachments.
foreach ( $attachment_ids as $attachment_id ) {
// If the attachment is the Featured Image, don't delete it, as this is handled separately
// by delete_featured_images_by_post_ids().
if ( is_array( $featured_images_ids ) && in_array( $attachment_id, $featured_images_ids, true ) ) {
continue;
}
// Delete attachment.
wp_delete_attachment( $attachment_id );
}
return true;
}
/**
* Deletes Featured Images assigned to the given Post IDs.
*
* @since 2.7.9
*
* @param array $post_ids Post IDs.
* @param int $group_id Group ID.
* @return bool Featured Image Attachments were deleted (false = no featured image attachments to delete)
*/
private function delete_featured_image_by_post_ids( $post_ids, $group_id ) {
// Get all Attachments that are Featured Images for the given Post IDs and Group.
$featured_images_ids = $this->get_featured_image_attachment_ids_by_post_ids( $post_ids, $group_id );
// If no Attachments found, return false, as there's nothing to delete.
if ( ! $featured_images_ids ) {
return false;
}
// Delete attachments.
foreach ( $featured_images_ids as $attachment_id ) {
// Delete attachment.
wp_delete_attachment( $attachment_id );
}
return true;
}
/**
* Main function to delete previously generated Terms
* for the given Group ID
*
* @since 1.6.1
*
* @param int $group_id Group ID.
* @param int $limit Number of Generated Posts to delete (-1 = all).
* @param bool|array $exclude_term_ids Exclude Term IDs from deletion.
* @return WP_Error|bool
*/
public function delete_terms( $group_id, $limit = 999999, $exclude_term_ids = false ) {
// Get all Term IDs generated by this Group.
$term_ids = $this->get_generated_term_ids( $group_id, $limit, $exclude_term_ids );
// Bail if an error occured.
if ( is_wp_error( $term_ids ) ) {
return $term_ids;
}
// Get Settings.
$settings = $this->base->get_class( 'groups_terms' )->get_settings( $group_id, false );
// Delete Terms.
foreach ( $term_ids as $term_id ) {
$result = wp_delete_term( $term_id, $settings['taxonomy'] );
if ( ! $result ) {
return new WP_Error(
'page_generator_pro_generate_delete_terms',
sprintf(
/* translators: Term ID */
__( 'Unable to delete generated Term with ID = %s', 'page-generator-pro' ),
$term_id
)
);
}
}
return true;
}
/**
* Removes wp_update_post() $post_args that are not selected for overwriting
*
* @since 2.3.5
*
* @param array $overwrite_sections Sections to Overwrite.
* @param array $post_args wp_update_post() compatible Post Arguments.
* @return array wp_update_post() compatible Post Arguments
*/
private function restrict_post_args_by_overwrite_sections( $overwrite_sections, $post_args ) {
// Fetch all available overwrite sections.
$all_possible_overwrite_sections = array_keys( $this->base->get_class( 'common' )->get_content_overwrite_sections() );
$overwrite_sections_to_ignore = array_diff( $all_possible_overwrite_sections, $overwrite_sections );
// If all overwrite sections are selected (i.e. no overwrite sections to ignore / skip), just return the post args.
if ( empty( $overwrite_sections_to_ignore ) ) {
return $post_args;
}
// For each overwrite section to ignore / skip, remove it from the Post Args.
foreach ( $overwrite_sections_to_ignore as $overwrite_section_to_ignore ) {
unset( $post_args[ $overwrite_section_to_ignore ] );
}
return $post_args;
}
/**
* Returns an array of data relating to the successfully generated Post or Term,
* logging the result if logging is enabled.
*
* @since 2.1.8
*
* @param int $group_id Group ID.
* @param int $post_or_term_id Post or Term ID.
* @param string $post_type_or_taxonomy Post Type or Taxonomy.
* @param bool $generated Post Generated (false = skipped).
* @param string $message Message to return (created, updated, skipped etc).
* @param int $start Start Time.
* @param bool $test_mode Test Mode.
* @param string $system System (browser|cron|cli).
* @param array $keywords_terms Keywords / Terms Key / Value array used.
* @param bool|string $last_generated_post_date_time Last Generated Post's Date and Time.
* @return array
*/
private function generate_return( $group_id, $post_or_term_id, $post_type_or_taxonomy, $generated, $message, $start, $test_mode, $system, $keywords_terms, $last_generated_post_date_time = false ) {
// Determine if we're returning data for a generated Post or Term.
// We check if it's a Taxonomy first as post_type_exists() fails for e.g. WooCommerce Products.
if ( taxonomy_exists( $post_type_or_taxonomy ) ) {
// Term.
$url = get_term_link( $post_or_term_id, $post_type_or_taxonomy );
} else {
$url = get_permalink( $post_or_term_id );
if ( $test_mode ) {
$url = add_query_arg(
array(
'preview' => 'true',
),
get_permalink( $post_or_term_id )
);
} else {
$url = get_permalink( $post_or_term_id );
}
}
// Performance debugging.
$end = ( function_exists( 'hrtime' ) ? hrtime( true ) : microtime( true ) );
// Strip HTML from Keywords Terms, to avoid issues with Generate via Browser log output.
foreach ( $keywords_terms as $keyword => $term ) {
$keywords_terms[ $keyword ] = wp_strip_all_tags( $term );
}
// Build result array.
$result = array(
// Item.
'post_id' => $post_or_term_id,
'url' => $url,
'type' => ( taxonomy_exists( $post_type_or_taxonomy ) ? 'term' : 'content' ),
'system' => $system,
'test_mode' => $test_mode,
'generated' => $generated,
'result' => 'success',
'keywords_terms' => $keywords_terms,
'last_generated_post_date_time' => $last_generated_post_date_time,
'message' => $message,
// Performance data.
'start' => $start,
'end' => $end,
'duration' => ( function_exists( 'hrtime' ) ? round( ( ( $end - $start ) / 1e+9 ), 3 ) : round( ( $end - $start ), 2 ) ),
'memory_usage' => round( memory_get_usage() / 1024 / 1024 ),
'memory_peak_usage' => round( memory_get_peak_usage() / 1024 / 1024 ),
);
// Add to log.
$this->base->get_class( 'log' )->add( $group_id, $result );
// Return.
return $result;
}
/**
* Returns the supplied WP_Error, logging the result if logging is enabled
*
* @since 2.8.0
*
* @param WP_Error $error WP_Error.
* @param int $group_id Group ID.
* @param int $post_or_term_id Post or Term ID.
* @param string $post_type_or_taxonomy Post Type or Taxonomy.
* @param bool $test_mode Test Mode.
* @param string $system System (browser|cron|cli).
* @param bool|array $keywords_terms Keywords and Terms.
* @return WP_Error Error
*/
private function generate_error_return( $error, $group_id, $post_or_term_id, $post_type_or_taxonomy, $test_mode, $system, $keywords_terms = false ) {
// Determine if we're returning data for a generated Post or Term.
// We check if it's a Taxonomy first as post_type_exists() fails for e.g. WooCommerce Products.
$url = '';
if ( $post_or_term_id ) {
if ( taxonomy_exists( $post_type_or_taxonomy ) ) {
// Term.
$url = get_term_link( $post_or_term_id, $post_type_or_taxonomy );
} elseif ( $test_mode ) {
// Post, Page or CPT, draft.
$url = get_bloginfo( 'url' ) . '?p=' . $post_or_term_id . '&preview=true';
} else {
// Post, Page or CPT, scheduled/published.
$url = get_permalink( $post_or_term_id );
}
}
// Build result array.
$result = array(
// Item.
'post_id' => $post_or_term_id,
'url' => $url,
'type' => ( taxonomy_exists( $post_type_or_taxonomy ) ? 'term' : 'content' ),
'system' => $system,
'test_mode' => $test_mode,
'generated' => 0,
'result' => 'error',
'keywords_terms' => $keywords_terms,
'message' => $error->get_error_message(),
// Performance data.
'start' => 0,
'end' => 0,
'duration' => 0,
'memory_usage' => round( memory_get_usage() / 1024 / 1024 ),
'memory_peak_usage' => round( memory_get_peak_usage() / 1024 / 1024 ),
);
// Add to log.
$this->base->get_class( 'log' )->add( $group_id, $result );
// Return original WP_Error.
return $error;
}
/**
* Helper method to call Log class' add_to_debug_log(), and output
* to the CLI if required
*
* @since 3.1.3
*
* @param string $message Message.
* @param string $system System.
* @param bool $is_error Is Error Message.
*/
private function add_to_debug_log( $message, $system, $is_error = false ) {
// Pass to logging class.
$this->base->get_class( 'log' )->add_to_debug_log( $message );
// Return if not in CLI.
if ( $system !== 'cli' ) {
return;
}
// For CLI, output the log message now.
if ( $is_error ) {
// Don't stop execution; generate() will determine whether to stop.
WP_CLI::error( $message, false );
} else {
WP_CLI::log( $message );
}
}
}