any-post.php 28.3 KB
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861
<?php

/**
 * The manager to rule all (post) managers.
 *
 * This class dynamically registers container modules for the available post types
 * (including custom post types) and does stuff that pertain to all of them, such
 * as handling save/delete hooks and (re)creating synch records.
 *
 * @package Broken Link Checker
 * @author Janis Elsts
 * @access private
 */
class blcPostTypeOverlord {
	public $enabled_post_types    = array();  //Post types currently selected for link checking
	public $enabled_post_statuses = array( 'publish' ); //Only posts that have one of these statuses shall be checked

	var $plugin_conf;
	var $resynch_already_done = false;

	/**
	 * Class "constructor". Can't use an actual constructor due to how PHP4 handles object references.
	 *
	 * Specifically, this class is a singleton. The function needs to pass $this to several other
	 * functions (to set up hooks), which will store the reference for later use. However, it appears
	 * that in PHP4 the actual value of $this is thrown away right after the constructor finishes, and
	 * `new` returns a *copy* of $this. The result is that getInstance() won't be returning a ref.
	 * to the same object as is used for hook callbacks. And that's horrible.
	 *
	 * Sets up hooks that monitor added/modified/deleted posts and registers
	 * virtual modules for all post types.
	 *
	 * @return void
	 */
	function init() {
		$this->plugin_conf = blc_get_configuration();

		if ( isset( $this->plugin_conf->options['enabled_post_statuses'] ) ) {
			$this->enabled_post_statuses = $this->plugin_conf->options['enabled_post_statuses'];
		}

		//Register a virtual container module for each enabled post type
		$module_manager = blcModuleManager::getInstance();

		$post_types = get_post_types( array(), 'objects' );
		$exceptions = array( 'revision', 'nav_menu_item', 'attachment' );

		foreach ( $post_types as $data ) {
			$post_type = $data->name;

			if ( in_array( $post_type, $exceptions ) ) {
				continue;
			}

			$module_manager->register_virtual_module(
				$post_type,
				array(
					'Name'            => $data->labels->name,
					'ModuleCategory'  => 'container',
					'ModuleContext'   => 'all',
					'ModuleClassName' => 'blcAnyPostContainerManager',
				)
			);
		}

		if ( ! WPMUDEV_BLC\App\Options\Settings\Model::instance()->get( 'use_legacy_blc_version' ) ) {
			if ( ! WPMUDEV_BLC\Core\Utils\Utilities::is_subsite() ) {
				return;
			}
			//return;
		}

		//These hooks update the synch & instance records when posts are added, deleted or modified.
		add_action( 'delete_post', array( &$this, 'post_deleted' ) );
		add_action( 'save_post', array( &$this, 'post_saved' ) );
		//We also treat post trashing/untrashing as delete/save.
		add_action( 'trashed_post', array( &$this, 'post_deleted' ) );
		add_action( 'untrash_post', array( &$this, 'post_saved' ) );

		//Highlight and nofollow broken links in posts & pages
		if ( $this->plugin_conf->options['mark_broken_links'] || $this->plugin_conf->options['nofollow_broken_links'] ) {
			add_filter( 'the_content', array( &$this, 'hook_the_content' ) );
			if ( $this->plugin_conf->options['mark_broken_links'] && ! empty( $this->plugin_conf->options['broken_link_css'] ) ) {
				add_action( 'wp_head', array( &$this, 'hook_wp_head' ) );
			}
		}
	}

	/**
	 * Retrieve an instance of the overlord class.
	 *
	 * @return blcPostTypeOverlord
	 */
	static function getInstance() {
		static $instance = null;
		if ( is_null( $instance ) ) {
			$instance = new blcPostTypeOverlord;
			$instance->init();
		}
		return $instance;
	}

