Browse Source

Merge pull request #2 from discoverygarden/ctda9-67

First pass at implementation.
pull/4/head
Noel Chiasson 3 years ago committed by GitHub
parent
commit
11f5b69ab8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 95
      README.md
  2. 8
      composer.json
  3. 7
      config/install/dgi_fixity.settings.yml
  4. 246
      config/install/views.view.fixity_check_source.yml
  5. 824
      config/install/views.view.fixity_check_status.yml
  6. 10
      config/optional/system.action.fixity_check_check_action.yml
  7. 10
      config/optional/system.action.fixity_check_delete_action.yml
  8. 10
      config/optional/system.action.fixity_check_periodic_disable_action.yml
  9. 10
      config/optional/system.action.fixity_check_periodic_enable_action.yml
  10. 11
      config/optional/system.action.media_check_action.yml
  11. 11
      config/optional/system.action.media_periodic_disable_action.yml
  12. 11
      config/optional/system.action.media_periodic_enable_action.yml
  13. 901
      config/optional/views.view.fixity_check_source_islandora.yml
  14. 25
      config/schema/dgi_fixity.schema.yml
  15. 12
      dgi_fixity.info.yml
  16. 34
      dgi_fixity.install
  17. 11
      dgi_fixity.links.menu.yml
  18. 45
      dgi_fixity.links.task.yml
  19. 233
      dgi_fixity.module
  20. 4
      dgi_fixity.permissions.yml
  21. 57
      dgi_fixity.routing.yml
  22. 18
      dgi_fixity.services.yml
  23. 54
      dgi_fixity.views.inc
  24. 6
      drush.services.yml
  25. 113
      src/Commands/FixityCheck.php
  26. 259
      src/Controller/FixityCheckController.php
  27. 343
      src/Entity/FixityCheck.php
  28. 50
      src/FixityCheckAccessControlHandler.php
  29. 286
      src/FixityCheckBatchCheck.php
  30. 117
      src/FixityCheckBatchGenerate.php
  31. 237
      src/FixityCheckInterface.php
  32. 123
      src/FixityCheckListBuilder.php
  33. 402
      src/FixityCheckService.php
  34. 136
      src/FixityCheckServiceInterface.php
  35. 139
      src/FixityCheckStorage.php
  36. 79
      src/FixityCheckStorageInterface.php
  37. 40
      src/FixityCheckStorageSchema.php
  38. 44
      src/FixityCheckViewsData.php
  39. 62
      src/Form/BatchForm.php
  40. 160
      src/Form/CheckForm.php
  41. 85
      src/Form/GenerateForm.php
  42. 132
      src/Form/RevisionDeleteForm.php
  43. 305
      src/Form/SettingsForm.php
  44. 26
      src/Plugin/Action/CheckAction.php
  45. 62
      src/Plugin/Action/Derivative/FixityCheckActionDeriver.php
  46. 85
      src/Plugin/Action/FixityCheckActionBase.php
  47. 27
      src/Plugin/Action/PeriodicDisableAction.php
  48. 27
      src/Plugin/Action/PeriodicEnableAction.php
  49. 80
      src/Plugin/Derivative/FixityCheckLocalTasks.php
  50. 47
      src/Plugin/Field/FieldFormatter/FileReferenceFormatter.php
  51. 33
      src/Plugin/Field/FieldFormatter/StateFormatter.php
  52. 68
      src/Plugin/QueueWorker/FixityCheckWorker.php
  53. 79
      src/Plugin/QueueWorker/ProcessSourceWorker.php
  54. 24
      src/Plugin/Validation/Constraint/UniqueFieldEntityReferenceConstraint.php
  55. 67
      src/Plugin/Validation/Constraint/UniqueFieldEntityReferenceConstraintValidator.php
  56. 23
      src/Plugin/views/wizard/FixityCheck.php
  57. 23
      src/Plugin/views/wizard/FixityCheckRevision.php
  58. 65
      src/Routing/FixityCheckConverter.php
  59. 25
      src/Routing/FixityCheckRouteProvider.php
  60. 113
      src/Routing/FixityCheckRouteSubscriber.php

95
README.md

