Nigel Banks
3 years ago
61 changed files with 6687 additions and 13 deletions
@ -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 |
@ -0,0 +1,8 @@
|
||||
{ |
||||
"name": "discoverygarden/dgi_fixity", |
||||
"type": "drupal-module", |
||||
"license": "GPL-2.0-or-later", |
||||
"require": { |
||||
"drupal/filehash": "^2.0" |
||||
} |
||||
} |
@ -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 |
@ -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: { } |
@ -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: { } |
@ -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: { } |
@ -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: { } |
@ -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: { } |
@ -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: { } |
@ -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: { } |
@ -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: { } |
@ -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: { } |
@ -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: { } |
@ -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' |
@ -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 |
@ -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; |
||||
} |
@ -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' |
@ -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 |
@ -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; |
||||
} |
||||
} |
@ -0,0 +1,4 @@
|
||||
administer fixity checks: |
||||
title: 'Administer Fixity Checks' |
||||
view fixity checks: |
||||
title: 'View Fixity Checks' |
@ -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 |
@ -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 } |
@ -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', |
||||
], |
||||
]; |
||||
} |
||||
} |
@ -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 } |
@ -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(); |
||||
} |
||||
|
||||
} |
@ -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)); |
||||
} |
||||
|
||||
} |
@ -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); |
||||
} |
||||
|
||||
} |
@ -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; |
||||
} |
||||
|
||||
} |
@ -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()); |
||||
} |
||||
|
||||
} |
@ -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); |
||||
} |
||||
} |
||||
} |
||||
|
||||
} |
@ -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); |
||||
} |
||||
} |
||||
} |
||||
|
||||
} |
@ -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; |
||||
|
||||
} |
@ -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(); |
||||
} |
||||
|
||||
} |
@ -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; |
||||
} |
||||
|
||||
} |
@ -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; |
||||
|
||||
} |
@ -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(); |
||||
} |
||||
|
||||
} |
@ -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); |
||||
|
||||
} |
@ -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; |
||||
} |
||||
|
||||
} |
@ -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; |
||||
} |
||||
|
||||
} |
@ -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()); |
||||
} |
||||
} |
||||
|
||||
} |
@ -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()); |
||||
} |
||||
|
||||
} |
@ -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); |
||||
} |
||||
|
||||
} |
@ -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(), |
||||
]); |
||||
} |
||||
|
||||
} |
@ -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); |
||||
} |
||||
|
||||
} |
@ -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); |
||||
} |
||||
} |
||||
|
||||
} |
@ -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); |
||||
} |
||||
|
||||
} |
@ -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(); |
||||
} |
||||
|
||||
} |
@ -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(); |
||||
} |
||||
} |
||||
|
||||
} |
@ -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(); |
||||
} |
||||
} |
||||
|
||||
} |
@ -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; |
||||
} |
||||
|
||||
} |
@ -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; |
||||
} |
||||
|
||||
} |
@ -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; |
||||
} |
||||
|
||||
} |
@ -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()); |
||||
} |
||||
} |
||||
|
||||
} |
@ -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(); |
||||
} |
||||
} |
||||
|
||||
} |
@ -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'; |
||||
|
||||
} |
@ -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; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
} |
@ -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'; |
||||
|
||||
} |
@ -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'; |
||||
|
||||
} |
@ -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; |
||||
} |
||||
|
||||
} |
@ -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; |
||||
} |
||||
} |
||||
|
||||
} |
@ -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…
Reference in new issue