Browse Source

First pass at implementation.

pull/2/head
Nigel Banks 3 years ago
parent
commit
439ee19ec1
  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. 22
      config/schema/dgi_fixity.schema.yml
  15. 12
      dgi_fixity.info.yml
  16. 36
      dgi_fixity.install
  17. 11
      dgi_fixity.links.menu.yml
  18. 45
      dgi_fixity.links.task.yml
  19. 206
      dgi_fixity.module
  20. 4
      dgi_fixity.permissions.yml
  21. 52
      dgi_fixity.routing.yml
  22. 18
      dgi_fixity.services.yml
  23. 56
      dgi_fixity.views.inc
  24. 6
      drush.services.yml
  25. 113
      src/Commands/FixityCheck.php
  26. 266
      src/Controller/FixityCheckController.php
  27. 335
      src/Entity/FixityCheck.php
  28. 125
      src/EntityTypeInfo.php
  29. 39
      src/FixityCheckAccessControlHandler.php
  30. 287
      src/FixityCheckBatchCheck.php
  31. 118
      src/FixityCheckBatchGenerate.php
  32. 229
      src/FixityCheckInterface.php
  33. 123
      src/FixityCheckListBuilder.php
  34. 390
      src/FixityCheckService.php
  35. 134
      src/FixityCheckServiceInterface.php
  36. 139
      src/FixityCheckStorage.php
  37. 79
      src/FixityCheckStorageInterface.php
  38. 40
      src/FixityCheckStorageSchema.php
  39. 44
      src/FixityCheckViewsData.php
  40. 62
      src/Form/BatchForm.php
  41. 160
      src/Form/CheckForm.php
  42. 85
      src/Form/GenerateForm.php
  43. 131
      src/Form/RevisionDeleteForm.php
  44. 305
      src/Form/SettingsForm.php
  45. 26
      src/Plugin/Action/CheckAction.php
  46. 62
      src/Plugin/Action/Derivative/FixityCheckActionDeriver.php
  47. 85
      src/Plugin/Action/FixityCheckActionBase.php
  48. 27
      src/Plugin/Action/PeriodicDisableAction.php
  49. 27
      src/Plugin/Action/PeriodicEnableAction.php
  50. 80
      src/Plugin/Derivative/FixityCheckLocalTasks.php
  51. 47
      src/Plugin/Field/FieldFormatter/FileReferenceFormatter.php
  52. 33
      src/Plugin/Field/FieldFormatter/StateFormatter.php
  53. 68
      src/Plugin/QueueWorker/FixityCheckWorker.php
  54. 79
      src/Plugin/QueueWorker/ProcessSourceWorker.php
  55. 24
      src/Plugin/Validation/Constraint/UniqueFieldEntityReferenceConstraint.php
  56. 67
      src/Plugin/Validation/Constraint/UniqueFieldEntityReferenceConstraintValidator.php
  57. 23
      src/Plugin/views/wizard/FixityCheck.php
  58. 23
      src/Plugin/views/wizard/FixityCheckRevision.php
  59. 65
      src/Routing/FixityCheckConverter.php
  60. 25
      src/Routing/FixityCheckRouteProvider.php
  61. 113
      src/Routing/FixityCheckRouteSubscriber.php

95
README.md