@ -1,41 +1,110 @@
# DGI Fixity # Fixity
## Introduction ## Introduction
A module to perform fixity checks on original media. Perform periodic fixity checks on selected files.
This module defines a new content entity type `fixity_check`. This entity is
used as an audit trail for fixity checks performed on a related `file` entity.
Wherein the revisions of the `fixity_check` record the results of previous
checks against that `file` entity.
This modules requires and enforces the following constraints on `fixity_check`
entities:
- **Must** be related to a `file`
- `file` relations **must** be unique
- `file` relation **cannot** be changed after creation
- `performed` and `state` properties **cannot** be modified after creation.
Users with the permission `Administer Fixity Checks` can:
- Manually perform checks
- Manually remove `fixity_check` entities and their revisions
- Manually mark files as requiring periodic checks
- Generate `fixity_check` entities for all previously existing files
Users with the permission `View Fixity Checks` can:
- View fixity audit log of Media entities
A `cron` hook is setup to automatically mark files as _requiring_ periodic
checks. As well as performing those checks on a regular basis. Email
notifications can be configured to alert the selected user of the status
of all performed checks on a regular basis or only when an error occurs.
## Requirements ## Requirements
This module requires the following modules/libraries: This module requires the following modules/libraries:
* [filehash](https://www.drupal.org/project/filehash) * [filehash]
## Configuration
The module can be configured at `admin/config/fixity`.
## Usage ## Drush
A number of drush commands come bundled with this module.
```bash
$ drush dgi_fixity:clear --help
Sets the periodic check flag to FALSE for all files.
```
```bash
$ drush dgi_fixity:generate --help
Creates a fixity_check entity for all previously created files.
```
```bash
$ drush dgi_fixity:check --help
Perform fixity checks on files.
Options:
--fids[=FIDS] Comma separated list of file identifiers, or a path to a file containing file identifiers.
The file should have each fid separated by a new line. If not specified the modules settings
for sources is used to determine which files to check.
--force Skip time elapsed threshold check when processing files.
```
## Installation ## Installation
Install as usual, see Install as usual, see [this][install] for further information.
[this](https://drupal.org/documentation/install/modules-themes/modules-8) for
further information. Additionally after this module is first enabled, you will need to generate
`fixity_check` entities for all pre-existing `file` entities. This does not
require that the checks are performed, only that one `fixity_check` entity
exists for every applicable `file` entity in the system.
This can be done with `drush`:
```bash
drush dgi_fixity:generate
```
Or via the admin form on the page `admin/config/fixity/generate`.
## Troubleshooting/Issues ## Troubleshooting/Issues
Having problems or solved a problem? Contact Having problems or solved a problem? Contact [discoverygarden].
[discoverygarden](http://support.discoverygarden.ca).
## Maintainers/Sponsors ## Maintainers/Sponsors
Current maintainers: Current maintainers:
* [discoverygarden](http://www.discoverygarden.ca) * [discoverygarden]
## Development ## Development
If you would like to contribute to this module create an issue, pull request If you would like to contribute to this module create an issue, pull request
and or contact and or contact [discoverygarden].
[discoverygarden](http://support.discoverygarden.ca).
## License ## License
[GPLv2](http://www.gnu.org/licenses/gpl-2.0.txt) [GPLv2][gplv2]
[discoverygarden]: http://support.discoverygarden.ca
[filehash]: https://www.drupal.org/project/filehash
[gplv2]: http://www.gnu.org/licenses/gpl-2.0.txt
[install]: https://drupal.org/documentation/install/modules-themes/modules-8

8
composer.json

@ -0,0 +1,8 @@
{
"name": "discoverygarden/dgi_fixity",
"type": "drupal-module",
"license": "GPL-2.0-or-later",
"require": {
"drupal/filehash": "^2.0"
}
}

7
config/install/dgi_fixity.settings.yml

@ -0,0 +1,7 @@
sources:
'fixity_check_source:all_files': 'fixity_check_source:all_files'
threshold: '-1 month'
batch_size: 100
notify_user: 1
notify_user_threshold: '-1 week'
notify_status: 2

246
config/install/views.view.fixity_check_source.yml

@ -0,0 +1,246 @@
langcode: en
status: true
dependencies:
enforced:
module:
- dgi_fixity
module:
- file
id: fixity_check_source
label: 'Fixity Check Source Default'
module: views
description: 'Default fixity check source, selects all permanent files.'
tag: fixity
base_table: file_managed
base_field: fid
display:
default:
display_plugin: default
id: default
display_title: Default
position: 0
display_options:
access:
type: none
options: { }
cache:
type: none
options: { }
query:
type: views_query
options:
disable_sql_rewrite: false
distinct: false
replica: false
query_comment: ''
query_tags: { }
exposed_form:
type: basic
options:
submit_button: Apply
reset_button: false
reset_button_label: Reset
exposed_sorts_label: 'Sort by'
expose_sort_order: true
sort_asc_label: Asc
sort_desc_label: Desc
pager:
type: mini
options:
items_per_page: 10
offset: 0
id: 0
total_pages: null
expose:
items_per_page: false
items_per_page_label: 'Items per page'
items_per_page_options: '5, 10, 25, 50'
items_per_page_options_all: false
items_per_page_options_all_label: '- All -'
offset: false
offset_label: Offset
tags:
previous: ‹‹
next: ››
style:
type: default
options:
grouping: { }
row_class: ''
default_row_class: true
uses_fields: false
row:
type: fields
options:
inline: { }
separator: ''
hide_empty: false
default_field_elements: true
fields:
fid:
id: fid
table: file_managed
field: fid
relationship: none
group_type: group
admin_label: 'File ID'
label: ''
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: false
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: false
empty: ''
hide_empty: true
empty_zero: false
hide_alter_empty: true
click_sort_column: value
type: number_unformatted
settings: { }
group_column: value
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
entity_type: file
entity_field: fid
plugin_id: field
filters:
status:
id: status
table: file_managed
field: status
relationship: none
group_type: group
admin_label: ''
operator: in
value:
1: '1'
group: 1
exposed: false
expose:
operator_id: ''
label: ''
description: ''
use_operator: false
operator: ''
operator_limit_selection: false
operator_list: { }
identifier: ''
required: false
remember: false
multiple: false
remember_roles:
authenticated: authenticated
reduce: false
is_grouped: false
group_info:
label: ''
description: ''
identifier: ''
optional: true
widget: select
multiple: false
remember: false
default_group: All
default_group_multiple: { }
group_items: { }
entity_type: file
entity_field: status
plugin_id: file_status
sorts:
fid:
id: fid
table: file_managed
field: fid
relationship: none
group_type: group
admin_label: 'File ID'
order: ASC
exposed: false
expose:
label: ''
entity_type: file
entity_field: fid
plugin_id: standard
header: { }
footer: { }
empty: { }
relationships: { }
arguments: { }
display_extenders: { }
show_admin_links: false
cache_metadata:
max-age: -1
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- url.query_args
tags: { }
all_files:
display_plugin: entity_reference
id: all_files
display_title: 'All Files'
position: 1
display_options:
display_extenders: { }
style:
type: entity_reference
options:
search_fields:
fid: fid
row:
type: entity_reference
options:
default_field_elements: false
inline: { }
separator: '-'
hide_empty: true
display_description: 'Gets all files regardless of state or usage'
pager:
type: some
options:
items_per_page: 10
offset: 0
cache_metadata:
max-age: -1
contexts:
- 'languages:language_content'
- 'languages:language_interface'
tags: { }

824
config/install/views.view.fixity_check_status.yml

@ -0,0 +1,824 @@
langcode: en
status: true
dependencies:
module:
- dgi_fixity
- file
- user
id: fixity_check_status
label: 'Fixity Check Status'
module: views
description: 'Find and manage fixity checks'
tag: fixity
base_table: fixity_check
base_field: id
display:
default:
display_plugin: default
id: default
display_title: Default
position: 0
display_options:
access:
type: perm
options:
perm: 'administer fixity checks'
cache:
type: tag
options: { }
query:
type: views_query
options:
disable_sql_rewrite: false
distinct: false
replica: false
query_comment: ''
query_tags: { }
exposed_form:
type: basic
options:
submit_button: Filter
reset_button: false
reset_button_label: Reset
exposed_sorts_label: 'Sort by'
expose_sort_order: true
sort_asc_label: Asc
sort_desc_label: Desc
pager:
type: mini
options:
items_per_page: 10
offset: 0
id: 0
total_pages: null
expose:
items_per_page: false
items_per_page_label: 'Items per page'
items_per_page_options: '5, 10, 25, 50'
items_per_page_options_all: false
items_per_page_options_all_label: '- All -'
offset: false
offset_label: Offset
tags:
previous: ‹‹
next: ››
style:
type: table
options:
grouping: { }
row_class: ''
default_row_class: true
override: true
sticky: false
caption: ''
summary: ''
description: ''
columns:
file: file
state: state
performed: performed
operations: operations
info:
file:
sortable: false
default_sort_order: asc
align: ''
separator: ''
empty_column: false
responsive: ''
state:
sortable: true
default_sort_order: asc
align: ''
separator: ''
empty_column: false
responsive: ''
performed:
sortable: true
default_sort_order: desc
align: ''
separator: ''
empty_column: false
responsive: ''
operations:
align: ''
separator: ''
empty_column: false
responsive: ''
default: performed
empty_table: false
row:
type: fields
fields:
fixity_check_bulk_form:
id: fixity_check_bulk_form
table: fixity_check
field: fixity_check_bulk_form
relationship: none
group_type: group
admin_label: ''
label: ''
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: false
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
action_title: Action
include_exclude: exclude
selected_actions: { }
entity_type: fixity_check
plugin_id: bulk_form
view_fixity_check:
id: view_fixity_check
table: fixity_check
field: view_fixity_check
relationship: none
group_type: group
admin_label: ''
label: ''
exclude: true
alter:
alter_text: false
text: '<a href="{{ view_fixity_check }}>{{ file }}</a>'
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: '{{ file }}'
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: false
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
text: ''
output_url_as_text: true
absolute: false
entity_type: fixity_check
plugin_id: entity_link
file_1:
id: file_1
table: fixity_check
field: file
relationship: none
group_type: group
admin_label: ''
label: Check
exclude: false
alter:
alter_text: false
text: '<a href="{{ view_fixity_check }}">{{ file_1 }}</a>'
make_link: true
path: '{{ view_fixity_check }}'
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: false
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
click_sort_column: target_id
type: entity_reference_label
settings:
link: false
group_column: target_id
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
entity_type: fixity_check
entity_field: file
plugin_id: field
state:
id: state
table: fixity_check
field: state
relationship: none
group_type: group
admin_label: ''
label: State
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: false
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
click_sort_column: value
type: dgi_fixity_state
settings: { }
group_column: value
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
entity_type: fixity_check
entity_field: state
plugin_id: field
performed:
id: performed
table: fixity_check
field: performed
relationship: none
group_type: group
admin_label: ''
label: Performed
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: false
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
click_sort_column: value
type: timestamp_ago
settings:
future_format: '@interval hence'
past_format: '@interval ago'
granularity: 2
group_column: value
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
entity_type: fixity_check
entity_field: performed
plugin_id: field
periodic:
id: periodic
table: fixity_check
field: periodic
relationship: none
group_type: group
admin_label: ''
label: Periodic
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: false
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
click_sort_column: value
type: boolean
settings:
format: default
format_custom_true: ''
format_custom_false: ''
group_column: value
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
entity_type: fixity_check
entity_field: periodic
plugin_id: field
operations:
table: fixity_check
field: operations
id: operations
entity_type: null
entity_field: null
plugin_id: entity_operations
relationship: none
group_type: group
admin_label: ''
label: 'Operations links'
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
destination: false
filters:
filename:
id: filename
table: file_managed
field: filename
relationship: file
group_type: group
admin_label: ''
operator: contains
value: ''
group: 1
exposed: true
expose:
operator_id: filename_op
label: Filename
description: ''
use_operator: false
operator: filename_op
operator_limit_selection: false
operator_list: { }
identifier: filename
required: false
remember: false
multiple: false
remember_roles:
authenticated: authenticated
anonymous: '0'
administrator: '0'
placeholder: ''
is_grouped: false
group_info:
label: ''
description: ''
identifier: ''
optional: true
widget: select
multiple: false
remember: false
default_group: All
default_group_multiple: { }
group_items: { }
entity_type: file
entity_field: filename
plugin_id: string
state:
id: state
table: fixity_check
field: state
relationship: none
group_type: group
admin_label: ''
operator: '='
value:
min: ''
max: ''
value: ''
group: 1
exposed: true
expose:
operator_id: state_op
label: State
description: ''
use_operator: false
operator: state_op
operator_limit_selection: false
operator_list: { }
identifier: state
required: false
remember: false
multiple: false
remember_roles:
authenticated: authenticated
anonymous: '0'
administrator: '0'
fedoraadmin: '0'
placeholder: ''
min_placeholder: ''
max_placeholder: ''
is_grouped: true
group_info:
label: State
description: ''
identifier: state
optional: true
widget: select
multiple: false
remember: false
default_group: All
default_group_multiple: { }
group_items:
1:
title: Passed
operator: '='
value:
value: '1'
min: ''
max: ''
2:
title: Failed
operator: '!='
value:
value: '1'
min: ''
max: ''
entity_type: fixity_check
entity_field: state
plugin_id: numeric
performed:
id: performed
table: fixity_check
field: performed
relationship: none
group_type: group
admin_label: ''
operator: '='
value:
min: ''
max: ''
value: '1970-01-01 00:00:00'
type: date
group: 1
exposed: true
expose:
operator_id: performed_op
label: Performed
description: ''
use_operator: false
operator: performed_op
operator_limit_selection: false
operator_list: { }
identifier: performed
required: false
remember: false
multiple: false
remember_roles:
authenticated: authenticated
anonymous: '0'
administrator: '0'
fedoraadmin: '0'
placeholder: ''
min_placeholder: ''
max_placeholder: ''
is_grouped: true
group_info:
label: Performed
description: ''
identifier: performed
optional: true
widget: select
multiple: false
remember: false
default_group: All
default_group_multiple: { }
group_items:
1:
title: 'True'
operator: '!='
value:
type: date
value: '1970-01-01 00:00:00'
min: ''
max: ''
2:
title: 'False'
operator: '='
value:
type: date
value: '1970-01-01 00:00:00'
min: ''
max: ''
entity_type: fixity_check
entity_field: performed
plugin_id: date
periodic:
id: periodic
table: fixity_check
field: periodic
relationship: none
group_type: group
admin_label: ''
operator: '='
value: All
group: 1
exposed: true
expose:
operator_id: ''
label: Periodic
description: ''
use_operator: false
operator: periodic_op
operator_limit_selection: false
operator_list: { }
identifier: periodic
required: false
remember: false
multiple: false
remember_roles:
authenticated: authenticated
anonymous: '0'
administrator: '0'
is_grouped: false
group_info:
label: ''
description: ''
identifier: ''
optional: true
widget: select
multiple: false
remember: false
default_group: All
default_group_multiple: { }
group_items: { }
entity_type: fixity_check
entity_field: periodic
plugin_id: boolean
sorts:
performed:
id: performed
table: fixity_check
field: performed
relationship: none
group_type: group
admin_label: ''
order: DESC
exposed: false
expose:
label: Performed
granularity: second
entity_type: fixity_check
entity_field: performed
plugin_id: date
title: Status
header: { }
footer: { }
empty:
area_text_custom:
id: area_text_custom
table: views
field: area_text_custom
relationship: none
group_type: group
admin_label: ''
empty: true
tokenize: false
content: 'No fixity checks have been performed.'
plugin_id: text_custom
relationships:
file:
id: file
table: fixity_check
field: file
relationship: none
group_type: group
admin_label: File
required: false
entity_type: fixity_check
entity_field: file
plugin_id: standard
arguments: { }
display_extenders: { }
filter_groups:
operator: AND
groups:
1: AND
cache_metadata:
max-age: 0
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- url
- url.query_args
- user.permissions
tags: { }
fixity_checks:
display_plugin: page
id: fixity_checks
display_title: Page
position: 1
display_options:
display_extenders: { }
path: admin/reports/fixity
enabled: true
cache_metadata:
max-age: 0
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- url
- url.query_args
- user.permissions
tags: { }

10
config/optional/system.action.fixity_check_check_action.yml

@ -0,0 +1,10 @@
langcode: en
status: true
dependencies:
module:
- dgi_fixity
id: fixity_check_check_action
label: 'Perform check'
type: fixity_check
plugin: dgi_fixity:check_action:fixity_check
configuration: { }

10
config/optional/system.action.fixity_check_delete_action.yml

@ -0,0 +1,10 @@
langcode: en
status: true
dependencies:
module:
- dgi_fixity
id: fixity_check_delete_action
label: 'Delete check'
type: fixity_check
plugin: entity:delete_action:fixity_check
configuration: { }

10
config/optional/system.action.fixity_check_periodic_disable_action.yml

@ -0,0 +1,10 @@
langcode: en
status: true
dependencies:
module:
- dgi_fixity
id: fixity_check_periodic_disable_action
label: 'Disable periodic checks'
type: fixity_check
plugin: dgi_fixity:periodic_disable_action:fixity_check
configuration: { }

10
config/optional/system.action.fixity_check_periodic_enable_action.yml

@ -0,0 +1,10 @@
langcode: en
status: true
dependencies:
module:
- dgi_fixity
id: fixity_check_periodic_enable_action
label: 'Enable periodic checks'
type: fixity_check
plugin: dgi_fixity:periodic_enable_action:fixity_check
configuration: { }

11
config/optional/system.action.media_check_action.yml

@ -0,0 +1,11 @@
langcode: en
status: true
dependencies:
module:
- media
- dgi_fixity
id: media_check_action
label: 'Check media'
type: media
plugin: dgi_fixity:check_action:media
configuration: { }

11
config/optional/system.action.media_periodic_disable_action.yml

@ -0,0 +1,11 @@
langcode: en
status: true
dependencies:
module:
- dgi_fixity
- media
id: media_periodic_disable_action
label: 'Disable periodic media checks'
type: media
plugin: dgi_fixity:periodic_disable_action:media
configuration: { }

11
config/optional/system.action.media_periodic_enable_action.yml

@ -0,0 +1,11 @@
langcode: en
status: true
dependencies:
module:
- dgi_fixity
- media
id: media_periodic_enable_action
label: 'Enable periodic media checks'
type: media
plugin: dgi_fixity:periodic_enable_action:media
configuration: { }

901
config/optional/views.view.fixity_check_source_islandora.yml

@ -0,0 +1,901 @@
langcode: en
status: true
dependencies:
enforced:
config:
# Requires the following fields to work.
- field.storage.media.field_media_use
- field.storage.taxonomy_term.field_external_uri
module:
- dgi_fixity
module:
- file
- media
- taxonomy
id: fixity_check_source_islandora
label: 'Fixity Check Source Islandora'
module: views
description: 'Fixity Check Source for islandora, selects all "Original Files"'
tag: fixity
base_table: file_managed
base_field: fid
display:
default:
display_plugin: default
id: default
display_title: Default
position: 0
display_options:
access:
type: none
options: { }
cache:
type: none
options: { }
query:
type: views_query
options:
disable_sql_rewrite: false
distinct: false
replica: false
query_comment: ''
query_tags: { }
exposed_form:
type: basic
options:
submit_button: Apply
reset_button: false
reset_button_label: Reset
exposed_sorts_label: 'Sort by'
expose_sort_order: true
sort_asc_label: Asc
sort_desc_label: Desc
pager:
type: mini
options:
items_per_page: 10
offset: 0
id: 0
total_pages: null
expose:
items_per_page: false
items_per_page_label: 'Items per page'
items_per_page_options: '5, 10, 25, 50'
items_per_page_options_all: false
items_per_page_options_all_label: '- All -'
offset: false
offset_label: Offset
tags:
previous: ‹‹
next: ››
style:
type: default
options:
grouping: { }
row_class: ''
default_row_class: true
uses_fields: false
row:
type: fields
options:
inline: { }
separator: ''
hide_empty: false
default_field_elements: true
fields:
fid:
id: fid
table: file_managed
field: fid
relationship: none
group_type: group
admin_label: ''
label: ''
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: false
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
click_sort_column: value
type: number_unformatted
settings: { }
group_column: value
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
entity_type: file
entity_field: fid
plugin_id: field
filters:
status:
id: status
table: file_managed
field: status
relationship: none
group_type: group
admin_label: ''
operator: in
value:
1: '1'
group: 1
exposed: false
expose:
operator_id: ''
label: ''
description: ''
use_operator: false
operator: ''
operator_limit_selection: false
operator_list: { }
identifier: ''
required: false
remember: false
multiple: false
remember_roles:
authenticated: authenticated
reduce: false
is_grouped: false
group_info:
label: ''
description: ''
identifier: ''
optional: true
widget: select
multiple: false
remember: false
default_group: All
default_group_multiple: { }
group_items: { }
entity_type: file
entity_field: status
plugin_id: file_status
sorts:
fid:
id: fid
table: file_managed
field: fid
relationship: none
group_type: group
admin_label: ''
order: ASC
exposed: false
expose:
label: ''
entity_type: file
entity_field: fid
plugin_id: standard
header: { }
footer: { }
empty: { }
relationships: { }
arguments: { }
display_extenders: { }
show_admin_links: false
cache_metadata:
max-age: -1
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- url.query_args
tags: { }
original_files_audio:
display_plugin: entity_reference
id: original_files_audio
display_title: 'Original File: Audio'
position: 1
display_options:
display_extenders: { }
style:
type: entity_reference
options:
search_fields:
fid: fid
display_description: 'Gets the "Original File" files of Audio Media'
row:
type: entity_reference
options:
default_field_elements: false
inline: { }
separator: '-'
hide_empty: true
relationships:
reverse_field_media_audio_file_media:
id: reverse_field_media_audio_file_media
table: file_managed
field: reverse_field_media_audio_file_media
relationship: none
group_type: group
admin_label: Audio
required: true
entity_type: file
plugin_id: entity_reverse
field_media_use:
id: field_media_use
table: media__field_media_use
field: field_media_use
relationship: reverse_field_media_audio_file_media
group_type: group
admin_label: 'Media Use'
required: true
plugin_id: standard
defaults:
relationships: false
filters: false
filter_groups: false
filters:
status:
id: status
table: file_managed
field: status
relationship: none
group_type: group
admin_label: ''
operator: in
value:
1: '1'
group: 1
exposed: false
expose:
operator_id: ''
label: ''
description: ''
use_operator: false
operator: ''
operator_limit_selection: false
operator_list: { }
identifier: ''
required: false
remember: false
multiple: false
remember_roles:
authenticated: authenticated
reduce: false
is_grouped: false
group_info:
label: ''
description: ''
identifier: ''
optional: true
widget: select
multiple: false
remember: false
default_group: All
default_group_multiple: { }
group_items: { }
entity_type: file
entity_field: status
plugin_id: file_status
field_external_uri_uri:
id: field_external_uri_uri
table: taxonomy_term__field_external_uri
field: field_external_uri_uri
relationship: field_media_use
group_type: group
admin_label: 'Original File'
operator: '='
value: 'http://pcdm.org/use#OriginalFile'
group: 1
exposed: false
expose:
operator_id: ''
label: ''
description: ''
use_operator: false
operator: ''
operator_limit_selection: false
operator_list: { }
identifier: ''
required: false
remember: false
multiple: false
remember_roles:
authenticated: authenticated
placeholder: ''
is_grouped: false
group_info:
label: ''
description: ''
identifier: ''
optional: true
widget: select
multiple: false
remember: false
default_group: All
default_group_multiple: { }
group_items: { }
plugin_id: string
filter_groups:
operator: AND
groups:
1: AND
cache_metadata:
max-age: -1
contexts:
- 'languages:language_content'
- 'languages:language_interface'
tags: { }
original_files_document:
display_plugin: entity_reference
id: original_files_document
display_title: 'Original File: Document'
position: 2
display_options:
display_extenders: { }
style:
type: entity_reference
options:
search_fields:
fid: fid
display_description: 'Gets the "Original File" files of Document Media'
row:
type: entity_reference
options:
default_field_elements: false
inline: { }
separator: '-'
hide_empty: true
relationships:
reverse_field_media_document_media:
id: reverse_field_media_document_media
table: file_managed
field: reverse_field_media_document_media
relationship: none
group_type: group
admin_label: Document
required: true
entity_type: file
plugin_id: entity_reverse
field_media_use:
id: field_media_use
table: media__field_media_use
field: field_media_use
relationship: reverse_field_media_document_media
group_type: group
admin_label: 'Media Use'
required: true
plugin_id: standard
defaults:
relationships: false
filters: false
filter_groups: false
filters:
status:
id: status
table: file_managed
field: status
relationship: none
group_type: group
admin_label: ''
operator: in
value:
1: '1'
group: 1
exposed: false
expose:
operator_id: ''
label: ''
description: ''
use_operator: false
operator: ''
operator_limit_selection: false
operator_list: { }
identifier: ''
required: false
remember: false
multiple: false
remember_roles:
authenticated: authenticated
reduce: false
is_grouped: false
group_info:
label: ''
description: ''
identifier: ''
optional: true
widget: select
multiple: false
remember: false
default_group: All
default_group_multiple: { }
group_items: { }
entity_type: file
entity_field: status
plugin_id: file_status
field_external_uri_uri:
id: field_external_uri_uri
table: taxonomy_term__field_external_uri
field: field_external_uri_uri
relationship: field_media_use
group_type: group
admin_label: 'Original File'
operator: '='
value: 'http://pcdm.org/use#OriginalFile'
group: 1
exposed: false
expose:
operator_id: ''
label: ''
description: ''
use_operator: false
operator: ''
operator_limit_selection: false
operator_list: { }
identifier: ''
required: false
remember: false
multiple: false
remember_roles:
authenticated: authenticated
placeholder: ''
is_grouped: false
group_info:
label: ''
description: ''
identifier: ''
optional: true
widget: select
multiple: false
remember: false
default_group: All
default_group_multiple: { }
group_items: { }
plugin_id: string
filter_groups:
operator: AND
groups:
1: AND
cache_metadata:
max-age: -1
contexts:
- 'languages:language_content'
- 'languages:language_interface'
tags: { }
original_files_file:
display_plugin: entity_reference
id: original_files_file
display_title: 'Original File: File'
position: 3
display_options:
display_extenders: { }
style:
type: entity_reference
options:
search_fields:
fid: fid
display_description: 'Gets the "Original File" files of File Media'
row:
type: entity_reference
options:
default_field_elements: false
inline: { }
separator: '-'
hide_empty: true
relationships:
reverse_field_media_file_media:
id: reverse_field_media_file_media
table: file_managed
field: reverse_field_media_file_media
relationship: none
group_type: group
admin_label: File
required: true
entity_type: file
plugin_id: entity_reverse
field_media_use:
id: field_media_use
table: media__field_media_use
field: field_media_use
relationship: reverse_field_media_file_media
group_type: group
admin_label: 'Media Use'
required: true
plugin_id: standard
defaults:
relationships: false
filters: false
filter_groups: false
filters:
status:
id: status
table: file_managed
field: status
relationship: none
group_type: group
admin_label: ''
operator: in
value:
1: '1'
group: 1
exposed: false
expose:
operator_id: ''
label: ''
description: ''
use_operator: false
operator: ''
operator_limit_selection: false
operator_list: { }
identifier: ''
required: false
remember: false
multiple: false
remember_roles:
authenticated: authenticated
reduce: false
is_grouped: false
group_info:
label: ''
description: ''
identifier: ''
optional: true
widget: select
multiple: false
remember: false
default_group: All
default_group_multiple: { }
group_items: { }
entity_type: file
entity_field: status
plugin_id: file_status
field_external_uri_uri:
id: field_external_uri_uri
table: taxonomy_term__field_external_uri
field: field_external_uri_uri
relationship: field_media_use
group_type: group
admin_label: 'Original File'
operator: '='
value: 'http://pcdm.org/use#OriginalFile'
group: 1
exposed: false
expose:
operator_id: ''
label: ''
description: ''
use_operator: false
operator: ''
operator_limit_selection: false
operator_list: { }
identifier: ''
required: false
remember: false
multiple: false
remember_roles:
authenticated: authenticated
placeholder: ''
is_grouped: false
group_info:
label: ''
description: ''
identifier: ''
optional: true
widget: select
multiple: false
remember: false
default_group: All
default_group_multiple: { }
group_items: { }
plugin_id: string
filter_groups:
operator: AND
groups:
1: AND
cache_metadata:
max-age: -1
contexts:
- 'languages:language_content'
- 'languages:language_interface'
tags: { }
original_files_image:
display_plugin: entity_reference
id: original_files_image
display_title: 'Original File: Image'
position: 3
display_options:
display_extenders: { }
style:
type: entity_reference
options:
search_fields:
fid: fid
display_description: 'Gets the "Original File" files of Image Media'
row:
type: entity_reference
options:
default_field_elements: false
inline: { }
separator: '-'
hide_empty: true
relationships:
reverse_field_media_image_media:
id: reverse_field_media_image_media
table: file_managed
field: reverse_field_media_image_media
relationship: none
group_type: group
admin_label: Image
required: true
entity_type: file
plugin_id: entity_reverse
field_media_use:
id: field_media_use
table: media__field_media_use
field: field_media_use
relationship: reverse_field_media_image_media
group_type: group
admin_label: 'Media Use'
required: true
plugin_id: standard
defaults:
relationships: false
filters: false
filter_groups: false
filters:
status:
id: status
table: file_managed
field: status
relationship: none
group_type: group
admin_label: ''
operator: in
value:
1: '1'
group: 1
exposed: false
expose:
operator_id: ''
label: ''
description: ''
use_operator: false
operator: ''
operator_limit_selection: false
operator_list: { }
identifier: ''
required: false
remember: false
multiple: false
remember_roles:
authenticated: authenticated
reduce: false
is_grouped: false
group_info:
label: ''
description: ''
identifier: ''
optional: true
widget: select
multiple: false
remember: false
default_group: All
default_group_multiple: { }
group_items: { }
entity_type: file
entity_field: status
plugin_id: file_status
field_external_uri_uri:
id: field_external_uri_uri
table: taxonomy_term__field_external_uri
field: field_external_uri_uri
relationship: field_media_use
group_type: group
admin_label: 'Original File'
operator: '='
value: 'http://pcdm.org/use#OriginalFile'
group: 1
exposed: false
expose:
operator_id: ''
label: ''
description: ''
use_operator: false
operator: ''
operator_limit_selection: false
operator_list: { }
identifier: ''
required: false
remember: false
multiple: false
remember_roles:
authenticated: authenticated
placeholder: ''
is_grouped: false
group_info:
label: ''
description: ''
identifier: ''
optional: true
widget: select
multiple: false
remember: false
default_group: All
default_group_multiple: { }
group_items: { }
plugin_id: string
filter_groups:
operator: AND
groups:
1: AND
cache_metadata:
max-age: -1
contexts:
- 'languages:language_content'
- 'languages:language_interface'
tags: { }
original_files_video:
display_plugin: entity_reference
id: original_files_video
display_title: 'Original File: Video'
position: 3
display_options:
display_extenders: { }
style:
type: entity_reference
options:
search_fields:
fid: fid
display_description: 'Gets the "Original File" files of Video Media'
row:
type: entity_reference
options:
default_field_elements: false
inline: { }
separator: '-'
hide_empty: true
relationships:
reverse_field_media_video_file_media:
id: reverse_field_media_video_file_media
table: file_managed
field: reverse_field_media_video_file_media
relationship: none
group_type: group
admin_label: Video
required: true
entity_type: file
plugin_id: entity_reverse
field_media_use:
id: field_media_use
table: media__field_media_use
field: field_media_use
relationship: reverse_field_media_video_file_media
group_type: group
admin_label: 'Media Use'
required: true
plugin_id: standard
defaults:
relationships: false
filters: false
filter_groups: false
filters:
status:
id: status
table: file_managed
field: status
relationship: none
group_type: group
admin_label: ''
operator: in
value:
1: '1'
group: 1
exposed: false
expose:
operator_id: ''
label: ''
description: ''
use_operator: false
operator: ''
operator_limit_selection: false
operator_list: { }
identifier: ''
required: false
remember: false
multiple: false
remember_roles:
authenticated: authenticated
reduce: false
is_grouped: false
group_info:
label: ''
description: ''
identifier: ''
optional: true
widget: select
multiple: false
remember: false
default_group: All
default_group_multiple: { }
group_items: { }
entity_type: file
entity_field: status
plugin_id: file_status
field_external_uri_uri:
id: field_external_uri_uri
table: taxonomy_term__field_external_uri
field: field_external_uri_uri
relationship: field_media_use
group_type: group
admin_label: ''
operator: '='
value: 'http://pcdm.org/use#OriginalFile'
group: 1
exposed: false
expose:
operator_id: ''
label: ''
description: ''
use_operator: false
operator: ''
operator_limit_selection: false
operator_list: { }
identifier: ''
required: false
remember: false
multiple: false
remember_roles:
authenticated: authenticated
placeholder: ''
is_grouped: false
group_info:
label: ''
description: ''
identifier: ''
optional: true
widget: select
multiple: false
remember: false
default_group: All
default_group_multiple: { }
group_items: { }
plugin_id: string
filter_groups:
operator: AND
groups:
1: AND
cache_metadata:
max-age: -1
contexts:
- 'languages:language_content'
- 'languages:language_interface'
tags: { }

25
config/schema/dgi_fixity.schema.yml

@ -0,0 +1,25 @@
dgi_fixity.settings:
type: config_object
label: 'Fixity check settings'
mapping:
sources:
type: sequence
label: 'File Selection for Fixity Checks'
sequence:
type: string
label: 'View and Display Identifier'
threshold:
type: string
label: 'Time elapsed between Fixity Checks'
batch_size:
type: integer
label: 'How many files will be processed at once when performing a batch / cron job'
notify_status:
type: integer
label: 'Notification trigger on status'
notify_user:
type: integer
label: 'User to notify'
notify_user_threshold:
type: string
label: 'Time elapsed between notifications'

12
dgi_fixity.info.yml

@ -0,0 +1,12 @@
name: 'Fixity'
description: "Performs fixity checks on files."
type: module
package: DGI
core_version_requirement: ^8 || ^9
configure: dgi_fixity.settings
dependencies:
- drupal:file
- drupal:media
- drupal:user
- drupal:views
- filehash:filehash

34
dgi_fixity.install

@ -0,0 +1,34 @@
<?php
/**
* @file
* Install hook implementations.
*/
/**
* Implements hook_requirements().
*/
function dgi_fixity_requirements($phase) {
$requirements = [];
if ($phase == 'runtime') {
/** @var \Drupal\dgi_fixity\FixityCheckServiceInterface $fixity */
$fixity = \Drupal::service('dgi_fixity.fixity_check');
$stats = $fixity->stats();
$elements = [];
foreach ($fixity->summary($stats) as $summary) {
$elements[] = [
'#markup' => $summary,
'#suffix' => '<br/>',
];
}
$failed = $stats['failed'] > 0;
$out_to_date = $stats['periodic']['expired'] > 0;
$requirements['dgi_fixity'] = [
'title' => \t('Fixity'),
'value' => $failed ? \t('Error') : ($out_to_date ? \t('Out of date') : \t('Up to date')),
'description' => \Drupal::service('renderer')->render($elements),
'severity' => $failed ? REQUIREMENT_ERROR : ($out_to_date ? REQUIREMENT_WARNING : REQUIREMENT_OK),
];
}
return $requirements;
}

11
dgi_fixity.links.menu.yml

@ -0,0 +1,11 @@
dgi_fixity.settings:
title: 'Fixity'
description: 'Configure Fixity Settings.'
route_name: dgi_fixity.settings
parent: 'system.admin_config_media'
dgi_fixity.report:
title: 'Fixity report'
description: 'Report of all fixity checks.'
route_name: entity.fixity_check.collection
parent: 'system.admin_reports'

45
dgi_fixity.links.task.yml

@ -0,0 +1,45 @@
dgi_fixity.entities:
class: \Drupal\Core\Menu\LocalTaskDefault
deriver: 'Drupal\dgi_fixity\Plugin\Derivative\FixityCheckLocalTasks'
dgi_fixity.config:
route_name: dgi_fixity.settings
base_route: dgi_fixity.settings
title: Configuration
dgi_fixity.batch:
route_name: dgi_fixity.batch
base_route: dgi_fixity.settings
title: Check
dgi_fixity.generate:
route_name: dgi_fixity.generate
base_route: dgi_fixity.settings
title: Generate
entity.fixity_check.view:
route_name: entity.fixity_check.canonical
base_route: entity.fixity_check.canonical
title: View
entity.fixity_check.edit:
route_name: entity.fixity_check.edit_form
base_route: entity.fixity_check.canonical
title: Edit
entity.fixity_check.delete_form:
route_name: entity.fixity_check.delete_form
base_route: entity.fixity_check.canonical
weight: 10
title: Delete
entity.fixity_check.revision:
route_name: entity.fixity_check.revision
base_route: entity.fixity_check.revision
title: Revision
entity.fixity_check.revision.delete_form:
route_name: entity.fixity_check.revision_delete_confirm
base_route: entity.fixity_check.revision
weight: 10
title: Delete

233
dgi_fixity.module

@ -0,0 +1,233 @@
<?php
/**
* @file
* General hook implementations.
*/
use Drupal\Core\Cache\Cache;
use Drupal\Core\Config\FileStorage;
use Drupal\Core\Config\InstallStorage;
use Drupal\Core\Config\StorageInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Mail\MailFormatHelper;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Url;
use Drupal\dgi_fixity\Form\SettingsForm;
use Drupal\user\Entity\User;
/**
* Implements hook_modules_installed().
*/
function dgi_fixity_modules_installed($modules) {
// Install optional configuration for islandora / action.
// This section is only entered when this module is installed prior to either
// of these optional dependencies installation.
// In particular the optional view:
// - views.view.fixity_check_source_islandora
// Which requires the following fields:
// - field.storage.media.field_media_use
// - field.storage.taxonomy_term.field_external_uri
// Which are typically provided by `islandora_core_feature`.
// All other optional configuration is for the `action` module.
if (in_array('islandora_core_feature', $modules) || in_array('action', $modules)) {
$optional_install_path = \Drupal::moduleHandler()
->getModule('dgi_fixity')
->getPath() . '/' . InstallStorage::CONFIG_OPTIONAL_DIRECTORY;
/** @var \Drupal\Core\Config\ConfigInstallerInterface $config_installer */
$config_installer = \Drupal::service('config.installer');
$storage = new FileStorage($optional_install_path, StorageInterface::DEFAULT_COLLECTION);
// This will not overwrite the existing optional configuration if already
// installed.
$config_installer->installOptionalConfig($storage);
}
}
/**
* Implements hook_mail().
*/
function dgi_fixity_mail($key, &$message, $params) {
switch ($key) {
case 'notify':
$config = \Drupal::config(SettingsForm::CONFIG_NAME);
$last = \Drupal::state()->get(SettingsForm::STATE_LAST_NOTIFICATION);
if ($last !== NULL) {
// If enough time has not elapsed since the last notification do not
// send again.
$threshold = strtotime($config->get(SettingsForm::NOTIFY_USER_THRESHOLD));
if ($last > $threshold) {
$message['send'] = FALSE;
return;
}
}
// Check if the configuration has enabled notifications.
$notify_status = $config->get(SettingsForm::NOTIFY_STATUS);
if ($notify_status === SettingsForm::NOTIFY_STATUS_NEVER) {
$message['send'] = FALSE;
return;
}
/** @var \Drupal\dgi_fixity\FixityCheckServiceInterface $fixity */
$fixity = \Drupal::service('dgi_fixity.fixity_check');
$stats = $fixity->stats();
// Only notify if an error has occurred.
if ($notify_status == SettingsForm::NOTIFY_STATUS_ERROR && $stats['failed'] === FALSE) {
$message['send'] = FALSE;
return;
}
$options = ['langcode' => $message['langcode']];
$now = \Drupal::time()->getRequestTime();
$subject = \t('Fixity Check Report - @now', ['@now' => date(DATE_RFC7231, $now)], $options)->render();
$body = $fixity->summary($stats, $options);
if ($stats['failed'] !== 0) {
$body[] = \t(
'There are failed checks which require your attention please review the current state of checks <a href="@site">here</a>.',
['@site' => Url::fromRoute('entity.fixity_check.collection', [], ['absolute' => TRUE])->toString()],
$options
)->render();
}
$message['subject'] = $subject;
foreach ($body as $line) {
$message['body'][] = MailFormatHelper::htmlToText($line);
}
// Track when the last message was sent.
\Drupal::state()->set(SettingsForm::STATE_LAST_NOTIFICATION, $now);
break;
}
}
/**
* Implements hook_cron().
*/
function dgi_fixity_cron() {
$queued = \Drupal::time()->getRequestTime();
$settings = \Drupal::config(SettingsForm::CONFIG_NAME);
$threshold = strtotime($settings->get(SettingsForm::THRESHOLD));
$sources = $settings->get(SettingsForm::SOURCES);
// Update enabled periodic checks.
$queue = \Drupal::queue('dgi_fixity.process_source');
foreach ($sources as $source) {
// It safe to have queue processing a source multiple times,
// they will steal work from each other but will not conflict.
$queue->createItem($source);
}
/** @var \Drupal\dgi_fixity\FixityCheckStorageInterface $storage */
$storage = \Drupal::entityTypeManager()->getStorage('fixity_check');
// Queue items that exceed the current threshold.
$storage->queue($queued, $threshold, 100);
// Dequeued items after 6 hours assuming the check has failed.
// They will be re-queued if appropriate on the next cron run.
$storage->dequeue($queued - (3600 * 6));
// Send notification if appropriate.
$uid = $settings->get(SettingsForm::NOTIFY_USER);
$user = User::load($uid);
if ($user) {
\Drupal::service('plugin.manager.mail')->mail('dgi_fixity', 'notify', $user->getEmail(), $user->getPreferredLangcode(TRUE));
}
}
/**
* Implements hook_entity_type_alter().
*/
function dgi_fixity_entity_type_alter(array &$entity_types) {
/** @var \Drupal\dgi_fixity\FixityCheckServiceInterface $fixity */
$fixity = \Drupal::service('dgi_fixity.fixity_check');
$supported_entity_types = $fixity->fromEntityTypes();
foreach ($supported_entity_types as $entity_type_id) {
$entity_type = &$entity_types[$entity_type_id];
$entity_type->setLinkTemplate('fixity-audit', "/fixity/$entity_type_id/{{$entity_type_id}}");
$entity_type->setLinkTemplate('fixity-check', "/fixity/$entity_type_id/{{$entity_type_id}}/check");
$entity_type->setFormClass('fixity-check', 'Drupal\dgi_fixity\Form\CheckForm');
}
}
/**
* Implements hook_entity_operation().
*/
function dgi_fixity_entity_operation(EntityInterface $entity) {
$current_user = \Drupal::service('current_user');
$operations = [];
if ($entity->hasLinkTemplate('fixity-audit') && $current_user->hasPermission('view fixity checks')) {
$operations['fixity-audit'] = [
'title' => \t('Audit'),
'weight' => 10,
'url' => $entity->toUrl('fixity-audit'),
];
if ($entity->hasLinkTemplate('fixity-check') && $current_user->hasPermission('administer fixity checks')) {
$operations['fixity-check'] = [
'title' => \t('Check'),
'weight' => 13,
'url' => $entity->toUrl('fixity-check', ['query' => \Drupal::service('redirect.destination')->getAsArray()]),
];
}
}
return $operations;
}
/**
* Implements hook_ENTITY_TYPE_insert().
*/
function dgi_fixity_file_insert(EntityInterface $entity) {
// Make sure the fixity_check table contains a row for every file.
\Drupal::entityTypeManager()
->getStorage('fixity_check')
->create([
'file' => $entity->id(),
])
->save();
}
/**
* Implements hook_ENTITY_TYPE_delete().
*/
function dgi_fixity_file_delete(EntityInterface $entity) {
/** @var \Drupal\dgi_fixity\FixityCheckStorageInterface $storage */
$storage = \Drupal::entityTypeManager()->getStorage('fixity_check');
$checks = $storage->loadByProperties([
'file' => $entity->id(),
]);
// Remove checks for non-existent files.
$storage->delete($checks);
}
/**
* Implements hook_ENTITY_TYPE_revision_create().
*/
function dgi_fixity_fixity_check_revision_create(EntityInterface $entity) {
/** @var \Drupal\dgi_fixity\FixityCheckInterface $entity*/
Cache::invalidateTags($entity->getAuditCacheTags());
}
/**
* Implements hook_ENTITY_TYPE_revision_delete().
*/
function dgi_fixity_fixity_check_revision_delete(EntityInterface $entity) {
/** @var \Drupal\dgi_fixity\FixityCheckInterface $entity*/
Cache::invalidateTags($entity->getAuditCacheTags());
}
/**
* Implements hook_help().
*/
function dgi_fixity_help($route_name, RouteMatchInterface $route_match) {
switch ($route_name) {
case 'help.page.dgi_fixity':
case 'dgi_fixity.settings':
$output = array_fill(0, 2, ['#type' => 'html_tag', '#tag' => 'p']);
$output[0]['#value'] = \t(
'The Fixity module validates selected files by generating hashes and comparing it against stored values produced by the <a href="@file_hash">File Hash module</a> for selected files uploaded to the site.',
['@file_hash' => URL::fromRoute('help.page', ['name' => 'filehash'])->toString()],
);
return $output;
}
}

4
dgi_fixity.permissions.yml

@ -0,0 +1,4 @@
administer fixity checks:
title: 'Administer Fixity Checks'
view fixity checks:
title: 'View Fixity Checks'

57
dgi_fixity.routing.yml

@ -0,0 +1,57 @@
dgi_fixity.settings:
path: '/admin/config/fixity'
defaults:
_form: '\Drupal\dgi_fixity\Form\SettingsForm'
_title: 'Fixity'
requirements:
_permission: 'access administration pages,administer site configuration,administer fixity checks'
dgi_fixity.batch:
path: '/admin/config/fixity/check'
defaults:
_form: '\Drupal\dgi_fixity\Form\BatchForm'
_title: 'Check'
requirements:
_permission: 'access administration pages,administer fixity checks'
dgi_fixity.generate:
path: '/admin/config/fixity/generate'
defaults:
_form: '\Drupal\dgi_fixity\Form\GenerateForm'
_title: 'Generate'
requirements:
_permission: 'access administration pages,administer fixity checks'
entity.fixity_check.revision:
path: '/fixity/{fixity_check}/revisions/{fixity_check_revision}'
defaults:
_controller: '\Drupal\Core\Entity\Controller\EntityViewController::viewRevision'
_title_callback: '\Drupal\Core\Entity\Controller\EntityController::title'
requirements:
_entity_access: 'fixity_check_revision.view revision'
fixity_check: \d+
fixity_check_revision: \d+
options:
_admin_route: TRUE
parameters:
fixity_check:
type: entity:fixity_check
fixity_check_revision:
type: entity_revision:fixity_check
entity.fixity_check.revision_delete_confirm:
path: '/fixity/{fixity_check}/revisions/{fixity_check_revision}/delete'
defaults:
_form: '\Drupal\dgi_fixity\Form\RevisionDeleteForm'
_title: 'Delete earlier check'
requirements:
_entity_access: 'fixity_check_revision.delete revision'
fixity_check: \d+
fixity_check_revision: \d+
options:
_admin_route: TRUE
parameters:
fixity_check:
type: entity:fixity_check
fixity_check_revision:
type: entity_revision:fixity_check

18
dgi_fixity.services.yml

@ -0,0 +1,18 @@
services:
logger.channel.dgi_fixity:
class: Drupal\Core\Logger\LoggerChannel
factory: logger.factory:get
arguments: ['dgi_fixity']
dgi_fixity.fixity_check:
class: Drupal\dgi_fixity\FixityCheckService
arguments: ['@string_translation', '@config.factory', '@entity_type.manager', '@datetime.time', '@plugin.manager.mail', '@logger.channel.dgi_fixity', '@filehash']
dgi_fixity.route_subscriber:
class: Drupal\dgi_fixity\Routing\FixityCheckRouteSubscriber
arguments: ['@entity_type.manager', '@dgi_fixity.fixity_check']
tags:
- { name: event_subscriber }
dgi_fixity.paramconverter.fixity:
class: Drupal\dgi_fixity\Routing\FixityCheckConverter
arguments: ['@entity_type.manager', '@entity.repository', '@dgi_fixity.fixity_check']
tags:
- { name: paramconverter }

54
dgi_fixity.views.inc

@ -0,0 +1,54 @@
<?php
/**
* @file
* Provide views data for file.module.
*/
/**
* Implements hook_views_data_alter().
*/
function dgi_fixity_views_data_alter(&$data) {
// Reverse relationship on fixity_check.file to file_managed.fid.
$field_id = 'file';
$entity_type_id = 'fixity_check';
$entity_type_manager = \Drupal::entityTypeManager();
$entity_type = $entity_type_manager->getDefinition($entity_type_id);
/** @var \Drupal\Core\Entity\EntityFieldManager $entity_field_manager */
$entity_field_manager = \Drupal::service('entity_field.manager');
$field_definitions = $entity_field_manager->getBaseFieldDefinitions($entity_type_id);
$field_type = $field_definitions[$field_id];
// Allow relations to both the entity and revision base table.
// The fixity_check entity does use data tables.
$tables = [$entity_type->getBaseTable(), $entity_type->getRevisionTable()];
foreach ($tables as $table) {
$group = $data[$table]['table']['group'];
$pseudo_field_name = 'reverse_' . $field_type->getName() . '_' . $table;
$data['file_managed'][$pseudo_field_name] = [
'real field' => $field_type->getName(),
'relationship' => [
'title' => \t('@entity using @field',
[
'@entity' => $entity_type->getLabel(),
'@field' => $field_type->getLabel(),
],
),
'label' => $group,
'help' => \t('Relate each @entity with a @field set to the file.',
[
'@entity' => $entity_type->getLabel(),
'@field' => $field_type->getLabel(),
],
),
'group' => $group,
'id' => 'standard',
'base' => $table,
'base field' => $field_type->getName(),
'relationship field' => 'fid',
],
];
}
}

6
drush.services.yml

@ -0,0 +1,6 @@
services:
dgi_fixity.commands.fixity_check:
class: \Drupal\dgi_fixity\Commands\FixityCheck
arguments: ['@string_translation', '@logger.dblog', '@entity_type.manager']
tags:
- { name: drush.command }

113
src/Commands/FixityCheck.php

@ -0,0 +1,113 @@
<?php
namespace Drupal\dgi_fixity\Commands;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\dgi_fixity\FixityCheckBatchCheck;
use Drupal\dgi_fixity\FixityCheckBatchGenerate;
use Drush\Commands\DrushCommands;
use Psr\Log\LoggerInterface;
/**
* Drush command to perform fixity checks.
*/
class FixityCheck extends DrushCommands {
use StringTranslationTrait;
/**
* A logger instance.
*
* @var \Psr\Log\LoggerInterface
*/
protected $logger;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Creates the drush command object.
*
* @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
* The translation manager.
* @param \Psr\Log\LoggerInterface $logger
* A logger instance.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity manager.
*/
public function __construct(TranslationInterface $string_translation, LoggerInterface $logger, EntityTypeManagerInterface $entity_type_manager) {
parent::__construct();
$this->stringTranslation = $string_translation;
$this->logger = $logger;
$this->entityTypeManager = $entity_type_manager;
}
/**
* Sets the periodic check flag to FALSE for all files.
*
* @command dgi_fixity:clear
*/
public function clear() {
/** @var \Drupal\dgi_fixity\FixityCheckStorageInterface $storage */
$storage = $this->entityTypeManager->getStorage('fixity_check');
$count = $storage->countPeriodic();
if ($this->io()->confirm("This will remove periodic checks on ${count} files, are you sure?", FALSE)) {
$storage->clearPeriodic();
}
}
/**
* Creates a fixity_check entity for all previously created files.
*
* @command dgi_fixity:generate
*/
public function generate() {
$batch = FixityCheckBatchGenerate::build();
batch_set($batch);
drush_backend_batch_process();
}
/**
* Perform fixity checks on files.
*
* @option fids Comma separated list of file identifiers, or a path to a
* file containing file identifiers. The file should have each
* fid separated by a new line. If not specified the modules
* settings for sources is used to determine which files to
* check.
* @option force Skip time elapsed threshold check when processing files.
*
* @command dgi_fixity:check
*/
public function check(array $options = [
'fids' => NULL,
'force' => FALSE,
]) {
$fids = $options['fids'];
if (!is_null($fids)) {
// If a file path is provided, parse it.
if (is_file($fids)) {
if (is_readable($fids)) {
$fids = explode("\n", trim(file_get_contents($fids)));
}
else {
$this->logger->error($this->t('Cannot read file @file', ['@file' => $fids]));
return;
}
}
else {
$fids = explode(',', $fids);
}
}
$batch = FixityCheckBatchCheck::build($fids, $options['force']);
batch_set($batch);
drush_backend_batch_process();
}
}

259
src/Controller/FixityCheckController.php

@ -0,0 +1,259 @@
<?php
namespace Drupal\dgi_fixity\Controller;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Datetime\DateFormatterInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Link;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Url;
use Drupal\dgi_fixity\FixityCheckInterface;
use Drupal\dgi_fixity\FixityCheckServiceInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Controller for fixity_check tasks.
*/
class FixityCheckController extends ControllerBase {
/**
* The date formatter service.
*
* @var \Drupal\Core\Datetime\DateFormatterInterface
*/
protected $dateFormatter;
/**
* The renderer service.
*
* @var \Drupal\Core\Render\RendererInterface
*/
protected $renderer;
/**
* The fixity check service.
*
* @var \Drupal\dgi_fixity\FixityCheckServiceInterface
*/
protected $fixity;
/**
* Constructs a controller for displaying fixity_check related tasks.
*
* @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter
* The date formatter service.
* @param \Drupal\Core\Render\RendererInterface $renderer
* The renderer service.
* @param \Drupal\dgi_fixity\FixityCheckServiceInterface $fixity
* The fixity service.
*/
public function __construct(DateFormatterInterface $date_formatter, RendererInterface $renderer, FixityCheckServiceInterface $fixity) {
$this->dateFormatter = $date_formatter;
$this->renderer = $renderer;
$this->fixity = $fixity;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('date.formatter'),
$container->get('renderer'),
$container->get('dgi_fixity.fixity_check'),
);
}
/**
* Returns the audit display for the current entity.
*
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* A RouteMatch object.
*
* @return array
* Array of page elements to render.
*/
public function entityAudit(RouteMatchInterface $route_match) {
$entity = $this->getEntityFromRouteMatch($route_match);
return $this->audit($entity);
}
/**
* Generates an overview table of all revisions of a given fixity_check.
*
* @param \Drupal\dgi_fixity\FixityCheckInterface $fixity_check
* A fixity_check entity.
*
* @return array
* An array expected by \Drupal\Core\Render\RendererInterface::render().
*/
public function audit(FixityCheckInterface $fixity_check) {
$account = $this->currentUser();
$storage = $this->entityTypeManager()->getStorage('fixity_check');
$build['#title'] = $this->t('Audit for %title', [
'%title' => $fixity_check->label(),
]);
$header = [
$this->t('Performed'),
$this->t('State'),
$this->t('Operations'),
];
$canDelete = $account->hasPermission('administer fixity checks') && $fixity_check->access('delete');
$rows = [];
$defaultRevision = $fixity_check->getRevisionId();
$currentRevisionDisplayed = FALSE;
foreach ($this->getRevisionIds($fixity_check, $storage) as $revision_id) {
/** @var \Drupal\dgi_fixity\FixityCheckInterface $revision */
$revision = $storage->loadRevision($revision_id);
// Use timestamp rather than timestamp_ago to allow for caching.
$date = ($revision->wasPerformed()) ?
$revision->performed->view([
'label' => 'hidden',
'type' => 'timestamp',
]) :
$this->t('never');
$isCurrentRevision = $revision_id == $defaultRevision || (!$currentRevisionDisplayed && $revision->wasDefaultRevision());
if (!$isCurrentRevision) {
$link = Link::fromTextAndUrl($date, new Url(
'entity.fixity_check.revision',
[
'fixity_check' => $fixity_check->id(),
'fixity_check_revision' => $revision_id,
]
));
}
else {
$link = $fixity_check->toLink($date);
$currentRevisionDisplayed = TRUE;
}
$state = $revision->state->view([
'label' => 'hidden',
'type' => 'dgi_fixity_state',
]);
$row = [
[
'data' => $link->toRenderable(),
],
[
'data' => $state,
],
];
if ($isCurrentRevision) {
$row[] = [
'data' => [
'#prefix' => '<em>',
'#markup' => $this->t('Current revision'),
'#suffix' => '</em>',
],
];
$rows[] = [
'data' => $row,
'class' => ['revision-current'],
];
}
else {
$links = [];
if ($canDelete) {
$links['delete'] = [
'title' => $this->t('Delete'),
'url' => Url::fromRoute(
'entity.fixity_check.revision_delete_confirm',
[
'fixity_check' => $fixity_check->id(),
'fixity_check_revision' => $revision_id,
]
),
];
}
$row[] = [
'data' => [
'#type' => 'operations',
'#links' => $links,
],
];
$rows[] = $row;
}
}
$build['fixity_check_revisions_table'] = [
'#theme' => 'table',
'#rows' => $rows,
'#header' => $header,
'#attributes' => [
'class' => 'fixity_check-revision-table',
],
];
$build['pager'] = [
'#type' => 'pager',
];
$build['#cache'] = [
'keys' => [
'entity_view', 'fixity_check', $fixity_check->id(), 'revisions',
],
'contexts' => [
// Date displayed varies by timezone.
'timezone',
],
'tags' => $fixity_check->getAuditCacheTags(),
'bin' => 'render',
];
$this->renderer->addCacheableDependency($build, $fixity_check);
return $build;
}
/**
* Gets a list of fixity_check revision IDs for a given fixity_check.
*
* @param \Drupal\dgi_fixity\FixityCheckInterface $fixity_check
* Media entity to search for revisions.
* @param \Drupal\Core\Entity\EntityStorageInterface $storage
* Media storage to load revisions from.
*
* @return int[]
* fixity_check revision IDs in descending order.
*/
protected function getRevisionIds(FixityCheckInterface $fixity_check, EntityStorageInterface $storage) {
$result = $storage->getQuery()
->allRevisions()
->condition('id', $fixity_check->id())
->sort('performed', 'DESC')
->pager(50)
->execute();
return array_keys($result);
}
/**
* Retrieves entity from route match.
*
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The route match.
*
* @return \Drupal\dgi_fixity\FixityCheckInterface|null
* The fixity check entity from the passed-in route match.
*/
protected function getEntityFromRouteMatch(RouteMatchInterface $route_match): ?FixityCheckInterface {
// Option added by Route Subscriber.
$parameter_name = $route_match->getRouteObject()->getOption('_fixity_entity_type_id');
return ($parameter_name == 'fixity_check') ?
$route_match->getParameter($parameter_name) :
$this->fixity->fromEntity($route_match->getParameter($parameter_name));
}
}

343
src/Entity/FixityCheck.php

@ -0,0 +1,343 @@
<?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();
}
/**
* {@inheritdoc}
*/
public function getAuditCacheTags() {
return [
'fixity_check:' . $this->id() . ':revisions_list',
];
}
/**
* Defines allowed states for AllowedValues constraints.
*
* @return int[]
* The allowed states.
*/
public static function getAllowedStates() {
return array_keys(static::STATES);
}
}

50
src/FixityCheckAccessControlHandler.php

@ -0,0 +1,50 @@
<?php
namespace Drupal\dgi_fixity;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Entity\EntityAccessControlHandler;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Session\AccountInterface;
/**
* Defines an access control handler for fixity_check entities.
*/
class FixityCheckAccessControlHandler extends EntityAccessControlHandler {
/**
* {@inheritdoc}
*/
protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) {
/** @var \Drupal\dgi_fixity\FixityCheckInterface $entity */
$admin_permission = $this->entityType->getAdminPermission();
switch ($operation) {
case 'view':
case 'view revision':
return AccessResult::allowedIfHasPermission($account, 'view fixity checks')->cachePerPermissions();
case 'delete':
return AccessResult::allowedIfHasPermission($account, $admin_permission)->cachePerPermissions();
case 'delete revision':
// Not possible to delete the default revision, instead the user
// should delete the actual entity.
if ($entity->isDefaultRevision()) {
return AccessResult::forbidden()->addCacheableDependency($entity);
}
return AccessResult::allowedIfHasPermission($account, $admin_permission)->cachePerPermissions();
default:
return AccessResult::forbidden()->cachePerPermissions();
}
}
/**
* {@inheritdoc}
*/
protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) {
return AccessResult::allowedIfHasPermission($account, $this->entityType->getAdminPermission());
}
}

286
src/FixityCheckBatchCheck.php

@ -0,0 +1,286 @@
<?php
namespace Drupal\dgi_fixity;
use Drupal\Core\Batch\BatchBuilder;
use Drupal\Core\StringTranslation\PluralTranslatableMarkup;
use Drupal\dgi_fixity\Form\SettingsForm;
/**
* Performs fixity checks.
*/
class FixityCheckBatchCheck {
/**
* Creates a batch for performing fixity checks.
*
* @param int[] $fids
* A list of file identifiers, if not specified files with periodic checks
* enabled will be selected.
* @param bool $force
* A flag to indicate if the check should be performed even if the time
* elapsed since the last check has not exceed the required threshold.
* @param int $batch_size
* The number of of files to process at a time.
* If not specified it will default to the modules configuration.
*/
public static function build(array $fids = NULL, bool $force = FALSE, int $batch_size = NULL) {
$batch_size = is_null($batch_size) ? \Drupal::config(SettingsForm::CONFIG_NAME)->get(SettingsForm::BATCH_SIZE) : $batch_size;
return is_null($fids) ?
static::buildPeriodic($force, $batch_size) :
static::buildFixed($fids, $force, $batch_size);
}
/**
* Creates a batch for processing a fixed list of file identifiers.
*/
protected static function buildFixed(array $fids, bool $force, int $batch_size) {
$builder = new BatchBuilder();
return $builder
->setTitle(\t('Performing checks on @count file(s)', ['@count' => count($fids)]))
->setInitMessage(\t('Starting'))
->setErrorMessage(\t('Batch has encountered an error'))
->addOperation([static::class, 'processFixedList'], [
$fids,
$force,
$batch_size,
])
->setFinishCallback([static::class, 'finished'])
->toArray();
}
/**
* Creates a batch for processing files that have periodic checks enabled.
*/
public static function buildPeriodic(bool $force, int $batch_size) {
$sources = \Drupal::config(SettingsForm::CONFIG_NAME)->get(SettingsForm::SOURCES);
if (empty($sources)) {
throw new \InvalidArgumentException("No sources specified, check the modules configuration.");
}
$builder = new BatchBuilder();
foreach ($sources as $source) {
$builder->addOperation(
[static::class, 'processSource'],
[$source, $batch_size],
);
}
return $builder
->setTitle(\t('Enumerating periodic checks from @count Source(s)', ['@count' => count($sources)]))
->setInitMessage(\t('Starting'))
->setErrorMessage(\t('Batch has encountered an error'))
->addOperation([static::class, 'processPeriodic'], [$force, $batch_size])
->setFinishCallback([static::class, 'finished'])
->toArray();
}
/**
* Check the given files.
*
* @param int[] $fids
* A list of file identifiers.
* @param bool $force
* A flag to indicate if the check should be performed even if the time
* elapsed since the last check has not exceed the required threshold.
* @param int $batch_size
* The amount of files each time this process runs.
* @param array|object $context
* Context for operations.
*/
public static function processFixedList(array $fids, bool $force, int $batch_size, &$context) {
$sandbox = &$context['sandbox'];
$results = &$context['results'];
if (!isset($sandbox['total'])) {
$sandbox['offset'] = 0;
$sandbox['total'] = count($fids);
$results['successful'] = 0;
$results['ignored'] = 0;
$results['skipped'] = 0;
$results['failed'] = 0;
$results['errors'] = [];
}
$chunk = array_slice($fids, $sandbox['offset'], $batch_size);
$end = min($sandbox['total'], $sandbox['offset'] + count($chunk));
$context['message'] = \t('Processing @start to @end of @total', [
'@start' => $sandbox['offset'],
'@end' => $end,
'@total' => $sandbox['total'],
]);
$files = \Drupal::service('entity_type.manager')->getStorage('file')->loadMultiple($chunk);
// It is possible for non existing fids to be listed in $chunk, in such
// cases this is ignored.
$results['ignored'] += count(array_diff($chunk, array_keys($files)));
static::check($files, $force, $results);
$sandbox['offset'] = $end;
$context['finished'] = $sandbox['offset'] / $sandbox['total'];
}
/**
* Enable periodic checks on files as returned by the give source view.
*/
public static function processSource(string $source, $batch_size, &$context) {
$results = &$context['results'];
// Do not track success/failure on processing source, as that is done for
// checks only. Errors however do get passed though.
if (!isset($results['errors'])) {
$results['errors'] = [];
}
/** @var \Drupal\dgi_fixity\FixityCheckServiceInterface $fixity */
$fixity = \Drupal::service('dgi_fixity.fixity_check');
$view = $fixity->source($source, $batch_size);
$view->execute();
// Only processes those which have not already enabled periodic checks.
foreach ($view->result as $row) {
try {
/** @var \Drupal\dgi_fixity\FixityCheckInterface $check */
$check = $view->field['periodic']->getEntity($row);
$check->setPeriodic(TRUE);
$check->save();
}
catch (\Exception $e) {
$results['errors'][] = \t('Encountered an exception: @exception', [
'@exception' => $e,
]);
// In practice exceptions in this case shouldn't arise, but if they do
// exit to prevent an infinite loop by exiting the operation.
$context['finished'] = 1;
return;
}
}
// End when we have exhausted all inputs.
$context['finished'] = count($view->result) == 0;
}
/**
* Checks all files which have enabled periodic fixity checks.
*
* @param bool $force
* A flag to indicate if the check should be performed even if the time
* elapsed since the last check has not exceed the required threshold.
* @param int $batch_size
* The amount of files each time this process runs.
* @param array|object $context
* Context for operations.
*/
public static function processPeriodic(bool $force, int $batch_size, &$context) {
/** @var \Drupal\dgi_fixity\FixityCheckStorageInterface $storage */
$storage = \Drupal::entityTypeManager()->getStorage('fixity_check');
$sandbox = &$context['sandbox'];
$results = &$context['results'];
if (!isset($sandbox['offset'])) {
$sandbox['offset'] = 0;
$sandbox['remaining'] = $storage->countPeriodic();
$results['successful'] = 0;
$results['ignored'] = 0;
$results['skipped'] = 0;
$results['failed'] = 0;
$results['errors'] = $results['errors'] ?? [];
}
$files = $storage->getPeriodic($sandbox['offset'], $batch_size);
$end = min($sandbox['total'], $sandbox['offset'] + count($files));
$context['message'] = \t('Processing @start to @end', [
'@start' => $sandbox['offset'],
'@end' => $end,
]);
static::check($files, $force, $results);
$sandbox['offset'] = $end;
$remaining = $storage->countPeriodic();
$progress_halted = $sandbox['remaining'] == $remaining;
$sandbox['remaining'] = $remaining;
// End when we have exhausted all inputs or progress has halted.
$context['finished'] = empty($files) || $progress_halted;
}
/**
* Performs a fixity check on the given list of files.
*
* @param \Drupal\file\FileInterface[] $files
* A list of file identifiers.
* @param bool $force
* A flag to indicate if the check should be performed even if the time
* elapsed since the last check has not exceed the required threshold.
* @param array &$results
* An associative array with the results of the fixity checks
* - missing: The number of file identifiers for which no files exist.
* - successful: The number of files that were successfully checked.
* - errors: A list of error messages if any occurred.
*/
protected static function check(array $files, bool $force, array &$results) {
/** @var \Drupal\dgi_fixity\FixityCheckServiceInterface $fixity */
$fixity = \Drupal::service('dgi_fixity.fixity_check');
foreach ($files as $file) {
try {
$result = $fixity->check($file, $force);
if ($result instanceof FixityCheckInterface) {
if ($result->passed()) {
$results['successful']++;
}
}
else {
// The check was not performed as the time elapsed since the last
// check did not exceed the required threshold.
$results['skipped']++;
}
}
catch (\Exception $e) {
$results['failed']++;
$results['errors'][] = \t('Encountered an exception: @exception', [
'@exception' => $e,
]);
}
}
}
/**
* Batch Finished callback.
*
* @param bool $success
* Success of the operation.
* @param array $results
* Array of results for post processing.
* @param array $operations
* Array of operations.
*/
public static function finished($success, array $results, array $operations) {
$messenger = \Drupal::messenger();
$messenger->addStatus(new PluralTranslatableMarkup(
$results['successful'] + $results['ignored'] + $results['skipped'] + $results['failed'],
'Processed @count item in total.',
'Processed @count items in total.'
));
$messenger->addStatus(new PluralTranslatableMarkup(
$results['successful'],
'@count was successful.',
'@count were successful.',
));
$messenger->addStatus(new PluralTranslatableMarkup(
$results['ignored'],
'@count was ignored.',
'@count were ignored.',
));
$messenger->addStatus(new PluralTranslatableMarkup(
$results['skipped'],
'@count was skipped.',
'@count were skipped.',
));
$messenger->addStatus(\t(
'@count failed.', ['@count' => $results['failed']]
));
$error_count = count($results['errors']);
if ($error_count > 0) {
$messenger->addMessage(new PluralTranslatableMarkup(
$error_count,
'@count error occurred.',
'@count errors occurred.',
));
foreach ($results['errors'] as $error) {
$messenger->addError($error);
}
}
}
}

117
src/FixityCheckBatchGenerate.php

@ -0,0 +1,117 @@
<?php
namespace Drupal\dgi_fixity;
use Drupal\Core\Batch\BatchBuilder;
use Drupal\Core\StringTranslation\PluralTranslatableMarkup;
use Drupal\dgi_fixity\Form\SettingsForm;
/**
* Generates a fixity_check for all previously created files.
*/
class FixityCheckBatchGenerate {
/**
* Creates a batch for this service.
*
* @param int $batch_size
* The number of of files to process at a time.
* If not specified it will default to the modules configuration.
*/
public static function build($batch_size = NULL) {
if (is_null($batch_size)) {
$batch_size = \Drupal::config(SettingsForm::CONFIG_NAME)->get(SettingsForm::BATCH_SIZE);
}
$builder = new BatchBuilder();
return $builder
->setTitle(\t('Generating Fixity Checks for previously created files'))
->setInitMessage(\t('Starting'))
->setErrorMessage(\t('Batch has encountered an error'))
->addOperation([static::class, 'generate'], [$batch_size])
->setFinishCallback([static::class, 'finished'])
->toArray();
}
/**
* Generates fixity_check entity for previously created files.
*
* @param int $batch_size
* The number of of files to process at a time.
* @param array|object $context
* Context for operations.
*/
public static function generate($batch_size, &$context) {
/** @var \Drupal\dgi_fixity\FixityCheckStorageInterface $storage */
$storage = \Drupal::entityTypeManager()->getStorage('fixity_check');
$sandbox = &$context['sandbox'];
$results = &$context['results'];
if (!isset($results['successful'])) {
$results['successful'] = 0;
$results['failed'] = 0;
$results['errors'] = [];
$sandbox['remaining'] = $storage->countMissing();
}
$files = $storage->getMissing(0, $batch_size);
foreach ($files as $file) {
$check = $storage->create(['file' => $file->id()]);
try {
$check->save();
$results['successful']++;
}
catch (\Exception $e) {
$results['failed']++;
$results['errors'][] = \t('Encountered an exception: @exception', [
'@exception' => $e,
]);
}
}
$remaining = $storage->countMissing();
$progress_halted = $sandbox['remaining'] == $remaining;
$sandbox['remaining'] = $remaining;
// End when we have exhausted all inputs or progress has halted.
$context['finished'] = empty($files) || $progress_halted;
}
/**
* Batch Finished callback.
*
* @param bool $success
* Success of the operation.
* @param array $results
* Array of results for post processing.
* @param array $operations
* Array of operations.
*/
public static function finished($success, array $results, array $operations) {
$messenger = \Drupal::messenger();
$messenger->addStatus(new PluralTranslatableMarkup(
$results['successful'] + $results['failed'],
'Processed @count item in total.',
'Processed @count items in total.'
));
$messenger->addStatus(new PluralTranslatableMarkup(
$results['successful'],
'@count was successful.',
'@count were successful.',
));
$messenger->addStatus(\t(
'@count failed.', ['@count' => $results['failed']]
));
$error_count = count($results['errors']);
if ($error_count > 0) {
$messenger->addMessage(new PluralTranslatableMarkup(
$error_count,
'@count error occurred.',
'@count errors occurred.',
));
foreach ($results['errors'] as $error) {
$messenger->addError($error);
}
}
}
}

237
src/FixityCheckInterface.php

@ -0,0 +1,237 @@
<?php
namespace Drupal\dgi_fixity;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\RevisionableInterface;
use Drupal\file\Entity\File;
/**
* Provides an interface defining a fixity_check entity.
*/
interface FixityCheckInterface extends ContentEntityInterface, RevisionableInterface {
/**
* The check has not been performed or is some other undefined state.
*/
const STATE_UNDEFINED = 0;
/**
* The generated checksums match the recorded values.
*/
const STATE_MATCHES = 1;
/**
* The generated checksums do not match the recorded values.
*/
const STATE_MISMATCHES = 2;
/**
* The file is missing.
*/
const STATE_MISSING = 3;
/**
* One or more checksum(s) are missing from the files recorded checksums.
*/
const STATE_NO_CHECKSUM = 4;
/**
* One or more checksum(s) could not be generated.
*/
const STATE_GENERATION_FAILED = 5;
/**
* Properties of each state.
*
* @var array
* An associative array with the following properties.
* - label: The label to use when displaying a single check.
* - singular: The singular label to use when aggregating checks.
* - plural: The plural label to use when aggregating checks.
* - passed: TRUE if this state indicates the check passed FALSE otherwise.
*/
const STATES = [
self::STATE_UNDEFINED => [
'label' => 'Undefined',
'singular' => '@count check is undefined',
'plural' => '@count checks are undefined',
'passed' => FALSE,
],
self::STATE_MATCHES => [
'label' => 'Matched recorded values',
'singular' => '@count check match the recorded checksum(s)',
'plural' => '@count checks matched the recorded checksum(s)',
'passed' => TRUE,
],
self::STATE_MISMATCHES => [
'label' => 'Did not match recorded values',
'singular' => '@count check did not match the recorded checksum(s)',
'plural' => '@count checks did not match the recorded checksum(s)',
'passed' => FALSE,
],
self::STATE_MISSING => [
'label' => 'Could not be performed: File missing',
'singular' => '@count file is missing and could not be checked',
'plural' => '@count files are missing and could not be checked',
'passed' => FALSE,
],
self::STATE_NO_CHECKSUM => [
'label' => 'Could not be performed: Missing recorded checksum(s)',
'singular' => '@count file is missing a recorded checksum',
'plural' => '@count files are missing recorded checksums',
'passed' => FALSE,
],
self::STATE_GENERATION_FAILED => [
'label' => 'Could not be performed: Could not generate checksum(s)',
'singular' => '@count check could not generate a checksum',
'plural' => '@count checks could not generate checksums',
'passed' => FALSE,
],
];
/**
* Gets the file this check was performed against.
*
* @return \Drupal\file\Entity\File
* The file associated with this check or NULL if not set.
*/
public function getFile(): ?File;
/**
* Sets the state of the check.
*
* @param \Drupal\file\Entity\File $file
* The state of the check.
*
* @return $this
*/
public function setFile(File $file): FixityCheckInterface;
/**
* Gets the state of the check.
*
* @return int
* The state of the check.
*/
public function getState(): int;
/**
* Sets the state of the check.
*
* @param int $state
* The state of the check.
*
* @return $this
*
* @throws \InvalidArgumentException
* If $state is not valid.
*/
public function setState(int $state): FixityCheckInterface;
/**
* Gets the human readable representation of the state.
*
* @return string
* The state.
*/
public function getStateLabel(): string;
/**
* Gets the given property of the given state if defined.
*
* @param int $state
* The state of whose properties are fetched.
* @param string $property
* The property to get.
*
* @return mixed|null
* The property if defined otherwise NULL.
*
* @see \Drupal\dgi_fixity\FixityCheckInterface::STATES
*/
public static function getStateProperty(int $state, string $property);
/**
* Checks if the check passed.
*
* @see \Drupal\dgi_fixity\FixityCheckInterface::STATES
*
* @return bool
* TRUE if the generated checksums match the recorded values, FALSE
* otherwise.
*/
public function passed(): bool;
/**
* Gets the timestamp of when the check was performed.
*
* @return int
* The timestamp of the check. 0 indicates the check was not performed.
*/
public function getPerformed(): int;
/**
* Sets the timestamp of when the check was performed.
*
* @param int $performed
* The timestamp when the check was performed.
*
* @return $this
*/
public function setPerformed(int $performed): FixityCheckInterface;
/**
* TRUE if this check was performed, FALSE otherwise.
*
* @return bool
* TRUE if this check was performed, FALSE otherwise.
*/
public function wasPerformed(): bool;
/**
* Checks if periodic checks are enabled.
*
* @return int
* TRUE if periodic checks are enabled, FALSE otherwise.
*/
public function getPeriodic(): bool;
/**
* Enable or disable periodic checks.
*
* @param bool $periodic
* TRUE to enable periodic checks, FALSE otherwise.
*
* @return $this
*/
public function setPeriodic(bool $periodic): FixityCheckInterface;
/**
* Gets the timestamp of when the check was queued.
*
* @return int
* The timestamp when the check was queued.
* 0 indicates the check is not queued.
*/
public function getQueued(): int;
/**
* Sets the timestamp of when the check was queued.
*
* @param int $queued
* The timestamp when queued.
*
* @return $this
*/
public function setQueued(int $queued): FixityCheckInterface;
/**
* The cache tags associated with the audit display of this entity.
*
* @return string[]
* The cache tags.
*/
public function getAuditCacheTags();
}

123
src/FixityCheckListBuilder.php

@ -0,0 +1,123 @@
<?php
namespace Drupal\dgi_fixity;
use Drupal\Core\Datetime\DateFormatterInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityListBuilder;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Render\RendererInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a listing of fixity check items.
*/
class FixityCheckListBuilder extends EntityListBuilder {
/**
* The date formatter service.
*
* @var \Drupal\Core\Datetime\DateFormatterInterface
*/
protected $dateFormatter;
/**
* The renderer service.
*
* @var \Drupal\Core\Render\RendererInterface
*/
protected $renderer;
/**
* Constructs a new FixityCheckListBuilder object.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type definition.
* @param \Drupal\Core\Entity\EntityStorageInterface $storage
* The entity storage class.
* @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter
* The date formatter service.
* @param \Drupal\Core\Render\RendererInterface $renderer
* The renderer service.
*/
public function __construct(EntityTypeInterface $entity_type, EntityStorageInterface $storage, DateFormatterInterface $date_formatter, RendererInterface $renderer) {
parent::__construct($entity_type, $storage);
$this->dateFormatter = $date_formatter;
$this->renderer = $renderer;
}
/**
* {@inheritdoc}
*/
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
$entity_type_manager = $container->get('entity_type.manager');
return new static(
$entity_type,
$entity_type_manager->getStorage($entity_type->id()),
$container->get('date.formatter'),
$container->get('renderer'),
);
}
/**
* {@inheritdoc}
*/
public function buildHeader() {
$header = [];
$header += [
'link' => [
'data' => $this->t('Check'),
],
'state' => [
'data' => $this->t('State'),
],
'performed' => [
'data' => $this->t('Performed'),
],
];
return $header + parent::buildHeader();
}
/**
* {@inheritdoc}
*/
public function buildRow(EntityInterface $entity) {
/** @var \Drupal\dgi_fixity\FixityCheckInterface $entity */
$row = [
'link' => $entity->toLink(),
];
$row['state']['data'] = $entity->state->view([
'label' => 'hidden',
'type' => 'dgi_fixity_state',
]);
// Use timestamp rather than timestamp_ago to allow for caching.
$row['performed']['data'] = $entity->wasPerformed() ?
$entity->performed->view([
'label' => 'hidden',
'type' => 'timestamp',
'weight' => 1,
]) :
['#markup' => $this->t('never')];
return $row + parent::buildRow($entity);
}
/**
* {@inheritdoc}
*/
protected function getEntityIds() {
$query = $this->getStorage()->getQuery()
->accessCheck(TRUE)
->sort('performed', 'DESC');
// Only add the pager if a limit is specified.
if ($this->limit) {
$query->pager($this->limit);
}
return $query->execute();
}
}

