]> BookStack Code Mirror - bookstack/blob - app/References/ReferenceUpdater.php
Merge pull request #5917 from BookStackApp/copy_references
[bookstack] / app / References / ReferenceUpdater.php
1 <?php
2
3 namespace BookStack\References;
4
5 use BookStack\Entities\Models\Book;
6 use BookStack\Entities\Models\HasDescriptionInterface;
7 use BookStack\Entities\Models\Entity;
8 use BookStack\Entities\Models\Page;
9 use BookStack\Entities\Repos\RevisionRepo;
10 use BookStack\Util\HtmlDocument;
11
12 class ReferenceUpdater
13 {
14     public function __construct(
15         protected ReferenceFetcher $referenceFetcher,
16         protected RevisionRepo $revisionRepo,
17     ) {
18     }
19
20     public function updateEntityReferences(Entity $entity, string $oldLink): void
21     {
22         $references = $this->getReferencesToUpdate($entity);
23         $newLink = $entity->getUrl();
24
25         foreach ($references as $reference) {
26             /** @var Entity $entity */
27             $entity = $reference->from;
28             $this->updateReferencesWithinEntity($entity, $oldLink, $newLink);
29         }
30     }
31
32     /**
33      * Change existing references for a range of entities using the given context.
34      */
35     public function changeReferencesUsingContext(ReferenceChangeContext $context): void
36     {
37         $bindings = [];
38         foreach ($context->getOldEntities() as $old) {
39             $bindings[] = $old->getMorphClass();
40             $bindings[] = $old->id;
41         }
42
43         // No targets to update within the context, so no need to continue.
44         if (count($bindings) < 2) {
45             return;
46         }
47
48         $toReferenceQuery = '(to_type, to_id) IN (' . rtrim(str_repeat('(?,?),', count($bindings) / 2), ',') . ')';
49
50         // Cycle each new entity in the context
51         foreach ($context->getNewEntities() as $new) {
52             // For each, get all references from it which lead to other items within the context of the change
53             $newReferencesInContext = $new->referencesFrom()->whereRaw($toReferenceQuery, $bindings)->get();
54             // For each reference, update the URL and the reference entry
55             foreach ($newReferencesInContext as $reference) {
56                 $oldToEntity = $reference->to;
57                 $newToEntity = $context->getNewForOld($oldToEntity);
58                 if ($newToEntity === null) {
59                     continue;
60                 }
61
62                 $this->updateReferencesWithinEntity($new, $oldToEntity->getUrl(), $newToEntity->getUrl());
63                 if ($newToEntity instanceof Page && $oldToEntity instanceof Page) {
64                     $this->updateReferencesWithinEntity($new, $oldToEntity->getPermalink(), $newToEntity->getPermalink());
65                 }
66                 $reference->to_id = $newToEntity->id;
67                 $reference->to_type = $newToEntity->getMorphClass();
68                 $reference->save();
69             }
70         }
71     }
72
73     /**
74      * @return Reference[]
75      */
76     protected function getReferencesToUpdate(Entity $entity): array
77     {
78         /** @var Reference[] $references */
79         $references = $this->referenceFetcher->getReferencesToEntity($entity, true)->values()->all();
80
81         if ($entity instanceof Book) {
82             $pages = $entity->pages()->get(['id']);
83             $chapters = $entity->chapters()->get(['id']);
84             $children = $pages->concat($chapters);
85             foreach ($children as $bookChild) {
86                 /** @var Reference[] $childRefs */
87                 $childRefs = $this->referenceFetcher->getReferencesToEntity($bookChild, true)->values()->all();
88                 array_push($references, ...$childRefs);
89             }
90         }
91
92         $deduped = [];
93         foreach ($references as $reference) {
94             $key = $reference->from_id . ':' . $reference->from_type;
95             $deduped[$key] = $reference;
96         }
97
98         return array_values($deduped);
99     }
100
101     protected function updateReferencesWithinEntity(Entity $entity, string $oldLink, string $newLink): void
102     {
103         if ($entity instanceof Page) {
104             $this->updateReferencesWithinPage($entity, $oldLink, $newLink);
105         }
106
107         if ($entity instanceof HasDescriptionInterface) {
108             $this->updateReferencesWithinDescription($entity, $oldLink, $newLink);
109         }
110     }
111
112     protected function updateReferencesWithinDescription(Entity&HasDescriptionInterface $entity, string $oldLink, string $newLink): void
113     {
114         $description = $entity->descriptionInfo();
115         $html = $this->updateLinksInHtml($description->getHtml(true) ?: '', $oldLink, $newLink);
116         $description->set($html);
117         $entity->save();
118     }
119
120     protected function updateReferencesWithinPage(Page $page, string $oldLink, string $newLink): void
121     {
122         $page = (clone $page)->refresh();
123         $html = $this->updateLinksInHtml($page->html, $oldLink, $newLink);
124         $markdown = $this->updateLinksInMarkdown($page->markdown, $oldLink, $newLink);
125
126         $page->html = $html;
127         $page->markdown = $markdown;
128         $page->revision_count++;
129         $page->save();
130
131         $summary = trans('entities.pages_references_update_revision');
132         $this->revisionRepo->storeNewForPage($page, $summary);
133     }
134
135     protected function updateLinksInMarkdown(string $markdown, string $oldLink, string $newLink): string
136     {
137         if (empty($markdown)) {
138             return $markdown;
139         }
140
141         $commonLinkRegex = '/(\[.*?\]\()' . preg_quote($oldLink, '/') . '(.*?\))/i';
142         $markdown = preg_replace($commonLinkRegex, '$1' . $newLink . '$2', $markdown);
143
144         $referenceLinkRegex = '/(\[.*?\]:\s?)' . preg_quote($oldLink, '/') . '(.*?)($|\s)/i';
145         $markdown = preg_replace($referenceLinkRegex, '$1' . $newLink . '$2$3', $markdown);
146
147         return $markdown;
148     }
149
150     protected function updateLinksInHtml(string $html, string $oldLink, string $newLink): string
151     {
152         if (empty($html)) {
153             return $html;
154         }
155
156         $doc = new HtmlDocument($html);
157         $anchors = $doc->queryXPath('//a[@href]');
158
159         /** @var \DOMElement $anchor */
160         foreach ($anchors as $anchor) {
161             $link = $anchor->getAttribute('href');
162             $updated = str_ireplace($oldLink, $newLink, $link);
163             $anchor->setAttribute('href', $updated);
164         }
165
166         return $doc->getBodyInnerHtml();
167     }
168 }