@ -1,41 +1,110 @@
# DGI Fixity
# Fixity
## 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
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
Install as usual, see
[this](https://drupal.org/documentation/install/modules-themes/modules-8) for
further information.
Install as usual, see [this][install] 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
Having problems or solved a problem? Contact
[discoverygarden](http://support.discoverygarden.ca).
Having problems or solved a problem? Contact [discoverygarden].
## Maintainers/Sponsors
Current maintainers:
* [discoverygarden](http://www.discoverygarden.ca)
* [discoverygarden]
## Development
If you would like to contribute to this module create an issue, pull request
and or contact
[discoverygarden](http://support.discoverygarden.ca).
and or contact [discoverygarden].
## 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: { }

22
config/schema/dgi_fixity.schema.yml

@ -0,0 +1,22 @@
dgi_fixity.settings:
type: config_object
label: 'Fixity check settings'
mapping:
sources:
type: sequence
label: 'File Selection for Fixity Checks'
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

36
dgi_fixity.install

@ -0,0 +1,36 @@
<?php
/**
* @file
* Install hook implementations.
*/
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* 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' => new TranslatableMarkup('Fixity'),
'value' => $failed ? new TranslatableMarkup('Error') : ($out_to_date ? new TranslatableMarkup('Out of date') : new TranslatableMarkup('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

206
dgi_fixity.module

@ -0,0 +1,206 @@
<?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\StringTranslation\TranslatableMarkup;
use Drupal\Core\Url;
use Drupal\dgi_fixity\EntityTypeInfo;
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.
if (in_array('islandora_defaults', $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;
}
$now = \Drupal::time()->getRequestTime();
$subject = (new TranslatableMarkup('Fixity Check Report - @now', ['@now' => date(DATE_RFC7231, $now)]))->render();
$body = $fixity->summary($stats);
if ($stats['failed'] !== 0) {
$body[] = (new TranslatableMarkup(
'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()]
))->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->getPreferredAdminLangcode(TRUE));
}
}
/**
* Implements hook_entity_type_alter().
*/
function dgi_fixity_entity_type_alter(array &$entity_types) {
return \Drupal::service('class_resolver')
->getInstanceFromDefinition(EntityTypeInfo::class)
->entityTypeAlter($entity_types);
}
/**
* Implements hook_entity_operation().
*/
function dgi_fixity_entity_operation(EntityInterface $entity) {
return \Drupal::service('class_resolver')
->getInstanceFromDefinition(EntityTypeInfo::class)
->entityOperation($entity);
}
/**
* 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) {
Cache::invalidateTags([
'fixity_check:' . $entity->id() . ':revisions_list',
]);
}
/**
* Implements hook_ENTITY_TYPE_revision_delete().
*/
function dgi_fixity_fixity_check_revision_delete(EntityInterface $entity) {
Cache::invalidateTags([
'fixity_check:' . $entity->id() . ':revisions_list',
]);
}
/**
* 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'] = new TranslatableMarkup(
'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'

52
dgi_fixity.routing.yml

@ -0,0 +1,52 @@
dgi_fixity.settings:
path: '/admin/config/fixity'
defaults:
_form: '\Drupal\dgi_fixity\Form\SettingsForm'
_title: 'Fixity'
requirements:
_permission: 'access administration pages,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:
_permission: 'view fixity checks'
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:
_permission: 'administer fixity checks'
fixity_check: \d+
fixity_check_revision: \d+
options:
_admin_route: TRUE

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 }

56
dgi_fixity.views.inc

@ -0,0 +1,56 @@
<?php
/**
* @file
* Provide views data for file.module.
*/
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* 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' => new TranslatableMarkup('@entity using @field',
[
'@entity' => $entity_type->getLabel(),
'@field' => $field_type->getLabel(),
],
),
'label' => $group,
'help' => new TranslatableMarkup('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();
}
}

266
src/Controller/FixityCheckController.php

@ -0,0 +1,266 @@
<?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,
]
))->toString();
}
else {
$link = $fixity_check->toLink($date)->toString();
$currentRevisionDisplayed = TRUE;
}
$state = $revision->state->view([
'label' => 'hidden',
'type' => 'dgi_fixity_state',
]);
$row = [
[
'data' => [
'#markup' => $link,
],
],
[
'data' => [
'#markup' => $this->renderer->renderRoot($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' => [
// Invalidated by revision create/delete hooks.
'fixity_check:' . $fixity_check->id() . ':revisions_list',
],
'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));
}
}

335
src/Entity/FixityCheck.php

@ -0,0 +1,335 @@
<?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\Core\StringTranslation\TranslatableMarkup;
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(new TranslatableMarkup('File'))
->setDescription(new TranslatableMarkup('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(new TranslatableMarkup('State'))
->setDescription(new TranslatableMarkup('A flag indicating the state of the whether the check passed or not.'))
->setTranslatable(FALSE)
->setRevisionable(TRUE)
->setInitialValue(static::STATE_UNDEFINED)
->setDefaultValue(static::STATE_UNDEFINED)
// Define this via an options provider once.
// https://www.drupal.org/node/2329937 is completed.
->addPropertyConstraints('value', [
'AllowedValues' => ['callback' => static::class . '::getAllowedStates'],
])
->setDisplayConfigurable('view', FALSE)
->setDisplayOptions('view', [
'type' => 'dgi_fixity_state',
'weight' => 1,
]);
// A value of 0 indicates the check was never performed even though it is
// a unix-timestamp, which means it is technically 1970-01-01 00:00:00.
$fields['performed'] = BaseFieldDefinition::create('timestamp')
->setLabel(t('Performed'))
->setDescription(t('The time the check was performed, 0 if never performed.'))
->setTranslatable(FALSE)
->setRevisionable(TRUE)
->setInitialValue(0)
->setDefaultValue(0)
->setDisplayConfigurable('view', FALSE)
->setDisplayOptions('view', [
'type' => 'timestamp_ago',
'weight' => 2,
]);
$fields['periodic'] = BaseFieldDefinition::create('boolean')
->setLabel(t('Periodic'))
->setDescription(t('Enable/disable periodic fixity checks.'))
->setRequired(TRUE)
->setTranslatable(FALSE)
->setRevisionable(FALSE)
->setInitialValue(FALSE)
->setDefaultValue(FALSE)
->setDisplayConfigurable('view', FALSE)
->setDisplayOptions('form', [
'type' => 'options_buttons',
'weight' => 3,
])
->setDisplayOptions('view', [
'type' => 'boolean',
'weight' => 3,
]);
$fields['queued'] = BaseFieldDefinition::create('timestamp')
->setLabel(t('Queued'))
->setDescription(t('Time when this file was queued for fixity, 0 if not queued.'))
->setTranslatable(FALSE)
->setRevisionable(FALSE)
->setInitialValue(0)
->setDefaultValue(0)
->setDisplayConfigurable('view', FALSE)
->setDisplayOptions('view', [
'region' => 'hidden',
]);
return $fields;
}
/**
* {@inheritdoc}
*/
public function preSave(EntityStorageInterface $storage) {
parent::preSave($storage);
// Disallow changes to state / performed on existing revisions.
// Unless this is the first revision and it the check was never performed.
if (!$this->isNewRevision() && !($this->isLatestRevision() && !$this->original->wasPerformed())) {
$immutable_fields = ['state', 'performed'];
foreach ($immutable_fields as $field) {
if ($this->{$field}->hasAffectingChanges($this->original->{$field}, LanguageInterface::LANGCODE_NOT_SPECIFIED)) {
throw new \LogicException("Entity type {$this->getEntityTypeId()} does not support modifying the '{$field}' field of existing revisions.");
}
}
}
// The file field is immutable after creation.
if ($this->original && $this->file->hasAffectingChanges($this->original->file, LanguageInterface::LANGCODE_NOT_SPECIFIED)) {
throw new \LogicException("Entity type {$this->getEntityTypeId()} does not support modifying the file field after creation.");
}
// If performed has changed force queued to zero.
if ($this->original && $this->performed->hasAffectingChanges($this->original->performed, LanguageInterface::LANGCODE_NOT_SPECIFIED)) {
$this->setQueued(0);
}
}
/**
* {@inheritdoc}
*/
public function preSaveRevision(EntityStorageInterface $storage, \stdClass $record) {
parent::preSaveRevision($storage, $record);
// Disallow creating revisions if performed is not set.
if (!$this->isNew() && $this->isNewRevision() && $record->performed === 0) {
throw new \LogicException("Entity type {$this->getEntityTypeId()} does not support creating new revisions without where the performed field is not set.");
}
}
/**
* {@inheritdoc}
*/
public function setNewRevision($value = TRUE) {
parent::setNewRevision($value);
// Reset the state and performed timestamp when creating a new revision
// from an existing fixity_check.
if (!$this->isNew() && $value) {
$this->state = static::STATE_UNDEFINED;
unset($this->performed);
}
}
/**
* {@inheritdoc}
*/
public function getFile(): ?File {
/** @var \Drupal\Core\Field\EntityReferenceFieldItemList $file */
$file = $this->file;
return $file->isEmpty() ? NULL : $file->referencedEntities()[0];
}
/**
* {@inheritdoc}
*/
public function setFile(File $file): FixityCheckInterface {
$this->set('file', $file);
return $this;
}
/**
* {@inheritdoc}
*/
public function getState(): int {
return $this->state->value;
}
/**
* {@inheritdoc}
*/
public function setState(int $state): FixityCheckInterface {
if (!in_array($state, static::getAllowedStates())) {
throw new \InvalidArgumentException("Invalid state '$state' has been given");
}
$this->set('state', $state);
return $this;
}
/**
* {@inheritdoc}
*/
public function getStateLabel(): string {
return static::getStateProperty($this->getState(), 'label');
}
/**
* {@inheritdoc}
*/
public static function getStateProperty(int $state, string $property) {
return static::STATES[$state][$property] ?? NULL;
}
/**
* {@inheritdoc}
*/
public function passed(): bool {
$state = $this->getState();
return static::STATES[$state]['passed'] ?? FALSE;
}
/**
* {@inheritdoc}
*/
public function getPeriodic(): bool {
return $this->periodic->value;
}
/**
* {@inheritdoc}
*/
public function setPeriodic(bool $periodic): FixityCheckInterface {
$this->set('periodic', $periodic);
return $this;
}
/**
* {@inheritdoc}
*/
public function getPerformed(): int {
return $this->performed->value;
}
/**
* {@inheritdoc}
*/
public function setPerformed(int $performed): FixityCheckInterface {
$this->set('performed', $performed);
return $this;
}
/**
* {@inheritdoc}
*/
public function wasPerformed(): bool {
return $this->getPerformed() !== 0;
}
/**
* {@inheritdoc}
*/
public function getQueued(): int {
return $this->queued->value;
}
/**
* {@inheritdoc}
*/
public function setQueued(int $queued): FixityCheckInterface {
$this->set('queued', $queued);
return $this;
}
/**
* {@inheritdoc}
*/
public function label() {
$file = $this->getFile();
return ($file === NULL) ?
$this->t('Fixity Check') :
$file->label();
}
/**
* Defines allowed states for AllowedValues constraints.
*
* @return int[]
* The allowed states.
*/
public static function getAllowedStates() {
return array_keys(static::STATES);
}
}

125
src/EntityTypeInfo.php

@ -0,0 +1,125 @@
<?php
namespace Drupal\dgi_fixity;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Routing\RedirectDestinationInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Manipulates entity type information.
*
* This class contains primarily bridged hooks for compile-time or
* cache-clear-time hooks. Runtime hooks should be placed in EntityOperations.
*/
class EntityTypeInfo implements ContainerInjectionInterface {
use StringTranslationTrait;
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
/**
* The redirect destination helper.
*
* @var \Drupal\Core\Routing\RedirectDestinationInterface
*/
protected $redirect;
/**
* The fixity check service.
*
* @var \Drupal\dgi_fixity\FixityCheckServiceInterface
*/
protected $fixity;
/**
* EntityTypeInfo constructor.
*
* @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
* The translation manager.
* @param \Drupal\Core\Session\AccountInterface $current_user
* Current user.
* @param \Drupal\Core\Routing\RedirectDestinationInterface $redirect
* The redirect destination helper.
* @param \Drupal\dgi_fixity\FixityCheckServiceInterface $fixity
* The fixity service.
*/
public function __construct(TranslationInterface $string_translation, AccountInterface $current_user, RedirectDestinationInterface $redirect, FixityCheckServiceInterface $fixity) {
$this->stringTranslation = $string_translation;
$this->currentUser = $current_user;
$this->redirect = $redirect;
$this->fixity = $fixity;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('string_translation'),
$container->get('current_user'),
$container->get('redirect.destination'),
$container->get('dgi_fixity.fixity_check'),
);
}
/**
* Gets fixity check links to appropriate entity types.
*
* This is an alter hook bridge.
*
* @param \Drupal\Core\Entity\EntityTypeInterface[] $entity_types
* The master entity type list to alter.
*
* @see hook_entity_type_alter()
*/
public function entityTypeAlter(array &$entity_types) {
$supported_entity_types = $this->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');
}
}
/**
* Gets fixity operations on entities that support it.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity on which to define an operation.
*
* @return array
* An array of operation definitions.
*
* @see hook_entity_operation()
*/
public function entityOperation(EntityInterface $entity) {
$operations = [];
if ($entity->hasLinkTemplate('fixity-audit') && $this->currentUser->hasPermission('view fixity checks')) {
$operations['fixity-audit'] = [
'title' => $this->t('Audit'),
'weight' => 10,
'url' => $entity->toUrl('fixity-audit'),
];
if ($entity->hasLinkTemplate('fixity-check') && $this->currentUser->hasPermission('administer fixity checks')) {
$operations['fixity-check'] = [
'title' => $this->t('Check'),
'weight' => 13,
'url' => $entity->toUrl('fixity-check', ['query' => $this->redirect->getAsArray()]),
];
}
}
return $operations;
}
}

