Perform periodic fixity checks on selected files.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

334 lines
10 KiB

<?php
namespace Drupal\dgi_fixity\Entity;
use Drupal\Core\Entity\ContentEntityBase;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\dgi_fixity\FixityCheckInterface;
use Drupal\file\Entity\File;
/**
* Defines the fixity_check entity class.
*
* @ContentEntityType(
* id = "fixity_check",
* label = @Translation("Fixity Check"),
* label_collection = @Translation("Audit"),
* label_singular = @Translation("Fixity Check"),
* label_plural = @Translation("Fixity Checks"),
* label_count = @PluralTranslation(
* singular = "@count Fixity Check",
* plural = "@count Fixity Checks"
* ),
* handlers = {
* "storage" = "Drupal\dgi_fixity\FixityCheckStorage",
* "storage_schema" = "Drupal\dgi_fixity\FixityCheckStorageSchema",
* "list_builder" = "Drupal\dgi_fixity\FixityCheckListBuilder",
* "views_data" = "Drupal\dgi_fixity\FixityCheckViewsData",
* "access" = "Drupal\dgi_fixity\FixityCheckAccessControlHandler",
* "form" = {
* "edit" = "Drupal\Core\Entity\ContentEntityForm",
* "fixity-check" = "Drupal\dgi_fixity\Form\CheckForm",
* "delete" = "Drupal\Core\Entity\ContentEntityDeleteForm",
* "delete-multiple-confirm" = "Drupal\Core\Entity\Form\DeleteMultipleForm",
* },
* "route_provider" = {
* "html" = "Drupal\dgi_fixity\Routing\FixityCheckRouteProvider"
* },
* },
* base_table = "fixity_check",
* revision_table = "fixity_check_revision",
* show_revision_ui = FALSE,
* translatable = FALSE,
* common_reference_target = FALSE,
* entity_keys = {
* "id" = "id",
* "revision" = "revision_id",
* },
* links = {
* "canonical" = "/fixity/{fixity_check}",
* "edit-form" = "/fixity/{fixity_check}/edit",
* "fixity-audit" = "/fixity/{fixity_check}/audit",
* "fixity-check" = "/fixity/{fixity_check}/check",
* "delete-form" = "/fixity/{fixity_check}/delete",
* "delete-multiple-form" = "/fixity/delete",
* "collection" = "/admin/reports/fixity",
* },
* admin_permission = "administer fixity checks",
* )
*/
class FixityCheck extends ContentEntityBase implements FixityCheckInterface {
use StringTranslationTrait;
/**
* {@inheritdoc}
*/
public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
$fields = parent::baseFieldDefinitions($entity_type);
$fields['file'] = BaseFieldDefinition::create('entity_reference')
->setLabel(\t('File'))
->setDescription(\t('The file entity the fixity check was performed against.'))
->setRequired(TRUE)
->setRevisionable(FALSE)
->setTranslatable(FALSE)
// It's not possible to have two fixity checks for the same file, they
// should result in different versions of the same fixity_check entity.
->addConstraint('UniqueFieldEntityReference')
->setSetting('target_type', 'file')
->setDisplayConfigurable('view', FALSE)
->setDisplayOptions('view', [
'type' => 'dgi_fixity_file_reference',
'weight' => 0,
]);
$fields['state'] = BaseFieldDefinition::create('integer')
->setLabel(\t('State'))
->setDescription(\t('A flag indicating the state of the whether the check passed or not.'))
->setTranslatable(FALSE)
->setRevisionable(TRUE)
->setInitialValue(static::STATE_UNDEFINED)
->setDefaultValue(static::STATE_UNDEFINED)
// Define this via an options provider once.
// https://www.drupal.org/node/2329937 is completed.
->addPropertyConstraints('value', [
'AllowedValues' => ['callback' => static::class . '::getAllowedStates'],
])
->setDisplayConfigurable('view', FALSE)
->setDisplayOptions('view', [
'type' => 'dgi_fixity_state',
'weight' => 1,
]);
// A value of 0 indicates the check was never performed even though it is
// a unix-timestamp, which means it is technically 1970-01-01 00:00:00.
$fields['performed'] = BaseFieldDefinition::create('timestamp')
->setLabel(t('Performed'))
->setDescription(t('The time the check was performed, 0 if never performed.'))
->setTranslatable(FALSE)
->setRevisionable(TRUE)
->setInitialValue(0)
->setDefaultValue(0)
->setDisplayConfigurable('view', FALSE)
->setDisplayOptions('view', [
'type' => 'timestamp_ago',
'weight' => 2,
]);
$fields['periodic'] = BaseFieldDefinition::create('boolean')
->setLabel(t('Periodic'))
->setDescription(t('Enable/disable periodic fixity checks.'))
->setRequired(TRUE)
->setTranslatable(FALSE)
->setRevisionable(FALSE)
->setInitialValue(FALSE)
->setDefaultValue(FALSE)
->setDisplayConfigurable('view', FALSE)
->setDisplayOptions('form', [
'type' => 'options_buttons',
'weight' => 3,
])
->setDisplayOptions('view', [
'type' => 'boolean',
'weight' => 3,
]);
$fields['queued'] = BaseFieldDefinition::create('timestamp')
->setLabel(t('Queued'))
->setDescription(t('Time when this file was queued for fixity, 0 if not queued.'))
->setTranslatable(FALSE)
->setRevisionable(FALSE)
->setInitialValue(0)
->setDefaultValue(0)
->setDisplayConfigurable('view', FALSE)
->setDisplayOptions('view', [
'region' => 'hidden',
]);
return $fields;
}
/**
* {@inheritdoc}
*/
public function preSave(EntityStorageInterface $storage) {
parent::preSave($storage);
// Disallow changes to state / performed on existing revisions.
// Unless this is the first revision and it the check was never performed.
if (!$this->isNewRevision() && !($this->isLatestRevision() && !$this->original->wasPerformed())) {
$immutable_fields = ['state', 'performed'];
foreach ($immutable_fields as $field) {
if ($this->{$field}->hasAffectingChanges($this->original->{$field}, LanguageInterface::LANGCODE_NOT_SPECIFIED)) {
throw new \LogicException("Entity type {$this->getEntityTypeId()} does not support modifying the '{$field}' field of existing revisions.");
}
}
}
// The file field is immutable after creation.
if ($this->original && $this->file->hasAffectingChanges($this->original->file, LanguageInterface::LANGCODE_NOT_SPECIFIED)) {
throw new \LogicException("Entity type {$this->getEntityTypeId()} does not support modifying the file field after creation.");
}
// If performed has changed force queued to zero.
if ($this->original && $this->performed->hasAffectingChanges($this->original->performed, LanguageInterface::LANGCODE_NOT_SPECIFIED)) {
$this->setQueued(0);
}
}
/**
* {@inheritdoc}
*/
public function preSaveRevision(EntityStorageInterface $storage, \stdClass $record) {
parent::preSaveRevision($storage, $record);
// Disallow creating revisions if performed is not set.
if (!$this->isNew() && $this->isNewRevision() && $record->performed === 0) {
throw new \LogicException("Entity type {$this->getEntityTypeId()} does not support creating new revisions without where the performed field is not set.");
}
}
/**
* {@inheritdoc}
*/
public function setNewRevision($value = TRUE) {
parent::setNewRevision($value);
// Reset the state and performed timestamp when creating a new revision
// from an existing fixity_check.
if (!$this->isNew() && $value) {
$this->state = static::STATE_UNDEFINED;
unset($this->performed);
}
}
/**
* {@inheritdoc}
*/
public function getFile(): ?File {
/** @var \Drupal\Core\Field\EntityReferenceFieldItemList $file */
$file = $this->file;
return $file->isEmpty() ? NULL : $file->referencedEntities()[0];
}
/**
* {@inheritdoc}
*/
public function setFile(File $file): FixityCheckInterface {
$this->set('file', $file);
return $this;
}
/**
* {@inheritdoc}
*/
public function getState(): int {
return $this->state->value;
}
/**
* {@inheritdoc}
*/
public function setState(int $state): FixityCheckInterface {
if (!in_array($state, static::getAllowedStates())) {
throw new \InvalidArgumentException("Invalid state '$state' has been given");
}
$this->set('state', $state);
return $this;
}
/**
* {@inheritdoc}
*/
public function getStateLabel(): string {
return static::getStateProperty($this->getState(), 'label');
}
/**
* {@inheritdoc}
*/
public static function getStateProperty(int $state, string $property) {
return static::STATES[$state][$property] ?? NULL;
}
/**
* {@inheritdoc}
*/
public function passed(): bool {
$state = $this->getState();
return static::STATES[$state]['passed'] ?? FALSE;
}
/**
* {@inheritdoc}
*/
public function getPeriodic(): bool {
return $this->periodic->value;
}
/**
* {@inheritdoc}
*/
public function setPeriodic(bool $periodic): FixityCheckInterface {
$this->set('periodic', $periodic);
return $this;
}
/**
* {@inheritdoc}
*/
public function getPerformed(): int {
return $this->performed->value;
}
/**
* {@inheritdoc}
*/
public function setPerformed(int $performed): FixityCheckInterface {
$this->set('performed', $performed);
return $this;
}
/**
* {@inheritdoc}
*/
public function wasPerformed(): bool {
return $this->getPerformed() !== 0;
}
/**
* {@inheritdoc}
*/
public function getQueued(): int {
return $this->queued->value;
}
/**
* {@inheritdoc}
*/
public function setQueued(int $queued): FixityCheckInterface {
$this->set('queued', $queued);
return $this;
}
/**
* {@inheritdoc}
*/
public function label() {
$file = $this->getFile();
return ($file === NULL) ?
$this->t('Fixity Check') :
$file->label();
}
/**
* Defines allowed states for AllowedValues constraints.
*
* @return int[]
* The allowed states.
*/
public static function getAllowedStates() {
return array_keys(static::STATES);
}
}