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( '' ) ); } }