1 | <?php |
||||
2 | /** |
||||
3 | * class DataObjectExtension|Firesphere\SolrSearch\Extensions\DataObjectExtension Adds checking if changes should be |
||||
4 | * pushed to Solr |
||||
5 | * |
||||
6 | * @package Firesphere\Solr\Search |
||||
7 | * @author Simon `Firesphere` Erkelens; Marco `Sheepy` Hermo |
||||
8 | * @copyright Copyright (c) 2018 - now() Firesphere & Sheepy |
||||
9 | */ |
||||
10 | |||||
11 | namespace Firesphere\SolrSearch\Extensions; |
||||
12 | |||||
13 | use Exception; |
||||
14 | use Firesphere\SolrSearch\Helpers\SolrLogger; |
||||
15 | use Firesphere\SolrSearch\Models\DirtyClass; |
||||
16 | use Firesphere\SolrSearch\Services\SolrCoreService; |
||||
17 | use Firesphere\SolrSearch\Tests\DataObjectExtensionTest; |
||||
18 | use Psr\Log\LoggerInterface; |
||||
19 | use Psr\SimpleCache\InvalidArgumentException; |
||||
20 | use ReflectionException; |
||||
21 | use SilverStripe\CMS\Model\SiteTree; |
||||
22 | use SilverStripe\Control\Controller; |
||||
23 | use SilverStripe\Core\Injector\Injector; |
||||
24 | use SilverStripe\ORM\ArrayList; |
||||
25 | use SilverStripe\ORM\DataExtension; |
||||
26 | use SilverStripe\ORM\DataObject; |
||||
27 | use SilverStripe\ORM\ValidationException; |
||||
28 | use SilverStripe\Security\InheritedPermissionsExtension; |
||||
29 | use SilverStripe\SiteConfig\SiteConfig; |
||||
30 | use SilverStripe\Versioned\Versioned; |
||||
31 | use Solarium\Exception\HttpException; |
||||
32 | |||||
33 | /** |
||||
34 | * Class \Firesphere\SolrSearch\Compat\DataObjectExtension |
||||
35 | * |
||||
36 | * Extend every DataObject with the option to update the index. |
||||
37 | * |
||||
38 | * @package Firesphere\Solr\Search |
||||
39 | * @property DataObject|DataObjectExtension $owner |
||||
40 | */ |
||||
41 | class DataObjectExtension extends DataExtension |
||||
42 | { |
||||
43 | /** |
||||
44 | * @var array Cached permission list |
||||
45 | */ |
||||
46 | public static $cachedClasses; |
||||
47 | /** |
||||
48 | * @var SiteConfig Current siteconfig |
||||
49 | */ |
||||
50 | protected static $siteConfig; |
||||
51 | |||||
52 | /** |
||||
53 | * Push the item to solr if it is not versioned |
||||
54 | * Update the index after write. |
||||
55 | * |
||||
56 | * @throws ValidationException |
||||
57 | * @throws HTTPException |
||||
58 | * @throws ReflectionException |
||||
59 | * @throws InvalidArgumentException |
||||
60 | */ |
||||
61 | 87 | public function onAfterWrite() |
|||
62 | { |
||||
63 | /** @var DataObject $owner */ |
||||
64 | 87 | $owner = $this->owner; |
|||
65 | |||||
66 | 87 | if ($this->shouldPush() && !$owner->hasExtension(Versioned::class)) { |
|||
67 | 85 | $this->pushToSolr($owner); |
|||
68 | } |
||||
69 | 87 | } |
|||
70 | |||||
71 | /** |
||||
72 | * Should this write be pushed to Solr |
||||
73 | * @return bool |
||||
74 | */ |
||||
75 | 87 | protected function shouldPush() |
|||
76 | { |
||||
77 | 87 | if (!Controller::has_curr()) { |
|||
78 | return false; |
||||
79 | } |
||||
80 | 87 | $request = Controller::curr()->getRequest(); |
|||
81 | |||||
82 | 87 | return (!($request->getURL() && |
|||
83 | 87 | strpos('dev/build', $request->getURL()) !== false)); |
|||
84 | } |
||||
85 | |||||
86 | /** |
||||
87 | * Try to push the newly updated item to Solr |
||||
88 | * |
||||
89 | * @param DataObject $owner |
||||
90 | * @throws ValidationException |
||||
91 | * @throws HTTPException |
||||
92 | * @throws ReflectionException |
||||
93 | * @throws InvalidArgumentException |
||||
94 | */ |
||||
95 | 85 | protected function pushToSolr(DataObject $owner) |
|||
96 | { |
||||
97 | 85 | $service = new SolrCoreService(); |
|||
98 | 85 | if (!$service->isValidClass($owner->ClassName)) { |
|||
99 | 85 | return; |
|||
100 | } |
||||
101 | |||||
102 | /** @var DataObject $owner */ |
||||
103 | 3 | $record = $this->getDirtyClass(SolrCoreService::UPDATE_TYPE); |
|||
104 | |||||
105 | 3 | $ids = json_decode($record->IDs, 1) ?: []; |
|||
106 | 3 | $mode = Versioned::get_reading_mode(); |
|||
107 | try { |
||||
108 | 3 | Versioned::set_reading_mode(Versioned::LIVE); |
|||
109 | 3 | $service->setDebug(false); |
|||
110 | 3 | $type = SolrCoreService::UPDATE_TYPE; |
|||
111 | // If the object should not show in search, remove it |
||||
112 | 3 | if ($owner->ShowInSearch !== null && (bool)$owner->ShowInSearch === false) { |
|||
113 | 1 | $type = SolrCoreService::DELETE_TYPE; |
|||
114 | } |
||||
115 | 3 | $service->updateItems(ArrayList::create([$owner]), $type); |
|||
116 | // If we don't get an exception, mark the item as clean |
||||
117 | // Added bonus, array_flip removes duplicates |
||||
118 | 3 | $this->clearIDs($owner, $ids, $record); |
|||
119 | // @codeCoverageIgnoreStart |
||||
120 | } catch (Exception $error) { |
||||
121 | Versioned::set_reading_mode($mode); |
||||
122 | $this->registerException($ids, $record, $error); |
||||
123 | } |
||||
124 | // @codeCoverageIgnoreEnd |
||||
125 | 3 | Versioned::set_reading_mode($mode); |
|||
126 | 3 | } |
|||
127 | |||||
128 | /** |
||||
129 | * Find or create a new DirtyClass for recording dirty IDs |
||||
130 | * |
||||
131 | * @param string $type |
||||
132 | * @return DirtyClass |
||||
133 | * @throws ValidationException |
||||
134 | */ |
||||
135 | 7 | protected function getDirtyClass(string $type) |
|||
136 | { |
||||
137 | // Get the DirtyClass object for this item |
||||
138 | /** @var null|DirtyClass $record */ |
||||
139 | 7 | $record = DirtyClass::get()->filter(['Class' => $this->owner->ClassName, 'Type' => $type])->first(); |
|||
0 ignored issues
–
show
Bug
Best Practice
introduced
by
![]() |
|||||
140 | 7 | if (!$record || !$record->exists()) { |
|||
141 | 6 | $record = DirtyClass::create([ |
|||
142 | 6 | 'Class' => $this->owner->ClassName, |
|||
143 | 6 | 'Type' => $type, |
|||
144 | ]); |
||||
145 | 6 | $record->write(); |
|||
146 | } |
||||
147 | |||||
148 | 7 | return $record; |
|||
149 | } |
||||
150 | |||||
151 | /** |
||||
152 | * Remove the owner ID from the dirty ID set |
||||
153 | * |
||||
154 | * @param DataObject $owner |
||||
155 | * @param array $ids |
||||
156 | * @param DirtyClass $record |
||||
157 | * @throws ValidationException |
||||
158 | */ |
||||
159 | 7 | protected function clearIDs(DataObject $owner, array $ids, DirtyClass $record): void |
|||
160 | { |
||||
161 | 7 | $values = array_flip($ids); |
|||
162 | 7 | unset($values[$owner->ID]); |
|||
163 | |||||
164 | 7 | $record->IDs = json_encode(array_keys($values)); |
|||
165 | 7 | $record->write(); |
|||
166 | 7 | } |
|||
167 | |||||
168 | /** |
||||
169 | * Register the exception of the attempted index for later clean-up use |
||||
170 | * |
||||
171 | * @codeCoverageIgnore This is actually tested through reflection. See {@link DataObjectExtensionTest} |
||||
172 | * @param array $ids |
||||
173 | * @param DirtyClass $record |
||||
174 | * @param Exception $error |
||||
175 | * @throws ValidationException |
||||
176 | * @throws HTTPException |
||||
177 | */ |
||||
178 | protected function registerException(array $ids, DirtyClass $record, Exception $error): void |
||||
179 | { |
||||
180 | /** @var DataObject $owner */ |
||||
181 | $owner = $this->owner; |
||||
182 | $ids[] = $owner->ID; |
||||
183 | // If we don't get an exception, mark the item as clean |
||||
184 | $record->IDs = json_encode($ids); |
||||
185 | $record->write(); |
||||
186 | $logger = Injector::inst()->get(LoggerInterface::class); |
||||
187 | $logger->warn( |
||||
188 | sprintf( |
||||
189 | 'Unable to alter %s with ID %s', |
||||
190 | $owner->ClassName, |
||||
191 | $owner->ID |
||||
192 | ) |
||||
193 | ); |
||||
194 | $solrLogger = new SolrLogger(); |
||||
195 | $solrLogger->saveSolrLog('Index'); |
||||
196 | |||||
197 | $logger->error($error->getMessage()); |
||||
198 | } |
||||
199 | |||||
200 | /** |
||||
201 | * Reindex this owner object in Solr |
||||
202 | * This is a simple stub for the push method, for semantic reasons |
||||
203 | * It should never be called on Objects that are not a valid class for any Index |
||||
204 | * It does not check if the class is valid to be pushed to Solr |
||||
205 | * |
||||
206 | * @throws HTTPException |
||||
207 | * @throws ReflectionException |
||||
208 | * @throws ValidationException |
||||
209 | * @throws InvalidArgumentException |
||||
210 | */ |
||||
211 | 1 | public function doReindex() |
|||
212 | { |
||||
213 | 1 | $this->pushToSolr($this->owner); |
|||
0 ignored issues
–
show
It seems like
$this->owner can also be of type Firesphere\SolrSearch\Ex...ons\DataObjectExtension ; however, parameter $owner of Firesphere\SolrSearch\Ex...Extension::pushToSolr() does only seem to accept SilverStripe\ORM\DataObject , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
214 | 1 | } |
|||
215 | |||||
216 | /** |
||||
217 | * Push the item to Solr after publishing |
||||
218 | * |
||||
219 | * @throws ValidationException |
||||
220 | * @throws HTTPException |
||||
221 | * @throws ReflectionException |
||||
222 | * @throws InvalidArgumentException |
||||
223 | */ |
||||
224 | 4 | public function onAfterPublish() |
|||
225 | { |
||||
226 | 4 | if ($this->shouldPush()) { |
|||
227 | /** @var DataObject $owner */ |
||||
228 | 3 | $owner = $this->owner; |
|||
229 | 3 | $this->pushToSolr($owner); |
|||
230 | } |
||||
231 | 4 | } |
|||
232 | |||||
233 | /** |
||||
234 | * Attempt to remove the item from Solr |
||||
235 | * |
||||
236 | * @throws ValidationException |
||||
237 | * @throws HTTPException |
||||
238 | */ |
||||
239 | 4 | public function onAfterDelete(): void |
|||
240 | { |
||||
241 | /** @var DataObject $owner */ |
||||
242 | 4 | $owner = $this->owner; |
|||
243 | /** @var DirtyClass $record */ |
||||
244 | 4 | $record = $this->getDirtyClass(SolrCoreService::DELETE_TYPE); |
|||
245 | |||||
246 | 4 | $ids = json_decode($record->IDs, 1) ?: []; |
|||
247 | |||||
248 | try { |
||||
249 | 4 | (new SolrCoreService()) |
|||
250 | 4 | ->updateItems(ArrayList::create([$owner]), SolrCoreService::DELETE_TYPE); |
|||
251 | // If successful, remove it from the array |
||||
252 | // Added bonus, array_flip removes duplicates |
||||
253 | 4 | $this->clearIDs($owner, $ids, $record); |
|||
254 | // @codeCoverageIgnoreStart |
||||
255 | } catch (Exception $error) { |
||||
256 | $this->registerException($ids, $record, $error); |
||||
257 | } |
||||
258 | // @codeCoverageIgnoreEnd |
||||
259 | 4 | } |
|||
260 | |||||
261 | /** |
||||
262 | * Get the view status for each member in this object |
||||
263 | * |
||||
264 | * @return array |
||||
265 | */ |
||||
266 | 9 | public function getViewStatus(): array |
|||
267 | { |
||||
268 | // return as early as possible |
||||
269 | /** @var DataObject|SiteTree $owner */ |
||||
270 | 9 | $owner = $this->owner; |
|||
271 | 9 | if (isset(static::$cachedClasses[$owner->ClassName])) { |
|||
272 | 1 | return static::$cachedClasses[$owner->ClassName]; |
|||
273 | } |
||||
274 | |||||
275 | // Make sure the siteconfig is loaded |
||||
276 | 9 | if (!static::$siteConfig) { |
|||
277 | 1 | static::$siteConfig = SiteConfig::current_site_config(); |
|||
278 | } |
||||
279 | // Return false if it's not allowed to show in search |
||||
280 | // The setting needs to be explicitly false, to avoid any possible collision |
||||
281 | // with objects not having the setting, thus being `null` |
||||
282 | // Return immediately if the owner has ShowInSearch not being `null` |
||||
283 | 9 | if ($owner->ShowInSearch === false || $owner->ShowInSearch === 0) { |
|||
284 | 1 | return ['false']; |
|||
285 | } |
||||
286 | |||||
287 | 9 | $permissions = $this->getGroupViewPermissions($owner); |
|||
288 | |||||
289 | 9 | if (!$owner->hasExtension(InheritedPermissionsExtension::class)) { |
|||
290 | 1 | static::$cachedClasses[$owner->ClassName] = $permissions; |
|||
291 | } |
||||
292 | |||||
293 | 9 | return $permissions; |
|||
294 | } |
||||
295 | |||||
296 | /** |
||||
297 | * Determine the view permissions based on group settings |
||||
298 | * |
||||
299 | * @param DataObject|SiteTree|SiteConfig $owner |
||||
300 | * @return array |
||||
301 | */ |
||||
302 | 9 | protected function getGroupViewPermissions($owner): array |
|||
303 | { |
||||
304 | // Switches are not ideal, but it's a lot more readable this way! |
||||
305 | 9 | switch ($owner->CanViewType) { |
|||
306 | 9 | case 'LoggedInUsers': |
|||
307 | 1 | $return = ['false', 'LoggedIn']; |
|||
308 | 1 | break; |
|||
309 | 9 | case 'OnlyTheseUsers': |
|||
310 | 1 | $return = ['false']; |
|||
311 | 1 | $return = array_merge($return, $owner->ViewerGroups()->column('Code')); |
|||
312 | 1 | break; |
|||
313 | 9 | case 'Inherit': |
|||
314 | 9 | $parent = !$owner->ParentID ? static::$siteConfig : $owner->Parent(); |
|||
315 | 9 | $return = $this->getGroupViewPermissions($parent); |
|||
316 | 9 | break; |
|||
317 | 9 | case 'Anyone': // View is either not implemented, or it's "Anyone" |
|||
318 | 9 | $return = ['null']; |
|||
319 | 9 | break; |
|||
320 | default: |
||||
321 | // Default to "Anyone can view" |
||||
322 | 1 | $return = ['null']; |
|||
323 | } |
||||
324 | |||||
325 | 9 | return $return; |
|||
326 | } |
||||
327 | } |
||||
328 |