39
src/FixityCheckAccessControlHandler.php

@ -0,0 +1,39 @@
<?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) {
if ($account->hasPermission($this->entityType->getAdminPermission())) {
return AccessResult::allowed()->cachePerPermissions();
}
switch ($operation) {
case 'view':
return AccessResult::allowedIfHasPermission($account, 'view fixity checks')->cachePerPermissions();
default:
return AccessResult::forbidden()->cachePerPermissions();
}
}
/**
* {@inheritdoc}
*/
protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) {
return AccessResult::allowedIfHasPermission($account, $this->entityType->getAdminPermission());
}
}

287
src/FixityCheckBatchCheck.php

@ -0,0 +1,287 @@
<?php
namespace Drupal\dgi_fixity;
use Drupal\Core\Batch\BatchBuilder;
use Drupal\Core\StringTranslation\PluralTranslatableMarkup;
use Drupal\Core\StringTranslation\TranslatableMarkup;
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(new TranslatableMarkup('Performing checks on @count file(s)', ['@count' => count($fids)]))
->setInitMessage(new TranslatableMarkup('Starting'))
->setErrorMessage(new TranslatableMarkup('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(new TranslatableMarkup('Enumerating periodic checks from @count Source(s)', ['@count' => count($sources)]))
->setInitMessage(new TranslatableMarkup('Starting'))
->setErrorMessage(new TranslatableMarkup('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'] = new TranslatableMarkup('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'][] = new TranslatableMarkup('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'] = new TranslatableMarkup('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'][] = new TranslatableMarkup('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(new TranslatableMarkup(
'@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);
}
}
}
}