	/**
	 * Notify the overlord that a post type is active.
	 *
	 * Called by individual instances of blcAnyPostContainerManager to let
	 * the overlord know that they've been created. Since a module instance
	 * is only created if the module is active, this event indicates that
	 * the user has enabled the corresponding post type for link checking.
	 *
	 * @param string $post_type
	 * @return void
	 */
	function post_type_enabled( $post_type ) {
		if ( ! in_array( $post_type, $this->enabled_post_types ) ) {
			$this->enabled_post_types[] = $post_type;
		}
	}

	/**
	* Remove the synch. record and link instances associated with a post when it's deleted
	*
	* @param int $post_id
	* @return void
	*/
	function post_deleted( $post_id ) {
		global $wpdb;

		$post_id = intval( $post_id );
		//Get the container type matching the type of the deleted post
		$post = get_post( $post_id );
		if ( ! $post ) {
			return;
		}
		//Get the associated container object
		$post_type      = get_post_type( $post );
		$post_container = blcContainerHelper::get_container( array( $post_type, $post_id ) );

		if ( $post_container ) {
			//Delete the container
			$post_container->delete();

			// Firstly: See if we have any current instances
			$q_current_instance_ids = $wpdb->prepare(
				'SELECT instance_id FROM `' . $wpdb->prefix . 'blc_instances` WHERE container_id = %d AND container_type = %s',
				$post_id,
				$post_type
			);

			$current_instance_ids_results = $wpdb->get_results( $q_current_instance_ids, ARRAY_A );

			if ( $wpdb->num_rows == 0 ) {
				// No current instances present, skip cleanup at once
				return;
			}

			$current_instance_ids = wp_list_pluck( $current_instance_ids_results, 'instance_id' );

			// Secondly: Get all link_ids used in our current instances
			$q_current_link_ids = 'SELECT DISTINCT link_id FROM `' . $wpdb->prefix . 'blc_instances` WHERE instance_id IN (\'' . implode( "', '", $current_instance_ids ) . '\')';

			$q_current_link_ids_results = $wpdb->get_results( $q_current_link_ids, ARRAY_A );

			$current_link_ids = wp_list_pluck( $q_current_link_ids_results, 'link_id' );

			// Go ahead and remove blc_instances for this container, blc_cleanup_links( $current_link_ids ) will find and remove any dangling links in the blc_links table
			$wpdb->query( 'DELETE FROM `' . $wpdb->prefix . 'blc_instances` WHERE instance_id IN (\'' . implode( "', '", $current_instance_ids ) . '\')' );

			//Clean up any dangling links
			blc_cleanup_links( $current_link_ids );
		}
	}

	/**
	 * When a post is saved or modified, mark it as unparsed.
	 *
	 * @param int $post_id
	 * @return void
	 */
	function post_saved( $post_id ) {
		//Get the container type matching the type of the deleted post
		$post = get_post( $post_id );
		if ( ! $post ) {
			return;
		}

		//Only check links in currently enabled post types
		if ( ! in_array( $post->post_type, $this->enabled_post_types ) ) {
			return;
		}

		//Only check posts that have one of the allowed statuses
		if ( ! in_array( $post->post_status, $this->enabled_post_statuses ) ) {
			return;
		}

		//Get the container & mark it as unparsed
		$args           = array( $post->post_type, intval( $post_id ) );
		$post_container = blcContainerHelper::get_container( $args );

		$post_container->mark_as_unsynched();
	}


