Plugin Directory

Changeset 3760546


Ignore:
Timestamp:
10/22/2026 09:20:01 PM (3 hours ago)
Author:
lebedgor
Message:

Release 1.2.8: fix photo/video uploads not attaching to reviews (empty comment_author_IP vs guest attach IP binding — success screen but no media in admin), honest thank-you screen for partial upload failures with Retry upload, photo webp variant URLs fixed (​http://pvr-reviews/...), Reply button in the details modal on surfaces without the reviews list (media gallery, hub, My Reviews, standalone form), product mini-card at the top of the details modal sidebar (gallery/hub/My Reviews context), mobile form fixes (product name wrap, input styling), updated banners

Location:
pvr-media-reviews-for-woocommerce
Files:
22 edited
1 copied

Legend:

Unmodified
Added
Removed
  • pvr-media-reviews-for-woocommerce/tags/1.2.8/frontend/css/pvr-frontend.css

    r3748498 r3760546  
    317317  align-items: center;
    318318  justify-content: center;
    319   padding: 20px;
     319  padding: 5px;
    320320  z-index: 100001;
    321321}
    … …  
    16691669  overflow: hidden;
    16701670  text-overflow: ellipsis;
    1671   white-space: nowrap;
     1671  /* nowrap stretched the identity row past the viewport on mobile —
     1672     long product names must wrap instead of forcing min-content width. */
     1673  overflow-wrap: anywhere;
    16721674}
    16731675
    … …  
    21602162.pvr-form input{
    21612163  outline: 0 !important;
     2164}
     2165
     2166/* Class-scoped text inputs: themes (Astra et al.) style input[type=...]
     2167   with the same specificity as the shared .pvr-form input rule above and
     2168   win by loading later — that is how sharp corners and theme borders
     2169   replaced the form look on client sites. Same declarations as the
     2170   textarea so the fields match. */
     2171.pvr-form .pvr-form__input,
     2172.pvr-reply-form .pvr-form__input {
     2173  border: 1px solid rgba(203, 213, 225, 0.9);
     2174  border-radius: 16px;
     2175  background: #ffffff;
     2176  color: var(--pvr-text-color);
     2177  padding: 12px 14px;
     2178  font-family: inherit;
     2179  font-size: 14px;
     2180  line-height: 1.45;
     2181  box-shadow: none;
     2182}
     2183
     2184.pvr-form .pvr-form__input:focus,
     2185.pvr-reply-form .pvr-form__input:focus {
     2186  border-color: var(--pvr-accent-color);
    21622187}
    21632188
    … …  
    27722797  margin: 0;
    27732798  line-height: 1.5;
     2799}
     2800
     2801/* Partial upload failure notice: the review IS submitted, so this stays
     2802   inside the thank-you screen (never an error screen) and offers a retry. */
     2803.pvr-thankyou__warning {
     2804  width: 100%;
     2805  max-width: 340px;
     2806  display: flex;
     2807  flex-direction: column;
     2808  gap: 10px;
     2809  padding: 12px 16px;
     2810  border: 1px solid rgba(245, 158, 11, 0.45);
     2811  background: rgba(245, 158, 11, 0.08);
     2812  border-radius: 12px;
     2813  box-sizing: border-box;
     2814}
     2815
     2816.pvr-thankyou__warning-text {
     2817  margin: 0;
     2818  font-size: 13px;
     2819  line-height: 1.5;
     2820  color: var(--pvr-text-color, #1f2937);
     2821}
     2822
     2823.pvr-thankyou__warning-actions {
     2824  display: flex;
     2825  justify-content: center;
     2826}
     2827
     2828.pvr-thankyou__retry {
     2829  padding: 8px 22px;
     2830  border-radius: 999px;
     2831  border: none;
     2832  background: var(--pvr-accent-color, #2563eb);
     2833  color: #fff;
     2834  font-size: 13px;
     2835  font-weight: 600;
     2836  cursor: pointer;
     2837  transition: opacity 0.15s ease;
     2838}
     2839
     2840.pvr-thankyou__retry:hover {
     2841  opacity: 0.88;
     2842}
     2843
     2844.pvr-thankyou__retry:disabled {
     2845  opacity: 0.55;
     2846  cursor: default;
    27742847}
    27752848
  • pvr-media-reviews-for-woocommerce/tags/1.2.8/frontend/js/pvr-frontend.js

    r3748498 r3760546  
    39823982
    39833983            // 2. If review saved successfully, start uploading media
     3984            var mediaIds = [];
     3985            var failedUploads = 0;
    39843986            if (commentId && window.PVREVUpload && typeof window.PVREVUpload.uploadPending === 'function' && window.PVREVUpload.hasPendingItems()) {
    39853987                if (submitButton) {
    … …  
    39883990
    39893991                // Associate with commentId
    3990                 var mediaIds = await window.PVREVUpload.uploadPending(commentId);
     3992                var uploadResult = await window.PVREVUpload.uploadPending(commentId);
     3993                // New contract: { mediaIds, failed, total }. Tolerate the
     3994                // legacy plain array for safety.
     3995                if (uploadResult && typeof uploadResult === 'object' && !Array.isArray(uploadResult)) {
     3996                    mediaIds = Array.isArray(uploadResult.mediaIds) ? uploadResult.mediaIds : [];
     3997                    failedUploads = parseInt(uploadResult.failed, 10) || 0;
     3998                } else if (Array.isArray(uploadResult)) {
     3999                    mediaIds = uploadResult;
     4000                }
    39914001
    39924002                // 3. Notify server that uploads are finished (so it can send emails)
    … …  
    40014011
    40024012            if (window.PVREVUpload && typeof window.PVREVUpload.showThankYou === 'function') {
    4003                 window.PVREVUpload.showThankYou();
     4013                // The review itself IS submitted — never turn this into an
     4014                // error screen. Partial upload failures are surfaced inside
     4015                // the thank-you screen with a retry.
     4016                window.PVREVUpload.showThankYou({ failed: failedUploads });
    40044017            }
    40054018
    … …  
    40384051                }
    40394052
    4040                 window.setTimeout(closeThankYouModal, 5000);
     4053                // The review itself IS submitted — never auto-close the modal
     4054                // while some media failed: the user needs the retry.
     4055                if (failedUploads === 0) {
     4056                    window.setTimeout(closeThankYouModal, 5000);
     4057                }
    40414058            }
    40424059        } catch (error) {
  • pvr-media-reviews-for-woocommerce/tags/1.2.8/frontend/js/pvr-media-upload.js

    r3732836 r3760546  
    2424    var isCancelled = false;
    2525    var watermarkImagePromise = null;
     26    // Comment the current upload round belongs to (retry after partial
     27    // failures re-runs the round against the same review).
     28    var lastCommentId = 0;
     29    var retryBusy = false;
    2630
    2731    function getText(key, fallback) {
    … …  
    479483    }
    480484
    481     function showThankYou() {
     485    function showThankYou(result) {
    482486        var form = getForm();
    483487        if (!form) return;
    484488        form.classList.remove('pvr-form--uploading');
    485489        form.classList.add('pvr-form--thankyou');
     490        var failed = 0;
     491        if (result && typeof result === 'object') {
     492            failed = parseInt(result.failed, 10) || 0;
     493        }
     494        renderThankYouNotice(failed);
     495    }
     496
     497    function getThankYouRoot() {
     498        return document.getElementById('pvr-thankyou');
     499    }
     500
     501    function removeThankYouNotice() {
     502        var root = getThankYouRoot();
     503        var warn = root ? root.querySelector('.pvr-thankyou__warning') : null;
     504        if (warn) {
     505            warn.parentNode.removeChild(warn);
     506        }
     507    }
     508
     509    function hasUnuploadedItems() {
     510        return state.items.some(function(item) {
     511            return !item.uploaded;
     512        });
     513    }
     514
     515    /**
     516     * Partial-upload notice inside the thank-you screen: the review IS
     517     * submitted, so this must never become an error screen — it only
     518     * reports the failed files and offers a retry.
     519     */
     520    function renderThankYouNotice(failed) {
     521        var root = getThankYouRoot();
     522        if (!root) return;
     523        removeThankYouNotice();
     524        if (!failed || failed <= 0) {
     525            return;
     526        }
     527
     528        var warning = document.createElement('div');
     529        warning.className = 'pvr-thankyou__warning';
     530
     531        var text = document.createElement('p');
     532        text.className = 'pvr-thankyou__warning-text';
     533        text.textContent = getText(
     534            'thankyou_media_failed',
     535            'Your review has been submitted, but some media files failed to upload.'
     536        ) + ' (' + failed + ')';
     537        warning.appendChild(text);
     538
     539        if (lastCommentId > 0 && hasUnuploadedItems()) {
     540            var actions = document.createElement('div');
     541            actions.className = 'pvr-thankyou__warning-actions';
     542
     543            var retry = document.createElement('button');
     544            retry.type = 'button';
     545            retry.className = 'pvr-btn pvr-thankyou__retry';
     546            retry.id = 'pvr-thankyou-retry';
     547            retry.textContent = getText('thankyou_retry_upload', 'Retry upload');
     548            retry.addEventListener('click', retryFailedUploads);
     549            actions.appendChild(retry);
     550
     551            warning.appendChild(actions);
     552        }
     553
     554        var closeBtn = document.getElementById('pvr-thankyou-close');
     555        if (closeBtn && closeBtn.parentNode === root) {
     556            root.insertBefore(warning, closeBtn);
     557        } else {
     558            root.appendChild(warning);
     559        }
     560    }
     561
     562    function retryFailedUploads() {
     563        if (retryBusy || !lastCommentId || !hasUnuploadedItems()) {
     564            return;
     565        }
     566        retryBusy = true;
     567        var retryBtn = document.getElementById('pvr-thankyou-retry');
     568        if (retryBtn) {
     569            retryBtn.disabled = true;
     570            retryBtn.classList.add('pvr-is-loading');
     571        }
     572
     573        uploadPendingItems(lastCommentId).then(function (result) {
     574            var failed = (result && typeof result === 'object') ? (parseInt(result.failed, 10) || 0) : 0;
     575            if (typeof window.pvrAjax === 'function') {
     576                window.pvrAjax('pvrev_finish_uploads', {
     577                    comment_id: lastCommentId,
     578                    media_ids: state.mediaIds.join(','),
     579                    nonce: (window.PVREV && window.PVREV.nonces ? window.PVREV.nonces.submit_review : (window.PVREV.nonce || ''))
     580                }).catch(function () {});
     581            }
     582            if (failed === 0) {
     583                removeThankYouNotice();
     584                if (window.pvrShowSuccess) {
     585                    window.pvrShowSuccess(getText('thankyou_media_complete', 'All media uploaded successfully.'));
     586                }
     587            } else {
     588                renderThankYouNotice(failed);
     589            }
     590        }).catch(function () {
     591            if (window.pvrShowError) {
     592                window.pvrShowError(getText('upload_failed', 'Upload failed'));
     593            }
     594        }).then(function () {
     595            retryBusy = false;
     596            if (retryBtn) {
     597                retryBtn.disabled = false;
     598                retryBtn.classList.remove('pvr-is-loading');
     599            }
     600        });
    486601    }
    487602
    … …  
    10351150        var total;
    10361151        var completed = 0;
     1152        var failed = 0;
    10371153
    10381154        isCancelled = false;
     1155        lastCommentId = parseInt(commentId, 10) || 0;
    10391156
    10401157        photos = state.items.filter(function(item) {
    … …  
    10511168        if (!total) {
    10521169            syncMediaIdsInput();
    1053             return state.mediaIds.slice();
     1170            return { mediaIds: state.mediaIds.slice(), failed: 0, total: 0 };
    10541171        }
    10551172
    … …  
    10911208                    elements.progressText = savedText;
    10921209                    resetFormMode();
    1093                     return state.mediaIds.slice();
     1210                    return { mediaIds: state.mediaIds.slice(), failed: 0, total: 0 };
    10941211                }
     1212                // Previously this failure was swallowed silently: the file
     1213                // was skipped without any visible error and the flow still
     1214                // ended on the success screen.
     1215                failed += 1;
    10951216                payloads.push(null);
     1217                var badItem = queue[q];
     1218                var badLabel = (badItem && badItem.name) || getText('upload_file', 'file');
     1219                var badErr = (err && err.message) ? err.message : getText('upload_failed', 'Upload failed');
     1220                showError(badLabel + ': ' + badErr);
    10961221            }
    10971222        }
    … …  
    11021227            elements.progressText = savedText;
    11031228            resetFormMode();
    1104             return state.mediaIds.slice();
     1229            return { mediaIds: state.mediaIds.slice(), failed: 0, total: 0 };
    11051230        }
    11061231
    … …  
    11701295                        elements.progressText = savedText;
    11711296                        resetFormMode();
    1172                         return state.mediaIds.slice();
     1297                        return { mediaIds: state.mediaIds.slice(), failed: 0, total: 0 };
    11731298                    }
    11741299                    if (!item.uploaded) {
    … …  
    11761301                    }
    11771302                    setItemProgress(item, 0);
     1303                    failed += 1;
    11781304                    var fileLabel = item.name || getText('upload_file', 'file');
    11791305                    var errMsg = (error && error.message) ? error.message : getText('upload_failed', 'Upload failed');
    … …  
    11861312            if (isCancelled) {
    11871313                resetFormMode();
    1188                 return state.mediaIds.slice();
     1314                return { mediaIds: state.mediaIds.slice(), failed: 0, total: 0 };
    11891315            }
    11901316
    … …  
    11981324            hideProgress();
    11991325        }
    1200         return state.mediaIds.slice();
     1326        return { mediaIds: state.mediaIds.slice(), failed: failed, total: total };
    12011327    }
    12021328
    … …  
    14471573        reset: function() {
    14481574            resetState();
     1575            removeThankYouNotice();
     1576            lastCommentId = 0;
    14491577            if (elements.preview) {
    14501578                elements.preview.innerHTML = '';
    … …  
    14691597        },
    14701598
    1471         showThankYou: function() {
    1472             showThankYou();
     1599        showThankYou: function(result) {
     1600            showThankYou(result);
    14731601        },
    14741602
  • pvr-media-reviews-for-woocommerce/tags/1.2.8/includes/class-pvr-hooks.php

    r3748498 r3760546  
    14721472                                'picker_no_results'        => __( 'No products found', 'pvr-media-reviews-for-woocommerce' ),
    14731473                                'picker_select_product'    => __( 'Please select a product first', 'pvr-media-reviews-for-woocommerce' ),
     1474                                'thankyou_media_failed'    => __( 'Your review has been submitted, but some media files failed to upload. You can retry.', 'pvr-media-reviews-for-woocommerce' ),
     1475                                'thankyou_retry_upload'    => __( 'Retry upload', 'pvr-media-reviews-for-woocommerce' ),
     1476                                'thankyou_media_complete'  => __( 'All media uploaded successfully.', 'pvr-media-reviews-for-woocommerce' ),
    14741477            ],
    14751478        ];
  • pvr-media-reviews-for-woocommerce/tags/1.2.8/includes/class-pvr-media.php

    r3750600 r3760546  
    459459                                // binding: a guest comment may receive media only from the
    460460                                // address that submitted it, so sequential comment IDs from
    461                                 // other visitors cannot be targeted.
     461                                // other visitors cannot be targeted. Same resolver as the
     462                                // submit side — REMOTE_ADDR alone breaks behind a reverse
     463                                // proxy / Cloudflare, where it is the proxy's address.
    462464                                $comment_ip = (string) $comment->comment_author_IP;
    463                                 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Same normalization wp_new_comment applies to REMOTE_ADDR.
    464                                 $current_ip = preg_replace( '/[^0-9a-fA-F:., ]/', '', (string) wp_unslash( $_SERVER['REMOTE_ADDR'] ?? '' ) );
     465                                $current_ip = PVREV_Hooks::get_client_ip();
    465466                                if ( '' === $comment_ip || ! hash_equals( $comment_ip, $current_ip ) ) {
    466467                                        $comment_id = 0;
    … …  
    513514                        }
    514515
     516                        // Variant paths come back from ensure_photo_variants() as paths
     517                        // relative to uploads; str_replace( basedir, baseurl, ... ) here
     518                        // was a no-op on them and esc_url() turned the bare relative path
     519                        // into "http://pvr-reviews/...". get_media_url() resolves both
     520                        // relative and absolute paths correctly.
    515521                        $thumbnail_url = null;
    516522                        if ( ! empty( $thumbnail_path ) ) {
    517                                 $thumbnail_url = str_replace( trailingslashit( $upload['basedir'] ), trailingslashit( $upload['baseurl'] ), $thumbnail_path );
     523                                $thumbnail_url = self::get_media_url( $thumbnail_path );
    518524                        }
    519525                        $large_url = null;
    520526                        if ( ! empty( $large_path ) ) {
    521                                 $large_url = str_replace( trailingslashit( $upload['basedir'] ), trailingslashit( $upload['baseurl'] ), $large_path );
     527                                $large_url = self::get_media_url( $large_path );
    522528                        }
    523529                        $popup_url = null;
    524530                        if ( ! empty( $popup_path ) ) {
    525                                 $popup_url = str_replace( trailingslashit( $upload['basedir'] ), trailingslashit( $upload['baseurl'] ), $popup_path );
     531                                $popup_url = self::get_media_url( $popup_path );
    526532                        }
    527533
  • pvr-media-reviews-for-woocommerce/tags/1.2.8/includes/class-pvr-review.php

    r3737674 r3760546  
    327327                                'comment_author_email' => $author_email,
    328328                                'comment_author_url'   => '',
     329                                // wp_insert_comment() does not fill the IP (unlike
     330                                // wp_new_comment()); guest media attach in PVREV_Media
     331                                // compares this value with the upload request's IP.
     332                                'comment_author_IP'    => PVREV_Hooks::get_client_ip(),
    329333                                'comment_content'      => $content,
    330334                                'comment_type'         => 'review',
  • pvr-media-reviews-for-woocommerce/tags/1.2.8/languages/pvr-media-reviews-for-woocommerce-en_US.json

    r3759180 r3760546  
    13941394  "Replies skipped (parent review missing)": "Replies skipped (parent review missing)",
    13951395  "Reply queue limit reached — reply row skipped.": "Reply queue limit reached — reply row skipped.",
    1396   "Review ID (source file)": "Review ID (source file)"
     1396  "Review ID (source file)": "Review ID (source file)",
     1397  "Your review has been submitted, but some media files failed to upload. You can retry.": "Your review has been submitted, but some media files failed to upload. You can retry.",
     1398  "Retry upload": "Retry upload",
     1399  "All media uploaded successfully.": "All media uploaded successfully."
    13971400}
  • pvr-media-reviews-for-woocommerce/tags/1.2.8/languages/pvr-media-reviews-for-woocommerce-es_ES.json

    r3759180 r3760546  
    13761376  "Replies skipped (parent review missing)": "Respuestas omitidas (falta la reseña principal)",
    13771377  "Reply queue limit reached — reply row skipped.": "Se alcanzó el límite de la cola de respuestas — fila de respuesta omitida.",
    1378   "Review ID (source file)": "ID de reseña (archivo de origen)"
     1378  "Review ID (source file)": "ID de reseña (archivo de origen)",
     1379  "Your review has been submitted, but some media files failed to upload. You can retry.": "Tu reseña se ha enviado, pero algunos archivos multimedia no se pudieron cargar. Puedes reintentarlo.",
     1380  "Retry upload": "Reintentar subida",
     1381  "All media uploaded successfully.": "Todos los archivos multimedia se subieron correctamente."
    13791382}
  • pvr-media-reviews-for-woocommerce/tags/1.2.8/languages/pvr-media-reviews-for-woocommerce-ru_RU.json

    r3759180 r3760546  
    13901390  "Replies skipped (parent review missing)": "Пропущено ответов (нет родительского отзыва)",
    13911391  "Reply queue limit reached — reply row skipped.": "Достигнут лимит очереди ответов — строка ответа пропущена.",
    1392   "Review ID (source file)": "ID отзыва (в файле-источнике)"
     1392  "Review ID (source file)": "ID отзыва (в файле-источнике)",
     1393  "Your review has been submitted, but some media files failed to upload. You can retry.": "Ваш отзыв отправлен, но часть медиафайлов не удалось загрузить. Можно повторить попытку.",
     1394  "Retry upload": "Повторить загрузку",
     1395  "All media uploaded successfully.": "Все медиафайлы успешно загружены."
    13931396}
  • pvr-media-reviews-for-woocommerce/tags/1.2.8/photo-video-reviews-for-woocommerce.php

    r3759180 r3760546  
    44 * Plugin URI: https://wordpress.org/plugins/pvr-media-reviews-for-woocommerce/
    55 * Description: Photo and video reviews for WooCommerce with client-side compression.
    6  * Version: 1.2.7
     6 * Version: 1.2.8
    77 * Author: Lebedgor
    88 * Author URI: https://demo.pv-reviews.site/
    … …  
    1818defined( 'ABSPATH' ) || exit;
    1919
    20 define( 'PVREV_VERSION', '1.2.7' );
     20define( 'PVREV_VERSION', '1.2.8' );
    2121define( 'PVREV_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
    2222define( 'PVREV_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
  • pvr-media-reviews-for-woocommerce/tags/1.2.8/readme.txt

    r3759180 r3760546  
    55Tested up to: 7.0
    66Requires PHP: 7.1
    7 Stable tag: 1.2.7
     7Stable tag: 1.2.8
    88WC requires at least: 7.0
    99WC tested up to: 9.0
    … …  
    175175== Changelog ==
    176176
     177= 1.2.8 =
     178* Fix: photo and video uploads failed to attach to reviews — the review saved and the customer saw the success screen, but the admin found no media: wp_insert_comment() does not fill comment_author_IP, so the anti-enumeration guest attach check (added in 1.2.0) silently re-bound every upload to comment 0. The submit now stores the client IP (proxy/Cloudflare-aware) and the attach check resolves the request IP through the same resolver
     179* Improvement: failed media uploads no longer end on a silent success screen — the thank-you screen shows which files failed, offers a Retry upload that re-runs only the failed files against the same review, and the 5-second auto-close only runs when everything is uploaded; prepare/compression errors now surface a toast instead of skipping the file invisibly
     180* Fix: photo thumbnail/large webp URLs in the upload response were built as "http://pvr-reviews/..." when the variant path came back relative (esc_url on a bare path); URLs are now resolved through PVREV_Media::get_media_url()
     181* Fix: the Reply button in the review details modal did nothing on pages rendered without the reviews list (media gallery block, hub page, My Reviews, standalone form) — the shared reply form modal is now printed once per request for every surface
     182* New: the review details modal shows a product mini-card (thumbnail + name) at the top of its sidebar, above the author — desktop aside and mobile slide both follow review flip-through, so mixed-product surfaces (media gallery, all-media strip, hub) always show which product the review belongs to
     183* Fix: the review form could stretch past the viewport on mobile — long product names no longer force min-content width (nowrap removed), and text inputs keep the form styling (rounded corners, border color) on themes that load their own input styles later
     184* Updated plugin banners
     185
    177186= 1.2.7 =
    178187* New: import preset for CusRev (Customer Reviews for WooCommerce) — a file format selector in the import wizard recognizes their export columns automatically (review_title, review_content, review_score, display_name, media), so a CusRev CSV maps without any manual column setup
  • pvr-media-reviews-for-woocommerce/trunk/frontend/css/pvr-frontend.css

    r3748498 r3760546  
    317317  align-items: center;
    318318  justify-content: center;
    319   padding: 20px;
     319  padding: 5px;
    320320  z-index: 100001;
    321321}
    … …  
    16691669  overflow: hidden;
    16701670  text-overflow: ellipsis;
    1671   white-space: nowrap;
     1671  /* nowrap stretched the identity row past the viewport on mobile —
     1672     long product names must wrap instead of forcing min-content width. */
     1673  overflow-wrap: anywhere;
    16721674}
    16731675
    … …  
    21602162.pvr-form input{
    21612163  outline: 0 !important;
     2164}
     2165
     2166/* Class-scoped text inputs: themes (Astra et al.) style input[type=...]
     2167   with the same specificity as the shared .pvr-form input rule above and
     2168   win by loading later — that is how sharp corners and theme borders
     2169   replaced the form look on client sites. Same declarations as the
     2170   textarea so the fields match. */
     2171.pvr-form .pvr-form__input,
     2172.pvr-reply-form .pvr-form__input {
     2173  border: 1px solid rgba(203, 213, 225, 0.9);
     2174  border-radius: 16px;
     2175  background: #ffffff;
     2176  color: var(--pvr-text-color);
     2177  padding: 12px 14px;
     2178  font-family: inherit;
     2179  font-size: 14px;
     2180  line-height: 1.45;
     2181  box-shadow: none;
     2182}
     2183
     2184.pvr-form .pvr-form__input:focus,
     2185.pvr-reply-form .pvr-form__input:focus {
     2186  border-color: var(--pvr-accent-color);
    21622187}
    21632188
    … …  
    27722797  margin: 0;
    27732798  line-height: 1.5;
     2799}
     2800
     2801/* Partial upload failure notice: the review IS submitted, so this stays
     2802   inside the thank-you screen (never an error screen) and offers a retry. */
     2803.pvr-thankyou__warning {
     2804  width: 100%;
     2805  max-width: 340px;
     2806  display: flex;
     2807  flex-direction: column;
     2808  gap: 10px;
     2809  padding: 12px 16px;
     2810  border: 1px solid rgba(245, 158, 11, 0.45);
     2811  background: rgba(245, 158, 11, 0.08);
     2812  border-radius: 12px;
     2813  box-sizing: border-box;
     2814}
     2815
     2816.pvr-thankyou__warning-text {
     2817  margin: 0;
     2818  font-size: 13px;
     2819  line-height: 1.5;
     2820  color: var(--pvr-text-color, #1f2937);
     2821}
     2822
     2823.pvr-thankyou__warning-actions {
     2824  display: flex;
     2825  justify-content: center;
     2826}
     2827
     2828.pvr-thankyou__retry {
     2829  padding: 8px 22px;
     2830  border-radius: 999px;
     2831  border: none;
     2832  background: var(--pvr-accent-color, #2563eb);
     2833  color: #fff;
     2834  font-size: 13px;
     2835  font-weight: 600;
     2836  cursor: pointer;
     2837  transition: opacity 0.15s ease;
     2838}
     2839
     2840.pvr-thankyou__retry:hover {
     2841  opacity: 0.88;
     2842}
     2843
     2844.pvr-thankyou__retry:disabled {
     2845  opacity: 0.55;
     2846  cursor: default;
    27742847}
    27752848
  • pvr-media-reviews-for-woocommerce/trunk/frontend/js/pvr-frontend.js

    r3748498 r3760546  
    39823982
    39833983            // 2. If review saved successfully, start uploading media
     3984            var mediaIds = [];
     3985            var failedUploads = 0;
    39843986            if (commentId && window.PVREVUpload && typeof window.PVREVUpload.uploadPending === 'function' && window.PVREVUpload.hasPendingItems()) {
    39853987                if (submitButton) {
    … …  
    39883990
    39893991                // Associate with commentId
    3990                 var mediaIds = await window.PVREVUpload.uploadPending(commentId);
     3992                var uploadResult = await window.PVREVUpload.uploadPending(commentId);
     3993                // New contract: { mediaIds, failed, total }. Tolerate the
     3994                // legacy plain array for safety.
     3995                if (uploadResult && typeof uploadResult === 'object' && !Array.isArray(uploadResult)) {
     3996                    mediaIds = Array.isArray(uploadResult.mediaIds) ? uploadResult.mediaIds : [];
     3997                    failedUploads = parseInt(uploadResult.failed, 10) || 0;
     3998                } else if (Array.isArray(uploadResult)) {
     3999                    mediaIds = uploadResult;
     4000                }
    39914001
    39924002                // 3. Notify server that uploads are finished (so it can send emails)
    … …  
    40014011
    40024012            if (window.PVREVUpload && typeof window.PVREVUpload.showThankYou === 'function') {
    4003                 window.PVREVUpload.showThankYou();
     4013                // The review itself IS submitted — never turn this into an
     4014                // error screen. Partial upload failures are surfaced inside
     4015                // the thank-you screen with a retry.
     4016                window.PVREVUpload.showThankYou({ failed: failedUploads });
    40044017            }
    40054018
    … …  
    40384051                }
    40394052
    4040                 window.setTimeout(closeThankYouModal, 5000);
     4053                // The review itself IS submitted — never auto-close the modal
     4054                // while some media failed: the user needs the retry.
     4055                if (failedUploads === 0) {
     4056                    window.setTimeout(closeThankYouModal, 5000);
     4057                }
    40414058            }
    40424059        } catch (error) {
  • pvr-media-reviews-for-woocommerce/trunk/frontend/js/pvr-media-upload.js

    r3732836 r3760546  
    2424    var isCancelled = false;
    2525    var watermarkImagePromise = null;
     26    // Comment the current upload round belongs to (retry after partial
     27    // failures re-runs the round against the same review).
     28    var lastCommentId = 0;
     29    var retryBusy = false;
    2630
    2731    function getText(key, fallback) {
    … …  
    479483    }
    480484
    481     function showThankYou() {
     485    function showThankYou(result) {
    482486        var form = getForm();
    483487        if (!form) return;
    484488        form.classList.remove('pvr-form--uploading');
    485489        form.classList.add('pvr-form--thankyou');
     490        var failed = 0;
     491        if (result && typeof result === 'object') {
     492            failed = parseInt(result.failed, 10) || 0;
     493        }
     494        renderThankYouNotice(failed);
     495    }
     496
     497    function getThankYouRoot() {
     498        return document.getElementById('pvr-thankyou');
     499    }
     500
     501    function removeThankYouNotice() {
     502        var root = getThankYouRoot();
     503        var warn = root ? root.querySelector('.pvr-thankyou__warning') : null;
     504        if (warn) {
     505            warn.parentNode.removeChild(warn);
     506        }
     507    }
     508
     509    function hasUnuploadedItems() {
     510        return state.items.some(function(item) {
     511            return !item.uploaded;
     512        });
     513    }
     514
     515    /**
     516     * Partial-upload notice inside the thank-you screen: the review IS
     517     * submitted, so this must never become an error screen — it only
     518     * reports the failed files and offers a retry.
     519     */
     520    function renderThankYouNotice(failed) {
     521        var root = getThankYouRoot();
     522        if (!root) return;
     523        removeThankYouNotice();
     524        if (!failed || failed <= 0) {
     525            return;
     526        }
     527
     528        var warning = document.createElement('div');
     529        warning.className = 'pvr-thankyou__warning';
     530
     531        var text = document.createElement('p');
     532        text.className = 'pvr-thankyou__warning-text';
     533        text.textContent = getText(
     534            'thankyou_media_failed',
     535            'Your review has been submitted, but some media files failed to upload.'
     536        ) + ' (' + failed + ')';
     537        warning.appendChild(text);
     538
     539        if (lastCommentId > 0 && hasUnuploadedItems()) {
     540            var actions = document.createElement('div');
     541            actions.className = 'pvr-thankyou__warning-actions';
     542
     543            var retry = document.createElement('button');
     544            retry.type = 'button';
     545            retry.className = 'pvr-btn pvr-thankyou__retry';
     546            retry.id = 'pvr-thankyou-retry';
     547            retry.textContent = getText('thankyou_retry_upload', 'Retry upload');
     548            retry.addEventListener('click', retryFailedUploads);
     549            actions.appendChild(retry);
     550
     551            warning.appendChild(actions);
     552        }
     553
     554        var closeBtn = document.getElementById('pvr-thankyou-close');
     555        if (closeBtn && closeBtn.parentNode === root) {
     556            root.insertBefore(warning, closeBtn);
     557        } else {
     558            root.appendChild(warning);
     559        }
     560    }
     561
     562    function retryFailedUploads() {
     563        if (retryBusy || !lastCommentId || !hasUnuploadedItems()) {
     564            return;
     565        }
     566        retryBusy = true;
     567        var retryBtn = document.getElementById('pvr-thankyou-retry');
     568        if (retryBtn) {
     569            retryBtn.disabled = true;
     570            retryBtn.classList.add('pvr-is-loading');
     571        }
     572
     573        uploadPendingItems(lastCommentId).then(function (result) {
     574            var failed = (result && typeof result === 'object') ? (parseInt(result.failed, 10) || 0) : 0;
     575            if (typeof window.pvrAjax === 'function') {
     576                window.pvrAjax('pvrev_finish_uploads', {
     577                    comment_id: lastCommentId,
     578                    media_ids: state.mediaIds.join(','),
     579                    nonce: (window.PVREV && window.PVREV.nonces ? window.PVREV.nonces.submit_review : (window.PVREV.nonce || ''))
     580                }).catch(function () {});
     581            }
     582            if (failed === 0) {
     583                removeThankYouNotice();
     584                if (window.pvrShowSuccess) {
     585                    window.pvrShowSuccess(getText('thankyou_media_complete', 'All media uploaded successfully.'));
     586                }
     587            } else {
     588                renderThankYouNotice(failed);
     589            }
     590        }).catch(function () {
     591            if (window.pvrShowError) {
     592                window.pvrShowError(getText('upload_failed', 'Upload failed'));
     593            }
     594        }).then(function () {
     595            retryBusy = false;
     596            if (retryBtn) {
     597                retryBtn.disabled = false;
     598                retryBtn.classList.remove('pvr-is-loading');
     599            }
     600        });
    486601    }
    487602
    … …  
    10351150        var total;
    10361151        var completed = 0;
     1152        var failed = 0;
    10371153
    10381154        isCancelled = false;
     1155        lastCommentId = parseInt(commentId, 10) || 0;
    10391156
    10401157        photos = state.items.filter(function(item) {
    … …  
    10511168        if (!total) {
    10521169            syncMediaIdsInput();
    1053             return state.mediaIds.slice();
     1170            return { mediaIds: state.mediaIds.slice(), failed: 0, total: 0 };
    10541171        }
    10551172
    … …  
    10911208                    elements.progressText = savedText;
    10921209                    resetFormMode();
    1093                     return state.mediaIds.slice();
     1210                    return { mediaIds: state.mediaIds.slice(), failed: 0, total: 0 };
    10941211                }
     1212                // Previously this failure was swallowed silently: the file
     1213                // was skipped without any visible error and the flow still
     1214                // ended on the success screen.
     1215                failed += 1;
    10951216                payloads.push(null);
     1217                var badItem = queue[q];
     1218                var badLabel = (badItem && badItem.name) || getText('upload_file', 'file');
     1219                var badErr = (err && err.message) ? err.message : getText('upload_failed', 'Upload failed');
     1220                showError(badLabel + ': ' + badErr);
    10961221            }
    10971222        }
    … …  
    11021227            elements.progressText = savedText;
    11031228            resetFormMode();
    1104             return state.mediaIds.slice();
     1229            return { mediaIds: state.mediaIds.slice(), failed: 0, total: 0 };
    11051230        }
    11061231
    … …  
    11701295                        elements.progressText = savedText;
    11711296                        resetFormMode();
    1172                         return state.mediaIds.slice();
     1297                        return { mediaIds: state.mediaIds.slice(), failed: 0, total: 0 };
    11731298                    }
    11741299                    if (!item.uploaded) {
    … …  
    11761301                    }
    11771302                    setItemProgress(item, 0);
     1303                    failed += 1;
    11781304                    var fileLabel = item.name || getText('upload_file', 'file');
    11791305                    var errMsg = (error && error.message) ? error.message : getText('upload_failed', 'Upload failed');
    … …  
    11861312            if (isCancelled) {
    11871313                resetFormMode();
    1188                 return state.mediaIds.slice();
     1314                return { mediaIds: state.mediaIds.slice(), failed: 0, total: 0 };
    11891315            }
    11901316
    … …  
    11981324            hideProgress();
    11991325        }
    1200         return state.mediaIds.slice();
     1326        return { mediaIds: state.mediaIds.slice(), failed: failed, total: total };
    12011327    }
    12021328
    … …  
    14471573        reset: function() {
    14481574            resetState();
     1575            removeThankYouNotice();
     1576            lastCommentId = 0;
    14491577            if (elements.preview) {
    14501578                elements.preview.innerHTML = '';
    … …  
    14691597        },
    14701598
    1471         showThankYou: function() {
    1472             showThankYou();
     1599        showThankYou: function(result) {
     1600            showThankYou(result);
    14731601        },
    14741602
  • pvr-media-reviews-for-woocommerce/trunk/includes/class-pvr-hooks.php

    r3748498 r3760546  
    14721472                                'picker_no_results'        => __( 'No products found', 'pvr-media-reviews-for-woocommerce' ),
    14731473                                'picker_select_product'    => __( 'Please select a product first', 'pvr-media-reviews-for-woocommerce' ),
     1474                                'thankyou_media_failed'    => __( 'Your review has been submitted, but some media files failed to upload. You can retry.', 'pvr-media-reviews-for-woocommerce' ),
     1475                                'thankyou_retry_upload'    => __( 'Retry upload', 'pvr-media-reviews-for-woocommerce' ),
     1476                                'thankyou_media_complete'  => __( 'All media uploaded successfully.', 'pvr-media-reviews-for-woocommerce' ),
    14741477            ],
    14751478        ];
  • pvr-media-reviews-for-woocommerce/trunk/includes/class-pvr-media.php

    r3750600 r3760546  
    459459                                // binding: a guest comment may receive media only from the
    460460                                // address that submitted it, so sequential comment IDs from
    461                                 // other visitors cannot be targeted.
     461                                // other visitors cannot be targeted. Same resolver as the
     462                                // submit side — REMOTE_ADDR alone breaks behind a reverse
     463                                // proxy / Cloudflare, where it is the proxy's address.
    462464                                $comment_ip = (string) $comment->comment_author_IP;
    463                                 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Same normalization wp_new_comment applies to REMOTE_ADDR.
    464                                 $current_ip = preg_replace( '/[^0-9a-fA-F:., ]/', '', (string) wp_unslash( $_SERVER['REMOTE_ADDR'] ?? '' ) );
     465                                $current_ip = PVREV_Hooks::get_client_ip();
    465466                                if ( '' === $comment_ip || ! hash_equals( $comment_ip, $current_ip ) ) {
    466467                                        $comment_id = 0;
    … …  
    513514                        }
    514515
     516                        // Variant paths come back from ensure_photo_variants() as paths
     517                        // relative to uploads; str_replace( basedir, baseurl, ... ) here
     518                        // was a no-op on them and esc_url() turned the bare relative path
     519                        // into "http://pvr-reviews/...". get_media_url() resolves both
     520                        // relative and absolute paths correctly.
    515521                        $thumbnail_url = null;
    516522                        if ( ! empty( $thumbnail_path ) ) {
    517                                 $thumbnail_url = str_replace( trailingslashit( $upload['basedir'] ), trailingslashit( $upload['baseurl'] ), $thumbnail_path );
     523                                $thumbnail_url = self::get_media_url( $thumbnail_path );
    518524                        }
    519525                        $large_url = null;
    520526                        if ( ! empty( $large_path ) ) {
    521                                 $large_url = str_replace( trailingslashit( $upload['basedir'] ), trailingslashit( $upload['baseurl'] ), $large_path );
     527                                $large_url = self::get_media_url( $large_path );
    522528                        }
    523529                        $popup_url = null;
    524530                        if ( ! empty( $popup_path ) ) {
    525                                 $popup_url = str_replace( trailingslashit( $upload['basedir'] ), trailingslashit( $upload['baseurl'] ), $popup_path );
     531                                $popup_url = self::get_media_url( $popup_path );
    526532                        }
    527533
  • pvr-media-reviews-for-woocommerce/trunk/includes/class-pvr-review.php

    r3737674 r3760546  
    327327                                'comment_author_email' => $author_email,
    328328                                'comment_author_url'   => '',
     329                                // wp_insert_comment() does not fill the IP (unlike
     330                                // wp_new_comment()); guest media attach in PVREV_Media
     331                                // compares this value with the upload request's IP.
     332                                'comment_author_IP'    => PVREV_Hooks::get_client_ip(),
    329333                                'comment_content'      => $content,
    330334                                'comment_type'         => 'review',
  • pvr-media-reviews-for-woocommerce/trunk/languages/pvr-media-reviews-for-woocommerce-en_US.json

    r3759180 r3760546  
    13941394  "Replies skipped (parent review missing)": "Replies skipped (parent review missing)",
    13951395  "Reply queue limit reached — reply row skipped.": "Reply queue limit reached — reply row skipped.",
    1396   "Review ID (source file)": "Review ID (source file)"
     1396  "Review ID (source file)": "Review ID (source file)",
     1397  "Your review has been submitted, but some media files failed to upload. You can retry.": "Your review has been submitted, but some media files failed to upload. You can retry.",
     1398  "Retry upload": "Retry upload",
     1399  "All media uploaded successfully.": "All media uploaded successfully."
    13971400}
  • pvr-media-reviews-for-woocommerce/trunk/languages/pvr-media-reviews-for-woocommerce-es_ES.json

    r3759180 r3760546  
    13761376  "Replies skipped (parent review missing)": "Respuestas omitidas (falta la reseña principal)",
    13771377  "Reply queue limit reached — reply row skipped.": "Se alcanzó el límite de la cola de respuestas — fila de respuesta omitida.",
    1378   "Review ID (source file)": "ID de reseña (archivo de origen)"
     1378  "Review ID (source file)": "ID de reseña (archivo de origen)",
     1379  "Your review has been submitted, but some media files failed to upload. You can retry.": "Tu reseña se ha enviado, pero algunos archivos multimedia no se pudieron cargar. Puedes reintentarlo.",
     1380  "Retry upload": "Reintentar subida",
     1381  "All media uploaded successfully.": "Todos los archivos multimedia se subieron correctamente."
    13791382}
  • pvr-media-reviews-for-woocommerce/trunk/languages/pvr-media-reviews-for-woocommerce-ru_RU.json

    r3759180 r3760546  
    13901390  "Replies skipped (parent review missing)": "Пропущено ответов (нет родительского отзыва)",
    13911391  "Reply queue limit reached — reply row skipped.": "Достигнут лимит очереди ответов — строка ответа пропущена.",
    1392   "Review ID (source file)": "ID отзыва (в файле-источнике)"
     1392  "Review ID (source file)": "ID отзыва (в файле-источнике)",
     1393  "Your review has been submitted, but some media files failed to upload. You can retry.": "Ваш отзыв отправлен, но часть медиафайлов не удалось загрузить. Можно повторить попытку.",
     1394  "Retry upload": "Повторить загрузку",
     1395  "All media uploaded successfully.": "Все медиафайлы успешно загружены."
    13931396}
  • pvr-media-reviews-for-woocommerce/trunk/photo-video-reviews-for-woocommerce.php

    r3759180 r3760546  
    44 * Plugin URI: https://wordpress.org/plugins/pvr-media-reviews-for-woocommerce/
    55 * Description: Photo and video reviews for WooCommerce with client-side compression.
    6  * Version: 1.2.7
     6 * Version: 1.2.8
    77 * Author: Lebedgor
    88 * Author URI: https://demo.pv-reviews.site/
    … …  
    1818defined( 'ABSPATH' ) || exit;
    1919
    20 define( 'PVREV_VERSION', '1.2.7' );
     20define( 'PVREV_VERSION', '1.2.8' );
    2121define( 'PVREV_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
    2222define( 'PVREV_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
  • pvr-media-reviews-for-woocommerce/trunk/readme.txt

    r3759180 r3760546  
    55Tested up to: 7.0
    66Requires PHP: 7.1
    7 Stable tag: 1.2.7
     7Stable tag: 1.2.8
    88WC requires at least: 7.0
    99WC tested up to: 9.0
    … …  
    175175== Changelog ==
    176176
     177= 1.2.8 =
     178* Fix: photo and video uploads failed to attach to reviews — the review saved and the customer saw the success screen, but the admin found no media: wp_insert_comment() does not fill comment_author_IP, so the anti-enumeration guest attach check (added in 1.2.0) silently re-bound every upload to comment 0. The submit now stores the client IP (proxy/Cloudflare-aware) and the attach check resolves the request IP through the same resolver
     179* Improvement: failed media uploads no longer end on a silent success screen — the thank-you screen shows which files failed, offers a Retry upload that re-runs only the failed files against the same review, and the 5-second auto-close only runs when everything is uploaded; prepare/compression errors now surface a toast instead of skipping the file invisibly
     180* Fix: photo thumbnail/large webp URLs in the upload response were built as "http://pvr-reviews/..." when the variant path came back relative (esc_url on a bare path); URLs are now resolved through PVREV_Media::get_media_url()
     181* Fix: the Reply button in the review details modal did nothing on pages rendered without the reviews list (media gallery block, hub page, My Reviews, standalone form) — the shared reply form modal is now printed once per request for every surface
     182* New: the review details modal shows a product mini-card (thumbnail + name) at the top of its sidebar, above the author — desktop aside and mobile slide both follow review flip-through, so mixed-product surfaces (media gallery, all-media strip, hub) always show which product the review belongs to
     183* Fix: the review form could stretch past the viewport on mobile — long product names no longer force min-content width (nowrap removed), and text inputs keep the form styling (rounded corners, border color) on themes that load their own input styles later
     184* Updated plugin banners
     185
    177186= 1.2.7 =
    178187* New: import preset for CusRev (Customer Reviews for WooCommerce) — a file format selector in the import wizard recognizes their export columns automatically (review_title, review_content, review_score, display_name, media), so a CusRev CSV maps without any manual column setup
Note: See TracChangeset for help on using the changeset viewer.