118
src/FixityCheckBatchGenerate.php

@ -0,0 +1,118 @@
<?php
namespace Drupal\dgi_fixity;
use Drupal\Core\Batch\BatchBuilder;
use Drupal\Core\StringTranslation\PluralTranslatableMarkup;
use Drupal\Core\StringTranslation\TranslatableMarkup;
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(new TranslatableMarkup('Generating Fixity Checks for previously created files'))
->setInitMessage(new TranslatableMarkup('Starting'))
->setErrorMessage(new TranslatableMarkup('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'][] = new TranslatableMarkup('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(new TranslatableMarkup(
'@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);
}
}
}
}

229
src/FixityCheckInterface.php

@ -0,0 +1,229 @@
<?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;
}

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();
}
}

390
src/FixityCheckService.php

@ -0,0 +1,390 @@
<?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 {
$summary = [];
$summary[] = $this->formatPlural(
$stats['revisions'],
'@count check has been performed since tracking started.',
'@count checks have been performed since tracking started.',
);
$summary[] = $this->formatPlural(
$stats['periodic']['total'],
'@count file is set to be checked periodically.',
'@count files are set to be checked periodically.',
);
$summary[] = $this->formatPlural(
$stats['periodic']['current'],
'@count periodic check is up to date.',
'@count periodic checks are up to date.',
);
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.',
);
}
if ($stats['failed'] > 0) {
$summary[] = $this->formatPlural(
$stats['failed'],
'@count check has failed.',
'@count checks have failed.',
);
foreach ($stats['states'] as $state => $count) {
$summary[] = $this->formatPlural(
$count,
FixityCheck::getStateProperty($state, 'singular'),
FixityCheck::getStateProperty($state, 'plural'),
);
}
}
return $summary;
}
}

134
src/FixityCheckServiceInterface.php

@ -0,0 +1,134 @@
<?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.
*
* @return \Drupal\Core\StringTranslation\TranslatableMarkup[]
* A list of messages that describe the current state of the system.
*/
public function summary(array $stats): 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);
}
}

131
src/Form/RevisionDeleteForm.php

@ -0,0 +1,131 @@
<?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 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, $fixity_check_revision = NULL) {
$this->revision = $this->storage->loadRevision($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