insert_with_markers( string $filename, string $marker, array|string $insertion ): bool

Inserts an array of strings into a file (.htaccess), placing it between BEGIN and END markers.

Description

Replaces existing marked info. Retains surrounding data. Creates file if none exists.

Parameters

$filenamestringrequired
Filename to alter.
$markerstringrequired
The marker to alter.
$insertionarray|stringrequired
The new content to insert.

Return

bool True on write success, false on failure.

Source

function insert_with_markers( $filename, $marker, $insertion ) {
	if ( ! file_exists( $filename ) ) {
		if ( ! is_writable( dirname( $filename ) ) ) {
			return false;
		}

		if ( ! touch( $filename ) ) {
			return false;
		}

		// Make sure the file is created with a minimum set of permissions.
		$perms = fileperms( $filename );

		if ( $perms ) {
			chmod( $filename, $perms | 0644 );
		}
	} elseif ( ! is_writable( $filename ) ) {
		return false;
	}

	if ( ! is_array( $insertion ) ) {
		$insertion = explode( "\n", $insertion );
	}

	$switched_locale = switch_to_locale( get_locale() );

	$instructions = sprintf(
		/* translators: 1: Marker. */
		__(
			'The directives (lines) between "BEGIN %1$s" and "END %1$s" are
dynamically generated, and should only be modified via WordPress filters.
Any changes to the directives between these markers will be overwritten.'
		),
		$marker
	);

	$instructions = explode( "\n", $instructions );

	foreach ( $instructions as $line => $text ) {
		$instructions[ $line ] = '# ' . $text;
	}

	/**
	 * Filters the inline instructions inserted before the dynamically generated content.
	 *
	 * @since 5.3.0
	 *
	 * @param string[] $instructions Array of lines with inline instructions.
	 * @param string   $marker       The marker being inserted.
	 */
	$instructions = apply_filters( 'insert_with_markers_inline_instructions', $instructions, $marker );

	if ( $switched_locale ) {
		restore_previous_locale();
	}

	$insertion = array_merge( $instructions, $insertion );

	$start_marker = "# BEGIN {$marker}";
	$end_marker   = "# END {$marker}";

	$fp = fopen( $filename, 'r+' );

	if ( ! $fp ) {
		return false;
	}

	// Attempt to get a lock. If the filesystem supports locking, this will block until the lock is acquired.
	flock( $fp, LOCK_EX );

	$lines = array();

	while ( ! feof( $fp ) ) {
		$lines[] = rtrim( fgets( $fp ), "\r\n" );
	}

	// Split out the existing file into the preceding lines, and those that appear after the marker.
	$pre_lines        = array();
	$post_lines       = array();
	$existing_lines   = array();
	$found_marker     = false;
	$found_end_marker = false;

	foreach ( $lines as $line ) {
		if ( ! $found_marker && str_contains( $line, $start_marker ) ) {
			$found_marker = true;
			continue;
		} elseif ( ! $found_end_marker && str_contains( $line, $end_marker ) ) {
			$found_end_marker = true;
			continue;
		}

		if ( ! $found_marker ) {
			$pre_lines[] = $line;
		} elseif ( $found_marker && $found_end_marker ) {
			$post_lines[] = $line;
		} else {
			$existing_lines[] = $line;
		}
	}

	// Check to see if there was a change.
	if ( $existing_lines === $insertion ) {
		flock( $fp, LOCK_UN );
		fclose( $fp );

		return true;
	}

	// Generate the new file data.
	$new_file_data = implode(
		"\n",
		array_merge(
			$pre_lines,
			array( $start_marker ),
			$insertion,
			array( $end_marker ),
			$post_lines
		)
	);

	// Write to the start of the file, and truncate it to that length.
	fseek( $fp, 0 );
	$bytes = fwrite( $fp, $new_file_data );

	if ( $bytes ) {
		ftruncate( $fp, ftell( $fp ) );
	}

	fflush( $fp );
	flock( $fp, LOCK_UN );
	fclose( $fp );

	return (bool) $bytes;
}

Hooks

apply_filters( ‘insert_with_markers_inline_instructions’, string[] $instructions, string $marker )

Filters the inline instructions inserted before the dynamically generated content.

Changelog

VersionDescription
1.5.0Introduced.

User Contributed Notes

  1. Skip to note 2 content

    I don’t think this is classed as a bug but worth noting.

    When splitting out the htaccess file, the code searches for: $start_marker = "# BEGIN {$marker}"; and will find the first instance of it in the file, regardless of where it is and even if this is in a commented line before the actual WordPress section.

    For example, if I add the code suggested by Wordfence to solve an issue with Wordfence scans and updates and LiteSpeed, and I want to add a comment to explain what this code does and where it needs to go (ie before the WordPress section), I may add:

    # Add fix to stop Litespeed aborting Wordfence scans and updates
    # https://www.wordfence.com/help/advanced/system-requirements/litespeed/
    # Insert just before # BEGIN WordPress
    ... Wordfence noabort code...

    The # BEGIN WordPress in this comment is found first by insert_with_markers and the new WordPress section is appended to the next line, overwriting the Wordfence noabort code.

    The workaround is not to have # BEGIN WordPress in a comment before the WordPress section, but ideally the function would be altered so that it only matches # BEGIN WordPress at the start of a line.

You must log in before being able to contribute a note or feedback.