[ Index ]

PHP Cross Reference of BuddyPress

title

Body

[close]

/src/bp-core/admin/ -> bp-core-admin-tools.php (source)

   1  <?php
   2  /**
   3   * BuddyPress Tools panel.
   4   *
   5   * @package BuddyPress
   6   * @subpackage Core
   7   * @since 2.0.0
   8   */
   9  
  10  // Exit if accessed directly.
  11  defined( 'ABSPATH' ) || exit;
  12  
  13  /**
  14   * Render the BuddyPress Tools page.
  15   *
  16   * @since 2.0.0
  17   */
  18  function bp_core_admin_tools() {
  19      ?>
  20      <div class="wrap">
  21  
  22          <h1><?php esc_html_e( 'BuddyPress Tools', 'buddypress' ) ?></h1>
  23  
  24          <p><?php esc_html_e( 'BuddyPress keeps track of various relationships between members, groups, and activity items. Occasionally these relationships become out of sync, most often after an import, update, or migration.', 'buddypress' ); ?></p>
  25          <p><?php esc_html_e( 'Use the tools below to manually recalculate these relationships.', 'buddypress' ); ?>
  26          </p>
  27          <p class="description"><?php esc_html_e( 'Some of these tools create substantial database overhead. Avoid running more than one repair job at a time.', 'buddypress' ); ?></p>
  28  
  29          <form class="settings" method="post" action="">
  30  
  31              <fieldset>
  32                  <legend><?php esc_html_e( 'Repair tools', 'buddypress' ) ?></legend>
  33  
  34                  <div class="checkbox">
  35                  <?php foreach ( bp_admin_repair_list() as $item ) : ?>
  36                      <label for="<?php echo esc_attr( str_replace( '_', '-', $item[0] ) ); ?>"><input type="checkbox" class="checkbox" name="<?php echo esc_attr( $item[0] ) . '" id="' . esc_attr( str_replace( '_', '-', $item[0] ) ); ?>" value="1" /> <?php echo esc_html( $item[1] ); ?></label>
  37                  <?php endforeach; ?>
  38                  </div>
  39  
  40                  <p class="submit">
  41                      <input class="button-primary" type="submit" name="bp-tools-submit" value="<?php esc_attr_e( 'Repair Items', 'buddypress' ); ?>" />
  42                      <?php wp_nonce_field( 'bp-do-counts' ); ?>
  43                  </p>
  44  
  45              </fieldset>
  46  
  47          </form>
  48  
  49      </div>
  50  
  51      <?php
  52  }
  53  
  54  /**
  55   * Handle the processing and feedback of the admin tools page.
  56   *
  57   * @since 2.0.0
  58   */
  59  function bp_admin_repair_handler() {
  60      if ( ! bp_is_post_request() || empty( $_POST['bp-tools-submit'] ) ) {
  61          return;
  62      }
  63  
  64      check_admin_referer( 'bp-do-counts' );
  65  
  66      // Bail if user cannot moderate.
  67      $capability = bp_core_do_network_admin() ? 'manage_network_options' : 'manage_options';
  68      if ( ! bp_current_user_can( $capability ) ) {
  69          return;
  70      }
  71  
  72      wp_cache_flush();
  73      $messages = array();
  74  
  75      foreach ( (array) bp_admin_repair_list() as $item ) {
  76          if ( isset( $item[2] ) && isset( $_POST[$item[0]] ) && 1 === absint( $_POST[$item[0]] ) && is_callable( $item[2] ) ) {
  77              $messages[] = call_user_func( $item[2] );
  78          }
  79      }
  80  
  81      if ( count( $messages ) ) {
  82          foreach ( $messages as $message ) {
  83              bp_admin_tools_feedback( $message[1] );
  84          }
  85      }
  86  }
  87  add_action( bp_core_admin_hook(), 'bp_admin_repair_handler' );
  88  
  89  /**
  90   * Get the array of the repair list.
  91   *
  92   * @return array
  93   */
  94  function bp_admin_repair_list() {
  95      $repair_list = array();
  96  
  97      // Members:
  98      // - member count
  99      // - last_activity migration (2.0).
 100      $repair_list[20] = array(
 101          'bp-total-member-count',
 102          __( 'Repair total members count.', 'buddypress' ),
 103          'bp_admin_repair_count_members',
 104      );
 105  
 106      $repair_list[25] = array(
 107          'bp-last-activity',
 108          __( 'Repair member "last activity" data.', 'buddypress' ),
 109          'bp_admin_repair_last_activity',
 110      );
 111  
 112      // Friends:
 113      // - user friend count.
 114      if ( bp_is_active( 'friends' ) ) {
 115          $repair_list[0] = array(
 116              'bp-user-friends',
 117              __( 'Repair total friends count for each member.', 'buddypress' ),
 118              'bp_admin_repair_friend_count',
 119          );
 120      }
 121  
 122      // Groups:
 123      // - user group count.
 124      if ( bp_is_active( 'groups' ) ) {
 125          $repair_list[10] = array(
 126              'bp-group-count',
 127              __( 'Repair total groups count for each member.', 'buddypress' ),
 128              'bp_admin_repair_group_count',
 129          );
 130      }
 131  
 132      // Blogs:
 133      // - user blog count.
 134      if ( bp_is_active( 'blogs' ) ) {
 135          $repair_list[90] = array(
 136              'bp-blog-records',
 137              __( 'Repopulate site tracking records.', 'buddypress' ),
 138              'bp_admin_repair_blog_records',
 139          );
 140      }
 141  
 142      // Emails:
 143      // - reinstall emails.
 144      $repair_list[100] = array(
 145          'bp-reinstall-emails',
 146          __( 'Reinstall emails (delete and restore from defaults).', 'buddypress' ),
 147          'bp_admin_reinstall_emails',
 148      );
 149  
 150      // Invitations:
 151      // - maybe create the database table and migrate any existing group invitations.
 152      $repair_list[110] = array(
 153          'bp-invitations-table',
 154          __( 'Create the database table for Invitations and migrate existing group invitations if needed.', 'buddypress' ),
 155          'bp_admin_invitations_table',
 156      );
 157  
 158      ksort( $repair_list );
 159  
 160      /**
 161       * Filters the array of the repair list.
 162       *
 163       * @since 2.0.0
 164       *
 165       * @param array $repair_list Array of values for the Repair list options.
 166       */
 167      return (array) apply_filters( 'bp_repair_list', $repair_list );
 168  }
 169  
 170  /**
 171   * Recalculate friend counts for each user.
 172   *
 173   * @since 2.0.0
 174   *
 175   * @return array
 176   */
 177  function bp_admin_repair_friend_count() {
 178      global $wpdb;
 179  
 180      if ( ! bp_is_active( 'friends' ) ) {
 181          return;
 182      }
 183  
 184      /* translators: %s: the result of the action performed by the repair tool */
 185      $statement = __( 'Counting the number of friends for each user&hellip; %s', 'buddypress' );
 186      $result    = __( 'Failed!', 'buddypress' );
 187  
 188      $sql_delete = "DELETE FROM {$wpdb->usermeta} WHERE meta_key IN ( 'total_friend_count' );";
 189      if ( is_wp_error( $wpdb->query( $sql_delete ) ) ) {
 190          return array( 1, sprintf( $statement, $result ) );
 191      }
 192  
 193      $bp = buddypress();
 194  
 195      // Walk through all users on the site.
 196      $total_users = $wpdb->get_row( "SELECT count(ID) as c FROM {$wpdb->users}" )->c;
 197  
 198      $updated = array();
 199      if ( $total_users > 0 ) {
 200          $per_query = 500;
 201          $offset = 0;
 202          while ( $offset < $total_users ) {
 203              // Only bother updating counts for users who actually have friendships.
 204              $friendships = $wpdb->get_results( $wpdb->prepare( "SELECT initiator_user_id, friend_user_id FROM {$bp->friends->table_name} WHERE is_confirmed = 1 AND ( ( initiator_user_id > %d AND initiator_user_id <= %d ) OR ( friend_user_id > %d AND friend_user_id <= %d ) )", $offset, $offset + $per_query, $offset, $offset + $per_query ) );
 205  
 206              // The previous query will turn up duplicates, so we
 207              // filter them here.
 208              foreach ( $friendships as $friendship ) {
 209                  if ( ! isset( $updated[ $friendship->initiator_user_id ] ) ) {
 210                      BP_Friends_Friendship::total_friend_count( $friendship->initiator_user_id );
 211                      $updated[ $friendship->initiator_user_id ] = 1;
 212                  }
 213  
 214                  if ( ! isset( $updated[ $friendship->friend_user_id ] ) ) {
 215                      BP_Friends_Friendship::total_friend_count( $friendship->friend_user_id );
 216                      $updated[ $friendship->friend_user_id ] = 1;
 217                  }
 218              }
 219  
 220              $offset += $per_query;
 221          }
 222      } else {
 223          return array( 2, sprintf( $statement, $result ) );
 224      }
 225  
 226      return array( 0, sprintf( $statement, __( 'Complete!', 'buddypress' ) ) );
 227  }
 228  
 229  /**
 230   * Recalculate group counts for each user.
 231   *
 232   * @since 2.0.0
 233   *
 234   * @return array
 235   */
 236  function bp_admin_repair_group_count() {
 237      global $wpdb;
 238  
 239      if ( ! bp_is_active( 'groups' ) ) {
 240          return;
 241      }
 242  
 243      /* translators: %s: the result of the action performed by the repair tool */
 244      $statement = __( 'Counting the number of groups for each user&hellip; %s', 'buddypress' );
 245      $result    = __( 'Failed!', 'buddypress' );
 246  
 247      $sql_delete = "DELETE FROM {$wpdb->usermeta} WHERE meta_key IN ( 'total_group_count' );";
 248      if ( is_wp_error( $wpdb->query( $sql_delete ) ) ) {
 249          return array( 1, sprintf( $statement, $result ) );
 250      }
 251  
 252      $bp = buddypress();
 253  
 254      // Walk through all users on the site.
 255      $total_users = $wpdb->get_row( "SELECT count(ID) as c FROM {$wpdb->users}" )->c;
 256  
 257      if ( $total_users > 0 ) {
 258          $per_query = 500;
 259          $offset = 0;
 260          while ( $offset < $total_users ) {
 261              // But only bother to update counts for users that have groups.
 262              $users = $wpdb->get_col( $wpdb->prepare( "SELECT user_id FROM {$bp->groups->table_name_members} WHERE is_confirmed = 1 AND is_banned = 0 AND user_id > %d AND user_id <= %d", $offset, $offset + $per_query ) );
 263  
 264              foreach ( $users as $user ) {
 265                  BP_Groups_Member::refresh_total_group_count_for_user( $user );
 266              }
 267  
 268              $offset += $per_query;
 269          }
 270      } else {
 271          return array( 2, sprintf( $statement, $result ) );
 272      }
 273  
 274      return array( 0, sprintf( $statement, __( 'Complete!', 'buddypress' ) ) );
 275  }
 276  
 277  /**
 278   * Recalculate user-to-blog relationships and useful blog meta data.
 279   *
 280   * @since 2.1.0
 281   *
 282   * @return array
 283   */
 284  function bp_admin_repair_blog_records() {
 285  
 286      /* translators: %s: the result of the action performed by the repair tool */
 287      $statement = __( 'Repopulating Blogs records&hellip; %s', 'buddypress' );
 288  
 289      // Default to failure text.
 290      $result    = __( 'Failed!',   'buddypress' );
 291  
 292      // Default to unrepaired.
 293      $repair    = false;
 294  
 295      // Run function if blogs component is active.
 296      if ( bp_is_active( 'blogs' ) ) {
 297          $repair = bp_blogs_record_existing_blogs();
 298      }
 299  
 300      // Setup success/fail messaging.
 301      if ( true === $repair ) {
 302          $result = __( 'Complete!', 'buddypress' );
 303      }
 304  
 305      // All done!
 306      return array( 0, sprintf( $statement, $result ) );
 307  }
 308  
 309  /**
 310   * Recalculate the total number of active site members.
 311   *
 312   * @since 2.0.0
 313   */
 314  function bp_admin_repair_count_members() {
 315      /* translators: %s: the result of the action performed by the repair tool */
 316      $statement = __( 'Counting the number of active members on the site&hellip; %s', 'buddypress' );
 317      delete_transient( 'bp_active_member_count' );
 318      bp_core_get_active_member_count();
 319      return array( 0, sprintf( $statement, __( 'Complete!', 'buddypress' ) ) );
 320  }
 321  
 322  /**
 323   * Repair user last_activity data.
 324   *
 325   * Re-runs the migration from usermeta introduced in BP 2.0.
 326   *
 327   * @since 2.0.0
 328   */
 329  function bp_admin_repair_last_activity() {
 330      /* translators: %s: the result of the action performed by the repair tool */
 331      $statement = __( 'Determining last activity dates for each user&hellip; %s', 'buddypress' );
 332      bp_last_activity_migrate();
 333      return array( 0, sprintf( $statement, __( 'Complete!', 'buddypress' ) ) );
 334  }
 335  
 336  /**
 337   * Create the invitations database table if it does not exist.
 338   * Migrate outstanding group invitations if needed.
 339   *
 340   * @since 6.0.0
 341   *
 342   * @return array
 343   */
 344  function bp_admin_invitations_table() {
 345      global $wpdb;
 346  
 347      require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
 348      require_once( buddypress()->plugin_dir . '/bp-core/admin/bp-core-admin-schema.php' );
 349  
 350      /* translators: %s: the result of the action performed by the repair tool */
 351      $statement = __( 'Creating the Invitations database table if it does not exist&hellip; %s', 'buddypress' );
 352      $result    = __( 'Failed to create table!', 'buddypress' );
 353  
 354      bp_core_install_invitations();
 355  
 356      // Check for existence of invitations table.
 357      $bp_prefix  = bp_core_get_table_prefix();
 358      $table_name = "{$bp_prefix}bp_invitations";
 359      $query = $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $table_name ) );
 360      if ( ! $wpdb->get_var( $query ) == $table_name ) {
 361          // Early return if table creation failed.
 362          return array( 2, sprintf( $statement, $result ) );
 363      } else {
 364          $result = __( 'Created invitations table!', 'buddypress' );
 365      }
 366  
 367      // Migrate group invitations if needed.
 368      if ( bp_is_active( 'groups' ) ) {
 369          $bp = buddypress();
 370  
 371          /* translators: %s: the result of the action performed by the repair tool */
 372          $migrate_statement = __( 'Migrating group invitations&hellip; %s', 'buddypress' );
 373          $migrate_result    = __( 'Failed to migrate invitations!', 'buddypress' );
 374  
 375          bp_groups_migrate_invitations();
 376  
 377          // Check that there are no outstanding group invites in the group_members table.
 378          $records = $wpdb->get_results( "SELECT id FROM {$bp->groups->table_name_members} WHERE is_confirmed = 0 AND is_banned = 0" );
 379          if ( empty( $records ) ) {
 380              $migrate_result = __( 'Migrated invitations!', 'buddypress' );
 381              return array( 0, sprintf( $statement . ' ' . $migrate_statement , $result, $migrate_result ) );
 382          } else {
 383              return array( 2, sprintf( $statement . ' ' . $migrate_statement , $result, $migrate_result ) );
 384          }
 385      }
 386  
 387      // Return a "create-only" success message.
 388      return array( 0, sprintf( $statement, $result ) );
 389  }
 390  
 391  /**
 392   * Assemble admin notices relating success/failure of repair processes.
 393   *
 394   * @since 2.0.0
 395   *
 396   * @param string      $message Feedback message.
 397   * @param string|bool $class   Unused.
 398   * @return false|Closure
 399   */
 400  function bp_admin_tools_feedback( $message, $class = false ) {
 401      if ( is_string( $message ) ) {
 402          $message = '<p>' . $message . '</p>';
 403          $class = $class ? $class : 'updated';
 404      } elseif ( is_wp_error( $message ) ) {
 405          $errors = $message->get_error_messages();
 406  
 407          switch ( count( $errors ) ) {
 408              case 0:
 409                  return false;
 410  
 411              case 1:
 412                  $message = '<p>' . $errors[0] . '</p>';
 413                  break;
 414  
 415              default:
 416                  $message = '<ul>' . "\n\t" . '<li>' . implode( '</li>' . "\n\t" . '<li>', $errors ) . '</li>' . "\n" . '</ul>';
 417                  break;
 418          }
 419  
 420          $class = $class ? $class : 'error';
 421      } else {
 422          return false;
 423      }
 424  
 425      $message = '<div id="message" class="' . esc_attr( $class ) . ' notice is-dismissible">' . $message . '</div>';
 426      $message = str_replace( "'", "\'", $message );
 427      $lambda  = function() use ( $message ) { echo $message; };
 428  
 429      add_action( bp_core_do_network_admin() ? 'network_admin_notices' : 'admin_notices', $lambda );
 430  
 431      return $lambda;
 432  }
 433  
 434  /**
 435   * Render the Available Tools page.
 436   *
 437   * We register this page on Network Admin as a top-level home for our
 438   * BuddyPress tools. This displays the default content.
 439   *
 440   * @since 2.0.0
 441   */
 442  function bp_core_admin_available_tools_page() {
 443      ?>
 444      <div class="wrap">
 445          <h1><?php esc_attr_e( 'Tools', 'buddypress' ) ?></h1>
 446  
 447          <?php
 448  
 449          /**
 450           * Fires inside the markup used to display the Available Tools page.
 451           *
 452           * @since 2.0.0
 453           */
 454          do_action( 'bp_network_tool_box' ); ?>
 455  
 456      </div>
 457      <?php
 458  }
 459  
 460  /**
 461   * Render an introduction of BuddyPress tools on Available Tools page.
 462   *
 463   * @since 2.0.0
 464   */
 465  function bp_core_admin_available_tools_intro() {
 466      $query_arg = array(
 467          'page' => 'bp-tools'
 468      );
 469  
 470      $page = bp_core_do_network_admin() ? 'admin.php' : 'tools.php' ;
 471      $url  = add_query_arg( $query_arg, bp_get_admin_url( $page ) );
 472      ?>
 473      <div class="card tool-box">
 474          <h2><?php esc_html_e( 'BuddyPress Tools', 'buddypress' ) ?></h2>
 475          <p>
 476              <?php esc_html_e( 'BuddyPress keeps track of various relationships between users, groups, and activity items. Occasionally these relationships become out of sync, most often after an import, update, or migration.', 'buddypress' ); ?>
 477              <?php
 478              printf(
 479                  /* translators: %s: the link to the BuddyPress repair tools */
 480                  esc_html_x( 'Use the %s to repair these relationships.', 'buddypress tools intro', 'buddypress' ),
 481                  '<a href="' . esc_url( $url ) . '">' . esc_html__( 'BuddyPress Tools', 'buddypress' ) . '</a>'
 482              );
 483              ?>
 484          </p>
 485      </div>
 486      <?php
 487  }
 488  
 489  /**
 490   * Delete emails and restore from defaults.
 491   *
 492   * @since 2.5.0
 493   *
 494   * @return array
 495   */
 496  function bp_admin_reinstall_emails() {
 497      $switched = false;
 498  
 499      // Switch to the root blog, where the email posts live.
 500      if ( ! bp_is_root_blog() ) {
 501          switch_to_blog( bp_get_root_blog_id() );
 502          bp_register_taxonomies();
 503  
 504          $switched = true;
 505      }
 506  
 507      $emails = get_posts( array(
 508          'fields'           => 'ids',
 509          'post_status'      => 'publish',
 510          'post_type'        => bp_get_email_post_type(),
 511          'posts_per_page'   => 200,
 512          'suppress_filters' => false,
 513      ) );
 514  
 515      if ( $emails ) {
 516          foreach ( $emails as $email_id ) {
 517              wp_trash_post( $email_id );
 518          }
 519      }
 520  
 521      // Make sure we have no orphaned email type terms.
 522      $email_types = get_terms( bp_get_email_tax_type(), array(
 523          'fields'                 => 'ids',
 524          'hide_empty'             => false,
 525          'update_term_meta_cache' => false,
 526      ) );
 527  
 528      if ( $email_types ) {
 529          foreach ( $email_types as $term_id ) {
 530              wp_delete_term( (int) $term_id, bp_get_email_tax_type() );
 531          }
 532      }
 533  
 534      require_once( buddypress()->plugin_dir . '/bp-core/admin/bp-core-admin-schema.php' );
 535      bp_core_install_emails();
 536  
 537      if ( $switched ) {
 538          restore_current_blog();
 539      }
 540  
 541      return array( 0, __( 'Emails have been successfully reinstalled.', 'buddypress' ) );
 542  }
 543  
 544  /**
 545   * Add notice on the "Tools > BuddyPress" page if more sites need recording.
 546   *
 547   * This notice only shows up in the network admin dashboard.
 548   *
 549   * @since 2.6.0
 550   */
 551  function bp_core_admin_notice_repopulate_blogs_resume() {
 552      $screen = get_current_screen();
 553      if ( 'tools_page_bp-tools-network' !== $screen->id ) {
 554          return;
 555      }
 556  
 557      if ( '' === bp_get_option( '_bp_record_blogs_offset' ) ) {
 558          return;
 559      }
 560  
 561      echo '<div class="error"><p>' . __( 'It looks like you have more sites to record. Resume recording by checking the "Repopulate site tracking records" option.', 'buddypress' ) . '</p></div>';
 562  }
 563  add_action( 'network_admin_notices', 'bp_core_admin_notice_repopulate_blogs_resume' );
 564  
 565  /**
 566   * Add BuddyPress debug info to the WordPress Site Health info screen.
 567   *
 568   * @since 5.0.0
 569   *
 570   * @param  array $debug_info The Site's debug info.
 571   * @return array             The Site's debug info, including the BuddyPress specific ones.
 572   */
 573  function bp_core_admin_debug_information( $debug_info = array() ) {
 574      global $wp_settings_fields;
 575      $active_components = array_intersect_key( bp_core_get_components(), buddypress()->active_components );
 576      $bp_settings       = array();
 577  
 578      foreach ( $wp_settings_fields['buddypress'] as $section => $settings ) {
 579          $prefix       = '';
 580          $component_id = str_replace( 'bp_', '', $section );
 581  
 582          if ( isset( $active_components[ $component_id ]['title'] ) ) {
 583              $prefix = $active_components[ $component_id ]['title'] .': ';
 584          }
 585  
 586          foreach( $settings as $bp_setting ) {
 587              $reverse = (
 588                  strpos( $bp_setting['id'], 'hide' ) !== false ||
 589                  strpos( $bp_setting['id'], 'restrict' ) !== false ||
 590                  strpos( $bp_setting['id'], 'disable' ) !== false
 591              );
 592  
 593              if ( ! isset( $bp_setting['id'] ) || '_bp_theme_package_id' === $bp_setting['id'] ) {
 594                  continue;
 595              }
 596  
 597              $bp_setting_value = bp_get_option( $bp_setting['id'] );
 598              if ( '0' === $bp_setting_value || '1' === $bp_setting_value ) {
 599                  if ( ( $reverse && '0' === $bp_setting_value ) || ( ! $reverse && '1' === $bp_setting_value ) ) {
 600                      $bp_setting_value = __( 'Yes', 'buddypress' );
 601                  } else {
 602                      $bp_setting_value = __( 'No', 'buddypress' );
 603                  }
 604              }
 605  
 606              // Make sure to show the setting is reversed when site info is copied to clipboard.
 607              $bp_settings_id = $bp_setting['id'];
 608              if ( $reverse ) {
 609                  $bp_settings_id = '! ' . $bp_settings_id;
 610              }
 611  
 612              $bp_settings[ $bp_settings_id ] = array(
 613                  'label' => $prefix . $bp_setting['title'],
 614                  'value' => $bp_setting_value,
 615              );
 616          }
 617      }
 618  
 619      $debug_info['buddypress'] = array(
 620          'label'  => __( 'BuddyPress', 'buddypress' ),
 621          'fields' => array_merge(
 622              array(
 623                  'version' => array(
 624                      'label' => __( 'Version', 'buddypress' ),
 625                      'value' => bp_get_version(),
 626                  ),
 627                  'active_components' => array(
 628                      'label' => __( 'Active components', 'buddypress' ),
 629                      'value' => implode( wp_list_pluck( $active_components, 'title' ), ', ' ),
 630                  ),
 631                  'template_pack' => array(
 632                      'label' => __( 'Active template pack', 'buddypress' ),
 633                      'value' => bp_get_theme_compat_name() . ' ' . bp_get_theme_compat_version(),
 634                  ),
 635              ),
 636              $bp_settings
 637          )
 638      );
 639  
 640      return $debug_info;
 641  }
 642  add_filter( 'debug_information', 'bp_core_admin_debug_information' );


Generated: Sun Jul 5 01:02:21 2020 Cross-referenced by PHPXref 0.7.1