diff --git a/islandora_spreadsheet_ingest.links.menu.yml b/islandora_spreadsheet_ingest.links.menu.yml index 005cf5e..8b350dc 100644 --- a/islandora_spreadsheet_ingest.links.menu.yml +++ b/islandora_spreadsheet_ingest.links.menu.yml @@ -4,3 +4,8 @@ islandora_spreadsheet_ingest.request.list: parent: system.admin_content title: 'Add from Spreadsheet' description: 'Permits ingest of content described in spreadsheets.' +islandora_spreadsheet_ingest.admin: + route_name: islandora_spreadsheet_ingest.admin + parent: system.admin_config_islandora + title: 'Islandora Spreadsheet Ingest Settings' + description: 'Configure settings for the Islandora Spreadsheet Ingest module.' diff --git a/islandora_spreadsheet_ingest.module b/islandora_spreadsheet_ingest.module index 6a8e84b..565768c 100644 --- a/islandora_spreadsheet_ingest.module +++ b/islandora_spreadsheet_ingest.module @@ -5,7 +5,9 @@ * General hook implementations. */ +use Drupal\Component\Utility\NestedArray; use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Session\AccountInterface; use Drupal\Core\Url; use Drupal\islandora_spreadsheet_ingest\RequestInterface; @@ -134,3 +136,37 @@ function islandora_spreadsheet_ingest_entity_operation(EntityInterface $entity) return $ops; } + +/** + * Helper for the migration access checks. + * + * @see islandora_spreadsheet_ingest_migration_group_entity_access() + * @see islandora_spreadsheet_ingest_migration_entity_access() + */ +function _islandora_spreadsheet_ingest_migration_entity_helper(EntityInterface $entity, AccountInterface $account, $uid, $tags) { + return AccessResult::allowedIf(in_array('isi_derived_migration', $tags)) + ->addCacheableDependency($entity) + ->andIf( + AccessResult::allowedIf($uid !== FALSE && $uid == $account->id()) + ->cachePerUser() + ); +} + +/** + * Implements hook_ENTITY_TYPE_entity_access() for migration_group entities. + */ +function islandora_spreadsheet_ingest_migration_group_entity_access(EntityInterface $entity, $operation, AccountInterface $account) { + $shared = $entity->shared_configuration ?? []; + $tags = NestedArray::getValue($shared, ['migration_tags']) ?? []; + $uid = NestedArray::getValue($shared, ['source', 'isi', 'uid']) ?? FALSE; + return _islandora_spreadsheet_ingest_migration_entity_helper($entity, $account, $uid, $tags); +} + +/** + * Implements hook_ENTITY_TYPE_entity_access() for migration entities. + */ +function islandora_spreadsheet_ingest_migration_entity_access(EntityInterface $entity, $operation, AccountInterface $account) { + $tags = $entity->migration_tags ?? []; + $uid = NestedArray::getValue($entity->source, ['isi', 'uid']) ?? FALSE; + return _islandora_spreadsheet_ingest_migration_entity_helper($entity, $account, $uid, $tags); +} diff --git a/islandora_spreadsheet_ingest.permissions.yml b/islandora_spreadsheet_ingest.permissions.yml new file mode 100644 index 0000000..7f22a4d --- /dev/null +++ b/islandora_spreadsheet_ingest.permissions.yml @@ -0,0 +1,15 @@ +--- +'administer islandora_spreadsheet_ingest requests': + title: 'Administer requests' +'create islandora_spreadsheet_ingest requests': + title: 'Create request' +'edit islandora_spreadsheet_ingest requests': + title: 'Edit request' +'edit islandora_spreadsheet_ingest request mapping': + title: 'Edit request mapping' +'view islandora_spreadsheet_ingest requests': + title: 'View requests' +'activate islandora_spreadsheet_ingest requests': + title: 'Activate requests' +'deleta islandora_spreadsheet_ingest requests': + title: 'Delete requests' diff --git a/islandora_spreadsheet_ingest.routing.yml b/islandora_spreadsheet_ingest.routing.yml index a626ac3..98f2a83 100644 --- a/islandora_spreadsheet_ingest.routing.yml +++ b/islandora_spreadsheet_ingest.routing.yml @@ -5,46 +5,54 @@ entity.isi_request.list: _entity_list: isi_request _title: Islandora Spreadsheet Ingest requirements: - _permission: 'access content overview' + _permission: 'view islandora_spreadsheet_ingest requests' entity.isi_request.add_form: path: '/admin/content/islandora_spreadsheet_ingest/add' defaults: _title: 'Add Content From Spreadsheet - Upload' _entity_form: 'isi_request.add' requirements: - _permission: 'access content overview' + _entity_create_access: 'isi_request' entity.isi_request.edit_form: path: '/admin/content/islandora_spreadsheet_ingest/{isi_request}/edit' defaults: _title: 'Edit' _entity_form: 'isi_request.edit' requirements: - _permission: 'access content overview' + _entity_access: 'isi_request.update' entity.isi_request.map_form: path: '/admin/content/islandora_spreadsheet_ingest/{isi_request}/mapping' defaults: _title: 'Mapping' _entity_form: 'isi_request.map' requirements: - _permission: 'access content overview' + _entity_access: 'isi_request.map' entity.isi_request.delete_form: path: '/admin/content/islandora_spreadsheet_ingest/{isi_request}/delete' defaults: _title: 'Delete' _entity_form: 'isi_request.delete' requirements: - _permission: 'access content overview' + _entity_access: 'isi_request.delete' entity.isi_request.view: path: '/admin/content/islandora_spreadsheet_ingest/{isi_request}' defaults: - #_title: 'View' - _entity_form: 'isi_request.view' + _entity_view: 'isi_request.full' requirements: - _permission: 'access content overview' + _entity_access: 'isi_request.view' entity.isi_request.activate_form: path: '/admin/content/islandora_spreadsheet_ingest/{isi_request}/activate' defaults: _title: 'Activate' _entity_form: 'isi_request.activate' requirements: - _permission: 'access content overview' + _entity_access: 'isi_request.activate' +islandora_spreadsheet_ingest.admin: + path: '/admin/config/islandora_spreadsheet_ingest' + defaults: + _title: 'Islandora Spreadsheet Ingest' + _form: '\Drupal\islandora_spreadsheet_ingest\Form\Admin' + requirements: + _permission: 'administer islandora_spreadsheet_ingest requests' + options: + _admin_route: TRUE diff --git a/modules/islandora_spreadsheet_ingest_example/config/install/migrate_plus.migration.isi_file.yml b/modules/islandora_spreadsheet_ingest_example/config/install/migrate_plus.migration.isi_file.yml index 9e65692..b6fa9de 100644 --- a/modules/islandora_spreadsheet_ingest_example/config/install/migrate_plus.migration.isi_file.yml +++ b/modules/islandora_spreadsheet_ingest_example/config/install/migrate_plus.migration.isi_file.yml @@ -12,6 +12,7 @@ process: - plugin: skip_on_empty method: row message: 'Field Digital_File is missing' + - plugin: file_is_accessible _destination_filename: - plugin: get source: '@_file_defined' diff --git a/src/Controller/RequestListBuilder.php b/src/Controller/RequestListBuilder.php index 2651b63..738e5b3 100644 --- a/src/Controller/RequestListBuilder.php +++ b/src/Controller/RequestListBuilder.php @@ -2,14 +2,25 @@ namespace Drupal\islandora_spreadsheet_ingest\Controller; +use Drupal\Core\Link; use Drupal\Core\Config\Entity\ConfigEntityListBuilder; use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Entity\EntityTypeInterface; + +use Symfony\Component\DependencyInjection\ContainerInterface; /** * Request config entity list builder. */ class RequestListBuilder extends ConfigEntityListBuilder { + /** + * The migration group deriver service. + * + * @var \Drupal\islandora_spreadsheet_ingest\MigrationGroupDeriverInterface + */ + protected $migrationGroupDeriver; + /** * {@inheritdoc} */ @@ -19,6 +30,7 @@ public function buildHeader() { $header['label'] = $this->t('Request'); $header['id'] = $this->t('ID'); $header['active'] = $this->t('Active'); + $header['migration_group'] = $this->t('Migration group'); return $header + parent::buildHeader(); } @@ -27,11 +39,55 @@ public function buildHeader() { * {@inheritdoc} */ public function buildRow(EntityInterface $entity) { - $row['label'] = $entity->label(); + $row['label'] = $entity->toLink(NULL, 'edit-form'); + if (!$row['label']->getUrl()->access()) { + $row['label'] = $entity->label(); + } $row['id'] = $entity->id(); - $row['active'] = $entity->getActive() ? $this->t('Active') : $this->t('Inactive'); + + $mg_name = $this->migrationGroupDeriver->deriveName($entity); + // XXX: The migration_group entity does not actually have this listed as one + // of its routes (it is instead on the "migration" entity for some reason... + // ... anyway... let's build out a link to it. + $mg_link = Link::createFromRoute( + $mg_name, + 'entity.migration.list', + ['migration_group' => $mg_name] + ); + $activate_link = $entity->toLink($entity->getActive() ? $this->t('Active') : $this->t('Inactive'), 'activate-form'); + $row['active'] = $activate_link->getUrl()->access() ? + $activate_link : + $activate_link->getText(); + $row['migration_group'] = $entity->getActive() ? + ($mg_link->getUrl()->access() ? + $mg_link : + $mg_link->getText()) : + ''; return $row + parent::buildRow($entity); } + /** + * {@inheritdoc} + */ + public function getOperations(EntityInterface $entity) { + $ops = parent::getOperations($entity); + + // Filter to only those operations to which the user has access. + return array_filter($ops, function ($op) { + return $op['url']->access(); + }); + } + + /** + * {@inheritdoc} + */ + public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) { + $instance = parent::createInstance($container, $entity_type); + + $instance->migrationGroupDeriver = $container->get('islandora_spreadsheet_ingest.migration_group_deriver'); + + return $instance; + } + } diff --git a/src/Entity/Request.php b/src/Entity/Request.php index 1aafd71..7eed19c 100644 --- a/src/Entity/Request.php +++ b/src/Entity/Request.php @@ -21,10 +21,12 @@ * "edit" = "Drupal\islandora_spreadsheet_ingest\Form\Ingest\FileUpload", * "map" = "Drupal\islandora_spreadsheet_ingest\Form\Ingest\Mapping", * "view" = "Drupal\islandora_spreadsheet_ingest\Form\Ingest\Review", - * } + * }, + * "access" = "Drupal\islandora_spreadsheet_ingest\RequestAccessControlHandler", + * "view_builder" = "Drupal\islandora_spreadsheet_ingest\RequestViewBuilder", * }, * config_prefix = "request", - * admin_permission = "administer site configuration", + * admin_permission = "administer islandora_spreadsheet_ingest requests", * entity_keys = { * "id" = "id", * "label" = "label", diff --git a/src/Form/Admin.php b/src/Form/Admin.php new file mode 100644 index 0000000..954d021 --- /dev/null +++ b/src/Form/Admin.php @@ -0,0 +1,96 @@ +streamWrapperManager = $stream_wrapper_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('config.factory'), + $container->get('stream_wrapper_manager') + ); + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'islandora_spreadsheet_ingest_admin_settings_form'; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + $form = []; + $config = $this->config('islandora_spreadsheet_ingest.settings'); + $current_whitelist = $config->get('binary_directory_whitelist'); + $form['schemes'] = [ + '#type' => 'checkboxes', + '#title' => $this->t('Schemes'), + '#description' => $this->t('Allowed list of schemes for which binaries can be referenced from.'), + '#default_value' => $config->get('schemes') ?? [], + '#options' => $this->streamWrapperManager->getNames(StreamWrapperInterface::READ_VISIBLE), + ]; + $form['paths'] = [ + '#type' => 'textarea', + '#title' => $this->t('Binary path whitelist'), + '#default_value' => $current_whitelist ? implode(',', $current_whitelist) : '', + '#description' => $this->t('A comma separated list of local locations from which spreadsheet ingests can use binaries.'), + ]; + $form['submit'] = [ + '#type' => 'submit', + '#value' => $this->t('Save'), + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + protected function getEditableConfigNames() { + return ['islandora_spreadsheet_ingest.settings']; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $config = $this->config('islandora_spreadsheet_ingest.settings'); + $whitelist = explode(',', $form_state->getValue('paths')); + $config->set('binary_directory_whitelist', $whitelist); + $config->set('schemes', array_filter($form_state->getValue('schemes'))); + $config->save(); + parent::submitForm($form, $form_state); + } + +} diff --git a/src/Form/Ingest/FileUpload.php b/src/Form/Ingest/FileUpload.php index dbc020d..9f5829e 100644 --- a/src/Form/Ingest/FileUpload.php +++ b/src/Form/Ingest/FileUpload.php @@ -363,7 +363,7 @@ public function save(array $form, FormStateInterface $form_state) { try { $request->save(); - $form_state->setRedirect('entity.isi_request.map_form', [ + $form_state->setRedirect('entity.isi_request.view', [ 'isi_request' => $request->id(), ]); } diff --git a/src/Form/Ingest/Mapping.php b/src/Form/Ingest/Mapping.php index 5eb9e09..c743df5 100644 --- a/src/Form/Ingest/Mapping.php +++ b/src/Form/Ingest/Mapping.php @@ -103,14 +103,19 @@ protected function mapMappings($entity_type, $entity, &$form, FormStateInterface protected function actions(array $form, FormStateInterface $form_state) { $actions = parent::actions($form, $form_state); + $entity_validation = [ + '::preValidateEntity', + '::validateEntity', + ]; + + // Add the validation to the normal submit. + $actions['submit']['#validate'] = $entity_validation; + // Add "save and review" button or whatever. $actions['save_and_review'] = [ '#type' => 'submit', '#value' => $this->t('Save and proceed'), - '#validate' => [ - '::preValidateEntity', - '::validateEntity', - ], + '#validate' => $entity_validation, '#submit' => array_merge( $actions['submit']['#submit'], [ diff --git a/src/MigrationDeriver.php b/src/MigrationDeriver.php index 71093a0..8dd5981 100644 --- a/src/MigrationDeriver.php +++ b/src/MigrationDeriver.php @@ -246,9 +246,12 @@ public function createAll(RequestInterface $request) { 'label' => $original_migration->label(), 'migration_group' => $mg_name, 'source' => [ - /* - * XXX: Doesn't appear necessary to specify the columns? + /* XXX: Doesn't appear necessary to specify the columns? * 'columns' => array_unique(iterator_to_array($this->getUsedColumns($info['mappings']))), + * + * @note: Constants and other things defined in the source on + * individual migrations will not be passed through. This is a design + * choice. If needed specify them on the group itself. */ ], 'process' => iterator_to_array( diff --git a/src/Plugin/migrate/process/AccessibleFile.php b/src/Plugin/migrate/process/AccessibleFile.php new file mode 100644 index 0000000..df155e8 --- /dev/null +++ b/src/Plugin/migrate/process/AccessibleFile.php @@ -0,0 +1,126 @@ +fileSystem = $file_system; + $this->streamWrapperManager = $stream_wrappers; + $this->configFactory = $config; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('stream_wrapper_manager'), + $container->get('file_system'), + $container->get('config.factory') + ); + } + + /** + * {@inheritdoc} + */ + public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) { + $allowed_paths = $this->configFactory->get('islandora_spreadsheet_ingest.settings')->get('binary_directory_whitelist'); + $allowed_schemes = $this->configFactory->get('islandora_spreadsheet_ingest.settings')->get('schemes'); + + // If it's a local path check to see if it's in our allowed list. + $file_path = $this->fileSystem->realpath($value); + if ($file_path) { + foreach ($allowed_paths as $path) { + if (strpos($file_path, $path) === 0) { + return $value; + } + } + } + + // If the file has a scheme check to see if it's in our allowed list. + $file_scheme = $this->streamWrapperManager->getScheme($value); + if ($file_scheme) { + foreach ($allowed_schemes as $scheme) { + if ($file_scheme === $scheme) { + return $value; + } + } + } + throw new MigrateSkipRowException(strtr("The file provided (:file) is not allowed within the configured approved list of directories or schemes.", [ + ':file' => $value, + ])); + } + +} diff --git a/src/RequestAccessControlHandler.php b/src/RequestAccessControlHandler.php new file mode 100644 index 0000000..475e195 --- /dev/null +++ b/src/RequestAccessControlHandler.php @@ -0,0 +1,60 @@ + ['edit islandora_spreadsheet_ingest requests'], + 'map' => ['edit islandora_spreadsheet_ingest request mapping'], + 'view' => ['view islandora_spreadsheet_ingest requests'], + 'activate' => ['activate islandora_spreadsheet_ingest requests'], + 'delete' => ['delete islandora_spreadsheet_ingest requests'], + ]; + + /** + * {@inheritdoc} + */ + protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) { + return parent::checkCreateAccess($account, $context, $entity_bundle) + ->orIf(AccessResult::allowedIfHasPermission($account, 'create islandora_spreadsheet_ingest requests')); + } + + /** + * {@inheritdoc} + */ + protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) { + return parent::checkAccess($entity, $operation, $account) + ->orIf($this->specificCheckAccess($entity, $operation, $account)); + } + + /** + * Helper; check our specific requirements. + * + * @see ::checkAccess() + * + * @return \Drupal\Core\Access\AccessResultInterface + * Our result. + */ + protected function specificCheckAccess(EntityInterface $entity, $operation, AccountInterface $account) { + if (!isset(static::MAP[$operation])) { + return AccessResult::neutral(); + } + else { + return AccessResult::allowedIfHasPermissions($account, static::MAP[$operation]) + ->andIf( + AccessResult::allowedIf($entity->getOwner() == $account->id()) + ->cachePerUser() + ); + } + } + +} diff --git a/src/RequestViewBuilder.php b/src/RequestViewBuilder.php new file mode 100644 index 0000000..7118f77 --- /dev/null +++ b/src/RequestViewBuilder.php @@ -0,0 +1,97 @@ + 'item', + '#title' => $this->t('Label'), + '#markup' => $item['#isi_request']->label(), + ]; + + $owner = $this->userStorage->load($item['#isi_request']->getOwner()); + $item['owner'] = [ + '#type' => 'item', + '#title' => $this->t('Owner'), + 'username' => ($owner ? + [ + '#theme' => 'username', + '#account' => $owner, + ] : + $this->t('Unknown user')), + ]; + $item['active'] = [ + '#type' => 'item', + '#title' => $this->t('Active'), + '#markup' => ($item['#isi_request']->getActive() ? + $this->t('Yes') : + $this->t('No')), + ]; + $mg_name = $this->migrationGroupDeriver->deriveName($item['#isi_request']); + $mg_link = Link::createFromRoute( + $mg_name, + 'entity.migration.list', + ['migration_group' => $mg_name] + ); + $item['migration_group'] = [ + '#type' => 'item', + '#title' => $this->t('Migration group'), + '#access' => $item['#isi_request']->getActive(), + ]; + if ($mg_link->getUrl()->access()) { + $item['migration_group']['link'] = $mg_link->toRenderable(); + } + else { + $item['migration_group']['#markup'] = $mg_link->getText(); + } + } + + return $build_list; + } + + /** + * {@inheritdoc} + */ + public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) { + $instance = parent::createInstance($container, $entity_type); + + $instance->userStorage = $container->get('entity_type.manager')->getStorage('user'); + $instance->migrationGroupDeriver = $container->get('islandora_spreadsheet_ingest.migration_group_deriver'); + + return $instance; + } + +}