Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
d343214
Add Product object typoe
Nov 8, 2018
fdd6037
Implement Webhook Security
Nov 12, 2018
89d0b89
Added check for Test webhook events and return 200 code.
Nov 19, 2018
a36022d
Add stripe invoice.finalized as a webhook event type constant
Nov 19, 2018
03c685b
Add invoice.finalized to subscriber.
Nov 19, 2018
d9e24b3
Updated Plans and Products to new objects
Nov 19, 2018
7552cdc
Added nickname and active to plan types
Nov 19, 2018
7e58e3c
Revert "Added nickname and active to plan types"
Nov 19, 2018
44da331
Add an wrapper for the Client
Nov 19, 2018
485efb8
Add the StripeClient as a service
Nov 19, 2018
e419840
Fix decimal type
Nov 19, 2018
4b4fa0e
Make definition of the webhook subscriber optional from the config. T…
Nov 19, 2018
f21bcc9
Make definition of the webhook subscriber optional from the config. T…
Nov 19, 2018
d085824
Update service definition.
Nov 19, 2018
fbf37b6
Redefine nickname
Nov 19, 2018
2957473
Add Products and Subscription Items to the StripeClient
Nov 20, 2018
e3b74f1
Add Products and Subscription Items to the StripeClient
Nov 23, 2018
d012ec5
Add Products and Subscription Items to the StripeClient
Nov 23, 2018
4012da8
Add invoice deleted event
mogilvie Jul 1, 2019
7b1d30d
Add Products and Subscription Items to the StripeClient
Sep 29, 2019
7c9ba91
Merge remote-tracking branch 'origin/master'
Sep 29, 2019
c481ce8
Updated for billing
Nov 17, 2019
234dbd0
Updated for billing
Nov 22, 2019
8a95e82
Add invoice.voided event
Nov 27, 2019
4cd5eb6
Add invoice.voided event
Nov 27, 2019
81569ef
Add invoice.voided event
Nov 27, 2019
af74432
Add invoice.voided event
Dec 2, 2019
58399f3
Update for Symfoy 5
May 23, 2020
8db559e
Update for Symfoy 5
mogilvie Jun 18, 2020
60b170a
Update StripeClient.php
mogilvie Aug 4, 2020
84edaf8
Update StripeEvent.php
mogilvie Aug 4, 2020
73fe59c
Merge branch 'master' of github.com:mogilvie/stripe-bundle
mogilvie Aug 10, 2020
8c56c70
Update for Symfoy 5
mogilvie Aug 10, 2020
81aad4a
Update for Symfoy 5
mogilvie Aug 10, 2020
6d965ae
Update README.md
mogilvie Oct 24, 2020
6b5be7f
Merge pull request #1 from mogilvie/Symfony5
mogilvie Dec 14, 2020
6550c18
Update controller for symfony5
mogilvie Dec 21, 2020
3f41211
Merge remote-tracking branch 'origin/master'
mogilvie Dec 21, 2020
cebab68
Register webhook contoller as service
mogilvie Jan 31, 2021
545d98e
Change naming format of service
mogilvie Jan 31, 2021
31c5c74
Update WebhookController.php
mogilvie Feb 1, 2021
34bfb03
Update services.xml
mogilvie Feb 1, 2021
1abadbe
Update WebhookController.php
mogilvie Feb 9, 2021
4852a90
Provide return type for symfony 6
mogilvie Mar 20, 2022
34e574d
Update StripeClient.php
mogilvie Apr 15, 2022
992039b
Add Payout object.
mogilvie Jul 12, 2023
0f07c1f
Add Payout object.
mogilvie Jul 12, 2023
50878c9
Add Payout object.
mogilvie Jul 12, 2023
21657cb
Add Payout object.
mogilvie Jul 12, 2023
bc4a3e5
Add stripe api version in params file.
mogilvie Aug 23, 2023
ceee51d
Add stripe api version in params file.
mogilvie Dec 15, 2023
fdbc9cd
Add stripe api version in params file.
mogilvie Dec 15, 2023
5ba2b75
Add Products and Prices.
mogilvie Feb 6, 2024
f6eea53
Add price and product
mogilvie Feb 7, 2024
c9d50ee
Subscribe to Price and Product updates
mogilvie Feb 7, 2024
297dfbf
Update StripeEventSubscriber.php
mogilvie Feb 7, 2024
4a34669
Update AnnotationTransformer.php
mogilvie Feb 7, 2024
9a49cee
Update StripeEvent.php
mogilvie Jul 1, 2024
9b63d33
Update StripeEvent.php
mogilvie Jul 1, 2024
bc18eec
Update StripeEvent.php
mogilvie Jul 1, 2024
351aa6b
Add Credit note events.
mogilvie Aug 8, 2024
7dfdf02
Add credit note listener
mogilvie Aug 8, 2024
9661094
Create AbstractCreditNoteModel
mogilvie Aug 8, 2024
574a9f8
Rename AbstractCreditNoteModel to AbstractCreditNoteModel.php
mogilvie Aug 8, 2024
1910cd7
Create AbstractDisputeModel.php
mogilvie Aug 8, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file modified .gitignore
100644 → 100755
Empty file.
Empty file modified .travis.yml
100644 → 100755
Empty file.
Empty file modified Annotation/StripeObjectParam.php
100644 → 100755
Empty file.
54 changes: 49 additions & 5 deletions Controller/WebhookController.php
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,26 @@
use Miracode\StripeBundle\Event\StripeEvent;
use Miracode\StripeBundle\Stripe\StripeObjectType;
use Miracode\StripeBundle\StripeException;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Stripe\Error\SignatureVerification;
use Stripe\Exception\SignatureVerificationException;
use Stripe\Webhook;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Stripe\Event as StripeEventApi;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;

