Plugin Directory

Changeset 3318202


Ignore:
Timestamp:
06/26/2025 11:04:30 AM (9 months ago)
Author:
thronspa
Message:

debug php errors anche fix crop

Location:
thron/trunk
Files:
9 edited

Legend:

Unmodified
Added
Removed
  • thron/trunk/README.txt

    r3298798 r3318202  
    22Contributors: thronspa, websolutedev, palazzinacreativa
    33Tags: DAM
    4 Stable tag: 1.4.1
     4Stable tag: 1.4.2
    55Requires at least: 5.9
    66Tested up to: 6.8.1
     
    8686== Changelog ==
    8787
     88= 1.4.2 =
     89- Fixed crop coordinates by matching with new editor canvas width
     90- Improved checks by inspecting debug log
     91- New mime types handling for .jpg and .webp
     92
    8893= 1.4.1 =
    8994- Fixed PHP 8.2 code deprecations such as "Undefined function 'wp_json_decode' and 'explode' 2nd parameter type.
  • thron/trunk/admin/class-thron-admin.php

    r3298798 r3318202  
    148148                            $listSubTags .= $subNodeIds[ $con ] . ',';
    149149                        }
    150                         $listSubTags = trim( $listSubTags, ',' );
     150                        $listSubTags = $listSubTags ? trim( $listSubTags, ',' ) : listSubTags;
    151151
    152152                        $getListItag = $thron_api->getListItag( '', $classificationID, $listSubTags );
     
    639639        $mime_type = is_array( $mime_type ) ? $mime_type[0] : $mime_type;
    640640
    641         foreach ( sanitize_key( wp_unslash( $_REQUEST['query'] ) ) as $key => $value ) {
     641        foreach ( wp_unslash( $_REQUEST['query'] ) as $key => $value ) {
    642642
    643643            if ( strpos( $key, 'thron_tags' ) !== false ) {
     
    647647        }
    648648
    649         $tags = trim( $tags, ',' );
     649        $tags = $tags ? trim( $tags, ',' ) : $tags;
    650650
    651651        $list = $this->thron_list_file( $term, $categories, $tags, $mime_type, $per_page, $paged );
     
    679679            return $attachment;
    680680        }
    681 
     681        $width_default = ( array_key_exists( 'thron_maxImageWidth', $this->thron_options ) ) ? $this->thron_options['thron_maxImageWidth'] : '0';
     682        $sizes    = get_post_meta( $attachment['id'], 'crop_sizes' ) ?? strval( $width_default ) . 'x0';
     683        $sizes    = is_array( $sizes ) ? ! empty( $sizes ) ? $sizes[0] : strval( $width_default ) . 'x0' : strval( $width_default ) . 'x0';
     684        $params   = get_post_meta( $attachment['id'], 'crop_params' ) ?? '';
     685        $params   = is_array( $params ) && ! empty( $params ) ? $params[0] : '';
    682686        $thron_id = get_post_meta( $attachment['id'], 'thron_id', true );
    683         $width_default = ( array_key_exists( 'thron_maxImageWidth', $this->thron_options ) ) ? $this->thron_options['thron_maxImageWidth'] : '0';
     687        $attachment_metadata = wp_get_attachment_metadata( $attachment['id'] );
    684688
    685689        if ( ! $thron_id ) {
     
    694698        $filename = basename( $url );
    695699        $path     = str_replace( $filename, '', $url );
    696 
    697         foreach ( $attachment['sizes'] as $size => $item ) {
    698             if ( 'full' !== $size ) {
    699                 $width  = $attachment['sizes'][ $size ]['width'];
    700                 $height = $attachment['sizes'][ $size ]['height'];
    701 
    702                 $attachment['sizes'][ $size ]['url'] = str_replace( $width_default . 'x0', '', str_replace( '1920x0', '', $path ) ) . $width . 'x' . $height . '/' . $filename;
    703             }
    704         }
     700        $end_of_path = strrpos( $path, '/std/' ) + 5;
     701        $path = substr( $path, 0, $end_of_path );
     702        if ( isset( $attachment['sizes'] ) && ! empty( $attachment['sizes'] ) ) {
     703            foreach ( $attachment['sizes'] as $size => $item ) {
     704                if ( 'full' !== $size ) {
     705                    $sizes = $sizes ? $sizes . '/' : strval( $width_default ) . 'x0/';
     706                    $width  = $attachment['sizes'][ $size ]['width'];
     707                    $height = $attachment['sizes'][ $size ]['height'];
     708                    $attachment['sizes'][ $size ]['url'] = str_contains( $attachment['sizes'][ $size ]['url'], 'cropx' ) || str_contains( $attachment['sizes'][ $size ]['url'], 'scalemode' )
     709                        ? $path . $sizes . $filename
     710                        : $path . $width . 'x' . $height . '/' . $filename;
     711                }
     712            }
     713        }
     714
     715        $url = str_contains( $url, 'cropx' ) || str_contains( $url, 'scalemode' ) ? $path . $sizes . '/' . $filename : $path . strval( $width_default ) . 'x0/' . $filename;
     716        $attachment_metadata['file'] = $url;
     717        update_post_meta( $attachment['id'], '_wp_attached_file', $url );
     718        update_post_meta( $attachment['id'], '_wp_attachment_metadata', $attachment_metadata );
    705719
    706720        return $attachment;
     
    869883
    870884                        /**
    871                          * $image_sizes Lista di tutti i formati suportati dal tema e dai vari plugin installati
     885                         * $image_sizes Lista di tutti i formati supportati dal tema e dai vari plugin installati
    872886                         */
    873887                        $image_sizes = get_intermediate_image_sizes();
     
    907921
    908922                        if(str_contains( $generic_post['icon'], '/private/' )) {
    909                             $specific_post['icon'] = str_replace( strval($width_default) . 'x0' , '300x0', $file_url);
     923                            $specific_post['icon'] = str_replace( strval( $width_default ) . 'x0' , '300x0', $file_url );
    910924                        }
    911925
     
    10271041        if ( is_numeric( $thron_id ) ) {
    10281042            $post = get_post( $thron_id );
    1029 
    1030             $post->url = wp_get_attachment_url( $post->ID );
    1031             wp_send_json_success( $post );
     1043            if ( $post && isset( $post->ID ) ) {
     1044                $post->url = wp_get_attachment_url( $post->ID );
     1045                wp_send_json_success( $post );
     1046            }
    10321047            die;
    10331048        }
     
    10761091        $language = $detail->content->locales[0];
    10771092        foreach ( $detail->content->locales as $locale ) {
    1078             if ( isset( $locale ) && 'EN' === $locale->lang ) {
     1093            if ( isset( $locale ) && isset( $locale->lang ) && 'EN' === $locale->lang ) {
    10791094                $language = $locale;
    10801095            }
    10811096        }
    10821097        foreach ( $detail->content->locales as $locale ) {
    1083             if ( isset( $locale ) && $this->wp_language === $locale->lang ) {
     1098            if ( isset( $locale ) && isset( $locale->lang ) && $this->wp_language === $locale->lang ) {
    10841099                $language = $locale;
    10851100            }
     
    12891304    public function wp_ajax_custom_crop() {
    12901305        if ( isset( $_REQUEST ) /* && isset( $_REQUEST['history'] ) */ ) {
    1291             var_dump( $_REQUEST );
     1306            //var_dump( $_REQUEST );
    12921307            //stream_preview_image( $_REQUEST['postid'] );
    12931308            return;
  • thron/trunk/admin/js/image-override.js

    r3251196 r3318202  
    2323var Fragment                   = wp.element?.Fragment;
    2424var addFilter                  = wp.hooks?.addFilter;
    25 
    2625var markBlockAsThron = createHigherOrderComponent(
    2726    function (BlockEdit) {
  • thron/trunk/admin/js/wp.media.editor.js

    r3251196 r3318202  
    5353       setTimeout(() => {
    5454          const image = document.querySelector('.attachment-info .thumbnail.thumbnail-image img')
    55           console.log(image)
     55          //console.log(image)
    5656          if(image) {
    5757            attachmentSrc.value = image.getAttribute('src')
     
    9393            const editImageModal = event.length && event[0].context?.el ? event[0].context.el : event
    9494
    95             function customCropSelection (attachmentId, type) {
     95            function customCropSelection (attachmentId, type = 'manual') {
    9696              // console.log('applico il tipo di ritaglio', wp.media.view.EditImage.prototype, attachmentId, type, myAjax)
    9797              const customCropSelect = editImageModal.querySelector('.imgedit-custom-crop')
     
    251251            if(mediaModal.classList.contains('is-cropped')) {
    252252              const image = mediaModal.querySelector(`#image-preview-${attachmentId}`)
    253               if(attachmentSrc.value) {
     253              //console.log(image)
     254              if(image && attachmentSrc.value) {
    254255                image.setAttribute('src', `${attachmentSrc.value}`)
    255256              }
  • thron/trunk/includes/class-thron-api.php

    r3251196 r3318202  
    167167
    168168        $criteria_tag .= '"itag": {"haveAll": [';
    169         if ( '' !== $tagList ) {
     169        if ( ! empty( $tagList ) ) {
    170170            $tags = explode( ',', $tagList );
    171171
  • thron/trunk/includes/class-thron.php

    r3298798 r3318202  
    6565     *
    6666     * @since    1.0.0
    67      */
     67    */
    6868    public function __construct() {
    6969        if ( defined( 'THRON_VERSION' ) ) {
     
    327327         * Delete crop data on asset update or restore
    328328         */
    329         $this->loader->add_filter( 'update_attached_file', $plugin_public, 'delete_crop_data', 11, 2 );
     329        $this->loader->add_filter( 'update_attached_file', $plugin_public, 'restore_crop', 99, 2 );
     330        $this->loader->add_filter( 'delete_post', $plugin_public, 'delete_image', 99, 2 );
    330331
    331332        /**
  • thron/trunk/includes/thron-helper.php

    r3251196 r3318202  
    136136        'image/jpx'                            => 'jp2',
    137137        'image/jpm'                            => 'jp2',
     138        'image/jpg'                            => 'jpg',
    138139        'image/jpeg'                           => 'jpeg',
    139140        'image/pjpeg'                          => 'jpeg',
     
    214215        'audio/wav'                            => 'wav',
    215216        'application/wbxml'                    => 'wbxml',
     217        'image/webp'                           => 'webp',
    216218        'video/webm'                           => 'webm',
    217219        'audio/x-ms-wma'                       => 'wma',
  • thron/trunk/public/class-thron-public.php

    r3251196 r3318202  
    9595
    9696        if ( 'manual' === $scalemode ) {
    97             $cropx = $atts['cropx'];
    98             $cropy = $atts['cropy'];
    99             $cropw = $atts['cropw'];
    100             $croph = $atts['croph'];
     97            $cropx = isset( $atts['cropx'] ) ? $atts['cropx'] : '';
     98            $cropy = isset( $atts['cropy'] ) ? $atts['cropy'] : '';
     99            $cropw = isset( $atts['cropw'] ) ? $atts['cropw'] : '';
     100            $croph = isset( $atts['croph'] ) ? $atts['croph'] : '';
    101101        }
    102102
     
    193193     */
    194194    public function wp_get_attachment_url( $url, $attachment_id ) {
    195 
    196195        $thron_options = get_option( 'thron_option_api_page' );
    197         $width_default = ( array_key_exists( 'thron_maxImageWidth', $thron_options ) ) ? $thron_options['thron_maxImageWidth'] : '0';
     196        $params        = get_post_meta( $attachment_id, 'crop_params' ) ?? '';
     197        $params        = is_array( $params ) && ! empty( $params ) ? $params[0] : '';
     198        $width_default = ( is_array( $thron_options ) && array_key_exists( 'thron_maxImageWidth', $thron_options ) ) ? $thron_options['thron_maxImageWidth'] : '0';
    198199        $post          = get_post( $attachment_id );
    199200
     
    214215        $path = str_replace( $filename, '', $url );
    215216
    216         return str_contains( $url, strval( $width_default ) . 'x0' ) ? $path . $filename : $path . strval( $width_default ) . 'x0/' . $filename;
     217        return /* str_contains( $url, strval( $width_default ) . 'x0' ) ||  */$params
     218            ? $path . $filename
     219            : substr( $path, 0, strrpos( $path, '/std/' ) + 5 ) . strval( $width_default ) . 'x0/' . $filename;
    217220    }
    218221
     
    386389        $attachment_id
    387390    ) {
     391        if ( isset( $_REQUEST['action'] ) && 'delete-post' === $_REQUEST['action'] ) {
     392            return $attachment_metadata;
     393        }
    388394        $thron_options = get_option( 'thron_option_api_page' );
    389395        $width_default = ( array_key_exists( 'thron_maxImageWidth', $thron_options ) ) ? $thron_options['thron_maxImageWidth'] : '0';
     
    393399        $sizes         = is_array( $sizes ) ? ! empty( $sizes ) ? $sizes[0] : strval( $width_default ) . 'x0' : strval( $width_default ) . 'x0';
    394400
    395         $attachment_metadata['file'] = str_replace( '/std//', '/std/' . strval( $width_default ) . 'x0/', $attachment_metadata['file'] );
     401        $attachment_metadata['file'] = ! str_contains( $attachment_metadata['file'], 'cropx' ) && ! str_contains( $attachment_metadata['file'], 'scalemode' ) ? str_replace( '/std//', '/std/' . strval( $width_default ) . 'x0/', $attachment_metadata['file'] ) : str_replace( '/std//', '/std/', wp_get_attachment_url( $attachment_id ) );
    396402        $attachment_metadata['file'] = ! str_contains( $attachment_metadata['file'], 'cropx' ) && ! str_contains( $attachment_metadata['file'], 'scalemode' ) ? str_replace( '/std/' . strval( $width_default ) . 'x0/', '/std/' . $sizes . '/', $attachment_metadata['file'] ) . $params : $attachment_metadata['file'];
    397403        foreach ( $attachment_metadata['sizes'] as $size => $size_data ) {
    398404            $size_url_params_start                          = strpos( $attachment_metadata['sizes'][ $size ]['file'], '?' ) ? strpos( $attachment_metadata['sizes'][ $size ]['file'], '?' ) : strlen( $attachment_metadata['sizes'][ $size ]['file'] );
    399             $attachment_metadata['sizes'][ $size ]['file']  = str_replace( strval( $width_default ) . 'x0', '', str_replace( '1920x0', '', str_replace( '1024x1024', '', str_replace( '768x0', '', str_replace( '300x300', '', str_replace( '150x150', '', substr( $attachment_metadata['sizes'][ $size ]['file'], 0, $size_url_params_start ) ) ) ) ) ) );
     405            $attachment_metadata['sizes'][ $size ]['file']  = str_replace( '1024x1024', '', str_replace( '768x0', '', str_replace( '300x300', '', str_replace( '150x150', '', substr( $attachment_metadata['sizes'][ $size ]['file'], 0, $size_url_params_start ) ) ) ) );
    400406            $attachment_metadata['sizes'][ $size ]['file'] .= $params;
    401407        }
     
    405411        return $attachment_metadata;
    406412    }
    407 
    408413
    409414    /**
     
    418423    public function save_image_crop_data( $saved, $filename, $image, $mime_type, $attachment_id ) {
    419424        if ( isset( $_REQUEST ) /* && wp_verify_nonce( $_REQUEST, '_ajax_nonce' ) */ && isset( $_REQUEST['history'] ) ) {
    420             $context = wp_unslash( $_REQUEST )['context'];
     425            $context = wp_unslash( $_REQUEST )['context'] ?? 'manual';
    421426            $crop_coordinates = json_decode( str_replace( '\\', '', wp_unslash( $_REQUEST )['history'] ) );
    422427            if ( $crop_coordinates && ! empty( $crop_coordinates ) ) {
    423                 // Se il context non è definito oppure è settato a "manual"
     428                // Se il context non è definito oppure è settato a "manual".
    424429                if ( ! in_array( $context, array( 'auto', 'product' ), true ) && isset( $crop_coordinates[0]->c ) ) {
     430                    $absolute_params = array();
    425431                    // Calculating crop proportions.
    426432                    $image_metadata = wp_get_attachment_metadata( $attachment_id );
    427433                    // WordPress cropping preview dimensions.
    428                     $absolute_params   = array();
    429                     $crop_editor_width = $image_metadata['width'] > $image_metadata['height'] ? CROP_EDITOR_WIDTH : round( ( CROP_EDITOR_WIDTH / $image_metadata['height'] ) * $image_metadata['width'] );
    430                     $image_ratio       = $image_metadata['width'] / $crop_editor_width;
     434                    $wp_version = str_replace( '.', '', substr( get_bloginfo( 'version' ), 0, 3 ) );
     435
     436                    $crop_editor_width = $image_metadata['width'] > $image_metadata['height']
     437                        ? CROP_EDITOR_WIDTH
     438                        : round( ( CROP_EDITOR_WIDTH / $image_metadata['height'] ) * $image_metadata['width'] );
     439
     440                    $image_ratio       = $wp_version > 66 ? 1 : $image_metadata['width'] / $crop_editor_width;
    431441                    $crop_width        = $crop_coordinates[0]->c->w * $image_ratio;
    432442                    $crop_height       = $crop_coordinates[0]->c->h * $image_ratio;
     
    470480        }
    471481
    472         if ( null !== $saved ) {
     482        if ( null !== $saved || $saved >= 0 ) {
    473483            return $saved;
    474484        }
     
    483493     */
    484494    public function enable_cropping_for_other_formats( $query ) {
     495        $thron_options = get_option( 'thron_option_api_page' );
    485496        $attachment_id = $query['post__in'][0];
    486497        $attachment    = get_post( $attachment_id );
     498        $width_default = ( array_key_exists( 'thron_maxImageWidth', $thron_options ) ) ? $thron_options['thron_maxImageWidth'] : '0';
    487499        $params        = get_post_meta( $attachment_id, 'crop_params' ) ?? '';
    488500        $params        = is_array( $params ) && ! empty( $params ) ? $params[0] : '';
    489         $sizes         = get_post_meta( $attachment_id, 'crop_sizes' ) ?? '1920x0';
     501        $sizes         = get_post_meta( $attachment_id, 'crop_sizes' ) ?? strval( $width_default ) . 'x0';
    490502        $sizes         = is_array( $sizes ) && ! empty( $sizes ) ? $sizes[0] : '';
    491503
     
    498510
    499511            if ( ! empty( $attachment_metadata ) ) {
     512                //$attachment_metadata['file'] = wp_get_attachment_url( $attachment_id );
    500513                // If image was already cropped.
    501                 if ( ! str_contains( $attachment_metadata['file'], 'cropx' ) || ! str_contains( $attachment_metadata['file'], 'scalemode' ) ) {
    502                     $attachment_metadata['file'] = ! $params ? str_replace( '1920x0/', '', $attachment_metadata['file'] ) : $attachment_metadata['file'];
     514                if ( ! $params ) {
     515                    // $attachment_metadata['file'] = ! $params ? str_replace( strval( $width_default ) . 'x0', '', str_replace( '1920x0/', '', $attachment_metadata['file'] ) ) : $attachment_metadata['file'];
    503516
    504517                    $main_url_extension_start = strrpos( $attachment_metadata['file'], '.' );
    505518                    $main_url_extension       = substr( $attachment_metadata['file'], $main_url_extension_start + 1 );
    506519                    // Forcing format to jpg to ensure cropping works.
    507                     $attachment_metadata['file'] = str_replace( '/std//', '/std/', str_replace( $main_url_extension, 'jpg', $attachment_metadata['file'] ) );
     520                    $attachment_metadata['file'] = str_replace( '/std//', '/std/', str_replace( $main_url_extension, 'jpg', str_replace( '1024x1024', '', str_replace( '768x0', '', str_replace( '300x300', '', str_replace( '150x150', '', $attachment_metadata['file'] ) ) ) ) ) );
    508521
    509522                    foreach ( $attachment_metadata['sizes'] as $size => $size_data ) {
    510                         $size_url_extension_start = strrpos( $attachment_metadata['sizes'][ $size ]['file'], '.' );
    511                         $size_extension           = substr( $attachment_metadata['sizes'][ $size ]['file'], $size_url_extension_start + 1 );
    512 
     523                        $size_url_extension_start                           = strrpos( $attachment_metadata['sizes'][ $size ]['file'], '.' );
     524                        $size_extension                                     = substr( $attachment_metadata['sizes'][ $size ]['file'], $size_url_extension_start + 1 );
     525                        //$attachment_metadata['sizes'][ $size ]['file']      = str_replace( '1024x1024', '', str_replace( '768x0', '', str_replace( '300x300', '', str_replace( '150x150', '', $attachment_metadata['sizes'][ $size ]['file'] ) ) ) );
    513526                        $attachment_metadata['sizes'][ $size ]['file']      = str_replace( $size_extension, 'jpg', $attachment_metadata['sizes'][ $size ]['file'] );
    514527                        $attachment_metadata['sizes'][ $size ]['mime-type'] = str_contains( $attachment_metadata['sizes'][ $size ]['mime-type'], 'image' ) ? 'image/jpeg' : $attachment_metadata['sizes'][ $size ]['mime-type'];
    515528                    }
    516529                }
     530
    517531                update_post_meta( $attachment_id, '_wp_attached_file', $attachment_metadata['file'] );
    518532                update_post_meta( $attachment_id, '_wp_attachment_metadata', $attachment_metadata );
     
    647661
    648662    /**
     663     * Delete image
     664     *
     665     * @param int    $attachment_id Attachment post ID.
     666     * @param object $attachment Attachment instance to be deleted.
     667     */
     668    public function delete_image( $attachment_id, $attachment ) {
     669        $post_type = get_post_type( $attachment_id );
     670        if ( isset( $_REQUEST['action'] ) && 'delete-post' === $_REQUEST['action'] && 'attachment' === $post_type ) {
     671            $this->delete_crop_data( '', $attachment_id );
     672        }
     673    }
     674
     675    /**
     676     * Restore crop
     677     *
     678     * @param string $file File instance to be restored.
     679     * @param int    $attachment_id Attachment post ID.
     680     */
     681    public function restore_crop( $file, $attachment_id ) {
     682        $post_type = get_post_type( $attachment_id );
     683        if ( isset( $_REQUEST['do'] ) && 'restore' === $_REQUEST['do'] && 'attachment' === $post_type ) {
     684            $this->delete_crop_data( $file, $attachment_id );
     685        }
     686    }
     687
     688    /**
    649689     * Delete crop data
    650690     *
    651      * @param object $file File instance to be update.
     691     * @param string $file File instance to be update.
    652692     * @param int    $attachment_id Attachment post ID.
    653693     */
    654     public function delete_crop_data( $file, $attachment_id ) {
    655         $current_file = get_post_meta( $attachment_id, '_wp_attached_file' );
    656         $current_file = $current_file && ! empty( $current_file ) ? $current_file[0] : $current_file;
     694    private function delete_crop_data( $file, $attachment_id ) {
     695        $thron_options       = get_option( 'thron_option_api_page' );
     696        $width_default       = ( array_key_exists( 'thron_maxImageWidth', $thron_options ) ) ? $thron_options['thron_maxImageWidth'] : '0';
     697        $params              = get_post_meta( $attachment_id, 'crop_params' ) ?? '';
     698        $params              = is_array( $params ) && ! empty( $params ) ? $params[0] : '';
     699        $sizes               = get_post_meta( $attachment_id, 'crop_sizes' ) ?? strval( $width_default ) . 'x0';
     700        $sizes               = is_array( $sizes ) ? ! empty( $sizes ) ? $sizes[0] : strval( $width_default ) . 'x0' : strval( $width_default ) . 'x0';
     701        $attachment_metadata = wp_get_attachment_metadata( $attachment_id );
     702        // $current_file        = get_post_meta( $attachment_id, '_wp_attached_file' );
     703        // $current_file        = wp_get_attachment_url( $attachment_id );
     704        // $current_file        = $current_file && ! empty( $current_file ) ? $current_file[0] : $current_file;
    657705        // If it is a file change or restore.
    658         if ( $current_file && ( str_contains( $current_file, '?cropx' ) || str_contains( $current_file, 'scalemode' ) ) ) {
    659             // Deleting crop data.
    660             update_post_meta( $attachment_id, 'crop_params', '' );
    661             update_post_meta( $attachment_id, 'crop_sizes', '' );
    662         }
     706        $attachment_metadata = wp_get_attachment_metadata( $attachment_id );
     707
     708        update_post_meta( $attachment_id, 'crop_absolute_params', '' );
     709        update_post_meta( $attachment_id, 'crop_params', '' );
     710        update_post_meta( $attachment_id, 'crop_sizes', '' );
     711
    663712        return $file;
    664713    }
  • thron/trunk/thron.php

    r3298798 r3318202  
    2222 * Plugin URI:
    2323 * Description:       Select the assets to insert within your pages directly from the DAM library
    24  * Version:           1.4.1
     24 * Version:           1.4.2
    2525 * Author:            THRON
    2626 * Author URI:        https://www.thron.com
Note: See TracChangeset for help on using the changeset viewer.