file = str_replace('\\', '/', __FILE__);
}
/**
* Setup the environment for the plugin
*/
public function bootstrap() {
// Install/uninstall actions.
register_activation_hook($this->file, array($this, 'activate'));
register_deactivation_hook($this->file, array($this, 'deactivate'));
register_uninstall_hook($this->file, array('Acumulus', 'uninstall'));
// Actions:
// - Add our forms to the admin menu.
add_action('admin_menu', array($this, 'addMenuLinks'), 900);
// - Admin notices , meta boxes, and ajax requests from them.
add_action('admin_notices', array($this, 'showAdminNotices'));
add_action('add_meta_boxes_shop_order', array($this, 'addShopOrderMetaBox'));
add_action('wp_ajax_acumulus_ajax_action', array($this, 'handleAjaxRequest'));
// - To process our own forms.
add_action('admin_post_acumulus_config', array($this, 'processConfigForm'));
add_action('admin_post_acumulus_advanced', array($this, 'processAdvancedForm'));
add_action('admin_post_acumulus_batch', array($this, 'processBatchForm'));
add_action('admin_post_acumulus_register', array($this,
'processRegisterForm'
));
// - WooCommerce order/refund events.
add_action('plugins_loaded', array($this, 'pluginsLoaded'));
add_action('woocommerce_order_refunded', array($this, 'woocommerceOrderRefunded'), 10, 2);
// - Our own invoice related events.
add_filter('acumulus_invoice_created', array($this, 'acumulusInvoiceCreated'), 10, 3);
}
/**
* Helper method for the ConfigStore object to get the version number from the
* comment at the top of this file, as is the official location for WordPress
* plugins.
*
* @return string
* The version number of this plugin.
*/
public function getVersionNumber() {
if (function_exists('get_plugins')) {
$plugin_data = get_plugins();
$version = $plugin_data['acumulus/acumulus.php']['Version'];
} else {
$version = get_option('acumulus_version');
}
return $version;
}
/**
* Returns the WooCommerce version.
*
* @return string
* The WooCommerce version.
*/
private function getWooCommerceVersion() {
global $woocommerce;
return $woocommerce->version;
}
/**
* Returns whether the WooCommerce version is 3 or higher.
*
* @return bool
* Whether the WooCommerce version is 3 or higher
*/
private function isWooCommerce3plus() {
return version_compare($this->getWooCommerceVersion(), '3', '>=');
}
/**
* Returns whether the current page being rendered is the dashboard.
*
* @return bool
*/
private function isDashboard() {
$screen = get_current_screen();
return $screen && $screen->id === 'dashboard';
}
/**
* Returns whether the current page being rendered is one of our own pages.
*
* @return bool
*/
private function isOwnPage() {
$screenIds = array('settings_page_acumulus_config', 'settings_page_acumulus_advanced', 'woocommerce_page_acumulus_batch');
$screen = get_current_screen();
return $screen && in_array($screen->id, $screenIds);
}
/**
* Helper method to translate strings.
*
* @param string $key
* The key to get a translation for.
*
* @return string
* The translation for the given key or the key itself if no translation
* could be found.
*/
private function t($key) {
return $this->container->getTranslator()->get($key);
}
/**
* Loads our library and creates a configuration object.
*/
public function init() {
if ($this->container === NULL) {
// Load autoloader
require_once __DIR__ . '/lib/siel/acumulus/SielAcumulusAutoloader.php';
SielAcumulusAutoloader::register();
// Get language
$languageCode = get_bloginfo('language');
if (empty($languageCode)) {
$languageCode = 'nl';
}
$languageCode = substr($languageCode, 0, 2);
// Get WC version to set the shop namespace.
if ($this->isWooCommerce3plus()) {
$shopNamespace = 'WooCommerce';
} else {
$shopNamespace = 'WooCommerce\WooCommerce2';
}
$this->container = new Container($shopNamespace, $languageCode);
// Check for any updates to perform.
$this->upgrade();
$this->container->getLog()->setLogLevel(Severity::Log);
}
}
/**
* Adds our pages to the admin menu.
*/
public function addMenuLinks() {
$this->init();
// Add the (advanced) config form translations.
$this->container->getTranslator()->add(new ConfigFormTranslations());
add_submenu_page('options-general.php',
$this->t('config_form_title'),
$this->t('config_form_header'),
'manage_options',
'acumulus_config',
array($this, 'processConfigForm')
);
add_submenu_page('options-general.php',
$this->t('advanced_form_title'),
$this->t('advanced_form_header'),
'manage_options',
'acumulus_advanced',
array($this, 'processAdvancedForm')
);
// Add the register form translations.
$this->container->getTranslator()->add(new RegisterFormTranslations());
add_submenu_page('acumulus_config',
$this->t('register_form_title'),
$this->t('register_form_header'),
'manage_options',
'acumulus_register',
array($this, 'processRegisterForm')
);
// Add the batch form translations.
$this->container->getTranslator()->add(new BatchFormTranslations());
add_submenu_page('woocommerce',
$this->t('batch_form_title'),
$this->t('batch_form_header'),
'manage_woocommerce',
'acumulus_batch',
array($this, 'processBatchForm')
);
}
/**
* Shows admin notices.
*
* Due to the order of execution and the habit of redirecting at the end of
* an action, just adding a notice may not work. Therefore we work with
* transients.
*/
public function showAdminNotices() {
// These notices should only show on the main dashboard and our own screens.
if ($this->isDashboard() || $this->isOwnPage()) {
// Notice about rating our plugin.
if (time() >= $this->container->getConfig()->getShowRatePluginMessage()) {
echo $this->processRatePluginForm();
}
// Notice about ending support for WooCommerce 2.
if ($this->isWooCommerce3plus()) {
// WooCommerce has been upgraded to a version >= 3. We do no longer need
// this transient.
delete_transient('acumulus_stop_support_woocommerce2');
} else {
$value = get_transient('acumulus_stop_support_woocommerce2');
if (empty($value) || time() >= $value + 2 * 24 * 60 * 60) {
echo '
' . $this->t('wc2_end_support') . '
';
// Store the next time we are displaying it: in 2 days.
set_transient('acumulus_stop_support_woocommerce2', time() + 2 * 24 * 60 * 60);
}
}
}
}
/**
* Handles ajax requests for this plugin.
*
* The invoice status overview and the rate plugin form use ajax requests.
*/
public function handleAjaxRequest()
{
check_ajax_referer('acumulus_ajax_action', 'acumulus_nonce');
$this->init();
// Check where the ajax call came from.
if (isset($_POST['area'])) {
switch ($_POST['area']) {
case 'acumulus-invoice':
$content = $this->processInvoiceStatusForm();
break;
case 'acumulus-rate':
$content = $this->processRatePluginForm();
break;
default:
$content = $this->renderNotice('Area parameter of ajax request unknown to Acumulus.', 'error');
}
} else {
$content = $this->renderNotice('No area parameter in ajax request for Acumulus.', 'error');
}
wp_send_json(array(
'content' => $content,
));
}
/**
* Action handler for the add_meta_boxes_shop_order action.
*
* @param WP_Post $shopOrderPost
*/
public function addShopOrderMetaBox(WP_Post $shopOrderPost) {
$this->init();
$invoiceStatusSettings = $this->container->getConfig()->getInvoiceStatusSettings();
if ($invoiceStatusSettings['showInvoiceStatus']) {
// Create form to already load form translations and to set the Source.
/** @var \Siel\Acumulus\Shop\InvoiceStatusForm $form */
$form = $this->getForm('invoice');
$orderId = $shopOrderPost->ID;
$source = $this->container->getSource(Source::Order, $orderId);
$form->setSource($source);
add_meta_box('acumulus-invoice-status-overview',
$this->t('acumulus_invoice_title'),
array($this, 'outputInvoiceStatusInfoBox'),
'shop_order',
'side',
'default');
}
}
/**
* Callback that renders the contents of the Acumulus invoice info box.
*
* param WP_Post $shopOrderPost
* The post for the current order.
*/
public function outputInvoiceStatusInfoBox(/*WP_Post $shopOrderPost*/) {
echo $this->processInvoiceStatusForm();
}
/**
* Getter for the form object.
*
* @param string $type
*
* @return \Siel\Acumulus\Helpers\Form
*/
private function getForm($type) {
$this->init();
return $this->container->getForm($type);
}
/**
* Implements the admin_post_acumulus_register action.
*
* Processes and renders the batch form.
*/
public function processRegisterForm() {
$this->checkCapability('manage_options');
$this->checkCapability('manage_woocommerce');
echo $this->processForm('register');
}
/**
* Implements the admin_post_acumulus_config action.
*
* Processes and renders the basic config form.
*/
public function processConfigForm() {
$this->checkCapability('manage_options');
$this->checkCapability('manage_woocommerce');
echo $this->processForm('config');
}
/**
* Implements the admin_post_acumulus_advanced action.
*
* Processes and renders the advanced config form.
*/
public function processAdvancedForm() {
$this->checkCapability('manage_options');
$this->checkCapability('manage_woocommerce');
echo $this->processForm('advanced');
}
/**
* Implements the admin_post_acumulus_batch action.
*
* Processes and renders the batch form.
*/
public function processBatchForm() {
$this->checkCapability('manage_woocommerce');
echo $this->processForm('batch');
}
/**
* Processes and renders the Acumulus invoice status overview form.
*
* Either called via:
* - Callback that renders the contents of the Acumulus invoice info box.
* - Ajax request handler.
*
* @return string
* The rendered form (embedded in any necessary html).
*/
public function processInvoiceStatusForm() {
return $this->processForm('invoice');
}
/**
* Processes and renders the Rate Acumulus plugin form.
*
* Either called via:
* - Render admin notice.
* - Ajax request handler.
*
* @return string
* The rendered form (embedded in any necessary html).
*/
public function processRatePluginForm() {
return $this->processForm('rate');
}
/**
* Processes and renders the form of the given type.
*
* @param string $type
* The form type: config, advanced, or batch.
*
* @return string
* the form html to output.
*/
public function processForm($type) {
$form = $this->getForm($type);
$this->preProcessForm($form);
$form->process();
$output = $this->renderForm($form);
$this->postProcessForm($form);
return $output;
}
/**
* Renders the form of the given $type.
*
* @param $form
* config, advanced, or batch.
*
* @return string
* the form html to output.
*/
private function renderForm(Form $form) {
$this->preRenderForm($form);
// Render the form first before wrapping it in its final format, so that any
// messages added during rendering can be shown on top.
$formOutput = $this->container->getFormRenderer()->render($form);
return $this->postRenderForm($form, $formOutput);
}
/**
* Performs form type specific actions prior to processing a form.
*
* @param \Siel\Acumulus\Helpers\Form $form
* The form that is going to be processed.
*/
private function preProcessForm(Form $form) {
$type = $form->getType();
// Check nonce.
if (!is_ajax() && $form->isSubmitted()) {
check_admin_referer("acumulus_{$type}_nonce");
}
// Form processing may depend on determining the payment status, but the
// default states as returned by wc_get_is_paid_statuses() are not how we
// would define "is paid".
if (in_array($type, array('batch', 'invoice'))) {
// WC 3.x: we use WC_Order::is_paid()
add_action('woocommerce_order_is_paid_statuses', array($this, 'woocommerceOrderIsPaidStatuses'), 10, 2);
// WC 2.x: we use WC_Order::needs_payment()
add_action('woocommerce_valid_order_statuses_for_payment', array($this, 'woocommerceValidOrderStatusesForPayment'), 10, 2);
}
}
/**
* Performs form type specific actions after a form has been processed.
*
* @param \Siel\Acumulus\Helpers\Form $form
* The form that has been processed.
*/
private function postProcessForm(Form $form) {
$type = $form->getType();
// Remove our actions that redefine "is paid".
if (in_array($type, array('batch', 'invoice'))) {
remove_action('woocommerce_order_is_paid_statuses', array($this, 'woocommerceOrderIsPaidStatuses'));
remove_action('woocommerce_valid_order_statuses_for_payment', array($this, 'woocommerceValidOrderStatusesForPayment'));
}
}
/**
* Performs form type specific actions prior to rendering a form
*
* @param \Siel\Acumulus\Helpers\Form $form
* The form that is going to be rendered.
*/
private function preRenderForm(Form $form) {
// Get a new FormRenderer as the rate plugin message may be shown inside our
// pages and that one has different settings.
$this->container->getFormRenderer(true);
// Add our own js.
$type = $form->getType();
$pluginUrl = plugins_url('/acumulus');
switch ($type) {
case 'batch':
// Add some js.
wp_enqueue_script('jquery-ui-datepicker');
wp_enqueue_script('acumulus.js', $pluginUrl . '/' . 'acumulus.js');
wp_enqueue_style('jquery-style', 'http://ajax.googleapis.com/ajax/libs/jqueryui/1.8/themes/base/jquery-ui.css');
break;
case 'invoice':
// Add some js.
wp_enqueue_script('jquery-ui-datepicker');
wp_enqueue_script('acumulus.js', $pluginUrl . '/' . 'acumulus.js');
wp_enqueue_script('acumulus-ajax.js', $pluginUrl . '/' . 'acumulus-ajax.js');
wp_localize_script('acumulus-ajax.js', 'acumulus_data',
array(
'ajax_nonce' => wp_create_nonce('acumulus_ajax_action'),
'wait' => $this->t('wait'),
)
);
// The invoice status overview is not rendered as other forms, therefore
// we change some properties of the form renderer.
$this->container->getFormRenderer()
->setProperty('usePopupDescription', true)
->setProperty('fieldsetContentWrapperClass', 'data')
->setProperty('detailsWrapperClass', '')
->setProperty('labelWrapperClass', 'label')
->setProperty('inputDescriptionWrapperClass', 'value')
->setProperty('markupWrapperTag', '');
break;
case 'rate':
// Add some js.
wp_enqueue_script('acumulus-ajax.js', $pluginUrl . '/' . 'acumulus-ajax.js');
wp_localize_script('acumulus-ajax.js', 'acumulus_data',
array(
'ajax_nonce' => wp_create_nonce('acumulus_ajax_action'),
'wait' => $this->t('wait'),
)
);
// The invoice status overview is not rendered as other forms, therefore
// we change some properties of the form renderer.
$this->container->getFormRenderer()
->setProperty('fieldsetContentWrapperTag', 'div')
->setProperty('fieldsetContentWrapperClass', '')
->setProperty('elementWrapperTag', '')
->setProperty('inputDescriptionWrapperTag', '')
->setProperty('renderEmptyLabel', false)
->setProperty('markupWrapperTag', '');
break;
}
// Add our own css.
wp_enqueue_style('acumulus_css_admin', $pluginUrl . '/acumulus.css');
}
/**
* Performs form type specific actions after a form has been rendered.
*
* @param \Siel\Acumulus\Helpers\Form $form
* The form that has been rendered.
* @param string $formOutput
* The html of the rendered form.
*
* @return string
* The rendered form with any wrapping around it.
*/
private function postRenderForm(Form $form, $formOutput) {
$output = '';
$type = $form->getType();
$id = "acumulus-$type";
switch ($type) {
case 'register':
case 'config':
case 'advanced':
case 'batch':
case 'invoice':
$id = "acumulus-$type";
// Wrap should actually be based on a property isFullPage or something like that.
$wrap = $form->needsFormAndSubmitButton();
$url = admin_url("admin.php?page=acumulus_$type");
$output .= $wrap ? '
';
break;
case 'rate':
$extraClasses = 'acumulus-area' . ($this->isOwnPage() ? ' inline' : '');
$output .= $this->showNotices($form);
$output .= $this->renderNotice($formOutput, 'success', $id, $extraClasses, true);
break;
}
return $output;
}
/**
* Action method that renders any notices coming from the form(s).
*
* @param \Siel\Acumulus\Helpers\Form $form
*
* @return string
*/
public function showNotices($form) {
$output = '';
if (isset($form)) {
foreach ($form->getMessages() as $message) {
$output .= $this->renderNotice($message->format(Message::Format_PlainWithSeverity), $this->SeverityToNoticeClass($message->getSeverity()), $message->getField());
}
}
return $output;
}
/**
* Converts a Severity constant into a WP notice class.
*
* @param int $severity
*
* @return string
*
*/
private function SeverityToNoticeClass($severity) {
switch ($severity) {
case Severity::Success:
$class = 'success';
break;
case Severity::Info:
case Severity::Notice:
$class = 'info';
break;
case Severity::Warning:
$class = 'warning';
break;
case Severity::Error:
case Severity::Exception:
$class = 'error';
break;
default:
$class = '';
break;
}
return $class;
}
/**
* Renders a notice.
*
* @param string $message
* @param string $type
* The type of notice, used to construct css classes to distinguish the
* different types of messages. error, warning, info, etc.
* @param string $id
* An optional id to use for the outer tag.
* @param string $class
* Optional css classes to add (besides those that are already added to
* get a (dismissible) notice in WP style.
* @param bool $isHtml
* Indicates whether $message is html or plain text. plain text will be
* embedded in a
.
*
* @return string
* The rendered notice.
*/
private function renderNotice($message, $type, $id = '', $class = '', $isHtml = false) {
$for = '';
if (func_num_args() === 3 && $id !== '') {
// Make it a clickable label.
$for = $id;
$id = '';
}
if (!empty($id)) {
$id = ' id="' . $id . '"';
}
if (!empty($class)) {
$class = " $class";
}
$result = "
";
if (!$isHtml) {
$result .= '
';
}
if ($for) {
$result .= "';
}
if (!$isHtml) {
$result .= '
';
}
$result .= '
';
return $result;
}
/**
* Checks access to the current form/page.
*
* @param string $capability
* The access right to check for.
*/
private function checkCapability($capability)
{
if (!empty($capability) && !current_user_can($capability)) {
wp_die(__('You do not have sufficient permissions to access this page.'));
}
}
/**
* Defines an order status changed action for each status.
*/
public function pluginsLoaded() {
// Get WC version to determine the action(s) to listen to.
if ($this->isWooCommerce3plus()) {
// @nth: can we prevent this init on each page load: cache or copy code of
// getShopOrderStatuses()?
$this->init();
$statuses = $this->container->getShopCapabilities()->getShopOrderStatuses();
foreach ($statuses as $status => $label) {
add_action('woocommerce_order_status_' . $status, array($this, 'woocommerceOrderStatusChanged'), 10, 1);
}
} else {
add_action('woocommerce_order_status_changed', array($this, 'woocommerceOrderStatusChanged'), 10, 1);
}
}
/**
* Filter function for the 'woocommerce_order_status_changed' action,
*
* This action gets called when the status of an order changes.
*
* @param int $orderId
* - For WC3+ on 'woocommerce_order_status_...'
* param WC_Order $order
* - For WC3+ on 'woocommerce_order_status_changed'
* param int $status
* param int $newStatus
* param WC_Order $Order
* - For WC2 on 'woocommerce_order_status_changed'
* param int $status
* param int $newStatus
*/
public function woocommerceOrderStatusChanged($orderId) {
$this->init();
// WC 3.x: we use WC_Order::is_paid() to determine the payment status,
// but the default states as returned by wc_get_is_paid_statuses() are
// not as we define "is paid".
add_action('woocommerce_order_is_paid_statuses', array($this, 'woocommerceOrderIsPaidStatuses'), 10, 2);
// WC 2.x: we use WC_Order::needs_payment()
add_action('woocommerce_valid_order_statuses_for_payment', array($this, 'woocommerceValidOrderStatusesForPayment'), 10, 2);
$source = $this->container->getSource(Source::Order, $orderId);
$this->container->getInvoiceManager()->sourceStatusChange($source);
remove_action('woocommerce_order_is_paid_statuses', array($this, 'woocommerceOrderIsPaidStatuses'));
remove_action('woocommerce_valid_order_statuses_for_payment', array($this, 'woocommerceValidOrderStatusesForPayment'));
}
/**
* Filter function that gets called when the status of an order changes.
*
* @param int $orderId
* @param int $refundId
*/
public function woocommerceOrderRefunded(/** @noinspection PhpUnusedParameterInspection */ $orderId, $refundId) {
$this->init();
$source = $this->container->getSource(Source::CreditNote, $refundId);
$this->container->getInvoiceManager()->sourceStatusChange($source);
}
/**
* Hook to correct the behaviour of WC_Order::is_paid().
*
* Note: this is used when running with WC 3.x.
*
* WooCommerce thinks that only orders in the processing or completed statuses
* are to be seen as paid, whereas for Acumulus, refunded orders are also
* paid. (if an order is refunded, a separate credit note invoice will be
* created in Acumulus and thus the invoice for the original order remains
* "paid".)
*
* @param array $statuses
* param WC_Order $order
*
* @return array
*/
public function woocommerceOrderIsPaidStatuses(array $statuses/*, WC_Order $order*/) {
return array_merge($statuses, array('refunded'));
}
/**
* Hook to correct the behaviour of WC_Order::needs_payment().
*
* Note: this is only used when running with WC 2.x.
*
* WooCommerce thinks that orders in the on-hold status are to be seen as
* paid, whereas for Acumulus they are seen as due. (On-hold means waiting for
* bank transfer to be booked on our account.)
*
* We also add the the cancelled and failed statuses, although for a cancelled
* no invoice should be created. But if 1 gets created, mark it as due.
*
* @param array $statuses
* param WC_Order $order
*
* @return array
*/
public function woocommerceValidOrderStatusesForPayment(array $statuses/*, WC_Order $order*/) {
return array_merge($statuses, array('on-hold', 'cancelled', 'failed'));
}
/**
* Processes the filter triggered before an invoice will be sent to Acumulus.
*
* @param array|null $invoice
* The invoice in Acumulus format as will be sent to Acumulus or null if
* another filter already decided that the invoice should not be sent to
* Acumulus.
* @param \Siel\Acumulus\Invoice\Source $invoiceSource
* Wrapper around the original WooCommerce order or refund for which the
* invoice has been created.
* @param \Siel\Acumulus\Invoice\Result $localResult
* Any local error or warning messages that were created locally.
*
* @return array|null
* The changed invoice or null if you do not want the invoice to be sent
* to Acumulus.
*/
public function acumulusInvoiceCreated($invoice, Source $invoiceSource, Result $localResult) {
if ($invoice !== null) {
$this->init();
// Get WC version: only for WC 3+ do we support the other plugins.
if ($this->isWooCommerce3plus()) {
/** @var \Siel\Acumulus\WooCommerce\Invoice\CreatorPluginSupport $pluginSupport */
$pluginSupport = $this->container->getInstance('CreatorPluginSupport', 'Invoice');
$invoice = $pluginSupport->acumulusInvoiceCreated($invoice, $invoiceSource, $localResult);
}
}
return $invoice;
}
/**
* Forwards the call to an instance of the setup class.
*/
public function activate() {
$this->init();
require_once 'AcumulusSetup.php';
$setup = new AcumulusSetup($this->container, $this->getVersionNumber());
$setup->activate();
}
/**
* Forwards the call to an instance of the setup class.
*
* To keep the lazy loading we do check the version number here.
*
* @return bool
*/
public function upgrade() {
$dbVersion = get_option('acumulus_version');
if (empty($dbVersion) || version_compare($dbVersion, $this->getVersionNumber(), '<')) {
require_once 'AcumulusSetup.php';
$setup = new AcumulusSetup($this->container, $this->getVersionNumber());
return $setup->upgrade($dbVersion);
}
return true;
}
/**
* Forwards the call to an instance of the setup class.
*
* @return bool
*/
public function deactivate() {
$this->init();
require_once 'AcumulusSetup.php';
$setup = new AcumulusSetup($this->container);
return $setup->deactivate();
}
/**
* Forwards the call to an instance of the setup class.
*
* @return bool
*/
public static function uninstall() {
$acumulus = static::create();
$acumulus->init();
require_once 'AcumulusSetup.php';
$setup = new AcumulusSetup($acumulus->container);
return $setup->uninstall();
}
}
// Entry point for WP: create and bootstrap our module.
Acumulus::create()->bootstrap();