	/**
	 * Create or update synchronization records for all posts.
	 *
	 * @param string $container_type
	 * @param bool $forced If true, assume that all synch. records are gone and will need to be recreated from scratch.
	 * @return void
	 */
	function resynch( $container_type = '', $forced = false ) {
		global $wpdb; /** @var wpdb $wpdb */
		global $blclog;

		//Resynch is expensive in terms of DB performance. Thus we only do it once, processing
		//all post types in one go and ignoring any further resynch requests during this pageload.
		//BUG: This might be a problem if there ever is an actual need to run resynch twice or
		//more per pageload.
		if ( $this->resynch_already_done ) {
			$blclog->log( sprintf( '...... Skipping "%s" resyncyh since all post types were already synched.', $container_type ) );
			return;
		}

		if ( empty( $this->enabled_post_types ) ) {
			$blclog->warn( sprintf( '...... Skipping "%s" resyncyh since no post types are enabled.', $container_type ) );
			return;
		}

		$escaped_post_types    = array_map( 'esc_sql', $this->enabled_post_types );
		$escaped_post_statuses = array_map( 'esc_sql', $this->enabled_post_statuses );

		if ( $forced ) {
			//Create new synchronization records for all posts.
			$blclog->log( '...... Creating synch records for these post types: ' . implode( ', ', $escaped_post_types ) . ' that have one of these statuses: ' . implode( ', ', $escaped_post_statuses ) );
			$start = microtime( true );
			$q     = "INSERT INTO {$wpdb->prefix}blc_synch(container_id, container_type, synched)
				  SELECT posts.id, posts.post_type, 0
				  FROM {$wpdb->posts} AS posts
				  WHERE
				  	posts.post_status IN (%s)
	 				AND posts.post_type IN (%s)";
			$q     = sprintf(
				$q,
				"'" . implode( "', '", $escaped_post_statuses ) . "'",
				"'" . implode( "', '", $escaped_post_types ) . "'"
			);
			$wpdb->query( $q );
			$blclog->log( sprintf( '...... %d rows inserted in %.3f seconds', $wpdb->rows_affected, microtime( true ) - $start ) );
		} else {
			//Delete synch records corresponding to posts that no longer exist.
			//Also delete posts that don't have enabled post status
			$blclog->log( '...... Deleting synch records for removed posts & post with invalid status' );
			$start            = microtime( true );
			$all_posts_id     = get_posts(
				array(
					'posts_per_page' => -1,
					'fields'         => 'ids',
					'post_type'      => $this->enabled_post_types,
					'post_status'    => $this->enabled_post_statuses,
				)
			);

			$q = "DELETE synch.* FROM {$wpdb->prefix}blc_synch AS synch WHERE synch.container_id NOT IN (%s)";

			$q     = sprintf(
				$q,
				"'" . implode( "', '", $all_posts_id ) . "'"
			);

			$wpdb->query( $q );
			$elapsed = microtime( true ) - $start;
			$blclog->debug( $q );
			$blclog->log( sprintf( '...... %d rows deleted in %.3f seconds', $wpdb->rows_affected, $elapsed ) );

			// //Delete records where the post status is not one of the enabled statuses.
			// $blclog->log( '...... Deleting synch records for posts that have a disallowed status' );
			// $start = microtime( true );
			// $all_posts_status     = get_posts(
			// 	array(
			// 		'posts_per_page' => -1,
			// 		'fields'         => 'ids',
			// 		'post_type'      => $this->enabled_post_types,
			// 		'post_status'    => $this->enabled_post_statuses,
			// 	)
			// );

			// $q     = "DELETE synch.*
			// 	  FROM
			// 		 {$wpdb->prefix}blc_synch AS synch
			// 	  WHERE
			// 		 posts.post_status NOT IN (%s)";
			// $q     = sprintf(
			// 	$q,
			// 	"'" . implode( "', '", $escaped_post_statuses ) . "'",
			// 	"'" . implode( "', '", wp_list_pluck( $all_posts, 'post_status' ) ) . "'",
			// );
			// $wpdb->query( $q );
			// $elapsed = microtime( true ) - $start;
			// $blclog->debug( $q );
			// $blclog->log( sprintf( '...... %d rows deleted in %.3f seconds', $wpdb->rows_affected, $elapsed ) );

			//Remove the 'synched' flag from all posts that have been updated
			//since the last time they were parsed/synchronized.
			$blclog->log( '...... Marking changed posts as unsynched' );
			$start = microtime( true );
			$q     = "UPDATE
					{$wpdb->prefix}blc_synch AS synch
					JOIN {$wpdb->posts} AS posts ON (synch.container_id = posts.ID and synch.container_type=posts.post_type)
				  SET
					synched = 0
				  WHERE
					synch.last_synch < posts.post_modified";
			$wpdb->query( $q );
			$elapsed = microtime( true ) - $start;
			$blclog->debug( $q );
			$blclog->log( sprintf( '...... %d rows updated in %.3f seconds', $wpdb->rows_affected, $elapsed ) );

			//Create synch. records for posts that don't have them.
			$blclog->log( '...... Creating synch records for new posts' );
			$start = microtime( true );
			$q     = "INSERT INTO {$wpdb->prefix}blc_synch(container_id, container_type, synched)
				  SELECT posts.id, posts.post_type, 0
				  FROM
				    {$wpdb->posts} AS posts LEFT JOIN {$wpdb->prefix}blc_synch AS synch
					ON (synch.container_id = posts.ID and synch.container_type=posts.post_type)
				  WHERE
				  	posts.post_status IN (%s)
	 				AND posts.post_type IN (%s)
					AND synch.container_id IS NULL";
			$q     = sprintf(
				$q,
				"'" . implode( "', '", $escaped_post_statuses ) . "'",
				"'" . implode( "', '", $escaped_post_types ) . "'"
			);
			$wpdb->query( $q );
			$elapsed = microtime( true ) - $start;
			$blclog->debug( $q );
			$blclog->log( sprintf( '...... %d rows inserted in %.3f seconds', $wpdb->rows_affected, $elapsed ) );
		}

		$this->resynch_already_done = true;
	}

