MailChimp for WooCommerce - Version 2.0.2

Version Description

  • Added new logs feature to help troubleshoot isolated sync and data feed issues.
  • Fixed bug with setting customers as Transactional during checkout if they had already opted in previously.
  • Fixed bug where abandoned cart automation still fired after a customer completed an order.
Download this release

Release Info

Developer MailChimp
Plugin Icon wp plugin MailChimp for WooCommerce
Version 2.0.2
Comparing to
See all releases

Code changes from version 2.0.1 to 2.0.2

Files changed (88) hide show
  1. LICENSE.txt +0 -0
  2. README.md +8 -7
  3. README.txt +16 -10
  4. admin/class-mailchimp-woocommerce-admin.php +20 -4
  5. admin/css/mailchimp-woocommerce-admin.css +0 -0
  6. admin/index.php +0 -0
  7. admin/js/mailchimp-woocommerce-admin.js +0 -0
  8. admin/partials/mailchimp-woocommerce-admin-tabs.php +32 -7
  9. admin/partials/tabs/api_key.php +0 -0
  10. admin/partials/tabs/campaign_defaults.php +0 -0
  11. admin/partials/tabs/errors/missing_api_key.php +0 -0
  12. admin/partials/tabs/errors/missing_campaign_defaults.php +0 -0
  13. admin/partials/tabs/errors/missing_list.php +0 -0
  14. admin/partials/tabs/errors/missing_store.php +0 -0
  15. admin/partials/tabs/errors/not_ready_for_sync.php +0 -0
  16. admin/partials/tabs/logs.php +80 -0
  17. admin/partials/tabs/newsletter_settings.php +1 -0
  18. admin/partials/tabs/notices.php +0 -0
  19. admin/partials/tabs/store_info.php +2 -0
  20. admin/partials/tabs/store_sync.php +0 -0
  21. admin/partials/tabs/success/re-sync-started.php +0 -0
  22. changelog.md +6 -0
  23. includes/api/assets/class-mailchimp-address.php +0 -0
  24. includes/api/assets/class-mailchimp-cart.php +0 -0
  25. includes/api/assets/class-mailchimp-customer.php +0 -0
  26. includes/api/assets/class-mailchimp-line-item.php +0 -0
  27. includes/api/assets/class-mailchimp-order.php +0 -0
  28. includes/api/assets/class-mailchimp-product-variation.php +0 -0
  29. includes/api/assets/class-mailchimp-product.php +0 -0
  30. includes/api/assets/class-mailchimp-store.php +0 -0
  31. includes/api/class-mailchimp-api.php +32 -13
  32. includes/api/class-mailchimp-woocommerce-api.php +0 -0
  33. includes/api/class-mailchimp-woocommerce-create-list-submission.php +0 -0
  34. includes/api/class-mailchimp-woocommerce-transform-orders-wc3.php +10 -1
  35. includes/api/class-mailchimp-woocommerce-transform-orders.php +27 -0
  36. includes/api/class-mailchimp-woocommerce-transform-products.php +0 -0
  37. includes/api/errors/class-mailchimp-error.php +0 -0
  38. includes/api/errors/class-mailchimp-server-error.php +0 -0
  39. includes/api/helpers/class-mailchimp-woocommerce-api-currency-codes.php +0 -0
  40. includes/api/helpers/class-mailchimp-woocommerce-api-locales.php +0 -0
  41. includes/class-mailchimp-woocommerce-activator.php +0 -0
  42. includes/class-mailchimp-woocommerce-deactivator.php +0 -0
  43. includes/class-mailchimp-woocommerce-i18n.php +0 -0
  44. includes/class-mailchimp-woocommerce-loader.php +0 -0
  45. includes/class-mailchimp-woocommerce-newsletter.php +0 -0
  46. includes/class-mailchimp-woocommerce-options.php +0 -0
  47. includes/class-mailchimp-woocommerce-service.php +2 -7
  48. includes/class-mailchimp-woocommerce.php +0 -0
  49. includes/index.php +0 -0
  50. includes/plugin-update-checker/README.md +105 -0
  51. includes/plugin-update-checker/composer.json +19 -0
  52. includes/plugin-update-checker/css/puc-debug-bar.css +62 -0
  53. includes/plugin-update-checker/debug-bar-panel.php +146 -0
  54. includes/plugin-update-checker/debug-bar-plugin.php +102 -0
  55. includes/plugin-update-checker/github-checker.php +458 -0
  56. includes/plugin-update-checker/js/debug-bar.js +52 -0
  57. includes/plugin-update-checker/languages/plugin-update-checker-fr_FR.mo +0 -0
  58. includes/plugin-update-checker/languages/plugin-update-checker-fr_FR.po +38 -0
  59. includes/plugin-update-checker/languages/plugin-update-checker-hu_HU.mo +0 -0
  60. includes/plugin-update-checker/languages/plugin-update-checker-hu_HU.po +41 -0
  61. includes/plugin-update-checker/languages/plugin-update-checker.pot +39 -0
  62. includes/plugin-update-checker/license.txt +7 -0
  63. includes/plugin-update-checker/plugin-update-checker.php +1641 -0
  64. includes/plugin-update-checker/vendor/Parsedown.php +1538 -0
  65. includes/plugin-update-checker/vendor/ParsedownLegacy.php +1535 -0
  66. includes/plugin-update-checker/vendor/readme-parser.php +331 -0
  67. includes/processes/class-mailchimp-woocommerce-abstract-sync.php +13 -1
  68. includes/processes/class-mailchimp-woocommerce-cart-update.php +0 -2
  69. includes/processes/class-mailchimp-woocommerce-process-orders.php +17 -13
  70. includes/processes/class-mailchimp-woocommerce-process-products.php +7 -5
  71. includes/processes/class-mailchimp-woocommerce-single-order.php +30 -9
  72. includes/processes/class-mailchimp-woocommerce-single-product.php +4 -4
  73. includes/processes/class-mailchimp-woocommerce-user-submit.php +11 -5
  74. includes/vendor/queue.php +0 -0
  75. includes/vendor/queue/classes/cli/queue-command.php +0 -0
  76. includes/vendor/queue/classes/worker/wp-http-worker.php +0 -0
  77. includes/vendor/queue/classes/worker/wp-worker.php +3 -0
  78. includes/vendor/queue/classes/wp-job.php +0 -0
  79. includes/vendor/queue/classes/wp-queue.php +0 -0
  80. index.php +0 -0
  81. languages/mailchimp-woocommerce.pot +0 -0
  82. mailchimp-woocommerce.php +141 -136
  83. public/class-mailchimp-woocommerce-public.php +0 -0
  84. public/css/mailchimp-woocommerce-public.css +0 -0
  85. public/index.php +0 -0
  86. public/js/mailchimp-woocommerce-public.js +0 -0
  87. public/partials/mailchimp-woocommerce-public-display.php +0 -0
  88. uninstall.php +0 -0
LICENSE.txt CHANGED
File without changes
README.md CHANGED
@@ -2,7 +2,7 @@
2
 
3
  In this article, you’ll learn how to connect MailChimp for WooCommerce.
4
 
5
- #Before You Start#
6
 
7
  **Here are some things to know before you begin this process.**
8
 
@@ -18,20 +18,21 @@ In this article, you’ll learn how to connect MailChimp for WooCommerce.
18
 
19
  - WooCommerce customers who haven't signed up for marketing emails will appear in the Transactional portion of your list, and cannot be exported.
20
 
21
- #A Note for Current WooCommerce Integration Users#
22
 
23
  This plugin supports our most powerful API 3.0 features, and is intended for users who have not yet integrated their WooCommerce stores with MailChimp. If your WooCommerce store is already integrated with MailChimp via an integration that runs on an older version of MailChimp’s API, consider your current sales volume before you make any changes that might disrupt business.
24
 
25
  You can run this new integration at the same time as your current WooCommerce integration for MailChimp. However, data from the older integration will display separately in subscriber profiles, and can’t be used with e-commerce features that require API 3.0.
26
 
27
- #Task Roadmap#
28
  **Here’s a brief overview of this multi-step process.**
29
 
30
  - Install the plugin on your WordPress Admin site.
31
  - Connect the plugin with your MailChimp API Key.
32
  - Configure your list settings to complete the data sync process.
 
33
 
34
- #Install the Plugin#
35
  **To install the plugin, follow these steps.**
36
 
37
  1) Log in to your WordPress admin panel.
@@ -53,7 +54,7 @@ You can run this new integration at the same time as your current WooCommerce in
53
 
54
  After you activate the plugin, you’ll be taken to the **Settings** page, where you will add your API key and configure your list settings.
55
 
56
- #Configure and Sync#
57
  **To configure your MailChimp settings for WooCommerce customers and sync them to MailChimp, follow these steps.**
58
 
