diff --git a/CHANGELOG.md b/CHANGELOG.md index cc461fd0..ad33e6b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Fixed + +- If a post has an author that no longer exists, the authorship of that post is re-allocated to the user with ID matching the `archive_author` option (if that options exists) + ## [6.0.2] - 2024-12-13 ### Fixed diff --git a/app/Theme/FixNonExistentAuthors.php b/app/Theme/FixNonExistentAuthors.php new file mode 100644 index 00000000..6e9f16aa --- /dev/null +++ b/app/Theme/FixNonExistentAuthors.php @@ -0,0 +1,51 @@ +ID); + if ($post_author_id > 1) { + $post_author = get_user_by('id', $post_author_id); + if (!($post_author)) { + error_log("author of post {$post->ID} is deleted user $post_author_id", 0); + add_filter('wp_insert_post_data', [$this, 'setArchiveAuthor'], 99, 2); + wp_update_post($post); + } + } + } + + public function setArchiveAuthor($postData, $postArray) + { + $fix_types = ["page", "post", "attachment"]; + + if (!in_array($postData['post_type'], $fix_types)) { + return $postData; + } + + $archive_user_option = get_network_option(null, 'archive_author'); + + if (!empty($archive_user_option)) { + if (is_int($archive_user_option)) { + $archive_author_id = $archive_user_option; + } elseif (is_string($archive_user_option) && ctype_digit($archive_user_option)) { + $archive_author_id = intval($archive_user_option); + } + if (!empty($archive_author_id)) { + $postData['post_author'] = $archive_author_id; + if (taxonomy_exists('author')) { + wp_delete_object_term_relationships($postArray['ID'], 'author'); + } + } + } + return $postData; + } +} diff --git a/app/di.php b/app/di.php index bee76543..dcc9d9a2 100644 --- a/app/di.php +++ b/app/di.php @@ -26,3 +26,4 @@ )); $registrar->addInstance(new \GovUKBlogs\Theme\ThemeSetup()); $registrar->addInstance(new \GovUKBlogs\Theme\OldRootsCleanup()); +$registrar->addInstance(new \GovUKBlogs\Theme\FixNonExistentAuthors()); diff --git a/lib/byline.php b/lib/byline.php index 4d180621..8b5a59d3 100644 --- a/lib/byline.php +++ b/lib/byline.php @@ -3,6 +3,7 @@ # Display the author for a post, using coauthors if available and falling back to the WordPress user if not. function gds_byline() { + do_action('gds_byline'); ?> Posted by: fixNonExistentAuthors = new \GovUKBlogs\Theme\FixNonExistentAuthors(); + }); + + it('implements the Registerable interface', function () { + expect($this->fixNonExistentAuthors)->toBeAnInstanceOf(\Dxw\Iguana\Registerable::class); + }); + + describe('->register()', function () { + it('adds the action', function () { + allow('add_action')->toBeCalled(); + expect('add_action')->toBeCalled()->once()->with('gds_byline', [$this->fixNonExistentAuthors, 'replaceAbsentAuthor']); + $this->fixNonExistentAuthors->register(); + }); + }); + + describe('->replaceAbsentAuthor()', function () { + context('the post author ID is 1', function () { + it('does nothing', function () { + global $post; + $post = (object) [ + "ID" => 123 + ]; + allow('get_post_field')->toBeCalled()->andReturn(1); + expect('wp_update_post')->not->toBeCalled(); + + $this->fixNonExistentAuthors->replaceAbsentAuthor(); + }); + }); + context('the post author ID is greater than 1', function () { + context('but the user exists', function () { + it('does nothing', function () { + global $post; + $post = (object) [ + "ID" => 123 + ]; + allow('get_post_field')->toBeCalled()->andReturn(2); + allow('get_user_by')->toBeCalled()->andReturn((object) [ + "ID" => 2, + "user_login" => "valid_user" + ]); + expect('wp_update_post')->not->toBeCalled(); + + $this->fixNonExistentAuthors->replaceAbsentAuthor(); + }); + }); + context('and the user does not exist', function () { + it('adds the filter and calls update_post', function () { + global $post; + $post = (object) [ + "ID" => 123 + ]; + allow('get_post_field')->toBeCalled()->andReturn(2); + allow('get_user_by')->toBeCalled()->andReturn(false); + allow('add_filter')->toBeCalled(); + allow('error_log')->toBeCalled(); + expect('error_log')->toBeCalled()->once()->with('author of post 123 is deleted user 2', 0); + expect('add_filter')->toBeCalled()->once()->with('wp_insert_post_data', [$this->fixNonExistentAuthors, 'setArchiveAuthor'], 99, 2); + allow('wp_update_post')->toBeCalled(); + expect('wp_update_post')->toBeCalled()->once(); + + $this->fixNonExistentAuthors->replaceAbsentAuthor(); + }); + }); + }); + }); + + describe('->setArchiveAuthor()', function () { + context('the post is not one of the types we want to fix', function () { + it('returns the post data unamended', function () { + $postData = [ + 'post_type' => 'custom_post_type', + 'post_author' => 123 + ]; + expect('get_network_option')->not->toBeCalled(); + + $result = $this->fixNonExistentAuthors->setArchiveAuthor($postData, []); + + expect($result)->toEqual($postData); + }); + }); + context('the post is one of the type we want to fix', function () { + beforeEach(function () { + $this->postData = [ + 'post_type' => 'post', + 'post_author' => 123 + ]; + }); + context('but the archive_author option is not set', function () { + it('returns the post data unamended', function () { + allow('get_network_option')->toBeCalled()->andReturn(false); + + $result = $this->fixNonExistentAuthors->setArchiveAuthor($this->postData, []); + + expect($result)->toEqual($this->postData); + }); + }); + context('and the archive_author option is an integer', function () { + it('amends the post data to set the post_author to the archive_author value', function () { + allow('get_network_option')->toBeCalled()->andReturn(456); + allow('taxonomy_exists')->toBeCalled()->andReturn(false); + + $result = $this->fixNonExistentAuthors->setArchiveAuthor($this->postData, []); + + expect($result)->toEqual([ + 'post_type' => 'post', + 'post_author' => 456 + ]); + }); + }); + context('and the archive_author option is a string containing only an integer', function () { + it('amends the post data to set the post_author to the archive_author value', function () { + allow('get_network_option')->toBeCalled()->andReturn('456'); + allow('taxonomy_exists')->toBeCalled()->andReturn(false); + + $result = $this->fixNonExistentAuthors->setArchiveAuthor($this->postData, []); + + expect($result)->toEqual([ + 'post_type' => 'post', + 'post_author' => 456 + ]); + }); + }); + context('and the archive_author option is a string containing non-numeric characters', function () { + it('returns the post data unamended', function () { + allow('get_network_option')->toBeCalled()->andReturn('456foo'); + + $result = $this->fixNonExistentAuthors->setArchiveAuthor($this->postData, []); + + expect($result)->toEqual($this->postData); + }); + }); + context('and the "author" taxonomy exists, indicating that co-authors is in use', function () { + it('removes any existing co-author relationships with this post, as well as amending the author ID', function () { + allow('get_network_option')->toBeCalled()->andReturn(456); + allow('taxonomy_exists')->toBeCalled()->andReturn(true); + allow('wp_delete_object_term_relationships')->toBeCalled(); + expect('wp_delete_object_term_relationships')->toBeCalled()->once()->with(123, 'author'); + + $result = $this->fixNonExistentAuthors->setArchiveAuthor($this->postData, ['ID' => 123]); + + expect($result)->toEqual([ + 'post_type' => 'post', + 'post_author' => 456 + ]); + }); + }); + }); + }); +});