402
src/FixityCheckService.php

@ -0,0 +1,402 @@
<?php
namespace Drupal\dgi_fixity;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Link;
use Drupal\Core\Mail\MailManagerInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\dgi_fixity\Entity\FixityCheck;
use Drupal\dgi_fixity\Form\SettingsForm;
use Drupal\file\Entity\File;
use Drupal\file\FileInterface;
use Drupal\filehash\FileHash;
use Drupal\media\MediaInterface;
use Drupal\views\ViewExecutable;
use Drupal\views\Views;
use Psr\Log\LoggerInterface;
/**
* Decorates the FileHash services adding additional functionality.
*/
class FixityCheckService implements FixityCheckServiceInterface {
use StringTranslationTrait;
/**
* Config factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $config;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* A date time instance.
*
* @var \Drupal\Component\Datetime\TimeInterface
*/
protected $time;
/**
* The mail manager service.
*
* @var \Drupal\Core\Mail\MailManagerInterface
*/
protected $mailManager;
/**
* The logger for this service.
*
* @var Psr\Log\LoggerInterface
*/
protected $logger;
/**
* The service to decorate.
*
* @var \Drupal\filehash\FileHash
*/
protected $filehash;
/**
* Constructor.
*/
public function __construct(TranslationInterface $string_translation, ConfigFactoryInterface $config, EntityTypeManagerInterface $entity_type_manager, TimeInterface $time, MailManagerInterface $mail_manager, LoggerInterface $logger, FileHash $filehash) {
$this->stringTranslation = $string_translation;
$this->config = $config;
$this->entityTypeManager = $entity_type_manager;
$this->time = $time;
$this->mailManager = $mail_manager;
$this->logger = $logger;
$this->filehash = $filehash;
}
/**
* {@inheritdoc}
*/
public function fromEntityTypes(): array {
return [
'media',
'file',
];
}
/**
* {@inheritdoc}
*/
public function fromEntity(EntityInterface $entity): ?FixityCheckInterface {
$entity_type_id = $entity->getEntityTypeId();
switch ($entity_type_id) {
case 'media':
/** @var \Drupal\media\MediaInterface $entity */
return $this->fromMedia($entity);
case 'file':
/** @var \Drupal\file\FileInterface $entity */
return $this->fromFile($entity);
default:
throw new \InvalidArgumentException("Cannot convert {$entity_type_id} to fixity_check.");
}
}
/**
* {@inheritdoc}
*/
public function fromFile($file): ?FixityCheckInterface {
$fid = $file instanceof FileInterface ? $file->id() : (int) $file;
// It is only possible to have a single fixity_check entity per-file.
$results = $this->entityTypeManager->getStorage('fixity_check')->loadByProperties(['file' => $fid]);
if (count($results) === 1) {
/** @var \Drupal\dgi_fixity\FixityCheckInterface $fixity_check */
$fixity_check = reset($results);
}
else {
$fixity_check = FixityCheck::create([
'file' => $fid,
]);
}
return $fixity_check;
}
/**
* {@inheritdoc}
*/
public function fromMedia(MediaInterface $media): ?FixityCheckInterface {
$fid = $media->getSource()->getSourceFieldValue($media);
return $this->fromFile($fid);
}
/**
* {@inheritdoc}
*/
public function threshold(): int {
$threshold = &drupal_static(__FUNCTION__);
if (is_null($threshold)) {
$settings = $this->config->get(SettingsForm::CONFIG_NAME);
$threshold = strtotime($settings->get(SettingsForm::THRESHOLD), $this->time->getRequestTime());
}
return $threshold;
}
/**
* {@inheritdoc}
*/
public function scheduled(FixityCheckInterface $check): ?int {
if ($check->getPeriodic()) {
$now = time();
if ($check->wasPerformed()) {
$diff = $now - $this->threshold();
return $check->getPerformed() + $diff;
}
// Never performed, can be performed immediately.
return $now;
}
// Not periodic therefore not scheduled.
return NULL;
}
/**
* {@inheritdoc}
*/
public function source(string $source, int $limit): ?ViewExecutable {
// Only process those which have not already enabled periodic checks.
[$view_id, $display_id] = explode(':', $source);
$view = Views::getView($view_id);
if ($view) {
$view->setDisplay($display_id);
$view->getDisplay()->setOption('entity_reference_options', ['limit' => $limit]);
$view->addHandler($display_id, 'relationship', 'file_managed', 'reverse_file_fixity_check');
$view->addHandler(
$display_id, 'filter', 'fixity_check', 'periodic',
['relationship' => 'reverse_file_fixity_check', 'value' => 0],
'periodic'
);
$view->addHandler(
$display_id, 'field', 'fixity_check', 'periodic',
['relationship' => 'reverse_file_fixity_check'],
'periodic'
);
}
return $view;
}
/**
* {@inheritdoc}
*/
public function check(File $file, bool $force = FALSE) {
/** @var \Drupal\dgi_fixity\Entity\FixityCheckInterface[] $existing_checks */
$existing_checks = $this->entityTypeManager->getStorage('fixity_check')->loadByProperties(['file' => $file->id()]);
if (empty($existing_checks)) {
$check = FixityCheck::create()->setFile($file);
}
else {
// Should only ever be at most one due to the UniqueFieldEntityReference
// constraint on the file field.
$check = reset($existing_checks);
// Do not perform if the threshold for time since the last check has not
// been exceeded.
if (!$force) {
if ($check->getPerformed() > $this->threshold()) {
return NULL;
}
}
// Trigger a new revision (clears the performed / state fields).
// If the check has never been performed before do not modify the
// existing version.
if ($check->wasPerformed()) {
$check->setNewRevision();
}
}
$uri = $file->getFileUri();
// Assume success until proven untrue.
$state = FixityCheck::STATE_MATCHES;
// If column is set, only generate that hash.
foreach ($this->filehash->algos() as $column => $algo) {
// Nothing to do if the previous checksum value is not known.
if (!isset($file->{$column})) {
$state = FixityCheck::STATE_NO_CHECKSUM;
break;
}
// Nothing to do if file URI is empty.
if (NULL === $uri || '' === $uri || !file_exists($uri)) {
$state = FixityCheck::STATE_MISSING;
break;
}
// Unreadable files will have NULL hash values.
elseif (preg_match('/^blake2b_([0-9]{3})$/', $algo, $matches)) {
$hash = $this->filehash->blake2b($uri, $matches[1] / 8) ?: NULL;
}
else {
$hash = hash_file($algo, $uri) ?: NULL;
}
if ($hash === NULL) {
$state = FixityCheck::STATE_GENERATION_FAILED;
break;
}
if ($file->{$column}->value !== $hash) {
$state = FixityCheck::STATE_MISMATCHES;
break;
}
}
$check->setState($state);
$check->setPerformed($this->time->getRequestTime());
$check->setQueued(0);
$check->save();
// Log results.
$message = '@entity-type %label: %state';
$args = [
'@entity-type' => $check->getEntityType()->getSingularLabel(),
'%label' => $check->label(),
'%state' => $check->getStateProperty($check->getState(), 'label'),
'link' => Link::createFromRoute(
$this->t('View'),
'entity.fixity_check.revision',
[
'fixity_check' => $check->id(),
'fixity_check_revision' => $check->getRevisionId(),
],
)->toString(),
];
if ($check->passed()) {
$this->logger->info($message, $args);
}
else {
$this->logger->error($message, $args);
}
return $check;
}
/**
* {@inheritdoc}
*/
public function stats(): array {
$storage = $this->entityTypeManager->getStorage('fixity_check');
// Group all current checks by their state.
// Ignore those that have not been performed yet.
$results = $storage->getAggregateQuery('AND')
->condition('performed', 0, '!=')
->groupBy('state')
->aggregate('id', 'COUNT')
->execute();
$failed = 0;
$states = [];
foreach ($results as $result) {
$state = $result['state'];
$count = $result['id_count'];
$states[$state] = $count;
// If there are any checks which have not 'passed', the aggregate state
// of all checks is failure.
if (FixityCheck::getStateProperty($state, 'passed') === FALSE) {
$failed += $count;
}
}
// All active checks.
$periodic = (int) $storage->getQuery('AND')
->count('id')
->condition('periodic', TRUE)
->execute();
// All checks performed ever.
$revisions = (int) $storage->getQuery('AND')
->allRevisions()
->count('id')
->execute();
// Checks which have exceeded the threshold and should be performed again.
$threshold = $this->threshold();
$current = (int) $storage->getQuery('AND')
->condition('periodic', TRUE)
->condition('performed', $threshold, '>=')
->count('id')
->execute();
// Up to date checks.
$expired = $periodic - $current;
return [
'periodic' => [
'total' => $periodic,
'current' => $current,
'expired' => $expired,
],
'revisions' => $revisions,
'states' => $states,
'failed' => $failed,
];
}
/**
* {@inheritdoc}
*/
public function summary(array $stats, array $options = []): array {
$summary = [];
$summary[] = $this->formatPlural(
$stats['revisions'],
'@count check has been performed since tracking started.',
'@count checks have been performed since tracking started.',
[],
$options
);
$summary[] = $this->formatPlural(
$stats['periodic']['total'],
'@count file is set to be checked periodically.',
'@count files are set to be checked periodically.',
[],
$options
);
$summary[] = $this->formatPlural(
$stats['periodic']['current'],
'@count periodic check is up to date.',
'@count periodic checks are up to date.',
[],
$options
);
if ($stats['periodic']['expired'] > 0) {
$summary[] = $this->formatPlural(
$stats['periodic']['expired'],
'@count periodic check is out to date.',
'@count periodic checks are out to date.',
[],
$options
);
}
if ($stats['failed'] > 0) {
$summary[] = $this->formatPlural(
$stats['failed'],
'@count check has failed.',
'@count checks have failed.',
[],
$options
);
foreach ($stats['states'] as $state => $count) {
$summary[] = $this->formatPlural(
$count,
FixityCheck::getStateProperty($state, 'singular'),
FixityCheck::getStateProperty($state, 'plural'),
[],
$options
);
}
}
return $summary;
}
}

