mirror of
https://github.com/Heretek-AI/comic-easel.git
synced 2026-10-12 02:03:28 +00:00
The save handler took a value that was new against an empty stored one to mean no row existed, and reached for add_post_meta() with $unique set. When a row does exist holding the empty string, that add is refused and nothing is written: the author's text disappears on save with no error. Empty rows are ordinary -- the comic post type supports custom-fields, and importers and wp-cli leave them behind too. update_post_meta() creates the row when it is absent, so the two branches are one. The delete-when-blank branch is untouched. The add_post_meta() stub now models the unique refusal, which is what lets a test tell an absent row from an empty one: get_post_meta() reports '' for both. Co-Authored-By: Claude Fable 5 <[email protected]>
161 lines
5.5 KiB
PHP
161 lines
5.5 KiB
PHP
<?php
|
|
|
|
use PHPUnit\Framework\Attributes\DataProvider;
|
|
|
|
/**
|
|
* ceo_meta_for_editor() — functions/admin-meta.php
|
|
*
|
|
* Normalises a stored comic meta value before it is re-displayed in the post editor. Comic
|
|
* meta arrives in two shapes -- entity-encoded when written through the plugin's meta boxes,
|
|
* which escape on save, and raw when written through WordPress's Custom Fields panel, which
|
|
* does not. Decoding first is what makes a single escape at the output sink correct for both.
|
|
*/
|
|
class MetaForEditorTest extends CE_TestCase {
|
|
|
|
protected function setUp(): void {
|
|
parent::setUp();
|
|
self::loadPluginFile( 'functions/admin-meta.php' );
|
|
}
|
|
|
|
/**
|
|
* The property the meta boxes depend on: a value stored the way the save handler stores
|
|
* it, then normalised and re-escaped for a textarea, must come back byte-identical. If
|
|
* this fails, every existing comic shows mangled text in the editor.
|
|
*/
|
|
#[DataProvider( 'authoredTextProvider' )]
|
|
public function testTextareaRoundTripIsLossless( $typed ) {
|
|
$stored = esc_textarea( $typed );
|
|
$this->assertSame( $stored, esc_textarea( ceo_meta_for_editor( $stored ) ) );
|
|
}
|
|
|
|
/** And the value the author actually typed is what they see again. */
|
|
#[DataProvider( 'authoredTextProvider' )]
|
|
public function testTheAuthorSeesWhatTheyTyped( $typed ) {
|
|
$this->assertSame( $typed, ceo_meta_for_editor( esc_textarea( $typed ) ) );
|
|
}
|
|
|
|
public static function authoredTextProvider() {
|
|
return array(
|
|
'markup' => array( '<b>bold</b>' ),
|
|
'an entity typed literally' => array( '<b>' ),
|
|
'a bare ampersand' => array( 'Tom & Jerry' ),
|
|
'quotes' => array( 'she said "hi"' ),
|
|
'an apostrophe' => array( "it's fine" ),
|
|
'a url with a query string' => array( 'https://x.test/?a=1&b=2' ),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* A raw value written past the meta boxes must not survive as live markup once the sink
|
|
* escapes it. This is the Custom Fields path.
|
|
*/
|
|
public function testRawMarkupFromCustomFieldsBecomesInert() {
|
|
$raw = '</textarea><script>alert(1)</script>';
|
|
$out = esc_textarea( ceo_meta_for_editor( $raw ) );
|
|
$this->assertStringNotContainsString( '</textarea>', $out );
|
|
$this->assertStringNotContainsString( '<script', $out );
|
|
}
|
|
|
|
public function testAttributeBreakingValueBecomesInert() {
|
|
$out = esc_attr( ceo_meta_for_editor( '" autofocus onfocus="alert(1)' ) );
|
|
$this->assertStringNotContainsString( '"', str_replace( '"', '', $out ) );
|
|
}
|
|
|
|
#[DataProvider( 'decodingProvider' )]
|
|
public function testKnownEntitiesAreDecodedOnce( $stored, $expected ) {
|
|
$this->assertSame( $expected, ceo_meta_for_editor( $stored ) );
|
|
}
|
|
|
|
public static function decodingProvider() {
|
|
return array(
|
|
array( '<b>', '<b>' ),
|
|
array( '&amp;', '&' ),
|
|
array( '"x"', '"x"' ),
|
|
array( ''', "'" ),
|
|
array( '<b>', '<b>' ),
|
|
array( '¬anentity;', '¬anentity;' ),
|
|
);
|
|
}
|
|
|
|
#[DataProvider( 'nonStringProvider' )]
|
|
public function testNonStringInputIsHandled( $input, $expected ) {
|
|
$this->assertSame( $expected, ceo_meta_for_editor( $input ) );
|
|
}
|
|
|
|
public static function nonStringProvider() {
|
|
return array(
|
|
array( null, '' ),
|
|
array( false, '' ),
|
|
array( 123, '123' ),
|
|
);
|
|
}
|
|
|
|
public function testSaveHandlerUnslashesEditorTextExactlyOnce() {
|
|
$post = $this->setGlobalPost( 17 );
|
|
$this->grantCurrentUserCap( 'edit_post' );
|
|
CE_Test_State::$valid_nonces['admin-meta.php'] = 'valid-nonce';
|
|
$typed = "C:\\comics\\today's <b>note</b>";
|
|
$_POST = array(
|
|
'comic_nonce' => 'valid-nonce',
|
|
'transcript' => addslashes( $typed ),
|
|
);
|
|
|
|
ceo_handle_edit_save_comic( 17, $post );
|
|
|
|
$this->assertSame( esc_textarea( $typed ), CE_Test_State::$post_meta['17:transcript'] );
|
|
unset( $_POST );
|
|
}
|
|
|
|
public function testSaveHandlerRejectsAnInvalidNonce() {
|
|
$post = $this->setGlobalPost( 17 );
|
|
$this->grantCurrentUserCap( 'edit_post' );
|
|
$_POST = array(
|
|
'comic_nonce' => 'invalid',
|
|
'transcript' => 'must not be saved',
|
|
);
|
|
|
|
ceo_handle_edit_save_comic( 17, $post );
|
|
|
|
$this->assertArrayNotHasKey( '17:transcript', CE_Test_State::$post_meta );
|
|
unset( $_POST );
|
|
}
|
|
|
|
/**
|
|
* A meta row can exist while holding the empty string -- the Custom Fields panel, an
|
|
* importer or wp-cli all leave one behind. get_post_meta() reports '' for that row and for
|
|
* no row at all, so a save handler that branches on '' and reaches for a unique
|
|
* add_post_meta() has its write refused and drops what the author typed.
|
|
*/
|
|
public function testSaveHandlerWritesOverAnExistingEmptyMetaRow() {
|
|
$post = $this->setGlobalPost( 17 );
|
|
$this->grantCurrentUserCap( 'edit_post' );
|
|
CE_Test_State::$valid_nonces['admin-meta.php'] = 'valid-nonce';
|
|
$this->setPostMeta( 17, 'transcript', '' );
|
|
$_POST = array(
|
|
'comic_nonce' => 'valid-nonce',
|
|
'transcript' => 'the newly typed transcript',
|
|
);
|
|
|
|
ceo_handle_edit_save_comic( 17, $post );
|
|
|
|
$this->assertSame( 'the newly typed transcript', CE_Test_State::$post_meta['17:transcript'] );
|
|
unset( $_POST );
|
|
}
|
|
|
|
public function testSaveHandlerIgnoresArrayShapedMetaValues() {
|
|
$post = $this->setGlobalPost( 17 );
|
|
$this->grantCurrentUserCap( 'edit_post' );
|
|
CE_Test_State::$valid_nonces['admin-meta.php'] = 'valid-nonce';
|
|
$this->setPostMeta( 17, 'transcript', 'existing transcript' );
|
|
$_POST = array(
|
|
'comic_nonce' => 'valid-nonce',
|
|
'transcript' => array( 'unexpected value' ),
|
|
);
|
|
|
|
ceo_handle_edit_save_comic( 17, $post );
|
|
|
|
$this->assertSame( 'existing transcript', CE_Test_State::$post_meta['17:transcript'] );
|
|
unset( $_POST );
|
|
}
|
|
}
|