class WebhookController extends Controller
class WebhookController extends AbstractController
{

private $eventDispatcher;

public function __construct(EventDispatcherInterface $eventDispatcher)
{
$this->eventDispatcher = $eventDispatcher;
}

/**
* @param Request $request
*
Expand All @@ -21,24 +33,56 @@ class WebhookController extends Controller
*/
public function handleAction(Request $request)
{

$requestData = json_decode($request->getContent());

if (!isset($requestData->id) || !isset($requestData->object)) {
throw new BadRequestHttpException('Invalid webhook request data');
}

// If event id ends with 14 zero's then it is a test webhook event. Return 200 status.
if(substr($requestData->id, -14 ) == "00000000000000"){
return new Response('Webhook test successful', 200);
}

if ($requestData->object !== StripeObjectType::EVENT) {
throw new StripeException('Unknown stripe object type in webhook');
}

// Secure webhook with event signature: https://stripe.com/docs/webhooks/signatures
$webhookSecret = $this->getParameter('miracode_stripe.webhook_secret');

$verifySignature = $this->getParameter('verify_stripe_signature');

if($verifySignature === true && $webhookSecret !== null) {
$sigHeader = $request->headers->get('Stripe-Signature');
try {
$event = Webhook::constructEvent(
$request->getContent(), $sigHeader, $webhookSecret
);
} catch(\UnexpectedValueException $e) {
// Invalid payload
throw new StripeException(
sprintf('Invalid event payload', $requestData->id)
);
} catch(SignatureVerificationException $e) {
// Invalid signature
throw new StripeException(
sprintf('Invalid event signature', $requestData->id)
);
}
}

$stripeEventApi = new StripeEventApi();

if (!$stripeEventObject = $stripeEventApi->retrieve($requestData->id)) {
throw new StripeException(
sprintf('Event does not exists, id %s', $requestData->id)
);
}

$event = new StripeEvent($stripeEventObject);
$this
->get('event_dispatcher')
->dispatch('stripe.' . $stripeEventObject->type, $event);
$this->eventDispatcher->dispatch($event, 'stripe.' . $stripeEventObject->type);

return new Response();
}
Expand Down
Empty file modified DependencyInjection/Compiler/RegisterDoctrineMappingPass.php
100644 → 100755
Empty file.
26 changes: 23 additions & 3 deletions DependencyInjection/Configuration.php
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,34 @@ class Configuration implements ConfigurationInterface
/**
* {@inheritdoc}
*/
public function getConfigTreeBuilder()
public function getConfigTreeBuilder(): TreeBuilder
{
$treeBuilder = new TreeBuilder();
$rootNode = $treeBuilder->root('miracode_stripe');
$treeBuilder = new TreeBuilder('miracode_stripe');

if (method_exists($treeBuilder, 'getRootNode')) {
$rootNode = $treeBuilder->getRootNode();
} else {
// for symfony/config 4.1 and older
$rootNode = $treeBuilder->root('miracode_stripe');
}

$supportedDrivers = array('orm', /** coming soon) */);

$rootNode
->children()
->scalarNode('api_version')
->defaultNull()
->end()
->scalarNode('secret_key')
->isRequired()
->cannotBeEmpty()
->end()
->scalarNode('webhook_secret')
->defaultNull()
->end()
->booleanNode('use_bundle_subscriber')
->defaultTrue()
->end()
->arrayNode('database')
->children()
->scalarNode('driver')
Expand All @@ -45,12 +60,17 @@ public function getConfigTreeBuilder()
->scalarNode('charge')->cannotBeEmpty()->end()
->scalarNode('coupon')->cannotBeEmpty()->end()
->scalarNode('customer')->cannotBeEmpty()->end()
->scalarNode('tax_id')->cannotBeEmpty()->end()
->scalarNode('discount')->cannotBeEmpty()->end()
->scalarNode('invoice')->cannotBeEmpty()->end()
->scalarNode('payout')->cannotBeEmpty()->end()
->scalarNode('product')->cannotBeEmpty()->end()
->scalarNode('price')->cannotBeEmpty()->end()
->scalarNode('plan')->cannotBeEmpty()->end()
->scalarNode('refund')->cannotBeEmpty()->end()
->scalarNode('subscription')->cannotBeEmpty()->end()
->scalarNode('subscription_item')->cannotBeEmpty()->end()
->scalarNode('tax_rate')->cannotBeEmpty()->end()
->end()
->end()
->end()
Expand Down
16 changes: 15 additions & 1 deletion DependencyInjection/MiracodeStripeExtension.php
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,16 @@ public function load(array $configs, ContainerBuilder $container)
$config['secret_key']
);

$container->setParameter(
'miracode_stripe.webhook_secret',
$config['webhook_secret']
);

$container->setParameter(
'miracode_stripe.api_version',
$config['api_version']
);

if (!empty($config['database']) && !empty($config['database']['model'])) {
if (!empty($config['database']['model_transformer'])) {
$container->setAlias(
Expand All @@ -48,7 +58,11 @@ public function load(array $configs, ContainerBuilder $container)
);
}
if ($this->configureDatabase($config['database'], $container)) {
$loader->load('listener.xml');

// If the bundle event listener is to be used.
if($config['use_bundle_subscriber'] === true) {
$loader->load('listener.xml');
}
}
}
}
Expand Down
110 changes: 75 additions & 35 deletions Event/StripeEvent.php
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -4,44 +4,84 @@

