/
FileLinkTracking.php
244 lines (216 loc) · 7.44 KB
/
FileLinkTracking.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
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
<?php
namespace SilverStripe\Assets\Shortcodes;
use DOMElement;
use SilverStripe\Assets\File;
use SilverStripe\Dev\Deprecation;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\FormScaffolder;
use SilverStripe\ORM\DataExtension;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\FieldType\DBHTMLText;
use SilverStripe\ORM\ManyManyList;
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 a relation which links to File items
* referenced in any HTMLText fields, and a boolean to indicate if there are any broken file links. Call
* augmentSyncFileLinkTracking 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.
*
* @property DataObject|FileLinkTracking $owner
* @method ManyManyList|File[] FileTracking() List of files linked on this dataobject
*/
class FileLinkTracking extends DataExtension
{
/**
* @var FileLinkTrackingParser
*/
protected $fileParser;
/**
* Inject parser for each page
*
* @var array
* @config
*/
private static $dependencies = [
'FileParser' => '%$' . FileLinkTrackingParser::class,
];
private static $owns = [
'FileTracking',
];
private static $many_many = [
'FileTracking' => [
'through' => FileLink::class,
'from' => 'Parent',
'to' => 'Linked',
],
];
/**
* Controls visibility of the File Tracking tab
*
* @config
* @see linktracking.yml
* @var boolean
*/
private static $show_file_link_tracking = false;
/**
* @deprecated 1.2.0 Use FileTracking() instead
* @return File[]|ManyManyList
*/
public function ImageTracking()
{
Deprecation::notice('1.2.0', 'Use FileTracking() instead');
return $this->FileTracking();
}
/**
* FileParser for link tracking
*
* @return FileLinkTrackingParser
*/
public function getFileParser()
{
return $this->fileParser;
}
/**
* @param FileLinkTrackingParser $parser
* @return $this
*/
public function setFileParser(FileLinkTrackingParser $parser = null)
{
$this->fileParser = $parser;
return $this;
}
public function onBeforeWrite()
{
// Trigger link tracking
// Note: SiteTreeLinkTracking::onBeforeWrite() has a check to
// prevent this being triggered multiple times on a single write.
$this->owner->syncLinkTracking();
}
/**
* Public method to call when triggering symlink extension. Can be called externally,
* or overridden by class implementations.
*
* {@see FileLinkTracking::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 (class_exists(Versioned::class) &&
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;
$hasTrackedFields = false;
foreach ($allFields as $field => $fieldSpec) {
$fieldObj = $this->owner->dbObject($field);
if ($fieldObj instanceof DBHTMLText) {
$hasTrackedFields = true;
// Merge links in this field with global list.
$linksInField = $this->trackLinksInField($field, $anyBroken);
$linkedPages = array_merge($linkedPages, $linksInField);
}
}
if (!$hasTrackedFields) {
return;
}
// Soft support for HasBrokenFile db field (e.g. SiteTree)
if ($this->owner->hasField('HasBrokenFile')) {
$this->owner->HasBrokenFile = $anyBroken;
}
// Update the "FileTracking" many_many.
$this->owner->FileTracking()->setByIDList($linkedPages);
}
public function onAfterDelete()
{
// If owner is versioned, skip tracking on live
if (class_exists(Versioned::class) &&
Versioned::get_stage() == Versioned::LIVE &&
$this->owner->hasExtension(Versioned::class)
) {
return;
}
$this->owner->FileTracking()->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
$record = $this->owner;
$htmlValue = HTMLValue::create($record->$fieldName);
// Process all links
$linkedFiles = [];
$links = $this->fileParser->process($htmlValue);
foreach ($links as $link) {
// Toggle highlight class to element
if ($link['DOMReference']) {
$this->toggleElementClass($link['DOMReference'], 'ss-broken', $link['Broken']);
}
// Flag broken
if ($link['Broken']) {
$anyBroken = true;
}
// Collect page ids
if ($link['Target'] && in_array($link['Type'], ['file', 'image'])) {
$fileID = (int)$link['Target'];
$linkedFiles[$fileID] = $fileID;
}
}
// Update any changed content
$record->$fieldName = $htmlValue->getContent();
return $linkedFiles;
}
/**
* 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_file_link_tracking')) {
$fields->removeByName('FileTracking');
} elseif ($this->owner->ID && !$this->owner->getField('FileTracking')) {
FormScaffolder::addManyManyRelationshipFields($fields, 'FileTracking', null, true, $this->owner);
}
}
}