	/**
	 * Hook for the 'the_content' filter. Scans the current post and adds the 'broken_link'
	 * CSS class to all links that are known to be broken. Currently works only on standard
	 * HTML links (i.e. the '<a href=...' kind).
	 *
	 * @param string $content Post content
	 * @return string Modified post content.
	 */
	function hook_the_content( $content ) {
		global $post, $wpdb; /** @var wpdb $wpdb */
		if ( empty( $post ) || ! in_array( $post->post_type, $this->enabled_post_types ) ) {
			return $content;
		}

		//Retrieve info about all occurrences of broken links in the current post
		$q     = "
			SELECT instances.raw_url
			FROM {$wpdb->prefix}blc_instances AS instances JOIN {$wpdb->prefix}blc_links AS links
				ON instances.link_id = links.link_id
			WHERE
				instances.container_type = %s
				AND instances.container_id = %d
				AND links.broken = 1
				AND parser_type = 'link'
		";
		$q     = $wpdb->prepare( $q, $post->post_type, $post->ID );
		$links = $wpdb->get_results( $q, ARRAY_A );

		//Return the content unmodified if there are no broken links in this post.
		if ( empty( $links ) || ! is_array( $links ) ) {
			return $content;
		}

		//Put the broken link URLs in an array
		$broken_link_urls = array();
		foreach ( $links as $link ) {
			$broken_link_urls[] = $link['raw_url'];
		}

		//Iterate over all HTML links and modify the broken ones
		$parser = blcParserHelper::get_parser( 'link' );
		if ( $parser ) {
			$content = $parser->multi_edit( $content, array( &$this, 'highlight_broken_link' ), $broken_link_urls );
		}

		return $content;
	}

