[ Index ]

PHP Cross Reference of WordPress

title

Body

[close]

/wp-admin/includes/ -> class-theme-upgrader.php (source)

   1  <?php
   2  /**
   3   * Upgrade API: Theme_Upgrader class
   4   *
   5   * @package WordPress
   6   * @subpackage Upgrader
   7   * @since 4.6.0
   8   */
   9  
  10  /**
  11   * Core class used for upgrading/installing themes.
  12   *
  13   * It is designed to upgrade/install themes from a local zip, remote zip URL,
  14   * or uploaded zip file.
  15   *
  16   * @since 2.8.0
  17   * @since 4.6.0 Moved to its own file from wp-admin/includes/class-wp-upgrader.php.
  18   *
  19   * @see WP_Upgrader
  20   */
  21  class Theme_Upgrader extends WP_Upgrader {
  22  
  23      /**
  24       * Result of the theme upgrade offer.
  25       *
  26       * @since 2.8.0
  27       * @var array|WP_Error $result
  28       * @see WP_Upgrader::$result
  29       */
  30      public $result;
  31  
  32      /**
  33       * Whether multiple themes are being upgraded/installed in bulk.
  34       *
  35       * @since 2.9.0
  36       * @var bool $bulk
  37       */
  38      public $bulk = false;
  39  
  40      /**
  41       * Initialize the upgrade strings.
  42       *
  43       * @since 2.8.0
  44       */
  45  	public function upgrade_strings() {
  46          $this->strings['up_to_date'] = __( 'The theme is at the latest version.' );
  47          $this->strings['no_package'] = __( 'Update package not available.' );
  48          /* translators: %s: Package URL. */
  49          $this->strings['downloading_package'] = sprintf( __( 'Downloading update from %s&#8230;' ), '<span class="code">%s</span>' );
  50          $this->strings['unpack_package']      = __( 'Unpacking the update&#8230;' );
  51          $this->strings['remove_old']          = __( 'Removing the old version of the theme&#8230;' );
  52          $this->strings['remove_old_failed']   = __( 'Could not remove the old theme.' );
  53          $this->strings['process_failed']      = __( 'Theme update failed.' );
  54          $this->strings['process_success']     = __( 'Theme updated successfully.' );
  55      }
  56  
  57      /**
  58       * Initialize the installation strings.
  59       *
  60       * @since 2.8.0
  61       */
  62  	public function install_strings() {
  63          $this->strings['no_package'] = __( 'Installation package not available.' );
  64          /* translators: %s: Package URL. */
  65          $this->strings['downloading_package'] = sprintf( __( 'Downloading installation package from %s&#8230;' ), '<span class="code">%s</span>' );
  66          $this->strings['unpack_package']      = __( 'Unpacking the package&#8230;' );
  67          $this->strings['installing_package']  = __( 'Installing the theme&#8230;' );
  68          $this->strings['no_files']            = __( 'The theme contains no files.' );
  69          $this->strings['process_failed']      = __( 'Theme installation failed.' );
  70          $this->strings['process_success']     = __( 'Theme installed successfully.' );
  71          /* translators: 1: Theme name, 2: Theme version. */
  72          $this->strings['process_success_specific'] = __( 'Successfully installed the theme <strong>%1$s %2$s</strong>.' );
  73          $this->strings['parent_theme_search']      = __( 'This theme requires a parent theme. Checking if it is installed&#8230;' );
  74          /* translators: 1: Theme name, 2: Theme version. */
  75          $this->strings['parent_theme_prepare_install'] = __( 'Preparing to install <strong>%1$s %2$s</strong>&#8230;' );
  76          /* translators: 1: Theme name, 2: Theme version. */
  77          $this->strings['parent_theme_currently_installed'] = __( 'The parent theme, <strong>%1$s %2$s</strong>, is currently installed.' );
  78          /* translators: 1: Theme name, 2: Theme version. */
  79          $this->strings['parent_theme_install_success'] = __( 'Successfully installed the parent theme, <strong>%1$s %2$s</strong>.' );
  80          /* translators: %s: Theme name. */
  81          $this->strings['parent_theme_not_found'] = sprintf( __( '<strong>The parent theme could not be found.</strong> You will need to install the parent theme, %s, before you can use this child theme.' ), '<strong>%s</strong>' );
  82      }
  83  
  84      /**
  85       * Check if a child theme is being installed and we need to install its parent.
  86       *
  87       * Hooked to the {@see 'upgrader_post_install'} filter by Theme_Upgrader::install().
  88       *
  89       * @since 3.4.0
  90       *
  91       * @param bool  $install_result
  92       * @param array $hook_extra
  93       * @param array $child_result
  94       * @return bool
  95       */
  96  	public function check_parent_theme_filter( $install_result, $hook_extra, $child_result ) {
  97          // Check to see if we need to install a parent theme.
  98          $theme_info = $this->theme_info();
  99  
 100          if ( ! $theme_info->parent() ) {
 101              return $install_result;
 102          }
 103  
 104          $this->skin->feedback( 'parent_theme_search' );
 105  
 106          if ( ! $theme_info->parent()->errors() ) {
 107              $this->skin->feedback( 'parent_theme_currently_installed', $theme_info->parent()->display( 'Name' ), $theme_info->parent()->display( 'Version' ) );
 108              // We already have the theme, fall through.
 109              return $install_result;
 110          }
 111  
 112          // We don't have the parent theme, let's install it.
 113          $api = themes_api(
 114              'theme_information',
 115              array(
 116                  'slug'   => $theme_info->get( 'Template' ),
 117                  'fields' => array(
 118                      'sections' => false,
 119                      'tags'     => false,
 120                  ),
 121              )
 122          ); // Save on a bit of bandwidth.
 123  
 124          if ( ! $api || is_wp_error( $api ) ) {
 125              $this->skin->feedback( 'parent_theme_not_found', $theme_info->get( 'Template' ) );
 126              // Don't show activate or preview actions after installation.
 127              add_filter( 'install_theme_complete_actions', array( $this, 'hide_activate_preview_actions' ) );
 128              return $install_result;
 129          }
 130  
 131          // Backup required data we're going to override:
 132          $child_api             = $this->skin->api;
 133          $child_success_message = $this->strings['process_success'];
 134  
 135          // Override them.
 136          $this->skin->api = $api;
 137  
 138          $this->strings['process_success_specific'] = $this->strings['parent_theme_install_success']; //, $api->name, $api->version );
 139  
 140          $this->skin->feedback( 'parent_theme_prepare_install', $api->name, $api->version );
 141  
 142          add_filter( 'install_theme_complete_actions', '__return_false', 999 ); // Don't show any actions after installing the theme.
 143  
 144          // Install the parent theme.
 145          $parent_result = $this->run(
 146              array(
 147                  'package'           => $api->download_link,
 148                  'destination'       => get_theme_root(),
 149                  'clear_destination' => false, // Do not overwrite files.
 150                  'clear_working'     => true,
 151              )
 152          );
 153  
 154          if ( is_wp_error( $parent_result ) ) {
 155              add_filter( 'install_theme_complete_actions', array( $this, 'hide_activate_preview_actions' ) );
 156          }
 157  
 158          // Start cleaning up after the parent's installation.
 159          remove_filter( 'install_theme_complete_actions', '__return_false', 999 );
 160  
 161          // Reset child's result and data.
 162          $this->result                     = $child_result;
 163          $this->skin->api                  = $child_api;
 164          $this->strings['process_success'] = $child_success_message;
 165  
 166          return $install_result;
 167      }
 168  
 169      /**
 170       * Don't display the activate and preview actions to the user.
 171       *
 172       * Hooked to the {@see 'install_theme_complete_actions'} filter by
 173       * Theme_Upgrader::check_parent_theme_filter() when installing
 174       * a child theme and installing the parent theme fails.
 175       *
 176       * @since 3.4.0
 177       *
 178       * @param array $actions Preview actions.
 179       * @return array
 180       */
 181  	public function hide_activate_preview_actions( $actions ) {
 182          unset( $actions['activate'], $actions['preview'] );
 183          return $actions;
 184      }
 185  
 186      /**
 187       * Install a theme package.
 188       *
 189       * @since 2.8.0
 190       * @since 3.7.0 The `$args` parameter was added, making clearing the update cache optional.
 191       *
 192       * @param string $package The full local path or URI of the package.
 193       * @param array  $args {
 194       *     Optional. Other arguments for installing a theme package. Default empty array.
 195       *
 196       *     @type bool $clear_update_cache Whether to clear the updates cache if successful.
 197       *                                    Default true.
 198       * }
 199       *
 200       * @return bool|WP_Error True if the installation was successful, false or a WP_Error object otherwise.
 201       */
 202  	public function install( $package, $args = array() ) {
 203  
 204          $defaults    = array(
 205              'clear_update_cache' => true,
 206          );
 207          $parsed_args = wp_parse_args( $args, $defaults );
 208  
 209          $this->init();
 210          $this->install_strings();
 211  
 212          add_filter( 'upgrader_source_selection', array( $this, 'check_package' ) );
 213          add_filter( 'upgrader_post_install', array( $this, 'check_parent_theme_filter' ), 10, 3 );
 214          if ( $parsed_args['clear_update_cache'] ) {
 215              // Clear cache so wp_update_themes() knows about the new theme.
 216              add_action( 'upgrader_process_complete', 'wp_clean_themes_cache', 9, 0 );
 217          }
 218  
 219          $this->run(
 220              array(
 221                  'package'           => $package,
 222                  'destination'       => get_theme_root(),
 223                  'clear_destination' => false, // Do not overwrite files.
 224                  'clear_working'     => true,
 225                  'hook_extra'        => array(
 226                      'type'   => 'theme',
 227                      'action' => 'install',
 228                  ),
 229              )
 230          );
 231  
 232          remove_action( 'upgrader_process_complete', 'wp_clean_themes_cache', 9 );
 233          remove_filter( 'upgrader_source_selection', array( $this, 'check_package' ) );
 234          remove_filter( 'upgrader_post_install', array( $this, 'check_parent_theme_filter' ) );
 235  
 236          if ( ! $this->result || is_wp_error( $this->result ) ) {
 237              return $this->result;
 238          }
 239  
 240          // Refresh the Theme Update information.
 241          wp_clean_themes_cache( $parsed_args['clear_update_cache'] );
 242  
 243          return true;
 244      }
 245  
 246      /**
 247       * Upgrade a theme.
 248       *
 249       * @since 2.8.0
 250       * @since 3.7.0 The `$args` parameter was added, making clearing the update cache optional.
 251       *
 252       * @param string $theme The theme slug.
 253       * @param array  $args {
 254       *     Optional. Other arguments for upgrading a theme. Default empty array.
 255       *
 256       *     @type bool $clear_update_cache Whether to clear the update cache if successful.
 257       *                                    Default true.
 258       * }
 259       * @return bool|WP_Error True if the upgrade was successful, false or a WP_Error object otherwise.
 260       */
 261  	public function upgrade( $theme, $args = array() ) {
 262  
 263          $defaults    = array(
 264              'clear_update_cache' => true,
 265          );
 266          $parsed_args = wp_parse_args( $args, $defaults );
 267  
 268          $this->init();
 269          $this->upgrade_strings();
 270  
 271          // Is an update available?
 272          $current = get_site_transient( 'update_themes' );
 273          if ( ! isset( $current->response[ $theme ] ) ) {
 274              $this->skin->before();
 275              $this->skin->set_result( false );
 276              $this->skin->error( 'up_to_date' );
 277              $this->skin->after();
 278              return false;
 279          }
 280  
 281          $r = $current->response[ $theme ];
 282  
 283          add_filter( 'upgrader_pre_install', array( $this, 'current_before' ), 10, 2 );
 284          add_filter( 'upgrader_post_install', array( $this, 'current_after' ), 10, 2 );
 285          add_filter( 'upgrader_clear_destination', array( $this, 'delete_old_theme' ), 10, 4 );
 286          if ( $parsed_args['clear_update_cache'] ) {
 287              // Clear cache so wp_update_themes() knows about the new theme.
 288              add_action( 'upgrader_process_complete', 'wp_clean_themes_cache', 9, 0 );
 289          }
 290  
 291          $this->run(
 292              array(
 293                  'package'           => $r['package'],
 294                  'destination'       => get_theme_root( $theme ),
 295                  'clear_destination' => true,
 296                  'clear_working'     => true,
 297                  'hook_extra'        => array(
 298                      'theme'  => $theme,
 299                      'type'   => 'theme',
 300                      'action' => 'update',
 301                  ),
 302              )
 303          );
 304  
 305          remove_action( 'upgrader_process_complete', 'wp_clean_themes_cache', 9 );
 306          remove_filter( 'upgrader_pre_install', array( $this, 'current_before' ) );
 307          remove_filter( 'upgrader_post_install', array( $this, 'current_after' ) );
 308          remove_filter( 'upgrader_clear_destination', array( $this, 'delete_old_theme' ) );
 309  
 310          if ( ! $this->result || is_wp_error( $this->result ) ) {
 311              return $this->result;
 312          }
 313  
 314          wp_clean_themes_cache( $parsed_args['clear_update_cache'] );
 315  
 316          return true;
 317      }
 318  
 319      /**
 320       * Upgrade several themes at once.
 321       *
 322       * @since 3.0.0
 323       * @since 3.7.0 The `$args` parameter was added, making clearing the update cache optional.
 324       *
 325       * @param string[] $themes Array of the theme slugs.
 326       * @param array    $args {
 327       *     Optional. Other arguments for upgrading several themes at once. Default empty array.
 328       *
 329       *     @type bool $clear_update_cache Whether to clear the update cache if successful.
 330       *                                    Default true.
 331       * }
 332       * @return array[]|false An array of results, or false if unable to connect to the filesystem.
 333       */
 334  	public function bulk_upgrade( $themes, $args = array() ) {
 335  
 336          $defaults    = array(
 337              'clear_update_cache' => true,
 338          );
 339          $parsed_args = wp_parse_args( $args, $defaults );
 340  
 341          $this->init();
 342          $this->bulk = true;
 343          $this->upgrade_strings();
 344  
 345          $current = get_site_transient( 'update_themes' );
 346  
 347          add_filter( 'upgrader_pre_install', array( $this, 'current_before' ), 10, 2 );
 348          add_filter( 'upgrader_post_install', array( $this, 'current_after' ), 10, 2 );
 349          add_filter( 'upgrader_clear_destination', array( $this, 'delete_old_theme' ), 10, 4 );
 350  
 351          $this->skin->header();
 352  
 353          // Connect to the filesystem first.
 354          $res = $this->fs_connect( array( WP_CONTENT_DIR ) );
 355          if ( ! $res ) {
 356              $this->skin->footer();
 357              return false;
 358          }
 359  
 360          $this->skin->bulk_header();
 361  
 362          /*
 363           * Only start maintenance mode if:
 364           * - running Multisite and there are one or more themes specified, OR
 365           * - a theme with an update available is currently in use.
 366           * @todo For multisite, maintenance mode should only kick in for individual sites if at all possible.
 367           */
 368          $maintenance = ( is_multisite() && ! empty( $themes ) );
 369          foreach ( $themes as $theme ) {
 370              $maintenance = $maintenance || get_stylesheet() === $theme || get_template() === $theme;
 371          }
 372          if ( $maintenance ) {
 373              $this->maintenance_mode( true );
 374          }
 375  
 376          $results = array();
 377  
 378          $this->update_count   = count( $themes );
 379          $this->update_current = 0;
 380          foreach ( $themes as $theme ) {
 381              $this->update_current++;
 382  
 383              $this->skin->theme_info = $this->theme_info( $theme );
 384  
 385              if ( ! isset( $current->response[ $theme ] ) ) {
 386                  $this->skin->set_result( true );
 387                  $this->skin->before();
 388                  $this->skin->feedback( 'up_to_date' );
 389                  $this->skin->after();
 390                  $results[ $theme ] = true;
 391                  continue;
 392              }
 393  
 394              // Get the URL to the zip file.
 395              $r = $current->response[ $theme ];
 396  
 397              $result = $this->run(
 398                  array(
 399                      'package'           => $r['package'],
 400                      'destination'       => get_theme_root( $theme ),
 401                      'clear_destination' => true,
 402                      'clear_working'     => true,
 403                      'is_multi'          => true,
 404                      'hook_extra'        => array(
 405                          'theme' => $theme,
 406                      ),
 407                  )
 408              );
 409  
 410              $results[ $theme ] = $this->result;
 411  
 412              // Prevent credentials auth screen from displaying multiple times.
 413              if ( false === $result ) {
 414                  break;
 415              }
 416          } // End foreach $themes.
 417  
 418          $this->maintenance_mode( false );
 419  
 420          // Refresh the Theme Update information.
 421          wp_clean_themes_cache( $parsed_args['clear_update_cache'] );
 422  
 423          /** This action is documented in wp-admin/includes/class-wp-upgrader.php */
 424          do_action(
 425              'upgrader_process_complete',
 426              $this,
 427              array(
 428                  'action' => 'update',
 429                  'type'   => 'theme',
 430                  'bulk'   => true,
 431                  'themes' => $themes,
 432              )
 433          );
 434  
 435          $this->skin->bulk_footer();
 436  
 437          $this->skin->footer();
 438  
 439          // Cleanup our hooks, in case something else does a upgrade on this connection.
 440          remove_filter( 'upgrader_pre_install', array( $this, 'current_before' ) );
 441          remove_filter( 'upgrader_post_install', array( $this, 'current_after' ) );
 442          remove_filter( 'upgrader_clear_destination', array( $this, 'delete_old_theme' ) );
 443  
 444          return $results;
 445      }
 446  
 447      /**
 448       * Check that the package source contains a valid theme.
 449       *
 450       * Hooked to the {@see 'upgrader_source_selection'} filter by Theme_Upgrader::install().
 451       * It will return an error if the theme doesn't have style.css or index.php
 452       * files.
 453       *
 454       * @since 3.3.0
 455       *
 456       * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
 457       *
 458       * @param string $source The full path to the package source.
 459       * @return string|WP_Error The source or a WP_Error.
 460       */
 461  	public function check_package( $source ) {
 462          global $wp_filesystem;
 463  
 464          if ( is_wp_error( $source ) ) {
 465              return $source;
 466          }
 467  
 468          // Check that the folder contains a valid theme.
 469          $working_directory = str_replace( $wp_filesystem->wp_content_dir(), trailingslashit( WP_CONTENT_DIR ), $source );
 470          if ( ! is_dir( $working_directory ) ) { // Sanity check, if the above fails, let's not prevent installation.
 471              return $source;
 472          }
 473  
 474          // A proper archive should have a style.css file in the single subdirectory.
 475          if ( ! file_exists( $working_directory . 'style.css' ) ) {
 476              return new WP_Error(
 477                  'incompatible_archive_theme_no_style',
 478                  $this->strings['incompatible_archive'],
 479                  sprintf(
 480                      /* translators: %s: style.css */
 481                      __( 'The theme is missing the %s stylesheet.' ),
 482                      '<code>style.css</code>'
 483                  )
 484              );
 485          }
 486  
 487          $info = get_file_data(
 488              $working_directory . 'style.css',
 489              array(
 490                  'Name'     => 'Theme Name',
 491                  'Template' => 'Template',
 492              )
 493          );
 494  
 495          if ( empty( $info['Name'] ) ) {
 496              return new WP_Error(
 497                  'incompatible_archive_theme_no_name',
 498                  $this->strings['incompatible_archive'],
 499                  sprintf(
 500                      /* translators: %s: style.css */
 501                      __( 'The %s stylesheet doesn&#8217;t contain a valid theme header.' ),
 502                      '<code>style.css</code>'
 503                  )
 504              );
 505          }
 506  
 507          // If it's not a child theme, it must have at least an index.php to be legit.
 508          if ( empty( $info['Template'] ) && ! file_exists( $working_directory . 'index.php' ) ) {
 509              return new WP_Error(
 510                  'incompatible_archive_theme_no_index',
 511                  $this->strings['incompatible_archive'],
 512                  sprintf(
 513                      /* translators: %s: index.php */
 514                      __( 'The theme is missing the %s file.' ),
 515                      '<code>index.php</code>'
 516                  )
 517              );
 518          }
 519  
 520          return $source;
 521      }
 522  
 523      /**
 524       * Turn on maintenance mode before attempting to upgrade the current theme.
 525       *
 526       * Hooked to the {@see 'upgrader_pre_install'} filter by Theme_Upgrader::upgrade() and
 527       * Theme_Upgrader::bulk_upgrade().
 528       *
 529       * @since 2.8.0
 530       *
 531       * @param bool|WP_Error $return Upgrade offer return.
 532       * @param array         $theme  Theme arguments.
 533       * @return bool|WP_Error The passed in $return param or WP_Error.
 534       */
 535  	public function current_before( $return, $theme ) {
 536          if ( is_wp_error( $return ) ) {
 537              return $return;
 538          }
 539  
 540          $theme = isset( $theme['theme'] ) ? $theme['theme'] : '';
 541  
 542          // Only run if current theme
 543          if ( get_stylesheet() !== $theme ) {
 544              return $return;
 545          }
 546  
 547          // Change to maintenance mode. Bulk edit handles this separately.
 548          if ( ! $this->bulk ) {
 549              $this->maintenance_mode( true );
 550          }
 551  
 552          return $return;
 553      }
 554  
 555      /**
 556       * Turn off maintenance mode after upgrading the current theme.
 557       *
 558       * Hooked to the {@see 'upgrader_post_install'} filter by Theme_Upgrader::upgrade()
 559       * and Theme_Upgrader::bulk_upgrade().
 560       *
 561       * @since 2.8.0
 562       *
 563       * @param bool|WP_Error $return Upgrade offer return.
 564       * @param array         $theme  Theme arguments.
 565       * @return bool|WP_Error The passed in $return param or WP_Error.
 566       */
 567  	public function current_after( $return, $theme ) {
 568          if ( is_wp_error( $return ) ) {
 569              return $return;
 570          }
 571  
 572          $theme = isset( $theme['theme'] ) ? $theme['theme'] : '';
 573  
 574          // Only run if current theme.
 575          if ( get_stylesheet() !== $theme ) {
 576              return $return;
 577          }
 578  
 579          // Ensure stylesheet name hasn't changed after the upgrade:
 580          if ( get_stylesheet() === $theme && $theme !== $this->result['destination_name'] ) {
 581              wp_clean_themes_cache();
 582              $stylesheet = $this->result['destination_name'];
 583              switch_theme( $stylesheet );
 584          }
 585  
 586          // Time to remove maintenance mode. Bulk edit handles this separately.
 587          if ( ! $this->bulk ) {
 588              $this->maintenance_mode( false );
 589          }
 590          return $return;
 591      }
 592  
 593      /**
 594       * Delete the old theme during an upgrade.
 595       *
 596       * Hooked to the {@see 'upgrader_clear_destination'} filter by Theme_Upgrader::upgrade()
 597       * and Theme_Upgrader::bulk_upgrade().
 598       *
 599       * @since 2.8.0
 600       *
 601       * @global WP_Filesystem_Base $wp_filesystem Subclass
 602       *
 603       * @param bool   $removed
 604       * @param string $local_destination
 605       * @param string $remote_destination
 606       * @param array  $theme
 607       * @return bool
 608       */
 609  	public function delete_old_theme( $removed, $local_destination, $remote_destination, $theme ) {
 610          global $wp_filesystem;
 611  
 612          if ( is_wp_error( $removed ) ) {
 613              return $removed; // Pass errors through.
 614          }
 615  
 616          if ( ! isset( $theme['theme'] ) ) {
 617              return $removed;
 618          }
 619  
 620          $theme      = $theme['theme'];
 621          $themes_dir = trailingslashit( $wp_filesystem->wp_themes_dir( $theme ) );
 622          if ( $wp_filesystem->exists( $themes_dir . $theme ) ) {
 623              if ( ! $wp_filesystem->delete( $themes_dir . $theme, true ) ) {
 624                  return false;
 625              }
 626          }
 627  
 628          return true;
 629      }
 630  
 631      /**
 632       * Get the WP_Theme object for a theme.
 633       *
 634       * @since 2.8.0
 635       * @since 3.0.0 The `$theme` argument was added.
 636       *
 637       * @param string $theme The directory name of the theme. This is optional, and if not supplied,
 638       *                      the directory name from the last result will be used.
 639       * @return WP_Theme|false The theme's info object, or false `$theme` is not supplied
 640       *                        and the last result isn't set.
 641       */
 642  	public function theme_info( $theme = null ) {
 643  
 644          if ( empty( $theme ) ) {
 645              if ( ! empty( $this->result['destination_name'] ) ) {
 646                  $theme = $this->result['destination_name'];
 647              } else {
 648                  return false;
 649              }
 650          }
 651          return wp_get_theme( $theme );
 652      }
 653  
 654  }


Generated: Mon Jun 1 01:00:03 2020 Cross-referenced by PHPXref 0.7.1