use Miracode\StripeBundle\StripeException;
use Stripe\StripeObject;
use Symfony\Component\EventDispatcher\Event;
use Symfony\Contracts\EventDispatcher\Event;

class StripeEvent extends Event
{
const CHARGE_CAPTURED = 'stripe.charge.captured';
const CHARGE_FAILED = 'stripe.charge.failed';
const CHARGE_PENDING = 'charge.pending';
const CHARGE_REFUNDED = 'stripe.charge.refunded';
const CHARGE_SUCCEEDED = 'stripe.charge.succeeded';
const CHARGE_UPDATED = 'stripe.charge.updated';
const COUPON_CREATED = 'stripe.coupon.created';
const COUPON_DELETED = 'stripe.coupon.deleted';
const COUPON_UPDATED = 'stripe.coupon.updated';
const CUSTOMER_CREATED = 'stripe.customer.created';
const CUSTOMER_DELETED = 'stripe.customer.deleted';
const CUSTOMER_UPDATED = 'stripe.customer.updated';
const CUSTOMER_DISCOUNT_CREATED = 'stripe.customer.discount.created';
const CUSTOMER_DISCOUNT_DELETED = 'stripe.customer.discount.deleted';
const CUSTOMER_DISCOUNT_UPDATED = 'stripe.customer.discount.updated';
const CUSTOMER_SOURCE_CREATED = 'stripe.customer.source.created';
const CUSTOMER_SOURCE_DELETED = 'stripe.customer.source.deleted';
const CUSTOMER_SOURCE_UPDATED = 'stripe.customer.source.updated';
const CUSTOMER_SUBSCRIPTION_CREATED = 'stripe.customer.subscription.created';
const CUSTOMER_SUBSCRIPTION_DELETED = 'stripe.customer.subscription.deleted';
const CUSTOMER_SUBSCRIPTION_UPDATED = 'stripe.customer.subscription.updated';
const CUSTOMER_SUBSCRIPTION_TRAIL_WILL_END = 'stripe.customer.subscription.trial_will_end';
const INVOICE_CREATED = 'stripe.invoice.created';
const INVOICE_PAYMENT_FAILED = 'stripe.invoice.payment_failed';
const INVOICE_PAYMENT_SUCCEEDED = 'stripe.invoice.payment_succeeded';
const INVOICE_SENT = 'stripe.invoice.sent';
const INVOICE_UPCOMING = 'stripe.invoice.upcoming';
const INVOICE_UPDATED = 'stripe.invoice.updated';
const PLAN_CREATED = 'stripe.plan.created';
const PLAN_DELETED = 'stripe.plan.deleted';
const PLAN_UPDATED = 'stripe.plan.updated';
const SOURCE_CANCELED = 'stripe.source.canceled';
const SOURCE_CHARGEABLE = 'stripe.source.chargeable';
const SOURCE_FAILED = 'stripe.source.failed';
public const CHARGE_CAPTURED = 'stripe.charge.captured';
public const CHARGE_FAILED = 'stripe.charge.failed';
public const CHARGE_PENDING = 'charge.pending';
public const CHARGE_REFUNDED = 'stripe.charge.refunded';
public const CHARGE_SUCCEEDED = 'stripe.charge.succeeded';
public const CHARGE_UPDATED = 'stripe.charge.updated';
public const CHARGE_DISPUTE_OPENED = 'stripe.charge.dispute.opened';
public const CHARGE_DISPUTE_FUNDS_WITHDRAWN = 'stripe.charge.dispute.funds_withdrawn';
public const CHARGE_DISPUTE_CLOSED = 'stripe.charge.dispute.closed';
public const COUPON_CREATED = 'stripe.coupon.created';
public const COUPON_DELETED = 'stripe.coupon.deleted';
public const COUPON_UPDATED = 'stripe.coupon.updated';
public const CREDIT_NOTE_CREATED = 'stripe.credit_note.created';
public const CREDIT_NOTE_UPDATED = 'stripe.credit_note.updated';
public const CREDIT_NOTE_VOIDED = 'stripe.credit_note.voided';
public const CUSTOMER_CREATED = 'stripe.customer.created';
public const CUSTOMER_DELETED = 'stripe.customer.deleted';
public const CUSTOMER_UPDATED = 'stripe.customer.updated';
public const CUSTOMER_DISCOUNT_CREATED = 'stripe.customer.discount.created';
public const CUSTOMER_DISCOUNT_DELETED = 'stripe.customer.discount.deleted';
public const CUSTOMER_DISCOUNT_UPDATED = 'stripe.customer.discount.updated';
public const CUSTOMER_SOURCE_CREATED = 'stripe.customer.source.created';
public const CUSTOMER_SOURCE_DELETED = 'stripe.customer.source.deleted';
public const CUSTOMER_SOURCE_UPDATED = 'stripe.customer.source.updated';
public const CUSTOMER_SUBSCRIPTION_CREATED = 'stripe.customer.subscription.created';
public const CUSTOMER_SUBSCRIPTION_DELETED = 'stripe.customer.subscription.deleted';
public const CUSTOMER_SUBSCRIPTION_UPDATED = 'stripe.customer.subscription.updated';
public const CUSTOMER_SUBSCRIPTION_TRIAL_WILL_END = 'stripe.customer.subscription.trial_will_end';
public const CUSTOMER_TAX_ID_CREATED = 'stripe.customer.tax_id.created';
public const CUSTOMER_TAX_ID_UPDATED = 'stripe.customer.tax_id.updated';
public const INVOICE_CREATED = 'stripe.invoice.created';
public const INVOICE_DELETED = 'stripe.invoice.deleted';
public const INVOICE_FINALIZED = 'stripe.invoice.finalized';
public const INVOICE_ITEM_UPDATED = 'stripe.invoiceitem.updated';
public const INVOICE_PAYMENT_ACTION_REQUIRED = 'stripe.invoice.payment_action_required';
public const INVOICE_PAYMENT_FAILED = 'stripe.invoice.payment_failed';
public const INVOICE_PAYMENT_SUCCEEDED = 'stripe.invoice.payment_succeeded';
public const INVOICE_SENT = 'stripe.invoice.sent';
public const INVOICE_UPCOMING = 'stripe.invoice.upcoming';
public const INVOICE_UPDATED = 'stripe.invoice.updated';
public const INVOICE_VOIDED = 'stripe.invoice.voided';
public const PRODUCT_CREATED = 'stripe.product.created';
public const PRODUCT_DELETED = 'stripe.product.deleted';
public const PRODUCT_UPDATED = 'stripe.product.updated';
public const PRICE_CREATED = 'stripe.price.created';
public const PRICE_DELETED = 'stripe.price.deleted';
public const PRICE_UPDATED = 'stripe.price.updated';
public const PLAN_CREATED = 'stripe.plan.created';
public const PLAN_DELETED = 'stripe.plan.deleted';
public const PLAN_UPDATED = 'stripe.plan.updated';
public const PAYMENT_INTENT_AMOUNT_CAPTURABLE_UPDATED = 'stripe.payment_intent.amount_capturable_updated';
public const PAYMENT_INTENT_CANCELLED = 'stripe.payment_intent.canceled';
public const PAYMENT_INTENT_CREATED = 'stripe.payment_intent.created';
public const PAYMENT_INTENT_PAYMENT_FILED = 'stripe.payment_intent.payment_failed';
public const PAYMENT_INTENT_PROCESSING = 'stripe.payment_intent.processing';
public const PAYMENT_INTENT_REQURIES_ACTION = 'stripe.payment_intent.requires_action';
public const PAYMENT_INTENT_SUCCEEDED = 'stripe.payment_intent.succeeded';
public const PAYMENT_METHOD_ATTACHED = 'stripe.payment_method.attached';
public const PAYMENT_METHOD_DETACHED = 'stripe.payment_method.detached';
public const PAYMENT_METHOD_UPDATED = 'stripe.payment_method.updated';
public const PAYMENT_METHOD_AUTOMATICALLY_UPDATED = 'stripe.payment_method.automatically_updated';
public const PAYOUT_PAID = 'stripe.payout.paid';
public const REPORT_RUN_SUCCEEDED = 'stripe.reporting.report_run.succeeded';
public const SETUP_INTENT_CANCELED = 'stripe.setup_intent.canceled';
public const SETUP_INTENT_CREATED = 'stripe.setup_intent.created';
public const SETUP_INTENT_REQUIRES_ACTION = 'stripe.setup_intent.requires_action';
public const SETUP_INTENT_SETUP_FILED = 'stripe.setup_intent.setup_failed';
public const SETUP_INTENT_SUCCEEDED = 'stripe.setup_intent.succeeded';
public const SOURCE_CANCELED = 'stripe.source.canceled';
public const SOURCE_CHARGEABLE = 'stripe.source.chargeable';
public const SOURCE_FAILED = 'stripe.source.failed';
public const TAX_RATE_CREATED = 'stripe.tax_rate.created';
public const TAX_RATE_UPDATED = 'stripe.tax_rate.updated';
public const CHECKOUT_SESSION_COMPLETED = 'stripe.checkout.session.completed';

/**
* @var StripeObject
Expand Down
20 changes: 16 additions & 4 deletions EventListener/StripeEventSubscriber.php
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -35,29 +35,39 @@ public static function getSubscribedEvents()
StripeEvent::CHARGE_REFUNDED => 'onStripeChargeEvent',
StripeEvent::CHARGE_SUCCEEDED => 'onStripeChargeEvent',
StripeEvent::CHARGE_UPDATED => 'onStripeChargeEvent',

StripeEvent::COUPON_CREATED => 'onStripeEvent',
StripeEvent::COUPON_UPDATED => 'onStripeEvent',
StripeEvent::CREDIT_NOTE_CREATED => 'onStripeEvent',
StripeEvent::CREDIT_NOTE_UPDATED => 'onStripeEvent',
StripeEvent::CREDIT_NOTE_VOIDED => 'onStripeEvent',
StripeEvent::CUSTOMER_CREATED => 'onStripeEvent',
StripeEvent::CUSTOMER_UPDATED => 'onStripeEvent',
StripeEvent::CUSTOMER_TAX_ID_CREATED => 'onStripeEvent',
StripeEvent::CUSTOMER_TAX_ID_UPDATED => 'onStripeEvent',
StripeEvent::CUSTOMER_SOURCE_CREATED => 'onStripeEvent',
StripeEvent::CUSTOMER_SOURCE_UPDATED => 'onStripeEvent',
StripeEvent::CUSTOMER_SUBSCRIPTION_CREATED => 'onStripeEvent',
StripeEvent::CUSTOMER_SUBSCRIPTION_UPDATED => 'onStripeEvent',
StripeEvent::CUSTOMER_SUBSCRIPTION_TRAIL_WILL_END => 'onStripeEvent',
StripeEvent::CUSTOMER_SUBSCRIPTION_DELETED => 'onStripeEvent',
StripeEvent::CUSTOMER_SUBSCRIPTION_TRIAL_WILL_END => 'onStripeEvent',
StripeEvent::INVOICE_CREATED => 'onStripeEvent',
StripeEvent::INVOICE_FINALIZED => 'onStripeEvent',
StripeEvent::INVOICE_PAYMENT_FAILED => 'onStripeEvent',
StripeEvent::INVOICE_PAYMENT_SUCCEEDED => 'onStripeEvent',
StripeEvent::INVOICE_SENT => 'onStripeEvent',
StripeEvent::INVOICE_UPDATED => 'onStripeEvent',
StripeEvent::PRODUCT_CREATED => 'onStripeEvent',
StripeEvent::PRODUCT_UPDATED => 'onStripeEvent',
StripeEvent::PRICE_CREATED => 'onStripeEvent',
StripeEvent::PRICE_UPDATED => 'onStripeEvent',
StripeEvent::PLAN_CREATED => 'onStripeEvent',
StripeEvent::PLAN_UPDATED => 'onStripeEvent',
StripeEvent::SOURCE_CANCELED => 'onStripeEvent',
StripeEvent::SOURCE_CHARGEABLE => 'onStripeEvent',
StripeEvent::SOURCE_FAILED => 'onStripeEvent',
StripeEvent::CUSTOMER_SUBSCRIPTION_DELETED => 'onStripeEvent',

StripeEvent::COUPON_DELETED => 'onStripeDeleteEvent',
StripeEvent::TAX_RATE_CREATED => 'onStripeEvent',
StripeEvent::TAX_RATE_UPDATED => 'onStripeEvent',
StripeEvent::CUSTOMER_DELETED => 'onStripeDeleteEvent',
StripeEvent::CUSTOMER_SOURCE_DELETED => 'onStripeDeleteEvent',
StripeEvent::PLAN_DELETED => 'onStripeDeleteEvent',
Expand All @@ -70,11 +80,13 @@ public static function getSubscribedEvents()
public function onStripeEvent(StripeEvent $event)
{
$object = $event->getDataObject();

if ($this->modelManager->support($object)) {
$this->modelManager->save($object, true);
}
}


/**
* @param StripeEvent $event
*/
Expand Down
Empty file modified LICENSE
100644 → 100755
Empty file.
Loading