respective book and chapter structure added.
This commit is contained in:
		
							parent
							
								
									90a8070518
								
							
						
					
					
						commit
						f606711463
					
				| 
						 | 
				
			
			@ -13,11 +13,6 @@ class ApiEntityListFormatter
 | 
			
		|||
     */
 | 
			
		||||
    protected array $list = [];
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Whether to include related titles in the response.
 | 
			
		||||
     */
 | 
			
		||||
    protected bool $includeRelatedTitles = false;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The fields to show in the formatted data.
 | 
			
		||||
     * Can be a plain string array item for a direct model field (If existing on model).
 | 
			
		||||
| 
						 | 
				
			
			@ -79,20 +74,18 @@ class ApiEntityListFormatter
 | 
			
		|||
    /**
 | 
			
		||||
     * Enable the inclusion of related book and chapter titles in the response.
 | 
			
		||||
     */
 | 
			
		||||
    public function withRelatedTitles(): self
 | 
			
		||||
    public function withRelatedData(): self
 | 
			
		||||
    {
 | 
			
		||||
        $this->includeRelatedTitles = true;
 | 
			
		||||
 | 
			
		||||
        $this->withField('book_title', function (Entity $entity) {
 | 
			
		||||
        $this->withField('book', function (Entity $entity) {
 | 
			
		||||
            if (method_exists($entity, 'book')) {
 | 
			
		||||
                return $entity->book?->name;
 | 
			
		||||
                return $entity->book()->select(['id', 'name', 'slug'])->first();
 | 
			
		||||
            }
 | 
			
		||||
            return null;
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        $this->withField('chapter_title', function (Entity $entity) {
 | 
			
		||||
        $this->withField('chapter', function (Entity $entity) {
 | 
			
		||||
            if ($entity instanceof Page && $entity->chapter_id) {
 | 
			
		||||
                return optional($entity->getAttribute('chapter'))->name;
 | 
			
		||||
                return $entity->chapter()->select(['id', 'name', 'slug'])->first();
 | 
			
		||||
            }
 | 
			
		||||
            return null;
 | 
			
		||||
        });
 | 
			
		||||
| 
						 | 
				
			
			@ -106,9 +99,7 @@ class ApiEntityListFormatter
 | 
			
		|||
     */
 | 
			
		||||
    public function format(): array
 | 
			
		||||
    {
 | 
			
		||||
        if ($this->includeRelatedTitles) {
 | 
			
		||||
            $this->loadRelatedTitles();
 | 
			
		||||
        }
 | 
			
		||||
        $this->loadRelatedData();
 | 
			
		||||
 | 
			
		||||
        $results = [];
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -122,7 +113,7 @@ class ApiEntityListFormatter
 | 
			
		|||
    /**
 | 
			
		||||
     * Eager load the related book and chapter data when needed.
 | 
			
		||||
     */
 | 
			
		||||
    protected function loadRelatedTitles(): void
 | 
			
		||||
    protected function loadRelatedData(): void
 | 
			
		||||
    {
 | 
			
		||||
        $pages = collect($this->list)->filter(fn($item) => $item instanceof Page);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -17,20 +17,9 @@ class SearchApiController extends ApiController
 | 
			
		|||
            'query' => ['required'],
 | 
			
		||||
            'page' => ['integer', 'min:1'],
 | 
			
		||||
            'count' => ['integer', 'min:1', 'max:100'],
 | 
			
		||||
            'include' => ['string', 'regex:/^[a-zA-Z,]*$/'],
 | 
			
		||||
        ],
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Valid include parameters and their corresponding formatter methods.
 | 
			
		||||
     * These parameters allow for additional related data, like titles or tags,
 | 
			
		||||
     * to be included in the search results when requested via the API.
 | 
			
		||||
     */
 | 
			
		||||
    protected const VALID_INCLUDES = [
 | 
			
		||||
        'titles' => 'withRelatedTitles',
 | 
			
		||||
        'tags' => 'withTags',
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    public function __construct(SearchRunner $searchRunner, SearchResultsFormatter $resultsFormatter)
 | 
			
		||||
    {
 | 
			
		||||
        $this->searchRunner = $searchRunner;
 | 
			
		||||
| 
						 | 
				
			
			@ -44,13 +33,6 @@ class SearchApiController extends ApiController
 | 
			
		|||
     * for a full list of search term options. Results contain a 'type' property to distinguish
 | 
			
		||||
     * between: bookshelf, book, chapter & page.
 | 
			
		||||
     *
 | 
			
		||||
     * This method now supports the 'include' parameter, which allows API clients to specify related
 | 
			
		||||
     * fields (such as titles or tags) that should be included in the search results.
 | 
			
		||||
     *
 | 
			
		||||
     * The 'include' parameter is a comma-separated string. For example, adding `include=titles,tags`
 | 
			
		||||
     * will include both titles and tags in the API response. If the parameter is not provided, only
 | 
			
		||||
     * basic entity data will be returned.
 | 
			
		||||
     *
 | 
			
		||||
     * The paging parameters and response format emulates a standard listing endpoint
 | 
			
		||||
     * but standard sorting and filtering cannot be done on this endpoint. If a count value
 | 
			
		||||
     * is provided this will only be taken as a suggestion. The results in the response
 | 
			
		||||
| 
						 | 
				
			
			@ -63,49 +45,22 @@ class SearchApiController extends ApiController
 | 
			
		|||
        $options = SearchOptions::fromString($request->get('query') ?? '');
 | 
			
		||||
        $page = intval($request->get('page', '0')) ?: 1;
 | 
			
		||||
        $count = min(intval($request->get('count', '0')) ?: 20, 100);
 | 
			
		||||
        $includes = $this->parseIncludes($request->get('include', ''));
 | 
			
		||||
 | 
			
		||||
        $results = $this->searchRunner->searchEntities($options, 'all', $page, $count);
 | 
			
		||||
        $this->resultsFormatter->format($results['results']->all(), $options);
 | 
			
		||||
 | 
			
		||||
        $formatter = new ApiEntityListFormatter($results['results']->all());
 | 
			
		||||
        $formatter->withType(); // Always include type as it's essential for search results
 | 
			
		||||
 | 
			
		||||
        foreach ($includes as $include) {
 | 
			
		||||
            if (isset(self::VALID_INCLUDES[$include])) {
 | 
			
		||||
                $method = self::VALID_INCLUDES[$include];
 | 
			
		||||
                $formatter->$method();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $formatter->withField('preview_html', function (Entity $entity) {
 | 
			
		||||
            return [
 | 
			
		||||
                'name' => (string) $entity->getAttribute('preview_name'),
 | 
			
		||||
                'content' => (string) $entity->getAttribute('preview_content'),
 | 
			
		||||
            ];
 | 
			
		||||
        });
 | 
			
		||||
        $data = (new ApiEntityListFormatter($results['results']->all()))
 | 
			
		||||
            ->withType()->withTags()->withRelatedData()
 | 
			
		||||
            ->withField('preview_html', function (Entity $entity) {
 | 
			
		||||
                return [
 | 
			
		||||
                    'name' => (string) $entity->getAttribute('preview_name'),
 | 
			
		||||
                    'content' => (string) $entity->getAttribute('preview_content'),
 | 
			
		||||
                ];
 | 
			
		||||
            })->format();
 | 
			
		||||
 | 
			
		||||
        return response()->json([
 | 
			
		||||
            'data' => $formatter->format(),
 | 
			
		||||
            'data' => $data,
 | 
			
		||||
            'total' => $results['total'],
 | 
			
		||||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Parse and validate the include parameter.
 | 
			
		||||
     *
 | 
			
		||||
     * @param string $includeString Comma-separated list of includes
 | 
			
		||||
     * @return array<string>
 | 
			
		||||
     */
 | 
			
		||||
    protected function parseIncludes(string $includeString): array
 | 
			
		||||
    {
 | 
			
		||||
        if (empty($includeString)) {
 | 
			
		||||
            return [];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return array_filter(
 | 
			
		||||
            explode(',', strtolower($includeString)),
 | 
			
		||||
            fn($include) => isset (self::VALID_INCLUDES[$include])
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1 +1 @@
 | 
			
		|||
GET /api/search?query=cats+{created_by:me}&page=1&count=2&include=titles,tags
 | 
			
		||||
GET /api/search?query=cats+{created_by:me}&page=1&count=2
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,72 +1,92 @@
 | 
			
		|||
{
 | 
			
		||||
  "data": [
 | 
			
		||||
    {
 | 
			
		||||
      "id": 84,
 | 
			
		||||
      "book_id": 1,
 | 
			
		||||
      "slug": "a-chapter-for-cats",
 | 
			
		||||
      "name": "A chapter for cats",
 | 
			
		||||
      "created_at": "2021-11-14T15:57:35.000000Z",
 | 
			
		||||
      "updated_at": "2021-11-14T15:57:35.000000Z",
 | 
			
		||||
      "type": "chapter",
 | 
			
		||||
      "url": "https://example.com/books/my-book/chapter/a-chapter-for-cats",
 | 
			
		||||
      "book_title": "Cats",
 | 
			
		||||
      "preview_html": {
 | 
			
		||||
        "name": "A chapter for <strong>cats</strong>",
 | 
			
		||||
        "content": "...once a bunch of <strong>cats</strong> named tony...behaviour of <strong>cats</strong> is unsuitable"
 | 
			
		||||
      },
 | 
			
		||||
      "tags": []
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "name": "The hows and whys of cats",
 | 
			
		||||
      "id": 396,
 | 
			
		||||
      "slug": "the-hows-and-whys-of-cats",
 | 
			
		||||
      "book_id": 1,
 | 
			
		||||
      "chapter_id": 75,
 | 
			
		||||
      "draft": false,
 | 
			
		||||
      "template": false,
 | 
			
		||||
      "created_at": "2021-05-15T16:28:10.000000Z",
 | 
			
		||||
      "updated_at": "2021-11-14T15:56:49.000000Z",
 | 
			
		||||
      "type": "page",
 | 
			
		||||
      "url": "https://example.com/books/my-book/page/the-hows-and-whys-of-cats",
 | 
			
		||||
      "book_title": "Cats",
 | 
			
		||||
      "chapter_title": "A chapter for cats",
 | 
			
		||||
      "preview_html": {
 | 
			
		||||
        "name": "The hows and whys of <strong>cats</strong>",
 | 
			
		||||
        "content": "...people ask why <strong>cats</strong>? but there are...the reason that <strong>cats</strong> are fast are due to..."
 | 
			
		||||
      },
 | 
			
		||||
      "tags": [
 | 
			
		||||
    "data": [
 | 
			
		||||
        {
 | 
			
		||||
          "name": "Animal",
 | 
			
		||||
          "value": "Cat",
 | 
			
		||||
          "order": 0
 | 
			
		||||
            "id": 84,
 | 
			
		||||
            "book_id": 1,
 | 
			
		||||
            "slug": "a-chapter-for-cats",
 | 
			
		||||
            "name": "A chapter for cats",
 | 
			
		||||
            "created_at": "2021-11-14T15:57:35.000000Z",
 | 
			
		||||
            "updated_at": "2021-11-14T15:57:35.000000Z",
 | 
			
		||||
            "type": "chapter",
 | 
			
		||||
            "url": "https://example.com/books/my-book/chapter/a-chapter-for-cats",
 | 
			
		||||
            "book": {
 | 
			
		||||
                "id": 1,
 | 
			
		||||
                "name": "Cats",
 | 
			
		||||
                "slug": "cats"
 | 
			
		||||
            },
 | 
			
		||||
            "preview_html": {
 | 
			
		||||
                "name": "A chapter for <strong>cats</strong>",
 | 
			
		||||
                "content": "...once a bunch of <strong>cats</strong> named tony...behaviour of <strong>cats</strong> is unsuitable"
 | 
			
		||||
            },
 | 
			
		||||
            "tags": []
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "name": "Category",
 | 
			
		||||
          "value": "Top Content",
 | 
			
		||||
          "order": 0
 | 
			
		||||
            "name": "The hows and whys of cats",
 | 
			
		||||
            "id": 396,
 | 
			
		||||
            "slug": "the-hows-and-whys-of-cats",
 | 
			
		||||
            "book_id": 1,
 | 
			
		||||
            "chapter_id": 75,
 | 
			
		||||
            "draft": false,
 | 
			
		||||
            "template": false,
 | 
			
		||||
            "created_at": "2021-05-15T16:28:10.000000Z",
 | 
			
		||||
            "updated_at": "2021-11-14T15:56:49.000000Z",
 | 
			
		||||
            "type": "page",
 | 
			
		||||
            "url": "https://example.com/books/my-book/page/the-hows-and-whys-of-cats",
 | 
			
		||||
            "book": {
 | 
			
		||||
                "id": 1,
 | 
			
		||||
                "name": "Cats",
 | 
			
		||||
                "slug": "cats"
 | 
			
		||||
            },
 | 
			
		||||
            "chapter": {
 | 
			
		||||
                "id": 84,
 | 
			
		||||
                "name": "A chapter for cats",
 | 
			
		||||
                "slug": "a-chapter-for-cats"
 | 
			
		||||
            },
 | 
			
		||||
            "preview_html": {
 | 
			
		||||
                "name": "The hows and whys of <strong>cats</strong>",
 | 
			
		||||
                "content": "...people ask why <strong>cats</strong>? but there are...the reason that <strong>cats</strong> are fast are due to..."
 | 
			
		||||
            },
 | 
			
		||||
            "tags": [
 | 
			
		||||
                {
 | 
			
		||||
                    "name": "Animal",
 | 
			
		||||
                    "value": "Cat",
 | 
			
		||||
                    "order": 0
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "name": "Category",
 | 
			
		||||
                    "value": "Top Content",
 | 
			
		||||
                    "order": 0
 | 
			
		||||
                }
 | 
			
		||||
            ]
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            "name": "How advanced are cats?",
 | 
			
		||||
            "id": 362,
 | 
			
		||||
            "slug": "how-advanced-are-cats",
 | 
			
		||||
            "book_id": 13,
 | 
			
		||||
            "chapter_id": 73,
 | 
			
		||||
            "draft": false,
 | 
			
		||||
            "template": false,
 | 
			
		||||
            "created_at": "2020-11-29T21:55:07.000000Z",
 | 
			
		||||
            "updated_at": "2021-11-14T16:02:39.000000Z",
 | 
			
		||||
            "type": "page",
 | 
			
		||||
            "url": "https://example.com/books/my-book/page/how-advanced-are-cats",
 | 
			
		||||
            "book": {
 | 
			
		||||
                "id": 1,
 | 
			
		||||
                "name": "Cats",
 | 
			
		||||
                "slug": "cats"
 | 
			
		||||
            },
 | 
			
		||||
            "chapter": {
 | 
			
		||||
                "id": 84,
 | 
			
		||||
                "name": "A chapter for cats",
 | 
			
		||||
                "slug": "a-chapter-for-cats"
 | 
			
		||||
            },
 | 
			
		||||
            "preview_html": {
 | 
			
		||||
                "name": "How advanced are <strong>cats</strong>?",
 | 
			
		||||
                "content": "<strong>cats</strong> are some of the most advanced animals in the world."
 | 
			
		||||
            },
 | 
			
		||||
            "tags": []
 | 
			
		||||
        }
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "name": "How advanced are cats?",
 | 
			
		||||
      "id": 362,
 | 
			
		||||
      "slug": "how-advanced-are-cats",
 | 
			
		||||
      "book_id": 13,
 | 
			
		||||
      "chapter_id": 73,
 | 
			
		||||
      "draft": false,
 | 
			
		||||
      "template": false,
 | 
			
		||||
      "created_at": "2020-11-29T21:55:07.000000Z",
 | 
			
		||||
      "updated_at": "2021-11-14T16:02:39.000000Z",
 | 
			
		||||
      "type": "page",
 | 
			
		||||
      "url": "https://example.com/books/my-book/page/how-advanced-are-cats",
 | 
			
		||||
      "book_title": "Cats",
 | 
			
		||||
      "chapter_title": "A chapter for cats",
 | 
			
		||||
      "preview_html": {
 | 
			
		||||
        "name": "How advanced are <strong>cats</strong>?",
 | 
			
		||||
        "content": "<strong>cats</strong> are some of the most advanced animals in the world."
 | 
			
		||||
      },
 | 
			
		||||
      "tags": []
 | 
			
		||||
    }
 | 
			
		||||
  ],
 | 
			
		||||
  "total": 3
 | 
			
		||||
    ],
 | 
			
		||||
    "total": 3
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,7 +2,6 @@
 | 
			
		|||
 | 
			
		||||
namespace Tests\Api;
 | 
			
		||||
 | 
			
		||||
use BookStack\Activity\Models\Tag;
 | 
			
		||||
use BookStack\Entities\Models\Book;
 | 
			
		||||
use BookStack\Entities\Models\Bookshelf;
 | 
			
		||||
use BookStack\Entities\Models\Chapter;
 | 
			
		||||
| 
						 | 
				
			
			@ -75,112 +74,4 @@ class SearchApiTest extends TestCase
 | 
			
		|||
        $resp = $this->actingAsApiEditor()->get($this->baseEndpoint . '?query=myqueryvalue');
 | 
			
		||||
        $resp->assertOk();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function test_all_endpoint_includes_book_and_chapter_titles_when_requested()
 | 
			
		||||
    {
 | 
			
		||||
        $this->actingAsApiEditor();
 | 
			
		||||
 | 
			
		||||
        $book = $this->entities->book();
 | 
			
		||||
        $chapter = $this->entities->chapter();
 | 
			
		||||
        $page = $this->entities->newPage();
 | 
			
		||||
 | 
			
		||||
        $book->name = 'My Test Book';
 | 
			
		||||
        $book->save();
 | 
			
		||||
 | 
			
		||||
        $chapter->name = 'My Test Chapter';
 | 
			
		||||
        $chapter->book_id = $book->id;
 | 
			
		||||
        $chapter->save();
 | 
			
		||||
 | 
			
		||||
        $page->name = 'My Test Page With UniqueSearchTerm';
 | 
			
		||||
        $page->book_id = $book->id;
 | 
			
		||||
        $page->chapter_id = $chapter->id;
 | 
			
		||||
        $page->save();
 | 
			
		||||
 | 
			
		||||
        $page->indexForSearch();
 | 
			
		||||
 | 
			
		||||
        // Test without include parameter
 | 
			
		||||
        $resp = $this->getJson($this->baseEndpoint . '?query=UniqueSearchTerm');
 | 
			
		||||
        $resp->assertOk();
 | 
			
		||||
        $resp->assertDontSee('book_title');
 | 
			
		||||
        $resp->assertDontSee('chapter_title');
 | 
			
		||||
 | 
			
		||||
        // Test with include parameter
 | 
			
		||||
        $resp = $this->getJson($this->baseEndpoint . '?query=UniqueSearchTerm&include=titles');
 | 
			
		||||
        $resp->assertOk();
 | 
			
		||||
        $resp->assertJsonFragment([
 | 
			
		||||
            'name' => 'My Test Page With UniqueSearchTerm',
 | 
			
		||||
            'book_title' => 'My Test Book',
 | 
			
		||||
            'chapter_title' => 'My Test Chapter',
 | 
			
		||||
            'type' => 'page'
 | 
			
		||||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function test_all_endpoint_validates_include_parameter()
 | 
			
		||||
    {
 | 
			
		||||
        $this->actingAsApiEditor();
 | 
			
		||||
 | 
			
		||||
        // Test invalid include value
 | 
			
		||||
        $resp = $this->getJson($this->baseEndpoint . '?query=test&include=invalid');
 | 
			
		||||
        $resp->assertOk();
 | 
			
		||||
        $resp->assertDontSee('book_title');
 | 
			
		||||
 | 
			
		||||
        // Test SQL injection attempt
 | 
			
		||||
        $resp = $this->getJson($this->baseEndpoint . '?query=test&include=titles;DROP TABLE users');
 | 
			
		||||
        $resp->assertStatus(422);
 | 
			
		||||
 | 
			
		||||
        // Test multiple includes
 | 
			
		||||
        $resp = $this->getJson($this->baseEndpoint . '?query=test&include=titles,tags');
 | 
			
		||||
        $resp->assertOk();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function test_all_endpoint_includes_tags_when_requested()
 | 
			
		||||
    {
 | 
			
		||||
        $this->actingAsApiEditor();
 | 
			
		||||
 | 
			
		||||
        // Create a page and give it a unique name for search
 | 
			
		||||
        $page = $this->entities->page();
 | 
			
		||||
        $page->name = 'Page With UniqueSearchTerm';
 | 
			
		||||
        $page->save();
 | 
			
		||||
 | 
			
		||||
        // Save tags to the page using the existing saveTagsToEntity method
 | 
			
		||||
        $tags = [
 | 
			
		||||
            ['name' => 'SampleTag', 'value' => 'SampleValue']
 | 
			
		||||
        ];
 | 
			
		||||
        app(\BookStack\Activity\TagRepo::class)->saveTagsToEntity($page, $tags);
 | 
			
		||||
 | 
			
		||||
        // Ensure the page is indexed for search
 | 
			
		||||
        $page->indexForSearch();
 | 
			
		||||
 | 
			
		||||
        // Test without the "tags" include
 | 
			
		||||
        $resp = $this->getJson($this->baseEndpoint . '?query=UniqueSearchTerm');
 | 
			
		||||
        $resp->assertOk();
 | 
			
		||||
        $resp->assertDontSee('tags');
 | 
			
		||||
 | 
			
		||||
        // Test with the "tags" include
 | 
			
		||||
        $resp = $this->getJson($this->baseEndpoint . '?query=UniqueSearchTerm&include=tags');
 | 
			
		||||
        $resp->assertOk();
 | 
			
		||||
        
 | 
			
		||||
        // Assert that tags are included in the response
 | 
			
		||||
        $resp->assertJsonFragment([
 | 
			
		||||
            'name' => 'SampleTag',
 | 
			
		||||
            'value' => 'SampleValue',
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        // Optionally: check the structure to match the tag order as well
 | 
			
		||||
        $resp->assertJsonStructure([
 | 
			
		||||
            'data' => [
 | 
			
		||||
                '*' => [
 | 
			
		||||
                    'tags' => [
 | 
			
		||||
                        '*' => [
 | 
			
		||||
                            'name',
 | 
			
		||||
                            'value',
 | 
			
		||||
                            'order',
 | 
			
		||||
                        ],
 | 
			
		||||
                    ],
 | 
			
		||||
                ],
 | 
			
		||||
            ],
 | 
			
		||||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue