+
+ /**
+ * Apply additional filter on every search result of BookmarkFilter calling plugins hooks.
+ *
+ * @param Bookmark $bookmark To check.
+ * @param array $context Additional info about search context, depends on the search source.
+ *
+ * @return bool True if the result must be kept in search results, false otherwise.
+ */
+ public function filterSearchEntry(Bookmark $bookmark, array $context): bool
+ {
+ if ($this->filterSearchEntryHooks === null) {
+ $this->loadFilterSearchEntryHooks();
+ }
+
+ if ($this->filterSearchEntryHooks === []) {
+ return true;
+ }
+
+ foreach ($this->filterSearchEntryHooks as $filterSearchEntryHook) {
+ if ($filterSearchEntryHook($bookmark, $context) === false) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * filterSearchEntry() method will be called for every search result,
+ * so for performances we preload existing functions to invoke them directly.
+ */
+ protected function loadFilterSearchEntryHooks(): void
+ {
+ $this->filterSearchEntryHooks = [];
+
+ foreach ($this->loadedPlugins as $plugin) {
+ $hookFunction = $this->buildHookName('filter_search_entry', $plugin);
+
+ if (function_exists($hookFunction)) {
+ $this->filterSearchEntryHooks[] = $hookFunction;
+ }
+ }
+ }
+
+ /**
+ * Checks whether provided input is valid to register a new route.
+ * It must contain keys `method`, `route`, `callable` (all strings).
+ *
+ * @param string[] $input
+ *
+ * @return bool
+ */
+ protected static function validateRouteRegistration(array $input): bool
+ {
+ if (
+ !array_key_exists('method', $input)
+ || !in_array(strtoupper($input['method']), ['GET', 'PUT', 'PATCH', 'POST', 'DELETE'])
+ ) {
+ return false;
+ }
+
+ if (!array_key_exists('route', $input) || !preg_match('#^[a-z\d/\.\-_]+$#', $input['route'])) {
+ return false;
+ }
+
+ if (!array_key_exists('callable', $input)) {
+ return false;
+ }
+
+ return true;
+ }