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\EntityContainerData;
9 use BookStack\Entities\Models\Page;
10 use BookStack\Entities\Repos\RevisionRepo;
11 use BookStack\Util\HtmlDocument;
13 class ReferenceUpdater
15 public function __construct(
16 protected ReferenceFetcher $referenceFetcher,
17 protected RevisionRepo $revisionRepo,
21 public function updateEntityReferences(Entity $entity, string $oldLink): void
23 $references = $this->getReferencesToUpdate($entity);
24 $newLink = $entity->getUrl();
26 foreach ($references as $reference) {
27 /** @var Entity $entity */
28 $entity = $reference->from;
29 $this->updateReferencesWithinEntity($entity, $oldLink, $newLink);
36 protected function getReferencesToUpdate(Entity $entity): array
38 /** @var Reference[] $references */
39 $references = $this->referenceFetcher->getReferencesToEntity($entity, true)->values()->all();
41 if ($entity instanceof Book) {
42 $pages = $entity->pages()->get(['id']);
43 $chapters = $entity->chapters()->get(['id']);
44 $children = $pages->concat($chapters);
45 foreach ($children as $bookChild) {
46 /** @var Reference[] $childRefs */
47 $childRefs = $this->referenceFetcher->getReferencesToEntity($bookChild, true)->values()->all();
48 array_push($references, ...$childRefs);
53 foreach ($references as $reference) {
54 $key = $reference->from_id . ':' . $reference->from_type;
55 $deduped[$key] = $reference;
58 return array_values($deduped);
61 protected function updateReferencesWithinEntity(Entity $entity, string $oldLink, string $newLink): void
63 if ($entity instanceof Page) {
64 $this->updateReferencesWithinPage($entity, $oldLink, $newLink);
67 if ($entity instanceof HasDescriptionInterface) {
68 $this->updateReferencesWithinDescription($entity, $oldLink, $newLink);
72 protected function updateReferencesWithinDescription(Entity&HasDescriptionInterface $entity, string $oldLink, string $newLink): void
74 $description = $entity->descriptionInfo();
75 $html = $this->updateLinksInHtml($description->getHtml(true) ?: '', $oldLink, $newLink);
76 $description->set($html);
80 protected function updateReferencesWithinPage(Page $page, string $oldLink, string $newLink): void
82 $page = (clone $page)->refresh();
83 $html = $this->updateLinksInHtml($page->html, $oldLink, $newLink);
84 $markdown = $this->updateLinksInMarkdown($page->markdown, $oldLink, $newLink);
87 $page->markdown = $markdown;
88 $page->revision_count++;
91 $summary = trans('entities.pages_references_update_revision');
92 $this->revisionRepo->storeNewForPage($page, $summary);
95 protected function updateLinksInMarkdown(string $markdown, string $oldLink, string $newLink): string
97 if (empty($markdown)) {
101 $commonLinkRegex = '/(\[.*?\]\()' . preg_quote($oldLink, '/') . '(.*?\))/i';
102 $markdown = preg_replace($commonLinkRegex, '$1' . $newLink . '$2', $markdown);
104 $referenceLinkRegex = '/(\[.*?\]:\s?)' . preg_quote($oldLink, '/') . '(.*?)($|\s)/i';
105 $markdown = preg_replace($referenceLinkRegex, '$1' . $newLink . '$2$3', $markdown);
110 protected function updateLinksInHtml(string $html, string $oldLink, string $newLink): string
116 $doc = new HtmlDocument($html);
117 $anchors = $doc->queryXPath('//a[@href]');
119 /** @var \DOMElement $anchor */
120 foreach ($anchors as $anchor) {
121 $link = $anchor->getAttribute('href');
122 $updated = str_ireplace($oldLink, $newLink, $link);
123 $anchor->setAttribute('href', $updated);
126 return $doc->getBodyInnerHtml();