skin->overwrite ) && '' !== (string) $upgrader->skin->overwrite ) {
return $source;
}
// The same local path WordPress's own package check reads from.
$local = str_replace( $wp_filesystem->wp_content_dir(), trailingslashit( WP_CONTENT_DIR ), untrailingslashit( (string) $source ) );
$folder = basename( $local );
if ( ! is_dir( $local ) || ! is_dir( WP_PLUGIN_DIR . '/' . $folder ) ) {
return $source; // A single-file package, or a fresh install: no comparison screen.
}
try {
self::$report = self::build( $local, WP_PLUGIN_DIR . '/' . $folder, $folder, (array) $upgrader->new_plugin_data );
} catch ( \Throwable $e ) {
// The review is a courtesy: it must never stand in the way of the upload itself.
self::$report = array( 'error' => $e->getMessage() );
}
return $source;
}
/**
* Builds the review.
*
* @param string $upload Unpacked upload.
* @param string $installed Installed folder.
* @param string $folder Folder name.
* @param array $uploaded Uploaded header data, when WordPress has read it.
* @return array
*/
public static function build( $upload, $installed, $folder, array $uploaded = array() ) {
$started = microtime( true );
if ( ! function_exists( 'get_plugins' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
$plugin_file = '';
$current = array();
foreach ( get_plugins() as $file => $data ) {
if ( 0 === strpos( $file, $folder . '/' ) ) {
$plugin_file = $file;
$current = $data;
break;
}
}
$old = new Package( $installed );
$new = new Package( $upload );
if ( ! $current ) {
$main = $old->main_file();
$current = '' !== $main ? $old->plugin_data( $main ) : array();
$plugin_file = '' !== $main ? $folder . '/' . $main : '';
}
if ( ! $uploaded ) {
$main = $new->main_file();
$uploaded = '' !== $main ? $new->plugin_data( $main ) : array();
}
$cmp = new Comparison( $old, $new );
$review = new Review( $cmp, $current, $uploaded, $plugin_file );
$payload = $cmp->payload();
$old_v = isset( $current['Version'] ) ? (string) $current['Version'] : '';
$new_v = isset( $uploaded['Version'] ) ? (string) $uploaded['Version'] : '';
$findings = $review->findings();
$api = $review->api();
$changelog = Changelog::between( $old, $new, $old_v, $new_v );
return array(
'version' => PATCHMARK_VERSION,
'plugin' => array(
'name' => isset( $uploaded['Name'] ) ? (string) $uploaded['Name'] : $folder,
'folder' => $folder,
'oldVersion' => $old_v,
'newVersion' => $new_v,
),
'counts' => $cmp->counts(),
'bytes' => array(
'old' => array_sum( wp_list_pluck( $old->files(), 'size' ) ),
'new' => array_sum( wp_list_pluck( $new->files(), 'size' ) ),
),
'truncated' => $old->truncated() || $new->truncated(),
'omitted' => $payload['omitted'],
'findings' => $findings,
'changelog' => $changelog,
'api' => $api,
'files' => $payload['files'],
'ms' => (int) round( ( microtime( true ) - $started ) * 1000 ),
);
}
/**
* Adds header rows to WordPress's table and the viewer below it.
*
* @param string $table Table HTML from WordPress.
* @param array $current Installed header data.
* @param array $uploaded Uploaded header data.
* @return string
*/
public static function render( $table, $current, $uploaded ) {
$table = self::header_rows( (string) $table, (array) $current, (array) $uploaded );
if ( null === self::$report ) {
return $table;
}
if ( isset( self::$report['error'] ) ) {
return $table . '
' . esc_html(
sprintf(
/* translators: %s: error message. */
__( 'Patchmark could not compare the files: %s', 'patchmark' ),
self::$report['error']
)
) . '
';
}
// The full header comparison travels with the review, so the viewer can replace
// WordPress's table when it takes over the page.
self::$report['headers'] = self::header_compare( (array) $current, (array) $uploaded, true );
self::enqueue();
$c = self::$report['counts'];
$changed = $c['added'] + $c['removed'] + $c['renamed'] + $c['modified'];
$summary = sprintf(
/* translators: 1: changed files, 2: unchanged files, 3: findings. */
__( '%1$d files changed, %2$d unchanged, %3$d findings. Turn on JavaScript to see the details.', 'patchmark' ),
$changed,
$c['unchanged'],
count( self::$report['findings'] )
);
return $table
. '' . esc_html__( 'Loading the comparison…', 'patchmark' ) . '
'
. '';
}
/**
* The script, with the review as data. Enqueued while the page renders, so it prints in the
* footer, and only when there is something to show.
*/
private static function enqueue() {
$asset = self::asset();
if ( null === $asset ) {
return;
}
wp_enqueue_script( self::HANDLE, PATCHMARK_URL . 'build/index.js', $asset['dependencies'], $asset['version'], true );
wp_set_script_translations( self::HANDLE, 'patchmark' );
// JSON_HEX_TAG keeps "" inside a diffed file from closing the tag.
$json = wp_json_encode( self::$report, JSON_HEX_TAG | JSON_HEX_AMP | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE );
wp_add_inline_script( self::HANDLE, 'window.patchmarkReport = ' . ( false === $json ? 'null' : $json ) . ';', 'before' );
}
/**
* Header fields side by side.
*
* @param array $current Installed header data.
* @param array $uploaded Uploaded header data.
* @param bool $all Include the fields WordPress's own table already shows.
* @return array
*/
private static function header_compare( array $current, array $uploaded, $all ) {
$fields = array();
if ( $all ) {
$fields = array(
'Name' => __( 'Plugin name', 'patchmark' ),
'Version' => __( 'Version', 'patchmark' ),
'Author' => __( 'Author', 'patchmark' ),
'RequiresWP' => __( 'Requires WordPress', 'patchmark' ),
'RequiresPHP' => __( 'Requires PHP', 'patchmark' ),
);
}
$fields += array(
'Description' => __( 'Description', 'patchmark' ),
'PluginURI' => __( 'Plugin URI', 'patchmark' ),
'AuthorURI' => __( 'Author URI', 'patchmark' ),
'UpdateURI' => __( 'Update URI', 'patchmark' ),
'TextDomain' => __( 'Text domain', 'patchmark' ),
'RequiresPlugins' => __( 'Requires plugins', 'patchmark' ),
'Network' => __( 'Network only', 'patchmark' ),
);
$rows = array();
foreach ( $fields as $key => $label ) {
$a = isset( $current[ $key ] ) ? $current[ $key ] : '';
$b = isset( $uploaded[ $key ] ) ? $uploaded[ $key ] : '';
if ( is_bool( $a ) || is_bool( $b ) ) {
$a = $a ? __( 'Yes', 'patchmark' ) : __( 'No', 'patchmark' );
$b = $b ? __( 'Yes', 'patchmark' ) : __( 'No', 'patchmark' );
}
$a = trim( wp_strip_all_tags( (string) $a ) );
$b = trim( wp_strip_all_tags( (string) $b ) );
if ( ( '' === $a && '' === $b ) || ( 'Network' === $key && $a === $b ) ) {
continue;
}
$rows[] = array(
'key' => $key,
'label' => $label,
'current' => $a,
'uploaded' => $b,
);
}
return $rows;
}
/**
* Header fields WordPress leaves out of its table, added to it. This is what shows when the
* viewer can't run.
*
* @param string $table Table HTML.
* @param array $current Installed header data.
* @param array $uploaded Uploaded header data.
* @return string
*/
private static function header_rows( $table, array $current, array $uploaded ) {
$rows = '';
foreach ( self::header_compare( $current, $uploaded, false ) as $row ) {
$rows .= sprintf(
'| %s | %s | %s |
',
esc_html( $row['label'] ),
esc_html( '' !== $row['current'] ? $row['current'] : '-' ),
$row['current'] !== $row['uploaded'] ? ' class="warning"' : '',
esc_html( '' !== $row['uploaded'] ? $row['uploaded'] : '-' )
);
}
$pos = strrpos( $table, '' );
if ( '' === $rows || false === $pos ) {
return $table;
}
return substr_replace( $table, $rows . '', $pos, strlen( '' ) );
}
}