 core/includes/common.inc                           |  49 +++++--
 core/includes/theme.inc                            |   8 +-
 core/lib/Drupal/Core/Utility/LinkGenerator.php     |   4 +-
 core/modules/block/block.module                    |   6 +-
 core/modules/contextual/contextual.module          |  13 +-
 .../tests/language_test/language_test.module       |   4 +-
 core/modules/node/node.module                      |   4 +-
 core/modules/quickedit/quickedit.module            |   4 +-
 core/modules/system/system.api.php                 | 153 ++++++++++++---------
 core/modules/system/system.module                  |  13 +-
 core/modules/taxonomy/taxonomy.module              |   4 +-
 core/modules/toolbar/src/Element/Toolbar.php       |   2 +-
 core/modules/toolbar/toolbar.module                |   8 +-
 core/modules/tour/tour.module                      |   6 +-
 core/modules/update/update.module                  |   4 +-
 core/modules/user/user.module                      |   4 +-
 16 files changed, 171 insertions(+), 115 deletions(-)

diff --git a/core/includes/common.inc b/core/includes/common.inc
index 4a789c8..3f78397 100644
--- a/core/includes/common.inc
+++ b/core/includes/common.inc
@@ -739,14 +739,14 @@ function drupal_http_header_attributes(array $attributes = array()) {
  *     For authenticated users, the "active" class will be calculated on the
  *     client (through JavaScript), only data- attributes are added to links to
  *     prevent breaking the render cache. The JavaScript is added in
- *     system_page_build().
+ *     system_page_attachments().
  *   - Additional $options elements used by the url() function.
  *
  * @return string
  *   An HTML string containing a link to the given path.
  *
  * @see _url()
- * @see system_page_build()
+ * @see system_page_attachments()
  * @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0.0.
  *   Use \Drupal::l($text, $url) where $url is an instance of
  *   \Drupal\Core\Url. To build a \Drupal\Core\Url object for internal paths
@@ -2473,15 +2473,44 @@ function drupal_prepare_page($page) {
     $page = element_info('page');
   }
 
-  // Modules can add elements to $page as needed in hook_page_build().
-  foreach (\Drupal::moduleHandler()->getImplementations('page_build') as $module) {
-    $function = $module . '_page_build';
-    $function($page);
+  // Modules can add attachments.
+  $pseudo_page_for_attachments = [];
+  foreach (\Drupal::moduleHandler()->getImplementations('page_attachments') as $module) {
+    $function = $module . '_page_attachments';
+    $function($pseudo_page_for_attachments);
+  }
+  // Modules and themes can alter page attachments.
+  \Drupal::moduleHandler()->alter('page_attachments', $pseudo_page_for_attachments);
+  \Drupal::theme()->alter('page_attachments', $pseudo_page_for_attachments);
+  if (isset($pseudo_page_for_attachments['#attached'])) {
+    $page['#attached'] = $pseudo_page_for_attachments['#attached'];
+  }
+  if (isset($pseudo_page_for_attachments['#post_render_cache'])) {
+    $page['#post_render_cache'] = $pseudo_page_for_attachments['#post_render_cache'];
+  }
+
+  // Modules can add renderable arrays to the top and bottom of the page.
+  $pseudo_page_top = [];
+  $pseudo_page_bottom = [];
+  foreach (\Drupal::moduleHandler()->getImplementations('page_top') as $module) {
+    $function = $module . '_page_top';
+    $function($pseudo_page_top);
+  }
+  foreach (\Drupal::moduleHandler()->getImplementations('page_bottom') as $module) {
+    $function = $module . '_page_bottom';
+    $function($pseudo_page_bottom);
+  }
+  if (!empty($pseudo_page_top)) {
+    $page['page_top'] = $pseudo_page_top;
+  }
+  if (!empty($pseudo_page_bottom)) {
+    $page['page_bottom'] = $pseudo_page_bottom;
+  }
+
+  // @todo Clean this up as part of https://www.drupal.org/node/2352155.
+  if (\Drupal::moduleHandler()->moduleExists('block')) {
+    _block_page_build($page);
   }
-  // Modules alter the $page as needed. Blocks are populated into regions like
-  // 'sidebar_first', 'footer', etc.
-  \Drupal::moduleHandler()->alter('page', $page);
-  \Drupal::theme()->alter('page', $page);
 
   // If no module has taken care of the main content, add it to the page now.
   // This allows the site to still be usable even if no modules that
diff --git a/core/includes/theme.inc b/core/includes/theme.inc
index b843029..8e32007 100644
--- a/core/includes/theme.inc
+++ b/core/includes/theme.inc
@@ -931,7 +931,7 @@ function template_preprocess_status_messages(&$variables) {
  *     For authenticated users, the "active" class will be calculated on the
  *     client (through JavaScript), only data- attributes are added to list
  *     items and contained links, to prevent breaking the render cache. The
- *     JavaScript is added in system_page_build().
+ *     JavaScript is added in system_page_attachments().
  *   - heading: (optional) A heading to precede the links. May be an
  *     associative array or a string. If it's an array, it can have the
  *     following elements:
@@ -959,7 +959,7 @@ function template_preprocess_status_messages(&$variables) {
  *
  * @see \Drupal\Core\Utility\LinkGenerator
  * @see \Drupal\Core\Utility\LinkGenerator::generate()
- * @see system_page_build()
+ * @see system_page_attachments()
  */
 function template_preprocess_links(&$variables) {
   $links = $variables['links'];
@@ -1938,7 +1938,7 @@ function theme_get_suggestions($args, $base, $delimiter = '__') {
  *   An associative array containing:
  *   - content - An array of page content.
  *
- * @see system_page_build()
+ * @see system_page_attachments()
  */
 function template_preprocess_maintenance_page(&$variables) {
   // @todo Rename the templates to page--maintenance + page--install.
@@ -1954,7 +1954,7 @@ function template_preprocess_maintenance_page(&$variables) {
   }
   $attributes['class'] = $classes;
 
-  // @see system_page_build()
+  // @see system_page_attachments()
   $variables['#attached']['library'][] = 'core/normalize';
   $variables['#attached']['library'][] = 'system/maintenance';
 }
diff --git a/core/lib/Drupal/Core/Utility/LinkGenerator.php b/core/lib/Drupal/Core/Utility/LinkGenerator.php
index f567558..df89ab7 100644
--- a/core/lib/Drupal/Core/Utility/LinkGenerator.php
+++ b/core/lib/Drupal/Core/Utility/LinkGenerator.php
@@ -64,9 +64,9 @@ public function generateFromLink(Link $link) {
    * For authenticated users, the "active" class will be calculated on the
    * client (through JavaScript), only data- attributes are added to links to
    * prevent breaking the render cache. The JavaScript is added in
-   * system_page_build().
+   * system_page_attachments().
    *
-   * @see system_page_build()
+   * @see system_page_attachments()
    */
   public function generate($text, Url $url) {
     // Performance: avoid Url::toString() needing to retrieve the URL generator
diff --git a/core/modules/block/block.module b/core/modules/block/block.module
index cbf56ab..e615100 100644
--- a/core/modules/block/block.module
+++ b/core/modules/block/block.module
@@ -64,11 +64,11 @@ function block_theme() {
 }
 
 /**
- * Implements hook_page_build().
- *
  * Renders blocks into their regions.
+ *
+ * @todo Clean this up as part of https://www.drupal.org/node/2352155.
  */
-function block_page_build(&$page) {
+function _block_page_build(&$page) {
   $theme = \Drupal::theme()->getActiveTheme()->getName();
 
   // Fetch a list of regions for the current theme.
diff --git a/core/modules/contextual/contextual.module b/core/modules/contextual/contextual.module
index 66550f2..10093ba 100644
--- a/core/modules/contextual/contextual.module
+++ b/core/modules/contextual/contextual.module
@@ -44,15 +44,14 @@ function contextual_toolbar() {
 }
 
 /**
- * Implements hook_page_build().
+ * Implements hook_page_attachments().
  *
  * Adds the drupal.contextual-links library to the page for any user who has the
  * 'access contextual links' permission.
  *
  * @see contextual_preprocess()
  */
-function contextual_page_build(&$page) {
-
+function contextual_page_attachments(array &$page) {
   if (!\Drupal::currentUser()->hasPermission('access contextual links')) {
     return;
   }
@@ -89,7 +88,7 @@ function contextual_help($route_name, RouteMatchInterface $route_match) {
  * Implements hook_preprocess().
  *
  * @see contextual_pre_render_placeholder()
- * @see contextual_page_build()
+ * @see contextual_page_attachments()
  * @see \Drupal\contextual\ContextualController::render()
  */
 function contextual_preprocess(&$variables, $hook, $info) {
@@ -111,9 +110,9 @@ function contextual_preprocess(&$variables, $hook, $info) {
 
     // Renders a contextual links placeholder unconditionally, thus not breaking
     // the render cache. Although the empty placeholder is rendered for all
-    // users, contextual_page_build() only adds the drupal.contextual-links
-    // library for users with the 'access contextual links' permission, thus
-    // preventing unnecessary HTTP requests for users without that permission.
+    // users, contextual_page_attachments() only adds the asset library for
+    // users with the 'access contextual links' permission, thus preventing
+    // unnecessary HTTP requests for users without that permission.
     $variables['title_suffix']['contextual_links'] = array(
       '#type' => 'contextual_links_placeholder',
       '#id' => _contextual_links_to_id($element['#contextual_links']),
diff --git a/core/modules/language/tests/language_test/language_test.module b/core/modules/language/tests/language_test/language_test.module
index 6e81581..1b7bbe4 100644
--- a/core/modules/language/tests/language_test/language_test.module
+++ b/core/modules/language/tests/language_test/language_test.module
@@ -9,9 +9,9 @@
 use Drupal\language\Plugin\LanguageNegotiation\LanguageNegotiationUI;
 
 /**
- * Implements hook_page_build().
+ * Implements hook_page_top().
  */
-function language_test_page_build() {
+function language_test_page_top() {
   if (\Drupal::moduleHandler()->moduleExists('language')) {
     language_test_store_language_negotiation();
     drupal_set_message(t('Language negotiation method: @name', array('@name' => \Drupal::languageManager()->getNegotiatedLanguageMethod())));
diff --git a/core/modules/node/node.module b/core/modules/node/node.module
index 6988f1e..ada6d70 100644
--- a/core/modules/node/node.module
+++ b/core/modules/node/node.module
@@ -891,9 +891,9 @@ function node_view_multiple($nodes, $view_mode = 'teaser', $langcode = NULL) {
 }
 
 /**
- * Implements hook_page_build().
+ * Implements hook_page_top().
  */
-function node_page_build(&$page) {
+function node_page_top(array &$page) {
   // Add 'Back to content editing' link on preview page.
   $route_match = \Drupal::routeMatch();
   if ($route_match->getRouteName() == 'entity.node.preview') {
diff --git a/core/modules/quickedit/quickedit.module b/core/modules/quickedit/quickedit.module
index 6b5a9cf..7ed6f92 100644
--- a/core/modules/quickedit/quickedit.module
+++ b/core/modules/quickedit/quickedit.module
@@ -36,12 +36,12 @@ function quickedit_help($route_name, RouteMatchInterface $route_match) {
 }
 
 /**
- * Implements hook_page_build().
+ * Implements hook_page_attachments().
  *
  * Adds the quickedit library to the page for any user who has the 'access
  * in-place editing' permission.
  */
-function quickedit_page_build(&$page) {
+function quickedit_page_attachments(array &$page) {
   if (!\Drupal::currentUser()->hasPermission('access in-place editing')) {
     return;
   }
diff --git a/core/modules/system/system.api.php b/core/modules/system/system.api.php
index a0ee294..0d8282e 100644
--- a/core/modules/system/system.api.php
+++ b/core/modules/system/system.api.php
@@ -288,26 +288,18 @@ function hook_ajax_render_alter(array &$data) {
 }
 
 /**
- * Add elements to a page before it is rendered.
+ * Add attachments (typically assets) to a page before it is rendered.
  *
- * Use this hook when you want to add elements at the page level. For your
- * additions to be printed, they have to be placed below a top level array key
- * of the $page array that has the name of a region of the active theme.
+ * Kept around for backwards compatibility, but now allows only attachments to
+ * be added, adding renderable arrays is no longer allowed.
  *
- * By default, valid region keys are 'page_top', 'header', 'sidebar_first',
- * 'content', 'sidebar_second' and 'page_bottom'. To get a list of all regions
- * of the active theme, use system_region_list($theme). Note that $theme is a
- * global variable.
- *
- * If you want to alter the elements added by other modules or if your module
- * depends on the elements of other modules, use hook_page_alter() instead which
- * runs after this hook.
+ * @deprecated in Drupal 8.x, will be removed before Drupal 9.0. Successor:
+ *   hook_page_attachments(). Is now effectively an alias of that hook.
  *
  * @param $page
- *   Nested array of renderable elements that make up the page.
+ *   The page to which to add attachments.
  *
- * @see hook_page_alter()
- * @see DefaultHtmlFragmentRenderer::render()
+ * @see hook_page_attachments()
  */
 function hook_page_build(&$page) {
   $path = drupal_get_path('module', 'foo');
@@ -321,17 +313,77 @@ function hook_page_build(&$page) {
   if (drupal_is_front_page()) {
     $page['#attached']['css'][] = $path . '/foo.front.css';
   }
+}
 
-  // Append a standard disclaimer to the content region on a node detail page.
-  if (\Drupal::request()->attributes->get('node')) {
-    $page['content']['disclaimer'] = array(
-      '#markup' => t('Acme, Inc. is not responsible for the contents of this sample code.'),
-      '#weight' => 25,
-    );
+/**
+ * Add attachments (typically assets) to a page before it is rendered.
+ *
+ * Use this hook when you want to conditionally add attachments to a page.
+ *
+ * If you want to alter the attachments added by other modules or if your module
+ * depends on the elements of other modules, use hook_page_attachments_alter()
+ * instead, which runs after this hook.
+ *
+ * @param array &$page
+ *   An empty renderable array representing the page.
+ *
+ * @see hook_page_attachments_alter()
+ */
+function hook_page_attachments(array &$page) {
+  // Unconditionally attach an asset to the page.
+  $page['#attached']['library'][] = 'core/domready';
+
+  // Conditionally attach an asset to the page.
+  if (!\Drupal::currentUser()->hasPermission('may pet kittens')) {
+    $page['#attached']['library'][] = 'core/jquery';
   }
 }
 
 /**
+ * Alter attachments (typically assets) to a page before it is rendered.
+ *
+ * Use this hook when you want to remove or alter attachments on the page, or
+ * add attachments to the page that depend on aonther module's attachments (this
+ * hook runs after hook_page_attachments().
+ *
+ * If you want to alter the attachments added by other modules or if your module
+ * depends on the elements of other modules, use hook_page_attachments_alter()
+ * instead, which runs after this hook.
+ *
+ * @param array &$page
+ *   An empty renderable array representing the page.
+ *
+ * @see hook_page_attachments_alter()
+ */
+function hook_page_attachments_alter(array &$page) {
+  // Conditionally remove an asset.
+  if (in_array('core/jquery', $page['#attached']['library'])) {
+    $index = array_search('core/jquery', $page['#attached']['library']);
+    unset($page['#attached']['library'][$index]);
+  }
+}
+
+/**
+ * Add a renderable array to the top of the page.
+ *
+ * @param array $page_top
+ *   A renderable array representing the top of the page.
+ */
+function hook_page_top(array &$page_top) {
+  $page_top['mymodule'] = ['#markup' => 'This is the top.'];
+}
+
+/**
+ * Add a renderable array to the bottom of the page.
+ *
+ * @param array $page_top
+ *   A renderable array representing the bottom of the page.
+ */
+function hook_page_bottom(array &$page) {
+  $page_bottom['mymodule'] = ['#markup' => 'This is the bottom.'];
+}
+
+/**
  * Alters all the menu links discovered by the menu link plugin manager.
  *
  * @param array $links
@@ -546,60 +598,27 @@ function hook_contextual_links_plugins_alter(array &$contextual_links) {
 /**
  * Perform alterations before a page is rendered.
  *
- * Use this hook when you want to remove or alter elements at the page
- * level, or add elements at the page level that depend on an other module's
- * elements (this hook runs after hook_page_build().
- *
- * If you are making changes to entities such as forms, menus, or user
- * profiles, use those objects' native alter hooks instead (hook_form_alter(),
- * for example).
- *
- * The $page array contains top level elements for each block region:
- * @code
- *   $page['page_top']
- *   $page['header']
- *   $page['sidebar_first']
- *   $page['content']
- *   $page['sidebar_second']
- *   $page['page_bottom']
- * @endcode
- *
- * The 'content' element contains the main content of the current page, and its
- * structure will vary depending on what module is responsible for building the
- * page. Some legacy modules may not return structured content at all: their
- * pre-rendered markup will be located in $page['content']['main']['#markup'].
+ * Kept around for backwards compatibility, but now allows only attachments to
+ * be added, altering the renderable array for the page is no longer allowed.
  *
- * Pages built by Drupal's core Node module use a standard structure:
- *
- * @code
- *   // Node body.
- *   $page['content']['system_main']['nodes'][$nid]['body']
- *   // Array of links attached to the node (add comments, read more).
- *   $page['content']['system_main']['nodes'][$nid]['links']
- *   // The node entity itself.
- *   $page['content']['system_main']['nodes'][$nid]['#node']
- *   // The results pager.
- *   $page['content']['system_main']['pager']
- * @endcode
+ * @deprecated in Drupal 8.x, will be removed before Drupal 9.0. Successor:
+ *   hook_page_attachments_alter(). Is now effectively an alias of that hook.
  *
- * Blocks may be referenced by their module/delta pair within a region:
- * @code
- *   // The login block in the first sidebar region.
- *   $page['sidebar_first']['user_login']['#block'];
- * @endcode
+ * Use this hook when you want to remove or alter attachments at the page
+ * level, or add attachments at the page level that depend on an other module's
+ * attachments (this hook runs after hook_page_build().
  *
  * @param $page
- *   Nested array of renderable elements that make up the page.
+ *   An empty renderable array representing the page.
  *
  * @see hook_page_build()
- * @see DefaultHtmlFragmentRenderer::render()
  */
 function hook_page_alter(&$page) {
-  // Add help text to the user login block.
-  $page['sidebar_first']['user_login']['help'] = array(
-    '#weight' => -10,
-    '#markup' => t('To post comments or add content, you first have to log in.'),
-  );
+  // Conditionally remove an asset.
+  if (in_array('core/jquery', $page['#attached']['library'])) {
+    $index = array_search('core/jquery', $page['#attached']['library']);
+    unset($page['#attached']['library'][$index]);
+  }
 }
 
 /**
diff --git a/core/modules/system/system.module b/core/modules/system/system.module
index 9e7164c..ee21a5e 100644
--- a/core/modules/system/system.module
+++ b/core/modules/system/system.module
@@ -550,12 +550,12 @@ function system_filetransfer_info() {
 }
 
 /**
- * Implements hook_page_build().
+ * Implements hook_page_attachments().
  *
  * @see template_preprocess_maintenance_page()
  * @see \Drupal\system\Controller\SystemController::setLinkActiveClass()
  */
-function system_page_build(&$page) {
+function system_page_attachments(array &$page) {
   // Ensure the same CSS is loaded in template_preprocess_maintenance_page().
   $page['#attached']['library'][] = 'core/normalize';
   $page['#attached']['library'][] = 'system/base';
@@ -584,6 +584,15 @@ function system_page_build(&$page) {
       )
     );
   }
+
+  // Invoke hook_page_build() for modules and hook_page_alter() for both modules
+  // and, for backwards compatibility.
+  foreach (\Drupal::moduleHandler()->getImplementations('page_build') as $module) {
+    $function = $module . '_page_build';
+    $function($page);
+  }
+  \Drupal::moduleHandler()->alter('page', $page);
+  \Drupal::theme()->alter('page', $page);
 }
 
 /**
diff --git a/core/modules/taxonomy/taxonomy.module b/core/modules/taxonomy/taxonomy.module
index a715c0a..5af4639 100644
--- a/core/modules/taxonomy/taxonomy.module
+++ b/core/modules/taxonomy/taxonomy.module
@@ -103,9 +103,9 @@ function taxonomy_term_uri($term) {
 }
 
 /**
- * Implements hook_page_build().
+ * Implements hook_page_attachments_alter().
  */
-function taxonomy_page_build(&$page) {
+function taxonomy_page_attachments_alter(array &$page) {
   $route_match = \Drupal::routeMatch();
   if ($route_match->getRouteName() == 'entity.taxonomy_term.canonical' && ($term = $route_match->getParameter('taxonomy_term')) && $term instanceof TermInterface) {
     foreach ($term->uriRelationships() as $rel) {
diff --git a/core/modules/toolbar/src/Element/Toolbar.php b/core/modules/toolbar/src/Element/Toolbar.php
index 1b682aa..fac39e2 100644
--- a/core/modules/toolbar/src/Element/Toolbar.php
+++ b/core/modules/toolbar/src/Element/Toolbar.php
@@ -64,7 +64,7 @@ public function getInfo() {
    * @return array
    *  A renderable array.
    *
-   * @see toolbar_page_build().
+   * @see toolbar_page_top().
    */
   public static function preRenderToolbar($element) {
     // Get the configured breakpoints to switch from vertical to horizontal
diff --git a/core/modules/toolbar/toolbar.module b/core/modules/toolbar/toolbar.module
index 3d960a8..fc03a2f 100644
--- a/core/modules/toolbar/toolbar.module
+++ b/core/modules/toolbar/toolbar.module
@@ -47,12 +47,12 @@ function toolbar_theme($existing, $type, $theme, $path) {
 }
 
 /**
- * Implements hook_page_build().
+ * Implements hook_page_top().
  *
- * Add admin toolbar to the page_top region automatically.
+ * Add admin toolbar to the top of the page automatically.
  */
-function toolbar_page_build(&$page) {
-  $page['page_top']['toolbar'] = array(
+function toolbar_page_top(array &$page_top) {
+  $page_top['toolbar'] = array(
     '#type' => 'toolbar',
     '#access' => \Drupal::currentUser()->hasPermission('access toolbar'),
   );
diff --git a/core/modules/tour/tour.module b/core/modules/tour/tour.module
index dc1e3e9..16acf19 100644
--- a/core/modules/tour/tour.module
+++ b/core/modules/tour/tour.module
@@ -63,9 +63,9 @@ function tour_toolbar() {
 }
 
 /**
- * Implements hook_page_build().
+ * Implements hook_page_bottom().
  */
-function tour_page_build(&$page) {
+function tour_page_bottom(array &$page_bottom) {
   if (!\Drupal::currentUser()->hasPermission('access tour')) {
     return;
   }
@@ -85,7 +85,7 @@ function tour_page_build(&$page) {
       }
     }
     if (!empty($tours)) {
-      $page['help']['tour'] = entity_view_multiple($tours, 'full');
+      $page_bottom['tour'] = entity_view_multiple($tours, 'full');
     }
   }
 }
diff --git a/core/modules/update/update.module b/core/modules/update/update.module
index f8ce628..89a76cb 100644
--- a/core/modules/update/update.module
+++ b/core/modules/update/update.module
@@ -114,9 +114,9 @@ function update_help($route_name, RouteMatchInterface $route_match) {
 }
 
 /**
- * Implements hook_page_build().
+ * Implements hook_page_top().
  */
-function update_page_build() {
+function update_page_top() {
   /** @var \Drupal\Core\Routing\AdminContext $admin_context */
   $admin_context = \Drupal::service('router.admin_context');
   if ($admin_context->isAdminRoute(\Drupal::request()->attributes->get(RouteObjectInterface::ROUTE_OBJECT)) && \Drupal::currentUser()->hasPermission('administer site configuration')) {
diff --git a/core/modules/user/user.module b/core/modules/user/user.module
index 92bebb0..061bc7c 100644
--- a/core/modules/user/user.module
+++ b/core/modules/user/user.module
@@ -110,9 +110,9 @@ function user_theme() {
 }
 
 /**
- * Implements hook_page_build().
+ * Implements hook_page_attachments().
  */
-function user_page_build(&$page) {
+function user_page_attachments(array &$page) {
   $path = drupal_get_path('module', 'user');
   $page['#attached']['css'][$path . '/css/user.module.css'] = array('every_page' => TRUE);
 }
