3 namespace BookStack\References;
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;
12 class ReferenceUpdater
14 public function __construct(
15 protected ReferenceFetcher $referenceFetcher,
16 protected RevisionRepo $revisionRepo,
20 public function updateEntityReferences(Entity $entity, string $oldLink): void
22 $references = $this->getReferencesToUpdate($entity);
23 $newLink = $entity->getUrl();
25 foreach ($references as $reference) {
26 /** @var Entity $entity */
27 $entity = $reference->from;
28 $this->updateReferencesWithinEntity($entity, $oldLink, $newLink);
33 * Change existing references for a range of entities using the given context.
35 public function changeReferencesUsingContext(ReferenceChangeContext $context): void
38 foreach ($context->getOldEntities() as $old) {
39 $bindings[] = $old->getMorphClass();
40 $bindings[] = $old->id;
43 // No targets to update within the context, so no need to continue.
44 if (count($bindings) < 2) {
48 $toReferenceQuery = '(to_type, to_id) IN (' . rtrim(str_repeat('(?,?),', count($bindings) / 2), ',') . ')';
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) {
62 $this->updateReferencesWithinEntity($new, $oldToEntity->getUrl(), $newToEntity->getUrl());
63 if ($newToEntity instanceof Page && $oldToEntity instanceof Page) {
64 $this->updateReferencesWithinEntity($new, $oldToEntity->getPermalink(), $newToEntity->getPermalink());
66 $reference->to_id = $newToEntity->id;
67 $reference->to_type = $newToEntity->getMorphClass();
76 protected function getReferencesToUpdate(Entity $entity): array
78 /** @var Reference[] $references */
79 $references = $this->referenceFetcher->getReferencesToEntity($entity, true)->values()->all();
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);
93 foreach ($references as $reference) {
94 $key = $reference->from_id . ':' . $reference->from_type;
95 $deduped[$key] = $reference;
98 return array_values($deduped);
101 protected function updateReferencesWithinEntity(Entity $entity, string $oldLink, string $newLink): void
103 if ($entity instanceof Page) {
104 $this->updateReferencesWithinPage($entity, $oldLink, $newLink);
107 if ($entity instanceof HasDescriptionInterface) {
108 $this->updateReferencesWithinDescription($entity, $oldLink, $newLink);
112 protected function updateReferencesWithinDescription(Entity&HasDescriptionInterface $entity, string $oldLink, string $newLink): void
114 $description = $entity->descriptionInfo();
115 $html = $this->updateLinksInHtml($description->getHtml(true) ?: '', $oldLink, $newLink);
116 $description->set($html);
120 protected function updateReferencesWithinPage(Page $page, string $oldLink, string $newLink): void
122 $page = (clone $page)->refresh();
123 $html = $this->updateLinksInHtml($page->html, $oldLink, $newLink);
124 $markdown = $this->updateLinksInMarkdown($page->markdown, $oldLink, $newLink);
127 $page->markdown = $markdown;
128 $page->revision_count++;
131 $summary = trans('entities.pages_references_update_revision');
132 $this->revisionRepo->storeNewForPage($page, $summary);
135 protected function updateLinksInMarkdown(string $markdown, string $oldLink, string $newLink): string
137 if (empty($markdown)) {
141 $commonLinkRegex = '/(\[.*?\]\()' . preg_quote($oldLink, '/') . '(.*?\))/i';
142 $markdown = preg_replace($commonLinkRegex, '$1' . $newLink . '$2', $markdown);
144 $referenceLinkRegex = '/(\[.*?\]:\s?)' . preg_quote($oldLink, '/') . '(.*?)($|\s)/i';
145 $markdown = preg_replace($referenceLinkRegex, '$1' . $newLink . '$2$3', $markdown);
150 protected function updateLinksInHtml(string $html, string $oldLink, string $newLink): string
156 $doc = new HtmlDocument($html);
157 $anchors = $doc->queryXPath('//a[@href]');
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);
166 return $doc->getBodyInnerHtml();