	/**
	 * Analyse a link and add 'broken_link' CSS class if the link is broken.
	 *
	 * @see blcHtmlLink::multi_edit()
	 *
	 * @param array $link Associative array of link data.
	 * @param array $broken_link_urls List of broken link URLs present in the current post.
	 * @return array|string The modified link
	 */
	function highlight_broken_link( $link, $broken_link_urls ) {
		if ( ! in_array( $link['href'], $broken_link_urls ) ) {
			//Link not broken = return the original link tag
			return $link['#raw'];
		}

		//Add 'broken_link' to the 'class' attribute (unless already present).
		if ( $this->plugin_conf->options['mark_broken_links'] ) {
			if ( isset( $link['class'] ) ) {
				$classes = explode( ' ', $link['class'] );
				if ( ! in_array( 'broken_link', $classes ) ) {
					$classes[]     = 'broken_link';
					$link['class'] = implode( ' ', $classes );
				}
			} else {
				$link['class'] = 'broken_link';
			}
		}

		//Nofollow the link (unless it's already nofollow'ed)
		if ( $this->plugin_conf->options['nofollow_broken_links'] ) {
			if ( isset( $link['rel'] ) ) {
				$relations = explode( ' ', $link['rel'] );
				if ( ! in_array( 'nofollow', $relations ) ) {
					$relations[] = 'nofollow';
					$link['rel'] = implode( ' ', $relations );
				}
			} else {
				$link['rel'] = 'nofollow';
			}
		}

		return $link;
	}

	/**
	 * A hook for the 'wp_head' action. Outputs the user-defined broken link CSS.
	 *
	 * @return void
	 */
	function hook_wp_head() {
		echo '<style type="text/css">',$this->plugin_conf->options['broken_link_css'],'</style>';
	}
}

//Start up the post overlord
blcPostTypeOverlord::getInstance();


/**
 * Universal container item class used for all post types.
 *
 * @package Broken Link Checker
 * @author Janis Elsts
 * @access public
 */
class blcAnyPostContainer extends blcContainer {
	var $default_field = 'post_content';

	public $updating_urls = null;

	/**
	 * Get action links for this post.
	 *
	 * @param string $container_field Ignored.
	 * @return array of action link HTML.
	 */
	function ui_get_action_links( $container_field = '' ) {
		$actions = array();

		//Fetch the post (it should be cached already)
		$post = $this->get_wrapped_object();
		if ( ! $post ) {
			return $actions;
		}

		$post_type_object = get_post_type_object( $post->post_type );

		//Each post type can have its own cap requirements
		if ( current_user_can( $post_type_object->cap->edit_post, $this->container_id ) ) {
			$actions['edit'] = sprintf(
				'<span class="edit"><a href="%s" title="%s">%s</a>',
				$this->get_edit_url(),
				$post_type_object->labels->edit_item,
				__( 'Edit' )
			);

			//Trash/Delete link
			if ( current_user_can( $post_type_object->cap->delete_post, $this->container_id ) ) {
				if ( $this->can_be_trashed() ) {
					$actions['trash'] = sprintf(
						"<span class='trash'><a class='submitdelete' title='%s' href='%s'>%s</a>",
						esc_attr( __( 'Move this item to the Trash' ) ),
						esc_attr( get_delete_post_link( $this->container_id, '', false ) ),
						__( 'Trash' )
					);
				} else {
					$actions['delete'] = sprintf(
						"<span><a class='submitdelete' title='%s' href='%s'>%s</a>",
						esc_attr( __( 'Delete this item permanently' ) ),
						esc_attr( get_delete_post_link( $this->container_id, '', true ) ),
						__( 'Delete' )
					);
				}
			}
		}

		//View/Preview link
		$title = get_the_title( $this->container_id );
		if ( in_array( $post->post_status, array( 'pending', 'draft' ) ) ) {
			if ( current_user_can( $post_type_object->cap->edit_post, $this->container_id ) ) {
				$actions['view'] = sprintf(
					'<span class="view"><a href="%s" title="%s" rel="permalink">%s</a>',
					esc_url( add_query_arg( 'preview', 'true', get_permalink( $this->container_id ) ) ),
					esc_attr( sprintf( __( 'Preview &#8220;%s&#8221;' ), $title ) ),
					__( 'Preview' )
				);
			}
		} elseif ( 'trash' != $post->post_status ) {
			$actions['view'] = sprintf(
				'<span class="view"><a href="%s" title="%s" rel="permalink">%s</a>',
				esc_url( get_permalink( $this->container_id ) ),
				esc_attr( sprintf( __( 'View &#8220;%s&#8221;' ), $title ) ),
				__( 'View' )
			);
		}

		return $actions;
	}

