1 | <?php |
||
2 | |||
3 | namespace Dynamic\ViewableDataObject\Extensions; |
||
4 | |||
5 | use SilverStripe\CMS\Controllers\ModelAsController; |
||
6 | use SilverStripe\CMS\Controllers\RootURLController; |
||
7 | use SilverStripe\CMS\Forms\SiteTreeURLSegmentField; |
||
8 | use SilverStripe\CMS\Model\SiteTree; |
||
9 | use SilverStripe\Control\ContentNegotiator; |
||
10 | use SilverStripe\Control\Controller; |
||
11 | use SilverStripe\Control\Director; |
||
12 | use SilverStripe\Control\RequestHandler; |
||
13 | use SilverStripe\Core\Config\Config; |
||
14 | use SilverStripe\Forms\FieldList; |
||
15 | use SilverStripe\Forms\TextareaField; |
||
16 | use SilverStripe\Forms\TextField; |
||
17 | use SilverStripe\Forms\ToggleCompositeField; |
||
18 | use SilverStripe\ORM\ArrayList; |
||
19 | use SilverStripe\ORM\DataExtension; |
||
20 | use SilverStripe\ORM\DataObject; |
||
21 | use SilverStripe\ORM\FieldType\DBField; |
||
22 | use SilverStripe\ORM\FieldType\DBHTMLText; |
||
23 | use SilverStripe\SiteConfig\SiteConfig; |
||
24 | use SilverStripe\Versioned\Versioned; |
||
25 | use SilverStripe\View\ArrayData; |
||
26 | use SilverStripe\View\HTML; |
||
27 | use SilverStripe\View\Parsers\URLSegmentFilter; |
||
28 | use SilverStripe\View\SSViewer; |
||
29 | |||
30 | /** |
||
31 | * Class ViewableDataObject |
||
32 | * @package Dynamic\ViewableDataObject\Extensions |
||
33 | * |
||
34 | * @property string $Title |
||
35 | * @property string $MenuTitle |
||
36 | * @property string $URLSegment |
||
37 | * @property string $MetaTitle |
||
38 | * @property string $MetaDescription |
||
39 | */ |
||
40 | class ViewableDataObject extends DataExtension |
||
41 | { |
||
42 | /** |
||
43 | * @var array |
||
44 | */ |
||
45 | private static $db = [ |
||
46 | 'Title' => 'Varchar(255)', |
||
47 | 'MenuTitle' => 'Varchar(255)', |
||
48 | 'URLSegment' => 'Varchar(255)', |
||
49 | 'MetaTitle' => 'Varchar(255)', |
||
50 | 'MetaDescription' => 'Varchar(255)', |
||
51 | ]; |
||
52 | |||
53 | /** |
||
54 | * @var array |
||
55 | */ |
||
56 | private static $defaults = array( |
||
57 | 'Title' => 'New Item', |
||
58 | 'URLSegment' => 'new-item', |
||
59 | ); |
||
60 | |||
61 | /** |
||
62 | * @var array |
||
63 | */ |
||
64 | private static $indexes = [ |
||
65 | 'URLSegment' => true, |
||
66 | ]; |
||
67 | |||
68 | /** |
||
69 | * @var array |
||
70 | */ |
||
71 | private static $casting = array( |
||
72 | 'Breadcrumbs' => 'HTMLFragment', |
||
73 | 'Link' => 'Text', |
||
74 | 'RelativeLink' => 'Text', |
||
75 | 'AbsoluteLink' => 'Text', |
||
76 | 'MetaTags' => 'HTMLFragment', |
||
77 | ); |
||
78 | |||
79 | /** |
||
80 | * @param FieldList $fields |
||
81 | */ |
||
82 | 1 | public function updateCMSFields(FieldList $fields) |
|
83 | { |
||
84 | 1 | $fields->removeByName(array( |
|
85 | 1 | 'MenuTitle', |
|
86 | 'URLSegment', |
||
87 | 'MetaTitle', |
||
88 | 'MetaDescription', |
||
89 | )); |
||
90 | |||
91 | 1 | $fields->insertAfter( |
|
92 | 1 | TextField::create('MenuTitle'), |
|
93 | 1 | 'Title' |
|
94 | ); |
||
95 | |||
96 | 1 | if ($page = $this->hasParentPage()) { |
|
97 | 1 | $fields->insertAfter( |
|
98 | 1 | SiteTreeURLSegmentField::create('URLSegment') |
|
99 | 1 | ->setURLPrefix($page->Link() . $this->hasViewAction() . '/'), |
|
100 | 1 | 'MenuTitle' |
|
101 | ); |
||
102 | } |
||
103 | |||
104 | 1 | $fields->addFieldToTab( |
|
105 | 1 | 'Root.Main', |
|
106 | 1 | ToggleCompositeField::create( |
|
107 | 1 | 'Metadata', |
|
108 | 1 | 'Metadata', |
|
109 | array( |
||
110 | 1 | new TextField('MetaTitle', $this->owner->fieldLabel('MetaTitle')), |
|
111 | 1 | new TextareaField('MetaDescription', $this->owner->fieldLabel('MetaDescription')), |
|
112 | ) |
||
113 | ) |
||
114 | ); |
||
115 | } |
||
116 | |||
117 | /** |
||
118 | * @return bool |
||
119 | */ |
||
120 | 3 | public function hasParentPage() |
|
121 | { |
||
122 | 3 | if ($this->owner->hasMethod('getParentPage')) { |
|
123 | 3 | return $this->owner->getParentPage(); |
|
124 | } |
||
125 | |||
126 | return false; |
||
127 | } |
||
128 | |||
129 | /** |
||
130 | * @return string |
||
131 | */ |
||
132 | 4 | public function hasViewAction() |
|
133 | { |
||
134 | 4 | if ($this->owner->hasMethod('getViewAction')) { |
|
135 | 4 | return $this->owner->getViewAction(); |
|
136 | } |
||
137 | |||
138 | return 'view'; |
||
139 | } |
||
140 | |||
141 | /** |
||
142 | * @param null $action |
||
143 | * |
||
144 | * @return bool|string |
||
145 | */ |
||
146 | 2 | public function Link($action = null) |
|
147 | { |
||
148 | 2 | if ($this->hasParentPage()) { |
|
149 | 2 | return Controller::join_links( |
|
150 | 2 | $this->hasParentPage()->Link(), |
|
151 | 2 | $this->hasViewAction(), |
|
152 | 2 | $this->owner->RelativeLink($action) |
|
153 | ); |
||
154 | } |
||
155 | |||
156 | return false; |
||
157 | } |
||
158 | |||
159 | /** |
||
160 | * @param null $action |
||
161 | * |
||
162 | * @return string |
||
163 | */ |
||
164 | 1 | public function AbsoluteLink($action = null) |
|
165 | { |
||
166 | 1 | if ($this->owner->hasMethod('alternateAbsoluteLink')) { |
|
167 | return $this->owner->alternateAbsoluteLink($action); |
||
168 | } else { |
||
169 | 1 | return Director::absoluteURL($this->owner->Link($action)); |
|
170 | } |
||
171 | } |
||
172 | |||
173 | /** |
||
174 | * @param null $action |
||
175 | * |
||
176 | * @return string |
||
177 | */ |
||
178 | 2 | public function RelativeLink($action = null) |
|
179 | { |
||
180 | 2 | if ($this->owner->ParentID && SiteConfig::current_site_config()->nested_urls) { |
|
181 | $parent = $this->owner->Parent(); |
||
182 | // If page is removed select parent from version history (for archive page view) |
||
183 | if ((!$parent || !$parent->exists()) && !$this->owner->isOnDraft()) { |
||
184 | $parent = Versioned::get_latest_version($this->owner->ClassName, $this->owner->ParentID); |
||
185 | } |
||
186 | $base = $parent->RelativeLink($this->owner->URLSegment); |
||
187 | 2 | } elseif (!$action && $this->owner->URLSegment == RootURLController::get_homepage_link()) { |
|
188 | // Unset base for root-level homepages. |
||
189 | // Note: Homepages with action parameters (or $action === true) |
||
190 | // need to retain their URLSegment. |
||
191 | $base = null; |
||
192 | } else { |
||
193 | 2 | $base = $this->owner->URLSegment; |
|
194 | } |
||
195 | |||
196 | 2 | $this->owner->extend('updateRelativeLink', $base, $action); |
|
197 | |||
198 | // Legacy support: If $action === true, retain URLSegment for homepages, |
||
199 | // but don't append any action |
||
200 | 2 | if ($action === true) { |
|
201 | $action = null; |
||
202 | } |
||
203 | |||
204 | 2 | return Controller::join_links($base, '/', $action); |
|
205 | } |
||
206 | |||
207 | /** |
||
208 | * @return bool|mixed |
||
209 | */ |
||
210 | 2 | public function validURLSegment() |
|
211 | { |
||
212 | 2 | if (SiteConfig::current_site_config()->nested_urls && $parent = $this->owner->Parent()) { |
|
213 | if ($controller = ModelAsController::controller_for($parent)) { |
||
214 | if ($controller instanceof Controller && $controller->hasAction($this->owner->URLSegment)) { |
||
215 | return false; |
||
216 | } |
||
217 | } |
||
218 | } |
||
219 | |||
220 | 2 | if (!SiteConfig::current_site_config()->nested_urls || !$this->owner->ParentID) { |
|
221 | 2 | if (class_exists($this->owner->URLSegment) && |
|
222 | 2 | is_subclass_of($this->owner->URLSegment, RequestHandler::class)) { |
|
223 | return false; |
||
224 | } |
||
225 | } |
||
226 | |||
227 | // Filters by url, id, and parent |
||
228 | 2 | $table = DataObject::getSchema()->tableForField($this->owner->ClassName, 'URLSegment'); |
|
229 | 2 | $filter = array('"' . $table . '"."URLSegment"' => $this->owner->URLSegment); |
|
230 | 2 | if ($this->owner->ID) { |
|
231 | 2 | $filter['"' . $table . '"."ID" <> ?'] = $this->owner->ID; |
|
232 | } |
||
233 | 2 | if (SiteConfig::current_site_config()->nested_urls) { |
|
234 | $filter['"' . $table . '"."ParentID"'] = $this->owner->ParentID ? $this->owner->ParentID : 0; |
||
235 | } |
||
236 | |||
237 | // If any of the extensions return `0` consider the segment invalid |
||
238 | 2 | $extensionResponses = array_filter( |
|
239 | 2 | (array)$this->owner->extend('augmentValidURLSegment'), |
|
240 | 2 | function ($response) { |
|
241 | return !is_null($response); |
||
242 | 2 | } |
|
243 | ); |
||
244 | 2 | if ($extensionResponses) { |
|
0 ignored issues
–
show
|
|||
245 | return min($extensionResponses); |
||
246 | } |
||
247 | |||
248 | // Check existence |
||
249 | 2 | return !DataObject::get($this->owner->ClassName, $filter)->exists(); |
|
250 | } |
||
251 | |||
252 | /** |
||
253 | * @param $title |
||
254 | * |
||
255 | * @return string |
||
256 | */ |
||
257 | public function generateURLSegment($title) |
||
258 | { |
||
259 | $filter = URLSegmentFilter::create(); |
||
260 | $t = $filter->filter($title); |
||
261 | // Fallback to generic page name if path is empty (= no valid, convertable characters) |
||
262 | if (!$t || $t == '-' || $t == '-1') { |
||
263 | $t = "page-$this->owner->ID"; |
||
264 | } |
||
265 | // Hook for extensions |
||
266 | $this->owner->extend('updateURLSegment', $t, $title); |
||
267 | |||
268 | return $t; |
||
269 | } |
||
270 | |||
271 | /** |
||
272 | * Generate custom meta tags to display on the DataObject view page. |
||
273 | * |
||
274 | * @param bool $includeTitle |
||
275 | * |
||
276 | * @return DBField |
||
277 | */ |
||
278 | public function MetaTags($includeTitle = true) |
||
279 | { |
||
280 | $tags = array(); |
||
281 | |||
282 | if ($includeTitle && strtolower($includeTitle) != 'false') { |
||
283 | $title = $this->owner->MetaTitle |
||
284 | ? $this->owner->MetaTitle |
||
285 | : $this->owner->Title; |
||
286 | $tags[] = HTML::createTag('title', array(), $title); |
||
287 | } |
||
288 | $generator = trim(Config::inst()->get(SiteTree::class, 'meta_generator')); |
||
289 | if (!empty($generator)) { |
||
290 | $tags[] = HTML::createTag('meta', array( |
||
291 | 'name' => 'generator', |
||
292 | 'content' => $generator, |
||
293 | )); |
||
294 | } |
||
295 | |||
296 | $charset = ContentNegotiator::config()->uninherited('encoding'); |
||
297 | $tags[] = HTML::createTag('meta', array( |
||
298 | 'http-equiv' => 'Content-Type', |
||
299 | 'content' => 'text/html; charset=' . $charset, |
||
300 | )); |
||
301 | |||
302 | if ($this->owner->MetaDescription) { |
||
303 | $tags[] = HTML::createTag('meta', array( |
||
304 | 'name' => 'description', |
||
305 | 'content' => $this->owner->MetaDescription, |
||
306 | )); |
||
307 | } |
||
308 | |||
309 | $this->owner->extend('updateMetaTagsArray', $tags); |
||
310 | $tagString = implode("\n", $tags); |
||
311 | $this->owner->extend('updateMetaTags', $tagString); |
||
312 | |||
313 | return DBField::create_field('HTMLFragment', $tagString); |
||
314 | } |
||
315 | |||
316 | /** |
||
317 | * Produce the correct breadcrumb trail for use on the DataObject Item Page. |
||
318 | * |
||
319 | * @param int $maxDepth |
||
320 | * @param bool $unlinked |
||
321 | * @param bool $stopAtPageType |
||
322 | * @param bool $showHidden |
||
323 | * |
||
324 | * @return DBHTMLText |
||
325 | */ |
||
326 | 1 | public function Breadcrumbs($maxDepth = 20, $unlinked = false, $stopAtPageType = false, $showHidden = false) |
|
327 | { |
||
328 | 1 | $page = Controller::curr(); |
|
329 | 1 | $pages = array(); |
|
330 | 1 | $pages[] = $this->owner; |
|
331 | 1 | while ($page |
|
332 | 1 | && (!$maxDepth || count($pages) < $maxDepth) |
|
333 | 1 | && (!$stopAtPageType || $page->ClassName != $stopAtPageType) |
|
334 | ) { |
||
335 | 1 | if ($showHidden || $page->ShowInMenus || ($page->ID == $this->owner->ID)) { |
|
336 | $pages[] = $page; |
||
337 | } |
||
338 | 1 | $page = $page->Parent; |
|
339 | } |
||
340 | 1 | $template = new SSViewer('BreadcrumbsTemplate'); |
|
341 | |||
342 | 1 | return $template->process($this->owner->customise(new ArrayData(array( |
|
343 | 1 | 'Pages' => new ArrayList(array_reverse($pages)), |
|
344 | )))); |
||
345 | } |
||
346 | |||
347 | /** |
||
348 | * |
||
349 | */ |
||
350 | 1 | public function onBeforeWrite() |
|
351 | { |
||
352 | 1 | if (!$this->owner->URLSegment) { |
|
353 | $siteTree = singleton(SiteTree::class); |
||
354 | $this->owner->URLSegment = $siteTree->generateURLSegment($this->owner->Title); |
||
355 | } |
||
356 | |||
357 | // Ensure that this object has a non-conflicting URLSegment value. |
||
358 | 1 | $count = 2; |
|
359 | 1 | while (!$this->owner->validURLSegment()) { |
|
360 | 1 | $this->owner->URLSegment = preg_replace('/-[0-9]+$/', null, $this->owner->URLSegment) . '-' . $count; |
|
361 | 1 | ++$count; |
|
362 | } |
||
363 | |||
364 | 1 | parent::onBeforeWrite(); |
|
365 | } |
||
366 | } |
||
367 |
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.