Commit 846c757c authored by JacobSingh's avatar JacobSingh

Moved DRUPAL-7--1 to HEAD to maintain stability w/ ALPHA releases

parent a09ea89c
// $Id$
* API documentation for the Media module.
The Media module provides a robust API for developers to extend functionality
in useful and novel ways.
CAUTION: This is pretty old!
To understand the API, it is first important to understand the underlying
architecture and the decisions behind its current implementation.
Stream Wrappers
First, the Media module makes heavy use of the core Stream Wrappers now
provided with Drupal 7. Streams are classes that encapsulate PHP file
functions, allowing an automatic override when calling the basic function.
For instance, a wrapper implementing an Amazon S3 Stream might override the
file_get_contents() using ->stream_open(), or a Flickr Stream Wrapper might
implement its own ->getExternalUrl to return the HTML URI for a specific
Flickr image.
See for more
information about Stream Wrappers in Drupal.
All Stream Wrappers will be registered to handle a specific scheme, which is
the part of a URI before ://, such as core Drupal's public:// and private://
schemes. (Technically, a scheme only requires :, but a bug in PHP 5.2
requires ://, so Drupal currently keeps the same requirement.)
The target after the scheme:// will be a unique identifier that the
implementing Stream Wrapper will use to determine the file resource being
accessed. For instance, public://my-document.txt might refer to a local file
stored in the server at /sites/, while a URI
of youtube://fe3fg8u34i might be the video stored at, and flickr://photoset/224 might use a lookup
table to determine this is a photoset accessed at
All files in Drupal are stored in the database with this unique URI, in
addition to its unique serial FID. In general, file objects are accessed
and loaded by the URI, which will automatically use the correct stream wrapper
implementation for its scheme.
File Field Widget
The Media module extends the core File field to make use of its browser for
editors, which is in turn exposed for other modules to extend. It does this
by create a new widget definition, defining a File (media) 'generic' file
widget, and suppressing the original File widget definition to avoid
confusion. All existing file fields will be converted to this new widget on
installation of the module, and if uninstalled, any defined media_generic file
fields will be reverted to their original definition to avoid loss of data.
Media Formatters
By default, the File (media) widget will use the original display behavior,
which is to display a generic icon followed by a link to the file. However, in
many (if not most) cases, a site administrator will want to display media in
very specific ways. For instance, they might want an image uploaded to be
displayed as a cropped thumbnail, as with any Flickr images attached to that
field, while a YouTube video might be displayed as a thumbnail that pops up
in a Shadowbox, and finally a PDF might be displayed in an iFrame. And that's
only when it's displayed in a node teaser...
To manage the various display formatting needs of an editor, the Media module
offers a Media Style API for other modules to implement and extend. Media
Styles of various mimetypes are grouped together and made available as new
formatters for field display, whether for a node, a comment, in a view, or
To implement a new Media Style, a module needs to register itself for a
specific mimetype, with the following hook. Note that Media styles will be
properly namespaced by the module, to avoid conflicts.
function hook_media_styles($mimetype = NULL) {
switch ($mimetype) {
case 'video/x-youtube':
return array(
'youtube_video' => t('YouTube Video'),
'youtube_thumbnail' => t('YouTube Thumbnail'),
'youtube_shadowbox' => t('YouTube Shadowbox'),
'youtube_link' => t('Youtube (link to original)'),
case 'image/x-flickr':
$styles = array(
'flickr__original' => t('Flickr (Original size)'),
foreach (image_styles() as $style_name => $style) {
$styles[$style_name] = t('Flickr (@style)', array('@style' => $style->name));
return $styles;
These styles will be available from the Media Styles configuration page, where
multiple formatters can be grouped together to create new formatter Style
bundles, available and intelligently applied to files of the appropriate
For instance, a new Media Style might be created, named 'Small image', which
will bundle an Image style for the various image mimetypes, another for Flickr
images, a shadowbox for YouTube thumbnails, and a fallback to a size styled
File icon. Then this style would be made available as a display formatter, and
the Media module will apply the correct style for the mimetype of the
particular instance.
A module may also define default Media styles to be made available, by
implementing hook_media_default_style_bundles(). Unlike the individual Media Styles,
Media Style bundles are not namespaced by module. If two or more modules use
the same Style bundle names, the behavior is undefined. Also note that the
administrator may override a specific Style bundle, for instance by replacing
the Style for a specific mimetype.
function hook_media_default_style_bundles() {
return array(
'shadowbox_images' => array(
'label' => t('Shadowbox images'),
'mimetypes' => array(
'image/*' => 'mymodule_image_shadowbox',
'video/*' => 'mymodule_video_shadowbox',
'*' => 'mymodule_default_shadowbox',
convert existing file fields to media_generic on hook_install
convert existing media_generic to file fields on hook_uninstall
allow theme_file_icon file directory override in widget display settings
create new default file icons and set its folder as the new default
maybe instead/in addition have fallbacks if icon not present?
rename image style square_thumbnail to use the media_ namespace
review Media Styles for feasibility, usability, and architecture
create Media Styles UI .inc file
look at hook_field_info_alter to see if we can affect file widget that way
CLEAN CRUFT (lots of functions that no longer apply or need to be rethought)
why do we want to override the file field widget again?
should we also move 'media_styles' to its own module, perhaps file_styles?
in media_field_formatter_info(),
should 'field types' => array('media_generic', 'file'), be only one or other?
// $Id$
July 2009
* Drupal 7 compatability (jmstacey)
CHANGELOG for Media module.
by aaron: Comment out pager from media library browser.
by aaron: Fix fatal error when a file doesn't exist in markup.
by aaron: Add ->override['browser'] so thumbs may be themed specially.
by aaron: Clean up browser variables.
#692618 by aaron: Tie in pager to query & theme.
#692618 by aaron: Continue adding pager support.
by aaron: Clean up json output for AJAX call.
#613782 by aaron: Add an empty message when there are no media.
#692618 by aaron: Begin framework to allow library paging.
#697106 by aaron: Allow modules to define & alter default streams & conditions.
#697106 by aaron: Send default conditions & streams to browser library.
#697106 by aaron: Filter fid's on conditions when filtering on stream.
#697106 by aaron: Begin support for stream filter to media_browser_list().
#694520 by aaron: check for !isset($default) in media_variable_get().
#694520 by aaron: Remove all media namespace variables on uninstall.
#694520 by aaron: Add media_variable_del().
#694520 by aaron: Remove use of &drupal_static() for the variable defaults.
#694520 by aaron: Allow media_variable_default() to return all variables.
#694520 by aaron: Use &drupal_static() for the variable defaults.
#694520 by aaron: Document the variable namespace registry.
#692786 by aaron: Prefill description in browser file selector.
#692592 by aaron: Add AJAX preview for media from url.
#692592 by aaron: Add media-add_from_url.js.
#692592 by aaron: Begin preview for from url form.
#692592 by aaron: Allow stream wrappers to validate & parse url's.
#692592 by aaron: Add hook_media_parse.
by aaron: Rename the Media module.
by aaron: Remove the media_emfield module.
by aaron: Add a wysiwyg flag to filter for youtube & the like.
by aaron: Add cache_media_xml table to schema.
by aaron: Add media_retrieve_xml().
by aaron: Allow for WYSIWYG attribute overrides.
by aaron: t('Format') in media_format_form().
by aaron: Change _parse_url in MediaReadOnlyStreamWrapper to explode on ://.
#671744 by aaron: Fix redirect after uploading new media from content admin.
#671744 by aaron: Fix error when uploading new media from media content admin.
#673998 by aaron: Fix typo (s/view/video).
#673998 by aaron: Move media type defs to hook_enable.
#678588 by aaron: Fix undefined index error on node view.
by aaron: Use the currently undocumented #attached property to load js & css.
by aaron: Defer to the Styles module to route formatting for the 'file'.
by aaron: Fix formatter build modes.
by aaron: Prefix formatters with 'media_'.
by aaron: Change default formatter to 'preview'.
by aaron: hook_field_formatter is now hook_field_formatter_view.
by aaron: Add blank settings/instance_settings to hook_field_info.
by aaron: Change hook_field_widget to hook_field_widget_form.
by JacobSingh: Begin work on new browser concept.
by aaron: Toggle 'match type' value on media type administrative form.
by aaron: Display administrative options only if we're handling the callback.
by aaron: Add administrative options for match type.
by aaron: Allow administrator to edit existing bundles.
by aaron: Filter media type bundles by stream.
by aaron: Fix various errors.
by aaron: Begin work on formatters.
by aaron: Add medium admin style.
by aaron: Add preview.
by aaron: Hyphens to underscores in #element.
by aaron: Check for existance of data in media_get_fields().
by aaron: Grab proper fields in _media_content_fields().
by aaron: Implement hook_field_schema().
by aaron: Fix error on content type admin page.
by aaron: Begin menu callback for admin/structure/media/.
by aaron: Fix Warning: include_once(): Failed opening '' for
by JacobSingh: Implement of hook_wysiwyg_plugin().
by JacobSingh: Remove hook_permission implementation.
by JacobSingh: Display content type admin overview page: admin/structure/media
by JacobSingh: Tie in MediaEntityController to media_add_files_submit.
by JacobSingh: Add menu callback for media_page_edit.
by JacobSingh: MediaEntityController.php.
by JacobSingh: Add
by aaron: Add hacks so that Media: YouTube can work.
by aaron: Attach subtab behaviors.
by aaron: Temporarily hardcode load media youtube js in browser.
by aaron: Integrate browser w/ optional Styles module.
by aaron: Add get_parameters to remote stream wrapper.
by aaron: Allow saving existing streams.
by aaron: Implement ->url_stat() for filesize().
by aaron: Save remote streams.
by aaron: Get MediaReadOnlyStreamWrapper in line with Drupal stream wrappers.
by aaron: Add MediaReadOnlyStreamWrapper.
by aaron: Add media_browser_thumbnails form element.
by aaron: theme_media_browser_thumbnail_radios.
by aaron: Coder review of concatenated spaces.
by aaron: Parameters to hook_form_alter have changed.
by aaron: Wrap js with (function($) {})(jQuery); for library compatibility.
by aaron: Add settings to each js behavior.
by aaron: Convert js behavior selection to once().
by aaron: Add attach methods to js behaviors.
by aaron: Attach css & js to form using #attached element.
by aaron: Convert class #attributes to arrays.
by aaron: Fix php notices for $registration['callbacks']['resource'].
by aaron: Update parameters to theme('image') call for d7.
by aaron: Update parameters to theme('item_list') calls for d7.
by aaron: Set static cache in media_get_fields() and fix notices for isset.
by aaron: In media_get_registered_modules() fix notices for isset.
by aaron: In media_active_fields_for_node_type() grab field info for d7.
by aaron: Use file_get_field_list() to get a listing of node type's filefields.
by aaron: $type is an object.
by aaron: Move menu settings to media config.
by aaron: Begin to fix content type settings page.
by jmstacey: Drupal 7 compatability.
* Update .info files.
* Added necessary files to new registry.
* Removed 'file' keys from hook_menu().
* Global media settings page fixed.
* Code styling cleanup (jmstacey).
* Untested rough sync up with D6 branch (jmstacey).
\ No newline at end of file
by jmstacey: Code styling cleanup.
by jmstacey: Untested rough sync up with D6 branch.
// $Id$
* Default implementation of DrupalEntityControllerInterface.
* Extends DrupalEntityControllerInterface.
* This class can be used as-is by most simple entity types. Entity types
* requiring special handling can extend the class.
* @see: (perhaps we should be using this)
class MediaEntityController extends DrupalDefaultEntityController {
protected $entityCache;
protected $entityType;
protected $entityInfo;
protected $hookLoadArguments;
protected $idKey;
protected $revisionKey;
protected $revisionTable;
protected $query;
public function load($ids = array(), $conditions = array()) {
public function load($ids = array(), $conditions = array()) {
$this->ids = $ids;
$this->conditions = $conditions;
......@@ -33,7 +24,6 @@ public function load($ids = array(), $conditions = array()) {
$this->revisionId = FALSE;
// Create a new variable which is either a prepared version of the $ids
// array for later comparison with the entity cache, or FALSE if no $ids
// were passed. The $ids array is reduced as items are loaded from cache,
......@@ -61,9 +51,6 @@ public function load($ids = array(), $conditions = array()) {
foreach ($queried_entities as &$entity) {
$entity->type = self::getBundleName($entity->filemime);
// Pass all entities loaded from the database through $this->attachLoad(),
// which attaches fields (if supported by the entity type) and calls the
// entity type specific load callback, for example hook_node_load().
......@@ -77,36 +64,148 @@ public function load($ids = array(), $conditions = array()) {
if (!empty($queried_entities) && !$this->revisionId) {
// Ensure that the returned array is ordered the same as the original
// $ids array if this was passed in and remove any invalid ids.
if ($passed_ids) {
// Remove any invalid ids from the array.
$passed_ids = array_intersect_key($passed_ids, $entities);
foreach ($entities as $entity) {
$passed_ids[$entity->{$this->idKey}] = $entity;
// Ensure that the returned array is ordered the same as the original
// $ids array if this was passed in and remove any invalid ids.
if ($passed_ids) {
// Remove any invalid ids from the array.
$passed_ids = array_intersect_key($passed_ids, $entities);
foreach ($entities as $entity) {
$passed_ids[$entity->{$this->idKey}] = $entity;
$entities = $passed_ids;
return $entities;
protected function buildQuery() {
$this->query = db_select($this->entityInfo['base table'], 'base');
$this->query->addTag($this->entityType . '_load_multiple');
if ($this->revisionId) {
$this->query->join($this->revisionTable, 'revision', "revision.{$this->idKey} = base.{$this->idKey} AND revision.{$this->revisionKey} = :revisionId", array(':revisionId' => $this->revisionId));
elseif ($this->revisionKey) {
$this->query->join($this->revisionTable, 'revision', "revision.{$this->revisionKey} = base.{$this->revisionKey}");
// Add fields from the {entity} table.
$entity_fields = drupal_schema_fields_sql($this->entityInfo['base table']);
if ($this->revisionKey) {
// Add all fields from the {entity_revision} table.
$entity_revision_fields = drupal_map_assoc(drupal_schema_fields_sql($this->revisionTable));
// The id field is provided by entity, so remove it.
// Change timestamp to revision_timestamp, and revision uid to
// revision_uid before adding them to the query.
// TODO: This is node specific and has to be moved into NodeController.
$this->query->addField('revision', 'timestamp', 'revision_timestamp');
$this->query->addField('revision', 'uid', 'revision_uid');
// Remove all fields from the base table that are also fields by the same
// name in the revision table.
$entity_field_keys = array_flip($entity_fields);
foreach ($entity_revision_fields as $key => $name) {
if (isset($entity_field_keys[$name])) {
$entities = $passed_ids;
$this->query->fields('revision', $entity_revision_fields);
$this->query->fields('base', $entity_fields);
if ($this->ids) {
$this->query->condition("base.{$this->idKey}", $this->ids, 'IN');
if ($this->conditions) {
foreach ($this->conditions as $field => $value) {
$this->query->condition('base.' . $field, $value);
* Attach data to entities upon loading.
* This will attach fields, if the entity is fieldable. It calls
* hook_entity_load() for modules which need to add data to all entities.
* It also calls hook_TYPE_load() on the loaded entities. For example
* hook_node_load() or hook_user_load(). If your hook_TYPE_load()
* expects special parameters apart from the queried entities, you can set
* $this->hookLoadArguments prior to calling the method.
* See NodeController::attachLoad() for an example.
protected function attachLoad(&$queried_entities) {
// Attach fields.
if ($this->entityInfo['fieldable']) {
if ($this->revisionId) {
field_attach_load_revision($this->entityType, $queried_entities);
else {
field_attach_load($this->entityType, $queried_entities);
// Call hook_entity_load().
foreach (module_implements('entity_load') as $module) {
$function = $module . '_entity_load';
$function($queried_entities, $this->entityType);
// Call hook_TYPE_load(). The first argument for hook_TYPE_load() are
// always the queried entities, followed by additional arguments set in
// $this->hookLoadArguments.
$args = array_merge(array($queried_entities), $this->hookLoadArguments);
foreach (module_implements($this->entityInfo['load hook']) as $module) {
call_user_func_array($module . '_' . $this->entityInfo['load hook'], $args);
* Get entities from the static cache.
* @param $ids
* If not empty, return entities that match these IDs.
* @param $conditions
* If set, return entities that match all of these conditions.
protected function cacheGet($ids, $conditions = array()) {
$entities = array();
// Load any available entities from the internal cache.
if (!empty($this->entityCache) && !$this->revisionId) {
if ($ids) {
$entities += array_intersect_key($this->entityCache, array_flip($ids));
// If loading entities only by conditions, fetch all available entities
// from the cache. Entities which don't match are removed later.
elseif ($conditions) {
$entities = $this->entityCache;
// Exclude any entities loaded from cache if they don't match $conditions.
// This ensures the same behavior whether loading from memory or database.
if ($conditions) {
foreach ($entities as $entity) {
$entity_values = (array) $entity;
if (array_diff_assoc($conditions, $entity_values)) {
return $entities;
public static function getBundleName($mime) {
$types = module_invoke_all('media_types');
$name = substr($mime, 0,strpos($mime, '/'));
if (in_array($name, array_keys($types))) {
return $name;
} else {
return FALSE;
// @todo: make this more flexible.
// $types = module_invoke_all('media_types');
// foreach ($types as $type) {
// if (preg_match($type->mimeTypes, ) {
// }
// }
* Store entities in the static entity cache.
protected function cacheSet($entities) {
$this->entityCache += $entities;
\ No newline at end of file
// $Id$
* Generic PHP stream wrapper interface.
* @see
interface MediaReadOnlyStreamWrapperInterface {
public function stream_open($uri, $mode, $options, &$opened_url);
public function stream_close();
public function stream_lock($operation);
public function stream_read($count);
public function stream_write($data);
public function stream_eof();
public function stream_seek($offset, $whence);
public function stream_flush();
public function stream_tell();
public function stream_stat();
// public function unlink($uri);
// public function rename($from_uri, $to_uri);
// public function mkdir($uri, $mode, $options);
// public function rmdir($uri, $options);
public function url_stat($uri, $flags);
public function dir_opendir($uri, $options);
public function dir_readdir();
public function dir_rewinddir();
public function dir_closedir();
* A base class for Resource Stream Wrappers.
* This class provides a complete stream wrapper implementation. It passes
* incoming URL's through an interpolation method then recursively calls
* the invoking PHP filesystem function.
* MediaReadOnlyStreamWrapper implementations need to override at least the
* interpolateUrl method to rewrite the URL before is it passed back into the
* calling function.
abstract class MediaReadOnlyStreamWrapper implements MediaReadOnlyStreamWrapperInterface {
protected $parameters = array();
protected $base_url = NULL;
private $_DEBUG_MODE = NULL;
public function get_parameters() {
return $this->parameters;
* "Template" for stat calls. All elements must be initialized.
* @var array
protected $_stat = array(
0 => 0, // device number
'dev' => 0,
1 => 0, // inode number
'ino' => 0,
2 => 0, // inode protection mode
'mode' => 0,
3 => 0, // number of links
'nlink' => 0,
4 => 0, // userid of owner
'uid' => 0,
5 => 0, // groupid of owner
'gid' => 0,
6 => -1, // device type, if inode device *
'rdev' => -1,
7 => 0, // size in bytes
'size' => 0,
8 => 0, // time of last access (Unix timestamp)
'atime' => 0,
9 => 0, // time of last modification (Unix timestamp)
'mtime' => 0,
10 => 0, // time of last inode change (Unix timestamp)
'ctime' => 0,
11 => -1, // blocksize of filesystem IO
'blksize' => -1,
12 => -1, // number of blocks allocated
'blocks' => -1,
function interpolateUrl() {
return $this->base_url .'?'. http_build_query($this->parameters);
* Returns a web accessible URL for the resource.
* This function should return a URL that can be embedded in a web page
* and accessed from a browser. For example, the external URL of
* "youtube://xIpLd0WQKCY" might be
* "".
* @return
* Returns a string containing a web accessible URL for the resource.
public function getExternalUrl() {
return $this->interpolateUrl();
* Base implementation of getMimeType().
static function getMimeType($uri, $mapping = NULL) {
return 'application/octet-stream';
* Base implementation of realpath().
function realpath() {
return $this->getExternalUrl();
* Stream context resource.
* @var Resource
public $context;
* A generic resource handle.
* @var Resource
public $handle = NULL;
* Instance URI (stream).
* A stream is referenced as "scheme://target".
* @var String
protected $uri;
* Base implementation of setUri().
function setUri($uri) {
$this->uri = $uri;
$this->parameters = $this->_parse_url($uri);
* Base implementation of getUri().
function getUri() {
return $this->uri;
* Report an error.
* @param $message
* The untranslated string to report.
* @param $options
* An optional array of options to send to t().
* @param $display
* If TRUE, then we display the error to the user.
* @return
* We return FALSE, since we sometimes pass that back from the reporting
* function.
private function _report_error($message, $options = array(), $display = FALSE) {
watchdog('resource', $message, $options, WATCHDOG_ERROR);
if ($display) {
drupal_set_message(t($message, $options), 'error');
return FALSE;
private function _debug($message, $type = 'status') {
if ($this->_DEBUG_MODE) {
drupal_set_message($message, $type);
* Returns an array of any parameters stored in the URL's path.
* @param $url
* The URL to parse, such as youtube://v/[video-code]/t/[tags+more-tags].
* @return
* An associative array of all the parameters in the path,
* or FALSE if the $url is ill-formed.
protected function _parse_url($url) {
$path = explode('://', $url);
$parts = explode('/', $path[1]);
$params = array();
$count = 0;