Added the Porter Stemmer module to improve searches. This doesn't deal with some...
[yaffs-website] / web / modules / contrib / security_review / src / Check.php
1 <?php
2
3 namespace Drupal\security_review;
4
5 use Drupal\Core\DependencyInjection\DependencySerializationTrait;
6 use Drupal\Core\Logger\RfcLogLevel;
7 use Drupal\Core\StringTranslation\StringTranslationTrait;
8 use Drupal\user\Entity\User;
9
10 /**
11  * Defines a security check.
12  */
13 abstract class Check {
14
15   use DependencySerializationTrait;
16   use StringTranslationTrait;
17
18   /**
19    * The configuration storage for this check.
20    *
21    * @var \Drupal\Core\Config\Config $config
22    */
23   protected $config;
24
25   /**
26    * The service container.
27    *
28    * @var \Symfony\Component\DependencyInjection\ContainerInterface
29    */
30   protected $container;
31
32   /**
33    * Settings handler for this check.
34    *
35    * @var \Drupal\security_review\CheckSettingsInterface $settings
36    */
37   protected $settings;
38
39   /**
40    * The State system.
41    *
42    * @var \Drupal\Core\State\State
43    */
44   protected $state;
45
46   /**
47    * The check's prefix in the State system.
48    *
49    * @var string
50    */
51   protected $statePrefix;
52
53   /**
54    * Initializes the configuration storage and the settings handler.
55    */
56   public function __construct() {
57     $this->container = \Drupal::getContainer();
58
59     $this->config = $this->configFactory()
60       ->getEditable('security_review.check.' . $this->id());
61     $this->settings = new CheckSettings($this, $this->config);
62     $this->state = $this->container->get('state');
63     $this->statePrefix = 'security_review.check.' . $this->id() . '.';
64
65     // Set check ID in config.
66     if ($this->config->get('id') != $this->id()) {
67       $this->config->set('id', $this->id());
68       $this->config->save();
69     }
70   }
71
72   /**
73    * Returns the namespace of the check.
74    *
75    * Usually it's the same as the module's name.
76    *
77    * Naming rules (if overridden):
78    *   - All characters should be lowerspace.
79    *   - Use characters only from the english alphabet.
80    *   - Don't use spaces (use "_" instead).
81    *
82    * @return string
83    *   Machine namespace of the check.
84    */
85   public function getMachineNamespace() {
86     $namespace = strtolower($this->getNamespace());
87     $namespace = preg_replace("/[^a-z0-9 ]/", '', $namespace);
88     $namespace = str_replace(' ', '_', $namespace);
89
90     return $namespace;
91   }
92
93   /**
94    * Returns the namespace of the check.
95    *
96    * Usually it's the same as the module's name.
97    *
98    * @return string
99    *   Human-readable namespace of the check.
100    */
101   public abstract function getNamespace();
102
103   /**
104    * Returns the machine name of the check.
105    *
106    * Naming rules (if overridden):
107    *   - All characters should be lowerspace.
108    *   - Use characters only from the english alphabet.
109    *   - Don't use spaces (use "_" instead).
110    *
111    * @return string
112    *   ID of check.
113    */
114   public function getMachineTitle() {
115     $title = strtolower($this->getTitle());
116     $title = preg_replace("/[^a-z0-9 ]/", '', $title);
117     $title = str_replace(' ', '_', $title);
118
119     return $title;
120   }
121
122   /**
123    * Returns the human-readable title of the check.
124    *
125    * @return string
126    *   Title of check.
127    */
128   public abstract function getTitle();
129
130   /**
131    * Returns the identifier constructed using the namespace and title values.
132    *
133    * @return string
134    *   Unique identifier of the check.
135    */
136   public final function id() {
137     return $this->getMachineNamespace() . '-' . $this->getMachineTitle();
138   }
139
140   /**
141    * Returns whether the findings should be stored or reproduced when needed.
142    *
143    * The only case when this function should return false is if the check can
144    * generate a lot of findings (like the File permissions check for example).
145    * Turning this off for checks that don't generate findings at all or just a
146    * few of them actually means more overhead as the check has to be re-run
147    * in order to get its last result.
148    *
149    * @return bool
150    *   Boolean indicating whether findings will be stored.
151    */
152   public function storesFindings() {
153     return TRUE;
154   }
155
156   /**
157    * Returns the check-specific settings' handler.
158    *
159    * @return \Drupal\security_review\CheckSettingsInterface
160    *   The settings interface of the check.
161    */
162   public function settings() {
163     return $this->settings;
164   }
165
166   /**
167    * The actual procedure of carrying out the check.
168    *
169    * @return \Drupal\security_review\CheckResult
170    *   The result of running the check.
171    */
172   public abstract function run();
173
174   /**
175    * Same as run(), but used in CLI context such as Drush.
176    *
177    * @return \Drupal\security_review\CheckResult
178    *   The result of running the check.
179    */
180   public function runCli() {
181     return $this->run();
182   }
183
184   /**
185    * Returns the check-specific help page.
186    *
187    * @return array
188    *   The render array of the check's help page.
189    */
190   public abstract function help();
191
192   /**
193    * Returns the evaluation page of a result.
194    *
195    * Usually this is a list of the findings and an explanation.
196    *
197    * @param \Drupal\security_review\CheckResult $result
198    *   The check result to evaluate.
199    *
200    * @return array
201    *   The render array of the evaluation page.
202    */
203   public function evaluate(CheckResult $result) {
204     return [];
205   }
206
207   /**
208    * Evaluates a CheckResult and returns a plaintext output.
209    *
210    * @param \Drupal\security_review\CheckResult $result
211    *   The check result to evaluate.
212    *
213    * @return string
214    *   The evaluation string.
215    */
216   public function evaluatePlain(CheckResult $result) {
217     return '';
218   }
219
220   /**
221    * Converts a result integer to a human-readable result message.
222    *
223    * @param int $result_const
224    *   The result integer.
225    *
226    * @return string
227    *   The human-readable result message.
228    */
229   public abstract function getMessage($result_const);
230
231   /**
232    * Returns the last stored result of the check.
233    *
234    * Returns null if no results have been stored yet.
235    *
236    * @param bool $get_findings
237    *   Whether to get the findings too.
238    *
239    * @return \Drupal\security_review\CheckResult|null
240    *   The last stored result (or null).
241    */
242   public function lastResult($get_findings = FALSE) {
243     // Get stored data from State system.
244     $state_prefix = $this->statePrefix . 'last_result.';
245     $result = $this->state->get($state_prefix . 'result');
246     if ($get_findings) {
247       $findings = $this->state->get($state_prefix . 'findings');
248     }
249     else {
250       $findings = [];
251     }
252     $time = $this->state->get($state_prefix . 'time');
253     // Force boolean value.
254     $visible = $this->state->get($state_prefix . 'visible') == TRUE;
255
256     // Check validity of stored data.
257     $valid_result = is_int($result)
258       && $result >= CheckResult::SUCCESS
259       && $result <= CheckResult::INFO;
260     $valid_findings = is_array($findings);
261     $valid_time = is_int($time) && $time > 0;
262
263     // If invalid, return NULL.
264     if (!$valid_result || !$valid_findings || !$valid_time) {
265       return NULL;
266     }
267
268     // Construct the CheckResult.
269     $last_result = new CheckResult($this, $result, $findings, $visible, $time);
270
271     // Do a check run for acquiring findings if required.
272     if ($get_findings && !$this->storesFindings()) {
273       // Run the check to get the findings.
274       $fresh_result = $this->run();
275
276       // If it malfunctioned return the last known good result.
277       if (!($fresh_result instanceof CheckResult)) {
278         return $last_result;
279       }
280
281       if ($fresh_result->result() != $last_result->result()) {
282         // If the result is not the same store the new result and return it.
283         $this->storeResult($fresh_result);
284         $this->securityReview()->logCheckResult($fresh_result);
285         return $fresh_result;
286       }
287       else {
288         // Else return the old result with the fresh one's findings.
289         return CheckResult::combine($last_result, $fresh_result);
290       }
291     }
292
293     return $last_result;
294   }
295
296   /**
297    * Returns the timestamp the check was last run.
298    *
299    * Returns 0 if it has not been run yet.
300    *
301    * @return int
302    *   The timestamp of the last stored result.
303    */
304   public function lastRun() {
305     $last_result_time = $this->state
306       ->get($this->statePrefix . 'last_result.time');
307
308     if (!is_int($last_result_time)) {
309       return 0;
310     }
311     return $last_result_time;
312   }
313
314   /**
315    * Returns whether the check is skipped. Checks are not skipped by default.
316    *
317    * @return bool
318    *   Boolean indicating whether the check is skipped.
319    */
320   public function isSkipped() {
321     $is_skipped = $this->config->get('skipped');
322
323     if (!is_bool($is_skipped)) {
324       return FALSE;
325     }
326     return $is_skipped;
327   }
328
329   /**
330    * Returns the user the check was skipped by.
331    *
332    * Returns null if it hasn't been skipped yet or the user that skipped the
333    * check is not valid anymore.
334    *
335    * @return \Drupal\user\Entity\User|null
336    *   The user the check was last skipped by (or null).
337    */
338   public function skippedBy() {
339     $skipped_by = $this->config->get('skipped_by');
340
341     if (!is_int($skipped_by)) {
342       return NULL;
343     }
344     return User::load($skipped_by);
345   }
346
347   /**
348    * Returns the timestamp the check was last skipped on.
349    *
350    * Returns 0 if it hasn't been skipped yet.
351    *
352    * @return int
353    *   The UNIX timestamp the check was last skipped on (or 0).
354    */
355   public function skippedOn() {
356     $skipped_on = $this->config->get('skipped_on');
357
358     if (!is_int($skipped_on)) {
359       return 0;
360     }
361     return $skipped_on;
362   }
363
364   /**
365    * Enables the check. Has no effect if the check was not skipped.
366    */
367   public function enable() {
368     if ($this->isSkipped()) {
369       $this->config->set('skipped', FALSE);
370       $this->config->save();
371
372       // Log.
373       $context = ['@name' => $this->getTitle()];
374       $this->securityReview()->log($this, '@name check no longer skipped', $context, RfcLogLevel::NOTICE);
375     }
376   }
377
378   /**
379    * Marks the check as skipped.
380    *
381    * It still can be ran manually, but will remain skipped on the Run & Review
382    * page.
383    */
384   public function skip() {
385     if (!$this->isSkipped()) {
386       // Store skip data.
387       $this->config->set('skipped', TRUE);
388       $this->config->set('skipped_by', $this->currentUser()->id());
389       $this->config->set('skipped_on', time());
390       $this->config->save();
391
392       // Log.
393       $context = ['@name' => $this->getTitle()];
394       $this->securityReview()->log($this, '@name check skipped', $context, RfcLogLevel::NOTICE);
395     }
396   }
397
398   /**
399    * Stores a result in the state system.
400    *
401    * @param \Drupal\security_review\CheckResult $result
402    *   The result to store.
403    */
404   public function storeResult(CheckResult $result) {
405     if ($result == NULL) {
406       $context = [
407         '@reviewcheck' => $this->getTitle(),
408         '@namespace' => $this->getNamespace(),
409       ];
410       $this->securityReview()->log($this, 'Unable to store check @reviewcheck for @namespace', $context, RfcLogLevel::CRITICAL);
411       return;
412     }
413
414     $findings = $this->storesFindings() ? $result->findings() : [];
415     $this->state->setMultiple([
416       $this->statePrefix . 'last_result.result' => $result->result(),
417       $this->statePrefix . 'last_result.time' => $result->time(),
418       $this->statePrefix . 'last_result.visible' => $result->isVisible(),
419       $this->statePrefix . 'last_result.findings' => $findings,
420     ]);
421   }
422
423   /**
424    * Creates a new CheckResult for this Check.
425    *
426    * @param int $result
427    *   The result integer (see the constants defined in CheckResult).
428    * @param array $findings
429    *   The findings.
430    * @param bool $visible
431    *   The visibility of the result.
432    * @param int $time
433    *   The time the test was run.
434    *
435    * @return \Drupal\security_review\CheckResult
436    *   The created CheckResult.
437    */
438   public function createResult($result, array $findings = [], $visible = TRUE, $time = NULL) {
439     return new CheckResult($this, $result, $findings, $visible, $time);
440   }
441
442   /**
443    * Returns the Security Review Checklist service.
444    *
445    * @return \Drupal\security_review\Checklist
446    *   Security Review's Checklist service.
447    */
448   protected function checklist() {
449     return $this->container->get('security_review.checklist');
450   }
451
452   /**
453    * Returns the Config factory.
454    *
455    * @return \Drupal\Core\Config\ConfigFactory
456    *   Config factory.
457    */
458   protected function configFactory() {
459     return $this->container->get('config.factory');
460   }
461
462   /**
463    * Returns the service container.
464    *
465    * @return \Symfony\Component\DependencyInjection\ContainerInterface
466    *   Service container.
467    */
468   protected function container() {
469     return $this->container;
470   }
471
472   /**
473    * Returns the current Drupal user.
474    *
475    * @return \Drupal\Core\Session\AccountProxy
476    *   Current Drupal user.
477    */
478   protected function currentUser() {
479     return $this->container->get('current_user');
480   }
481
482   /**
483    * Returns the database connection.
484    *
485    * @return \Drupal\Core\Database\Connection
486    *   Database connection.
487    */
488   protected function database() {
489     return $this->container->get('database');
490   }
491
492   /**
493    * Returns the entity manager.
494    *
495    * @return \Drupal\Core\Entity\EntityManagerInterface
496    *   Entity manager.
497    */
498   protected function entityManager() {
499     return $this->container->get('entity.manager');
500   }
501
502   /**
503    * Returns the Drupal Kernel.
504    *
505    * @return \Drupal\Core\DrupalKernel
506    *   Drupal Kernel.
507    */
508   protected function kernel() {
509     return $this->container->get('kernel');
510   }
511
512   /**
513    * Returns the module handler.
514    *
515    * @return \Drupal\Core\Extension\ModuleHandler
516    *   Module handler.
517    */
518   protected function moduleHandler() {
519     return $this->container->get('module_handler');
520   }
521
522   /**
523    * Returns the Security Review Security service.
524    *
525    * @return \Drupal\security_review\Security
526    *   Security Review's Security service.
527    */
528   protected function security() {
529     return $this->container->get('security_review.security');
530   }
531
532   /**
533    * Returns the Security Review service.
534    *
535    * @return \Drupal\security_review\SecurityReview
536    *   Security Review service.
537    */
538   protected function securityReview() {
539     return $this->container->get('security_review');
540   }
541
542 }