-
Notifications
You must be signed in to change notification settings - Fork 333
/
SiteTreeLinkTracking.php
217 lines (192 loc) · 6.96 KB
/
SiteTreeLinkTracking.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
<?php
namespace SilverStripe\CMS\Model;
use DOMElement;
use SilverStripe\Assets\Shortcodes\FileLinkTracking;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\FormScaffolder;
use SilverStripe\ORM\DataExtension;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\FieldType\DBHTMLText;
use SilverStripe\ORM\ManyManyThroughList;
use SilverStripe\Versioned\Versioned;
use SilverStripe\View\Parsers\HTMLValue;
/**
* Adds tracking of links in any HTMLText fields which reference SiteTree or File items.
*
* Attaching this to any DataObject will add four fields which contain all links to SiteTree and File items
* referenced in any HTMLText fields, and two booleans to indicate if there are any broken links. Call
* augmentSyncLinkTracking to update those fields with any changes to those fields.
*
* Note that since both SiteTree and File are versioned, LinkTracking and FileTracking will
* only be enabled for the Stage record.
*
* Note: To support `HasBrokenLink` for non-SiteTree classes, add a boolean `HasBrokenLink`
* field to your `db` config and this extension will ensure it's flagged appropriately.
*
* @property DataObject|SiteTreeLinkTracking $owner
* @method ManyManyThroughList LinkTracking() List of site pages linked on this dataobject
*/
class SiteTreeLinkTracking extends DataExtension
{
/**
* @var SiteTreeLinkTracking_Parser
*/
protected $parser;
/**
* Inject parser for each page
*
* @var array
* @config
*/
private static $dependencies = [
'Parser' => '%$' . SiteTreeLinkTracking_Parser::class
];
private static $many_many = [
"LinkTracking" => [
'through' => SiteTreeLink::class,
'from' => 'Parent',
'to' => 'Linked',
],
];
/**
* Controls visibility of the Link Tracking tab
*
* @config
* @see linktracking.yml
* @var boolean
*/
private static $show_sitetree_link_tracking = false;
/**
* Parser for link tracking
*
* @return SiteTreeLinkTracking_Parser
*/
public function getParser()
{
return $this->parser;
}
/**
* @param SiteTreeLinkTracking_Parser $parser
* @return $this
*/
public function setParser(SiteTreeLinkTracking_Parser $parser = null)
{
$this->parser = $parser;
return $this;
}
public function onBeforeWrite()
{
// Trigger link tracking (unless this would also be triggered by FileLinkTracking)
if (!$this->owner->hasExtension(FileLinkTracking::class)) {
$this->owner->syncLinkTracking();
}
}
/**
* Public method to call when triggering symlink extension. Can be called externally,
* or overridden by class implementations.
*
* {@see SiteTreeLinkTracking::augmentSyncLinkTracking}
*/
public function syncLinkTracking()
{
$this->owner->extend('augmentSyncLinkTracking');
}
/**
* Find HTMLText fields on {@link owner} to scrape for links that need tracking
*/
public function augmentSyncLinkTracking()
{
// If owner is versioned, skip tracking on live
if (Versioned::get_stage() == Versioned::LIVE && $this->owner->hasExtension(Versioned::class)) {
return;
}
// Build a list of HTMLText fields, merging all linked pages together.
$allFields = DataObject::getSchema()->fieldSpecs($this->owner);
$linkedPages = [];
$anyBroken = false;
foreach ($allFields as $field => $fieldSpec) {
$fieldObj = $this->owner->dbObject($field);
if ($fieldObj instanceof DBHTMLText) {
// Merge links in this field with global list.
$linksInField = $this->trackLinksInField($field, $anyBroken);
$linkedPages = array_merge($linkedPages, $linksInField);
}
}
// Soft support for HasBrokenLink db field (e.g. SiteTree)
if ($this->owner->hasField('HasBrokenLink')) {
$this->owner->HasBrokenLink = $anyBroken;
}
// Update the "LinkTracking" many_many.
$this->owner->LinkTracking()->setByIDList($linkedPages);
}
public function onAfterDelete()
{
// If owner is versioned, skip tracking on live
if (Versioned::get_stage() == Versioned::LIVE && $this->owner->hasExtension(Versioned::class)) {
return;
}
$this->owner->LinkTracking()->removeAll();
}
/**
* Scrape the content of a field to detect anly links to local SiteTree pages or files
*
* @param string $fieldName The name of the field on {@link @owner} to scrape
* @param bool &$anyBroken Will be flagged to true (by reference) if a link is broken.
* @return int[] Array of page IDs found (associative array)
*/
public function trackLinksInField($fieldName, &$anyBroken = false)
{
// Pull down current field content
$htmlValue = HTMLValue::create($this->owner->$fieldName);
// Process all links
$linkedPages = [];
$links = $this->parser->process($htmlValue);
foreach ($links as $link) {
// Toggle highlight class to element
$this->toggleElementClass($link['DOMReference'], 'ss-broken', $link['Broken']);
// Flag broken
if ($link['Broken']) {
$anyBroken = true;
}
// Collect page ids
if ($link['Type'] === 'sitetree' && $link['Target']) {
$pageID = (int)$link['Target'];
$linkedPages[$pageID] = $pageID;
}
}
// Update any changed content
$this->owner->$fieldName = $htmlValue->getContent();
return $linkedPages;
}
/**
* Add the given css class to the DOM element.
*
* @param DOMElement $domReference Element to modify.
* @param string $class Class name to toggle.
* @param bool $toggle On or off.
*/
protected function toggleElementClass(DOMElement $domReference, $class, $toggle)
{
// Get all existing classes.
$classes = array_filter(explode(' ', trim($domReference->getAttribute('class'))));
// Add or remove the broken class from the link, depending on the link status.
if ($toggle) {
$classes = array_unique(array_merge($classes, [$class]));
} else {
$classes = array_diff($classes, [$class]);
}
if (!empty($classes)) {
$domReference->setAttribute('class', implode(' ', $classes));
} else {
$domReference->removeAttribute('class');
}
}
public function updateCMSFields(FieldList $fields)
{
if (!$this->owner->config()->get('show_sitetree_link_tracking')) {
$fields->removeByName('LinkTracking');
} elseif ($this->owner->ID && !$this->owner->getField('LinkTracking')) {
FormScaffolder::addManyManyRelationshipFields($fields, 'LinkTracking', null, true, $this->owner);
}
}
}