Add the Receipts Rendering Engine (#43502)

This commit is contained in:
Néstor Soriano 2024-02-19 12:03:46 +01:00 committed by GitHub
parent 0b55f4ee12
commit 89c6fbbb7c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 1410 additions and 10 deletions

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add the receipts rendering engine

View File

@ -8,6 +8,7 @@
use Automattic\Jetpack\Constants;
use Automattic\WooCommerce\Admin\Notes\Notes;
use Automattic\WooCommerce\Internal\TransientFiles\TransientFilesEngine;
use Automattic\WooCommerce\Internal\DataStores\Orders\{ CustomOrdersTableController, DataSynchronizer, OrdersTableDataStore };
use Automattic\WooCommerce\Internal\Features\FeaturesController;
use Automattic\WooCommerce\Internal\ProductAttributesLookup\DataRegenerator;
@ -243,6 +244,9 @@ class WC_Install {
'8.6.0' => array(
'wc_update_860_remove_recommended_marketing_plugins_transient',
),
'8.7.0' => array(
'wc_update_870_prevent_listing_of_transient_files_directory',
),
);
/**
@ -456,6 +460,11 @@ class WC_Install {
// plugin version update. We base plugin age off of this value.
add_option( 'woocommerce_admin_install_timestamp', time() );
// Force a flush of rewrite rules even if the corresponding hook isn't initialized yet.
if ( ! has_action( 'woocommerce_flush_rewrite_rules' ) ) {
flush_rewrite_rules();
}
/**
* Flush the rewrite rules after install or update.
*
@ -557,6 +566,7 @@ class WC_Install {
WC()->query->add_endpoints();
WC_API::add_endpoint();
WC_Auth::add_endpoint();
TransientFilesEngine::add_endpoint();
}
/**

View File

@ -37,9 +37,13 @@ class Server {
* Register REST API routes.
*/
public function register_rest_routes() {
$container = wc_get_container();
foreach ( $this->get_rest_namespaces() as $namespace => $controllers ) {
foreach ( $controllers as $controller_name => $controller_class ) {
$this->controllers[ $namespace ][ $controller_name ] = new $controller_class();
$this->controllers[ $namespace ][ $controller_name ] =
0 === strpos( $controller_class, 'WC_REST_' ) ?
new $controller_class() :
$container->get( $controller_class );
$this->controllers[ $namespace ][ $controller_name ]->register_routes();
}
}

View File

@ -2649,3 +2649,21 @@ LIMIT 250
function wc_update_860_remove_recommended_marketing_plugins_transient() {
delete_transient( 'wc_marketing_recommended_plugins' );
}
/**
* Create an .htaccess file and an empty index.html file to prevent listing of the default transient files directory,
* if the directory exists.
*/
function wc_update_870_prevent_listing_of_transient_files_directory() {
global $wp_filesystem;
$default_transient_files_dir = untrailingslashit( wp_upload_dir()['basedir'] ) . '/woocommerce_transient_files';
if ( ! is_dir( $default_transient_files_dir ) ) {
return;
}
require_once ABSPATH . 'wp-admin/includes/file.php';
\WP_Filesystem();
$wp_filesystem->put_contents( $default_transient_files_dir . '/.htaccess', 'deny from all' );
$wp_filesystem->put_contents( $default_transient_files_dir . '/index.html', '' );
}

View File

@ -5,7 +5,8 @@
namespace Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders;
use Automattic\WooCommerce\Internal\DependencyManagement\AbstractServiceProvider;
use Automattic\WooCommerce\Internal\ReceiptRendering\ReceiptRenderingEngine;
use Automattic\WooCommerce\Internal\ReceiptRendering\ReceiptRenderingRestController;
use Automattic\WooCommerce\Proxies\LegacyProxy;
use Automattic\WooCommerce\Internal\TransientFiles\TransientFilesEngine;
@ -21,6 +22,8 @@ class EnginesServiceProvider extends AbstractInterfaceServiceProvider {
*/
protected $provides = array(
TransientFilesEngine::class,
ReceiptRenderingEngine::class,
ReceiptRenderingRestController::class,
);
/**
@ -28,5 +31,7 @@ class EnginesServiceProvider extends AbstractInterfaceServiceProvider {
*/
public function register() {
$this->share_with_implements_tags( TransientFilesEngine::class )->addArgument( LegacyProxy::class );
$this->share( ReceiptRenderingEngine::class )->addArguments( array( TransientFilesEngine::class, LegacyProxy::class ) );
$this->share_with_implements_tags( ReceiptRenderingRestController::class );
}
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg viewBox="0 0 750 471" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns" preserveAspectRatio="xMidYMid meet">
<!-- Generator: Sketch 3.3.2 (12043) - http://www.bohemiancoding.com/sketch -->
<title>diners</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
<g id="diners" sketch:type="MSLayerGroup">
<rect id="rectangle" fill="#0079BE" sketch:type="MSShapeGroup" x="0" y="0" width="750" height="471" rx="40"></rect>
<path d="M584.933911,237.947339 C584.933911,138.53154 501.952976,69.8140806 411.038924,69.8471464 L332.79674,69.8471464 C240.793699,69.8140806 165.066089,138.552041 165.066089,237.947339 C165.066089,328.877778 240.793699,403.587432 332.79674,403.150963 L411.038924,403.150963 C501.952976,403.586771 584.933911,328.857939 584.933911,237.947339 L584.933911,237.947339 Z" id="Shape-path" fill="#FFFFFF" sketch:type="MSShapeGroup"></path>
<path d="M333.280302,83.9308394 C249.210378,83.9572921 181.085889,152.238282 181.066089,236.510581 C181.085889,320.768331 249.209719,389.042708 333.280302,389.069161 C417.370025,389.042708 485.508375,320.768331 485.520254,236.510581 C485.507715,152.238282 417.370025,83.9572921 333.280302,83.9308394 L333.280302,83.9308394 Z" id="Shape-path" fill="#0079BE" sketch:type="MSShapeGroup"></path>
<path d="M237.066089,236.09774 C237.145288,194.917524 262.812421,159.801587 299.006443,145.847134 L299.006443,326.327183 C262.812421,312.380667 237.144628,277.283907 237.066089,236.09774 Z M368.066089,326.372814 L368.066089,145.847134 C404.273312,159.767859 429.980043,194.903637 430.046043,236.103692 C429.980043,277.316312 404.273312,312.425636 368.066089,326.372814 Z" id="Path" fill="#FFFFFF" sketch:type="MSShapeGroup"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg viewBox="0 0 780 501" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns" preserveAspectRatio="xMidYMid meet">
<!-- Generator: Sketch 3.3.2 (12043) - http://www.bohemiancoding.com/sketch -->
<title>discover</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
<g id="discover" sketch:type="MSLayerGroup">
<path d="M54.992188,0 C24.626565,0 -4.7369516e-15,24.629374 0,55.003906 L0,445.99609 C0,476.37636 24.618673,501 54.992188,501 L725.00781,501 C755.37344,501 780,476.37062 780,445.99609 L780,268.55664 L780,55.003906 C780,24.623637 755.38133,-4.7369516e-15 725.00781,0 L54.992188,0 L54.992188,0 Z" id="rectangle" fill="#4D4D4D" sketch:type="MSShapeGroup"></path>
<path d="M415.13086,161.21289 C446.07103,161.21289 471.15234,184.79287 471.15234,213.92188 L471.15234,213.95508 C471.15234,243.08408 446.07103,266.69727 415.13086,266.69727 C384.19069,266.69727 359.10938,243.08408 359.10938,213.95508 L359.10938,213.92188 C359.10938,184.79287 384.19069,161.21289 415.13086,161.21289 L415.13086,161.21289 Z M327.15234,161.89258 C335.9889,161.89258 343.40028,163.67723 352.41992,167.98242 L352.41992,190.73438 C343.87628,182.87089 336.46483,179.58008 326.66406,179.58008 C307.4002,179.58008 292.25,194.59455 292.25,213.63086 C292.25,233.70517 306.93133,247.82617 327.61914,247.82617 C336.93171,247.82617 344.20582,244.70584 352.41992,236.96875 L352.41992,259.73242 C343.07888,263.87291 335.50876,265.50781 326.66406,265.50781 C295.38621,265.50781 271.08203,242.91198 271.08203,213.77148 C271.08203,184.94507 296.03316,161.89258 327.15234,161.89258 L327.15234,161.89258 Z M230.03906,162.51953 C241.58477,162.51953 252.14952,166.24004 260.98242,173.51367 L250.23438,186.76172 C244.88362,181.11594 239.82337,178.73438 233.66992,178.73438 C224.81668,178.73437 218.36914,183.47936 218.36914,189.72266 C218.36914,195.07734 221.98883,197.91138 234.31445,202.20508 C257.67927,210.24859 264.60352,217.3809 264.60352,233.13086 C264.60352,252.32421 249.62806,265.68359 228.2832,265.68359 C212.65323,265.68359 201.29008,259.88895 191.82617,246.8125 L205.09375,234.78125 C209.82489,243.39164 217.71615,248.00391 227.51367,248.00391 C236.67693,248.00391 243.46094,242.05155 243.46094,234.01953 C243.46094,229.85606 241.40612,226.28585 237.30273,223.76172 C235.2368,222.56668 231.1447,220.78491 223.10352,218.11523 C203.81198,211.57701 197.19336,204.58834 197.19336,190.92969 C197.19336,174.70478 211.40702,162.51953 230.03906,162.51953 L230.03906,162.51953 Z M464.76172,164.24805 L487.19922,164.24805 L515.2832,230.83984 L543.72852,164.24805 L565.99609,164.24805 L520.50195,265.93359 L509.44922,265.93359 L464.76172,164.24805 L464.76172,164.24805 Z M67.414062,164.40039 L97.564453,164.40039 C130.87609,164.40039 154.09766,184.78179 154.09766,214.04102 C154.09766,228.63041 146.99364,242.73654 134.98047,252.09766 C124.87172,259.99945 113.35396,263.54297 97.40625,263.54297 L67.414062,263.54297 L67.414062,164.40039 L67.414062,164.40039 Z M163.54883,164.40039 L184.08984,164.40039 L184.08984,263.54297 L163.54883,263.54297 L163.54883,164.40039 L163.54883,164.40039 Z M575.2832,164.40039 L633.53516,164.40039 L633.53516,181.19922 L595.80859,181.19922 L595.80859,203.20508 L632.14453,203.20508 L632.14453,219.99609 L595.80859,219.99609 L595.80859,246.75781 L633.53516,246.75781 L633.53516,263.54297 L575.2832,263.54297 L575.2832,164.40039 L575.2832,164.40039 Z M647.14062,164.40039 L677.5957,164.40039 C701.28599,164.40039 714.86133,175.11052 714.86133,193.67188 C714.86133,208.85113 706.34712,218.81273 690.875,221.77734 L724.02344,263.54297 L698.76367,263.54297 L670.33398,223.71484 L667.65625,223.71484 L667.65625,263.54297 L647.14062,263.54297 L647.14062,164.40039 L647.14062,164.40039 Z M667.65625,180.01562 L667.65625,210.04102 L673.6582,210.04102 C686.77472,210.04102 693.72656,204.67918 693.72656,194.71289 C693.72656,185.06451 686.77347,180.01562 673.98242,180.01562 L667.65625,180.01562 L667.65625,180.01562 Z M87.939453,181.19922 L87.939453,246.75781 L93.451172,246.75781 C106.72432,246.75781 115.10685,244.36382 121.56055,238.87891 C128.66438,232.92288 132.9375,223.41276 132.9375,213.89844 C132.9375,204.39943 128.66438,195.17283 121.56055,189.2168 C114.77608,183.43696 106.72432,181.19922 93.451172,181.19922 L87.939453,181.19922 L87.939453,181.19922 Z" fill="#FFFFFF" sketch:type="MSShapeGroup"></path>
<path d="M779.981917,288.361069 C753.932037,306.691919 558.904907,437.700579 221.228007,500.98412 L724.989727,500.98412 C755.355357,500.98412 779.981917,476.35474 779.981917,445.980209 L779.981917,288.361069 L779.981917,288.361069 Z" id="Shape-9" fill="#F47216" sketch:type="MSShapeGroup"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.8 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg viewBox="0 0 750 471" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns" preserveAspectRatio="xMidYMid meet">
<!-- Generator: Sketch 3.3.1 (12005) - http://www.bohemiancoding.com/sketch -->
<title>Slice 1</title>
<desc>Created with Sketch.</desc>
<defs>
<linearGradient x1="0.031607858%" y1="49.9998574%" x2="99.9743153%" y2="49.9998574%" id="linearGradient-1">
<stop stop-color="#007B40" offset="0%"></stop>
<stop stop-color="#55B330" offset="100%"></stop>
</linearGradient>
<linearGradient x1="0.471693172%" y1="49.999826%" x2="99.9860086%" y2="49.999826%" id="linearGradient-2">
<stop stop-color="#1D2970" offset="0%"></stop>
<stop stop-color="#006DBA" offset="100%"></stop>
</linearGradient>
<linearGradient x1="0.113880772%" y1="50.0008964%" x2="99.9860003%" y2="50.0008964%" id="linearGradient-3">
<stop stop-color="#6E2B2F" offset="0%"></stop>
<stop stop-color="#E30138" offset="100%"></stop>
</linearGradient>
</defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
<g id="jcb" sketch:type="MSLayerGroup">
<rect id="Rectangle-1" fill="#0E4C96" sketch:type="MSShapeGroup" x="0" y="0" width="750" height="471" rx="40"></rect>
<path d="M617.243183,346.766281 C617.243183,388.380887 583.514892,422.125974 541.88349,422.125974 L132.756823,422.125974 L132.756823,124.244916 C132.756823,82.6186826 166.489851,48.8744567 208.121683,48.8744567 L617.242752,48.874026 L617.242752,346.766281 L617.243183,346.766281 L617.243183,346.766281 Z" id="path3494" fill="#FFFFFF" sketch:type="MSShapeGroup"></path>
<path d="M483.858874,242.044797 C495.542699,242.298285 507.296188,241.528806 518.936004,242.444883 C530.723244,244.645678 533.563915,262.487874 523.09234,268.332511 C515.950746,272.182115 507.459496,269.764696 499.713328,270.446208 L483.858874,270.446208 L483.858874,242.044797 L483.858874,242.044797 Z M525.691826,209.900487 C528.288491,219.064679 519.453903,227.292118 510.625917,226.030566 L483.858874,226.030566 C484.043758,217.388441 483.491345,208.008973 484.131053,199.821663 C494.854942,200.123386 505.679576,199.205849 516.340394,200.301853 C520.921799,201.451558 524.753935,205.217712 525.691826,209.900487 L525.691826,209.900487 Z M590.120412,73.9972254 C590.617872,91.498454 590.191471,109.92365 590.33359,127.780192 C590.299137,200.376358 590.405942,272.974174 590.278896,345.569303 C589.81042,372.776592 565.696524,396.413678 538.678749,396.956694 C511.63292,397.068451 484.584297,396.972628 457.537396,397.004497 L457.537396,287.253291 C487.007,287.099803 516.49604,287.561 545.953521,287.021594 C559.62072,286.162769 574.586027,277.145695 575.22328,262.107374 C576.833661,247.005483 562.592128,236.557185 549.071096,234.905684 C543.872773,234.770542 544.027132,233.390846 549.071096,232.788972 C561.96307,230.002483 572.090675,216.655787 568.296786,203.290229 C565.06052,189.232374 549.523839,183.79142 536.600366,183.817768 C510.248548,183.638612 483.891299,183.792359 457.537396,183.74111 C457.708585,163.252408 457.182916,142.740653 457.82271,122.267364 C459.910361,95.5513766 484.628603,73.5195319 511.269759,73.997656 C537.553166,73.9973692 563.837737,73.9982301 590.120412,73.9972254 L590.120412,73.9972254 Z" id="path3496" fill="url(#linearGradient-1)" sketch:type="MSShapeGroup"></path>
<path d="M159.740429,125.040498 C160.413689,97.8766592 184.628619,74.4290299 211.614797,74.0325398 C238.559493,73.9499686 265.506204,74.0209119 292.451671,73.9972254 C292.37764,164.882488 292.599905,255.773672 292.340301,346.655222 C291.302298,373.488802 267.350548,396.488661 240.661356,396.962292 C213.665015,397.060957 186.666275,396.976074 159.669012,397.004497 L159.669012,283.550875 C185.891623,289.745491 213.391138,292.382518 240.142406,288.272242 C256.134509,285.697368 273.629935,277.848026 279.044261,261.257567 C283.030122,247.066267 280.785723,232.131602 281.378027,217.566465 L281.378027,183.741541 L235.081246,183.741541 C234.873106,206.112145 235.507258,228.522447 234.746146,250.867107 C233.49785,264.601214 219.900147,273.326996 206.946428,272.861801 C190.879747,273.030535 159.04755,261.221796 159.04755,261.221796 C158.967492,219.3048 159.514314,166.814385 159.740429,125.040498 L159.740429,125.040498 Z" id="path3498" fill="url(#linearGradient-2)" sketch:type="MSShapeGroup"></path>
<path d="M309.719995,197.390136 C307.285788,197.90738 309.229141,189.089459 308.606298,185.743964 C308.772233,164.593637 308.260045,143.420951 308.889718,122.285827 C310.972541,95.4570827 335.881262,73.3701105 362.628748,73.997656 L441.39456,73.997656 C441.320658,164.882346 441.542493,255.77294 441.283406,346.653934 C440.244412,373.488027 416.291344,396.487102 389.602087,396.962292 C362.604605,397.061991 335.604707,396.976504 308.606298,397.004928 L308.606298,272.707624 C327.04641,287.835846 352.105738,290.192248 375.077953,290.233484 C392.39501,290.227455 409.611861,287.557865 426.428143,283.562934 L426.428143,260.790297 C407.474658,270.236609 385.194808,276.235815 364.184745,270.807966 C349.529051,267.157367 338.89089,252.996683 339.128513,237.872204 C337.43001,222.143684 346.652631,205.536885 362.110237,200.860855 C381.300923,194.852545 402.217787,199.448454 420.206344,207.258795 C424.060526,209.27695 427.97066,211.780342 426.428143,205.338044 L426.428143,187.438358 C396.343581,180.280951 364.326644,177.646405 334.099438,185.433619 C325.351193,187.901774 316.82819,191.644647 309.719995,197.390136 L309.719995,197.390136 Z" id="path3500" fill="url(#linearGradient-3)" sketch:type="MSShapeGroup"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.7 KiB

View File

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg viewBox="0 0 750 471" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid meet">
<!-- Generator: Sketch 39.1 (31720) - http://www.bohemiancoding.com/sketch -->
<title>Slice 1</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="mastercard">
<rect id="Rectangle-1" fill="#F4F4F4" x="0" y="0" width="750" height="471" rx="40"></rect>
<g id="mark" transform="translate(125.719997, 41.850862)">
<g id="text" transform="translate(25.142679, 328.360022)" fill="#000000">
<path d="M467.715561,51.9326899 C466.502604,51.9623585 465.503405,52.3648948 464.717962,53.1403001 C463.932516,53.9157321 463.526027,54.8861098 463.498494,56.0514362 C463.526027,57.2079497 463.932516,58.1758036 464.717963,58.9550005 C465.503406,59.7342125 466.502604,60.1392726 467.715561,60.1701825 C468.900764,60.1392726 469.887352,59.7342123 470.675326,58.9550002 C471.463285,58.175803 471.872297,57.2079493 471.902362,56.0514362 C471.872927,54.8861098 471.465177,53.915732 470.679109,53.1402998 C469.893026,52.3648943 468.905178,51.9623581 467.715561,51.9326899 L467.715561,51.9326899 L467.715561,51.9326899 Z M467.715561,59.2616355 C466.791392,59.2389292 466.029277,58.9259854 465.429214,58.3228033 C464.829145,57.7196374 464.518499,56.9625159 464.497273,56.0514362 C464.518499,55.1363804 464.829146,54.379679 465.429214,53.78133 C466.029277,53.1830062 466.791392,52.8730071 467.715561,52.8513318 C468.620383,52.8730076 469.370728,53.1830066 469.966597,53.7813302 C470.562452,54.379679 470.871417,55.1363804 470.893494,56.0514362 C470.871417,56.9625161 470.562452,57.7196378 469.966597,58.3228033 C469.370728,58.925985 468.620384,59.2389287 467.715561,59.2616355 L467.715561,59.2616355 L467.715561,59.2616355 Z M467.957689,54.1232975 L466.19217,54.1232975 L466.19217,57.9492899 L467.009353,57.9492899 L467.009353,56.5158046 L467.382634,56.5158046 L468.542832,57.9492899 L469.521434,57.9492899 L468.270438,56.5057097 C468.661158,56.4567169 468.961716,56.330109 469.172113,56.1258861 C469.382498,55.9216836 469.488849,55.6613181 469.491168,55.3447885 C469.488429,54.9670796 469.355174,54.6701195 469.091404,54.4539073 C468.827621,54.237719 468.449717,54.1275158 467.957689,54.1232975 L467.957689,54.1232975 L467.957689,54.1232975 Z M467.9476,54.8400402 C468.166813,54.840262 468.338741,54.8827453 468.463383,54.9674885 C468.588015,55.0522552 468.651489,55.1780218 468.653808,55.3447882 C468.651483,55.5164129 468.588015,55.6451235 468.463383,55.73092 C468.338741,55.8167385 468.166813,55.8596412 467.9476,55.859631 L467.009353,55.859631 L467.009353,54.8400393 L467.9476,54.8400402 L467.9476,54.8400402 Z" id="path3078"></path>
<path d="M9.34331724,57.5428029 L0.588175916,57.5428029 L0.588175916,16.6600045 L9.17164757,16.6600045 L9.17164757,21.6415186 C9.17164757,21.6415186 16.7107355,15.5532485 21.1885083,15.6293508 C29.8949298,15.7773317 35.093729,23.1875098 35.093729,23.1875098 C35.093729,23.1875098 39.3109893,15.6293508 48.8272918,15.6293508 C62.8997988,15.6293508 64.9642149,28.5125858 64.9642149,28.5125858 L64.9642149,57.3710273 L56.5524108,57.3710273 L56.5524108,31.9481087 C56.5524108,31.9481087 56.5825922,24.2181741 47.4539323,24.2181741 C38.0139747,24.2181741 37.1537629,31.9481087 37.1537629,31.9481087 L37.1537629,57.3710273 L28.3986215,57.3710273 L28.3986215,31.7763331 C28.3986215,31.7763331 27.5575496,23.7028366 19.6434834,23.7028366 C9.3650113,23.7028366 9.17164757,31.9481087 9.17164757,31.9481087 L9.34331724,57.5428029 L9.34331724,57.5428029 Z" id="path3006"></path>
<path d="M275.596898,15.623773 C271.119122,15.5476814 263.580741,21.6355967 263.580741,21.6355967 L263.580741,16.6649268 L254.988182,16.6649268 L254.988182,57.5386115 L263.748565,57.5386115 L263.580741,31.9463783 C263.580741,31.9463783 263.77445,23.7179044 274.052923,23.7179044 C275.961824,23.7179044 277.444363,24.180569 278.61772,24.8934007 L278.61772,24.8598688 L281.470718,16.9000798 C279.749092,16.1750176 277.791223,15.6610664 275.596898,15.623773 L275.596898,15.623773 L275.596898,15.623773 Z" id="path3008"></path>
<path d="M398.92774,15.623773 C394.449964,15.5476814 386.911582,21.6355967 386.911582,21.6355967 L386.911582,16.6649268 L378.319023,16.6649268 L378.319023,57.5386115 L387.079406,57.5386115 L386.911582,31.9463783 C386.911582,31.9463783 387.105291,23.7179044 397.383764,23.7179044 C399.292666,23.7179044 400.775204,24.180569 401.948561,24.8934007 L401.948561,24.8598688 L404.801559,16.9000798 C403.079933,16.1750713 401.122064,15.6611201 398.92774,15.6238267 L398.92774,15.623773 L398.92774,15.623773 Z" id="path3013"></path>
<path d="M93.2735295,15.4558449 C80.1708646,15.4558449 73.2368626,27.2396859 73.2018479,37.0849763 C73.1658666,47.1762746 81.0955959,58.8148646 93.6427411,58.8148646 C100.962678,58.8148646 106.976041,53.4075817 106.976041,53.4075817 L106.960145,57.5721971 L115.577998,57.5721971 L115.577998,16.6488272 L106.929792,16.6488272 L106.929792,21.8035248 C106.929792,21.8035248 101.282654,15.4558449 93.2735725,15.4558449 L93.2735295,15.4558449 L93.2735295,15.4558449 Z M94.9517638,23.7850756 C101.991433,23.7850756 107.706344,29.9122942 107.706344,37.454418 C107.706344,44.9965418 101.991433,51.0901748 94.9517638,51.0901748 C87.9120947,51.0901748 82.2307482,44.9965418 82.2307482,37.454418 C82.2307482,29.9122942 87.9120947,23.7850756 94.9517638,23.7850756 L94.9517638,23.7850756 L94.9517638,23.7850756 Z" id="path3015"></path>
<path d="M344.597578,15.4558449 C331.494913,15.4558449 324.560911,27.2396859 324.525896,37.0849763 C324.489915,47.1762746 332.419644,58.8148646 344.966789,58.8148646 C352.286726,58.8148646 358.300089,53.4075817 358.300089,53.4075817 L358.284193,57.5721971 L366.902046,57.5721971 L366.902046,16.6488272 L358.25384,16.6488272 L358.25384,21.8035248 C358.25384,21.8035248 352.606702,15.4558449 344.59762,15.4558449 L344.597578,15.4558449 L344.597578,15.4558449 Z M346.275812,23.7850756 C353.315481,23.7850756 359.030392,29.9122942 359.030392,37.454418 C359.030392,44.9965418 353.315481,51.0901748 346.275812,51.0901748 C339.236143,51.0901748 333.554796,44.9965418 333.554796,37.454418 C333.554796,29.9122942 339.236143,23.7850756 346.275812,23.7850756 L346.275812,23.7850756 L346.275812,23.7850756 Z" id="path3020"></path>
<path d="M427.342249,15.4558449 C414.239584,15.4558449 407.305582,27.2396859 407.270567,37.0849763 C407.234586,47.1762746 415.164315,58.8148646 427.71146,58.8148646 C435.031397,58.8148646 441.04476,53.4075817 441.04476,53.4075817 L441.028864,57.5721971 L449.646718,57.5721971 L449.646718,0.49407462 L440.998511,0.49407462 L440.998511,21.8035248 C440.998511,21.8035248 435.351373,15.4558449 427.342292,15.4558449 L427.342249,15.4558449 L427.342249,15.4558449 Z M429.020483,23.7850756 C436.060152,23.7850756 441.775063,29.9122942 441.775063,37.454418 C441.775063,44.9965418 436.060152,51.0901748 429.020483,51.0901748 C421.980814,51.0901748 416.299467,44.9965418 416.299467,37.454418 C416.299467,29.9122942 421.980814,23.7850756 429.020483,23.7850756 L429.020483,23.7850756 L429.020483,23.7850756 Z" id="path3022"></path>
<path d="M141.872122,58.9170078 C132.94558,58.9170078 124.705176,53.4201669 124.705176,53.4201669 L128.481907,47.5797641 C128.481907,47.5797641 136.278978,51.1870733 141.872122,51.1870733 C145.50613,51.1870733 151.583937,50.0128667 151.657274,46.3773348 C151.734822,42.5349478 141.442945,41.39581 141.442945,41.39581 C141.442945,41.39581 126.078536,41.1860853 126.078536,28.5125858 C126.078536,20.5421246 133.751938,15.4575752 143.588818,15.4575752 C149.272667,15.4575752 159.89741,20.4390893 159.89741,20.4390893 L155.605674,27.1383702 C155.605674,27.1383702 147.402218,23.858921 143.073802,23.7028366 C139.418806,23.5710413 135.005346,25.3221465 135.005346,28.5125858 C135.005346,37.1806926 160.584084,27.837198 160.584084,45.3466704 C160.584084,56.8338188 150.166691,58.9170078 141.872122,58.9170078 L141.872122,58.9170078 L141.872122,58.9170078 Z" id="path3024"></path>
<path d="M174.802149,4.80920724 L174.802149,16.6985124 L167.182966,16.6985124 L167.182966,25.296428 L174.802149,25.296428 L174.802149,45.85082 C174.802149,45.85082 174.127827,59.7552616 189.067141,59.7552616 C193.19753,59.7552616 201.284686,56.6989713 201.284686,56.6989713 L197.827523,47.7651996 C197.827523,47.7651996 194.611468,50.5102454 190.980328,50.4184626 C184.076195,50.2440217 184.267391,45.8172343 184.267391,45.8172343 L184.267391,25.296428 L198.498817,25.296428 L198.498817,16.6985124 L184.267391,16.6985124 L184.267391,4.80920724 L174.802149,4.80920724 L174.802149,4.80920724 L174.802149,4.80920724 Z" id="path3026"></path>
<path d="M226.659588,15.959629 C212.610087,15.959629 205.590417,27.5389793 205.648095,37.5887604 C205.707384,47.9238419 212.040304,59.5537479 227.498705,59.5537479 C234.115072,59.5537479 243.408366,53.7434378 243.408366,53.7434378 L239.414168,46.791217 C239.414168,46.791217 233.072548,51.2916884 227.498705,51.2916884 C216.339172,51.2916884 215.616806,40.3763659 215.616806,40.3763659 L245.489376,40.3763659 C245.489376,40.3763659 247.717985,15.959629 226.659588,15.959629 L226.659588,15.959629 L226.659588,15.959629 Z M225.38413,23.9865893 C225.715416,23.9677813 226.070568,23.9865893 226.424635,23.9865893 C236.937954,23.9865893 236.863252,33.9279292 236.863252,33.9279292 L215.616806,33.9279292 C215.616806,33.9279292 215.114206,24.5707962 225.38413,23.9865893 L225.38413,23.9865893 L225.38413,23.9865893 Z" id="path3034"></path>
<path d="M315.5162,46.686 L319.52203,54.7026427 C319.52203,54.7026427 313.17302,58.8324258 306.047898,58.8324258 C291.296557,58.8324258 283.105442,47.7234997 283.105442,37.2117848 C283.105442,20.6912339 296.140698,15.8340672 304.955397,15.8340672 C312.956369,15.8340672 319.886193,20.4497077 319.886193,20.4497077 L315.394819,28.4663505 C315.394819,28.4663505 312.671859,24.2151004 304.712614,24.2151004 C296.766655,24.2151004 292.573755,31.0702185 292.573755,37.5761752 C292.573755,44.8669927 297.454114,51.0587064 304.834005,51.0587064 C310.623393,51.0587064 315.5162,46.686 315.5162,46.686 L315.5162,46.686 L315.5162,46.686 Z" id="path3037"></path>
</g>
<path d="M498.787985,236.781279 L498.787985,231.260623 L497.347484,231.260623 L495.690436,235.057252 L494.033388,231.260623 L492.592886,231.260623 L492.592886,236.781279 L493.609711,236.781279 L493.609711,232.617235 L495.163193,236.206603 L496.217678,236.206603 L497.771161,232.607814 L497.771161,236.781279 L498.787985,236.781279 L498.787985,236.781279 Z M489.664807,236.781279 L489.664807,232.202715 L491.510156,232.202715 L491.510156,231.270044 L486.812049,231.270044 L486.812049,232.202715 L488.657397,232.202715 L488.657397,236.781279 L489.664807,236.781279 L489.664807,236.781279 Z" id="path3057" fill="#F79F1A"></path>
<path d="M499.076678,154.709802 C499.076678,240.135159 429.999707,309.386105 344.788929,309.386105 C259.578151,309.386105 190.501159,240.135159 190.501159,154.709802 C190.501159,69.2844326 259.578151,0.0334920174 344.788929,0.0334920174 C429.999707,0.0334920174 499.076678,69.2844326 499.076678,154.709802 L499.076678,154.709802 L499.076678,154.709802 Z" id="path2997" fill="#F79F1A"></path>
<path d="M308.73932,154.709802 C308.73932,240.135159 239.662349,309.386105 154.451571,309.386105 C69.2407931,309.386105 0.163801275,240.135159 0.163801275,154.709802 C0.163801275,69.2844326 69.2407931,0.0334920174 154.451571,0.0334920174 C239.662349,0.0334920174 308.73932,69.2844326 308.73932,154.709802 L308.73932,154.709802 L308.73932,154.709802 Z" id="path2995" fill="#EA001B"></path>
<path d="M249.620562,32.9474812 C213.621326,61.2636823 190.513152,105.265345 190.513152,154.695309 C190.513152,204.125274 213.621326,248.16052 249.620562,276.476723 C285.619799,248.16052 308.727973,204.125274 308.727973,154.695309 C308.727973,105.265345 285.619799,61.2636823 249.620562,32.9474812 L249.620562,32.9474812 L249.620562,32.9474812 Z" id="path2999" fill="#FF5F01"></path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 750 471" preserveAspectRatio="xMidYMid meet"><defs><style>.cls-1{fill:#75787c;}</style></defs><title>credit-card</title><g id="Page-1"><g id="amex"><g id="Rectangle-1"><path class="cls-1" d="M711,40V431H41V40H711m0-40H41A40,40,0,0,0,1,40V431a40,40,0,0,0,40,40H711a40,40,0,0,0,40-40V40A40,40,0,0,0,711,0Z" transform="translate(-1)"/></g></g></g><rect class="cls-1" x="11" y="113" width="728" height="100.73"/><rect class="cls-1" x="45" y="354.08" width="93" height="32.92"/><rect class="cls-1" x="172" y="354.08" width="155.94" height="32.92"/></svg>

After

Width:  |  Height:  |  Size: 635 B

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg viewBox="0 0 750 471" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns" preserveAspectRatio="xMidYMid meet">
<!-- Generator: Sketch 3.3.1 (12005) - http://www.bohemiancoding.com/sketch -->
<title>Slice 1</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
<g id="visa" sketch:type="MSLayerGroup">
<rect id="Rectangle-1" fill="#0E4595" sketch:type="MSShapeGroup" x="0" y="0" width="750" height="471" rx="40"></rect>
<path d="M278.1975,334.2275 L311.5585,138.4655 L364.9175,138.4655 L331.5335,334.2275 L278.1975,334.2275 L278.1975,334.2275 Z" id="Shape" fill="#FFFFFF" sketch:type="MSShapeGroup"></path>
<path d="M524.3075,142.6875 C513.7355,138.7215 497.1715,134.4655 476.4845,134.4655 C423.7605,134.4655 386.6205,161.0165 386.3045,199.0695 C386.0075,227.1985 412.8185,242.8905 433.0585,252.2545 C453.8275,261.8495 460.8105,267.9695 460.7115,276.5375 C460.5795,289.6595 444.1255,295.6545 428.7885,295.6545 C407.4315,295.6545 396.0855,292.6875 378.5625,285.3785 L371.6865,282.2665 L364.1975,326.0905 C376.6605,331.5545 399.7065,336.2895 423.6355,336.5345 C479.7245,336.5345 516.1365,310.2875 516.5505,269.6525 C516.7515,247.3835 502.5355,230.4355 471.7515,216.4645 C453.1005,207.4085 441.6785,201.3655 441.7995,192.1955 C441.7995,184.0585 451.4675,175.3575 472.3565,175.3575 C489.8055,175.0865 502.4445,178.8915 512.2925,182.8575 L517.0745,185.1165 L524.3075,142.6875" id="path13" fill="#FFFFFF" sketch:type="MSShapeGroup"></path>
<path d="M661.6145,138.4655 L620.3835,138.4655 C607.6105,138.4655 598.0525,141.9515 592.4425,154.6995 L513.1975,334.1025 L569.2285,334.1025 C569.2285,334.1025 578.3905,309.9805 580.4625,304.6845 C586.5855,304.6845 641.0165,304.7685 648.7985,304.7685 C650.3945,311.6215 655.2905,334.1025 655.2905,334.1025 L704.8025,334.1025 L661.6145,138.4655 L661.6145,138.4655 Z M596.1975,264.8725 C600.6105,253.5935 617.4565,210.1495 617.4565,210.1495 C617.1415,210.6705 621.8365,198.8155 624.5315,191.4655 L628.1385,208.3435 C628.1385,208.3435 638.3555,255.0725 640.4905,264.8715 L596.1975,264.8715 L596.1975,264.8725 L596.1975,264.8725 Z" id="Path" fill="#FFFFFF" sketch:type="MSShapeGroup"></path>
<path d="M232.9025,138.4655 L180.6625,271.9605 L175.0965,244.8315 C165.3715,213.5575 135.0715,179.6755 101.1975,162.7125 L148.9645,333.9155 L205.4195,333.8505 L289.4235,138.4655 L232.9025,138.4655" id="path16" fill="#FFFFFF" sketch:type="MSShapeGroup"></path>
<path d="M131.9195,138.4655 L45.8785,138.4655 L45.1975,142.5385 C112.1365,158.7425 156.4295,197.9015 174.8155,244.9525 L156.1065,154.9925 C152.8765,142.5965 143.5085,138.8975 131.9195,138.4655" id="path18" fill="#F2AE14" sketch:type="MSShapeGroup"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -0,0 +1,350 @@
<?php
namespace Automattic\WooCommerce\Internal\ReceiptRendering;
use Automattic\WooCommerce\Internal\TransientFiles\TransientFilesEngine;
use Automattic\WooCommerce\Proxies\LegacyProxy;
use Automattic\WooCommerce\Utilities\ArrayUtil;
use Automattic\WooCommerce\Utilities\StringUtil;
use \Exception;
use \WC_Order;
/**
* This class generates printable order receipts as transient files (see src/Internal/TransientFiles).
* The template for the receipt is Templates/order-receipt.php, it uses the variables returned as array keys
* 'get_order_data'.
*
* When a receipt is generated for an order with 'generate_receipt' the receipt file name is stored as order meta
* (see RECEIPT_FILE_NAME_META_KEY) for later retrieval with 'get_existing_receipt'. Beware! The files pointed
* by such meta keys could have expired and thus no longer exist. 'get_existing_receipt' will appropriately return null
* if the meta entry exists but the file doesn't.
*/
class ReceiptRenderingEngine {
private const FONT_SIZE = 12;
private const LINE_HEIGHT = self::FONT_SIZE * 1.5;
private const ICON_HEIGHT = self::LINE_HEIGHT;
private const ICON_WIDTH = self::ICON_HEIGHT * ( 4 / 3 );
private const MARGIN = 16;
private const TITLE_FONT_SIZE = 24;
private const FOOTER_FONT_SIZE = 10;
/**
* This array must contain all the names of the files in the CardIcons directory (without extension),
* except 'unknown'.
*/
private const KNOWN_CARD_TYPES = array( 'amex', 'diners', 'discover', 'interac', 'jcb', 'mastercard', 'visa' );
/**
* Order meta key that stores the file name of the last generated receipt.
*/
public const RECEIPT_FILE_NAME_META_KEY = '_receipt_file_name';
/**
* The instance of TransientFilesEngine to use.
*
* @var TransientFilesEngine
*/
private TransientFilesEngine $transient_files_engine;
/**
* The instance of LegacyProxy to use.
*
* @var LegacyProxy
*/
private LegacyProxy $legacy_proxy;
/**
* Initializes the class.
*
* @param TransientFilesEngine $transient_files_engine The instance of TransientFilesEngine to use.
* @param LegacyProxy $legacy_proxy The instance of LegacyProxy to use.
* @internal
*/
final public function init( TransientFilesEngine $transient_files_engine, LegacyProxy $legacy_proxy ) {
$this->transient_files_engine = $transient_files_engine;
$this->legacy_proxy = $legacy_proxy;
}
/**
* Get the (transient) file name of the receipt for an order, creating a new file if necessary.
*
* If $force_new is false, and a receipt file for the order already exists (as pointed by order meta key
* RECEIPT_FILE_NAME_META_KEY), then the name of the already existing receipt file is returned.
*
* If $force_new is true, OR if it's false but no receipt file for the order exists (no order meta with key
* RECEIPT_FILE_NAME_META_KEY exists, OR it exists but the file it points to doesn't), then a new receipt
* transient file is created with the supplied expiration date (defaulting to "tomorrow"), and the new file name
* is stored as order meta with the key RECEIPT_FILE_NAME_META_KEY.
*
* @param int|WC_Order $order The order object or order id to get the receipt for.
* @param string|int|null $expiration_date GMT expiration date formatted as yyyy-mm-dd, or as a timestamp, or null for "tomorrow".
* @param bool $force_new If true, creates a new receipt file even if one already exists for the order.
* @return string|null The file name of the new or already existing receipt file, null if an order id is passed and the order doesn't exist.
* @throws \InvalidArgumentException Invalid expiration date (wrongly formatted, or it's a date in the past).
* @throws Exception The directory to store the file doesn't exist and can't be created.
*/
public function generate_receipt( $order, $expiration_date = null, bool $force_new = false ) : ?string {
if ( ! $order instanceof WC_Order ) {
$order = wc_get_order( $order );
if ( false === $order ) {
return null;
}
}
if ( ! $force_new ) {
$existing_receipt_filename = $this->get_existing_receipt( $order );
if ( ! is_null( $existing_receipt_filename ) ) {
return $existing_receipt_filename;
}
}
$expiration_date ??=
$this->legacy_proxy->call_function(
'gmdate',
'Y-m-d',
$this->legacy_proxy->call_function(
'strtotime',
'+1 days'
)
);
// phpcs:ignore WordPress.PHP.DontExtract.extract_extract
extract( $this->get_order_data( $order ) );
ob_start();
include __dir__ . '/Templates/order-receipt.php';
$rendered_template = ob_get_contents();
ob_end_clean();
$file_name = $this->transient_files_engine->create_transient_file( $rendered_template, $expiration_date );
$order->update_meta_data( self::RECEIPT_FILE_NAME_META_KEY, $file_name );
$order->save_meta_data();
return $file_name;
}
/**
* Get the file name of an existing receipt file for an order.
*
* A receipt is considered to be available for the order if there's an order meta entry with key
* RECEIPT_FILE_NAME_META_KEY AND the transient file it points to exists AND it has not expired.
*
* @param WC_Order $order The order object or order id to get the receipt for.
* @return string|null The receipt file name, or null if no receipt is currently available for the order.
* @throws Exception Thrown if a wrong file path is passed.
*/
public function get_existing_receipt( $order ): ?string {
if ( ! $order instanceof WC_Order ) {
$order = wc_get_order( $order );
if ( false === $order ) {
return null;
}
}
$existing_receipt_filename = $order->get_meta( self::RECEIPT_FILE_NAME_META_KEY, true );
if ( '' === $existing_receipt_filename ) {
return null;
}
$file_path = $this->transient_files_engine->get_transient_file_path( $existing_receipt_filename );
if ( is_null( $file_path ) ) {
return null;
}
return $this->transient_files_engine->file_has_expired( $file_path ) ? null : $existing_receipt_filename;
}
/**
* Get the order data that the receipt template will use.
*
* @param WC_Order $order The order to get the data from.
* @return array The order data as an associative array.
*/
private function get_order_data( WC_Order $order ): array {
$store_name = get_bloginfo( 'name' );
if ( $store_name ) {
/* translators: %s = store name */
$receipt_title = sprintf( __( 'Receipt from %s', 'woocommerce' ), $store_name );
} else {
$receipt_title = __( 'Receipt', 'woocommerce' );
}
$order_id = $order->get_id();
if ( $order_id ) {
/* translators: %d = order id */
$summary_title = sprintf( __( 'Summary: Order #%d', 'woocommerce' ), $order->get_id() );
} else {
$summary_title = __( 'Summary', 'woocommerce' );
}
$get_price_args = array( 'currency' => $order->get_currency() );
$line_items_info = array();
$line_items = $order->get_items( 'line_item' );
foreach ( $line_items as $line_item ) {
$line_item_product = $line_item->get_product();
$line_item_title =
( $line_item_product instanceof \WC_Product_Variation ) ?
( wc_get_product( $line_item_product->get_parent_id() )->get_name() ) . '. ' . $line_item_product->get_attribute_summary() :
$line_item_product->get_name();
$line_items_info[] = array(
'title' => wp_kses( $line_item_title, array() ),
'quantity' => $line_item->get_quantity(),
'amount' => wc_price( $line_item->get_subtotal(), $get_price_args ),
);
}
$line_items_info[] = array(
'title' => __( 'Subtotal', 'woocommerce' ),
'amount' => wc_price( $order->get_subtotal(), $get_price_args ),
);
$coupon_names = ArrayUtil::select( $order->get_coupons(), 'get_name', ArrayUtil::SELECT_BY_OBJECT_METHOD );
if ( ! empty( $coupon_names ) ) {
$line_items_info[] = array(
/* translators: %s = comma-separated list of coupon codes */
'title' => sprintf( __( 'Discount (%s)', 'woocommerce' ), join( ', ', $coupon_names ) ),
'amount' => wc_price( -$order->get_total_discount(), $get_price_args ),
);
}
foreach ( $order->get_fees() as $fee ) {
$name = $fee->get_name();
$line_items_info[] = array(
'title' => '' === $name ? __( 'Fee', 'woocommerce' ) : $name,
'amount' => wc_price( $fee->get_total(), $get_price_args ),
);
}
$shipping_total = (float) $order->get_shipping_total();
if ( $shipping_total ) {
$line_items_info[] = array(
'title' => __( 'Shipping', 'woocommerce' ),
'amount' => wc_price( $order->get_shipping_total(), $get_price_args ),
);
}
$total_taxes = 0;
foreach ( $order->get_taxes() as $tax ) {
$total_taxes += (float) $tax->get_tax_total() + (float) $tax->get_shipping_tax_total();
}
if ( $total_taxes ) {
$line_items_info[] = array(
'title' => __( 'Taxes', 'woocommerce' ),
'amount' => wc_price( $total_taxes, $get_price_args ),
);
}
$line_items_info[] = array(
'title' => __( 'Amount Paid', 'woocommerce' ),
'amount' => wc_price( $order->get_total(), $get_price_args ),
);
return array(
'constants' => array(
'font_size' => self::FONT_SIZE,
'margin' => self::MARGIN,
'title_font_size' => self::TITLE_FONT_SIZE,
'footer_font_size' => self::FOOTER_FONT_SIZE,
'line_height' => self::LINE_HEIGHT,
'icon_height' => self::ICON_HEIGHT,
'icon_width' => self::ICON_WIDTH,
),
'texts' => array(
'receipt_title' => $receipt_title,
'amount_paid_section_title' => __( 'Amount Paid', 'woocommerce' ),
'date_paid_section_title' => __( 'Date Paid', 'woocommerce' ),
'payment_method_section_title' => __( 'Payment method', 'woocommerce' ),
'summary_section_title' => $summary_title,
'order_notes_section_title' => __( 'Notes', 'woocommerce' ),
'app_name' => __( 'Application Name', 'woocommerce' ),
'aid' => __( 'AID', 'woocommerce' ),
'account_type' => __( 'Account Type', 'woocommerce' ),
),
'formatted_amount' => wc_price( $order->get_total(), $get_price_args ),
'formatted_date' => wc_format_datetime( $order->get_date_paid() ),
'line_items' => $line_items_info,
'payment_method' => $order->get_payment_method_title(),
'notes' => array_map( 'get_comment_text', $order->get_customer_order_notes() ),
'payment_info' => $this->get_woo_pay_data( $order ),
);
}
/**
* Get the order data related to WooCommerce Payments.
*
* It will return null if any of these is true:
*
* - Payment method is not 'woocommerce_payments".
* - WooCommerce Payments is not installed.
* - No intent id is stored for the order.
* - Retrieving the payment information from Stripe API (providing the intent id) fails.
* - The received data set doesn't contain the expected information.
*
* @param WC_Order $order The order to get the data from.
* @return array|null An array of payment information for the order, or null if not available.
*/
private function get_woo_pay_data( WC_Order $order ): ?array {
// For testing purposes: if WooCommerce Payments development mode is enabled,
// an order meta item with key '_wcpay_payment_details' will be used if it exists as a replacement
// for the call to the Stripe API's 'get intent' endpoint.
// The value must be the JSON encoding of an array simulating the "payment_details" part of the response from the endpoint
// (at the very least it must contain the "card_present" key).
$payment_details = json_decode( defined( 'WCPAY_DEV_MODE' ) && WCPAY_DEV_MODE ? $order->get_meta( '_wcpay_payment_details' ) : false, true );
if ( ! $payment_details ) {
if ( 'woocommerce_payments' !== $order->get_payment_method() ) {
return null;
}
if ( ! class_exists( \WC_Payments::class ) ) {
return null;
}
$intent_id = $order->get_meta( '_intent_id' );
if ( ! $intent_id ) {
return null;
}
try {
$payment_details = \WC_Payments::get_payments_api_client()->get_intent( $intent_id )->get_charge()->get_payment_method_details();
} catch ( Exception $ex ) {
$order_id = $order->get_id();
$message = $ex->getMessage();
wc_get_logger()->error( StringUtil::class_name_without_namespace( static::class ) . " - retrieving info for charge {$intent_id} for order {$order_id}: {$message}" );
return null;
}
}
$card_data = $payment_details['card_present'] ?? null;
if ( is_null( $card_data ) ) {
return null;
}
$card_brand = $card_data['brand'] ?? '';
if ( ! in_array( $card_brand, self::KNOWN_CARD_TYPES, true ) ) {
$card_brand = 'unknown';
}
// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode, WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
$card_svg = base64_encode( file_get_contents( __DIR__ . "/CardIcons/{$card_brand}.svg" ) );
return array(
'card_icon' => $card_svg,
'card_last4' => wp_kses( $card_data['last4'] ?? '', array() ),
'app_name' => wp_kses( $card_data['receipt']['application_preferred_name'] ?? null, array() ),
'aid' => wp_kses( $card_data['receipt']['dedicated_file_name'] ?? null, array() ),
'account_type' => wp_kses( $card_data['receipt']['account_type'] ?? null, array() ),
);
}
}

View File

@ -0,0 +1,203 @@
<?php
namespace Automattic\WooCommerce\Internal\ReceiptRendering;
use Automattic\WooCommerce\Internal\TransientFiles\TransientFilesEngine;
use \WP_REST_Server;
use \WP_REST_Request;
use \WP_Error;
use \InvalidArgumentException;
use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;
/**
* Controller for the REST endpoints associated to the receipt rendering engine.
* The endpoints require the read_shop_order capability for the order at hand.
*/
class ReceiptRenderingRestController extends RestApiControllerBase {
use AccessiblePrivateMethods;
/**
* Get the WooCommerce REST API namespace for the class.
*
* @return string
*/
protected function get_rest_api_namespace(): string {
return 'order-receipts';
}
/**
* Register the REST API endpoints handled by this controller.
*/
public function register_routes() {
register_rest_route(
$this->route_namespace,
'/orders/(?P<id>[\d]+)/receipt',
array(
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => fn( $request ) => $this->run( $request, 'create_order_receipt' ),
'permission_callback' => fn( $request ) => $this->check_permission( $request, 'read_shop_order', $request->get_param( 'id' ) ),
'args' => $this->get_args_for_create_order_receipt(),
'schema' => $this->get_schema_for_get_and_post_order_receipt(),
),
)
);
register_rest_route(
$this->route_namespace,
'/orders/(?P<id>[\d]+)/receipt',
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => fn( $request ) => $this->run( $request, 'get_order_receipt' ),
'permission_callback' => fn( $request ) => $this->check_permission( $request, 'read_shop_order', $request->get_param( 'id' ) ),
'args' => $this->get_args_for_get_order_receipt(),
'schema' => $this->get_schema_for_get_and_post_order_receipt(),
),
)
);
}
/**
* Handle the GET /orders/id/receipt:
*
* Return the data for a receipt if it exists, or a 404 error if it doesn't.
*
* @param WP_REST_Request $request The received request.
* @return array|WP_Error
*/
public function get_order_receipt( WP_REST_Request $request ) {
$order_id = $request->get_param( 'id' );
$filename = wc_get_container()->get( ReceiptRenderingEngine::class )->get_existing_receipt( $order_id );
return is_null( $filename ) ?
new WP_Error( 'woocommerce_rest_not_found', __( 'Receipt not found', 'woocommerce' ), array( 'status' => 404 ) ) :
$this->get_response_for_file( $filename );
}
/**
* Handle the POST /orders/id/receipt:
*
* Return the data for a receipt if it exists, or create a new receipt and return its data otherwise.
*
* Optional query string arguments:
*
* expiration_date: formatted as yyyy-mm-dd.
* expiration_days: a number, 0 is today, 1 is tomorrow, etc.
* force_new: defaults to false, if true, create a new receipt even if one already exists for the order.
*
* If neither expiration_date nor expiration_days are supplied, the default is expiration_days = 1.
*
* @param WP_REST_Request $request The received request.
* @return array|WP_Error Request response or an error.
*/
public function create_order_receipt( WP_REST_Request $request ) {
$expiration_date =
$request->get_param( 'expiration_date' ) ??
gmdate( 'Y-m-d', strtotime( "+{$request->get_param('expiration_days')} days" ) );
$order_id = $request->get_param( 'id' );
$filename = wc_get_container()->get( ReceiptRenderingEngine::class )->generate_receipt( $order_id, $expiration_date, $request->get_param( 'force_new' ) );
return is_null( $filename ) ?
new WP_Error( 'woocommerce_rest_not_found', __( 'Order not found', 'woocommerce' ), array( 'status' => 404 ) ) :
$this->get_response_for_file( $filename );
}
/**
* Formats the response for both the GET and POST endpoints.
*
* @param string $filename The filename to return the information for.
* @return array The data for the actual response to be returned.
*/
private function get_response_for_file( string $filename ): array {
$expiration_date = TransientFilesEngine::get_expiration_date( $filename );
$public_url = wc_get_container()->get( TransientFilesEngine::class )->get_public_url( $filename );
return array(
'receipt_url' => $public_url,
'expiration_date' => $expiration_date,
);
}
/**
* Get the accepted arguments for the GET request.
*
* @return array[] The accepted arguments for the GET request.
*/
private function get_args_for_get_order_receipt(): array {
return array(
'id' => array(
'description' => __( 'Unique identifier of the order.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
);
}
/**
* Get the schema for both the GET and the POST requests.
*
* @return array[]
*/
private function get_schema_for_get_and_post_order_receipt(): array {
$schema = $this->get_base_schema();
$schema['properties'] = array(
'receipt_url' => array(
'description' => __( 'Public url of the receipt.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'expiration_date' => array(
'description' => __( 'Expiration date of the receipt, formatted as yyyy-mm-dd.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
);
return $schema;
}
/**
* Get the accepted arguments for the POST request.
*
* @return array[]
*/
private function get_args_for_create_order_receipt(): array {
return array(
'id' => array(
'description' => __( 'Unique identifier of the order.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'expiration_date' => array(
'description' => __( 'Expiration date formatted as yyyy-mm-dd.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'default' => null,
),
'expiration_days' => array(
'description' => __( 'Number of days to be added to the current date to get the expiration date.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'default' => 1,
),
'force_new' => array(
'description' => __( 'True to force the creation of a new receipt even if one already exists and has not expired yet.', 'woocommerce' ),
'type' => 'boolean',
'required' => false,
'context' => array( 'view', 'edit' ),
'readonly' => true,
'default' => false,
),
);
}
}

View File

@ -0,0 +1,106 @@
<?php /* phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped */ ?>
<html>
<head>
<meta http-equiv="Content-Type" content="<?php bloginfo( 'html_type' ); ?>; charset=<?php echo get_option( 'blog_charset' ); ?>" />
<style>
html { font-family: "Helvetica Neue", sans-serif; font-size: <?php echo $constants['font_size']; ?>pt; }
header { margin-top: <?php echo $constants['margin']; ?>; }
h1 { font-size: <?php echo $constants['title_font_size']; ?>pt; font-weight: 500; text-align: center; }
h3 { color: #707070; margin:0; }
table {
background-color:#F5F5F5;
width:100%;
color: #707070;
margin: <?php echo $constants['margin'] / 2; ?>pt 0;
padding: <?php echo $constants['margin'] / 2; ?>pt;
}
table td:last-child { width: 30%; text-align: right; }
table tr:last-child { color: #000000; font-weight: bold; }
footer {
font-size: <?php echo $constants['footer_font_size']; ?>pt;
border-top: 1px solid #707070;
margin-top: <?php echo $constants['margin']; ?>pt;
padding-top: <?php echo $constants['margin']; ?>pt;
}
p { line-height: <?php echo $constants['line_height']; ?>pt; margin: 0 0 <?php echo $constants['margin'] / 2; ?> 0; }
<?php if ( $payment_info ) { ?>
.card-icon {
width: <?php echo $constants['icon_width']; ?>pt;
height: <?php echo $constants['icon_height']; ?>pt;
vertical-align: top;
background-repeat: no-repeat;
background-position-y: center;
display: inline-block;
background-image: url("data:image/svg+xml;base64,<?php echo $payment_info['card_icon']; ?>");
}
<?php } ?>
</style>
</head>
<body>
<header>
<h1><?php echo $texts['receipt_title']; ?></h1>
<h3><?php echo strtoupper( $texts['amount_paid_section_title'] ); ?></h3>
<p>
<?php echo $formatted_amount; ?>
</p>
<h3><?php echo strtoupper( $texts['date_paid_section_title'] ); ?></h3>
<p>
<?php echo $formatted_date; ?>
</p>
<?php if ( $payment_method ) { ?>
<h3><?php echo strtoupper( $texts['payment_method_section_title'] ); ?></h3>
<p>
<?php if ( $payment_info ) { ?>
<span class="card-icon"></span> - <?php echo $payment_info['card_last4']; ?>
<?php } else { ?>
<p><?php echo $payment_method; ?></p>
<?php } ?>
</p>
<?php } ?>
</header>
<h3><?php echo strtoupper( $texts['summary_section_title'] ); ?></h3>
<table>
<?php
foreach ( $line_items as $line_item ) {
if ( isset( $line_item['quantity'] ) ) {
?>
<tr><td><?php echo $line_item['title']; ?> × <?php echo $line_item['quantity']; ?></td><td><?php echo $line_item['amount']; ?></td></tr>
<?php } else { ?>
<tr><td><?php echo $line_item['title']; ?></td><td><?php echo $line_item['amount']; ?></td></tr>
<?php
}
}
?>
</table>
<?php if ( ! empty( $notes ) ) { ?>
<h3><?php echo strtoupper( $texts['order_notes_section_title'] ); ?></h3>
<?php foreach ( $notes as $note ) { ?>
<p><?php echo $note; ?></p>
<?php
}
}
if ( $payment_info ) {
?>
<footer>
<p>
<?php
if ( $payment_info['app_name'] ) {
echo $texts['app_name'] . ': ' . $payment_info['app_name'] . '<br/>';
}
if ( $payment_info['aid'] ) {
echo $texts['aid'] . ': ' . $payment_info['aid'] . '<br/>';
}
if ( $payment_info['account_type'] ) {
echo $texts['account_type'] . ': ' . $payment_info['account_type'];
}
?>
</p>
</footer>
<?php } ?>
</body>
</html>

View File

@ -0,0 +1,233 @@
<?php
namespace Automattic\WooCommerce\Internal\ReceiptRendering;
use Automattic\WooCommerce\Internal\RegisterHooksInterface;
use Automattic\WooCommerce\Utilities\StringUtil;
use \WP_HTTP_Response;
use \WP_REST_Request;
use \WP_REST_Response;
use \WP_Error;
use \InvalidArgumentException;
use \Exception;
use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;
/**
* Base class for REST API controllers defined inside the 'src' directory.
*
* Besides implementing the abstract methods, derived classes must be registered in the dependency injection
* container with the 'share_with_implements_tags' method inside a service provider that inherits from
* 'AbstractInterfaceServiceProvider'. This ensures that 'register_routes' is invoked.
*
* Derived classes must also contain this line:
* use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;
*
* Minimal controller example:
*
* class FoobarsController extends RestApiControllerBase {
*
* use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;
*
* protected function get_rest_api_namespace(): string {
* return 'foobars';
* }
*
* public function register_routes() {
* register_rest_route(
* $this->route_namespace,
* '/foobars/(?P<id>[\d]+)',
* array(
* array(
* 'methods' => \WP_REST_Server::READABLE,
* 'callback' => fn( $request ) => $this->run( $request, 'get_foobar' ),
* 'permission_callback' => fn( $request ) => $this->check_permission( $request, 'read_foobars', $request->get_param( 'id' ) ),
* 'args' => $this->get_args_for_get_foobar(),
* 'schema' => $this->get_schema_for_get_foobar(),
* ),
* )
* );
* }
*
* protected function get_foobar( \WP_REST_Request $request ) {
* return array( 'message' => 'Get foobar with id ' . $request->get_param(' id' ) );
* }
*
* private function get_args_for_get_foobar(): array {
* return array(
* 'id' => array(
* 'description' => __( 'Unique identifier of the foobar.', 'woocommerce' ),
* 'type' => 'integer',
* 'context' => array( 'view', 'edit' ),
* 'readonly' => true,
* ),
* );
* }
*
* private function get_schema_for_get_foobar(): array {
* $schema = $this->get_base_schema();
* $schema['properties'] = array(
* 'message' => array(
* 'description' => __( 'A message.', 'woocommerce' ),
* 'type' => 'string',
* 'context' => array( 'view', 'edit' ),
* 'readonly' => true,
* ),
* );
* return $schema;
* }
*
* }
*/
abstract class RestApiControllerBase implements RegisterHooksInterface {
use AccessiblePrivateMethods;
/**
* The root namespace for the JSON REST API endpoints.
*
* @var string
*/
protected string $route_namespace = 'wc/v3';
/**
* Holds authentication error messages for each HTTP verb.
*
* @var array
*/
protected array $authentication_errors_by_method;
/**
* Class constructor.
*/
public function __construct() {
$this->authentication_errors_by_method = array(
'GET' => array(
'code' => 'woocommerce_rest_cannot_view',
'message' => __( 'Sorry, you cannot view resources.', 'woocommerce' ),
),
'POST' => array(
'code' => 'woocommerce_rest_cannot_create',
'message' => __( 'Sorry, you cannot create resources.', 'woocommerce' ),
),
'DELETE' => array(
'code' => 'woocommerce_rest_cannot_delete',
'message' => __( 'Sorry, you cannot delete resources.', 'woocommerce' ),
),
);
}
/**
* Register the hooks used by the class.
*/
public function register() {
static::add_filter( 'woocommerce_rest_api_get_rest_namespaces', array( $this, 'handle_woocommerce_rest_api_get_rest_namespaces' ) );
}
/**
* Handle the woocommerce_rest_api_get_rest_namespaces filter
* to add ourselves to the list of REST API controllers registered by WooCommerce.
*
* @param array $namespaces The original list of WooCommerce REST API namespaces/controllers.
* @return array The updated list of WooCommerce REST API namespaces/controllers.
*/
protected function handle_woocommerce_rest_api_get_rest_namespaces( array $namespaces ): array {
$namespaces['wc/v3'][ $this->get_rest_api_namespace() ] = static::class;
return $namespaces;
}
/**
* Get the WooCommerce REST API namespace for the class. It must be unique across all other derived classes
* and the keys returned by the 'get_vX_controllers' methods in includes/rest-api/Server.php.
* Note that this value is NOT related to the route namespace.
*
* @return string
*/
abstract protected function get_rest_api_namespace(): string;
/**
* Register the REST API endpoints handled by this controller.
*
* Use 'register_rest_route' in the usual way, it's recommended to use the 'run' method for 'callback'
* and the 'check_permission' method for 'permission_check', see the example in the class comment.
*/
abstract public function register_routes();
/**
* Handle a request for one of the provided REST API endpoints.
*
* If an exception is thrown, the exception message will be returned as part of the response
* if the user has the 'manage_woocommerce' capability.
*
* Note that the method specified in $method_name must have a 'protected' visibility and accept one argument of type 'WP_REST_Request'.
*
* @param WP_REST_Request $request The incoming HTTP REST request.
* @param string $method_name The name of the class method to execute. It must be protected and accept one argument of type 'WP_REST_Request'.
* @return WP_Error|WP_HTTP_Response|WP_REST_Response The response to send back to the client.
*/
protected function run( WP_REST_Request $request, string $method_name ) {
try {
return rest_ensure_response( $this->$method_name( $request ) );
} catch ( InvalidArgumentException $ex ) {
$message = $ex->getMessage();
return new WP_Error( 'woocommerce_rest_invalid_argument', $message ? $message : __( 'Internal server error', 'woocommerce' ), array( 'status' => 400 ) );
} catch ( Exception $ex ) {
wc_get_logger()->error( StringUtil::class_name_without_namespace( static::class ) . ": when executing method $method_name: {$ex->getMessage()}" );
return $this->internal_wp_error( $ex );
}
}
/**
* Return an WP_Error object for an internal server error, with exception information if the current user is an admin.
*
* @param Exception $exception The exception to maybe include information from.
* @return WP_Error
*/
protected function internal_wp_error( Exception $exception ): WP_Error {
$data = array( 'status' => 500 );
if ( current_user_can( 'manage_woocommerce' ) ) {
$data['exception_class'] = get_class( $exception );
$data['exception_message'] = $exception->getMessage();
$data['exception_trace'] = (array) $exception->getTrace();
}
$data['exception_message'] = $exception->getMessage();
return new WP_Error( 'woocommerce_rest_internal_error', __( 'Internal server error', 'woocommerce' ), $data );
}
/**
* Permission check for REST API endpoints, given the request method.
*
* @param WP_REST_Request $request The request for which the permission is checked.
* @param string $required_capability_name The name of the required capability.
* @param mixed ...$extra_args Extra arguments to be used for the permission check.
* @return bool|WP_Error True if the current user has the capability, otherwise an "Unauthorized" error or False if no error is available for the request method.
*/
protected function check_permission( WP_REST_Request $request, string $required_capability_name, ...$extra_args ) {
if ( current_user_can( $required_capability_name, $extra_args ) ) {
return true;
}
$error_information = $this->authentication_errors_by_method[ $request->get_method() ] ?? null;
if ( is_null( $error_information ) ) {
return false;
}
return new WP_Error(
$error_information['code'],
$error_information['message'],
array( 'status' => rest_authorization_required_code() )
);
}
/**
* Get the base schema for the REST API endpoints.
*
* @return array
*/
protected function get_base_schema(): array {
return array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'order receipts',
'type' => 'object',
);
}
}

View File

@ -54,7 +54,7 @@ class TransientFilesEngine implements RegisterHooksInterface {
self::add_action( self::CLEANUP_ACTION_NAME, array( $this, 'handle_expired_files_cleanup_action' ) );
self::add_filter( 'woocommerce_debug_tools', array( $this, 'add_debug_tools_entries' ), 999, 1 );
self::add_action( 'init', array( $this, 'handle_init' ), 0 );
self::add_action( 'init', array( $this, 'add_endpoint' ), 0 );
self::add_filter( 'query_vars', array( $this, 'handle_query_vars' ), 0 );
self::add_action( 'parse_request', array( $this, 'handle_parse_request' ), 0 );
}
@ -109,6 +109,14 @@ class TransientFilesEngine implements RegisterHooksInterface {
if ( ! $this->legacy_proxy->call_function( 'wp_mkdir_p', $transient_files_directory ) ) {
throw new Exception( "Can't create directory: $transient_files_directory" );
}
// Create infrastructure to prevent listing the contents of the transient files directory.
require_once ABSPATH . 'wp-admin/includes/file.php';
\WP_Filesystem();
$wp_filesystem = $this->legacy_proxy->get_global( 'wp_filesystem' );
$wp_filesystem->put_contents( $transient_files_directory . '/.htaccess', 'deny from all' );
$wp_filesystem->put_contents( $transient_files_directory . '/index.html', '' );
$realpathed_transient_files_directory = $this->legacy_proxy->call_function( 'realpath', $transient_files_directory );
} else {
throw new Exception( "The base transient files directory doesn't exist: $transient_files_directory" );
@ -153,7 +161,8 @@ class TransientFilesEngine implements RegisterHooksInterface {
}
$filepath = $transient_files_directory . '/' . $filename;
WP_Filesystem();
require_once ABSPATH . 'wp-admin/includes/file.php';
\WP_Filesystem();
$wp_filesystem = $this->legacy_proxy->get_global( 'wp_filesystem' );
if ( false === $wp_filesystem->put_contents( $filepath, $file_contents ) ) {
throw new Exception( "Can't create file: $filepath" );
@ -175,6 +184,23 @@ class TransientFilesEngine implements RegisterHooksInterface {
* @return string|null The full physical path of the file, or null if the files doesn't exist.
*/
public function get_transient_file_path( string $filename ): ?string {
$expiration_date = self::get_expiration_date( $filename );
if ( is_null( $expiration_date ) ) {
return null;
}
$file_path = $this->get_transient_files_directory() . '/' . $expiration_date . '/' . substr( $filename, 6 );
return is_file( $file_path ) ? $file_path : null;
}
/**
* Get the expiration date of a transient file based on its file name. The actual existence of the file is NOT checked.
*
* @param string $filename The name of the transient file to get the expiration date for.
* @return string|null Expiration date formatted as Y-m-d, null if the file name isn't encoding a proper date.
*/
public static function get_expiration_date( string $filename ) : ?string {
if ( strlen( $filename ) < 7 || ! ctype_xdigit( $filename ) ) {
return null;
}
@ -185,13 +211,18 @@ class TransientFilesEngine implements RegisterHooksInterface {
hexdec( substr( $filename, 3, 1 ) ),
hexdec( substr( $filename, 4, 2 ) )
);
if ( ! TimeUtil::is_valid_date( $expiration_date, 'Y-m-d' ) ) {
return null;
return TimeUtil::is_valid_date( $expiration_date, 'Y-m-d' ) ? $expiration_date : null;
}
$file_path = $this->get_transient_files_directory() . '/' . $expiration_date . '/' . substr( $filename, 6 );
return is_file( $file_path ) ? $file_path : null;
/**
* Get the public URL of a transient file. The file name is NOT checked for validity or actual existence.
*
* @param string $filename The name of the transient file to get the public URL for.
* @return string The public URL of the file.
*/
public function get_public_url( string $filename ) {
return $this->legacy_proxy->call_function( 'get_site_url', null, '/wc/file/transient/' . $filename );
}
/**
@ -394,7 +425,7 @@ class TransientFilesEngine implements RegisterHooksInterface {
/**
* Handle the "init" action, add rewrite rules for the "wc/file" endpoint.
*/
private function handle_init() {
public static function add_endpoint() {
add_rewrite_rule( '^wc/file/transient/?$', 'index.php?wc-transient-file-name=', 'top' );
add_rewrite_rule( '^wc/file/transient/(.+)$', 'index.php?wc-transient-file-name=$matches[1]', 'top' );
add_rewrite_endpoint( 'wc/file/transient', EP_ALL );

View File

@ -0,0 +1,255 @@
<?php
namespace Automattic\WooCommerce\Tests\Internal\ReceiptRendering;
use Automattic\WooCommerce\Internal\ReceiptRendering\ReceiptRenderingEngine;
use Automattic\WooCommerce\Internal\TransientFiles\TransientFilesEngine;
use Automattic\WooCommerce\Proxies\LegacyProxy;
use Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper;
/**
* Tests for the ReceiptRenderingEngine class.
*/
class ReceiptRenderingEngineTest extends \WC_Unit_Test_Case {
/**
* The System Under Test.
*
* @var ReceiptRenderingEngine
*/
private ReceiptRenderingEngine $sut;
/**
* Mock of the TransientFilesEngine class, used by the tested object.
*
* @var TransientFilesEngine
*/
private TransientFilesEngine $tfe_mock;
/**
* Runs before each test.
*/
public function setUp(): void {
parent::setUp();
$this->tfe_mock = $this->getMockBuilder( TransientFilesEngine::class )->getMock();
$this->sut = new ReceiptRenderingEngine();
$this->sut->init( $this->tfe_mock, wc_get_container()->get( LegacyProxy::class ) );
}
/**
* @testdox 'generate_receipt' returns null if the id of a not existing order is passed.
*/
public function test_generate_receipt_for_no_existing_order_returns_null() {
$filename = $this->sut->generate_receipt( -1 );
$this->assertNull( $filename );
}
/**
* @testdox 'generate_receipt' returns the file name of an already existing receipt if 'force_new' is not true.
*/
public function test_generate_receipt_returns_existing_receipt_if_force_new_is_not_true() {
$order = OrderHelper::create_order();
$order->update_meta_data( ReceiptRenderingEngine::RECEIPT_FILE_NAME_META_KEY, 'the_existing_receipt_filename' );
$this->tfe_mock
->expects( self::never() )
->method( 'create_transient_file' );
$this->tfe_mock
->expects( self::once() )
->method( 'get_transient_file_path' )
->with( 'the_existing_receipt_filename' )
->willReturn( 'transient_files/the_existing_receipt_filename' );
$filename = $this->sut->generate_receipt( $order );
$this->assertEquals( 'the_existing_receipt_filename', $filename );
$meta = $order->get_meta( ReceiptRenderingEngine::RECEIPT_FILE_NAME_META_KEY );
$this->assertEquals( 'the_existing_receipt_filename', $meta );
}
/**
* @testdox 'generate_receipt' returns the file name of a new receipt if 'force_new' is not true but no receipt file exists.
*/
public function test_generate_receipt_returns_new_receipt_file_if_no_receipt_exists_and_force_new_is_not_true() {
$this->register_legacy_proxy_function_mocks(
array(
'strtotime' => fn ( $arg) => '+1 days' === $arg ? -1 : strtotime( $arg ),
'gmdate' => fn ( $format, $value) => 'Y-m-d' === $format && -1 === $value ? '2999-12-31' : gmdate( $format, $value ),
)
);
$order = OrderHelper::create_order();
$this->tfe_mock
->expects( self::once() )
->method( 'create_transient_file' )
->with( self::isType( 'string' ), self::equalTo( '2999-12-31' ) )
->willReturn( 'the_generated_file_name' );
$filename = $this->sut->generate_receipt( $order );
$this->assertEquals( 'the_generated_file_name', $filename );
$meta = $order->get_meta( ReceiptRenderingEngine::RECEIPT_FILE_NAME_META_KEY );
$this->assertEquals( 'the_generated_file_name', $meta );
}
/**
* @testdox 'generate_receipt' returns the file name of a new receipt if a receipt file already exists but 'force_new' is true.
*/
public function test_generate_receipt_returns_new_receipt_file_if_receipt_exists_but_force_new_is_true() {
$this->register_legacy_proxy_function_mocks(
array(
'strtotime' => fn ( $arg) => '+1 days' === $arg ? -1 : strtotime( $arg ),
'gmdate' => fn ( $format, $value) => 'Y-m-d' === $format && -1 === $value ? '2999-12-31' : gmdate( $format, $value ),
)
);
$order = OrderHelper::create_order();
$order->update_meta_data( ReceiptRenderingEngine::RECEIPT_FILE_NAME_META_KEY, 'the_existing_receipt_filename' );
$this->tfe_mock
->expects( self::never() )
->method( 'get_transient_file_path' );
$this->tfe_mock
->expects( self::once() )
->method( 'create_transient_file' )
->with( self::isType( 'string' ), self::equalTo( '2999-12-31' ) )
->willReturn( 'the_generated_file_name' );
$filename = $this->sut->generate_receipt( $order, null, true );
$this->assertEquals( 'the_generated_file_name', $filename );
$meta = $order->get_meta( ReceiptRenderingEngine::RECEIPT_FILE_NAME_META_KEY );
$this->assertEquals( 'the_generated_file_name', $meta );
}
/**
* @testdox 'generate_receipt' uses the supplied expiration date to create the transient file.
*/
public function test_generate_receipt_with_custom_expiration_date() {
$order = OrderHelper::create_order();
$this->tfe_mock
->expects( self::once() )
->method( 'create_transient_file' )
->with( self::isType( 'string' ), self::equalTo( '2888-10-20' ) )
->willReturn( 'the_generated_file_name' );
$filename = $this->sut->generate_receipt( $order, '2888-10-20' );
$this->assertEquals( 'the_generated_file_name', $filename );
$meta = $order->get_meta( ReceiptRenderingEngine::RECEIPT_FILE_NAME_META_KEY );
$this->assertEquals( 'the_generated_file_name', $meta );
}
/**
* @testdox 'generate_receipt' throws an exception if an invalid expiration date is supplied.
*
* @testWith ["NOT_A_DATE"]
* ["2000-01-01"]
* ["2999-34-89"]
*
* @param string $expiration_date The expiration date to test.
*/
public function test_generate_receipt_throws_for_bad_expiration_date( string $expiration_date ) {
$this->expectException( \InvalidArgumentException::class );
$sut = wc_get_container()->get( ReceiptRenderingEngine::class );
$order = OrderHelper::create_order();
$sut->generate_receipt( $order, $expiration_date );
}
/**
* @testdox 'get_existing_receipt' returns null if the id of a not existing order is passed.
*/
public function test_get_existing_receipt_for_no_existing_order_returns_null() {
$filename = $this->sut->get_existing_receipt( -1 );
$this->assertNull( $filename );
}
/**
* @testdox 'get_existing_receipt' returns the file name of an existing receipt if the appropriate order meta entry exists and the file actually exists too and has not expired.
*/
public function test_get_existing_receipt_returns_existing_receipt() {
$order = OrderHelper::create_order();
$order->update_meta_data( ReceiptRenderingEngine::RECEIPT_FILE_NAME_META_KEY, 'the_existing_receipt_filename' );
$this->tfe_mock
->expects( self::once() )
->method( 'get_transient_file_path' )
->with( 'the_existing_receipt_filename' )
->willReturn( 'transient_files/the_existing_receipt_filename' );
$this->tfe_mock
->expects( self::once() )
->method( 'file_has_expired' )
->with( 'transient_files/the_existing_receipt_filename' )
->willReturn( false );
$filename = $this->sut->get_existing_receipt( $order );
$this->assertEquals( 'the_existing_receipt_filename', $filename );
}
/**
* @testdox 'get_existing_receipt' returns the file name of an existing receipt if the appropriate order meta entry doesn't exist.
*/
public function test_get_existing_receipt_returns_null_if_no_meta_entry() {
$order = OrderHelper::create_order();
$this->tfe_mock
->expects( self::never() )
->method( 'get_transient_file_path' );
$filename = $this->sut->get_existing_receipt( $order );
$this->assertNull( $filename );
}
/**
* @testdox 'get_existing_receipt' returns the file name of an existing receipt if the appropriate order meta entry exists but the file doesn't.
*/
public function test_get_existing_receipt_returns_null_if_meta_entry_exists_but_file_doesnt() {
$order = OrderHelper::create_order();
$order->update_meta_data( ReceiptRenderingEngine::RECEIPT_FILE_NAME_META_KEY, 'the_existing_receipt_filename' );
$this->tfe_mock
->expects( self::once() )
->method( 'get_transient_file_path' )
->with( 'the_existing_receipt_filename' )
->willReturn( null );
$filename = $this->sut->get_existing_receipt( $order );
$this->assertNull( $filename );
}
/**
* @testdox 'get_existing_receipt' returns the file name of an existing receipt if the appropriate order meta entry exists and the file exists but has expired.
*/
public function test_get_existing_receipt_returns_null_if_meta_entry_exists_but_file_has_expired() {
$order = OrderHelper::create_order();
$order->update_meta_data( ReceiptRenderingEngine::RECEIPT_FILE_NAME_META_KEY, 'the_existing_receipt_filename' );
$this->tfe_mock
->expects( self::once() )
->method( 'get_transient_file_path' )
->with( 'the_existing_receipt_filename' )
->willReturn( 'transient_files/the_existing_receipt_filename' );
$this->tfe_mock
->expects( self::once() )
->method( 'file_has_expired' )
->with( 'transient_files/the_existing_receipt_filename' )
->willReturn( true );
$filename = $this->sut->get_existing_receipt( $order );
$this->assertNull( $filename );
}
}

View File

@ -258,8 +258,31 @@ class TransientFilesEngineTest extends \WC_REST_Unit_Test_Case {
)
);
// phpcs:disable Squiz.Commenting
$fake_wp_filesystem = new class() {
public $created_files = array();
public function put_contents( string $file, string $contents, $mode = false ): bool {
$this->created_files[ $file ] = $contents;
return strlen( $contents );
}
};
// phpcs:enable Squiz.Commenting
$this->register_legacy_proxy_global_mocks(
array(
'wp_filesystem' => $fake_wp_filesystem,
)
);
$this->sut->get_transient_files_directory();
$this->assertEquals( '/wordpress/uploads/woocommerce_transient_files', $created_dir );
$expected_created_files = array(
'/wordpress/uploads/woocommerce_transient_files/.htaccess' => 'deny from all',
'/wordpress/uploads/woocommerce_transient_files/index.html' => '',
);
$this->assertEquals( $expected_created_files, $fake_wp_filesystem->created_files );
}
/**
@ -301,6 +324,42 @@ class TransientFilesEngineTest extends \WC_REST_Unit_Test_Case {
$this->assertEquals( static::$transient_files_dir . '/2023-12-02/000102030405060708090a0b0c0d0e0f', $result );
}
/**
* @testdox get_expiration_date returns null for file names without a properly encoded expiration date.
*
* @testWith [""]
* ["123"]
* ["NOT_HEX_DATE112233"]
* ["7e8f01"]
*
* @param string $filename Filename to test.
*/
public function test_get_expiration_date_returns_null_for_wrongly_formatted_date( string $filename ) {
$this->assertNull( TransientFilesEngine::get_expiration_date( $filename ) );
}
/**
* @testdox get_expiration_date returns the date encoded in a proper transient file name.
*/
public function test_get_expiration_date_correctly_extracts_date_from_filename() {
$actual = TransientFilesEngine::get_expiration_date( '7e821b00000' );
$this->assertEquals( '2024-02-27', $actual );
}
/**
* @testdox get_public_url returns the full public URL of a transient file given its name.
*/
public function test_get_public_url() {
$this->register_legacy_proxy_function_mocks(
array(
'get_site_url' => fn( $blog_id, $path) => 'http://example.com' . $path,
)
);
$actual = $this->sut->get_public_url( '1234abcd' );
$this->assertEquals( 'http://example.com/wc/file/transient/1234abcd', $actual );
}
/**
* @testdox file_has_expired return false for a file that hasn't expired.
*