]> BookStack Code Mirror - bookstack/blob - app/Exports/ZipExports/ZipReferenceParser.php
ZIP Imports: Added API examples, finished testing
[bookstack] / app / Exports / ZipExports / ZipReferenceParser.php
1 <?php
2
3 namespace BookStack\Exports\ZipExports;
4
5 use BookStack\App\Model;
6 use BookStack\Entities\Queries\EntityQueries;
7 use BookStack\References\ModelResolvers\AttachmentModelResolver;
8 use BookStack\References\ModelResolvers\BookLinkModelResolver;
9 use BookStack\References\ModelResolvers\ChapterLinkModelResolver;
10 use BookStack\References\ModelResolvers\CrossLinkModelResolver;
11 use BookStack\References\ModelResolvers\ImageModelResolver;
12 use BookStack\References\ModelResolvers\PageLinkModelResolver;
13 use BookStack\References\ModelResolvers\PagePermalinkModelResolver;
14 use BookStack\Uploads\ImageStorage;
15
16 class ZipReferenceParser
17 {
18     /**
19      * @var CrossLinkModelResolver[]|null
20      */
21     protected ?array $modelResolvers = null;
22
23     public function __construct(
24         protected EntityQueries $queries
25     ) {
26     }
27
28     /**
29      * Parse and replace references in the given content.
30      * Calls the handler for each model link detected and replaces the link
31      * with the handler return value if provided.
32      * Returns the resulting content with links replaced.
33      * @param callable(Model):(string|null) $handler
34      */
35     public function parseLinks(string $content, callable $handler): string
36     {
37         $linkRegex = $this->getLinkRegex();
38         $matches = [];
39         preg_match_all($linkRegex, $content, $matches);
40
41         if (count($matches) < 2) {
42             return $content;
43         }
44
45         foreach ($matches[1] as $link) {
46             $model = $this->linkToModel($link);
47             if ($model) {
48                 $result = $handler($model);
49                 if ($result !== null) {
50                     $content = str_replace($link, $result, $content);
51                 }
52             }
53         }
54
55         return $content;
56     }
57
58     /**
59      * Parse and replace references in the given content.
60      * Calls the handler for each reference detected and replaces the link
61      * with the handler return value if provided.
62      * Returns the resulting content string with references replaced.
63      * @param callable(string $type, int $id):(string|null) $handler
64      */
65     public function parseReferences(string $content, callable $handler): string
66     {
67         $referenceRegex = '/\[\[bsexport:([a-z]+):(\d+)]]/';
68         $matches = [];
69         preg_match_all($referenceRegex, $content, $matches);
70
71         if (count($matches) < 3) {
72             return $content;
73         }
74
75         for ($i = 0; $i < count($matches[0]); $i++) {
76             $referenceText = $matches[0][$i];
77             $type = strtolower($matches[1][$i]);
78             $id = intval($matches[2][$i]);
79             $result = $handler($type, $id);
80             if ($result !== null) {
81                 $content = str_replace($referenceText, $result, $content);
82             }
83         }
84
85         return $content;
86     }
87
88
89     /**
90      * Attempt to resolve the given link to a model using the instance model resolvers.
91      */
92     protected function linkToModel(string $link): ?Model
93     {
94         foreach ($this->getModelResolvers() as $resolver) {
95             $model = $resolver->resolve($link);
96             if (!is_null($model)) {
97                 return $model;
98             }
99         }
100
101         return null;
102     }
103
104     protected function getModelResolvers(): array
105     {
106         if (isset($this->modelResolvers)) {
107             return $this->modelResolvers;
108         }
109
110         $this->modelResolvers = [
111             new PagePermalinkModelResolver($this->queries->pages),
112             new PageLinkModelResolver($this->queries->pages),
113             new ChapterLinkModelResolver($this->queries->chapters),
114             new BookLinkModelResolver($this->queries->books),
115             new ImageModelResolver(),
116             new AttachmentModelResolver(),
117         ];
118
119         return $this->modelResolvers;
120     }
121
122     /**
123      * Build the regex to identify links we should handle in content.
124      */
125     protected function getLinkRegex(): string
126     {
127         $urls = [rtrim(url('/'), '/')];
128         $imageUrl = rtrim(ImageStorage::getPublicUrl('/'), '/');
129         if ($urls[0] !== $imageUrl) {
130             $urls[] = $imageUrl;
131         }
132
133
134         $urlBaseRegex = implode('|', array_map(function ($url) {
135             return preg_quote($url, '/');
136         }, $urls));
137
138         return "/(({$urlBaseRegex}).*?)[\\t\\n\\f>\"'=?#()]/";
139     }
140 }