136
src/FixityCheckServiceInterface.php

@ -0,0 +1,136 @@
<?php
namespace Drupal\dgi_fixity;
use Drupal\Core\Entity\EntityInterface;
use Drupal\file\Entity\File;
use Drupal\media\MediaInterface;
use Drupal\views\ViewExecutable;
/**
* Interface for FixityCheckService.
*/
interface FixityCheckServiceInterface {
/**
* A list of entity types which be converted into a fixity_check entity.
*
* @return string[]
* A list of entity types which be converted into a fixity_check entity.
*/
public function fromEntityTypes(): array;
/**
* Fetches or creates a fixity_check entity from the given media entity.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* A media entity.
*
* @return \Drupal\dgi_fixity\FixityCheckInterface
* The fixity_check entity for the given entity if possible NULL otherwise.
*/
public function fromEntity(EntityInterface $entity): ?FixityCheckInterface;
/**
* Fetches or creates a fixity_check entity from the given file entity.
*
* @param \Drupal\file\FileInterface|int $file
* A file entity or file entity identifier.
*
* @return \Drupal\dgi_fixity\FixityCheckInterface
* The fixity_check entity for the given file.
*/
public function fromFile($file): ?FixityCheckInterface;
/**
* Fetches or creates a fixity_check entity from the given media entity.
*
* @param \Drupal\media\MediaInterface $media
* A media entity.
*
* @return \Drupal\dgi_fixity\FixityCheckInterface
* The fixity_check entity for the given media.
*/
public function fromMedia(MediaInterface $media): ?FixityCheckInterface;
/**
* Gets the threshold for determining if checks should be performed.
*
* @return int
* The timestamp for the threshold relative to the current request time.
*/
public function threshold(): int;
/**
* Gets when the given check should be performed again.
*
* Only periodic checks can be scheduled.
*
* @return int|null
* The timestamp when the check should be performed again if scheduled to,
* NULL otherwise.
*/
public function scheduled(FixityCheckInterface $check): ?int;
/**
* Gets the view for the given source, filtered to non-periodic files only.
*
* The source must comply with checks performed by this modules settings form.
* This function does not validate it.
*
* @param string $source
* The view display identifier as selected in this modules settings form.
* @param int $limit
* The maximum results the view should return.
*
* @return \Drupal\views\ViewExecutable|null
* The filtered view.
*/
public function source(string $source, int $limit): ?ViewExecutable;
/**
* Generates a fixity_check entity from the given file.
*
* Either adds a new revision or creates a new fixity_check.
*
* @param \Drupal\file\Entity\File $file
* The file to perform the fixity check against.
* @param bool $force
* A flag to indicate if the check should be performed even if the time
* elapsed since the last check has not exceed the required threshold.
*
* @return \Drupal\dgi_fixity\Entity\FixityCheckInterface|null
* The resulting fixity_check if successful.
* NULL if the check was not performed because the time elapsed since the
* last check has not exceed the required threshold.
*/
public function check(File $file, bool $force = FALSE);
/**
* Get an associative array of statistics relating to FixityChecks.
*
* @return array
* An associative array with the following fields:
* - total: The number of active fixity checks.
* - revisions: The total number of fixity checks ever performed.
* - states: An associative array of states and their active counts.
* - current: The number of checks that are up to date.
* - expired: The number of checks that are out of date.
* - failed: The number of checks in a failed state.
*/
public function stats(): array;
/**
* Given stats provided by this service generate a summary.
*
* @param array $stats
* The stats as returned by this service.
* @param array $options
* An associative array of additional options for the TranslatableMarkup.
*
* @return \Drupal\Core\StringTranslation\TranslatableMarkup[]
* A list of messages that describe the current state of the system.
*/
public function summary(array $stats, array $options = []): array;
}