	/**
	 * Get the HTML for displaying the post title in the "Source" column.
	 *
	 * @param string $container_field Ignored.
	 * @param string $context How to filter the output. Optional, defaults to 'display'.
	 * @return string HTML
	 */
	function ui_get_source( $container_field = '', $context = 'display' ) {
		$source = '<a class="row-title" href="%s" title="%s">%s</a>';
		$source = sprintf(
			$source,
			$this->get_edit_url(),
			esc_attr( __( 'Edit this item' ) ),
			get_the_title( $this->container_id )
		);

		return $source;
	}

	/**
	 * Get edit URL for this container. Returns the URL of the Dashboard page where the item
	 * associated with this container can be edited.
	 *
	 * @access protected
	 *
	 * @return string
	 */
	function get_edit_url() {
		/*
		The below is a near-exact copy of the get_post_edit_link() function.
		Unfortunately we can't just call that function because it has a hardcoded
		caps-check which fails when called from the email notification script
		executed by Cron.
		*/
		$post = $this->get_wrapped_object();
		if ( ! $post ) {
			return '';
		}

		$context = 'display';
		$action  = '&amp;action=edit';

		$post_type_object = get_post_type_object( $post->post_type );
		if ( ! $post_type_object ) {
			return '';
		}

		return apply_filters( 'get_edit_post_link', admin_url( sprintf( $post_type_object->_edit_link . $action, $post->ID ) ), $post->ID, $context );
	}

	/**
	 * Retrieve the post associated with this container.
	 *
	 * @access protected
	 *
	 * @param bool $ensure_consistency Set this to true to ignore the cached $wrapped_object value and retrieve an up-to-date copy of the wrapped object from the DB (or WP's internal cache).
	 * @return object Post data.
	 */
	function get_wrapped_object( $ensure_consistency = false ) {
		if ( $ensure_consistency || is_null( $this->wrapped_object ) ) {
			$this->wrapped_object = get_post( $this->container_id );
		}
		return $this->wrapped_object;
	}

	/**
	 * Update the post associated with this container.
	 *
	 * @access protected
	 *
	 * @return bool|WP_Error True on success, an error if something went wrong.
	 */
	function update_wrapped_object() {
		if ( is_null( $this->wrapped_object ) ) {
			return new WP_Error(
				'no_wrapped_object',
				__( 'Nothing to update', 'broken-link-checker' )
			);
		}

		$post                        = $this->wrapped_object;
		$post->blc_post_modified     = $post->post_modified;
		$post->blc_post_modified_gmt = $post->post_modified_gmt;
		$post_id                     = wp_update_post( $post, true );

		if ( is_wp_error( $post_id ) ) {
			return $post_id;
		} elseif ( $post_id == 0 ) {
			return new WP_Error(
				'update_failed',
				sprintf( __( 'Updating post %d failed', 'broken-link-checker' ), $this->container_id )
			);
		} else {
			$this->update_pagebuilders( $post_id );
			return true;
		}
	}

	/**
	 * Update the the links on pagebuilders
	 *
	 * @param int $post_id  Post ID of whose content to update.
	 */
	function update_pagebuilders( $post_id ) {

		if ( ! $post_id ) {
			return;
		}

		global $wpdb;
		//support for elementor page builder.
		if ( class_exists( '\Elementor\Plugin' ) && \Elementor\Plugin::$instance->db->is_built_with_elementor( $post_id ) ) {
			// @codingStandardsIgnoreStart cannot use `$wpdb->prepare` because it remove's the backslashes
			$rows_affected = $wpdb->query(
				"UPDATE {$wpdb->postmeta} " .
				"SET `meta_value` = REPLACE(`meta_value`, '" . str_replace( '/', '\\\/', $this->updating_urls['old_url'] ) . "', '" . str_replace( '/', '\\\/', $this->updating_urls['new_url'] ) . "') " .
				"WHERE `meta_key` = '_elementor_data' AND `post_id` = '" . $post_id . "' AND `meta_value` LIKE '[%' ;" ); // meta_value LIKE '[%' are json formatted
			// @codingStandardsIgnoreEnd
		}
	}

