Skip to content

Commit 9934c9e

Browse files
committed
Collaboration: Add collaboration_storage filter for pluggable storage backends
Allow hosts and plugins to replace the default database-backed storage with a custom WP_Collaboration_Storage implementation via mu-plugin. Combined with the existing sync.providers JS filter, hosts can now provide WebSocket-based collaboration without patching core. Includes an in-memory storage implementation and integration test verifying the filter correctly replaces the backend.
1 parent faa9926 commit 9934c9e

File tree

2 files changed

+174
-1
lines changed

2 files changed

+174
-1
lines changed

‎src/wp-includes/rest-api.php‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -431,7 +431,18 @@ function create_initial_rest_routes() {
431431

432432
// Collaboration.
433433
if ( wp_is_collaboration_enabled() ) {
434-
$collaboration_storage = new WP_Collaboration_Table_Storage();
434+
/**
435+
* Filters the storage backend used for collaborative editing.
436+
*
437+
* Allows hosts and plugins to replace the default database-backed
438+
* storage with a custom implementation (e.g. Redis, Memcached).
439+
* The returned object must implement WP_Collaboration_Storage.
440+
*
441+
* @since 7.0.0
442+
*
443+
* @param WP_Collaboration_Storage $storage Storage backend instance.
444+
*/
445+
$collaboration_storage = apply_filters( 'collaboration_storage', new WP_Collaboration_Table_Storage() );
435446
$collaboration_server = new WP_HTTP_Polling_Collaboration_Server( $collaboration_storage );
436447
$collaboration_server->register_routes();
437448
}

‎tests/phpunit/tests/rest-api/rest-collaboration-server.php‎

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1779,4 +1779,166 @@ public function test_collaboration_idle_poll_query_count(): void {
17791779
sprintf( 'Idle poll should use at most 3 queries per room, used %d.', $query_count )
17801780
);
17811781
}
1782+
1783+
/**
1784+
* Verifies that the collaboration_storage filter allows replacing the
1785+
* default storage backend with a custom implementation.
1786+
*
1787+
* @ticket 64696
1788+
*/
1789+
public function test_collaboration_storage_filter_replaces_backend() {
1790+
wp_set_current_user( self::$editor_id );
1791+
1792+
$custom_storage = new WP_Test_In_Memory_Collaboration_Storage();
1793+
1794+
add_filter(
1795+
'collaboration_storage',
1796+
static function () use ( $custom_storage ) {
1797+
return $custom_storage;
1798+
}
1799+
);
1800+
1801+
// Reset the REST server so routes are re-registered with the filtered storage.
1802+
global $wp_rest_server;
1803+
$wp_rest_server = null;
1804+
rest_get_server();
1805+
1806+
$room = $this->get_post_room();
1807+
$update = array(
1808+
'type' => 'update',
1809+
'data' => base64_encode( 'custom-storage-test' ),
1810+
);
1811+
1812+
// Client 1 sends an update.
1813+
$response = $this->dispatch_collaboration(
1814+
array(
1815+
$this->build_room( $room, 1, 0, array( 'user' => 'c1' ), array( $update ) ),
1816+
)
1817+
);
1818+
1819+
$this->assertSame( 200, $response->get_status(), 'Custom storage should handle updates.' );
1820+
1821+
// Client 2 polls and should receive client 1's update.
1822+
$response = $this->dispatch_collaboration(
1823+
array(
1824+
$this->build_room( $room, 2, 0, array( 'user' => 'c2' ) ),
1825+
)
1826+
);
1827+
1828+
$data = $response->get_data();
1829+
$update_data = wp_list_pluck( $data['rooms'][0]['updates'], 'data' );
1830+
1831+
$this->assertContains(
1832+
base64_encode( 'custom-storage-test' ),
1833+
$update_data,
1834+
'Client 2 should receive the update stored by the custom backend.'
1835+
);
1836+
1837+
$this->assertTrue( $custom_storage->was_used, 'The custom storage backend should have been called.' );
1838+
1839+
remove_all_filters( 'collaboration_storage' );
1840+
}
1841+
}
1842+
1843+
/**
1844+
* In-memory collaboration storage for testing the collaboration_storage filter.
1845+
*
1846+
* Stores all data in arrays with no database interaction.
1847+
*/
1848+
class WP_Test_In_Memory_Collaboration_Storage implements WP_Collaboration_Storage {
1849+
public $was_used = false;
1850+
1851+
private $updates = array();
1852+
private $awareness = array();
1853+
private $next_id = 1;
1854+
private $cursors = array();
1855+
private $counts = array();
1856+
1857+
public function add_update( string $room, $update ): bool {
1858+
$this->was_used = true;
1859+
$this->updates[ $room ][] = array(
1860+
'id' => $this->next_id++,
1861+
'update' => $update,
1862+
);
1863+
return true;
1864+
}
1865+
1866+
public function get_awareness_state( string $room, int $timeout = 30 ): array {
1867+
$this->was_used = true;
1868+
$cutoff = time() - $timeout;
1869+
$entries = array();
1870+
foreach ( $this->awareness[ $room ] ?? array() as $entry ) {
1871+
if ( $entry['time'] >= $cutoff ) {
1872+
$entries[] = array(
1873+
'client_id' => $entry['client_id'],
1874+
'state' => $entry['state'],
1875+
'wp_user_id' => $entry['wp_user_id'],
1876+
);
1877+
}
1878+
}
1879+
return $entries;
1880+
}
1881+
1882+
public function get_cursor( string $room ): int {
1883+
return $this->cursors[ $room ] ?? 0;
1884+
}
1885+
1886+
public function get_update_count( string $room ): int {
1887+
return $this->counts[ $room ] ?? 0;
1888+
}
1889+
1890+
public function get_updates_after_cursor( string $room, int $cursor ): array {
1891+
$this->was_used = true;
1892+
$updates = array();
1893+
$max_id = 0;
1894+
$total = count( $this->updates[ $room ] ?? array() );
1895+
1896+
foreach ( $this->updates[ $room ] ?? array() as $entry ) {
1897+
if ( $entry['id'] > $max_id ) {
1898+
$max_id = $entry['id'];
1899+
}
1900+
if ( $entry['id'] > $cursor ) {
1901+
$updates[] = $entry['update'];
1902+
}
1903+
}
1904+
1905+
$this->cursors[ $room ] = $max_id;
1906+
$this->counts[ $room ] = $total;
1907+
return $updates;
1908+
}
1909+
1910+
public function remove_updates_before_cursor( string $room, int $cursor ): bool {
1911+
if ( ! isset( $this->updates[ $room ] ) ) {
1912+
return true;
1913+
}
1914+
$this->updates[ $room ] = array_values(
1915+
array_filter(
1916+
$this->updates[ $room ],
1917+
static function ( $entry ) use ( $cursor ) {
1918+
return $entry['id'] > $cursor;
1919+
}
1920+
)
1921+
);
1922+
return true;
1923+
}
1924+
1925+
public function set_awareness_state( string $room, int $client_id, array $state, int $wp_user_id ): bool {
1926+
$this->was_used = true;
1927+
// Remove existing entry for this client.
1928+
$this->awareness[ $room ] = array_values(
1929+
array_filter(
1930+
$this->awareness[ $room ] ?? array(),
1931+
static function ( $entry ) use ( $client_id ) {
1932+
return $entry['client_id'] !== $client_id;
1933+
}
1934+
)
1935+
);
1936+
$this->awareness[ $room ][] = array(
1937+
'client_id' => $client_id,
1938+
'state' => $state,
1939+
'wp_user_id' => $wp_user_id,
1940+
'time' => time(),
1941+
);
1942+
return true;
1943+
}
17821944
}

0 commit comments

Comments
 (0)