�PNG  IHDR22?�� pHYs  �� OiCCPPhotoshop ICC profilexڝSgTS�=���BK���KoR RB���&*! J�!��Q�EEȠ�����Q, � ��!���������{�kּ������>���� �H3Q5� �B�������.@� $p�d!s�#�~<<+"��x� �M��0���B�\���t�8K�@z�B�@F���&S�`�cb�P-`'������{[�!�� e�Dh;��V�EX0fK�9�-0IWfH���� � 0Q��){`�##x��F�W<�+��*x��<�$9E�[-qWW.(�I+6aa�@.�y�2�4���������x����6��_-��"bb���ϫp@�t~��,/��;�m��%�h^ �u��f�@����W�p�~<�5�j>{�-�]c�K'Xt����o��(�h���w��?�G�%�fI�q^D$.Tʳ?�D��*�A��,���� �`6�B$��BB d�r`)��B(�Ͱ*`/�@4�Qh��p.�U�=p�a��(�� A�a!ڈb�X#����!�H�$ ɈQ"K�5H1R�T UH�=r9�\F��;�2����G1���Q=� �C��7�F� �dt1�����r�=�6��Ыhڏ>C�0��3�l0.��B�8, �c˱"� ���V����cϱw�E� 6wB aAHXLXN�H� $4� 7 �Q�'"��K�&���b21�XH,#��/{�C�7$�C2'��I��T��F�nR#�,��4H#���dk�9�, +ȅ����3��!�[ �b@q��S�(R�jJ��4�e�2AU��Rݨ�T5�ZB���R�Q��4u�9̓IK�����hh�i��t�ݕN��W���G���w ��Ljg(�gw��L�Ӌ�T071���oUX*�*|�� �J�&�*/T����ު U�U�T��^S}�FU3S� Ԗ�U��P�SSg�;���g�oT?�~Y��Y�L�OC�Q��_�� c�x,!k ��u�5�&���|v*�����=���9C3J3W�R�f?�q��tN �(���~���)�)�4L�1e\k����X�H�Q�G�6������E�Y��A�J'\'Gg����S�Sݧ �M=:��.�k���Dw�n��^��Lo��y���}/�T�m���G X� $� �<�5qo</���QC]�@C�a�a�ᄑ��<��F�F�i�\�$�m�mƣ&&!&KM�M�RM��)�;L;L���͢�֙5�=1�2��כ߷`ZxZ,����eI��Z�Yn�Z9Y�XUZ]�F���%ֻ�����N�N���gð�ɶ�����ۮ�m�}agbg�Ů��}�}��= ���Z~s�r:V:ޚΜ�?}����/gX���3��)�i�S��Ggg�s�󈋉K��.�>.���Ƚ�Jt�q]�z���������ۯ�6�i�ܟ�4�)�Y3s���C�Q��? ��0k߬~OCO�g��#/c/�W�װ��w��a�>�>r��>�<7�2�Y_�7��ȷ�O�o�_��C#�d�z����%g��A�[��z|!��?:�e����A���AA�������!h�쐭!��Α�i�P~���a�a��~ '���W�?�p�X�1�5w��Cs�D�D�Dޛg1O9�-J5*>�.j<�7�4�?�.fY��X�XIlK9.*�6nl������� �{�/�]py�����.,:�@L�N8��A*��%�w%� y��g"/�6ш�C\*N�H*Mz�쑼5y$�3�,幄'���L Lݛ:��v m2=:�1����qB�!M��g�g�fvˬe����n��/��k���Y- �B��TZ(�*�geWf�͉�9���+��̳�ې7�����ᒶ��KW-X潬j9�������(�x��oʿ�ܔ���Ĺd�f�f���-�[����n �ڴ �V����E�/��(ۻ��C���<��e����;?T�T�T�T6��ݵa��n��{��4���[���>ɾ�UUM�f�e�I���?�������m]�Nmq����#�׹���=TR��+�G�����w- 6 U����#pDy��� �� :�v�{���vg/jB��F�S��[b[�O�>����z�G��499�?r����C�d�&����ˮ/~�����јѡ�򗓿m|������������x31^�V���w�w��O�| (�h���SЧ��������c3-� cHRMz%������u0�`:�o�_�F5IDATx��ytUս�?�;���fN$$��@B�!�k��� -*�gE���j�O�kQ�ʠ`U�B_ �AÐ0%�Ȕ�@r3ޛ;߳��$�@��v����^�s����������$��ߢ !�W��5�̱'-٘������4�>�Rƫ$�G��0U�H}�_ _� #��k�MJ/=�9Du *ڲ�k�PQ;0��j|�*�$�^컂脛y���>�z×� B��� ��(�:$�ʔ_�6��C!B����MVx��a��ζyY����j��ƋY2�9�F�{r��r�딟F��Q��hY6��� x��[l��RC��������nt@��;P��3��� F�w��7��:�P���ɋ'���mj���Bƪ�$�Q�L��y��߷g��\���z����v5�p�v$�v櫻�`6~K �Y�X����,�:��"�o�e0II"(��,������l_�C���/4�+f�E�!q��:����u����c��樼.UE�@� �]K�W���$-�h]�JG���Dz)�`��.�A/ ���mᏣc�3jK� �!�>���$�Q�!�_�? \�C2(E �M$Z I{�U���s�ǣ���=��@Բe ޻��W�����@ �w����}�9v,)��.}*|"�s��di���,���G�]W�AC�������|<����[��ߕ������K�*�G nߴ!��‹��J�!��6(L�a)��Am��������;adf��:�xm��nh�;֦I��(���o�C~ډa?�=��s����h��E������xp����)���_���t��W�����´;8���`dz=(��@@�+,�;�`�fhl��XȌ�3,uI�~z(J����� �ZX�A����H'�À�㜦�82|��a�n�z2��I���� ���EY�h�ε<�0�q�{��C��v�˅��?"@��χ�x�WS+��N�~�KQ�J�8ل�� _.W��� !TU{�����,��"�� �;�������HI S� ��))�o�1�H�D� ���� (� ;����"Ҕ��,���� �F=���O��&�Ph>�`���y��߲ &S��P�<��'��1s�1cĂ�N����2)����@�u��tv��<�+�-&�����h��[!FSG���' � z z0��<����8d&�����E0�,��<��"!P�b4��� �{S�����X����.�囤���T��hEF��X�q0"-<����h���]�Py*����F⩍��@v�˂�Y08�� .q��a��s����(!<̫1�3�L�H��7sYW�`��qtu�\7��?�@�Y�� %Ka�x�GZ�*W�P��y�����N0r9��Z�c�%���q,��k�����!!,��>���IB��� �x1�Xu�Y���v-���ֺ��N������JP��a�2`�e5��w��u;�6�i���̃��(K���ťa��(l51��JL��O'%���]���ٖ뿉!��{*�p+f ���i8��D�ȑ���ܩ�M��V��ӟz��x�7Z�1i}�^͜���6e����x��)u�d�ˏ�V�eG�\�X1�����=n�U'�~Y����L�\,�(d!x ,�:�TW8� M`r���@��r+v�Q0��>^;˴�VbF��('����� �*O��i��j�Ӆ����^�S3&� ���{������.ی�� N;��NU%d0�Ce\r"�C�b��&t7N�7��� �ȪD�[%l�۪�$��'m?�A9tI�9>��NVL ��� 6v~M�����ъ�B���0����9$&=ώm��W�0���L���&��E���f�f��'+�9��hv��7,���M��6�D�&�^� �����8 D֮�����o��k^z��O��hP�8q�5d*��YVq3�{��������#LX4�?��jP=3�~�؅c�ݰ�̱�ұ��8̲��2�G�����j��έ�9��w7���U�X�w�85C��?!I&���o&����L���I#�^�eV�,�^t5�k� �xs˛\��U�8��."���{���ǵ�l4��ݼ�;��r"���G�/~���gJV�N糦��ZZ^k+NN�D��I$�=OE�z����|*�*{��%k���G��>�`9��b;�s���8,�qs��c��.b��q�%�>r��n�R�Ƨ�%�,�A��le#�8���}; 21�0�b�&�QX�f���q�00����$>�h�N�'�a3��A���/�r ��L5�8�GԲ'�����w;��c#�q1���h$*��V^�k��QĽ�b:i������� ����H F#�_���,��]g����Ը����`RUU���� ��"�qD���>-��ϤI�~��3�̴�����NZ[[555�eeek7�m����YF#[��Đ��R��i��L�H�r ��@��J-�Pŝ��_|�-��6y��ks�V�U~��6���w.����(��0A� 4��f��� /���C�xŊ�ٳ����z=6�����rg���mKo��o�/����a�g���?'9%9��P��斓��~���L{�� �H��{AW��B��$����v�1b�&Mb�ر��ر#�0`�`xMBp! )Z�Nmo�mZ�g�}�eذaWTVV ���'99��C5��&�&+5�bT>|8��PUUETT����&���t��~TE%��'������b�tEGG;RSS�S�N�v��ѯ��t����u1@�ޚ"f��͈���r'L�Ph��m��ƌ�74o��`�x�q�P���NRSS�X,�����l&&&��MFFA!-��`��ҷ�n��Yuu�����x<I�LB%����KԒ50�!�3�bz�����ʸKL���;---�l6[��`��������Ȏ;�����egg4��!D�T��jWujyZ��������K����%����p�^���X,6��sV=�ܖ����6�3�X ���4ҿ��A2�&����_k�;d�� �VfOIEND�B`�
Warning: session_start(): Session cannot be started after headers have already been sent in /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php on line 124

Warning: Cannot modify header information - headers already sent by (output started at /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php:1) in /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php on line 130

Warning: Cannot modify header information - headers already sent by (output started at /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php:1) in /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php on line 131

Warning: Cannot modify header information - headers already sent by (output started at /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php:1) in /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php on line 132

Warning: Cannot modify header information - headers already sent by (output started at /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php:1) in /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php on line 133

Warning: Cannot modify header information - headers already sent by (output started at /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php:1) in /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php on line 134

Warning: Cannot modify header information - headers already sent by (output started at /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php:1) in /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php on line 135

Warning: Cannot modify header information - headers already sent by (output started at /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php:1) in /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php on line 547

Warning: Cannot modify header information - headers already sent by (output started at /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php:1) in /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php on line 548

Warning: Cannot modify header information - headers already sent by (output started at /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php:1) in /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php on line 549

Warning: Cannot modify header information - headers already sent by (output started at /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php:1) in /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php on line 550

Warning: Cannot modify header information - headers already sent by (output started at /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php:1) in /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php on line 551
0 ? get_post( $option_id ) : null; if ( $option_page instanceof WP_Post && 'page' === $option_page->post_type && 'publish' === $option_page->post_status && false !== strpos( (string) $option_page->post_content, '[' . self::SHORTCODE . ']' ) ) { return; } // Reconcile: adopt the slug-routed page when it also embeds our // shortcode. The combined signal avoids hijacking a merchant page // that happens to share either the slug or the shortcode alone. $canonical = $this->find_canonical_host_page(); if ( $canonical instanceof WP_Post ) { $needs_save = false; if ( $option_id !== (int) $canonical->ID ) { update_option( 'woocommerce_review_order_page_id', (int) $canonical->ID ); $needs_save = true; } if ( 'publish' !== $canonical->post_status ) { wp_update_post( array( 'ID' => (int) $canonical->ID, 'post_status' => 'publish', ) ); $needs_save = true; } if ( $needs_save ) { update_option( 'woocommerce_review_order_flush_rewrite_pending', 'yes' ); } return; } // No slug-canonical page. If the merchant renamed the host page away // from our default slug but the stored option still resolves to a // non-trashed page, respect it and only republish a draft we own. if ( $option_page instanceof WP_Post && 'page' === $option_page->post_type && 'trash' !== $option_page->post_status ) { if ( 'publish' !== $option_page->post_status ) { wp_update_post( array( 'ID' => (int) $option_page->ID, 'post_status' => 'publish', ) ); update_option( 'woocommerce_review_order_flush_rewrite_pending', 'yes' ); } return; } // No managed page anywhere. The permanent `woocommerce_create_pages` // filter (registered in `init()`) makes the call inject our entry. \WC_Install::create_pages(); // Defer the rewrite flush to wp_loaded; rewrite_rule fires later on init. update_option( 'woocommerce_review_order_flush_rewrite_pending', 'yes' ); } /** * Append the Review Order page to any caller of * `WC_Install::create_pages()` — keeps Status → Tools' "Create default * pages" repair path and any third-party callers seeded with our page * whenever the feature is on, without having to call create_pages() * with a one-off filter in `maybe_create_host_page()`. * * @since 10.8.0 * * @internal Public only because WP filter callbacks need to be callable from outside. * * @param array>|mixed $pages Existing page definitions. * @return array>|mixed */ public function inject_review_order_page( $pages ) { if ( ! is_array( $pages ) ) { return $pages; } $pages[ self::PAGE_KEY ] = array( 'name' => _x( 'review-order', 'Page slug', 'woocommerce' ), 'title' => _x( 'Review your order', 'Page title', 'woocommerce' ), 'content' => '[' . self::SHORTCODE . ']', ); return $pages; } /** * Return the slug-routed page if it also embeds our shortcode, so we only * adopt rows that are unambiguously WC-owned (matching slug alone or the * shortcode alone would hijack merchant-authored pages). * * @since 10.8.0 * * @return WP_Post|null */ private function find_canonical_host_page(): ?WP_Post { $page = get_page_by_path( _x( 'review-order', 'Page slug', 'woocommerce' ), OBJECT, 'page' ); if ( ! $page instanceof WP_Post || 'trash' === $page->post_status ) { return null; } if ( false === strpos( (string) $page->post_content, '[' . self::SHORTCODE . ']' ) ) { return null; } return $page; } /** * Label the Review Order page in the admin Pages list ("— Review Order * Page"), mirroring how `WC_Admin_Post_Types` labels Shop / Cart / * Checkout / My account so editors can spot it at a glance. * * @since 10.8.0 * * @internal Public only because WP filter callbacks need to be callable from outside. * * @param array|mixed $post_states Existing post-state labels keyed by id. * @param \WP_Post|mixed $post Current post being listed. * @return array|mixed */ public function add_post_state_label( $post_states, $post ) { if ( ! is_array( $post_states ) || ! $post instanceof \WP_Post ) { return $post_states; } $page_id = (int) wc_get_page_id( self::PAGE_KEY ); if ( $page_id > 0 && $page_id === (int) $post->ID ) { $post_states['wc_page_for_review_order'] = __( 'Review Order Page', 'woocommerce' ); } return $post_states; } /** * Hide the Review Order page from `get_pages()` results. * * Block themes' `core/page-list` block (and any classic theme using * `wp_list_pages()`) calls `get_pages()` to populate its list. Without * this filter the tokenised landing page would appear in the site * navigation alongside Cart / Checkout / My account, which is wrong: * the page is reachable only through the per-order email link. * * @param \WP_Post[]|mixed $pages Page objects returned by get_pages(). * @return \WP_Post[]|mixed */ public function exclude_self_from_page_list( $pages ) { if ( ! is_array( $pages ) || empty( $pages ) ) { return $pages; } $page_id = (int) wc_get_page_id( self::PAGE_KEY ); if ( $page_id <= 0 ) { return $pages; } return array_values( array_filter( $pages, static function ( $page ) use ( $page_id ) { return ! ( $page instanceof \WP_Post ) || (int) $page->ID !== $page_id; } ) ); } /** * Suppress the theme-rendered page title for classic themes on the * Review Order page. * * The page body (`templates/order/customer-review-order.php` and the * empty-state template) already prints its own `

`, so the chrome * heading would duplicate the text both visually and for screen readers. * * `gate_request()` registers this filter only after the request passes * the auth check, so on any unrelated render it isn't even on the hook. * Two in-method guards narrow the scope to the page title slot of the * Review Order render itself: * * - The post id must match the Review Order page id, so within the same * render a nav menu item or "recent posts" widget pointing at another * post stays intact. * - `in_the_loop() && is_main_query()` keeps the filter scoped to the * actual page title slot. WP's `wp_get_document_title()` reads the * post title outside the loop, so the `` tag stays meaningful. * * @since 10.8.0 * * @param string|mixed $title Title being rendered. * @param int|mixed $post_id Post id the title belongs to. * @return string|mixed */ public function maybe_hide_page_title( $title, $post_id = 0 ) { $page_id = (int) wc_get_page_id( self::PAGE_KEY ); if ( (int) $post_id !== $page_id ) { return $title; } if ( ! in_the_loop() || ! is_main_query() ) { return $title; } return ''; } /** * Suppress the `core/post-title` block on block themes when it is bound * to the Review Order page itself. * * Block themes render the page title through `core/post-title` rather * than `the_title`, so the classic-theme filter above doesn't catch it. * Two guards keep the suppression narrow (registration is gated by * `gate_request()` so the filter isn't even on the hook for unrelated * renders): * * - The hook is `render_block_core/post-title` so unrelated block types * (headings, paragraphs, navigation, etc.) never reach this method. * - The block's resolved `context['postId']` must match the Review Order * page id, so a `core/post-title` rendered inside a Query Loop, a * related-posts template part, or a footer "recent posts" panel for a * different post on the same render is untouched. * * @since 10.8.0 * * @param string|mixed $block_content Block markup. * @param array<string,mixed> $block Parsed block (unused but kept for filter signature). * @param \WP_Block|mixed|null $instance Rendering instance carrying context. * @return string|mixed */ public function maybe_hide_post_title_block( $block_content, $block, $instance = null ) { unset( $block ); if ( ! $instance instanceof \WP_Block ) { return $block_content; } $page_id = (int) wc_get_page_id( self::PAGE_KEY ); $block_postid = isset( $instance->context['postId'] ) ? (int) $instance->context['postId'] : 0; if ( $block_postid !== $page_id ) { return $block_content; } return ''; } /** * Keep the Review Order page out of nav menus that have "Auto add new * top-level pages" enabled. * * The page is reachable only through the tokenised URL the email sends * out; nobody navigates to it from a menu, so it should never appear * there. WP's `_wp_auto_add_pages_to_menu()` runs on * `transition_post_status` at priority 10. Detach it just before that * for our specific page, then restore it on priority 11 so other * transitions are unaffected. * * Compares by slug rather than by stored option id so it also fires on * the very first install — before `woocommerce_review_order_page_id` * is written. * * @param string $new_status New post status. * @param string $old_status Old post status. * @param \WP_Post $post Post object. */ public function skip_auto_menu_for_self( $new_status, $old_status, $post ): void { unset( $new_status, $old_status ); if ( ! $post instanceof \WP_Post || 'page' !== $post->post_type ) { return; } // Identify the page by stored option id (post-install) or by the // shortcode in its content (during install, before the option // exists). Don't compare $post->post_name to 'review-order' alone: // WP appends -2/-3/... if the slug already exists. $stored_id = (int) get_option( 'woocommerce_review_order_page_id' ); $is_by_id = $stored_id > 0 && $stored_id === (int) $post->ID; $is_by_slug = '' === $post->post_name ? false : ( 'review-order' === $post->post_name || 0 === strpos( $post->post_name, 'review-order-' ) ); $is_by_body = false !== strpos( (string) $post->post_content, '[' . self::SHORTCODE . ']' ); if ( ! $is_by_id && ! $is_by_slug && ! $is_by_body ) { return; } remove_action( 'transition_post_status', '_wp_auto_add_pages_to_menu', 10 ); add_action( 'transition_post_status', static function () { add_action( 'transition_post_status', '_wp_auto_add_pages_to_menu', 10, 3 ); }, 11 ); } /** * Flush rewrite rules once after the Review Order page is seeded or * republished. * * `maybe_create_host_page()` runs on `init` priority 4 and queues the * flush by setting `woocommerce_review_order_flush_rewrite_pending`; * `add_rewrite_rule()` doesn't fire until `init` priority 10, so the * flush has to happen later. `wp_loaded` runs after every `init` * callback, which is the earliest safe moment. */ public function maybe_flush_pending_rewrite(): void { if ( 'yes' !== get_option( 'woocommerce_review_order_flush_rewrite_pending' ) ) { return; } flush_rewrite_rules( false ); delete_option( 'woocommerce_review_order_flush_rewrite_pending' ); } /** * Register the rewrite rule for the review-order endpoint. * * Maps `/<page-slug>/{id}/` to the WC-managed Review Order page so the * active theme renders its standard page chrome around the shortcode. */ public function add_rewrite_rule(): void { $page_id = (int) wc_get_page_id( self::PAGE_KEY ); if ( $page_id <= 0 ) { return; } $page = get_post( $page_id ); if ( ! $page instanceof WP_Post || 'publish' !== $page->post_status ) { return; } // Use the full page-permalink path so hierarchical pages // (Review Order page moved under a parent) keep working. $permalink = get_permalink( $page_id ); if ( ! is_string( $permalink ) || '' === $permalink ) { return; } $path = trim( (string) wp_make_link_relative( $permalink ), '/' ); if ( '' === $path ) { return; } add_rewrite_rule( '^' . preg_quote( $path, '/' ) . '/([0-9]+)/?$', 'index.php?page_id=' . $page_id . '&' . self::QUERY_VAR . '=$matches[1]', 'top' ); } /** * Allow the query var through `WP::parse_request()`. * * @param string[] $vars Query vars. * @return string[] */ public function add_query_var( array $vars ): array { $vars[] = self::QUERY_VAR; return $vars; } /** * Run the gating checks before the page template renders. * * Auth failures fall through to a 404 here rather than inside the * shortcode so the response status is set before any output begins. * On success the request continues into normal page rendering and the * shortcode echoes the body inside `the_content`. */ public function gate_request(): void { global $wp; // Only act when the request resolves to the WC-managed Review Order // page. A leftover review-order query var on some other page (manual // URL tampering, third-party plugin) shouldn't trigger our auth // path or 404 an unrelated page. $page_id = (int) wc_get_page_id( self::PAGE_KEY ); if ( $page_id <= 0 || ! is_page( $page_id ) ) { return; } // Use isset() rather than empty() so the literal "0" doesn't slip // through to normal WP routing; the auth check 404s on order_id 0. if ( ! isset( $wp->query_vars[ self::QUERY_VAR ] ) ) { // Visiting the host page directly (no order id in the URL) is a // dead end — the shortcode renders nothing and the customer // sees a chrome-only page. Send them to the home page instead. wp_safe_redirect( home_url( '/' ) ); exit; } $order_id = absint( $wp->query_vars[ self::QUERY_VAR ] ); $order_key = $this->read_order_key(); $order = $order_id ? wc_get_order( $order_id ) : false; if ( ! $this->is_authorised( $order, $order_key ) ) { $this->render_404(); exit; } // Register the page-title suppression filters now that the request // is fully authorised. Doing this here instead of `init()` keeps the // filters out of every unrelated page render and removes the need // for a per-instance "is this an authorised render" boolean. add_filter( 'the_title', array( $this, 'maybe_hide_page_title' ), 10, 2 ); // Block-specific filter so only `core/post-title` is touched — // `render_block` would fire for every block on the page. The third // arg is the `WP_Block` instance carrying `context['postId']`, used // to scope to the host page. add_filter( 'render_block_core/post-title', array( $this, 'maybe_hide_post_title_block' ), 10, 3 ); if ( $order instanceof WC_Order ) { $this->maybe_mark_no_actionable_rows( $order ); } // template_redirect fires after wp_enqueue_scripts but before // wp_head, so styles registered here are still output in <head>. $this->enqueue_assets(); } /** * Render the Review Order page body for the WC-managed page. * * Called by `the_content` on the page that hosts `[woocommerce_review_order]`. * Confirms the current page and order key before rendering. * * @return string */ public function render_shortcode(): string { global $wp; $page_id = (int) wc_get_page_id( self::PAGE_KEY ); if ( $page_id <= 0 || ! is_page( $page_id ) ) { return ''; } if ( ! isset( $wp->query_vars[ self::QUERY_VAR ] ) ) { return ''; } $order_id = absint( $wp->query_vars[ self::QUERY_VAR ] ); $order_key = $this->read_order_key(); $order = $order_id ? wc_get_order( $order_id ) : false; if ( ! $this->is_authorised( $order, $order_key ) ) { return ''; } ob_start(); wc_get_template( 'order/customer-review-order.php', array( 'order' => $order ) ); return (string) ob_get_clean(); } /** * Render the Review Order body directly. Public so unit tests can drive * the rendering path without staging a global request and the rewrite. * * @internal * * @param int $order_id Order id parsed from the URL. */ public function render( int $order_id ): void { $order_key = $this->read_order_key(); $order = $order_id ? wc_get_order( $order_id ) : false; if ( ! $this->is_authorised( $order, $order_key ) ) { $this->render_404(); return; } if ( $order instanceof WC_Order ) { $this->maybe_mark_no_actionable_rows( $order ); } wc_get_template( 'order/customer-review-order.php', array( 'order' => $order ) ); } /** * Stamp the completed-at meta when the Review Order page would render the * empty-state, so back-button visits and direct revisits also record * completion. The persistent write lives here, in the controller, so the * page template stays read-only. * * Scope differs from `SubmissionHandler::maybe_mark_order_complete()`: * that one counts the customer's reviews per product across all of their * history, while this one walks the per-item decisions ItemEligibility * produces (order-scoped, mirroring exactly what the page renders). * * @param WC_Order $order Order being reviewed. */ private function maybe_mark_no_actionable_rows( WC_Order $order ): void { $completed_meta_key = SubmissionHandler::COMPLETED_META_KEY; if ( $order->get_meta( $completed_meta_key ) ) { return; } // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment -- documented on customer-review-order.php template. $items = (array) apply_filters( 'woocommerce_review_order_eligible_items', $order->get_items(), $order ); ItemEligibility::preload_for_items( $items, $order ); foreach ( $items as $item ) { if ( ! $item instanceof \WC_Order_Item_Product ) { continue; } $decision = ItemEligibility::decide( $item, $order ); // Skip rows are intentionally treated as "done": an order whose // items all have reviews disabled renders the empty-state, so we // stamp completion to match what the customer sees on the page. if ( ItemEligibility::STATUS_SKIP === $decision['status'] ) { continue; } // Any non-skip row without a review tied to this order means the // customer still has something to submit — order isn't complete. if ( ! ( $decision['comment'] instanceof \WP_Comment ) ) { return; } } $order->update_meta_data( $completed_meta_key, (string) time() ); try { $order->save(); } catch ( \Exception $e ) { wc_get_logger()->warning( sprintf( /* translators: 1: order ID, 2: error message */ __( 'Could not stamp Review Order completion meta on order %1$d: %2$s.', 'woocommerce' ), $order->get_id(), $e->getMessage() ), array( 'source' => 'order-reviews' ) ); } } /** * Build the public, tokenised URL for an order's review-order page. * * @param WC_Order $order Order to build the URL for. * @return string */ public static function get_url( WC_Order $order ): string { $page_id = (int) wc_get_page_id( self::PAGE_KEY ); $permalink = (string) ( $page_id > 0 ? get_permalink( $page_id ) : '' ); if ( '' === $permalink ) { $url = ''; } elseif ( false === strpos( $permalink, '?' ) ) { // Pretty permalinks: append the order id as a path segment. $url = trailingslashit( $permalink ) . (string) $order->get_id() . '/'; $url = add_query_arg( 'key', $order->get_order_key(), $url ); } else { // Plain permalinks: page permalink is /?page_id=NNN, so add the // order id as a query var rather than munging the path. $url = add_query_arg( array( self::QUERY_VAR => (string) $order->get_id(), 'key' => $order->get_order_key(), ), $permalink ); } /** * Filter the Review Order URL that the review-request email links to. * * @since 10.8.0 * * @param string $url The review-order URL. * @param WC_Order $order The order object. */ return (string) apply_filters( 'woocommerce_review_order_url', $url, $order ); } /** * Read the order key from the request, sanitised. * * @return string */ private function read_order_key(): string { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only landing page; the order key is the auth. $raw = ( isset( $_GET['key'] ) && is_string( $_GET['key'] ) ) ? wc_clean( wp_unslash( $_GET['key'] ) ) : ''; return is_string( $raw ) ? $raw : ''; } /** * Decide whether the request is allowed to render the page. * * @param mixed $order The candidate order. Anything other than a `WC_Order` fails. * @param string $order_key The order key supplied via query arg. * @return bool */ private function is_authorised( $order, string $order_key ): bool { if ( ! $order instanceof WC_Order ) { return false; } if ( '' === $order_key || ! hash_equals( $order->get_order_key(), $order_key ) ) { return false; } /** * Filter the order statuses that are eligible to access the Review Order page. * * The scheduler unschedules pending sends on refund/cancel/trash/delete, but * emails already in the customer's inbox can still be clicked. The route-level * check blocks those late clicks for orders that have moved out of the * eligible set. * * @since 10.8.0 * * @param string[] $eligible_statuses Status slugs without the `wc-` prefix. * @param WC_Order $order The order being reviewed. */ $eligible_statuses = (array) apply_filters( 'woocommerce_review_order_eligible_statuses', array( OrderStatus::COMPLETED ), $order ); if ( ! in_array( $order->get_status(), $eligible_statuses, true ) ) { return false; } // Logged-in customer must own the order. Guests with the order key still pass. if ( $order->get_customer_id() && is_user_logged_in() && get_current_user_id() !== $order->get_customer_id() ) { return false; } return true; } /** * Enqueue the JS and CSS that progressively enhance the page. * * Both files live under `client/legacy/` and are built into * `assets/{js|css}/` by the classic-assets pipeline. */ private function enqueue_assets(): void { $plugin_url = untrailingslashit( plugins_url( '', WC_PLUGIN_FILE ) ); $suffix = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min'; $version = Constants::get_constant( 'WC_VERSION' ); $asset_url = static function ( string $path ) use ( $plugin_url ): string { // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment -- documented in includes/class-wc-frontend-scripts.php. return (string) apply_filters( 'woocommerce_get_asset_url', $plugin_url . $path, $path ); }; wp_enqueue_style( 'wc-order-review', $asset_url( '/assets/css/order-review.css' ), array(), $version ); // Tell WP to swap to the *-rtl.css variant on RTL sites. wp_style_add_data( 'wc-order-review', 'rtl', 'replace' ); wp_enqueue_script( 'wc-order-review', $asset_url( '/assets/js/frontend/order-review' . $suffix . '.js' ), array(), $version, array( 'strategy' => 'defer', 'in_footer' => true, ) ); wp_localize_script( 'wc-order-review', 'wcOrderReview', array( 'i18n' => array( 'ok' => __( 'Thanks, your review is live.', 'woocommerce' ), 'pending_moderation' => __( 'Thanks, your review is pending approval.', 'woocommerce' ), 'error' => __( 'Something went wrong, please try again.', 'woocommerce' ), 'rating_required' => __( 'Please rate this product before submitting your review.', 'woocommerce' ), ), ) ); } /** * Mark the current request as a 404 and load the theme's 404 template. * * Fails closed on every gating check so a stale or tampered link cannot * disclose order existence. */ private function render_404(): void { global $wp_query; $wp_query->set_404(); status_header( 404 ); nocache_headers(); $template = get_query_template( '404' ); if ( ! empty( $template ) && file_exists( $template ) ) { include $template; return; } // Fallback when the active theme has no 404 template: emit a minimal // page so the response body isn't empty. printf( '<!doctype html><html><head><meta charset="utf-8"><title>%1$s

%1$s

', esc_html__( 'Page not found', 'woocommerce' ) ); } }