• File: Inserter.php
  • Full Path: /home/matthif/www/wp-content/plugins/optinmonster/OMAPI/Inserter.php
  • Date Modified: 01/24/2025 1:58 PM
  • File size: 11.29 KB
  • MIME-type: text/x-php
  • Charset: utf-8
<?php
/**
 * Content Inserter class.
 *
 * @since 2.0.0
 *
 * @package OMAPI
 * @author  Justin Sternberg
 */

// Exit if accessed directly.
if ( ! defined'ABSPATH' ) ) {
    exit;
}

/**
 * Content Inserter class.
 *
 * @since 2.0.0
 */
class OMAPI_Inserter {

    
/**
     * Content to insert.
     *
     * @since 2.0.0
     *
     * @var string
     */
    
protected $to_insert '';

    
/**
     * Original content.
     *
     * @since 2.0.0
     *
     * @var string
     */
    
protected $content '';

    
/**
     * Any tags which we should not insert content in.
     * The order is important ("a" tag should be after any other tags starting with "a") in order
     * to avoid false positives (where "a" tag is "found", but it's really an "abbr" tag).
     *
     * @since 2.0.0
     *
     * @var string
     */
    
const INSERT_AFTER_TAGS 'address,abbr,acronym,area,audio,a,bdo,big,blockquote,button,b,caption,cite,code,col,colgroup,del,dfn,dl,dt,em,figure,figcaption,font,h1,h2,h3,h4,h5,h6,hgroup,ins,i,kbd,label,legend,map,mark,menu,pre,samp,small,strike,strong,sub,sup,s,table,tbody,textarea,tfoot,thead,title,track,tt,tr,ul,u,ol,var,video';

    
/**
     * Constructor.
     *
     * @since 2.0.0
     *
     * @param string $content   The original content.
     * @param string $to_insert The content to insert into the original content.
     */
    
public function __construct$content$to_insert ) {
        
$this->content   $content;
        
$this->to_insert $to_insert;
    }

    
/**
     * Prepend the original content with the content to insert.
     *
     * @since  2.0.0
     *
     * @return string The combined content.
     */
    
public function prepend() {
        return 
$this->to_insert $this->content;
    }

    
/**
     * Append the original content with the content to insert.
     *
     * @since  2.0.0
     *
     * @return string The combined content.
     */
    
public function append() {
        return 
$this->content $this->to_insert;
    }

    
/**
     * Inserts the insert-content after X paragraphs in the original content.
     *
     * @since  2.0.0
     *
     * @param  int $paragraph_number The paragraph number to insert after.
     *
     * @return string The combined content.
     */
    
public function after_paragraph$paragraph_number ) {

        
// If "0", then prepend.
        
if ( empty( $paragraph_number ) ) {
            return 
$this->prepend();
        }

        
$closing_p  '</p>';
        
$paragraphs explode$closing_p$this->content );
        
$count      count$paragraphs );

        
// If the number of paragraphs in the content is less than
        // the number we asked for, just append it to the end.
        
if ( $count $paragraph_number ) {
            return 
$this->append();
        }

        
$valid_paragraphs 0;
        
$invalid_tag      '';
        
$appended         false;

        foreach ( 
$paragraphs as $index => $paragraph ) {

            
// Only add closing tag to non-empty paragraphs.
            
if ( trim$paragraph ) ) {

                
// Adding closing markup now, rather than at implode, means insertion
                // is outside of the paragraph markup, and not just inside of it.
                
$paragraphs$index ] .= $closing_p;
            }

            
// If it has already appended, it doesn't need to keep checking.
            // It needs to keep the loop to close the paragraph tag.
            
if ( $appended ) {
                continue;
            }

            
// If it has an invalid tag from the last index,
            // search if this index has the closing tag.
            // If it doesn't have, it need to continue to the next index.
            
if ( ! empty( $invalid_tag ) && self::find_tag$invalid_tag$paragraphs$index ] ) ) {
                
// Found it, let's clean it.
                
$invalid_tag '';
            }

            
// Check if it has any after_tags inside this block.
            
