Editor: Introduce new API method that register block from block.json
metadata file
Backports changes added to Gutenberg in: - https://github.com/WordPress/gutenberg/pull/20794 - https://github.com/WordPress/gutenberg/pull/22519 `register_block_type_from_metadata` function is going to be used to register all blocks on the server using `block.json` metadata files. Props ocean90, azaozz, aduth, mcsf, jorgefilipecosta, spacedmonkey, nosolosw, swissspidy and noahtallen. Fixes #50263. git-svn-id: https://develop.svn.wordpress.org/trunk@48141 602fd350-edb4-49c9-b593-d223f7449a82
This commit is contained in:
parent
6408e197bd
commit
5f6ab44340
@ -40,6 +40,217 @@ function unregister_block_type( $name ) {
|
|||||||
return WP_Block_Type_Registry::get_instance()->unregister( $name );
|
return WP_Block_Type_Registry::get_instance()->unregister( $name );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the block asset's path prefix if provided.
|
||||||
|
*
|
||||||
|
* @since 5.5.0
|
||||||
|
*
|
||||||
|
* @param string $asset_handle_or_path Asset handle or prefixed path.
|
||||||
|
* @return string Path without the prefix or the original value.
|
||||||
|
*/
|
||||||
|
function remove_block_asset_path_prefix( $asset_handle_or_path ) {
|
||||||
|
$path_prefix = 'file:';
|
||||||
|
if ( 0 !== strpos( $asset_handle_or_path, $path_prefix ) ) {
|
||||||
|
return $asset_handle_or_path;
|
||||||
|
}
|
||||||
|
return substr(
|
||||||
|
$asset_handle_or_path,
|
||||||
|
strlen( $path_prefix )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates the name for an asset based on the name of the block
|
||||||
|
* and the field name provided.
|
||||||
|
*
|
||||||
|
* @since 5.5.0
|
||||||
|
*
|
||||||
|
* @param string $block_name Name of the block.
|
||||||
|
* @param string $field_name Name of the metadata field.
|
||||||
|
* @return string Generated asset name for the block's field.
|
||||||
|
*/
|
||||||
|
function generate_block_asset_handle( $block_name, $field_name ) {
|
||||||
|
$field_mappings = array(
|
||||||
|
'editorScript' => 'editor-script',
|
||||||
|
'script' => 'script',
|
||||||
|
'editorStyle' => 'editor-style',
|
||||||
|
'style' => 'style',
|
||||||
|
);
|
||||||
|
return str_replace( '/', '-', $block_name ) .
|
||||||
|
'-' . $field_mappings[ $field_name ];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds a script handle for the selected block metadata field. It detects
|
||||||
|
* when a path to file was provided and finds a corresponding
|
||||||
|
* asset file with details necessary to register the script under
|
||||||
|
* automatically generated handle name. It returns unprocessed script handle
|
||||||
|
* otherwise.
|
||||||
|
*
|
||||||
|
* @since 5.5.0
|
||||||
|
*
|
||||||
|
* @param array $metadata Block metadata.
|
||||||
|
* @param string $field_name Field name to pick from metadata.
|
||||||
|
* @return string|bool Script handle provided directly or created through
|
||||||
|
* script's registration, or false on failure.
|
||||||
|
*/
|
||||||
|
function register_block_script_handle( $metadata, $field_name ) {
|
||||||
|
if ( empty( $metadata[ $field_name ] ) ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$script_handle = $metadata[ $field_name ];
|
||||||
|
$script_path = remove_block_asset_path_prefix( $metadata[ $field_name ] );
|
||||||
|
if ( $script_handle === $script_path ) {
|
||||||
|
return $script_handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
$script_handle = generate_block_asset_handle( $metadata['name'], $field_name );
|
||||||
|
$script_asset_path = realpath(
|
||||||
|
dirname( $metadata['file'] ) . '/' .
|
||||||
|
substr_replace( $script_path, '.asset.php', - strlen( '.js' ) )
|
||||||
|
);
|
||||||
|
if ( ! file_exists( $script_asset_path ) ) {
|
||||||
|
$message = sprintf(
|
||||||
|
/* translators: %1: field name. %2: block name */
|
||||||
|
__( 'The asset file for the "%1$s" defined in "%2$s" block definition is missing.', 'default' ),
|
||||||
|
$field_name,
|
||||||
|
$metadata['name']
|
||||||
|
);
|
||||||
|
_doing_it_wrong( __FUNCTION__, $message, '5.5.0' );
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$script_asset = require $script_asset_path;
|
||||||
|
$result = wp_register_script(
|
||||||
|
$script_handle,
|
||||||
|
plugins_url( $script_path, $metadata['file'] ),
|
||||||
|
$script_asset['dependencies'],
|
||||||
|
$script_asset['version']
|
||||||
|
);
|
||||||
|
return $result ? $script_handle : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds a style handle for the block metadata field. It detects when a path
|
||||||
|
* to file was provided and registers the style under automatically
|
||||||
|
* generated handle name. It returns unprocessed style handle otherwise.
|
||||||
|
*
|
||||||
|
* @since 5.5.0
|
||||||
|
*
|
||||||
|
* @param array $metadata Block metadata.
|
||||||
|
* @param string $field_name Field name to pick from metadata.
|
||||||
|
* @return string|boolean Style handle provided directly or created through
|
||||||
|
* style's registration, or false on failure.
|
||||||
|
*/
|
||||||
|
function register_block_style_handle( $metadata, $field_name ) {
|
||||||
|
if ( empty( $metadata[ $field_name ] ) ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$style_handle = $metadata[ $field_name ];
|
||||||
|
$style_path = remove_block_asset_path_prefix( $metadata[ $field_name ] );
|
||||||
|
if ( $style_handle === $style_path ) {
|
||||||
|
return $style_handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
$style_handle = generate_block_asset_handle( $metadata['name'], $field_name );
|
||||||
|
$block_dir = dirname( $metadata['file'] );
|
||||||
|
$result = wp_register_style(
|
||||||
|
$style_handle,
|
||||||
|
plugins_url( $style_path, $metadata['file'] ),
|
||||||
|
array(),
|
||||||
|
filemtime( realpath( "$block_dir/$style_path" ) )
|
||||||
|
);
|
||||||
|
return $result ? $style_handle : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a block type from metadata stored in the `block.json` file.
|
||||||
|
*
|
||||||
|
* @since 5.5.0
|
||||||
|
*
|
||||||
|
* @param string $file_or_folder Path to the JSON file with metadata definition for
|
||||||
|
* the block or path to the folder where the `block.json` file is located.
|
||||||
|
* @param array $args {
|
||||||
|
* Optional. Array of block type arguments. Any arguments may be defined, however the
|
||||||
|
* ones described below are supported by default. Default empty array.
|
||||||
|
*
|
||||||
|
* @type callable $render_callback Callback used to render blocks of this block type.
|
||||||
|
* }
|
||||||
|
* @return WP_Block_Type|false The registered block type on success, or false on failure.
|
||||||
|
*/
|
||||||
|
function register_block_type_from_metadata( $file_or_folder, $args = array() ) {
|
||||||
|
$filename = 'block.json';
|
||||||
|
$metadata_file = ( substr( $file_or_folder, -strlen( $filename ) ) !== $filename ) ?
|
||||||
|
trailingslashit( $file_or_folder ) . $filename :
|
||||||
|
$file_or_folder;
|
||||||
|
if ( ! file_exists( $metadata_file ) ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$metadata = json_decode( file_get_contents( $metadata_file ), true );
|
||||||
|
if ( ! is_array( $metadata ) || empty( $metadata['name'] ) ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$metadata['file'] = $metadata_file;
|
||||||
|
|
||||||
|
$settings = array();
|
||||||
|
$property_mappings = array(
|
||||||
|
'title' => 'title',
|
||||||
|
'category' => 'category',
|
||||||
|
'parent' => 'parent',
|
||||||
|
'icon' => 'icon',
|
||||||
|
'description' => 'description',
|
||||||
|
'keywords' => 'keywords',
|
||||||
|
'attributes' => 'attributes',
|
||||||
|
'providesContext' => 'provides_context',
|
||||||
|
'usesContext' => 'uses_context',
|
||||||
|
'supports' => 'supports',
|
||||||
|
'styles' => 'styles',
|
||||||
|
'example' => 'example',
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ( $property_mappings as $key => $mapped_key ) {
|
||||||
|
if ( isset( $metadata[ $key ] ) ) {
|
||||||
|
$settings[ $mapped_key ] = $metadata[ $key ];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! empty( $metadata['editorScript'] ) ) {
|
||||||
|
$settings['editor_script'] = register_block_script_handle(
|
||||||
|
$metadata,
|
||||||
|
'editorScript'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! empty( $metadata['script'] ) ) {
|
||||||
|
$settings['script'] = register_block_script_handle(
|
||||||
|
$metadata,
|
||||||
|
'script'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! empty( $metadata['editorStyle'] ) ) {
|
||||||
|
$settings['editor_style'] = register_block_style_handle(
|
||||||
|
$metadata,
|
||||||
|
'editorStyle'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! empty( $metadata['style'] ) ) {
|
||||||
|
$settings['style'] = register_block_style_handle(
|
||||||
|
$metadata,
|
||||||
|
'style'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return register_block_type(
|
||||||
|
$metadata['name'],
|
||||||
|
array_merge(
|
||||||
|
$settings,
|
||||||
|
$args
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine whether a post or content string has blocks.
|
* Determine whether a post or content string has blocks.
|
||||||
*
|
*
|
||||||
|
6
tests/phpunit/tests/blocks/fixtures/block.asset.php
Normal file
6
tests/phpunit/tests/blocks/fixtures/block.asset.php
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'dependencies' => array(),
|
||||||
|
'version' => 'test',
|
||||||
|
);
|
1
tests/phpunit/tests/blocks/fixtures/block.css
Normal file
1
tests/phpunit/tests/blocks/fixtures/block.css
Normal file
@ -0,0 +1 @@
|
|||||||
|
/* Test CSS file */
|
1
tests/phpunit/tests/blocks/fixtures/block.js
Normal file
1
tests/phpunit/tests/blocks/fixtures/block.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
/* Test JavaScript file. */
|
52
tests/phpunit/tests/blocks/fixtures/block.json
Normal file
52
tests/phpunit/tests/blocks/fixtures/block.json
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
{
|
||||||
|
"name": "my-plugin/notice",
|
||||||
|
"title": "Notice",
|
||||||
|
"category": "common",
|
||||||
|
"parent": [
|
||||||
|
"core/group"
|
||||||
|
],
|
||||||
|
"providesContext": {
|
||||||
|
"my-plugin/message": "message"
|
||||||
|
},
|
||||||
|
"usesContext": [
|
||||||
|
"groupId"
|
||||||
|
],
|
||||||
|
"icon": "star",
|
||||||
|
"description": "Shows warning, error or success notices…",
|
||||||
|
"keywords": [
|
||||||
|
"alert",
|
||||||
|
"message"
|
||||||
|
],
|
||||||
|
"textDomain": "my-plugin",
|
||||||
|
"attributes": {
|
||||||
|
"message": {
|
||||||
|
"type": "string",
|
||||||
|
"source": "html",
|
||||||
|
"selector": ".message"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"supports": {
|
||||||
|
"align": true,
|
||||||
|
"lightBlockWrapper": true
|
||||||
|
},
|
||||||
|
"styles": [
|
||||||
|
{
|
||||||
|
"name": "default",
|
||||||
|
"label": "Default",
|
||||||
|
"isDefault": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "other",
|
||||||
|
"label": "Other"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"example": {
|
||||||
|
"attributes": {
|
||||||
|
"message": "This is a notice!"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"editorScript": "my-plugin-notice-editor-script",
|
||||||
|
"script": "my-plugin-notice-script",
|
||||||
|
"editorStyle": "my-plugin-notice-editor-style",
|
||||||
|
"style": "my-plugin-notice-style"
|
||||||
|
}
|
@ -102,6 +102,248 @@ class WP_Test_Block_Register extends WP_UnitTestCase {
|
|||||||
$this->assertFalse( $registry->is_registered( $name ) );
|
$this->assertFalse( $registry->is_registered( $name ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ticket 50263
|
||||||
|
*/
|
||||||
|
function test_does_not_remove_block_asset_path_prefix() {
|
||||||
|
$result = remove_block_asset_path_prefix( 'script-handle' );
|
||||||
|
|
||||||
|
$this->assertSame( 'script-handle', $result );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ticket 50263
|
||||||
|
*/
|
||||||
|
function test_removes_block_asset_path_prefix() {
|
||||||
|
$result = remove_block_asset_path_prefix( 'file:./block.js' );
|
||||||
|
|
||||||
|
$this->assertSame( './block.js', $result );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ticket 50263
|
||||||
|
*/
|
||||||
|
function test_generate_block_asset_handle() {
|
||||||
|
$block_name = 'unit-tests/my-block';
|
||||||
|
|
||||||
|
$this->assertSame(
|
||||||
|
'unit-tests-my-block-editor-script',
|
||||||
|
generate_block_asset_handle( $block_name, 'editorScript' )
|
||||||
|
);
|
||||||
|
$this->assertSame(
|
||||||
|
'unit-tests-my-block-script',
|
||||||
|
generate_block_asset_handle( $block_name, 'script' )
|
||||||
|
);
|
||||||
|
$this->assertSame(
|
||||||
|
'unit-tests-my-block-editor-style',
|
||||||
|
generate_block_asset_handle( $block_name, 'editorStyle' )
|
||||||
|
);
|
||||||
|
$this->assertSame(
|
||||||
|
'unit-tests-my-block-style',
|
||||||
|
generate_block_asset_handle( $block_name, 'style' )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ticket 50263
|
||||||
|
*/
|
||||||
|
function test_field_not_found_register_block_script_handle() {
|
||||||
|
$result = register_block_script_handle( array(), 'script' );
|
||||||
|
|
||||||
|
$this->assertFalse( $result );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ticket 50263
|
||||||
|
*/
|
||||||
|
function test_empty_value_register_block_script_handle() {
|
||||||
|
$metadata = array( 'script' => '' );
|
||||||
|
$result = register_block_script_handle( $metadata, 'script' );
|
||||||
|
|
||||||
|
$this->assertFalse( $result );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @expectedIncorrectUsage register_block_script_handle
|
||||||
|
* @ticket 50263
|
||||||
|
*/
|
||||||
|
function test_missing_asset_file_register_block_script_handle() {
|
||||||
|
$metadata = array(
|
||||||
|
'file' => __FILE__,
|
||||||
|
'name' => 'unit-tests/test-block',
|
||||||
|
'script' => 'file:./fixtures/missing-asset.js',
|
||||||
|
);
|
||||||
|
$result = register_block_script_handle( $metadata, 'script' );
|
||||||
|
|
||||||
|
$this->assertFalse( $result );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ticket 50263
|
||||||
|
*/
|
||||||
|
function test_handle_passed_register_block_script_handle() {
|
||||||
|
$metadata = array(
|
||||||
|
'editorScript' => 'test-script-handle',
|
||||||
|
);
|
||||||
|
$result = register_block_script_handle( $metadata, 'editorScript' );
|
||||||
|
|
||||||
|
$this->assertSame( 'test-script-handle', $result );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ticket 50263
|
||||||
|
*/
|
||||||
|
function test_success_register_block_script_handle() {
|
||||||
|
$metadata = array(
|
||||||
|
'file' => __FILE__,
|
||||||
|
'name' => 'unit-tests/test-block',
|
||||||
|
'script' => 'file:./fixtures/block.js',
|
||||||
|
);
|
||||||
|
$result = register_block_script_handle( $metadata, 'script' );
|
||||||
|
|
||||||
|
$this->assertSame( 'unit-tests-test-block-script', $result );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ticket 50263
|
||||||
|
*/
|
||||||
|
function test_field_not_found_register_block_style_handle() {
|
||||||
|
$result = register_block_style_handle( array(), 'style' );
|
||||||
|
|
||||||
|
$this->assertFalse( $result );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ticket 50263
|
||||||
|
*/
|
||||||
|
function test_empty_value_found_register_block_style_handle() {
|
||||||
|
$metadata = array( 'style' => '' );
|
||||||
|
$result = register_block_style_handle( $metadata, 'style' );
|
||||||
|
|
||||||
|
$this->assertFalse( $result );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ticket 50263
|
||||||
|
*/
|
||||||
|
function test_handle_passed_register_block_style_handle() {
|
||||||
|
$metadata = array(
|
||||||
|
'style' => 'test-style-handle',
|
||||||
|
);
|
||||||
|
$result = register_block_style_handle( $metadata, 'style' );
|
||||||
|
|
||||||
|
$this->assertSame( 'test-style-handle', $result );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ticket 50263
|
||||||
|
*/
|
||||||
|
function test_success_register_block_style_handle() {
|
||||||
|
$metadata = array(
|
||||||
|
'file' => __FILE__,
|
||||||
|
'name' => 'unit-tests/test-block',
|
||||||
|
'style' => 'file:./fixtures/block.css',
|
||||||
|
);
|
||||||
|
$result = register_block_style_handle( $metadata, 'style' );
|
||||||
|
|
||||||
|
$this->assertSame( 'unit-tests-test-block-style', $result );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests that the function returns false when the `block.json` is not found
|
||||||
|
* in the WordPress core.
|
||||||
|
*
|
||||||
|
* @ticket 50263
|
||||||
|
*/
|
||||||
|
function test_metadata_not_found_in_wordpress_core() {
|
||||||
|
$result = register_block_type_from_metadata( 'unknown' );
|
||||||
|
|
||||||
|
$this->assertFalse( $result );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests that the function returns false when the `block.json` is not found
|
||||||
|
* in the current directory.
|
||||||
|
*
|
||||||
|
* @ticket 50263
|
||||||
|
*/
|
||||||
|
function test_metadata_not_found_in_the_current_directory() {
|
||||||
|
$result = register_block_type_from_metadata( __DIR__ );
|
||||||
|
|
||||||
|
$this->assertFalse( $result );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests that the function returns the registered block when the `block.json`
|
||||||
|
* is found in the fixtures directory.
|
||||||
|
*
|
||||||
|
* @ticket 50263
|
||||||
|
*/
|
||||||
|
function test_block_registers_with_metadata_fixture() {
|
||||||
|
$result = register_block_type_from_metadata(
|
||||||
|
__DIR__ . '/fixtures'
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertInstanceOf( 'WP_Block_Type', $result );
|
||||||
|
$this->assertSame( 'my-plugin/notice', $result->name );
|
||||||
|
$this->assertSame( 'Notice', $result->title );
|
||||||
|
$this->assertSame( 'common', $result->category );
|
||||||
|
$this->assertEqualSets( array( 'core/group' ), $result->parent );
|
||||||
|
$this->assertSame( 'star', $result->icon );
|
||||||
|
$this->assertSame( 'Shows warning, error or success notices…', $result->description );
|
||||||
|
$this->assertEqualSets( array( 'alert', 'message' ), $result->keywords );
|
||||||
|
$this->assertEquals(
|
||||||
|
array(
|
||||||
|
'message' => array(
|
||||||
|
'type' => 'string',
|
||||||
|
'source' => 'html',
|
||||||
|
'selector' => '.message',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
$result->attributes
|
||||||
|
);
|
||||||
|
$this->assertEquals(
|
||||||
|
array(
|
||||||
|
'my-plugin/message' => 'message',
|
||||||
|
),
|
||||||
|
$result->provides_context
|
||||||
|
);
|
||||||
|
$this->assertEqualSets( array( 'groupId' ), $result->uses_context );
|
||||||
|
$this->assertEquals(
|
||||||
|
array(
|
||||||
|
'align' => true,
|
||||||
|
'lightBlockWrapper' => true,
|
||||||
|
),
|
||||||
|
$result->supports
|
||||||
|
);
|
||||||
|
$this->assertEquals(
|
||||||
|
array(
|
||||||
|
array(
|
||||||
|
'name' => 'default',
|
||||||
|
'label' => 'Default',
|
||||||
|
'isDefault' => true,
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'name' => 'other',
|
||||||
|
'label' => 'Other',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
$result->styles
|
||||||
|
);
|
||||||
|
$this->assertEquals(
|
||||||
|
array(
|
||||||
|
'attributes' => array(
|
||||||
|
'message' => 'This is a notice!',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
$result->example
|
||||||
|
);
|
||||||
|
$this->assertSame( 'my-plugin-notice-editor-script', $result->editor_script );
|
||||||
|
$this->assertSame( 'my-plugin-notice-script', $result->script );
|
||||||
|
$this->assertSame( 'my-plugin-notice-editor-style', $result->editor_style );
|
||||||
|
$this->assertSame( 'my-plugin-notice-style', $result->style );
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @ticket 45109
|
* @ticket 45109
|
||||||
*/
|
*/
|
||||||
|
Loading…
Reference in New Issue
Block a user