139
src/FixityCheckStorage.php

@ -0,0 +1,139 @@
<?php
namespace Drupal\dgi_fixity;
use Drupal\Core\Entity\Sql\SqlContentEntityStorage;
/**
* File storage for files.
*/
class FixityCheckStorage extends SqlContentEntityStorage implements FixityCheckStorageInterface {
/**
* {@inheritdoc}
*/
public function countMissing(): int {
/** @var \Drupal\file\FileStorage $storage */
$storage = $this->entityTypeManager->getStorage('file');
$query = $this->database->select($storage->getBaseTable(), 'file_managed');
$query->leftJoin($this->baseTable, 'fixity_check', '[file_managed].[fid] = [fixity_check].[file]');
return $query
->isNull('fixity_check.file')
->countQuery()
->execute()
->fetchField();
}
/**
* {@inheritdoc}
*/
public function getMissing(int $offset, int $limit): array {
/** @var \Drupal\file\FileStorage $storage */
$storage = $this->entityTypeManager->getStorage('file');
$query = $this->database->select($storage->getBaseTable(), 'file_managed');
$query->fields('file_managed', ['fid']);
$query->leftJoin($this->baseTable, 'fixity_check', '[file_managed].[fid] = [fixity_check].[file]');
$ids = $query
->isNull('fixity_check.file')
->orderBy('file_managed.fid')
->range($offset, $limit)
->execute()
->fetchCol();
return $storage->loadMultiple($ids);
}
/**
* {@inheritdoc}
*/
public function clearPeriodic() {
$this->database
->update($this->baseTable)
->fields([
'periodic' => 0,
])
->execute();
}
/**
* {@inheritdoc}
*/
public function countPeriodic(): int {
return $this->database
->select($this->baseTable, 'c')
->condition('c.periodic', 1)
->countQuery()
->execute()
->fetchField();
}
/**
* {@inheritdoc}
*/
public function getPeriodic(int $offset, int $limit): array {
/** @var \Drupal\file\FileStorage $storage */
$storage = $this->entityTypeManager->getStorage('file');
$ids = $this->database
->select($this->baseTable, 'c')
->condition('c.periodic', 1)
->fields('c', ['file'])
->range($offset, $limit)
->orderBy('id')
->execute()
->fetchCol();
return $storage->loadMultiple($ids);
}
/**
* {@inheritdoc}
*/
public function queue(int $queued, int $threshold, int $limit) {
// Do not over-saturate the queue.
// 10x the limit is the max we allow to be queued at a time.
$queue = \Drupal::queue('dgi_fixity.fixity_check');
if ($queue->numberOfItems() > (10 * $limit)) {
return;
}
$query = $this->database->select($this->baseTable, 'c');
// Either never performed or out of date.
$performed = $query->orConditionGroup()
->condition('c.performed', 0, '=')
->condition('c.performed', $threshold, '<=');
// Only those which have enabled periodic checking, and are not already
// queued.
$ids = $query
->fields('c', ['id'])
->condition('c.periodic', 1)
->condition('c.queued', 0)
->condition($performed)
->range(0, $limit)
->execute()
->fetchCol();
/** @var \Drupal\dgi_fixity\FixityCheckInterface $check */
foreach ($this->doLoadMultiple($ids) as $check) {
// Queue checks for processing.
if ($queue->createItem($check)) {
// Add timestamp to avoid queueing item more than once.
$check->setQueued($queued);
$check->save();
}
}
}
/**
* {@inheritdoc}
*/
public function dequeue(int $queued) {
$this->database
->update($this->baseTable)
->fields([
'queued' => 0,
])
->condition('queued', 0, '<>')
->condition('queued', $queued, '<')
->execute();
}
}

