diff --git a/includes/media.filter.inc b/includes/media.filter.inc index aacd189..2d8a27b 100644 --- a/includes/media.filter.inc +++ b/includes/media.filter.inc @@ -7,6 +7,9 @@ * @TODO: Rename this file? */ +define('MEDIA_TOKEN_REGEX', '/\[\[.*?\]\]/s'); +define('MEDIA_TOKEN_REGEX_ALT', '/%7B.*?%7D/s'); + /** * Implements hook_wysiwyg_include_directory(). */ @@ -26,12 +29,204 @@ function media_wysiwyg_include_directory($type) { */ function media_filter($text) { $text = ' ' . $text . ' '; - $text = preg_replace_callback("/\[\[.*?\]\]/s", 'media_token_to_markup', $text); + $text = preg_replace_callback(MEDIA_TOKEN_REGEX, 'media_token_to_markup', $text); return $text; } /** + * Implements hook_field_attach_insert(). + * + * Track file usage for media files included in formatted text. Note that this + * is heavy-handed, and should be replaced when Drupal's filter system is + * context-aware. + */ +function media_field_attach_insert($entity_type, $entity) { + _media_filter_add_file_usage_from_fields($entity_type, $entity); +} + +/** + * Implements hook_field_attach_update(). + * + * @see media_field_attach_insert(). + */ +function media_field_attach_update($entity_type, $entity) { + _media_filter_add_file_usage_from_fields($entity_type, $entity); +} + +/** + * Add file usage from file references in an entity's text fields. + */ +function _media_filter_add_file_usage_from_fields($entity_type, $entity) { + // Track the total usage for files from all fields combined. + $entity_files = array(); + foreach (media_filter_parse_from_fields($entity_type, $entity) as $file_reference) { + if (empty($entity_files[$file_reference['fid']])) { + $entity_files[$file_reference['fid']] = 1; + } + else { + $entity_files[$file_reference['fid']]++; + } + } + + $entity_id = entity_extract_ids($entity_type, $entity); + // If this is neither a brand new node nor a new revision, then we have to do some magic to increment or decrement the usage table properly. + if (((!empty($entity->original) && !empty($entity_id)) && ((!isset($entity->revision)) || ($entity->revision == FALSE)))) { + // We'll look at the original version of this revision and compare its file usages to what's being saved. + $original_entity_files = array(); + foreach (media_filter_parse_from_fields($entity_type, $entity->original) as $file_reference) { + // We're making an array of file usages that were on the pre-save version of this node. + // If this file is new to this node we'll add its first usage. + if (empty($original_entity_files[$file_reference['fid']])) { + $original_entity_files[$file_reference['fid']] = 1; + } + // Otherwise we'll increment the usage that's already there. + else { + $original_entity_files[$file_reference['fid']]++; + } + } + // Now count up the files on the new version being saved. + $entity_files_delta = array(); + foreach($entity_files as $fid => $file_count) { + $file_delta = $file_count - $original_entity_files[$fid]; + $file = file_load($fid); + if ($file_delta < 0) { + // If the change is negative, decrement recorded usages. + file_usage_delete($file, 'media', 'node', $entity->nid, abs($file_delta)); + } elseif ($file_delta > 0) { + // If the change is positive, add usages. + file_usage_add($file, 'media', 'node', $entity->nid, $file_delta); + } + } + // Now we get out of this function because we're done incrementing and/or decrementing. + return; + } + + list($entity_id, $entity_vid) = entity_extract_ids($entity_type, $entity); + + // When the current revision of this entity is the only revision of this entity + // and files have been removed from this entity the file_usage entry for this + // entity can be removed. The code following this will add the files back correctly. +// if ($entity->revision == FALSE) { +// db_delete('file_usage') +// ->condition('module', 'media') +// ->condition('type', $entity_type) +// ->condition('id', $entity_id) +// ->execute(); +// } + + // Each entity revision counts for file usage. If versions are not enabled + // the file_usage table will have no entries for this because of the delete + // query above. + foreach ($entity_files as $fid => $entity_count) { + $file = file_load($fid); + file_usage_add($file, 'media', $entity_type, $entity_id, $entity_count); + } + +} + +/** + * Parse file references from an entity's text fields and return them as an array. + */ +function media_filter_parse_from_fields($entity_type, $entity) { + $file_references = array(); + + foreach (_media_filter_fields_with_text_filtering($entity_type, $entity) as $field_name) { + if ($field_items = field_get_items($entity_type, $entity, $field_name)) { + foreach ($field_items as $field_item) { + preg_match_all(MEDIA_TOKEN_REGEX, $field_item['value'], $matches); + foreach ($matches[0] as $tag) { + $tag = str_replace(array('[[', ']]'), '', $tag); + $tag_info = drupal_json_decode($tag); + if (isset($tag_info['fid']) && $tag_info['type'] == 'media') { + $file_references[] = $tag_info; + } + } + + preg_match_all(MEDIA_TOKEN_REGEX_ALT, $field_item['value'], $matches_alt); + foreach ($matches_alt[0] as $tag) { + $tag = urldecode($tag); + $tag_info = drupal_json_decode($tag); + if (isset($tag_info['fid']) && $tag_info['type'] == 'media') { + $file_references[] = $tag_info; + } + } + } + } + } + + return $file_references; +} + +/** + * Returns an array containing the names of all fields that perform text filtering. + */ +function _media_filter_fields_with_text_filtering($entity_type, $entity) { + list($entity_id, $revision_id, $bundle) = entity_extract_ids($entity_type, $entity); + $fields = field_info_instances($entity_type, $bundle); + + // Get all of the fields on this entity that allow text filtering. + $fields_with_text_filtering = array(); + foreach ($fields as $field_name => $field) { + if (!empty($field['settings']['text_processing'])) { + $fields_with_text_filtering[] = $field_name; + } + } + + return $fields_with_text_filtering; +} + +/** + * Utility function to get the file count in this entity + * + * @param type $entity + * @param type $entity_type + * @return int + */ +function media_entity_field_count_files($entity_type, $entity) { + $entity_files = array(); + foreach (media_filter_parse_from_fields($entity_type, $entity) as $file_reference) { + if (empty($entity_files[$file_reference['fid']])) { + $entity_files[$file_reference['fid']] = 1; + } + else { + $entity_files[$file_reference['fid']]++; + } + } + return $entity_files; +} + + +/** + * Implements hook_entity_delete(). + */ +function media_entity_delete($entity, $type) { + list($entity_id) = entity_extract_ids($type, $entity); + + db_delete('file_usage') + ->condition('module', 'media') + ->condition('type', $type) + ->condition('id', $entity_id) + ->execute(); +} + +/** + * Implements hook_field_attach_delete_revision(). + * + * @param type $entity_type + * @param type $entity + */ +function media_field_attach_delete_revision($entity_type, $entity) { + list($entity_id) = entity_extract_ids($entity_type, $entity); + $files = media_entity_field_count_files($entity_type, $entity); + foreach ($files as $fid => $count) { + if ($file = file_load($fid)) { + file_usage_delete($file, 'media', $entity_type , $entity_id, $count); + } + } +} + +/** * Parses the contents of a CSS declaration block and returns a keyed array of property names and values. * * @param $declarations diff --git a/media.info b/media.info index 28b07cd..3398d7e 100644 --- a/media.info +++ b/media.info @@ -17,5 +17,6 @@ files[] = includes/media_views_plugin_display_media_browser.inc files[] = includes/media_views_plugin_style_media_browser.inc files[] = tests/media.test files[] = tests/media.entity.test +files[] = tests/media.file.usage.test configure = admin/config/media/browser diff --git a/tests/media.file.usage.test b/tests/media.file.usage.test new file mode 100644 index 0000000..71aac4d --- /dev/null +++ b/tests/media.file.usage.test @@ -0,0 +1,182 @@ + t('File usage tracking'), + 'description' => t('Tests tracking of usage for files added via a WYSIWYG interface'), + 'group' => t('Media'), + ); + } + + function setUp() { + parent::setUp(array('media', 'file_entity')); + } + + public function TestFileUsageIncrementing() { + // Create and log in a user. + $admin = $this->drupalCreateUser(array('administer nodes', 'create article content')); + $user = $this->drupalCreateUser(); + $this->drupalLogin($admin); + // Create a file. + $this->drupalGet('node/add/article'); + $files = $this->drupalGetTestFiles('image'); + $file = file_save($files[0]); + $fid = $file->fid; + //There should be zero usages prior to node creation, + $file_uses = file_usage_list($file); + $this->assertEqual(empty($file_uses), TRUE, t('Created a new file with zero usages.')); + + function generate_file_usage_markup($fid, $count=0) { + //creates markup that adds our file to the body field. + $file_usage_markup = ''; + $i = 0; + //it adds as many instances of the file as we specify with $count. + while ($i<$count) { + //set the variables that go in the markup + $view_mode = 'preview'; + $type = 'media'; + $attributes = array( + 'height' => 180, + 'width' => 180, + 'classes' => 'media-element file_preview', + ); + + //build the markup + $fid_set = '"fid":"' . $fid . '"'; + $view_mode_set = '"view_mode":"' . $view_mode . '"'; + $type_set = '"type":"' . $type . '"'; + $attribute_set = '"attributes":{'; + $attribute_set .= '"height":' . $attributes['height'] . ','; + $attribute_set .= '"width":' . $attributes['width'] . ','; + $attribute_set .= '"class":"' . $attributes['classes'] . '"'; + $attribute_set .='}'; + + //put it all together + $file_usage_markup .= '

[[{'; + + $file_usage_markup .= $fid_set . ','; + $file_usage_markup .= $view_mode_set . ','; + $file_usage_markup .= $type_set . ','; + $file_usage_markup .= $attribute_set; + + $file_usage_markup .= '}]]

'; + + $i++; + } + + return $file_usage_markup; + } + + // Create an article. + $edit = array( + 'title' => $this->randomName(8), + 'body[und][0][value]' => generate_file_usage_markup($fid, 1), + ); + // First argument is the URL, then the value array and the third is the label the button that should be "clicked". + $this->drupalPost('node/add/article', $edit, t('Save')); + //and refresh our value for file_uses too. + $file_uses = file_usage_list($file); + + // Get info about the node so we can grab the right file to test. + $node = $this->drupalGetNodeByTitle($edit['title']); + $nid = $node->nid; + + // Verify that the file usage was incremented when we added it to the node. + $this->assertEqual($node->vid, 1, 'New node with one file inserted, vid == 1'); + $this->assertEqual($file_uses['media']['node'][$nid], 1, t('File usage == 1')); + + // Create a new revision that has the file on it once. + $node = node_load($nid); + $node->body['und'][0]['value'] = generate_file_usage_markup($fid, 1); + $node->revision = TRUE; + node_save($node); + $node = node_load($nid); + $file_uses = file_usage_list($file); + $vid = $node->vid; + $this->assertEqual($vid, 2, 'Resaved node with vid == 2.'); + $this->assertEqual($file_uses['media']['node'][$nid], 2, t('Resaved node has file usages == 2.')); + + // Create a new revision that has the file on it twice. + $node = node_load($nid); + $node->body['und'][0]['value'] = generate_file_usage_markup($fid, 2); + $node->revision = TRUE; + node_save($node); + $node = node_load($nid); + $file_uses = file_usage_list($file); + $vid = $node->vid; + $this->assertEqual($vid, 3, 'Resaved node with vid == 3.'); + $this->assertEqual($file_uses['media']['node'][$nid], 4, t('Resaved node has file usages == 4.')); + + // Create a new revision that has no file on it. + $node = node_load($nid); + $node->body['und'][0]['value'] = 'no file in body'; + $node->revision = TRUE; + node_save($node); + $node = node_load($nid); + $file_uses = file_usage_list($file); + $vid = $node->vid; + $this->assertEqual($vid, 4, 'Resaved node with new revision, vid == 4.'); + $this->assertEqual($file_uses['media']['node'][$nid], 4, t('Resaved node has no file in body, file usages == 4.')); + + // Create a new revision that has the file on it once. + $node = node_load($nid); + $node->body['und'][0]['value'] = generate_file_usage_markup($fid, 1); + $node->revision = TRUE; + node_save($node); + $node = node_load($nid); + $file_uses = file_usage_list($file); + $vid = $node->vid; + $this->assertEqual($vid, 5, 'Resaved node with new revision, vid == 5.'); + $this->assertEqual($file_uses['media']['node'][$nid], 5, t('Resaved node has one file in body, file usages == 5.')); + + // Delete a revision that has the file on it once. + node_revision_delete(2); + $node = node_load($nid); + $file_uses = file_usage_list($file); + $this->assertEqual($file_uses['media']['node'][$nid], 4, 'Deleted revision with a usage (vid==2), file usage == 4.'); + + // Delete a revision that has no file on it. + node_revision_delete(5); + $node = node_load($nid); + $file_uses = file_usage_list($file); + $this->assertEqual($file_uses['media']['node'][$nid], 4, 'Deleted revision with no usage (vid==5), file usage == 4'); + + // Delete a revision that has the file on it twice. + node_revision_delete(3); + $node = node_load($nid); + $file_uses = file_usage_list($file); + $this->assertEqual($file_uses['media']['node'][$nid], 2, 'Deleted revision with two usages (vid==3), file usages == 2'); + + // Create a new revision that has the file on it twice. + $node->body['und'][0]['value'] = generate_file_usage_markup($fid, 2); + $node->revision = TRUE; + node_save($node); + $node = node_load($nid); + $this->assertEqual($node->vid, 6, 'We\'re looking at the revision with vid == 6.'); + $file_uses = file_usage_list($file); + $this->assertEqual($file_uses['media']['node'][$nid], 4, t('New revision has file on it twice, file usages == 4.')); + + // Re-save current revision with file on it once instead of twice. + $node->body['und'][0]['value'] = generate_file_usage_markup($fid, 1); + node_save($node); + $node = node_load($nid); + $this->assertEqual($node->vid, 6, 'We\'re looking at the same revision, vid == 6.'); + $file_uses = file_usage_list($file); + $this->assertEqual($file_uses['media']['node'][$nid], 3, t('Resaved revision has one file in the body now, instead of two. file usages == 3.')); + + // Delete the node and check that usages are at zero. + $node = node_load($nid); + node_delete($nid); + $node = node_load($nid); + $this->assertEqual(empty($node), TRUE, 'The node has been deleted.'); + media_entity_delete($file, 'file'); + $file_uses = file_usage_list($file); + $this->assertEqual(empty($file_uses), TRUE, 'The file is no longer in use. There is no recorded usage.'); + } +} \ No newline at end of file