$invalid_tag self::find_invalid_word_tag$paragraphs$index ] );
            if ( ! empty( 
$invalid_tag ) ) {
                
// If so, then it must continue counting to the next paragraph.
                
continue;
            }

            
// This is a valid paragraph.
            
$valid_paragraphs++;

            
// If it has enough valid paragraphs, it can continue
            
if ( $valid_paragraphs === $paragraph_number ) {

                
// If it doesn't have an invalid tag inside this paragraph,
                // it can append after this paragraph.
                
$paragraphs$index ] .= $this->to_insert;
                
$appended              true;
            }
        }

        
// If it appended, it can output all paragraphs.
        
if ( $appended ) {
            return 
implode''$paragraphs );
        }

        
// If it didn't append, then it didn't have enough valid paragraphs,
        // so we'll append it to the end.
        
return $this->append();
    }

    
/**
     * Inserts the insert-content after X words in the original content.
     *
     * @since  2.0.0
     *
     * @param  int $word_number The word number to insert after.
     *
     * @return string The combined content.
     */
    
public function after_words$word_number ) {

        
// If "0", then prepend.
        
if ( empty( $word_number ) ) {
            return 
$this->prepend();
        }

        
// The following splitting into words code is copied from the wp_trim_words function.

        
$rawtext wp_strip_all_tags$this->content );

        
/*
         * translators: If your word count is based on single characters (e.g. East Asian characters),
         * enter 'characters_excluding_spaces' or 'characters_including_spaces'. Otherwise, enter 'words'.
         * Do not translate into your own language.
         */
        
if ( strpos_x'words''Word count type. Do not translate!''optin-monster-api' ), 'characters' ) === && preg_match'/^utf\-?8$/i'get_option'blog_charset' ) ) ) {
            
$rawtext trimpreg_replace"/[\n\r\t ]+/"' '$rawtext ), ' ' );
            
preg_match_all'/./u'$rawtext$words_array );
            
$words_array array_slice$words_array[0], 0$word_number );
        } else {
            
$words_array preg_split"/[\n\r\t ]+/"$rawtext$word_number 1PREG_SPLIT_NO_EMPTY );
        }

        
// If the number of words in the content is less than
        // the number we asked for, just append it to the end.
        
if ( count$words_array ) <= $word_number ) {
            return 
$this->append();
        }

        
// Now we need to clean up the words, removing punctuation,
        // so our chances of matching are greater.
        
foreach ( $words_array as $index => $word ) {
            
$words_array$index ] = preg_replace'~[^\w\s]~'''$word );
        }

        
$after_word         $words_array$word_number ];
        
$number_occurrences 0;

        
$rest array_pop$words_array );

        foreach ( 
$words_array as $word ) {
            if ( ! empty( 
$after_word ) && false !== strpos$word$after_word ) ) {
                
$number_occurrences++;
            }
        }

        
$to_replace $this->content;

        
// We need to loop through the number of occurrences...
        
while ( $number_occurrences-- ) {

            
// Then find the word in the content to replace...
            
$pos strpos$to_replace$after_word ) + strlen$after_word );

            
// And split that content where the word was found...
            
$to_replace substr$to_replace$pos );

            
// And keep doing that until we've reached our final occurrence.
        
}

        
// Ok, no we know where we want to insert, but we can't insert inside any
        // of our self::INSERT_AFTER_TAGS tags, so we need to insert _after_ them.
        
$to_replace self::after_tags$to_replace );

        
// Now insert into the content.
        
$updated_content str_replace$to_replace$this->to_insert $to_replace$this->content );

        return 
$updated_content;
    }

    
/**
     * Takes given content and returns first acceptable content outside of any
     * self::INSERT_AFTER_TAGS tags.
     *
     * @since  2.0.0
     *
     * @param  string $content Content to replace/find.
     *
     * @return string           Updated content.
     */
    
protected static function after_tags$content ) {
        foreach ( 
explode','self::INSERT_AFTER_TAGS ) as $tag ) {
            
$positions self::find_tag$tag$content );
            if ( ! 
$positions ) {
                continue;
            }

            
// Ok... we found a tag that we should scoot behind.
            
return substr$content$positions['closing'] + strlen$positions['closing_tag'] ) );
        }

        return 