79
src/FixityCheckStorageInterface.php

@ -0,0 +1,79 @@
<?php
namespace Drupal\dgi_fixity;
use Drupal\Core\Entity\ContentEntityStorageInterface;
/**
* Defines an interface for fixity_check entity storage classes.
*/
interface FixityCheckStorageInterface extends ContentEntityStorageInterface {
/**
* Gets the number of files that have no related fixity_check entity.
*
* @return int
* The number of files that have no related fixity_check entity.
*/
public function countMissing(): int;
/**
* Gets a list of files which have no related fixity_check entity.
*
* @param int $offset
* The offset into the list of files.
* @param int $limit
* The maximum number of files to return.
*
* @return \Drupal\file\FileInterface[]
* The files selected by the given parameters.
*/
public function getMissing(int $offset, int $limit): array;
/**
* Gets the number of files that have enabled periodic checking.
*
* @return int
* The number of files that have enabled periodic checking.
*/
public function countPeriodic(): int;
/**
* Gets a list of files which have enabled periodic checking.
*
* @param int $offset
* The offset into the list files.
* @param int $limit
* The maximum number of files to return.
*
* @return \Drupal\file\FileInterface[]
* The file selected by the given parameters.
*/
public function getPeriodic(int $offset, int $limit): array;
/**
* Sets the periodic check flag on all files to FALSE.
*/
public function clearPeriodic();
/**
* Queues checks to be performed during cron up to at most the given limit.
*
* @param int $queued
* The timestamp newly queued items will be recorded under.
* @param int $threshold
* Only queue checks which were performed before the given threshold.
* @param int $limit
* The number of items to queue at most.
*/
public function queue(int $queued, int $threshold, int $limit);
/**
* Dequeues checks which have not been performed before the given timestamp.
*
* @param int $queued
* Items which were queued before this timestamp will be dequeued.
*/
public function dequeue(int $queued);
}

40
src/FixityCheckStorageSchema.php

@ -0,0 +1,40 @@
<?php
namespace Drupal\dgi_fixity;
use Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
/**
* Defines the file schema handler.
*/
class FixityCheckStorageSchema extends SqlContentEntityStorageSchema {
/**
* {@inheritdoc}
*/
protected function getSharedTableFieldSchema(FieldStorageDefinitionInterface $storage_definition, $table_name, array $column_mapping) {
$schema = parent::getSharedTableFieldSchema($storage_definition, $table_name, $column_mapping);
$field_name = $storage_definition->getName();
if ($table_name == $this->storage->getBaseTable()) {
switch ($field_name) {
case 'file':
$this->addSharedTableFieldUniqueKey($storage_definition, $schema, TRUE);
$this->addSharedTableFieldForeignKey($storage_definition, $schema, 'file_managed', 'fid');
break;
case 'state':
case 'performed':
case 'periodic':
case 'queued':
$this->addSharedTableFieldIndex($storage_definition, $schema, TRUE);
break;
}
}
return $schema;
}
}

44
src/FixityCheckViewsData.php

@ -0,0 +1,44 @@
<?php
namespace Drupal\dgi_fixity;
use Drupal\views\EntityViewsData;
/**
* Provides the views data for the fixity_check entity type.
*/
class FixityCheckViewsData extends EntityViewsData {
/**
* {@inheritdoc}
*/
public function getViewsData() {
$data = parent::getViewsData();
$data['fixity_check']['table']['wizard_id'] = 'fixity_check';
$data['fixity_check']['table']['group'] = $this->t('Fixity Check');
$data['fixity_check']['table']['join'] = [
'fixity_check_revision' => [
'left_field' => 'revision_id',
'field' => 'revision_id',
],
'file_managed' => [
'left_field' => 'fid',
'field' => 'file',
],
];
$data['fixity_check_revision']['table']['wizard_id'] = 'fixity_check_revision';
$data['fixity_check_revision']['table']['group'] = $this->t('Fixity Check revision');
$data['fixity_check_revision']['table']['join'] = [
'fixity_check' => [
'left_field' => 'revision_id',
'field' => 'revision_id',
],
'file_managed' => [
'left_field' => 'fid',
'field' => 'file',
],
];
return $data;
}
}

62
src/Form/BatchForm.php

@ -0,0 +1,62 @@
<?php
namespace Drupal\dgi_fixity\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\dgi_fixity\FixityCheckBatchCheck;
/**
* Trigger a batch check of the files selected by the modules configuration.
*
* @internal
*/
class BatchForm extends FormBase {
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'dgi_fixity_batch_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$form['info'] = [
'#type' => 'markup',
'#markup' => $this->t('Submitting this form will perform fixity checks against all files with periodic checks enabled.<br>This will automatically be done via cron, but it can be performed manually here.'),
];
$form['force'] = [
'#type' => 'checkbox',
'#title' => $this->t('Skip time elapsed check'),
'#description' => $this->t('If enabled, all files will be checked without regard to the time elapsed since the previous check was performed on the selected file.'),
'#default' => FALSE,
];
$form['actions'] = [
'#type' => 'actions',
'submit' => [
'#type' => 'submit',
'#value' => $this->t('Check'),
'#button_type' => 'primary',
],
];
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$force = boolval($form_state->getValue('force'));
try {
$batch = FixityCheckBatchCheck::build(NULL, $force);
batch_set($batch);
}
catch (\InvalidArgumentException $e) {
$this->messenger()->addError($e->getMessage());
}
}
}

160
src/Form/CheckForm.php

@ -0,0 +1,160 @@
<?php
namespace Drupal\dgi_fixity\Form;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\Core\Entity\ContentEntityConfirmFormBase;
use Drupal\Core\Entity\EntityRepositoryInterface;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\dgi_fixity\FixityCheckInterface;
use Drupal\dgi_fixity\FixityCheckServiceInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Perform a fixity check on the given fixity_check or related entity.
*
* @internal
*/
class CheckForm extends ContentEntityConfirmFormBase {
/**
* The fixity service.
*
* @var \Drupal\dgi_fixity\FixityCheckServiceInterface
*/
protected $fixity;
/**
* The entity used to derive the fixity_check entity.
*
* @var \Drupal\Core\Entity\ContentEntityInterface
*/
protected $sourceEntity;
/**
* Constructs the form.
*
* @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository
* The entity repository service.
* @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info
* The entity type bundle service.
* @param \Drupal\Component\Datetime\TimeInterface $time
* The time service.
* @param \Drupal\dgi_fixity\FixityCheckServiceInterface $fixity
* The fixity service.
*/
public function __construct(EntityRepositoryInterface $entity_repository, EntityTypeBundleInfoInterface $entity_type_bundle_info, TimeInterface $time, FixityCheckServiceInterface $fixity) {
parent::__construct($entity_repository, $entity_type_bundle_info, $time);
$this->fixity = $fixity;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity.repository'),
$container->get('entity_type.bundle.info'),
$container->get('datetime.time'),
$container->get('dgi_fixity.fixity_check')
);
}
/**
* {@inheritdoc}
*/
public function getEntityFromRouteMatch(RouteMatchInterface $route_match, $entity_type_id) {
// Support checking entity types from which we can determine the associated
// fixity_check.
$entity = parent::getEntityFromRouteMatch($route_match, $entity_type_id);
// Allow the original to affect the redirect url. For example return to the
// media's audit page rather than the fixity_check's audit page.
$this->sourceEntity = $entity;
return ($entity instanceof FixityCheckInterface) ?
$entity :
$this->fixity->fromEntity($entity);
}
/**
* {@inheritdoc}
*/
public function getDescription() {
/** @var \Drupal\dgi_fixity\FixityCheckInterface $entity */
$entity = $this->getEntity();
if ($entity->wasPerformed()) {
$scheduled = $this->fixity->scheduled($entity);
if ($scheduled) {
return $this->t('
<strong>Latest Result:</strong> %state<br/>
<strong>Last Performed:</strong> %performed<br/>
<strong>Next Scheduled:</strong> %scheduled
', [
'%state' => $entity->getStateLabel(),
'%performed' => date(DATE_RFC7231, $entity->getPerformed()),
'%scheduled' => date(DATE_RFC7231, $scheduled),
]);
}
return $this->t('
<strong>Latest Result:</strong> %state<br/>
<strong>Last Performed:</strong> %performed
', [
'%state' => $entity->getStateLabel(),
'%performed' => date(DATE_RFC7231, $entity->getPerformed()),
'%scheduled' => date(DATE_RFC7231, $scheduled),
]);
}
return $this->t('No prior check has been performed.');
}
/**
* {@inheritdoc}
*/
public function getQuestion() {
/** @var \Drupal\dgi_fixity\FixityCheckInterface $entity */
$entity = $this->getEntity();
return $this->t('Are you sure you want to perform a check on %label?', [
'%label' => $this->getEntity()->label(),
'%performed' => date(DATE_RFC7231, $entity->getPerformed()),
]);
}
/**
* {@inheritdoc}
*/
public function getCancelUrl() {
return $this->sourceEntity->toUrl('fixity-audit');
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
parent::submitForm($form, $form_state);
/** @var \Drupal\dgi_fixity\FixityCheckInterface $entity */
$entity = $this->getEntity();
/** @var \Drupal\dgi_fixity\FixityCheckInterface $check */
$check = $this->fixity->check($entity->getFile(), TRUE);
$this->setEntity($check);
unset($entity);
$message = $this->t('The @entity-type %label: %state.', [
'@entity-type' => $check->getEntityType()->getSingularLabel(),
'%label' => $check->toLink()->toString(),
'%state' => $check->getStateLabel(),
]);
if ($check->passed()) {
$this->messenger()->addStatus($message);
}
else {
$this->messenger()->addError($message);
}
// If no destination set return to the fixity check audit page.
$form_state->setRedirectUrl($this->getCancelUrl());
}
}

