Plugin Directory

Changeset 3368693


Ignore:
Timestamp:
09/26/2025 08:08:08 PM (6 months ago)
Author:
nuagelab
Message:

Fixed structure and coding standards

Location:
safe-attachment-names/trunk
Files:
2 added
6 edited

Legend:

Unmodified
Added
Removed
  • safe-attachment-names/trunk/languages/safe-attachment-names-es_ES.po

    r1152900 r3368693  
    66msgid ""
    77msgstr ""
    8 "Project-Id-Version: Auto Domain Changer\n"
     8"Project-Id-Version: Safe Attachment Names\n"
    99"Report-Msgid-Bugs-To: \n"
    10 "POT-Creation-Date: 2015-05-01 14:34-0500\n"
    11 "PO-Revision-Date: 2015-05-01 14:35-0500\n"
     10"POT-Creation-Date: 2025-09-26 16:01-0400\n"
     11"PO-Revision-Date: 2025-09-26 16:01-0400\n"
    1212"Last-Translator: Tommy Lacroix <tlacroix@nuagelab.com>\n"
    1313"Language-Team: NuageLab <wordpress-plugins@nuagelab.com>\n"
     
    1818"Plural-Forms: nplurals=2; plural=(n>1);\n"
    1919"X-Poedit-SourceCharset: UTF-8\n"
    20 "X-Poedit-Bookmarks: -1,430,-1,-1,-1,-1,-1,-1,-1,-1\n"
    21 "X-Generator: Poedit 1.7.4\n"
    22 "X-Poedit-Basepath: ./\n"
     20"X-Generator: Poedit 3.1\n"
     21"X-Poedit-Basepath: .\n"
    2322"X-Poedit-KeywordsList: __;_e;_n\n"
    2423"X-Poedit-SearchPath-0: ..\n"
    2524
    26 #: ../safe-attachment-name.php:101
     25#: ../safe-attachment-names.php:234
     26msgid "You do not have sufficient permissions to access this page."
     27msgstr "No tienes permisos suficientes para acceder a esta página."
     28
     29#: ../safe-attachment-names.php:263
    2730msgid "Sanitize Attachment Names"
    2831msgstr "Limpiar nombres de adjuntos"
    2932
    30 #: ../safe-attachment-name.php:113
     33#: ../safe-attachment-names.php:276
    3134msgid ""
    3235"I have backed up my database and will assume the responsability of any data "
     
    3639"responsabilidad por cualquier pérdida o corrupción de datos."
    3740
    38 #: ../safe-attachment-name.php:118
     41#: ../safe-attachment-names.php:281
    3942msgid "Sanitize"
    4043msgstr "Limpiar"
    4144
    42 #: ../safe-attachment-name.php:180 ../safe-attachment-name.php:212
     45#: ../safe-attachment-names.php:353 ../safe-attachment-names.php:388
    4346msgid ": error, cannot find source file."
    4447msgstr ": error, no puede encontrar el archivo de origen."
    4548
    46 #: ../safe-attachment-name.php:184 ../safe-attachment-name.php:216
     49#: ../safe-attachment-names.php:357 ../safe-attachment-names.php:392
    4750msgid ": OK."
    4851msgstr ": OK."
    4952
    50 #: ../safe-attachment-name.php:249
     53#: ../safe-attachment-names.php:452
    5154msgid "Back"
    5255msgstr "Volver"
     
    10811084#~ msgid "Show as link"
    10821085#~ msgstr "Lien court"
    1083 
    1084 #, fuzzy
    1085 #~ msgid "You do not have sufficient permissions to access this page"
    1086 #~ msgstr ""
    1087 #~ "Vous n&rsquo;avez pas les droits suffisants pour modérer les commentaires."
    10881086
    10891087#, fuzzy
  • safe-attachment-names/trunk/languages/safe-attachment-names-fr_FR.po

    r1152900 r3368693  
    66msgid ""
    77msgstr ""
    8 "Project-Id-Version: Auto Domain Changer\n"
     8"Project-Id-Version: Safe Attachment Names\n"
    99"Report-Msgid-Bugs-To: \n"
    10 "POT-Creation-Date: 2015-05-01 14:28-0500\n"
    11 "PO-Revision-Date: 2015-05-01 14:34-0500\n"
     10"POT-Creation-Date: 2025-09-26 16:00-0400\n"
     11"PO-Revision-Date: 2025-09-26 16:01-0400\n"
    1212"Last-Translator: Tommy Lacroix <tlacroix@nuagelab.com>\n"
    1313"Language-Team: NuageLab <wordpress-plugins@nuagelab.com>\n"
     
    1818"Plural-Forms: nplurals=2; plural=(n>1);\n"
    1919"X-Poedit-SourceCharset: UTF-8\n"
    20 "X-Poedit-Bookmarks: -1,430,-1,-1,-1,-1,-1,-1,-1,-1\n"
    21 "X-Generator: Poedit 1.7.4\n"
    22 "X-Poedit-Basepath: ./\n"
     20"X-Generator: Poedit 3.1\n"
     21"X-Poedit-Basepath: .\n"
    2322"X-Poedit-KeywordsList: __;_e;_n\n"
    2423"X-Poedit-SearchPath-0: ..\n"
    2524
    26 #: ../safe-attachment-name.php:69
    27 msgid "Sanitize attachment names"
    28 msgstr "Assainir noms des pièces jointes"
     25#: ../safe-attachment-names.php:234
     26msgid "You do not have sufficient permissions to access this page."
     27msgstr ""
    2928
    30 #: ../safe-attachment-name.php:101
     29#: ../safe-attachment-names.php:263
    3130msgid "Sanitize Attachment Names"
    3231msgstr "Assainir noms des pièces jointes"
    3332
    34 #: ../safe-attachment-name.php:113
     33#: ../safe-attachment-names.php:276
    3534msgid ""
    3635"I have backed up my database and will assume the responsability of any data "
     
    4039"responsabilité en cas de perte ou de corruption de données."
    4140
    42 #: ../safe-attachment-name.php:118
     41#: ../safe-attachment-names.php:281
    4342msgid "Sanitize"
    4443msgstr "Assainir"
    4544
    46 #: ../safe-attachment-name.php:179 ../safe-attachment-name.php:210
     45#: ../safe-attachment-names.php:353 ../safe-attachment-names.php:388
    4746msgid ": error, cannot find source file."
    4847msgstr ": erreur, impossible de trouver la source"
    4948
    50 #: ../safe-attachment-name.php:184 ../safe-attachment-name.php:220
     49#: ../safe-attachment-names.php:357 ../safe-attachment-names.php:392
    5150msgid ": OK."
    5251msgstr ": OK."
    5352
    54 #: ../safe-attachment-name.php:245
     53#: ../safe-attachment-names.php:452
    5554msgid "Back"
    5655msgstr "Retour"
     56
     57#~ msgid "Sanitize attachment names"
     58#~ msgstr "Assainir noms des pièces jointes"
    5759
    5860#~ msgid ""
  • safe-attachment-names/trunk/readme.txt

    r3368671 r3368693  
    5555
    5656== Changelog ==
     57= 1.1.0 =
     58* Reworked structure.
     59* Tested up to WordPress 6.8.2. More than 100+ stable active installs.
     60
    5761= 1.0.1 =
    5862* Make filenames lowercase
  • safe-attachment-names/trunk/safe-attachment-names.php

    r3368671 r3368693  
    11<?php
    2 /*
    3 Plugin Name: Safe Attachment Names
    4 Plugin URI: http://www.nuagelab.com/wordpress-plugins/safe-attachment-names
    5 Description: Automatically detect and change the name of attachments containing special characters such as accented letters.
    6 Author: NuageLab <wordpress-plugins@nuagelab.com>
    7 Version: 1.0.1
    8 License: GPLv2 or later
    9 Author URI: http://www.nuagelab.com/wordpress-plugins
    10 */
    11 
    12 // --
    13 
    142/**
    15  * Safe Attachment Name class
     3 * Plugin Name: Safe Attachment Names
     4 * Plugin URI: http://www.nuagelab.com/wordpress-plugins/safe-attachment-names
     5 * Description: Automatically detect and change the name of attachments containing special characters such as accented letters.
     6 * Version: 1.1.0
     7 * Author: NuageLab <wordpress-plugins@nuagelab.com>
     8 * Author URI: http://www.nuagelab.com/wordpress-plugins
     9 * License: GPLv2 or later
     10 * License URI: https://www.gnu.org/licenses/gpl-2.0.html
     11 * Text Domain: safe-attachment-names
     12 * Domain Path: /languages
     13 * Requires at least: 5.0
     14 * Tested up to: 6.8.2
     15 * Requires PHP: 7.4
    1616 *
    17  * @author  Tommy Lacroix <tlacroix@nuagelab.com>
     17 * @package SafeAttachmentNames
    1818 */
    19 class safe_attachment_names {
    20 
    21     private static $_instance = null;
    22 
    23     /**
    24      * Bootstrap
     19
     20// Prevent direct access
     21if (!defined('ABSPATH')) {
     22    exit;
     23}
     24
     25// Define plugin constants
     26define('SAFE_ATTACHMENT_NAMES_VERSION', '1.1.0');
     27define('SAFE_ATTACHMENT_NAMES_PLUGIN_DIR', plugin_dir_path(__FILE__));
     28define('SAFE_ATTACHMENT_NAMES_PLUGIN_URL', plugin_dir_url(__FILE__));
     29
     30/**
     31 * Main Safe Attachment Names class
     32 *
     33 * @since 1.0.0
     34 * @author Tommy Lacroix <tlacroix@nuagelab.com>
     35 */
     36class Safe_Attachment_Names {
     37
     38    /**
     39     * Plugin instance
     40     *
     41     * @since 1.0.0
     42     * @var Safe_Attachment_Names|null
     43     */
     44    private static $instance = null;
     45
     46    /**
     47     * Logger instance
     48     *
     49     * @since 1.1.0
     50     * @var WP_Error|null
     51     */
     52    private $logger = null;
     53
     54
     55    /**
     56     * Get plugin instance (singleton pattern)
     57     *
     58     * @since 1.0.0
     59     * @return Safe_Attachment_Names
     60     */
     61    public static function get_instance() {
     62        if (null === self::$instance) {
     63            self::$instance = new self();
     64        }
     65        return self::$instance;
     66    }
     67
     68    /**
     69     * Initialize the plugin
     70     *
     71     * @since 1.0.0
     72     * @return void
     73     */
     74    public static function init() {
     75        $instance = self::get_instance();
     76        $instance->setup();
     77    }
     78
     79    /**
     80     * Private constructor to prevent direct instantiation
     81     *
     82     * @since 1.1.0
     83     */
     84    private function __construct() {
     85        $this->logger = new WP_Error();
     86    }
     87
     88
     89    /**
     90     * Setup plugin hooks and filters
     91     *
     92     * @since 1.0.0
     93     * @return void
     94     */
     95    private function setup() {
     96        // Load text domain
     97        add_action('plugins_loaded', array($this, 'load_textdomain'));
     98
     99        // Add admin menu
     100        add_action('admin_menu', array($this, 'add_admin_menu'));
     101
     102        // Set upload prefilter
     103        add_filter('wp_handle_upload_prefilter', array($this, 'upload_prefilter'));
     104
     105        // Add activation/deactivation hooks
     106        register_activation_hook(__FILE__, array($this, 'activate'));
     107        register_deactivation_hook(__FILE__, array($this, 'deactivate'));
     108    }
     109
     110    /**
     111     * Load plugin text domain for internationalization
     112     *
     113     * @since 1.1.0
     114     * @return void
     115     */
     116    public function load_textdomain() {
     117        load_plugin_textdomain(
     118            'safe-attachment-names',
     119            false,
     120            dirname(plugin_basename(__FILE__)) . '/languages/'
     121        );
     122    }
     123
     124    /**
     125     * Plugin activation hook
     126     *
     127     * @since 1.1.0
     128     * @return void
     129     */
     130    public function activate() {
     131        // Set default options if needed
     132        if (!get_option('safe_attachment_names_version')) {
     133            add_option('safe_attachment_names_version', SAFE_ATTACHMENT_NAMES_VERSION);
     134        }
     135
     136        $this->log_info('Plugin activated successfully');
     137    }
     138
     139    /**
     140     * Plugin deactivation hook
     141     *
     142     * @since 1.1.0
     143     * @return void
     144     */
     145    public function deactivate() {
     146        $this->log_info('Plugin deactivated');
     147    }
     148
     149    /**
     150     * Log an informational message
     151     *
     152     * @since 1.1.0
     153     * @param string $message The message to log
     154     * @return void
     155     */
     156    private function log_info($message) {
     157        if (defined('WP_DEBUG') && WP_DEBUG) {
     158            error_log(sprintf('[Safe Attachment Names] INFO: %s', $message));
     159        }
     160    }
     161
     162    /**
     163     * Log an error message
     164     *
     165     * @since 1.1.0
     166     * @param string $message The error message
     167     * @param string $context Additional context
     168     * @return void
     169     */
     170    private function log_error($message, $context = '') {
     171        $log_message = sprintf('[Safe Attachment Names] ERROR: %s', $message);
     172        if (!empty($context)) {
     173            $log_message .= sprintf(' Context: %s', $context);
     174        }
     175        error_log($log_message);
     176
     177        // Store in WP_Error object for later retrieval
     178        $this->logger->add('safe_attachment_error', $message, $context);
     179    }
     180
     181    /**
     182     * Log a warning message
     183     *
     184     * @since 1.1.0
     185     * @param string $message The warning message
     186     * @return void
     187     */
     188    private function log_warning($message) {
     189        if (defined('WP_DEBUG') && WP_DEBUG) {
     190            error_log(sprintf('[Safe Attachment Names] WARNING: %s', $message));
     191        }
     192    }
     193
     194    /**
     195     * Get logged errors
     196     *
     197     * @since 1.1.0
     198     * @return WP_Error
     199     */
     200    public function get_errors() {
     201        return $this->logger;
     202    }
     203
     204
     205    /**
     206     * Add admin menu action; added by setup()
    25207     *
    26208     * @author  Tommy Lacroix <tlacroix@nuagelab.com>
    27209     * @access  public
    28210     */
    29     public static function boot()
    30     {
    31         if (self::$_instance === null) {
    32             self::$_instance = new safe_attachment_names();
    33             self::$_instance->setup();
    34             return true;
    35         }
    36         return false;
    37     } // boot()
    38 
    39 
    40     /**
    41      * Setup plugin
    42      *
    43      * @author  Tommy Lacroix <tlacroix@nuagelab.com>
    44      * @access  public
    45      */
    46     public function setup()
    47     {
    48         global $current_blog;
    49 
    50         // Add admin menu
    51         add_action('admin_menu', array(&$this, 'add_admin_menu'));
    52 
    53         // Set wp_handle_upload_prefilter
    54         add_filter('wp_handle_upload_prefilter', array($this, 'upload_prefilter'));
    55 
    56         // Load text domain
    57         load_plugin_textdomain('safe-attachment-name', false, dirname( plugin_basename( __FILE__ ) ) . '/languages/');
    58     } // setup()
    59 
    60 
    61     /**
    62      * Add admin menu action; added by setup()
    63      *
    64      * @author  Tommy Lacroix <tlacroix@nuagelab.com>
    65      * @access  public
    66      */
    67     public function add_admin_menu()
    68     {
     211    public function add_admin_menu() {
    69212        // Check if user has proper capabilities
    70213        if (current_user_can('manage_options')) {
    71             //add_management_page(__("Sanitize attachment names",'safe-attachment-name'), __("Sanitize attachment names",'safe-attachment-name'), 'manage_options', basename(__FILE__), array(&$this, 'admin_page'));
    72         }
    73     } // add_admin_menu()
    74 
    75 
    76     /**
    77      * Admin page action; added by add_admin_menu()
    78      *
    79      * @author  Tommy Lacroix <tlacroix@nuagelab.com>
    80      * @access  public
    81      */
    82     public function admin_page()
    83     {
     214            /*add_management_page(
     215                __('Sanitize Attachment Names', 'safe-attachment-names'),
     216                __('Sanitize Attachment Names', 'safe-attachment-names'),
     217                'manage_options',
     218                'safe-attachment-names',
     219                array($this, 'admin_page')
     220            );*/
     221        }
     222    }
     223
     224
     225    /**
     226     * Render admin page
     227     *
     228     * @since 1.0.0
     229     * @return void
     230     */
     231    public function admin_page() {
    84232        // Security check: verify user capabilities
    85233        if (!current_user_can('manage_options')) {
     
    113261
    114262        echo '<div id="icon-tools" class="icon32"><br></div>';
    115         echo '<h2>'.__('Sanitize Attachment Names','safe-attachment-name').'</h2>';
     263        echo '<h2>'.__('Sanitize Attachment Names','safe-attachment-names').'</h2>';
    116264        echo '<form method="post">';
    117265
     
    126274
    127275        echo '<tr valign="top">';
    128         echo '<td colspan="2"><input type="checkbox" name="accept-terms" id="accept-terms" value="1" /> <label for="accept-terms"'.($error_terms?' style="color:red;font-weight:bold;"':'').'>'.__('I have backed up my database and will assume the responsability of any data loss or corruption.','safe-attachment-name').'</label></td>';
     276        echo '<td colspan="2"><input type="checkbox" name="accept-terms" id="accept-terms" value="1" /> <label for="accept-terms"'.($error_terms?' style="color:red;font-weight:bold;"':'').'>'.__('I have backed up my database and will assume the responsibility of any data loss or corruption.','safe-attachment-names').'</label></td>';
    129277        echo '</tr>';
    130278
    131279        echo '</tbody></table>';
    132280
    133         echo '<p class="submit"><input type="submit" name="submit" id="submit" class="button-primary" value="'.esc_html(__('Sanitize','safe-attachment-name')).'"></p>';
     281        echo '<p class="submit"><input type="submit" name="submit" id="submit" class="button-primary" value="'.esc_html(__('Sanitize','safe-attachment-names')).'"></p>';
    134282
    135283        echo '</form>';
    136284
    137285        echo '</div>';
    138     } // admin_page()
    139 
    140 
    141     /**
    142      * Change domain. This is where the magic happens.
     286    }
     287
     288
     289    /**
     290     * Process attachment sanitization
     291     *
     292     * This method handles the bulk sanitization of existing attachments.
    143293     * Called by admin_page() upon form submission.
    144294     *
    145      * @author  Tommy Lacroix <tlacroix@nuagelab.com>
    146      * @access  private
    147      */
    148     private function do_change()
    149     {
     295     * @since 1.0.0
     296     * @return void
     297     */
     298    private function do_change() {
    150299        global $wpdb;
    151300
    152         @set_time_limit(0);
     301        // Increase time limit for large operations
     302        if (function_exists('set_time_limit')) {
     303            set_time_limit(300); // 5 minutes
     304        }
    153305
    154306        echo '<div class="wrap">';
     
    187339            if ($fn1 != $fn2) {
    188340                $error = false;
    189                 $fn4 = $this->sanitizeFilename($fn1);
     341                $fn4 = $this->sanitize_filename($fn1);
    190342
    191343                echo $fn1 . ' -> '.$fn4;
     344
     345                // Initialize WordPress filesystem API if needed
     346                global $wp_filesystem;
     347                if (empty($wp_filesystem)) {
     348                    require_once(ABSPATH . '/wp-admin/includes/file.php');
     349                    WP_Filesystem();
     350                }
    192351
    193352                $fnx = false;
    194353                foreach ($fns as $x) {
    195                     if (file_exists($upload_dir['basedir'].'/'.$x)) {
     354                    if ($wp_filesystem->exists($upload_dir['basedir'].'/'.$x)) {
    196355                        $fnx = $x;
    197356                        break;
     
    221380                    foreach ( $info['sizes'] as &$size ) {
    222381                        $sf1 = dirname( $fn1 ) . '/' . $size['file'];
    223                         $sf4 = $this->sanitizeFilename( $sf1 );
     382                        $sf4 = $this->sanitize_filename( $sf1 );
    224383                        if ( $sf1 != $sf4 ) {
    225384                            echo $sf1 . ' -> ' . $sf4;
     
    228387                            $sfx = false;
    229388                            foreach ( $fns as $x ) {
    230                                 if ( file_exists( $upload_dir['basedir'] . '/' . $x ) ) {
     389                                if ( $wp_filesystem->exists( $upload_dir['basedir'] . '/' . $x ) ) {
    231390                                    $sfx = $x;
    232391                                    break;
     
    298457        echo '</pre>';
    299458        echo '<hr>';
    300         echo '<form method="post"><input type="submit" value="'.esc_html(__('Back','safe-attachment-name')).'" />';
    301     } // do_change()
     459        echo '<form method="post"><input type="submit" value="'.esc_html(__('Back','safe-attachment-names')).'" />';
     460    }
    302461
    303462
     
    305464     * Prefilter uploaded files to remove accents
    306465     *
     466     * @since 1.0.0
    307467     * @param array $file The uploaded file array
    308468     * @return array Modified file array
    309469     */
    310     public function upload_prefilter($file)
    311     {
     470    public function upload_prefilter($file) {
    312471        // Validate file array structure
    313472        if (!is_array($file) || !isset($file['name'])) {
     473            $this->log_warning('Invalid file array structure in upload_prefilter');
    314474            return $file;
    315475        }
    316476
    317         // Sanitize and process filename
    318         $file['name'] = sanitize_file_name($this->sanitizeFilename($file['name']));
     477        try {
     478            // Sanitize and process filename
     479            $original_name = $file['name'];
     480            $sanitized_name = $this->sanitize_filename($file['name']);
     481            $file['name'] = sanitize_file_name($sanitized_name);
     482
     483            if ($original_name !== $file['name']) {
     484                $this->log_info(sprintf('Filename sanitized: %s -> %s', $original_name, $file['name']));
     485            }
     486        } catch (Exception $e) {
     487            $this->log_error('Error sanitizing filename: ' . $e->getMessage(), $file['name']);
     488        }
     489
    319490        return $file;
    320     } // upload_prefilter()
     491    }
    321492
    322493
     
    324495     * Sanitize a file name
    325496     *
    326      * @param   string    $fn1    File name
    327      * @return  string
    328      */
    329     private function sanitizeFilename($fn1) {
     497     * @since 1.0.0
     498     * @param string $filename File name to sanitize
     499     * @return string Sanitized filename
     500     */
     501    private function sanitize_filename($filename) {
    330502        $upload_dir = wp_upload_dir();
    331503
     
    337509        }
    338510
    339         $fn3 = strtolower($this->stripAccents($fn1));
     511        $fn3 = strtolower($this->strip_accents($filename));
    340512        $parts = explode('/', $fn3);
    341513        foreach ($parts as &$part) {
     
    357529
    358530    /**
    359      * Strip accents from string and replace by the unaccented letter
    360      *
    361      * @param $stripAccents
    362      * @return string
    363      */
    364     private function stripAccents($stripAccents){
    365         return utf8_encode(strtr(utf8_decode($stripAccents),utf8_decode('àáâãäçèéêëìíîïñòóôõöùúûüýÿÀÁÂÃÄÇÈÉÊËÌÍÎÏÑÒÓÔÕÖÙÚÛÜÝ'),'aaaaaceeeeiiiinooooouuuuyyAAAAACEEEEIIIINOOOOOUUUUY'));
    366     } // stripAccents()
    367 
    368 } // safe_attachment_names class
    369 
    370 
    371 // Initialize
    372 safe_attachment_names::boot();
     531     * Strip accents from string and replace with unaccented letters
     532     *
     533     * @since 1.0.0
     534     * @param string $text Text to strip accents from
     535     * @return string Text without accents
     536     */
     537    private function strip_accents($text) {
     538        // Use WordPress built-in function if available
     539        if (function_exists('remove_accents')) {
     540            return remove_accents($text);
     541        }
     542
     543        // Fallback method
     544        $accents = 'àáâãäåąćčđèéêëìíîïłñòóôõöøśšťùúûüýÿźžÀÁÂÃÄÅĄĆČĐÈÉÊËÌÍÎÏŁÑÒÓÔÕÖØŚŠŤÙÚÛÜÝŸŹŽ';
     545        $no_accents = 'aaaaaaalccdeeeeiiilnoooooosstuyyzAAAAAAACCDEEEEIIILNOOOOOOSSRUUUUYYZZ';
     546
     547        return strtr($text, $accents, $no_accents);
     548    }
     549
     550}
     551
     552/**
     553 * Initialize the plugin
     554 *
     555 * @since 1.0.0
     556 */
     557function safe_attachment_names_init() {
     558    Safe_Attachment_Names::init();
     559}
     560
     561// Hook into WordPress
     562add_action('init', 'safe_attachment_names_init');
     563
     564/**
     565 * Get plugin instance
     566 *
     567 * @since 1.1.0
     568 * @return Safe_Attachment_Names
     569 */
     570function safe_attachment_names() {
     571    return Safe_Attachment_Names::get_instance();
     572}
Note: See TracChangeset for help on using the changeset viewer.