	/**
	 * Get the base URL of the container. For posts, the post permalink is used
	 * as the base URL when normalizing relative links.
	 *
	 * @return string
	 */
	function base_url() {
		return get_permalink( $this->container_id );
	}

	/**
	 * Delete or trash the post corresponding to this container.
	 * Will always move to trash instead of deleting if trash is enabled.
	 *
	 * @return bool|WP_error
	 */
	function delete_wrapped_object() {
		//Note that we don't need to delete the synch record and instances here -
		//wp_delete_post()/wp_trash_post() will run the post_delete/trash hook,
		//which will be caught by blcPostContainerManager, which will in turn
		//delete anything that needs to be deleted.
		if ( EMPTY_TRASH_DAYS ) {
			return $this->trash_wrapped_object();
		} else {
			if ( wp_delete_post( $this->container_id, true ) ) {
				return true;
			} else {
				return new WP_Error(
					'delete_failed',
					sprintf(
						__( 'Failed to delete post "%1$s" (%2$d)', 'broken-link-checker' ),
						get_the_title( $this->container_id ),
						$this->container_id
					)
				);
			}
		}
	}

	/**
	 * Move the post corresponding to this container to the Trash.
	 *
	 * @return bool|WP_Error
	 */
	function trash_wrapped_object() {
		if ( ! EMPTY_TRASH_DAYS ) {
			return new WP_Error(
				'trash_disabled',
				sprintf(
					__( 'Can\'t move post "%1$s" (%2$d) to the trash because the trash feature is disabled', 'broken-link-checker' ),
					get_the_title( $this->container_id ),
					$this->container_id
				)
			);
		}

		$post = get_post( $this->container_id );
		if ( $post->post_status == 'trash' ) {
			//Prevent conflicts between post and custom field containers trying to trash the same post.
			//BUG: Post and custom field containers shouldn't wrap the same object
			return true;
		}

		if ( wp_trash_post( $this->container_id ) ) {
			return true;
		} else {
			return new WP_Error(
				'trash_failed',
				sprintf(
					__( 'Failed to move post "%1$s" (%2$d) to the trash', 'broken-link-checker' ),
					get_the_title( $this->container_id ),
					$this->container_id
				)
			);
		}
	}

	/**
	 * Check if the current user can delete/trash this post.
	 *
	 * @return bool
	 */
	function current_user_can_delete() {
		$post             = $this->get_wrapped_object();
		$post_type_object = get_post_type_object( $post->post_type );
		return current_user_can( $post_type_object->cap->delete_post, $this->container_id );
	}

	function can_be_trashed() {
		return defined( 'EMPTY_TRASH_DAYS' ) && EMPTY_TRASH_DAYS;
	}
}

/**
 * Universal manager usable for most post types.
 *
 * @package Broken Link Checker
 * @access public
 */
class blcAnyPostContainerManager extends blcContainerManager {
	var $container_class_name = 'blcAnyPostContainer';
	var $fields               = array( 'post_content' => 'html', 'post_excerpt' => 'html' );

	function init() {
		parent::init();

		//Notify the overlord that the post/container type that this instance is
		//responsible for is enabled.
		$overlord = blcPostTypeOverlord::getInstance();
		$overlord->post_type_enabled( $this->container_type );
	}