85
src/Form/GenerateForm.php

@ -0,0 +1,85 @@
<?php
namespace Drupal\dgi_fixity\Form;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\dgi_fixity\FixityCheckBatchGenerate;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Generates a fixity check entity for all previously existing files.
*
* @internal
*/
class GenerateForm extends FormBase {
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Constructs the form.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager) {
$this->entityTypeManager = $entity_type_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity_type.manager'),
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'dgi_fixity_generate_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
/** @var \Drupal\dgi_fixity\FixityCheckStorageInterface $storage */
$storage = $this->entityTypeManager->getStorage('fixity_check');
$form['info'] = [
'#type' => 'markup',
'#markup' => $this->t('
<p>Submitting this form generate fixity checks for @count files.</p>
<p>Generally this should only be required when the module is first installed.</p>
',
['@count' => $storage->countMissing()]
),
];
$form['actions'] = [
'#type' => 'actions',
'submit' => [
'#type' => 'submit',
'#value' => $this->t('Generate'),
'#button_type' => 'primary',
],
];
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$batch = FixityCheckBatchGenerate::build();
batch_set($batch);
}
}

132
src/Form/RevisionDeleteForm.php

@ -0,0 +1,132 @@
<?php
namespace Drupal\dgi_fixity\Form;
use Drupal\Core\Datetime\DateFormatterInterface;
use Drupal\Core\Entity\RevisionableStorageInterface;
use Drupal\Core\Form\ConfirmFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
use Drupal\dgi_fixity\FixityCheckInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Delete a fixity_check revision.
*
* @internal
*/
class RevisionDeleteForm extends ConfirmFormBase {
/**
* The fixity_check revision to delete.
*
* @var \Drupal\dgi_fixity\FixityCheckInterface
*/
protected $revision;
/**
* Entity revision storage.
*
* @var \Drupal\Core\Entity\RevisionableStorageInterface
*/
protected $storage;
/**
* The date formatter service.
*
* @var \Drupal\Core\Datetime\DateFormatterInterface
*/
protected $dateFormatter;
/**
* Constructs the form.
*
* @param \Drupal\Core\Entity\RevisionableStorageInterface $storage
* The revisionable storage.
* @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter
* The date formatter service.
*/
public function __construct(RevisionableStorageInterface $storage, DateFormatterInterface $date_formatter) {
$this->storage = $storage;
$this->dateFormatter = $date_formatter;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity_type.manager')->getStorage('fixity_check'),
$container->get('date.formatter')
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'dgi_fixity_revision_delete_confirm';
}
/**
* {@inheritdoc}
*/
public function getQuestion() {
return $this->t('Are you sure you want to delete the revision from %revision-date?', [
'%revision-date' => $this->dateFormatter->format(
$this->revision->getPerformed()
),
]);
}
/**
* {@inheritdoc}
*/
public function getCancelUrl() {
return new Url('entity.fixity_check.fixity_audit', [
'fixity_check' => $this->revision->id(),
]);
}
/**
* {@inheritdoc}
*/
public function getConfirmText() {
return $this->t('Delete');
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, FixityCheckInterface $fixity_check_revision = NULL) {
$this->revision = $fixity_check_revision;
$form = parent::buildForm($form, $form_state);
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$this->storage->deleteRevision($this->revision->getRevisionId());
$this->logger('content')->notice('Fixity Check: deleted %title revision %revision.', [
'%title' => $this->revision->label(),
'%revision' => $this->revision->getRevisionId(),
]);
$this->messenger()->addStatus(
$this->t('Revision from %revision-date of %title has been deleted.',
[
'%revision-date' => $this->dateFormatter->format(
$this->revision->getPerformed()
),
'%title' => $this->revision->label(),
]
));
$form_state->setRedirect('entity.fixity_check.fixity_audit', [
'fixity_check' => $this->revision->id(),
]);
}
}

305
src/Form/SettingsForm.php

