diff --git a/core/composer.json b/core/composer.json
index 71f07d0..8f69344 100644
--- a/core/composer.json
+++ b/core/composer.json
@@ -109,6 +109,7 @@
         "drupal/link": "self.version",
         "drupal/locale": "self.version",
         "drupal/minimal": "self.version",
+        "drupal/media": "self.version",
         "drupal/menu_link_content": "self.version",
         "drupal/menu_ui": "self.version",
         "drupal/migrate": "self.version",
diff --git a/core/modules/media/config/install/core.entity_view_mode.media.full.yml b/core/modules/media/config/install/core.entity_view_mode.media.full.yml
new file mode 100644
index 0000000..dfdbb3a
--- /dev/null
+++ b/core/modules/media/config/install/core.entity_view_mode.media.full.yml
@@ -0,0 +1,9 @@
+langcode: en
+status: false
+dependencies:
+  module:
+    - media
+id: media.full
+label: 'Full content'
+targetEntityType: media
+cache: true
diff --git a/core/modules/media/config/install/media.settings.yml b/core/modules/media/config/install/media.settings.yml
new file mode 100644
index 0000000..853e575
--- /dev/null
+++ b/core/modules/media/config/install/media.settings.yml
@@ -0,0 +1 @@
+icon_base_uri: 'public://media-icons/generic'
diff --git a/core/modules/media/config/optional/system.action.media_delete_action.yml b/core/modules/media/config/optional/system.action.media_delete_action.yml
new file mode 100644
index 0000000..62af35e
--- /dev/null
+++ b/core/modules/media/config/optional/system.action.media_delete_action.yml
@@ -0,0 +1,10 @@
+langcode: en
+status: true
+dependencies:
+  module:
+    - media
+id: media_delete_action
+label: 'Delete media'
+type: media
+plugin: media_delete_action
+configuration: {  }
diff --git a/core/modules/media/config/optional/system.action.media_publish_action.yml b/core/modules/media/config/optional/system.action.media_publish_action.yml
new file mode 100644
index 0000000..95e173d
--- /dev/null
+++ b/core/modules/media/config/optional/system.action.media_publish_action.yml
@@ -0,0 +1,10 @@
+langcode: en
+status: true
+dependencies:
+  module:
+    - media
+id: media_publish_action
+label: 'Publish media'
+type: media
+plugin: media_publish_action
+configuration: {  }
diff --git a/core/modules/media/config/optional/system.action.media_save_action.yml b/core/modules/media/config/optional/system.action.media_save_action.yml
new file mode 100644
index 0000000..c4d098f
--- /dev/null
+++ b/core/modules/media/config/optional/system.action.media_save_action.yml
@@ -0,0 +1,10 @@
+langcode: en
+status: true
+dependencies:
+  module:
+    - media
+id: media_save_action
+label: 'Save media'
+type: media
+plugin: media_save_action
+configuration: {  }
diff --git a/core/modules/media/config/optional/system.action.media_unpublish_action.yml b/core/modules/media/config/optional/system.action.media_unpublish_action.yml
new file mode 100644
index 0000000..4189e4e
--- /dev/null
+++ b/core/modules/media/config/optional/system.action.media_unpublish_action.yml
@@ -0,0 +1,10 @@
+langcode: en
+status: true
+dependencies:
+  module:
+    - media
+id: media_unpublish_action
+label: 'Unpublish media'
+type: media
+plugin: media_unpublish_action
+configuration: {  }
diff --git a/core/modules/media/config/optional/views.view.media.yml b/core/modules/media/config/optional/views.view.media.yml
new file mode 100644
index 0000000..745420c
--- /dev/null
+++ b/core/modules/media/config/optional/views.view.media.yml
@@ -0,0 +1,854 @@
+langcode: en
+status: true
+dependencies:
+  module:
+    - image
+    - media
+    - user
+id: media
+label: Media
+module: views
+description: ''
+tag: ''
+base_table: media_field_data
+base_field: mid
+core: 8.x
+display:
+  default:
+    display_plugin: default
+    id: default
+    display_title: Master
+    position: 0
+    display_options:
+      access:
+        type: perm
+        options:
+          perm: 'access media overview'
+      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: 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: full
+        options:
+          items_per_page: 50
+          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: '‹ Previous'
+            next: 'Next ›'
+            first: '« First'
+            last: 'Last »'
+          quantity: 9
+      style:
+        type: table
+        options:
+          grouping: {  }
+          row_class: ''
+          default_row_class: true
+          override: true
+          sticky: false
+          caption: ''
+          summary: ''
+          description: ''
+          columns:
+            name: name
+            bundle: bundle
+            changed: changed
+            uid: uid
+            status: status
+            thumbnail__target_id: thumbnail__target_id
+          info:
+            name:
+              sortable: true
+              default_sort_order: asc
+              align: ''
+              separator: ''
+              empty_column: false
+              responsive: ''
+            bundle:
+              sortable: true
+              default_sort_order: asc
+              align: ''
+              separator: ''
+              empty_column: false
+              responsive: ''
+            changed:
+              sortable: true
+              default_sort_order: desc
+              align: ''
+              separator: ''
+              empty_column: false
+              responsive: ''
+            uid:
+              sortable: false
+              default_sort_order: asc
+              align: ''
+              separator: ''
+              empty_column: false
+              responsive: ''
+            status:
+              sortable: true
+              default_sort_order: asc
+              align: ''
+              separator: ''
+              empty_column: false
+              responsive: ''
+            thumbnail__target_id:
+              sortable: false
+              default_sort_order: asc
+              align: ''
+              separator: ''
+              empty_column: false
+              responsive: ''
+          default: changed
+          empty_table: false
+      row:
+        type: fields
+      fields:
+        media_bulk_form:
+          id: media_bulk_form
+          table: media
+          field: media_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: media
+          plugin_id: media_bulk_form
+        thumbnail__target_id:
+          id: thumbnail__target_id
+          table: media_field_data
+          field: thumbnail__target_id
+          relationship: none
+          group_type: group
+          admin_label: ''
+          label: Thumbnail
+          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
+          click_sort_column: target_id
+          type: image
+          settings:
+            image_style: thumbnail
+            image_link: ''
+          group_column: ''
+          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: media
+          entity_field: thumbnail
+          plugin_id: field
+        name:
+          id: name
+          table: media_field_data
+          field: name
+          entity_type: media
+          entity_field: media
+          alter:
+            alter_text: false
+            make_link: false
+            absolute: false
+            trim: false
+            word_boundary: false
+            ellipsis: false
+            strip_tags: false
+            html: false
+          hide_empty: false
+          empty_zero: false
+          settings:
+            link_to_entity: true
+          plugin_id: field
+          relationship: none
+          group_type: group
+          admin_label: ''
+          label: 'Media name'
+          exclude: 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_alter_empty: true
+          click_sort_column: value
+          type: string
+          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
+        bundle:
+          id: bundle
+          table: media_field_data
+          field: bundle
+          relationship: none
+          group_type: group
+          admin_label: ''
+          label: Provider
+          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
+          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: media
+          entity_field: bundle
+          plugin_id: field
+        uid:
+          id: uid
+          table: media_field_data
+          field: uid
+          relationship: none
+          group_type: group
+          admin_label: ''
+          label: Author
+          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
+          click_sort_column: target_id
+          type: entity_reference_label
+          settings:
+            link: true
+          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: media
+          entity_field: uid
+          plugin_id: field
+        status:
+          id: status
+          table: media_field_data
+          field: status
+          relationship: none
+          group_type: group
+          admin_label: ''
+          label: Status
+          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
+          click_sort_column: value
+          type: boolean
+          settings:
+            format: custom
+            format_custom_true: Published
+            format_custom_false: Unpublished
+          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: media
+          entity_field: status
+          plugin_id: field
+        changed:
+          id: changed
+          table: media_field_data
+          field: changed
+          relationship: none
+          group_type: group
+          admin_label: ''
+          label: Updated
+          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
+          click_sort_column: value
+          type: timestamp
+          settings:
+            date_format: short
+            custom_date_format: ''
+            timezone: ''
+          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: media
+          entity_field: changed
+          plugin_id: field
+        operations:
+          id: operations
+          table: media
+          field: operations
+          relationship: none
+          group_type: group
+          admin_label: ''
+          label: Operations
+          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: true
+          entity_type: media
+          plugin_id: entity_operations
+      filters:
+        status:
+          id: status
+          table: media_field_data
+          field: status
+          relationship: none
+          group_type: group
+          admin_label: ''
+          operator: '='
+          value: '1'
+          group: 1
+          exposed: true
+          expose:
+            operator_id: ''
+            label: 'True'
+            description: null
+            use_operator: false
+            operator: status_op
+            identifier: status
+            required: true
+            remember: false
+            multiple: false
+            remember_roles:
+              authenticated: authenticated
+          is_grouped: true
+          group_info:
+            label: 'Publishing status'
+            description: ''
+            identifier: status
+            optional: true
+            widget: select
+            multiple: false
+            remember: false
+            default_group: All
+            default_group_multiple: {  }
+            group_items:
+              1:
+                title: Published
+                operator: '='
+                value: '1'
+              2:
+                title: Unpublished
+                operator: '='
+                value: '0'
+          plugin_id: boolean
+          entity_type: media
+          entity_field: status
+        bundle:
+          id: bundle
+          table: media_field_data
+          field: bundle
+          relationship: none
+          group_type: group
+          admin_label: ''
+          operator: in
+          value: {  }
+          group: 1
+          exposed: true
+          expose:
+            operator_id: bundle_op
+            label: Provider
+            description: ''
+            use_operator: false
+            operator: bundle_op
+            identifier: provider
+            required: false
+            remember: false
+            multiple: false
+            remember_roles:
+              authenticated: authenticated
+              anonymous: '0'
+              administrator: '0'
+            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: media
+          entity_field: bundle
+          plugin_id: bundle
+        name:
+          id: name
+          table: media_field_data
+          field: name
+          relationship: none
+          group_type: group
+          admin_label: ''
+          operator: contains
+          value: ''
+          group: 1
+          exposed: true
+          expose:
+            operator_id: name_op
+            label: 'Media name'
+            description: ''
+            use_operator: false
+            operator: name_op
+            identifier: name
+            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: media
+          entity_field: name
+          plugin_id: string
+        langcode:
+          id: langcode
+          table: media_field_data
+          field: langcode
+          relationship: none
+          group_type: group
+          admin_label: ''
+          operator: in
+          value: {  }
+          group: 1
+          exposed: true
+          expose:
+            operator_id: langcode_op
+            label: Language
+            description: ''
+            use_operator: false
+            operator: langcode_op
+            identifier: langcode
+            required: false
+            remember: false
+            multiple: false
+            remember_roles:
+              authenticated: authenticated
+              anonymous: '0'
+              administrator: '0'
+            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: media
+          entity_field: langcode
+          plugin_id: language
+      sorts:
+        created:
+          id: created
+          table: media_field_data
+          field: created
+          order: DESC
+          entity_type: media
+          entity_field: created
+          plugin_id: date
+          relationship: none
+          group_type: group
+          admin_label: ''
+          exposed: false
+          expose:
+            label: ''
+          granularity: second
+      title: Media
+      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 content available.'
+          plugin_id: text_custom
+      relationships: {  }
+      arguments: {  }
+      display_extenders: {  }
+    cache_metadata:
+      max-age: 0
+      contexts:
+        - 'languages:language_content'
+        - 'languages:language_interface'
+        - url
+        - url.query_args
+        - user.permissions
+      tags: {  }
+  media_page_list:
+    display_plugin: page
+    id: media_page_list
+    display_title: Media
+    position: 1
+    display_options:
+      display_extenders: {  }
+      path: admin/content/media
+      menu:
+        type: tab
+        title: Media
+        description: ''
+        expanded: false
+        parent: ''
+        weight: 0
+        context: '0'
+        menu_name: main
+      display_description: ''
+    cache_metadata:
+      max-age: 0
+      contexts:
+        - 'languages:language_content'
+        - 'languages:language_interface'
+        - url
+        - url.query_args
+        - user.permissions
+      tags: {  }
diff --git a/core/modules/media/config/schema/media.schema.yml b/core/modules/media/config/schema/media.schema.yml
new file mode 100644
index 0000000..3942a9f
--- /dev/null
+++ b/core/modules/media/config/schema/media.schema.yml
@@ -0,0 +1,68 @@
+media.settings:
+  type: config_object
+  label: 'Media settings'
+  mapping:
+    icon_base_uri:
+      type: string
+      label: 'Full URI to a folder where the media icons will be installed'
+
+media.type.*:
+  type: config_entity
+  label: 'Media type'
+  mapping:
+    id:
+      type: string
+      label: 'Machine name'
+    label:
+      type: label
+      label: 'Name'
+    description:
+      type: text
+      label: 'Description'
+    handler:
+      type: string
+      label: 'Handler'
+    handler_configuration:
+      type: media.handler.[%parent.handler]
+    queue_thumbnail_downloads:
+      type: boolean
+      label: 'Whether the thumbnail downloads should be queued'
+    new_revision:
+      type: boolean
+      label: 'Whether a new revision should be created by default'
+    field_map:
+      type: sequence
+      label: 'Field map'
+      sequence:
+        type: string
+
+action.configuration.media_delete_action:
+  type: action_configuration_default
+  label: 'Delete media configuration'
+
+action.configuration.media_save_action:
+  type: action_configuration_default
+  label: 'Save media configuration'
+
+action.configuration.media_publish_action:
+  type: action_configuration_default
+  label: 'Publish media configuration'
+
+action.configuration.media_unpublish_action:
+  type: action_configuration_default
+  label: 'Unpublish media configuration'
+
+field.formatter.settings.media_thumbnail:
+  type: field.formatter.settings.image
+  label: 'Media thumbnail field display format settings'
+
+media.handler.*:
+  type: mapping
+  label: 'Media handler settings'
+
+media.handler.field_aware:
+  type: mapping
+  mapping:
+    source_field:
+      type: string
+      label: 'Source field'
diff --git a/core/modules/media/config/schema/media.views.schema.yml b/core/modules/media/config/schema/media.views.schema.yml
new file mode 100644
index 0000000..0c7371d
--- /dev/null
+++ b/core/modules/media/config/schema/media.views.schema.yml
@@ -0,0 +1,5 @@
+# Schema for the views plugins of the Media module.
+
+views.field.media_bulk_form:
+  type: views_field_bulk_form
+  label: 'Media bulk form'
diff --git a/core/modules/media/images/icons/generic.png b/core/modules/media/images/icons/generic.png
new file mode 100644
index 0000000..062b154
--- /dev/null
+++ b/core/modules/media/images/icons/generic.png
@@ -0,0 +1,4 @@
+PNG
+
+   IHDR          Y  dIDATx0@ޠ(333333,[D߉y$hР4hI4hРA4hРA4hРAkۀ}6ņ訆Dllр6\C!{ޒvHmG5AQQ_.4"
+3Y3׋20zBf֐i"3s]Γ@*⋹vi"h>N-z\[%eY*hi=7bM`7[\GD	s}v۷ou=%RAђ{'|ˣD{*44hРAVoّ#]к<-kAjMuˣX./~4hйFVg*W+VͷV}}ۧ+2Ft*B}8Ճ[7v\gOL9I|hQo#ZjWE+ٻUzy:#񡋋/̤#DРANI4Et4hOǀˀ- }keks!#Ѡ7ȵǎ^n#;xk#8quW7#պ˓@`4hРA4hРA4hРA4hР7	+F*    IENDB`
\ No newline at end of file
diff --git a/core/modules/media/js/media_form.js b/core/modules/media/js/media_form.js
new file mode 100644
index 0000000..c93eac6
--- /dev/null
+++ b/core/modules/media/js/media_form.js
@@ -0,0 +1,40 @@
+/**
+ * @file
+ * Defines Javascript behaviors for the media form.
+ */
+
+(function ($, Drupal) {
+
+  'use strict';
+
+  /**
+   * Behaviors for summaries for tabs in the media edit form.
+   *
+   * @type {Drupal~behavior}
+   *
+   * @prop {Drupal~behaviorAttach} attach
+   *   Attaches summary behavior for tabs in the media edit form.
+   */
+  Drupal.behaviors.mediaFormSummaries = {
+    attach: function (context) {
+      var $context = $(context);
+
+      $context.find('.media-form-author').drupalSetSummary(function (context) {
+        var $authorContext = $(context);
+        var name = $authorContext.find('.field--name-uid input').val();
+        var date = $authorContext.find('.field--name-created input').val();
+
+        if (name && date) {
+          return Drupal.t('By @name on @date', {'@name': name, '@date': date});
+        }
+        else if (name) {
+          return Drupal.t('By @name', {'@name': name});
+        }
+        else if (date) {
+          return Drupal.t('Authored on @date', {'@date': date});
+        }
+      });
+    }
+  };
+
+})(jQuery, Drupal);
diff --git a/core/modules/media/js/media_type_form.js b/core/modules/media/js/media_type_form.js
new file mode 100644
index 0000000..a6d9051
--- /dev/null
+++ b/core/modules/media/js/media_type_form.js
@@ -0,0 +1,46 @@
+/**
+ * @file
+ * Javascript for the media type form.
+ */
+
+(function ($, Drupal) {
+
+  'use strict';
+
+  /**
+   * Behaviors for setting summaries on media type form.
+   *
+   * @type {Drupal~behavior}
+   *
+   * @prop {Drupal~behaviorAttach} attach
+   *   Attaches summary behaviors on media type edit forms.
+   */
+  Drupal.behaviors.mediaTypeFormSummaries = {
+    attach: function (context) {
+      var $context = $(context);
+      // Provide the vertical tab summaries.
+      $context.find('#edit-workflow').drupalSetSummary(function (context) {
+        var vals = [];
+        $(context).find('input[name^="options"]:checked').parent().each(function () {
+          vals.push(Drupal.checkPlain($(this).find('label').text()));
+        });
+        if (!$(context).find('#edit-options-status').is(':checked')) {
+          vals.unshift(Drupal.t('Not published'));
+        }
+        return vals.join(', ');
+      });
+      $(context).find('#edit-language').drupalSetSummary(function (context) {
+        var vals = [];
+
+        vals.push($(context).find('.js-form-item-language-configuration-langcode select option:selected').text());
+
+        $(context).find('input:checked').next('label').each(function () {
+          vals.push(Drupal.checkPlain($(this).text()));
+        });
+
+        return vals.join(', ');
+      });
+    }
+  };
+
+})(jQuery, Drupal);
diff --git a/core/modules/media/media.api.php b/core/modules/media/media.api.php
new file mode 100644
index 0000000..48ffe5d
--- /dev/null
+++ b/core/modules/media/media.api.php
@@ -0,0 +1,25 @@
+<?php
+
+/**
+ * @file
+ * Hooks related to Media and its plugins.
+ */
+
+/**
+ * @addtogroup hooks
+ * @{
+ */
+
+/**
+ * Alters the information provided in \Drupal\media\Annotation\MediaHandler.
+ *
+ * @param array $handlers
+ *   The array of media handler plugin definitions, keyed by plugin ID.
+ */
+function hook_media_handler_info_alter(array &$handlers) {
+  $handlers['youtube']['label'] = t('Youtube rocks!');
+}
+
+/**
+ * @} End of "addtogroup hooks".
+ */
diff --git a/core/modules/media/media.info.yml b/core/modules/media/media.info.yml
new file mode 100644
index 0000000..8926804
--- /dev/null
+++ b/core/modules/media/media.info.yml
@@ -0,0 +1,11 @@
+name: Media
+description: 'Allows for media to be created.'
+type: module
+package: Core
+version: VERSION
+core: 8.x
+hidden: true
+dependencies:
+  - file
+  - image
+  - user
diff --git a/core/modules/media/media.install b/core/modules/media/media.install
new file mode 100644
index 0000000..3d0e753
--- /dev/null
+++ b/core/modules/media/media.install
@@ -0,0 +1,57 @@
+<?php
+
+/**
+ * @file
+ * Install, uninstall and update hooks for Media module.
+ */
+
+/**
+ * Implements hook_install().
+ */
+function media_install() {
+  $source = drupal_get_path('module', 'media') . '/images/icons';
+  $destination = \Drupal::config('media.settings')->get('icon_base_uri');
+  file_prepare_directory($destination, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS);
+
+  $files = file_scan_directory($source, '/.*\.(svg|png|jpg|jpeg|gif)$/');
+  foreach ($files as $file) {
+    $result = file_unmanaged_copy($file->uri, $destination, FILE_EXISTS_REPLACE);
+  }
+}
+
+/**
+ * Implements hook_requirements().
+ */
+function media_requirements($phase) {
+  $requirements = [];
+  if ($phase == 'install') {
+    $source = drupal_get_path('module', 'media') . '/images/icons';
+    $destination = 'public://media-icons/generic';
+    file_prepare_directory($destination, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS);
+    $is_writable = is_writable($destination);
+    $is_directory = is_dir($destination);
+    $error = '';
+    if (!$is_writable || !$is_directory) {
+      if (!$is_directory) {
+        $error = t('The directory %directory does not exist.', ['%directory' => $destination]);
+      }
+      else {
+        $error = t('The directory %directory is not writable.', ['%directory' => $destination]);
+      }
+      $description = t('An automated attempt to create this directory failed, possibly due to a permissions problem. To proceed with the installation, either create the directory and modify its permissions manually or ensure that the installer has the permissions to create it automatically. For more information, see INSTALL.txt or the <a href=":handbook_url">online handbook</a>.', [':handbook_url' => 'https://www.drupal.org/server-permissions']);
+      if (!empty($error)) {
+        $description = [
+          '#type' => 'inline_template',
+          '#template' => '{{ error }} {{ description }}',
+          '#context' => [
+            'error' => $error,
+            'description' => $description,
+          ],
+        ];
+        $requirements['media']['description'] = $description;
+        $requirements['media']['severity'] = REQUIREMENT_ERROR;
+      }
+    }
+  }
+  return $requirements;
+}
diff --git a/core/modules/media/media.libraries.yml b/core/modules/media/media.libraries.yml
new file mode 100644
index 0000000..eecaf8e
--- /dev/null
+++ b/core/modules/media/media.libraries.yml
@@ -0,0 +1,13 @@
+media_form:
+  version: VERSION
+  js:
+    js/media_form.js: {}
+  dependencies:
+    - core/drupal.form
+
+media_type_form:
+  version: VERSION
+  js:
+    js/media_type_form.js: {}
+  dependencies:
+    - core/drupal.form
diff --git a/core/modules/media/media.links.action.yml b/core/modules/media/media.links.action.yml
new file mode 100644
index 0000000..a056d35
--- /dev/null
+++ b/core/modules/media/media.links.action.yml
@@ -0,0 +1,12 @@
+media.bundle_add:
+  route_name: entity.media_type.add_form
+  title: 'Add media type'
+  appears_on:
+    - entity.media_type.collection
+
+media.add:
+  route_name: entity.media.add_page
+  title: 'Add media'
+  weight: 10
+  appears_on:
+    - view.media.media_page_list
diff --git a/core/modules/media/media.links.contextual.yml b/core/modules/media/media.links.contextual.yml
new file mode 100644
index 0000000..1945ef5
--- /dev/null
+++ b/core/modules/media/media.links.contextual.yml
@@ -0,0 +1,10 @@
+entity.media.edit_form:
+  route_name: entity.media.edit_form
+  group: media
+  title: Edit
+
+entity.media.delete_form:
+  route_name: entity.media.delete_form
+  group: media
+  title: Delete
+  weight: 10
diff --git a/core/modules/media/media.links.menu.yml b/core/modules/media/media.links.menu.yml
new file mode 100644
index 0000000..33dd679
--- /dev/null
+++ b/core/modules/media/media.links.menu.yml
@@ -0,0 +1,6 @@
+entity.media_type.collection:
+  title: 'Media types'
+  parent: system.admin_structure
+  description: 'Manage media types.'
+  route_name: entity.media_type.collection
+
diff --git a/core/modules/media/media.links.task.yml b/core/modules/media/media.links.task.yml
new file mode 100644
index 0000000..c7a669c
--- /dev/null
+++ b/core/modules/media/media.links.task.yml
@@ -0,0 +1,25 @@
+entity.media.canonical:
+  title: View
+  route_name: entity.media.canonical
+  base_route: entity.media.canonical
+
+entity.media.edit_form:
+  title: Edit
+  route_name: entity.media.edit_form
+  base_route: entity.media.canonical
+
+entity.media.delete_form:
+  title: Delete
+  route_name: entity.media.delete_form
+  base_route: entity.media.canonical
+  weight: 10
+
+entity.media_type.edit_form:
+  title: Edit
+  route_name: entity.media_type.edit_form
+  base_route: entity.media_type.edit_form
+
+entity.media_type.collection:
+  title: List
+  route_name: entity.media_type.collection
+  base_route: entity.media_type.collection
diff --git a/core/modules/media/media.module b/core/modules/media/media.module
new file mode 100644
index 0000000..6bc7382
--- /dev/null
+++ b/core/modules/media/media.module
@@ -0,0 +1,116 @@
+<?php
+
+/**
+ * @file
+ * Provides media items.
+ */
+
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\field\FieldConfigInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Render\Element;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Url;
+
+/**
+ * Implements hook_help().
+ */
+function media_help($route_name, RouteMatchInterface $route_match) {
+  switch ($route_name) {
+    case 'help.page.media':
+      $output = '<h3>' . t('About') . '</h3>';
+      $output .= '<p>' . t('The Media module manages the creation, editing, deletion, settings and display of media. Items are typically images, documents, slideshows, YouTube videos, Tweets, Instagram photos, etc. You can reference media items from any other content on your site. For more information, see the <a href=":media">online documentation for the Media module</a>.', [':media' => 'https://www.drupal.org/docs/8/core/modules/media']) . '</p>';
+      $output .= '<h3>' . t('Uses') . '</h3>';
+      $output .= '<dl>';
+      $output .= '<dt>' . t('Creating media items') . '</dt>';
+      $output .= '<dd>' . t('When a new media item is created, the Media module records basic information about it, including the author, date of creation, and the <a href=":media-type">Media type</a>. It also manages the <em>publishing options</em>, which define whether or not the item is published. Default settings can be configured for each <a href=":media-type">type of media</a> on your site.', [':media-type' => Url::fromRoute('entity.media_type.collection')->toString()]) . '</dd>';
+      $output .= '<dt>' . t('Creating custom media types') . '</dt>';
+      $output .= '<dd>' . t('The Media module gives users with the <em>Administer media types</em> permission the ability to <a href=":media-new">create new media types</a> in addition to the default ones already configured. Each media type has an associated media handler (such as the image handler) which support thumbnail generation and metadata extraction. Fields managed by the <a href=":field">Field module</a> may be added for storing that metadata, such as width and height, as well as any other associated values.', [':media-new' => Url::fromRoute('entity.media_type.add_form')->toString(), ':field' => Url::fromRoute('help.page', ['name' => 'field'])->toString()]) . '</dd>';
+      $output .= '<dt>' . t('Creating revisions') . '</dt>';
+      $output .= '<dd>' . t('The Media module also enables you to create multiple versions of any media item, and revert to older versions using the <em>Revision information</em> settings.') . '</dd>';
+      $output .= '<dt>' . t('User permissions') . '</dt>';
+      $output .= '<dd>' . t('The Media module makes a number of permissions available, which can be set by role on the <a href=":permissions">permissions page</a>.', [':permissions' => Url::fromRoute('user.admin_permissions', [], ['fragment' => 'module-media'])->toString()]) . '</dd>';
+      $output .= '</dl>';
+      return $output;
+  }
+}
+
+/**
+ * Implements hook_theme().
+ */
+function media_theme() {
+  return [
+    'media' => [
+      'render element' => 'elements',
+    ],
+  ];
+}
+
+/**
+ * Implements hook_entity_operation_alter().
+ *
+ * Fix broken operations array in field UI for entities with restricted access.
+ *
+ * @todo: This hook can be removed when issue 2836384 is fixed.
+ * @see https://www.drupal.org/node/2836384
+ */
+function media_entity_operation_alter(array &$operations, EntityInterface $entity) {
+  if ($entity instanceof FieldConfigInterface && $entity->getTargetEntityTypeId() === 'media') {
+    /** @var \Drupal\media\MediaTypeInterface $media_type */
+    $media_type = \Drupal::entityTypeManager()->getStorage('media_type')->load($entity->getTargetBundle());
+    if ($entity->id() === 'media.' . $media_type->id() . '.' . $media_type->getHandler()->getConfiguration()['source_field']) {
+      unset($operations['delete']);
+    }
+  }
+}
+
+/**
+ * Implements hook_entity_access().
+ */
+function media_entity_access(EntityInterface $entity, $operation, AccountInterface $account) {
+  if ($operation === 'delete' && $entity instanceof FieldConfigInterface && $entity->getTargetEntityTypeId() === 'media') {
+    /** @var \Drupal\media\MediaTypeInterface $media_type */
+    $media_type = \Drupal::entityTypeManager()->getStorage('media_type')->load($entity->getTargetBundle());
+    return AccessResult::forbiddenIf($entity->id() === 'media.' . $media_type->id() . '.' . $media_type->getHandler()->getConfiguration()['source_field']);
+  }
+  return AccessResult::neutral();
+}
+
+/**
+ * Implements hook_theme_suggestions_HOOK().
+ */
+function media_theme_suggestions_media(array $variables) {
+  $suggestions = [];
+  $media = $variables['elements']['#media'];
+  $sanitized_view_mode = strtr($variables['elements']['#view_mode'], '.', '_');
+
+  $suggestions[] = 'media__' . $sanitized_view_mode;
+  $suggestions[] = 'media__' . $media->bundle();
+  $suggestions[] = 'media__' . $media->bundle() . '__' . $sanitized_view_mode;
+
+  return $suggestions;
+}
+
+/**
+ * Prepares variables for media templates.
+ *
+ * Default template: media.html.twig.
+ *
+ * @param array $variables
+ *   An associative array containing:
+ *   - elements: An array of elements to display in view mode.
+ *   - media: The media item.
+ *   - name: The label for the media item.
+ *   - view_mode: View mode; e.g., 'full', 'teaser', etc.
+ */
+function template_preprocess_media(array &$variables) {
+  $variables['media'] = $variables['elements']['#media'];
+  $variables['view_mode'] = $variables['elements']['#view_mode'];
+  $variables['name'] = $variables['media']->label();
+
+  // Helpful $content variable for templates.
+  foreach (Element::children($variables['elements']) as $key) {
+    $variables['content'][$key] = $variables['elements'][$key];
+  }
+}
diff --git a/core/modules/media/media.permissions.yml b/core/modules/media/media.permissions.yml
new file mode 100644
index 0000000..f0d41a3
--- /dev/null
+++ b/core/modules/media/media.permissions.yml
@@ -0,0 +1,26 @@
+administer media:
+  title: 'Administer media'
+  restrict access: TRUE
+
+administer media types:
+  title: 'Administer media types'
+  restrict access: TRUE
+
+view media:
+  title: 'View media'
+
+update media:
+  title: 'Update own media'
+
+update any media:
+  title: 'Update any media'
+
+delete media:
+  title:  'Delete own media'
+
+delete any media:
+  title:  'Delete any media'
+  restrict access: TRUE
+
+create media:
+  title: 'Create media'
diff --git a/core/modules/media/media.routing.yml b/core/modules/media/media.routing.yml
new file mode 100644
index 0000000..97ae2fd
--- /dev/null
+++ b/core/modules/media/media.routing.yml
@@ -0,0 +1,6 @@
+entity.media.multiple_delete_confirm:
+  path: '/admin/content/media/delete'
+  defaults:
+    _form: '\Drupal\media\Form\MediaDeleteMultipleConfirmForm'
+  requirements:
+    _permission: 'delete any media'
diff --git a/core/modules/media/media.services.yml b/core/modules/media/media.services.yml
new file mode 100644
index 0000000..730e2d8
--- /dev/null
+++ b/core/modules/media/media.services.yml
@@ -0,0 +1,8 @@
+services:
+  plugin.manager.media.handler:
+    class: Drupal\media\MediaHandlerManager
+    parent: default_plugin_manager
+
+  media.thumbnail_handler:
+    class: Drupal\media\MediaThumbnailHandler
+    arguments: ['@entity_type.manager', '@string_translation']
diff --git a/core/modules/media/media.tokens.inc b/core/modules/media/media.tokens.inc
new file mode 100644
index 0000000..f67909a
--- /dev/null
+++ b/core/modules/media/media.tokens.inc
@@ -0,0 +1,175 @@
+<?php
+
+/**
+ * @file
+ * Builds placeholder replacement tokens for media-related data.
+ */
+
+use Drupal\Core\Datetime\Entity\DateFormat;
+use Drupal\Core\Language\LanguageInterface;
+use Drupal\Core\Render\BubbleableMetadata;
+
+/**
+ * Implements hook_token_info().
+ */
+function media_token_info() {
+  $type = [
+    'name' => t('Media'),
+    'description' => t('Tokens related to individual media items.'),
+    'needs-data' => 'media',
+  ];
+
+  // Core tokens for media.
+  $media['mid'] = [
+    'name' => t('Media ID'),
+    'description' => t('The unique ID of the media item.'),
+  ];
+  $media['uuid'] = [
+    'name' => t('Media UUID'),
+    'description' => t('The unique UUID of the media item.'),
+  ];
+  $media['vid'] = [
+    'name' => t('Revision ID'),
+    'description' => t("'The unique ID of the media item's latest revision."),
+  ];
+  $media['bundle'] = [
+    'name' => t('Media type'),
+  ];
+  $media['bundle-name'] = [
+    'name' => t('Media type name'),
+    'description' => t('The human-readable name of the media type.'),
+  ];
+  $media['langcode'] = [
+    'name' => t('Language code'),
+    'description' => t('The language code of the language the media item is written in.'),
+  ];
+  $media['name'] = [
+    'name' => t('Name'),
+    'description' => t('The name of this media item.'),
+  ];
+  $media['author'] = [
+    'name' => t('Author'),
+    'type' => 'user',
+  ];
+  $media['url'] = [
+    'name' => t('URL'),
+    'description' => t('The URL of the media item.'),
+  ];
+  $media['edit-url'] = [
+    'name' => t('Edit URL'),
+    'description' => t("The URL of the media item's edit page."),
+  ];
+
+  // Chained tokens for media.
+  $media['created'] = [
+    'name' => t('Date created'),
+    'type' => 'date',
+  ];
+  $media['changed'] = [
+    'name' => t('Date changed'),
+    'description' => t('The date the media item was most recently updated.'),
+    'type' => 'date',
+  ];
+
+  return [
+    'types' => ['media' => $type],
+    'tokens' => ['media' => $media],
+  ];
+}
+
+/**
+ * Implements hook_tokens().
+ */
+function media_tokens($type, $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) {
+  $token_service = \Drupal::token();
+
+  $url_options = ['absolute' => TRUE];
+  if (isset($options['langcode'])) {
+    $url_options['language'] = \Drupal::languageManager()->getLanguage($options['langcode']);
+    $langcode = $options['langcode'];
+  }
+  else {
+    $langcode = LanguageInterface::LANGCODE_DEFAULT;
+  }
+
+  $replacements = [];
+  if ($type === 'media' && !empty($data['media'])) {
+    /** @var \Drupal\media\MediaInterface $media */
+    $media = \Drupal::service('entity.repository')->getTranslationFromContext($data['media'], $langcode, ['operation' => 'media_entity_tokens']);
+
+    foreach ($tokens as $name => $original) {
+      switch ($name) {
+        // Simple key values on the media_entity.
+        case 'mid':
+          $replacements[$original] = $media->id();
+          break;
+
+        case 'uuid':
+          $replacements[$original] = $media->uuid();
+          break;
+
+        case 'vid':
+          $replacements[$original] = $media->getRevisionId();
+          break;
+
+        case 'bundle':
+          $replacements[$original] = $media->bundle();
+          break;
+
+        case 'bundle-name':
+          $replacements[$original] = $media->bundle->entity->label();
+          break;
+
+        case 'langcode':
+          $replacements[$original] = $media->language()->getId();
+          break;
+
+        case 'name':
+          $replacements[$original] = $media->label();
+          break;
+
+        case 'url':
+          $replacements[$original] = $media->toUrl('canonical', $url_options);
+          break;
+
+        case 'edit-url':
+          $replacements[$original] = $media->toUrl('edit-form', $url_options);
+          break;
+
+        // Default values for the chained tokens handled below.
+        case 'author':
+          $account = $media->getOwner();
+          $bubbleable_metadata->addCacheableDependency($account);
+          $replacements[$original] = $account->label();
+          break;
+
+        case 'created':
+          $date_format = DateFormat::load('medium');
+          $bubbleable_metadata->addCacheableDependency($date_format);
+          $replacements[$original] = \Drupal::service('date.formatter')->format($media->getCreatedTime(), $date_format->id(), '', NULL, $langcode);
+          break;
+
+        case 'changed':
+          $date_format = DateFormat::load('medium');
+          $bubbleable_metadata->addCacheableDependency($date_format);
+          $replacements[$original] = \Drupal::service('date.formatter')->format($media->getChangedTime(), $date_format->id(), '', NULL, $langcode);
+          break;
+      }
+    }
+
+    if ($author_tokens = $token_service->findWithPrefix($tokens, 'author')) {
+      $account = $media->get('uid')->entity;
+      $replacements += $token_service->generate('user', $author_tokens, ['user' => $account], $options, $bubbleable_metadata);
+    }
+
+    if ($created_tokens = $token_service->findWithPrefix($tokens, 'created')) {
+      $replacements += $token_service->generate('date', $created_tokens, ['date' => $media->getCreatedTime()], $options, $bubbleable_metadata);
+    }
+
+    if ($changed_tokens = $token_service->findWithPrefix($tokens, 'changed')) {
+      $replacements += $token_service->generate('date', $changed_tokens, ['date' => $media->getChangedTime()], $options, $bubbleable_metadata);
+    }
+  }
+
+  return $replacements;
+}
diff --git a/core/modules/media/src/Annotation/MediaHandler.php b/core/modules/media/src/Annotation/MediaHandler.php
new file mode 100644
index 0000000..6286a73
--- /dev/null
+++ b/core/modules/media/src/Annotation/MediaHandler.php
@@ -0,0 +1,60 @@
+<?php
+
+namespace Drupal\media\Annotation;
+
+use Drupal\Component\Annotation\Plugin;
+
+/**
+ * Defines a media handler plugin annotation object.
+ *
+ * Media handlers are responsible for implementing all the logic for dealing
+ * with a particular type of media. They provide various universal and
+ * type-specific metadata about media of the type they handle.
+ *
+ * Plugin namespace: Plugin\media\Handler
+ *
+ * For a working example, see \Drupal\media\Plugin\media\Handler\File.
+ *
+ * @see \Drupal\media\MediaHandlerInterface
+ * @see \Drupal\media\MediaHandlerBase
+ * @see \Drupal\media\MediaHandlerManager
+ * @see hook_media_handler_info_alter()
+ * @see plugin_api
+ *
+ * @Annotation
+ */
+class MediaHandler extends Plugin {
+
+  /**
+   * The plugin ID.
+   *
+   * @var string
+   */
+  public $id;
+
+  /**
+   * The human-readable name of the media handler.
+   *
+   * @var \Drupal\Core\Annotation\Translation
+   *
+   * @ingroup plugin_translatable
+   */
+  public $label;
+
+  /**
+   * A brief description of the media handler.
+   *
+   * @var \Drupal\Core\Annotation\Translation
+   *
+   * @ingroup plugin_translatable
+   */
+  public $description = '';
+
+  /**
+   * The field types that can be used as a source field for this media handler.
+   *
+   * @var string[]
+   */
+  public $allowed_field_types = [];
+
+}
diff --git a/core/modules/media/src/Entity/Media.php b/core/modules/media/src/Entity/Media.php
new file mode 100644
index 0000000..dbf79d0
--- /dev/null
+++ b/core/modules/media/src/Entity/Media.php
@@ -0,0 +1,400 @@
+<?php
+
+namespace Drupal\media\Entity;
+
+use Drupal\Core\Entity\ContentEntityBase;
+use Drupal\Core\Entity\EntityPublishedTrait;
+use Drupal\Core\Entity\EntityStorageInterface;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Field\BaseFieldDefinition;
+use Drupal\media\MediaInterface;
+use Drupal\Core\Entity\EntityChangedTrait;
+use Drupal\user\UserInterface;
+
+/**
+ * Defines the media entity class.
+ *
+ * @todo Remove default/fallback entity form operation when #2006348 lands.
+ * @see https://www.drupal.org/node/2006348.
+ *
+ * @ContentEntityType(
+ *   id = "media",
+ *   label = @Translation("Media"),
+ *   label_singular = @Translation("media item"),
+ *   label_plural = @Translation("media items"),
+ *   label_count = @PluralTranslation(
+ *     singular = "@count media item",
+ *     plural = "@count media items"
+ *   ),
+ *   bundle_label = @Translation("Media type"),
+ *   handlers = {
+ *     "storage" = "Drupal\Core\Entity\Sql\SqlContentEntityStorage",
+ *     "view_builder" = "Drupal\Core\Entity\EntityViewBuilder",
+ *     "list_builder" = "Drupal\Core\Entity\EntityListBuilder",
+ *     "access" = "Drupal\media\MediaAccessControlHandler",
+ *     "form" = {
+ *       "default" = "Drupal\media\MediaForm",
+ *       "add" = "Drupal\media\MediaForm",
+ *       "edit" = "Drupal\media\MediaForm",
+ *       "delete" = "Drupal\Core\Entity\ContentEntityDeleteForm",
+ *     },
+ *     "translation" = "Drupal\content_translation\ContentTranslationHandler",
+ *     "views_data" = "Drupal\media\MediaViewsData",
+ *     "route_provider" = {
+ *       "html" = "Drupal\Core\Entity\Routing\AdminHtmlRouteProvider",
+ *     }
+ *   },
+ *   base_table = "media",
+ *   data_table = "media_field_data",
+ *   revision_table = "media_revision",
+ *   revision_data_table = "media_field_revision",
+ *   translatable = TRUE,
+ *   show_revision_ui = TRUE,
+ *   entity_keys = {
+ *     "id" = "mid",
+ *     "revision" = "vid",
+ *     "bundle" = "bundle",
+ *     "label" = "name",
+ *     "langcode" = "langcode",
+ *     "uuid" = "uuid",
+ *     "published" = "status",
+ *   },
+ *   bundle_entity_type = "media_type",
+ *   permission_granularity = "entity_type",
+ *   admin_permission = "administer media",
+ *   field_ui_base_route = "entity.media_type.edit_form",
+ *   common_reference_target = TRUE,
+ *   links = {
+ *     "add-page" = "/media/add",
+ *     "add-form" = "/media/add/{media_type}",
+ *     "canonical" = "/media/{media}",
+ *     "delete-form" = "/media/{media}/delete",
+ *     "edit-form" = "/media/{media}/edit",
+ *     "admin-form" = "/admin/structure/media/manage/{media_type}"
+ *   }
+ * )
+ */
+class Media extends ContentEntityBase implements MediaInterface {
+
+  use EntityChangedTrait;
+  use EntityPublishedTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCreatedTime() {
+    return $this->get('created')->value;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setCreatedTime($timestamp) {
+    $this->set('created', $timestamp);
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getOwner() {
+    return $this->get('uid')->entity;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setOwner(UserInterface $account) {
+    $this->set('uid', $account->id());
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getOwnerId() {
+    return $this->get('uid')->target_id;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setOwnerId($uid) {
+    $this->set('uid', $uid);
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getHandler() {
+    return $this->bundle->entity->getHandler();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function preSave(EntityStorageInterface $storage) {
+    parent::preSave($storage);
+
+    // Try to set fields provided by the media handler and mapped in
+    // media type config.
+    foreach ($this->bundle->entity->getFieldMap() as $source_field => $destination_field) {
+      // Only save value in entity field if empty. Do not overwrite existing
+      // data.
+      if ($this->hasField($destination_field) && $this->get($destination_field)->isEmpty() && ($value = $this->getHandler()->getField($this, $source_field))) {
+        $this->set($destination_field, $value);
+      }
+    }
+
+    // Try to set a default name for this media item if no label is provided.
+    if (!$this->label()) {
+      $this->set('name', $this->getHandler()->getDefaultName($this));
+    }
+
+    // Set thumbnail.
+    if ($this->getHandler()->shouldUpdateThumbnail($this)) {
+      \Drupal::service('media.thumbnail_handler')->updateThumbnail($this);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function postSave(EntityStorageInterface $storage, $update = TRUE) {
+    parent::postSave($storage, $update);
+    $is_new = !$update;
+    if ($this->bundle->entity->thumbnailDownloadsAreQueued() && $this->getHandler()->shouldUpdateThumbnail($this, $is_new)) {
+      $queue = \Drupal::queue('media_entity_thumbnail');
+      $queue->createItem(['id' => $this->id()]);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function preSaveRevision(EntityStorageInterface $storage, \stdClass $record) {
+    parent::preSaveRevision($storage, $record);
+
+    if (!$this->isNewRevision() && isset($this->original) && empty($record->revision_log)) {
+      // If we are updating an existing media item without adding a
+      // new revision, we need to make sure $entity->revision_log is reset
+      // whenever it is empty.
+      // Therefore, this code allows us to avoid clobbering an existing log
+      // entry with an empty one.
+      $record->revision_log = $this->original->revision_log->value;
+    }
+
+    if ($this->isNewRevision()) {
+      $record->revision_timestamp = self::getRequestTime();
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validate() {
+    $this->getHandler()->attachConstraints($this);
+    return parent::validate();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
+    $fields = parent::baseFieldDefinitions($entity_type);
+    $fields += static::publishedBaseFieldDefinitions($entity_type);
+
+    $fields['name'] = BaseFieldDefinition::create('string')
+      ->setLabel(t('Name'))
+      ->setRequired(TRUE)
+      ->setTranslatable(TRUE)
+      ->setRevisionable(TRUE)
+      ->setDefaultValue('')
+      ->setSetting('max_length', 255)
+      ->setDisplayOptions('form', [
+        'type' => 'string_textfield',
+        'weight' => -5,
+      ])
+      ->setDisplayConfigurable('form', TRUE)
+      ->setDisplayOptions('view', [
+        'label' => 'hidden',
+        'type' => 'string',
+        'weight' => -5,
+      ]);
+
+    $fields['thumbnail'] = BaseFieldDefinition::create('image')
+      ->setLabel(t('Thumbnail'))
+      ->setDescription(t('The thumbnail of the media item.'))
+      ->setRevisionable(TRUE)
+      ->setDisplayOptions('view', [
+        'type' => 'image',
+        'weight' => 5,
+        'label' => 'hidden',
+        'settings' => [
+          'image_style' => 'thumbnail',
+        ],
+      ])
+      ->setDisplayConfigurable('view', TRUE)
+      ->setReadOnly(TRUE);
+
+    $fields['uid'] = BaseFieldDefinition::create('entity_reference')
+      ->setLabel(t('Authored by'))
+      ->setDescription(t('The user ID of the author.'))
+      ->setRevisionable(TRUE)
+      ->setDefaultValueCallback(static::class . '::getCurrentUserId')
+      ->setSetting('target_type', 'user')
+      ->setTranslatable(TRUE)
+      ->setDisplayOptions('form', [
+        'type' => 'entity_reference_autocomplete',
+        'weight' => 5,
+        'settings' => [
+          'match_operator' => 'CONTAINS',
+          'size' => '60',
+          'autocomplete_type' => 'tags',
+          'placeholder' => '',
+        ],
+      ])
+      ->setDisplayConfigurable('form', TRUE)
+      ->setDisplayOptions('view', [
+        'label' => 'hidden',
+        'type' => 'author',
+        'weight' => 0,
+      ])
+      ->setDisplayConfigurable('view', TRUE);
+
+    $fields['created'] = BaseFieldDefinition::create('created')
+      ->setLabel(t('Authored on'))
+      ->setDescription(t('The time the media item was created.'))
+      ->setTranslatable(TRUE)
+      ->setRevisionable(TRUE)
+      ->setDefaultValueCallback(static::class . '::getRequestTime')
+      ->setDisplayOptions('form', [
+        'type' => 'datetime_timestamp',
+        'weight' => 10,
+      ])
+      ->setDisplayConfigurable('form', TRUE)
+      ->setDisplayOptions('view', [
+        'label' => 'hidden',
+        'type' => 'timestamp',
+        'weight' => 0,
+      ])
+      ->setDisplayConfigurable('view', TRUE);
+
+    $fields['changed'] = BaseFieldDefinition::create('changed')
+      ->setLabel(t('Changed'))
+      ->setDescription(t('The time the media item was last edited.'))
+      ->setTranslatable(TRUE)
+      ->setRevisionable(TRUE);
+
+    $fields['revision_timestamp'] = BaseFieldDefinition::create('created')
+      ->setLabel(t('Revision timestamp'))
+      ->setDescription(t('The time the current revision was created.'))
+      ->setQueryable(FALSE)
+      ->setRevisionable(TRUE);
+
+    $fields['revision_uid'] = BaseFieldDefinition::create('entity_reference')
+      ->setLabel(t('Revision authored by'))
+      ->setDescription(t('The user ID of the author of the current revision.'))
+      ->setDefaultValueCallback(static::class . '::getCurrentUserId')
+      ->setSetting('target_type', 'user')
+      ->setQueryable(FALSE)
+      ->setRevisionable(TRUE);
+
+    $fields['revision_log'] = BaseFieldDefinition::create('string_long')
+      ->setLabel(t('Revision Log'))
+      ->setDescription(t('The log entry explaining the changes in this revision.'))
+      ->setRevisionable(TRUE)
+      ->setTranslatable(TRUE)
+      ->setDefaultValue('')
+      ->setDisplayOptions('form', [
+        'type' => 'string_textarea',
+        'weight' => 25,
+        'settings' => [
+          'rows' => 4,
+        ],
+      ]);
+
+    return $fields;
+  }
+
+  /**
+   * Default value callback for 'uid' base field definition.
+   *
+   * @see ::baseFieldDefinitions()
+   *
+   * @return int[]
+   *   An array of default values.
+   */
+  public static function getCurrentUserId() {
+    return [\Drupal::currentUser()->id()];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getRequestTime() {
+    return \Drupal::time()->getRequestTime();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getRevisionCreationTime() {
+    return $this->revision_timestamp->value;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setRevisionCreationTime($timestamp) {
+    $this->revision_timestamp->value = $timestamp;
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getRevisionUser() {
+    return $this->revision_uid->entity;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setRevisionUser(UserInterface $account) {
+    $this->revision_uid->entity = $account;
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getRevisionUserId() {
+    return $this->revision_uid->target_id;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setRevisionUserId($user_id) {
+    $this->revision_uid->target_id = $user_id;
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getRevisionLogMessage() {
+    return $this->revision_log->value;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setRevisionLogMessage($revision_log_message) {
+    $this->revision_log->value = $revision_log_message;
+    return $this;
+  }
+
+}
diff --git a/core/modules/media/src/Entity/MediaType.php b/core/modules/media/src/Entity/MediaType.php
new file mode 100644
index 0000000..aca44b1
--- /dev/null
+++ b/core/modules/media/src/Entity/MediaType.php
@@ -0,0 +1,295 @@
+<?php
+
+namespace Drupal\media\Entity;
+
+use Drupal\Core\Config\Entity\ConfigEntityBundleBase;
+use Drupal\Core\Entity\EntityStorageInterface;
+use Drupal\Core\Entity\EntityWithPluginCollectionInterface;
+use Drupal\Core\Plugin\DefaultSingleLazyPluginCollection;
+use Drupal\field\FieldStorageConfigInterface;
+use Drupal\media\MediaTypeInterface;
+use Drupal\media\SourceFieldInterface;
+
+/**
+ * Defines the Media type configuration entity.
+ *
+ * @ConfigEntityType(
+ *   id = "media_type",
+ *   label = @Translation("Media type"),
+ *   label_singular = @Translation("media type"),
+ *   label_plural = @Translation("media types"),
+ *   label_count = @PluralTranslation(
+ *     singular = "@count media type",
+ *     plural = "@count media types"
+ *   ),
+ *   handlers = {
+ *     "form" = {
+ *       "add" = "Drupal\media\MediaTypeForm",
+ *       "edit" = "Drupal\media\MediaTypeForm",
+ *       "delete" = "Drupal\media\Form\MediaTypeDeleteConfirmForm"
+ *     },
+ *     "list_builder" = "Drupal\media\MediaTypeListBuilder",
+ *     "route_provider" = {
+ *       "html" = "Drupal\Core\Entity\Routing\DefaultHtmlRouteProvider",
+ *     }
+ *   },
+ *   admin_permission = "administer media types",
+ *   config_prefix = "type",
+ *   bundle_of = "media",
+ *   entity_keys = {
+ *     "id" = "id",
+ *     "label" = "label",
+ *     "status" = "status",
+ *   },
+ *   config_export = {
+ *     "id",
+ *     "label",
+ *     "description",
+ *     "handler",
+ *     "queue_thumbnail_downloads",
+ *     "new_revision",
+ *     "handler_configuration",
+ *     "field_map",
+ *     "status",
+ *   },
+ *   links = {
+ *     "add-form" = "/admin/structure/media/add",
+ *     "edit-form" = "/admin/structure/media/manage/{media_type}",
+ *     "delete-form" = "/admin/structure/media/manage/{media_type}/delete",
+ *     "collection" = "/admin/structure/media",
+ *   },
+ * )
+ */
+class MediaType extends ConfigEntityBundleBase implements MediaTypeInterface, EntityWithPluginCollectionInterface {
+
+  /**
+   * The machine name of this media type.
+   *
+   * @var string
+   */
+  protected $id;
+
+  /**
+   * The human-readable name of the media type.
+   *
+   * @var string
+   */
+  protected $label;
+
+  /**
+   * A brief description of this media type.
+   *
+   * @var string
+   */
+  protected $description;
+
+  /**
+   * The media handler ID.
+   *
+   * @var string
+   */
+  protected $handler;
+
+  /**
+   * Whether media items should be published by default.
+   *
+   * @var bool
+   */
+  protected $status = TRUE;
+
+  /**
+   * Whether thumbnail downloads are queued.
+   *
+   * @var bool
+   */
+  protected $queue_thumbnail_downloads = FALSE;
+
+  /**
+   * Default value of the 'Create new revision' checkbox of this media type.
+   *
+   * @var bool
+   */
+  protected $new_revision = FALSE;
+
+  /**
+   * The media handler configuration.
+   *
+   * @var array
+   */
+  protected $handler_configuration = [];
+
+  /**
+   * Lazy collection for the media handler.
+   *
+   * @var \Drupal\Core\Plugin\DefaultSingleLazyPluginCollection
+   */
+  protected $handlerPluginCollection;
+
+  /**
+   * Field map. Fields provided by type plugin to be stored as entity fields.
+   *
+   * @var array
+   */
+  protected $field_map = [];
+
+  /**
+   * Whether to auto-create source field.
+   *
+   * This is used internally when saving a media type.
+   *
+   * @var bool
+   */
+  protected $auto_create_source_field = FALSE;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getPluginCollections() {
+    return [
+      'handler_configuration' => $this->handlerPluginCollection(),
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDescription() {
+    return $this->description;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setDescription($description) {
+    $this->description = $description;
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function thumbnailDownloadsAreQueued() {
+    return $this->queue_thumbnail_downloads;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setQueueThumbnailDownloadsStatus($queue_thumbnail_downloads) {
+    $this->queue_thumbnail_downloads = $queue_thumbnail_downloads;
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getHandler() {
+    return $this->handlerPluginCollection()->get($this->handler);
+  }
+
+  /**
+   * Returns media handler lazy plugin collection.
+   *
+   * @return \Drupal\Core\Plugin\DefaultSingleLazyPluginCollection
+   *   The tag plugin collection.
+   */
+  protected function handlerPluginCollection() {
+    if (!$this->handlerPluginCollection) {
+      $this->handlerPluginCollection = new DefaultSingleLazyPluginCollection(\Drupal::service('plugin.manager.media.handler'), $this->handler, $this->handler_configuration);
+    }
+    return $this->handlerPluginCollection;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getStatus() {
+    return $this->status;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function shouldCreateNewRevision() {
+    return $this->new_revision;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setNewRevision($new_revision) {
+    $this->new_revision = $new_revision;
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function preSave(EntityStorageInterface $storage) {
+    // If the media handler uses a source field, we'll need to store
+    // its name before saving. We'd need to double-save if we did
+    // this in postSave().
+    $handler = $this->getHandler();
+    if ($this->auto_create_source_field && $handler instanceof SourceFieldInterface) {
+      $field_storage = $handler->getSourceField($this)->getFieldStorageDefinition();
+      // If the field storage is a new (unsaved) config entity, save it.
+      if ($field_storage instanceof FieldStorageConfigInterface && $field_storage->isNew()) {
+        $field_storage->save();
+      }
+    }
+
+    parent::preSave($storage);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function postSave(EntityStorageInterface $storage, $update = TRUE) {
+    parent::postSave($storage, $update);
+
+    // If the media handler is using a source field, we may need to save
+    // it if it's new. The field storage is guaranteed to exist already
+    // because preSave() took care of that.
+    $handler = $this->getHandler();
+    if ($this->auto_create_source_field && $handler instanceof SourceFieldInterface) {
+      $field = $handler->getSourceField($this);
+
+      // If the field is new, save it and add it to this media type's view
+      // and form displays.
+      if ($field->isNew()) {
+        // Ensure the field is saved correctly before adding it to the displays.
+        $field->save();
+
+        $entity_type = $field->getTargetEntityTypeId();
+        $media_type = $field->getTargetBundle();
+
+        if ($field->isDisplayConfigurable('form')) {
+          // Use the default widget and settings.
+          $component = \Drupal::service('plugin.manager.field.widget')
+            ->prepareConfiguration($field->getType(), []);
+
+          entity_get_form_display($entity_type, $media_type, 'default')
+            ->setComponent($field->getName(), $component)
+            ->save();
+        }
+        if ($field->isDisplayConfigurable('view')) {
+          // Use the default formatter and settings.
+          $component = \Drupal::service('plugin.manager.field.formatter')
+            ->prepareConfiguration($field->getType(), []);
+
+          entity_get_display($entity_type, $media_type, 'default')
+            ->setComponent($field->getName(), $component)
+            ->save();
+        }
+      }
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFieldMap() {
+    return $this->field_map;
+  }
+
+}
diff --git a/core/modules/media/src/Form/MediaDeleteMultipleConfirmForm.php b/core/modules/media/src/Form/MediaDeleteMultipleConfirmForm.php
new file mode 100644
index 0000000..c533289
--- /dev/null
+++ b/core/modules/media/src/Form/MediaDeleteMultipleConfirmForm.php
@@ -0,0 +1,207 @@
+<?php
+
+namespace Drupal\media\Form;
+
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Form\ConfirmFormBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Url;
+use Drupal\user\PrivateTempStoreFactory;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\HttpFoundation\RedirectResponse;
+
+/**
+ * Provides a confirmation form to delete media items.
+ */
+class MediaDeleteMultipleConfirmForm extends ConfirmFormBase {
+
+  /**
+   * The array of media items to delete.
+   *
+   * @var array
+   */
+  protected $mediaItems = [];
+
+  /**
+   * The tempstore factory.
+   *
+   * @var \Drupal\user\PrivateTempStoreFactory
+   */
+  protected $tempStoreFactory;
+
+  /**
+   * The entity storage.
+   *
+   * @var \Drupal\Core\Entity\EntityStorageInterface
+   */
+  protected $storage;
+
+  /**
+   * Constructs a MediaDeleteMultipleConfirmForm form object.
+   *
+   * @param \Drupal\user\PrivateTempStoreFactory $temp_store_factory
+   *   The tempstore factory.
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $manager
+   *   The entity manager.
+   */
+  public function __construct(PrivateTempStoreFactory $temp_store_factory, EntityTypeManagerInterface $manager) {
+    $this->tempStoreFactory = $temp_store_factory;
+    $this->storage = $manager->getStorage('media');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('user.private_tempstore'),
+      $container->get('entity_type.manager')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'media_multiple_delete_confirm';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getQuestion() {
+    return $this->formatPlural(count($this->mediaItems), 'Are you sure you want to delete this item?', 'Are you sure you want to delete these items?');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCancelUrl() {
+    // @todo Change to media library when #2834729 is done.
+    // https://www.drupal.org/node/2834729.
+    return new Url('system.admin_content');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getConfirmText() {
+    return $this->t('Delete');
+  }
+
+  /**
+   * {@inheritdoc}
+   *
+   * @todo: Change to trait or base class when #2843395 is done.
+   * @see https://www.drupal.org/node/2843395
+   */
+  public function buildForm(array $form, FormStateInterface $form_state) {
+    $this->mediaItems = $this->tempStoreFactory->get('media_multiple_delete_confirm')->get($this->currentUser()->id());
+    if (empty($this->mediaItems)) {
+      return new RedirectResponse($this->getCancelUrl()->setAbsolute()->toString());
+    }
+    /** @var \Drupal\media\MediaInterface[] $entities */
+    $entities = $this->storage->loadMultiple(array_keys($this->mediaItems));
+
+    $items = [];
+    foreach ($this->mediaItems as $id => $langcodes) {
+      foreach ($langcodes as $langcode) {
+        $entity = $entities[$id]->getTranslation($langcode);
+        $key = $id . ':' . $langcode;
+        $default_key = $id . ':' . $entity->getUntranslated()->language()->getId();
+
+        // If we have a translated entity we build a nested list of translations
+        // that will be deleted.
+        $languages = $entity->getTranslationLanguages();
+        if (count($languages) > 1 && $entity->isDefaultTranslation()) {
+          $names = [];
+          foreach ($languages as $translation_langcode => $language) {
+            $names[] = $language->getName();
+            unset($items[$id . ':' . $translation_langcode]);
+          }
+          $items[$default_key] = [
+            'label' => [
+              '#markup' => $this->t('@label (Original translation) - <em>The following translations will be deleted:</em>', ['@label' => $entity->label()]),
+            ],
+            'deleted_translations' => [
+              '#theme' => 'item_list',
+              '#items' => $names,
+            ],
+          ];
+        }
+        elseif (!isset($items[$default_key])) {
+          $items[$key] = $entity->label();
+        }
+      }
+    }
+
+    $form['entities'] = [
+      '#theme' => 'item_list',
+      '#items' => $items,
+    ];
+    return parent::buildForm($form, $form_state);
+  }
+
+  /**
+   * {@inheritdoc}
+   *
+   * @todo: Change to trait or base class when #2843395 is done.
+   * @see https://www.drupal.org/node/2843395
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    if ($form_state->getValue('confirm') && !empty($this->mediaItems)) {
+      $total_count = 0;
+      $delete_entities = [];
+      /** @var \Drupal\Core\Entity\ContentEntityInterface[][] $delete_translations */
+      $delete_translations = [];
+      /** @var \Drupal\media\MediaInterface[] $entities */
+      $entities = $this->storage->loadMultiple(array_keys($this->mediaItems));
+
+      foreach ($this->mediaItems as $id => $langcodes) {
+        foreach ($langcodes as $langcode) {
+          $entity = $entities[$id]->getTranslation($langcode);
+          if ($entity->isDefaultTranslation()) {
+            $delete_entities[$id] = $entity;
+            unset($delete_translations[$id]);
+            $total_count += count($entity->getTranslationLanguages());
+          }
+          elseif (!isset($delete_entities[$id])) {
+            $delete_translations[$id][] = $entity;
+          }
+        }
+      }
+
+      if ($delete_entities) {
+        $this->storage->delete($delete_entities);
+        $this->logger('media')->notice('Deleted @count media items.', ['@count' => count($delete_entities)]);
+      }
+
+      if ($delete_translations) {
+        $count = 0;
+        foreach ($delete_translations as $id => $translations) {
+          $entity = $entities[$id]->getUntranslated();
+          foreach ($translations as $translation) {
+            $entity->removeTranslation($translation->language()->getId());
+          }
+          $entity->save();
+          $count += count($translations);
+        }
+        if ($count) {
+          $total_count += $count;
+          $this->logger('media')->notice('Deleted @count media translations.', ['@count' => $count]);
+        }
+      }
+
+      if ($total_count) {
+        drupal_set_message($this->formatPlural($total_count, 'Deleted 1 media item.', 'Deleted @count media items.'));
+      }
+
+      $this->tempStoreFactory->get('media_multiple_delete_confirm')->delete(\Drupal::currentUser()->id());
+    }
+
+    // @todo Change to media library when #2834729 is done.
+    // https://www.drupal.org/node/2834729.
+    $form_state->setRedirect('system.admin_content');
+  }
+
+}
diff --git a/core/modules/media/src/Form/MediaTypeDeleteConfirmForm.php b/core/modules/media/src/Form/MediaTypeDeleteConfirmForm.php
new file mode 100644
index 0000000..e501d36
--- /dev/null
+++ b/core/modules/media/src/Form/MediaTypeDeleteConfirmForm.php
@@ -0,0 +1,68 @@
+<?php
+
+namespace Drupal\media\Form;
+
+use Drupal\Core\Entity\Query\QueryFactory;
+use Drupal\Core\Entity\EntityDeleteForm;
+use Drupal\Core\Form\FormStateInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Provides a form for media type deletion.
+ */
+class MediaTypeDeleteConfirmForm extends EntityDeleteForm {
+
+  /**
+   * The query factory to create entity queries.
+   *
+   * @var \Drupal\Core\Entity\Query\QueryFactory
+   */
+  protected $queryFactory;
+
+  /**
+   * Constructs a new MediaTypeDeleteConfirm object.
+   *
+   * @param \Drupal\Core\Entity\Query\QueryFactory $query_factory
+   *   The entity query object.
+   */
+  public function __construct(QueryFactory $query_factory) {
+    $this->queryFactory = $query_factory;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('entity.query')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state) {
+    $num_entities = $this->queryFactory->get('media')
+      ->condition('bundle', $this->entity->id())
+      ->count()
+      ->execute();
+    if ($num_entities) {
+      $form['#title'] = $this->getQuestion();
+      $form['description'] = [
+        '#type' => 'inline_template',
+        '#template' => '<p>{{ message }}</p>',
+        '#context' => [
+          'message' => $this->formatPlural($num_entities,
+            '%type is used by @count media item on your site. You can not remove this media type until you have removed all of the %type media items.',
+            '%type is used by @count media items on your site. You can not remove this media type until you have removed all of the %type media items.',
+            ['%type' => $this->entity->label()]),
+        ],
+      ];
+
+      return $form;
+    }
+
+    return parent::buildForm($form, $form_state);
+  }
+
+}
diff --git a/core/modules/media/src/MediaAccessControlHandler.php b/core/modules/media/src/MediaAccessControlHandler.php
new file mode 100644
index 0000000..874a4ec
--- /dev/null
+++ b/core/modules/media/src/MediaAccessControlHandler.php
@@ -0,0 +1,52 @@
+<?php
+
+namespace Drupal\media;
+
+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 the media entity.
+ */
+class MediaAccessControlHandler extends EntityAccessControlHandler {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) {
+    if ($account->hasPermission('administer media')) {
+      return AccessResult::allowed()->cachePerPermissions();
+    }
+
+    $is_owner = ($account->id() && $account->id() === $entity->getOwnerId());
+    switch ($operation) {
+      case 'view':
+        return AccessResult::allowedIf($account->hasPermission('view media') && $entity->isPublished())->cachePerPermissions()->addCacheableDependency($entity);
+
+      case 'update':
+        if ($account->hasPermission('update any media')) {
+          return AccessResult::allowed()->cachePerPermissions();
+        }
+        return AccessResult::allowedIf($account->hasPermission('update media') && $is_owner)->cachePerPermissions()->cachePerUser()->addCacheableDependency($entity);
+
+      case 'delete':
+        if ($account->hasPermission('delete any media')) {
+          return AccessResult::allowed()->cachePerPermissions();
+        }
+        return AccessResult::allowedIf($account->hasPermission('delete media') && $is_owner)->cachePerPermissions()->cachePerUser()->addCacheableDependency($entity);
+
+      default:
+        return AccessResult::neutral()->cachePerPermissions();
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) {
+    return AccessResult::allowedIfHasPermission($account, 'create media');
+  }
+
+}
diff --git a/core/modules/media/src/MediaForm.php b/core/modules/media/src/MediaForm.php
new file mode 100644
index 0000000..89c178f
--- /dev/null
+++ b/core/modules/media/src/MediaForm.php
@@ -0,0 +1,157 @@
+<?php
+
+namespace Drupal\media;
+
+use Drupal\Core\Entity\ContentEntityForm;
+use Drupal\Core\Form\FormStateInterface;
+
+/**
+ * Form controller for the media edit forms.
+ */
+class MediaForm extends ContentEntityForm {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function form(array $form, FormStateInterface $form_state) {
+    $form = parent::form($form, $form_state);
+    /** @var \Drupal\media\MediaTypeInterface $media_type */
+    $media_type = $this->entity->bundle->entity;
+
+    if ($this->operation === 'edit') {
+      $form['#title'] = $this->t('Edit %type_label @label', [
+        '%type_label' => $media_type->label(),
+        '@label' => $this->entity->label(),
+      ]);
+    }
+
+    // Media author information for administrators.
+    if (isset($form['uid']) || isset($form['created'])) {
+      $form['author'] = [
+        '#type' => 'details',
+        '#title' => $this->t('Authoring information'),
+        '#group' => 'advanced',
+        '#attributes' => [
+          'class' => ['media-form-author'],
+        ],
+        '#weight' => 90,
+        '#optional' => TRUE,
+      ];
+    }
+
+    if (isset($form['uid'])) {
+      $form['uid']['#group'] = 'author';
+    }
+
+    if (isset($form['created'])) {
+      $form['created']['#group'] = 'author';
+    }
+
+    $form['#attached']['library'][] = 'media/media_form';
+
+    $form['#entity_builders']['update_status'] = [$this, 'updateStatus'];
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function actions(array $form, FormStateInterface $form_state) {
+    $element = parent::actions($form, $form_state);
+    $media = $this->entity;
+
+    // Add a "Publish" button.
+    $element['publish'] = $element['submit'];
+    // If the "Publish" button is clicked, we want to update the status to
+    // "published".
+    $element['publish']['#published_status'] = TRUE;
+    $element['publish']['#dropbutton'] = 'save';
+    if ($media->isNew()) {
+      $element['publish']['#value'] = $this->t('Save and publish');
+    }
+    else {
+      $element['publish']['#value'] = $media->isPublished() ? $this->t('Save and keep published') : $this->t('Save and publish');
+    }
+    $element['publish']['#weight'] = 0;
+
+    // Add a "Unpublish" button.
+    $element['unpublish'] = $element['submit'];
+    // If the "Unpublish" button is clicked, we want to update the status to
+    // "unpublished".
+    $element['unpublish']['#published_status'] = FALSE;
+    $element['unpublish']['#dropbutton'] = 'save';
+    if ($media->isNew()) {
+      $element['unpublish']['#value'] = $this->t('Save as unpublished');
+    }
+    else {
+      $element['unpublish']['#value'] = !$media->isPublished() ? $this->t('Save and keep unpublished') : $this->t('Save and unpublish');
+    }
+    $element['unpublish']['#weight'] = 10;
+
+    // If already published, the 'publish' button is primary.
+    if ($media->isPublished()) {
+      unset($element['unpublish']['#button_type']);
+    }
+    // Otherwise, the 'unpublish' button is primary and should come first.
+    else {
+      unset($element['publish']['#button_type']);
+      $element['unpublish']['#weight'] = -10;
+    }
+
+    // Remove the "Save" button.
+    $element['submit']['#access'] = FALSE;
+
+    $element['delete']['#access'] = $media->access('delete');
+    $element['delete']['#weight'] = 100;
+
+    return $element;
+  }
+
+  /**
+   * Entity builder updating the media status with the submitted value.
+   *
+   * @param string $entity_type_id
+   *   The entity type identifier.
+   * @param \Drupal\media\MediaInterface $media
+   *   The media updated with the submitted values.
+   * @param array $form
+   *   The complete form array.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   *
+   * @see \Drupal\media\MediaForm::form()
+   */
+  public function updateStatus($entity_type_id, MediaInterface $media, array $form, FormStateInterface $form_state) {
+    $element = $form_state->getTriggeringElement();
+    if (!empty($element['#published_status'])) {
+      $media->setPublished();
+    }
+    else {
+      $media->setUnpublished();
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function save(array $form, FormStateInterface $form_state) {
+    $saved = parent::save($form, $form_state);
+    $context = ['@type' => $this->entity->bundle(), '%label' => $this->entity->label()];
+    $logger = $this->logger('media');
+    $t_args = ['@type' => $this->entity->bundle->entity->label(), '%label' => $this->entity->label()];
+
+    if ($saved === SAVED_NEW) {
+      $logger->notice('@type: added %label.', $context);
+      drupal_set_message($this->t('@type %label has been created.', $t_args));
+    }
+    else {
+      $logger->notice('@type: updated %label.', $context);
+      drupal_set_message($this->t('@type %label has been updated.', $t_args));
+    }
+
+    $form_state->setRedirectUrl($this->entity->toUrl('canonical'));
+    return $saved;
+  }
+
+}
diff --git a/core/modules/media/src/MediaHandlerBase.php b/core/modules/media/src/MediaHandlerBase.php
new file mode 100644
index 0000000..a52a1f1
--- /dev/null
+++ b/core/modules/media/src/MediaHandlerBase.php
@@ -0,0 +1,340 @@
+<?php
+
+namespace Drupal\media;
+
+use Drupal\Component\Utility\NestedArray;
+use Drupal\Core\Entity\EntityFieldManagerInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Field\FieldTypePluginManagerInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Plugin\PluginBase;
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Base implementation of media handler plugin.
+ */
+abstract class MediaHandlerBase extends PluginBase implements SourceFieldInterface, ContainerFactoryPluginInterface {
+
+  /**
+   * Plugin label.
+   *
+   * @var string
+   */
+  protected $label;
+
+  /**
+   * The entity type manager service.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * The entity field manager service.
+   *
+   * @var \Drupal\Core\Entity\EntityFieldManagerInterface
+   */
+  protected $entityFieldManager;
+
+  /**
+   * The config factory service.
+   *
+   * @var \Drupal\Core\Config\ConfigFactoryInterface
+   */
+  protected $configFactory;
+
+  /**
+   * The field type plugin manager service.
+   *
+   * @var \Drupal\Core\Field\FieldTypePluginManagerInterface
+   */
+  protected $fieldTypeManager;
+
+  /**
+   * Constructs a new class 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\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   Entity type manager service.
+   * @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
+   *   Entity field manager service.
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
+   *   The config factory service.
+   * @param \Drupal\Core\Field\FieldTypePluginManagerInterface $field_type_manager
+   *   The field type plugin manager service.
+   */
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $entity_field_manager, ConfigFactoryInterface $config_factory, FieldTypePluginManagerInterface $field_type_manager) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+    $this->entityTypeManager = $entity_type_manager;
+    $this->entityFieldManager = $entity_field_manager;
+    $this->configFactory = $config_factory;
+    $this->fieldTypeManager = $field_type_manager;
+    $this->setConfiguration($configuration);
+  }
+
+  /**
+   * {@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('entity_field.manager'),
+      $container->get('config.factory'),
+      $container->get('plugin.manager.field.field_type')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setConfiguration(array $configuration) {
+    $this->configuration = NestedArray::mergeDeep(
+      $this->defaultConfiguration(),
+      $configuration
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getConfiguration() {
+    return $this->configuration;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function defaultConfiguration() {
+    return [
+      'source_field' => '',
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function shouldUpdateThumbnail(MediaInterface $media, $is_new = FALSE) {
+    // @todo This should never be possible, fixing the auto_create_source_field stuff will likely also remove the need for this.
+    if (empty($this->configuration['source_field'])) {
+      return TRUE;
+    }
+
+    $source_field_name = $this->getSourceField($media->bundle->entity)->getName();
+
+    // Update thumbnail if we don't have a thumbnail yet, for new items,
+    // or if the value of the source field changed.
+    if (!$media->get('thumbnail')->entity || $is_new || (isset($media->original) && $media->get($source_field_name)->getValue() != $media->original->get($source_field_name)->getValue())) {
+      return TRUE;
+    }
+    return FALSE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDefaultThumbnail() {
+    return $this->configFactory->get('media.settings')->get('icon_base_uri') . '/generic.png';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function attachConstraints(MediaInterface $media) {
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function calculateDependencies() {
+    return [];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
+    // If there are existing fields to choose from, allow the user to reuse one.
+    $options = [];
+    foreach ($this->entityFieldManager->getFieldStorageDefinitions('media') as $field_name => $field) {
+      $allowed_type = in_array($field->getType(), $this->pluginDefinition['allowed_field_types'], TRUE);
+      if ($allowed_type && !$field->isBaseField()) {
+        $options[$field_name] = $field->getLabel();
+      }
+    }
+
+    $form['source_field'] = [
+      '#type' => 'select',
+      '#title' => $this->t('Field with source information'),
+      '#default_value' => $this->configuration['source_field'],
+      '#empty_option' => $this->t('- Create -'),
+      '#options' => $options,
+      '#description' => $this->t('Select the field that will store essential information about the media item. If "Create" is selected a new field will be automatically created.'),
+    ];
+
+    if (!$options && $form_state->get('operation') === 'add') {
+      $form['source_field']['#access'] = FALSE;
+      $field_definition = $this->fieldTypeManager->getDefinition(reset($this->pluginDefinition['allowed_field_types']));
+      $form['source_field_message'] = [
+        '#markup' => $this->t('%field_name field will be automatically created on this type to store the essential information about the media item.', [
+          '%field_name' => $field_definition['label'],
+        ]),
+      ];
+    }
+    elseif ($form_state->get('operation') === 'edit') {
+      $form['source_field']['#access'] = FALSE;
+      $fields = $this->entityFieldManager->getFieldDefinitions('media', $form_state->get('type')->id());
+      $form['source_field_message'] = [
+        '#markup' => $this->t('%field_name field is used to store the essential information about the media item.', [
+          '%field_name' => $fields[$this->configuration['source_field']]->getLabel(),
+        ]),
+      ];
+    }
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
+    foreach ($form_state->getValues() as $config_key => $config_value) {
+      if (isset($this->configuration[$config_key])) {
+        $this->configuration[$config_key] = $config_value;
+      }
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDefaultName(MediaInterface $media) {
+    return 'media:' . $media->bundle() . ':' . $media->uuid();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getSourceField(MediaTypeInterface $type, $retry = TRUE) {
+    // If we don't know the name of the source field, we definitely need to
+    // create it.
+    if (empty($this->configuration['source_field'])) {
+      return $this->createSourceField($type);
+    }
+    // Even if we do know the name of the source field, there is no guarantee
+    // that it already exists. So check for the field and create it if needed.
+    $field = $this->configuration['source_field'];
+    $fields = $this->entityFieldManager->getFieldDefinitions('media', $type->id());
+    return isset($fields[$field]) ? $fields[$field] : $this->createSourceField($type);
+  }
+
+  /**
+   * Creates the source field storage definition.
+   *
+   * By default, the first field type listed in the plugin definition's
+   * allowed_field_types array will be the generated field's type.
+   *
+   * @return \Drupal\field\FieldStorageConfigInterface
+   *   The unsaved field storage definition.
+   */
+  protected function createSourceFieldStorage() {
+    return $this->entityTypeManager
+      ->getStorage('field_storage_config')
+      ->create([
+        'entity_type' => 'media',
+        'field_name' => $this->getSourceFieldName(),
+        'type' => reset($this->pluginDefinition['allowed_field_types']),
+      ]);
+  }
+
+  /**
+   * Returns the source field storage definition.
+   *
+   * @return \Drupal\Core\Field\FieldStorageDefinitionInterface|null
+   *   The field storage definition or NULL if it doesn't exists.
+   */
+  protected function getSourceFieldStorage() {
+    // If we don't know the name of the source field, we can't get its storage.
+    if (empty($this->configuration['source_field'])) {
+      return NULL;
+    }
+    // Even if we do know the name of the source field, we cannot guarantee that
+    // its storage exists. So check for the storage.
+    $field = $this->configuration['source_field'];
+    $fields = $this->entityFieldManager->getFieldStorageDefinitions('media');
+    return isset($fields[$field]) ? $fields[$field] : NULL;
+  }
+
+  /**
+   * Creates the source field definition for a type.
+   *
+   * @param \Drupal\media\MediaTypeInterface $type
+   *   The media type.
+   *
+   * @return \Drupal\field\FieldConfigInterface
+   *   The unsaved field definition. The field storage definition, if new,
+   *   should also be unsaved.
+   */
+  protected function createSourceField(MediaTypeInterface $type) {
+    // Get the storage or create if it doesn't exists yet.
+    $storage = $this->getSourceFieldStorage();
+    if (is_null($storage)) {
+      $storage = $this->createSourceFieldStorage();
+      $this->configuration['source_field'] = $storage->getName();
+    }
+    return $this->entityTypeManager
+      ->getStorage('field_config')
+      ->create([
+        'field_storage' => $storage,
+        'bundle' => $type->id(),
+        'label' => $this->pluginDefinition['label'],
+        'required' => TRUE,
+      ]);
+  }
+
+  /**
+   * Determine the name of the source field.
+   *
+   * @return string
+   *   The source field name. If one is already stored in configuration, it is
+   *   returned. Otherwise, a new, unused one is generated.
+   */
+  protected function getSourceFieldName() {
+    if ($this->configuration['source_field']) {
+      return $this->configuration['source_field'];
+    }
+
+    $base_id = 'field_media_' . $this->getPluginId();
+    $tries = 0;
+    $storage = $this->entityTypeManager->getStorage('field_storage_config');
+
+    // Iterate at least once, until no field with the generated ID is found.
+    do {
+      $id = $base_id;
+      // If we've tried before, increment and append the suffix.
+      if ($tries) {
+        $id .= '_' . $tries;
+      }
+      $field = $storage->load('media.' . $id);
+      $tries++;
+    } while ($field);
+
+    return $id;
+  }
+
+}
diff --git a/core/modules/media/src/MediaHandlerInterface.php b/core/modules/media/src/MediaHandlerInterface.php
new file mode 100644
index 0000000..a910221
--- /dev/null
+++ b/core/modules/media/src/MediaHandlerInterface.php
@@ -0,0 +1,148 @@
+<?php
+
+namespace Drupal\media;
+
+use Drupal\Component\Plugin\ConfigurablePluginInterface;
+use Drupal\Component\Plugin\PluginInspectionInterface;
+use Drupal\Core\Plugin\PluginFormInterface;
+
+/**
+ * Defines the interface for media handler plugins.
+ *
+ * Media handlers are the essential part of the media type as they implement the
+ * knowledge and business logic related to a given sort of media. Media entity,
+ * without a handler, doesn't have any knowledge about media it represents.
+ *
+ * Each Media type needs exactly one handler. A single handler can be used on
+ * many media types.
+ *
+ * Examples of possible handlers are:
+ * - File: handles local files,
+ * - Image: handles local images,
+ * - oEmbed: handles resources that are exposed through the oEmbed standard,
+ * - YouTube: handles YouTube videos,
+ * - SoundCould: handles SoundCould audio,
+ * - Instagram: handles Instagram posts,
+ * - Twitter: handles Tweets,
+ * - ...
+ *
+ * Their responsibilities are:
+ * - Defining how media is represented (stored). Handlers are not responsible to
+ *   actually store the media. They only define how it is represented on a media
+ *   entity (usually using some kind of a field).
+ * - Providing thumbnails. Handlers that are responsible for remote media will
+ *   generally fetch the image from the 3rd party API and make it available for
+ *   the local usage. Handlers that represent local resources (such as images)
+ *   will usually use some locally provided image. Both will also fall back to
+ *   a pre-defined default thumbnail if everything else fails.
+ * - Validating a media entity before it is saved. Entity constraint system will
+ *   be used to ensure the valid structure of the media entity. Handlers that
+ *   represent remote media will usually ensure that the URL or and other
+ *   identifier is correct. Handlers that are responsible for local files could
+ *   ensure the mime type of the file is correct or something along those lines.
+ * - Providing a default value for the media name field. This will save users
+ *   from manually entering the name where it can be reliably set automatically.
+ *   Handlers for local files will generally use filename. Handlers for remote
+ *   resources might use something like a video/photo title or something similar
+ *   to that. Name can be always overridden by the user.
+ * - Providing metadata specific to the given media type. Remote media handlers
+ *   will generally get information available through the 3rd party API and make
+ *   it available to Drupal. Local media handlers can expose things such as EXIF
+ *   or ID3.
+ * - Handling mapping metadata to the media entity fields. Metadata that handler
+ *   exposes can be automatically mapped to the fields on the media entity.
+ *   Handler will be able to define how this is done.
+ *
+ * @see \Drupal\media\Annotation\MediaHandler
+ * @see \Drupal\media\MediaHandlerBase
+ * @see \Drupal\media\MediaHandlerManager
+ * @see \Drupal\media\MediaTypeInterface
+ * @see plugin_api
+ */
+interface MediaHandlerInterface extends PluginInspectionInterface, ConfigurablePluginInterface, PluginFormInterface {
+
+  /**
+   * Gets list of fields provided by this plugin.
+   *
+   * @return array
+   *   Associative array with field names as keys and arrays as values. Each
+   *   of those arrays should have the following values:
+   *   - label: human-readable label of the field
+   *   - field_type: (optional) entity field type that the field should be
+   *     mapped to by default. "string" will be assumed if omitted.
+   */
+  public function getProvidedFields();
+
+  /**
+   * Gets a media-related field/value.
+   *
+   * @param \Drupal\media\MediaInterface $media
+   *   A media item.
+   * @param string $name
+   *   Name of field to fetch.
+   *
+   * @return mixed|false
+   *   Field value or FALSE if data unavailable.
+   */
+  public function getField(MediaInterface $media, $name);
+
+  /**
+   * Attaches media handler-specific validation constraints to a media item.
+   *
+   * @param \Drupal\media\MediaInterface $media
+   *   A media item.
+   */
+  public function attachConstraints(MediaInterface $media);
+
+  /**
+   * Determines if the thumbnail should be updated for a media item.
+   *
+   * @param \Drupal\media\MediaInterface $media
+   *   A media item.
+   * @param bool $is_new
+   *   Specifies whether the media item is new.
+   *
+   * @return bool
+   *   TRUE if the thumbnail should be updated, FALSE otherwise.
+   */
+  public function shouldUpdateThumbnail(MediaInterface $media, $is_new = FALSE);
+
+  /**
+   * Gets thumbnail image.
+   *
+   * Media handler is responsible for returning file URI of the generic
+   * thumbnail if no other is available. This function should always return a
+   * valid file URI.
+   *
+   * @param \Drupal\media\MediaInterface $media
+   *   A media item.
+   *
+   * @return string
+   *   File URI of the thumbnail.
+   */
+  public function getThumbnail(MediaInterface $media);
+
+  /**
+   * Gets the default thumbnail image.
+   *
+   * @return string
+   *   File URI of the default thumbnail image.
+   */
+  public function getDefaultThumbnail();
+
+  /**
+   * Provide a default name for the media item.
+   *
+   * Plugins defining a media handler are suggested to override this method and
+   * provide a default name, to be used when there is no user-defined label
+   * available.
+   *
+   * @param \Drupal\media\MediaInterface $media
+   *   A media item.
+   *
+   * @return string
+   *   A string that should be used as default media name.
+   */
+  public function getDefaultName(MediaInterface $media);
+
+}
diff --git a/core/modules/media/src/MediaHandlerManager.php b/core/modules/media/src/MediaHandlerManager.php
new file mode 100644
index 0000000..045cf03
--- /dev/null
+++ b/core/modules/media/src/MediaHandlerManager.php
@@ -0,0 +1,33 @@
+<?php
+
+namespace Drupal\media;
+
+use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Plugin\DefaultPluginManager;
+use Drupal\media\Annotation\MediaHandler;
+
+/**
+ * Manages media handler plugins.
+ */
+class MediaHandlerManager extends DefaultPluginManager {
+
+  /**
+   * Constructs a new MediaHandlerManager.
+   *
+   * @param \Traversable $namespaces
+   *   An object that implements \Traversable which contains the root paths
+   *   keyed by the corresponding namespace to look for plugin implementations.
+   * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
+   *   Cache backend instance to use.
+   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
+   *   The module handler.
+   */
+  public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
+    parent::__construct('Plugin/media/Handler', $namespaces, $module_handler, MediaHandlerInterface::class, MediaHandler::class);
+
+    $this->alterInfo('media_handler_info');
+    $this->setCacheBackend($cache_backend, 'media_handler_plugins');
+  }
+
+}
diff --git a/core/modules/media/src/MediaInterface.php b/core/modules/media/src/MediaInterface.php
new file mode 100644
index 0000000..1055434
--- /dev/null
+++ b/core/modules/media/src/MediaInterface.php
@@ -0,0 +1,43 @@
+<?php
+
+namespace Drupal\media;
+
+use Drupal\Core\Entity\EntityChangedInterface;
+use Drupal\Core\Entity\ContentEntityInterface;
+use Drupal\Core\Entity\EntityPublishedInterface;
+use Drupal\Core\Entity\RevisionLogInterface;
+use Drupal\user\EntityOwnerInterface;
+
+/**
+ * Provides an interface defining a entity for media items.
+ */
+interface MediaInterface extends ContentEntityInterface, EntityChangedInterface, RevisionLogInterface, EntityOwnerInterface, EntityPublishedInterface {
+
+  /**
+   * Returns the media item creation timestamp.
+   *
+   * @return int
+   *   Creation timestamp of the media item.
+   */
+  public function getCreatedTime();
+
+  /**
+   * Sets the media item creation timestamp.
+   *
+   * @param int $timestamp
+   *   The media creation timestamp.
+   *
+   * @return \Drupal\media\MediaInterface
+   *   The called media item.
+   */
+  public function setCreatedTime($timestamp);
+
+  /**
+   * Returns the media handler.
+   *
+   * @return $this
+   *   The media handler.
+   */
+  public function getHandler();
+
+}
diff --git a/core/modules/media/src/MediaThumbnailHandler.php b/core/modules/media/src/MediaThumbnailHandler.php
new file mode 100644
index 0000000..247c1ca
--- /dev/null
+++ b/core/modules/media/src/MediaThumbnailHandler.php
@@ -0,0 +1,95 @@
+<?php
+
+namespace Drupal\media;
+
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\Core\StringTranslation\TranslationInterface;
+
+/**
+ * Provides a service for handling media item thumbnails.
+ */
+class MediaThumbnailHandler implements MediaThumbnailHandlerInterface {
+
+  use StringTranslationTrait;
+
+  /**
+   * The file entity storage handler.
+   *
+   * @var \Drupal\file\FileStorageInterface
+   */
+  protected $fileStorage;
+
+  /**
+   * Constructs a new MediaThumbnailHandler.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity type manager service.
+   * @param \Drupal\Core\StringTranslation\TranslationInterface $translation
+   *   The string translation service.
+   */
+  public function __construct(EntityTypeManagerInterface $entity_type_manager, TranslationInterface $translation) {
+    $this->fileStorage = $entity_type_manager->getStorage('file');
+    $this->stringTranslation = $translation;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function updateThumbnail(MediaInterface $media, $from_queue = FALSE) {
+    $thumbnail_uri = $this->getThumbnailUri($media, $from_queue);
+    $existing = $this->fileStorage->getQuery()
+      ->condition('uri', $thumbnail_uri)
+      ->execute();
+
+    if ($existing) {
+      $media->thumbnail->target_id = reset($existing);
+    }
+    else {
+      /** @var \Drupal\file\FileInterface $file */
+      $file = $this->fileStorage->create(['uri' => $thumbnail_uri]);
+      if ($owner = $media->getOwner()) {
+        $file->setOwner($owner);
+      }
+      $file->setPermanent();
+      $file->save();
+      $media->thumbnail->target_id = $file->id();
+    }
+
+    $media->thumbnail->alt = $this->t('Thumbnail');
+    $media->thumbnail->title = $media->label();
+
+    return $media;
+  }
+
+  /**
+   * Gets a file URI for a media item.
+   *
+   * If thumbnail fetching should be queued then temporary use default
+   * thumbnail for new files or temporary keep existing thumbnail for
+   * updates.
+   * Immediately fetch a new thumbnail from the media handler otherwise.
+   *
+   * @param \Drupal\media\MediaInterface $media
+   *   A media item.
+   * @param bool $from_queue
+   *   Specifies whether the thumbnail is being fetched from the queue.
+   *
+   * @return \Drupal\media\MediaInterface
+   *   The file URI for the thumbnail of the media item.
+   */
+  protected function getThumbnailUri(MediaInterface $media, $from_queue) {
+    if ($media->bundle->entity->thumbnailDownloadsAreQueued() && $media->isNew()) {
+      $thumbnail_uri = $media->getHandler()->getDefaultThumbnail();
+    }
+    elseif ($media->bundle->entity->thumbnailDownloadsAreQueued() && !$from_queue) {
+      $thumbnail_uri = $media->get('thumbnail')->entity->getFileUri();
+    }
+    else {
+      $thumbnail_uri = $media->getHandler()->getThumbnail($media);
+    }
+
+    return $thumbnail_uri;
+  }
+
+}
diff --git a/core/modules/media/src/MediaThumbnailHandlerInterface.php b/core/modules/media/src/MediaThumbnailHandlerInterface.php
new file mode 100644
index 0000000..01ecd9b
--- /dev/null
+++ b/core/modules/media/src/MediaThumbnailHandlerInterface.php
@@ -0,0 +1,23 @@
+<?php
+
+namespace Drupal\media;
+
+/**
+ * Provides an interface for a service handling media thumbnails.
+ */
+interface MediaThumbnailHandlerInterface {
+
+  /**
+   * Update the thumbnail for a media item.
+   *
+   * @param \Drupal\media\MediaInterface $media
+   *   A media item.
+   * @param bool $from_queue
+   *   Specifies whether the thumbnail is being fetched from the queue.
+   *
+   * @return \Drupal\media\MediaInterface
+   *   The changed media item.
+   */
+  public function updateThumbnail(MediaInterface $media, $from_queue = FALSE);
+
+}
diff --git a/core/modules/media/src/MediaTypeForm.php b/core/modules/media/src/MediaTypeForm.php
new file mode 100644
index 0000000..9210e96
--- /dev/null
+++ b/core/modules/media/src/MediaTypeForm.php
@@ -0,0 +1,334 @@
+<?php
+
+namespace Drupal\media;
+
+use Drupal\Core\Ajax\AjaxResponse;
+use Drupal\Core\Ajax\ReplaceCommand;
+use Drupal\Core\Entity\EntityFieldManagerInterface;
+use Drupal\Core\Entity\EntityForm;
+use Drupal\Core\Field\BaseFieldDefinition;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Form\SubformState;
+use Drupal\language\Entity\ContentLanguageSettings;
+use Drupal\media\Entity\MediaType;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Form controller for media type forms.
+ */
+class MediaTypeForm extends EntityForm {
+
+  /**
+   * Media handler plugin manager.
+   *
+   * @var \Drupal\media\MediaHandlerManager
+   */
+  protected $handlerManager;
+
+  /**
+   * Entity field manager service.
+   *
+   * @var \Drupal\Core\Entity\EntityFieldManagerInterface
+   */
+  protected $entityFieldManager;
+
+  /**
+   * Constructs a new class instance.
+   *
+   * @param \Drupal\media\MediaHandlerManager $handler_manager
+   *   Media handler plugin manager.
+   * @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
+   *   Entity field manager service.
+   */
+  public function __construct(MediaHandlerManager $handler_manager, EntityFieldManagerInterface $entity_field_manager) {
+    $this->handlerManager = $handler_manager;
+    $this->entityFieldManager = $entity_field_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('plugin.manager.media.handler'),
+      $container->get('entity_field.manager')
+    );
+  }
+
+  /**
+   * Ajax callback triggered by the type provider select element.
+   */
+  public function ajaxHandlerData(array $form, FormStateInterface $form_state) {
+    $response = new AjaxResponse();
+    $response->addCommand(new ReplaceCommand('#handler-dependent', $form['handler_dependent']));
+    return $response;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function form(array $form, FormStateInterface $form_state) {
+    $form = parent::form($form, $form_state);
+
+    // Handler is not set when the entity is initially created.
+    /** @var \Drupal\media\MediaHandlerInterface $handler */
+    $handler = $this->entity->get('handler') ? $this->entity->getHandler() : NULL;
+
+    if ($this->operation === 'add') {
+      $form['#title'] = $this->t('Add media type');
+    }
+
+    $form['label'] = [
+      '#title' => $this->t('Name'),
+      '#type' => 'textfield',
+      '#default_value' => $this->entity->label(),
+      '#description' => $this->t('The human-readable name of this media type.'),
+      '#required' => TRUE,
+      '#size' => 30,
+    ];
+
+    $form['id'] = [
+      '#type' => 'machine_name',
+      '#default_value' => $this->entity->id(),
+      '#maxlength' => 32,
+      '#disabled' => !$this->entity->isNew(),
+      '#machine_name' => [
+        'exists' => [MediaType::class, 'load'],
+      ],
+      '#description' => $this->t('A unique machine-readable name for this media type.'),
+    ];
+
+    $form['description'] = [
+      '#title' => $this->t('Description'),
+      '#type' => 'textarea',
+      '#default_value' => $this->entity->getDescription(),
+      '#description' => $this->t('Describe this media type. The text will be displayed on the <em>Add new media</em> page.'),
+    ];
+
+    $plugins = $this->handlerManager->getDefinitions();
+    $options = [];
+    foreach ($plugins as $plugin_id => $definition) {
+      $options[$plugin_id] = $definition['label'];
+    }
+
+    $form['handler_dependent'] = [
+      '#type' => 'container',
+      '#attributes' => ['id' => 'handler-dependent'],
+    ];
+
+    $form['handler_dependent']['handler'] = [
+      '#type' => 'select',
+      '#title' => $this->t('Media handler'),
+      '#default_value' => $handler ? $handler->getPluginId() : NULL,
+      '#options' => $options,
+      '#description' => $this->t('Media handler that is responsible for additional logic related to this media type.'),
+      '#ajax' => ['callback' => '::ajaxHandlerData'],
+      '#required' => TRUE,
+    ];
+
+    if (!$handler) {
+      $form['type']['#empty_option'] = $this->t('- Select media handler -');
+    }
+
+    if ($handler) {
+      // Media handler plugin configuration.
+      $form['handler_dependent']['handler_configuration'] = [
+        '#type' => 'fieldset',
+        '#title' => $this->t('Media handler configuration'),
+        '#tree' => TRUE,
+      ];
+
+      $form['handler_dependent']['handler_configuration'] = $handler->buildConfigurationForm($form['handler_dependent']['handler_configuration'], $this->getHandlerSubFormState($form, $form_state));
+    }
+
+    // Field mapping configuration.
+    $form['handler_dependent']['field_map'] = [
+      '#type' => 'fieldset',
+      '#title' => $this->t('Field mapping'),
+      '#tree' => TRUE,
+      'description' => [
+        '#markup' => '<p>' . $this->t('Media handlers can provide metadata fields such as title, caption, size information, credits, etc. Media can automatically save this metadata information to entity fields, which can be configured below. Information will only be mapped if the entity field is empty.') . '</p>',
+      ],
+    ];
+
+    if (empty($handler) || empty($handler->getProvidedFields())) {
+      $form['handler_dependent']['field_map']['#access'] = FALSE;
+    }
+    else {
+      $options = ['_none' => $this->t('- Skip field -')];
+      foreach ($this->entityFieldManager->getFieldDefinitions('media', $this->entity->id()) as $field_name => $field) {
+        if (!($field instanceof BaseFieldDefinition) || $field_name === 'name') {
+          $options[$field_name] = $field->getLabel();
+        }
+      }
+
+      $field_map = $this->entity->getFieldMap();
+      foreach ($handler->getProvidedFields() as $field_name => $field_definition) {
+        $form['handler_dependent']['field_map'][$field_name] = [
+          '#type' => 'select',
+          '#title' => $field_definition['label'],
+          '#options' => $options,
+          '#default_value' => isset($field_map[$field_name]) ? $field_map[$field_name] : '_none',
+        ];
+      }
+    }
+
+    $form['additional_settings'] = [
+      '#type' => 'vertical_tabs',
+      '#attached' => [
+        'library' => ['media/media_type_form'],
+      ],
+    ];
+
+    $form['workflow'] = [
+      '#type' => 'details',
+      '#title' => $this->t('Publishing options'),
+      '#group' => 'additional_settings',
+    ];
+
+    $form['workflow']['options'] = [
+      '#type' => 'checkboxes',
+      '#title' => $this->t('Default options'),
+      '#default_value' => $this->getWorkflowOptions(),
+      '#options' => [
+        'status' => $this->t('Published'),
+        'new_revision' => $this->t('Create new revision'),
+        'queue_thumbnail_downloads' => $this->t('Queue thumbnail downloads'),
+      ],
+    ];
+
+    $form['workflow']['options']['status']['#description'] = $this->t('Media will be automatically published when created.');
+    $form['workflow']['options']['new_revision']['#description'] = $this->t('Automatically create new revisions. Users with the Administer media permission will be able to override this option.');
+    $form['workflow']['options']['queue_thumbnail_downloads']['#description'] = $this->t('Download thumbnails via a queue.');
+
+    if ($this->moduleHandler->moduleExists('language')) {
+      $form['language'] = [
+        '#type' => 'details',
+        '#title' => $this->t('Language settings'),
+        '#group' => 'additional_settings',
+      ];
+
+      $language_configuration = ContentLanguageSettings::loadByEntityTypeBundle('media', $this->entity->id());
+      $form['language']['language_configuration'] = [
+        '#type' => 'language_configuration',
+        '#entity_information' => [
+          'entity_type' => 'media',
+          'bundle' => $this->entity->id(),
+        ],
+        '#default_value' => $language_configuration,
+      ];
+    }
+
+    return $form;
+  }
+
+  /**
+   * Prepares workflow options to be used in the 'checkboxes' form element.
+   *
+   * @return array
+   *   Array of options ready to be used in #options.
+   */
+  protected function getWorkflowOptions() {
+    $workflow_options = [
+      'status' => $this->entity->getStatus(),
+      'new_revision' => $this->entity->shouldCreateNewRevision(),
+      'queue_thumbnail_downloads' => $this->entity->thumbnailDownloadsAreQueued(),
+    ];
+    // Prepare workflow options to be used for 'checkboxes' form element.
+    $keys = array_keys(array_filter($workflow_options));
+    return array_combine($keys, $keys);
+  }
+
+  /**
+   * Gets subform state for the media handler configuration subform.
+   *
+   * @param array $form
+   *   Full form array.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   Parent form state.
+   *
+   * @return \Drupal\Core\Form\SubFormStateInterface
+   *   Sub-form state for the media handler configuration form.
+   */
+  protected function getHandlerSubFormState(array $form, FormStateInterface $form_state) {
+    return SubformState::createForSubform($form['handler_dependent']['handler_configuration'], $form, $form_state)
+      ->set('operation', $this->operation)
+      ->set('type', $this->entity);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateForm(array &$form, FormStateInterface $form_state) {
+    parent::validateForm($form, $form_state);
+
+    if ($form['handler_dependent']['handler_configuration']) {
+      // Let the selected plugin validate its settings.
+      $this->entity->getHandler()->validateConfigurationForm($form['handler_dependent']['handler_configuration'], $this->getHandlerSubFormState($form, $form_state));
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    $form_state->setValue('field_map', array_filter(
+      $form_state->getValue('field_map', []),
+      function ($item) {
+        return $item != '_none';
+      }
+    ));
+
+    parent::submitForm($form, $form_state);
+
+    $this->entity->setQueueThumbnailDownloadsStatus((bool) $form_state->getValue(['options', 'queue_thumbnail_downloads']));
+    $this->entity->setStatus((bool) $form_state->getValue(['options', 'status']));
+    $this->entity->setNewRevision((bool) $form_state->getValue(['options', 'new_revision']));
+
+    if ($form['handler_dependent']['handler_configuration']) {
+      // Let the selected plugin save its settings.
+      $this->entity->getHandler()->submitConfigurationForm($form['handler_dependent']['handler_configuration'], $this->getHandlerSubFormState($form, $form_state));
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function actions(array $form, FormStateInterface $form_state) {
+    $actions = parent::actions($form, $form_state);
+    $actions['submit']['#value'] = $this->t('Save');
+    $actions['delete']['#value'] = $this->t('Delete');
+    $actions['delete']['#access'] = $this->entity->access('delete');
+    return $actions;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function save(array $form, FormStateInterface $form_state) {
+    $this->entity->set('auto_create_source_field', TRUE);
+    $status = parent::save($form, $form_state);
+    $media_type = $this->entity;
+
+    $t_args = ['%name' => $media_type->label()];
+    if ($status === SAVED_UPDATED) {
+      drupal_set_message($this->t('The media type %name has been updated.', $t_args));
+    }
+    elseif ($status === SAVED_NEW) {
+      drupal_set_message($this->t('The media type %name has been added.', $t_args));
+      $this->logger('media')->notice('Added type %name.', $t_args);
+    }
+
+    // Override the "status" base field default value, for this media type.
+    $fields = $this->entityFieldManager->getFieldDefinitions('media', $media_type->id());
+    /** @var \Drupal\media\MediaInterface $media */
+    $media = $this->entityTypeManager->getStorage('media')->create(['bundle' => $media_type->id()]);
+    $value = (bool) $form_state->getValue(['options', 'status']);
+    if ($media->status->value != $value) {
+      $fields['status']->getConfig($media_type->id())->setDefaultValue($value)->save();
+    }
+
+    $form_state->setRedirectUrl($media_type->toUrl('collection'));
+  }
+
+}
diff --git a/core/modules/media/src/MediaTypeInterface.php b/core/modules/media/src/MediaTypeInterface.php
new file mode 100644
index 0000000..071440b
--- /dev/null
+++ b/core/modules/media/src/MediaTypeInterface.php
@@ -0,0 +1,86 @@
+<?php
+
+namespace Drupal\media;
+
+use Drupal\Core\Config\Entity\ConfigEntityInterface;
+use Drupal\Core\Entity\EntityDescriptionInterface;
+use Drupal\Core\Entity\RevisionableEntityBundleInterface;
+
+/**
+ * Provides an interface defining a media type entity.
+ *
+ * Media types are bundles for the media entity. They are used to group media
+ * with the same semantics. Media types are not about where media comes from.
+ * They are about the semantics that media has in the context of a given Drupal
+ * site.
+ *
+ * Media handlers, on the other hand, are aware where media comes from and know
+ * how to represent and handle it in Drupal's context. They are aware of the low
+ * level details, while the media types don't care about them at all. That said,
+ * media types can not exist without handlers.
+ *
+ * Consider the following examples:
+ * - oEmbed media handler which can represent any oEmbed resource. Media types
+ *   that could be used with this handler are "Videos", "Charts", "Music", etc.
+ *   All of them are retrieved using the same protocol, but they represent very
+ *   different things.
+ * - Handler that represents files could be used with media types like
+ *   "Invoices", "Subtitles", "Meeting notes", etc. They are all files stored on
+ *   some kind of storage, but their meaning is different.
+ *
+ * @see \Drupal\media\MediaHandlerInterface
+ */
+interface MediaTypeInterface extends ConfigEntityInterface, EntityDescriptionInterface, RevisionableEntityBundleInterface {
+
+  /**
+   * Returns whether thumbnail downloads are queued.
+   *
+   * @return bool
+   *   TRUE if thumbnails are queued for download later, FALSE if they should be
+   *   downloaded now.
+   */
+  public function thumbnailDownloadsAreQueued();
+
+  /**
+   * Sets a flag to indicate that thumbnails should be downloaded via a queue.
+   *
+   * @param bool $queue_thumbnail_downloads
+   *   The queue downloads flag.
+   *
+   * @return $this
+   */
+  public function setQueueThumbnailDownloadsStatus($queue_thumbnail_downloads);
+
+  /**
+   * Returns the media handler.
+   *
+   * @return \Drupal\media\MediaHandlerInterface
+   *   The media handler.
+   */
+  public function getHandler();
+
+  /**
+   * Sets whether new revisions should be created by default.
+   *
+   * @param bool $new_revision
+   *   TRUE if media items of this type should create new revisions by default.
+   *
+   * @return $this
+   */
+  public function setNewRevision($new_revision);
+
+  /**
+   * Returns the metadata field map.
+   *
+   * Field mapping allows site builders to map media item related metadata to
+   * entity fields. This information will be used when saving a given media item
+   * and if metadata values will be available they are going to be automatically
+   * copied to the corresponding entity fields.
+   *
+   * @return array
+   *   Field mapping array with fields provided by the type plugin as keys and
+   *   Drupal Entity fields as values.
+   */
+  public function getFieldMap();
+
+}
diff --git a/core/modules/media/src/MediaTypeListBuilder.php b/core/modules/media/src/MediaTypeListBuilder.php
new file mode 100644
index 0000000..815eeb8
--- /dev/null
+++ b/core/modules/media/src/MediaTypeListBuilder.php
@@ -0,0 +1,50 @@
+<?php
+
+namespace Drupal\media;
+
+use Drupal\Core\Config\Entity\ConfigEntityListBuilder;
+use Drupal\Core\Entity\EntityHandlerInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Url;
+
+/**
+ * Provides a listing of media types.
+ */
+class MediaTypeListBuilder extends ConfigEntityListBuilder implements EntityHandlerInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildHeader() {
+    $header['title'] = $this->t('Name');
+    $header['description'] = [
+      'data' => $this->t('Description'),
+      'class' => [RESPONSIVE_PRIORITY_MEDIUM],
+    ];
+    return $header + parent::buildHeader();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildRow(EntityInterface $entity) {
+    $row['title'] = [
+      'data' => $entity->label(),
+      'class' => ['menu-label'],
+    ];
+    $row['description']['data'] = ['#markup' => $entity->getDescription()];
+    return $row + parent::buildRow($entity);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function render() {
+    $build = parent::render();
+    $build['table']['#empty'] = $this->t('No media types available. <a href=":url">Add media type</a>.', [
+      ':url' => Url::fromRoute('entity.media_type.add_form')->toString(),
+    ]);
+    return $build;
+  }
+
+}
diff --git a/core/modules/media/src/MediaViewsData.php b/core/modules/media/src/MediaViewsData.php
new file mode 100644
index 0000000..f186871
--- /dev/null
+++ b/core/modules/media/src/MediaViewsData.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace Drupal\media;
+
+use Drupal\views\EntityViewsData;
+
+/**
+ * Provides the views data for the media entity type.
+ */
+class MediaViewsData extends EntityViewsData {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getViewsData() {
+    $data = parent::getViewsData();
+
+    $data['media_field_data']['table']['wizard_id'] = 'media';
+    $data['media_field_revision']['table']['wizard_id'] = 'media_revision';
+
+    return $data;
+  }
+
+}
diff --git a/core/modules/media/src/Plugin/Action/DeleteMedia.php b/core/modules/media/src/Plugin/Action/DeleteMedia.php
new file mode 100644
index 0000000..8d364fe
--- /dev/null
+++ b/core/modules/media/src/Plugin/Action/DeleteMedia.php
@@ -0,0 +1,98 @@
+<?php
+
+namespace Drupal\media\Plugin\Action;
+
+use Drupal\Core\Action\ActionBase;
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\user\PrivateTempStoreFactory;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Redirects to a media item deletion form.
+ *
+ * @Action(
+ *   id = "media_delete_action",
+ *   label = @Translation("Delete media"),
+ *   type = "media",
+ *   confirm_form_route_name = "entity.media.multiple_delete_confirm",
+ * )
+ */
+class DeleteMedia extends ActionBase implements ContainerFactoryPluginInterface {
+
+  /**
+   * The tempstore object.
+   *
+   * @var \Drupal\user\PrivateTempStoreFactory
+   */
+  protected $tempStore;
+
+  /**
+   * The current user.
+   *
+   * @var \Drupal\Core\Session\AccountInterface
+   */
+  protected $currentUser;
+
+  /**
+   * Constructs a new DeleteMedia 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\user\PrivateTempStoreFactory $temp_store_factory
+   *   The tempstore factory.
+   * @param \Drupal\Core\Session\AccountInterface $current_user
+   *   Current user.
+   */
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, PrivateTempStoreFactory $temp_store_factory, AccountInterface $current_user) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+    $this->currentUser = $current_user;
+    $this->tempStore = $temp_store_factory->get('media_multiple_delete_confirm');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $configuration,
+      $plugin_id,
+      $plugin_definition,
+      $container->get('user.private_tempstore'),
+      $container->get('current_user')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function executeMultiple(array $entities) {
+    $info = [];
+    /** @var \Drupal\media\MediaInterface $media */
+    foreach ($entities as $media) {
+      $langcode = $media->language()->getId();
+      $info[$media->id()][$langcode] = $langcode;
+    }
+    $this->tempStore->set($this->currentUser->id(), $info);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function execute($object = NULL) {
+    $this->executeMultiple($object ? [$object] : []);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) {
+    /** @var \Drupal\media\MediaInterface $object */
+    return $object->access('delete', $account, $return_as_object);
+  }
+
+}
diff --git a/core/modules/media/src/Plugin/Action/PublishMedia.php b/core/modules/media/src/Plugin/Action/PublishMedia.php
new file mode 100644
index 0000000..df19af4
--- /dev/null
+++ b/core/modules/media/src/Plugin/Action/PublishMedia.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace Drupal\media\Plugin\Action;
+
+use Drupal\Core\Action\ActionBase;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\media\Entity\Media;
+
+/**
+ * Publishes a media item.
+ *
+ * @Action(
+ *   id = "media_publish_action",
+ *   label = @Translation("Publish media"),
+ *   type = "media"
+ * )
+ */
+class PublishMedia extends ActionBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function execute(Media $entity = NULL) {
+    $entity->setPublished()->save();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) {
+    /** @var \Drupal\media\MediaInterface $object */
+    $result = $object->access('update', $account, TRUE)
+      ->andIf($object->status->access('update', $account, TRUE));
+
+    return $return_as_object ? $result : $result->isAllowed();
+  }
+
+}
diff --git a/core/modules/media/src/Plugin/Action/SaveMedia.php b/core/modules/media/src/Plugin/Action/SaveMedia.php
new file mode 100644
index 0000000..4a20360
--- /dev/null
+++ b/core/modules/media/src/Plugin/Action/SaveMedia.php
@@ -0,0 +1,37 @@
+<?php
+
+namespace Drupal\media\Plugin\Action;
+
+use Drupal\Core\Action\ActionBase;
+use Drupal\Core\Session\AccountInterface;
+
+/**
+ * Provides an action that can save any entity.
+ *
+ * @Action(
+ *   id = "media_save_action",
+ *   label = @Translation("Save media"),
+ *   type = "media"
+ * )
+ */
+class SaveMedia extends ActionBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function execute($entity = NULL) {
+    // We need to change at least one value, otherwise the changed timestamp
+    // will not be updated.
+    $entity->changed = 0;
+    $entity->save();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) {
+    /** @var \Drupal\media\MediaInterface $object */
+    return $object->access('update', $account, $return_as_object);
+  }
+
+}
diff --git a/core/modules/media/src/Plugin/Action/UnpublishMedia.php b/core/modules/media/src/Plugin/Action/UnpublishMedia.php
new file mode 100644
index 0000000..53cd471
--- /dev/null
+++ b/core/modules/media/src/Plugin/Action/UnpublishMedia.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace Drupal\media\Plugin\Action;
+
+use Drupal\Core\Action\ActionBase;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\media\Entity\Media;
+
+/**
+ * Unpublishes a media item.
+ *
+ * @Action(
+ *   id = "media_unpublish_action",
+ *   label = @Translation("Unpublish media"),
+ *   type = "media"
+ * )
+ */
+class UnpublishMedia extends ActionBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function execute(Media $entity = NULL) {
+    $entity->setUnpublished()->save();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) {
+    /** @var \Drupal\media\MediaInterface $object */
+    $result = $object->access('update', $account, TRUE)
+      ->andIf($object->status->access('update', $account, TRUE));
+
+    return $return_as_object ? $result : $result->isAllowed();
+  }
+
+}
diff --git a/core/modules/media/src/Plugin/Field/FieldFormatter/MediaThumbnailFormatter.php b/core/modules/media/src/Plugin/Field/FieldFormatter/MediaThumbnailFormatter.php
new file mode 100644
index 0000000..f9a9556
--- /dev/null
+++ b/core/modules/media/src/Plugin/Field/FieldFormatter/MediaThumbnailFormatter.php
@@ -0,0 +1,190 @@
+<?php
+
+namespace Drupal\media\Plugin\Field\FieldFormatter;
+
+use Drupal\Core\Field\FieldItemListInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\image\ImageStyleStorageInterface;
+use Drupal\image\Plugin\Field\FieldFormatter\ImageFormatter;
+use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem;
+use Drupal\Core\Render\RendererInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Drupal\Core\Field\FieldDefinitionInterface;
+
+/**
+ * Plugin implementation of the 'media_thumbnail' formatter.
+ *
+ * @FieldFormatter(
+ *   id = "media_thumbnail",
+ *   label = @Translation("Thumbnail"),
+ *   field_types = {
+ *     "entity_reference"
+ *   }
+ * )
+ */
+class MediaThumbnailFormatter extends ImageFormatter {
+
+  /**
+   * The renderer service.
+   *
+   * @var \Drupal\Core\Render\RendererInterface
+   */
+  protected $renderer;
+
+  /**
+   * Constructs an ImageFormatter object.
+   *
+   * @param string $plugin_id
+   *   The plugin_id for the formatter.
+   * @param mixed $plugin_definition
+   *   The plugin implementation definition.
+   * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
+   *   The definition of the field to which the formatter is associated.
+   * @param array $settings
+   *   The formatter settings.
+   * @param string $label
+   *   The formatter label display setting.
+   * @param string $view_mode
+   *   The view mode.
+   * @param array $third_party_settings
+   *   Any third party settings settings.
+   * @param \Drupal\Core\Session\AccountInterface $current_user
+   *   The current user.
+   * @param \Drupal\image\ImageStyleStorageInterface $image_style_storage
+   *   The image style entity storage handler.
+   * @param \Drupal\Core\Render\RendererInterface $renderer
+   *   The renderer service.
+   */
+  public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, $label, $view_mode, array $third_party_settings, AccountInterface $current_user, ImageStyleStorageInterface $image_style_storage, RendererInterface $renderer) {
+    parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $label, $view_mode, $third_party_settings, $current_user, $image_style_storage);
+    $this->renderer = $renderer;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $plugin_id,
+      $plugin_definition,
+      $configuration['field_definition'],
+      $configuration['settings'],
+      $configuration['label'],
+      $configuration['view_mode'],
+      $configuration['third_party_settings'],
+      $container->get('current_user'),
+      $container->get('entity_type.manager')->getStorage('image_style'),
+      $container->get('renderer')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   *
+   * This has to be overridden because FileFormatterBase expects $item to be
+   * of type \Drupal\file\Plugin\Field\FieldType\FileItem and calls
+   * isDisplayed() which is not in FieldItemInterface.
+   */
+  protected function needsEntityLoad(EntityReferenceItem $item) {
+    return !$item->hasNewEntity();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function settingsForm(array $form, FormStateInterface $form_state) {
+    $element = parent::settingsForm($form, $form_state);
+
+    $link_types = [
+      'content' => $this->t('Content'),
+      'media' => $this->t('Media entity'),
+    ];
+    $element['image_link']['#options'] = $link_types;
+
+    return $element;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function settingsSummary() {
+    $summary = parent::settingsSummary();
+
+    $link_types = [
+      'content' => $this->t('Linked to content'),
+      'media' => $this->t('Linked to media item'),
+    ];
+    // Display this setting only if image is linked.
+    $image_link_setting = $this->getSetting('image_link');
+    if (isset($link_types[$image_link_setting])) {
+      $summary[] = $link_types[$image_link_setting];
+    }
+
+    return $summary;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function viewElements(FieldItemListInterface $items, $langcode) {
+    $elements = [];
+    $media = $this->getEntitiesToView($items, $langcode);
+
+    // Early opt-out if the field is empty.
+    if (empty($media)) {
+      return $elements;
+    }
+
+    $url = NULL;
+    $image_link_setting = $this->getSetting('image_link');
+    // Check if the formatter involves a link.
+    if ($image_link_setting == 'content') {
+      $entity = $items->getEntity();
+      if (!$entity->isNew()) {
+        $url = $entity->toUrl();
+      }
+    }
+    elseif ($image_link_setting === 'media') {
+      $link_media = TRUE;
+    }
+
+    $image_style_setting = $this->getSetting('image_style');
+
+    /** @var \Drupal\media\MediaInterface[] $media */
+    foreach ($media as $delta => $media_item) {
+      if (isset($link_media)) {
+        $url = $media_item->toUrl();
+      }
+
+      $elements[$delta] = [
+        '#theme' => 'image_formatter',
+        '#item' => $media_item->get('thumbnail'),
+        '#item_attributes' => [],
+        '#image_style' => $image_style_setting,
+        '#url' => $url,
+      ];
+
+      // Add cacheability of each item in the field.
+      $this->renderer->addCacheableDependency($elements[$delta], $media_item);
+    }
+
+    // Add cacheability of the image style setting.
+    if ($image_link_setting && ($image_style = $this->imageStyleStorage->load($image_style_setting))) {
+      $this->renderer->addCacheableDependency($elements, $image_style);
+    }
+
+    return $elements;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function isApplicable(FieldDefinitionInterface $field_definition) {
+    // This formatter is only available for entity types that reference
+    // media items.
+    $target_type = $field_definition->getFieldStorageDefinition()->getSetting('target_type');
+    return $target_type == 'media';
+  }
+
+}
diff --git a/core/modules/media/src/Plugin/QueueWorker/ThumbnailDownloader.php b/core/modules/media/src/Plugin/QueueWorker/ThumbnailDownloader.php
new file mode 100644
index 0000000..84f78e3
--- /dev/null
+++ b/core/modules/media/src/Plugin/QueueWorker/ThumbnailDownloader.php
@@ -0,0 +1,70 @@
+<?php
+
+namespace Drupal\media\Plugin\QueueWorker;
+
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\media\Entity\Media;
+use Drupal\Core\Queue\QueueWorkerBase;
+use Drupal\media\MediaThumbnailHandlerInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Process a queue of media items to fetch their thumbnails.
+ *
+ * @QueueWorker(
+ *   id = "media_entity_thumbnail",
+ *   title = @Translation("Thumbnail downloader"),
+ *   cron = {"time" = 60}
+ * )
+ */
+class ThumbnailDownloader extends QueueWorkerBase implements ContainerFactoryPluginInterface {
+
+  /**
+   * The thumbnail handler service.
+   *
+   * @var \Drupal\media\MediaThumbnailHandlerInterface
+   */
+  protected $thumbnailHandler;
+
+  /**
+   * Constructs a new ThumbnailDownloader 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\media\MediaThumbnailHandlerInterface $thumbnail_handler
+   *   The thumbnail handler service.
+   */
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, MediaThumbnailHandlerInterface $thumbnail_handler) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+
+    $this->thumbnailHandler = $thumbnail_handler;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $configuration,
+      $plugin_id,
+      $plugin_definition,
+      $container->get('media.thumbnail_handler')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function processItem($data) {
+    /** @var \Drupal\media\MediaInterface $entity */
+    if ($entity = Media::load($data['id'])) {
+      $this->thumbnailHandler->updateThumbnail($entity, TRUE);
+      $entity->save();
+    }
+  }
+
+}
diff --git a/core/modules/media/src/Plugin/views/wizard/Media.php b/core/modules/media/src/Plugin/views/wizard/Media.php
new file mode 100644
index 0000000..e77c21e
--- /dev/null
+++ b/core/modules/media/src/Plugin/views/wizard/Media.php
@@ -0,0 +1,87 @@
+<?php
+
+namespace Drupal\media\Plugin\views\wizard;
+
+use Drupal\views\Plugin\views\wizard\WizardPluginBase;
+
+/**
+ * Provides views creation wizard for Media.
+ *
+ * @ViewsWizard(
+ *   id = "media",
+ *   base_table = "media_field_data",
+ *   title = @Translation("Media")
+ * )
+ */
+class Media extends WizardPluginBase {
+
+  /**
+   * Set the created column.
+   *
+   * @var string
+   */
+  protected $createdColumn = 'media_field_data-created';
+
+  /**
+   * Set default values for the filters.
+   *
+   * @var array
+   */
+  protected $filters = [
+    'status' => [
+      'value' => TRUE,
+      'table' => 'media_field_data',
+      'field' => 'status',
+      'plugin_id' => 'boolean',
+      'entity_type' => 'media',
+      'entity_field' => 'status',
+    ],
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getAvailableSorts() {
+    return [
+      'media_field_data-name:DESC' => $this->t('Media name'),
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function defaultDisplayOptions() {
+    $display_options = parent::defaultDisplayOptions();
+
+    // Add permission-based access control.
+    $display_options['access']['type'] = 'perm';
+    $display_options['access']['options']['perm'] = 'view media';
+
+    // Remove the default fields, since we are customizing them here.
+    unset($display_options['fields']);
+
+    // Add the name field, so that the display has content if the user switches
+    // to a row style that uses fields.
+    $display_options['fields']['name']['id'] = 'name';
+    $display_options['fields']['name']['table'] = 'media_field_data';
+    $display_options['fields']['name']['field'] = 'name';
+    $display_options['fields']['name']['entity_type'] = 'media';
+    $display_options['fields']['name']['entity_field'] = 'media';
+    $display_options['fields']['name']['label'] = '';
+    $display_options['fields']['name']['alter']['alter_text'] = 0;
+    $display_options['fields']['name']['alter']['make_link'] = 0;
+    $display_options['fields']['name']['alter']['absolute'] = 0;
+    $display_options['fields']['name']['alter']['trim'] = 0;
+    $display_options['fields']['name']['alter']['word_boundary'] = 0;
+    $display_options['fields']['name']['alter']['ellipsis'] = 0;
+    $display_options['fields']['name']['alter']['strip_tags'] = 0;
+    $display_options['fields']['name']['alter']['html'] = 0;
+    $display_options['fields']['name']['hide_empty'] = 0;
+    $display_options['fields']['name']['empty_zero'] = 0;
+    $display_options['fields']['name']['settings']['link_to_entity'] = 1;
+    $display_options['fields']['name']['plugin_id'] = 'field';
+
+    return $display_options;
+  }
+
+}
diff --git a/core/modules/media/src/Plugin/views/wizard/MediaRevision.php b/core/modules/media/src/Plugin/views/wizard/MediaRevision.php
new file mode 100644
index 0000000..e0c256f
--- /dev/null
+++ b/core/modules/media/src/Plugin/views/wizard/MediaRevision.php
@@ -0,0 +1,99 @@
+<?php
+
+namespace Drupal\media\Plugin\views\wizard;
+
+use Drupal\views\Plugin\views\wizard\WizardPluginBase;
+
+/**
+ * Provides views creation wizard for Media revisions.
+ *
+ * @ViewsWizard(
+ *   id = "media_revision",
+ *   base_table = "media_field_revision",
+ *   title = @Translation("Media revisions")
+ * )
+ */
+class MediaRevision extends WizardPluginBase {
+
+  /**
+   * Set the created column.
+   *
+   * @var string
+   */
+  protected $createdColumn = 'created';
+
+  /**
+   * Set default values for the filters.
+   *
+   * @var array
+   */
+  protected $filters = [
+    'status' => [
+      'value' => '1',
+      'table' => 'media_field_revision',
+      'field' => 'status',
+      'plugin_id' => 'boolean',
+      'entity_type' => 'media',
+      'entity_field' => 'status',
+    ],
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function defaultDisplayOptions() {
+    $display_options = parent::defaultDisplayOptions();
+
+    // Add permission-based access control.
+    $display_options['access']['type'] = 'perm';
+    $display_options['access']['options']['perm'] = 'view all revisions';
+
+    // Remove the default fields, since we are customizing them here.
+    unset($display_options['fields']);
+
+    // Add the changed field.
+    $display_options['fields']['changed']['id'] = 'changed';
+    $display_options['fields']['changed']['table'] = 'media_field_revision';
+    $display_options['fields']['changed']['field'] = 'changed';
+    $display_options['fields']['changed']['entity_type'] = 'media';
+    $display_options['fields']['changed']['entity_field'] = 'changed';
+    $display_options['fields']['changed']['alter']['alter_text'] = FALSE;
+    $display_options['fields']['changed']['alter']['make_link'] = FALSE;
+    $display_options['fields']['changed']['alter']['absolute'] = FALSE;
+    $display_options['fields']['changed']['alter']['trim'] = FALSE;
+    $display_options['fields']['changed']['alter']['word_boundary'] = FALSE;
+    $display_options['fields']['changed']['alter']['ellipsis'] = FALSE;
+    $display_options['fields']['changed']['alter']['strip_tags'] = FALSE;
+    $display_options['fields']['changed']['alter']['html'] = FALSE;
+    $display_options['fields']['changed']['hide_empty'] = FALSE;
+    $display_options['fields']['changed']['empty_zero'] = FALSE;
+    $display_options['fields']['changed']['plugin_id'] = 'field';
+    $display_options['fields']['changed']['type'] = 'timestamp';
+    $display_options['fields']['changed']['settings']['date_format'] = 'medium';
+    $display_options['fields']['changed']['settings']['custom_date_format'] = '';
+    $display_options['fields']['changed']['settings']['timezone'] = '';
+
+    // Add the name field.
+    $display_options['fields']['name']['id'] = 'name';
+    $display_options['fields']['name']['table'] = 'media_field_revision';
+    $display_options['fields']['name']['field'] = 'name';
+    $display_options['fields']['name']['entity_type'] = 'media';
+    $display_options['fields']['name']['entity_field'] = 'name';
+    $display_options['fields']['name']['label'] = '';
+    $display_options['fields']['name']['alter']['alter_text'] = 0;
+    $display_options['fields']['name']['alter']['make_link'] = 0;
+    $display_options['fields']['name']['alter']['absolute'] = 0;
+    $display_options['fields']['name']['alter']['trim'] = 0;
+    $display_options['fields']['name']['alter']['word_boundary'] = 0;
+    $display_options['fields']['name']['alter']['ellipsis'] = 0;
+    $display_options['fields']['name']['alter']['strip_tags'] = 0;
+    $display_options['fields']['name']['alter']['html'] = 0;
+    $display_options['fields']['name']['hide_empty'] = 0;
+    $display_options['fields']['name']['empty_zero'] = 0;
+    $display_options['fields']['name']['settings']['link_to_entity'] = 0;
+    $display_options['fields']['name']['plugin_id'] = 'field';
+
+    return $display_options;
+  }
+
+}
diff --git a/core/modules/media/src/SourceFieldInterface.php b/core/modules/media/src/SourceFieldInterface.php
new file mode 100644
index 0000000..73f184b
--- /dev/null
+++ b/core/modules/media/src/SourceFieldInterface.php
@@ -0,0 +1,21 @@
+<?php
+
+namespace Drupal\media;
+
+/**
+ * Interface for media handler plugins that depend on a field.
+ */
+interface SourceFieldInterface extends MediaHandlerInterface {
+
+  /**
+   * Returns the source field for a media type using this media handler.
+   *
+   * @param \Drupal\media\MediaTypeInterface $type
+   *   A media type.
+   *
+   * @return \Drupal\field\FieldConfigInterface
+   *   The source field definition.
+   */
+  public function getSourceField(MediaTypeInterface $type);
+
+}
diff --git a/core/modules/media/templates/media.html.twig b/core/modules/media/templates/media.html.twig
new file mode 100644
index 0000000..baa97c3
--- /dev/null
+++ b/core/modules/media/templates/media.html.twig
@@ -0,0 +1,48 @@
+{#
+/**
+ * @file
+ * Default theme implementation to present a media item.
+ *
+ * Available variables:
+ * - media: The media item with limited access to object properties and
+ *   methods. Only method names starting with "get", "has", or "is" and
+ *   a few common methods such as "id", "label", and "bundle" are available.
+ *   For example:
+ *   - entity.getEntityTypeId() will return the entity type ID.
+ *   - entity.hasField('field_example') returns TRUE if the entity includes
+ *     field_example. (This does not indicate the presence of a value in this
+ *     field.)
+ *   Calling other methods, such as entity.delete(), will result in
+ *   an exception.
+ *   See \Drupal\Core\Entity\EntityInterface for a full list of methods.
+ * - name: Name of the media item.
+ * - content: Media content.
+ * - title_prefix: Additional output populated by modules, intended to be
+ *   displayed in front of the main title tag that appears in the template.
+ * - title_suffix: Additional output populated by modules, intended to be
+ *   displayed after the main title tag that appears in the template.
+ * - view_mode: View mode; for example, "teaser" or "full".
+ * - attributes: HTML attributes for the containing element.
+ * - title_attributes: Same as attributes, except applied to the main title
+ *   tag that appears in the template.
+ *
+ * @see template_preprocess_media()
+ *
+ * @ingroup themeable
+ */
+#}
+<article{{ attributes }}>
+  {#
+    In the 'full' view mode the entity label is assumed to be displayed as the
+    page title, so we do not display it here.
+  #}
+  {{ title_prefix }}
+  {% if label and view_mode != 'full' %}
+    <h2{{ title_attributes }}>
+      {{ label }}
+    </h2>
+  {% endif %}
+  {{ title_suffix }}
+
+  {{ content }}
+</article>
diff --git a/core/modules/media/tests/modules/media_test_handler/config/schema/media_test_handler.schema.yml b/core/modules/media/tests/modules/media_test_handler/config/schema/media_test_handler.schema.yml
new file mode 100644
index 0000000..ac25367
--- /dev/null
+++ b/core/modules/media/tests/modules/media_test_handler/config/schema/media_test_handler.schema.yml
@@ -0,0 +1,7 @@
+media.handler.test:
+  type: media.handler.field_aware
+  label: 'Test media handler configuration'
+  mapping:
+    test_config_value:
+      type: string
+      label: 'Test config value'
diff --git a/core/modules/media/tests/modules/media_test_handler/media_test_handler.info.yml b/core/modules/media/tests/modules/media_test_handler/media_test_handler.info.yml
new file mode 100644
index 0000000..e29fe8c
--- /dev/null
+++ b/core/modules/media/tests/modules/media_test_handler/media_test_handler.info.yml
@@ -0,0 +1,6 @@
+name: 'Test media handler'
+type: module
+description: 'Provides test media handler to test configuration forms.'
+core: 8.x
+package: Testing
+version: VERSION
diff --git a/core/modules/media/tests/modules/media_test_handler/src/Plugin/media/Handler/Test.php b/core/modules/media/tests/modules/media_test_handler/src/Plugin/media/Handler/Test.php
new file mode 100644
index 0000000..7bdf5bb
--- /dev/null
+++ b/core/modules/media/tests/modules/media_test_handler/src/Plugin/media/Handler/Test.php
@@ -0,0 +1,73 @@
+<?php
+
+namespace Drupal\media_test_handler\Plugin\media\Handler;
+
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\media\MediaInterface;
+use Drupal\media\MediaHandlerBase;
+
+/**
+ * Provides generic media type.
+ *
+ * @MediaHandler(
+ *   id = "test",
+ *   label = @Translation("Test handler"),
+ *   description = @Translation("Test media handler."),
+ *   allowed_field_types = {"string"},
+ * )
+ */
+class Test extends MediaHandlerBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getThumbnail(MediaInterface $media) {
+    return DRUPAL_ROOT . '/core/misc/druplicon.png';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getProvidedFields() {
+    return [
+      'field_1' => [
+        'label' => $this->t('Field 1'),
+      ],
+      'field_2' => [
+        'label' => $this->t('Field 2'),
+      ],
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getField(MediaInterface $media, $name) {
+    return FALSE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function defaultConfiguration() {
+    return parent::defaultConfiguration() + [
+      'test_config_value' => 'This is default value.',
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
+    $form = parent::buildConfigurationForm($form, $form_state);
+
+    $form['test_config_value'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Test config value'),
+      '#default_value' => empty($this->configuration['test_config_value']) ? NULL : $this->configuration['test_config_value'],
+    ];
+
+    return $form;
+  }
+
+}
diff --git a/core/modules/media/tests/modules/media_test_type/config/install/media.type.test.yml b/core/modules/media/tests/modules/media_test_type/config/install/media.type.test.yml
new file mode 100644
index 0000000..3256baf
--- /dev/null
+++ b/core/modules/media/tests/modules/media_test_type/config/install/media.type.test.yml
@@ -0,0 +1,10 @@
+id: test
+label: 'Test type'
+description: 'Test type.'
+handler: test
+handler_configuration:
+  test_config_value: 'Kakec'
+status: true
+langcode: en
+dependencies: {  }
+field_map: {  }
diff --git a/core/modules/media/tests/modules/media_test_type/media_test_type.info.yml b/core/modules/media/tests/modules/media_test_type/media_test_type.info.yml
new file mode 100644
index 0000000..07877e3
--- /dev/null
+++ b/core/modules/media/tests/modules/media_test_type/media_test_type.info.yml
@@ -0,0 +1,8 @@
+name: 'Media test type'
+type: module
+description: 'Provides test type for a media item.'
+core: 8.x
+package: Testing
+version: VERSION
+dependencies:
+  - media_test_handler
diff --git a/core/modules/media/tests/modules/media_test_views/config/install/views.view.test_media_bulk_form.yml b/core/modules/media/tests/modules/media_test_views/config/install/views.view.test_media_bulk_form.yml
new file mode 100644
index 0000000..dd432bb
--- /dev/null
+++ b/core/modules/media/tests/modules/media_test_views/config/install/views.view.test_media_bulk_form.yml
@@ -0,0 +1,154 @@
+langcode: en
+status: true
+dependencies:
+  module:
+    - media
+    - user
+id: test_media_bulk_form
+label: ''
+module: views
+description: ''
+tag: ''
+base_table: media_field_data
+base_field: mid
+core: 8.x
+display:
+  default:
+    display_plugin: default
+    id: default
+    display_title: Master
+    position: 0
+    display_options:
+      style:
+        type: table
+      row:
+        type: fields
+      fields:
+        media_bulk_form:
+          id: media_bulk_form
+          table: media
+          field: media_bulk_form
+          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: 'With selection'
+          include_exclude: exclude
+          selected_actions: {  }
+          entity_type: media
+          plugin_id: media_bulk_form
+        name:
+          id: name
+          table: media_field_data
+          field: name
+          entity_type: media
+          entity_field: media
+          hide_empty: false
+          empty_zero: false
+          settings:
+            link_to_entity: false
+          plugin_id: field
+          relationship: none
+          group_type: group
+          admin_label: ''
+          label: 'Media name'
+          exclude: 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_alter_empty: true
+          click_sort_column: value
+          type: string
+          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
+        status:
+          id: status
+          table: media_field_data
+          field: status
+          relationship: none
+          group_type: group
+          admin_label: ''
+          label: Status
+          exclude: 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
+          click_sort_column: value
+          type: boolean
+          settings:
+            format: custom
+            format_custom_true: Published
+            format_custom_false: Unpublished
+          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: media
+          entity_field: status
+          plugin_id: field
+      sorts:
+        mid:
+          id: mid
+          table: media_field_data
+          field: mid
+          relationship: none
+          group_type: group
+          admin_label: ''
+          order: ASC
+          exposed: false
+          expose:
+            label: ''
+          entity_type: media
+          entity_field: mid
+          plugin_id: standard
+      title: 'Entity bulk form test view'
+      header: {  }
+      footer: {  }
+      empty: {  }
+      relationships: {  }
+      arguments: {  }
+      display_extenders: {  }
+  page_1:
+    display_plugin: page
+    id: page_1
+    display_title: Page
+    position: 1
+    display_options:
+      path: test-media-bulk-form
diff --git a/core/modules/media/tests/modules/media_test_views/media_test_views.info.yml b/core/modules/media/tests/modules/media_test_views/media_test_views.info.yml
new file mode 100644
index 0000000..4962319
--- /dev/null
+++ b/core/modules/media/tests/modules/media_test_views/media_test_views.info.yml
@@ -0,0 +1,9 @@
+name: 'Media test views'
+type: module
+description: 'Provides default views for views media tests.'
+package: Testing
+version: VERSION
+core: 8.x
+dependencies:
+  - media
+  - views
diff --git a/core/modules/media/tests/src/Functional/MediaAccessTest.php b/core/modules/media/tests/src/Functional/MediaAccessTest.php
new file mode 100644
index 0000000..bd8b600
--- /dev/null
+++ b/core/modules/media/tests/src/Functional/MediaAccessTest.php
@@ -0,0 +1,119 @@
+<?php
+
+namespace Drupal\Tests\media\Functional;
+
+use Drupal\media\Entity\Media;
+use Drupal\system\Tests\Cache\AssertPageCacheContextsAndTagsTrait;
+use Drupal\user\Entity\Role;
+use Drupal\user\RoleInterface;
+
+/**
+ * Basic access tests for Media.
+ *
+ * @group media
+ */
+class MediaAccessTest extends MediaFunctionalTestBase {
+
+  use AssertPageCacheContextsAndTagsTrait;
+
+  /**
+   * The test media type.
+   *
+   * @var \Drupal\media\MediaTypeInterface
+   */
+  protected $testMediaType;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+    $this->testMediaType = $this->createMediaType();
+  }
+
+  /**
+   * Test some access control functionality.
+   */
+  public function testMediaAccess() {
+    $assert_session = $this->assertSession();
+
+    // Create media.
+    $media = Media::create([
+      'bundle' => $this->testMediaType->id(),
+      'name' => 'Unnamed',
+    ]);
+    $media->save();
+    $user_media = Media::create([
+      'bundle' => $this->testMediaType->id(),
+      'name' => 'Unnamed',
+      'uid' => $this->nonAdminUser->id(),
+    ]);
+    $user_media->save();
+
+    // We are logged in as admin, so test 'administer media' permission.
+    $this->drupalGet('media/' . $user_media->id());
+    $this->assertCacheContext('user.permissions');
+    $assert_session->statusCodeEquals(200);
+    $this->drupalGet('media/' . $user_media->id() . '/edit');
+    $this->assertCacheContext('user.permissions');
+    $assert_session->statusCodeEquals(200);
+    $this->drupalGet('media/' . $user_media->id() . '/delete');
+    $this->assertCacheContext('user.permissions');
+    $assert_session->statusCodeEquals(200);
+
+    $this->drupalLogin($this->nonAdminUser);
+    /** @var \Drupal\user\RoleInterface $role */
+    $role = Role::load(RoleInterface::AUTHENTICATED_ID);
+
+    // Test 'view media' permission.
+    $this->drupalGet('media/' . $media->id());
+    $this->assertCacheContext('user.permissions');
+    $assert_session->statusCodeEquals(403);
+    $this->grantPermissions($role, ['view media']);
+    $this->drupalGet('media/' . $media->id());
+    $this->assertCacheContext('user.permissions');
+    $assert_session->statusCodeEquals(200);
+
+    // Test 'create media' permission.
+    $this->drupalGet('media/add/' . $this->testMediaType->id());
+    $this->assertCacheContext('user.permissions');
+    $assert_session->statusCodeEquals(403);
+    $this->grantPermissions($role, ['create media']);
+    $this->drupalGet('media/add/' . $this->testMediaType->id());
+    $this->assertCacheContext('user.permissions');
+    $assert_session->statusCodeEquals(200);
+
+    // Test 'update media' and 'delete media' permissions.
+    $this->drupalGet('media/' . $user_media->id() . '/edit');
+    $this->assertCacheContext('user');
+    $assert_session->statusCodeEquals(403);
+    $this->drupalGet('media/' . $user_media->id() . '/delete');
+    $this->assertCacheContext('user');
+    $assert_session->statusCodeEquals(403);
+    $this->grantPermissions($role, ['update media']);
+    $this->grantPermissions($role, ['delete media']);
+    $this->drupalGet('media/' . $user_media->id() . '/edit');
+    $this->assertCacheContext('user');
+    $assert_session->statusCodeEquals(200);
+    $this->drupalGet('media/' . $user_media->id() . '/delete');
+    $this->assertCacheContext('user');
+    $assert_session->statusCodeEquals(200);
+
+    // Test 'update any media' and 'delete any media' permissions.
+    $this->drupalGet('media/' . $media->id() . '/edit');
+    $this->assertCacheContext('user');
+    $assert_session->statusCodeEquals(403);
+    $this->drupalGet('media/' . $media->id() . '/delete');
+    $this->assertCacheContext('user');
+    $assert_session->statusCodeEquals(403);
+    $this->grantPermissions($role, ['update any media']);
+    $this->grantPermissions($role, ['delete any media']);
+    $this->drupalGet('media/' . $media->id() . '/edit');
+    $this->assertCacheContext('user.permissions');
+    $assert_session->statusCodeEquals(200);
+    $this->drupalGet('media/' . $media->id() . '/delete');
+    $this->assertCacheContext('user.permissions');
+    $assert_session->statusCodeEquals(200);
+  }
+
+}
diff --git a/core/modules/media/tests/src/Functional/MediaBulkFormTest.php b/core/modules/media/tests/src/Functional/MediaBulkFormTest.php
new file mode 100644
index 0000000..cb9e340
--- /dev/null
+++ b/core/modules/media/tests/src/Functional/MediaBulkFormTest.php
@@ -0,0 +1,113 @@
+<?php
+
+namespace Drupal\Tests\media\Functional;
+
+use Drupal\media\Entity\Media;
+use Drupal\views\Views;
+
+/**
+ * Tests a media bulk form.
+ *
+ * @group media
+ */
+class MediaBulkFormTest extends MediaFunctionalTestBase {
+
+  /**
+   * Modules to be enabled.
+   *
+   * @var array
+   */
+  public static $modules = ['media_test_views'];
+
+  /**
+   * The test media type.
+   *
+   * @var \Drupal\media\MediaTypeInterface
+   */
+  protected $testMediaType;
+
+  /**
+   * The test media items.
+   *
+   * @var \Drupal\media\MediaInterface[]
+   */
+  protected $mediaItems;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->testMediaType = $this->createMediaType();
+
+    // Create some test media items.
+    $this->mediaItems = [];
+    for ($i = 1; $i <= 5; $i++) {
+      $media = Media::create([
+        'bundle' => $this->testMediaType->id(),
+      ]);
+      $media->save();
+      $this->mediaItems[] = $media;
+    }
+  }
+
+  /**
+   * Tests the media bulk form.
+   */
+  public function testBulkForm() {
+    $session = $this->getSession();
+    $page = $session->getPage();
+    $assert_session = $this->assertSession();
+
+    // Check that all created items are present in the test view.
+    $view = Views::getView('test_media_bulk_form');
+    $view->execute();
+    $this->assertEquals($view->total_rows, 5);
+
+    // Check the operations are accessible to the logged in user.
+    $this->drupalGet('test-media-bulk-form');
+    // Current available actions: Delete, Save, Publish, Unpublish.
+    $available_actions = [
+      'media_delete_action',
+      'media_publish_action',
+      'media_save_action',
+      'media_unpublish_action',
+    ];
+    foreach ($available_actions as $action_name) {
+      $assert_session->optionExists('action', $action_name);
+    }
+
+    // Test unpublishing in bulk.
+    $page->checkField('media_bulk_form[0]');
+    $page->checkField('media_bulk_form[1]');
+    $page->checkField('media_bulk_form[2]');
+    $page->selectFieldOption('action', 'media_unpublish_action');
+    $page->pressButton('Apply to selected items');
+    $assert_session->pageTextContains('Unpublish media was applied to 3 items');
+    $this->assertFalse($this->storage->loadUnchanged(1)->isPublished(), 'The unpublish action failed in some of the media items.');
+    $this->assertFalse($this->storage->loadUnchanged(2)->isPublished(), 'The unpublish action failed in some of the media items.');
+    $this->assertFalse($this->storage->loadUnchanged(3)->isPublished(), 'The unpublish action failed in some of the media items.');
+
+    // Test publishing in bulk.
+    $page->checkField('media_bulk_form[0]');
+    $page->checkField('media_bulk_form[1]');
+    $page->selectFieldOption('action', 'media_publish_action');
+    $page->pressButton('Apply to selected items');
+    $assert_session->pageTextContains('Publish media was applied to 2 items');
+    $this->assertTrue($this->storage->loadUnchanged(1)->isPublished(), 'The publish action failed in some of the media items.');
+    $this->assertTrue($this->storage->loadUnchanged(2)->isPublished(), 'The publish action failed in some of the media items.');
+
+    // Test deletion in bulk.
+    $page->checkField('media_bulk_form[0]');
+    $page->checkField('media_bulk_form[1]');
+    $page->selectFieldOption('action', 'media_delete_action');
+    $page->pressButton('Apply to selected items');
+    $assert_session->pageTextContains('Are you sure you want to delete these items?');
+    $page->pressButton('Delete');
+    $assert_session->pageTextContains('Deleted 2 media items.');
+    $this->assertNull($this->storage->loadUnchanged(1), 'Could not delete some of the media items.');
+    $this->assertNull($this->storage->loadUnchanged(2), 'Could not delete some of the media items.');
+  }
+
+}
diff --git a/core/modules/media/tests/src/Functional/MediaCacheTagsTest.php b/core/modules/media/tests/src/Functional/MediaCacheTagsTest.php
new file mode 100644
index 0000000..3b673d4
--- /dev/null
+++ b/core/modules/media/tests/src/Functional/MediaCacheTagsTest.php
@@ -0,0 +1,86 @@
+<?php
+
+namespace Drupal\Tests\media\Functional;
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\media\Entity\Media;
+use Drupal\media\Entity\MediaType;
+use Drupal\system\Tests\Entity\EntityWithUriCacheTagsTestBase;
+use Drupal\user\Entity\Role;
+use Drupal\user\RoleInterface;
+
+/**
+ * Tests the media entity's cache tags.
+ *
+ * @group media
+ */
+class MediaCacheTagsTest extends EntityWithUriCacheTagsTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = [
+    'media',
+    'media_test_handler',
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    // Give anonymous users permission to view media, so that we can
+    // verify the cache tags of cached versions of media items.
+    $user_role = Role::load(RoleInterface::ANONYMOUS_ID);
+    $user_role->grantPermission('view media');
+    $user_role->save();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function createEntity() {
+    // Create a media type.
+    $id = strtolower($this->randomMachineName());
+    MediaType::create([
+      'id' => $id,
+      'label' => $id,
+      'handler' => 'test',
+      'handler_configuration' => [],
+      'field_map' => [],
+      'new_revision' => FALSE,
+      'auto_create_source_field' => FALSE,
+    ])->save();
+
+    // Create a media item.
+    $media = Media::create([
+      'bundle' => $id,
+      'name' => 'Unnamed',
+    ]);
+    $media->save();
+
+    return $media;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getAdditionalCacheContextsForEntity(EntityInterface $media) {
+    return ['timezone'];
+  }
+
+  /**
+   * {@inheritdoc}
+   *
+   * Each media item must have an author and a thumbnail.
+   */
+  protected function getAdditionalCacheTagsForEntity(EntityInterface $media) {
+    return [
+      'user:' . $media->getOwnerId(),
+      'config:image.style.thumbnail',
+      'file:' . $media->get('thumbnail')->entity->id(),
+    ];
+  }
+
+}
diff --git a/core/modules/media/tests/src/Functional/MediaFunctionalTestBase.php b/core/modules/media/tests/src/Functional/MediaFunctionalTestBase.php
new file mode 100644
index 0000000..0ea8118
--- /dev/null
+++ b/core/modules/media/tests/src/Functional/MediaFunctionalTestBase.php
@@ -0,0 +1,94 @@
+<?php
+
+namespace Drupal\Tests\media\Functional;
+
+use Drupal\Tests\BrowserTestBase;
+
+/**
+ * Base class for Media functional tests.
+ *
+ * @package Drupal\Tests\media\Functional
+ */
+abstract class MediaFunctionalTestBase extends BrowserTestBase {
+
+  use MediaFunctionalTestTrait;
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = [
+    'system',
+    'node',
+    'field_ui',
+    'views_ui',
+    'media',
+    'media_test_handler',
+  ];
+
+  /**
+   * Permissions for the admin user that will be logged in for test.
+   *
+   * @var array
+   */
+  protected static $adminUserPermissions = [
+    // Media entity permissions.
+    'administer media',
+    'administer media fields',
+    'administer media form display',
+    'administer media display',
+    'administer media types',
+    'view media',
+    'create media',
+    'update media',
+    'update any media',
+    'delete media',
+    'delete any media',
+    // Other permissions.
+    'administer views',
+    'access content overview',
+    'view all revisions',
+    'administer content types',
+    'administer node fields',
+    'administer node form display',
+    'bypass node access',
+  ];
+
+  /**
+   * An admin test user account.
+   *
+   * @var \Drupal\Core\Session\AccountInterface
+   */
+  protected $adminUser;
+
+  /**
+   * A non-admin test user account.
+   *
+   * @var \Drupal\Core\Session\AccountInterface
+   */
+  protected $nonAdminUser;
+
+  /**
+   * The storage handler.
+   *
+   * @var \Drupal\Core\Entity\EntityStorageInterface
+   */
+  protected $storage;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    // Have two users ready to be used in tests.
+    $this->adminUser = $this->drupalCreateUser(static::$adminUserPermissions);
+    $this->nonAdminUser = $this->drupalCreateUser([]);
+    // Start off logged in as admin.
+    $this->drupalLogin($this->adminUser);
+
+    $this->storage = $this->container->get('entity_type.manager')->getStorage('media');
+  }
+
+}
diff --git a/core/modules/media/tests/src/Functional/MediaFunctionalTestTrait.php b/core/modules/media/tests/src/Functional/MediaFunctionalTestTrait.php
new file mode 100644
index 0000000..5cd021e
--- /dev/null
+++ b/core/modules/media/tests/src/Functional/MediaFunctionalTestTrait.php
@@ -0,0 +1,49 @@
+<?php
+
+namespace Drupal\Tests\media\Functional;
+
+use Drupal\media\Entity\MediaType;
+
+/**
+ * Trait with helpers for Media functional tests.
+ */
+trait MediaFunctionalTestTrait {
+
+  /**
+   * Creates a media type.
+   *
+   * @param array $values
+   *   The media type values.
+   * @param string $handler
+   *   (optional) The media handler plugin that is responsible for additional
+   *   logic related to this media type.
+   *
+   * @return \Drupal\media\MediaTypeInterface
+   *   A newly created media type.
+   */
+  protected function createMediaType(array $values = [], $handler = 'test') {
+    if (!isset($values['bundle'])) {
+      $id = strtolower($this->randomMachineName());
+    }
+    else {
+      $id = $values['bundle'];
+    }
+    $values += [
+      'id' => $id,
+      'label' => $id,
+      'handler' => $handler,
+      'handler_configuration' => [],
+      'field_map' => [],
+      'new_revision' => FALSE,
+      'auto_create_source_field' => FALSE,
+    ];
+
+    $media_type = MediaType::create($values);
+    $status = $media_type->save();
+
+    $this->assertEquals(SAVED_NEW, $status, 'Media type was created successfully.');
+
+    return $media_type;
+  }
+
+}
diff --git a/core/modules/media/tests/src/Functional/MediaUiFunctionalTest.php b/core/modules/media/tests/src/Functional/MediaUiFunctionalTest.php
new file mode 100644
index 0000000..0bd91b0
--- /dev/null
+++ b/core/modules/media/tests/src/Functional/MediaUiFunctionalTest.php
@@ -0,0 +1,200 @@
+<?php
+
+namespace Drupal\Tests\media\Functional;
+
+use Drupal\media\Entity\Media;
+
+/**
+ * Ensures that media UI works correctly.
+ *
+ * @group media
+ */
+class MediaUiFunctionalTest extends MediaFunctionalTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = [
+    'block',
+    'media_test_handler',
+  ];
+
+  /**
+   * The test media type.
+   *
+   * @var \Drupal\media\MediaTypeInterface
+   */
+  protected $testMediaType;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+    $this->drupalPlaceBlock('local_actions_block');
+    $this->drupalPlaceBlock('local_tasks_block');
+  }
+
+  /**
+   * Tests the media actions (add/edit/delete).
+   */
+  public function testMediaWithOnlyOneMediaType() {
+    $session = $this->getSession();
+    $page = $session->getPage();
+    $assert_session = $this->assertSession();
+
+    $media_type = $this->createMediaType([
+      'new_revision' => FALSE,
+      'queue_thumbnail_downloads' => FALSE,
+    ]);
+
+    $this->drupalGet('media/add');
+    $assert_session->statusCodeEquals(200);
+    $assert_session->addressEquals('media/add/' . $media_type->id());
+    $assert_session->elementNotExists('css', '#edit-revision');
+
+    // Tests media add form.
+    $media_name = $this->randomMachineName();
+    $page->fillField('name[0][value]', $media_name);
+    $revision_log_message = $this->randomString();
+    $page->fillField('revision_log[0][value]', $revision_log_message);
+    $page->pressButton('Save and publish');
+    $media_id = $this->container->get('entity.query')->get('media')->execute();
+    $media_id = reset($media_id);
+    /** @var \Drupal\media\MediaInterface $media */
+    $media = $this->container->get('entity_type.manager')
+      ->getStorage('media')
+      ->loadUnchanged($media_id);
+    $this->assertEquals($media->getRevisionLogMessage(), $revision_log_message);
+    $assert_session->titleEquals($media->label() . ' | Drupal');
+
+    // Tests media edit form.
+    $media_type->setNewRevision(FALSE);
+    $media_type->save();
+    $media_name2 = $this->randomMachineName();
+    $this->drupalGet('media/' . $media_id . '/edit');
+    $assert_session->checkboxNotChecked('edit-revision');
+    $media_name = $this->randomMachineName();
+    $page->fillField('name[0][value]', $media_name2);
+    $page->pressButton('Save and keep published');
+    $assert_session->titleEquals($media_name2 . ' | Drupal');
+
+    // Test that there is no empty vertical tabs element, if the container is
+    // empty (see #2750697).
+    // Make the "Publisher ID" and "Created" fields hidden.
+    $this->drupalGet('/admin/structure/media/manage/' . $media_type->id() . '/form-display');
+    $page->selectFieldOption('fields[created][parent]', 'hidden');
+    $page->selectFieldOption('fields[uid][parent]', 'hidden');
+    $page->pressButton('Save');
+    // Assure we are testing with a user without permission to manage revisions.
+    $this->drupalLogin($this->nonAdminUser);
+    // Check the container is not present.
+    $this->drupalGet('media/' . $media_id . '/edit');
+    // An empty tab container would look like this.
+    $raw_html = '<div data-drupal-selector="edit-advanced" data-vertical-tabs-panes><input class="vertical-tabs__active-tab" data-drupal-selector="edit-advanced-active-tab" type="hidden" name="advanced__active_tab" value="" />' . "\n" . '</div>';
+    $assert_session->responseNotContains($raw_html);
+    // Continue testing as admin.
+    $this->drupalLogin($this->adminUser);
+
+    // Enable revisions by default.
+    $media_type->setNewRevision(TRUE);
+    $media_type->save();
+    $this->drupalGet('media/' . $media_id . '/edit');
+    $assert_session->checkboxChecked('edit-revision');
+    $page->fillField('name[0][value]', $media_name);
+    $page->fillField('revision_log[0][value]', $revision_log_message);
+    $page->pressButton('Save and keep published');
+    $assert_session->titleEquals($media_name . ' | Drupal');
+    /** @var \Drupal\media\MediaInterface $media */
+    $media = $this->container->get('entity_type.manager')
+      ->getStorage('media')
+      ->loadUnchanged($media_id);
+    $this->assertEquals($media->getRevisionLogMessage(), $revision_log_message);
+
+    // Tests media delete form.
+    $this->drupalGet('media/' . $media_id . '/edit');
+    $page->clickLink('Delete');
+    $assert_session->pageTextContains('This action cannot be undone');
+    $page->pressButton('Delete');
+    $media_id = \Drupal::entityQuery('media')->execute();
+    $this->assertFalse($media_id);
+  }
+
+  /**
+   * Tests the "media/add" and "media/mid" pages.
+   *
+   * Tests if the "media/add" page gives you a selecting option if there are
+   * multiple media types available.
+   */
+  public function testMediaWithMultipleMediaTypes() {
+    $assert_session = $this->assertSession();
+
+    // Tests and creates the first media type.
+    $first_media_type = $this->createMediaType(['description' => $this->randomMachineName(32)]);
+
+    // Test and create a second media type.
+    $second_media_type = $this->createMediaType(['description' => $this->randomMachineName(32)]);
+
+    // Test if media/add displays two media type options.
+    $this->drupalGet('media/add');
+
+    // Checks for the first media type.
+    $assert_session->pageTextContains($first_media_type->label());
+    $assert_session->pageTextContains($first_media_type->getDescription());
+    // Checks for the second media type.
+    $assert_session->pageTextContains($second_media_type->label());
+    $assert_session->pageTextContains($second_media_type->getDescription());
+
+    // Continue testing media type filter.
+    $first_media_item = Media::create(['bundle' => $first_media_type->id()]);
+    $first_media_item->save();
+    $second_media_item = Media::create(['bundle' => $second_media_type->id()]);
+    $second_media_item->save();
+
+    // Go to first media item.
+    $this->drupalGet('media/' . $first_media_item->id());
+    $assert_session->statusCodeEquals(200);
+    $assert_session->pageTextContains($first_media_item->label());
+
+    // Go to second media item.
+    $this->drupalGet('media/' . $second_media_item->id());
+    $assert_session->statusCodeEquals(200);
+    $assert_session->pageTextContains($second_media_item->label());
+  }
+
+  /**
+   * Tests the queue worker for media item thumbnails.
+   */
+  public function testMediaThumbnailQueueWorker() {
+    // Tests and creates the media type with queued thumbnails.
+    $media_type = $this->createMediaType(['queue_thumbnail_downloads' => TRUE]);
+
+    // Create a media item.
+    $media_item = Media::create(['bundle' => $media_type->id()]);
+    $media_item->save();
+
+    $default_thumb_uri = $media_item->getHandler()->getDefaultThumbnail();
+    $handler_thumb_uri = $media_item->getHandler()->getThumbnail($media_item);
+
+    // Media thumbnail should be the default image.
+    $this->assertEquals($default_thumb_uri, $media_item->get('thumbnail')->entity->getFileUri());
+
+    // Check if queue item was created.
+    $queue_name = 'media_entity_thumbnail';
+    $queue_worker = \Drupal::service('plugin.manager.queue_worker')->createInstance($queue_name);
+    $queue = \Drupal::queue($queue_name);
+    $item = $queue->claimItem();
+    $this->assertEquals($media_item->id(), $item->data['id']);
+
+    // Process queue worker.
+    $queue_worker->processItem($item->data);
+    $queue->deleteItem($item);
+
+    // Load updated media item to check changed thumbnail.
+    $updated_media_item = Media::load($media_item->id());
+    $this->assertEquals($handler_thumb_uri, $updated_media_item->get('thumbnail')->entity->getFileUri());
+  }
+
+}
diff --git a/core/modules/media/tests/src/FunctionalJavascript/MediaJavascriptTestBase.php b/core/modules/media/tests/src/FunctionalJavascript/MediaJavascriptTestBase.php
new file mode 100644
index 0000000..b3fb4ac
--- /dev/null
+++ b/core/modules/media/tests/src/FunctionalJavascript/MediaJavascriptTestBase.php
@@ -0,0 +1,110 @@
+<?php
+
+namespace Drupal\Tests\media\FunctionalJavascript;
+
+use Drupal\FunctionalJavascriptTests\JavascriptTestBase;
+use Drupal\Tests\media\Functional\MediaFunctionalTestTrait;
+
+/**
+ * Base class for Media functional JavaScript tests.
+ *
+ * @package Drupal\Tests\media\FunctionalJavascript
+ */
+abstract class MediaJavascriptTestBase extends JavascriptTestBase {
+
+  use MediaFunctionalTestTrait;
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = [
+    'system',
+    'node',
+    'field_ui',
+    'views_ui',
+    'media',
+    'media_test_handler',
+  ];
+
+  /**
+   * Permissions for the admin user that will be logged-in for test.
+   *
+   * @var array
+   */
+  protected static $adminUserPermissions = [
+    // Media entity permissions.
+    'administer media',
+    'administer media fields',
+    'administer media form display',
+    'administer media display',
+    'administer media types',
+    'view media',
+    'create media',
+    'update media',
+    'update any media',
+    'delete media',
+    'delete any media',
+    // Other permissions.
+    'administer views',
+    'access content overview',
+    'view all revisions',
+    'administer content types',
+    'administer node fields',
+    'administer node form display',
+    'bypass node access',
+  ];
+
+  /**
+   * An admin test user account.
+   *
+   * @var \Drupal\Core\Session\AccountInterface
+   */
+  protected $adminUser;
+
+  /**
+   * A non-admin test user account.
+   *
+   * @var \Drupal\Core\Session\AccountInterface
+   */
+  protected $nonAdminUser;
+
+  /**
+   * The storage service.
+   *
+   * @var \Drupal\Core\Entity\EntityStorageInterface
+   */
+  protected $storage;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    // Have two users ready to be used in tests.
+    $this->adminUser = $this->drupalCreateUser(static::$adminUserPermissions);
+    $this->nonAdminUser = $this->drupalCreateUser([]);
+    // Start off logged in as admin.
+    $this->drupalLogin($this->adminUser);
+
+    $this->storage = $this->container->get('entity_type.manager')->getStorage('media');
+  }
+
+  /**
+   * Waits and asserts that a given element is visible.
+   *
+   * @param string $selector
+   *   The CSS selector.
+   * @param int $timeout
+   *   (Optional) Timeout in milliseconds, defaults to 1000.
+   * @param string $message
+   *   (Optional) Message to pass to assertJsCondition().
+   */
+  protected function waitUntilVisible($selector, $timeout = 1000, $message = '') {
+    $condition = "jQuery('" . $selector . ":visible').length > 0";
+    $this->assertJsCondition($condition, $timeout, $message);
+  }
+
+}
diff --git a/core/modules/media/tests/src/FunctionalJavascript/MediaTypeCreationTest.php b/core/modules/media/tests/src/FunctionalJavascript/MediaTypeCreationTest.php
new file mode 100644
index 0000000..98d2ac9
--- /dev/null
+++ b/core/modules/media/tests/src/FunctionalJavascript/MediaTypeCreationTest.php
@@ -0,0 +1,98 @@
+<?php
+
+namespace Drupal\Tests\media\FunctionalJavascript;
+
+/**
+ * Tests the media type creation.
+ *
+ * @group media
+ */
+class MediaTypeCreationTest extends MediaJavascriptTestBase {
+
+  /**
+   * Tests the media type creation form.
+   */
+  public function testMediaTypeCreationFormWithDefaultField() {
+    $label = 'Type with Default Field';
+    $mediaTypeMachineName = str_replace(' ', '_', strtolower($label));
+
+    $this->drupalGet('admin/structure/media/add');
+    $page = $this->getSession()->getPage();
+
+    // Fill in a label to the media type.
+    $page->fillField('label', $label);
+    // Wait for machine name generation. Default: waitUntilVisible(), does not
+    // work properly.
+    $this->getSession()
+      ->wait(5000, "jQuery('.machine-name-value').text() === '{$mediaTypeMachineName}'");
+
+    // Select the media handler used by our media type.
+    $this->assertSession()->fieldExists('Media handler');
+    $this->assertSession()->optionExists('Media handler', 'test');
+    $page->selectFieldOption('Media handler', 'test');
+    $this->assertSession()->assertWaitOnAjaxRequest();
+
+    $page->pressButton('Save');
+
+    // Check whether the source field was correctly created.
+    $this->drupalGet("admin/structure/media/manage/{$mediaTypeMachineName}/fields");
+
+    // Check 2nd column of first data row, to be machine name for field name.
+    $this->assertSession()
+      ->elementContains('xpath', '(//table[@id="field-overview"]//tr)[2]//td[2]', 'field_media_test');
+    // Check 3rd column of first data row, to be correct field type.
+    $this->assertSession()
+      ->elementTextContains('xpath', '(//table[@id="field-overview"]//tr)[2]//td[3]', 'Text (plain)');
+
+    // Check that the source field is correctly assigned to media type.
+    $this->drupalGet("admin/structure/media/manage/{$mediaTypeMachineName}");
+
+    $this->assertSession()->pageTextContains('Test handler field is used to store the essential information about the media item.');
+  }
+
+  /**
+   * Test creation of media type, reusing an existing source field.
+   */
+  public function testMediaTypeCreationReuseSourceField() {
+    // Create a new media type, which should create a new field we can reuse.
+    $this->drupalGet('/admin/structure/media/add');
+    $page = $this->getSession()->getPage();
+    $page->fillField('label', 'Pastafazoul');
+    $this->getSession()
+      ->wait(5000, "jQuery('.machine-name-value').text() === 'pastafazoul'");
+    $page->selectFieldOption('Media handler', 'test');
+    $this->assertSession()->assertWaitOnAjaxRequest();
+    $page->pressButton('Save');
+
+    $label = 'Type reusing Default Field';
+    $mediaTypeMachineName = str_replace(' ', '_', strtolower($label));
+
+    $this->drupalGet('admin/structure/media/add');
+    $page = $this->getSession()->getPage();
+
+    // Fill in a label to the media type.
+    $page->fillField('label', $label);
+
+    // Wait for machine name generation. Default: waitUntilVisible(), does not
+    // work properly.
+    $this->getSession()
+      ->wait(5000, "jQuery('.machine-name-value').text() === '{$mediaTypeMachineName}'");
+
+    // Select the media handler used by our media type.
+    $this->assertSession()->fieldExists('Media handler');
+    $this->assertSession()->optionExists('Media handler', 'test');
+    $page->selectFieldOption('Media handler', 'test');
+    $this->assertSession()->assertWaitOnAjaxRequest();
+    // Select the existing field for re-use.
+    $page->selectFieldOption('handler_configuration[source_field]', 'field_media_test');
+    $page->pressButton('Save');
+
+    // Check that there are not fields created.
+    $this->drupalGet("admin/structure/media/manage/{$mediaTypeMachineName}/fields");
+    // The reused field should be present...
+    $this->assertSession()->pageTextContains('field_media_test');
+    // ...not a new, unique one.
+    $this->assertSession()->pageTextNotContains('field_media_test_1');
+  }
+
+}
diff --git a/core/modules/media/tests/src/FunctionalJavascript/MediaUiJavascriptTest.php b/core/modules/media/tests/src/FunctionalJavascript/MediaUiJavascriptTest.php
new file mode 100644
index 0000000..a153d50
--- /dev/null
+++ b/core/modules/media/tests/src/FunctionalJavascript/MediaUiJavascriptTest.php
@@ -0,0 +1,179 @@
+<?php
+
+namespace Drupal\Tests\media\FunctionalJavascript;
+
+use Drupal\media\Entity\Media;
+
+/**
+ * Ensures that media UI works correctly.
+ *
+ * @group media
+ */
+class MediaUiJavascriptTest extends MediaJavascriptTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = [
+    'block',
+    'media_test_handler',
+  ];
+
+  /**
+   * The test media type.
+   *
+   * @var \Drupal\media\MediaTypeInterface
+   */
+  protected $testMediaType;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+    $this->drupalPlaceBlock('local_actions_block');
+    $this->drupalPlaceBlock('local_tasks_block');
+  }
+
+  /**
+   * Tests a media type administration.
+   */
+  public function testMediaTypes() {
+    $session = $this->getSession();
+    $page = $session->getPage();
+    $assert_session = $this->assertSession();
+
+    $this->drupalGet('admin/structure/media');
+    $assert_session->statusCodeEquals(200);
+    $assert_session->pageTextContains('No media types available. Add media type.');
+    $assert_session->linkExists('Add media type');
+
+    // Test the creation of a media type using the UI.
+    $name = $this->randomMachineName();
+    $description = $this->randomMachineName();
+    $this->drupalGet('admin/structure/media/add');
+    $page->fillField('label', $name);
+    $machine_name = strtolower($name);
+    $this->assertJsCondition("jQuery('.machine-name-value').html() == '$machine_name'");
+    $page->selectFieldOption('handler', 'test');
+    $this->assertJsCondition("jQuery('.form-item-handler-configuration-test-config-value').length > 0;");
+    $page->fillField('description', $description);
+    $page->pressButton('Save');
+    $assert_session->statusCodeEquals(200);
+    $assert_session->pageTextContains('The media type ' . $name . ' has been added.');
+    $this->drupalGet('admin/structure/media');
+    $assert_session->statusCodeEquals(200);
+    $assert_session->pageTextContains($name);
+    $assert_session->pageTextContains($description);
+
+    /** @var \Drupal\media\MediaTypeInterface $media_type_storage */
+    $media_type_storage = $this->container->get('entity_type.manager')->getStorage('media_type');
+    $this->testMediaType = $media_type_storage->load(strtolower($name));
+
+    // Check if all action links exist.
+    $assert_session->linkByHrefExists('admin/structure/media/add');
+    $assert_session->linkByHrefExists('admin/structure/media/manage/' . $this->testMediaType->id());
+    $assert_session->linkByHrefExists('admin/structure/media/manage/' . $this->testMediaType->id() . '/fields');
+    $assert_session->linkByHrefExists('admin/structure/media/manage/' . $this->testMediaType->id() . '/form-display');
+    $assert_session->linkByHrefExists('admin/structure/media/manage/' . $this->testMediaType->id() . '/display');
+
+    // Assert that fields have expected values before editing.
+    $page->clickLink('Edit');
+    $assert_session->fieldValueEquals('label', $name);
+    $assert_session->fieldValueEquals('description', $description);
+    $assert_session->fieldValueEquals('handler', 'test');
+    $assert_session->fieldValueEquals('label', $name);
+    $assert_session->checkboxNotChecked('edit-options-new-revision');
+    $assert_session->checkboxChecked('edit-options-status');
+    $assert_session->checkboxNotChecked('edit-options-queue-thumbnail-downloads');
+    $assert_session->pageTextContains('Create new revision');
+    $assert_session->pageTextContains('Automatically create new revisions. Users with the Administer media permission will be able to override this option.');
+    $assert_session->pageTextContains('Download thumbnails via a queue.');
+    $assert_session->pageTextContains('Media will be automatically published when created.');
+    $assert_session->pageTextContains('Media handlers can provide metadata fields such as title, caption, size information, credits, etc. Media can automatically save this metadata information to entity fields, which can be configured below. Information will only be mapped if the entity field is empty.');
+
+    // Try to change media type and check if new configuration sub-form appears.
+    $page->selectFieldOption('handler', 'test');
+    $assert_session->assertWaitOnAjaxRequest();
+    $assert_session->fieldExists('Test config value');
+    $assert_session->fieldValueEquals('Test config value', 'This is default value.');
+    $assert_session->fieldExists('Field 1');
+    $assert_session->fieldExists('Field 2');
+
+    // Test if the edit machine name is not editable.
+    $assert_session->fieldDisabled('Machine-readable name');
+
+    // Edit and save media type form fields with new values.
+    $new_name = $this->randomMachineName();
+    $new_description = $this->randomMachineName();
+    $page->fillField('label', $new_name);
+    $page->fillField('description', $new_description);
+    $page->selectFieldOption('handler', 'test');
+    $page->fillField('Test config value', 'This is new config value.');
+    $page->selectFieldOption('field_map[field_1]', 'name');
+    $page->checkField('options[new_revision]');
+    $page->uncheckField('options[status]');
+    $page->checkField('options[queue_thumbnail_downloads]');
+    $page->pressButton('Save');
+    $assert_session->statusCodeEquals(200);
+
+    // Test if edit worked and if new field values have been saved as expected.
+    $this->drupalGet('admin/structure/media/manage/' . $this->testMediaType->id());
+    $assert_session->fieldValueEquals('label', $new_name);
+    $assert_session->fieldValueEquals('description', $new_description);
+    $assert_session->fieldValueEquals('handler', 'test');
+    $assert_session->checkboxChecked('options[new_revision]');
+    $assert_session->checkboxNotChecked('options[status]');
+    $assert_session->checkboxChecked('options[queue_thumbnail_downloads]');
+    $assert_session->fieldValueEquals('Test config value', 'This is new config value.');
+    $assert_session->fieldValueEquals('Field 1', 'name');
+    $assert_session->fieldValueEquals('Field 2', '_none');
+
+    /** @var \Drupal\media\MediaTypeInterface $loaded_media_type */
+    $loaded_media_type = $this->container->get('entity_type.manager')
+      ->getStorage('media_type')
+      ->load($this->testMediaType->id());
+    $this->assertEquals($loaded_media_type->id(), $this->testMediaType->id());
+    $this->assertEquals($loaded_media_type->label(), $new_name);
+    $this->assertEquals($loaded_media_type->getDescription(), $new_description);
+    $this->assertEquals($loaded_media_type->getHandler()->getPluginId(), 'test');
+    $this->assertEquals($loaded_media_type->getHandler()->getConfiguration()['test_config_value'], 'This is new config value.');
+    $this->assertTrue($loaded_media_type->shouldCreateNewRevision());
+    $this->assertTrue($loaded_media_type->thumbnailDownloadsAreQueued());
+    $this->assertFalse($loaded_media_type->getStatus());
+    $this->assertEquals($loaded_media_type->getFieldMap(), ['field_1' => 'name']);
+
+    // We need to clear the statically cached field definitions to account for
+    // fields that have been created by API calls in this test, since they exist
+    // in a separate memory space from the web server.
+    $this->container->get('entity_field.manager')->clearCachedFieldDefinitions();
+
+    // Test that a media item being created with default status to "FALSE",
+    // will be created unpublished.
+    /** @var \Drupal\media\MediaInterface $unpublished_media */
+    $unpublished_media = Media::create(['name' => 'unpublished test media', 'bundle' => $loaded_media_type->id()]);
+    $this->assertFalse($unpublished_media->isPublished());
+    $unpublished_media->delete();
+
+    // Tests media type delete form.
+    $page->clickLink('Delete');
+    $assert_session->addressEquals('admin/structure/media/manage/' . $this->testMediaType->id() . '/delete');
+    $page->pressButton('Delete');
+    $assert_session->addressEquals('admin/structure/media');
+    $assert_session->pageTextContains('The media type ' . $new_name . ' has been deleted.');
+
+    // Test type delete prevention when there is existing media.
+    $media_type2 = $this->createMediaType(['auto_create_source_field' => TRUE]);
+    $label2 = $media_type2->label();
+    $media = Media::create(['name' => 'lorem ipsum', 'bundle' => $media_type2->id()]);
+    $media->save();
+    $this->drupalGet('admin/structure/media/manage/' . $media_type2->id());
+    $page->clickLink('Delete');
+    $assert_session->addressEquals('admin/structure/media/manage/' . $media_type2->id() . '/delete');
+    $assert_session->fieldNotExists('edit-submit');
+    $assert_session->pageTextContains("$label2 is used by 1 media item on your site. You can not remove this media type until you have removed all of the $label2 media items.");
+  }
+
+}
diff --git a/core/modules/media/tests/src/FunctionalJavascript/MediaViewsWizardTest.php b/core/modules/media/tests/src/FunctionalJavascript/MediaViewsWizardTest.php
new file mode 100644
index 0000000..99515a2
--- /dev/null
+++ b/core/modules/media/tests/src/FunctionalJavascript/MediaViewsWizardTest.php
@@ -0,0 +1,88 @@
+<?php
+
+namespace Drupal\Tests\media\FunctionalJavascript;
+
+use Drupal\views\Views;
+
+/**
+ * Tests the media entity type integration into the wizard.
+ *
+ * @group media
+ *
+ * @see \Drupal\media\Plugin\views\wizard\Media
+ * @see \Drupal\media\Plugin\views\wizard\MediaRevision
+ */
+class MediaViewsWizardTest extends MediaJavascriptTestBase {
+
+  /**
+   * Tests adding a view of media.
+   */
+  public function testMediaWizard() {
+    $session = $this->getSession();
+    $page = $session->getPage();
+    $assert_session = $this->assertSession();
+
+    $view_id = strtolower($this->randomMachineName(16));
+    $this->drupalGet('admin/structure/views/add');
+    $page->fillField('label', $view_id);
+    $this->waitUntilVisible('.machine-name-value');
+    $page->selectFieldOption('show[wizard_key]', 'media');
+    $assert_session->assertWaitOnAjaxRequest();
+    $page->checkField('page[create]');
+    $page->fillField('page[path]', $this->randomMachineName(16));
+    $page->pressButton('Save and edit');
+    $assert_session->assertWaitOnAjaxRequest();
+    $this->assertEquals($session->getCurrentUrl(), $this->baseUrl . '/admin/structure/views/view/' . $view_id);
+
+    $view = Views::getView($view_id);
+    $view->initHandlers();
+    $row = $view->display_handler->getOption('row');
+    $this->assertEquals($row['type'], 'fields');
+    // Check for the default filters.
+    $this->assertEquals($view->filter['status']->table, 'media_field_data');
+    $this->assertEquals($view->filter['status']->field, 'status');
+    $this->assertTrue($view->filter['status']->value);
+    // Check for the default fields.
+    $this->assertEquals($view->field['name']->table, 'media_field_data');
+    $this->assertEquals($view->field['name']->field, 'name');
+
+  }
+
+  /**
+   * Tests adding a view of media revisions.
+   */
+  public function testMediaRevisionWizard() {
+    $session = $this->getSession();
+    $page = $session->getPage();
+    $assert_session = $this->assertSession();
+
+    $view_id = strtolower($this->randomMachineName(16));
+    $this->drupalGet('admin/structure/views/add');
+    $page->fillField('label', $view_id);
+    $this->waitUntilVisible('.machine-name-value');
+    $page->selectFieldOption('show[wizard_key]', 'media_revision');
+    $assert_session->assertWaitOnAjaxRequest();
+    $page->checkField('page[create]');
+    $page->fillField('page[path]', $this->randomMachineName(16));
+    $page->pressButton('Save and edit');
+    $assert_session->assertWaitOnAjaxRequest();
+    $this->assertEquals($session->getCurrentUrl(), $this->baseUrl . '/admin/structure/views/view/' . $view_id);
+
+    $view = Views::getView($view_id);
+    $view->initHandlers();
+    $row = $view->display_handler->getOption('row');
+    $this->assertEquals($row['type'], 'fields');
+
+    // Check for the default filters.
+    $this->assertEquals($view->filter['status']->table, 'media_field_revision');
+    $this->assertEquals($view->filter['status']->field, 'status');
+    $this->assertTrue($view->filter['status']->value);
+
+    // Check for the default fields.
+    $this->assertEquals($view->field['name']->table, 'media_field_revision');
+    $this->assertEquals($view->field['name']->field, 'name');
+    $this->assertEquals($view->field['changed']->table, 'media_field_revision');
+    $this->assertEquals($view->field['changed']->field, 'changed');
+  }
+
+}
diff --git a/core/modules/media/tests/src/Kernel/BasicCreationTest.php b/core/modules/media/tests/src/Kernel/BasicCreationTest.php
new file mode 100644
index 0000000..fe70aff
--- /dev/null
+++ b/core/modules/media/tests/src/Kernel/BasicCreationTest.php
@@ -0,0 +1,204 @@
+<?php
+
+namespace Drupal\Tests\media\Kernel;
+
+use Drupal\field\Entity\FieldConfig;
+use Drupal\KernelTests\KernelTestBase;
+use Drupal\media\Entity\Media;
+use Drupal\media\Entity\MediaType;
+use Drupal\media\MediaInterface;
+use Drupal\media\MediaTypeInterface;
+
+/**
+ * Tests creation of media types and media items.
+ *
+ * @group media
+ */
+class BasicCreationTest extends KernelTestBase {
+
+  /**
+   * Modules to install.
+   *
+   * @var array
+   */
+  public static $modules = [
+    'media',
+    'media_test_handler',
+    'image',
+    'user',
+    'field',
+    'system',
+    'file',
+  ];
+
+  /**
+   * The test media type.
+   *
+   * @var \Drupal\media\MediaTypeInterface
+   */
+  protected $testMediaType;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->installEntitySchema('user');
+    $this->installEntitySchema('file');
+    $this->installSchema('file', 'file_usage');
+    $this->installEntitySchema('media');
+    $this->installConfig(['field', 'system', 'image', 'file']);
+
+    // Create a test media type.
+    $id = strtolower($this->randomMachineName());
+    $this->testMediaType = MediaType::create([
+      'id' => $id,
+      'label' => $id,
+      'handler' => 'test',
+      'new_revision' => FALSE,
+      'auto_create_source_field' => TRUE,
+    ]);
+    $this->testMediaType->save();
+  }
+
+  /**
+   * Tests creating a media type programmatically.
+   */
+  public function testMediaTypeCreation() {
+    $media_type_storage = $this->container->get('entity_type.manager')->getStorage('media_type');
+
+    $this->assertInstanceOf(MediaTypeInterface::class, MediaType::load($this->testMediaType->id()), 'The new media type has not been correctly created in the database.');
+
+    // Test a media type created from default configuration.
+    $this->container->get('module_installer')->install(['media_test_type']);
+    $test_media_type = $media_type_storage->load('test');
+    $this->assertInstanceOf(MediaTypeInterface::class, $test_media_type, 'The media type from default configuration has not been created in the database.');
+    $this->assertEquals('Test type', $test_media_type->get('label'), 'Could not assure the correct type name.');
+    $this->assertEquals('Test type.', $test_media_type->get('description'), 'Could not assure the correct type description.');
+    $this->assertEquals('test', $test_media_type->get('handler'), 'Could not assure the correct media handler.');
+    // Source field is not set on the media handler, but it should never
+    // be created automatically when a config is being imported.
+    $this->assertEquals(['source_field' => '', 'test_config_value' => 'Kakec'], $test_media_type->get('handler_configuration'), 'Could not assure the correct media handler configuration.');
+    $this->assertEquals([], $test_media_type->get('field_map'), 'Could not assure the correct field map.');
+  }
+
+  /**
+   * Tests creating a media item programmatically.
+   */
+  public function testMediaEntityCreation() {
+    $media = Media::create([
+      'bundle' => $this->testMediaType->id(),
+      'name' => 'Unnamed',
+      'field_media_test' => 'Nation of sheep, ruled by wolves, owned by pigs.',
+    ]);
+    $media->save();
+
+    $this->assertNotInstanceOf(MediaInterface::class, Media::load(rand(1000, 9999)), 'Failed asserting a non-existent media.');
+
+    $this->assertInstanceOf(MediaInterface::class, Media::load($media->id()), 'The new media item has not been created in the database.');
+    $this->assertEquals($this->testMediaType->id(), $media->bundle(), 'The media item was not created with the correct type.');
+    $this->assertEquals('Unnamed', $media->label(), 'The media item was not created with the correct name.');
+    $source_field_name = $media->bundle->entity->getHandler()->getSourceField($media->bundle->entity)->getName();
+    $this->assertEquals('Nation of sheep, ruled by wolves, owned by pigs.', $media->get($source_field_name)->first()->getValue()['value'], 'Handler returns correct source field.');
+
+    // Test the creation of a media item without user-defined label and
+    // check if a default name is provided.
+    $media = Media::create([
+      'bundle' => $this->testMediaType->id(),
+    ]);
+    $media->save();
+    $expected_name = 'media' . ':' . $this->testMediaType->id() . ':' . $media->uuid();
+    $this->assertEquals($this->testMediaType->id(), $media->bundle(), 'The media item was not created with correct type.');
+    $this->assertEquals($expected_name, $media->label(), 'The media item was not created with a default name.');
+  }
+
+  /**
+   * Tests creating and updating media type programmatically.
+   */
+  public function testProgrammaticMediaTypeManipulation() {
+    // Creating a media type programmatically without specifying a source field
+    // should create one automagically.
+    /** @var FieldConfig $field */
+    $field = $this->testMediaType->getHandler()->getSourceField($this->testMediaType);
+    $this->assertInstanceOf(FieldConfig::class, $field);
+    $this->assertEquals('field_media_test', $field->getName());
+    $this->assertFalse($field->isNew());
+
+    // Saving with a non-existent source field should create it if the related
+    // flag is set.
+    $this->testMediaType->getHandler()->setConfiguration([
+      'source_field' => 'field_magick',
+    ]);
+    $this->testMediaType->set('auto_create_source_field', TRUE);
+    $this->testMediaType->save();
+    $field = $this->testMediaType->getHandler()->getSourceField($this->testMediaType);
+    $this->assertInstanceOf(FieldConfig::class, $field);
+    $this->assertEquals('field_magick', $field->getName());
+    $this->assertFalse($field->isNew());
+
+    // Trying to save without a source field should create a new, de-duped one.
+    $this->testMediaType->getHandler()->setConfiguration([]);
+    $this->testMediaType->set('auto_create_source_field', TRUE);
+    $this->testMediaType->save();
+    $field = $this->testMediaType->getHandler()->getSourceField($this->testMediaType);
+    $this->assertInstanceOf(FieldConfig::class, $field);
+    $this->assertEquals('field_media_test_1', $field->getName());
+    $this->assertFalse($field->isNew());
+
+    // Trying to reuse an existing field should, well, reuse the existing field
+    // even if the auto create flag is set.
+    $this->testMediaType->getHandler()->setConfiguration([
+      'source_field' => 'field_magick',
+    ]);
+    $this->testMediaType->set('auto_create_source_field', TRUE);
+    $this->testMediaType->save();
+    $field = $this->testMediaType->getHandler()->getSourceField($this->testMediaType);
+    $this->assertInstanceOf(FieldConfig::class, $field);
+    $this->assertEquals('field_magick', $field->getName());
+    $this->assertFalse($field->isNew());
+    // No new de-duped fields should have been created.
+    $duplicates = FieldConfig::loadMultiple([
+      'media.' . $this->testMediaType->id() . '.field_magick_1',
+      'media.' . $this->testMediaType->id() . '.field_media_generic_2',
+    ]);
+    $this->assertEmpty($duplicates);
+
+    // If auto-create flag is not set fields shouldn't be created.
+    // ... if an imaginary field is set.
+    $this->testMediaType->getHandler()->setConfiguration([
+      'source_field' => 'field_imaginary',
+    ]);
+    $this->testMediaType->set('auto_create_source_field', FALSE);
+    $this->testMediaType->save();
+    $field = $this->container->get('entity_type.manager')
+      ->getStorage('field_config')
+      ->load('media.' . $this->testMediaType->id() . '.field_imaginary');
+    $field_storage = $this->container->get('entity_type.manager')
+      ->getStorage('field_storage_config')
+      ->load('media.field_imaginary');
+    $this->assertNull($field, 'Field was not automatically created.');
+    $this->assertNull($field_storage, 'Field was not automatically created.');
+
+    // ... if no field is set.
+    $this->testMediaType->getHandler()->setConfiguration([]);
+    $this->testMediaType->set('auto_create_source_field', FALSE);
+    $this->testMediaType->save();
+    $config = $this->testMediaType->getHandler()->getConfiguration();
+    $this->assertEquals('', $config['source_field'], 'Source field value was not automatically populated.');
+    // Check what would the field be named like if it was created.
+    $proposed_field = $this->testMediaType->getHandler()->getSourceField($this->testMediaType);
+    $this->assertInstanceOf(FieldConfig::class, $proposed_field);
+    $this->assertEquals('field_media_test_2', $proposed_field->getName());
+    $this->assertTrue($proposed_field->isNew());
+    $field = $this->container->get('entity_type.manager')
+      ->getStorage('field_config')
+      ->load('media.' . $this->testMediaType->id() . '.' . $proposed_field->getName());
+    $field_storage = $this->container->get('entity_type.manager')
+      ->getStorage('field_storage_config')
+      ->load('media.' . $proposed_field->getName());
+    $this->assertNull($field, 'Field was not automatically created.');
+    $this->assertNull($field_storage, 'Field was not automatically created.');
+  }
+
+}
diff --git a/core/modules/media/tests/src/Kernel/TokensTest.php b/core/modules/media/tests/src/Kernel/TokensTest.php
new file mode 100644
index 0000000..3e9bd5a
--- /dev/null
+++ b/core/modules/media/tests/src/Kernel/TokensTest.php
@@ -0,0 +1,74 @@
+<?php
+
+namespace Drupal\Tests\media\Kernel;
+
+use Drupal\Core\Language\Language;
+use Drupal\KernelTests\Core\Entity\EntityKernelTestBase;
+use Drupal\media\Entity\Media;
+use Drupal\media\Entity\MediaType;
+
+/**
+ * Tests token handling.
+ *
+ * @group media
+ */
+class TokensTest extends EntityKernelTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = [
+    'media',
+    'media_test_handler',
+    'path',
+    'file',
+    'image',
+    'datetime',
+    'language',
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+    $this->installEntitySchema('file');
+    $this->installSchema('file', 'file_usage');
+    $this->installEntitySchema('media');
+    $this->installConfig(['language', 'datetime', 'field', 'system']);
+  }
+
+  /**
+   * Tests some of the tokens provided by Media.
+   */
+  public function testMediaEntityTokens() {
+    // Create a test media type.
+    $media_type_name = $this->randomMachineName();
+
+    MediaType::create([
+      'id' => $media_type_name,
+      'label' => $media_type_name,
+      'handler' => 'test',
+      'handler_configuration' => ['test_config_value' => 'Kakec'],
+      'field_map' => [],
+      'status' => 1,
+      'new_revision' => FALSE,
+    ])->save();
+
+    // Create a media item.
+    $media = Media::create([
+      'name' => $this->randomMachineName(),
+      'bundle' => $media_type_name,
+      'uid' => '1',
+      'langcode' => Language::LANGCODE_DEFAULT,
+      'status' => TRUE,
+    ]);
+    $media->save();
+
+    $token_service = $this->container->get('token');
+
+    $replaced_value = $token_service->replace('[media:name]', ['media' => $media]);
+    $this->assertEquals($media->label(), $replaced_value, 'Token replacement for the media label was successful.');
+  }
+
+}
diff --git a/core/themes/classy/templates/content/media.html.twig b/core/themes/classy/templates/content/media.html.twig
new file mode 100644
index 0000000..831a3e6
--- /dev/null
+++ b/core/themes/classy/templates/content/media.html.twig
@@ -0,0 +1,27 @@
+{#
+/**
+ * @file
+ * Theme override to display a media.
+ *
+ * Available variables:
+ * - name: Name of the media.
+ * - content: Media content.
+ *
+ * @see template_preprocess_media()
+ *
+ * @ingroup themeable
+ */
+#}
+{%
+  set classes = [
+    'media',
+    'media--type-' ~ media.bundle()|clean_class,
+    not media.isPublished() ? 'media--unpublished',
+    view_mode ? 'media--view-mode-' ~ view_mode.id()|clean_class,
+  ]
+%}
+<article{{ attributes.addClass(classes) }}>
+  {% if content %}
+    {{ content }}
+  {% endif %}
+</article>
diff --git a/core/themes/stable/templates/content/media.html.twig b/core/themes/stable/templates/content/media.html.twig
new file mode 100644
index 0000000..2bdd59e
--- /dev/null
+++ b/core/themes/stable/templates/content/media.html.twig
@@ -0,0 +1,19 @@
+{#
+/**
+ * @file
+ * Theme override to display a media.
+ *
+ * Available variables:
+ * - name: Name of the media.
+ * - content: Media content.
+ *
+ * @see template_preprocess_media()
+ *
+ * @ingroup themeable
+ */
+#}
+<article{{ attributes }}>
+  {% if content %}
+    {{ content }}
+  {% endif %}
+</article>