59
  1) On the **Connect** tab, paste your MailChimp API key into the field, choose whether or not you want to send debugging logs to MailChimp, and click **Save all changes**. To learn how to generate a MailChimp API Key, read [About API Keys](http://kb.mailchimp.com/integrations/api-integrations/about-api-keys).
@@ -82,12 +83,12 @@ If you have no lists in your MailChimp account, you will be given the option to
82
 
83
  ![List Defaults tab](https://cloud.githubusercontent.com/assets/19805049/18956260/cffd3926-8628-11e6-9c68-9fe3c964c75c.png)
84
 
85
- #Next Steps#
86
  After you connect, you can do a lot with the the data you collect, like build segments, send Automation workflows, track purchases, and view results.
87
 
88
  Find out everything MailChimp has to offer in our article, [How to Use MailChimp for E-Commerce](http://kb.mailchimp.com/integrations/e-commerce/how-to-use-mailchimp-for-e-commerce).
89
 
90
- #Deactivate or Delete the Plugin#
91
  When you deactivate MailChimp for WooCommerce, it stops the sync but doesn’t remove the plugin. You can always re-activate the sync, which will backfill data at a later point in time.
92
  To deactivate MailChimp for WooCommerce, follow these steps.
93
 
2
 
3
  In this article, you’ll learn how to connect MailChimp for WooCommerce.
4
 
5
+ ## Before You Start
6
 
7
  **Here are some things to know before you begin this process.**
8
 
18
 
19
  - WooCommerce customers who haven't signed up for marketing emails will appear in the Transactional portion of your list, and cannot be exported.
20
 
21
+ ## A Note for Current WooCommerce Integration Users
22
 
23
  This plugin supports our most powerful API 3.0 features, and is intended for users who have not yet integrated their WooCommerce stores with MailChimp. If your WooCommerce store is already integrated with MailChimp via an integration that runs on an older version of MailChimp’s API, consider your current sales volume before you make any changes that might disrupt business.
24
 
25
  You can run this new integration at the same time as your current WooCommerce integration for MailChimp. However, data from the older integration will display separately in subscriber profiles, and can’t be used with e-commerce features that require API 3.0.
26
 
27
+ ## Task Roadmap
28
  **Here’s a brief overview of this multi-step process.**
29
 
30
  - Install the plugin on your WordPress Admin site.
31
  - Connect the plugin with your MailChimp API Key.
32
  - Configure your list settings to complete the data sync process.
33
+ - Troubleshoot any sync or data feed issues by sharing logs with MailChimp support.
34
 
35
+ ## Install the Plugin
36
  **To install the plugin, follow these steps.**
37
 
38
  1) Log in to your WordPress admin panel.
54
 
55
  After you activate the plugin, you’ll be taken to the **Settings** page, where you will add your API key and configure your list settings.
56
 
57
+ ## Configure and Sync
58
  **To configure your MailChimp settings for WooCommerce customers and sync them to MailChimp, follow these steps.**
59
 
60
  1) On the **Connect** tab, paste your MailChimp API key into the field, choose whether or not you want to send debugging logs to MailChimp, and click **Save all changes**. To learn how to generate a MailChimp API Key, read [About API Keys](http://kb.mailchimp.com/integrations/api-integrations/about-api-keys).
83
 
84
  ![List Defaults tab](https://cloud.githubusercontent.com/assets/19805049/18956260/cffd3926-8628-11e6-9c68-9fe3c964c75c.png)
85
 
86
+ ## Next Steps
87
  After you connect, you can do a lot with the the data you collect, like build segments, send Automation workflows, track purchases, and view results.
88
 
89
  Find out everything MailChimp has to offer in our article, [How to Use MailChimp for E-Commerce](http://kb.mailchimp.com/integrations/e-commerce/how-to-use-mailchimp-for-e-commerce).
90
 
91
+ # Deactivate or Delete the Plugin
92
  When you deactivate MailChimp for WooCommerce, it stops the sync but doesn’t remove the plugin. You can always re-activate the sync, which will backfill data at a later point in time.
93
  To deactivate MailChimp for WooCommerce, follow these steps.
94
 
README.txt CHANGED
@@ -11,17 +11,17 @@ License URI: http://www.gnu.org/licenses/gpl-2.0.html
11
  Connect your store to your MailChimp list to track sales, create targeted emails, send abandoned cart emails, and more.
12
 
13
  == Description ==
14
- - MailChimp for WooCommerce is a free plugin that connects your WooCommerce store with your MailChimp account.
15
- - Your customers and their purchase data are automatically synced with MailChimp, so you can create targeted email campaigns based on buying behavior.
16
 
17
- You’ll have the power to:
18
 
19
- - Sync list and purchase data
20
- - Create abandoned cart Automation workflows
21
- - Send product recommendations
22
- - Segment based on purchase history
23
- - View your results and measure ROI
24
- - Grow your audience and sell more stuff with Facebook Ad Campaigns in MailChimp
 
25
 
26
  ###A note for current WooCommerce integration users
27
  This plugin supports our most powerful API 3.0 features, and is intended for users who have not yet integrated their WooCommerce stores with MailChimp.
@@ -36,7 +36,7 @@ Here are some things to know before you begin this process.
36
  - Your hosting environment must meet [WooCommerce's minimum requirements](https://docs.woocommerce.com/document/server-requirements), including PHP 5.6 or greater.
37
  - We recommend you use this plugin in a staging environment before installing it on production servers. To learn more about staging environments, [check out these related Wordpress plugins](https://wordpress.org/plugins/search.php?q=staging).
38
  - MailChimp for WooCommerce syncs the customer’s first name, last name, email address, and orders.
39
- - WooCommerce customers who haven't signed up for marketing emails will appear in the **Transactional** portion of your list, and cannot be exported.
40
 
41
  ###Task Roadmap
42
  You’ll need to do a few things to connect your WooCommerce store to MailChimp.
@@ -49,6 +49,12 @@ You’ll need to do a few things to connect your WooCommerce store to MailChimp.
49
  For more information on settings and configuration, please visit our Knowledge Base: [http://kb.mailchimp.com/integrations/e-commerce/connect-or-disconnect-mailchimp-for-woocommerce](http://kb.mailchimp.com/integrations/e-commerce/connect-or-disconnect-mailchimp-for-woocommerce)
50
 
51
  == Changelog ==
 
 
 
 
 
 
52
  = 2.0.1 =
53
  * Added support for "Connected Site" scripts.
54
  * Made physical address a required field for store setup.
11
  Connect your store to your MailChimp list to track sales, create targeted emails, send abandoned cart emails, and more.
12
 
13
  == Description ==
14
+ Join the 16 million customers who use MailChimp, the world's largest marketing automation platform, to develop their e-commerce marketing strategy. With the official MailChimp for WooCommerce integration, your customers and their purchase data are automatically synced with your MailChimp account, making it easy to send targeted campaigns, automatically follow up with customers post-purchase, recommend products, recover abandoned carts, and measure the ROI of your marketing efforts. And it's completely free.
 
15
 
16
+ With MailChimp for WooCommerce, you’ll have the power to:
17
 
18
+ - Sync list and purchase data.
19
+ - Set up marketing automations to remind customers about items they left in their cart or viewed on your site, win back lapsed customers, and follow up post-purchase. (Now available for free accounts!)
20
+ - Showcase product recommendations.
21
+ - Track and segment customers based on purchase history and purchase frequency.
22
+ - View detailed data on your marketing performance in your MailChimp Dashboard.
23
+ - Grow your audience and sell more stuff with Facebook and Instagram Ad Campaigns in MailChimp.
24
+ - Automatically embed a pop-up form that converts your website visitors to subscribers.
25
 
26
  ###A note for current WooCommerce integration users
27
  This plugin supports our most powerful API 3.0 features, and is intended for users who have not yet integrated their WooCommerce stores with MailChimp.
36
  - Your hosting environment must meet [WooCommerce's minimum requirements](https://docs.woocommerce.com/document/server-requirements), including PHP 5.6 or greater.
37
  - We recommend you use this plugin in a staging environment before installing it on production servers. To learn more about staging environments, [check out these related Wordpress plugins](https://wordpress.org/plugins/search.php?q=staging).
38
  - MailChimp for WooCommerce syncs the customer’s first name, last name, email address, and orders.
39
+ - WooCommerce customers who haven't signed up for marketing emails will appear in the **Transactional** portion of your list, and cannot be exported.
40
 
41
  ###Task Roadmap
42
  You’ll need to do a few things to connect your WooCommerce store to MailChimp.
49
  For more information on settings and configuration, please visit our Knowledge Base: [http://kb.mailchimp.com/integrations/e-commerce/connect-or-disconnect-mailchimp-for-woocommerce](http://kb.mailchimp.com/integrations/e-commerce/connect-or-disconnect-mailchimp-for-woocommerce)
50
 
51
  == Changelog ==
52
+
53
+ = 2.0.2 =
54
+ * Added new logs feature to help troubleshoot isolated sync and data feed issues.
55
+ * Fixed bug with setting customers as Transactional during checkout if they had already opted in previously.
56
+ * Fixed bug where abandoned cart automation still fired after a customer completed an order.
57
+
58
  = 2.0.1 =
59
  * Added support for "Connected Site" scripts.
60
  * Made physical address a required field for store setup.
admin/class-mailchimp-woocommerce-admin.php CHANGED
@@ -243,10 +243,26 @@ class MailChimp_Woocommerce_Admin extends MailChimp_Woocommerce_Options {
243
  break;
244
 
245
  case 'sync':
246
- $this->startSync();
247
- $this->showSyncStartedMessage();
 
 
 
 
248
  $this->setData('sync.config.resync', true);
249
  break;
 
 
 
 
 
 
 
 
 
 
 
 
250
  }
251
 
252
  return (isset($data) && is_array($data)) ? array_merge($this->getOptions(), $data) : $this->getOptions();
@@ -486,7 +502,7 @@ class MailChimp_Woocommerce_Admin extends MailChimp_Woocommerce_Options {
486
  'mailchimp_auto_subscribe' => isset($input['mailchimp_auto_subscribe']) ? (bool) $input['mailchimp_auto_subscribe'] : $this->getOption('mailchimp_auto_subscribe', '0'),
487
  'mailchimp_checkbox_defaults' => $checkbox,
488
  'mailchimp_checkbox_action' => isset($input['mailchimp_checkbox_action']) ? $input['mailchimp_checkbox_action'] : $this->getOption('mailchimp_checkbox_action', 'woocommerce_after_checkout_billing_form'),
489
- );
490
 
491
  if ($data['mailchimp_list'] === 'create_new') {
492
  $data['mailchimp_list'] = $this->createMailChimpList(array_merge($this->getOptions(), $data));
@@ -870,7 +886,7 @@ class MailChimp_Woocommerce_Admin extends MailChimp_Woocommerce_Options {
870
  {
871
  $job = new MailChimp_WooCommerce_Process_Products();
872
  $job->flagStartSync();
873
- wp_queue($job, 10);
874
  }
875
 
876
  /**
243
  break;
244
 
245
  case 'sync':
246
+ // remove all the pointers to be sure
247
+ $service = new MailChimp_Service();
248
+ $service->removePointers(true, true);
249
+
250
+ $this->startSync();
251
+ $this->showSyncStartedMessage();
252
  $this->setData('sync.config.resync', true);
253
  break;
254
+
255
+ case 'logs':
256
+
257
+ if (isset($_POST['mc_action']) && in_array($_POST['mc_action'], array('view_log', 'remove_log'))) {
258
+ wp_redirect('options-general.php?page=mailchimp-woocommerce&tab=logs');
259
+ exit();
260
+ }
261
+
262
+ $data = array(
263
+ 'mailchimp_logging' => isset($input['mailchimp_logging']) ? $input['mailchimp_logging'] : 'none',
264
+ );
265
+ break;
266
  }
267
 
268
  return (isset($data) && is_array($data)) ? array_merge($this->getOptions(), $data) : $this->getOptions();
502
  'mailchimp_auto_subscribe' => isset($input['mailchimp_auto_subscribe']) ? (bool) $input['mailchimp_auto_subscribe'] : $this->getOption('mailchimp_auto_subscribe', '0'),
503
  'mailchimp_checkbox_defaults' => $checkbox,
504
  'mailchimp_checkbox_action' => isset($input['mailchimp_checkbox_action']) ? $input['mailchimp_checkbox_action'] : $this->getOption('mailchimp_checkbox_action', 'woocommerce_after_checkout_billing_form'),
505
+ );
506
 
507
  if ($data['mailchimp_list'] === 'create_new') {
508
  $data['mailchimp_list'] = $this->createMailChimpList(array_merge($this->getOptions(), $data));
886
  {
887
  $job = new MailChimp_WooCommerce_Process_Products();
888
  $job->flagStartSync();
889
+ wp_queue($job);
890
  }
891
 
892
  /**
admin/css/mailchimp-woocommerce-admin.css CHANGED
File without changes
admin/index.php CHANGED
File without changes
admin/js/mailchimp-woocommerce-admin.js CHANGED
File without changes
admin/partials/mailchimp-woocommerce-admin-tabs.php CHANGED
@@ -37,6 +37,27 @@ if (isset($options['mailchimp_api_key']) && $handler->hasValidApiKey()) {
37
  #sync-status-message strong {
38
  font-weight:inherit;
39
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  </style>
41
 
42
  <?php if (!defined('PHP_VERSION_ID') || (PHP_VERSION_ID < 50600)): ?>
@@ -61,6 +82,7 @@ if (isset($options['mailchimp_api_key']) && $handler->hasValidApiKey()) {
61
  <a href="?page=mailchimp-woocommerce&tab=newsletter_settings" class="nav-tab <?php echo $active_tab == 'newsletter_settings' ? 'nav-tab-active' : ''; ?>">List Settings</a>
62
  <?php if($show_sync_tab): ?>
63
  <a href="?page=mailchimp-woocommerce&tab=sync" class="nav-tab <?php echo $active_tab == 'sync' ? 'nav-tab-active' : ''; ?>">Sync</a>
 
64
  <?php endif; ?>
65
  <?php endif;?>
66
  <?php endif; ?>
@@ -74,34 +96,37 @@ if (isset($options['mailchimp_api_key']) && $handler->hasValidApiKey()) {
74
  if (!$clicked_sync_button) {
75
  settings_fields($this->plugin_name);
76
  do_settings_sections($this->plugin_name);
77
- //settings_errors();
78
  include('tabs/notices.php');
79
  }
80
  ?>
81
 
82
  <input type="hidden" name="<?php echo $this->plugin_name; ?>[mailchimp_active_tab]" value="<?php echo $active_tab; ?>"/>
83
 
84
- <?php if( $active_tab == 'api_key' ): ?>
85
  <?php include_once 'tabs/api_key.php'; ?>
86
  <?php endif; ?>
87
 
88
- <?php if( $active_tab == 'store_info' && $has_valid_api_key): ?>
89
  <?php include_once 'tabs/store_info.php'; ?>
90
  <?php endif; ?>
91
 
92
- <?php if( $active_tab == 'campaign_defaults' ): ?>
93
  <?php include_once 'tabs/campaign_defaults.php'; ?>
94
  <?php endif; ?>
95
 
96
- <?php if( $active_tab == 'newsletter_settings' ): ?>
97
  <?php include_once 'tabs/newsletter_settings.php'; ?>
98
  <?php endif; ?>
99
 
100
- <?php if( $active_tab == 'sync' && $show_sync_tab): ?>
101
  <?php include_once 'tabs/store_sync.php'; ?>
102
  <?php endif; ?>
103
 
104
- <?php if ($active_tab !== 'sync') submit_button('Save all changes', 'primary','submit', TRUE); ?>
 
 
 
 
105
 
106
  </form>
107
 
37
  #sync-status-message strong {
38
  font-weight:inherit;
39
  }
40
+ #log-viewer {
41
+ background: #fff;
42
+ border: 1px solid #e5e5e5;
43
+ box-shadow: 0 1px 1px rgba(0,0,0,.04);
44
+ padding: 5px 20px;
45
+ }
46
+ #log-viewer-select {
47
+ padding: 10px 0 8px;
48
+ line-height: 28px;
49
+ }
50
+ #log-viewer pre {
51
+ font-family: monospace;
52
+ white-space: pre-wrap;
53
+ }
54
+ user agent stylesheet
55
+ pre, xmp, plaintext, listing {
56
+ display: block;
57
+ font-family: monospace;
58
+ white-space: pre;
59
+ margin: 1em 0px;
60
+ }
61
  </style>
62
 
63
  <?php if (!defined('PHP_VERSION_ID') || (PHP_VERSION_ID < 50600)): ?>
82
  <a href="?page=mailchimp-woocommerce&tab=newsletter_settings" class="nav-tab <?php echo $active_tab == 'newsletter_settings' ? 'nav-tab-active' : ''; ?>">List Settings</a>
83
  <?php if($show_sync_tab): ?>
84
  <a href="?page=mailchimp-woocommerce&tab=sync" class="nav-tab <?php echo $active_tab == 'sync' ? 'nav-tab-active' : ''; ?>">Sync</a>
85
+ <a href="?page=mailchimp-woocommerce&tab=logs" class="nav-tab <?php echo $active_tab == 'logs' ? 'nav-tab-active' : ''; ?>">Logs</a>
86
  <?php endif; ?>
87
  <?php endif;?>
88
  <?php endif; ?>
96
  if (!$clicked_sync_button) {
97
  settings_fields($this->plugin_name);
98
  do_settings_sections($this->plugin_name);
 
99
  include('tabs/notices.php');
100
  }
101
  ?>
102
 
103
  <input type="hidden" name="<?php echo $this->plugin_name; ?>[mailchimp_active_tab]" value="<?php echo $active_tab; ?>"/>
104
 
105
+ <?php if ($active_tab == 'api_key' ): ?>
106
  <?php include_once 'tabs/api_key.php'; ?>
107
  <?php endif; ?>
108
 
109
+ <?php if ($active_tab == 'store_info' && $has_valid_api_key): ?>
110
  <?php include_once 'tabs/store_info.php'; ?>
111
  <?php endif; ?>
112
 
113
+ <?php if ($active_tab == 'campaign_defaults' ): ?>
114
  <?php include_once 'tabs/campaign_defaults.php'; ?>
115
  <?php endif; ?>
116
 
117
+ <?php if ($active_tab == 'newsletter_settings' ): ?>
118
  <?php include_once 'tabs/newsletter_settings.php'; ?>
119
  <?php endif; ?>
120
 
121
+ <?php if ($active_tab == 'sync' && $show_sync_tab): ?>
122
  <?php include_once 'tabs/store_sync.php'; ?>
123
  <?php endif; ?>
124
 
125
+ <?php if ($active_tab == 'logs' && $show_sync_tab): ?>
126
+ <?php include_once 'tabs/logs.php'; ?>
127
+ <?php endif; ?>
128
+
129
+ <?php if ($active_tab !== 'sync' && $active_tab !== 'logs') submit_button('Save all changes', 'primary','submit', TRUE); ?>
130
 
131
  </form>
132
 
admin/partials/tabs/api_key.php CHANGED
File without changes
admin/partials/tabs/campaign_defaults.php CHANGED
File without changes
admin/partials/tabs/errors/missing_api_key.php CHANGED
File without changes
admin/partials/tabs/errors/missing_campaign_defaults.php CHANGED
File without changes
admin/partials/tabs/errors/missing_list.php CHANGED
File without changes
admin/partials/tabs/errors/missing_store.php CHANGED
File without changes
admin/partials/tabs/errors/not_ready_for_sync.php CHANGED
File without changes
admin/partials/tabs/logs.php ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ if (!empty( $_REQUEST['handle'])) {
3
+ if (!empty($_REQUEST['_wpnonce']) && wp_verify_nonce($_REQUEST['_wpnonce'], 'remove_log')) {
4
+ $log_handler = new WC_Log_Handler_File();
5
+ $log_handler->remove($_REQUEST['handle']);
6
+ }
7
+ }
8
+ $files = defined('WC_LOG_DIR') ? @scandir( WC_LOG_DIR ) : array();
9
+ $logs = array();
10
+ if (!empty($files)) {
11
+ foreach ($files as $key => $value) {
12
+ if (!in_array( $value, array( '.', '..' ))) {
13
+ if (!is_dir($value) && mailchimp_string_contains($value, 'mailchimp_woocommerce')) {
14
+ $logs[sanitize_title($value)] = $value;
15
+ }
16
+ }
17
+ }
18
+ }
19
+ if (!empty($_REQUEST['log_file']) && isset($logs[sanitize_title( $_REQUEST['log_file'])])) {
20
+ $viewed_log = $logs[sanitize_title($_REQUEST['log_file'])];
21
+ } elseif (!empty($logs)) {
22
+ $viewed_log = current( $logs );
23
+ }
24
+ $handle = !empty($viewed_log) ? substr($viewed_log, 0, strlen($viewed_log) > 37 ? strlen($viewed_log) - 37 : strlen($viewed_log) - 4) : '';
25
+ ?>
26
+
27
+ <h2 style="padding-top: 1em;">Logging Preference</h2>
28
+ <p>
29
+ Advanced troubleshooting can be conducted with the logging capability turned on.
30
+ By default, it’s set to “none” and you may toggle to either “standard” or “debug” as needed.
31
+ With standard logging, you can see basic information about the data submission to MailChimp including any errors.
32
+ “Debug” gives a much deeper insight that is useful to share with support if problems arise.
33
+ </p>
34
+ <fieldset>
35
+ <legend class="screen-reader-text">
36
+ <span>Logging Preference</span>
37
+ </legend>
38
+ <label for="<?php echo $this->plugin_name; ?>-logging">
39
+ <select name="<?php echo $this->plugin_name; ?>[mailchimp_logging]" style="width:30%" required>
40
+ <?php $logging_preference = mailchimp_environment_variables()->logging; ?>
41
+ <?php
42
+ foreach(array('none' => 'None', 'debug' => 'Debug', 'standard' => 'Standard',) as $log_value => $log_label) {
43
+ echo '<option value="'.esc_attr($log_value).'" '.selected($log_value === $logging_preference, true, false ) . '>' . esc_html($log_label) . '</option>';
44
+ }
45
+ ?>
46
+ </select>
47
+ </label>
48
+ </fieldset>
49
+
50
+ <?php submit_button('Save all changes', 'primary','submit', TRUE);?>
51
+
52
+ <?php if (isset($logs) && isset($viewed_log)) : ?>
53
+ <div id="log-viewer-select">
54
+ <div class="alignleft">
55
+ <h2>
56
+ <?php echo esc_html( $viewed_log ); ?>
57
+ <?php if ( ! empty( $handle ) ) : ?>
58
+ <a class="page-title-action" href="<?php echo esc_url( wp_nonce_url( add_query_arg( array( 'handle' => $handle ), admin_url( 'options-general.php?page=mailchimp-woocommerce&tab=logs&mc_action=remove_log' ) ), 'remove_log' ) ); ?>" class="button"><?php esc_html_e( 'Delete log', 'woocommerce' );?></a>
59
+ <?php endif; ?>
60
+ </h2>
61
+ </div>
62
+ <div class="alignright">
63
+ <form action="<?php echo admin_url( 'options-general.php?page=mailchimp-woocommerce&tab=logs&mc_action=view_log' ); ?>" method="post">
64
+ <input type="hidden" name="<?php echo $this->plugin_name; ?>[mailchimp_active_tab]" value="logs"/>
65
+ <select name="log_file">
66
+ <?php foreach ( $logs as $log_key => $log_file ) : ?>
67
+ <option value="<?php echo esc_attr( $log_key ); ?>" <?php selected( sanitize_title( $viewed_log ), $log_key ); ?>><?php echo esc_html( $log_file ); ?> (<?php echo date_i18n( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), filemtime( WC_LOG_DIR . $log_file ) ); ?>)</option>
68
+ <?php endforeach; ?>
69
+ </select>
70
+ <input type="submit" class="button" value="<?php esc_attr_e( 'View', 'woocommerce' ); ?>" />
71
+ </form>
72
+ </div>
73
+ <div class="clear"></div>
74
+ </div>
75
+ <div id="log-viewer">
76
+ <pre><?php echo esc_html( file_get_contents( WC_LOG_DIR . $viewed_log ) ); ?></pre>
77
+ </div>
78
+ <?php else : ?>
79
+ <div class="updated woocommerce-message inline"><p><?php _e( 'There are currently no logs to view.', 'woocommerce' ); ?></p></div>
80
+ <?php endif; ?>
admin/partials/tabs/newsletter_settings.php CHANGED
@@ -125,3 +125,4 @@ $list_is_configured = isset($options['mailchimp_list']) && (!empty($options['mai
125
  <span><?php esc_attr_e('WooCommerce Action', $this->plugin_name); ?></span>
126
  </label>
127
  </fieldset>
 
125
  <span><?php esc_attr_e('WooCommerce Action', $this->plugin_name); ?></span>
126
  </label>
127
  </fieldset>
128
+
admin/partials/tabs/notices.php CHANGED
File without changes
admin/partials/tabs/store_info.php CHANGED
@@ -211,3 +211,5 @@ if (!$handler->hasValidApiKey()) {
211
  <span><?php esc_attr_e('Timezone', $this->plugin_name); ?></span>
212
  </label>
213
  </fieldset>
 
 
211
  <span><?php esc_attr_e('Timezone', $this->plugin_name); ?></span>
212
  </label>
213
  </fieldset>
214
+
215
+
admin/partials/tabs/store_sync.php CHANGED
File without changes
admin/partials/tabs/success/re-sync-started.php CHANGED
File without changes
changelog.md CHANGED
@@ -1,3 +1,9 @@
 
 
 
 
 
 
1
  ** 2.0.1 **
2
 
3
  ##### New Features: #####
1
+ ** 2.0.2 **
2
+
3
+ * Added new logs feature to help troubleshoot isolated sync and data feed issues.
4
+ * Fixed bug with setting customers as Transactional during checkout if they had already opted in previously.
5
+ * Fixed bug where abandoned cart automation still fired after a customer completed an order.
6
+
7
  ** 2.0.1 **
8
 
9
  ##### New Features: #####
includes/api/assets/class-mailchimp-address.php CHANGED
File without changes
includes/api/assets/class-mailchimp-cart.php CHANGED
File without changes
includes/api/assets/class-mailchimp-customer.php CHANGED
File without changes
includes/api/assets/class-mailchimp-line-item.php CHANGED
File without changes
includes/api/assets/class-mailchimp-order.php CHANGED
File without changes
includes/api/assets/class-mailchimp-product-variation.php CHANGED
File without changes
includes/api/assets/class-mailchimp-product.php CHANGED
File without changes
includes/api/assets/class-mailchimp-store.php CHANGED
File without changes
includes/api/class-mailchimp-api.php CHANGED
@@ -122,7 +122,7 @@ class MailChimp_WooCommerce_MailChimpApi
122
  */
123
  public function member($list_id, $email)
124
  {
125
- $hash = md5(strtolower($email));
126
  return $this->get("lists/$list_id/members/$hash", array());
127
  }
128
 
@@ -142,7 +142,7 @@ class MailChimp_WooCommerce_MailChimpApi
142
  */
143
  public function deleteMember($list_id, $email)
144
  {
145
- $hash = md5(strtolower($email));
146
  return $this->delete("lists/$list_id/members/$hash", array());
147
  }
148
 
@@ -185,7 +185,7 @@ class MailChimp_WooCommerce_MailChimpApi
185
  */
186
  public function update($list_id, $email, $subscribed = true, $merge_fields = array(), $list_interests = array())
187
  {
188
- $hash = md5(strtolower($email));
189
 
190
  $data = array(
191
  'email_address' => $email,
@@ -216,7 +216,7 @@ class MailChimp_WooCommerce_MailChimpApi
216
  */
217
  public function updateOrCreate($list_id, $email, $subscribed = true, $merge_fields = array(), $list_interests = array())
218
  {
219
- $hash = md5(strtolower($email));
220
 
221
  $data = array(
222
  'email_address' => $email,
@@ -913,8 +913,14 @@ class MailChimp_WooCommerce_MailChimpApi
913
  // process the patch request the normal way
914
  $curl = curl_init();
915
 
916
- $options = $this->applyCurlOptions('PATCH', $url, array());
917
- $options[CURLOPT_POSTFIELDS] = json_encode($body);
 
 
 
 
 
 
918
 
919
  curl_setopt_array($curl, $options);
920
 
@@ -931,8 +937,14 @@ class MailChimp_WooCommerce_MailChimpApi
931
  {
932
  $curl = curl_init();
933
 
934
- $options = $this->applyCurlOptions('POST', $url, array());
935
- $options[CURLOPT_POSTFIELDS] = json_encode($body);
 
 
 
 
 
 
936
 
937
  curl_setopt_array($curl, $options);
938
 
@@ -949,8 +961,14 @@ class MailChimp_WooCommerce_MailChimpApi
949
  {
950
  $curl = curl_init();
951
 
952
- $options = $this->applyCurlOptions('PUT', $url, array());
953
- $options[CURLOPT_POSTFIELDS] = json_encode($body);
 
 
 
 
 
 
954
 
955
  curl_setopt_array($curl, $options);
956
 
@@ -999,9 +1017,10 @@ class MailChimp_WooCommerce_MailChimpApi
999
  * @param $method
1000
  * @param $url
1001
  * @param array $params
 
1002
  * @return array
1003
  */
1004
- protected function applyCurlOptions($method, $url, $params = array())
1005
  {
1006
  $env = mailchimp_environment_variables();
1007
 
@@ -1015,10 +1034,10 @@ class MailChimp_WooCommerce_MailChimpApi
1015
  CURLOPT_TIMEOUT => 30,
1016
  CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
1017
  CURLINFO_HEADER_OUT => true,
1018
- CURLOPT_HTTPHEADER => array(
1019
  'content-type: application/json',
1020
  "user-agent: MailChimp for WooCommerce/{$env->version}; WordPress/{$env->wp_version}",
1021
- )
1022
  );
1023
  }
1024
 
122
  */
123
  public function member($list_id, $email)
124
  {
125
+ $hash = md5(strtolower(trim($email)));
126
  return $this->get("lists/$list_id/members/$hash", array());
127
  }
128
 
142
  */
143
  public function deleteMember($list_id, $email)
144
  {
145
+ $hash = md5(strtolower(trim($email)));
146
  return $this->delete("lists/$list_id/members/$hash", array());
147
  }
148
 
185
  */
186
  public function update($list_id, $email, $subscribed = true, $merge_fields = array(), $list_interests = array())
187
  {
188
+ $hash = md5(strtolower(trim($email)));
189
 
190
  $data = array(
191
  'email_address' => $email,
216
  */
217
  public function updateOrCreate($list_id, $email, $subscribed = true, $merge_fields = array(), $list_interests = array())
218
  {
219
+ $hash = md5(strtolower(trim($email)));
220
 
221
  $data = array(
222
  'email_address' => $email,
913
  // process the patch request the normal way
914
  $curl = curl_init();
915
 
916
+ $json = json_encode($body);
917
+
918
+ $options = $this->applyCurlOptions('PATCH', $url, array(), array(
919
+ 'Expect:',
920
+ 'Content-Length: '.strlen($json),
921
+ ));
922
+
923
+ $options[CURLOPT_POSTFIELDS] = $json;
924
 
925
  curl_setopt_array($curl, $options);
926
 
937
  {
938
  $curl = curl_init();
939
 
940
+ $json = json_encode($body);
941
+
942
+ $options = $this->applyCurlOptions('POST', $url, array(), array(
943
+ 'Expect:',
944
+ 'Content-Length: '.strlen($json),
945
+ ));
946
+
947
+ $options[CURLOPT_POSTFIELDS] = $json;
948
 
949
  curl_setopt_array($curl, $options);
950
 
961
  {
962
  $curl = curl_init();
963
 
964
+ $json = json_encode($body);
965
+
966
+ $options = $this->applyCurlOptions('PUT', $url, array(), array(
967
+ 'Expect:',
968
+ 'Content-Length: '.strlen($json),
969
+ ));
970
+
971
+ $options[CURLOPT_POSTFIELDS] = $json;
972
 
973
  curl_setopt_array($curl, $options);
974
 
1017
  * @param $method
1018
  * @param $url
1019
  * @param array $params
1020
+ * @param array $headers
1021
  * @return array
1022
  */
1023
+ protected function applyCurlOptions($method, $url, $params = array(), $headers = array())
1024
  {
1025
  $env = mailchimp_environment_variables();
1026
 
1034
  CURLOPT_TIMEOUT => 30,
1035
  CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
1036
  CURLINFO_HEADER_OUT => true,
1037
+ CURLOPT_HTTPHEADER => array_merge(array(
1038
  'content-type: application/json',
1039
  "user-agent: MailChimp for WooCommerce/{$env->version}; WordPress/{$env->wp_version}",
1040
+ ), $headers)
1041
  );
1042
  }
1043
 
includes/api/class-mailchimp-woocommerce-api.php CHANGED
File without changes
includes/api/class-mailchimp-woocommerce-create-list-submission.php CHANGED
File without changes
includes/api/class-mailchimp-woocommerce-transform-orders-wc3.php CHANGED
@@ -172,15 +172,24 @@ class MailChimp_WooCommerce_Transform_Orders
172
  if (!($stats = $this->getCustomerOrderTotals($order->get_user_id()))) {
173
  $stats = (object) array('count' => 0, 'total' => 0);
174
  }
 
175
  $customer->setOrdersCount($stats->count);
176
  $customer->setTotalSpent($stats->total);
177
 
178
  // we are saving the post meta for subscribers on each order... so if they have subscribed on checkout
179
  $subscriber_meta = get_post_meta($order->get_id(), 'mailchimp_woocommerce_is_subscribed', true);
180
  $subscribed_on_order = $subscriber_meta === '' ? false : (bool) $subscriber_meta;
181
-
182
  $customer->setOptInStatus($subscribed_on_order);
183
 
 
 
 
 
 
 
 
 
 
184
  return $customer;
185
  }
186
 
172
  if (!($stats = $this->getCustomerOrderTotals($order->get_user_id()))) {
173
  $stats = (object) array('count' => 0, 'total' => 0);
174
  }
175
+
176
  $customer->setOrdersCount($stats->count);
177
  $customer->setTotalSpent($stats->total);
178
 
179
  // we are saving the post meta for subscribers on each order... so if they have subscribed on checkout
180
  $subscriber_meta = get_post_meta($order->get_id(), 'mailchimp_woocommerce_is_subscribed', true);
181
  $subscribed_on_order = $subscriber_meta === '' ? false : (bool) $subscriber_meta;
 
182
  $customer->setOptInStatus($subscribed_on_order);
183
 
184
+ // if they didn't subscribe on the order, we need to check to make sure they're not already a subscriber
185
+ // if they are, we just need to make sure that we don't unsubscribe them just because they unchecked this box.
186
+ if (!$subscribed_on_order) {
187
+ try {
188
+ $subscriber = mailchimp_get_api()->member(mailchimp_get_list_id(), $customer->getEmailAddress());
189
+ $customer->setOptInStatus(($subscriber['status'] !== 'unsubscribed'));
190
+ } catch (\Exception $e) {}
191
+ }
192
+
193
  return $customer;
194
  }
195
 
includes/api/class-mailchimp-woocommerce-transform-orders.php CHANGED
@@ -49,6 +49,25 @@ class MailChimp_WooCommerce_Transform_Orders
49
  return $response;
50
  }
51
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
  /**
53
  * @param WP_Post $post
54
  * @return MailChimp_WooCommerce_Order
@@ -177,6 +196,14 @@ class MailChimp_WooCommerce_Transform_Orders
177
  $subscribed_on_order = $subscriber_meta === '' ? false : (bool) $subscriber_meta;
178
 
179
  $customer->setOptInStatus($subscribed_on_order);
 
 
 
 
 
 
 
 
180
 
181
  // use the info from the order to compile an address.
182
  $address = new MailChimp_WooCommerce_Address();
49
  return $response;
50
  }
51
 
52
+ /**
53
+ * @param WC_Order $woo
54
+ * @return array
55
+ */
56
+ protected function dates(WC_Order $woo)
57
+ {
58
+ if (method_exists($woo, 'get_date_modified')) {
59
+ $created_at = $woo->get_date_modified();
60
+ $updated_at = $woo->get_date_modified();
61
+ } elseif (property_exists($woo, 'order_date') && property_exists($woo, 'modified_date')) {
62
+ $created_at = $woo->order_date ? new \DateTime($woo->order_date) : null;
63
+ $updated_at = $woo->modified_date ? new \DateTime($woo->modified_date) : null;
64
+ } else {
65
+ $created_at = $updated_at = new \DateTime();
66
+ }
67
+
68
+ return array($created_at, $updated_at);
69
+ }
70
+
71
  /**
72
  * @param WP_Post $post
73
  * @return MailChimp_WooCommerce_Order
196
  $subscribed_on_order = $subscriber_meta === '' ? false : (bool) $subscriber_meta;
197
 
198
  $customer->setOptInStatus($subscribed_on_order);
199
+ // if they didn't subscribe on the order, we need to check to make sure they're not already a subscriber
200
+ // if they are, we just need to make sure that we don't unsubscribe them just because they unchecked this box.
201
+ if (!$subscribed_on_order) {
202
+ try {
203
+ $subscriber = mailchimp_get_api()->member(mailchimp_get_list_id(), $customer->getEmailAddress());
204
+ $customer->setOptInStatus(($subscriber['status'] !== 'unsubscribed'));
205
+ } catch (\Exception $e) {}
206
+ }
207
 
208
  // use the info from the order to compile an address.
209
  $address = new MailChimp_WooCommerce_Address();
includes/api/class-mailchimp-woocommerce-transform-products.php CHANGED
File without changes
includes/api/errors/class-mailchimp-error.php CHANGED
File without changes
includes/api/errors/class-mailchimp-server-error.php CHANGED
File without changes
includes/api/helpers/class-mailchimp-woocommerce-api-currency-codes.php CHANGED
File without changes
includes/api/helpers/class-mailchimp-woocommerce-api-locales.php CHANGED
File without changes
includes/class-mailchimp-woocommerce-activator.php CHANGED
File without changes
includes/class-mailchimp-woocommerce-deactivator.php CHANGED
File without changes
includes/class-mailchimp-woocommerce-i18n.php CHANGED
File without changes
includes/class-mailchimp-woocommerce-loader.php CHANGED
File without changes
includes/class-mailchimp-woocommerce-newsletter.php CHANGED
File without changes
includes/class-mailchimp-woocommerce-options.php CHANGED
File without changes
includes/class-mailchimp-woocommerce-service.php CHANGED
@@ -83,14 +83,9 @@ class MailChimp_Service extends MailChimp_Woocommerce_Options
83
  // expire the landing site cookie so we can rinse and repeat tracking
84
  $this->expireLandingSiteCookie();
85
 
86
- mailchimp_log('new_order', "New order #$order_id", array(
87
- 'campaign_id' => $campaign_id,
88
- 'landing_site' => $landing_site,
89
- ));
90
-
91
  // queue up the single order to be processed.
92
  $handler = new MailChimp_WooCommerce_Single_Order($order_id, null, $campaign_id, $landing_site);
93
- wp_queue($handler);
94
  }
95
  }
96
 
@@ -107,7 +102,7 @@ class MailChimp_Service extends MailChimp_Woocommerce_Options
107
  // queue up the single order to be processed.
108
  $handler = new MailChimp_WooCommerce_Single_Order($order_id, null, null, null);
109
  $handler->is_update = true;
110
- wp_queue($handler);
111
  }
112
  }
113
 
83
  // expire the landing site cookie so we can rinse and repeat tracking
84
  $this->expireLandingSiteCookie();
85
 
 
 
 
 
 
86
  // queue up the single order to be processed.
87
  $handler = new MailChimp_WooCommerce_Single_Order($order_id, null, $campaign_id, $landing_site);
88
+ wp_queue($handler, 60);
89
  }
90
  }
91
 
102
  // queue up the single order to be processed.
103
  $handler = new MailChimp_WooCommerce_Single_Order($order_id, null, null, null);
104
  $handler->is_update = true;
105
+ wp_queue($handler, 90);
106
  }
107
  }
108
 
includes/class-mailchimp-woocommerce.php CHANGED
File without changes
includes/index.php CHANGED
File without changes
includes/plugin-update-checker/README.md ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Plugin Update Checker
2
+ =====================
3
+
4
+ This is a custom update checker library for WordPress plugins. It lets you add automatic update notifications and one-click upgrades to your commercial and private plugins. All you need to do is put your plugin details in a JSON file, place the file on your server, and pass the URL to the library. The library periodically checks the URL to see if there's a new version available and displays an update notification to the user if necessary.
5
+
6
+ From the users' perspective, it works just like with plugins hosted on WordPress.org. The update checker uses the default plugin upgrade UI that will already be familiar to most WordPress users.
7
+
8
+ [See this blog post](http://w-shadow.com/blog/2010/09/02/automatic-updates-for-any-plugin/) for more information and usage instructions.
9
+
10
+ Getting Started
11
+ ---------------
12
+
13
+ ### Self-hosted Plugins
14
+
15
+ 1. Make a JSON file that describes your plugin. Here's a minimal example:
16
+
17
+ ```json
18
+ {
19
+ "name" : "My Cool Plugin",
20
+ "version" : "2.0",
21
+ "author" : "John Smith",
22
+ "download_url" : "http://example.com/plugins/my-cool-plugin.zip",
23
+ "sections" : {
24
+ "description" : "Plugin description here. You can use HTML."
25
+ }
26
+ }
27
+ ```
28
+ See [this table](https://spreadsheets.google.com/pub?key=0AqP80E74YcUWdEdETXZLcXhjd2w0cHMwX2U1eDlWTHc&authkey=CK7h9toK&hl=en&single=true&gid=0&output=html) for a full list of supported fields.
29
+ 2. Upload this file to a publicly accessible location.
30
+ 3. Download [the update checker](https://github.com/YahnisElsts/plugin-update-checker/releases/latest), unzip the archive and copy the `plugin-update-checker` directory to your plugin.
31
+ 4. Add the following code to the main plugin file:
32
+
33
+ ```php
34
+ require 'plugin-update-checker/plugin-update-checker.php';
35
+ $myUpdateChecker = PucFactory::buildUpdateChecker(
36
+ 'http://example.com/path/to/metadata.json',
37
+ __FILE__
38
+ );
39
+ ```
40
+
41
+ #### Notes
42
+ - You could use [wp-update-server](https://github.com/YahnisElsts/wp-update-server) to automatically generate JSON metadata from ZIP packages.
43
+ - The second argument passed to `buildUpdateChecker` should be the full path to the main plugin file.
44
+ - There are more options available - see the [blog](http://w-shadow.com/blog/2010/09/02/automatic-updates-for-any-plugin/) for details.
45
+
46
+ ### Plugins Hosted on GitHub
47
+
48
+ *(GitHub support is experimental.)*
49
+
50
+ 1. Download [the latest release](https://github.com/YahnisElsts/plugin-update-checker/releases/latest), unzip it and copy the `plugin-update-checker` directory to your plugin.
51
+ 2. Add the following code to the main file of your plugin:
52
+
53
+ ```php
54
+ require 'plugin-update-checker/plugin-update-checker.php';
55
+ $className = PucFactory::getLatestClassVersion('PucGitHubChecker');
56
+ $myUpdateChecker = new $className(
57
+ 'https://github.com/user-name/plugin-repo-name/',
58
+ __FILE__,
59
+ 'master'
60
+ );
61
+ ```
62
+ The third argument specifies the branch to use for updating your plugin. The default is `master`. If the branch name is omitted or set to `master`, the update checker will use the latest release or tag (if available). Otherwise it will use the specified branch.
63
+ 3. Optional: Add a `readme.txt` file formatted according to the [WordPress.org plugin readme standard](https://wordpress.org/plugins/about/readme.txt). The contents of this file will be shown when the user clicks the "View version 1.2.3 details" link.
64
+
65
+ #### Notes
66
+
67
+ If your GitHub repository requires an access token, you can specify it like this:
68
+ ```php
69
+ $myUpdateChecker->setAccessToken('your-token-here');
70
+ ```
71
+
72
+ The GitHub version of the library will pull update details from the following parts of a release/tag/branch:
73
+
74
+ - Changelog
75
+ - The "Changelog" section of `readme.txt`.
76
+ - One of the following files:
77
+ CHANGES.md, CHANGELOG.md, changes.md, changelog.md
78
+ - Release notes.
79
+ - Version number
80
+ - The "Version" plugin header.
81
+ - The latest release or tag name.
82
+ - Required and tested WordPress versions
83
+ - The "Requires at least" and "Tested up to" fields in `readme.txt`.
84
+ - The following plugin headers:
85
+ `Required WP`, `Tested WP`, `Requires at least`, `Tested up to`
86
+ - "Last updated" timestamp
87
+ - The creation timestamp of the latest release.
88
+ - The latest commit of the selected tag or branch that changed the main plugin file.
89
+ - Number of downloads
90
+ - The `download_count` statistic of the latest release.
91
+ - If you're not using GitHub releases, there will be no download stats.
92
+ - Other plugin details - author, homepage URL, description
93
+ - The "Description" section of `readme.txt`.
94
+ - Remote plugin headers (i.e. the latest version on GitHub).
95
+ - Local plugin headers (i.e. the currently installed version).
96
+ - Ratings, banners, screenshots
97
+ - Not supported.
98
+
99
+ Resources
100
+ ---------
101
+
102
+ - [Theme Update Checker](http://w-shadow.com/blog/2011/06/02/automatic-updates-for-commercial-themes/)
103
+ - [Debug Bar](https://wordpress.org/plugins/debug-bar/) - useful for testing and debugging the update checker.
104
+ - [Securing download links](http://w-shadow.com/blog/2013/03/19/plugin-updates-securing-download-links/) - a general overview.
105
+ - [A GUI for entering download credentials](http://open-tools.net/documentation/tutorial-automatic-updates.html#wordpress)
includes/plugin-update-checker/composer.json ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "yahnis-elsts/plugin-update-checker",
3
+ "type": "library",
4
+ "description": "A custom update checker for WordPress plugins. Useful if you can't host your plugin in the official WP plugin repository but still want it to support automatic plugin updates.",
5
+ "keywords": ["wordpress", "plugin updates", "automatic updates"],
6
+ "homepage": "https://github.com/YahnisElsts/plugin-update-checker/",
7
+ "license": "MIT",
8
+ "authors": [
9
+ {
10
+ "name": "Yahnis Elsts",
11
+ "email": "whiteshadow@w-shadow.com",
12
+ "homepage": "http://w-shadow.com/",
13
+ "role": "Developer"
14
+ }
15
+ ],
16
+ "require": {
17
+ "php": ">=5.2.0"
18
+ }
19
+ }
includes/plugin-update-checker/css/puc-debug-bar.css ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .puc-debug-bar-panel pre {
2
+ margin-top: 0;
3
+ }
4
+
5
+ /* Style the debug data table to match "widefat" table style used by WordPress. */
6
+ table.puc-debug-data {
7
+ width: 100%;
8
+ clear: both;
9
+ margin: 0;
10
+
11
+ border-spacing: 0;
12
+ background-color: #f9f9f9;
13
+
14
+ border-radius: 3px;
15
+ border: 1px solid #dfdfdf;
16
+ border-collapse: separate;
17
+ }
18
+
19
+ table.puc-debug-data * {
20
+ word-wrap: break-word;
21
+ }
22
+
23
+ table.puc-debug-data th {
24
+ width: 11em;
25
+ padding: 7px 7px 8px;
26
+ text-align: left;
27
+
28
+ font-family: "Georgia", "Times New Roman", "Bitstream Charter", "Times", serif;
29
+ font-weight: 400;
30
+ font-size: 14px;
31
+ line-height: 1.3em;
32
+ text-shadow: rgba(255, 255, 255, 0.804) 0 1px 0;
33
+ }
34
+
35
+ table.puc-debug-data td, table.puc-debug-data th {
36
+ border-width: 1px 0;
37
+ border-style: solid;
38
+
39
+ border-top-color: #fff;
40
+ border-bottom-color: #dfdfdf;
41
+
42
+ text-transform: none;
43
+ }
44
+
45
+ table.puc-debug-data td {
46
+ color: #555;
47
+ font-size: 12px;
48
+ padding: 4px 7px 2px;
49
+ vertical-align: top;
50
+ }
51
+
52
+ .puc-ajax-response {
53
+ border: 1px solid #dfdfdf;
54
+ border-radius: 3px;
55
+ padding: 0.5em;
56
+ margin: 5px 0;
57
+ background-color: white;
58
+ }
59
+
60
+ .puc-ajax-nonce {
61
+ display: none;
62
+ }
includes/plugin-update-checker/debug-bar-panel.php ADDED
@@ -0,0 +1,146 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ if ( !class_exists('PluginUpdateCheckerPanel_3_1', false) && class_exists('Debug_Bar_Panel', false) ) {
4
+
5
+ /**
6
+ * A Debug Bar panel for the plugin update checker.
7
+ */
8
+ class PluginUpdateCheckerPanel_3_1 extends Debug_Bar_Panel {
9
+ /** @var PluginUpdateChecker_3_1 */
10
+ private $updateChecker;
11
+
12
+ private $responseBox = '<div class="puc-ajax-response" style="display: none;"></div>';
13
+
14
+ public function __construct($updateChecker) {
15
+ $this->updateChecker = $updateChecker;
16
+ $title = sprintf(
17
+ '<span id="puc-debug-menu-link-%s">PUC (%s)</span>',
18
+ esc_attr($this->updateChecker->slug),
19
+ $this->updateChecker->slug
20
+ );
21
+ parent::Debug_Bar_Panel($title);
22
+ }
23
+
24
+ public function render() {
25
+ printf(
26
+ '<div class="puc-debug-bar-panel" id="puc-debug-bar-panel_%1$s" data-slug="%1$s" data-nonce="%2$s">',
27
+ esc_attr($this->updateChecker->slug),
28
+ esc_attr(wp_create_nonce('puc-ajax'))
29
+ );
30
+
31
+ $this->displayConfiguration();
32
+ $this->displayStatus();
33
+ $this->displayCurrentUpdate();
34
+
35
+ echo '</div>';
36
+ }
37
+
38
+ private function displayConfiguration() {
39
+ echo '<h3>Configuration</h3>';
40
+ echo '<table class="puc-debug-data">';
41
+ $this->row('Plugin file', htmlentities($this->updateChecker->pluginFile));
42
+ $this->row('Slug', htmlentities($this->updateChecker->slug));
43
+ $this->row('DB option', htmlentities($this->updateChecker->optionName));
44
+
45
+ $requestInfoButton = '';
46
+ if ( function_exists('get_submit_button') ) {
47
+ $requestInfoButton = get_submit_button('Request Info', 'secondary', 'puc-request-info-button', false, array('id' => 'puc-request-info-button-' . $this->updateChecker->slug));
48
+ }
49
+ $this->row('Metadata URL', htmlentities($this->updateChecker->metadataUrl) . ' ' . $requestInfoButton . $this->responseBox);
50
+
51
+ $scheduler = $this->updateChecker->scheduler;
52
+ if ( $scheduler->checkPeriod > 0 ) {
53
+ $this->row('Automatic checks', 'Every ' . $scheduler->checkPeriod . ' hours');
54
+ } else {
55
+ $this->row('Automatic checks', 'Disabled');
56
+ }
57
+
58
+ if ( isset($scheduler->throttleRedundantChecks) ) {
59
+ if ( $scheduler->throttleRedundantChecks && ($scheduler->checkPeriod > 0) ) {
60
+ $this->row(
61
+ 'Throttling',
62
+ sprintf(
63
+ 'Enabled. If an update is already available, check for updates every %1$d hours instead of every %2$d hours.',
64
+ $scheduler->throttledCheckPeriod,
65
+ $scheduler->checkPeriod
66
+ )
67
+ );
68
+ } else {
69
+ $this->row('Throttling', 'Disabled');
70
+ }
71
+ }
72
+ echo '</table>';
73
+ }
74
+
75
+ private function displayStatus() {
76
+ echo '<h3>Status</h3>';
77
+ echo '<table class="puc-debug-data">';
78
+ $state = $this->updateChecker->getUpdateState();
79
+ $checkNowButton = '';
80
+ if ( function_exists('get_submit_button') ) {
81
+ $checkNowButton = get_submit_button('Check Now', 'secondary', 'puc-check-now-button', false, array('id' => 'puc-check-now-button-' . $this->updateChecker->slug));
82
+ }
83
+
84
+ if ( isset($state, $state->lastCheck) ) {
85
+ $this->row('Last check', $this->formatTimeWithDelta($state->lastCheck) . ' ' . $checkNowButton . $this->responseBox);
86
+ } else {
87
+ $this->row('Last check', 'Never');
88
+ }
89
+
90
+ $nextCheck = wp_next_scheduled($this->updateChecker->scheduler->getCronHookName());
91
+ $this->row('Next automatic check', $this->formatTimeWithDelta($nextCheck));
92
+
93
+ if ( isset($state, $state->checkedVersion) ) {
94
+ $this->row('Checked version', htmlentities($state->checkedVersion));
95
+ $this->row('Cached update', $state->update);
96
+ }
97
+ $this->row('Update checker class', htmlentities(get_class($this->updateChecker)));
98
+ echo '</table>';
99
+ }
100
+
101
+ private function displayCurrentUpdate() {
102
+ $update = $this->updateChecker->getUpdate();
103
+ if ( $update !== null ) {
104
+ echo '<h3>An Update Is Available</h3>';
105
+ echo '<table class="puc-debug-data">';
106
+ $fields = array('version', 'download_url', 'slug', 'homepage', 'upgrade_notice');
107
+ foreach($fields as $field) {
108
+ $this->row(ucwords(str_replace('_', ' ', $field)), htmlentities($update->$field));
109
+ }
110
+ echo '</table>';
111
+ } else {
112
+ echo '<h3>No updates currently available</h3>';
113
+ }
114
+ }
115
+
116
+ private function formatTimeWithDelta($unixTime) {
117
+ if ( empty($unixTime) ) {
118
+ return 'Never';
119
+ }
120
+
121
+ $delta = time() - $unixTime;
122
+ $result = human_time_diff(time(), $unixTime);
123
+ if ( $delta < 0 ) {
124
+ $result = 'after ' . $result;
125
+ } else {
126
+ $result = $result . ' ago';
127
+ }
128
+ $result .= ' (' . $this->formatTimestamp($unixTime) . ')';
129
+ return $result;
130
+ }
131
+
132
+ private function formatTimestamp($unixTime) {
133
+ return gmdate('Y-m-d H:i:s', $unixTime + (get_option('gmt_offset') * 3600));
134
+ }
135
+
136
+ private function row($name, $value) {
137
+ if ( is_object($value) || is_array($value) ) {
138
+ $value = '<pre>' . htmlentities(print_r($value, true)) . '</pre>';
139
+ } else if ($value === null) {
140
+ $value = '<code>null</code>';
141
+ }
142
+ printf('<tr><th scope="row">%1$s</th> <td>%2$s</td></tr>', $name, $value);
143
+ }
144
+ }
145
+
146
+ }
includes/plugin-update-checker/debug-bar-plugin.php ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ if ( !class_exists('PucDebugBarPlugin_3_1', false) ) {
3
+
4
+ class PucDebugBarPlugin_3_1 {
5
+ /** @var PluginUpdateChecker_3_1 */
6
+ private $updateChecker;
7
+
8
+ public function __construct($updateChecker) {
9
+ $this->updateChecker = $updateChecker;
10
+
11
+ add_filter('debug_bar_panels', array($this, 'addDebugBarPanel'));
12
+ add_action('debug_bar_enqueue_scripts', array($this, 'enqueuePanelDependencies'));
13
+
14
+ add_action('wp_ajax_puc_debug_check_now', array($this, 'ajaxCheckNow'));
15
+ add_action('wp_ajax_puc_debug_request_info', array($this, 'ajaxRequestInfo'));
16
+ }
17
+
18
+ /**
19
+ * Register the PUC Debug Bar panel.
20
+ *
21
+ * @param array $panels
22
+ * @return array
23
+ */
24
+ public function addDebugBarPanel($panels) {
25
+ require_once dirname(__FILE__) . '/debug-bar-panel.php';
26
+ if ( current_user_can('update_plugins') && class_exists('PluginUpdateCheckerPanel_3_1', false) ) {
27
+ $panels[] = new PluginUpdateCheckerPanel_3_1($this->updateChecker);
28
+ }
29
+ return $panels;
30
+ }
31
+
32
+ /**
33
+ * Enqueue our Debug Bar scripts and styles.
34
+ */
35
+ public function enqueuePanelDependencies() {
36
+ wp_enqueue_style(
37
+ 'puc-debug-bar-style',
38
+ plugins_url( "/css/puc-debug-bar.css", __FILE__ ),
39
+ array('debug-bar'),
40
+ '20130927'
41
+ );
42
+
43
+ wp_enqueue_script(
44
+ 'puc-debug-bar-js',
45
+ plugins_url( "/js/debug-bar.js", __FILE__ ),
46
+ array('jquery'),
47
+ '20121026'
48
+ );
49
+ }
50
+
51
+ /**
52
+ * Run an update check and output the result. Useful for making sure that
53
+ * the update checking process works as expected.
54
+ */
55
+ public function ajaxCheckNow() {
56
+ if ( $_POST['slug'] !== $this->updateChecker->slug ) {
57
+ return;
58
+ }
59
+ $this->preAjaxReqest();
60
+ $update = $this->updateChecker->checkForUpdates();
61
+ if ( $update !== null ) {
62
+ echo "An update is available:";
63
+ echo '<pre>', htmlentities(print_r($update, true)), '</pre>';
64
+ } else {
65
+ echo 'No updates found.';
66
+ }
67
+ exit;
68
+ }
69
+
70
+ /**
71
+ * Request plugin info and output it.
72
+ */
73
+ public function ajaxRequestInfo() {
74
+ if ( $_POST['slug'] !== $this->updateChecker->slug ) {
75
+ return;
76
+ }
77
+ $this->preAjaxReqest();
78
+ $info = $this->updateChecker->requestInfo();
79
+ if ( $info !== null ) {
80
+ echo 'Successfully retrieved plugin info from the metadata URL:';
81
+ echo '<pre>', htmlentities(print_r($info, true)), '</pre>';
82
+ } else {
83
+ echo 'Failed to retrieve plugin info from the metadata URL.';
84
+ }
85
+ exit;
86
+ }
87
+
88
+ /**
89
+ * Check access permissions and enable error display (for debugging).
90
+ */
91
+ private function preAjaxReqest() {
92
+ if ( !current_user_can('update_plugins') ) {
93
+ die('Access denied');
94
+ }
95
+ check_ajax_referer('puc-ajax');
96
+
97
+ error_reporting(E_ALL);
98
+ @ini_set('display_errors','On');
99
+ }
100
+ }
101
+
102
+ }
includes/plugin-update-checker/github-checker.php ADDED
@@ -0,0 +1,458 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ if ( !class_exists('PucGitHubChecker_3_1', false) ):
4
+
5
+ class PucGitHubChecker_3_1 extends PluginUpdateChecker_3_1 {
6
+ /**
7
+ * @var string GitHub username.
8
+ */
9
+ protected $userName;
10
+ /**
11
+ * @var string GitHub repository name.
12
+ */
13
+ protected $repositoryName;
14
+
15
+ /**
16
+ * @var string Either a fully qualified repository URL, or just "user/repo-name".
17
+ */
18
+ protected $repositoryUrl;
19
+
20
+ /**
21
+ * @var string The branch to use as the latest version. Defaults to "master".
22
+ */
23
+ protected $branch;
24
+
25
+ /**
26
+ * @var string GitHub authentication token. Optional.
27
+ */
28
+ protected $accessToken;
29
+
30
+ public function __construct(
31
+ $repositoryUrl,
32
+ $pluginFile,
33
+ $branch = 'master',
34
+ $checkPeriod = 1,
35
+ $optionName = '',
36
+ $muPluginFile = ''
37
+ ) {
38
+
39
+ $this->repositoryUrl = $repositoryUrl;
40
+ $this->branch = empty($branch) ? 'master' : $branch;
41
+
42
+ $path = @parse_url($repositoryUrl, PHP_URL_PATH);
43
+ if ( preg_match('@^/?(?P<username>[^/]+?)/(?P<repository>[^/#?&]+?)/?$@', $path, $matches) ) {
44
+ $this->userName = $matches['username'];
45
+ $this->repositoryName = $matches['repository'];
46
+ } else {
47
+ throw new InvalidArgumentException('Invalid GitHub repository URL: "' . $repositoryUrl . '"');
48
+ }
49
+
50
+ parent::__construct($repositoryUrl, $pluginFile, '', $checkPeriod, $optionName, $muPluginFile);
51
+ }
52
+
53
+ /**
54
+ * Retrieve details about the latest plugin version from GitHub.
55
+ *
56
+ * @param array $unusedQueryArgs Unused.
57
+ * @return PluginInfo_3_1
58
+ */
59
+ public function requestInfo($unusedQueryArgs = array()) {
60
+ $info = new PluginInfo_3_1();
61
+ $info->filename = $this->pluginFile;
62
+ $info->slug = $this->slug;
63
+
64
+ $this->setInfoFromHeader($this->getPluginHeader(), $info);
65
+
66
+ //Figure out which reference (tag or branch) we'll use to get the latest version of the plugin.
67
+ $ref = $this->branch;
68
+ if ( $this->branch === 'master' ) {
69
+ //Use the latest release.
70
+ $release = $this->getLatestRelease();
71
+ if ( $release !== null ) {
72
+ $ref = $release->tag_name;
73
+ $info->version = ltrim($release->tag_name, 'v'); //Remove the "v" prefix from "v1.2.3".
74
+ $info->last_updated = $release->created_at;
75
+ $info->download_url = $release->zipball_url;
76
+
77
+ if ( !empty($release->body) ) {
78
+ $info->sections['changelog'] = $this->parseMarkdown($release->body);
79
+ }
80
+ if ( isset($release->assets[0]) ) {
81
+ $info->downloaded = $release->assets[0]->download_count;
82
+ }
83
+ } else {
84
+ //Failing that, use the tag with the highest version number.
85
+ $tag = $this->getLatestTag();
86
+ if ( $tag !== null ) {
87
+ $ref = $tag->name;
88
+ $info->version = $tag->name;
89
+ $info->download_url = $tag->zipball_url;
90
+ }
91
+ }
92
+ }
93
+
94
+ if ( empty($info->download_url) ) {
95
+ $info->download_url = $this->buildArchiveDownloadUrl($ref);
96
+ } else if ( !empty($this->accessToken) ) {
97
+ $info->download_url = add_query_arg('access_token', $this->accessToken, $info->download_url);
98
+ }
99
+
100
+ //Get headers from the main plugin file in this branch/tag. Its "Version" header and other metadata
101
+ //are what the WordPress install will actually see after upgrading, so they take precedence over releases/tags.
102
+ $mainPluginFile = basename($this->pluginFile);
103
+ $remotePlugin = $this->getRemoteFile($mainPluginFile, $ref);
104
+ if ( !empty($remotePlugin) ) {
105
+ $remoteHeader = $this->getFileHeader($remotePlugin);
106
+ $this->setInfoFromHeader($remoteHeader, $info);
107
+ }
108
+
109
+ //Try parsing readme.txt. If it's formatted according to WordPress.org standards, it will contain
110
+ //a lot of useful information like the required/tested WP version, changelog, and so on.
111
+ if ( $this->readmeTxtExistsLocally() ) {
112
+ $this->setInfoFromRemoteReadme($ref, $info);
113
+ }
114
+
115
+ //The changelog might be in a separate file.
116
+ if ( empty($info->sections['changelog']) ) {
117
+ $info->sections['changelog'] = $this->getRemoteChangelog($ref);
118
+ if ( empty($info->sections['changelog']) ) {
119
+ $info->sections['changelog'] = __('There is no changelog available.', 'plugin-update-checker');
120
+ }
121
+ }
122
+
123
+ if ( empty($info->last_updated) ) {
124
+ //Fetch the latest commit that changed the main plugin file and use it as the "last_updated" date.
125
+ //It's reasonable to assume that every update will change the version number in that file.
126
+ $latestCommit = $this->getLatestCommit($mainPluginFile, $ref);
127
+ if ( $latestCommit !== null ) {
128
+ $info->last_updated = $latestCommit->commit->author->date;
129
+ }
130
+ }
131
+ $info = apply_filters('puc_request_info_result-' . $this->slug, $info, null);
132
+ return $info;
133
+ }
134
+
135
+ /**
136
+ * Get the latest release from GitHub.
137
+ *
138
+ * @return StdClass|null
139
+ */
140
+ protected function getLatestRelease() {
141
+ $releases = $this->api('/repos/:user/:repo/releases');
142
+ if ( is_wp_error($releases) || !is_array($releases) || !isset($releases[0]) ) {
143
+ return null;
144
+ }
145
+
146
+ $latestRelease = $releases[0];
147
+ return $latestRelease;
148
+ }
149
+
150
+ /**
151
+ * Get the tag that looks like the highest version number.
152
+ *
153
+ * @return StdClass|null
154
+ */
155
+ protected function getLatestTag() {
156
+ $tags = $this->api('/repos/:user/:repo/tags');
157
+
158
+ if ( is_wp_error($tags) || empty($tags) || !is_array($tags) ) {
159
+ return null;
160
+ }
161
+
162
+ usort($tags, array($this, 'compareTagNames')); //Sort from highest to lowest.
163
+ return $tags[0];
164
+ }
165
+
166
+ /**
167
+ * Compare two GitHub tags as if they were version number.
168
+ *
169
+ * @param string $tag1
170
+ * @param string $tag2
171
+ * @return int
172
+ */
173
+ protected function compareTagNames($tag1, $tag2) {
174
+ if ( !isset($tag1->name) ) {
175
+ return 1;
176
+ }
177
+ if ( !isset($tag2->name) ) {
178
+ return -1;
179
+ }
180
+ return -version_compare($tag1->name, $tag2->name);
181
+ }
182
+
183
+ /**
184
+ * Get the latest commit that changed the specified file.
185
+ *
186
+ * @param string $filename
187
+ * @param string $ref Reference name (e.g. branch or tag).
188
+ * @return StdClass|null
189
+ */
190
+ protected function getLatestCommit($filename, $ref = 'master') {
191
+ $commits = $this->api(
192
+ '/repos/:user/:repo/commits',
193
+ array(
194
+ 'path' => $filename,
195
+ 'sha' => $ref,
196
+ )
197
+ );
198
+ if ( !is_wp_error($commits) && is_array($commits) && isset($commits[0]) ) {
199
+ return $commits[0];
200
+ }
201
+ return null;
202
+ }
203
+
204
+ protected function getRemoteChangelog($ref = '') {
205
+ $filename = $this->getChangelogFilename();
206
+ if ( empty($filename) ) {
207
+ return null;
208
+ }
209
+
210
+ $changelog = $this->getRemoteFile($filename, $ref);
211
+ if ( $changelog === null ) {
212
+ return null;
213
+ }
214
+ return $this->parseMarkdown($changelog);
215
+ }
216
+
217
+ protected function getChangelogFilename() {
218
+ $pluginDirectory = dirname($this->pluginAbsolutePath);
219
+ if ( empty($this->pluginAbsolutePath) || !is_dir($pluginDirectory) || ($pluginDirectory === '.') ) {
220
+ return null;
221
+ }
222
+
223
+ $possibleNames = array('CHANGES.md', 'CHANGELOG.md', 'changes.md', 'changelog.md');
224
+ $files = scandir($pluginDirectory);
225
+ $foundNames = array_intersect($possibleNames, $files);
226
+
227
+ if ( !empty($foundNames) ) {
228
+ return reset($foundNames);
229
+ }
230
+ return null;
231
+ }
232
+
233
+ /**
234
+ * Convert Markdown to HTML.
235
+ *
236
+ * @param string $markdown
237
+ * @return string
238
+ */
239
+ protected function parseMarkdown($markdown) {
240
+ if ( !class_exists('Parsedown', false) ) {
241
+ require_once(dirname(__FILE__) . '/vendor/Parsedown' . (version_compare(PHP_VERSION, '5.3.0', '>=') ? '' : 'Legacy') . '.php');
242
+ }
243
+
244
+ $instance = Parsedown::instance();
245
+ return $instance->text($markdown);
246
+ }
247
+
248
+ /**
249
+ * Perform a GitHub API request.
250
+ *
251
+ * @param string $url
252
+ * @param array $queryParams
253
+ * @return mixed|WP_Error
254
+ */
255
+ protected function api($url, $queryParams = array()) {
256
+ $variables = array(
257
+ 'user' => $this->userName,
258
+ 'repo' => $this->repositoryName,
259
+ );
260
+ foreach ($variables as $name => $value) {
261
+ $url = str_replace('/:' . $name, '/' . urlencode($value), $url);
262
+ }
263
+ $url = 'https://api.github.com' . $url;
264
+
265
+ if ( !empty($this->accessToken) ) {
266
+ $queryParams['access_token'] = $this->accessToken;
267
+ }
268
+ if ( !empty($queryParams) ) {
269
+ $url = add_query_arg($queryParams, $url);
270
+ }
271
+
272
+ $response = wp_remote_get($url, array('timeout' => 10));
273
+ if ( is_wp_error($response) ) {
274
+ return $response;
275
+ }
276
+
277
+ $code = wp_remote_retrieve_response_code($response);
278
+ $body = wp_remote_retrieve_body($response);
279
+ if ( $code === 200 ) {
280
+ $document = json_decode($body);
281
+ return $document;
282
+ }
283
+
284
+ return new WP_Error(
285
+ 'puc-github-http-error',
286
+ 'GitHub API error. HTTP status: ' . $code
287
+ );
288
+ }
289
+
290
+ /**
291
+ * Set the access token that will be used to make authenticated GitHub API requests.
292
+ *
293
+ * @param string $accessToken
294
+ */
295
+ public function setAccessToken($accessToken) {
296
+ $this->accessToken = $accessToken;
297
+ }
298
+
299
+ /**
300
+ * Get the contents of a file from a specific branch or tag.
301
+ *
302
+ * @param string $path File name.
303
+ * @param string $ref
304
+ * @return null|string Either the contents of the file, or null if the file doesn't exist or there's an error.
305
+ */
306
+ protected function getRemoteFile($path, $ref = 'master') {
307
+ $apiUrl = '/repos/:user/:repo/contents/' . $path;
308
+ $response = $this->api($apiUrl, array('ref' => $ref));
309
+
310
+ if ( is_wp_error($response) || !isset($response->content) || ($response->encoding !== 'base64') ) {
311
+ return null;
312
+ }
313
+ return base64_decode($response->content);
314
+ }
315
+
316
+ /**
317
+ * Parse plugin metadata from the header comment.
318
+ * This is basically a simplified version of the get_file_data() function from /wp-includes/functions.php.
319
+ *
320
+ * @param $content
321
+ * @return array
322
+ */
323
+ protected function getFileHeader($content) {
324
+ $headers = array(
325
+ 'Name' => 'Plugin Name',
326
+ 'PluginURI' => 'Plugin URI',
327
+ 'Version' => 'Version',
328
+ 'Description' => 'Description',
329
+ 'Author' => 'Author',
330
+ 'AuthorURI' => 'Author URI',
331
+ 'TextDomain' => 'Text Domain',
332
+ 'DomainPath' => 'Domain Path',
333
+ 'Network' => 'Network',
334
+
335
+ //The newest WordPress version that this plugin requires or has been tested with.
336
+ //We support several different formats for compatibility with other libraries.
337
+ 'Tested WP' => 'Tested WP',
338
+ 'Requires WP' => 'Requires WP',
339
+ 'Tested up to' => 'Tested up to',
340
+ 'Requires at least' => 'Requires at least',
341
+ );
342
+
343
+ $content = str_replace("\r", "\n", $content); //Normalize line endings.
344
+ $results = array();
345
+ foreach ($headers as $field => $name) {
346
+ $success = preg_match('/^[ \t\/*#@]*' . preg_quote($name, '/') . ':(.*)$/mi', $content, $matches);
347
+ if ( ($success === 1) && $matches[1] ) {
348
+ $results[$field] = _cleanup_header_comment($matches[1]);
349
+ } else {
350
+ $results[$field] = '';
351
+ }
352
+ }
353
+
354
+ return $results;
355
+ }
356
+
357
+ /**
358
+ * Copy plugin metadata from a file header to a PluginInfo object.
359
+ *
360
+ * @param array $fileHeader
361
+ * @param PluginInfo_3_1 $pluginInfo
362
+ */
363
+ protected function setInfoFromHeader($fileHeader, $pluginInfo) {
364
+ $headerToPropertyMap = array(
365
+ 'Version' => 'version',
366
+ 'Name' => 'name',
367
+ 'PluginURI' => 'homepage',
368
+ 'Author' => 'author',
369
+ 'AuthorName' => 'author',
370
+ 'AuthorURI' => 'author_homepage',
371
+
372
+ 'Requires WP' => 'requires',
373
+ 'Tested WP' => 'tested',
374
+ 'Requires at least' => 'requires',
375
+ 'Tested up to' => 'tested',
376
+ );
377
+ foreach ($headerToPropertyMap as $headerName => $property) {
378
+ if ( isset($fileHeader[$headerName]) && !empty($fileHeader[$headerName]) ) {
379
+ $pluginInfo->$property = $fileHeader[$headerName];
380
+ }
381
+ }
382
+
383
+ if ( !empty($fileHeader['Description']) ) {
384
+ $pluginInfo->sections['description'] = $fileHeader['Description'];
385
+ }
386
+ }
387
+
388
+ /**
389
+ * Copy plugin metadata from the remote readme.txt file.
390
+ *
391
+ * @param string $ref GitHub tag or branch where to look for the readme.
392
+ * @param PluginInfo_3_1 $pluginInfo
393
+ */
394
+ protected function setInfoFromRemoteReadme($ref, $pluginInfo) {
395
+ $readmeTxt = $this->getRemoteFile('readme.txt', $ref);
396
+ if ( empty($readmeTxt) ) {
397
+ return;
398
+ }
399
+
400
+ $readme = $this->parseReadme($readmeTxt);
401
+
402
+ if ( isset($readme['sections']) ) {
403
+ $pluginInfo->sections = array_merge($pluginInfo->sections, $readme['sections']);
404
+ }
405
+ if ( !empty($readme['tested_up_to']) ) {
406
+ $pluginInfo->tested = $readme['tested_up_to'];
407
+ }
408
+ if ( !empty($readme['requires_at_least']) ) {
409
+ $pluginInfo->requires = $readme['requires_at_least'];
410
+ }
411
+
412
+ if ( isset($readme['upgrade_notice'], $readme['upgrade_notice'][$pluginInfo->version]) ) {
413
+ $pluginInfo->upgrade_notice = $readme['upgrade_notice'][$pluginInfo->version];
414
+ }
415
+ }
416
+
417
+ protected function parseReadme($content) {
418
+ if ( !class_exists('PucReadmeParser', false) ) {
419
+ require_once(dirname(__FILE__) . '/vendor/readme-parser.php');
420
+ }
421
+ $parser = new PucReadmeParser();
422
+ return $parser->parse_readme_contents($content);
423
+ }
424
+
425
+ /**
426
+ * Check if the currently installed version has a readme.txt file.
427
+ *
428
+ * @return bool
429
+ */
430
+ protected function readmeTxtExistsLocally() {
431
+ $pluginDirectory = dirname($this->pluginAbsolutePath);
432
+ if ( empty($this->pluginAbsolutePath) || !is_dir($pluginDirectory) || ($pluginDirectory === '.') ) {
433
+ return false;
434
+ }
435
+ return is_file($pluginDirectory . '/readme.txt');
436
+ }
437
+
438
+ /**
439
+ * Generate a URL to download a ZIP archive of the specified branch/tag/etc.
440
+ *
441
+ * @param string $ref
442
+ * @return string
443
+ */
444
+ protected function buildArchiveDownloadUrl($ref = 'master') {
445
+ $url = sprintf(
446
+ 'https://api.github.com/repos/%1$s/%2$s/zipball/%3$s',
447
+ urlencode($this->userName),
448
+ urlencode($this->repositoryName),
449
+ urlencode($ref)
450
+ );
451
+ if ( !empty($this->accessToken) ) {
452
+ $url = add_query_arg('access_token', $this->accessToken, $url);
453
+ }
454
+ return $url;
455
+ }
456
+ }
457
+
458
+ endif;
includes/plugin-update-checker/js/debug-bar.js ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ jQuery(function($) {
2
+
3
+ function runAjaxAction(button, action) {
4
+ button = $(button);
5
+ var panel = button.closest('.puc-debug-bar-panel');
6
+ var responseBox = button.closest('td').find('.puc-ajax-response');
7
+
8
+ responseBox.text('Processing...').show();
9
+ $.post(
10
+ ajaxurl,
11
+ {
12
+ action : action,
13
+ slug : panel.data('slug'),
14
+ _wpnonce: panel.data('nonce')
15
+ },
16
+ function(data) {
17
+ responseBox.html(data);
18
+ },
19
+ 'html'
20
+ );
21
+ }
22
+
23
+ $('.puc-debug-bar-panel input[name="puc-check-now-button"]').click(function() {
24
+ runAjaxAction(this, 'puc_debug_check_now');
25
+ return false;
26
+ });
27
+
28
+ $('.puc-debug-bar-panel input[name="puc-request-info-button"]').click(function() {
29
+ runAjaxAction(this, 'puc_debug_request_info');
30
+ return false;
31
+ });
32
+
33
+
34
+ // Debug Bar uses the panel class name as part of its link and container IDs. This means we can
35
+ // end up with multiple identical IDs if more than one plugin uses the update checker library.
36
+ // Fix it by replacing the class name with the plugin slug.
37
+ var panels = $('#debug-menu-targets').find('.puc-debug-bar-panel');
38
+ panels.each(function(index) {
39
+ var panel = $(this);
40
+ var slug = panel.data('slug');
41
+ var target = panel.closest('.debug-menu-target');
42
+
43
+ //Change the panel wrapper ID.
44
+ target.attr('id', 'debug-menu-target-puc-' + slug);
45
+
46
+ //Change the menu link ID as well and point it at the new target ID.
47
+ $('#puc-debug-menu-link-' + panel.data('slug'))
48
+ .closest('.debug-menu-link')
49
+ .attr('id', 'debug-menu-link-puc-' + slug)
50
+ .attr('href', '#' + target.attr('id'));
51
+ });
52
+ });
includes/plugin-update-checker/languages/plugin-update-checker-fr_FR.mo ADDED
Binary file
includes/plugin-update-checker/languages/plugin-update-checker-fr_FR.po ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ msgid ""
2
+ msgstr ""
3
+ "Project-Id-Version: plugin-update-checker\n"
4
+ "POT-Creation-Date: 2016-02-17 14:21+0100\n"
5
+ "PO-Revision-Date: 2016-02-17 14:22+0100\n"
6
+ "Last-Translator: studio RVOLA <hello@rvola.com>\n"
7
+ "Language-Team: studio RVOLA <hello@rvola.com>\n"
8
+ "Language: fr_FR\n"
9
+ "MIME-Version: 1.0\n"
10
+ "Content-Type: text/plain; charset=UTF-8\n"
11
+ "Content-Transfer-Encoding: 8bit\n"
12
+ "X-Generator: Poedit 1.8.7\n"
13
+ "X-Poedit-Basepath: ..\n"
14
+ "Plural-Forms: nplurals=2; plural=(n > 1);\n"
15
+ "X-Poedit-SourceCharset: UTF-8\n"
16
+ "X-Poedit-KeywordsList: __;_e\n"
17
+ "X-Poedit-SearchPath-0: .\n"
18
+
19
+ #: github-checker.php:120
20
+ msgid "There is no changelog available."
21
+ msgstr "Il n’y a aucun changelog disponible."
22
+
23
+ #: plugin-update-checker.php:637
24
+ msgid "Check for updates"
25
+ msgstr "Vérifier les mises à jour"
26
+
27
+ #: plugin-update-checker.php:681
28
+ msgid "This plugin is up to date."
29
+ msgstr "Ce plugin est à jour."
30
+
31
+ #: plugin-update-checker.php:683
32
+ msgid "A new version of this plugin is available."
33
+ msgstr "Une nouvelle version de ce plugin est disponible."
34
+
35
+ #: plugin-update-checker.php:685
36
+ #, php-format
37
+ msgid "Unknown update checker status \"%s\""
38
+ msgstr "Un problème inconnu est survenu \"%s\""
includes/plugin-update-checker/languages/plugin-update-checker-hu_HU.mo ADDED
Binary file
includes/plugin-update-checker/languages/plugin-update-checker-hu_HU.po ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ msgid ""
2
+ msgstr ""
3
+ "Project-Id-Version: plugin-update-checker\n"
4
+ "POT-Creation-Date: 2016-01-11 21:23+0100\n"
5
+ "PO-Revision-Date: 2016-01-11 21:25+0100\n"
6
+ "Last-Translator: Tamás András Horváth <htomy92@gmail.com>\n"
7
+ "Language-Team: \n"
8
+ "Language: hu_HU\n"
9
+ "MIME-Version: 1.0\n"
10
+ "Content-Type: text/plain; charset=UTF-8\n"
11
+ "Content-Transfer-Encoding: 8bit\n"
12
+ "X-Generator: Poedit 1.8.6\n"
13
+ "X-Poedit-Basepath: ..\n"
14
+ "Plural-Forms: nplurals=2; plural=(n != 1);\n"
15
+ "X-Poedit-SourceCharset: UTF-8\n"
16
+ "X-Poedit-KeywordsList: __;_e\n"
17
+ "X-Poedit-SearchPath-0: .\n"
18
+
19
+ #: github-checker.php:137
20
+ msgid "There is no changelog available."
21
+ msgstr "Nem érhető el a changelog."
22
+
23
+ #: plugin-update-checker.php:852
24
+ msgid "Check for updates"
25
+ msgstr "Frissítés ellenőrzése"
26
+
27
+ #: plugin-update-checker.php:896
28
+ msgid "This plugin is up to date."
29
+ msgstr "Ez a plugin naprakész."
30
+
31
+ #: plugin-update-checker.php:898
32
+ msgid "A new version of this plugin is available."
33
+ msgstr "Új verzió érhető el a kiegészítőhöz"
34
+
35
+ #: plugin-update-checker.php:900
36
+ #, php-format
37
+ msgid "Unknown update checker status \"%s\""
38
+ msgstr "Ismeretlen a frissítés ellenőrző státusza \"%s\""
39
+
40
+ #~ msgid "Every %d hours"
41
+ #~ msgstr "Minden %d órában"
includes/plugin-update-checker/languages/plugin-update-checker.pot ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #, fuzzy
2
+ msgid ""
3
+ msgstr ""
4
+ "Project-Id-Version: plugin-update-checker\n"
5
+ "POT-Creation-Date: 2016-01-11 21:22+0100\n"
6
+ "PO-Revision-Date: 2016-01-10 20:59+0100\n"
7
+ "Last-Translator: Tamás András Horváth <htomy92@gmail.com>\n"
8
+ "Language-Team: \n"
9
+ "Language: en_US\n"
10
+ "MIME-Version: 1.0\n"
11
+ "Content-Type: text/plain; charset=UTF-8\n"
12
+ "Content-Transfer-Encoding: 8bit\n"
13
+ "X-Generator: Poedit 1.8.6\n"
14
+ "X-Poedit-Basepath: ..\n"
15
+ "Plural-Forms: nplurals=2; plural=(n != 1);\n"
16
+ "X-Poedit-SourceCharset: UTF-8\n"
17
+ "X-Poedit-KeywordsList: __;_e\n"
18
+ "X-Poedit-SearchPath-0: .\n"
19
+
20
+ #: github-checker.php:137
21
+ msgid "There is no changelog available."
22
+ msgstr ""
23
+
24
+ #: plugin-update-checker.php:852
25
+ msgid "Check for updates"
26
+ msgstr ""
27
+
28
+ #: plugin-update-checker.php:896
29
+ msgid "This plugin is up to date."
30
+ msgstr ""
31
+
32
+ #: plugin-update-checker.php:898
33
+ msgid "A new version of this plugin is available."
34
+ msgstr ""
35
+
36
+ #: plugin-update-checker.php:900
37
+ #, php-format
38
+ msgid "Unknown update checker status \"%s\""
39
+ msgstr ""
includes/plugin-update-checker/license.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
1
+ Copyright (c) 2014 Jānis Elsts
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
includes/plugin-update-checker/plugin-update-checker.php ADDED
@@ -0,0 +1,1641 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * Plugin Update Checker Library 3.1
4
+ * http://w-shadow.com/
5
+ *
6
+ * Copyright 2016 Janis Elsts
7
+ * Released under the MIT license. See license.txt for details.
8
+ */
9
+
10
+ if ( !class_exists('PluginUpdateChecker_3_1', false) ):
11
+
12
+ /**
13
+ * A custom plugin update checker.
14
+ *
15
+ * @author Janis Elsts
16
+ * @copyright 2016
17
+ * @version 3.0
18
+ * @access public
19
+ */
20
+ class PluginUpdateChecker_3_1 {
21
+ public $metadataUrl = ''; //The URL of the plugin's metadata file.
22
+ public $pluginAbsolutePath = ''; //Full path of the main plugin file.
23
+ public $pluginFile = ''; //Plugin filename relative to the plugins directory. Many WP APIs use this to identify plugins.
24
+ public $slug = ''; //Plugin slug.
25
+ public $optionName = ''; //Where to store the update info.
26
+ public $muPluginFile = ''; //For MU plugins, the plugin filename relative to the mu-plugins directory.
27
+
28
+ public $debugMode = false; //Set to TRUE to enable error reporting. Errors are raised using trigger_error()
29
+ //and should be logged to the standard PHP error log.
30
+ public $scheduler;
31
+
32
+ protected $upgraderStatus;
33
+
34
+ private $debugBarPlugin = null;
35
+ private $cachedInstalledVersion = null;
36
+
37
+ private $metadataHost = ''; //The host component of $metadataUrl.
38
+
39
+ /**
40
+ * Class constructor.
41
+ *
42
+ * @param string $metadataUrl The URL of the plugin's metadata file.
43
+ * @param string $pluginFile Fully qualified path to the main plugin file.
44
+ * @param string $slug The plugin's 'slug'. If not specified, the filename part of $pluginFile sans '.php' will be used as the slug.
45
+ * @param integer $checkPeriod How often to check for updates (in hours). Defaults to checking every 12 hours. Set to 0 to disable automatic update checks.
46
+ * @param string $optionName Where to store book-keeping info about update checks. Defaults to 'external_updates-$slug'.
47
+ * @param string $muPluginFile Optional. The plugin filename relative to the mu-plugins directory.
48
+ */
49
+ public function __construct($metadataUrl, $pluginFile, $slug = '', $checkPeriod = 12, $optionName = '', $muPluginFile = ''){
50
+ $this->metadataUrl = $metadataUrl;
51
+ $this->pluginAbsolutePath = $pluginFile;
52
+ $this->pluginFile = plugin_basename($this->pluginAbsolutePath);
53
+ $this->muPluginFile = $muPluginFile;
54
+ $this->slug = $slug;
55
+ $this->optionName = $optionName;
56
+ $this->debugMode = (bool)(constant('WP_DEBUG'));
57
+
58
+ //If no slug is specified, use the name of the main plugin file as the slug.
59
+ //For example, 'my-cool-plugin/cool-plugin.php' becomes 'cool-plugin'.
60
+ if ( empty($this->slug) ){
61
+ $this->slug = basename($this->pluginFile, '.php');
62
+ }
63
+
64
+ if ( empty($this->optionName) ){
65
+ $this->optionName = 'external_updates-' . $this->slug;
66
+ }
67
+
68
+ //Backwards compatibility: If the plugin is a mu-plugin but no $muPluginFile is specified, assume
69
+ //it's the same as $pluginFile given that it's not in a subdirectory (WP only looks in the base dir).
70
+ if ( (strpbrk($this->pluginFile, '/\\') === false) && $this->isUnknownMuPlugin() ) {
71
+ $this->muPluginFile = $this->pluginFile;
72
+ }
73
+
74
+ $this->scheduler = $this->createScheduler($checkPeriod);
75
+ $this->upgraderStatus = new PucUpgraderStatus_3_1();
76
+
77
+ $this->installHooks();
78
+ }
79
+
80
+ /**
81
+ * Create an instance of the scheduler.
82
+ *
83
+ * This is implemented as a method to make it possible for plugins to subclass the update checker
84
+ * and substitute their own scheduler.
85
+ *
86
+ * @param int $checkPeriod
87
+ * @return PucScheduler_3_1
88
+ */
89
+ protected function createScheduler($checkPeriod) {
90
+ return new PucScheduler_3_1($this, $checkPeriod);
91
+ }
92
+
93
+ /**
94
+ * Install the hooks required to run periodic update checks and inject update info
95
+ * into WP data structures.
96
+ *
97
+ * @return void
98
+ */
99
+ protected function installHooks(){
100
+ //Override requests for plugin information
101
+ add_filter('plugins_api', array($this, 'injectInfo'), 20, 3);
102
+
103
+ //Insert our update info into the update array maintained by WP.
104
+ add_filter('site_transient_update_plugins', array($this,'injectUpdate')); //WP 3.0+
105
+ add_filter('transient_update_plugins', array($this,'injectUpdate')); //WP 2.8+
106
+ add_filter('site_transient_update_plugins', array($this, 'injectTranslationUpdates'));
107
+
108
+ add_filter('plugin_row_meta', array($this, 'addCheckForUpdatesLink'), 10, 2);
109
+ add_action('admin_init', array($this, 'handleManualCheck'));
110
+ add_action('all_admin_notices', array($this, 'displayManualCheckResult'));
111
+
112
+ //Clear the version number cache when something - anything - is upgraded or WP clears the update cache.
113
+ add_filter('upgrader_post_install', array($this, 'clearCachedVersion'));
114
+ add_action('delete_site_transient_update_plugins', array($this, 'clearCachedVersion'));
115
+ //Clear translation updates when WP clears the update cache.
116
+ //This needs to be done directly because the library doesn't actually remove obsolete plugin updates,
117
+ //it just hides them (see getUpdate()). We can't do that with translations - too much disk I/O.
118
+ add_action('delete_site_transient_update_plugins', array($this, 'clearCachedTranslationUpdates'));
119
+
120
+ if ( did_action('plugins_loaded') ) {
121
+ $this->initDebugBarPanel();
122
+ } else {
123
+ add_action('plugins_loaded', array($this, 'initDebugBarPanel'));
124
+ }
125
+
126
+ //Rename the update directory to be the same as the existing directory.
127
+ add_filter('upgrader_source_selection', array($this, 'fixDirectoryName'), 10, 3);
128
+
129
+ //Enable language support (i18n).
130
+ load_plugin_textdomain('plugin-update-checker', false, plugin_basename(dirname(__FILE__)) . '/languages');
131
+
132
+ //Allow HTTP requests to the metadata URL even if it's on a local host.
133
+ $this->metadataHost = @parse_url($this->metadataUrl, PHP_URL_HOST);
134
+ add_filter('http_request_host_is_external', array($this, 'allowMetadataHost'), 10, 2);
135
+ }
136
+
137
+ /**
138
+ * Explicitly allow HTTP requests to the metadata URL.
139
+ *
140
+ * WordPress has a security feature where the HTTP API will reject all requests that are sent to
141
+ * another site hosted on the same server as the current site (IP match), a local host, or a local
142
+ * IP, unless the host exactly matches the current site.
143
+ *
144
+ * This feature is opt-in (at least in WP 4.4). Apparently some people enable it.
145
+ *
146
+ * That can be a problem when you're developing your plugin and you decide to host the update information
147
+ * on the same server as your test site. Update requests will mysteriously fail.
148
+ *
149
+ * We fix that by adding an exception for the metadata host.
150
+ *
151
+ * @param bool $allow
152
+ * @param string $host
153
+ * @return bool
154
+ */
155
+ public function allowMetadataHost($allow, $host) {
156
+ if ( strtolower($host) === strtolower($this->metadataHost) ) {
157
+ return true;
158
+ }
159
+ return $allow;
160
+ }
161
+
162
+ /**
163
+ * Retrieve plugin info from the configured API endpoint.
164
+ *
165
+ * @uses wp_remote_get()
166
+ *
167
+ * @param array $queryArgs Additional query arguments to append to the request. Optional.
168
+ * @return PluginInfo_3_1
169
+ */
170
+ public function requestInfo($queryArgs = array()){
171
+ //Query args to append to the URL. Plugins can add their own by using a filter callback (see addQueryArgFilter()).
172
+ $installedVersion = $this->getInstalledVersion();
173
+ $queryArgs['installed_version'] = ($installedVersion !== null) ? $installedVersion : '';
174
+ $queryArgs = apply_filters('puc_request_info_query_args-'.$this->slug, $queryArgs);
175
+
176
+ //Various options for the wp_remote_get() call. Plugins can filter these, too.
177
+ $options = array(
178
+ 'timeout' => 10, //seconds
179
+ 'headers' => array(
180
+ 'Accept' => 'application/json'
181
+ ),
182
+ );
183
+ $options = apply_filters('puc_request_info_options-'.$this->slug, $options);
184
+
185
+ //The plugin info should be at 'http://your-api.com/url/here/$slug/info.json'
186
+ $url = $this->metadataUrl;
187
+ if ( !empty($queryArgs) ){
188
+ $url = add_query_arg($queryArgs, $url);
189
+ }
190
+
191
+ $result = wp_remote_get(
192
+ $url,
193
+ $options
194
+ );
195
+
196
+ //Try to parse the response
197
+ $status = $this->validateApiResponse($result);
198
+ $pluginInfo = null;
199
+ if ( !is_wp_error($status) ){
200
+ $pluginInfo = PluginInfo_3_1::fromJson($result['body']);
201
+ if ( $pluginInfo !== null ) {
202
+ $pluginInfo->filename = $this->pluginFile;
203
+ $pluginInfo->slug = $this->slug;
204
+ }
205
+ } else {
206
+ $this->triggerError(
207
+ sprintf('The URL %s does not point to a valid plugin metadata file. ', $url)
208
+ . $status->get_error_message(),
209
+ E_USER_WARNING
210
+ );
211
+ }
212
+
213
+ $pluginInfo = apply_filters('puc_request_info_result-'.$this->slug, $pluginInfo, $result);
214
+ return $pluginInfo;
215
+ }
216
+
217
+ /**
218
+ * Check if $result is a successful update API response.
219
+ *
220
+ * @param array|WP_Error $result
221
+ * @return true|WP_Error
222
+ */
223
+ private function validateApiResponse($result) {
224
+ if ( is_wp_error($result) ) { /** @var WP_Error $result */
225
+ return new WP_Error($result->get_error_code(), 'WP HTTP Error: ' . $result->get_error_message());
226
+ }
227
+
228
+ if ( !isset($result['response']['code']) ) {
229
+ return new WP_Error('puc_no_response_code', 'wp_remote_get() returned an unexpected result.');
230
+ }
231
+
232
+ if ( $result['response']['code'] !== 200 ) {
233
+ return new WP_Error(
234
+ 'puc_unexpected_response_code',
235
+ 'HTTP response code is ' . $result['response']['code'] . ' (expected: 200)'
236
+ );
237
+ }
238
+
239
+ if ( empty($result['body']) ) {
240
+ return new WP_Error('puc_empty_response', 'The metadata file appears to be empty.');
241
+ }
242
+
243
+ return true;
244
+ }
245
+
246
+ /**
247
+ * Retrieve the latest update (if any) from the configured API endpoint.
248
+ *
249
+ * @uses PluginUpdateChecker::requestInfo()
250
+ *
251
+ * @return PluginUpdate_3_1 An instance of PluginUpdate, or NULL when no updates are available.
252
+ */
253
+ public function requestUpdate(){
254
+ //For the sake of simplicity, this function just calls requestInfo()
255
+ //and transforms the result accordingly.
256
+ $pluginInfo = $this->requestInfo(array('checking_for_updates' => '1'));
257
+ if ( $pluginInfo == null ){
258
+ return null;
259
+ }
260
+ $update = PluginUpdate_3_1::fromPluginInfo($pluginInfo);
261
+
262
+ //Keep only those translation updates that apply to this site.
263
+ $update->translations = $this->filterApplicableTranslations($update->translations);
264
+
265
+ return $update;
266
+ }
267
+
268
+ /**
269
+ * Filter a list of translation updates and return a new list that contains only updates
270
+ * that apply to the current site.
271
+ *
272
+ * @param array $translations
273
+ * @return array
274
+ */
275
+ private function filterApplicableTranslations($translations) {
276
+ $languages = array_flip(array_values(get_available_languages()));
277
+ $installedTranslations = wp_get_installed_translations('plugins');
278
+ if ( isset($installedTranslations[$this->slug]) ) {
279
+ $installedTranslations = $installedTranslations[$this->slug];
280
+ } else {
281
+ $installedTranslations = array();
282
+ }
283
+
284
+ $applicableTranslations = array();
285
+ foreach($translations as $translation) {
286
+ //Does it match one of the available core languages?
287
+ $isApplicable = array_key_exists($translation->language, $languages);
288
+ //Is it more recent than an already-installed translation?
289
+ if ( isset($installedTranslations[$translation->language]) ) {
290
+ $updateTimestamp = strtotime($translation->updated);
291
+ $installedTimestamp = strtotime($installedTranslations[$translation->language]['PO-Revision-Date']);
292
+ $isApplicable = $updateTimestamp > $installedTimestamp;
293
+ }
294
+
295
+ if ( $isApplicable ) {
296
+ $applicableTranslations[] = $translation;
297
+ }
298
+ }
299
+
300
+ return $applicableTranslations;
301
+ }
302
+
303
+ /**
304
+ * Get the currently installed version of the plugin.
305
+ *
306
+ * @return string Version number.
307
+ */
308
+ public function getInstalledVersion(){
309
+ if ( isset($this->cachedInstalledVersion) ) {
310
+ return $this->cachedInstalledVersion;
311
+ }
312
+
313
+ $pluginHeader = $this->getPluginHeader();
314
+ if ( isset($pluginHeader['Version']) ) {
315
+ $this->cachedInstalledVersion = $pluginHeader['Version'];
316
+ return $pluginHeader['Version'];
317
+ } else {
318
+ //This can happen if the filename points to something that is not a plugin.
319
+ $this->triggerError(
320
+ sprintf(
321
+ "Can't to read the Version header for '%s'. The filename is incorrect or is not a plugin.",
322
+ $this->pluginFile
323
+ ),
324
+ E_USER_WARNING
325
+ );
326
+ return null;
327
+ }
328
+ }
329
+
330
+ /**
331
+ * Get plugin's metadata from its file header.
332
+ *
333
+ * @return array
334
+ */
335
+ protected function getPluginHeader() {
336
+ if ( !is_file($this->pluginAbsolutePath) ) {
337
+ //This can happen if the plugin filename is wrong.
338
+ $this->triggerError(
339
+ sprintf(
340
+ "Can't to read the plugin header for '%s'. The file does not exist.",
341
+ $this->pluginFile
342
+ ),
343
+ E_USER_WARNING
344
+ );
345
+ return array();
346
+ }
347
+
348
+ if ( !function_exists('get_plugin_data') ){
349
+ /** @noinspection PhpIncludeInspection */
350
+ require_once( ABSPATH . '/wp-admin/includes/plugin.php' );
351
+ }
352
+ return get_plugin_data($this->pluginAbsolutePath, false, false);
353
+ }
354
+
355
+ /**
356
+ * Check for plugin updates.
357
+ * The results are stored in the DB option specified in $optionName.
358
+ *
359
+ * @return PluginUpdate_3_1|null
360
+ */
361
+ public function checkForUpdates(){
362
+ $installedVersion = $this->getInstalledVersion();
363
+ //Fail silently if we can't find the plugin or read its header.
364
+ if ( $installedVersion === null ) {
365
+ $this->triggerError(
366
+ sprintf('Skipping update check for %s - installed version unknown.', $this->pluginFile),
367
+ E_USER_WARNING
368
+ );
369
+ return null;
370
+ }
371
+
372
+ $state = $this->getUpdateState();
373
+ if ( empty($state) ){
374
+ $state = new stdClass;
375
+ $state->lastCheck = 0;
376
+ $state->checkedVersion = '';
377
+ $state->update = null;
378
+ }
379
+
380
+ $state->lastCheck = time();
381
+ $state->checkedVersion = $installedVersion;
382
+ $this->setUpdateState($state); //Save before checking in case something goes wrong
383
+
384
+ $state->update = $this->requestUpdate();
385
+ $this->setUpdateState($state);
386
+
387
+ return $this->getUpdate();
388
+ }
389
+
390
+ /**
391
+ * Load the update checker state from the DB.
392
+ *
393
+ * @return stdClass|null
394
+ */
395
+ public function getUpdateState() {
396
+ $state = get_site_option($this->optionName, null);
397
+ if ( empty($state) || !is_object($state)) {
398
+ $state = null;
399
+ }
400
+
401
+ if ( isset($state, $state->update) && is_object($state->update) ) {
402
+ $state->update = PluginUpdate_3_1::fromObject($state->update);
403
+ }
404
+ return $state;
405
+ }
406
+
407
+
408
+ /**
409
+ * Persist the update checker state to the DB.
410
+ *
411
+ * @param StdClass $state
412
+ * @return void
413
+ */
414
+ private function setUpdateState($state) {
415
+ if ( isset($state->update) && is_object($state->update) && method_exists($state->update, 'toStdClass') ) {
416
+ $update = $state->update; /** @var PluginUpdate_3_1 $update */
417
+ $state->update = $update->toStdClass();
418
+ }
419
+ update_site_option($this->optionName, $state);
420
+ }
421
+
422
+ /**
423
+ * Reset update checker state - i.e. last check time, cached update data and so on.
424
+ *
425
+ * Call this when your plugin is being uninstalled, or if you want to
426
+ * clear the update cache.
427
+ */
428
+ public function resetUpdateState() {
429
+ delete_site_option($this->optionName);
430
+ }
431
+
432
+ /**
433
+ * Intercept plugins_api() calls that request information about our plugin and
434
+ * use the configured API endpoint to satisfy them.
435
+ *
436
+ * @see plugins_api()
437
+ *
438
+ * @param mixed $result
439
+ * @param string $action
440
+ * @param array|object $args
441
+ * @return mixed
442
+ */
443
+ public function injectInfo($result, $action = null, $args = null){
444
+ $relevant = ($action == 'plugin_information') && isset($args->slug) && (
445
+ ($args->slug == $this->slug) || ($args->slug == dirname($this->pluginFile))
446
+ );
447
+ if ( !$relevant ) {
448
+ return $result;
449
+ }
450
+
451
+ $pluginInfo = $this->requestInfo();
452
+ $pluginInfo = apply_filters('puc_pre_inject_info-' . $this->slug, $pluginInfo);
453
+ if ( $pluginInfo ) {
454
+ return $pluginInfo->toWpFormat();
455
+ }
456
+
457
+ return $result;
458
+ }
459
+
460
+ /**
461
+ * Insert the latest update (if any) into the update list maintained by WP.
462
+ *
463
+ * @param StdClass $updates Update list.
464
+ * @return StdClass Modified update list.
465
+ */
466
+ public function injectUpdate($updates){
467
+ //Is there an update to insert?
468
+ $update = $this->getUpdate();
469
+
470
+ //No update notifications for mu-plugins unless explicitly enabled. The MU plugin file
471
+ //is usually different from the main plugin file so the update wouldn't show up properly anyway.
472
+ if ( $this->isUnknownMuPlugin() ) {
473
+ $update = null;
474
+ }
475
+
476
+ if ( !empty($update) ) {
477
+ //Let plugins filter the update info before it's passed on to WordPress.
478
+ $update = apply_filters('puc_pre_inject_update-' . $this->slug, $update);
479
+ $updates = $this->addUpdateToList($updates, $update);
480
+ } else {
481
+ //Clean up any stale update info.
482
+ $updates = $this->removeUpdateFromList($updates);
483
+ }
484
+
485
+ return $updates;
486
+ }
487
+
488
+ /**
489
+ * @param StdClass|null $updates
490
+ * @param PluginUpdate_3_1 $updateToAdd
491
+ * @return StdClass
492
+ */
493
+ private function addUpdateToList($updates, $updateToAdd) {
494
+ if ( !is_object($updates) ) {
495
+ $updates = new stdClass();
496
+ $updates->response = array();
497
+ }
498
+
499
+ $wpUpdate = $updateToAdd->toWpFormat();
500
+ $pluginFile = $this->pluginFile;
501
+
502
+ if ( $this->isMuPlugin() ) {
503
+ //WP does not support automatic update installation for mu-plugins, but we can still display a notice.
504
+ $wpUpdate->package = null;
505
+ $pluginFile = $this->muPluginFile;
506
+ }
507
+ $updates->response[$pluginFile] = $wpUpdate;
508
+ return $updates;
509
+ }
510
+
511
+ /**
512
+ * @param stdClass|null $updates
513
+ * @return stdClass|null
514
+ */
515
+ private function removeUpdateFromList($updates) {
516
+ if ( isset($updates, $updates->response) ) {
517
+ unset($updates->response[$this->pluginFile]);
518
+ if ( !empty($this->muPluginFile) ) {
519
+ unset($updates->response[$this->muPluginFile]);
520
+ }
521
+ }
522
+ return $updates;
523
+ }
524
+
525
+ /**
526
+ * Insert translation updates into the list maintained by WordPress.
527
+ *
528
+ * @param stdClass $updates
529
+ * @return stdClass
530
+ */
531
+ public function injectTranslationUpdates($updates) {
532
+ $translationUpdates = $this->getTranslationUpdates();
533
+ if ( empty($translationUpdates) ) {
534
+ return $updates;
535
+ }
536
+
537
+ //Being defensive.
538
+ if ( !is_object($updates) ) {
539
+ $updates = new stdClass();
540
+ }
541
+ if ( !isset($updates->translations) ) {
542
+ $updates->translations = array();
543
+ }
544
+
545
+ //In case there's a name collision with a plugin hosted on wordpress.org,
546
+ //remove any preexisting updates that match our plugin.
547
+ $translationType = 'plugin';
548
+ $filteredTranslations = array();
549
+ foreach($updates->translations as $translation) {
550
+ if ( ($translation['type'] === $translationType) && ($translation['slug'] === $this->slug) ) {
551
+ continue;
552
+ }
553
+ $filteredTranslations[] = $translation;
554
+ }
555
+ $updates->translations = $filteredTranslations;
556
+
557
+ //Add our updates to the list.
558
+ foreach($translationUpdates as $update) {
559
+ $convertedUpdate = array_merge(
560
+ array(
561
+ 'type' => $translationType,
562
+ 'slug' => $this->slug,
563
+ 'autoupdate' => 0,
564
+ //AFAICT, WordPress doesn't actually use the "version" field for anything.
565
+ //But lets make sure it's there, just in case.
566
+ 'version' => isset($update->version) ? $update->version : ('1.' . strtotime($update->updated)),
567
+ ),
568
+ (array)$update
569
+ );
570
+
571
+ $updates->translations[] = $convertedUpdate;
572
+ }
573
+
574
+ return $updates;
575
+ }
576
+
577
+ /**
578
+ * Rename the update directory to match the existing plugin directory.
579
+ *
580
+ * When WordPress installs a plugin or theme update, it assumes that the ZIP file will contain
581
+ * exactly one directory, and that the directory name will be the same as the directory where
582
+ * the plugin/theme is currently installed.
583
+ *
584
+ * GitHub and other repositories provide ZIP downloads, but they often use directory names like
585
+ * "project-branch" or "project-tag-hash". We need to change the name to the actual plugin folder.
586
+ *
587
+ * This is a hook callback. Don't call it from a plugin.
588
+ *
589
+ * @param string $source The directory to copy to /wp-content/plugins. Usually a subdirectory of $remoteSource.
590
+ * @param string $remoteSource WordPress has extracted the update to this directory.
591
+ * @param WP_Upgrader $upgrader
592
+ * @return string|WP_Error
593
+ */
594
+ public function fixDirectoryName($source, $remoteSource, $upgrader) {
595
+ global $wp_filesystem; /** @var WP_Filesystem_Base $wp_filesystem */
596
+
597
+ //Basic sanity checks.
598
+ if ( !isset($source, $remoteSource, $upgrader, $upgrader->skin, $wp_filesystem) ) {
599
+ return $source;
600
+ }
601
+
602
+ //If WordPress is upgrading anything other than our plugin, leave the directory name unchanged.
603
+ if ( !$this->isPluginBeingUpgraded($upgrader) ) {
604
+ return $source;
605
+ }
606
+
607
+ //Rename the source to match the existing plugin directory.
608
+ $pluginDirectoryName = dirname($this->pluginFile);
609
+ if ( $pluginDirectoryName === '.' ) {
610
+ return $source;
611
+ }
612
+ $correctedSource = trailingslashit($remoteSource) . $pluginDirectoryName . '/';
613
+ if ( $source !== $correctedSource ) {
614
+ //The update archive should contain a single directory that contains the rest of plugin files. Otherwise,
615
+ //WordPress will try to copy the entire working directory ($source == $remoteSource). We can't rename
616
+ //$remoteSource because that would break WordPress code that cleans up temporary files after update.
617
+ if ( $this->isBadDirectoryStructure($remoteSource) ) {
618
+ return new WP_Error(
619
+ 'puc-incorrect-directory-structure',
620
+ sprintf(
621
+ 'The directory structure of the update is incorrect. All plugin files should be inside ' .
622
+ 'a directory named <span class="code">%s</span>, not at the root of the ZIP file.',
623
+ htmlentities($this->slug)
624
+ )
625
+ );
626
+ }
627
+
628
+ /** @var WP_Upgrader_Skin $upgrader->skin */
629
+ $upgrader->skin->feedback(sprintf(
630
+ 'Renaming %s to %s&#8230;',
631
+ '<span class="code">' . basename($source) . '</span>',
632
+ '<span class="code">' . $pluginDirectoryName . '</span>'
633
+ ));
634
+
635
+ if ( $wp_filesystem->move($source, $correctedSource, true) ) {
636
+ $upgrader->skin->feedback('Plugin directory successfully renamed.');
637
+ return $correctedSource;
638
+ } else {
639
+ return new WP_Error(
640
+ 'puc-rename-failed',
641
+ 'Unable to rename the update to match the existing plugin directory.'
642
+ );
643
+ }
644
+ }
645
+
646
+ return $source;
647
+ }
648
+
649
+ /**
650
+ * Check for incorrect update directory structure. An update must contain a single directory,
651
+ * all other files should be inside that directory.
652
+ *
653
+ * @param string $remoteSource Directory path.
654
+ * @return bool
655
+ */
656
+ private function isBadDirectoryStructure($remoteSource) {
657
+ global $wp_filesystem; /** @var WP_Filesystem_Base $wp_filesystem */
658
+
659
+ $sourceFiles = $wp_filesystem->dirlist($remoteSource);
660
+ if ( is_array($sourceFiles) ) {
661
+ $sourceFiles = array_keys($sourceFiles);
662
+ $firstFilePath = trailingslashit($remoteSource) . $sourceFiles[0];
663
+ return (count($sourceFiles) > 1) || (!$wp_filesystem->is_dir($firstFilePath));
664
+ }
665
+
666
+ //Assume it's fine.
667
+ return false;
668
+ }
669
+
670
+ /**
671
+ * Is there and update being installed RIGHT NOW, for this specific plugin?
672
+ *
673
+ * @param WP_Upgrader|null $upgrader The upgrader that's performing the current update.
674
+ * @return bool
675
+ */
676
+ public function isPluginBeingUpgraded($upgrader = null) {
677
+ return $this->upgraderStatus->isPluginBeingUpgraded($this->pluginFile, $upgrader);
678
+ }
679
+
680
+ /**
681
+ * Get the details of the currently available update, if any.
682
+ *
683
+ * If no updates are available, or if the last known update version is below or equal
684
+ * to the currently installed version, this method will return NULL.
685
+ *
686
+ * Uses cached update data. To retrieve update information straight from
687
+ * the metadata URL, call requestUpdate() instead.
688
+ *
689
+ * @return PluginUpdate_3_1|null
690
+ */
691
+ public function getUpdate() {
692
+ $state = $this->getUpdateState(); /** @var StdClass $state */
693
+
694
+ //Is there an update available?
695
+ if ( isset($state, $state->update) ) {
696
+ $update = $state->update;
697
+ //Check if the update is actually newer than the currently installed version.
698
+ $installedVersion = $this->getInstalledVersion();
699
+ if ( ($installedVersion !== null) && version_compare($update->version, $installedVersion, '>') ){
700
+ $update->filename = $this->pluginFile;
701
+ return $update;
702
+ }
703
+ }
704
+ return null;
705
+ }
706
+
707
+ /**
708
+ * Get a list of available translation updates.
709
+ *
710
+ * This method will return an empty array if there are no updates.
711
+ * Uses cached update data.
712
+ *
713
+ * @return array
714
+ */
715
+ public function getTranslationUpdates() {
716
+ $state = $this->getUpdateState();
717
+ if ( isset($state, $state->update, $state->update->translations) ) {
718
+ return $state->update->translations;
719
+ }
720
+ return array();
721
+ }
722
+
723
+ /**
724
+ * Remove all cached translation updates.
725
+ *
726
+ * @see wp_clean_update_cache
727
+ */
728
+ public function clearCachedTranslationUpdates() {
729
+ $state = $this->getUpdateState();
730
+ if ( isset($state, $state->update, $state->update->translations) ) {
731
+ $state->update->translations = array();
732
+ $this->setUpdateState($state);
733
+ }
734
+ }
735
+
736
+ /**
737
+ * Add a "Check for updates" link to the plugin row in the "Plugins" page. By default,
738
+ * the new link will appear after the "Visit plugin site" link.
739
+ *
740
+ * You can change the link text by using the "puc_manual_check_link-$slug" filter.
741
+ * Returning an empty string from the filter will disable the link.
742
+ *
743
+ * @param array $pluginMeta Array of meta links.
744
+ * @param string $pluginFile
745
+ * @return array
746
+ */
747
+ public function addCheckForUpdatesLink($pluginMeta, $pluginFile) {
748
+ $isRelevant = ($pluginFile == $this->pluginFile)
749
+ || (!empty($this->muPluginFile) && $pluginFile == $this->muPluginFile);
750
+
751
+ if ( $isRelevant && current_user_can('update_plugins') ) {
752
+ $linkUrl = wp_nonce_url(
753
+ add_query_arg(
754
+ array(
755
+ 'puc_check_for_updates' => 1,
756
+ 'puc_slug' => $this->slug,
757
+ ),
758
+ self_admin_url('plugins.php')
759
+ ),
760
+ 'puc_check_for_updates'
761
+ );
762
+
763
+ $linkText = apply_filters('puc_manual_check_link-' . $this->slug, __('Check for updates', 'plugin-update-checker'));
764
+ if ( !empty($linkText) ) {
765
+ $pluginMeta[] = sprintf('<a href="%s">%s</a>', esc_attr($linkUrl), $linkText);
766
+ }
767
+ }
768
+ return $pluginMeta;
769
+ }
770
+
771
+ /**
772
+ * Check for updates when the user clicks the "Check for updates" link.
773
+ * @see self::addCheckForUpdatesLink()
774
+ *
775
+ * @return void
776
+ */
777
+ public function handleManualCheck() {
778
+ $shouldCheck =
779
+ isset($_GET['puc_check_for_updates'], $_GET['puc_slug'])
780
+ && $_GET['puc_slug'] == $this->slug
781
+ && current_user_can('update_plugins')
782
+ && check_admin_referer('puc_check_for_updates');
783
+
784
+ if ( $shouldCheck ) {
785
+ $update = $this->checkForUpdates();
786
+ $status = ($update === null) ? 'no_update' : 'update_available';
787
+ wp_redirect(add_query_arg(
788
+ array(
789
+ 'puc_update_check_result' => $status,
790
+ 'puc_slug' => $this->slug,
791
+ ),
792
+ self_admin_url('plugins.php')
793
+ ));
794
+ }
795
+ }
796
+
797
+ /**
798
+ * Display the results of a manual update check.
799
+ * @see self::handleManualCheck()
800
+ *
801
+ * You can change the result message by using the "puc_manual_check_message-$slug" filter.
802
+ */
803
+ public function displayManualCheckResult() {
804
+ if ( isset($_GET['puc_update_check_result'], $_GET['puc_slug']) && ($_GET['puc_slug'] == $this->slug) ) {
805
+ $status = strval($_GET['puc_update_check_result']);
806
+ if ( $status == 'no_update' ) {
807
+ $message = __('This plugin is up to date.', 'plugin-update-checker');
808
+ } else if ( $status == 'update_available' ) {
809
+ $message = __('A new version of this plugin is available.', 'plugin-update-checker');
810
+ } else {
811
+ $message = sprintf(__('Unknown update checker status "%s"', 'plugin-update-checker'), htmlentities($status));
812
+ }
813
+ printf(
814
+ '<div class="updated notice is-dismissible"><p>%s</p></div>',
815
+ apply_filters('puc_manual_check_message-' . $this->slug, $message, $status)
816
+ );
817
+ }
818
+ }
819
+
820
+ /**
821
+ * Check if the plugin file is inside the mu-plugins directory.
822
+ *
823
+ * @return bool
824
+ */
825
+ protected function isMuPlugin() {
826
+ static $cachedResult = null;
827
+
828
+ if ( $cachedResult === null ) {
829
+ //Convert both paths to the canonical form before comparison.
830
+ $muPluginDir = realpath(WPMU_PLUGIN_DIR);
831
+ $pluginPath = realpath($this->pluginAbsolutePath);
832
+
833
+ $cachedResult = (strpos($pluginPath, $muPluginDir) === 0);
834
+ }
835
+
836
+ return $cachedResult;
837
+ }
838
+
839
+ /**
840
+ * MU plugins are partially supported, but only when we know which file in mu-plugins
841
+ * corresponds to this plugin.
842
+ *
843
+ * @return bool
844
+ */
845
+ protected function isUnknownMuPlugin() {
846
+ return empty($this->muPluginFile) && $this->isMuPlugin();
847
+ }
848
+
849
+ /**
850
+ * Clear the cached plugin version. This method can be set up as a filter (hook) and will
851
+ * return the filter argument unmodified.
852
+ *
853
+ * @param mixed $filterArgument
854
+ * @return mixed
855
+ */
856
+ public function clearCachedVersion($filterArgument = null) {
857
+ $this->cachedInstalledVersion = null;
858
+ return $filterArgument;
859
+ }
860
+
861
+ /**
862
+ * Register a callback for filtering query arguments.
863
+ *
864
+ * The callback function should take one argument - an associative array of query arguments.
865
+ * It should return a modified array of query arguments.
866
+ *
867
+ * @uses add_filter() This method is a convenience wrapper for add_filter().
868
+ *
869
+ * @param callable $callback
870
+ * @return void
871
+ */
872
+ public function addQueryArgFilter($callback){
873
+ add_filter('puc_request_info_query_args-'.$this->slug, $callback);
874
+ }
875
+
876
+ /**
877
+ * Register a callback for filtering arguments passed to wp_remote_get().
878
+ *
879
+ * The callback function should take one argument - an associative array of arguments -
880
+ * and return a modified array or arguments. See the WP documentation on wp_remote_get()
881
+ * for details on what arguments are available and how they work.
882
+ *
883
+ * @uses add_filter() This method is a convenience wrapper for add_filter().
884
+ *
885
+ * @param callable $callback
886
+ * @return void
887
+ */
888
+ public function addHttpRequestArgFilter($callback){
889
+ add_filter('puc_request_info_options-'.$this->slug, $callback);
890
+ }
891
+
892
+ /**
893
+ * Register a callback for filtering the plugin info retrieved from the external API.
894
+ *
895
+ * The callback function should take two arguments. If the plugin info was retrieved
896
+ * successfully, the first argument passed will be an instance of PluginInfo. Otherwise,
897
+ * it will be NULL. The second argument will be the corresponding return value of
898
+ * wp_remote_get (see WP docs for details).
899
+ *
900
+ * The callback function should return a new or modified instance of PluginInfo or NULL.
901
+ *
902
+ * @uses add_filter() This method is a convenience wrapper for add_filter().
903
+ *
904
+ * @param callable $callback
905
+ * @return void
906
+ */
907
+ public function addResultFilter($callback){
908
+ add_filter('puc_request_info_result-'.$this->slug, $callback, 10, 2);
909
+ }
910
+
911
+ /**
912
+ * Register a callback for one of the update checker filters.
913
+ *
914
+ * Identical to add_filter(), except it automatically adds the "puc_" prefix
915
+ * and the "-$plugin_slug" suffix to the filter name. For example, "request_info_result"
916
+ * becomes "puc_request_info_result-your_plugin_slug".
917
+ *
918
+ * @param string $tag
919
+ * @param callable $callback
920
+ * @param int $priority
921
+ * @param int $acceptedArgs
922
+ */
923
+ public function addFilter($tag, $callback, $priority = 10, $acceptedArgs = 1) {
924
+ add_filter('puc_' . $tag . '-' . $this->slug, $callback, $priority, $acceptedArgs);
925
+ }
926
+
927
+ /**
928
+ * Initialize the update checker Debug Bar plugin/add-on thingy.
929
+ */
930
+ public function initDebugBarPanel() {
931
+ $debugBarPlugin = dirname(__FILE__) . '/debug-bar-plugin.php';
932
+ if ( class_exists('Debug_Bar', false) && file_exists($debugBarPlugin) ) {
933
+ /** @noinspection PhpIncludeInspection */
934
+ require_once $debugBarPlugin;
935
+ $this->debugBarPlugin = new PucDebugBarPlugin_3_1($this);
936
+ }
937
+ }
938
+
939
+ /**
940
+ * Trigger a PHP error, but only when $debugMode is enabled.
941
+ *
942
+ * @param string $message
943
+ * @param int $errorType
944
+ */
945
+ protected function triggerError($message, $errorType) {
946
+ if ( $this->debugMode ) {
947
+ trigger_error($message, $errorType);
948
+ }
949
+ }
950
+ }
951
+
952
+ endif;
953
+
954
+ if ( !class_exists('PluginInfo_3_1', false) ):
955
+
956
+ /**
957
+ * A container class for holding and transforming various plugin metadata.
958
+ *
959
+ * @author Janis Elsts
960
+ * @copyright 2016
961
+ * @version 3.0
962
+ * @access public
963
+ */
964
+ class PluginInfo_3_1 {
965
+ //Most fields map directly to the contents of the plugin's info.json file.
966
+ //See the relevant docs for a description of their meaning.
967
+ public $name;
968
+ public $slug;
969
+ public $version;
970
+ public $homepage;
971
+ public $sections = array();
972
+ public $banners;
973
+ public $translations = array();
974
+ public $download_url;
975
+
976
+ public $author;
977
+ public $author_homepage;
978
+
979
+ public $requires;
980
+ public $tested;
981
+ public $upgrade_notice;
982
+
983
+ public $rating;
984
+ public $num_ratings;
985
+ public $downloaded;
986
+ public $active_installs;
987
+ public $last_updated;
988
+
989
+ public $id = 0; //The native WP.org API returns numeric plugin IDs, but they're not used for anything.
990
+
991
+ public $filename; //Plugin filename relative to the plugins directory.
992
+
993
+ /**
994
+ * Create a new instance of PluginInfo from JSON-encoded plugin info
995
+ * returned by an external update API.
996
+ *
997
+ * @param string $json Valid JSON string representing plugin info.
998
+ * @return PluginInfo_3_1|null New instance of PluginInfo, or NULL on error.
999
+ */
1000
+ public static function fromJson($json){
1001
+ /** @var StdClass $apiResponse */
1002
+ $apiResponse = json_decode($json);
1003
+ if ( empty($apiResponse) || !is_object($apiResponse) ){
1004
+ trigger_error(
1005
+ "Failed to parse plugin metadata. Try validating your .json file with http://jsonlint.com/",
1006
+ E_USER_NOTICE
1007
+ );
1008
+ return null;
1009
+ }
1010
+
1011
+ $valid = self::validateMetadata($apiResponse);
1012
+ if ( is_wp_error($valid) ){
1013
+ trigger_error($valid->get_error_message(), E_USER_NOTICE);
1014
+ return null;
1015
+ }
1016
+
1017
+ $info = new self();
1018
+ foreach(get_object_vars($apiResponse) as $key => $value){
1019
+ $info->$key = $value;
1020
+ }
1021
+
1022
+ //json_decode decodes assoc. arrays as objects. We want it as an array.
1023
+ $info->sections = (array)$info->sections;
1024
+
1025
+ return $info;
1026
+ }
1027
+
1028
+ /**
1029
+ * Very, very basic validation.
1030
+ *
1031
+ * @param StdClass $apiResponse
1032
+ * @return bool|WP_Error
1033
+ */
1034
+ protected static function validateMetadata($apiResponse) {
1035
+ if (
1036
+ !isset($apiResponse->name, $apiResponse->version)
1037
+ || empty($apiResponse->name)
1038
+ || empty($apiResponse->version)
1039
+ ) {
1040
+ return new WP_Error(
1041
+ 'puc-invalid-metadata',
1042
+ "The plugin metadata file does not contain the required 'name' and/or 'version' keys."
1043
+ );
1044
+ }
1045
+ return true;
1046
+ }
1047
+
1048
+
1049
+ /**
1050
+ * Transform plugin info into the format used by the native WordPress.org API
1051
+ *
1052
+ * @return object
1053
+ */
1054
+ public function toWpFormat(){
1055
+ $info = new stdClass;
1056
+
1057
+ //The custom update API is built so that many fields have the same name and format
1058
+ //as those returned by the native WordPress.org API. These can be assigned directly.
1059
+ $sameFormat = array(
1060
+ 'name', 'slug', 'version', 'requires', 'tested', 'rating', 'upgrade_notice',
1061
+ 'num_ratings', 'downloaded', 'active_installs', 'homepage', 'last_updated',
1062
+ );
1063
+ foreach($sameFormat as $field){
1064
+ if ( isset($this->$field) ) {
1065
+ $info->$field = $this->$field;
1066
+ } else {
1067
+ $info->$field = null;
1068
+ }
1069
+ }
1070
+
1071
+ //Other fields need to be renamed and/or transformed.
1072
+ $info->download_link = $this->download_url;
1073
+ $info->author = $this->getFormattedAuthor();
1074
+ $info->sections = array_merge(array('description' => ''), $this->sections);
1075
+
1076
+ if ( !empty($this->banners) ) {
1077
+ //WP expects an array with two keys: "high" and "low". Both are optional.
1078
+ //Docs: https://wordpress.org/plugins/about/faq/#banners
1079
+ $info->banners = is_object($this->banners) ? get_object_vars($this->banners) : $this->banners;
1080
+ $info->banners = array_intersect_key($info->banners, array('high' => true, 'low' => true));
1081
+ }
1082
+
1083
+ return $info;
1084
+ }
1085
+
1086
+ protected function getFormattedAuthor() {
1087
+ if ( !empty($this->author_homepage) ){
1088
+ return sprintf('<a href="%s">%s</a>', $this->author_homepage, $this->author);
1089
+ }
1090
+ return $this->author;
1091
+ }
1092
+ }
1093
+
1094
+ endif;
1095
+
1096
+ if ( !class_exists('PluginUpdate_3_1', false) ):
1097
+
1098
+ /**
1099
+ * A simple container class for holding information about an available update.
1100
+ *
1101
+ * @author Janis Elsts
1102
+ * @copyright 2016
1103
+ * @version 3.0
1104
+ * @access public
1105
+ */
1106
+ class PluginUpdate_3_1 {
1107
+ public $id = 0;
1108
+ public $slug;
1109
+ public $version;
1110
+ public $homepage;
1111
+ public $download_url;
1112
+ public $upgrade_notice;
1113
+ public $tested;
1114
+ public $translations = array();
1115
+ public $filename; //Plugin filename relative to the plugins directory.
1116
+
1117
+ private static $fields = array(
1118
+ 'id', 'slug', 'version', 'homepage', 'tested',
1119
+ 'download_url', 'upgrade_notice', 'filename',
1120
+ 'translations'
1121
+ );
1122
+
1123
+ /**
1124
+ * Create a new instance of PluginUpdate from its JSON-encoded representation.
1125
+ *
1126
+ * @param string $json
1127
+ * @return PluginUpdate_3_1|null
1128
+ */
1129
+ public static function fromJson($json){
1130
+ //Since update-related information is simply a subset of the full plugin info,
1131
+ //we can parse the update JSON as if it was a plugin info string, then copy over
1132
+ //the parts that we care about.
1133
+ $pluginInfo = PluginInfo_3_1::fromJson($json);
1134
+ if ( $pluginInfo != null ) {
1135
+ return self::fromPluginInfo($pluginInfo);
1136
+ } else {
1137
+ return null;
1138
+ }
1139
+ }
1140
+
1141
+ /**
1142
+ * Create a new instance of PluginUpdate based on an instance of PluginInfo.
1143
+ * Basically, this just copies a subset of fields from one object to another.
1144
+ *
1145
+ * @param PluginInfo_3_1 $info
1146
+ * @return PluginUpdate_3_1
1147
+ */
1148
+ public static function fromPluginInfo($info){
1149
+ return self::fromObject($info);
1150
+ }
1151
+
1152
+ /**
1153
+ * Create a new instance of PluginUpdate by copying the necessary fields from
1154
+ * another object.
1155
+ *
1156
+ * @param StdClass|PluginInfo_3_1|PluginUpdate_3_1 $object The source object.
1157
+ * @return PluginUpdate_3_1 The new copy.
1158
+ */
1159
+ public static function fromObject($object) {
1160
+ $update = new self();
1161
+ $fields = self::$fields;
1162
+ if ( !empty($object->slug) ) {
1163
+ $fields = apply_filters('puc_retain_fields-' . $object->slug, $fields);
1164
+ }
1165
+ foreach($fields as $field){
1166
+ if (property_exists($object, $field)) {
1167
+ $update->$field = $object->$field;
1168
+ }
1169
+ }
1170
+ return $update;
1171
+ }
1172
+
1173
+ /**
1174
+ * Create an instance of StdClass that can later be converted back to
1175
+ * a PluginUpdate. Useful for serialization and caching, as it avoids
1176
+ * the "incomplete object" problem if the cached value is loaded before
1177
+ * this class.
1178
+ *
1179
+ * @return StdClass
1180
+ */
1181
+ public function toStdClass() {
1182
+ $object = new stdClass();
1183
+ $fields = self::$fields;
1184
+ if ( !empty($this->slug) ) {
1185
+ $fields = apply_filters('puc_retain_fields-' . $this->slug, $fields);
1186
+ }
1187
+ foreach($fields as $field){
1188
+ if (property_exists($this, $field)) {
1189
+ $object->$field = $this->$field;
1190
+ }
1191
+ }
1192
+ return $object;
1193
+ }
1194
+
1195
+
1196
+ /**
1197
+ * Transform the update into the format used by WordPress native plugin API.
1198
+ *
1199
+ * @return object
1200
+ */
1201
+ public function toWpFormat(){
1202
+ $update = new stdClass;
1203
+
1204
+ $update->id = $this->id;
1205
+ $update->slug = $this->slug;
1206
+ $update->new_version = $this->version;
1207
+ $update->url = $this->homepage;
1208
+ $update->package = $this->download_url;
1209
+ $update->tested = $this->tested;
1210
+ $update->plugin = $this->filename;
1211
+
1212
+ if ( !empty($this->upgrade_notice) ){
1213
+ $update->upgrade_notice = $this->upgrade_notice;
1214
+ }
1215
+
1216
+ return $update;
1217
+ }
1218
+ }
1219
+
1220
+ endif;
1221
+
1222
+ if ( !class_exists('PucScheduler_3_1', false) ):
1223
+
1224
+ /**
1225
+ * The scheduler decides when and how often to check for updates.
1226
+ * It calls @see PluginUpdateChecker::checkForUpdates() to perform the actual checks.
1227
+ *
1228
+ * @version 3.0
1229
+ */
1230
+ class PucScheduler_3_1 {
1231
+ public $checkPeriod = 12; //How often to check for updates (in hours).
1232
+ public $throttleRedundantChecks = false; //Check less often if we already know that an update is available.
1233
+ public $throttledCheckPeriod = 72;
1234
+
1235
+ /**
1236
+ * @var PluginUpdateChecker_3_1
1237
+ */
1238
+ protected $updateChecker;
1239
+
1240
+ private $cronHook = null;
1241
+
1242
+ /**
1243
+ * Scheduler constructor.
1244
+ *
1245
+ * @param PluginUpdateChecker_3_1 $updateChecker
1246
+ * @param int $checkPeriod How often to check for updates (in hours).
1247
+ */
1248
+ public function __construct($updateChecker, $checkPeriod) {
1249
+ $this->updateChecker = $updateChecker;
1250
+ $this->checkPeriod = $checkPeriod;
1251
+
1252
+ //Set up the periodic update checks
1253
+ $this->cronHook = 'check_plugin_updates-' . $this->updateChecker->slug;
1254
+ if ( $this->checkPeriod > 0 ){
1255
+
1256
+ //Trigger the check via Cron.
1257
+ //Try to use one of the default schedules if possible as it's less likely to conflict
1258
+ //with other plugins and their custom schedules.
1259
+ $defaultSchedules = array(
1260
+ 1 => 'hourly',
1261
+ 12 => 'twicedaily',
1262
+ 24 => 'daily',
1263
+ );
1264
+ if ( array_key_exists($this->checkPeriod, $defaultSchedules) ) {
1265
+ $scheduleName = $defaultSchedules[$this->checkPeriod];
1266
+ } else {
1267
+ //Use a custom cron schedule.
1268
+ $scheduleName = 'every' . $this->checkPeriod . 'hours';
1269
+ add_filter('cron_schedules', array($this, '_addCustomSchedule'));
1270
+ }
1271
+
1272
+ if ( !wp_next_scheduled($this->cronHook) && !defined('WP_INSTALLING') ) {
1273
+ wp_schedule_event(time(), $scheduleName, $this->cronHook);
1274
+ }
1275
+ add_action($this->cronHook, array($this, 'maybeCheckForUpdates'));
1276
+
1277
+ register_deactivation_hook($this->updateChecker->pluginFile, array($this, '_removeUpdaterCron'));
1278
+
1279
+ //In case Cron is disabled or unreliable, we also manually trigger
1280
+ //the periodic checks while the user is browsing the Dashboard.
1281
+ add_action( 'admin_init', array($this, 'maybeCheckForUpdates') );
1282
+
1283
+ //Like WordPress itself, we check more often on certain pages.
1284
+ /** @see wp_update_plugins */
1285
+ add_action('load-update-core.php', array($this, 'maybeCheckForUpdates'));
1286
+ add_action('load-plugins.php', array($this, 'maybeCheckForUpdates'));
1287
+ add_action('load-update.php', array($this, 'maybeCheckForUpdates'));
1288
+ //This hook fires after a bulk update is complete.
1289
+ add_action('upgrader_process_complete', array($this, 'maybeCheckForUpdates'), 11, 0);
1290
+
1291
+ } else {
1292
+ //Periodic checks are disabled.
1293
+ wp_clear_scheduled_hook($this->cronHook);
1294
+ }
1295
+ }
1296
+
1297
+ /**
1298
+ * Check for updates if the configured check interval has already elapsed.
1299
+ * Will use a shorter check interval on certain admin pages like "Dashboard -> Updates" or when doing cron.
1300
+ *
1301
+ * You can override the default behaviour by using the "puc_check_now-$slug" filter.
1302
+ * The filter callback will be passed three parameters:
1303
+ * - Current decision. TRUE = check updates now, FALSE = don't check now.
1304
+ * - Last check time as a Unix timestamp.
1305
+ * - Configured check period in hours.
1306
+ * Return TRUE to check for updates immediately, or FALSE to cancel.
1307
+ *
1308
+ * This method is declared public because it's a hook callback. Calling it directly is not recommended.
1309
+ */
1310
+ public function maybeCheckForUpdates(){
1311
+ if ( empty($this->checkPeriod) ){
1312
+ return;
1313
+ }
1314
+
1315
+ $state = $this->updateChecker->getUpdateState();
1316
+ $shouldCheck =
1317
+ empty($state) ||
1318
+ !isset($state->lastCheck) ||
1319
+ ( (time() - $state->lastCheck) >= $this->getEffectiveCheckPeriod() );
1320
+
1321
+ //Let plugin authors substitute their own algorithm.
1322
+ $shouldCheck = apply_filters(
1323
+ 'puc_check_now-' . $this->updateChecker->slug,
1324
+ $shouldCheck,
1325
+ (!empty($state) && isset($state->lastCheck)) ? $state->lastCheck : 0,
1326
+ $this->checkPeriod
1327
+ );
1328
+
1329
+ if ( $shouldCheck ) {
1330
+ $this->updateChecker->checkForUpdates();
1331
+ }
1332
+ }
1333
+
1334
+ /**
1335
+ * Calculate the actual check period based on the current status and environment.
1336
+ *
1337
+ * @return int Check period in seconds.
1338
+ */
1339
+ protected function getEffectiveCheckPeriod() {
1340
+ $currentFilter = current_filter();
1341
+ if ( in_array($currentFilter, array('load-update-core.php', 'upgrader_process_complete')) ) {
1342
+ //Check more often when the user visits "Dashboard -> Updates" or does a bulk update.
1343
+ $period = 60;
1344
+ } else if ( in_array($currentFilter, array('load-plugins.php', 'load-update.php')) ) {
1345
+ //Also check more often on the "Plugins" page and /wp-admin/update.php.
1346
+ $period = 3600;
1347
+ } else if ( $this->throttleRedundantChecks && ($this->updateChecker->getUpdate() !== null) ) {
1348
+ //Check less frequently if it's already known that an update is available.
1349
+ $period = $this->throttledCheckPeriod * 3600;
1350
+ } else if ( defined('DOING_CRON') && constant('DOING_CRON') ) {
1351
+ //WordPress cron schedules are not exact, so lets do an update check even
1352
+ //if slightly less than $checkPeriod hours have elapsed since the last check.
1353
+ $cronFuzziness = 20 * 60;
1354
+ $period = $this->checkPeriod * 3600 - $cronFuzziness;
1355
+ } else {
1356
+ $period = $this->checkPeriod * 3600;
1357
+ }
1358
+
1359
+ return $period;
1360
+ }
1361
+
1362
+ /**
1363
+ * Add our custom schedule to the array of Cron schedules used by WP.
1364
+ *
1365
+ * @param array $schedules
1366
+ * @return array
1367
+ */
1368
+ public function _addCustomSchedule($schedules){
1369
+ if ( $this->checkPeriod && ($this->checkPeriod > 0) ){
1370
+ $scheduleName = 'every' . $this->checkPeriod . 'hours';
1371
+ $schedules[$scheduleName] = array(
1372
+ 'interval' => $this->checkPeriod * 3600,
1373
+ 'display' => sprintf('Every %d hours', $this->checkPeriod),
1374
+ );
1375
+ }
1376
+ return $schedules;
1377
+ }
1378
+
1379
+ /**
1380
+ * Remove the scheduled cron event that the library uses to check for updates.
1381
+ *
1382
+ * @return void
1383
+ */
1384
+ public function _removeUpdaterCron(){
1385
+ wp_clear_scheduled_hook($this->cronHook);
1386
+ }
1387
+
1388
+ /**
1389
+ * Get the name of the update checker's WP-cron hook. Mostly useful for debugging.
1390
+ *
1391
+ * @return string
1392
+ */
1393
+ public function getCronHookName() {
1394
+ return $this->cronHook;
1395
+ }
1396
+ }
1397
+
1398
+ endif;
1399
+
1400
+
1401
+ if ( !class_exists('PucUpgraderStatus_3_1', false) ):
1402
+
1403
+ /**
1404
+ * A utility class that helps figure out which plugin WordPress is upgrading.
1405
+ *
1406
+ * It may seem strange to have an separate class just for that, but the task is surprisingly complicated.
1407
+ * Core classes like Plugin_Upgrader don't expose the plugin file name during an in-progress update (AFAICT).
1408
+ * This class uses a few workarounds and heuristics to get the file name.
1409
+ */
1410
+ class PucUpgraderStatus_3_1 {
1411
+ private $upgradedPluginFile = null; //The plugin that is currently being upgraded by WordPress.
1412
+
1413
+ public function __construct() {
1414
+ //Keep track of which plugin WordPress is currently upgrading.
1415
+ add_filter('upgrader_pre_install', array($this, 'setUpgradedPlugin'), 10, 2);
1416
+ add_filter('upgrader_package_options', array($this, 'setUpgradedPluginFromOptions'), 10, 1);
1417
+ add_filter('upgrader_post_install', array($this, 'clearUpgradedPlugin'), 10, 1);
1418
+ add_action('upgrader_process_complete', array($this, 'clearUpgradedPlugin'), 10, 1);
1419
+ }
1420
+
1421
+ /**
1422
+ * Is there and update being installed RIGHT NOW, for a specific plugin?
1423
+ *
1424
+ * Caution: This method is unreliable. WordPress doesn't make it easy to figure out what it is upgrading,
1425
+ * and upgrader implementations are liable to change without notice.
1426
+ *
1427
+ * @param string $pluginFile The plugin to check.
1428
+ * @param WP_Upgrader|null $upgrader The upgrader that's performing the current update.
1429
+ * @return bool True if the plugin identified by $pluginFile is being upgraded.
1430
+ */
1431
+ public function isPluginBeingUpgraded($pluginFile, $upgrader = null) {
1432
+ if ( isset($upgrader) ) {
1433
+ $upgradedPluginFile = $this->getPluginBeingUpgradedBy($upgrader);
1434
+ if ( !empty($upgradedPluginFile) ) {
1435
+ $this->upgradedPluginFile = $upgradedPluginFile;
1436
+ }
1437
+ }
1438
+ return ( !empty($this->upgradedPluginFile) && ($this->upgradedPluginFile === $pluginFile) );
1439
+ }
1440
+
1441
+ /**
1442
+ * Get the file name of the plugin that's currently being upgraded.
1443
+ *
1444
+ * @param Plugin_Upgrader|WP_Upgrader $upgrader
1445
+ * @return string|null
1446
+ */
1447
+ private function getPluginBeingUpgradedBy($upgrader) {
1448
+ if ( !isset($upgrader, $upgrader->skin) ) {
1449
+ return null;
1450
+ }
1451
+
1452
+ //Figure out which plugin is being upgraded.
1453
+ $pluginFile = null;
1454
+ $skin = $upgrader->skin;
1455
+ if ( $skin instanceof Plugin_Upgrader_Skin ) {
1456
+ if ( isset($skin->plugin) && is_string($skin->plugin) && ($skin->plugin !== '') ) {
1457
+ $pluginFile = $skin->plugin;
1458
+ }
1459
+ } elseif ( isset($skin->plugin_info) && is_array($skin->plugin_info) ) {
1460
+ //This case is tricky because Bulk_Plugin_Upgrader_Skin (etc) doesn't actually store the plugin
1461
+ //filename anywhere. Instead, it has the plugin headers in $plugin_info. So the best we can
1462
+ //do is compare those headers to the headers of installed plugins.
1463
+ $pluginFile = $this->identifyPluginByHeaders($skin->plugin_info);
1464
+ }
1465
+
1466
+ return $pluginFile;
1467
+ }
1468
+
1469
+ /**
1470
+ * Identify an installed plugin based on its headers.
1471
+ *
1472
+ * @param array $searchHeaders The plugin file header to look for.
1473
+ * @return string|null Plugin basename ("foo/bar.php"), or NULL if we can't identify the plugin.
1474
+ */
1475
+ private function identifyPluginByHeaders($searchHeaders) {
1476
+ if ( !function_exists('get_plugins') ){
1477
+ /** @noinspection PhpIncludeInspection */
1478
+ require_once( ABSPATH . '/wp-admin/includes/plugin.php' );
1479
+ }
1480
+
1481
+ $installedPlugins = get_plugins();
1482
+ $matches = array();
1483
+ foreach($installedPlugins as $pluginBasename => $headers) {
1484
+ $diff1 = array_diff_assoc($headers, $searchHeaders);
1485
+ $diff2 = array_diff_assoc($searchHeaders, $headers);
1486
+ if ( empty($diff1) && empty($diff2) ) {
1487
+ $matches[] = $pluginBasename;
1488
+ }
1489
+ }
1490
+
1491
+ //It's possible (though very unlikely) that there could be two plugins with identical
1492
+ //headers. In that case, we can't unambiguously identify the plugin that's being upgraded.
1493
+ if ( count($matches) !== 1 ) {
1494
+ return null;
1495
+ }
1496
+
1497
+ return reset($matches);
1498
+ }
1499
+
1500
+ /**
1501
+ * @access private
1502
+ *
1503
+ * @param mixed $input
1504
+ * @param array $hookExtra
1505
+ * @return mixed Returns $input unaltered.
1506
+ */
1507
+ public function setUpgradedPlugin($input, $hookExtra) {
1508
+ if (!empty($hookExtra['plugin']) && is_string($hookExtra['plugin'])) {
1509
+ $this->upgradedPluginFile = $hookExtra['plugin'];
1510
+ } else {
1511
+ $this->upgradedPluginFile = null;
1512
+ }
1513
+ return $input;
1514
+ }
1515
+
1516
+ /**
1517
+ * @access private
1518
+ *
1519
+ * @param array $options
1520
+ * @return array
1521
+ */
1522
+ public function setUpgradedPluginFromOptions($options) {
1523
+ if (isset($options['hook_extra']['plugin']) && is_string($options['hook_extra']['plugin'])) {
1524
+ $this->upgradedPluginFile = $options['hook_extra']['plugin'];
1525
+ } else {
1526
+ $this->upgradedPluginFile = null;
1527
+ }
1528
+ return $options;
1529
+ }
1530
+
1531
+ /**
1532
+ * @access private
1533
+ *
1534
+ * @param mixed $input
1535
+ * @return mixed Returns $input unaltered.
1536
+ */
1537
+ public function clearUpgradedPlugin($input = null) {
1538
+ $this->upgradedPluginFile = null;
1539
+ return $input;
1540
+ }
1541
+ }
1542
+
1543
+ endif;
1544
+
1545
+
1546
+ if ( !class_exists('PucFactory', false) ):
1547
+
1548
+ /**
1549
+ * A factory that builds instances of other classes from this library.
1550
+ *
1551
+ * When multiple versions of the same class have been loaded (e.g. PluginUpdateChecker 1.2
1552
+ * and 1.3), this factory will always use the latest available version. Register class
1553
+ * versions by calling {@link PucFactory::addVersion()}.
1554
+ *
1555
+ * At the moment it can only build instances of the PluginUpdateChecker class. Other classes
1556
+ * are intended mainly for internal use and refer directly to specific implementations. If you
1557
+ * want to instantiate one of them anyway, you can use {@link PucFactory::getLatestClassVersion()}
1558
+ * to get the class name and then create it with <code>new $class(...)</code>.
1559
+ */
1560
+ class PucFactory {
1561
+ protected static $classVersions = array();
1562
+ protected static $sorted = false;
1563
+
1564
+ /**
1565
+ * Create a new instance of PluginUpdateChecker.
1566
+ *
1567
+ * @see PluginUpdateChecker::__construct()
1568
+ *
1569
+ * @param $metadataUrl
1570
+ * @param $pluginFile
1571
+ * @param string $slug
1572
+ * @param int $checkPeriod
1573
+ * @param string $optionName
1574
+ * @param string $muPluginFile
1575
+ * @return PluginUpdateChecker_3_1
1576
+ */
1577
+ public static function buildUpdateChecker($metadataUrl, $pluginFile, $slug = '', $checkPeriod = 12, $optionName = '', $muPluginFile = '') {
1578
+ $class = self::getLatestClassVersion('PluginUpdateChecker');
1579
+ return new $class($metadataUrl, $pluginFile, $slug, $checkPeriod, $optionName, $muPluginFile);
1580
+ }
1581
+
1582
+ /**
1583
+ * Get the specific class name for the latest available version of a class.
1584
+ *
1585
+ * @param string $class
1586
+ * @return string|null
1587
+ */
1588
+ public static function getLatestClassVersion($class) {
1589
+ if ( !self::$sorted ) {
1590
+ self::sortVersions();
1591
+ }
1592
+
1593
+ if ( isset(self::$classVersions[$class]) ) {
1594
+ return reset(self::$classVersions[$class]);
1595
+ } else {
1596
+ return null;
1597
+ }
1598
+ }
1599
+
1600
+ /**
1601
+ * Sort available class versions in descending order (i.e. newest first).
1602
+ */
1603
+ protected static function sortVersions() {
1604
+ foreach ( self::$classVersions as $class => $versions ) {
1605
+ uksort($versions, array(__CLASS__, 'compareVersions'));
1606
+ self::$classVersions[$class] = $versions;
1607
+ }
1608
+ self::$sorted = true;
1609
+ }
1610
+
1611
+ protected static function compareVersions($a, $b) {
1612
+ return -version_compare($a, $b);
1613
+ }
1614
+
1615
+ /**
1616
+ * Register a version of a class.
1617
+ *
1618
+ * @access private This method is only for internal use by the library.
1619
+ *
1620
+ * @param string $generalClass Class name without version numbers, e.g. 'PluginUpdateChecker'.
1621
+ * @param string $versionedClass Actual class name, e.g. 'PluginUpdateChecker_1_2'.
1622
+ * @param string $version Version number, e.g. '1.2'.
1623
+ */
1624
+ public static function addVersion($generalClass, $versionedClass, $version) {
1625
+ if ( !isset(self::$classVersions[$generalClass]) ) {
1626
+ self::$classVersions[$generalClass] = array();
1627
+ }
1628
+ self::$classVersions[$generalClass][$version] = $versionedClass;
1629
+ self::$sorted = false;
1630
+ }
1631
+ }
1632
+
1633
+ endif;
1634
+
1635
+ require_once(dirname(__FILE__) . '/github-checker.php');
1636
+
1637
+ //Register classes defined in this file with the factory.
1638
+ PucFactory::addVersion('PluginUpdateChecker', 'PluginUpdateChecker_3_1', '3.1');
1639
+ PucFactory::addVersion('PluginUpdate', 'PluginUpdate_3_1', '3.1');
1640
+ PucFactory::addVersion('PluginInfo', 'PluginInfo_3_1', '3.1');
1641
+ PucFactory::addVersion('PucGitHubChecker', 'PucGitHubChecker_3_1', '3.1');
includes/plugin-update-checker/vendor/Parsedown.php ADDED
@@ -0,0 +1,1538 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ #
4
+ #
5
+ # Parsedown
6
+ # http://parsedown.org
7
+ #
8
+ # (c) Emanuil Rusev
9
+ # http://erusev.com
10
+ #
11
+ # For the full license information, view the LICENSE file that was distributed
12
+ # with this source code.
13
+ #
14
+ #
15
+
16
+ class Parsedown
17
+ {
18
+ # ~
19
+
20
+ const version = '1.6.0';
21
+
22
+ # ~
23
+
24
+ function text($text)
25
+ {
26
+ # make sure no definitions are set
27
+ $this->DefinitionData = array();
28
+
29
+ # standardize line breaks
30
+ $text = str_replace(array("\r\n", "\r"), "\n", $text);
31
+
32
+ # remove surrounding line breaks
33
+ $text = trim($text, "\n");
34
+
35
+ # split text into lines
36
+ $lines = explode("\n", $text);
37
+
38
+ # iterate through lines to identify blocks
39
+ $markup = $this->lines($lines);
40
+
41
+ # trim line breaks
42
+ $markup = trim($markup, "\n");
43
+
44
+ return $markup;
45
+ }
46
+
47
+ #
48
+ # Setters
49
+ #
50
+
51
+ function setBreaksEnabled($breaksEnabled)
52
+ {
53
+ $this->breaksEnabled = $breaksEnabled;
54
+
55
+ return $this;
56
+ }
57
+
58
+ protected $breaksEnabled;
59
+
60
+ function setMarkupEscaped($markupEscaped)
61
+ {
62
+ $this->markupEscaped = $markupEscaped;
63
+
64
+ return $this;
65
+ }
66
+
67
+ protected $markupEscaped;
68
+
69
+ function setUrlsLinked($urlsLinked)
70
+ {
71
+ $this->urlsLinked = $urlsLinked;
72
+
73
+ return $this;
74
+ }
75
+
76
+ protected $urlsLinked = true;
77
+
78
+ #
79
+ # Lines
80
+ #
81
+
82
+ protected $BlockTypes = array(
83
+ '#' => array('Header'),
84
+ '*' => array('Rule', 'List'),
85
+ '+' => array('List'),
86
+ '-' => array('SetextHeader', 'Table', 'Rule', 'List'),
87
+ '0' => array('List'),
88
+ '1' => array('List'),
89
+ '2' => array('List'),
90
+ '3' => array('List'),
91
+ '4' => array('List'),
92
+ '5' => array('List'),
93
+ '6' => array('List'),
94
+ '7' => array('List'),
95
+ '8' => array('List'),
96
+ '9' => array('List'),
97
+ ':' => array('Table'),
98
+ '<' => array('Comment', 'Markup'),
99
+ '=' => array('SetextHeader'),
100
+ '>' => array('Quote'),
101
+ '[' => array('Reference'),
102
+ '_' => array('Rule'),
103
+ '`' => array('FencedCode'),
104
+ '|' => array('Table'),
105
+ '~' => array('FencedCode'),
106
+ );
107
+
108
+ # ~
109
+
110
+ protected $unmarkedBlockTypes = array(
111
+ 'Code',
112
+ );
113
+
114
+ #
115
+ # Blocks
116
+ #
117
+
118
+ protected function lines(array $lines)
119
+ {
120
+ $CurrentBlock = null;
121
+
122
+ foreach ($lines as $line)
123
+ {
124
+ if (chop($line) === '')
125
+ {
126
+ if (isset($CurrentBlock))
127
+ {
128
+ $CurrentBlock['interrupted'] = true;
129
+ }
130
+
131
+ continue;
132
+ }
133
+
134
+ if (strpos($line, "\t") !== false)
135
+ {
136
+ $parts = explode("\t", $line);
137
+
138
+ $line = $parts[0];
139
+
140
+ unset($parts[0]);
141
+
142
+ foreach ($parts as $part)
143
+ {
144
+ $shortage = 4 - mb_strlen($line, 'utf-8') % 4;
145
+
146
+ $line .= str_repeat(' ', $shortage);
147
+ $line .= $part;
148
+ }
149
+ }
150
+
151
+ $indent = 0;
152
+
153
+ while (isset($line[$indent]) and $line[$indent] === ' ')
154
+ {
155
+ $indent ++;
156
+ }
157
+
158
+ $text = $indent > 0 ? substr($line, $indent) : $line;
159
+
160
+ # ~
161
+
162
+ $Line = array('body' => $line, 'indent' => $indent, 'text' => $text);
163
+
164
+ # ~
165
+
166
+ if (isset($CurrentBlock['continuable']))
167
+ {
168
+ $Block = $this->{'block'.$CurrentBlock['type'].'Continue'}($Line, $CurrentBlock);
169
+
170
+ if (isset($Block))
171
+ {
172
+ $CurrentBlock = $Block;
173
+
174
+ continue;
175
+ }
176
+ else
177
+ {
178
+ if ($this->isBlockCompletable($CurrentBlock['type']))
179
+ {
180
+ $CurrentBlock = $this->{'block'.$CurrentBlock['type'].'Complete'}($CurrentBlock);
181
+ }
182
+ }
183
+ }
184
+
185
+ # ~
186
+
187
+ $marker = $text[0];
188
+
189
+ # ~
190
+
191
+ $blockTypes = $this->unmarkedBlockTypes;
192
+
193
+ if (isset($this->BlockTypes[$marker]))
194
+ {
195
+ foreach ($this->BlockTypes[$marker] as $blockType)
196
+ {
197
+ $blockTypes []= $blockType;
198
+ }
199
+ }
200
+
201
+ #
202
+ # ~
203
+
204
+ foreach ($blockTypes as $blockType)
205
+ {
206
+ $Block = $this->{'block'.$blockType}($Line, $CurrentBlock);
207
+
208
+ if (isset($Block))
209
+ {
210
+ $Block['type'] = $blockType;
211
+
212
+ if ( ! isset($Block['identified']))
213
+ {
214
+ $Blocks []= $CurrentBlock;
215
+
216
+ $Block['identified'] = true;
217
+ }
218
+
219
+ if ($this->isBlockContinuable($blockType))
220
+ {
221
+ $Block['continuable'] = true;
222
+ }
223
+
224
+ $CurrentBlock = $Block;
225
+
226
+ continue 2;
227
+ }
228
+ }
229
+
230
+ # ~
231
+
232
+ if (isset($CurrentBlock) and ! isset($CurrentBlock['type']) and ! isset($CurrentBlock['interrupted']))
233
+ {
234
+ $CurrentBlock['element']['text'] .= "\n".$text;
235
+ }
236
+ else
237
+ {
238
+ $Blocks []= $CurrentBlock;
239
+
240
+ $CurrentBlock = $this->paragraph($Line);
241
+
242
+ $CurrentBlock['identified'] = true;
243
+ }
244
+ }
245
+
246
+ # ~
247
+
248
+ if (isset($CurrentBlock['continuable']) and $this->isBlockCompletable($CurrentBlock['type']))
249
+ {
250
+ $CurrentBlock = $this->{'block'.$CurrentBlock['type'].'Complete'}($CurrentBlock);
251
+ }
252
+
253
+ # ~
254
+
255
+ $Blocks []= $CurrentBlock;
256
+
257
+ unset($Blocks[0]);
258
+
259
+ # ~
260
+
261
+ $markup = '';
262
+
263
+ foreach ($Blocks as $Block)
264
+ {
265
+ if (isset($Block['hidden']))
266
+ {
267
+ continue;
268
+ }
269
+
270
+ $markup .= "\n";
271
+ $markup .= isset($Block['markup']) ? $Block['markup'] : $this->element($Block['element']);
272
+ }
273
+
274
+ $markup .= "\n";
275
+
276
+ # ~
277
+
278
+ return $markup;
279
+ }
280
+
281
+ protected function isBlockContinuable($Type)
282
+ {
283
+ return method_exists($this, 'block'.$Type.'Continue');
284
+ }
285
+
286
+ protected function isBlockCompletable($Type)
287
+ {
288
+ return method_exists($this, 'block'.$Type.'Complete');
289
+ }
290
+
291
+ #
292
+ # Code
293
+
294
+ protected function blockCode($Line, $Block = null)
295
+ {
296
+ if (isset($Block) and ! isset($Block['type']) and ! isset($Block['interrupted']))
297
+ {
298
+ return;
299
+ }
300
+
301
+ if ($Line['indent'] >= 4)
302
+ {
303
+ $text = substr($Line['body'], 4);
304
+
305
+ $Block = array(
306
+ 'element' => array(
307
+ 'name' => 'pre',
308
+ 'handler' => 'element',
309
+ 'text' => array(
310
+ 'name' => 'code',
311
+ 'text' => $text,
312
+ ),
313
+ ),
314
+ );
315
+
316
+ return $Block;
317
+ }
318
+ }
319
+
320
+ protected function blockCodeContinue($Line, $Block)
321
+ {
322
+ if ($Line['indent'] >= 4)
323
+ {
324
+ if (isset($Block['interrupted']))
325
+ {
326
+ $Block['element']['text']['text'] .= "\n";
327
+
328
+ unset($Block['interrupted']);
329
+ }
330
+
331
+ $Block['element']['text']['text'] .= "\n";
332
+
333
+ $text = substr($Line['body'], 4);
334
+
335
+ $Block['element']['text']['text'] .= $text;
336
+
337
+ return $Block;
338
+ }
339
+ }
340
+
341
+ protected function blockCodeComplete($Block)
342
+ {
343
+ $text = $Block['element']['text']['text'];
344
+
345
+ $text = htmlspecialchars($text, ENT_NOQUOTES, 'UTF-8');
346
+
347
+ $Block['element']['text']['text'] = $text;
348
+
349
+ return $Block;
350
+ }
351
+
352
+ #
353
+ # Comment
354
+
355
+ protected function blockComment($Line)
356
+ {
357
+ if ($this->markupEscaped)
358
+ {
359
+ return;
360
+ }
361
+
362
+ if (isset($Line['text'][3]) and $Line['text'][3] === '-' and $Line['text'][2] === '-' and $Line['text'][1] === '!')
363
+ {
364
+ $Block = array(
365
+ 'markup' => $Line['body'],
366
+ );
367
+
368
+ if (preg_match('/-->$/', $Line['text']))
369
+ {
370
+ $Block['closed'] = true;
371
+ }
372
+
373
+ return $Block;
374
+ }
375
+ }
376
+
377
+ protected function blockCommentContinue($Line, array $Block)
378
+ {
379
+ if (isset($Block['closed']))
380
+ {
381
+ return;
382
+ }
383
+
384
+ $Block['markup'] .= "\n" . $Line['body'];
385
+
386
+ if (preg_match('/-->$/', $Line['text']))
387
+ {
388
+ $Block['closed'] = true;
389
+ }
390
+
391
+ return $Block;
392
+ }
393
+
394
+ #
395
+ # Fenced Code
396
+
397
+ protected function blockFencedCode($Line)
398
+ {
399
+ if (preg_match('/^['.$Line['text'][0].']{3,}[ ]*([\w-]+)?[ ]*$/', $Line['text'], $matches))
400
+ {
401
+ $Element = array(
402
+ 'name' => 'code',
403
+ 'text' => '',
404
+ );
405
+
406
+ if (isset($matches[1]))
407
+ {
408
+ $class = 'language-'.$matches[1];
409
+
410
+ $Element['attributes'] = array(
411
+ 'class' => $class,
412
+ );
413
+ }
414
+
415
+ $Block = array(
416
+ 'char' => $Line['text'][0],
417
+ 'element' => array(
418
+ 'name' => 'pre',
419
+ 'handler' => 'element',
420
+ 'text' => $Element,
421
+ ),
422
+ );
423
+
424
+ return $Block;
425
+ }
426
+ }
427
+
428
+ protected function blockFencedCodeContinue($Line, $Block)
429
+ {
430
+ if (isset($Block['complete']))
431
+ {
432
+ return;
433
+ }
434
+
435
+ if (isset($Block['interrupted']))
436
+ {
437
+ $Block['element']['text']['text'] .= "\n";
438
+
439
+ unset($Block['interrupted']);
440
+ }
441
+
442
+ if (preg_match('/^'.$Block['char'].'{3,}[ ]*$/', $Line['text']))
443
+ {
444
+ $Block['element']['text']['text'] = substr($Block['element']['text']['text'], 1);
445
+
446
+ $Block['complete'] = true;
447
+
448
+ return $Block;
449
+ }
450
+
451
+ $Block['element']['text']['text'] .= "\n".$Line['body'];;
452
+
453
+ return $Block;
454
+ }
455
+
456
+ protected function blockFencedCodeComplete($Block)
457
+ {
458
+ $text = $Block['element']['text']['text'];
459
+
460
+ $text = htmlspecialchars($text, ENT_NOQUOTES, 'UTF-8');
461
+
462
+ $Block['element']['text']['text'] = $text;
463
+
464
+ return $Block;
465
+ }
466
+
467
+ #
468
+ # Header
469
+
470
+ protected function blockHeader($Line)
471
+ {
472
+ if (isset($Line['text'][1]))
473
+ {
474
+ $level = 1;
475
+
476
+ while (isset($Line['text'][$level]) and $Line['text'][$level] === '#')
477
+ {
478
+ $level ++;
479
+ }
480
+
481
+ if ($level > 6)
482
+ {
483
+ return;
484
+ }
485
+
486
+ $text = trim($Line['text'], '# ');
487
+
488
+ $Block = array(
489
+ 'element' => array(
490
+ 'name' => 'h' . min(6, $level),
491
+ 'text' => $text,
492
+ 'handler' => 'line',
493
+ ),
494
+ );
495
+
496
+ return $Block;
497
+ }
498
+ }
499
+
500
+ #
501
+ # List
502
+
503
+ protected function blockList($Line)
504
+ {
505
+ list($name, $pattern) = $Line['text'][0] <= '-' ? array('ul', '[*+-]') : array('ol', '[0-9]+[.]');
506
+
507
+ if (preg_match('/^('.$pattern.'[ ]+)(.*)/', $Line['text'], $matches))
508
+ {
509
+ $Block = array(
510
+ 'indent' => $Line['indent'],
511
+ 'pattern' => $pattern,
512
+ 'element' => array(
513
+ 'name' => $name,
514
+ 'handler' => 'elements',
515
+ ),
516
+ );
517
+
518
+ $Block['li'] = array(
519
+ 'name' => 'li',
520
+ 'handler' => 'li',
521
+ 'text' => array(
522
+ $matches[2],
523
+ ),
524
+ );
525
+
526
+ $Block['element']['text'] []= & $Block['li'];
527
+
528
+ return $Block;
529
+ }
530
+ }
531
+
532
+ protected function blockListContinue($Line, array $Block)
533
+ {
534
+ if ($Block['indent'] === $Line['indent'] and preg_match('/^'.$Block['pattern'].'(?:[ ]+(.*)|$)/', $Line['text'], $matches))
535
+ {
536
+ if (isset($Block['interrupted']))
537
+ {
538
+ $Block['li']['text'] []= '';
539
+
540
+ unset($Block['interrupted']);
541
+ }
542
+
543
+ unset($Block['li']);
544
+
545
+ $text = isset($matches[1]) ? $matches[1] : '';
546
+
547
+ $Block['li'] = array(
548
+ 'name' => 'li',
549
+ 'handler' => 'li',
550
+ 'text' => array(
551
+ $text,
552
+ ),
553
+ );
554
+
555
+ $Block['element']['text'] []= & $Block['li'];
556
+
557
+ return $Block;
558
+ }
559
+
560
+ if ($Line['text'][0] === '[' and $this->blockReference($Line))
561
+ {
562
+ return $Block;
563
+ }
564
+
565
+ if ( ! isset($Block['interrupted']))
566
+ {
567
+ $text = preg_replace('/^[ ]{0,4}/', '', $Line['body']);
568
+
569
+ $Block['li']['text'] []= $text;
570
+
571
+ return $Block;
572
+ }
573
+
574
+ if ($Line['indent'] > 0)
575
+ {
576
+ $Block['li']['text'] []= '';
577
+
578
+ $text = preg_replace('/^[ ]{0,4}/', '', $Line['body']);
579
+
580
+ $Block['li']['text'] []= $text;
581
+
582
+ unset($Block['interrupted']);
583
+
584
+ return $Block;
585
+ }
586
+ }
587
+
588
+ #
589
+ # Quote
590
+
591
+ protected function blockQuote($Line)
592
+ {
593
+ if (preg_match('/^>[ ]?(.*)/', $Line['text'], $matches))
594
+ {
595
+ $Block = array(
596
+ 'element' => array(
597
+ 'name' => 'blockquote',
598
+ 'handler' => 'lines',
599
+ 'text' => (array) $matches[1],
600
+ ),
601
+ );
602
+
603
+ return $Block;
604
+ }
605
+ }
606
+
607
+ protected function blockQuoteContinue($Line, array $Block)
608
+ {
609
+ if ($Line['text'][0] === '>' and preg_match('/^>[ ]?(.*)/', $Line['text'], $matches))
610
+ {
611
+ if (isset($Block['interrupted']))
612
+ {
613
+ $Block['element']['text'] []= '';
614
+
615
+ unset($Block['interrupted']);
616
+ }
617
+
618
+ $Block['element']['text'] []= $matches[1];
619
+
620
+ return $Block;
621
+ }
622
+
623
+ if ( ! isset($Block['interrupted']))
624
+ {
625
+ $Block['element']['text'] []= $Line['text'];
626
+
627
+ return $Block;
628
+ }
629
+ }
630
+
631
+ #
632
+ # Rule
633
+
634
+ protected function blockRule($Line)
635
+ {
636
+ if (preg_match('/^(['.$Line['text'][0].'])([ ]*\1){2,}[ ]*$/', $Line['text']))
637
+ {
638
+ $Block = array(
639
+ 'element' => array(
640
+ 'name' => 'hr'
641
+ ),
642
+ );
643
+
644
+ return $Block;
645
+ }
646
+ }
647
+
648
+ #
649
+ # Setext
650
+
651
+ protected function blockSetextHeader($Line, array $Block = null)
652
+ {
653
+ if ( ! isset($Block) or isset($Block['type']) or isset($Block['interrupted']))
654
+ {
655
+ return;
656
+ }
657
+
658
+ if (chop($Line['text'], $Line['text'][0]) === '')
659
+ {
660
+ $Block['element']['name'] = $Line['text'][0] === '=' ? 'h1' : 'h2';
661
+
662
+ return $Block;
663
+ }
664
+ }
665
+
666
+ #
667
+ # Markup
668
+
669
+ protected function blockMarkup($Line)
670
+ {
671
+ if ($this->markupEscaped)
672
+ {
673
+ return;
674
+ }
675
+
676
+ if (preg_match('/^<(\w*)(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*(\/)?>/', $Line['text'], $matches))
677
+ {
678
+ $element = strtolower($matches[1]);
679
+
680
+ if (in_array($element, $this->textLevelElements))
681
+ {
682
+ return;
683
+ }
684
+
685
+ $Block = array(
686
+ 'name' => $matches[1],
687
+ 'depth' => 0,
688
+ 'markup' => $Line['text'],
689
+ );
690
+
691
+ $length = strlen($matches[0]);
692
+
693
+ $remainder = substr($Line['text'], $length);
694
+
695
+ if (trim($remainder) === '')
696
+ {
697
+ if (isset($matches[2]) or in_array($matches[1], $this->voidElements))
698
+ {
699
+ $Block['closed'] = true;
700
+
701
+ $Block['void'] = true;
702
+ }
703
+ }
704
+ else
705
+ {
706
+ if (isset($matches[2]) or in_array($matches[1], $this->voidElements))
707
+ {
708
+ return;
709
+ }
710
+
711
+ if (preg_match('/<\/'.$matches[1].'>[ ]*$/i', $remainder))
712
+ {
713
+ $Block['closed'] = true;
714
+ }
715
+ }
716
+
717
+ return $Block;
718
+ }
719
+ }
720
+
721
+ protected function blockMarkupContinue($Line, array $Block)
722
+ {
723
+ if (isset($Block['closed']))
724
+ {
725
+ return;
726
+ }
727
+
728
+ if (preg_match('/^<'.$Block['name'].'(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*>/i', $Line['text'])) # open
729
+ {
730
+ $Block['depth'] ++;
731
+ }
732
+
733
+ if (preg_match('/(.*?)<\/'.$Block['name'].'>[ ]*$/i', $Line['text'], $matches)) # close
734
+ {
735
+ if ($Block['depth'] > 0)
736
+ {
737
+ $Block['depth'] --;
738
+ }
739
+ else
740
+ {
741
+ $Block['closed'] = true;
742
+ }
743
+ }
744
+
745
+ if (isset($Block['interrupted']))
746
+ {
747
+ $Block['markup'] .= "\n";
748
+
749
+ unset($Block['interrupted']);
750
+ }
751
+
752
+ $Block['markup'] .= "\n".$Line['body'];
753
+
754
+ return $Block;
755
+ }
756
+
757
+ #
758
+ # Reference
759
+
760
+ protected function blockReference($Line)
761
+ {
762
+ if (preg_match('/^\[(.+?)\]:[ ]*<?(\S+?)>?(?:[ ]+["\'(](.+)["\')])?[ ]*$/', $Line['text'], $matches))
763
+ {
764
+ $id = strtolower($matches[1]);
765
+
766
+ $Data = array(
767
+ 'url' => $matches[2],
768
+ 'title' => null,
769
+ );
770
+
771
+ if (isset($matches[3]))
772
+ {
773
+ $Data['title'] = $matches[3];
774
+ }
775
+
776
+ $this->DefinitionData['Reference'][$id] = $Data;
777
+
778
+ $Block = array(
779
+ 'hidden' => true,
780
+ );
781
+
782
+ return $Block;
783
+ }
784
+ }
785
+
786
+ #
787
+ # Table
788
+
789
+ protected function blockTable($Line, array $Block = null)
790
+ {
791
+ if ( ! isset($Block) or isset($Block['type']) or isset($Block['interrupted']))
792
+ {
793
+ return;
794
+ }
795
+
796
+ if (strpos($Block['element']['text'], '|') !== false and chop($Line['text'], ' -:|') === '')
797
+ {
798
+ $alignments = array();
799
+
800
+ $divider = $Line['text'];
801
+
802
+ $divider = trim($divider);
803
+ $divider = trim($divider, '|');
804
+
805
+ $dividerCells = explode('|', $divider);
806
+
807
+ foreach ($dividerCells as $dividerCell)
808
+ {
809
+ $dividerCell = trim($dividerCell);
810
+
811
+ if ($dividerCell === '')
812
+ {
813
+ continue;
814
+ }
815
+
816
+ $alignment = null;
817
+
818
+ if ($dividerCell[0] === ':')
819
+ {
820
+ $alignment = 'left';
821
+ }
822
+
823
+ if (substr($dividerCell, - 1) === ':')
824
+ {
825
+ $alignment = $alignment === 'left' ? 'center' : 'right';
826
+ }
827
+
828
+ $alignments []= $alignment;
829
+ }
830
+
831
+ # ~
832
+
833
+ $HeaderElements = array();
834
+
835
+ $header = $Block['element']['text'];
836
+
837
+ $header = trim($header);
838
+ $header = trim($header, '|');
839
+
840
+ $headerCells = explode('|', $header);
841
+
842
+ foreach ($headerCells as $index => $headerCell)
843
+ {
844
+ $headerCell = trim($headerCell);
845
+
846
+ $HeaderElement = array(
847
+ 'name' => 'th',
848
+ 'text' => $headerCell,
849
+ 'handler' => 'line',
850
+ );
851
+
852
+ if (isset($alignments[$index]))
853
+ {
854
+ $alignment = $alignments[$index];
855
+
856
+ $HeaderElement['attributes'] = array(
857
+ 'style' => 'text-align: '.$alignment.';',
858
+ );
859
+ }
860
+
861
+ $HeaderElements []= $HeaderElement;
862
+ }
863
+
864
+ # ~
865
+
866
+ $Block = array(
867
+ 'alignments' => $alignments,
868
+ 'identified' => true,
869
+ 'element' => array(
870
+ 'name' => 'table',
871
+ 'handler' => 'elements',
872
+ ),
873
+ );
874
+
875
+ $Block['element']['text'] []= array(
876
+ 'name' => 'thead',
877
+ 'handler' => 'elements',
878
+ );
879
+
880
+ $Block['element']['text'] []= array(
881
+ 'name' => 'tbody',
882
+ 'handler' => 'elements',
883
+ 'text' => array(),
884
+ );
885
+
886
+ $Block['element']['text'][0]['text'] []= array(
887
+ 'name' => 'tr',
888
+ 'handler' => 'elements',
889
+ 'text' => $HeaderElements,
890
+ );
891
+
892
+ return $Block;
893
+ }
894
+ }
895
+
896
+ protected function blockTableContinue($Line, array $Block)
897
+ {
898
+ if (isset($Block['interrupted']))
899
+ {
900
+ return;
901
+ }
902
+
903
+ if ($Line['text'][0] === '|' or strpos($Line['text'], '|'))
904
+ {
905
+ $Elements = array();
906
+
907
+ $row = $Line['text'];
908
+
909
+ $row = trim($row);
910
+ $row = trim($row, '|');
911
+
912
+ preg_match_all('/(?:(\\\\[|])|[^|`]|`[^`]+`|`)+/', $row, $matches);
913
+
914
+ foreach ($matches[0] as $index => $cell)
915
+ {
916
+ $cell = trim($cell);
917
+
918
+ $Element = array(
919
+ 'name' => 'td',
920
+ 'handler' => 'line',
921
+ 'text' => $cell,
922
+ );
923
+
924
+ if (isset($Block['alignments'][$index]))
925
+ {
926
+ $Element['attributes'] = array(
927
+ 'style' => 'text-align: '.$Block['alignments'][$index].';',
928
+ );
929
+ }
930
+
931
+ $Elements []= $Element;
932
+ }
933
+
934
+ $Element = array(
935
+ 'name' => 'tr',
936
+ 'handler' => 'elements',
937
+ 'text' => $Elements,
938
+ );
939
+
940
+ $Block['element']['text'][1]['text'] []= $Element;
941
+
942
+ return $Block;
943
+ }
944
+ }
945
+
946
+ #
947
+ # ~
948
+ #
949
+
950
+ protected function paragraph($Line)
951
+ {
952
+ $Block = array(
953
+ 'element' => array(
954
+ 'name' => 'p',
955
+ 'text' => $Line['text'],
956
+ 'handler' => 'line',
957
+ ),
958
+ );
959
+
960
+ return $Block;
961
+ }
962
+
963
+ #
964
+ # Inline Elements
965
+ #
966
+
967
+ protected $InlineTypes = array(
968
+ '"' => array('SpecialCharacter'),
969
+ '!' => array('Image'),
970
+ '&' => array('SpecialCharacter'),
971
+ '*' => array('Emphasis'),
972
+ ':' => array('Url'),
973
+ '<' => array('UrlTag', 'EmailTag', 'Markup', 'SpecialCharacter'),
974
+ '>' => array('SpecialCharacter'),
975
+ '[' => array('Link'),
976
+ '_' => array('Emphasis'),
977
+ '`' => array('Code'),
978
+ '~' => array('Strikethrough'),
979
+ '\\' => array('EscapeSequence'),
980
+ );
981
+
982
+ # ~
983
+
984
+ protected $inlineMarkerList = '!"*_&[:<>`~\\';
985
+
986
+ #
987
+ # ~
988
+ #
989
+
990
+ public function line($text)
991
+ {
992
+ $markup = '';
993
+
994
+ # $excerpt is based on the first occurrence of a marker
995
+
996
+ while ($excerpt = strpbrk($text, $this->inlineMarkerList))
997
+ {
998
+ $marker = $excerpt[0];
999
+
1000
+ $markerPosition = strpos($text, $marker);
1001
+
1002
+ $Excerpt = array('text' => $excerpt, 'context' => $text);
1003
+
1004
+ foreach ($this->InlineTypes[$marker] as $inlineType)
1005
+ {
1006
+ $Inline = $this->{'inline'.$inlineType}($Excerpt);
1007
+
1008
+ if ( ! isset($Inline))
1009
+ {
1010
+ continue;
1011
+ }
1012
+
1013
+ # makes sure that the inline belongs to "our" marker
1014
+
1015
+ if (isset($Inline['position']) and $Inline['position'] > $markerPosition)
1016
+ {
1017
+ continue;
1018
+ }
1019
+
1020
+ # sets a default inline position
1021
+
1022
+ if ( ! isset($Inline['position']))
1023
+ {
1024
+ $Inline['position'] = $markerPosition;
1025
+ }
1026
+
1027
+ # the text that comes before the inline
1028
+ $unmarkedText = substr($text, 0, $Inline['position']);
1029
+
1030
+ # compile the unmarked text
1031
+ $markup .= $this->unmarkedText($unmarkedText);
1032
+
1033
+ # compile the inline
1034
+ $markup .= isset($Inline['markup']) ? $Inline['markup'] : $this->element($Inline['element']);
1035
+
1036
+ # remove the examined text
1037
+ $text = substr($text, $Inline['position'] + $Inline['extent']);
1038
+
1039
+ continue 2;
1040
+ }
1041
+
1042
+ # the marker does not belong to an inline
1043
+
1044
+ $unmarkedText = substr($text, 0, $markerPosition + 1);
1045
+
1046
+ $markup .= $this->unmarkedText($unmarkedText);
1047
+
1048
+ $text = substr($text, $markerPosition + 1);
1049
+ }
1050
+
1051
+ $markup .= $this->unmarkedText($text);
1052
+
1053
+ return $markup;
1054
+ }
1055
+
1056
+ #
1057
+ # ~
1058
+ #
1059
+
1060
+ protected function inlineCode($Excerpt)
1061
+ {
1062
+ $marker = $Excerpt['text'][0];
1063
+
1064
+ if (preg_match('/^('.$marker.'+)[ ]*(.+?)[ ]*(?<!'.$marker.')\1(?!'.$marker.')/s', $Excerpt['text'], $matches))
1065
+ {
1066
+ $text = $matches[2];
1067
+ $text = htmlspecialchars($text, ENT_NOQUOTES, 'UTF-8');
1068
+ $text = preg_replace("/[ ]*\n/", ' ', $text);
1069
+
1070
+ return array(
1071
+ 'extent' => strlen($matches[0]),
1072
+ 'element' => array(
1073
+ 'name' => 'code',
1074
+ 'text' => $text,
1075
+ ),
1076
+ );
1077
+ }
1078
+ }
1079
+
1080
+ protected function inlineEmailTag($Excerpt)
1081
+ {
1082
+ if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<((mailto:)?\S+?@\S+?)>/i', $Excerpt['text'], $matches))
1083
+ {
1084
+ $url = $matches[1];
1085
+
1086
+ if ( ! isset($matches[2]))
1087
+ {
1088
+ $url = 'mailto:' . $url;
1089
+ }
1090
+
1091
+ return array(
1092
+ 'extent' => strlen($matches[0]),
1093
+ 'element' => array(
1094
+ 'name' => 'a',
1095
+ 'text' => $matches[1],
1096
+ 'attributes' => array(
1097
+ 'href' => $url,
1098
+ ),
1099
+ ),
1100
+ );
1101
+ }
1102
+ }
1103
+
1104
+ protected function inlineEmphasis($Excerpt)
1105
+ {
1106
+ if ( ! isset($Excerpt['text'][1]))
1107
+ {
1108
+ return;
1109
+ }
1110
+
1111
+ $marker = $Excerpt['text'][0];
1112
+
1113
+ if ($Excerpt['text'][1] === $marker and preg_match($this->StrongRegex[$marker], $Excerpt['text'], $matches))
1114
+ {
1115
+ $emphasis = 'strong';
1116
+ }
1117
+ elseif (preg_match($this->EmRegex[$marker], $Excerpt['text'], $matches))
1118
+ {
1119
+ $emphasis = 'em';
1120
+ }
1121
+ else
1122
+ {
1123
+ return;
1124
+ }
1125
+
1126
+ return array(
1127
+ 'extent' => strlen($matches[0]),
1128
+ 'element' => array(
1129
+ 'name' => $emphasis,
1130
+ 'handler' => 'line',
1131
+ 'text' => $matches[1],
1132
+ ),
1133
+ );
1134
+ }
1135
+
1136
+ protected function inlineEscapeSequence($Excerpt)
1137
+ {
1138
+ if (isset($Excerpt['text'][1]) and in_array($Excerpt['text'][1], $this->specialCharacters))
1139
+ {
1140
+ return array(
1141
+ 'markup' => $Excerpt['text'][1],
1142
+ 'extent' => 2,
1143
+ );
1144
+ }
1145
+ }
1146
+
1147
+ protected function inlineImage($Excerpt)
1148
+ {
1149
+ if ( ! isset($Excerpt['text'][1]) or $Excerpt['text'][1] !== '[')
1150
+ {
1151
+ return;
1152
+ }
1153
+
1154
+ $Excerpt['text']= substr($Excerpt['text'], 1);
1155
+
1156
+ $Link = $this->inlineLink($Excerpt);
1157
+
1158
+ if ($Link === null)
1159
+ {
1160
+ return;
1161
+ }
1162
+
1163
+ $Inline = array(
1164
+ 'extent' => $Link['extent'] + 1,
1165
+ 'element' => array(
1166
+ 'name' => 'img',
1167
+ 'attributes' => array(
1168
+ 'src' => $Link['element']['attributes']['href'],
1169
+ 'alt' => $Link['element']['text'],
1170
+ ),
1171
+ ),
1172
+ );
1173
+
1174
+ $Inline['element']['attributes'] += $Link['element']['attributes'];
1175
+
1176
+ unset($Inline['element']['attributes']['href']);
1177
+
1178
+ return $Inline;
1179
+ }
1180
+
1181
+ protected function inlineLink($Excerpt)
1182
+ {
1183
+ $Element = array(
1184
+ 'name' => 'a',
1185
+ 'handler' => 'line',
1186
+ 'text' => null,
1187
+ 'attributes' => array(
1188
+ 'href' => null,
1189
+ 'title' => null,
1190
+ ),
1191
+ );
1192
+
1193
+ $extent = 0;
1194
+
1195
+ $remainder = $Excerpt['text'];
1196
+
1197
+ if (preg_match('/\[((?:[^][]|(?R))*)\]/', $remainder, $matches))
1198
+ {
1199
+ $Element['text'] = $matches[1];
1200
+
1201
+ $extent += strlen($matches[0]);
1202
+
1203
+ $remainder = substr($remainder, $extent);
1204
+ }
1205
+ else
1206
+ {
1207
+ return;
1208
+ }
1209
+
1210
+ if (preg_match('/^[(]((?:[^ ()]|[(][^ )]+[)])+)(?:[ ]+("[^"]*"|\'[^\']*\'))?[)]/', $remainder, $matches))
1211
+ {
1212
+ $Element['attributes']['href'] = $matches[1];
1213
+
1214
+ if (isset($matches[2]))
1215
+ {
1216
+ $Element['attributes']['title'] = substr($matches[2], 1, - 1);
1217
+ }
1218
+
1219
+ $extent += strlen($matches[0]);
1220
+ }
1221
+ else
1222
+ {
1223
+ if (preg_match('/^\s*\[(.*?)\]/', $remainder, $matches))
1224
+ {
1225
+ $definition = strlen($matches[1]) ? $matches[1] : $Element['text'];
1226
+ $definition = strtolower($definition);
1227
+
1228
+ $extent += strlen($matches[0]);
1229
+ }
1230
+ else
1231
+ {
1232
+ $definition = strtolower($Element['text']);
1233
+ }
1234
+
1235
+ if ( ! isset($this->DefinitionData['Reference'][$definition]))
1236
+ {
1237
+ return;
1238
+ }
1239
+
1240
+ $Definition = $this->DefinitionData['Reference'][$definition];
1241
+
1242
+ $Element['attributes']['href'] = $Definition['url'];
1243
+ $Element['attributes']['title'] = $Definition['title'];
1244
+ }
1245
+
1246
+ $Element['attributes']['href'] = str_replace(array('&', '<'), array('&amp;', '&lt;'), $Element['attributes']['href']);
1247
+
1248
+ return array(
1249
+ 'extent' => $extent,
1250
+ 'element' => $Element,
1251
+ );
1252
+ }
1253
+
1254
+ protected function inlineMarkup($Excerpt)
1255
+ {
1256
+ if ($this->markupEscaped or strpos($Excerpt['text'], '>') === false)
1257
+ {
1258
+ return;
1259
+ }
1260
+
1261
+ if ($Excerpt['text'][1] === '/' and preg_match('/^<\/\w*[ ]*>/s', $Excerpt['text'], $matches))
1262
+ {
1263
+ return array(
1264
+ 'markup' => $matches[0],
1265
+ 'extent' => strlen($matches[0]),
1266
+ );
1267
+ }
1268
+
1269
+ if ($Excerpt['text'][1] === '!' and preg_match('/^<!---?[^>-](?:-?[^-])*-->/s', $Excerpt['text'], $matches))
1270
+ {
1271
+ return array(
1272
+ 'markup' => $matches[0],
1273
+ 'extent' => strlen($matches[0]),
1274
+ );
1275
+ }
1276
+
1277
+ if ($Excerpt['text'][1] !== ' ' and preg_match('/^<\w*(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*\/?>/s', $Excerpt['text'], $matches))
1278
+ {
1279
+ return array(
1280
+ 'markup' => $matches[0],
1281
+ 'extent' => strlen($matches[0]),
1282
+ );
1283
+ }
1284
+ }
1285
+
1286
+ protected function inlineSpecialCharacter($Excerpt)
1287
+ {
1288
+ if ($Excerpt['text'][0] === '&' and ! preg_match('/^&#?\w+;/', $Excerpt['text']))
1289
+ {
1290
+ return array(
1291
+ 'markup' => '&amp;',
1292
+ 'extent' => 1,
1293
+ );
1294
+ }
1295
+
1296
+ $SpecialCharacter = array('>' => 'gt', '<' => 'lt', '"' => 'quot');
1297
+
1298
+ if (isset($SpecialCharacter[$Excerpt['text'][0]]))
1299
+ {
1300
+ return array(
1301
+ 'markup' => '&'.$SpecialCharacter[$Excerpt['text'][0]].';',
1302
+ 'extent' => 1,
1303
+ );
1304
+ }
1305
+ }
1306
+
1307
+ protected function inlineStrikethrough($Excerpt)
1308
+ {
1309
+ if ( ! isset($Excerpt['text'][1]))
1310
+ {
1311
+ return;
1312
+ }
1313
+
1314
+ if ($Excerpt['text'][1] === '~' and preg_match('/^~~(?=\S)(.+?)(?<=\S)~~/', $Excerpt['text'], $matches))
1315
+ {
1316
+ return array(
1317
+ 'extent' => strlen($matches[0]),
1318
+ 'element' => array(
1319
+ 'name' => 'del',
1320
+ 'text' => $matches[1],
1321
+ 'handler' => 'line',
1322
+ ),
1323
+ );
1324
+ }
1325
+ }
1326
+
1327
+ protected function inlineUrl($Excerpt)
1328
+ {
1329
+ if ($this->urlsLinked !== true or ! isset($Excerpt['text'][2]) or $Excerpt['text'][2] !== '/')
1330
+ {
1331
+ return;
1332
+ }
1333
+
1334
+ if (preg_match('/\bhttps?:[\/]{2}[^\s<]+\b\/*/ui', $Excerpt['context'], $matches, PREG_OFFSET_CAPTURE))
1335
+ {
1336
+ $Inline = array(
1337
+ 'extent' => strlen($matches[0][0]),
1338
+ 'position' => $matches[0][1],
1339
+ 'element' => array(
1340
+ 'name' => 'a',
1341
+ 'text' => $matches[0][0],
1342
+ 'attributes' => array(
1343
+ 'href' => $matches[0][0],
1344
+ ),
1345
+ ),
1346
+ );
1347
+
1348
+ return $Inline;
1349
+ }
1350
+ }
1351
+
1352
+ protected function inlineUrlTag($Excerpt)
1353
+ {
1354
+ if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<(\w+:\/{2}[^ >]+)>/i', $Excerpt['text'], $matches))
1355
+ {
1356
+ $url = str_replace(array('&', '<'), array('&amp;', '&lt;'), $matches[1]);
1357
+
1358
+ return array(
1359
+ 'extent' => strlen($matches[0]),
1360
+ 'element' => array(
1361
+ 'name' => 'a',
1362
+ 'text' => $url,
1363
+ 'attributes' => array(
1364
+ 'href' => $url,
1365
+ ),
1366
+ ),
1367
+ );
1368
+ }
1369
+ }
1370
+
1371
+ # ~
1372
+
1373
+ protected function unmarkedText($text)
1374
+ {
1375
+ if ($this->breaksEnabled)
1376
+ {
1377
+ $text = preg_replace('/[ ]*\n/', "<br />\n", $text);
1378
+ }
1379
+ else
1380
+ {
1381
+ $text = preg_replace('/(?:[ ][ ]+|[ ]*\\\\)\n/', "<br />\n", $text);
1382
+ $text = str_replace(" \n", "\n", $text);
1383
+ }
1384
+
1385
+ return $text;
1386
+ }
1387
+
1388
+ #
1389
+ # Handlers
1390
+ #
1391
+
1392
+ protected function element(array $Element)
1393
+ {
1394
+ $markup = '<'.$Element['name'];
1395
+
1396
+ if (isset($Element['attributes']))
1397
+ {
1398
+ foreach ($Element['attributes'] as $name => $value)
1399
+ {
1400
+ if ($value === null)
1401
+ {
1402
+ continue;
1403
+ }
1404
+
1405
+ $markup .= ' '.$name.'="'.$value.'"';
1406
+ }
1407
+ }
1408
+
1409
+ if (isset($Element['text']))
1410
+ {
1411
+ $markup .= '>';
1412
+
1413
+ if (isset($Element['handler']))
1414
+ {
1415
+ $markup .= $this->{$Element['handler']}($Element['text']);
1416
+ }
1417
+ else
1418
+ {
1419
+ $markup .= $Element['text'];
1420
+ }
1421
+
1422
+ $markup .= '</'.$Element['name'].'>';
1423
+ }
1424
+ else
1425
+ {
1426
+ $markup .= ' />';
1427
+ }
1428
+
1429
+ return $markup;
1430
+ }
1431
+
1432
+ protected function elements(array $Elements)
1433
+ {
1434
+ $markup = '';
1435
+
1436
+ foreach ($Elements as $Element)
1437
+ {
1438
+ $markup .= "\n" . $this->element($Element);
1439
+ }
1440
+
1441
+ $markup .= "\n";
1442
+
1443
+ return $markup;
1444
+ }
1445
+
1446
+ # ~
1447
+
1448
+ protected function li($lines)
1449
+ {
1450
+ $markup = $this->lines($lines);
1451
+
1452
+ $trimmedMarkup = trim($markup);
1453
+
1454
+ if ( ! in_array('', $lines) and substr($trimmedMarkup, 0, 3) === '<p>')
1455
+ {
1456
+ $markup = $trimmedMarkup;
1457
+ $markup = substr($markup, 3);
1458
+
1459
+ $position = strpos($markup, "</p>");
1460
+
1461
+ $markup = substr_replace($markup, '', $position, 4);
1462
+ }
1463
+
1464
+ return $markup;
1465
+ }
1466
+
1467
+ #
1468
+ # Deprecated Methods
1469
+ #
1470
+
1471
+ function parse($text)
1472
+ {
1473
+ $markup = $this->text($text);
1474
+
1475
+ return $markup;
1476
+ }
1477
+
1478
+ #
1479
+ # Static Methods
1480
+ #
1481
+
1482
+ static function instance($name = 'default')
1483
+ {
1484
+ if (isset(self::$instances[$name]))
1485
+ {
1486
+ return self::$instances[$name];
1487
+ }
1488
+
1489
+ $instance = new static();
1490
+
1491
+ self::$instances[$name] = $instance;
1492
+
1493
+ return $instance;
1494
+ }
1495
+
1496
+ private static $instances = array();
1497
+
1498
+ #
1499
+ # Fields
1500
+ #
1501
+
1502
+ protected $DefinitionData;
1503
+
1504
+ #
1505
+ # Read-Only
1506
+
1507
+ protected $specialCharacters = array(
1508
+ '\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '>', '#', '+', '-', '.', '!', '|',
1509
+ );
1510
+
1511
+ protected $StrongRegex = array(
1512
+ '*' => '/^[*]{2}((?:\\\\\*|[^*]|[*][^*]*[*])+?)[*]{2}(?![*])/s',
1513
+ '_' => '/^__((?:\\\\_|[^_]|_[^_]*_)+?)__(?!_)/us',
1514
+ );
1515
+
1516
+ protected $EmRegex = array(
1517
+ '*' => '/^[*]((?:\\\\\*|[^*]|[*][*][^*]+?[*][*])+?)[*](?![*])/s',
1518
+ '_' => '/^_((?:\\\\_|[^_]|__[^_]*__)+?)_(?!_)\b/us',
1519
+ );
1520
+
1521
+ protected $regexHtmlAttribute = '[a-zA-Z_:][\w:.-]*(?:\s*=\s*(?:[^"\'=<>`\s]+|"[^"]*"|\'[^\']*\'))?';
1522
+
1523
+ protected $voidElements = array(
1524
+ 'area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source',
1525
+ );
1526
+
1527
+ protected $textLevelElements = array(
1528
+ 'a', 'br', 'bdo', 'abbr', 'blink', 'nextid', 'acronym', 'basefont',
1529
+ 'b', 'em', 'big', 'cite', 'small', 'spacer', 'listing',
1530
+ 'i', 'rp', 'del', 'code', 'strike', 'marquee',
1531
+ 'q', 'rt', 'ins', 'font', 'strong',
1532
+ 's', 'tt', 'sub', 'mark',
1533
+ 'u', 'xm', 'sup', 'nobr',
1534
+ 'var', 'ruby',
1535
+ 'wbr', 'span',
1536
+ 'time',
1537
+ );
1538
+ }
includes/plugin-update-checker/vendor/ParsedownLegacy.php ADDED
@@ -0,0 +1,1535 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ #
4
+ #
5
+ # Parsedown
6
+ # http://parsedown.org
7
+ #
8
+ # (c) Emanuil Rusev
9
+ # http://erusev.com
10
+ #
11
+ # For the full license information, view the LICENSE file that was distributed
12
+ # with this source code.
13
+ #
14
+ #
15
+
16
+ class Parsedown
17
+ {
18
+ # ~
19
+
20
+ const version = '1.5.0';
21
+
22
+ # ~
23
+
24
+ function text($text)
25
+ {
26
+ # make sure no definitions are set
27
+ $this->DefinitionData = array();
28
+
29
+ # standardize line breaks
30
+ $text = str_replace(array("\r\n", "\r"), "\n", $text);
31
+
32
+ # remove surrounding line breaks
33
+ $text = trim($text, "\n");
34
+
35
+ # split text into lines
36
+ $lines = explode("\n", $text);
37
+
38
+ # iterate through lines to identify blocks
39
+ $markup = $this->lines($lines);
40
+
41
+ # trim line breaks
42
+ $markup = trim($markup, "\n");
43
+
44
+ return $markup;
45
+ }
46
+
47
+ #
48
+ # Setters
49
+ #
50
+
51
+ function setBreaksEnabled($breaksEnabled)
52
+ {
53
+ $this->breaksEnabled = $breaksEnabled;
54
+
55
+ return $this;
56
+ }
57
+
58
+ protected $breaksEnabled;
59
+
60
+ function setMarkupEscaped($markupEscaped)
61
+ {
62
+ $this->markupEscaped = $markupEscaped;
63
+
64
+ return $this;
65
+ }
66
+
67
+ protected $markupEscaped;
68
+
69
+ function setUrlsLinked($urlsLinked)
70
+ {
71
+ $this->urlsLinked = $urlsLinked;
72
+
73
+ return $this;
74
+ }
75
+
76
+ protected $urlsLinked = true;
77
+
78
+ #
79
+ # Lines
80
+ #
81
+
82
+ protected $BlockTypes = array(
83
+ '#' => array('Header'),
84
+ '*' => array('Rule', 'List'),
85
+ '+' => array('List'),
86
+ '-' => array('SetextHeader', 'Table', 'Rule', 'List'),
87
+ '0' => array('List'),
88
+ '1' => array('List'),
89
+ '2' => array('List'),
90
+ '3' => array('List'),
91
+ '4' => array('List'),
92
+ '5' => array('List'),
93
+ '6' => array('List'),
94
+ '7' => array('List'),
95
+ '8' => array('List'),
96
+ '9' => array('List'),
97
+ ':' => array('Table'),
98
+ '<' => array('Comment', 'Markup'),
99
+ '=' => array('SetextHeader'),
100
+ '>' => array('Quote'),
101
+ '[' => array('Reference'),
102
+ '_' => array('Rule'),
103
+ '`' => array('FencedCode'),
104
+ '|' => array('Table'),
105
+ '~' => array('FencedCode'),
106
+ );
107
+
108
+ # ~
109
+
110
+ protected $DefinitionTypes = array(
111
+ '[' => array('Reference'),
112
+ );
113
+
114
+ # ~
115
+
116
+ protected $unmarkedBlockTypes = array(
117
+ 'Code',
118
+ );
119
+
120
+ #
121
+ # Blocks
122
+ #
123
+
124
+ private function lines(array $lines)
125
+ {
126
+ $CurrentBlock = null;
127
+
128
+ foreach ($lines as $line)
129
+ {
130
+ if (chop($line) === '')
131
+ {
132
+ if (isset($CurrentBlock))
133
+ {
134
+ $CurrentBlock['interrupted'] = true;
135
+ }
136
+
137
+ continue;
138
+ }
139
+
140
+ if (strpos($line, "\t") !== false)
141
+ {
142
+ $parts = explode("\t", $line);
143
+
144
+ $line = $parts[0];
145
+
146
+ unset($parts[0]);
147
+
148
+ foreach ($parts as $part)
149
+ {
150
+ $shortage = 4 - mb_strlen($line, 'utf-8') % 4;
151
+
152
+ $line .= str_repeat(' ', $shortage);
153
+ $line .= $part;
154
+ }
155
+ }
156
+
157
+ $indent = 0;
158
+
159
+ while (isset($line[$indent]) and $line[$indent] === ' ')
160
+ {
161
+ $indent ++;
162
+ }
163
+
164
+ $text = $indent > 0 ? substr($line, $indent) : $line;
165
+
166
+ # ~
167
+
168
+ $Line = array('body' => $line, 'indent' => $indent, 'text' => $text);
169
+
170
+ # ~
171
+
172
+ if (isset($CurrentBlock['incomplete']))
173
+ {
174
+ $Block = $this->{'block'.$CurrentBlock['type'].'Continue'}($Line, $CurrentBlock);
175
+
176
+ if (isset($Block))
177
+ {
178
+ $CurrentBlock = $Block;
179
+
180
+ continue;
181
+ }
182
+ else
183
+ {
184
+ if (method_exists($this, 'block'.$CurrentBlock['type'].'Complete'))
185
+ {
186
+ $CurrentBlock = $this->{'block'.$CurrentBlock['type'].'Complete'}($CurrentBlock);
187
+ }
188
+
189
+ unset($CurrentBlock['incomplete']);
190
+ }
191
+ }
192
+
193
+ # ~
194
+
195
+ $marker = $text[0];
196
+
197
+ # ~
198
+
199
+ $blockTypes = $this->unmarkedBlockTypes;
200
+
201
+ if (isset($this->BlockTypes[$marker]))
202
+ {
203
+ foreach ($this->BlockTypes[$marker] as $blockType)
204
+ {
205
+ $blockTypes []= $blockType;
206
+ }
207
+ }
208
+
209
+ #
210
+ # ~
211
+
212
+ foreach ($blockTypes as $blockType)
213
+ {
214
+ $Block = $this->{'block'.$blockType}($Line, $CurrentBlock);
215
+
216
+ if (isset($Block))
217
+ {
218
+ $Block['type'] = $blockType;
219
+
220
+ if ( ! isset($Block['identified']))
221
+ {
222
+ $Blocks []= $CurrentBlock;
223
+
224
+ $Block['identified'] = true;
225
+ }
226
+
227
+ if (method_exists($this, 'block'.$blockType.'Continue'))
228
+ {
229
+ $Block['incomplete'] = true;
230
+ }
231
+
232
+ $CurrentBlock = $Block;
233
+
234
+ continue 2;
235
+ }
236
+ }
237
+
238
+ # ~
239
+
240
+ if (isset($CurrentBlock) and ! isset($CurrentBlock['type']) and ! isset($CurrentBlock['interrupted']))
241
+ {
242
+ $CurrentBlock['element']['text'] .= "\n".$text;
243
+ }
244
+ else
245
+ {
246
+ $Blocks []= $CurrentBlock;
247
+
248
+ $CurrentBlock = $this->paragraph($Line);
249
+
250
+ $CurrentBlock['identified'] = true;
251
+ }
252
+ }
253
+
254
+ # ~
255
+
256
+ if (isset($CurrentBlock['incomplete']) and method_exists($this, 'block'.$CurrentBlock['type'].'Complete'))
257
+ {
258
+ $CurrentBlock = $this->{'block'.$CurrentBlock['type'].'Complete'}($CurrentBlock);
259
+ }
260
+
261
+ # ~
262
+
263
+ $Blocks []= $CurrentBlock;
264
+
265
+ unset($Blocks[0]);
266
+
267
+ # ~
268
+
269
+ $markup = '';
270
+
271
+ foreach ($Blocks as $Block)
272
+ {
273
+ if (isset($Block['hidden']))
274
+ {
275
+ continue;
276
+ }
277
+
278
+ $markup .= "\n";
279
+ $markup .= isset($Block['markup']) ? $Block['markup'] : $this->element($Block['element']);
280
+ }
281
+
282
+ $markup .= "\n";
283
+
284
+ # ~
285
+
286
+ return $markup;
287
+ }
288
+
289
+ #
290
+ # Code
291
+
292
+ protected function blockCode($Line, $Block = null)
293
+ {
294
+ if (isset($Block) and ! isset($Block['type']) and ! isset($Block['interrupted']))
295
+ {
296
+ return;
297
+ }
298
+
299
+ if ($Line['indent'] >= 4)
300
+ {
301
+ $text = substr($Line['body'], 4);
302
+
303
+ $Block = array(
304
+ 'element' => array(
305
+ 'name' => 'pre',
306
+ 'handler' => 'element',
307
+ 'text' => array(
308
+ 'name' => 'code',
309
+ 'text' => $text,
310
+ ),
311
+ ),
312
+ );
313
+
314
+ return $Block;
315
+ }
316
+ }
317
+
318
+ protected function blockCodeContinue($Line, $Block)
319
+ {
320
+ if ($Line['indent'] >= 4)
321
+ {
322
+ if (isset($Block['interrupted']))
323
+ {
324
+ $Block['element']['text']['text'] .= "\n";
325
+
326
+ unset($Block['interrupted']);
327
+ }
328
+
329
+ $Block['element']['text']['text'] .= "\n";
330
+
331
+ $text = substr($Line['body'], 4);
332
+
333
+ $Block['element']['text']['text'] .= $text;
334
+
335
+ return $Block;
336
+ }
337
+ }
338
+
339
+ protected function blockCodeComplete($Block)
340
+ {
341
+ $text = $Block['element']['text']['text'];
342
+
343
+ $text = htmlspecialchars($text, ENT_NOQUOTES, 'UTF-8');
344
+
345
+ $Block['element']['text']['text'] = $text;
346
+
347
+ return $Block;
348
+ }
349
+
350
+ #
351
+ # Comment
352
+
353
+ protected function blockComment($Line)
354
+ {
355
+ if ($this->markupEscaped)
356
+ {
357
+ return;
358
+ }
359
+
360
+ if (isset($Line['text'][3]) and $Line['text'][3] === '-' and $Line['text'][2] === '-' and $Line['text'][1] === '!')
361
+ {
362
+ $Block = array(
363
+ 'markup' => $Line['body'],
364
+ );
365
+
366
+ if (preg_match('/-->$/', $Line['text']))
367
+ {
368
+ $Block['closed'] = true;
369
+ }
370
+
371
+ return $Block;
372
+ }
373
+ }
374
+
375
+ protected function blockCommentContinue($Line, array $Block)
376
+ {
377
+ if (isset($Block['closed']))
378
+ {
379
+ return;
380
+ }
381
+
382
+ $Block['markup'] .= "\n" . $Line['body'];
383
+
384
+ if (preg_match('/-->$/', $Line['text']))
385
+ {
386
+ $Block['closed'] = true;
387
+ }
388
+
389
+ return $Block;
390
+ }
391
+
392
+ #
393
+ # Fenced Code
394
+
395
+ protected function blockFencedCode($Line)
396
+ {
397
+ if (preg_match('/^(['.$Line['text'][0].']{3,})[ ]*([\w-]+)?[ ]*$/', $Line['text'], $matches))
398
+ {
399
+ $Element = array(
400
+ 'name' => 'code',
401
+ 'text' => '',
402
+ );
403
+
404
+ if (isset($matches[2]))
405
+ {
406
+ $class = 'language-'.$matches[2];
407
+
408
+ $Element['attributes'] = array(
409
+ 'class' => $class,
410
+ );
411
+ }
412
+
413
+ $Block = array(
414
+ 'char' => $Line['text'][0],
415
+ 'element' => array(
416
+ 'name' => 'pre',
417
+ 'handler' => 'element',
418
+ 'text' => $Element,
419
+ ),
420
+ );
421
+
422
+ return $Block;
423
+ }
424
+ }
425
+
426
+ protected function blockFencedCodeContinue($Line, $Block)
427
+ {
428
+ if (isset($Block['complete']))
429
+ {
430
+ return;
431
+ }
432
+
433
+ if (isset($Block['interrupted']))
434
+ {
435
+ $Block['element']['text']['text'] .= "\n";
436
+
437
+ unset($Block['interrupted']);
438
+ }
439
+
440
+ if (preg_match('/^'.$Block['char'].'{3,}[ ]*$/', $Line['text']))
441
+ {
442
+ $Block['element']['text']['text'] = substr($Block['element']['text']['text'], 1);
443
+
444
+ $Block['complete'] = true;
445
+
446
+ return $Block;
447
+ }
448
+
449
+ $Block['element']['text']['text'] .= "\n".$Line['body'];;
450
+
451
+ return $Block;
452
+ }
453
+
454
+ protected function blockFencedCodeComplete($Block)
455
+ {
456
+ $text = $Block['element']['text']['text'];
457
+
458
+ $text = htmlspecialchars($text, ENT_NOQUOTES, 'UTF-8');
459
+
460
+ $Block['element']['text']['text'] = $text;
461
+
462
+ return $Block;
463
+ }
464
+
465
+ #
466
+ # Header
467
+
468
+ protected function blockHeader($Line)
469
+ {
470
+ if (isset($Line['text'][1]))
471
+ {
472
+ $level = 1;
473
+
474
+ while (isset($Line['text'][$level]) and $Line['text'][$level] === '#')
475
+ {
476
+ $level ++;
477
+ }
478
+
479
+ if ($level > 6)
480
+ {
481
+ return;
482
+ }
483
+
484
+ $text = trim($Line['text'], '# ');
485
+
486
+ $Block = array(
487
+ 'element' => array(
488
+ 'name' => 'h' . min(6, $level),
489
+ 'text' => $text,
490
+ 'handler' => 'line',
491
+ ),
492
+ );
493
+
494
+ return $Block;
495
+ }
496
+ }
497
+
498
+ #
499
+ # List
500
+
501
+ protected function blockList($Line)
502
+ {
503
+ list($name, $pattern) = $Line['text'][0] <= '-' ? array('ul', '[*+-]') : array('ol', '[0-9]+[.]');
504
+
505
+ if (preg_match('/^('.$pattern.'[ ]+)(.*)/', $Line['text'], $matches))
506
+ {
507
+ $Block = array(
508
+ 'indent' => $Line['indent'],
509
+ 'pattern' => $pattern,
510
+ 'element' => array(
511
+ 'name' => $name,
512
+ 'handler' => 'elements',
513
+ ),
514
+ );
515
+
516
+ $Block['li'] = array(
517
+ 'name' => 'li',
518
+ 'handler' => 'li',
519
+ 'text' => array(
520
+ $matches[2],
521
+ ),
522
+ );
523
+
524
+ $Block['element']['text'] []= & $Block['li'];
525
+
526
+ return $Block;
527
+ }
528
+ }
529
+
530
+ protected function blockListContinue($Line, array $Block)
531
+ {
532
+ if ($Block['indent'] === $Line['indent'] and preg_match('/^'.$Block['pattern'].'(?:[ ]+(.*)|$)/', $Line['text'], $matches))
533
+ {
534
+ if (isset($Block['interrupted']))
535
+ {
536
+ $Block['li']['text'] []= '';
537
+
538
+ unset($Block['interrupted']);
539
+ }
540
+
541
+ unset($Block['li']);
542
+
543
+ $text = isset($matches[1]) ? $matches[1] : '';
544
+
545
+ $Block['li'] = array(
546
+ 'name' => 'li',
547
+ 'handler' => 'li',
548
+ 'text' => array(
549
+ $text,
550
+ ),
551
+ );
552
+
553
+ $Block['element']['text'] []= & $Block['li'];
554
+
555
+ return $Block;
556
+ }
557
+
558
+ if ($Line['text'][0] === '[' and $this->blockReference($Line))
559
+ {
560
+ return $Block;
561
+ }
562
+
563
+ if ( ! isset($Block['interrupted']))
564
+ {
565
+ $text = preg_replace('/^[ ]{0,4}/', '', $Line['body']);
566
+
567
+ $Block['li']['text'] []= $text;
568
+
569
+ return $Block;
570
+ }
571
+
572
+ if ($Line['indent'] > 0)
573
+ {
574
+ $Block['li']['text'] []= '';
575
+
576
+ $text = preg_replace('/^[ ]{0,4}/', '', $Line['body']);
577
+
578
+ $Block['li']['text'] []= $text;
579
+
580
+ unset($Block['interrupted']);
581
+
582
+ return $Block;
583
+ }
584
+ }
585
+
586
+ #
587
+ # Quote
588
+
589
+ protected function blockQuote($Line)
590
+ {
591
+ if (preg_match('/^>[ ]?(.*)/', $Line['text'], $matches))
592
+ {
593
+ $Block = array(
594
+ 'element' => array(
595
+ 'name' => 'blockquote',
596
+ 'handler' => 'lines',
597
+ 'text' => (array) $matches[1],
598
+ ),
599
+ );
600
+
601
+ return $Block;
602
+ }
603
+ }
604
+
605
+ protected function blockQuoteContinue($Line, array $Block)
606
+ {
607
+ if ($Line['text'][0] === '>' and preg_match('/^>[ ]?(.*)/', $Line['text'], $matches))
608
+ {
609
+ if (isset($Block['interrupted']))
610
+ {
611
+ $Block['element']['text'] []= '';
612
+
613
+ unset($Block['interrupted']);
614
+ }
615
+
616
+ $Block['element']['text'] []= $matches[1];
617
+
618
+ return $Block;
619
+ }
620
+
621
+ if ( ! isset($Block['interrupted']))
622
+ {
623
+ $Block['element']['text'] []= $Line['text'];
624
+
625
+ return $Block;
626
+ }
627
+ }
628
+
629
+ #
630
+ # Rule
631
+
632
+ protected function blockRule($Line)
633
+ {
634
+ if (preg_match('/^(['.$Line['text'][0].'])([ ]*\1){2,}[ ]*$/', $Line['text']))
635
+ {
636
+ $Block = array(
637
+ 'element' => array(
638
+ 'name' => 'hr'
639
+ ),
640
+ );
641
+
642
+ return $Block;
643
+ }
644
+ }
645
+
646
+ #
647
+ # Setext
648
+
649
+ protected function blockSetextHeader($Line, array $Block = null)
650
+ {
651
+ if ( ! isset($Block) or isset($Block['type']) or isset($Block['interrupted']))
652
+ {
653
+ return;
654
+ }
655
+
656
+ if (chop($Line['text'], $Line['text'][0]) === '')
657
+ {
658
+ $Block['element']['name'] = $Line['text'][0] === '=' ? 'h1' : 'h2';
659
+
660
+ return $Block;
661
+ }
662
+ }
663
+
664
+ #
665
+ # Markup
666
+
667
+ protected function blockMarkup($Line)
668
+ {
669
+ if ($this->markupEscaped)
670
+ {
671
+ return;
672
+ }
673
+
674
+ if (preg_match('/^<(\w*)(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*(\/)?>/', $Line['text'], $matches))
675
+ {
676
+ if (in_array($matches[1], $this->textLevelElements))
677
+ {
678
+ return;
679
+ }
680
+
681
+ $Block = array(
682
+ 'name' => $matches[1],
683
+ 'depth' => 0,
684
+ 'markup' => $Line['text'],
685
+ );
686
+
687
+ $length = strlen($matches[0]);
688
+
689
+ $remainder = substr($Line['text'], $length);
690
+
691
+ if (trim($remainder) === '')
692
+ {
693
+ if (isset($matches[2]) or in_array($matches[1], $this->voidElements))
694
+ {
695
+ $Block['closed'] = true;
696
+
697
+ $Block['void'] = true;
698
+ }
699
+ }
700
+ else
701
+ {
702
+ if (isset($matches[2]) or in_array($matches[1], $this->voidElements))
703
+ {
704
+ return;
705
+ }
706
+
707
+ if (preg_match('/<\/'.$matches[1].'>[ ]*$/i', $remainder))
708
+ {
709
+ $Block['closed'] = true;
710
+ }
711
+ }
712
+
713
+ return $Block;
714
+ }
715
+ }
716
+
717
+ protected function blockMarkupContinue($Line, array $Block)
718
+ {
719
+ if (isset($Block['closed']))
720
+ {
721
+ return;
722
+ }
723
+
724
+ if (preg_match('/^<'.$Block['name'].'(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*>/i', $Line['text'])) # open
725
+ {
726
+ $Block['depth'] ++;
727
+ }
728
+
729
+ if (preg_match('/(.*?)<\/'.$Block['name'].'>[ ]*$/i', $Line['text'], $matches)) # close
730
+ {
731
+ if ($Block['depth'] > 0)
732
+ {
733
+ $Block['depth'] --;
734
+ }
735
+ else
736
+ {
737
+ $Block['closed'] = true;
738
+ }
739
+
740
+ $Block['markup'] .= $matches[1];
741
+ }
742
+
743
+ if (isset($Block['interrupted']))
744
+ {
745
+ $Block['markup'] .= "\n";
746
+
747
+ unset($Block['interrupted']);
748
+ }
749
+
750
+ $Block['markup'] .= "\n".$Line['body'];
751
+
752
+ return $Block;
753
+ }
754
+
755
+ #
756
+ # Reference
757
+
758
+ protected function blockReference($Line)
759
+ {
760
+ if (preg_match('/^\[(.+?)\]:[ ]*<?(\S+?)>?(?:[ ]+["\'(](.+)["\')])?[ ]*$/', $Line['text'], $matches))
761
+ {
762
+ $id = strtolower($matches[1]);
763
+
764
+ $Data = array(
765
+ 'url' => $matches[2],
766
+ 'title' => null,
767
+ );
768
+
769
+ if (isset($matches[3]))
770
+ {
771
+ $Data['title'] = $matches[3];
772
+ }
773
+
774
+ $this->DefinitionData['Reference'][$id] = $Data;
775
+
776
+ $Block = array(
777
+ 'hidden' => true,
778
+ );
779
+
780
+ return $Block;
781
+ }
782
+ }
783
+
784
+ #
785
+ # Table
786
+
787
+ protected function blockTable($Line, array $Block = null)
788
+ {
789
+ if ( ! isset($Block) or isset($Block['type']) or isset($Block['interrupted']))
790
+ {
791
+ return;
792
+ }
793
+
794
+ if (strpos($Block['element']['text'], '|') !== false and chop($Line['text'], ' -:|') === '')
795
+ {
796
+ $alignments = array();
797
+
798
+ $divider = $Line['text'];
799
+
800
+ $divider = trim($divider);
801
+ $divider = trim($divider, '|');
802
+
803
+ $dividerCells = explode('|', $divider);
804
+
805
+ foreach ($dividerCells as $dividerCell)
806
+ {
807
+ $dividerCell = trim($dividerCell);
808
+
809
+ if ($dividerCell === '')
810
+ {
811
+ continue;
812
+ }
813
+
814
+ $alignment = null;
815
+
816
+ if ($dividerCell[0] === ':')
817
+ {
818
+ $alignment = 'left';
819
+ }
820
+
821
+ if (substr($dividerCell, - 1) === ':')
822
+ {
823
+ $alignment = $alignment === 'left' ? 'center' : 'right';
824
+ }
825
+
826
+ $alignments []= $alignment;
827
+ }
828
+
829
+ # ~
830
+
831
+ $HeaderElements = array();
832
+
833
+ $header = $Block['element']['text'];
834
+
835
+ $header = trim($header);
836
+ $header = trim($header, '|');
837
+
838
+ $headerCells = explode('|', $header);
839
+
840
+ foreach ($headerCells as $index => $headerCell)
841
+ {
842
+ $headerCell = trim($headerCell);
843
+
844
+ $HeaderElement = array(
845
+ 'name' => 'th',
846
+ 'text' => $headerCell,
847
+ 'handler' => 'line',
848
+ );
849
+
850
+ if (isset($alignments[$index]))
851
+ {
852
+ $alignment = $alignments[$index];
853
+
854
+ $HeaderElement['attributes'] = array(
855
+ 'style' => 'text-align: '.$alignment.';',
856
+ );
857
+ }
858
+
859
+ $HeaderElements []= $HeaderElement;
860
+ }
861
+
862
+ # ~
863
+
864
+ $Block = array(
865
+ 'alignments' => $alignments,
866
+ 'identified' => true,
867
+ 'element' => array(
868
+ 'name' => 'table',
869
+ 'handler' => 'elements',
870
+ ),
871
+ );
872
+
873
+ $Block['element']['text'] []= array(
874
+ 'name' => 'thead',
875
+ 'handler' => 'elements',
876
+ );
877
+
878
+ $Block['element']['text'] []= array(
879
+ 'name' => 'tbody',
880
+ 'handler' => 'elements',
881
+ 'text' => array(),
882
+ );
883
+
884
+ $Block['element']['text'][0]['text'] []= array(
885
+ 'name' => 'tr',
886
+ 'handler' => 'elements',
887
+ 'text' => $HeaderElements,
888
+ );
889
+
890
+ return $Block;
891
+ }
892
+ }
893
+
894
+ protected function blockTableContinue($Line, array $Block)
895
+ {
896
+ if (isset($Block['interrupted']))
897
+ {
898
+ return;
899
+ }
900
+
901
+ if ($Line['text'][0] === '|' or strpos($Line['text'], '|'))
902
+ {
903
+ $Elements = array();
904
+
905
+ $row = $Line['text'];
906
+
907
+ $row = trim($row);
908
+ $row = trim($row, '|');
909
+
910
+ preg_match_all('/(?:(\\\\[|])|[^|`]|`[^`]+`|`)+/', $row, $matches);
911
+
912
+ foreach ($matches[0] as $index => $cell)
913
+ {
914
+ $cell = trim($cell);
915
+
916
+ $Element = array(
917
+ 'name' => 'td',
918
+ 'handler' => 'line',
919
+ 'text' => $cell,
920
+ );
921
+
922
+ if (isset($Block['alignments'][$index]))
923
+ {
924
+ $Element['attributes'] = array(
925
+ 'style' => 'text-align: '.$Block['alignments'][$index].';',
926
+ );
927
+ }
928
+
929
+ $Elements []= $Element;
930
+ }
931
+
932
+ $Element = array(
933
+ 'name' => 'tr',
934
+ 'handler' => 'elements',
935
+ 'text' => $Elements,
936
+ );
937
+
938
+ $Block['element']['text'][1]['text'] []= $Element;
939
+
940
+ return $Block;
941
+ }
942
+ }
943
+
944
+ #
945
+ # ~
946
+ #
947
+
948
+ protected function paragraph($Line)
949
+ {
950
+ $Block = array(
951
+ 'element' => array(
952
+ 'name' => 'p',
953
+ 'text' => $Line['text'],
954
+ 'handler' => 'line',
955
+ ),
956
+ );
957
+
958
+ return $Block;
959
+ }
960
+
961
+ #
962
+ # Inline Elements
963
+ #
964
+
965
+ protected $InlineTypes = array(
966
+ '"' => array('SpecialCharacter'),
967
+ '!' => array('Image'),
968
+ '&' => array('SpecialCharacter'),
969
+ '*' => array('Emphasis'),
970
+ ':' => array('Url'),
971
+ '<' => array('UrlTag', 'EmailTag', 'Markup', 'SpecialCharacter'),
972
+ '>' => array('SpecialCharacter'),
973
+ '[' => array('Link'),
974
+ '_' => array('Emphasis'),
975
+ '`' => array('Code'),
976
+ '~' => array('Strikethrough'),
977
+ '\\' => array('EscapeSequence'),
978
+ );
979
+
980
+ # ~
981
+
982
+ protected $inlineMarkerList = '!"*_&[:<>`~\\';
983
+
984
+ #
985
+ # ~
986
+ #
987
+
988
+ public function line($text)
989
+ {
990
+ $markup = '';
991
+
992
+ $unexaminedText = $text;
993
+
994
+ $markerPosition = 0;
995
+
996
+ while ($excerpt = strpbrk($unexaminedText, $this->inlineMarkerList))
997
+ {
998
+ $marker = $excerpt[0];
999
+
1000
+ $markerPosition += strpos($unexaminedText, $marker);
1001
+
1002
+ $Excerpt = array('text' => $excerpt, 'context' => $text);
1003
+
1004
+ foreach ($this->InlineTypes[$marker] as $inlineType)
1005
+ {
1006
+ $Inline = $this->{'inline'.$inlineType}($Excerpt);
1007
+
1008
+ if ( ! isset($Inline))
1009
+ {
1010
+ continue;
1011
+ }
1012
+
1013
+ if (isset($Inline['position']) and $Inline['position'] > $markerPosition) # position is ahead of marker
1014
+ {
1015
+ continue;
1016
+ }
1017
+
1018
+ if ( ! isset($Inline['position']))
1019
+ {
1020
+ $Inline['position'] = $markerPosition;
1021
+ }
1022
+
1023
+ $unmarkedText = substr($text, 0, $Inline['position']);
1024
+
1025
+ $markup .= $this->unmarkedText($unmarkedText);
1026
+
1027
+ $markup .= isset($Inline['markup']) ? $Inline['markup'] : $this->element($Inline['element']);
1028
+
1029
+ $text = substr($text, $Inline['position'] + $Inline['extent']);
1030
+
1031
+ $unexaminedText = $text;
1032
+
1033
+ $markerPosition = 0;
1034
+
1035
+ continue 2;
1036
+ }
1037
+
1038
+ $unexaminedText = substr($excerpt, 1);
1039
+
1040
+ $markerPosition ++;
1041
+ }
1042
+
1043
+ $markup .= $this->unmarkedText($text);
1044
+
1045
+ return $markup;
1046
+ }
1047
+
1048
+ #
1049
+ # ~
1050
+ #
1051
+
1052
+ protected function inlineCode($Excerpt)
1053
+ {
1054
+ $marker = $Excerpt['text'][0];
1055
+
1056
+ if (preg_match('/^('.$marker.'+)[ ]*(.+?)[ ]*(?<!'.$marker.')\1(?!'.$marker.')/s', $Excerpt['text'], $matches))
1057
+ {
1058
+ $text = $matches[2];
1059
+ $text = htmlspecialchars($text, ENT_NOQUOTES, 'UTF-8');
1060
+ $text = preg_replace("/[ ]*\n/", ' ', $text);
1061
+
1062
+ return array(
1063
+ 'extent' => strlen($matches[0]),
1064
+ 'element' => array(
1065
+ 'name' => 'code',
1066
+ 'text' => $text,
1067
+ ),
1068
+ );
1069
+ }
1070
+ }
1071
+
1072
+ protected function inlineEmailTag($Excerpt)
1073
+ {
1074
+ if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<((mailto:)?\S+?@\S+?)>/i', $Excerpt['text'], $matches))
1075
+ {
1076
+ $url = $matches[1];
1077
+
1078
+ if ( ! isset($matches[2]))
1079
+ {
1080
+ $url = 'mailto:' . $url;
1081
+ }
1082
+
1083
+ return array(
1084
+ 'extent' => strlen($matches[0]),
1085
+ 'element' => array(
1086
+ 'name' => 'a',
1087
+ 'text' => $matches[1],
1088
+ 'attributes' => array(
1089
+ 'href' => $url,
1090
+ ),
1091
+ ),
1092
+ );
1093
+ }
1094
+ }
1095
+
1096
+ protected function inlineEmphasis($Excerpt)
1097
+ {
1098
+ if ( ! isset($Excerpt['text'][1]))
1099
+ {
1100
+ return;
1101
+ }
1102
+
1103
+ $marker = $Excerpt['text'][0];
1104
+
1105
+ if ($Excerpt['text'][1] === $marker and preg_match($this->StrongRegex[$marker], $Excerpt['text'], $matches))
1106
+ {
1107
+ $emphasis = 'strong';
1108
+ }
1109
+ elseif (preg_match($this->EmRegex[$marker], $Excerpt['text'], $matches))
1110
+ {
1111
+ $emphasis = 'em';
1112
+ }
1113
+ else
1114
+ {
1115
+ return;
1116
+ }
1117
+
1118
+ return array(
1119
+ 'extent' => strlen($matches[0]),
1120
+ 'element' => array(
1121
+ 'name' => $emphasis,
1122
+ 'handler' => 'line',
1123
+ 'text' => $matches[1],
1124
+ ),
1125
+ );
1126
+ }
1127
+
1128
+ protected function inlineEscapeSequence($Excerpt)
1129
+ {
1130
+ if (isset($Excerpt['text'][1]) and in_array($Excerpt['text'][1], $this->specialCharacters))
1131
+ {
1132
+ return array(
1133
+ 'markup' => $Excerpt['text'][1],
1134
+ 'extent' => 2,
1135
+ );
1136
+ }
1137
+ }
1138
+
1139
+ protected function inlineImage($Excerpt)
1140
+ {
1141
+ if ( ! isset($Excerpt['text'][1]) or $Excerpt['text'][1] !== '[')
1142
+ {
1143
+ return;
1144
+ }
1145
+
1146
+ $Excerpt['text']= substr($Excerpt['text'], 1);
1147
+
1148
+ $Link = $this->inlineLink($Excerpt);
1149
+
1150
+ if ($Link === null)
1151
+ {
1152
+ return;
1153
+ }
1154
+
1155
+ $Inline = array(
1156
+ 'extent' => $Link['extent'] + 1,
1157
+ 'element' => array(
1158
+ 'name' => 'img',
1159
+ 'attributes' => array(
1160
+ 'src' => $Link['element']['attributes']['href'],
1161
+ 'alt' => $Link['element']['text'],
1162
+ ),
1163
+ ),
1164
+ );
1165
+
1166
+ $Inline['element']['attributes'] += $Link['element']['attributes'];
1167
+
1168
+ unset($Inline['element']['attributes']['href']);
1169
+
1170
+ return $Inline;
1171
+ }
1172
+
1173
+ protected function inlineLink($Excerpt)
1174
+ {
1175
+ $Element = array(
1176
+ 'name' => 'a',
1177
+ 'handler' => 'line',
1178
+ 'text' => null,
1179
+ 'attributes' => array(
1180
+ 'href' => null,
1181
+ 'title' => null,
1182
+ ),
1183
+ );
1184
+
1185
+ $extent = 0;
1186
+
1187
+ $remainder = $Excerpt['text'];
1188
+
1189
+ if (preg_match('/\[((?:[^][]|(?R))*)\]/', $remainder, $matches))
1190
+ {
1191
+ $Element['text'] = $matches[1];
1192
+
1193
+ $extent += strlen($matches[0]);
1194
+
1195
+ $remainder = substr($remainder, $extent);
1196
+ }
1197
+ else
1198
+ {
1199
+ return;
1200
+ }
1201
+
1202
+ if (preg_match('/^[(]((?:[^ (]|[(][^ )]+[)])+)(?:[ ]+("[^"]+"|\'[^\']+\'))?[)]/', $remainder, $matches))
1203
+ {
1204
+ $Element['attributes']['href'] = $matches[1];
1205
+
1206
+ if (isset($matches[2]))
1207
+ {
1208
+ $Element['attributes']['title'] = substr($matches[2], 1, - 1);
1209
+ }
1210
+
1211
+ $extent += strlen($matches[0]);
1212
+ }
1213
+ else
1214
+ {
1215
+ if (preg_match('/^\s*\[(.*?)\]/', $remainder, $matches))
1216
+ {
1217
+ $definition = $matches[1] ? $matches[1] : $Element['text'];
1218
+ $definition = strtolower($definition);
1219
+
1220
+ $extent += strlen($matches[0]);
1221
+ }
1222
+ else
1223
+ {
1224
+ $definition = strtolower($Element['text']);
1225
+ }
1226
+
1227
+ if ( ! isset($this->DefinitionData['Reference'][$definition]))
1228
+ {
1229
+ return;
1230
+ }
1231
+
1232
+ $Definition = $this->DefinitionData['Reference'][$definition];
1233
+
1234
+ $Element['attributes']['href'] = $Definition['url'];
1235
+ $Element['attributes']['title'] = $Definition['title'];
1236
+ }
1237
+
1238
+ $Element['attributes']['href'] = str_replace(array('&', '<'), array('&amp;', '&lt;'), $Element['attributes']['href']);
1239
+
1240
+ return array(
1241
+ 'extent' => $extent,
1242
+ 'element' => $Element,
1243
+ );
1244
+ }
1245
+
1246
+ protected function inlineMarkup($Excerpt)
1247
+ {
1248
+ if ($this->markupEscaped or strpos($Excerpt['text'], '>') === false)
1249
+ {
1250
+ return;
1251
+ }
1252
+
1253
+ if ($Excerpt['text'][1] === '/' and preg_match('/^<\/\w*[ ]*>/s', $Excerpt['text'], $matches))
1254
+ {
1255
+ return array(
1256
+ 'markup' => $matches[0],
1257
+ 'extent' => strlen($matches[0]),
1258
+ );
1259
+ }
1260
+
1261
+ if ($Excerpt['text'][1] === '!' and preg_match('/^<!---?[^>-](?:-?[^-])*-->/s', $Excerpt['text'], $matches))
1262
+ {
1263
+ return array(
1264
+ 'markup' => $matches[0],
1265
+ 'extent' => strlen($matches[0]),
1266
+ );
1267
+ }
1268
+
1269
+ if ($Excerpt['text'][1] !== ' ' and preg_match('/^<\w*(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*\/?>/s', $Excerpt['text'], $matches))
1270
+ {
1271
+ return array(
1272
+ 'markup' => $matches[0],
1273
+ 'extent' => strlen($matches[0]),
1274
+ );
1275
+ }
1276
+ }
1277
+
1278
+ protected function inlineSpecialCharacter($Excerpt)
1279
+ {
1280
+ if ($Excerpt['text'][0] === '&' and ! preg_match('/^&#?\w+;/', $Excerpt['text']))
1281
+ {
1282
+ return array(
1283
+ 'markup' => '&amp;',
1284
+ 'extent' => 1,
1285
+ );
1286
+ }
1287
+
1288
+ $SpecialCharacter = array('>' => 'gt', '<' => 'lt', '"' => 'quot');
1289
+
1290
+ if (isset($SpecialCharacter[$Excerpt['text'][0]]))
1291
+ {
1292
+ return array(
1293
+ 'markup' => '&'.$SpecialCharacter[$Excerpt['text'][0]].';',
1294
+ 'extent' => 1,
1295
+ );
1296
+ }
1297
+ }
1298
+
1299
+ protected function inlineStrikethrough($Excerpt)
1300
+ {
1301
+ if ( ! isset($Excerpt['text'][1]))
1302
+ {
1303
+ return;
1304
+ }
1305
+
1306
+ if ($Excerpt['text'][1] === '~' and preg_match('/^~~(?=\S)(.+?)(?<=\S)~~/', $Excerpt['text'], $matches))
1307
+ {
1308
+ return array(
1309
+ 'extent' => strlen($matches[0]),
1310
+ 'element' => array(
1311
+ 'name' => 'del',
1312
+ 'text' => $matches[1],
1313
+ 'handler' => 'line',
1314
+ ),
1315
+ );
1316
+ }
1317
+ }
1318
+
1319
+ protected function inlineUrl($Excerpt)
1320
+ {
1321
+ if ($this->urlsLinked !== true or ! isset($Excerpt['text'][2]) or $Excerpt['text'][2] !== '/')
1322
+ {
1323
+ return;
1324
+ }
1325
+
1326
+ if (preg_match('/\bhttps?:[\/]{2}[^\s<]+\b\/*/ui', $Excerpt['context'], $matches, PREG_OFFSET_CAPTURE))
1327
+ {
1328
+ $Inline = array(
1329
+ 'extent' => strlen($matches[0][0]),
1330
+ 'position' => $matches[0][1],
1331
+ 'element' => array(
1332
+ 'name' => 'a',
1333
+ 'text' => $matches[0][0],
1334
+ 'attributes' => array(
1335
+ 'href' => $matches[0][0],
1336
+ ),
1337
+ ),
1338
+ );
1339
+
1340
+ return $Inline;
1341
+ }
1342
+ }
1343
+
1344
+ protected function inlineUrlTag($Excerpt)
1345
+ {
1346
+ if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<(\w+:\/{2}[^ >]+)>/i', $Excerpt['text'], $matches))
1347
+ {
1348
+ $url = str_replace(array('&', '<'), array('&amp;', '&lt;'), $matches[1]);
1349
+
1350
+ return array(
1351
+ 'extent' => strlen($matches[0]),
1352
+ 'element' => array(
1353
+ 'name' => 'a',
1354
+ 'text' => $url,
1355
+ 'attributes' => array(
1356
+ 'href' => $url,
1357
+ ),
1358
+ ),
1359
+ );
1360
+ }
1361
+ }
1362
+
1363
+ #
1364
+ # ~
1365
+
1366
+ protected $unmarkedInlineTypes = array("\n" => 'Break', '://' => 'Url');
1367
+
1368
+ # ~
1369
+
1370
+ protected function unmarkedText($text)
1371
+ {
1372
+ if ($this->breaksEnabled)
1373
+ {
1374
+ $text = preg_replace('/[ ]*\n/', "<br />\n", $text);
1375
+ }
1376
+ else
1377
+ {
1378
+ $text = preg_replace('/(?:[ ][ ]+|[ ]*\\\\)\n/', "<br />\n", $text);
1379
+ $text = str_replace(" \n", "\n", $text);
1380
+ }
1381
+
1382
+ return $text;
1383
+ }
1384
+
1385
+ #
1386
+ # Handlers
1387
+ #
1388
+
1389
+ protected function element(array $Element)
1390
+ {
1391
+ $markup = '<'.$Element['name'];
1392
+
1393
+ if (isset($Element['attributes']))
1394
+ {
1395
+ foreach ($Element['attributes'] as $name => $value)
1396
+ {
1397
+ if ($value === null)
1398
+ {
1399
+ continue;
1400
+ }
1401
+
1402
+ $markup .= ' '.$name.'="'.$value.'"';
1403
+ }
1404
+ }
1405
+
1406
+ if (isset($Element['text']))
1407
+ {
1408
+ $markup .= '>';
1409
+
1410
+ if (isset($Element['handler']))
1411
+ {
1412
+ $markup .= $this->$Element['handler']($Element['text']);
1413
+ }
1414
+ else
1415
+ {
1416
+ $markup .= $Element['text'];
1417
+ }
1418
+
1419
+ $markup .= '</'.$Element['name'].'>';
1420
+ }
1421
+ else
1422
+ {
1423
+ $markup .= ' />';
1424
+ }
1425
+
1426
+ return $markup;
1427
+ }
1428
+
1429
+ protected function elements(array $Elements)
1430
+ {
1431
+ $markup = '';
1432
+
1433
+ foreach ($Elements as $Element)
1434
+ {
1435
+ $markup .= "\n" . $this->element($Element);
1436
+ }
1437
+
1438
+ $markup .= "\n";
1439
+
1440
+ return $markup;
1441
+ }
1442
+
1443
+ # ~
1444
+
1445
+ protected function li($lines)
1446
+ {
1447
+ $markup = $this->lines($lines);
1448
+
1449
+ $trimmedMarkup = trim($markup);
1450
+
1451
+ if ( ! in_array('', $lines) and substr($trimmedMarkup, 0, 3) === '<p>')
1452
+ {
1453
+ $markup = $trimmedMarkup;
1454
+ $markup = substr($markup, 3);
1455
+
1456
+ $position = strpos($markup, "</p>");
1457
+
1458
+ $markup = substr_replace($markup, '', $position, 4);
1459
+ }
1460
+
1461
+ return $markup;
1462
+ }
1463
+
1464
+ #
1465
+ # Deprecated Methods
1466
+ #
1467
+
1468
+ function parse($text)
1469
+ {
1470
+ $markup = $this->text($text);
1471
+
1472
+ return $markup;
1473
+ }
1474
+
1475
+ #
1476
+ # Static Methods
1477
+ #
1478
+
1479
+ static function instance($name = 'default')
1480
+ {
1481
+ if (isset(self::$instances[$name]))
1482
+ {
1483
+ return self::$instances[$name];
1484
+ }
1485
+
1486
+ $instance = new self();
1487
+
1488
+ self::$instances[$name] = $instance;
1489
+
1490
+ return $instance;
1491
+ }
1492
+
1493
+ private static $instances = array();
1494
+
1495
+ #
1496
+ # Fields
1497
+ #
1498
+
1499
+ protected $DefinitionData;
1500
+
1501
+ #
1502
+ # Read-Only
1503
+
1504
+ protected $specialCharacters = array(
1505
+ '\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '>', '#', '+', '-', '.', '!', '|',
1506
+ );
1507
+
1508
+ protected $StrongRegex = array(
1509
+ '*' => '/^[*]{2}((?:\\\\\*|[^*]|[*][^*]*[*])+?)[*]{2}(?![*])/s',
1510
+ '_' => '/^__((?:\\\\_|[^_]|_[^_]*_)+?)__(?!_)/us',
1511
+ );
1512
+
1513
+ protected $EmRegex = array(
1514
+ '*' => '/^[*]((?:\\\\\*|[^*]|[*][*][^*]+?[*][*])+?)[*](?![*])/s',
1515
+ '_' => '/^_((?:\\\\_|[^_]|__[^_]*__)+?)_(?!_)\b/us',
1516
+ );
1517
+
1518
+ protected $regexHtmlAttribute = '[a-zA-Z_:][\w:.-]*(?:\s*=\s*(?:[^"\'=<>`\s]+|"[^"]*"|\'[^\']*\'))?';
1519
+
1520
+ protected $voidElements = array(
1521
+ 'area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source',
1522
+ );
1523
+
1524
+ protected $textLevelElements = array(
1525
+ 'a', 'br', 'bdo', 'abbr', 'blink', 'nextid', 'acronym', 'basefont',
1526
+ 'b', 'em', 'big', 'cite', 'small', 'spacer', 'listing',
1527
+ 'i', 'rp', 'del', 'code', 'strike', 'marquee',
1528
+ 'q', 'rt', 'ins', 'font', 'strong',
1529
+ 's', 'tt', 'sub', 'mark',
1530
+ 'u', 'xm', 'sup', 'nobr',
1531
+ 'var', 'ruby',
1532
+ 'wbr', 'span',
1533
+ 'time',
1534
+ );
1535
+ }
includes/plugin-update-checker/vendor/readme-parser.php ADDED
@@ -0,0 +1,331 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * This is a slightly modified version of github.com/markjaquith/WordPress-Plugin-Readme-Parser
5
+ * It uses Parsedown instead of the "Markdown Extra" parser.
6
+ */
7
+
8
+ Class PucReadmeParser {
9
+
10
+ function __construct() {
11
+ // This space intentially blank
12
+ }
13
+
14
+ function parse_readme( $file ) {
15
+ $file_contents = @implode('', @file($file));
16
+ return $this->parse_readme_contents( $file_contents );
17
+ }
18
+
19
+ function parse_readme_contents( $file_contents ) {
20
+ $file_contents = str_replace(array("\r\n", "\r"), "\n", $file_contents);
21
+ $file_contents = trim($file_contents);
22
+ if ( 0 === strpos( $file_contents, "\xEF\xBB\xBF" ) )
23
+ $file_contents = substr( $file_contents, 3 );
24
+
25
+ // Markdown transformations
26
+ $file_contents = preg_replace( "|^###([^#]+)#*?\s*?\n|im", '=$1='."\n", $file_contents );
27
+ $file_contents = preg_replace( "|^##([^#]+)#*?\s*?\n|im", '==$1=='."\n", $file_contents );
28
+ $file_contents = preg_replace( "|^#([^#]+)#*?\s*?\n|im", '===$1==='."\n", $file_contents );
29
+
30
+ // === Plugin Name ===
31
+ // Must be the very first thing.
32
+ if ( !preg_match('|^===(.*)===|', $file_contents, $_name) )
33
+ return array(); // require a name
34
+ $name = trim($_name[1], '=');
35
+ $name = $this->sanitize_text( $name );
36
+
37
+ $file_contents = $this->chop_string( $file_contents, $_name[0] );
38
+
39
+
40
+ // Requires at least: 1.5
41
+ if ( preg_match('|Requires at least:(.*)|i', $file_contents, $_requires_at_least) )
42
+ $requires_at_least = $this->sanitize_text($_requires_at_least[1]);
43
+ else
44
+ $requires_at_least = NULL;
45
+
46
+
47
+ // Tested up to: 2.1
48
+ if ( preg_match('|Tested up to:(.*)|i', $file_contents, $_tested_up_to) )
49
+ $tested_up_to = $this->sanitize_text( $_tested_up_to[1] );
50
+ else
51
+ $tested_up_to = NULL;
52
+
53
+
54
+ // Stable tag: 10.4-ride-the-fire-eagle-danger-day
55
+ if ( preg_match('|Stable tag:(.*)|i', $file_contents, $_stable_tag) )
56
+ $stable_tag = $this->sanitize_text( $_stable_tag[1] );
57
+ else
58
+ $stable_tag = NULL; // we assume trunk, but don't set it here to tell the difference between specified trunk and default trunk
59
+
60
+
61
+ // Tags: some tag, another tag, we like tags
62
+ if ( preg_match('|Tags:(.*)|i', $file_contents, $_tags) ) {
63
+ $tags = preg_split('|,[\s]*?|', trim($_tags[1]));
64
+ foreach ( array_keys($tags) as $t )
65
+ $tags[$t] = $this->sanitize_text( $tags[$t] );
66
+ } else {
67
+ $tags = array();
68
+ }
69
+
70
+
71
+ // Contributors: markjaquith, mdawaffe, zefrank
72
+ $contributors = array();
73
+ if ( preg_match('|Contributors:(.*)|i', $file_contents, $_contributors) ) {
74
+ $temp_contributors = preg_split('|,[\s]*|', trim($_contributors[1]));
75
+ foreach ( array_keys($temp_contributors) as $c ) {
76
+ $tmp_sanitized = $this->user_sanitize( $temp_contributors[$c] );
77
+ if ( strlen(trim($tmp_sanitized)) > 0 )
78
+ $contributors[$c] = $tmp_sanitized;
79
+ unset($tmp_sanitized);
80
+ }
81
+ }
82
+
83
+
84
+ // Donate Link: URL
85
+ if ( preg_match('|Donate link:(.*)|i', $file_contents, $_donate_link) )
86
+ $donate_link = esc_url( $_donate_link[1] );
87
+ else
88
+ $donate_link = NULL;
89
+
90
+
91
+ // togs, conts, etc are optional and order shouldn't matter. So we chop them only after we've grabbed their values.
92
+ foreach ( array('tags', 'contributors', 'requires_at_least', 'tested_up_to', 'stable_tag', 'donate_link') as $chop ) {
93
+ if ( $$chop ) {
94
+ $_chop = '_' . $chop;
95
+ $file_contents = $this->chop_string( $file_contents, ${$_chop}[0] );
96
+ }
97
+ }
98
+
99
+ $file_contents = trim($file_contents);
100
+
101
+
102
+ // short-description fu
103
+ if ( !preg_match('/(^(.*?))^[\s]*=+?[\s]*.+?[\s]*=+?/ms', $file_contents, $_short_description) )
104
+ $_short_description = array( 1 => &$file_contents, 2 => &$file_contents );
105
+ $short_desc_filtered = $this->sanitize_text( $_short_description[2] );
106
+ $short_desc_length = strlen($short_desc_filtered);
107
+ $short_description = substr($short_desc_filtered, 0, 150);
108
+ if ( $short_desc_length > strlen($short_description) )
109
+ $truncated = true;
110
+ else
111
+ $truncated = false;
112
+ if ( $_short_description[1] )
113
+ $file_contents = $this->chop_string( $file_contents, $_short_description[1] ); // yes, the [1] is intentional
114
+
115
+ // == Section ==
116
+ // Break into sections
117
+ // $_sections[0] will be the title of the first section, $_sections[1] will be the content of the first section
118
+ // the array alternates from there: title2, content2, title3, content3... and so forth
119
+ $_sections = preg_split('/^[\s]*==[\s]*(.+?)[\s]*==/m', $file_contents, -1, PREG_SPLIT_DELIM_CAPTURE|PREG_SPLIT_NO_EMPTY);
120
+
121
+ $sections = array();
122
+ for ( $i=1; $i <= count($_sections); $i +=2 ) {
123
+ $_sections[$i] = preg_replace('/^[\s]*=[\s]+(.+?)[\s]+=/m', '<h4>$1</h4>', $_sections[$i]);
124
+ $_sections[$i] = $this->filter_text( $_sections[$i], true );
125
+ $title = $this->sanitize_text( $_sections[$i-1] );
126
+ $sections[str_replace(' ', '_', strtolower($title))] = array('title' => $title, 'content' => $_sections[$i]);
127
+ }
128
+
129
+
130
+ // Special sections
131
+ // This is where we nab our special sections, so we can enforce their order and treat them differently, if needed
132
+ // upgrade_notice is not a section, but parse it like it is for now
133
+ $final_sections = array();
134
+ foreach ( array('description', 'installation', 'frequently_asked_questions', 'screenshots', 'changelog', 'change_log', 'upgrade_notice') as $special_section ) {
135
+ if ( isset($sections[$special_section]) ) {
136
+ $final_sections[$special_section] = $sections[$special_section]['content'];
137
+ unset($sections[$special_section]);
138
+ }
139
+ }
140
+ if ( isset($final_sections['change_log']) && empty($final_sections['changelog']) )
141
+ $final_sections['changelog'] = $final_sections['change_log'];
142
+
143
+
144
+ $final_screenshots = array();
145
+ if ( isset($final_sections['screenshots']) ) {
146
+ preg_match_all('|<li>(.*?)</li>|s', $final_sections['screenshots'], $screenshots, PREG_SET_ORDER);
147
+ if ( $screenshots ) {
148
+ foreach ( (array) $screenshots as $ss )
149
+ $final_screenshots[] = $ss[1];
150
+ }
151
+ }
152
+
153
+ // Parse the upgrade_notice section specially:
154
+ // 1.0 => blah, 1.1 => fnord
155
+ $upgrade_notice = array();
156
+ if ( isset($final_sections['upgrade_notice']) ) {
157
+ $split = preg_split( '#<h4>(.*?)</h4>#', $final_sections['upgrade_notice'], -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY );
158
+ for ( $i = 0; $i < count( $split ); $i += 2 )
159
+ $upgrade_notice[$this->sanitize_text( $split[$i] )] = substr( $this->sanitize_text( $split[$i + 1] ), 0, 300 );
160
+ unset( $final_sections['upgrade_notice'] );
161
+ }
162
+
163
+ // No description?
164
+ // No problem... we'll just fall back to the old style of description
165
+ // We'll even let you use markup this time!
166
+ $excerpt = false;
167
+ if ( !isset($final_sections['description']) ) {
168
+ $final_sections = array_merge(array('description' => $this->filter_text( $_short_description[2], true )), $final_sections);
169
+ $excerpt = true;
170
+ }
171
+
172
+
173
+ // dump the non-special sections into $remaining_content
174
+ // their order will be determined by their original order in the readme.txt
175
+ $remaining_content = '';
176
+ foreach ( $sections as $s_name => $s_data ) {
177
+ $remaining_content .= "\n<h3>{$s_data['title']}</h3>\n{$s_data['content']}";
178
+ }
179
+ $remaining_content = trim($remaining_content);
180
+
181
+
182
+ // All done!
183
+ // $r['tags'] and $r['contributors'] are simple arrays
184
+ // $r['sections'] is an array with named elements
185
+ $r = array(
186
+ 'name' => $name,
187
+ 'tags' => $tags,
188
+ 'requires_at_least' => $requires_at_least,
189
+ 'tested_up_to' => $tested_up_to,
190
+ 'stable_tag' => $stable_tag,
191
+ 'contributors' => $contributors,
192
+ 'donate_link' => $donate_link,
193
+ 'short_description' => $short_description,
194
+ 'screenshots' => $final_screenshots,
195
+ 'is_excerpt' => $excerpt,
196
+ 'is_truncated' => $truncated,
197
+ 'sections' => $final_sections,
198
+ 'remaining_content' => $remaining_content,
199
+ 'upgrade_notice' => $upgrade_notice
200
+ );
201
+
202
+ return $r;
203
+ }
204
+
205
+ function chop_string( $string, $chop ) { // chop a "prefix" from a string: Agressive! uses strstr not 0 === strpos
206
+ if ( $_string = strstr($string, $chop) ) {
207
+ $_string = substr($_string, strlen($chop));
208
+ return trim($_string);
209
+ } else {
210
+ return trim($string);
211
+ }
212
+ }
213
+
214
+ function user_sanitize( $text, $strict = false ) { // whitelisted chars
215
+ if ( function_exists('user_sanitize') ) // bbPress native
216
+ return user_sanitize( $text, $strict );
217
+
218
+ if ( $strict ) {
219
+ $text = preg_replace('/[^a-z0-9-]/i', '', $text);
220
+ $text = preg_replace('|-+|', '-', $text);
221
+ } else {
222
+ $text = preg_replace('/[^a-z0-9_-]/i', '', $text);
223
+ }
224
+ return $text;
225
+ }
226
+
227
+ function sanitize_text( $text ) { // not fancy
228
+ $text = strip_tags($text);
229
+ $text = esc_html($text);
230
+ $text = trim($text);
231
+ return $text;
232
+ }
233
+
234
+ function filter_text( $text, $markdown = false ) { // fancy, Markdown
235
+ $text = trim($text);
236
+
237
+ $text = call_user_func( array( __CLASS__, 'code_trick' ), $text, $markdown ); // A better parser than Markdown's for: backticks -> CODE
238
+
239
+ if ( $markdown ) { // Parse markdown.
240
+ if ( !class_exists('Parsedown', false) ) {
241
+ require_once(dirname(__FILE__) . '/Parsedown' . (version_compare(PHP_VERSION, '5.3.0', '>=') ? '' : 'Legacy') . '.php');
242
+ }
243
+ $instance = Parsedown::instance();
244
+ $text = $instance->text($text);
245
+ }
246
+
247
+ $allowed = array(
248
+ 'a' => array(
249
+ 'href' => array(),
250
+ 'title' => array(),
251
+ 'rel' => array()),
252
+ 'blockquote' => array('cite' => array()),
253
+ 'br' => array(),
254
+ 'p' => array(),
255
+ 'code' => array(),
256
+ 'pre' => array(),
257
+ 'em' => array(),
258
+ 'strong' => array(),
259
+ 'ul' => array(),
260
+ 'ol' => array(),
261
+ 'li' => array(),
262
+ 'h3' => array(),
263
+ 'h4' => array()
264
+ );
265
+
266
+ $text = balanceTags($text);
267
+
268
+ $text = wp_kses( $text, $allowed );
269
+ $text = trim($text);
270
+ return $text;
271
+ }
272
+
273
+ function code_trick( $text, $markdown ) { // Don't use bbPress native function - it's incompatible with Markdown
274
+ // If doing markdown, first take any user formatted code blocks and turn them into backticks so that
275
+ // markdown will preserve things like underscores in code blocks
276
+ if ( $markdown )
277
+ $text = preg_replace_callback("!(<pre><code>|<code>)(.*?)(</code></pre>|</code>)!s", array( __CLASS__,'decodeit'), $text);
278
+
279
+ $text = str_replace(array("\r\n", "\r"), "\n", $text);
280
+ if ( !$markdown ) {
281
+ // This gets the "inline" code blocks, but can't be used with Markdown.
282
+ $text = preg_replace_callback("|(`)(.*?)`|", array( __CLASS__, 'encodeit'), $text);
283
+ // This gets the "block level" code blocks and converts them to PRE CODE
284
+ $text = preg_replace_callback("!(^|\n)`(.*?)`!s", array( __CLASS__, 'encodeit'), $text);
285
+ } else {
286
+ // Markdown can do inline code, we convert bbPress style block level code to Markdown style
287
+ $text = preg_replace_callback("!(^|\n)([ \t]*?)`(.*?)`!s", array( __CLASS__, 'indent'), $text);
288
+ }
289
+ return $text;
290
+ }
291
+
292
+ function indent( $matches ) {
293
+ $text = $matches[3];
294
+ $text = preg_replace('|^|m', $matches[2] . ' ', $text);
295
+ return $matches[1] . $text;
296
+ }
297
+
298
+ function encodeit( $matches ) {
299
+ if ( function_exists('encodeit') ) // bbPress native
300
+ return encodeit( $matches );
301
+
302
+ $text = trim($matches[2]);
303
+ $text = htmlspecialchars($text, ENT_QUOTES);
304
+ $text = str_replace(array("\r\n", "\r"), "\n", $text);
305
+ $text = preg_replace("|\n\n\n+|", "\n\n", $text);
306
+ $text = str_replace('&amp;lt;', '&lt;', $text);
307
+ $text = str_replace('&amp;gt;', '&gt;', $text);
308
+ $text = "<code>$text</code>";
309
+ if ( "`" != $matches[1] )
310
+ $text = "<pre>$text</pre>";
311
+ return $text;
312
+ }
313
+
314
+ function decodeit( $matches ) {
315
+ if ( function_exists('decodeit') ) // bbPress native
316
+ return decodeit( $matches );
317
+
318
+ $text = $matches[2];
319
+ $trans_table = array_flip(get_html_translation_table(HTML_ENTITIES));
320
+ $text = strtr($text, $trans_table);
321
+ $text = str_replace('<br />', '', $text);
322
+ $text = str_replace('&#38;', '&', $text);
323
+ $text = str_replace('&#39;', "'", $text);
324
+ if ( '<pre><code>' == $matches[1] )
325
+ $text = "\n$text\n";
326
+ return "`$text`";
327
+ }
328
+
329
+ } // end class
330
+
331
+ Class Automattic_Readme extends PucReadmeParser {}
includes/processes/class-mailchimp-woocommerce-abstract-sync.php CHANGED
@@ -81,6 +81,12 @@ abstract class MailChimp_WooCommerce_Abtstract_Sync extends WP_Job
81
  return false;
82
  }
83
 
 
 
 
 
 
 
84
  $page = $this->getResources();
85
 
86
  if (empty($page)) {
@@ -261,7 +267,13 @@ abstract class MailChimp_WooCommerce_Abtstract_Sync extends WP_Job
261
  $time = $this->getData('sync.'.$resource.'.completed_at', false);
262
 
263
  if ($time > 0) {
264
- return new \DateTime($time);
 
 
 
 
 
 
265
  }
266
 
267
  return false;
81
  return false;
82
  }
83
 
84
+ // don't let recursion happen.
85
+ if ($this->getResourceType() === 'orders' && $this->getResourceCompleteTime()) {
86
+ mailchimp_log('sync.stop', "halting the sync for :: {$this->getResourceType()}");
87
+ return false;
88
+ }
89
+
90
  $page = $this->getResources();
91
 
92
  if (empty($page)) {
267
  $time = $this->getData('sync.'.$resource.'.completed_at', false);
268
 
269
  if ($time > 0) {
270
+ try {
271
+ $date = new \DateTime();
272
+ $date->setTimestamp($time);
273
+ return $date;
274
+ } catch (\Exception $e) {
275
+ return false;
276
+ }
277
  }
278
 
279
  return false;
includes/processes/class-mailchimp-woocommerce-cart-update.php CHANGED
@@ -118,8 +118,6 @@ class MailChimp_WooCommerce_Cart_Update extends WP_Job
118
  $cart->setOrderTotal($order_total);
119
 
120
  try {
121
- mailchimp_log('abandoned_cart.submitting', "email: {$customer->getEmailAddress()}");
122
-
123
  // if the post is successful we're all good.
124
  $api->addCart($store_id, $cart, false);
125
 
118
  $cart->setOrderTotal($order_total);
119
 
120
  try {
 
 
121
  // if the post is successful we're all good.
122
  $api->addCart($store_id, $cart, false);
123
 
includes/processes/class-mailchimp-woocommerce-process-orders.php CHANGED
@@ -35,46 +35,50 @@ class MailChimp_WooCommerce_Process_Orders extends MailChimp_WooCommerce_Abtstra
35
 
36
  // since we're syncing the customer for the first time, this is where we need to add the override
37
  // for subscriber status. We don't get the checkbox until this plugin is actually installed and working!
38
-
39
- if ((bool) $this->getOption('mailchimp_auto_subscribe', true)) {
40
- $item->getCustomer()->setOptInStatus(true);
 
 
 
 
 
41
  }
42
 
 
 
43
  $type = $this->mailchimp()->getStoreOrder($this->store_id, $item->getId()) ? 'update' : 'create';
44
  $call = $type === 'create' ? 'addStoreOrder' : 'updateStoreOrder';
45
 
46
  try {
47
 
48
- $log = "$call :: #{$item->getId()} :: email: {$item->getCustomer()->getEmailAddress()}";
49
-
50
- mailchimp_log('sync.orders.submitting', $log);
51
-
52
  // make the call
53
  $response = $this->mailchimp()->$call($this->store_id, $item, false);
54
 
55
  if (empty($response)) {
 
56
  return $response;
57
  }
58
 
59
- mailchimp_log('sync.orders.success', $log);
60
 
61
  $this->items[] = array('response' => $response, 'item' => $item);
62
 
63
  return $response;
64
 
65
  } catch (MailChimp_WooCommerce_ServerError $e) {
66
- mailchimp_log('sync.orders.error', "$call :: MailChimp_WooCommerce_ServerError :: {$e->getMessage()}");
67
  return false;
68
  } catch (MailChimp_WooCommerce_Error $e) {
69
- mailchimp_log('sync.orders.error', "$call :: MailChimp_WooCommerce_Error :: {$e->getMessage()}");
70
  return false;
71
  } catch (Exception $e) {
72
- mailchimp_log('sync.orders.error', "$call :: Uncaught Exception :: {$e->getMessage()}");
73
  return false;
74
  }
75
  }
76
 
77
- mailchimp_debug('iterate.order', 'no order found', $item);
78
 
79
  return false;
80
  }
@@ -84,7 +88,7 @@ class MailChimp_WooCommerce_Process_Orders extends MailChimp_WooCommerce_Abtstra
84
  */
85
  protected function complete()
86
  {
87
- mailchimp_log('sync.orders.completed', 'Done with the order sync.');
88
 
89
  // add a timestamp for the orders sync completion
90
  $this->setResourceCompleteTime();
35
 
36
  // since we're syncing the customer for the first time, this is where we need to add the override
37
  // for subscriber status. We don't get the checkbox until this plugin is actually installed and working!
38
+ if (!($status = $item->getCustomer()->getOptInStatus())) {
39
+ try {
40
+ $subscriber = $this->mailchimp()->member(mailchimp_get_list_id(), $item->getCustomer()->getEmailAddress());
41
+ $status = $subscriber['status'] !== 'unsubscribed';
42
+ } catch (\Exception $e) {
43
+ $status = (bool) $this->getOption('mailchimp_auto_subscribe', true);
44
+ }
45
+ $item->getCustomer()->setOptInStatus($status);
46
  }
47
 
48
+ mailchimp_debug('order_sync', "#{$item->getId()}", $item->toArray());
49
+
50
  $type = $this->mailchimp()->getStoreOrder($this->store_id, $item->getId()) ? 'update' : 'create';
51
  $call = $type === 'create' ? 'addStoreOrder' : 'updateStoreOrder';
52
 
53
  try {
54
 
 
 
 
 
55
  // make the call
56
  $response = $this->mailchimp()->$call($this->store_id, $item, false);
57
 
58
  if (empty($response)) {
59
+ mailchimp_log('order_submit.failure', "$call :: #{$item->getId()} :: email: {$item->getCustomer()->getEmailAddress()} produced a blank response from MailChimp");
60
  return $response;
61
  }
62
 
63
+ mailchimp_log('order_submit.success', "$call :: #{$item->getId()} :: email: {$item->getCustomer()->getEmailAddress()}");
64
 
65
  $this->items[] = array('response' => $response, 'item' => $item);
66
 
67
  return $response;
68
 
69
  } catch (MailChimp_WooCommerce_ServerError $e) {
70
+ mailchimp_log('order_submit.error', "$call :: {$item->getId()} :: MailChimp_WooCommerce_ServerError :: {$e->getMessage()}");
71
  return false;
72
  } catch (MailChimp_WooCommerce_Error $e) {
73
+ mailchimp_log('order_submit.error', "$call :: {$item->getId()} :: MailChimp_WooCommerce_Error :: {$e->getMessage()}");
74
  return false;
75
  } catch (Exception $e) {
76
+ mailchimp_log('order_submit.error', "$call :: {$item->getId()} :: Uncaught Exception :: {$e->getMessage()}");
77
  return false;
78
  }
79
  }
80
 
81
+ mailchimp_debug('order_submit', 'no order found', $item);
82
 
83
  return false;
84
  }
88
  */
89
  protected function complete()
90
  {
91
+ mailchimp_log('order_submit.completed', 'Done with the order sync.');
92
 
93
  // add a timestamp for the orders sync completion
94
  $this->setResourceCompleteTime();
includes/processes/class-mailchimp-woocommerce-process-products.php CHANGED
@@ -41,6 +41,8 @@ class MailChimp_WooCommerce_Process_Products extends MailChimp_WooCommerce_Abtst
41
 
42
  if ($item instanceof MailChimp_WooCommerce_Product) {
43
 
 
 
44
  // need to run the delete option on this before submitting because the API does not support PATCH yet.
45
  $this->mailchimp()->deleteStoreProduct($this->store_id, $item->getId());
46
 
@@ -49,16 +51,16 @@ class MailChimp_WooCommerce_Process_Products extends MailChimp_WooCommerce_Abtst
49
  // make the call
50
  $response = $this->mailchimp()->addStoreProduct($this->store_id, $item, false);
51
 
52
- mailchimp_log('sync.products.success', "addStoreProduct :: #{$response->getId()}");
53
 
54
  return $response;
55
 
56
  } catch (MailChimp_WooCommerce_ServerError $e) {
57
- mailchimp_log('sync.products.error', "addStoreProduct :: MailChimp_WooCommerce_ServerError :: {$e->getMessage()}");
58
  } catch (MailChimp_WooCommerce_Error $e) {
59
- mailchimp_log('sync.products.error', "addStoreProduct :: MailChimp_WooCommerce_Error :: {$e->getMessage()}");
60
  } catch (Exception $e) {
61
- mailchimp_log('sync.products.error', "addStoreProduct :: Uncaught Exception :: {$e->getMessage()}");
62
  }
63
  }
64
 
@@ -70,7 +72,7 @@ class MailChimp_WooCommerce_Process_Products extends MailChimp_WooCommerce_Abtst
70
  */
71
  protected function complete()
72
  {
73
- mailchimp_log('sync.products.completed', 'Done with the product sync :: queuing up the orders next!');
74
 
75
  // add a timestamp for the product sync completion
76
  $this->setResourceCompleteTime();
41
 
42
  if ($item instanceof MailChimp_WooCommerce_Product) {
43
 
44
+ mailchimp_debug('product_sync', "#{$item->getId()}", $item->toArray());
45
+
46
  // need to run the delete option on this before submitting because the API does not support PATCH yet.
47
  $this->mailchimp()->deleteStoreProduct($this->store_id, $item->getId());
48
 
51
  // make the call
52
  $response = $this->mailchimp()->addStoreProduct($this->store_id, $item, false);
53
 
54
+ mailchimp_log('product_sync.success', "addStoreProduct :: #{$response->getId()}");
55
 
56
  return $response;
57
 
58
  } catch (MailChimp_WooCommerce_ServerError $e) {
59
+ mailchimp_log('product_sync.error', "addStoreProduct :: {$item->getId()} :: MailChimp_WooCommerce_ServerError :: {$e->getMessage()}");
60
  } catch (MailChimp_WooCommerce_Error $e) {
61
+ mailchimp_log('product_sync.error', "addStoreProduct :: {$item->getId()} :: MailChimp_WooCommerce_Error :: {$e->getMessage()}");
62
  } catch (Exception $e) {
63
+ mailchimp_log('product_sync.error', "addStoreProduct :: {$item->getId()} :: Uncaught Exception :: {$e->getMessage()}");
64
  }
65
  }
66
 
72
  */
73
  protected function complete()
74
  {
75
+ mailchimp_log('product_sync.completed', 'Done with the product sync :: queuing up the orders next!');
76
 
77
  // add a timestamp for the product sync completion
78
  $this->setResourceCompleteTime();
includes/processes/class-mailchimp-woocommerce-single-order.php CHANGED
@@ -16,6 +16,7 @@ class MailChimp_WooCommerce_Single_Order extends WP_Job
16
  public $landing_site;
17
  public $is_update = false;
18
  public $partially_refunded = false;
 
19
 
20
  /**
21
  * MailChimp_WooCommerce_Single_Order constructor.
@@ -43,23 +44,23 @@ class MailChimp_WooCommerce_Single_Order extends WP_Job
43
 
44
  public function process()
45
  {
46
- if (empty($this->order_id)) {
47
- return false;
48
- }
49
-
50
  $options = get_option('mailchimp-woocommerce', array());
51
  $store_id = mailchimp_get_store_id();
52
 
53
  // only if we have the right parameters to do the work
54
  if (!empty($store_id) && is_array($options) && isset($options['mailchimp_api_key'])) {
55
 
 
 
 
 
56
  $job = new MailChimp_WooCommerce_Transform_Orders();
57
  $api = new MailChimp_WooCommerce_MailChimpApi($options['mailchimp_api_key']);
58
 
59
  // set the campaign ID
60
  $job->campaign_id = $this->campaign_id;
61
 
62
- $call = ($api_response = $api->getStoreOrder($store_id, $this->order_id)) ? 'updateStoreOrder' : 'addStoreOrder';
63
 
64
  if ($call === 'addStoreOrder' && $this->is_update === true) {
65
  return false;
@@ -83,6 +84,8 @@ class MailChimp_WooCommerce_Single_Order extends WP_Job
83
  // transform the order
84
  $order = $job->transform($order_post);
85
 
 
 
86
  // if we're overriding this we need to set it here.
87
  if ($this->partially_refunded) {
88
  $order->setFinancialStatus('partially_refunded');
@@ -110,22 +113,22 @@ class MailChimp_WooCommerce_Single_Order extends WP_Job
110
 
111
  }
112
 
113
- mailchimp_log('order_submit.submitting', $log);
114
-
115
  // update or create
116
  $api_response = $api->$call($store_id, $order, false);
117
 
118
  if (empty($api_response)) {
 
119
  return $api_response;
120
  }
121
 
122
- mailchimp_log('order_submit.success', $log);
123
-
124
  // if we're adding a new order and the session id is here, we need to delete the AC cart record.
125
  if (!empty($this->cart_session_id)) {
126
  $api->deleteCartByID($store_id, $this->cart_session_id);
 
127
  }
128
 
 
 
129
  return $api_response;
130
 
131
  } catch (\Exception $e) {
@@ -175,5 +178,23 @@ class MailChimp_WooCommerce_Single_Order extends WP_Job
175
 
176
  return false;
177
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
178
  }
179
 
16
  public $landing_site;
17
  public $is_update = false;
18
  public $partially_refunded = false;
19
+ protected $woo_order_number = false;
20
 
21
  /**
22
  * MailChimp_WooCommerce_Single_Order constructor.
44
 
45
  public function process()
46
  {
 
 
 
 
47
  $options = get_option('mailchimp-woocommerce', array());
48
  $store_id = mailchimp_get_store_id();
49
 
50
  // only if we have the right parameters to do the work
51
  if (!empty($store_id) && is_array($options) && isset($options['mailchimp_api_key'])) {
52
 
53
+ if (!($woo_order_number = $this->getRealOrderNumber())) {
54
+ return false;
55
+ }
56
+
57
  $job = new MailChimp_WooCommerce_Transform_Orders();
58
  $api = new MailChimp_WooCommerce_MailChimpApi($options['mailchimp_api_key']);
59
 
60
  // set the campaign ID
61
  $job->campaign_id = $this->campaign_id;
62
 
63
+ $call = ($api_response = $api->getStoreOrder($store_id, $woo_order_number)) ? 'updateStoreOrder' : 'addStoreOrder';
64
 
65
  if ($call === 'addStoreOrder' && $this->is_update === true) {
66
  return false;
84
  // transform the order
85
  $order = $job->transform($order_post);
86
 
87
+ mailchimp_debug('order_submit', "#{$woo_order_number}", $order->toArray());
88
+
89
  // if we're overriding this we need to set it here.
90
  if ($this->partially_refunded) {
91
  $order->setFinancialStatus('partially_refunded');
113
 
114
  }
115
 
 
 
116
  // update or create
117
  $api_response = $api->$call($store_id, $order, false);
118
 
119
  if (empty($api_response)) {
120
+ mailchimp_log('order_submit.failure', "$call :: #{$order->getId()} :: email: {$order->getCustomer()->getEmailAddress()} produced a blank response from MailChimp");
121
  return $api_response;
122
  }
123
 
 
 
124
  // if we're adding a new order and the session id is here, we need to delete the AC cart record.
125
  if (!empty($this->cart_session_id)) {
126
  $api->deleteCartByID($store_id, $this->cart_session_id);
127
+ $log .= " :: abandoned cart deleted [{$this->cart_session_id}]";
128
  }
129
 
130
+ mailchimp_log('order_submit.success', $log);
131
+
132
  return $api_response;
133
 
134
  } catch (\Exception $e) {
178
 
179
  return false;
180
  }
181
+
182
+ /**
183
+ * @return bool
184
+ */
185
+ public function getRealOrderNumber()
186
+ {
187
+ try {
188
+ if (empty($this->order_id) || !($order_post = get_post($this->order_id))) {
189
+ return false;
190
+ }
191
+ $woo = new WC_Order($order_post);
192
+ return $this->woo_order_number = $woo->get_order_number();
193
+ } catch (\Exception $e) {
194
+ $this->woo_order_number = false;
195
+ mailchimp_log('order_sync.failure', "{$this->order_id} could not be loaded {$e->getMessage()}");
196
+ return false;
197
+ }
198
+ }
199
  }
200
 
includes/processes/class-mailchimp-woocommerce-single-product.php CHANGED
@@ -57,7 +57,7 @@ class MailChimp_WooCommerce_Single_Product extends WP_Job
57
 
58
  $product = $this->transformer()->transform($product_post);
59
 
60
- mailchimp_log('product_submit.submitting', "addStoreProduct :: #{$product->getId()}");
61
 
62
  $this->api()->addStoreProduct($this->store_id, $product, false);
63
 
@@ -68,11 +68,11 @@ class MailChimp_WooCommerce_Single_Product extends WP_Job
68
  return $product;
69
 
70
  } catch (MailChimp_WooCommerce_ServerError $e) {
71
- mailchimp_log('product_submit.error', "addStoreProduct :: MailChimp_WooCommerce_ServerError :: {$e->getMessage()}");
72
  } catch (MailChimp_WooCommerce_Error $e) {
73
- mailchimp_log('product_submit.error', "addStoreProduct :: MailChimp_WooCommerce_Error :: {$e->getMessage()}");
74
  } catch (Exception $e) {
75
- mailchimp_log('product_submit.error', "addStoreProduct :: Uncaught Exception :: {$e->getMessage()}");
76
  }
77
 
78
  return false;
57
 
58
  $product = $this->transformer()->transform($product_post);
59
 
60
+ mailchimp_debug('product_submit.debug', "#{$this->product_id}", $product->toArray());
61
 
62
  $this->api()->addStoreProduct($this->store_id, $product, false);
63
 
68
  return $product;
69
 
70
  } catch (MailChimp_WooCommerce_ServerError $e) {
71
+ mailchimp_log('product_submit.error', "addStoreProduct :: #{$this->product_id} :: MailChimp_WooCommerce_ServerError :: {$e->getMessage()}");
72
  } catch (MailChimp_WooCommerce_Error $e) {
73
+ mailchimp_log('product_submit.error', "addStoreProduct :: #{$this->product_id} :: MailChimp_WooCommerce_Error :: {$e->getMessage()}");
74
  } catch (Exception $e) {
75
+ mailchimp_log('product_submit.error', "addStoreProduct :: #{$this->product_id} :: Uncaught Exception :: {$e->getMessage()}");
76
  }
77
 
78
  return false;
includes/processes/class-mailchimp-woocommerce-user-submit.php CHANGED
@@ -60,14 +60,19 @@ class MailChimp_WooCommerce_User_Submit extends WP_Job
60
  $user = new WP_User($this->user_id);
61
 
62
  if ($user->ID <= 0 || empty($store_id) || !is_array($options)) {
63
- mailchimp_log('member.sync', 'Invalid Data For Submission :: '.$user->user_email);
64
  return false;
65
  }
66
  }
67
 
68
  // if we have a null value, we need to grab the correct user meta for is_subscribed
69
  if (is_null($this->subscribed)) {
70
- $this->subscribed = (bool) get_user_meta($this->user_id, 'mailchimp_woocommerce_is_subscribed', true);
 
 
 
 
 
71
  }
72
 
73
  $api_key = isset($options['mailchimp_api_key']) ? $options['mailchimp_api_key'] : false;
@@ -75,7 +80,7 @@ class MailChimp_WooCommerce_User_Submit extends WP_Job
75
 
76
  // we need a valid api key and list id to continue
77
  if (empty($api_key) || empty($list_id)) {
78
- mailchimp_log('member.sync', 'Invalid Api Key or ListID :: '.$user->user_email);
79
  return false;
80
  }
81
 
@@ -113,7 +118,8 @@ class MailChimp_WooCommerce_User_Submit extends WP_Job
113
 
114
  // ok let's update this member
115
  $api->update($list_id, $user->user_email, $this->subscribed, $merge_vars);
116
- mailchimp_log('member.sync', 'Updated Member '.$user->user_email, $merge_vars);
 
117
  } catch (\Exception $e) {
118
 
119
  // if we have a 404 not found, we can create the member
@@ -121,7 +127,7 @@ class MailChimp_WooCommerce_User_Submit extends WP_Job
121
 
122
  try {
123
  $api->subscribe($list_id, $user->user_email, $this->subscribed, $merge_vars);
124
- mailchimp_log('member.sync', 'Subscribed Member '.$user->user_email, $merge_vars);
125
  } catch (\Exception $e) {
126
  mailchimp_log('member.sync', $e->getMessage());
127
  }
60
  $user = new WP_User($this->user_id);
61
 
62
  if ($user->ID <= 0 || empty($store_id) || !is_array($options)) {
63
+ mailchimp_log('member.sync', "Invalid Data For Submission :: {$user->user_email}");
64
  return false;
65
  }
66
  }
67
 
68
  // if we have a null value, we need to grab the correct user meta for is_subscribed
69
  if (is_null($this->subscribed)) {
70
+ $user_subscribed = get_user_meta($this->user_id, 'mailchimp_woocommerce_is_subscribed', true);
71
+ if ($user_subscribed === '' || $user_subscribed === null) {
72
+ mailchimp_log('member.sync', "Skipping sync for {$user->user_email} because no subscriber status has been set");
73
+ return false;
74
+ }
75
+ $this->subscribed = (bool) $user_subscribed;
76
  }
77
 
78
  $api_key = isset($options['mailchimp_api_key']) ? $options['mailchimp_api_key'] : false;
80
 
81
  // we need a valid api key and list id to continue
82
  if (empty($api_key) || empty($list_id)) {
83
+ mailchimp_log('member.sync', "Invalid Api Key or ListID :: {$user->user_email}");
84
  return false;
85
  }
86
 
118
 
119
  // ok let's update this member
120
  $api->update($list_id, $user->user_email, $this->subscribed, $merge_vars);
121
+
122
+ mailchimp_log('member.sync', "Updated Member {$user->user_email}", $merge_vars);
123
  } catch (\Exception $e) {
124
 
125
  // if we have a 404 not found, we can create the member
127
 
128
  try {
129
  $api->subscribe($list_id, $user->user_email, $this->subscribed, $merge_vars);
130
+ mailchimp_log('member.sync', "Subscribed Member {$user->user_email}", $merge_vars);
131
  } catch (\Exception $e) {
132
  mailchimp_log('member.sync', $e->getMessage());
133
  }
includes/vendor/queue.php CHANGED
File without changes
includes/vendor/queue/classes/cli/queue-command.php CHANGED
File without changes
includes/vendor/queue/classes/worker/wp-http-worker.php CHANGED
File without changes
includes/vendor/queue/classes/worker/wp-worker.php CHANGED
@@ -71,6 +71,9 @@ if ( ! class_exists( 'WP_Worker' ) ) {
71
  $this->queue->delete( $job );
72
  }
73
  } catch ( Exception $e ) {
 
 
 
74
  $this->queue->release( $job );
75
 
76
  return false;
71
  $this->queue->delete( $job );
72
  }
73
  } catch ( Exception $e ) {
74
+
75
+ mailchimp_log('queue.error', "{$e->getMessage()} on {$e->getLine()} in {$e->getFile()}", array('job' => get_class($this->payload)));
76
+
77
  $this->queue->release( $job );
78
 
79
  return false;
includes/vendor/queue/classes/wp-job.php CHANGED
File without changes
includes/vendor/queue/classes/wp-queue.php CHANGED
File without changes
index.php CHANGED
File without changes
languages/mailchimp-woocommerce.pot CHANGED
File without changes
mailchimp-woocommerce.php CHANGED
@@ -16,7 +16,7 @@
16
  * Plugin Name: MailChimp for WooCommerce
17
  * Plugin URI: https://mailchimp.com/connect-your-store/
18
  * Description: MailChimp - WooCommerce plugin
19
- * Version: 2.0.1
20
  * Author: MailChimp
21
  * Author URI: https://mailchimp.com
22
  * License: GPL-2.0+
@@ -29,33 +29,44 @@
29
 
30
  // If this file is called directly, abort.
31
  if ( ! defined( 'WPINC' ) ) {
32
- die;
33
  }
34
 
35
  /**
36
  * @return object
37
  */
38
  function mailchimp_environment_variables() {
39
- return require 'env.php';
 
 
 
 
 
 
 
 
 
 
 
40
  }
41
 
42
  /**
43
  * @return bool|int
44
  */
45
  function mailchimp_get_list_id() {
46
- if (($options = get_option('mailchimp-woocommerce', false)) && is_array($options)) {
47
- if (isset($options['mailchimp_list'])) {
48
- return $options['mailchimp_list'];
49
- }
50
- }
51
- return false;
52
  }
53
 
54
  /**
55
  * @return string
56
  */
57
  function mailchimp_get_store_id() {
58
- $store_id = mailchimp_get_data('store_id', false);
59
  if (empty($store_id)) {
60
  // this is for the previous installs that had been applying the MC store id as the siteurl.
61
  // patched to the random hash because people were changing this value for various reasons.
@@ -69,12 +80,12 @@ function mailchimp_get_store_id() {
69
  * @return bool|MailChimp_WooCommerce_MailChimpApi
70
  */
71
  function mailchimp_get_api() {
72
- if (($options = get_option('mailchimp-woocommerce', false)) && is_array($options)) {
73
- if (isset($options['mailchimp_api_key'])) {
74
- return new MailChimp_WooCommerce_MailChimpApi($options['mailchimp_api_key']);
75
- }
76
- }
77
- return false;
78
  }
79
 
80
  /**
@@ -83,14 +94,14 @@ function mailchimp_get_api() {
83
  * @return null
84
  */
85
  function mailchimp_get_option($key, $default = null) {
86
- $options = get_option('mailchimp-woocommerce');
87
- if (!is_array($options)) {
88
- return $default;
89
- }
90
- if (!array_key_exists($key, $options)) {
91
- return $default;
92
- }
93
- return $options[$key];
94
  }
95
 
96
  /**
@@ -99,7 +110,7 @@ function mailchimp_get_option($key, $default = null) {
99
  * @return mixed
100
  */
101
  function mailchimp_get_data($key, $default = null) {
102
- return get_option('mailchimp-woocommerce-'.$key, $default);
103
  }
104
 
105
  /**
@@ -117,18 +128,17 @@ function mailchimp_set_data($key, $value, $autoload = 'yes') {
117
  * @return DateTime
118
  */
119
  function mailchimp_date_utc($date) {
120
- $timezone = wc_timezone_string();
121
- //$timezone = mailchimp_get_option('store_timezone', 'America/New_York');
122
- if (is_numeric($date)) {
123
- $stamp = $date;
124
- $date = new \DateTime('now', new DateTimeZone($timezone));
125
- $date->setTimestamp($stamp);
126
- } else {
127
- $date = new \DateTime($date, new DateTimeZone($timezone));
128
- }
129
-
130
- $date->setTimezone(new DateTimeZone('UTC'));
131
- return $date;
132
  }
133
 
134
  /**
@@ -137,13 +147,13 @@ function mailchimp_date_utc($date) {
137
  */
138
  function mailchimp_date_local($date) {
139
  $timezone = mailchimp_get_option('store_timezone', 'America/New_York');
140
- if (is_numeric($date)) {
141
- $stamp = $date;
142
- $date = new \DateTime('now', new DateTimeZone('UTC'));
143
- $date->setTimestamp($stamp);
144
- } else {
145
- $date = new \DateTime($date, new DateTimeZone('UTC'));
146
- }
147
 
148
  $date->setTimezone(new DateTimeZone($timezone));
149
  return $date;
@@ -154,52 +164,51 @@ function mailchimp_date_local($date) {
154
  * @return mixed
155
  */
156
  function mailchimp_array_remove_empty($data) {
157
- if (empty($data) || !is_array($data)) {
158
- return array();
159
- }
160
- foreach ($data as $key => $value) {
161
- if ($value === null || $value === '') {
162
- unset($data[$key]);
163
- }
164
- }
165
- return $data;
166
  }
167
 
168
  /**
169
  * @return array
170
  */
171
  function mailchimp_get_timezone_list() {
172
- $zones_array = array();
173
- $timestamp = time();
174
- $current = date_default_timezone_get();
175
-
176
- foreach(timezone_identifiers_list() as $key => $zone) {
177
- date_default_timezone_set($zone);
178
- $zones_array[$key]['zone'] = $zone;
179
- $zones_array[$key]['diff_from_GMT'] = 'UTC/GMT ' . date('P', $timestamp);
180
- }
181
 
182
- date_default_timezone_set($current);
183
 
184
- return $zones_array;
185
  }
186
 
187
  /**
188
  * The code that runs during plugin activation.
189
  * This action is documented in includes/class-mailchimp-woocommerce-activator.php
190
  */
191
- function activate_mailchimp_woocommerce()
192
- {
193
- // if we don't have woocommerce we need to display a horrible error message before the plugin is installed.
194
- if (!is_plugin_active('woocommerce/woocommerce.php')) {
195
-
196
- $active = false;
197
-
198
- // some people may have uploaded a specific version of woo, so we need a fallback checker here.
199
- foreach (array_keys(get_plugins()) as $plugin) {
200
- if (mailchimp_string_contains($plugin, 'woocommerce.php')) {
201
- $active = true;
202
- break;
203
  }
204
  }
205
 
@@ -209,21 +218,20 @@ function activate_mailchimp_woocommerce()
209
  $error_message = __('The MailChimp For WooCommerce plugin requires the <a href="http://wordpress.org/extend/plugins/woocommerce/">WooCommerce</a> plugin to be active!', 'woocommerce');
210
  wp_die($error_message);
211
  }
212
- }
213
 
214
- // ok we can activate this thing.
215
- require_once plugin_dir_path( __FILE__ ) . 'includes/class-mailchimp-woocommerce-activator.php';
216
 
217
- MailChimp_Woocommerce_Activator::activate();
218
  }
219
 
220
  /**
221
  * Create the queue tables
222
  */
223
- function install_mailchimp_queue()
224
- {
225
- require_once plugin_dir_path( __FILE__ ) . 'includes/class-mailchimp-woocommerce-activator.php';
226
- MailChimp_Woocommerce_Activator::create_queue_tables();
227
  }
228
 
229
  /**
@@ -231,14 +239,19 @@ function install_mailchimp_queue()
231
  * This action is documented in includes/class-mailchimp-woocommerce-deactivator.php
232
  */
233
  function deactivate_mailchimp_woocommerce() {
234
- require_once plugin_dir_path( __FILE__ ) . 'includes/class-mailchimp-woocommerce-deactivator.php';
235
- MailChimp_Woocommerce_Deactivator::deactivate();
236
  }
237
 
238
- function mailchimp_debug($action, $message, $data = null)
239
- {
240
- if (defined('WP_CLI') && WP_CLI) {
241
- WP_CLI::debug(print_r(array('message' => $message, 'data' => $data), true));
 
 
 
 
 
242
  }
243
  }
244
 
@@ -248,17 +261,10 @@ function mailchimp_debug($action, $message, $data = null)
248
  * @param array $data
249
  * @return array|WP_Error
250
  */
251
- function mailchimp_log($action, $message, $data = array())
252
- {
253
- $options = MailChimp_Woocommerce::getLoggingConfig();
254
-
255
- if (!$options->enable_logging || !$options->account_id || !$options->username) {
256
- return false;
257
- }
258
-
259
- if (defined('WP_CLI') && WP_CLI) {
260
- WP_CLI::log(print_r(array('message' => $message, 'data' => $data), true));
261
- return null;
262
  }
263
  }
264
 
@@ -269,15 +275,14 @@ function mailchimp_log($action, $message, $data = array())
269
  * @param string|array $needles
270
  * @return bool
271
  */
272
- function mailchimp_string_contains($haystack, $needles)
273
- {
274
- foreach ((array) $needles as $needle) {
275
- if ($needle != '' && mb_strpos($haystack, $needle) !== false) {
276
- return true;
277
- }
278
- }
279
-
280
- return false;
281
  }
282
 
283
 
@@ -285,26 +290,26 @@ function mailchimp_string_contains($haystack, $needles)
285
  * @return int
286
  */
287
  function mailchimp_get_product_count() {
288
- $posts = mailchimp_count_posts('product');
289
  unset($posts['auto-draft'], $posts['trash']);
290
- $total = 0;
291
- foreach ($posts as $status => $count) {
292
- $total += $count;
293
- }
294
- return $total;
295
  }
296
 
297
  /**
298
  * @return int
299
  */
300
  function mailchimp_get_order_count() {
301
- $posts = mailchimp_count_posts('shop_order');
302
- unset($posts['auto-draft'], $posts['trash']);
303
- $total = 0;
304
- foreach ($posts as $status => $count) {
305
- $total += $count;
306
- }
307
- return $total;
308
  }
309
 
310
  /**
@@ -312,14 +317,14 @@ function mailchimp_get_order_count() {
312
  * @return array|null|object
313
  */
314
  function mailchimp_count_posts($type) {
315
- global $wpdb;
316
- $query = "SELECT post_status, COUNT( * ) AS num_posts FROM {$wpdb->posts} WHERE post_type = %s GROUP BY post_status";
317
- $posts = $wpdb->get_results( $wpdb->prepare($query, $type));
318
- $response = array();
319
- foreach ($posts as $post) {
320
- $response[$post->post_status] = $post->num_posts;
321
- }
322
- return $response;
323
  }
324
 
325
  /**
@@ -394,14 +399,14 @@ require plugin_dir_path( __FILE__ ) . 'includes/class-mailchimp-woocommerce.php'
394
  * @since 1.0.0
395
  */
396
  function run_mailchimp_woocommerce() {
397
- $env = mailchimp_environment_variables();
398
- $plugin = new MailChimp_Woocommerce($env->environment, $env->version);
399
- $plugin->run();
400
  }
401
 
402
  if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
403
- $forwarded_address = explode(',',$_SERVER['HTTP_X_FORWARDED_FOR']);
404
- $_SERVER['REMOTE_ADDR'] = $forwarded_address[0];
405
  }
406
 
407
  function mailchimp_woocommerce_add_meta_tags() {
16
  * Plugin Name: MailChimp for WooCommerce
17
  * Plugin URI: https://mailchimp.com/connect-your-store/
18
  * Description: MailChimp - WooCommerce plugin
19
+ * Version: 2.0.2
20
  * Author: MailChimp
21
  * Author URI: https://mailchimp.com
22
  * License: GPL-2.0+
29
 
30
  // If this file is called directly, abort.
31
  if ( ! defined( 'WPINC' ) ) {
32
+ die;
33
  }
34
 
35
  /**
36
  * @return object
37
  */
38
  function mailchimp_environment_variables() {
39
+ global $wp_version;
40
+
41
+ $o = get_option('mailchimp-woocommerce', false);
42
+
43
+ return (object) array(
44
+ 'repo' => 'master',
45
+ 'environment' => 'production',
46
+ 'version' => '2.0.2',
47
+ 'wp_version' => (empty($wp_version) ? 'Unknown' : $wp_version),
48
+ 'wc_version' => class_exists('WC') ? WC()->version : null,
49
+ 'logging' => ($o && is_array($o) && isset($o['mailchimp_logging'])) ? $o['mailchimp_logging'] : 'none',
50
+ );
51
  }
52
 
53
  /**
54
  * @return bool|int
55
  */
56
  function mailchimp_get_list_id() {
57
+ if (($options = get_option('mailchimp-woocommerce', false)) && is_array($options)) {
58
+ if (isset($options['mailchimp_list'])) {
59
+ return $options['mailchimp_list'];
60
+ }
61
+ }
62
+ return false;
63
  }
64
 
65
  /**
66
  * @return string
67
  */
68
  function mailchimp_get_store_id() {
69
+ $store_id = mailchimp_get_data('store_id', false);
70
  if (empty($store_id)) {
71
  // this is for the previous installs that had been applying the MC store id as the siteurl.
72
  // patched to the random hash because people were changing this value for various reasons.
80
  * @return bool|MailChimp_WooCommerce_MailChimpApi
81
  */
82
  function mailchimp_get_api() {
83
+ if (($options = get_option('mailchimp-woocommerce', false)) && is_array($options)) {
84
+ if (isset($options['mailchimp_api_key'])) {
85
+ return new MailChimp_WooCommerce_MailChimpApi($options['mailchimp_api_key']);
86
+ }
87
+ }
88
+ return false;
89
  }
90
 
91
  /**
94
  * @return null
95
  */
96
  function mailchimp_get_option($key, $default = null) {
97
+ $options = get_option('mailchimp-woocommerce');
98
+ if (!is_array($options)) {
99
+ return $default;
100
+ }
101
+ if (!array_key_exists($key, $options)) {
102
+ return $default;
103
+ }
104
+ return $options[$key];
105
  }
106
 
107
  /**
110
  * @return mixed
111
  */
112
  function mailchimp_get_data($key, $default = null) {
113
+ return get_option('mailchimp-woocommerce-'.$key, $default);
114
  }
115
 
116
  /**
128
  * @return DateTime
129
  */
130
  function mailchimp_date_utc($date) {
131
+ $timezone = wc_timezone_string();
132
+ if (is_numeric($date)) {
133
+ $stamp = $date;
134
+ $date = new \DateTime('now', new DateTimeZone($timezone));
135
+ $date->setTimestamp($stamp);
136
+ } else {
137
+ $date = new \DateTime($date, new DateTimeZone($timezone));
138
+ }
139
+
140
+ $date->setTimezone(new DateTimeZone('UTC'));
141
+ return $date;
 
142
  }
143
 
144
  /**
147
  */
148
  function mailchimp_date_local($date) {
149
  $timezone = mailchimp_get_option('store_timezone', 'America/New_York');
150
+ if (is_numeric($date)) {
151
+ $stamp = $date;
152
+ $date = new \DateTime('now', new DateTimeZone('UTC'));
153
+ $date->setTimestamp($stamp);
154
+ } else {
155
+ $date = new \DateTime($date, new DateTimeZone('UTC'));
156
+ }
157
 
158
  $date->setTimezone(new DateTimeZone($timezone));
159
  return $date;
164
  * @return mixed
165
  */
166
  function mailchimp_array_remove_empty($data) {
167
+ if (empty($data) || !is_array($data)) {
168
+ return array();
169
+ }
170
+ foreach ($data as $key => $value) {
171
+ if ($value === null || $value === '') {
172
+ unset($data[$key]);
173
+ }
174
+ }
175
+ return $data;
176
  }
177
 
178
  /**
179
  * @return array
180
  */
181
  function mailchimp_get_timezone_list() {
182
+ $zones_array = array();
183
+ $timestamp = time();
184
+ $current = date_default_timezone_get();
185
+
186
+ foreach(timezone_identifiers_list() as $key => $zone) {
187
+ date_default_timezone_set($zone);
188
+ $zones_array[$key]['zone'] = $zone;
189
+ $zones_array[$key]['diff_from_GMT'] = 'UTC/GMT ' . date('P', $timestamp);
190
+ }
191
 
192
+ date_default_timezone_set($current);
193
 
194
+ return $zones_array;
195
  }
196
 
197
  /**
198
  * The code that runs during plugin activation.
199
  * This action is documented in includes/class-mailchimp-woocommerce-activator.php
200
  */
201
+ function activate_mailchimp_woocommerce() {
202
+ // if we don't have woocommerce we need to display a horrible error message before the plugin is installed.
203
+ if (!is_plugin_active('woocommerce/woocommerce.php')) {
204
+
205
+ $active = false;
206
+
207
+ // some people may have uploaded a specific version of woo, so we need a fallback checker here.
208
+ foreach (array_keys(get_plugins()) as $plugin) {
209
+ if (mailchimp_string_contains($plugin, 'woocommerce.php')) {
210
+ $active = true;
211
+ break;
 
212
  }
213
  }
214
 
218
  $error_message = __('The MailChimp For WooCommerce plugin requires the <a href="http://wordpress.org/extend/plugins/woocommerce/">WooCommerce</a> plugin to be active!', 'woocommerce');
219
  wp_die($error_message);
220
  }
221
+ }
222
 
223
+ // ok we can activate this thing.
224
+ require_once plugin_dir_path( __FILE__ ) . 'includes/class-mailchimp-woocommerce-activator.php';
225
 
226
+ MailChimp_Woocommerce_Activator::activate();
227
  }
228
 
229
  /**
230
  * Create the queue tables
231
  */
232
+ function install_mailchimp_queue() {
233
+ require_once plugin_dir_path( __FILE__ ) . 'includes/class-mailchimp-woocommerce-activator.php';
234
+ MailChimp_Woocommerce_Activator::create_queue_tables();
 
235
  }
236
 
237
  /**
239
  * This action is documented in includes/class-mailchimp-woocommerce-deactivator.php
240
  */
241
  function deactivate_mailchimp_woocommerce() {
242
+ require_once plugin_dir_path( __FILE__ ) . 'includes/class-mailchimp-woocommerce-deactivator.php';
243
+ MailChimp_Woocommerce_Deactivator::deactivate();
244
  }
245
 
246
+ /**
247
+ * @param $action
248
+ * @param $message
249
+ * @param null $data
250
+ */
251
+ function mailchimp_debug($action, $message, $data = null) {
252
+ if (mailchimp_environment_variables()->logging === 'debug') {
253
+ if (is_array($data) && !empty($data)) $message .= " :: ".wc_print_r($data, true);
254
+ wc_get_logger()->debug("{$action} :: {$message}", array('source' => 'mailchimp_woocommerce'));
255
  }
256
  }
257
 
261
  * @param array $data
262
  * @return array|WP_Error
263
  */
264
+ function mailchimp_log($action, $message, $data = array()) {
265
+ if (mailchimp_environment_variables()->logging !== 'none') {
266
+ if (is_array($data) && !empty($data)) $message .= " :: ".wc_print_r($data, true);
267
+ wc_get_logger()->notice("{$action} :: {$message}", array('source' => 'mailchimp_woocommerce'));
 
 
 
 
 
 
 
268
  }
269
  }
270
 
275
  * @param string|array $needles
276
  * @return bool
277
  */
278
+ function mailchimp_string_contains($haystack, $needles) {
279
+ foreach ((array) $needles as $needle) {
280
+ if ($needle != '' && mb_strpos($haystack, $needle) !== false) {
281
+ return true;
282
+ }
283
+ }
284
+
285
+ return false;
 
286
  }
287
 
288
 
290
  * @return int
291
  */
292
  function mailchimp_get_product_count() {
293
+ $posts = mailchimp_count_posts('product');
294
  unset($posts['auto-draft'], $posts['trash']);
295
+ $total = 0;
296
+ foreach ($posts as $status => $count) {
297
+ $total += $count;
298
+ }
299
+ return $total;
300
  }
301
 
302
  /**
303
  * @return int
304
  */
305
  function mailchimp_get_order_count() {
306
+ $posts = mailchimp_count_posts('shop_order');
307
+ unset($posts['auto-draft'], $posts['trash']);
308
+ $total = 0;
309
+ foreach ($posts as $status => $count) {
310
+ $total += $count;
311
+ }
312
+ return $total;
313
  }
314
 
315
  /**
317
  * @return array|null|object
318
  */
319
  function mailchimp_count_posts($type) {
320
+ global $wpdb;
321
+ $query = "SELECT post_status, COUNT( * ) AS num_posts FROM {$wpdb->posts} WHERE post_type = %s GROUP BY post_status";
322
+ $posts = $wpdb->get_results( $wpdb->prepare($query, $type));
323
+ $response = array();
324
+ foreach ($posts as $post) {
325
+ $response[$post->post_status] = $post->num_posts;
326
+ }
327
+ return $response;
328
  }
329
 
330
  /**
399
  * @since 1.0.0
400
  */
401
  function run_mailchimp_woocommerce() {
402
+ $env = mailchimp_environment_variables();
403
+ $plugin = new MailChimp_Woocommerce($env->environment, $env->version);
404
+ $plugin->run();
405
  }
406
 
407
  if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
408
+ $forwarded_address = explode(',',$_SERVER['HTTP_X_FORWARDED_FOR']);
409
+ $_SERVER['REMOTE_ADDR'] = $forwarded_address[0];
410
  }
411
 
412
  function mailchimp_woocommerce_add_meta_tags() {
public/class-mailchimp-woocommerce-public.php CHANGED
File without changes
public/css/mailchimp-woocommerce-public.css CHANGED
File without changes
public/index.php CHANGED
File without changes
public/js/mailchimp-woocommerce-public.js CHANGED
File without changes
public/partials/mailchimp-woocommerce-public-display.php CHANGED
File without changes
uninstall.php CHANGED
File without changes