@ -0,0 +1,305 @@
<?php
namespace Drupal\dgi_fixity\Form;
use Drupal\Component\Utility\Tags;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\State\StateInterface;
use Drupal\views\Views;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Configure this module.
*
* @internal
*/
class SettingsForm extends ConfigFormBase {
const CONFIG_NAME = 'dgi_fixity.settings';
const SOURCES = 'sources';
const THRESHOLD = 'threshold';
const BATCH_SIZE = 'batch_size';
const NOTIFY_STATUS = 'notify_status';
const NOTIFY_USER = 'notify_user';
const NOTIFY_USER_THRESHOLD = 'notify_user_threshold';
const NOTIFY_STATUS_NEVER = 0;
const NOTIFY_STATUS_ALWAYS = 1;
const NOTIFY_STATUS_ERROR = 2;
// This is not stored as a configuration value but as a state value
// Though we allow the user to view it and reset it via this form.
const STATE_LAST_NOTIFICATION = 'dgi_fixity.last_notification';
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The state manager.
*
* @var \Drupal\Core\State\StateInterface
*/
protected $state;
/**
* Constructs a \Drupal\system\ConfigFormBase object.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The factory for configuration objects.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* Manages entity type plugin definitions.
* @param \Drupal\Core\State\StateInterface $state
* State manager.
*/
public function __construct(ConfigFactoryInterface $config_factory, EntityTypeManagerInterface $entity_type_manager, StateInterface $state) {
parent::__construct($config_factory);
$this->entityTypeManager = $entity_type_manager;
$this->state = $state;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('config.factory'),
$container->get('entity_type.manager'),
$container->get('state'),
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'dgi_fixity_settings_form';
}
/**
* {@inheritdoc}
*/
protected function getEditableConfigNames() {
return [
static::CONFIG_NAME,
];
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$config = $this->config(static::CONFIG_NAME);
$displays = Views::getApplicableViews('entity_reference_display');
$view_storage = $this->entityTypeManager->getStorage('view');
$sources = [];
foreach ($displays as $data) {
[$view_id, $display_id] = $data;
/** @var \Drupal\views\Entity\View $view */
$view = $view_storage->load($view_id);
$tag = $view->getExecutable()->storage->get('tag');
// Only list views tagged with 'fixity'.
if (in_array('fixity', Tags::explode($tag))) {
$entity_type = $view->getExecutable()->getBaseEntityType();
// Only use views that return 'file' entities, as the batch and workers
// dynamically filter the view by relating it to 'fixity_checks' that
// do not yet have their 'periodic' flag set to TRUE.
if ($entity_type && $entity_type->id() == 'file') {
$display = $view->get('display');
$set_name = $view_id . ':' . $display_id;
$sources[$set_name] = $display[$display_id]['display_title'] . ' (' . $set_name . ')';
}
}
}
$form['checks'] = [
'#type' => 'details',
'#title' => $this->t('Fixity Checks'),
'#open' => TRUE,
static::SOURCES => [
'#title' => $this->t('File Selection'),
'#description' => $this->t('
<p>Select one or more <strong>Views</strong>. Whose results are used to determine which files have periodic checks enabled according to the schedule below.</p>
<p>Only <em>File Entity</em> <strong>Views</strong> are supported.</p>
<p>Only <strong>entity_reference</strong> or <strong>entity_reference_revisions</strong> displays are supported.</p>
<p>If selecting multiple Views ideally they should not overlap, if only for the sake of efficiency.</p>
<p>Views must be have an administrative tag <strong>fixity</strong> to appear in this list.</p>
'),
'#type' => 'checkboxes',
'#options' => $sources,
'#default_value' => $config->get(static::SOURCES) ?: [],
],
static::THRESHOLD => [
'#type' => 'textfield',
'#required' => TRUE,
'#title' => $this->t('Time elapsed'),
'#description' => $this->t('
<p>Time threshold is relative to "<em>now</em>". For example "<em>-1 month</em>" would prevent any checks that occurred less than a month ago.</p>
<p>Check <a href="https://www.php.net/manual/en/datetime.formats.relative.php">Relative Formats</a> for acceptable values</p>
'),
'#default_value' => $config->get(static::THRESHOLD) ?: '-1 month',
'#element_validate' => [
[$this, 'validateThreshold'],
],
],
static::BATCH_SIZE => [
'#type' => 'number',
'#required' => TRUE,
'#title' => $this->t('Batch size'),
'#description' => $this->t('
<p>Set how many files will be processed at once when performing a batch / cron job</p>
'),
'#default_value' => 100,
],
];
// Default to the admin user if not given.
$user = $config->get(static::NOTIFY_USER);
$user = is_int($user) ?
$this->entityTypeManager->getStorage('user')->load($user) :
$this->entityTypeManager->getStorage('user')->load(1);
$notification_threshold = $config->get(static::NOTIFY_USER_THRESHOLD) ?: '-1 week';
$last_notification = $this->state->get(static::STATE_LAST_NOTIFICATION);
if ($last_notification !== NULL) {
$next_notification = $last_notification + (time() - strtotime($notification_threshold));
}
$form['notify'] = [
'#type' => 'details',
'#title' => $this->t('Notifications'),
'#description' => $this->t('Notifications are sent by email to the selected user.'),
'#open' => TRUE,
static::NOTIFY_STATUS => [
'#type' => 'select',
'#title' => $this->t('Notification Status'),
'#description' => $this->t('
<p>Choose under what conditions should notifications be sent to the selected user.</p>
'),
'#options' => [
static::NOTIFY_STATUS_NEVER => $this->t('Never'),
static::NOTIFY_STATUS_ALWAYS => $this->t('Always'),
static::NOTIFY_STATUS_ERROR => $this->t('Only if error'),
],
'#default_value' => $config->get(static::NOTIFY_STATUS) ?? static::NOTIFY_STATUS_ERROR,
],
static::NOTIFY_USER => [
'#type' => 'entity_autocomplete',
'#required' => TRUE,
'#title' => $this->t('User'),
'#description' => $this->t('
<p>The user to be notified should one or more fixity checks fail.</p>
<p>The user will be notified by email.</p>
'),
'#target_type' => 'user',
'#default_value' => $user,
'#selection_settings' => [
'include_anonymous' => FALSE,
],
],
static::NOTIFY_USER_THRESHOLD => [
'#type' => 'textfield',
'#required' => TRUE,
'#title' => $this->t('Time elapsed'),
'#description' => $this->t('
<p>Time threshold is relative to "<em>now</em>". For example "<em>-1 week</em>" would mean a week must pass between notifications.</p>
<p>Check <a href="https://www.php.net/manual/en/datetime.formats.relative.php">Relative Formats</a> for acceptable values</p>
'),
'#default_value' => $notification_threshold,
'#element_validate' => [
[$this, 'validateThreshold'],
],
],
'last' => $last_notification ?
[
'#type' => 'details',
'#title' => $this->t('Last notification'),
'#description' => $this->t('
<p>The last notification was sent on %last.</p>
<p>At earliest the next message can be sent %next.</p>
', [
'%last' => date(DATE_RFC7231, $last_notification),
'%next' => date(DATE_RFC7231, $next_notification),
]
),
static::STATE_LAST_NOTIFICATION => [
'#type' => 'button',
'#value' => $this->t('Reset'),
'#limit_validation_errors' => [],
'#executes_submit_callback' => TRUE,
'#submit' => [
[$this, 'resetLastNotification'],
],
],
] : NULL,
];
return parent::buildForm($form, $form_state);
}
/**
* Element validate callback; validate the threshold is valid.
*/
public function validateThreshold(array $element, FormStateInterface $form_state, array $form) {
$value = $form_state->getValue($element['#parents']);
if (strtotime($value) === FALSE) {
$form_state->setError($element, $this->t('The given threshold is not valid.'));
}
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
$value = $form_state->getValue(static::NOTIFY_USER);
/** @var \Drupal\user\Entity\User $user */
$user = is_numeric($value) ?
$this->entityTypeManager->getStorage('user')->load($value) :
NULL;
if ($user === NULL) {
// Just a precaution the default form element validation should
// catch this case anyways.
$form_state->setError($form['notify'][static::NOTIFY_USER], $this->t('The given user does not exist.'));
}
elseif ($user->getEmail() === NULL) {
$form_state->setError($form['notify'][static::NOTIFY_USER], $this->t('The given user does <em>not</em> have an email address associated with their account.'));
}
}
/**
* Resets the stored last notification.
*
* @param array $form
* An associative array containing the structure of the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*/
public function resetLastNotification(array &$form, FormStateInterface $form_state) {
$this->state->delete(static::STATE_LAST_NOTIFICATION);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$values = $form_state->getValues();
$config = $this->config(static::CONFIG_NAME);
$sources = array_keys(array_filter($values[static::SOURCES]));
$config
->set(static::SOURCES, array_combine($sources, $sources))
->set(static::THRESHOLD, $values[static::THRESHOLD])
->set(static::BATCH_SIZE, $values[static::BATCH_SIZE])
->set(static::NOTIFY_STATUS, $values[static::NOTIFY_STATUS])
->set(static::NOTIFY_USER, $values[static::NOTIFY_USER])
->set(static::NOTIFY_USER_THRESHOLD, $values[static::NOTIFY_USER_THRESHOLD])
->save();
parent::submitForm($form, $form_state);
}
}

26
src/Plugin/Action/CheckAction.php

@ -0,0 +1,26 @@
<?php
namespace Drupal\dgi_fixity\Plugin\Action;
/**
* Performs a fixity checks on the entity.
*
* @Action(
* id = "dgi_fixity:check_action",
* action_label = @Translation("Check"),
* deriver = "Drupal\dgi_fixity\Plugin\Action\Derivative\FixityCheckActionDeriver",
* )
*/
class CheckAction extends FixityCheckActionBase {
/**
* {@inheritdoc}
*/
public function execute($entity = NULL) {
$check = $this->getCheck($entity);
if ($check) {
$this->fixity->check($check->getFile(), TRUE);
}
}
}

62
src/Plugin/Action/Derivative/FixityCheckActionDeriver.php

@ -0,0 +1,62 @@
<?php
namespace Drupal\dgi_fixity\Plugin\Action\Derivative;
use Drupal\Core\Action\Plugin\Action\Derivative\EntityActionDeriverBase;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\dgi_fixity\FixityCheckServiceInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides an action deriver that finds publishable entity types.
*
* @see \Drupal\Core\Action\Plugin\Action\PublishAction
* @see \Drupal\Core\Action\Plugin\Action\UnpublishAction
*/
class FixityCheckActionDeriver extends EntityActionDeriverBase {
/**
* The fixity check service.
*
* @var \Drupal\dgi_fixity\FixityCheckServiceInterface
*/
protected $fixity;
/**
* Constructs a new EntityActionDeriverBase object.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
* The string translation service.
* @param \Drupal\dgi_fixity\FixityCheckServiceInterface $fixity
* The fixity service.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, TranslationInterface $string_translation, FixityCheckServiceInterface $fixity) {
$this->entityTypeManager = $entity_type_manager;
$this->stringTranslation = $string_translation;
$this->fixity = $fixity;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, $base_plugin_id) {
return new static(
$container->get('entity_type.manager'),
$container->get('string_translation'),
$container->get('dgi_fixity.fixity_check'),
);
}
/**
* {@inheritdoc}
*/
protected function isApplicable(EntityTypeInterface $entity_type) {
$supported_entity_types = array_merge(['fixity_check'], $this->fixity->fromEntityTypes());
return in_array($entity_type->id(), $supported_entity_types);
}
}

85
src/Plugin/Action/FixityCheckActionBase.php

@ -0,0 +1,85 @@
<?php
namespace Drupal\dgi_fixity\Plugin\Action;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Action\Plugin\Action\EntityActionBase;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\dgi_fixity\FixityCheckInterface;
use Drupal\dgi_fixity\FixityCheckServiceInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Base class for fixity_check entity-based actions.
*/
abstract class FixityCheckActionBase extends EntityActionBase {
/**
* The fixity check service.
*
* @var \Drupal\dgi_fixity\FixityCheckServiceInterface
*/
protected $fixity;
/**
* Constructs a CheckAction object.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\dgi_fixity\FixityCheckServiceInterface $fixity
* The fixity service.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, FixityCheckServiceInterface $fixity) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $entity_type_manager);
$this->fixity = $fixity;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('entity_type.manager'),
$container->get('dgi_fixity.fixity_check'),
);
}
/**
* Gets the related fixity_check entity for the given entity if possible.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The related entity.
*
* @return \Drupal\dgi_fixity\FixityCheckInterface
* The related fixity_check if found or NULL.
*/
protected function getCheck(EntityInterface $entity): ?FixityCheckInterface {
return $entity instanceof FixityCheckInterface ?
$entity :
$this->fixity->fromEntity($entity);
}
/**
* {@inheritdoc}
*/
public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) {
// If it exists and the user has permission to administer fixity checks.
$result = $this->getCheck($object) && $account->hasPermission('administer fixity checks') ?
AccessResult::allowed() :
AccessResult::forbidden();
return $return_as_object ? $result : $result->isAllowed();
}
}

27
src/Plugin/Action/PeriodicDisableAction.php

@ -0,0 +1,27 @@
<?php
namespace Drupal\dgi_fixity\Plugin\Action;
/**
* Disables periodic checks on the the entity.
*
* @Action(
* id = "dgi_fixity:periodic_disable_action",
* action_label = @Translation("Disable periodic checks"),
* deriver = "Drupal\dgi_fixity\Plugin\Action\Derivative\FixityCheckActionDeriver",
* )
*/
class PeriodicDisableAction extends FixityCheckActionBase {
/**
* {@inheritdoc}
*/
public function execute($entity = NULL) {
$check = $this->getCheck($entity);
if ($check) {
$check->setPeriodic(FALSE);
$check->save();
}
}
}

27
src/Plugin/Action/PeriodicEnableAction.php

@ -0,0 +1,27 @@
<?php
namespace Drupal\dgi_fixity\Plugin\Action;
/**
* Enable periodic checks on the the entity.
*
* @Action(
* id = "dgi_fixity:periodic_enable_action",
* action_label = @Translation("Enable periodic checks"),
* deriver = "Drupal\dgi_fixity\Plugin\Action\Derivative\FixityCheckActionDeriver",
* )
*/
class PeriodicEnableAction extends FixityCheckActionBase {
/**
* {@inheritdoc}
*/
public function execute($entity = NULL) {
$check = $this->getCheck($entity);
if ($check) {
$check->setPeriodic(TRUE);
$check->save();
}
}
}

80
src/Plugin/Derivative/FixityCheckLocalTasks.php

@ -0,0 +1,80 @@
<?php
namespace Drupal\dgi_fixity\Plugin\Derivative;
use Drupal\Component\Plugin\Derivative\DeriverBase;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Generates fixity related local tasks.
*/
class FixityCheckLocalTasks extends DeriverBase implements ContainerDeriverInterface {
use StringTranslationTrait;
/**
* The entity manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Creates a local tasks for fixity checks on supported entities.
*
* @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
* The translation manager.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity manager.
*/
public function __construct(TranslationInterface $string_translation, EntityTypeManagerInterface $entity_type_manager) {
$this->stringTranslation = $string_translation;
$this->entityTypeManager = $entity_type_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, $base_plugin_id) {
return new static(
$container->get('string_translation'),
$container->get('entity_type.manager'),
);
}
/**
* {@inheritdoc}
*/
public function getDerivativeDefinitions($base_plugin_definition) {
foreach ($this->entityTypeManager->getDefinitions() as $entity_type_id => $entity_type) {
if ($entity_type->hasLinkTemplate('fixity-audit')) {
$this->derivatives["$entity_type_id.fixity"] = [
'route_name' => "entity.{$entity_type_id}.fixity_audit",
'title' => $this->t('Fixity'),
'base_route' => "entity.{$entity_type_id}.canonical",
'weight' => 10,
];
$this->derivatives["$entity_type_id.fixity.audit"] = [
'route_name' => "entity.$entity_type_id.fixity_audit",
'title' => $this->t('Audit'),
'parent_id' => "dgi_fixity.entities:$entity_type_id.fixity",
'weight' => 10,
];
if ($entity_type->hasLinkTemplate('fixity-check')) {
$this->derivatives["$entity_type_id.fixity.check"] = [
'route_name' => "entity.$entity_type_id.fixity_check",
'title' => $this->t('Check'),
'parent_id' => "dgi_fixity.entities:$entity_type_id.fixity",
'weight' => 10,
];
}
}
}
return $this->derivatives;
}
}

47
src/Plugin/Field/FieldFormatter/FileReferenceFormatter.php

@ -0,0 +1,47 @@
<?php
namespace Drupal\dgi_fixity\Plugin\Field\FieldFormatter;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\Plugin\Field\FieldFormatter\EntityReferenceFormatterBase;
use Drupal\Core\Url;
/**
* Formats file reference as a link to the file.
*
* @FieldFormatter(
* id = "dgi_fixity_file_reference",
* label = @Translation("Fixity File Reference"),
* description = @Translation("Display a link to the file being referenced by a Fixity Check."),
* field_types = {
* "entity_reference"
* }
* )
*/
class FileReferenceFormatter extends EntityReferenceFormatterBase {
/**
* {@inheritdoc}
*/
public function viewElements(FieldItemListInterface $items, $langcode) {
$elements = [];
/** @var \Drupal\file\Entity\File $entity */
foreach ($this->getEntitiesToView($items, $langcode) as $delta => $entity) {
$label = $entity->label();
$uri = $entity->createFileUrl(FALSE);
if (isset($uri)) {
$elements[$delta] = [
'#type' => 'link',
'#title' => $label,
'#url' => Url::fromUri($uri),
];
}
else {
$elements[$delta] = ['#plain_text' => $label];
}
$elements[$delta]['#cache']['tags'] = $entity->getCacheTags();
}
return $elements;
}
}

33
src/Plugin/Field/FieldFormatter/StateFormatter.php

@ -0,0 +1,33 @@
<?php
namespace Drupal\dgi_fixity\Plugin\Field\FieldFormatter;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\FormatterBase;
use Drupal\dgi_fixity\Entity\FixityCheck;
/**
* Displays a human readable label for the state.
*
* @FieldFormatter(
* id = "dgi_fixity_state",
* label = @Translation("State"),
* field_types = {
* "integer"
* },
* )
*/
class StateFormatter extends FormatterBase {
/**
* {@inheritdoc}
*/
public function viewElements(FieldItemListInterface $items, $langcode) {
$elements = [];
foreach ($items as $delta => $item) {
$elements[$delta] = ['#markup' => FixityCheck::getStateProperty($item->value, 'label')];
}
return $elements;
}
}

68
src/Plugin/QueueWorker/FixityCheckWorker.php

@ -0,0 +1,68 @@
<?php
namespace Drupal\dgi_fixity\Plugin\QueueWorker;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Queue\QueueWorkerBase;
use Drupal\dgi_fixity\FixityCheckServiceInterface;
use Drupal\dgi_fixity\FixityCheckInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Performs a fixity check.
*
* @QueueWorker(
* id = "dgi_fixity.fixity_check",
* title = @Translation("Fixity Checks"),
* cron = {"time" = 15}
* )
*/
class FixityCheckWorker extends QueueWorkerBase implements ContainerFactoryPluginInterface {
/**
* The fixity check service.
*
* @var \Drupal\dgi_fixity\FixityCheckServiceInterface
*/
protected $fixity;
/**
* Constructs a new FixityCheckWorker instance.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\dgi_fixity\FixityCheckServiceInterface $fixity
* The fixity check service.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, FixityCheckServiceInterface $fixity) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->fixity = $fixity;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('dgi_fixity.fixity_check'),
);
}
/**
* {@inheritdoc}
*/
public function processItem($data) {
if ($data instanceof FixityCheckInterface) {
/** @var \Drupal\dgi_fixity\FixityCheckInterface $data */
$this->fixity->check($data->getFile());
}
}
}

79
src/Plugin/QueueWorker/ProcessSourceWorker.php

@ -0,0 +1,79 @@
<?php
namespace Drupal\dgi_fixity\Plugin\QueueWorker;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Queue\QueueWorkerBase;
use Drupal\Core\Queue\RequeueException;
use Drupal\dgi_fixity\FixityCheckServiceInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Processes configured sources to find new files to periodically check.
*
* @QueueWorker(
* id = "dgi_fixity.process_source",
* title = @Translation("Fixity Check Process Source"),
* cron = {"time" = 15}
* )
*/
class ProcessSourceWorker extends QueueWorkerBase implements ContainerFactoryPluginInterface {
/**
* The fixity check service.
*
* @var \Drupal\dgi_fixity\FixityCheckServiceInterface
*/
protected $fixity;
/**
* Constructs a new FixityCheckWorker instance.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\dgi_fixity\FixityCheckServiceInterface $fixity
* The fixity check service.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, FixityCheckServiceInterface $fixity) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->fixity = $fixity;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('dgi_fixity.fixity_check'),
);
}
/**
* {@inheritdoc}
*/
public function processItem($data) {
/** @var \Drupal\dgi_fixity\FixityCheckServiceInterface $fixity */
$fixity = \Drupal::service('dgi_fixity.fixity_check');
$view = $fixity->source($data, 1000);
$view->execute();
// Only processes those which have not already enabled periodic checks.
foreach ($view->result as $row) {
/** @var \Drupal\dgi_fixity\FixityCheckInterface $check */
$check = $view->field['periodic']->getEntity($row);
$check->setPeriodic(TRUE);
$check->save();
}
// Not finished processing.
if (count($view->result) !== 0) {
throw new RequeueException();
}
}
}

24
src/Plugin/Validation/Constraint/UniqueFieldEntityReferenceConstraint.php

@ -0,0 +1,24 @@
<?php
namespace Drupal\dgi_fixity\Plugin\Validation\Constraint;
use Symfony\Component\Validator\Constraint;
/**
* Checks if an entity field has a unique entity reference value.
*
* @Constraint(
* id = "UniqueFieldEntityReference",
* label = @Translation("Unique field entity reference constraint", context = "Validation"),
* type = { "entity_reference" }
* )
*/
class UniqueFieldEntityReferenceConstraint extends Constraint {
/**
* The message to display to the user on invalid condition.
*
* @var string
*/
public $message = 'The @referenced_entity_type: %entity_referenced is already a referenced by @entity_type: %entity';
}

67
src/Plugin/Validation/Constraint/UniqueFieldEntityReferenceConstraintValidator.php

@ -0,0 +1,67 @@
<?php
namespace Drupal\dgi_fixity\Plugin\Validation\Constraint;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
/**
* Validates the UniqueFieldEntityReferenceConstraint constraint.
*/
class UniqueFieldEntityReferenceConstraintValidator extends ConstraintValidator {
/**
* {@inheritdoc}
*/
public function validate($items, Constraint $constraint) {
/** @var \Drupal\Core\Field\EntityReferenceFieldItemList $items */
/** @var UniqueFieldEntityReferenceConstraint $constraint */
/** @var \Drupal\Core\Field\EntityReferenceFieldItem $item */
if (!$item = $items->first()) {
return;
}
$target_property = $item->mainPropertyName();
$field_name = $items->getFieldDefinition()->getName();
/** @var \Drupal\Core\Entity\EntityInterface $entity */
$entity = $items->getEntity();
$entity_type_id = $entity->getEntityTypeId();
$id_key = $entity->getEntityType()->getKey('id');
$query = \Drupal::entityQuery($entity_type_id);
$query->accessCheck(FALSE);
$entity_id = $entity->id();
// Using isset() instead of !empty() as 0 and '0' are valid ID values for
// entity types using string IDs.
if (isset($entity_id)) {
$query->condition($id_key, $entity_id, '<>');
}
$targets = [];
foreach ($items as $item) {
$targets[] = $item->{$target_property};
}
$targets = array_filter($targets);
$results = $query
->condition($field_name, $targets, 'IN')
->range(0, 1)
->execute();
if (count($results) > 0) {
$used_by_entity_id = reset($results);
$used_by_entity = \Drupal::entityTypeManager()->getStorage($entity_type_id)->load($used_by_entity_id);
foreach ($used_by_entity->{$field_name}->referencedEntities() as $reference) {
if (in_array($reference->id(), $targets)) {
$this->context->addViolation($constraint->message, [
'@referenced_entity_type' => $reference->getEntityTypeId(),
'%entity_referenced' => $reference->id(),
'@entity_type' => $entity_type_id,
'%entity' => $used_by_entity_id,
]);
break;
}
}
}
}
}

23
src/Plugin/views/wizard/FixityCheck.php

@ -0,0 +1,23 @@
<?php
namespace Drupal\dgi_fixity\Plugin\views\wizard;
use Drupal\views\Plugin\views\wizard\WizardPluginBase;
/**
* Used for creating 'fixity_check' views with the wizard.
*
* @ViewsWizard(
* id = "fixity_check",
* base_table = "fixity_check",
* title = @Translation("Fixity Check"),
* )
*/
class FixityCheck extends WizardPluginBase {
/**
* {@inheritdoc}
*/
protected $createdColumn = 'fixity_check-performed';
}

23
src/Plugin/views/wizard/FixityCheckRevision.php

@ -0,0 +1,23 @@
<?php
namespace Drupal\dgi_fixity\Plugin\views\wizard;
use Drupal\views\Plugin\views\wizard\WizardPluginBase;
/**
* Used for creating 'fixity_check' views with the wizard.
*
* @ViewsWizard(
* id = "fixity_check_revision",
* base_table = "fixity_check_revision",
* title = @Translation("Fixity Check Revision"),
* )
*/
class FixityCheckRevision extends WizardPluginBase {
/**
* {@inheritdoc}
*/
protected $createdColumn = 'fixity_check_revision-performed';
}

65
src/Routing/FixityCheckConverter.php

@ -0,0 +1,65 @@
<?php
namespace Drupal\dgi_fixity\Routing;
use Drupal\Core\Entity\EntityRepositoryInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\ParamConverter\EntityConverter;
use Drupal\dgi_fixity\FixityCheckServiceInterface;
use Symfony\Component\Routing\Route;
/**
* Converts an entity identifier into fixity_check entity.
*/
class FixityCheckConverter extends EntityConverter {
/**
* The fixity service.
*
* @var \Drupal\dgi_fixity\FixityCheckServiceInterface
*/
protected $fixity;
/**
* Construct a new FixityCheckConverter.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository
* The entity repository.
* @param \Drupal\dgi_fixity\FixityCheckServiceInterface $fixity
* The fixity service.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, EntityRepositoryInterface $entity_repository, FixityCheckServiceInterface $fixity) {
parent::__construct($entity_type_manager, $entity_repository);
$this->fixity = $fixity;
}
/**
* {@inheritdoc}
*/
public function convert($value, $definition, $name, array $defaults) {
/** @var \Drupal\Core\Entity\EntityInterface $entity */
$entity = parent::convert($value, $definition, $name, $defaults);
return $this->fixity->fromEntity($entity);
}
/**
* {@inheritdoc}
*/
public function applies($definition, $name, Route $route) {
$supported_entity_types = $this->fixity->fromEntityTypes();
if (!empty($definition['type']) && strpos($definition['type'], 'fixity:') === 0) {
$entity_type_id = substr($definition['type'], strlen('fixity:'));
if (strpos($definition['type'], '{') !== FALSE) {
$entity_type_slug = substr($entity_type_id, 1, -1);
if ($name != $entity_type_slug && in_array($entity_type_slug, $route->compile()->getVariables(), TRUE)) {
return in_array($entity_type_slug, $supported_entity_types);
}
}
return in_array($entity_type_id, $supported_entity_types);
}
return FALSE;
}
}

25
src/Routing/FixityCheckRouteProvider.php

@ -0,0 +1,25 @@
<?php
namespace Drupal\dgi_fixity\Routing;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\Routing\AdminHtmlRouteProvider;
/**
* Sets defaults on HTML routes for the fixity_check entity.
*
* @see \Drupal\Core\Entity\Routing\AdminHtmlRouteProvider.
*/
class FixityCheckRouteProvider extends AdminHtmlRouteProvider {
/**
* {@inheritdoc}
*/
protected function getCanonicalRoute(EntityTypeInterface $entity_type) {
if ($route = parent::getCanonicalRoute($entity_type)) {
$route->setOption('_admin_route', TRUE);
return $route;
}
}
}

113
src/Routing/FixityCheckRouteSubscriber.php

@ -0,0 +1,113 @@
<?php
namespace Drupal\dgi_fixity\Routing;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Routing\RouteSubscriberBase;
use Drupal\dgi_fixity\FixityCheckServiceInterface;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
/**
* Listens to the dynamic route events.
*/
class FixityCheckRouteSubscriber extends RouteSubscriberBase {
/**
* The fixity check service.
*
* @var \Drupal\dgi_fixity\FixityCheckServiceInterface
*/
protected $fixity;
/**
* Subscriber for Fixity Check routes.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_manager
* The entity type manager.
* @param \Drupal\dgi_fixity\FixityCheckServiceInterface $fixity
* The fixity check service.
*/
public function __construct(EntityTypeManagerInterface $entity_manager, FixityCheckServiceInterface $fixity) {
$this->entityTypeManager = $entity_manager;
$this->fixity = $fixity;
}
/**
* {@inheritdoc}
*/
protected function alterRoutes(RouteCollection $collection) {
$supported_entity_types = array_merge(['fixity_check'], $this->fixity->fromEntityTypes());
$definitions = $this->entityTypeManager->getDefinitions();
foreach ($supported_entity_types as $entity_type_id) {
$entity_type = $definitions[$entity_type_id];
if ($route = $this->getFixityAuditRoute($entity_type)) {
$collection->add("entity.$entity_type_id.fixity_audit", $route);
}
if ($route = $this->getFixityCheckRoute($entity_type)) {
$collection->add("entity.$entity_type_id.fixity_check", $route);
}
}
}
/**
* Gets the fixity check 'Audit' route for the given entity type.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type.
*
* @return \Symfony\Component\Routing\Route|null
* The generated route, if available.
*/
protected function getFixityAuditRoute(EntityTypeInterface $entity_type) {
if ($fixity_audit = $entity_type->getLinkTemplate('fixity-audit')) {
$entity_type_id = $entity_type->id();
$route = new Route($fixity_audit);
$route
->addDefaults([
'_controller' => '\Drupal\dgi_fixity\Controller\FixityCheckController::entityAudit',
'_title' => 'Audit',
])
->addRequirements([
'_permission' => 'view fixity checks',
])
->setOption('_admin_route', TRUE)
->setOption('_fixity_entity_type_id', $entity_type_id)
->setOption('parameters', [
$entity_type_id => ['type' => 'entity:' . $entity_type_id],
]);
return $route;
}
}
/**
* Gets the fixity check 'Check' route for the given entity type.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type.
*
* @return \Symfony\Component\Routing\Route|null
* The generated route, if available.
*/
protected function getFixityCheckRoute(EntityTypeInterface $entity_type) {
if ($fixity_audit = $entity_type->getLinkTemplate('fixity-check')) {
$entity_type_id = $entity_type->id();
$route = new Route($fixity_audit);
$route
->addDefaults([
'_entity_form' => "{$entity_type_id}.fixity-check",
])
->addRequirements([
'_permission' => 'administer fixity checks',
])
->setOption('_admin_route', TRUE)
->setOption('_fixity_entity_type_id', $entity_type_id)
->setOption('parameters', [
$entity_type_id => ['type' => 'entity:' . $entity_type_id],
]);
return $route;
}
}
}
Loading…
Cancel
Save