	/**
	 * Instantiate multiple containers of the container type managed by this class.
	 *
	 * @param array $containers Array of assoc. arrays containing container data.
	 * @param string $purpose An optional code indicating how the retrieved containers will be used.
	 * @param bool $load_wrapped_objects Preload wrapped objects regardless of purpose.
	 *
	 * @return array of blcPostContainer indexed by "container_type|container_id"
	 */
	function get_containers( $containers, $purpose = '', $load_wrapped_objects = false ) {
		global $blclog;
		$containers = $this->make_containers( $containers );

		//Preload post data if it is likely to be useful later
		$preload = $load_wrapped_objects || in_array( $purpose, array( BLC_FOR_DISPLAY, BLC_FOR_PARSING ) );
		if ( $preload ) {
			$post_ids = array();
			foreach ( $containers as $container ) {
				$post_ids[] = $container->container_id;
			}

			$args  = array( 'include' => implode( ',', $post_ids ) );
			$posts = get_posts( $args );

			foreach ( $posts as $post ) {
				$key = $this->container_type . '|' . $post->ID;
				if ( isset( $containers[ $key ] ) ) {
					$containers[ $key ]->wrapped_object = $post;
				}
			}
		}

		return $containers;
	}

	/**
	 * Create or update synchronization records for all posts.
	 *
	 * @param bool $forced If true, assume that all synch. records are gone and will need to be recreated from scratch.
	 * @return void
	 */
	function resynch( $forced = false ) {
		$overlord = blcPostTypeOverlord::getInstance();
		$overlord->resynch( $this->container_type, $forced );
	}

	/**
	 * Get the message to display after $n posts have been deleted.
	 *
	 * @param int $n Number of deleted posts.
	 * @return string A delete confirmation message, e.g. "5 posts were moved deleted"
	 */
	function ui_bulk_delete_message( $n ) {
		//Since the "Trash" feature has been introduced, calling wp_delete_post
		//doesn't actually delete the post (unless you set force_delete to True),
		//just moves it to the trash. So we pick the message accordingly.
		//(If possible, BLC *always* moves to trash instead of deleting permanently.)
		if ( function_exists( 'wp_trash_post' ) && EMPTY_TRASH_DAYS ) {
			return blcAnyPostContainerManager::ui_bulk_trash_message( $n );
		} else {
			$post_type_object = get_post_type_object( $this->container_type );
			$type_name        = '';

			if ( $this->container_type == 'post' || is_null( $post_type_object ) ) {
				$delete_msg = _n( '%d post deleted.', '%d posts deleted.', $n, 'broken-link-checker' );
			} elseif ( $this->container_type == 'page' ) {
				$delete_msg = _n( '%d page deleted.', '%d pages deleted.', $n, 'broken-link-checker' );
			} else {
				$delete_msg = _n( '%1$d "%2$s" deleted.', '%1$d "%2$s" deleted.', $n, 'broken-link-checker' );
				$type_name  = ( $n == 1 ? $post_type_object->labels->singular_name : $post_type_object->labels->name );
			}
			return sprintf( $delete_msg, $n, $type_name );
		}
	}


	/**
	 * Get the message to display after $n posts have been trashed.
	 *
	 * @param int $n Number of deleted posts.
	 * @return string A confirmation message, e.g. "5 posts were moved to trash"
	 */
	function ui_bulk_trash_message( $n ) {
		$post_type_object = get_post_type_object( $this->container_type );
		$type_name        = '';

		if ( $this->container_type == 'post' || is_null( $post_type_object ) ) {
			$delete_msg = _n( '%d post moved to the Trash.', '%d posts moved to the Trash.', $n, 'broken-link-checker' );
		} elseif ( $this->container_type == 'page' ) {
			$delete_msg = _n( '%d page moved to the Trash.', '%d pages moved to the Trash.', $n, 'broken-link-checker' );
		} else {
			$delete_msg = _n( '%1$d "%2$s" moved to the Trash.', '%1$d "%2$s" moved to the Trash.', $n, 'broken-link-checker' );
			$type_name  = ( $n == 1 ? $post_type_object->labels->singular_name : $post_type_object->labels->name );
		}
		return sprintf( $delete_msg, $n, $type_name );
	}
}