Make WordPress Core

Changeset 39412


Ignore:
Timestamp:
12/02/2016 12:25:47 AM (8 years ago)
Author:
westonruter
Message:

Customize: Reuse existing non-auto-draft posts and existing auto-draft posts in the customized state with matching slugs when applying starter content.

  • Updates wp_unique_post_slug() to ignore auto-draft posts. Prevents publishing multiple posts that have the same slugs from starter content.
  • Fixes fatal error when attempting to save an header_image setting from a non-admin context.
  • Fixes substituting attachment symbols in options and theme mods.
  • Fixes applying starter content for header images and background images.

Merges [39411] to 4.7 branch.
See #38114.
Fixes #38928 for 4.7.

Location:
branches/4.7
Files:
6 edited

Legend:

Unmodified
Added
Removed
  • branches/4.7

  • branches/4.7/src/wp-includes/class-wp-customize-manager.php

    r39410 r39412  
    972972        }
    973973
     974        // Make an index of all the posts needed and what their slugs are.
     975        $needed_posts = array();
     976        $attachments = $this->prepare_starter_content_attachments( $attachments );
     977        foreach ( $attachments as $attachment ) {
     978            $key = 'attachment:' . $attachment['post_name'];
     979            $needed_posts[ $key ] = true;
     980        }
     981        foreach ( array_keys( $posts ) as $post_symbol ) {
     982            if ( empty( $posts[ $post_symbol ]['post_name'] ) && empty( $posts[ $post_symbol ]['post_title'] ) ) {
     983                unset( $posts[ $post_symbol ] );
     984                continue;
     985            }
     986            if ( empty( $posts[ $post_symbol ]['post_name'] ) ) {
     987                $posts[ $post_symbol ]['post_name'] = sanitize_title( $posts[ $post_symbol ]['post_title'] );
     988            }
     989            if ( empty( $posts[ $post_symbol ]['post_type'] ) ) {
     990                $posts[ $post_symbol ]['post_type'] = 'post';
     991            }
     992            $needed_posts[ $posts[ $post_symbol ]['post_type'] . ':' . $posts[ $post_symbol ]['post_name'] ] = true;
     993        }
     994        $all_post_slugs = array_merge(
     995            wp_list_pluck( $attachments, 'post_name' ),
     996            wp_list_pluck( $posts, 'post_name' )
     997        );
     998
     999        // Re-use auto-draft starter content posts referenced in the current customized state.
    9741000        $existing_starter_content_posts = array();
    9751001        if ( ! empty( $starter_content_auto_draft_post_ids ) ) {
     
    9851011        }
    9861012
     1013        // Re-use non-auto-draft posts.
     1014        if ( ! empty( $all_post_slugs ) ) {
     1015            $existing_posts_query = new WP_Query( array(
     1016                'post_name__in' => $all_post_slugs,
     1017                'post_status' => array_diff( get_post_stati(), array( 'auto-draft' ) ),
     1018                'post_type' => 'any',
     1019                'number' => -1,
     1020            ) );
     1021            foreach ( $existing_posts_query->posts as $existing_post ) {
     1022                $key = $existing_post->post_type . ':' . $existing_post->post_name;
     1023                if ( isset( $needed_posts[ $key ] ) && ! isset( $existing_starter_content_posts[ $key ] ) ) {
     1024                    $existing_starter_content_posts[ $key ] = $existing_post;
     1025                }
     1026            }
     1027        }
     1028
    9871029        // Attachments are technically posts but handled differently.
    9881030        if ( ! empty( $attachments ) ) {
    989             // Such is The WordPress Way.
    990             require_once( ABSPATH . 'wp-admin/includes/file.php' );
    991             require_once( ABSPATH . 'wp-admin/includes/media.php' );
    992             require_once( ABSPATH . 'wp-admin/includes/image.php' );
    9931031
    9941032            $attachment_ids = array();
    9951033
    9961034            foreach ( $attachments as $symbol => $attachment ) {
    997 
    998                 // A file is required and URLs to files are not currently allowed.
    999                 if ( empty( $attachment['file'] ) || preg_match( '#^https?://$#', $attachment['file'] ) ) {
    1000                     continue;
    1001                 }
    1002 
    1003                 $file_array = array();
    1004                 $file_path = null;
    1005                 if ( file_exists( $attachment['file'] ) ) {
    1006                     $file_path = $attachment['file']; // Could be absolute path to file in plugin.
    1007                 } elseif ( is_child_theme() && file_exists( get_stylesheet_directory() . '/' . $attachment['file'] ) ) {
    1008                     $file_path = get_stylesheet_directory() . '/' . $attachment['file'];
    1009                 } elseif ( file_exists( get_template_directory() . '/' . $attachment['file'] ) ) {
    1010                     $file_path = get_template_directory() . '/' . $attachment['file'];
    1011                 } else {
    1012                     continue;
    1013                 }
    1014                 $file_array['name'] = basename( $attachment['file'] );
    1015 
    1016                 // Skip file types that are not recognized.
    1017                 $checked_filetype = wp_check_filetype( $file_array['name'] );
    1018                 if ( empty( $checked_filetype['type'] ) ) {
    1019                     continue;
    1020                 }
    1021 
    1022                 // Ensure post_name is set since not automatically derived from post_title for new auto-draft posts.
    1023                 if ( empty( $attachment['post_name'] ) ) {
    1024                     if ( ! empty( $attachment['post_title'] ) ) {
    1025                         $attachment['post_name'] = sanitize_title( $attachment['post_title'] );
    1026                     } else {
    1027                         $attachment['post_name'] = sanitize_title( preg_replace( '/\.\w+$/', '', $file_array['name'] ) );
    1028                     }
    1029                 }
    1030 
     1035                $file_array = array(
     1036                    'name' => $attachment['file_name'],
     1037                );
     1038                $file_path = $attachment['file_path'];
    10311039                $attachment_id = null;
    10321040                $attached_file = null;
     
    10811089
    10821090                $attachment_ids[ $symbol ] = $attachment_id;
    1083                 $starter_content_auto_draft_post_ids = array_merge( $starter_content_auto_draft_post_ids, array_values( $attachment_ids ) );
    1084             }
     1091            }
     1092            $starter_content_auto_draft_post_ids = array_merge( $starter_content_auto_draft_post_ids, array_values( $attachment_ids ) );
    10851093        }
    10861094
     
    10881096        if ( ! empty( $posts ) ) {
    10891097            foreach ( array_keys( $posts ) as $post_symbol ) {
    1090                 if ( empty( $posts[ $post_symbol ]['post_type'] ) ) {
     1098                if ( empty( $posts[ $post_symbol ]['post_type'] ) || empty( $posts[ $post_symbol ]['post_name'] ) ) {
    10911099                    continue;
    10921100                }
     
    12101218        // Options.
    12111219        foreach ( $options as $name => $value ) {
    1212             if ( preg_match( '/^{{(?P<symbol>.+)}}$/', $value, $matches ) && isset( $posts[ $matches['symbol'] ] ) ) {
    1213                 $value = $posts[ $matches['symbol'] ]['ID'];
     1220            if ( preg_match( '/^{{(?P<symbol>.+)}}$/', $value, $matches ) ) {
     1221                if ( isset( $posts[ $matches['symbol'] ] ) ) {
     1222                    $value = $posts[ $matches['symbol'] ]['ID'];
     1223                } elseif ( isset( $attachment_ids[ $matches['symbol'] ] ) ) {
     1224                    $value = $attachment_ids[ $matches['symbol'] ];
     1225                } else {
     1226                    continue;
     1227                }
    12141228            }
    12151229
     
    12221236        // Theme mods.
    12231237        foreach ( $theme_mods as $name => $value ) {
    1224             if ( preg_match( '/^{{(?P<symbol>.+)}}$/', $value, $matches ) && isset( $posts[ $matches['symbol'] ] ) ) {
    1225                 $value = $posts[ $matches['symbol'] ]['ID'];
     1238            if ( preg_match( '/^{{(?P<symbol>.+)}}$/', $value, $matches ) ) {
     1239                if ( isset( $posts[ $matches['symbol'] ] ) ) {
     1240                    $value = $posts[ $matches['symbol'] ]['ID'];
     1241                } elseif ( isset( $attachment_ids[ $matches['symbol'] ] ) ) {
     1242                    $value = $attachment_ids[ $matches['symbol'] ];
     1243                } else {
     1244                    continue;
     1245                }
     1246            }
     1247
     1248            // Handle header image as special case since setting has a legacy format.
     1249            if ( 'header_image' === $name ) {
     1250                $name = 'header_image_data';
     1251                $metadata = wp_get_attachment_metadata( $value );
     1252                if ( empty( $metadata ) ) {
     1253                    continue;
     1254                }
     1255                $value = array(
     1256                    'attachment_id' => $value,
     1257                    'url' => wp_get_attachment_url( $value ),
     1258                    'height' => $metadata['height'],
     1259                    'width' => $metadata['width'],
     1260                );
     1261            } elseif ( 'background_image' === $name ) {
     1262                $value = wp_get_attachment_url( $value );
    12261263            }
    12271264
     
    12391276            }
    12401277        }
     1278    }
     1279
     1280    /**
     1281     * Prepare starter content attachments.
     1282     *
     1283     * Ensure that the attachments are valid and that they have slugs and file name/path.
     1284     *
     1285     * @since 4.7.0
     1286     * @access private
     1287     *
     1288     * @param array $attachments Attachments.
     1289     * @return array Prepared attachments.
     1290     */
     1291    protected function prepare_starter_content_attachments( $attachments ) {
     1292        $prepared_attachments = array();
     1293        if ( empty( $attachments ) ) {
     1294            return $prepared_attachments;
     1295        }
     1296
     1297        // Such is The WordPress Way.
     1298        require_once( ABSPATH . 'wp-admin/includes/file.php' );
     1299        require_once( ABSPATH . 'wp-admin/includes/media.php' );
     1300        require_once( ABSPATH . 'wp-admin/includes/image.php' );
     1301
     1302        foreach ( $attachments as $symbol => $attachment ) {
     1303
     1304            // A file is required and URLs to files are not currently allowed.
     1305            if ( empty( $attachment['file'] ) || preg_match( '#^https?://$#', $attachment['file'] ) ) {
     1306                continue;
     1307            }
     1308
     1309            $file_path = null;
     1310            if ( file_exists( $attachment['file'] ) ) {
     1311                $file_path = $attachment['file']; // Could be absolute path to file in plugin.
     1312            } elseif ( is_child_theme() && file_exists( get_stylesheet_directory() . '/' . $attachment['file'] ) ) {
     1313                $file_path = get_stylesheet_directory() . '/' . $attachment['file'];
     1314            } elseif ( file_exists( get_template_directory() . '/' . $attachment['file'] ) ) {
     1315                $file_path = get_template_directory() . '/' . $attachment['file'];
     1316            } else {
     1317                continue;
     1318            }
     1319            $file_name = basename( $attachment['file'] );
     1320
     1321            // Skip file types that are not recognized.
     1322            $checked_filetype = wp_check_filetype( $file_name );
     1323            if ( empty( $checked_filetype['type'] ) ) {
     1324                continue;
     1325            }
     1326
     1327            // Ensure post_name is set since not automatically derived from post_title for new auto-draft posts.
     1328            if ( empty( $attachment['post_name'] ) ) {
     1329                if ( ! empty( $attachment['post_title'] ) ) {
     1330                    $attachment['post_name'] = sanitize_title( $attachment['post_title'] );
     1331                } else {
     1332                    $attachment['post_name'] = sanitize_title( preg_replace( '/\.\w+$/', '', $file_name ) );
     1333                }
     1334            }
     1335
     1336            $attachment['file_name'] = $file_name;
     1337            $attachment['file_path'] = $file_path;
     1338            $prepared_attachments[ $symbol ] = $attachment;
     1339        }
     1340        return $prepared_attachments;
    12411341    }
    12421342
  • branches/4.7/src/wp-includes/customize/class-wp-customize-header-image-setting.php

    r35385 r39412  
    3030        global $custom_image_header;
    3131
     32        // If _custom_header_background_just_in_time() fails to initialize $custom_image_header when not is_admin().
     33        if ( empty( $custom_image_header ) ) {
     34            require_once( ABSPATH . 'wp-admin/custom-header.php' );
     35            $args = get_theme_support( 'custom-header' );
     36            $admin_head_callback = isset( $args[0]['admin-head-callback'] ) ? $args[0]['admin-head-callback'] : null;
     37            $admin_preview_callback = isset( $args[0]['admin-preview-callback'] ) ? $args[0]['admin-preview-callback'] : null;
     38            $custom_image_header = new Custom_Image_Header( $admin_head_callback, $admin_preview_callback );
     39        }
     40
    3241        // If the value doesn't exist (removed or random),
    3342        // use the header_image value.
  • branches/4.7/src/wp-includes/post.php

    r39346 r39412  
    36743674    if ( 'attachment' == $post_type ) {
    36753675        // Attachment slugs must be unique across all types.
    3676         $check_sql = "SELECT post_name FROM $wpdb->posts WHERE post_name = %s AND ID != %d LIMIT 1";
     3676        $check_sql = "SELECT post_name FROM $wpdb->posts WHERE post_status != 'auto-draft' AND post_name = %s AND ID != %d LIMIT 1";
    36773677        $post_name_check = $wpdb->get_var( $wpdb->prepare( $check_sql, $slug, $post_ID ) );
    36783678
     
    37023702         * namespace than posts so page slugs are allowed to overlap post slugs.
    37033703         */
    3704         $check_sql = "SELECT post_name FROM $wpdb->posts WHERE post_name = %s AND post_type IN ( %s, 'attachment' ) AND ID != %d AND post_parent = %d LIMIT 1";
     3704        $check_sql = "SELECT post_name FROM $wpdb->posts WHERE post_status != 'auto-draft' AND post_name = %s AND post_type IN ( %s, 'attachment' ) AND ID != %d AND post_parent = %d LIMIT 1";
    37053705        $post_name_check = $wpdb->get_var( $wpdb->prepare( $check_sql, $slug, $post_type, $post_ID, $post_parent ) );
    37063706
     
    37263726    } else {
    37273727        // Post slugs must be unique across all posts.
    3728         $check_sql = "SELECT post_name FROM $wpdb->posts WHERE post_name = %s AND post_type = %s AND ID != %d LIMIT 1";
     3728        $check_sql = "SELECT post_name FROM $wpdb->posts WHERE post_status != 'auto-draft' AND post_name = %s AND post_type = %s AND ID != %d LIMIT 1";
    37293729        $post_name_check = $wpdb->get_var( $wpdb->prepare( $check_sql, $slug, $post_type, $post_ID ) );
    37303730
  • branches/4.7/tests/phpunit/tests/customize/manager.php

    r39410 r39412  
    315315        wp_set_current_user( self::$admin_user_id );
    316316        register_nav_menu( 'top', 'Top' );
     317        add_theme_support( 'custom-logo' );
     318        add_theme_support( 'custom-header' );
     319        add_theme_support( 'custom-background' );
     320
     321        $canola_file = DIR_TESTDATA . '/images/canola.jpg';
     322        $existing_canola_attachment_id = self::factory()->attachment->create_object( $canola_file, 0, array(
     323            'post_mime_type' => 'image/jpeg',
     324            'post_type' => 'attachment',
     325            'post_name' => 'canola',
     326        ) );
     327        $existing_published_home_page_id = $this->factory()->post->create( array(
     328            'post_name' => 'home',
     329            'post_type' => 'page',
     330            'post_status' => 'publish'
     331        ) );
     332        $existing_auto_draft_about_page_id = $this->factory()->post->create( array(
     333            'post_name' => 'about',
     334            'post_type' => 'page',
     335            'post_status' => 'auto-draft'
     336        ) );
    317337
    318338        global $wp_customize;
     
    352372                    'post_type' => 'post',
    353373                    'post_title' => 'Custom',
    354                     'thumbnail' => '{{featured-image-logo}}',
     374                    'thumbnail' => '{{waffles}}',
    355375                ),
    356376            ),
    357377            'attachments' => array(
    358                 'featured-image-logo' => array(
    359                     'post_title' => 'Featured Image',
    360                     'post_content' => 'Attachment Description',
    361                     'post_excerpt' => 'Attachment Caption',
     378                'waffles' => array(
     379                    'post_title' => 'Waffles',
     380                    'post_content' => 'Waffles Attachment Description',
     381                    'post_excerpt' => 'Waffles Attachment Caption',
    362382                    'file' => DIR_TESTDATA . '/images/waffles.jpg',
     383                ),
     384                'canola' => array(
     385                    'post_title' => 'Canola',
     386                    'post_content' => 'Canola Attachment Description',
     387                    'post_excerpt' => 'Canola Attachment Caption',
     388                    'file' => DIR_TESTDATA . '/images/canola.jpg',
    363389                ),
    364390            ),
     
    369395                'page_on_front'  => '{{home}}',
    370396                'page_for_posts' => '{{blog}}',
     397            ),
     398            'theme_mods' => array(
     399                'custom_logo' => '{{canola}}',
     400                'header_image' => '{{waffles}}',
     401                'background_image' => '{{waffles}}',
    371402            ),
    372403        );
     
    379410            'blogname',
    380411            'blogdescription',
     412            'custom_logo',
     413            'header_image_data',
     414            'background_image',
    381415            'widget_text[2]',
    382416            'widget_meta[3]',
     
    407441
    408442        $posts_by_name = array();
    409         $this->assertCount( 5, $changeset_values['nav_menus_created_posts'] );
     443        $this->assertCount( 6, $changeset_values['nav_menus_created_posts'] );
     444        $this->assertContains( $existing_published_home_page_id, $changeset_values['nav_menus_created_posts'], 'Expected reuse of non-auto-draft posts.' );
     445        $this->assertContains( $existing_canola_attachment_id, $changeset_values['nav_menus_created_posts'], 'Expected reuse of non-auto-draft attachment.' );
     446        $this->assertNotContains( $existing_auto_draft_about_page_id, $changeset_values['nav_menus_created_posts'], 'Expected non-reuse of auto-draft posts.' );
    410447        foreach ( $changeset_values['nav_menus_created_posts'] as $post_id ) {
    411448            $post = get_post( $post_id );
    412             $this->assertEquals( 'auto-draft', $post->post_status );
     449            if ( $post->ID === $existing_published_home_page_id ) {
     450                $this->assertEquals( 'publish', $post->post_status );
     451            } elseif ( $post->ID === $existing_canola_attachment_id ) {
     452                $this->assertEquals( 'inherit', $post->post_status );
     453            } else {
     454                $this->assertEquals( 'auto-draft', $post->post_status );
     455            }
    413456            $posts_by_name[ $post->post_name ] = $post->ID;
    414457        }
    415         $this->assertEquals( array( 'featured-image', 'home', 'about', 'blog', 'custom' ), array_keys( $posts_by_name ) );
     458        $this->assertEquals( array( 'waffles', 'canola', 'home', 'about', 'blog', 'custom' ), array_keys( $posts_by_name ) );
    416459        $this->assertEquals( 'Custom', get_post( $posts_by_name['custom'] )->post_title );
    417460        $this->assertEquals( 'sample-page-template.php', get_page_template_slug( $posts_by_name['about'] ) );
    418461        $this->assertEquals( '', get_page_template_slug( $posts_by_name['blog'] ) );
    419         $this->assertEquals( $posts_by_name['featured-image'], get_post_thumbnail_id( $posts_by_name['custom'] ) );
     462        $this->assertEquals( $posts_by_name['waffles'], get_post_thumbnail_id( $posts_by_name['custom'] ) );
    420463        $this->assertEquals( '', get_post_thumbnail_id( $posts_by_name['blog'] ) );
    421         $attachment_metadata = wp_get_attachment_metadata( $posts_by_name['featured-image'] );
    422         $this->assertEquals( 'Featured Image', get_post( $posts_by_name['featured-image'] )->post_title );
     464        $attachment_metadata = wp_get_attachment_metadata( $posts_by_name['waffles'] );
     465        $this->assertEquals( 'Waffles', get_post( $posts_by_name['waffles'] )->post_title );
    423466        $this->assertArrayHasKey( 'file', $attachment_metadata );
    424467        $this->assertContains( 'waffles', $attachment_metadata['file'] );
     
    473516
    474517        // Publish.
     518        $this->assertEmpty( get_custom_logo() );
     519        $this->assertEmpty( get_header_image() );
     520        $this->assertEmpty( get_background_image() );
     521        $this->assertEmpty( get_theme_mod( 'custom_logo' ) );
     522        $this->assertEmpty( get_theme_mod( 'header_image' ) );
     523        $this->assertEmpty( get_theme_mod( 'background_image' ) );
    475524        $this->assertEquals( 'auto-draft', get_post( $posts_by_name['about'] )->post_status );
    476         $this->assertEquals( 'auto-draft', get_post( $posts_by_name['featured-image'] )->post_status );
     525        $this->assertEquals( 'auto-draft', get_post( $posts_by_name['waffles'] )->post_status );
    477526        $this->assertNotEquals( $changeset_data['blogname']['value'], get_option( 'blogname' ) );
    478527        $r = $wp_customize->save_changeset_post( array( 'status' => 'publish' ) );
    479528        $this->assertInternalType( 'array', $r );
    480529        $this->assertEquals( 'publish', get_post( $posts_by_name['about'] )->post_status );
    481         $this->assertEquals( 'inherit', get_post( $posts_by_name['featured-image'] )->post_status );
     530        $this->assertEquals( 'inherit', get_post( $posts_by_name['waffles'] )->post_status );
    482531        $this->assertEquals( $changeset_data['blogname']['value'], get_option( 'blogname' ) );
     532        $this->assertNotEmpty( get_theme_mod( 'custom_logo' ) );
     533        $this->assertNotEmpty( get_theme_mod( 'header_image' ) );
     534        $this->assertNotEmpty( get_theme_mod( 'background_image' ) );
     535        $this->assertNotEmpty( get_custom_logo() );
     536        $this->assertNotEmpty( get_header_image() );
     537        $this->assertNotEmpty( get_background_image() );
     538        $this->assertContains( 'canola', get_custom_logo() );
     539        $this->assertContains( 'waffles', get_header_image() );
     540        $this->assertContains( 'waffles', get_background_image() );
    483541    }
    484542
  • branches/4.7/tests/phpunit/tests/post/wpUniquePostSlug.php

    r38938 r39412  
    348348        $this->assertSame( 'embed-2', $found );
    349349    }
     350
     351    /**
     352     * @ticket 38928
     353     */
     354    public function test_non_unique_slugs_for_existing_auto_draft_posts() {
     355        $auto_draft_post_id = self::factory()->post->create( array(
     356            'post_type' => 'post',
     357            'post_name' => 'existing-post',
     358            'post_status' => 'auto-draft',
     359        ) );
     360        $auto_draft_page_id = self::factory()->post->create( array(
     361            'post_type' => 'page',
     362            'post_name' => 'existing-page',
     363            'post_status' => 'auto-draft',
     364        ) );
     365        $auto_draft_attachment_id = self::factory()->attachment->create_object( 'image.jpg', $auto_draft_page_id, array(
     366            'post_mime_type' => 'image/jpeg',
     367            'post_type' => 'attachment',
     368            'post_name' => 'existing-attachment',
     369            'post_status' => 'auto-draft',
     370        ) );
     371
     372        $post_id = self::factory()->post->create( array( 'post_type' => 'post' ) );
     373        $page_id = self::factory()->post->create( array( 'post_type' => 'page' ) );
     374        $attachment_id = self::factory()->attachment->create_object( 'image2.jpg', $page_id, array(
     375            'post_mime_type' => 'image/jpeg',
     376            'post_type' => 'attachment',
     377            'post_name' => 'existing-image',
     378        ) );
     379
     380        $this->assertEquals( 'existing-post', wp_unique_post_slug( 'existing-post', $post_id, 'publish', get_post_type( $post_id ), 0 ) );
     381        wp_publish_post( $auto_draft_post_id );
     382        $this->assertEquals( 'existing-post-2', wp_unique_post_slug( 'existing-post', $post_id, 'publish', get_post_type( $post_id ), 0 ) );
     383
     384        $this->assertEquals( 'existing-page', wp_unique_post_slug( 'existing-page', $page_id, 'publish', get_post_type( $page_id ), 0 ) );
     385        wp_publish_post( $auto_draft_page_id );
     386        $this->assertEquals( 'existing-page-2', wp_unique_post_slug( 'existing-page', $page_id, 'publish', get_post_type( $page_id ), 0 ) );
     387
     388        $this->assertEquals( 'existing-attachment', wp_unique_post_slug( 'existing-attachment', $attachment_id, 'publish', get_post_type( $attachment_id ), 0 ) );
     389        wp_publish_post( $auto_draft_attachment_id );
     390        $this->assertEquals( 'existing-attachment-2', wp_unique_post_slug( 'existing-attachment', $attachment_id, 'publish', get_post_type( $attachment_id ), 0 ) );
     391    }
    350392}
Note: See TracChangeset for help on using the changeset viewer.