diff --git a/config/schema/google_vision.schema.yml b/config/schema/google_vision.schema.yml index bcb997e..884df2a 100644 --- a/config/schema/google_vision.schema.yml +++ b/config/schema/google_vision.schema.yml @@ -13,3 +13,11 @@ google_vision.settings: api_key: type: string label: 'Google Vision Api Key' + +field.field.*.*.*.third_party.google_vision: + type: mapping + label: 'Per-image field safe search settings' + mapping: + safe_search: + type: boolean + label: 'Safe Search' diff --git a/google_vision.module b/google_vision.module index a4c567c..4855e94 100644 --- a/google_vision.module +++ b/google_vision.module @@ -7,6 +7,11 @@ use Drupal\Core\Form\FormStateInterface; use Drupal\field\FieldConfigInterface; use Drupal\google_vision\GoogleVisionAPI; +use Drupal\field\FieldStorageConfigInterface; +use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Entity\EntityTypeInterface; +use Drupal\Core\Field\FieldItemListInterface; +use Drupal\Core\Field\FieldDefinitionInterface; /** * Implements hook_entity_presave(). @@ -49,18 +54,50 @@ function google_vision_form_field_config_edit_form_alter(&$form, \Drupal\Core\Fo '#default_value' => !empty($settings['google_vision']) ? $settings['google_vision'] : FALSE, ]; - $form['#entity_builders'][] = 'google_vision_form_field_config_form_builder'; + $form['#entity_builders'][] = 'google_vision_form_field_config_form_taxonomy_builder'; + } + + if($field_entity->getType() == 'image') { + $settings = $form_state->getFormObject()->getEntity()->getThirdPartySettings('google_vision'); + $form['safe_search'] = [ + '#type' => 'checkbox', + '#title' => t('Enable Safe Search'), + '#description' => t('Detects and avoids explicit contents.'), + '#default_value' => !empty($settings['safe_search']) ? $settings['safe_search'] : FALSE, + ]; + + $form['#entity_builders'][] = 'google_vision_form_field_config_form_image_builder'; } } /** - * Form builder to save the settings. + * Form builder to save the settings for entity reference. */ -function google_vision_form_field_config_form_builder($entity_type, FieldConfigInterface $type, &$form, FormStateInterface $form_state) { +function google_vision_form_field_config_form_taxonomy_builder($entity_type, FieldConfigInterface $type, &$form, FormStateInterface $form_state) { $type->setThirdPartySetting('google_vision', 'google_vision', $form_state->getValue('google_vision')); } /** + * Implements hook_entity_bundle_field_info_alter(). + */ +function google_vision_entity_bundle_field_info_alter(&$fields, EntityTypeInterface $entity_type, $bundle) { + if ($entity_type->id() == 'node') { + foreach ($fields as $field) { + if ($field->getType() == 'image') { + $fields[$field->getName()]->addConstraint('SafeSearch'); + } + } + } +} + +/** + * Form builder to save the settings for images. + */ +function google_vision_form_field_config_form_image_builder($entity_type, FieldConfigInterface $type, &$form, FormStateInterface $form_state) { + $type->setThirdPartySetting('google_vision', 'safe_search', $form_state->getValue('safe_search')); +} + +/** * Try to get and add labels for the file entity. */ function google_vision_file_entity_add_labels($file, $field, $vid) { diff --git a/src/Plugin/Validation/Constraint/SafeSearchConstraint.php b/src/Plugin/Validation/Constraint/SafeSearchConstraint.php new file mode 100755 index 0000000..9bf064d --- /dev/null +++ b/src/Plugin/Validation/Constraint/SafeSearchConstraint.php @@ -0,0 +1,18 @@ +googleVisionAPI = $google_vision; + $this->fileSystem = $file_system; + } + + /** + * {@inheritdoc} + */ + public function validate($data, Constraint $constraint) { + $field_def = $data->getFieldDefinition(); + $settings = $field_def->getThirdPartySettings('google_vision'); + // if the Safe Search detection is on. + if (!empty($settings['safe_search'])) { + // if the image is uploaded. + if (!empty($data->getValue('target_id'))) { + // Retrieve the file uri. + $file_uri = $data->entity->getFileUri(); + if ($filepath = $this->fileSystem->realpath($file_uri)) { + $result = $this->googleVisionAPI->safeSearchDetection($filepath); + if (!empty($result['responses'][0]['safeSearchAnnotation'])) { + $adult = $result['responses'][0]['safeSearchAnnotation']['adult']; + $likelihood = array('POSSIBLE', 'LIKELY', 'VERY_LIKELY'); + // if the image has explicit content. + if (in_array($adult, $likelihood)) { + $this->context->addViolation($constraint->message); + } + } + } + } + } + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static ( + $container->get('google_vision.api'), + $container->get('file_system') + ); + } +} diff --git a/src/Tests/SafeSearchConstraintValidationTest.php b/src/Tests/SafeSearchConstraintValidationTest.php new file mode 100644 index 0000000..a87d83f --- /dev/null +++ b/src/Tests/SafeSearchConstraintValidationTest.php @@ -0,0 +1,205 @@ +drupalCreateContentType(array('type' => 'test_images', 'name' => 'Test Images')); + // Creates administrative user. + $this->adminUser = $this->drupalCreateUser(array('administer google vision','create test_images content', 'access content', 'access administration pages', 'administer node fields', 'administer nodes', 'administer node display') + ); + $this->drupalLogin($this->adminUser); + //Check whether the api key is set. + $this->drupalGet(Url::fromRoute('google_vision.settings')); + $this->assertNotNull('api_key', 'The api key is set'); + } + + /** + * Create a new image field. + * + * @param string $name + * The name of the new field (all lowercase), exclude the "field_" prefix. + * @param string $type_name + * The node type that this field will be added to. + * @param array $storage_settings + * A list of field storage settings that will be added to the defaults. + * @param array $field_settings + * A list of instance settings that will be added to the instance defaults. + * @param array $widget_settings + * Widget settings to be added to the widget defaults. + * @param array $formatter_settings + * Formatter settings to be added to the formatter defaults. + * @param string $description + * A description for the field. + */ + public function createImageField($name, $type_name, $storage_settings = array(), $field_settings = array(), $widget_settings = array(), $formatter_settings = array(), $description = '') { + FieldStorageConfig::create(array( + 'field_name' => $name, + 'entity_type' => 'node', + 'type' => 'image', + 'settings' => $storage_settings, + 'cardinality' => 1, + ))->save(); + + $field_config = FieldConfig::create([ + 'field_name' => $name, + 'label' => $name, + 'entity_type' => 'node', + 'bundle' => $type_name, + 'settings' => $field_settings, + 'description' => $description, + ])->addConstraint('SafeSearch'); + $field_config->save(); + + entity_get_form_display('node', $type_name, 'default') + ->setComponent($name, array( + 'type' => 'image_image', + 'settings' => $widget_settings, + )) + ->save(); + + entity_get_display('node', $type_name, 'default') + ->setComponent($name, array( + 'type' => 'image', + 'settings' => $formatter_settings, + )) + ->save(); + + return $field_config; + } + + /** + * Get the field id of the image field formed. + */ + public function getImageFieldId() { + // Create an image field and add an field to the custom content type. + $storage_settings['default_image'] = array( + 'uuid' => 1, + 'alt' => '', + 'title' => '', + 'width' => 0, + 'height' => 0, + ); + $field_settings['default_image'] = array( + 'uuid' => 1, + 'alt' => '', + 'title' => '', + 'width' => 0, + 'height' => 0, + ); + $widget_settings = array( + 'preview_image_style' => 'medium', + ); + $field = $this->createImageField('images', 'test_images', $storage_settings, $field_settings, $widget_settings); + + $field_id = $field->id(); + return $field_id; + } + + /** + * Create a node of type test_images and also upload an image. + */ + public function createNodeWithImage() { + //Get an image. + $images = $this->drupalGetTestFiles('image'); + + $edit = array( + 'title[0][value]' => $this->randomMachineName(), + 'files[images_0]' => drupal_realpath($images[0]->uri), + ); + + $this->drupalPostForm('node/add/test_images' , $edit, t('Save and publish')); + + // Add alt text. + $this->drupalPostForm(NULL, ['images[0][alt]' => $this->randomMachineName()], t('Save and publish')); + + // Retrieve ID of the newly created node from the current URL. + $matches = array(); + preg_match('/node\/([0-9]+)/', $this->getUrl(), $matches); + return isset($matches[1]) ? $matches[1] : FALSE; + } + + /** + * Test to ensure explicit content is detected when Safe Search is enabled. + */ + public function testSafeSearchConstraint() { + //Get the image field id. + $field_id = $this->getImageFieldId(); + + //Enable the Safe Search. + $edit = array( + 'safe_search' => 1, + ); + $this->drupalPostForm("admin/structure/types/manage/test_images/fields/$field_id", $edit, t('Save settings')); + + //Ensure that the safe search is enabled. + $this->drupalGet("admin/structure/types/manage/test_images/fields/$field_id"); + + // Save the node. + $node_id = $this->createNodeWithImage(); + + //Assert the constraint message. + $this->assertText('This image contains explicit content and will not be saved.', 'Constraint message found'); + //Assert that the node is not saved. + $this->assertFalse($node_id, 'The node has not been saved'); + } + + /** + * Test to ensure no explicit content is detected when Safe Search is disabled. + */ + public function testNoSafeSearchConstraint() { + // Get the image field id. + $field_id = $this->getImageFieldId(); + + //Ensure that the safe search is disabled. + $this->drupalGet("admin/structure/types/manage/test_images/fields/$field_id"); + + // Save the node. + $node_id = $this->createNodeWithImage(); + + //Assert that no constraint message appears. + $this->assertNoText('This image contains explicit content and will not be saved.', 'No Constraint message found'); + //Display the node. + $this->drupalGet('node/' . $node_id); + } +} diff --git a/tests/modules/google_vision_test/google_vision_test.info.yml b/tests/modules/google_vision_test/google_vision_test.info.yml new file mode 100644 index 0000000..1db3d18 --- /dev/null +++ b/tests/modules/google_vision_test/google_vision_test.info.yml @@ -0,0 +1,7 @@ +name: Google Vision Test Helper +type: module +description: 'Mocks the Google Vision Service' +package: Testing +core: 8.x +dependencies: + - google_vision diff --git a/tests/modules/google_vision_test/google_vision_test.install b/tests/modules/google_vision_test/google_vision_test.install new file mode 100644 index 0000000..943cf03 --- /dev/null +++ b/tests/modules/google_vision_test/google_vision_test.install @@ -0,0 +1,10 @@ +getEditable('google_vision.settings'); + $config->set('api_key', 'test_23key') + ->save(); +} diff --git a/tests/modules/google_vision_test/src/GoogleVisionAPIFake.php b/tests/modules/google_vision_test/src/GoogleVisionAPIFake.php new file mode 100644 index 0000000..c35f123 --- /dev/null +++ b/tests/modules/google_vision_test/src/GoogleVisionAPIFake.php @@ -0,0 +1,76 @@ +configFactory = $config_factory; + $this->httpClient = $http_client; + $this->apiKey = $this->configFactory->get('google_vision.settings') + ->get('api_key'); + } + + /** + * Function to return the response showing the image contains explicit content. + * + * @param string $filepath . + * + * @return Array|bool. + */ + public function safeSearchDetection($filepath) { + if (!$this->apiKey) { + return FALSE; + } + $response = array( + 'responses' => array( + '0' => array( + 'safeSearchAnnotation' => array( + 'adult' => 'LIKELY', + 'spoof' => 'VERY_UNLIKELY', + 'medical' => 'POSSIBLE', + 'violence' => 'POSSIBLE' + ), + ), + ), + ); + return $response; + } +} diff --git a/tests/modules/google_vision_test/src/GoogleVisionTestServiceProvider.php b/tests/modules/google_vision_test/src/GoogleVisionTestServiceProvider.php new file mode 100644 index 0000000..ef9d68f --- /dev/null +++ b/tests/modules/google_vision_test/src/GoogleVisionTestServiceProvider.php @@ -0,0 +1,18 @@ +getDefinition('google_vision.api'); + $definition->setClass('Drupal\google_vision_test\GoogleVisionAPIFake'); + } +}