$content;
    }

    
/**
     * Get the opening/closing positions of given tag.
     *
     * @since 2.10.0
     *
     * @param  string $tag     The html tag.
     * @param  string $content The html content to search.
     *
     * @return array            Array of tag position data.
     */
    
public static function get_tag_positions$tag$content ) {
        if ( 
self::is_void_element_tag$tag ) ) {
            
$result self::find_void_element_tag$tag$content );
            return array(
                
'opening'     => $result['position'],
                
'closing'     => $result['position'],
                
'opening_tag' => $result['tag'],
                
'closing_tag' => $result['tag'],
            );
        }

        
$opening_tag '<' $tag '>';
        
$opening_pos stripos$content$opening_tag );
        if ( 
false === $opening_pos ) {
            
$opening_tag '<' $tag ' ';
            
$opening_pos stripos$content$opening_tag );
        }

        
$closing_tag '</' $tag '>';
        
$closing_pos stripos$content$closing_tag );

        return array(
            
'opening'     => $opening_pos,
            
'closing'     => $closing_pos,
            
'opening_tag' => $opening_tag,
            
'closing_tag' => $closing_tag,
        );
    }

    
/**
     * Find the opening/closing positions of given tag, if found.
     *
     * @since 2.10.0
     *
     * @param  string $tag     The html tag.
     * @param  string $content The html content to search.
     *
     * @return array|bool       Array of tag position data, if found.
     */
    
public static function find_tag$tag$content ) {
        
$positions self::get_tag_positions$tag$content );

        
// Not found, move along.
        
if ( false === $positions['closing'] ) {
            return 
false;
        }

        
// If we found an opening tag but it comes before the closing,
        // then this is not the one we were looking at.
        
if (
            
false !== $positions['opening']
            && 
$positions['opening'] < $positions['closing']
        ) {
            return 
false;
        }

        return 
$positions;
    }

    
/**
     * Search for invalid tags within given content.
     *
     * @since 2.10.0
     *
     * @param  string $content html content to search.
     *
     * @return string           Invalid tag, if found.
     */
    
public static function find_invalid_word_tag$content ) {
        
// Check if it has any after_tags inside this block.
        // If so, then it must continue counting to the next paragraph.
        
foreach ( explode','self::INSERT_AFTER_TAGS ) as $tag ) {
            
$positions self::get_tag_positions$tag$content );

            
// Not found, continue to the next
            
if ( false === $positions['opening'] ) {
                continue;
            }

            
// It has it, so it can ignore since it opens and closes inside the paragraph.
            // It also needs to check if the closing comes after the opening.
            
if ( false !== $positions['closing'] && $positions['opening'] < $positions['closing'] ) {
                continue;
            }

            
// It doesn't have a closing tag, so this paragraph lives inside an invalid tag.
            
return $tag;
        }

        return 
'';
    }

    
/**
     * Find the given void element tag position/occurrence.
     *
     * @since 2.10.0
     *
     * @param  string $tag     The void element tag name.
     * @param  string $content html content to search.
     *
     * @return array Array of found element occurrence and string position.
     */
    
public static function find_void_element_tag$tag$content ) {
        
preg_match_all'~<' $tag '.*?\/?>~i'$content$matches );
        if ( ! empty( 
$matches[0][0] ) ) {
            return array(
                
'tag'      => $matches[0][0],
                
'position' => strpos$content$matches[0][0] ),
            );
        }

        return array(
            
'tag'      => '<' $tag '/>',
            
'position' => false,
        );
    }

    
/**
     * Check if given tag is a void element tag.
     *
     * @since 2.10.0
     *
     * @param  string $tag HTML tag name.
     *
     * @return boolean
     */
    
public static function is_void_element_tag$tag ) {
        return 
in_array(
            
strtolower$tag ),
            array(
                
'area',
                
'base',
                
'br',
                
'col',
                
'embed',
                
'hr',
                
'img',
                
'input',
                
'link',
                
'meta',
                
'param',
                
'source',
                
'track',
                
'wbr',
                
'command',
                
'keygen',
                
'menuitem',
            ),
            
true
        
);
    }
}