MailChimp for WooCommerce - Version 1.0.0

Version Description

Download this release

Release Info

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

Version 1.0.0

Files changed (93) hide show
  1. LICENSE.txt +339 -0
  2. README.md +97 -0
  3. README.txt +13 -0
  4. admin/class-mailchimp-woocommerce-admin.php +631 -0
  5. admin/css/mailchimp-woocommerce-admin.css +4 -0
  6. admin/index.php +1 -0
  7. admin/js/mailchimp-woocommerce-admin.js +32 -0
  8. admin/partials/mailchimp-woocommerce-admin-tabs.php +90 -0
  9. admin/partials/tabs/api_key.php +46 -0
  10. admin/partials/tabs/campaign_defaults.php +73 -0
  11. admin/partials/tabs/errors/missing_api_key.php +3 -0
  12. admin/partials/tabs/errors/missing_campaign_defaults.php +3 -0
  13. admin/partials/tabs/errors/missing_list.php +3 -0
  14. admin/partials/tabs/errors/missing_store.php +3 -0
  15. admin/partials/tabs/errors/not_ready_for_sync.php +3 -0
  16. admin/partials/tabs/newsletter_settings.php +89 -0
  17. admin/partials/tabs/notices.php +8 -0
  18. admin/partials/tabs/store_info.php +148 -0
  19. admin/partials/tabs/store_sync.php +30 -0
  20. admin/partials/tabs/success/re-sync-started.php +3 -0
  21. changelog.md +17 -0
  22. includes/api/assets/class-mailchimp-address.php +337 -0
  23. includes/api/assets/class-mailchimp-cart.php +275 -0
  24. includes/api/assets/class-mailchimp-customer.php +257 -0
  25. includes/api/assets/class-mailchimp-line-item.php +180 -0
  26. includes/api/assets/class-mailchimp-order.php +405 -0
  27. includes/api/assets/class-mailchimp-product-variation.php +250 -0
  28. includes/api/assets/class-mailchimp-product.php +281 -0
  29. includes/api/assets/class-mailchimp-store.php +320 -0
  30. includes/api/class-mailchimp-api.php +1003 -0
  31. includes/api/class-mailchimp-woocommerce-api.php +100 -0
  32. includes/api/class-mailchimp-woocommerce-create-list-submission.php +162 -0
  33. includes/api/class-mailchimp-woocommerce-transform-orders.php +398 -0
  34. includes/api/class-mailchimp-woocommerce-transform-products.php +193 -0
  35. includes/api/errors/class-mailchimp-error.php +14 -0
  36. includes/api/errors/class-mailchimp-server-error.php +14 -0
  37. includes/api/helpers/class-mailchimp-woocommerce-api-currency-codes.php +133 -0
  38. includes/api/helpers/class-mailchimp-woocommerce-api-locales.php +469 -0
  39. includes/class-mailchimp-woocommerce-activator.php +117 -0
  40. includes/class-mailchimp-woocommerce-deactivator.php +39 -0
  41. includes/class-mailchimp-woocommerce-i18n.php +47 -0
  42. includes/class-mailchimp-woocommerce-loader.php +129 -0
  43. includes/class-mailchimp-woocommerce-newsletter.php +83 -0
  44. includes/class-mailchimp-woocommerce-options.php +238 -0
  45. includes/class-mailchimp-woocommerce-service.php +492 -0
  46. includes/class-mailchimp-woocommerce.php +398 -0
  47. includes/index.php +1 -0
  48. includes/plugin-update-checker/README.md +105 -0
  49. includes/plugin-update-checker/composer.json +19 -0
  50. includes/plugin-update-checker/css/puc-debug-bar.css +62 -0
  51. includes/plugin-update-checker/debug-bar-panel.php +146 -0
  52. includes/plugin-update-checker/debug-bar-plugin.php +102 -0
  53. includes/plugin-update-checker/github-checker.php +458 -0
  54. includes/plugin-update-checker/js/debug-bar.js +52 -0
  55. includes/plugin-update-checker/languages/plugin-update-checker-fr_FR.mo +0 -0
  56. includes/plugin-update-checker/languages/plugin-update-checker-fr_FR.po +38 -0
  57. includes/plugin-update-checker/languages/plugin-update-checker-hu_HU.mo +0 -0
  58. includes/plugin-update-checker/languages/plugin-update-checker-hu_HU.po +41 -0
  59. includes/plugin-update-checker/languages/plugin-update-checker.pot +39 -0
  60. includes/plugin-update-checker/license.txt +7 -0
  61. includes/plugin-update-checker/plugin-update-checker.php +1641 -0
  62. includes/plugin-update-checker/vendor/Parsedown.php +1538 -0
  63. includes/plugin-update-checker/vendor/ParsedownLegacy.php +1535 -0
  64. includes/plugin-update-checker/vendor/readme-parser.php +331 -0
  65. includes/processes/class-mailchimp-woocommerce-abstract-sync.php +323 -0
  66. includes/processes/class-mailchimp-woocommerce-cart-update.php +176 -0
  67. includes/processes/class-mailchimp-woocommerce-process-orders.php +86 -0
  68. includes/processes/class-mailchimp-woocommerce-process-products.php +74 -0
  69. includes/processes/class-mailchimp-woocommerce-single-order.php +144 -0
  70. includes/processes/class-mailchimp-woocommerce-single-product.php +105 -0
  71. includes/slack/Contracts/Http/Interactor.php +34 -0
  72. includes/slack/Contracts/Http/Response.php +26 -0
  73. includes/slack/Contracts/Http/ResponseFactory.php +15 -0
  74. includes/slack/Core/Commander.php +508 -0
  75. includes/slack/Http/CurlInteractor.php +106 -0
  76. includes/slack/Http/SlackResponse.php +84 -0
  77. includes/slack/Http/SlackResponseFactory.php +13 -0
  78. includes/slack/Logger.php +93 -0
  79. includes/vendor/queue.php +43 -0
  80. includes/vendor/queue/classes/cli/queue-command.php +120 -0
  81. includes/vendor/queue/classes/worker/wp-http-worker.php +323 -0
  82. includes/vendor/queue/classes/worker/wp-worker.php +86 -0
  83. includes/vendor/queue/classes/wp-job.php +103 -0
  84. includes/vendor/queue/classes/wp-queue.php +231 -0
  85. index.php +1 -0
  86. languages/mailchimp-woocommerce.pot +0 -0
  87. mailchimp-woocommerce.php +274 -0
  88. public/class-mailchimp-woocommerce-public.php +109 -0
  89. public/css/mailchimp-woocommerce-public.css +4 -0
  90. public/index.php +1 -0
  91. public/js/mailchimp-woocommerce-public.js +312 -0
  92. public/partials/mailchimp-woocommerce-public-display.php +16 -0
  93. uninstall.php +44 -0
LICENSE.txt ADDED
@@ -0,0 +1,339 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ GNU GENERAL PUBLIC LICENSE
2
+ Version 2, June 1991
3
+
4
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
5
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
6
+ Everyone is permitted to copy and distribute verbatim copies
7
+ of this license document, but changing it is not allowed.
8
+
9
+ Preamble
10
+
11
+ The licenses for most software are designed to take away your
12
+ freedom to share and change it. By contrast, the GNU General Public
13
+ License is intended to guarantee your freedom to share and change free
14
+ software--to make sure the software is free for all its users. This
15
+ General Public License applies to most of the Free Software
16
+ Foundation's software and to any other program whose authors commit to
17
+ using it. (Some other Free Software Foundation software is covered by
18
+ the GNU Lesser General Public License instead.) You can apply it to
19
+ your programs, too.
20
+
21
+ When we speak of free software, we are referring to freedom, not
22
+ price. Our General Public Licenses are designed to make sure that you
23
+ have the freedom to distribute copies of free software (and charge for
24
+ this service if you wish), that you receive source code or can get it
25
+ if you want it, that you can change the software or use pieces of it
26
+ in new free programs; and that you know you can do these things.
27
+
28
+ To protect your rights, we need to make restrictions that forbid
29
+ anyone to deny you these rights or to ask you to surrender the rights.
30
+ These restrictions translate to certain responsibilities for you if you
31
+ distribute copies of the software, or if you modify it.
32
+
33
+ For example, if you distribute copies of such a program, whether
34
+ gratis or for a fee, you must give the recipients all the rights that
35
+ you have. You must make sure that they, too, receive or can get the
36
+ source code. And you must show them these terms so they know their
37
+ rights.
38
+
39
+ We protect your rights with two steps: (1) copyright the software, and
40
+ (2) offer you this license which gives you legal permission to copy,
41
+ distribute and/or modify the software.
42
+
43
+ Also, for each author's protection and ours, we want to make certain
44
+ that everyone understands that there is no warranty for this free
45
+ software. If the software is modified by someone else and passed on, we
46
+ want its recipients to know that what they have is not the original, so
47
+ that any problems introduced by others will not reflect on the original
48
+ authors' reputations.
49
+
50
+ Finally, any free program is threatened constantly by software
51
+ patents. We wish to avoid the danger that redistributors of a free
52
+ program will individually obtain patent licenses, in effect making the
53
+ program proprietary. To prevent this, we have made it clear that any
54
+ patent must be licensed for everyone's free use or not licensed at all.
55
+
56
+ The precise terms and conditions for copying, distribution and
57
+ modification follow.
58
+
59
+ GNU GENERAL PUBLIC LICENSE
60
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
61
+
62
+ 0. This License applies to any program or other work which contains
63
+ a notice placed by the copyright holder saying it may be distributed
64
+ under the terms of this General Public License. The "Program", below,
65
+ refers to any such program or work, and a "work based on the Program"
66
+ means either the Program or any derivative work under copyright law:
67
+ that is to say, a work containing the Program or a portion of it,
68
+ either verbatim or with modifications and/or translated into another
69
+ language. (Hereinafter, translation is included without limitation in
70
+ the term "modification".) Each licensee is addressed as "you".
71
+
72
+ Activities other than copying, distribution and modification are not
73
+ covered by this License; they are outside its scope. The act of
74
+ running the Program is not restricted, and the output from the Program
75
+ is covered only if its contents constitute a work based on the
76
+ Program (independent of having been made by running the Program).
77
+ Whether that is true depends on what the Program does.
78
+
79
+ 1. You may copy and distribute verbatim copies of the Program's
80
+ source code as you receive it, in any medium, provided that you
81
+ conspicuously and appropriately publish on each copy an appropriate
82
+ copyright notice and disclaimer of warranty; keep intact all the
83
+ notices that refer to this License and to the absence of any warranty;
84
+ and give any other recipients of the Program a copy of this License
85
+ along with the Program.
86
+
87
+ You may charge a fee for the physical act of transferring a copy, and
88
+ you may at your option offer warranty protection in exchange for a fee.
89
+
90
+ 2. You may modify your copy or copies of the Program or any portion
91
+ of it, thus forming a work based on the Program, and copy and
92
+ distribute such modifications or work under the terms of Section 1
93
+ above, provided that you also meet all of these conditions:
94
+
95
+ a) You must cause the modified files to carry prominent notices
96
+ stating that you changed the files and the date of any change.
97
+
98
+ b) You must cause any work that you distribute or publish, that in
99
+ whole or in part contains or is derived from the Program or any
100
+ part thereof, to be licensed as a whole at no charge to all third
101
+ parties under the terms of this License.
102
+
103
+ c) If the modified program normally reads commands interactively
104
+ when run, you must cause it, when started running for such
105
+ interactive use in the most ordinary way, to print or display an
106
+ announcement including an appropriate copyright notice and a
107
+ notice that there is no warranty (or else, saying that you provide
108
+ a warranty) and that users may redistribute the program under
109
+ these conditions, and telling the user how to view a copy of this
110
+ License. (Exception: if the Program itself is interactive but
111
+ does not normally print such an announcement, your work based on
112
+ the Program is not required to print an announcement.)
113
+
114
+ These requirements apply to the modified work as a whole. If
115
+ identifiable sections of that work are not derived from the Program,
116
+ and can be reasonably considered independent and separate works in
117
+ themselves, then this License, and its terms, do not apply to those
118
+ sections when you distribute them as separate works. But when you
119
+ distribute the same sections as part of a whole which is a work based
120
+ on the Program, the distribution of the whole must be on the terms of
121
+ this License, whose permissions for other licensees extend to the
122
+ entire whole, and thus to each and every part regardless of who wrote it.
123
+
124
+ Thus, it is not the intent of this section to claim rights or contest
125
+ your rights to work written entirely by you; rather, the intent is to
126
+ exercise the right to control the distribution of derivative or
127
+ collective works based on the Program.
128
+
129
+ In addition, mere aggregation of another work not based on the Program
130
+ with the Program (or with a work based on the Program) on a volume of
131
+ a storage or distribution medium does not bring the other work under
132
+ the scope of this License.
133
+
134
+ 3. You may copy and distribute the Program (or a work based on it,
135
+ under Section 2) in object code or executable form under the terms of
136
+ Sections 1 and 2 above provided that you also do one of the following:
137
+
138
+ a) Accompany it with the complete corresponding machine-readable
139
+ source code, which must be distributed under the terms of Sections
140
+ 1 and 2 above on a medium customarily used for software interchange; or,
141
+
142
+ b) Accompany it with a written offer, valid for at least three
143
+ years, to give any third party, for a charge no more than your
144
+ cost of physically performing source distribution, a complete
145
+ machine-readable copy of the corresponding source code, to be
146
+ distributed under the terms of Sections 1 and 2 above on a medium
147
+ customarily used for software interchange; or,
148
+
149
+ c) Accompany it with the information you received as to the offer
150
+ to distribute corresponding source code. (This alternative is
151
+ allowed only for noncommercial distribution and only if you
152
+ received the program in object code or executable form with such
153
+ an offer, in accord with Subsection b above.)
154
+
155
+ The source code for a work means the preferred form of the work for
156
+ making modifications to it. For an executable work, complete source
157
+ code means all the source code for all modules it contains, plus any
158
+ associated interface definition files, plus the scripts used to
159
+ control compilation and installation of the executable. However, as a
160
+ special exception, the source code distributed need not include
161
+ anything that is normally distributed (in either source or binary
162
+ form) with the major components (compiler, kernel, and so on) of the
163
+ operating system on which the executable runs, unless that component
164
+ itself accompanies the executable.
165
+
166
+ If distribution of executable or object code is made by offering
167
+ access to copy from a designated place, then offering equivalent
168
+ access to copy the source code from the same place counts as
169
+ distribution of the source code, even though third parties are not
170
+ compelled to copy the source along with the object code.
171
+
172
+ 4. You may not copy, modify, sublicense, or distribute the Program
173
+ except as expressly provided under this License. Any attempt
174
+ otherwise to copy, modify, sublicense or distribute the Program is
175
+ void, and will automatically terminate your rights under this License.
176
+ However, parties who have received copies, or rights, from you under
177
+ this License will not have their licenses terminated so long as such
178
+ parties remain in full compliance.
179
+
180
+ 5. You are not required to accept this License, since you have not
181
+ signed it. However, nothing else grants you permission to modify or
182
+ distribute the Program or its derivative works. These actions are
183
+ prohibited by law if you do not accept this License. Therefore, by
184
+ modifying or distributing the Program (or any work based on the
185
+ Program), you indicate your acceptance of this License to do so, and
186
+ all its terms and conditions for copying, distributing or modifying
187
+ the Program or works based on it.
188
+
189
+ 6. Each time you redistribute the Program (or any work based on the
190
+ Program), the recipient automatically receives a license from the
191
+ original licensor to copy, distribute or modify the Program subject to
192
+ these terms and conditions. You may not impose any further
193
+ restrictions on the recipients' exercise of the rights granted herein.
194
+ You are not responsible for enforcing compliance by third parties to
195
+ this License.
196
+
197
+ 7. If, as a consequence of a court judgment or allegation of patent
198
+ infringement or for any other reason (not limited to patent issues),
199
+ conditions are imposed on you (whether by court order, agreement or
200
+ otherwise) that contradict the conditions of this License, they do not
201
+ excuse you from the conditions of this License. If you cannot
202
+ distribute so as to satisfy simultaneously your obligations under this
203
+ License and any other pertinent obligations, then as a consequence you
204
+ may not distribute the Program at all. For example, if a patent
205
+ license would not permit royalty-free redistribution of the Program by
206
+ all those who receive copies directly or indirectly through you, then
207
+ the only way you could satisfy both it and this License would be to
208
+ refrain entirely from distribution of the Program.
209
+
210
+ If any portion of this section is held invalid or unenforceable under
211
+ any particular circumstance, the balance of the section is intended to
212
+ apply and the section as a whole is intended to apply in other
213
+ circumstances.
214
+
215
+ It is not the purpose of this section to induce you to infringe any
216
+ patents or other property right claims or to contest validity of any
217
+ such claims; this section has the sole purpose of protecting the
218
+ integrity of the free software distribution system, which is
219
+ implemented by public license practices. Many people have made
220
+ generous contributions to the wide range of software distributed
221
+ through that system in reliance on consistent application of that
222
+ system; it is up to the author/donor to decide if he or she is willing
223
+ to distribute software through any other system and a licensee cannot
224
+ impose that choice.
225
+
226
+ This section is intended to make thoroughly clear what is believed to
227
+ be a consequence of the rest of this License.
228
+
229
+ 8. If the distribution and/or use of the Program is restricted in
230
+ certain countries either by patents or by copyrighted interfaces, the
231
+ original copyright holder who places the Program under this License
232
+ may add an explicit geographical distribution limitation excluding
233
+ those countries, so that distribution is permitted only in or among
234
+ countries not thus excluded. In such case, this License incorporates
235
+ the limitation as if written in the body of this License.
236
+
237
+ 9. The Free Software Foundation may publish revised and/or new versions
238
+ of the General Public License from time to time. Such new versions will
239
+ be similar in spirit to the present version, but may differ in detail to
240
+ address new problems or concerns.
241
+
242
+ Each version is given a distinguishing version number. If the Program
243
+ specifies a version number of this License which applies to it and "any
244
+ later version", you have the option of following the terms and conditions
245
+ either of that version or of any later version published by the Free
246
+ Software Foundation. If the Program does not specify a version number of
247
+ this License, you may choose any version ever published by the Free Software
248
+ Foundation.
249
+
250
+ 10. If you wish to incorporate parts of the Program into other free
251
+ programs whose distribution conditions are different, write to the author
252
+ to ask for permission. For software which is copyrighted by the Free
253
+ Software Foundation, write to the Free Software Foundation; we sometimes
254
+ make exceptions for this. Our decision will be guided by the two goals
255
+ of preserving the free status of all derivatives of our free software and
256
+ of promoting the sharing and reuse of software generally.
257
+
258
+ NO WARRANTY
259
+
260
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
261
+ FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
262
+ OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
263
+ PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
264
+ OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
265
+ MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
266
+ TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
267
+ PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
268
+ REPAIR OR CORRECTION.
269
+
270
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
271
+ WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
272
+ REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
273
+ INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
274
+ OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
275
+ TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
276
+ YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
277
+ PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
278
+ POSSIBILITY OF SUCH DAMAGES.
279
+
280
+ END OF TERMS AND CONDITIONS
281
+
282
+ How to Apply These Terms to Your New Programs
283
+
284
+ If you develop a new program, and you want it to be of the greatest
285
+ possible use to the public, the best way to achieve this is to make it
286
+ free software which everyone can redistribute and change under these terms.
287
+
288
+ To do so, attach the following notices to the program. It is safest
289
+ to attach them to the start of each source file to most effectively
290
+ convey the exclusion of warranty; and each file should have at least
291
+ the "copyright" line and a pointer to where the full notice is found.
292
+
293
+ <one line to give the program's name and a brief idea of what it does.>
294
+ Copyright (C) <year> <name of author>
295
+
296
+ This program is free software; you can redistribute it and/or modify
297
+ it under the terms of the GNU General Public License as published by
298
+ the Free Software Foundation; either version 2 of the License, or
299
+ (at your option) any later version.
300
+
301
+ This program is distributed in the hope that it will be useful,
302
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
303
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
304
+ GNU General Public License for more details.
305
+
306
+ You should have received a copy of the GNU General Public License along
307
+ with this program; if not, write to the Free Software Foundation, Inc.,
308
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
309
+
310
+ Also add information on how to contact you by electronic and paper mail.
311
+
312
+ If the program is interactive, make it output a short notice like this
313
+ when it starts in an interactive mode:
314
+
315
+ Gnomovision version 69, Copyright (C) year name of author
316
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
317
+ This is free software, and you are welcome to redistribute it
318
+ under certain conditions; type `show c' for details.
319
+
320
+ The hypothetical commands `show w' and `show c' should show the appropriate
321
+ parts of the General Public License. Of course, the commands you use may
322
+ be called something other than `show w' and `show c'; they could even be
323
+ mouse-clicks or menu items--whatever suits your program.
324
+
325
+ You should also get your employer (if you work as a programmer) or your
326
+ school, if any, to sign a "copyright disclaimer" for the program, if
327
+ necessary. Here is a sample; alter the names:
328
+
329
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
330
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
331
+
332
+ <signature of Ty Coon>, 1 April 1989
333
+ Ty Coon, President of Vice
334
+
335
+ This General Public License does not permit incorporating your program into
336
+ proprietary programs. If your program is a subroutine library, you may
337
+ consider it more useful to permit linking proprietary applications with the
338
+ library. If this is what you want to do, use the GNU Lesser General
339
+ Public License instead of this License.
README.md ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # MailChimp for Woocommerce Integration
2
+
3
+ # Updated 9.23.16 with a fix to delete carts for completed orders.
4
+ MailChimp for WooCommerce is a plugin that adds customers and their order information to your MailChimp list. Use WooCommerce data to create targeted campaigns, track sales, recommend products, send Abandoned Cart emails, and more.
5
+
6
+ In this article, you’ll learn how to connect MailChimp for WooCommerce.
7
+
8
+ #Before You Start#
9
+
10
+ **Here are some things to know before you begin this process.**
11
+
12
+ - This tutorial is intended for use with a beta version of MailChimp for WooCommerce.
13
+ - We recommend you use this plugin in a staging environment before installing on production servers.
14
+ - This process requires an API Key from your MailChimp account. If you aren’t sure how to generate a MailChimp API Key, read [About API Keys.](http://kb.mailchimp.com/integrations/api-integrations/about-api-keys)
15
+ - This plugin supports MailChimp’s [Abandoned Cart Automation](http://kb.mailchimp.com/automation/create-an-abandoned-cart-workflow) feature.
16
+ WooCommerce customers who haven't signed up for marketing emails will appear in the Transactional portion of your list, and cannot be exported. See [View or Export a List.](http://kb.mailchimp.com/lists/managing-subscribers/view-or-export-a-list)
17
+ - To switch lists or accounts, you must deactivate and delete the plugin, then re-install it.
18
+ - In e-commerce reports and on subscriber profile pages, product variants will display as the parent product.
19
+ - This beta version of MailChimp for WooCommerce may have issues displaying grouped orders.
20
+
21
+ #Task Roadmap#
22
+ **Here’s a brief overview of this multi-step process.**
23
+
24
+ - Install the plugin on your WordPress Admin site.
25
+ - Connect the plugin with your MailChimp API Key, and configure your list settings to complete the data sync process.
26
+
27
+ #Install the Plugin#
28
+ **To install the plugin, follow these steps.**
29
+
30
+ 1) Log in to your WordPress admin panel.
31
+ 2) In the left navigation panel, click **Plugins**, and choose **Add New**.
32
+
33
+ ![Add new] (https://cloud.githubusercontent.com/assets/6547700/18677991/a7622bcc-7f28-11e6-8e8c-9bbdfa9861c7.png)
34
+
35
+ 3) Click **Upload Plugin**.
36
+
37
+ ![Upload] (https://cloud.githubusercontent.com/assets/6547700/18677997/a76dab82-7f28-11e6-98e4-4309739cd840.png)
38
+
39
+ 4) Click **Choose File** to select the ZIP file for the plugin, then click **Install Now**.
40
+
41
+ ![Install Now](https://cloud.githubusercontent.com/assets/6547700/18677988/a760949c-7f28-11e6-9e13-13c23d044ad4.png)
42
+
43
+ 5) Click **Activate Plugin**.
44
+
45
+ ![Activate plugin](https://cloud.githubusercontent.com/assets/6547700/18677990/a760d7c2-7f28-11e6-8741-12c1efa7a991.png)
46
+
47
+ 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.
48
+
49
+ #Configure and Sync#
50
+ **To configure your MailChimp settings for WooCommerce customers and sync them to MailChimp, follow these steps.**
51
+
52
+ 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**.
53
+
54
+ ![API key] (https://cloud.githubusercontent.com/assets/19805049/18877771/3fca90e8-849c-11e6-9e3a-161a7b3936dd.png)
55
+
56
+ 2) Navigate to the **Store Settings** tab.
57
+
58
+ ![Store Settings](https://cloud.githubusercontent.com/assets/6547700/18677998/a76e5640-7f28-11e6-9fd3-d66949fa1413.png)
59
+
60
+ 3) Enter the contact and location details for your WooCommerce Store, and click **Save all changes**.
61
+
62
+ ![Save all changes] (https://cloud.githubusercontent.com/assets/6547700/18677996/a76d126c-7f28-11e6-9150-4b289d20f057.png)
63
+
64
+ 4) Navigate to the **List Settings** tab.
65
+
66
+ ![List Settings tab] (https://cloud.githubusercontent.com/assets/19805049/18878446/961221d0-849e-11e6-99bb-175c22bf921e.png)
67
+
68
+ 5) Choose the list you want to sync, decide whether or not you want to auto-subscribe existing customers, set the subscribe message you want customers to see at checkout, and click **Save all changes**.
69
+
70
+ ![Save all changes](https://cloud.githubusercontent.com/assets/19805049/18877772/3fd24162-849c-11e6-8442-79ec4550b8ac.png)
71
+
72
+ All set! When you click **Save all changes**, we’ll start syncing your WooCommerce customers to MailChimp. To view progress, check the **Sync Status** tab.
73
+
74
+ If you have no lists in your MailChimp account, you will be given the option to create a new list on the **List Defaults** tab. To create a new list, set your list defaults, and click **Save all Changes** when you’re done. We’ll create a MailChimp list for you, and begin the data sync.
75
+
76
+ ![List Defaults tab](https://cloud.githubusercontent.com/assets/19805049/18956260/cffd3926-8628-11e6-9c68-9fe3c964c75c.png)
77
+
78
+ #Next Steps#
79
+ 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.
80
+
81
+ 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).
82
+
83
+ #Deactivate or Delete the Plugin#
84
+ 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.
85
+ To deactivate MailChimp for WooCommerce, follow these steps.
86
+
87
+ 1) Log in to your WordPress admin panel.
88
+
89
+ 2) In the left navigation panel, click **Plugins**, and choose **Installed Plugins**.
90
+
91
+ ![Installed Plugins](https://cloud.githubusercontent.com/assets/6547700/18677993/a76542ee-7f28-11e6-99dd-cfd6c1f5c24a.png)
92
+
93
+ 3) Click the box next to the MailChimp for WooCommerce plugin, and click **Deactivate**.
94
+
95
+ ![Deactivate](https://cloud.githubusercontent.com/assets/6547700/18677992/a762b844-7f28-11e6-9679-8d6c6a1d731d.png)
96
+
97
+ After you deactivate the plugin, you will have the option to **Delete** it. If you delete the plugin, you will retain customers’ email addresses in your list, but remove all associated e-commerce data.
README.txt ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ === vextras-woocommerce ===
2
+ Contributors: ryanhungate
3
+ Tags: ecommerce,email,workflows,mailchimp
4
+ Donate link: https://mailchimp.com
5
+ Requires at least: 4.3
6
+ Tested up to: 4.4.2
7
+ Stable tag: 4.4.2
8
+ License: GPLv2 or later
9
+ License URI: http://www.gnu.org/licenses/gpl-2.0.html
10
+
11
+ MailChimp - WooCommerce integration.
12
+
13
+ == Installation ==
admin/class-mailchimp-woocommerce-admin.php ADDED
@@ -0,0 +1,631 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * The admin-specific functionality of the plugin.
5
+ *
6
+ * @link https://mailchimp.com
7
+ * @since 1.0.1
8
+ *
9
+ * @package MailChimp_Woocommerce
10
+ * @subpackage MailChimp_Woocommerce/admin
11
+ */
12
+
13
+ /**
14
+ * The admin-specific functionality of the plugin.
15
+ *
16
+ * Defines the plugin name, version, and two examples hooks for how to
17
+ * enqueue the admin-specific stylesheet and JavaScript.
18
+ *
19
+ * @package MailChimp_Woocommerce
20
+ * @subpackage MailChimp_Woocommerce/admin
21
+ * @author Ryan Hungate <ryan@mailchimp.com>
22
+ */
23
+ class MailChimp_Woocommerce_Admin extends MailChimp_Woocommerce_Options {
24
+
25
+ /**
26
+ * @return MailChimp_Woocommerce_Admin
27
+ */
28
+ public static function connect()
29
+ {
30
+ $env = mailchimp_environment_variables();
31
+
32
+ return new self('mailchimp-woocommerce', $env->version);
33
+ }
34
+
35
+ /**
36
+ * Initialize the class and set its properties.
37
+ *
38
+ * @since 1.0.0
39
+ * @param string $plugin_name The name of this plugin.
40
+ * @param string $version The version of this plugin.
41
+ */
42
+ public function __construct( $plugin_name, $version ) {
43
+ $this->plugin_name = $plugin_name;
44
+ $this->version = $version;
45
+ }
46
+
47
+ /**
48
+ * Register the stylesheets for the admin area.
49
+ *
50
+ * @since 1.0.0
51
+ */
52
+ public function enqueue_styles() {
53
+ wp_enqueue_style( $this->plugin_name, plugin_dir_url( __FILE__ ) . 'css/mailchimp-woocommerce-admin.css', array(), $this->version, 'all' );
54
+ }
55
+
56
+ /**
57
+ * Register the JavaScript for the admin area.
58
+ *
59
+ * @since 1.0.0
60
+ */
61
+ public function enqueue_scripts() {
62
+ wp_enqueue_script( $this->plugin_name, plugin_dir_url( __FILE__ ) . 'js/mailchimp-woocommerce-admin.js', array( 'jquery' ), $this->version, false );
63
+ }
64
+
65
+ /**
66
+ * Register the administration menu for this plugin into the WordPress Dashboard menu.
67
+ *
68
+ * @since 1.0.0
69
+ */
70
+
71
+ public function add_plugin_admin_menu() {
72
+ /*
73
+ * Documentation : http://codex.wordpress.org/Administration_Menus
74
+ */
75
+ add_options_page( 'MailChimp - WooCommerce Setup', 'MailChimp', 'manage_options', $this->plugin_name, array($this, 'display_plugin_setup_page'));
76
+ }
77
+
78
+ /**
79
+ * Add settings action link to the plugins page.
80
+ *
81
+ * @since 1.0.0
82
+ */
83
+ public function add_action_links($links) {
84
+ /*
85
+ * Documentation : https://codex.wordpress.org/Plugin_API/Filter_Reference/plugin_action_links_(plugin_file_name)
86
+ */
87
+ $settings_link = array(
88
+ '<a href="' . admin_url( 'options-general.php?page=' . $this->plugin_name ) . '">' . __('Settings', $this->plugin_name) . '</a>',
89
+ );
90
+
91
+ return array_merge($settings_link, $links);
92
+ }
93
+
94
+ /**
95
+ * Admin bar
96
+ *
97
+ * @param WP_Admin_Bar $wp_admin_bar
98
+ */
99
+ public function admin_bar( $wp_admin_bar ) {
100
+ if ( ! current_user_can( 'manage_options' ) ) {
101
+ return;
102
+ }
103
+ $wp_admin_bar->add_menu( array(
104
+ 'id' => 'mailchimp-woocommerce',
105
+ 'title' => __('MailChimp', 'mailchimp-woocommerce' ),
106
+ 'href' => '#',
107
+ ));
108
+ $wp_admin_bar->add_menu( array(
109
+ 'parent' => 'mailchimp-woocommerce',
110
+ 'id' => 'mailchimp-woocommerce-api-key',
111
+ 'title' => __('API Key', 'mailchimp-woocommerce' ),
112
+ 'href' => wp_nonce_url(admin_url('options-general.php?page=mailchimp-woocommerce&tab=api_key'), 'mc-api-key'),
113
+ ));
114
+ $wp_admin_bar->add_menu( array(
115
+ 'parent' => 'mailchimp-woocommerce',
116
+ 'id' => 'mailchimp-woocommerce-store-info',
117
+ 'title' => __('Store Info', 'mailchimp-woocommerce' ),
118
+ 'href' => wp_nonce_url(admin_url('options-general.php?page=mailchimp-woocommerce&tab=store_info'), 'mc-store-info'),
119
+ ));
120
+ $wp_admin_bar->add_menu( array(
121
+ 'parent' => 'mailchimp-woocommerce',
122
+ 'id' => 'mailchimp-woocommerce-campaign-defaults',
123
+ 'title' => __('Campaign Defaults', 'mailchimp-woocommerce' ),
124
+ 'href' => wp_nonce_url(admin_url('options-general.php?page=mailchimp-woocommerce&tab=campaign_defaults'), 'mc-campaign-defaults'),
125
+ ));
126
+ $wp_admin_bar->add_menu( array(
127
+ 'parent' => 'mailchimp-woocommerce',
128
+ 'id' => 'mailchimp-woocommerce-newsletter-settings',
129
+ 'title' => __('Newsletter Settings', 'mailchimp-woocommerce' ),
130
+ 'href' => wp_nonce_url(admin_url('options-general.php?page=mailchimp-woocommerce&tab=newsletter_settings'), 'mc-newsletter-settings'),
131
+ ));
132
+
133
+ // only display this button if the data is not syncing and we have a valid api key
134
+ if ((bool) $this->getOption('mailchimp_list', false) && (bool) $this->getData('sync.syncing', false) === false) {
135
+ $wp_admin_bar->add_menu( array(
136
+ 'parent' => 'mailchimp-woocommerce',
137
+ 'id' => 'mailchimp-woocommerce-sync',
138
+ 'title' => __('Sync', 'mailchimp-woocommerce'),
139
+ 'href' => wp_nonce_url(admin_url('?mailchimp-woocommerce[action]=sync&mailchimp-woocommerce[action]=sync'), 'mc-sync'),
140
+ ));
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Render the settings page for this plugin.
146
+ *
147
+ * @since 1.0.0
148
+ */
149
+ public function display_plugin_setup_page() {
150
+ include_once( 'partials/mailchimp-woocommerce-admin-tabs.php' );
151
+ }
152
+
153
+ /**
154
+ *
155
+ */
156
+ public function options_update() {
157
+ register_setting($this->plugin_name, $this->plugin_name, array($this, 'validate'));
158
+ }
159
+
160
+ /**
161
+ * @param $input
162
+ * @return array
163
+ */
164
+ public function validate($input) {
165
+
166
+ $active_tab = isset($input['mailchimp_active_tab']) ? $input['mailchimp_active_tab'] : null;
167
+
168
+ if (empty($active_tab)) {
169
+ return $this->getOptions();
170
+ }
171
+
172
+ switch ($active_tab) {
173
+
174
+ case 'api_key':
175
+ $data = $this->validatePostApiKey($input);
176
+ break;
177
+
178
+ case 'store_info':
179
+ $data = $this->validatePostStoreInfo($input);
180
+ break;
181
+
182
+ case 'campaign_defaults' :
183
+ $data = $this->validatePostCampaignDefaults($input);
184
+ break;
185
+
186
+ case 'newsletter_settings':
187
+ $data = $this->validatePostNewsletterSettings($input);
188
+ break;
189
+ }
190
+
191
+ return (isset($data) && is_array($data)) ? array_merge($this->getOptions(), $data) : $this->getOptions();
192
+ }
193
+
194
+ /**
195
+ * STEP 1.
196
+ *
197
+ * Handle the 'api_key' tab post.
198
+ *
199
+ * @param $input
200
+ * @return array
201
+ */
202
+ protected function validatePostApiKey($input)
203
+ {
204
+ $data = array(
205
+ 'mailchimp_api_key' => isset($input['mailchimp_api_key']) ? $input['mailchimp_api_key'] : false,
206
+ 'mailchimp_debugging' => isset($input['mailchimp_debugging']) ? $input['mailchimp_debugging'] : false,
207
+ 'mailchimp_account_info_id' => null,
208
+ 'mailchimp_account_info_username' => null,
209
+ );
210
+
211
+ $api = new MailChimpApi($data['mailchimp_api_key']);
212
+
213
+ $valid = true;
214
+
215
+ if (empty($data['mailchimp_api_key']) || !($profile = $api->ping(true))) {
216
+ unset($data['mailchimp_api_key']);
217
+ $valid = false;
218
+ }
219
+
220
+ // tell our reporting system whether or not we had a valid ping.
221
+ $this->setData('validation.api.ping', $valid);
222
+
223
+ if ($valid && isset($profile) && is_array($profile) && array_key_exists('account_id', $profile)) {
224
+ $data['mailchimp_account_info_id'] = $profile['account_id'];
225
+ $data['mailchimp_account_info_username'] = $profile['username'];
226
+ }
227
+
228
+ return $data;
229
+ }
230
+
231
+ /**
232
+ * STEP 2.
233
+ *
234
+ * Handle the 'store_info' tab post.
235
+ *
236
+ * @param $input
237
+ * @return array
238
+ */
239
+ protected function validatePostStoreInfo($input)
240
+ {
241
+ $data = array(
242
+
243
+ // store basics
244
+ 'store_name' => isset($input['store_name']) ? $input['store_name'] : get_option('blogname'),
245
+ 'store_street' => isset($input['store_street']) ? $input['store_street'] : false,
246
+ 'store_city' => isset($input['store_city']) ? $input['store_city'] : false,
247
+ 'store_state' => isset($input['store_state']) ? $input['store_state'] : false,
248
+ 'store_postal_code' => isset($input['store_postal_code']) ? $input['store_postal_code'] : false,
249
+ 'store_country' => isset($input['store_country']) ? $input['store_country'] : false,
250
+ 'store_phone' => isset($input['store_phone']) ? $input['store_phone'] : false,
251
+
252
+ // locale info
253
+ 'store_locale' => isset($input['store_locale']) ? $input['store_locale'] : false,
254
+ 'store_timezone' => isset($input['store_timezone']) ? $input['store_timezone'] : false,
255
+ 'store_currency_code' => isset($input['store_currency_code']) ? $input['store_currency_code'] : false,
256
+
257
+ 'admin_email' => isset($input['admin_email']) && is_email($input['admin_email']) ? $input['admin_email'] : $this->getOption('admin_email', false),
258
+ );
259
+
260
+ if (!$this->hasValidStoreInfo($data)) {
261
+
262
+ if (empty($data['admin_email']) || empty($data['store_city']) || empty($data['store_state']) || empty($data['store_postal_code']) || empty($data['store_country']) || empty($data['store_street'])) {
263
+ add_settings_error('mailchimp_store_settings', '', 'As part of the MailChimp Terms of Use, we require a contact email and a physical mailing address.');
264
+ }
265
+
266
+ $this->setData('validation.store_info', false);
267
+ return array();
268
+ }
269
+
270
+ $this->setData('validation.store_info', true);
271
+
272
+ if ($this->hasValidMailChimpList()) {
273
+ $this->syncStore(array_merge($this->getOptions(), $data));
274
+ }
275
+
276
+ return $data;
277
+ }
278
+
279
+ /**
280
+ * STEP 3.
281
+ *
282
+ * Handle the 'campaign_defaults' tab post.
283
+ *
284
+ * @param $input
285
+ * @return array
286
+ */
287
+ protected function validatePostCampaignDefaults($input)
288
+ {
289
+ $data = array(
290
+ 'campaign_from_name' => isset($input['campaign_from_name']) ? $input['campaign_from_name'] : false,
291
+ 'campaign_from_email' => isset($input['campaign_from_email']) && is_email($input['campaign_from_email']) ? $input['campaign_from_email'] : false,
292
+ 'campaign_subject' => isset($input['campaign_subject']) ? $input['campaign_subject'] : get_option('blogname'),
293
+ 'campaign_language' => isset($input['campaign_language']) ? $input['campaign_language'] : 'en',
294
+ 'campaign_permission_reminder' => isset($input['campaign_permission_reminder']) ? $input['campaign_permission_reminder'] : 'You were subscribed to the newsletter from '.get_option('blogname'),
295
+ );
296
+
297
+ if (!$this->hasValidCampaignDefaults($data)) {
298
+ $this->setData('validation.campaign_defaults', false);
299
+ return array();
300
+ }
301
+
302
+ $this->setData('validation.campaign_defaults', true);
303
+
304
+ return $data;
305
+ }
306
+
307
+ /**
308
+ * STEP 4.
309
+ *
310
+ * Handle the 'newsletter_settings' tab post.
311
+ *
312
+ * @param $input
313
+ * @return array
314
+ */
315
+ protected function validatePostNewsletterSettings($input)
316
+ {
317
+ $data = array(
318
+ 'mailchimp_list' => isset($input['mailchimp_list']) ? $input['mailchimp_list'] : $this->getOption('mailchimp_list', ''),
319
+ 'newsletter_label' => isset($input['newsletter_label']) ? $input['newsletter_label'] : $this->getOption('newsletter_label', 'Subscribe to our newsletter'),
320
+ 'mailchimp_auto_subscribe' => isset($input['mailchimp_auto_subscribe']) ? $input['mailchimp_auto_subscribe'] : $this->getOption('mailchimp_auto_subscribe', '0'),
321
+ );
322
+
323
+ if ($data['mailchimp_list'] === 'create_new') {
324
+ $data['mailchimp_list'] = $this->createMailChimpList(array_merge($this->getOptions(), $data));
325
+ }
326
+
327
+ // as long as we have a list set, and it's currently in MC as a valid list, let's sync the store.
328
+ if (!empty($data['mailchimp_list']) && $this->api()->hasList($data['mailchimp_list'])) {
329
+
330
+ // sync the store with MC
331
+ $this->syncStore(array_merge($this->getOptions(), $data));
332
+
333
+ // start the sync automatically if the sync is false
334
+ if ((bool) $this->getData('sync.started_at', false) === false) {
335
+ $job = new MailChimp_WooCommerce_Process_Products();
336
+ wp_queue($job);
337
+ $job->flagStartSync();
338
+ }
339
+ }
340
+
341
+ return $data;
342
+ }
343
+
344
+ /**
345
+ * @param null|array $data
346
+ * @return bool
347
+ */
348
+ public function hasValidStoreInfo($data = null)
349
+ {
350
+ return $this->validateOptions(array(
351
+ 'store_name', 'store_street', 'store_city', 'store_state',
352
+ 'store_postal_code', 'store_country', 'store_phone',
353
+ 'store_locale', 'store_timezone', 'store_currency_code',
354
+ 'store_phone',
355
+ ), $data);
356
+ }
357
+
358
+ /**
359
+ * @param null|array $data
360
+ * @return bool
361
+ */
362
+ public function hasValidCampaignDefaults($data = null)
363
+ {
364
+ return $this->validateOptions(array(
365
+ 'campaign_from_name', 'campaign_from_email', 'campaign_subject', 'campaign_language',
366
+ 'campaign_permission_reminder'
367
+ ), $data);
368
+ }
369
+
370
+ /**
371
+ * @param null|array $data
372
+ * @return bool
373
+ */
374
+ public function hasValidApiKey($data = null)
375
+ {
376
+ if (!$this->validateOptions(array('mailchimp_api_key'), $data)) {
377
+ return false;
378
+ }
379
+
380
+ if (($pinged = $this->getCached('api-ping-check', null)) === null) {
381
+ if (($pinged = $this->api()->ping())) {
382
+ $this->setCached('api-ping-check', true, 120);
383
+ }
384
+ return $pinged;
385
+ }
386
+ return $pinged;
387
+ }
388
+
389
+ /**
390
+ * @return bool
391
+ */
392
+ public function hasValidMailChimpList()
393
+ {
394
+ if (!$this->hasValidApiKey()) {
395
+ add_settings_error('mailchimp_api_key', '', 'You must supply your MailChimp API key to pull the lists.');
396
+ return false;
397
+ }
398
+
399
+ if (!($this->validateOptions(array('mailchimp_list')))) {
400
+ return $this->api()->getLists(true);
401
+ }
402
+
403
+ return $this->api()->hasList($this->getOption('mailchimp_list'));
404
+ }
405
+
406
+ /**
407
+ * @return array|bool
408
+ */
409
+ public function getMailChimpLists()
410
+ {
411
+ if (!$this->hasValidApiKey()) {
412
+ return false;
413
+ }
414
+
415
+ try {
416
+ if (($pinged = $this->getCached('api-lists', null)) === null) {
417
+ $pinged = $this->api()->getLists(true);
418
+ if ($pinged) {
419
+ $this->setCached('api-lists', $pinged, 120);
420
+ }
421
+ return $pinged;
422
+ }
423
+ return $pinged;
424
+ } catch (\Exception $e) {
425
+ return array();
426
+ }
427
+ }
428
+
429
+ /**
430
+ * @return bool
431
+ */
432
+ public function isReadyForSync()
433
+ {
434
+ if (!$this->hasValidApiKey()) {
435
+ return false;
436
+ }
437
+
438
+ if (!$this->getOption('mailchimp_list', false)) {
439
+ return false;
440
+ }
441
+
442
+ if (!$this->api()->hasList($this->getOption('mailchimp_list'))) {
443
+ return false;
444
+ }
445
+
446
+ if (!$this->api()->getStore($this->getUniqueStoreID())) {
447
+ return false;
448
+ }
449
+
450
+ return true;
451
+ }
452
+
453
+ /**
454
+ * @param null|array $data
455
+ * @return bool|string
456
+ */
457
+ private function createMailChimpList($data = null)
458
+ {
459
+ if (empty($data)) {
460
+ $data = $this->getOptions();
461
+ }
462
+
463
+ $required = array(
464
+ 'store_name', 'store_street', 'store_city', 'store_state',
465
+ 'store_postal_code', 'store_country', 'campaign_from_name',
466
+ 'campaign_from_email', 'campaign_subject', 'campaign_permission_reminder',
467
+ );
468
+
469
+ foreach ($required as $requirement) {
470
+ if (!isset($data[$requirement]) || empty($data[$requirement])) {
471
+ return false;
472
+ }
473
+ }
474
+
475
+ $submission = new MailChimp_CreateListSubmission();
476
+
477
+ // allow the subscribers to choose preferred email type (html or text).
478
+ $submission->setEmailTypeOption(true);
479
+
480
+ // set the store name
481
+ $submission->setName($data['store_name']);
482
+
483
+ // set the campaign defaults
484
+ $submission->setCampaignDefaults(
485
+ $data['campaign_from_name'],
486
+ $data['campaign_from_email'],
487
+ $data['campaign_subject'],
488
+ $data['campaign_language']
489
+ );
490
+
491
+ // set the permission reminder message.
492
+ $submission->setPermissionReminder($data['campaign_permission_reminder']);
493
+
494
+ if (isset($data['admin_email']) && !empty($data['admin_email'])) {
495
+ $submission->setNotifyOnSubscribe($data['admin_email']);
496
+ $submission->setNotifyOnUnSubscribe($data['admin_email']);
497
+ }
498
+
499
+ $submission->setContact($this->address($data));
500
+
501
+ try {
502
+ $response = $this->api()->createList($submission);
503
+
504
+ $list_id = array_key_exists('id', $response) ? $response['id'] : false;
505
+
506
+ $this->setData('errors.mailchimp_list', false);
507
+
508
+ return $list_id;
509
+
510
+ } catch (MailChimp_Error $e) {
511
+ $this->setData('errors.mailchimp_list', $e->getMessage());
512
+ return false;
513
+ }
514
+ }
515
+
516
+ /**
517
+ * @param null $data
518
+ * @return bool
519
+ */
520
+ private function syncStore($data = null)
521
+ {
522
+ if (empty($data)) {
523
+ $data = $this->getOptions();
524
+ }
525
+
526
+ $site_url = $this->getUniqueStoreID();
527
+
528
+ $new = false;
529
+
530
+ if (!($store = $this->api()->getStore($site_url))) {
531
+ $new = true;
532
+ $store = new MailChimp_Store();
533
+ }
534
+
535
+ $list_id = $this->array_get($data, 'mailchimp_list', false);
536
+ $call = $new ? 'addStore' : 'updateStore';
537
+ $time_key = $new ? 'store_created_at' : 'store_updated_at';
538
+
539
+ $store->setId($site_url);
540
+ $store->setPlatform('woocommerce');
541
+
542
+ // set the locale data
543
+ $store->setPrimaryLocale($this->array_get($data, 'store_locale', 'en'));
544
+ $store->setTimezone($this->array_get($data, 'store_timezone', 'America\New_York'));
545
+ $store->setCurrencyCode($this->array_get($data, 'store_currency_code', 'USD'));
546
+
547
+ // set the basics
548
+ $store->setName($this->array_get($data, 'store_name'));
549
+ $store->setDomain(get_option('siteurl'));
550
+ $store->setEmailAddress($this->array_get($data, 'campaign_from_email'));
551
+ $store->setAddress($this->address($data));
552
+ $store->setPhone($this->array_get($data, 'store_phone'));
553
+ $store->setListId($list_id);
554
+
555
+ try {
556
+ // let's create a new store for this user through the API
557
+ $this->api()->$call($store);
558
+
559
+ // apply extra meta for store created at
560
+ $this->setData('errors.store_info', false);
561
+ $this->setData($time_key, time());
562
+
563
+ return true;
564
+
565
+ } catch (\Exception $e) {
566
+ $this->setData('errors.store_info', $e->getMessage());
567
+ }
568
+
569
+ return false;
570
+ }
571
+
572
+ /**
573
+ * @param array $data
574
+ * @return MailChimp_Address
575
+ */
576
+ private function address(array $data)
577
+ {
578
+ $address = new MailChimp_Address();
579
+
580
+ if (isset($data['store_street']) && $data['store_street']) {
581
+ $address->setAddress1($data['store_street']);
582
+ }
583
+
584
+ if (isset($data['store_city']) && $data['store_city']) {
585
+ $address->setCity($data['store_city']);
586
+ }
587
+
588
+ if (isset($data['store_state']) && $data['store_state']) {
589
+ $address->setProvince($data['store_state']);
590
+ }
591
+
592
+ if (isset($data['store_country']) && $data['store_country']) {
593
+ $address->setCountry($data['store_country']);
594
+ }
595
+
596
+ if (isset($data['store_postal_code']) && $data['store_postal_code']) {
597
+ $address->setPostalCode($data['store_postal_code']);
598
+ }
599
+
600
+ if (isset($data['store_name']) && $data['store_name']) {
601
+ $address->setCompany($data['store_name']);
602
+ }
603
+
604
+ if (isset($data['store_phone']) && $data['store_phone']) {
605
+ $address->setPhone($data['store_phone']);
606
+ }
607
+
608
+ $address->setCountryCode($this->array_get($data, 'store_currency_code', 'USD'));
609
+
610
+ return $address;
611
+ }
612
+
613
+ /**
614
+ * @param array $required
615
+ * @param null $options
616
+ * @return bool
617
+ */
618
+ private function validateOptions(array $required, $options = null)
619
+ {
620
+ $options = is_array($options) ? $options : $this->getOptions();
621
+
622
+ foreach ($required as $requirement) {
623
+ if (!isset($options[$requirement]) || empty($options[$requirement])) {
624
+ return false;
625
+ }
626
+ }
627
+
628
+ return true;
629
+ }
630
+
631
+ }
admin/css/mailchimp-woocommerce-admin.css ADDED
@@ -0,0 +1,4 @@
 
 
 
 
1
+ /**
2
+ * All of the CSS for your admin-specific functionality should be
3
+ * included in this file.
4
+ */
admin/index.php ADDED
@@ -0,0 +1 @@
 
1
+ <?php // Silence is golden
admin/js/mailchimp-woocommerce-admin.js ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ (function( $ ) {
2
+ 'use strict';
3
+
4
+ /**
5
+ * All of the code for your admin-facing JavaScript source
6
+ * should reside in this file.
7
+ *
8
+ * Note: It has been assumed you will write jQuery code here, so the
9
+ * $ function reference has been prepared for usage within the scope
10
+ * of this function.
11
+ *
12
+ * This enables you to define handlers, for when the DOM is ready:
13
+ *
14
+ * $(function() {
15
+ *
16
+ * });
17
+ *
18
+ * When the window is loaded:
19
+ *
20
+ * $( window ).load(function() {
21
+ *
22
+ * });
23
+ *
24
+ * ...and/or other possibilities.
25
+ *
26
+ * Ideally, it is not considered best practise to attach more than a
27
+ * single DOM-ready or window-load handler for a particular page.
28
+ * Although scripts in the WordPress core, Plugins and Themes may be
29
+ * practising this, we should strive to set a better example in our own work.
30
+ */
31
+
32
+ })( jQuery );
admin/partials/mailchimp-woocommerce-admin-tabs.php ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ $active_tab = isset($_GET['tab']) ? $_GET['tab'] : 'api_key';
3
+
4
+ $handler = MailChimp_Woocommerce_Admin::connect();
5
+
6
+ //Grab all options for this particular tab we're viewing.
7
+ $options = get_option($this->plugin_name, array());
8
+
9
+ $show_sync_tab = false;
10
+ $show_campaign_defaults = true;
11
+ $has_valid_api_key = false;
12
+ $allow_new_list = true;
13
+
14
+ if (isset($options['mailchimp_api_key']) && $handler->hasValidApiKey()) {
15
+ $has_valid_api_key = true;
16
+ // if we don't have a valid api key we need to redirect back to the 'api_key' tab.
17
+ if (($mailchimp_lists = $handler->getMailChimpLists()) && is_array($mailchimp_lists)) {
18
+ $show_campaign_defaults = false;
19
+ $allow_new_list = false;
20
+ }
21
+
22
+ // only display this button if the data is not syncing and we have a valid api key
23
+ if ((bool) $this->getData('sync.started_at', false)) {
24
+ $show_sync_tab = true;
25
+ }
26
+ }
27
+
28
+ ?>
29
+
30
+ <!-- Create a header in the default WordPress 'wrap' container -->
31
+ <div class="wrap">
32
+ <div id="icon-themes" class="icon32"></div>
33
+ <h2>MailChimp Settings</h2>
34
+
35
+ <h2 class="nav-tab-wrapper">
36
+ <a href="?page=mailchimp-woocommerce&tab=api_key" class="nav-tab <?php echo $active_tab == 'api_key' ? 'nav-tab-active' : ''; ?>">Connect</a>
37
+ <?php if($has_valid_api_key): ?>
38
+ <a href="?page=mailchimp-woocommerce&tab=store_info" class="nav-tab <?php echo $active_tab == 'store_info' ? 'nav-tab-active' : ''; ?>">Store Settings</a>
39
+ <?php if ($handler->hasValidStoreInfo()) : ?>
40
+ <?php if($show_campaign_defaults): ?>
41
+ <a href="?page=mailchimp-woocommerce&tab=campaign_defaults" class="nav-tab <?php echo $active_tab == 'campaign_defaults' ? 'nav-tab-active' : ''; ?>">List Defaults</a>
42
+ <?php endif; ?>
43
+ <a href="?page=mailchimp-woocommerce&tab=newsletter_settings" class="nav-tab <?php echo $active_tab == 'newsletter_settings' ? 'nav-tab-active' : ''; ?>">List Settings</a>
44
+ <?php if($show_sync_tab): ?>
45
+ <a href="?page=mailchimp-woocommerce&tab=sync" class="nav-tab <?php echo $active_tab == 'sync' ? 'nav-tab-active' : ''; ?>">Sync Status</a>
46
+ <?php endif; ?>
47
+ <?php endif;?>
48
+ <?php endif; ?>
49
+ </h2>
50
+
51
+ <form method="post" name="cleanup_options" action="options.php">
52
+
53
+ <?php
54
+ settings_fields($this->plugin_name);
55
+ do_settings_sections($this->plugin_name);
56
+ //settings_errors();
57
+ include('tabs/notices.php');
58
+ ?>
59
+
60
+ <input type="hidden" name="<?php echo $this->plugin_name; ?>[mailchimp_active_tab]" value="<?php echo $active_tab; ?>"/>
61
+
62
+ <?php if( $active_tab == 'api_key' ): ?>
63
+ <?php include_once 'tabs/api_key.php'; ?>
64
+ <?php endif; ?>
65
+
66
+ <?php if( $active_tab == 'store_info' && $has_valid_api_key): ?>
67
+ <?php include_once 'tabs/store_info.php'; ?>
68
+ <?php endif; ?>
69
+
70
+ <?php if( $active_tab == 'campaign_defaults' ): ?>
71
+ <?php include_once 'tabs/campaign_defaults.php'; ?>
72
+ <?php endif; ?>
73
+
74
+ <?php if( $active_tab == 'newsletter_settings' ): ?>
75
+ <?php include_once 'tabs/newsletter_settings.php'; ?>
76
+ <?php endif; ?>
77
+
78
+ <?php if( $active_tab == 'sync' && $show_sync_tab): ?>
79
+ <?php include_once 'tabs/store_sync.php'; ?>
80
+ <?php endif; ?>
81
+
82
+ <?php if ($active_tab !== 'sync') submit_button('Save all changes', 'primary','submit', TRUE); ?>
83
+
84
+ <?php if($show_sync_tab && isset($_GET['show_sync']) && $_GET['show_sync'] === '1'): ?>
85
+ <p><a style="float:left;" class="btn" href="/?mailchimp_woocommerce[action]=sync">Re-Sync</a></p>
86
+ <?php endif; ?>
87
+
88
+ </form>
89
+
90
+ </div><!-- /.wrap -->
admin/partials/tabs/api_key.php ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+
3
+
4
+ <?php
5
+
6
+ if (isset($options['mailchimp_api_key']) && !$handler->hasValidApiKey()) {
7
+ include_once __DIR__.'/errors/missing_api_key.php';
8
+ }
9
+
10
+ ?>
11
+
12
+ <h2 style="padding-top: 1em;">API Information</h2>
13
+ <p>To find your MailChimp API key, log into your account settings > Extras > API keys. From there, either grab an existing key or generate a new one for your WooCommerce store. </p>
14
+
15
+ <!-- remove some meta and generators from the <head> -->
16
+ <fieldset>
17
+ <legend class="screen-reader-text">
18
+ <span>MailChimp API Key</span>
19
+ </legend>
20
+ <label for="<?php echo $this->plugin_name; ?>-mailchimp-api-key">
21
+ <input style="width: 30%;" type="password" id="<?php echo $this->plugin_name; ?>-mailchimp-api-key" name="<?php echo $this->plugin_name; ?>[mailchimp_api_key]" value="<?php echo isset($options['mailchimp_api_key']) ? $options['mailchimp_api_key'] : '' ?>" />
22
+ <span><?php esc_attr_e('Enter your MailChimp API key.', $this->plugin_name); ?></span>
23
+ </label>
24
+ </fieldset>
25
+
26
+ <fieldset>
27
+ <legend class="screen-reader-text">
28
+ <span>Enable Debugging</span>
29
+ </legend>
30
+ <label for="<?php echo $this->plugin_name; ?>-mailchimp-debugging">
31
+ <select name="<?php echo $this->plugin_name; ?>[mailchimp_debugging]" style="width:30%">
32
+
33
+ <?php
34
+
35
+ $enable_mailchimp_debugging = (array_key_exists('mailchimp_debugging', $options) && !is_null($options['mailchimp_debugging'])) ? $options['mailchimp_debugging'] : '1';
36
+
37
+ foreach (['0' => 'No', '1' => 'Yes'] as $key => $value ) {
38
+ echo '<option value="' . esc_attr($key) . '" ' . selected($key == $enable_mailchimp_debugging, true, false ) . '>' . esc_html( $value ) . '</option>';
39
+ }
40
+ ?>
41
+
42
+ </select>
43
+ <span><?php esc_attr_e('Enable debugging logs to be sent to MailChimp.', $this->plugin_name); ?></span>
44
+ </label>
45
+ </fieldset>
46
+
admin/partials/tabs/campaign_defaults.php ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ $handler = MailChimp_Woocommerce_Admin::connect();
4
+
5
+ // if we don't have valid campaign defaults we need to redirect back to the 'campaign_defaults' tab.
6
+ if (!$handler->hasValidApiKey()) {
7
+ wp_redirect('options-general.php?page=mailchimp-woocommerce&tab=api_key&error_notice=missing_api_key');
8
+ }
9
+ if (!$handler->hasValidStoreInfo()) {
10
+ wp_redirect('options-general.php?page=mailchimp-woocommerce&tab=store_info&error_notice=missing_store');
11
+ }
12
+
13
+ ?>
14
+
15
+ <h2 style="padding-top: 1em;">List Defaults</h2>
16
+ <p>Please fill out the default campaign information.</p>
17
+
18
+ <fieldset>
19
+ <legend class="screen-reader-text">
20
+ <span>Contact Name</span>
21
+ </legend>
22
+ <label for="<?php echo $this->plugin_name; ?>-campaign-from-name-label">
23
+ <input style="width: 30%;" type="text" id="<?php echo $this->plugin_name; ?>-campaign-from-name-label" name="<?php echo $this->plugin_name; ?>[campaign_from_name]" value="<?php echo isset($options['campaign_from_name']) ? $options['campaign_from_name'] : '' ?>" />
24
+ <span><?php esc_attr_e('Default from name', $this->plugin_name); ?></span>
25
+ </label>
26
+ </fieldset>
27
+
28
+ <fieldset>
29
+ <legend class="screen-reader-text">
30
+ <span>From Email</span>
31
+ </legend>
32
+ <label for="<?php echo $this->plugin_name; ?>-campaign-from-email-label">
33
+ <input style="width: 30%;" type="text" id="<?php echo $this->plugin_name; ?>-campaign-from-email-label" name="<?php echo $this->plugin_name; ?>[campaign_from_email]" value="<?php echo isset($options['campaign_from_email']) ? $options['campaign_from_email'] : get_option('admin_email') ?>" />
34
+ <span><?php esc_attr_e('Default from email', $this->plugin_name); ?></span>
35
+ </label>
36
+ </fieldset>
37
+
38
+ <fieldset>
39
+ <legend class="screen-reader-text">
40
+ <span>Default Subject</span>
41
+ </legend>
42
+ <label for="<?php echo $this->plugin_name; ?>-campaign-subject-label">
43
+ <input style="width: 30%;" type="text" id="<?php echo $this->plugin_name; ?>-campaign-subject-label" name="<?php echo $this->plugin_name; ?>[campaign_subject]" value="<?php echo isset($options['campaign_subject']) ? $options['campaign_subject'] : get_option('blogname') ?>" />
44
+ <span><?php esc_attr_e('Default subject', $this->plugin_name); ?></span>
45
+ </label>
46
+ </fieldset>
47
+
48
+ <fieldset>
49
+ <legend class="screen-reader-text">
50
+ <span>Default Language</span>
51
+ </legend>
52
+ <label for="<?php echo $this->plugin_name; ?>-campaign-language-label">
53
+ <select id="<?php echo $this->plugin_name; ?>-campaign-language-label" name="<?php echo $this->plugin_name; ?>[campaign_language]" style="width:30%" required>
54
+ <?php $selected_locale = isset($options['campaign_language']) && !empty($options['campaign_language']) ? $options['campaign_language'] : 'en'; ?>
55
+ <?php
56
+ foreach(MailChimp_Api_Locales::simple() as $locale_key => $local_value) {
57
+ echo '<option value="' . esc_attr( $locale_key ) . '" ' . selected($locale_key === $selected_locale, true, false ) . '>' . esc_html( $local_value ) . '</option>';
58
+ }
59
+ ?>
60
+ </select>
61
+ <span><?php esc_attr_e('Default language', $this->plugin_name); ?></span>
62
+ </label>
63
+ </fieldset>
64
+
65
+ <fieldset>
66
+ <legend class="screen-reader-text">
67
+ <span>Permission Reminder</span>
68
+ </legend>
69
+ <label for="<?php echo $this->plugin_name; ?>-campaign-permission-reminder-label">
70
+ <textarea style="width: 30%;" id="<?php echo $this->plugin_name; ?>-campaign-permission-reminder-label" name="<?php echo $this->plugin_name; ?>[campaign_permission_reminder]"><?php echo isset($options['campaign_permission_reminder']) ? $options['campaign_permission_reminder'] : 'You were subscribed to the newsletter from '.get_option('blogname') ?></textarea>
71
+ <span><?php esc_attr_e('Permission reminder message', $this->plugin_name); ?></span>
72
+ </label>
73
+ </fieldset>
admin/partials/tabs/errors/missing_api_key.php ADDED
@@ -0,0 +1,3 @@
 
 
 
1
+ <div class="error notice is-dismissable">
2
+ <p><?php _e('MailChimp says: You must enter in a valid API key.', 'mailchimp-woocommerce'); ?></p>
3
+ </div>
admin/partials/tabs/errors/missing_campaign_defaults.php ADDED
@@ -0,0 +1,3 @@
 
 
 
1
+ <div class="error notice is-dismissable">
2
+ <p><?php _e('MailChimp says: Sorry you must set up your campaign defaults before you proceed!', 'mailchimp-woocommerce'); ?></p>
3
+ </div>
admin/partials/tabs/errors/missing_list.php ADDED
@@ -0,0 +1,3 @@
 
 
 
1
+ <div class="error notice is-dismissable">
2
+ <p><?php _e('MailChimp says: You must select a marketing list.', 'mailchimp-woocommerce'); ?></p>
3
+ </div>
admin/partials/tabs/errors/missing_store.php ADDED
@@ -0,0 +1,3 @@
 
 
 
1
+ <div class="error notice is-dismissable">
2
+ <p><?php _e('MailChimp says: Sorry you must set up your store before you proceed!', 'mailchimp-woocommerce'); ?></p>
3
+ </div>
admin/partials/tabs/errors/not_ready_for_sync.php ADDED
@@ -0,0 +1,3 @@
 
 
 
1
+ <div class="error notice is-dismissable">
2
+ <p><?php _e('MailChimp says: You are not fully ready to run the Store Sync, please verify your settings before proceeding.', 'mailchimp-woocommerce'); ?></p>
3
+ </div>
admin/partials/tabs/newsletter_settings.php ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ // if we don't have valid campaign defaults we need to redirect back to the 'campaign_defaults' tab.
3
+ if (!$handler->hasValidApiKey()) {
4
+ wp_redirect('options-general.php?page=mailchimp-woocommerce&tab=api_key&error_notice=missing_api_key');
5
+ }
6
+
7
+ // if we don't have valid store information, we need to redirect back to the 'store_info' tab.
8
+ if (!$handler->hasValidStoreInfo()) {
9
+ wp_redirect('options-general.php?page=mailchimp-woocommerce&tab=store_info&error_notice=missing_store');
10
+ }
11
+
12
+ // if we don't have a valid api key we need to redirect back to the 'api_key' tab.
13
+ if (!isset($mailchimp_lists) && ($mailchimp_lists = $handler->getMailChimpLists()) === false) {
14
+ wp_redirect('options-general.php?page=mailchimp-woocommerce&tab=api_key&error_notice=missing_api_key');
15
+ }
16
+
17
+ // if we don't have valid campaign defaults we need to redirect back to the 'campaign_defaults' tab.
18
+ if (empty($mailchimp_lists) && !$handler->hasValidCampaignDefaults()) {
19
+ wp_redirect('options-general.php?page=mailchimp-woocommerce&tab=campaign_defaults&error_notice=missing_campaign_defaults');
20
+ }
21
+
22
+ $list_is_configured = isset($options['mailchimp_list']) && (!empty($options['mailchimp_list'])) && array_key_exists($options['mailchimp_list'], $mailchimp_lists);
23
+ ?>
24
+
25
+ <?php if(($newsletter_settings_error = $this->getData('errors.mailchimp_list', false))) : ?>
26
+ <div class="error notice is-dismissable">
27
+ <p><?php _e($newsletter_settings_error, 'mailchimp-woocommerce'); ?></p>
28
+ </div>
29
+ <?php endif; ?>
30
+
31
+ <h2 style="padding-top: 1em;">List Settings</h2>
32
+ <p>Please apply your list settings. If you don't have a list, you can choose to create one.</p>
33
+
34
+ <fieldset>
35
+ <legend class="screen-reader-text">
36
+ <span>List Name</span>
37
+ </legend>
38
+ <label for="<?php echo $this->plugin_name; ?>-mailchimp-list-label">
39
+ <select name="<?php echo $this->plugin_name; ?>[mailchimp_list]" style="width:30%" required <?php if($list_is_configured): ?> disabled <?php endif; ?>>
40
+
41
+ <?php if(!isset($allow_new_list) || $allow_new_list === true): ?>
42
+ <option value="create_new">Create New List</option>
43
+ <?php endif ?>
44
+
45
+ <?php if(isset($allow_new_list) && $allow_new_list === false): ?>
46
+ <option value="">-- Select List --</option>
47
+ <?php endif; ?>
48
+
49
+ <?php
50
+ if (is_array($mailchimp_lists)) {
51
+ foreach ($mailchimp_lists as $key => $value ) {
52
+ echo '<option value="' . esc_attr( $key ) . '" ' . selected( $key === $options['mailchimp_list'], true, false ) . '>' . esc_html( $value ) . '</option>';
53
+ }
54
+ }
55
+ ?>
56
+ </select>
57
+ <span><?php esc_attr_e('Choose a list to sync with your store.', $this->plugin_name); ?></span>
58
+ </label>
59
+ </fieldset>
60
+
61
+ <fieldset>
62
+ <legend class="screen-reader-text">
63
+ <span>Auto Subscribe On Initial Sync</span>
64
+ </legend>
65
+ <label for="<?php echo $this->plugin_name; ?>-mailchimp-auto-subscribe">
66
+ <select name="<?php echo $this->plugin_name; ?>[mailchimp_auto_subscribe]" style="width:30%" required <?php if($list_is_configured): ?> disabled <?php endif; ?>>
67
+
68
+ <?php
69
+ $enable_auto_subscribe = (array_key_exists('mailchimp_auto_subscribe', $options) && !is_null($options['mailchimp_auto_subscribe'])) ? $options['mailchimp_auto_subscribe'] : '1';
70
+
71
+ foreach (['0' => 'No', '1' => 'Yes'] as $key => $value ) {
72
+ echo '<option value="' . esc_attr( $key ) . '" ' . selected($key == $enable_auto_subscribe, true, false ) . '>' . esc_html( $value ) . '</option>';
73
+ }
74
+ ?>
75
+
76
+ </select>
77
+ <span><?php esc_attr_e('During initial sync, auto subscribe the existing customers.', $this->plugin_name); ?></span>
78
+ </label>
79
+ </fieldset>
80
+
81
+ <fieldset>
82
+ <legend class="screen-reader-text">
83
+ <span>MailChimp Newsletter Label</span>
84
+ </legend>
85
+ <label for="<?php echo $this->plugin_name; ?>-newsletter-checkbox-label">
86
+ <input style="width: 30%;" type="text" id="<?php echo $this->plugin_name; ?>-newsletter-checkbox-label" name="<?php echo $this->plugin_name; ?>[newsletter_label]" value="<?php echo isset($options['newsletter_label']) ? $options['newsletter_label'] : 'Subscribe to our newsletter' ?>" />
87
+ <span><?php esc_attr_e('Write a subscribe message for customers at checkout.', $this->plugin_name); ?></span>
88
+ </label>
89
+ </fieldset>
admin/partials/tabs/notices.php ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
1
+ <?php if(isset($_GET['error_notice']) && file_exists(__DIR__.'/errors/'.$_GET['error_notice'].'.php')): ?>
2
+ <?php include(__DIR__.'/errors/'.$_GET['error_notice'].'.php'); ?>
3
+ <?php endif; ?>
4
+
5
+ <?php if(isset($_GET['success_notice']) && file_exists(__DIR__.'/success/'.$_GET['success_notice'].'.php')): ?>
6
+ <?php include(__DIR__.'/success/'.$_GET['success_notice'].'.php'); ?>
7
+ <?php endif; ?>
8
+
admin/partials/tabs/store_info.php ADDED
@@ -0,0 +1,148 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ $handler = MailChimp_Woocommerce_Admin::connect();
4
+
5
+ // if we don't have valid campaign defaults we need to redirect back to the 'campaign_defaults' tab.
6
+ if (!$handler->hasValidApiKey()) {
7
+ wp_redirect('options-general.php?page=mailchimp-woocommerce&tab=api_key&error_notice=missing_api_key');
8
+ }
9
+
10
+ ?>
11
+
12
+ <h2 style="padding-top: 1em;">Store Settings</h2>
13
+ <p>Please provide the following information about your WooCommerce store.</p>
14
+
15
+ <fieldset>
16
+ <legend class="screen-reader-text">
17
+ <span>Store Name</span>
18
+ </legend>
19
+ <label for="<?php echo $this->plugin_name; ?>-store-name-label">
20
+ <input style="width: 30%;" type="text" id="<?php echo $this->plugin_name; ?>-store-name-label" name="<?php echo $this->plugin_name; ?>[store_name]" value="<?php echo isset($options['store_name']) ? $options['store_name'] : get_option('blogname') ?>" />
21
+ <span><?php esc_attr_e('Name', $this->plugin_name); ?></span>
22
+ </label>
23
+ </fieldset>
24
+
25
+ <fieldset>
26
+ <legend class="screen-reader-text">
27
+ <span>Email</span>
28
+ </legend>
29
+ <label for="<?php echo $this->plugin_name; ?>-admin-email-label">
30
+ <input style="width: 30%;" type="email" id="<?php echo $this->plugin_name; ?>-admin-email-label" name="<?php echo $this->plugin_name; ?>[admin_email]" value="<?php echo isset($options['admin_email']) ? $options['admin_email'] : get_option('admin_email') ?>" />
31
+ <span><?php esc_attr_e('Email', $this->plugin_name); ?></span>
32
+ </label>
33
+ </fieldset>
34
+
35
+ <fieldset>
36
+ <legend class="screen-reader-text">
37
+ <span>Street Address</span>
38
+ </legend>
39
+ <label for="<?php echo $this->plugin_name; ?>-store-address-label">
40
+ <input style="width: 30%;" type="text" id="<?php echo $this->plugin_name; ?>-store-address-label" name="<?php echo $this->plugin_name; ?>[store_street]" value="<?php echo isset($options['store_street']) ? $options['store_street'] : '' ?>" />
41
+ <span><?php esc_attr_e('Street address', $this->plugin_name); ?></span>
42
+ </label>
43
+ </fieldset>
44
+
45
+ <fieldset>
46
+ <legend class="screen-reader-text">
47
+ <span>City</span>
48
+ </legend>
49
+ <label for="<?php echo $this->plugin_name; ?>-store-city-label">
50
+ <input style="width: 30%;" type="text" id="<?php echo $this->plugin_name; ?>-store-city-label" name="<?php echo $this->plugin_name; ?>[store_city]" value="<?php echo isset($options['store_city']) ? $options['store_city'] : '' ?>" />
51
+ <span><?php esc_attr_e('City', $this->plugin_name); ?></span>
52
+ </label>
53
+ </fieldset>
54
+
55
+ <fieldset>
56
+ <legend class="screen-reader-text">
57
+ <span>State</span>
58
+ </legend>
59
+ <label for="<?php echo $this->plugin_name; ?>-store-state-label">
60
+ <input style="width: 30%;" type="text" id="<?php echo $this->plugin_name; ?>-store-state-label" name="<?php echo $this->plugin_name; ?>[store_state]" value="<?php echo isset($options['store_state']) ? $options['store_state'] : '' ?>" />
61
+ <span><?php esc_attr_e('State', $this->plugin_name); ?></span>
62
+ </label>
63
+ </fieldset>
64
+
65
+ <fieldset>
66
+ <legend class="screen-reader-text">
67
+ <span>Postal Code</span>
68
+ </legend>
69
+ <label for="<?php echo $this->plugin_name; ?>-store-state-label">
70
+ <input style="width: 30%;" type="text" id="<?php echo $this->plugin_name; ?>-store-postal-code-label" name="<?php echo $this->plugin_name; ?>[store_postal_code]" value="<?php echo isset($options['store_postal_code']) ? $options['store_postal_code'] : '' ?>" />
71
+ <span><?php esc_attr_e('Postal Code', $this->plugin_name); ?></span>
72
+ </label>
73
+ </fieldset>
74
+
75
+ <fieldset>
76
+ <legend class="screen-reader-text">
77
+ <span>Country</span>
78
+ </legend>
79
+ <label for="<?php echo $this->plugin_name; ?>-store-country-label">
80
+ <input style="width: 30%;" type="text" id="<?php echo $this->plugin_name; ?>-store-country-label" name="<?php echo $this->plugin_name; ?>[store_country]" value="<?php echo isset($options['store_country']) ? $options['store_country'] : 'US' ?>" />
81
+ <span><?php esc_attr_e('Country', $this->plugin_name); ?></span>
82
+ </label>
83
+ </fieldset>
84
+
85
+ <fieldset>
86
+ <legend class="screen-reader-text">
87
+ <span>Phone</span>
88
+ </legend>
89
+ <label for="<?php echo $this->plugin_name; ?>-store-phone-label">
90
+ <input style="width: 30%;" type="text" id="<?php echo $this->plugin_name; ?>-store-phone-label" name="<?php echo $this->plugin_name; ?>[store_phone]" value="<?php echo isset($options['store_phone']) ? $options['store_phone'] : '' ?>" />
91
+ <span><?php esc_attr_e('Phone Number', $this->plugin_name); ?></span>
92
+ </label>
93
+ </fieldset>
94
+
95
+ <h2 style="padding-top: 1em;">Locale Settings</h2>
96
+
97
+ <p>Please apply your locale settings. If you're unsure about these, use the defaults.</p>
98
+
99
+ <fieldset>
100
+ <legend class="screen-reader-text">
101
+ <span>Locale</span>
102
+ </legend>
103
+ <label for="<?php echo $this->plugin_name; ?>-store-locale-label">
104
+ <select name="<?php echo $this->plugin_name; ?>[store_locale]" style="width:30%" required>
105
+ <?php $selected_locale = isset($options['store_locale']) && !empty($options['store_locale']) ? $options['store_locale'] : 'en'; ?>
106
+ <?php
107
+ foreach(MailChimp_Api_Locales::simple() as $locale_key => $local_value) {
108
+ echo '<option value="' . esc_attr( $locale_key ) . '" ' . selected($locale_key === $selected_locale, true, false ) . '>' . esc_html( $local_value ) . '</option>';
109
+ }
110
+ ?>
111
+ </select>
112
+ <span><?php esc_attr_e('Locale', $this->plugin_name); ?></span>
113
+ </label>
114
+ </fieldset>
115
+
116
+ <fieldset>
117
+ <legend class="screen-reader-text">
118
+ <span>Currency Code</span>
119
+ </legend>
120
+ <label for="<?php echo $this->plugin_name; ?>-store-currency-code-label">
121
+ <select name="<?php echo $this->plugin_name; ?>[store_currency_code]" style="width:30%" required>
122
+ <?php
123
+ $selected_currency_code = isset($options['store_currency_code']) && !empty($options['store_currency_code']) ? $options['store_currency_code'] : 'USD';
124
+ foreach (MailChimp_Api_CurrencyCodes::lists() as $key => $value ) {
125
+ echo '<option value="' . esc_attr( $key ) . '" ' . selected($key === $selected_currency_code, true, false ) . '>' . esc_html( $value ) . '</option>';
126
+ }
127
+ ?>
128
+ </select>
129
+ <span><?php esc_attr_e('Currency', $this->plugin_name); ?></span>
130
+ </label>
131
+ </fieldset>
132
+
133
+ <fieldset>
134
+ <legend class="screen-reader-text">
135
+ <span>Timezone</span>
136
+ </legend>
137
+ <label for="<?php echo $this->plugin_name; ?>-store-timezone-label">
138
+ <select name="<?php echo $this->plugin_name; ?>[store_timezone]" style="width:30%" required>
139
+ <?php $selected_timezone = isset($options['store_timezone']) && !empty($options['store_timezone']) ? $options['store_timezone'] : 'America/New_York'; ?>
140
+ <?php
141
+ foreach(mailchimp_get_timezone_list() as $t) {
142
+ echo '<option value="' . esc_attr( $t['zone'] ) . '" ' . selected($t['zone'] === $selected_timezone, true, false ) . '>' . esc_html( $t['diff_from_GMT'] . ' - ' . $t['zone'] ) . '</option>';
143
+ }
144
+ ?>
145
+ </select>
146
+ <span><?php esc_attr_e('Timezone', $this->plugin_name); ?></span>
147
+ </label>
148
+ </fieldset>
admin/partials/tabs/store_sync.php ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ $handler = MailChimp_Woocommerce_Admin::connect();
4
+
5
+ // if we're not ready for a sync, we need to redirect out of this page like now.
6
+ //if (!$handler->isReadyForSync()) {
7
+ // wp_redirect('options-general.php?page=mailchimp-woocommerce&tab=api_key&error_notice=not_ready_for_sync');
8
+ //}
9
+
10
+ if (($sync_started_at = $this->getData('sync.started_at', false))) {
11
+ $date = mailchimp_date_local(date("c", $sync_started_at));
12
+ $sync_started_at = $date->format('D, M j, Y g:i A');
13
+ } else {
14
+ $sync_started_at = 'N/A';
15
+ }
16
+
17
+ if (($sync_complete_at = $this->getData('sync.completed_at', false))) {
18
+ $date = mailchimp_date_local(date("c", $sync_complete_at));
19
+ $sync_complete_at = $date->format('D, M j, Y g:i A');
20
+ } else {
21
+ $sync_complete_at = $sync_started_at !== 'N/A' ? 'In Progress' : 'N/A';
22
+ }
23
+
24
+ ?>
25
+
26
+ <h2 style="padding-top: 1em;">Sync Timeline</h2>
27
+
28
+ <p>Sync Started: <?php echo $sync_started_at; ?></p>
29
+ <p>Sync Completed: <?php echo $sync_complete_at; ?></p>
30
+
admin/partials/tabs/success/re-sync-started.php ADDED
@@ -0,0 +1,3 @@
 
 
 
1
+ <div class="success notice is-dismissable">
2
+ <p><?php _e('MailChimp says: Your re-sync has been started!', 'mailchimp-woocommerce'); ?></p>
3
+ </div>
changelog.md ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ** 0.1.11 **
2
+ * removed an extra debug log that was not needed
3
+
4
+ ** 0.1.10 **
5
+ * altered debug logging and fixed store settings validation requirements
6
+
7
+ ** 0.1.9 **
8
+ * using fallback to stream context during failed patch requests
9
+
10
+ ** 0.1.8 **
11
+ * fixing http request header for larger patch requests
12
+
13
+ ** 0.1.7 **
14
+ * fixing various bugs with the sync and product issues.
15
+
16
+ ** 0.1.2 **
17
+ * fixed admin order update hook.
includes/api/assets/class-mailchimp-address.php ADDED
@@ -0,0 +1,337 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Created by Vextras.
5
+ *
6
+ * Name: Ryan Hungate
7
+ * Email: ryan@mailchimp.com
8
+ * Date: 3/8/16
9
+ * Time: 2:22 PM
10
+ */
11
+ class MailChimp_Address
12
+ {
13
+ protected $type;
14
+ protected $name;
15
+ protected $address1;
16
+ protected $address2;
17
+ protected $city;
18
+ protected $province;
19
+ protected $province_code;
20
+ protected $postal_code;
21
+ protected $country;
22
+ protected $country_code;
23
+ protected $longitude;
24
+ protected $latitude;
25
+ protected $phone;
26
+ protected $company;
27
+
28
+ /**
29
+ * @return array
30
+ */
31
+ public function getValidation()
32
+ {
33
+ return array(
34
+ 'address1' => 'string',
35
+ 'address2' => 'string',
36
+ 'city' => 'string',
37
+ 'province' => 'string',
38
+ 'province_code' => 'string|digits:2',
39
+ 'postal_code' => 'string',
40
+ 'country' => 'string',
41
+ 'country_code' => 'string|digits:2',
42
+ 'latitude' => 'numeric',
43
+ 'longitude' => 'numeric',
44
+ );
45
+ }
46
+
47
+ /**
48
+ * @return mixed
49
+ */
50
+ public function getName()
51
+ {
52
+ return $this->name;
53
+ }
54
+
55
+ /**
56
+ * @param mixed $name
57
+ * @return MailChimp_Address
58
+ */
59
+ public function setName($name)
60
+ {
61
+ $this->name = $name;
62
+
63
+ return $this;
64
+ }
65
+
66
+ /**
67
+ * @return mixed
68
+ */
69
+ public function getAddress1()
70
+ {
71
+ return $this->address1;
72
+ }
73
+
74
+ /**
75
+ * @param mixed $address1
76
+ * @return MailChimp_Address
77
+ */
78
+ public function setAddress1($address1)
79
+ {
80
+ $this->address1 = $address1;
81
+
82
+ return $this;
83
+ }
84
+
85
+ /**
86
+ * @return mixed
87
+ */
88
+ public function getAddress2()
89
+ {
90
+ return $this->address2;
91
+ }
92
+
93
+ /**
94
+ * @param mixed $address2
95
+ * @return MailChimp_Address
96
+ */
97
+ public function setAddress2($address2)
98
+ {
99
+ $this->address2 = $address2;
100
+
101
+ return $this;
102
+ }
103
+
104
+ /**
105
+ * @return mixed
106
+ */
107
+ public function getCity()
108
+ {
109
+ return $this->city;
110
+ }
111
+
112
+ /**
113
+ * @param mixed $city
114
+ * @return MailChimp_Address
115
+ */
116
+ public function setCity($city)
117
+ {
118
+ $this->city = $city;
119
+
120
+ return $this;
121
+ }
122
+
123
+ /**
124
+ * @return mixed
125
+ */
126
+ public function getProvince()
127
+ {
128
+ return $this->province;
129
+ }
130
+
131
+ /**
132
+ * @param mixed $province
133
+ * @return MailChimp_Address
134
+ */
135
+ public function setProvince($province)
136
+ {
137
+ $this->province = $province;
138
+
139
+ return $this;
140
+ }
141
+
142
+ /**
143
+ * @return mixed
144
+ */
145
+ public function getProvinceCode()
146
+ {
147
+ return $this->province_code;
148
+ }
149
+
150
+ /**
151
+ * @param mixed $province_code
152
+ * @return MailChimp_Address
153
+ */
154
+ public function setProvinceCode($province_code)
155
+ {
156
+ $this->province_code = $province_code;
157
+
158
+ return $this;
159
+ }
160
+
161
+ /**
162
+ * @return mixed
163
+ */
164
+ public function getPostalCode()
165
+ {
166
+ return $this->postal_code;
167
+ }
168
+
169
+ /**
170
+ * @param mixed $postal_code
171
+ * @return MailChimp_Address
172
+ */
173
+ public function setPostalCode($postal_code)
174
+ {
175
+ $this->postal_code = $postal_code;
176
+
177
+ return $this;
178
+ }
179
+
180
+ /**
181
+ * @return mixed
182
+ */
183
+ public function getCountry()
184
+ {
185
+ return $this->country;
186
+ }
187
+
188
+ /**
189
+ * @param mixed $country
190
+ * @return MailChimp_Address
191
+ */
192
+ public function setCountry($country)
193
+ {
194
+ $this->country = $country;
195
+
196
+ return $this;
197
+ }
198
+
199
+ /**
200
+ * @return mixed
201
+ */
202
+ public function getCountryCode()
203
+ {
204
+ return $this->country_code;
205
+ }
206
+
207
+ /**
208
+ * @param mixed $country_code
209
+ * @return MailChimp_Address
210
+ */
211
+ public function setCountryCode($country_code)
212
+ {
213
+ $this->country_code = $country_code;
214
+
215
+ return $this;
216
+ }
217
+
218
+ /**
219
+ * @return mixed
220
+ */
221
+ public function getLongitude()
222
+ {
223
+ return $this->longitude;
224
+ }
225
+
226
+ /**
227
+ * @param mixed $longitude
228
+ * @return MailChimp_Address
229
+ */
230
+ public function setLongitude($longitude)
231
+ {
232
+ $this->longitude = $longitude;
233
+
234
+ return $this;
235
+ }
236
+
237
+ /**
238
+ * @return mixed
239
+ */
240
+ public function getLatitude()
241
+ {
242
+ return $this->latitude;
243
+ }
244
+
245
+ /**
246
+ * @param mixed $latitude
247
+ * @return MailChimp_Address
248
+ */
249
+ public function setLatitude($latitude)
250
+ {
251
+ $this->latitude = $latitude;
252
+
253
+ return $this;
254
+ }
255
+
256
+ /**
257
+ * @return mixed
258
+ */
259
+ public function getPhone()
260
+ {
261
+ return $this->phone;
262
+ }
263
+
264
+ /**
265
+ * @param mixed $phone
266
+ * @return MailChimp_Address
267
+ */
268
+ public function setPhone($phone)
269
+ {
270
+ $this->phone = $phone;
271
+
272
+ return $this;
273
+ }
274
+
275
+ /**
276
+ * @return mixed
277
+ */
278
+ public function getCompany()
279
+ {
280
+ return $this->company;
281
+ }
282
+
283
+ /**
284
+ * @param mixed $company
285
+ * @return MailChimp_Address
286
+ */
287
+ public function setCompany($company)
288
+ {
289
+ $this->company = $company;
290
+
291
+ return $this;
292
+ }
293
+
294
+ /**
295
+ * @return array
296
+ */
297
+ public function toArray()
298
+ {
299
+ return mailchimp_array_remove_empty(array(
300
+ 'name' => (string) $this->name,
301
+ 'address1' => (string) $this->address1,
302
+ 'address2' => (string) $this->address2,
303
+ 'city' => (string) $this->city,
304
+ 'province' => (string) $this->province,
305
+ 'province_code' => (string) $this->province_code,
306
+ 'postal_code' => (string) $this->postal_code,
307
+ 'country' => (string) $this->country,
308
+ 'country_code' => (string) $this->country_code,
309
+ 'longitude' => $this->longitude ? (int) $this->longitude : null,
310
+ 'latitude' => $this->latitude ? (int) $this->latitude : null,
311
+ 'phone' => (string) $this->phone,
312
+ 'company' => (string) $this->company,
313
+ ));
314
+ }
315
+
316
+ /**
317
+ * @param array $data
318
+ * @return MailChimp_Address
319
+ */
320
+ public function fromArray(array $data)
321
+ {
322
+ $singles = array(
323
+ 'name', 'address1', 'address2', 'city',
324
+ 'province', 'province_code', 'postal_code',
325
+ 'country', 'country_code', 'longitude',
326
+ 'phone', 'company',
327
+ );
328
+
329
+ foreach ($singles as $key) {
330
+ if (array_key_exists($key, $data)) {
331
+ $this->$key = $data[$key];
332
+ }
333
+ }
334
+
335
+ return $this;
336
+ }
337
+ }
includes/api/assets/class-mailchimp-cart.php ADDED
@@ -0,0 +1,275 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Created by Vextras.
5
+ *
6
+ * Name: Ryan Hungate
7
+ * Email: ryan@mailchimp.com
8
+ * Date: 7/15/16
9
+ * Time: 1:26 PM
10
+ */
11
+ class MailChimp_Cart
12
+ {
13
+ protected $store_id;
14
+ protected $id;
15
+ protected $customer;
16
+ protected $campaign_id;
17
+ protected $checkout_url;
18
+ protected $currency_code;
19
+ protected $order_total;
20
+ protected $tax_total;
21
+ protected $lines = array();
22
+
23
+ /**
24
+ * @param $unique_id
25
+ * @return $this
26
+ */
27
+ public function setId($unique_id)
28
+ {
29
+ $this->id = $unique_id;
30
+
31
+ return $this;
32
+ }
33
+
34
+ /**
35
+ * @return mixed
36
+ */
37
+ public function getId()
38
+ {
39
+ return $this->id;
40
+ }
41
+
42
+ /**
43
+ * @param $store_id
44
+ * @return $this
45
+ */
46
+ public function setStoreID($store_id)
47
+ {
48
+ $this->store_id = $store_id;
49
+
50
+ return $this;
51
+ }
52
+
53
+ /**
54
+ * @return mixed
55
+ */
56
+ public function getStoreID()
57
+ {
58
+ if (empty($this->store_id)) {
59
+ $this->store_id = mailchimp_get_store_id();
60
+ }
61
+
62
+ return $this->store_id;
63
+ }
64
+
65
+ /**
66
+ * @param MailChimp_Customer $customer
67
+ * @return $this
68
+ */
69
+ public function setCustomer(MailChimp_Customer $customer)
70
+ {
71
+ $this->customer = $customer;
72
+
73
+ return $this;
74
+ }
75
+
76
+ /**
77
+ * @return MailChimp_Customer
78
+ */
79
+ public function getCustomer()
80
+ {
81
+ if (empty($this->customer)) {
82
+ $this->customer = new MailChimp_Customer();
83
+ }
84
+
85
+ return $this->customer;
86
+ }
87
+
88
+ /**
89
+ * @param $id
90
+ * @return $this
91
+ */
92
+ public function setCampaignID($id)
93
+ {
94
+ $this->campaign_id = $id;
95
+
96
+ return $this;
97
+ }
98
+
99
+ /**
100
+ * @return mixed
101
+ */
102
+ public function getCampaignID()
103
+ {
104
+ return $this->campaign_id;
105
+ }
106
+
107
+ /**
108
+ * @param $url
109
+ * @return $this
110
+ */
111
+ public function setCheckoutUrl($url)
112
+ {
113
+ $this->checkout_url = $url;
114
+
115
+ return $this;
116
+ }
117
+
118
+ /**
119
+ * @return string
120
+ */
121
+ public function getCheckoutURL()
122
+ {
123
+ if (empty($this->checkout_url)) {
124
+ $this->checkout_url = wc_get_checkout_url();
125
+ }
126
+
127
+ return $this->checkout_url;
128
+ }
129
+
130
+ /**
131
+ * @param $code
132
+ * @return $this
133
+ */
134
+ public function setCurrencyCode($code)
135
+ {
136
+ $this->currency_code = $code;
137
+
138
+ return $this;
139
+ }
140
+
141
+ /**
142
+ * @return string
143
+ */
144
+ public function getCurrencyCode()
145
+ {
146
+ if (empty($this->currency_code)) {
147
+ $options = get_option('mailchimp-woocommerce', array());
148
+ $this->currency_code = isset($options['store_currency_code']) ? $options['store_currency_code'] : 'USD';
149
+ }
150
+
151
+ return $this->currency_code;
152
+ }
153
+
154
+ /**
155
+ * @param $total
156
+ * @return $this
157
+ */
158
+ public function setOrderTotal($total)
159
+ {
160
+ $this->order_total = number_format($total, 2);
161
+
162
+ return $this;
163
+ }
164
+
165
+ /**
166
+ * @return float
167
+ */
168
+ public function getOrderTotal()
169
+ {
170
+ return $this->order_total;
171
+ }
172
+
173
+ /**
174
+ * @param $total
175
+ * @return $this
176
+ */
177
+ public function setTaxTotal($total)
178
+ {
179
+ $this->tax_total = number_format($total, 2);
180
+
181
+ return $this;
182
+ }
183
+
184
+ /**
185
+ * @return float
186
+ */
187
+ public function getTaxTotal()
188
+ {
189
+ return $this->tax_total;
190
+ }
191
+
192
+ /**
193
+ * @param MailChimp_LineItem $item
194
+ * @return $this
195
+ */
196
+ public function addItem(MailChimp_LineItem $item)
197
+ {
198
+ $this->lines[] = $item;
199
+ return $this;
200
+ }
201
+
202
+ /**
203
+ * @return array
204
+ */
205
+ public function items()
206
+ {
207
+ return $this->lines;
208
+ }
209
+
210
+ /**
211
+ * @return mixed
212
+ */
213
+ public function toArray()
214
+ {
215
+ return mailchimp_array_remove_empty(array(
216
+ 'id' => (string) $this->getId(),
217
+ 'customer' => $this->getCustomer()->toArray(),
218
+ 'campaign_id' => (string) $this->getCampaignID(),
219
+ 'checkout_url' => (string) $this->getCheckoutURL(),
220
+ 'currency_code' => (string) $this->getCurrencyCode(),
221
+ 'order_total' => $this->getOrderTotal(),
222
+ 'tax_total' => $this->getTaxTotal() > 0 ? $this->getTaxTotal() : null,
223
+ 'lines' => array_map(function($item) {
224
+ return $item->toArray();
225
+ }, $this->items()),
226
+ ));
227
+ }
228
+
229
+ /**
230
+ * @return array
231
+ */
232
+ public function toArrayForUpdate()
233
+ {
234
+ return mailchimp_array_remove_empty(array(
235
+ 'campaign_id' => (string) $this->getCampaignID(),
236
+ 'checkout_url' => (string) $this->getCheckoutURL(),
237
+ 'currency_code' => (string) $this->getCurrencyCode(),
238
+ 'order_total' => $this->getOrderTotal(),
239
+ 'tax_total' => $this->getTaxTotal() > 0 ? $this->getTaxTotal() : null,
240
+ 'lines' => array_map(function($item) {
241
+ return $item->toArray();
242
+ }, $this->items()),
243
+ ));
244
+ }
245
+
246
+ /**
247
+ * @param array $data
248
+ * @return MailChimp_Cart
249
+ */
250
+ public function fromArray(array $data)
251
+ {
252
+ $singles = [
253
+ 'store_id', 'id', 'campaign_id', 'checkout_url',
254
+ 'currency_code', 'order_total', 'tax_total',
255
+ ];
256
+
257
+ foreach ($singles as $key) {
258
+ if (array_key_exists($key, $data)) {
259
+ $this->$key = $data[$key];
260
+ }
261
+ }
262
+
263
+ if (array_key_exists('customer', $data) && is_array($data['customer'])) {
264
+ $this->customer = (new MailChimp_Customer())->fromArray($data['customer']);
265
+ }
266
+
267
+ if (array_key_exists('lines', $data) && is_array($data['lines'])) {
268
+ foreach ($data['lines'] as $line_item) {
269
+ $this->lines[] = (new MailChimp_LineItem)->fromArray($line_item);
270
+ }
271
+ }
272
+
273
+ return $this;
274
+ }
275
+ }
includes/api/assets/class-mailchimp-customer.php ADDED
@@ -0,0 +1,257 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Created by Vextras.
5
+ *
6
+ * Name: Ryan Hungate
7
+ * Email: ryan@mailchimp.com
8
+ * Date: 3/8/16
9
+ * Time: 2:16 PM
10
+ */
11
+ class MailChimp_Customer
12
+ {
13
+ protected $id = null;
14
+ protected $email_address = null;
15
+ protected $opt_in_status = null;
16
+ protected $company = null;
17
+ protected $first_name = null;
18
+ protected $last_name = null;
19
+ protected $orders_count = null;
20
+ protected $total_spent = null;
21
+ protected $address;
22
+
23
+ /**
24
+ * @return array
25
+ */
26
+ public function getValidation()
27
+ {
28
+ return array(
29
+ 'id' => 'required',
30
+ 'email_address' => 'required|email',
31
+ 'opt_in_status' => 'required|boolean',
32
+ 'company' => 'string',
33
+ 'first_name' => 'string',
34
+ 'last_name' => 'string',
35
+ 'orders_count' => 'integer',
36
+ 'total_spent' => 'integer',
37
+ );
38
+ }
39
+
40
+ /**
41
+ * @return null
42
+ */
43
+ public function getId()
44
+ {
45
+ return $this->id;
46
+ }
47
+
48
+ /**
49
+ * @param null $id
50
+ * @return MailChimp_Customer
51
+ */
52
+ public function setId($id)
53
+ {
54
+ $this->id = $id;
55
+
56
+ return $this;
57
+ }
58
+
59
+ /**
60
+ * @return null
61
+ */
62
+ public function getEmailAddress()
63
+ {
64
+ return $this->email_address;
65
+ }
66
+
67
+ /**
68
+ * @param null $email_address
69
+ * @return MailChimp_Customer
70
+ */
71
+ public function setEmailAddress($email_address)
72
+ {
73
+ $this->email_address = $email_address;
74
+
75
+ return $this;
76
+ }
77
+
78
+ /**
79
+ * @return null
80
+ */
81
+ public function getOptInStatus()
82
+ {
83
+ return $this->opt_in_status;
84
+ }
85
+
86
+ /**
87
+ * @param null $opt_in_status
88
+ * @return MailChimp_Customer
89
+ */
90
+ public function setOptInStatus($opt_in_status)
91
+ {
92
+ $this->opt_in_status = $opt_in_status;
93
+
94
+ return $this;
95
+ }
96
+
97
+ /**
98
+ * @return null
99
+ */
100
+ public function getCompany()
101
+ {
102
+ return $this->company;
103
+ }
104
+
105
+ /**
106
+ * @param null $company
107
+ * @return MailChimp_Customer
108
+ */
109
+ public function setCompany($company)
110
+ {
111
+ $this->company = $company;
112
+
113
+ return $this;
114
+ }
115
+
116
+ /**
117
+ * @return null
118
+ */
119
+ public function getFirstName()
120
+ {
121
+ return $this->first_name;
122
+ }
123
+
124
+ /**
125
+ * @param null $first_name
126
+ * @return MailChimp_Customer
127
+ */
128
+ public function setFirstName($first_name)
129
+ {
130
+ $this->first_name = $first_name;
131
+
132
+ return $this;
133
+ }
134
+
135
+ /**
136
+ * @return null
137
+ */
138
+ public function getLastName()
139
+ {
140
+ return $this->last_name;
141
+ }
142
+
143
+ /**
144
+ * @param null $last_name
145
+ * @return MailChimp_Customer
146
+ */
147
+ public function setLastName($last_name)
148
+ {
149
+ $this->last_name = $last_name;
150
+
151
+ return $this;
152
+ }
153
+
154
+ /**
155
+ * @return null
156
+ */
157
+ public function getOrdersCount()
158
+ {
159
+ return $this->orders_count;
160
+ }
161
+
162
+ /**
163
+ * @param null $orders_count
164
+ * @return MailChimp_Customer
165
+ */
166
+ public function setOrdersCount($orders_count)
167
+ {
168
+ $this->orders_count = $orders_count;
169
+
170
+ return $this;
171
+ }
172
+
173
+ /**
174
+ * @return null
175
+ */
176
+ public function getTotalSpent()
177
+ {
178
+ return $this->total_spent;
179
+ }
180
+
181
+ /**
182
+ * @param null $total_spent
183
+ * @return MailChimp_Customer
184
+ */
185
+ public function setTotalSpent($total_spent)
186
+ {
187
+ $this->total_spent = $total_spent;
188
+
189
+ return $this;
190
+ }
191
+
192
+ /**
193
+ * @return MailChimp_Address
194
+ */
195
+ public function getAddress()
196
+ {
197
+ if (empty($this->address)) {
198
+ $this->address = new MailChimp_Address();
199
+ }
200
+ return $this->address;
201
+ }
202
+
203
+ /**
204
+ * @param MailChimp_Address $address
205
+ * @return MailChimp_Customer
206
+ */
207
+ public function setAddress(MailChimp_Address $address)
208
+ {
209
+ $this->address = $address;
210
+
211
+ return $this;
212
+ }
213
+
214
+ /**
215
+ * @return array
216
+ */
217
+ public function toArray()
218
+ {
219
+ $address = $this->getAddress()->toArray();
220
+
221
+ return mailchimp_array_remove_empty(array(
222
+ 'id' => (string) $this->getId(),
223
+ 'email_address' => (string) $this->getEmailAddress(),
224
+ 'opt_in_status' => $this->getOptInStatus(),
225
+ 'company' => (string) $this->getCompany(),
226
+ 'first_name' => (string) $this->getFirstName(),
227
+ 'last_name' => (string) $this->getLastName(),
228
+ 'orders_count' => (int) $this->getOrdersCount(),
229
+ 'total_spent' => number_format($this->getTotalSpent(), 2),
230
+ 'address' => empty($address) ? null : $address,
231
+ ));
232
+ }
233
+
234
+ /**
235
+ * @param array $data
236
+ * @return MailChimp_Customer
237
+ */
238
+ public function fromArray(array $data)
239
+ {
240
+ $singles = [
241
+ 'id', 'email_address', 'opt_in_status', 'company',
242
+ 'first_name', 'last_name', 'orders_count', 'total_spent',
243
+ ];
244
+
245
+ foreach ($singles as $key) {
246
+ if (array_key_exists($key, $data)) {
247
+ $this->$key = $data[$key];
248
+ }
249
+ }
250
+
251
+ if (array_key_exists('address', $data) && is_array($data['address'])) {
252
+ $this->address = (new MailChimp_Address())->fromArray($data['address']);
253
+ }
254
+
255
+ return $this;
256
+ }
257
+ }
includes/api/assets/class-mailchimp-line-item.php ADDED
@@ -0,0 +1,180 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Created by Vextras.
5
+ *
6
+ * Name: Ryan Hungate
7
+ * Email: ryan@mailchimp.com
8
+ * Date: 3/8/16
9
+ * Time: 2:16 PM
10
+ */
11
+ class MailChimp_LineItem
12
+ {
13
+ protected $id;
14
+ protected $product_id;
15
+ protected $product_variant_id;
16
+ protected $quantity;
17
+ protected $price;
18
+
19
+ /**
20
+ * @return array
21
+ */
22
+ public function getValidation()
23
+ {
24
+ return array(
25
+ 'id' => 'required|string',
26
+ 'product_id' => 'required|string',
27
+ 'product_variant_id' => 'required|string',
28
+ 'quantity' => 'required|integer',
29
+ 'price' => 'required|numeric',
30
+ );
31
+ }
32
+
33
+ /**
34
+ * @param $id
35
+ * @param $product_id
36
+ * @param $variant_id
37
+ * @param $quantity
38
+ * @param $price
39
+ * @return MailChimp_LineItem
40
+ */
41
+ public static function make($id, $product_id, $variant_id, $quantity, $price)
42
+ {
43
+ $item = new MailChimp_LineItem();
44
+ $item->id = $id;
45
+ $item->product_id = $product_id;
46
+ $item->product_variant_id = $variant_id;
47
+ $item->quantity = $quantity;
48
+ $item->price = $price;
49
+
50
+ return $item;
51
+ }
52
+
53
+ /**
54
+ * @return mixed
55
+ */
56
+ public function getId()
57
+ {
58
+ return $this->id;
59
+ }
60
+
61
+ /**
62
+ * @param mixed $id
63
+ * @return MailChimp_LineItem
64
+ */
65
+ public function setId($id)
66
+ {
67
+ $this->id = $id;
68
+
69
+ return $this;
70
+ }
71
+
72
+ /**
73
+ * @return mixed
74
+ */
75
+ public function getProductId()
76
+ {
77
+ return $this->product_id;
78
+ }
79
+
80
+ /**
81
+ * @param mixed $product_id
82
+ * @return MailChimp_LineItem
83
+ */
84
+ public function setProductId($product_id)
85
+ {
86
+ $this->product_id = $product_id;
87
+
88
+ return $this;
89
+ }
90
+
91
+ /**
92
+ * @return mixed
93
+ */
94
+ public function getProductVariantId()
95
+ {
96
+ return $this->product_variant_id;
97
+ }
98
+
99
+ /**
100
+ * @param mixed $product_variant_id
101
+ * @return MailChimp_LineItem
102
+ */
103
+ public function setProductVariantId($product_variant_id)
104
+ {
105
+ $this->product_variant_id = $product_variant_id;
106
+
107
+ return $this;
108
+ }
109
+
110
+ /**
111
+ * @return mixed
112
+ */
113
+ public function getQuantity()
114
+ {
115
+ return $this->quantity;
116
+ }
117
+
118
+ /**
119
+ * @param mixed $quantity
120
+ * @return MailChimp_LineItem
121
+ */
122
+ public function setQuantity($quantity)
123
+ {
124
+ $this->quantity = $quantity;
125
+
126
+ return $this;
127
+ }
128
+
129
+ /**
130
+ * @return mixed
131
+ */
132
+ public function getPrice()
133
+ {
134
+ return $this->price;
135
+ }
136
+
137
+ /**
138
+ * @param mixed $price
139
+ * @return MailChimp_LineItem
140
+ */
141
+ public function setPrice($price)
142
+ {
143
+ $this->price = $price;
144
+
145
+ return $this;
146
+ }
147
+
148
+ /**
149
+ * @return array
150
+ */
151
+ public function toArray()
152
+ {
153
+ return mailchimp_array_remove_empty(array(
154
+ 'id' => (string) $this->id,
155
+ 'product_id' => (string) $this->product_id,
156
+ 'product_variant_id' => (string) $this->product_variant_id,
157
+ 'quantity' => (int) $this->quantity,
158
+ 'price' => (string) $this->price,
159
+ ));
160
+ }
161
+
162
+ /**
163
+ * @param array $data
164
+ * @return MailChimp_LineItem
165
+ */
166
+ public function fromArray(array $data)
167
+ {
168
+ $singles = array(
169
+ 'id', 'product_id', 'product_variant_id', 'quantity', 'price',
170
+ );
171
+
172
+ foreach ($singles as $key) {
173
+ if (array_key_exists($key, $data)) {
174
+ $this->$key = $data[$key];
175
+ }
176
+ }
177
+
178
+ return $this;
179
+ }
180
+ }
includes/api/assets/class-mailchimp-order.php ADDED
@@ -0,0 +1,405 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Created by Vextras.
5
+ *
6
+ * Name: Ryan Hungate
7
+ * Email: ryan@mailchimp.com
8
+ * Date: 3/8/16
9
+ * Time: 2:16 PM
10
+ */
11
+ class MailChimp_Order
12
+ {
13
+ protected $id = null;
14
+ protected $customer = null;
15
+ protected $campaign_id = null;
16
+ protected $financial_status = null;
17
+ protected $fulfillment_status = null;
18
+ protected $currency_code = null;
19
+ protected $order_total = null;
20
+ protected $tax_total = null;
21
+ protected $shipping_total = null;
22
+ protected $updated_at_foreign = null;
23
+ protected $processed_at_foreign = null;
24
+ protected $cancelled_at_foreign = null;
25
+ protected $shipping_address = null;
26
+ protected $billing_address = null;
27
+ protected $lines = [];
28
+
29
+ /**
30
+ * @return array
31
+ */
32
+ public function getValidation()
33
+ {
34
+ return array(
35
+ 'id' => 'required|string',
36
+ 'customer' => 'required',
37
+ 'campaign_id' => 'string',
38
+ 'financial_status' => 'string',
39
+ 'fulfillment_status' => 'string',
40
+ 'currency_code' => 'required|currency_code',
41
+ 'order_total' => 'required|numeric',
42
+ 'tax_total' => 'numeric',
43
+ 'processed_at_foreign' => 'date',
44
+ 'updated_at_foreign' => 'date',
45
+ 'cancelled_at_foreign' => 'date',
46
+ 'lines' => 'required|array',
47
+ );
48
+ }
49
+
50
+ /**
51
+ * @param $id
52
+ * @return MailChimp_Order
53
+ */
54
+ public function setId($id)
55
+ {
56
+ $this->id = $id;
57
+
58
+ return $this;
59
+ }
60
+
61
+ /**
62
+ * @return null|string
63
+ */
64
+ public function getId()
65
+ {
66
+ return $this->id;
67
+ }
68
+
69
+ /**
70
+ * @param MailChimp_Customer $customer
71
+ * @return MailChimp_Order
72
+ */
73
+ public function setCustomer(MailChimp_Customer $customer)
74
+ {
75
+ $this->customer = $customer;
76
+
77
+ return $this;
78
+ }
79
+
80
+ /**
81
+ * @return null|MailChimp_Customer
82
+ */
83
+ public function getCustomer()
84
+ {
85
+ if (empty($this->customer)) {
86
+ $this->customer = new MailChimp_Customer();
87
+ }
88
+ return $this->customer;
89
+ }
90
+
91
+ /**
92
+ * @param MailChimp_LineItem $item
93
+ * @return $this
94
+ */
95
+ public function addItem(MailChimp_LineItem $item)
96
+ {
97
+ $this->lines[] = $item;
98
+ return $this;
99
+ }
100
+
101
+ /**
102
+ * @return array
103
+ */
104
+ public function items()
105
+ {
106
+ return $this->lines;
107
+ }
108
+
109
+ /**
110
+ * @return null
111
+ */
112
+ public function getCampaignId()
113
+ {
114
+ return $this->campaign_id;
115
+ }
116
+
117
+ /**
118
+ * @param null $campaign_id
119
+ * @return MailChimp_Order
120
+ */
121
+ public function setCampaignId($campaign_id)
122
+ {
123
+ $this->campaign_id = $campaign_id;
124
+
125
+ return $this;
126
+ }
127
+
128
+ /**
129
+ * @return null
130
+ */
131
+ public function getFinancialStatus()
132
+ {
133
+ return $this->financial_status;
134
+ }
135
+
136
+ /**
137
+ * @param null $financial_status
138
+ * @return Order
139
+ */
140
+ public function setFinancialStatus($financial_status)
141
+ {
142
+ $this->financial_status = $financial_status;
143
+
144
+ return $this;
145
+ }
146
+
147
+ /**
148
+ * @return null
149
+ */
150
+ public function getFulfillmentStatus()
151
+ {
152
+ return $this->fulfillment_status;
153
+ }
154
+
155
+ /**
156
+ * @param null $fulfillment_status
157
+ * @return MailChimp_Order
158
+ */
159
+ public function setFulfillmentStatus($fulfillment_status)
160
+ {
161
+ $this->fulfillment_status = $fulfillment_status;
162
+
163
+ return $this;
164
+ }
165
+
166
+ /**
167
+ * @return null
168
+ */
169
+ public function getCurrencyCode()
170
+ {
171
+ return $this->currency_code;
172
+ }
173
+
174
+ /**
175
+ * @param null $currency_code
176
+ * @return MailChimp_Order
177
+ */
178
+ public function setCurrencyCode($currency_code)
179
+ {
180
+ $this->currency_code = $currency_code;
181
+
182
+ return $this;
183
+ }
184
+
185
+ /**
186
+ * @return mixed
187
+ */
188
+ public function getOrderTotal()
189
+ {
190
+ return $this->order_total;
191
+ }
192
+
193
+ /**
194
+ * @param mixed $order_total
195
+ * @return MailChimp_Order
196
+ */
197
+ public function setOrderTotal($order_total)
198
+ {
199
+ $this->order_total = $order_total;
200
+
201
+ return $this;
202
+ }
203
+
204
+ /**
205
+ * @return mixed
206
+ */
207
+ public function getTaxTotal()
208
+ {
209
+ return $this->tax_total;
210
+ }
211
+
212
+ /**
213
+ * @param mixed $tax_total
214
+ * @return MailChimp_Order
215
+ */
216
+ public function setTaxTotal($tax_total)
217
+ {
218
+ $this->tax_total = $tax_total;
219
+
220
+ return $this;
221
+ }
222
+
223
+ /**
224
+ * @return mixed
225
+ */
226
+ public function getShippingTotal()
227
+ {
228
+ return $this->shipping_total;
229
+ }
230
+
231
+ /**
232
+ * @param mixed $shipping_total
233
+ * @return MailChimp_Order
234
+ */
235
+ public function setShippingTotal($shipping_total)
236
+ {
237
+ $this->shipping_total = $shipping_total;
238
+
239
+ return $this;
240
+ }
241
+
242
+ /**
243
+ * @param \DateTime $time
244
+ * @return $this
245
+ */
246
+ public function setProcessedAt(\DateTime $time)
247
+ {
248
+ $this->processed_at_foreign = $time->format('Y-m-d H:i:s');
249
+
250
+ return $this;
251
+ }
252
+
253
+ /**
254
+ * @return null
255
+ */
256
+ public function getProcessedAt()
257
+ {
258
+ return $this->processed_at_foreign;
259
+ }
260
+
261
+ /**
262
+ * @param \DateTime $time
263
+ * @return $this
264
+ */
265
+ public function setCancelledAt(\DateTime $time)
266
+ {
267
+ $this->cancelled_at_foreign = $time->format('Y-m-d H:i:s');
268
+
269
+ return $this;
270
+ }
271
+
272
+ /**
273
+ * @return null
274
+ */
275
+ public function getCancelledAt()
276
+ {
277
+ return $this->cancelled_at_foreign;
278
+ }
279
+
280
+ /**
281
+ * @param \DateTime $time
282
+ * @return $this
283
+ */
284
+ public function setUpdatedAt(\DateTime $time)
285
+ {
286
+ $this->updated_at_foreign = $time->format('Y-m-d H:i:s');
287
+
288
+ return $this;
289
+ }
290
+
291
+ /**
292
+ * @return null
293
+ */
294
+ public function getUpdatedAt()
295
+ {
296
+ return $this->updated_at_foreign;
297
+ }
298
+
299
+ /**
300
+ * @param MailChimp_Address $address
301
+ * @return $this
302
+ */
303
+ public function setShippingAddress(MailChimp_Address $address)
304
+ {
305
+ $this->shipping_address = $address;
306
+
307
+ return $this;
308
+ }
309
+
310
+ /**
311
+ * @return MailChimp_Address
312
+ */
313
+ public function getShippingAddress()
314
+ {
315
+ if (empty($this->shipping_address)) {
316
+ $this->shipping_address = new MailChimp_Address('shipping');
317
+ }
318
+ return $this->shipping_address;
319
+ }
320
+
321
+ /**
322
+ * @param MailChimp_Address $address
323
+ * @return $this
324
+ */
325
+ public function setBillingAddress(MailChimp_Address $address)
326
+ {
327
+ $this->billing_address = $address;
328
+
329
+ return $this;
330
+ }
331
+
332
+ /**
333
+ * @return MailChimp_Address
334
+ */
335
+ public function getBillingAddress()
336
+ {
337
+ if (empty($this->billing_address)) {
338
+ $this->billing_address = new MailChimp_Address('billing');
339
+ }
340
+ return $this->billing_address;
341
+ }
342
+
343
+ /**
344
+ * @return array
345
+ */
346
+ public function toArray()
347
+ {
348
+ return mailchimp_array_remove_empty(array_filter(array(
349
+ 'id' => (string) $this->getId(),
350
+ 'customer' => $this->getCustomer()->toArray(),
351
+ 'campaign_id' => (string) $this->getCampaignId(),
352
+ 'financial_status' => (string) $this->getFinancialStatus(),
353
+ 'fulfillment_status' => (string) $this->getFulfillmentStatus(),
354
+ 'currency_code' => (string) $this->getCurrencyCode(),
355
+ 'order_total' => floatval($this->getOrderTotal()),
356
+ 'tax_total' => floatval($this->getTaxTotal()),
357
+ 'shipping_total' => floatval($this->getShippingTotal()),
358
+ 'processed_at_foreign' => (string) $this->getProcessedAt(),
359
+ 'cancelled_at_foreign' => (string) $this->getCancelledAt(),
360
+ 'updated_at_foreign' => (string) $this->getUpdatedAt(),
361
+ 'shipping_address' => $this->getShippingAddress()->toArray(),
362
+ 'billing_address' => $this->getBillingAddress()->toArray(),
363
+ 'lines' => array_map(function ($item) {
364
+ /** @var MailChimp_LineItem $item */
365
+ return $item->toArray();
366
+ }, $this->items()),
367
+ )));
368
+ }
369
+
370
+ /**
371
+ * @param array $data
372
+ * @return MailChimp_Order
373
+ */
374
+ public function fromArray(array $data)
375
+ {
376
+ $singles = array(
377
+ 'id', 'campaign_id', 'financial_status', 'fulfillment_status',
378
+ 'currency_code', 'order_total', 'tax_total', 'processed_at_foreign',
379
+ 'cancelled_at_foreign', 'updated_at_foreign'
380
+ );
381
+
382
+ foreach ($singles as $key) {
383
+ if (array_key_exists($key, $data)) {
384
+ $this->$key = $data[$key];
385
+ }
386
+ }
387
+
388
+ if (array_key_exists('shipping_address', $data) && is_array($data['shipping_address'])) {
389
+ $this->shipping_address = (new MailChimp_Address())->fromArray($data['shipping_address']);
390
+ }
391
+
392
+ if (array_key_exists('billing_address', $data) && is_array($data['billing_address'])) {
393
+ $this->billing_address = (new MailChimp_Address())->fromArray($data['billing_address']);
394
+ }
395
+
396
+ if (array_key_exists('lines', $data) && is_array($data['lines'])) {
397
+ $this->lines = [];
398
+ foreach ($data['lines'] as $line_item) {
399
+ $this->lines[] = (new MailChimp_LineItem())->fromArray($line_item);
400
+ }
401
+ }
402
+
403
+ return $this;
404
+ }
405
+ }
includes/api/assets/class-mailchimp-product-variation.php ADDED
@@ -0,0 +1,250 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Created by Vextras.
5
+ *
6
+ * Name: Ryan Hungate
7
+ * Email: ryan@mailchimp.com
8
+ * Date: 3/8/16
9
+ * Time: 2:17 PM
10
+ */
11
+ class MailChimp_ProductVariation
12
+ {
13
+ protected $id = null;
14
+ protected $title = null;
15
+ protected $url = null;
16
+ protected $sku = null;
17
+ protected $price = null;
18
+ protected $inventory_quantity = null;
19
+ protected $image_url = null;
20
+ protected $backorders = null;
21
+ protected $visibility = null;
22
+
23
+ /**
24
+ * @return array
25
+ */
26
+ public function getValidation()
27
+ {
28
+ return array(
29
+ 'id' => 'required|string',
30
+ 'title' => 'required|string',
31
+ 'url' => 'url',
32
+ 'sku' => 'string',
33
+ 'price' => 'numeric',
34
+ 'inventory_quantity' => 'integer',
35
+ 'image_url' => 'url',
36
+ 'backorders' => 'string',
37
+ 'visibility' => 'string',
38
+ );
39
+ }
40
+
41
+ /**
42
+ * @return null
43
+ */
44
+ public function getId()
45
+ {
46
+ return $this->id;
47
+ }
48
+
49
+ /**
50
+ * @param null $id
51
+ * @return MailChimp_ProductVariation
52
+ */
53
+ public function setId($id)
54
+ {
55
+ $this->id = $id;
56
+
57
+ return $this;
58
+ }
59
+
60
+ /**
61
+ * @return null
62
+ */
63
+ public function getTitle()
64
+ {
65
+ return $this->title;
66
+ }
67
+
68
+ /**
69
+ * @param null $title
70
+ * @return MailChimp_ProductVariation
71
+ */
72
+ public function setTitle($title)
73
+ {
74
+ $this->title = $title;
75
+
76
+ return $this;
77
+ }
78
+
79
+ /**
80
+ * @return null
81
+ */
82
+ public function getUrl()
83
+ {
84
+ return $this->url;
85
+ }
86
+
87
+ /**
88
+ * @param null $url
89
+ * @return MailChimp_ProductVariation
90
+ */
91
+ public function setUrl($url)
92
+ {
93
+ $this->url = $url;
94
+
95
+ return $this;
96
+ }
97
+
98
+ /**
99
+ * @return null
100
+ */
101
+ public function getSku()
102
+ {
103
+ return $this->sku;
104
+ }
105
+
106
+ /**
107
+ * @param null $sku
108
+ * @return MailChimp_ProductVariation
109
+ */
110
+ public function setSku($sku)
111
+ {
112
+ $this->sku = $sku;
113
+
114
+ return $this;
115
+ }
116
+
117
+ /**
118
+ * @return null
119
+ */
120
+ public function getPrice()
121
+ {
122
+ return $this->price;
123
+ }
124
+
125
+ /**
126
+ * @param null $price
127
+ * @return MailChimp_ProductVariation
128
+ */
129
+ public function setPrice($price)
130
+ {
131
+ $this->price = $price;
132
+
133
+ return $this;
134
+ }
135
+
136
+ /**
137
+ * @return null
138
+ */
139
+ public function getInventoryQuantity()
140
+ {
141
+ return $this->inventory_quantity;
142
+ }
143
+
144
+ /**
145
+ * @param null $inventory_quantity
146
+ * @return MailChimp_ProductVariation
147
+ */
148
+ public function setInventoryQuantity($inventory_quantity)
149
+ {
150
+ $this->inventory_quantity = $inventory_quantity;
151
+
152
+ return $this;
153
+ }
154
+
155
+ /**
156
+ * @return null
157
+ */
158
+ public function getImageUrl()
159
+ {
160
+ return !empty($this->image_url) ? $this->image_url : null;
161
+ }
162
+
163
+ /**
164
+ * @param null $image_url
165
+ * @return MailChimp_ProductVariation
166
+ */
167
+ public function setImageUrl($image_url)
168
+ {
169
+ $this->image_url = $image_url;
170
+
171
+ return $this;
172
+ }
173
+
174
+ /**
175
+ * @return null
176
+ */
177
+ public function getBackorders()
178
+ {
179
+ return $this->backorders;
180
+ }
181
+
182
+ /**
183
+ * @param null $backorders
184
+ * @return MailChimp_ProductVariation
185
+ */
186
+ public function setBackorders($backorders)
187
+ {
188
+ $this->backorders = $backorders;
189
+
190
+ return $this;
191
+ }
192
+
193
+ /**
194
+ * @return null
195
+ */
196
+ public function getVisibility()
197
+ {
198
+ return $this->visibility;
199
+ }
200
+
201
+ /**
202
+ * @param null $visibility
203
+ * @return MailChimp_ProductVariation
204
+ */
205
+ public function setVisibility($visibility)
206
+ {
207
+ $this->visibility = $visibility;
208
+
209
+ return $this;
210
+ }
211
+
212
+ /**
213
+ * @return array
214
+ */
215
+ public function toArray()
216
+ {
217
+ return mailchimp_array_remove_empty(array(
218
+ 'id' => (string) $this->getId(),
219
+ 'title' => $this->getTitle(),
220
+ 'url' => (string) $this->getUrl(),
221
+ 'sku' => (string) $this->getSku(),
222
+ 'price' => $this->getPrice(),
223
+ 'inventory_quantity' => (int) $this->getInventoryQuantity(),
224
+ 'image_url' => (string) $this->getImageUrl(),
225
+ 'backorders' => (string) $this->getBackorders(),
226
+ 'visibility' => (string) $this->getVisibility(),
227
+ ));
228
+ }
229
+
230
+ /**
231
+ * @param array $data
232
+ * @return MailChimp_ProductVariation
233
+ */
234
+ public function fromArray(array $data)
235
+ {
236
+ $singles = array(
237
+ 'id', 'title', 'url', 'sku',
238
+ 'price', 'inventory_quantity', 'image_url', 'backorders',
239
+ 'visibility',
240
+ );
241
+
242
+ foreach ($singles as $key) {
243
+ if (array_key_exists($key, $data)) {
244
+ $this->$key = $data[$key];
245
+ }
246
+ }
247
+
248
+ return $this;
249
+ }
250
+ }
includes/api/assets/class-mailchimp-product.php ADDED
@@ -0,0 +1,281 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Created by Vextras.
5
+ *
6
+ * Name: Ryan Hungate
7
+ * Email: ryan@mailchimp.com
8
+ * Date: 3/8/16
9
+ * Time: 2:17 PM
10
+ */
11
+ class MailChimp_Product
12
+ {
13
+ protected $id;
14
+ protected $title;
15
+ protected $handle = null;
16
+ protected $url = null;
17
+ protected $description = null;
18
+ protected $type = null;
19
+ protected $vendor = null;
20
+ protected $image_url = null;
21
+ protected $variants = array();
22
+ protected $published_at_foreign = null;
23
+
24
+ /**
25
+ * @return array
26
+ */
27
+ public function getValidation()
28
+ {
29
+ return array(
30
+ 'id' => 'required|string',
31
+ 'title' => 'required|string',
32
+ 'handle' => 'string',
33
+ 'url' => 'url',
34
+ 'description' => 'string',
35
+ 'type' => 'string',
36
+ 'vendor' => 'string',
37
+ 'image_url' => 'url',
38
+ 'variants' => 'required|array',
39
+ 'published_at_foreign' => 'date',
40
+ );
41
+ }
42
+
43
+ /**
44
+ * @return mixed
45
+ */
46
+ public function getId()
47
+ {
48
+ return $this->id;
49
+ }
50
+
51
+ /**
52
+ * @param mixed $id
53
+ * @return MailChimp_Product
54
+ */
55
+ public function setId($id)
56
+ {
57
+ $this->id = $id;
58
+
59
+ return $this;
60
+ }
61
+
62
+ /**
63
+ * @return mixed
64
+ */
65
+ public function getTitle()
66
+ {
67
+ return $this->title;
68
+ }
69
+
70
+ /**
71
+ * @param mixed $title
72
+ * @return MailChimp_Product
73
+ */
74
+ public function setTitle($title)
75
+ {
76
+ $this->title = $title;
77
+
78
+ return $this;
79
+ }
80
+
81
+ /**
82
+ * @return null
83
+ */
84
+ public function getHandle()
85
+ {
86
+ return $this->handle;
87
+ }
88
+
89
+ /**
90
+ * @param null $handle
91
+ * @return MailChimp_Product
92
+ */
93
+ public function setHandle($handle)
94
+ {
95
+ $this->handle = $handle;
96
+
97
+ return $this;
98
+ }
99
+
100
+ /**
101
+ * @return null
102
+ */
103
+ public function getUrl()
104
+ {
105
+ return $this->url;
106
+ }
107
+
108
+ /**
109
+ * @param null $url
110
+ * @return MailChimp_Product
111
+ */
112
+ public function setUrl($url)
113
+ {
114
+ $this->url = $url;
115
+
116
+ return $this;
117
+ }
118
+
119
+ /**
120
+ * @return null
121
+ */
122
+ public function getDescription()
123
+ {
124
+ return $this->description;
125
+ }
126
+
127
+ /**
128
+ * @param null $description
129
+ * @return MailChimp_Product
130
+ */
131
+ public function setDescription($description)
132
+ {
133
+ $this->description = $description;
134
+
135
+ return $this;
136
+ }
137
+
138
+ /**
139
+ * @return null
140
+ */
141
+ public function getType()
142
+ {
143
+ return $this->type;
144
+ }
145
+
146
+ /**
147
+ * @param null $type
148
+ * @return MailChimp_Product
149
+ */
150
+ public function setType($type)
151
+ {
152
+ $this->type = $type;
153
+
154
+ return $this;
155
+ }
156
+
157
+ /**
158
+ * @return null
159
+ */
160
+ public function getVendor()
161
+ {
162
+ return $this->vendor;
163
+ }
164
+
165
+ /**
166
+ * @param null $vendor
167
+ * @return MailChimp_Product
168
+ */
169
+ public function setVendor($vendor)
170
+ {
171
+ $this->vendor = $vendor;
172
+
173
+ return $this;
174
+ }
175
+
176
+ /**
177
+ * @return null
178
+ */
179
+ public function getImageUrl()
180
+ {
181
+ return $this->image_url;
182
+ }
183
+
184
+ /**
185
+ * @param null $image_url
186
+ * @return MailChimp_Product
187
+ */
188
+ public function setImageUrl($image_url)
189
+ {
190
+ $this->image_url = $image_url;
191
+
192
+ return $this;
193
+ }
194
+
195
+ /**
196
+ * @return array
197
+ */
198
+ public function getVariations()
199
+ {
200
+ return $this->variants;
201
+ }
202
+
203
+ /**
204
+ * @param MailChimp_ProductVariation $variation
205
+ * @return MailChimp_Product
206
+ */
207
+ public function addVariant(MailChimp_ProductVariation $variation)
208
+ {
209
+ $this->variants[] = $variation;
210
+
211
+ return $this;
212
+ }
213
+
214
+ /**
215
+ * @return string
216
+ */
217
+ public function getPublishedAtForeign()
218
+ {
219
+ return $this->published_at_foreign;
220
+ }
221
+
222
+ /**
223
+ * @param \DateTime $time
224
+ * @return MailChimp_Product
225
+ */
226
+ public function setPublishedAtForeign(\DateTime $time)
227
+ {
228
+ $this->published_at_foreign = $time->format('Y-m-d H:i:s');
229
+
230
+ return $this;
231
+ }
232
+
233
+ /**
234
+ * @return array
235
+ */
236
+ public function toArray()
237
+ {
238
+ return mailchimp_array_remove_empty(array(
239
+ 'id' => (string) $this->getId(),
240
+ 'title' => $this->getTitle(),
241
+ 'handle' => (string) $this->getHandle(),
242
+ 'url' => (string) $this->getUrl(),
243
+ 'description' => (string) $this->getDescription(),
244
+ 'type' => (string) $this->getType(),
245
+ 'vendor' => (string) $this->getVendor(),
246
+ 'image_url' => (string) $this->getImageUrl(),
247
+ 'variants' => array_map(function ($item) {
248
+ return $item->toArray();
249
+ }, $this->getVariations()),
250
+ 'published_at_foreign' => (string) $this->getPublishedAtForeign(),
251
+ ));
252
+ }
253
+
254
+ /**
255
+ * @param array $data
256
+ * @return MailChimp_Product
257
+ */
258
+ public function fromArray(array $data)
259
+ {
260
+ $singles = array(
261
+ 'id', 'title', 'handle', 'url',
262
+ 'description', 'type', 'vendor', 'image_url',
263
+ 'published_at_foreign',
264
+ );
265
+
266
+ foreach ($singles as $key) {
267
+ if (array_key_exists($key, $data)) {
268
+ $this->$key = $data[$key];
269
+ }
270
+ }
271
+
272
+ if (array_key_exists('variants', $data) && is_array($data['variants'])) {
273
+ $this->variants = array();
274
+ foreach ($data['variants'] as $variant) {
275
+ $this->variants[] = (new MailChimp_ProductVariation())->fromArray($variant);
276
+ }
277
+ }
278
+
279
+ return $this;
280
+ }
281
+ }
includes/api/assets/class-mailchimp-store.php ADDED
@@ -0,0 +1,320 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Created by Vextras.
5
+ *
6
+ * Name: Ryan Hungate
7
+ * Email: ryan@mailchimp.com
8
+ * Date: 3/8/16
9
+ * Time: 3:13 PM
10
+ */
11
+ class MailChimp_Store
12
+ {
13
+ protected $id = null;
14
+ protected $list_id = null;
15
+ protected $name = null;
16
+ protected $domain = null;
17
+ protected $email_address = null;
18
+ protected $currency_code = null;
19
+ protected $money_format = null;
20
+ protected $primary_locale = null;
21
+ protected $timezone = null;
22
+ protected $phone = null;
23
+ protected $address = null;
24
+ protected $platform = null;
25
+
26
+ /**
27
+ * @return array
28
+ */
29
+ public function getValidation()
30
+ {
31
+ return array(
32
+ 'id' => 'required|string',
33
+ 'list_id' => 'required|string',
34
+ 'name' => 'required|string',
35
+ 'domain' => 'string',
36
+ 'email_address' => 'email',
37
+ 'currency_code' => 'required|currency_code',
38
+ 'primary_locale' => 'locale_basic',
39
+ 'timezone' => 'timezone',
40
+ 'phone' => 'string',
41
+ );
42
+ }
43
+
44
+ /**
45
+ * @return null
46
+ */
47
+ public function getId()
48
+ {
49
+ return $this->id;
50
+ }
51
+
52
+ /**
53
+ * @param null $id
54
+ * @return MailChimp_Store
55
+ */
56
+ public function setId($id)
57
+ {
58
+ $this->id = $id;
59
+
60
+ return $this;
61
+ }
62
+
63
+ /**
64
+ * @return null
65
+ */
66
+ public function getListId()
67
+ {
68
+ return $this->list_id;
69
+ }
70
+
71
+ /**
72
+ * @param null $list_id
73
+ * @return MailChimp_Store
74
+ */
75
+ public function setListId($list_id)
76
+ {
77
+ $this->list_id = $list_id;
78
+
79
+ return $this;
80
+ }
81
+
82
+ /**
83
+ * @return null
84
+ */
85
+ public function getName()
86
+ {
87
+ return $this->name;
88
+ }
89
+
90
+ /**
91
+ * @param null $name
92
+ * @return MailChimp_Store;
93
+ */
94
+ public function setName($name)
95
+ {
96
+ $this->name = $name;
97
+
98
+ return $this;
99
+ }
100
+
101
+ /**
102
+ * @return null
103
+ */
104
+ public function getDomain()
105
+ {
106
+ return $this->domain;
107
+ }
108
+
109
+ /**
110
+ * @param null $domain
111
+ * @return MailChimp_Store;
112
+ */
113
+ public function setDomain($domain)
114
+ {
115
+ $this->domain = $domain;
116
+
117
+ return $this;
118
+ }
119
+
120
+ /**
121
+ * @return null
122
+ */
123
+ public function getEmailAddress()
124
+ {
125
+ return $this->email_address;
126
+ }
127
+
128
+ /**
129
+ * @param null $email_address
130
+ * @return MailChimp_Store;
131
+ */
132
+ public function setEmailAddress($email_address)
133
+ {
134
+ $this->email_address = $email_address;
135
+
136
+ return $this;
137
+ }
138
+
139
+ /**
140
+ * @return null
141
+ */
142
+ public function getCurrencyCode()
143
+ {
144
+ return $this->currency_code;
145
+ }
146
+
147
+ /**
148
+ * @param null $currency_code
149
+ * @return MailChimp_Store;
150
+ */
151
+ public function setCurrencyCode($currency_code)
152
+ {
153
+ $this->currency_code = $currency_code;
154
+
155
+ return $this;
156
+ }
157
+
158
+ /**
159
+ * @return null
160
+ */
161
+ public function getMoneyFormat()
162
+ {
163
+ return $this->money_format;
164
+ }
165
+
166
+ /**
167
+ * @param null $money_format
168
+ * @return MailChimp_Store;
169
+ */
170
+ public function setMoneyFormat($money_format)
171
+ {
172
+ $this->money_format = $money_format;
173
+
174
+ return $this;
175
+ }
176
+
177
+ /**
178
+ * @return null
179
+ */
180
+ public function getPrimaryLocale()
181
+ {
182
+ return $this->primary_locale;
183
+ }
184
+
185
+ /**
186
+ * @param null $primary_locale
187
+ * @return MailChimp_Store;
188
+ */
189
+ public function setPrimaryLocale($primary_locale)
190
+ {
191
+ $this->primary_locale = $primary_locale;
192
+
193
+ return $this;
194
+ }
195
+
196
+ /**
197
+ * @return null
198
+ */
199
+ public function getTimezone()
200
+ {
201
+ return $this->timezone;
202
+ }
203
+
204
+ /**
205
+ * @param null $timezone
206
+ * @return MailChimp_Store;
207
+ */
208
+ public function setTimezone($timezone)
209
+ {
210
+ $this->timezone = $timezone;
211
+
212
+ return $this;
213
+ }
214
+
215
+ /**
216
+ * @return null
217
+ */
218
+ public function getPhone()
219
+ {
220
+ return $this->phone;
221
+ }
222
+
223
+ /**
224
+ * @param null $phone
225
+ * @return MailChimp_Store;
226
+ */
227
+ public function setPhone($phone)
228
+ {
229
+ $this->phone = $phone;
230
+
231
+ return $this;
232
+ }
233
+
234
+ /**
235
+ * @param $platform
236
+ * @return $this
237
+ */
238
+ public function setPlatform($platform)
239
+ {
240
+ $this->platform = $platform;
241
+
242
+ return $this;
243
+ }
244
+
245
+ /**
246
+ * @return string
247
+ */
248
+ public function getPlatform()
249
+ {
250
+ return $this->platform;
251
+ }
252
+
253
+ /**
254
+ * @return MailChimp_Address
255
+ */
256
+ public function getAddress()
257
+ {
258
+ if (empty($this->address)) {
259
+ $this->address = new MailChimp_Address();
260
+ }
261
+ return $this->address;
262
+ }
263
+
264
+ /**
265
+ * @param MailChimp_Address $address
266
+ * @return Store;
267
+ */
268
+ public function setAddress(MailChimp_Address $address)
269
+ {
270
+ $this->address = $address;
271
+
272
+ return $this;
273
+ }
274
+
275
+ /**
276
+ * @return array
277
+ */
278
+ public function toArray()
279
+ {
280
+ return mailchimp_array_remove_empty(array(
281
+ 'id' => $this->getId(),
282
+ 'platform' => $this->getPlatform(),
283
+ 'list_id' => $this->getListId(),
284
+ 'name' => $this->getName(),
285
+ 'domain' => $this->getDomain(),
286
+ 'email_address' => $this->getEmailAddress(),
287
+ 'currency_code' => $this->getCurrencyCode(),
288
+ 'money_format' => $this->getMoneyFormat(),
289
+ 'primary_locale' => $this->getPrimaryLocale(),
290
+ 'timezone' => $this->getTimezone(),
291
+ 'phone' => $this->getPhone(),
292
+ 'address' => $this->getAddress()->toArray(),
293
+ ));
294
+ }
295
+
296
+ /**
297
+ * @param array $data
298
+ * @return MailChimp_Store
299
+ */
300
+ public function fromArray(array $data)
301
+ {
302
+ $singles = array(
303
+ 'id', 'list_id', 'name', 'domain',
304
+ 'email_address', 'currency_code', 'money_format',
305
+ 'primary_locale', 'timezone', 'phone', 'platform',
306
+ );
307
+
308
+ foreach ($singles as $key) {
309
+ if (array_key_exists($key, $data)) {
310
+ $this->$key = $data[$key];
311
+ }
312
+ }
313
+
314
+ if (array_key_exists('address', $data)) {
315
+ $this->address = (new MailChimp_Address())->fromArray($data['address']);
316
+ }
317
+
318
+ return $this;
319
+ }
320
+ }
includes/api/class-mailchimp-api.php ADDED
@@ -0,0 +1,1003 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Created by PhpStorm.
5
+ *
6
+ * User: kingpin
7
+ * Email: ryan@mailchimp.com
8
+ * Date: 11/4/15
9
+ * Time: 3:35 PM
10
+ */
11
+ class MailChimpApi
12
+ {
13
+ protected $version = '3.0';
14
+ protected $data_center = 'us2';
15
+ protected $api_key = null;
16
+ protected $auth_type = 'key';
17
+
18
+ /**
19
+ * MailChimpService constructor.
20
+ * @param null $api_key
21
+ */
22
+ public function __construct($api_key = null)
23
+ {
24
+ if (!empty($api_key)) {
25
+ $this->setApiKey($api_key);
26
+ }
27
+ }
28
+
29
+ /**
30
+ * @param $key
31
+ * @return $this
32
+ */
33
+ public function setApiKey($key)
34
+ {
35
+ $parts = str_getcsv($key, '-');
36
+
37
+ if (count($parts) == 2) {
38
+ $this->data_center = $parts[1];
39
+ }
40
+
41
+ $this->api_key = $parts[0];
42
+
43
+ return $this;
44
+ }
45
+
46
+ /**
47
+ * @param $dc
48
+ * @return $this
49
+ */
50
+ public function setDataCenter($dc)
51
+ {
52
+ $this->data_center = $dc;
53
+
54
+ return $this;
55
+ }
56
+
57
+ /**
58
+ * @param $version
59
+ * @return $this
60
+ */
61
+ public function setVersion($version)
62
+ {
63
+ $this->version = $version;
64
+
65
+ return $this;
66
+ }
67
+
68
+ /**
69
+ * @param bool $return_profile
70
+ * @return array|bool
71
+ */
72
+ public function ping($return_profile = false)
73
+ {
74
+ try {
75
+ $profile = $this->get('/');
76
+ return $return_profile ? $profile : true;
77
+ } catch (MailChimp_Error $e) {
78
+ return false;
79
+ }
80
+ }
81
+
82
+ /**
83
+ * @return array
84
+ */
85
+ public function getProfile()
86
+ {
87
+ return $this->get('/');
88
+ }
89
+
90
+ /**
91
+ * @return array|bool
92
+ */
93
+ public function getAuthorizedApps()
94
+ {
95
+ return $this->get('authorized-apps');
96
+ }
97
+
98
+ /**
99
+ * @return array|bool
100
+ */
101
+ public function getAuthorizedAppDetails($id)
102
+ {
103
+ return $this->get("authorized-apps/$id");
104
+ }
105
+
106
+ /**
107
+ * Returns an array of ['access_token' => '', 'viewer_token' => '']
108
+ *
109
+ * @param $client_id
110
+ * @param $client_secret
111
+ * @return array|bool
112
+ */
113
+ public function linkAuthorizedApp($client_id, $client_secret)
114
+ {
115
+ return $this->post('authorized-apps', array('client_id' => $client_id, 'client_secret' => $client_secret));
116
+ }
117
+
118
+ /**
119
+ * @param $list_id
120
+ * @param $email
121
+ * @return array|bool
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
+
129
+ /**
130
+ * @param $list_id
131
+ * @return array|bool
132
+ */
133
+ public function members($list_id)
134
+ {
135
+ return $this->get("lists/$list_id/members");
136
+ }
137
+
138
+ /**
139
+ * @param $list_id
140
+ * @param $email
141
+ * @param bool $subscribed
142
+ * @param array $merge_fields
143
+ * @param array $list_interests
144
+ * @return array|bool
145
+ */
146
+ public function subscribe($list_id, $email, $subscribed = true, $merge_fields = array(), $list_interests = array())
147
+ {
148
+ $data = array(
149
+ 'email_type' => 'html',
150
+ 'email_address' => $email,
151
+ 'status' => $subscribed === true ? 'subscribed' : 'pending',
152
+ 'merge_fields' => $merge_fields,
153
+ 'interests' => $list_interests,
154
+ );
155
+
156
+ if (empty($data['merge_fields'])) {
157
+ unset($data['merge_fields']);
158
+ }
159
+
160
+ if (empty($data['interests'])) {
161
+ unset($data['interests']);
162
+ }
163
+
164
+ return $this->post("lists/$list_id/members", $data);
165
+ }
166
+
167
+ /**
168
+ * @param $list_id
169
+ * @param $email
170
+ * @param bool $subscribed
171
+ * @param array $merge_fields
172
+ * @param array $list_interests
173
+ * @return array|bool
174
+ */
175
+ public function update($list_id, $email, $subscribed = true, $merge_fields = array(), $list_interests = array())
176
+ {
177
+ $hash = md5(strtolower($email));
178
+
179
+ $data = array(
180
+ 'email_address' => $email,
181
+ 'status' => ($subscribed === null ? 'cleaned' : ($subscribed === true ? 'subscribed' : 'unsubscribed')),
182
+ 'merge_fields' => $merge_fields,
183
+ 'interests' => $list_interests,
184
+ );
185
+
186
+ if (empty($data['merge_fields'])) {
187
+ unset($data['merge_fields']);
188
+ }
189
+
190
+
191
+ if (empty($data['interests'])) {
192
+ unset($data['interests']);
193
+ }
194
+
195
+ return $this->patch("lists/$list_id/members/$hash", $data);
196
+ }
197
+
198
+ /**
199
+ * @param $list_id
200
+ * @param $email
201
+ * @param bool $subscribed
202
+ * @param array $merge_fields
203
+ * @param array $list_interests
204
+ * @return array|bool
205
+ */
206
+ public function updateOrCreate($list_id, $email, $subscribed = true, $merge_fields = array(), $list_interests = array())
207
+ {
208
+ $hash = md5(strtolower($email));
209
+
210
+ $data = array(
211
+ 'email_address' => $email,
212
+ 'status' => ($subscribed === null ? 'cleaned' : ($subscribed === true ? 'subscribed' : 'unsubscribed')),
213
+ 'status_if_new' => $subscribed === true ? 'subscribed' : 'pending',
214
+ 'merge_fields' => $merge_fields,
215
+ 'interests' => $list_interests,
216
+ );
217
+
218
+ if (empty($data['merge_fields'])) {
219
+ unset($data['merge_fields']);
220
+ }
221
+
222
+ if (empty($data['interests'])) {
223
+ unset($data['interests']);
224
+ }
225
+
226
+ return $this->put("lists/$list_id/members/$hash", $data);
227
+ }
228
+
229
+ /**
230
+ * @param MailChimp_CreateListSubmission $submission
231
+ * @return array|bool
232
+ */
233
+ public function createList(MailChimp_CreateListSubmission $submission)
234
+ {
235
+ return $this->post('lists', $submission->getSubmission());
236
+ }
237
+
238
+ /**
239
+ * @param bool $as_list
240
+ * @param int $count
241
+ * @return array|mixed
242
+ */
243
+ public function getLists($as_list = false, $count = 100)
244
+ {
245
+ $result = $this->get('lists', array('count' => $count));
246
+
247
+ if ($as_list) {
248
+ $lists = array();
249
+ if ($result) {
250
+ $result = (object)$result;
251
+ if (isset($result->lists) && is_array($result->lists)) {
252
+ foreach ($result->lists as $list) {
253
+ $list = (object)$list;
254
+ $lists[$list->id] = $list->name;
255
+ }
256
+ }
257
+ }
258
+
259
+ return $lists;
260
+ }
261
+
262
+ return $result;
263
+ }
264
+
265
+ /**
266
+ * @param $id
267
+ * @return bool
268
+ */
269
+ public function hasList($id)
270
+ {
271
+ try {
272
+ return (bool) $this->getList($id);
273
+ } catch (\Exception $e) {
274
+ return false;
275
+ }
276
+ }
277
+ /**
278
+ * @param $id
279
+ * @return mixed
280
+ */
281
+ public function getList($id)
282
+ {
283
+ return $this->get('lists/' . $id);
284
+ }
285
+
286
+ /**
287
+ * @param $id
288
+ * @return array|bool
289
+ */
290
+ public function deleteList($id)
291
+ {
292
+ return $this->delete('lists/'.$id);
293
+ }
294
+
295
+ /**
296
+ * @return array|mixed
297
+ */
298
+ public function getListsWithMergeFields()
299
+ {
300
+ $lists = $this->getLists(true);
301
+ foreach ($lists as $id => $name) {
302
+ $lists[$id] = $this->mergeFields($id, 100);
303
+ }
304
+
305
+ return $lists;
306
+ }
307
+
308
+ /**
309
+ * @param $list_id
310
+ * @param int $count
311
+ * @return array|bool
312
+ */
313
+ public function mergeFields($list_id, $count = 10)
314
+ {
315
+ $result = $this->get("lists/$list_id/merge-fields", array('count' => $count,));
316
+
317
+ return $result;
318
+ }
319
+
320
+ /**
321
+ * @param $list_id
322
+ * @return array|bool
323
+ */
324
+ public function getInterestGroups($list_id)
325
+ {
326
+ if (empty($list_id)) {
327
+ return array();
328
+ }
329
+ $result = $this->get("lists/$list_id/interest-categories");
330
+
331
+ return $result;
332
+ }
333
+
334
+ /**
335
+ * @param $list_id
336
+ * @param $group_id
337
+ * @return array|bool
338
+ */
339
+ public function getInterestGroupOptions($list_id, $group_id)
340
+ {
341
+ if (empty($list_id) || empty($group_id)) {
342
+ return array();
343
+ }
344
+ $result = $this->get("lists/$list_id/interest-categories/$group_id/interests");
345
+
346
+ return $result;
347
+ }
348
+
349
+ /**
350
+ * @param $store_id
351
+ * @param int $page
352
+ * @param int $count
353
+ * @param DateTime|null $since
354
+ * @param null $campaign_id
355
+ * @return array|bool
356
+ */
357
+ public function orders($store_id, $page = 1, $count = 10, \DateTime $since = null, $campaign_id = null)
358
+ {
359
+ $result = $this->get('ecommerce/stores/'.$store_id.'/orders', array(
360
+ 'start' => $page,
361
+ 'count' => $count,
362
+ 'offset' => ($page * $count),
363
+ 'since' => $since ? $since->format('Y-m-d H:i:s') : null,
364
+ 'cid' => $campaign_id,
365
+ ));
366
+
367
+ return $result;
368
+ }
369
+
370
+ /**
371
+ * @param $store_id
372
+ * @return MailChimp_Store|bool
373
+ */
374
+ public function getStore($store_id)
375
+ {
376
+ try {
377
+ $data = $this->get("ecommerce/stores/$store_id");
378
+ if (!isset($data['id']) || !isset($data['name'])) {
379
+ return false;
380
+ }
381
+ return (new MailChimp_Store)->fromArray($data);
382
+ } catch (MailChimp_Error $e) {
383
+ return false;
384
+ }
385
+ }
386
+
387
+ /**
388
+ * @return array|bool
389
+ */
390
+ public function stores()
391
+ {
392
+ try {
393
+ $data = $this->get("ecommerce/stores");
394
+
395
+ if (!isset($data['stores']) || empty($data['stores'])) {
396
+ return array();
397
+ }
398
+
399
+ $response = array();
400
+
401
+ foreach ($data['stores'] as $store_data) {
402
+ $response[] = (new MailChimp_Store)->fromArray($store_data);
403
+ }
404
+
405
+ return $response;
406
+ } catch (MailChimp_Error $e) {
407
+ return false;
408
+ }
409
+ }
410
+
411
+ /**
412
+ * @param MailChimp_Store $store
413
+ * @param bool $silent
414
+ * @return bool|MailChimp_Store
415
+ * @throws Exception
416
+ */
417
+ public function addStore(MailChimp_Store $store, $silent = true)
418
+ {
419
+ try {
420
+ $this->validateStoreSubmission($store);
421
+ $data = $this->post("ecommerce/stores", $store->toArray());
422
+ return (new MailChimp_Store)->fromArray($data);
423
+ } catch (\Exception $e) {
424
+ if (!$silent) throw $e;
425
+ return false;
426
+ }
427
+ }
428
+
429
+ /**
430
+ * @param MailChimp_Store $store
431
+ * @param bool $silent
432
+ * @return bool|MailChimp_Store
433
+ * @throws Exception
434
+ */
435
+ public function updateStore(MailChimp_Store $store, $silent = true)
436
+ {
437
+ try {
438
+ $this->validateStoreSubmission($store);
439
+ $data = $this->patch("ecommerce/stores/{$store->getId()}", $store->toArray());
440
+ return (new MailChimp_Store)->fromArray($data);
441
+ } catch (\Exception $e) {
442
+ if (!$silent) throw $e;
443
+ return false;
444
+ }
445
+ }
446
+
447
+ /**
448
+ * @param $store_id
449
+ * @return bool
450
+ */
451
+ public function deleteStore($store_id)
452
+ {
453
+ try {
454
+ $this->delete("ecommerce/stores/$store_id");
455
+ return true;
456
+ } catch (MailChimp_Error $e) {
457
+ return false;
458
+ }
459
+ }
460
+
461
+ /**
462
+ * @param $store_id
463
+ * @param string $customer_id
464
+ * @return MailChimp_Customer|bool
465
+ */
466
+ public function getCustomer($store_id, $customer_id)
467
+ {
468
+ try {
469
+ $data = $this->get("ecommerce/stores/$store_id/customers/$customer_id");
470
+ return (new MailChimp_Customer)->fromArray($data);
471
+ } catch (MailChimp_Error $e) {
472
+ return false;
473
+ }
474
+ }
475
+
476
+ /**
477
+ * @param MailChimp_Customer $store
478
+ * @return MailChimp_Customer
479
+ * @throws MailChimp_Error
480
+ */
481
+ public function addCustomer(MailChimp_Customer $store)
482
+ {
483
+ $this->validateStoreSubmission($store);
484
+ $data = $this->post("ecommerce/stores", $store->toArray());
485
+ return (new MailChimp_Customer)->fromArray($data);
486
+ }
487
+
488
+ /**
489
+ * @param $store_id
490
+ * @param int $page
491
+ * @param int $count
492
+ * @return array|bool
493
+ */
494
+ public function carts($store_id, $page = 1, $count = 10)
495
+ {
496
+ $result = $this->get('ecommerce/stores/'.$store_id.'/carts', array(
497
+ 'start' => $page,
498
+ 'count' => $count,
499
+ 'offset' => ($page * $count),
500
+ ));
501
+
502
+ return $result;
503
+ }
504
+
505
+ /**
506
+ * @param $store_id
507
+ * @param MailChimp_Cart $cart
508
+ * @param bool $silent
509
+ * @return bool|MailChimp_Cart
510
+ * @throws MailChimp_Error
511
+ */
512
+ public function addCart($store_id, MailChimp_Cart $cart, $silent = true)
513
+ {
514
+ try {
515
+ $data = $this->post("ecommerce/stores/$store_id/carts", $cart->toArray());
516
+ return (new MailChimp_Cart)->setStoreID($store_id)->fromArray($data);
517
+ } catch (MailChimp_Error $e) {
518
+ if (!$silent) throw $e;
519
+ mailchimp_log('api.addCart', $e->getMessage());
520
+ return false;
521
+ }
522
+ }
523
+
524
+ /**
525
+ * @param $store_id
526
+ * @param MailChimp_Cart $cart
527
+ * @param bool $silent
528
+ * @return bool|MailChimp_Cart
529
+ * @throws MailChimp_Error
530
+ */
531
+ public function updateCart($store_id, MailChimp_Cart $cart, $silent = true)
532
+ {
533
+ try {
534
+ $data = $this->patch("ecommerce/stores/$store_id/carts/{$cart->getId()}", $cart->toArrayForUpdate());
535
+ return (new MailChimp_Cart)->setStoreID($store_id)->fromArray($data);
536
+ } catch (MailChimp_Error $e) {
537
+ if (!$silent) throw $e;
538
+ mailchimp_log('api.updateCart', $e->getMessage());
539
+ return false;
540
+ }
541
+ }
542
+
543
+ /**
544
+ * @param $store_id
545
+ * @param $id
546
+ * @return bool|MailChimp_Cart
547
+ */
548
+ public function getCart($store_id, $id)
549
+ {
550
+ try {
551
+ $data = $this->get("ecommerce/stores/$store_id/carts/$id");
552
+ return (new MailChimp_Cart)->setStoreID($store_id)->fromArray($data);
553
+ } catch (MailChimp_Error $e) {
554
+ return false;
555
+ }
556
+ }
557
+
558
+ /**
559
+ * @param $store_id
560
+ * @param $id
561
+ * @return bool
562
+ */
563
+ public function deleteCartByID($store_id, $id)
564
+ {
565
+ try {
566
+ $this->delete("ecommerce/stores/$store_id/carts/$id");
567
+ return true;
568
+ } catch (MailChimp_Error $e) {
569
+ return false;
570
+ }
571
+ }
572
+
573
+ /**
574
+ * @param $store_id
575
+ * @param MailChimp_Customer $customer
576
+ * @param bool $silent
577
+ * @return bool|MailChimp_Customer
578
+ * @throws MailChimp_Error
579
+ */
580
+ public function updateCustomer($store_id, MailChimp_Customer $customer, $silent = true)
581
+ {
582
+ try {
583
+ $this->validateStoreSubmission($customer);
584
+ $data = $this->patch("ecommerce/stores/$store_id/customers/{$customer->getId()}", $customer->toArray());
585
+ return (new MailChimp_Customer)->fromArray($data);
586
+ } catch (MailChimp_Error $e) {
587
+ if (!$silent) throw $e;
588
+ return false;
589
+ }
590
+ }
591
+
592
+ /**
593
+ * @param $store_id
594
+ * @param $customer_id
595
+ * @return bool
596
+ */
597
+ public function deleteCustomer($store_id, $customer_id)
598
+ {
599
+ try {
600
+ $this->delete("ecommerce/stores/$store_id/customers/$customer_id");
601
+ return true;
602
+ } catch (MailChimp_Error $e) {
603
+ return false;
604
+ }
605
+ }
606
+
607
+ /**
608
+ * @param $store_id
609
+ * @param MailChimp_Order $order
610
+ * @param bool $silent
611
+ * @return bool|MailChimp_Order
612
+ * @throws Exception
613
+ */
614
+ public function addStoreOrder($store_id, MailChimp_Order $order, $silent = true)
615
+ {
616
+ try {
617
+ $this->validateStoreSubmission($order);
618
+ $data = $this->post("ecommerce/stores/$store_id/orders", $order->toArray());
619
+ return (new MailChimp_Order)->fromArray($data);
620
+ } catch (\Exception $e) {
621
+ if (!$silent) throw $e;
622
+ mailchimp_log('api.add_order.error', $e->getMessage(), array('submission' => $order->toArray()));
623
+ return false;
624
+ }
625
+ }
626
+
627
+ /**
628
+ * @param $store_id
629
+ * @param MailChimp_Order $order
630
+ * @param bool $silent
631
+ * @return bool|MailChimp_Order
632
+ * @throws Exception
633
+ */
634
+ public function updateStoreOrder($store_id, MailChimp_Order $order, $silent = true)
635
+ {
636
+ try {
637
+ $this->validateStoreSubmission($order);
638
+ $id = $order->getId();
639
+ $data = $this->patch("ecommerce/stores/$store_id/orders/$id", $order->toArray());
640
+ return (new MailChimp_Order)->fromArray($data);
641
+ } catch (\Exception $e) {
642
+ if (!$silent) throw $e;
643
+ mailchimp_log('api.update_order.error', $e->getMessage(), array('submission' => $order->toArray()));
644
+ return false;
645
+ }
646
+ }
647
+
648
+ /**
649
+ * @param $store_id
650
+ * @param $order_id
651
+ * @return MailChimp_Order|bool
652
+ */
653
+ public function getStoreOrder($store_id, $order_id)
654
+ {
655
+ try {
656
+ $data = $this->get("ecommerce/stores/$store_id/orders/$order_id");
657
+ return (new MailChimp_Order)->fromArray($data);
658
+ } catch (MailChimp_Error $e) {
659
+ return false;
660
+ }
661
+ }
662
+
663
+ /**
664
+ * @param $store_id
665
+ * @param $order_id
666
+ * @return bool
667
+ */
668
+ public function deleteStoreOrder($store_id, $order_id)
669
+ {
670
+ try {
671
+ $this->delete("ecommerce/stores/$store_id/orders/$order_id");
672
+ return true;
673
+ } catch (MailChimp_Error $e) {
674
+ return false;
675
+ }
676
+ }
677
+
678
+ /**
679
+ * @param $store_id
680
+ * @param $product_id
681
+ * @return MailChimp_Product|bool
682
+ */
683
+ public function getStoreProduct($store_id, $product_id)
684
+ {
685
+ try {
686
+ $data = $this->get("ecommerce/stores/$store_id/products/$product_id");
687
+ return (new MailChimp_Product)->fromArray($data);
688
+ } catch (MailChimp_Error $e) {
689
+ return false;
690
+ }
691
+ }
692
+
693
+ /**
694
+ * @param $store_id
695
+ * @param int $page
696
+ * @param int $count
697
+ * @return array|bool
698
+ */
699
+ public function products($store_id, $page = 1, $count = 10)
700
+ {
701
+ $result = $this->get('ecommerce/stores/'.$store_id.'/products', array(
702
+ 'start' => $page,
703
+ 'count' => $count,
704
+ 'offset' => ($page * $count),
705
+ ));
706
+
707
+ return $result;
708
+ }
709
+
710
+ /**
711
+ * @param $store_id
712
+ * @param MailChimp_Product $product
713
+ * @param bool $silent
714
+ * @return bool|MailChimp_Product
715
+ * @throws Exception
716
+ */
717
+ public function addStoreProduct($store_id, MailChimp_Product $product, $silent = true)
718
+ {
719
+ try {
720
+ $this->validateStoreSubmission($product);
721
+ $data = $this->post("ecommerce/stores/$store_id/products", $product->toArray());
722
+ return (new MailChimp_Product)->fromArray($data);
723
+ } catch (\Exception $e) {
724
+ if (!$silent) throw $e;
725
+ mailchimp_log('api.add_product.error', $e->getMessage(), array('submission' => $product->toArray()));
726
+ return false;
727
+ }
728
+ }
729
+
730
+ /**
731
+ * @param $store_id
732
+ * @param $product_id
733
+ * @return bool
734
+ */
735
+ public function deleteStoreProduct($store_id, $product_id)
736
+ {
737
+ try {
738
+ $this->delete("ecommerce/stores/$store_id/products/$product_id");
739
+ return true;
740
+ } catch (MailChimp_Error $e) {
741
+ return false;
742
+ }
743
+ }
744
+
745
+ /**
746
+ * @param MailChimp_Store|MailChimp_Order|MailChimp_Product|MailChimp_Customer $target
747
+ * @return bool
748
+ * @throws MailChimp_Error
749
+ */
750
+ protected function validateStoreSubmission($target)
751
+ {
752
+ return true;
753
+ }
754
+
755
+ /**
756
+ * @param $url
757
+ * @param null $params
758
+ * @return array|bool
759
+ * @throws MailChimp_Error
760
+ */
761
+ protected function delete($url, $params = null)
762
+ {
763
+ $curl = curl_init();
764
+
765
+ $options = $this->applyCurlOptions('DELETE', $url, $params);
766
+
767
+ curl_setopt_array($curl, $options);
768
+
769
+ return $this->processCurlResponse($curl);
770
+ }
771
+
772
+ /**
773
+ * @param $url
774
+ * @param null $params
775
+ * @return array|bool
776
+ * @throws MailChimp_Error
777
+ */
778
+ protected function get($url, $params = null)
779
+ {
780
+ $curl = curl_init();
781
+
782
+ $options = $this->applyCurlOptions('GET', $url, $params);
783
+
784
+ curl_setopt_array($curl, $options);
785
+
786
+ return $this->processCurlResponse($curl);
787
+ }
788
+
789
+ /**
790
+ * @param $url
791
+ * @param $body
792
+ * @return array|mixed|null|object
793
+ * @throws Exception
794
+ * @throws MailChimp_Error
795
+ */
796
+ protected function patch($url, $body)
797
+ {
798
+ try {
799
+ // process the patch request the normal way
800
+ $curl = curl_init();
801
+
802
+ $options = $this->applyCurlOptions('PATCH', $url, array());
803
+ $options[CURLOPT_POSTFIELDS] = json_encode($body);
804
+
805
+ curl_setopt_array($curl, $options);
806
+
807
+ return $this->processCurlResponse($curl);
808
+
809
+ } catch (\Exception $e) {
810
+
811
+ // if the error that we get is not the json parsing error, throw it.
812
+ if (strpos(strtolower($e->getMessage()), 'json parsing error') === false) {
813
+ throw $e;
814
+ }
815
+
816
+ // ah snap, gotta try the file get contents fallback.
817
+ mailchimp_log('api.patch.fallback', 'stream', array('curl_version' => curl_version()));
818
+
819
+ $context = stream_context_create([
820
+ 'http' => [
821
+ 'method' => 'PATCH',
822
+ 'header' => [
823
+ 'Authorization: Basic '.base64_encode('mailchimp:'.$this->api_key),
824
+ 'Accept: application/json',
825
+ 'Content-Type: application/json'
826
+ ],
827
+ 'content' => json_encode($body)
828
+ ]
829
+ ]);
830
+
831
+ $response = file_get_contents($this->url($url), FALSE, $context);
832
+
833
+ if ($response === false) {
834
+ throw new MailChimp_Error('Invalid patch request');
835
+ }
836
+
837
+ return json_decode($response, true);
838
+ }
839
+ }
840
+
841
+ /**
842
+ * @param $url
843
+ * @param $body
844
+ * @return array|bool
845
+ * @throws MailChimp_Error
846
+ */
847
+ protected function post($url, $body)
848
+ {
849
+ $curl = curl_init();
850
+
851
+ $options = $this->applyCurlOptions('POST', $url, array());
852
+ $options[CURLOPT_POSTFIELDS] = json_encode($body);
853
+
854
+ curl_setopt_array($curl, $options);
855
+
856
+ return $this->processCurlResponse($curl);
857
+ }
858
+
859
+ /**
860
+ * @param $url
861
+ * @param $body
862
+ * @return array|bool
863
+ * @throws MailChimp_Error
864
+ */
865
+ protected function put($url, $body)
866
+ {
867
+ $curl = curl_init();
868
+
869
+ $options = $this->applyCurlOptions('PUT', $url, array());
870
+ $options[CURLOPT_POSTFIELDS] = json_encode($body);
871
+
872
+ curl_setopt_array($curl, $options);
873
+
874
+ return $this->processCurlResponse($curl);
875
+ }
876
+
877
+ /**
878
+ * @param string $extra
879
+ * @param null|array $params
880
+ * @return string
881
+ */
882
+ protected function url($extra = '', $params = null)
883
+ {
884
+ $url = "https://{$this->data_center}.api.mailchimp.com/{$this->version}/";
885
+
886
+ if (!empty($extra)) {
887
+ $url .= $extra;
888
+ }
889
+
890
+ if (!empty($params)) {
891
+ $url .= '?'.(is_array($params) ? http_build_query($params) : $params);
892
+ }
893
+
894
+ return $url;
895
+ }
896
+
897
+ /**
898
+ * @param $method
899
+ * @param $url
900
+ * @param $body
901
+ * @return array|WP_Error
902
+ */
903
+ protected function sendWithHttpClient($method, $url, $body)
904
+ {
905
+ return _wp_http_get_object()->request($this->url($url), array(
906
+ 'method' => strtoupper($method),
907
+ 'headers' => array(
908
+ 'Authorization' => 'Basic ' . base64_encode('mailchimp:'.$this->api_key),
909
+ 'Content-Type' => 'application/json',
910
+ ),
911
+ 'body' => json_encode($body),
912
+ ));
913
+ }
914
+
915
+ /**
916
+ * @param $method
917
+ * @param $url
918
+ * @param array $params
919
+ * @return array
920
+ */
921
+ protected function applyCurlOptions($method, $url, $params = array())
922
+ {
923
+ return array(
924
+ CURLOPT_USERPWD => "mailchimp:{$this->api_key}",
925
+ CURLOPT_CUSTOMREQUEST => strtoupper($method),
926
+ CURLOPT_URL => $this->url($url, $params),
927
+ CURLOPT_RETURNTRANSFER => true,
928
+ CURLOPT_ENCODING => "",
929
+ CURLOPT_MAXREDIRS => 10,
930
+ CURLOPT_TIMEOUT => 30,
931
+ CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
932
+ CURLINFO_HEADER_OUT => true,
933
+ CURLOPT_HTTPHEADER => ['content-type: application/json']
934
+ );
935
+ }
936
+
937
+ /**
938
+ * @param $curl
939
+ * @return array|mixed|null|object
940
+ * @throws Exception
941
+ * @throws MailChimp_Error
942
+ * @throws MailChimp_ServerError
943
+ */
944
+ protected function processCurlResponse($curl)
945
+ {
946
+ $response = curl_exec($curl);
947
+
948
+ $err = curl_error($curl);
949
+ $info = curl_getinfo($curl);
950
+ curl_close($curl);
951
+
952
+ if ($err) {
953
+ throw new MailChimp_Error('CURL error :: '.$err, '500');
954
+ }
955
+
956
+ $data = json_decode($response, true);
957
+
958
+ if (empty($info) || ($info['http_code'] >= 200 && $info['http_code'] <= 400)) {
959
+ if (is_array($data)) {
960
+ try {
961
+ $this->checkForErrors($data);
962
+ } catch (\Exception $e) {
963
+ throw $e;
964
+ }
965
+ }
966
+ return $data;
967
+ }
968
+
969
+ if ($info['http_code'] >= 400 && $info['http_code'] <= 500) {
970
+ throw new MailChimp_Error($data['title'] .' :: '.$data['detail'], $data['status']);
971
+ }
972
+
973
+ if ($info['http_code'] >= 500) {
974
+ throw new MailChimp_ServerError($data['detail'], $data['status']);
975
+ }
976
+
977
+ return null;
978
+ }
979
+
980
+ /**
981
+ * @param array $data
982
+ * @return bool
983
+ * @throws MailChimp_Error
984
+ */
985
+ protected function checkForErrors(array $data)
986
+ {
987
+ // if we have an array of error data push it into a message
988
+ if (isset($data['errors'])) {
989
+ $message = '';
990
+ foreach ($data['errors'] as $error) {
991
+ $message .= '<p>'.$error['field'].': '.$error['message'].'</p>';
992
+ }
993
+ throw new MailChimp_Error($message, $data['status']);
994
+ }
995
+
996
+ // make sure the response is correct from the data in the response array
997
+ if (isset($data['status']) && $data['status'] >= 400) {
998
+ throw new MailChimp_Error($data['detail'], $data['status']);
999
+ }
1000
+
1001
+ return false;
1002
+ }
1003
+ }
includes/api/class-mailchimp-woocommerce-api.php ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Created by Vextras.
5
+ *
6
+ * Name: Ryan Hungate
7
+ * Email: ryan@mailchimp.com
8
+ * Date: 7/13/16
9
+ * Time: 2:32 PM
10
+ */
11
+ class MailChimp_WooCommerce_Api
12
+ {
13
+ protected static $filterable_actions = array(
14
+ 'paginate-resource',
15
+ );
16
+
17
+ /**
18
+ * @param int $default_page
19
+ * @param int $default_per
20
+ * @return array
21
+ */
22
+ public static function filter($default_page = null, $default_per = null)
23
+ {
24
+ if (isset($_GET['mailchimp-woocommerce']) && isset($_GET['mailchimp-woocommerce']['action'])) {
25
+ if (in_array($_GET['mailchimp-woocommerce']['action'], static::$filterable_actions)) {
26
+ if (empty($default_page)) {
27
+ $page = isset($_GET['page']) ? (int) $_GET['page'] : null;
28
+ }
29
+ if (empty($default_per)) {
30
+ $per = isset($_GET['per']) ? (int) $_GET['per'] : null;
31
+ }
32
+ }
33
+ }
34
+
35
+ if (empty($page)) $page = 1;
36
+ if (empty($per)) $per = 10;
37
+
38
+ return array($page, $per);
39
+ }
40
+
41
+ /**
42
+ * @param null $page
43
+ * @param null $per
44
+ * @return object|stdClass
45
+ */
46
+ public function paginateProducts($page = null, $per = null)
47
+ {
48
+ return $this->paginate('products', $page, $per);
49
+ }
50
+
51
+ /**
52
+ * @param null $page
53
+ * @param null $per
54
+ * @return object|stdClass
55
+ */
56
+ public function paginateOrders($page = null, $per = null)
57
+ {
58
+ return $this->paginate('orders', $page, $per);
59
+ }
60
+
61
+ /**
62
+ * @param $resource
63
+ * @param int $page
64
+ * @param int $per
65
+ * @return object|stdClass
66
+ */
67
+ public function paginate($resource, $page = 1, $per = 10)
68
+ {
69
+ if (($sync = $this->engine($resource))) {
70
+ return $sync->compile($page, $per);
71
+ }
72
+
73
+ return (object) array(
74
+ 'endpoint' => $resource,
75
+ 'page' => $page,
76
+ 'count' => 0,
77
+ 'stuffed' => false,
78
+ 'items' => array(),
79
+ );
80
+ }
81
+
82
+ /**
83
+ * @param $resource
84
+ * @return bool|MailChimp_WooCommerce_Transform_Orders|MailChimp_WooCommerce_Transform_Products
85
+ */
86
+ public function engine($resource)
87
+ {
88
+ switch ($resource) {
89
+ case 'products' :
90
+ return new MailChimp_WooCommerce_Transform_Products();
91
+ break;
92
+ case 'orders' :
93
+ return new MailChimp_WooCommerce_Transform_Orders();
94
+ break;
95
+
96
+ default:
97
+ return false;
98
+ }
99
+ }
100
+ }
includes/api/class-mailchimp-woocommerce-create-list-submission.php ADDED
@@ -0,0 +1,162 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Created by Vextras.
5
+ *
6
+ * Name: Ryan Hungate
7
+ * Email: ryan@mailchimp.com
8
+ * Date: 7/8/16
9
+ * Time: 4:16 PM
10
+ */
11
+ class MailChimp_CreateListSubmission
12
+ {
13
+ /**
14
+ * @var array
15
+ */
16
+ protected $props = array();
17
+
18
+ /**
19
+ * @param $name
20
+ * @return $this
21
+ */
22
+ public function setName($name)
23
+ {
24
+ $this->props['name'] = $name;
25
+
26
+ return $this;
27
+ }
28
+
29
+ /**
30
+ * @param $bool
31
+ * @return $this
32
+ */
33
+ public function setUseArchiveBar($bool)
34
+ {
35
+ $this->props['use_archive_bar'] = (bool) $bool;
36
+
37
+ return $this;
38
+ }
39
+
40
+ /**
41
+ * @param $reminder
42
+ * @return $this
43
+ */
44
+ public function setPermissionReminder($reminder)
45
+ {
46
+ $this->props['permission_reminder'] = $reminder;
47
+
48
+ return $this;
49
+ }
50
+
51
+ /**
52
+ * @param $email
53
+ * @return $this
54
+ */
55
+ public function setNotifyOnSubscribe($email)
56
+ {
57
+ $this->props['notify_on_subscribe'] = $email;
58
+
59
+ return $this;
60
+ }
61
+
62
+ /**
63
+ * @param string $email
64
+ * @return $this
65
+ */
66
+ public function setNotifyOnUnSubscribe($email)
67
+ {
68
+ $this->props['notify_on_unsubscribe'] = $email;
69
+
70
+ return $this;
71
+ }
72
+
73
+ /**
74
+ * @param $bool
75
+ * @return $this
76
+ */
77
+ public function setEmailTypeOption($bool)
78
+ {
79
+ $this->props['email_type_option'] = (bool) $bool;
80
+
81
+ return $this;
82
+ }
83
+
84
+ /**
85
+ * @param bool $public
86
+ * @return $this
87
+ */
88
+ public function setVisibility($public = true)
89
+ {
90
+ $this->props['visibility'] = $public ? 'pub' : 'prv';
91
+
92
+ return $this;
93
+ }
94
+
95
+ /**
96
+ * @param $name
97
+ * @param $email
98
+ * @param $subject
99
+ * @param string $language
100
+ * @return $this
101
+ */
102
+ public function setCampaignDefaults($name, $email, $subject, $language = 'en')
103
+ {
104
+ $this->props['campaign_defaults'] = array(
105
+ 'from_name' => $name,
106
+ 'from_email' => $email,
107
+ 'subject' => $subject,
108
+ 'language' => $language,
109
+ );
110
+
111
+ return $this;
112
+ }
113
+
114
+ /**
115
+ * @param MailChimp_Address $address
116
+ * @return $this
117
+ */
118
+ public function setContact(MailChimp_Address $address)
119
+ {
120
+ $data = array();
121
+
122
+ if (($company = $address->getCompany()) && !empty($company)) {
123
+ $data['company'] = $company;
124
+ }
125
+
126
+ if (($street = $address->getAddress1()) && !empty($address)) {
127
+ $data['address1'] = $street;
128
+ }
129
+
130
+ if (($city = $address->getCity()) && !empty($city)) {
131
+ $data['city'] = $city;
132
+ }
133
+
134
+ if (($state = $address->getProvince()) && !empty($state)) {
135
+ $data['state'] = $state;
136
+ }
137
+
138
+ if (($zip = $address->getPostalCode()) && !empty($zip)) {
139
+ $data['zip'] = $zip;
140
+ }
141
+
142
+ if (($country = $address->getCountry()) && !empty($country)) {
143
+ $data['country'] = $country;
144
+ }
145
+
146
+ if (($phone = $address->getPhone()) && !empty($phone)) {
147
+ $data['phone'] = $phone;
148
+ }
149
+
150
+ $this->props['contact'] = $data;
151
+
152
+ return $this;
153
+ }
154
+
155
+ /**
156
+ * @return array
157
+ */
158
+ public function getSubmission()
159
+ {
160
+ return $this->props;
161
+ }
162
+ }
includes/api/class-mailchimp-woocommerce-transform-orders.php ADDED
@@ -0,0 +1,398 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Created by Vextras.
5
+ *
6
+ * Name: Ryan Hungate
7
+ * Email: ryan@mailchimp.com
8
+ * Date: 7/13/16
9
+ * Time: 8:29 AM
10
+ */
11
+ class MailChimp_WooCommerce_Transform_Orders
12
+ {
13
+ public $campaign_id = null;
14
+
15
+ /**
16
+ * @param int $page
17
+ * @param int $limit
18
+ * @return \stdClass
19
+ */
20
+ public function compile($page = 1, $limit = 10)
21
+ {
22
+ $response = (object) array(
23
+ 'endpoint' => 'orders',
24
+ 'page' => $page,
25
+ 'limit' => (int) $limit,
26
+ 'count' => 0,
27
+ 'valid' => 0,
28
+ 'drafts' => 0,
29
+ 'stuffed' => false,
30
+ 'items' => array(),
31
+ );
32
+
33
+ if ((($orders = $this->getOrderPosts($page, $limit)) && !empty($orders))) {
34
+ foreach ($orders as $post) {
35
+ $response->count++;
36
+ if ($post->post_status === 'auto-draft') {
37
+ $response->drafts++;
38
+ continue;
39
+ }
40
+
41
+ $response->valid++;
42
+ $response->items[] = $this->transform($post);
43
+ }
44
+ }
45
+
46
+ $response->stuffed = ($response->count > 0 && (int) $response->count === (int) $limit) ? true : false;
47
+
48
+ return $response;
49
+ }
50
+
51
+ /**
52
+ * @param WP_Post $post
53
+ * @return MailChimp_Order
54
+ */
55
+ public function transform(WP_Post $post)
56
+ {
57
+ $woo = new WC_Order($post);
58
+
59
+ $order = new MailChimp_Order();
60
+
61
+ $order->setId($woo->id);
62
+
63
+ // if we have a campaign id let's set it now.
64
+ if (!empty($this->campaign_id)) {
65
+ $order->setCampaignId($this->campaign_id);
66
+ }
67
+
68
+ $order->setFulfillmentStatus($woo->get_status());
69
+ $order->setProcessedAt(mailchimp_date_utc($woo->order_date));
70
+
71
+ if ($woo->get_status() === 'cancelled') {
72
+ $order->setCancelledAt(mailchimp_date_utc($woo->modified_date));
73
+ }
74
+
75
+ $order->setCurrencyCode($woo->get_order_currency());
76
+ $order->setFinancialStatus($woo->is_paid() ? 'paid' : 'pending');
77
+
78
+ $order->setOrderTotal($woo->get_total());
79
+
80
+ // if we have any tax
81
+ $order->setTaxTotal($woo->get_total_tax());
82
+
83
+ // if we have shipping.
84
+ $order->setShippingTotal($woo->get_total_shipping());
85
+
86
+ // set the customer
87
+ $order->setCustomer($this->buildCustomerFromOrder($woo));
88
+
89
+ // apply the addresses to the order
90
+ $addresses = $this->getOrderAddresses($woo);
91
+ $order->setShippingAddress($addresses->shipping);
92
+ $order->setBillingAddress($addresses->billing);
93
+
94
+ // loop through all the order items
95
+ foreach ($woo->get_items() as $key => $order_detail) {
96
+
97
+ // add it into the order item container.
98
+ $item = $this->buildLineItem($key, $order_detail);
99
+
100
+ // if we don't have a product post with this id, we need to add a deleted product to the MC side
101
+ if (!($product_post = get_post($item->getProductId()))) {
102
+
103
+ // check if it exists, otherwise create a new one.
104
+ if (($deleted_product = MailChimp_WooCommerce_Transform_Products::deleted($item->getProductId()))) {
105
+
106
+ $deleted_product_id = "deleted_{$item->getProductId()}";
107
+
108
+ // swap out the old item id and product variant id with the deleted version.
109
+ $item->setProductId($deleted_product_id);
110
+ $item->setProductVariantId($deleted_product_id);
111
+
112
+ // add the item and continue on the loop.
113
+ $order->addItem($item);
114
+ continue;
115
+ }
116
+
117
+ mailchimp_log('order.items.error', "Order #{$woo->id} :: Product {$item->getProductId()} does not exist!");
118
+ continue;
119
+ }
120
+
121
+ $order->addItem($item);
122
+ }
123
+
124
+ return $order;
125
+ }
126
+
127
+ /**
128
+ * @param WC_Order $order
129
+ * @return MailChimp_Customer
130
+ */
131
+ public function buildCustomerFromOrder(WC_Order $order)
132
+ {
133
+ $customer = new MailChimp_Customer();
134
+
135
+ $customer->setId(md5(trim(strtolower($order->billing_email))));
136
+ $customer->setCompany($order->billing_company);
137
+ $customer->setEmailAddress(trim($order->billing_email));
138
+ $customer->setFirstName($order->billing_first_name);
139
+ $customer->setLastName($order->billing_last_name);
140
+ $customer->setOrdersCount(1);
141
+ $customer->setTotalSpent($order->get_total());
142
+
143
+ // we are saving the post meta for subscribers on each order... so if they have subscribed on checkout
144
+ $subscriber_meta = get_post_meta($order->id, 'mailchimp_woocommerce_is_subscribed', true);
145
+ $subscribed_on_order = $subscriber_meta === '' ? false : (bool) $subscriber_meta;
146
+
147
+ $customer->setOptInStatus($subscribed_on_order);
148
+
149
+ // use the info from the order to compile an address.
150
+ $address = new MailChimp_Address();
151
+ $address->setAddress1($order->billing_address_1);
152
+ $address->setAddress2($order->billing_address_2);
153
+ $address->setCity($order->billing_city);
154
+ $address->setProvince($order->billing_state);
155
+ $address->setPostalCode($order->billing_postcode);
156
+ $address->setCountry($order->billing_country);
157
+ $address->setPhone($order->billing_phone);
158
+ $address->setName('billing');
159
+
160
+ $customer->setAddress($address);
161
+
162
+ if (($user = get_userdata($order->customer_user))) {
163
+
164
+ /** IF we wanted to use the user data instead we would do it here.
165
+ * but we discussed using the billing address instead.
166
+ */
167
+
168
+ /*
169
+ $customer->setId($user->ID);
170
+ $customer->setEmailAddress($user->user_email);
171
+ $customer->setFirstName($user->first_name);
172
+ $customer->setLastName($user->last_name);
173
+
174
+ if (($address = $this->getUserAddress($user->ID))) {
175
+ if (count($address->toArray()) > 3) {
176
+ $customer->setAddress($address);
177
+ }
178
+ }
179
+ */
180
+
181
+ if (!($stats = $this->getCustomerOrderTotals($order->customer_user))) {
182
+ $stats = (object) array('count' => 0, 'total' => 0);
183
+ }
184
+
185
+ $customer->setOrdersCount($stats->count);
186
+ $customer->setTotalSpent($stats->total);
187
+ }
188
+
189
+ return $customer;
190
+ }
191
+
192
+ /**
193
+ * @param $key
194
+ * @param $order_detail
195
+ * @return MailChimp_LineItem
196
+ */
197
+ protected function buildLineItem($key, $order_detail)
198
+ {
199
+ // fire up a new MC line item
200
+ $item = new MailChimp_LineItem();
201
+ $item->setId($key);
202
+
203
+ if (isset($order_detail['item_meta']) && is_array($order_detail['item_meta'])) {
204
+
205
+ foreach ($order_detail['item_meta'] as $meta_key => $meta_data_array) {
206
+
207
+ if (!isset($meta_data_array[0])) {
208
+ continue;
209
+ }
210
+
211
+ switch ($meta_key) {
212
+
213
+ case '_line_subtotal':
214
+ $item->setPrice($meta_data_array[0]);
215
+ break;
216
+
217
+ case '_product_id':
218
+ $item->setProductId($meta_data_array[0]);
219
+ break;
220
+
221
+ case '_variation_id':
222
+ $item->setProductVariantId($meta_data_array[0]);
223
+ break;
224
+
225
+ case '_qty':
226
+ $item->setQuantity($meta_data_array[0]);
227
+ break;
228
+
229
+ }
230
+ }
231
+
232
+ if ($item->getProductVariantId() <= 0) {
233
+ $item->setProductVariantId($item->getProductId());
234
+ }
235
+
236
+ } elseif (isset($order_detail['item_meta_array']) && is_array($order_detail['item_meta_array'])) {
237
+
238
+ /// Some users have the newer version of the item meta.
239
+
240
+ foreach ($order_detail['item_meta_array'] as $meta_id => $object) {
241
+
242
+ if (!isset($object->key)) {
243
+ continue;
244
+ }
245
+
246
+ switch ($object->key) {
247
+
248
+ case '_line_subtotal':
249
+ $item->setPrice($object->value);
250
+ break;
251
+
252
+ case '_product_id':
253
+ $item->setProductId($object->value);
254
+ break;
255
+
256
+ case '_variation_id':
257
+ $item->setProductVariantId($object->value);
258
+ break;
259
+
260
+ case '_qty':
261
+ $item->setQuantity($object->value);
262
+ break;
263
+ }
264
+ }
265
+
266
+ if ($item->getProductVariantId() <= 0) {
267
+ $item->setProductVariantId($item->getProductId());
268
+ }
269
+ }
270
+
271
+ if ($item->getQuantity() > 1) {
272
+ $current_price = $item->getPrice();
273
+ $price = ($current_price/$item->getQuantity());
274
+ $item->setPrice($price);
275
+ }
276
+
277
+ return $item;
278
+ }
279
+
280
+ /**
281
+ * @param int $page
282
+ * @param int $posts
283
+ * @return array|bool
284
+ */
285
+ public function getOrderPosts($page = 1, $posts = 10)
286
+ {
287
+ $orders = get_posts(array(
288
+ 'post_type' => 'shop_order',
289
+ //'post_status' => 'publish',
290
+ 'posts_per_page' => $posts,
291
+ 'paged' => $page,
292
+ 'orderby' => 'id',
293
+ 'order' => 'ASC'
294
+ ));
295
+
296
+ if (empty($orders)) {
297
+ return false;
298
+ }
299
+
300
+ return $orders;
301
+ }
302
+
303
+ /**
304
+ * returns an object with a 'total' and a 'count'.
305
+ *
306
+ * @param $user_id
307
+ * @return object
308
+ */
309
+ public function getCustomerOrderTotals($user_id)
310
+ {
311
+ $stats = (object) array('count' => 0, 'total' => 0);
312
+
313
+ if (!empty($user_id)) {
314
+ $orders = get_posts(apply_filters('woocommerce_my_account_my_orders_query', array(
315
+ 'numberposts' => -1,
316
+ 'meta_key' => '_customer_user',
317
+ 'meta_value' => $user_id,
318
+ 'post_type' => 'shop_order',
319
+ 'post_status' => 'publish'
320
+ )));
321
+
322
+ foreach ($orders as $order) {
323
+ $woo = new WC_Order($order);
324
+ $stats->total += $woo->get_total();
325
+ $stats->count++;
326
+ }
327
+ return $stats;
328
+ }
329
+
330
+ return false;
331
+ }
332
+
333
+ /**
334
+ * @param WC_Order $order
335
+ * @return object
336
+ */
337
+ public function getOrderAddresses(WC_Order $order)
338
+ {
339
+ // use the info from the order to compile an address.
340
+ $billing = new MailChimp_Address();
341
+ $billing->setAddress1($order->billing_address_1);
342
+ $billing->setAddress2($order->billing_address_2);
343
+ $billing->setCity($order->billing_city);
344
+ $billing->setProvince($order->billing_state);
345
+ $billing->setPostalCode($order->billing_postcode);
346
+ $billing->setCountry($order->billing_country);
347
+ $billing->setPhone($order->billing_phone);
348
+ $billing->setName('billing');
349
+
350
+ $shipping = new MailChimp_Address();
351
+ $shipping->setAddress1($order->shipping_address_1);
352
+ $shipping->setAddress2($order->shipping_address_2);
353
+ $shipping->setCity($order->shipping_city);
354
+ $shipping->setProvince($order->shipping_state);
355
+ $shipping->setPostalCode($order->shipping_postcode);
356
+ $shipping->setCountry($order->shipping_country);
357
+ if (isset($order->shipping_phone)) {
358
+ $shipping->setPhone($order->shipping_phone);
359
+ }
360
+ $shipping->setName('shipping');
361
+
362
+ return (object) array('billing' => $billing, 'shipping' => $shipping);
363
+ }
364
+
365
+ /**
366
+ * @param $user_id
367
+ * @param string $type
368
+ * @return MailChimp_Address
369
+ */
370
+ public function getUserAddress($user_id, $type = 'billing')
371
+ {
372
+ $address = new MailChimp_Address();
373
+
374
+ // pull all the meta for this user.
375
+ $meta = get_user_meta($user_id);
376
+
377
+ // loop through all the possible address properties, and if we have on on the user, set the property
378
+ // because it's more up to date.
379
+ $address_props = array(
380
+ $type.'_address_1' => 'setAddress1',
381
+ $type.'_address_2' => 'setAddress2',
382
+ $type.'_city' => 'setCity',
383
+ $type.'_state' => 'setProvince',
384
+ $type.'_postcode' => 'setPostalCode',
385
+ $type.'_country' => 'setCountry',
386
+ $type.'_phone' => 'setPhone',
387
+ );
388
+
389
+ // loop through all the address properties and set the values if we have one.
390
+ foreach ($address_props as $address_key => $address_call) {
391
+ if (isset($meta[$address_key]) && !empty($meta[$address_key]) && isset($meta[$address_key][0])) {
392
+ $address->$address_call($meta[$address_key][0]);
393
+ }
394
+ }
395
+
396
+ return $address;
397
+ }
398
+ }
includes/api/class-mailchimp-woocommerce-transform-products.php ADDED
@@ -0,0 +1,193 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Created by Vextras.
5
+ *
6
+ * Name: Ryan Hungate
7
+ * Email: ryan@mailchimp.com
8
+ * Date: 7/13/16
9
+ * Time: 8:29 AM
10
+ */
11
+ class MailChimp_WooCommerce_Transform_Products
12
+ {
13
+ /**
14
+ * @param int $page
15
+ * @param int $limit
16
+ * @return \stdClass
17
+ */
18
+ public function compile($page = 1, $limit = 10)
19
+ {
20
+ $response = (object) array(
21
+ 'endpoint' => 'products',
22
+ 'page' => $page,
23
+ 'limit' => (int) $limit,
24
+ 'count' => 0,
25
+ 'stuffed' => false,
26
+ 'items' => array(),
27
+ );
28
+
29
+ if ((($products = $this->getProductPosts($page, $limit)) && !empty($products))) {
30
+ foreach ($products as $post) {
31
+ $response->items[] = $this->transform($post);
32
+ $response->count++;
33
+ }
34
+ }
35
+
36
+ $response->stuffed = ($response->count > 0 && (int) $response->count === (int) $limit) ? true : false;
37
+
38
+ return $response;
39
+ }
40
+
41
+ /**
42
+ * @param WP_Post $post
43
+ * @return MailChimp_Product
44
+ */
45
+ public function transform(WP_Post $post)
46
+ {
47
+ $woo = new WC_Product($post);
48
+
49
+ $variant_posts = $this->getProductVariantPosts($post->ID);
50
+
51
+ $variants = $variant_posts ? array_merge(array($woo), $variant_posts) : array($woo);
52
+
53
+ $is_variant = count($variants) > 1;
54
+
55
+ $product = new MailChimp_Product();
56
+
57
+ $product->setId($woo->get_id());
58
+ $product->setHandle($post->post_name);
59
+ $product->setImageUrl(get_the_post_thumbnail_url($post));
60
+ $product->setDescription($post->post_content);
61
+ $product->setPublishedAtForeign(mailchimp_date_utc($post->post_date));
62
+ $product->setTitle($woo->get_title());
63
+ $product->setUrl($woo->get_permalink());
64
+
65
+ foreach ($variants as $variant) {
66
+
67
+ $product_variant = $this->variant($is_variant, $variant);
68
+
69
+ $product_variant_title = $product_variant->getTitle();
70
+
71
+ if (empty($product_variant_title)) {
72
+ $product_variant->setTitle($woo->get_title());
73
+ }
74
+
75
+ $product_variant_image = $product_variant->getImageUrl();
76
+
77
+ if (empty($product_variant_image)) {
78
+ $product_variant->setImageUrl($product->getImageUrl());
79
+ }
80
+
81
+ $product->addVariant($product_variant);
82
+ }
83
+
84
+ return $product;
85
+ }
86
+
87
+ /**
88
+ * @param $is_variant
89
+ * @param WP_Post $post
90
+ * @return MailChimp_ProductVariation
91
+ */
92
+ public function variant($is_variant, $post)
93
+ {
94
+ if ($post instanceof WC_Product || $post instanceof WC_Product_Variation) {
95
+ $woo = $post;
96
+ } else {
97
+ if (isset($post->post_type) && $post->post_type === 'product_variation') {
98
+ $woo = new WC_Product_Variation($post->ID);
99
+ } else {
100
+ $woo = new WC_Product($post);
101
+ }
102
+ }
103
+
104
+ $variant = new MailChimp_ProductVariation();
105
+
106
+ $variant->setId($woo->get_id());
107
+ $variant->setUrl($woo->get_permalink());
108
+ $variant->setTitle($woo->get_title());
109
+ $variant->setBackorders($woo->backorders_allowed());
110
+ $variant->setImageUrl(get_the_post_thumbnail_url($post));
111
+ $variant->setInventoryQuantity(($woo->managing_stock() ? $woo->get_stock_quantity() : 0));
112
+ $variant->setPrice($woo->get_price());
113
+ $variant->setSku($woo->get_sku());
114
+
115
+ if ($woo instanceof WC_Product_Variation) {
116
+ $variant->setVisibility(($woo->variation_is_visible() ? 'visible' : ''));
117
+ } else {
118
+ $variant->setVisibility(($woo->is_visible() ? 'visible' : ''));
119
+ }
120
+
121
+ return $variant;
122
+ }
123
+
124
+ /**
125
+ * @param int $page
126
+ * @param int $posts
127
+ * @return array|bool
128
+ */
129
+ public function getProductPosts($page = 1, $posts = 10)
130
+ {
131
+ $products = get_posts(array(
132
+ 'post_type' => array('product'),
133
+ 'posts_per_page' => $posts,
134
+ 'paged' => $page,
135
+ 'orderby' => 'ID',
136
+ 'order' => 'ASC',
137
+ ));
138
+
139
+ if (empty($products)) {
140
+ return false;
141
+ }
142
+
143
+ return $products;
144
+ }
145
+
146
+ /**
147
+ * @param $id
148
+ * @return array|bool
149
+ */
150
+ public function getProductVariantPosts($id)
151
+ {
152
+ $variants = get_posts(array(
153
+ 'numberposts' => 99999,
154
+ 'order' => 'ASC',
155
+ 'orderby' => 'ID',
156
+ 'post_type' => 'product_variation',
157
+ 'post_parent' => $id,
158
+ ));
159
+
160
+ if (empty($variants)) {
161
+ return false;
162
+ }
163
+
164
+ return $variants;
165
+ }
166
+
167
+ /**
168
+ * @param $id
169
+ * @return MailChimp_Product
170
+ */
171
+ public static function deleted($id)
172
+ {
173
+ $store_id = mailchimp_get_store_id();
174
+ $api = mailchimp_get_api();
175
+
176
+ if (!($product = $api->getStoreProduct($store_id, "deleted_{$id}"))) {
177
+ $product = new MailChimp_Product();
178
+
179
+ $product->setId("deleted_{$id}");
180
+ $product->setTitle("deleted_{$id}");
181
+
182
+ $variant = new MailChimp_ProductVariation();
183
+ $variant->setId("deleted_{$id}");
184
+ $variant->setTitle("deleted_{$id}");
185
+
186
+ $product->addVariant($variant);
187
+
188
+ return $api->addStoreProduct($store_id, $product);
189
+ }
190
+
191
+ return $product;
192
+ }
193
+ }
includes/api/errors/class-mailchimp-error.php ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Created by PhpStorm.
5
+ *
6
+ * User: kingpin
7
+ * Email: ryan@mailchimp.com
8
+ * Date: 6/18/15
9
+ * Time: 11:13 AM
10
+ */
11
+ class MailChimp_Error extends \Exception
12
+ {
13
+
14
+ }
includes/api/errors/class-mailchimp-server-error.php ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Created by PhpStorm.
5
+ *
6
+ * User: kingpin
7
+ * Email: ryan@mailchimp.com
8
+ * Date: 6/18/15
9
+ * Time: 11:13 AM
10
+ */
11
+ class MailChimp_ServerError extends \Exception
12
+ {
13
+
14
+ }
includes/api/helpers/class-mailchimp-woocommerce-api-currency-codes.php ADDED
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Created by Vextras.
5
+ *
6
+ * Name: Ryan Hungate
7
+ * Email: ryan@mailchimp.com
8
+ * Date: 7/12/16
9
+ * Time: 1:38 PM
10
+ */
11
+ class MailChimp_Api_CurrencyCodes
12
+ {
13
+ /**
14
+ * @return array
15
+ */
16
+ public static function all()
17
+ {
18
+ return array(
19
+ 'AFA' => array('Afghan Afghani', '971'),
20
+ 'AWG' => array('Aruban Florin', '533'),
21
+ 'AUD' => array('Australian Dollars', '036'),
22
+ 'ARS' => array('Argentine Pes', '032'),
23
+ 'AZN' => array('Azerbaijanian Manat', '944'),
24
+ 'BSD' => array('Bahamian Dollar', '044'),
25
+ 'BDT' => array('Bangladeshi Taka', '050'),
26
+ 'BBD' => array('Barbados Dollar', '052'),
27
+ 'BYR' => array('Belarussian Rouble', '974'),
28
+ 'BOB' => array('Bolivian Boliviano', '068'),
29
+ 'BRL' => array('Brazilian Real', '986'),
30
+ 'GBP' => array('British Pounds Sterling', '826'),
31
+ 'BGN' => array('Bulgarian Lev', '975'),
32
+ 'KHR' => array('Cambodia Riel', '116'),
33
+ 'CAD' => array('Canadian Dollars', '124'),
34
+ 'KYD' => array('Cayman Islands Dollar', '136'),
35
+ 'CLP' => array('Chilean Peso', '152'),
36
+ 'CNY' => array('Chinese Renminbi Yuan', '156'),
37
+ 'COP' => array('Colombian Peso', '170'),
38
+ 'CRC' => array('Costa Rican Colon', '188'),
39
+ 'HRK' => array('Croatia Kuna', '191'),
40
+ 'CPY' => array('Cypriot Pounds', '196'),
41
+ 'CZK' => array('Czech Koruna', '203'),
42
+ 'DKK' => array('Danish Krone', '208'),
43
+ 'DOP' => array('Dominican Republic Peso', '214'),
44
+ 'XCD' => array('East Caribbean Dollar', '951'),
45
+ 'EGP' => array('Egyptian Pound', '818'),
46
+ 'ERN' => array('Eritrean Nakfa', '232'),
47
+ 'EEK' => array('Estonia Kroon', '233'),
48
+ 'EUR' => array('Euro', '978'),
49
+ 'GEL' => array('Georgian Lari', '981'),
50
+ 'GHC' => array('Ghana Cedi', '288'),
51
+ 'GIP' => array('Gibraltar Pound', '292'),
52
+ 'GTQ' => array('Guatemala Quetzal', '320'),
53
+ 'HNL' => array('Honduras Lempira', '340'),
54
+ 'HKD' => array('Hong Kong Dollars', '344'),
55
+ 'HUF' => array('Hungary Forint', '348'),
56
+ 'ISK' => array('Icelandic Krona', '352'),
57
+ 'INR' => array('Indian Rupee', '356'),
58
+ 'IDR' => array('Indonesia Rupiah', '360'),
59
+ 'ILS' => array('Israel Shekel', '376'),
60
+ 'JMD' => array('Jamaican Dollar', '388'),
61
+ 'JPY' => array('Japanese yen', '392'),
62
+ 'KZT' => array('Kazakhstan Tenge', '368'),
63
+ 'KES' => array('Kenyan Shilling', '404'),
64
+ 'KWD' => array('Kuwaiti Dinar', '414'),
65
+ 'LVL' => array('Latvia Lat', '428'),
66
+ 'LBP' => array('Lebanese Pound', '422'),
67
+ 'LTL' => array('Lithuania Litas', '440'),
68
+ 'MOP' => array('Macau Pataca', '446'),
69
+ 'MKD' => array('Macedonian Denar', '807'),
70
+ 'MGA' => array('Malagascy Ariary', '969'),
71
+ 'MYR' => array('Malaysian Ringgit', '458'),
72
+ 'MTL' => array('Maltese Lira', '470'),
73
+ 'BAM' => array('Marka', '977'),
74
+ 'MUR' => array('Mauritius Rupee', '480'),
75
+ 'MXN' => array('Mexican Pesos', '484'),
76
+ 'MZM' => array('Mozambique Metical', '508'),
77
+ 'NPR' => array('Nepalese Rupee', '524'),
78
+ 'ANG' => array('Netherlands Antilles Guilder', '532'),
79
+ 'TWD' => array('New Taiwanese Dollars', '901'),
80
+ 'NZD' => array('New Zealand Dollars', '554'),
81
+ 'NIO' => array('Nicaragua Cordoba', '558'),
82
+ 'NGN' => array('Nigeria Naira', '566'),
83
+ 'KPW' => array('North Korean Won', '408'),
84
+ 'NOK' => array('Norwegian Krone', '578'),
85
+ 'OMR' => array('Omani Riyal', '512'),
86
+ 'PKR' => array('Pakistani Rupee', '586'),
87
+ 'PYG' => array('Paraguay Guarani', '600'),
88
+ 'PEN' => array('Peru New Sol', '604'),
89
+ 'PHP' => array('Philippine Pesos', '608'),
90
+ 'QAR' => array('Qatari Riyal', '634'),
91
+ 'RON' => array('Romanian New Leu', '946'),
92
+ 'RUB' => array('Russian Federation Ruble', '643'),
93
+ 'SAR' => array('Saudi Riyal', '682'),
94
+ 'CSD' => array('Serbian Dinar', '891'),
95
+ 'SCR' => array('Seychelles Rupee', '690'),
96
+ 'SGD' => array('Singapore Dollars', '702'),
97
+ 'SKK' => array('Slovak Koruna', '703'),
98
+ 'SIT' => array('Slovenia Tolar', '705'),
99
+ 'ZAR' => array('South African Rand', '710'),
100
+ 'KRW' => array('South Korean Won', '410'),
101
+ 'LKR' => array('Sri Lankan Rupee', '144'),
102
+ 'SRD' => array('Surinam Dollar', '968'),
103
+ 'SEK' => array('Swedish Krona', '752'),
104
+ 'CHF' => array('Swiss Francs', '756'),
105
+ 'TZS' => array('Tanzanian Shilling', '834'),
106
+ 'THB' => array('Thai Baht', '764'),
107
+ 'TTD' => array('Trinidad and Tobago Dollar', '780'),
108
+ 'TRY' => array('Turkish New Lira', '949'),
109
+ 'AED' => array('UAE Dirham', '784'),
110
+ 'USD' => array('US Dollars', '840'),
111
+ 'UGX' => array('Ugandian Shilling', '800'),
112
+ 'UAH' => array('Ukraine Hryvna', '980'),
113
+ 'UYU' => array('Uruguayan Peso', '858'),
114
+ 'UZS' => array('Uzbekistani Som', '860'),
115
+ 'VEB' => array('Venezuela Bolivar', '862'),
116
+ 'VND' => array('Vietnam Dong', '704'),
117
+ 'AMK' => array('Zambian Kwacha', '894'),
118
+ 'ZWD' => array('Zimbabwe Dollar', '716'),
119
+ );
120
+ }
121
+
122
+ /**
123
+ * @return array
124
+ */
125
+ public static function lists()
126
+ {
127
+ $response = array();
128
+ foreach (static::all() as $key => $data) {
129
+ $response[$key] = $data[0];
130
+ }
131
+ return $response;
132
+ }
133
+ }
includes/api/helpers/class-mailchimp-woocommerce-api-locales.php ADDED
@@ -0,0 +1,469 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Created by Vextras.
5
+ *
6
+ * Name: Ryan Hungate
7
+ * Email: ryan@mailchimp.com
8
+ * Date: 7/12/16
9
+ * Time: 2:07 PM
10
+ */
11
+ class MailChimp_Api_Locales
12
+ {
13
+ /**
14
+ * @return array
15
+ */
16
+ public function all()
17
+ {
18
+ return [
19
+ "af_NA"=> "Afrikaans (Namibia)",
20
+ "af_ZA"=> "Afrikaans (South Africa)",
21
+ "af"=> "Afrikaans",
22
+ "ak_GH"=> "Akan (Ghana)",
23
+ "ak"=> "Akan",
24
+ "sq_AL"=> "Albanian (Albania)",
25
+ "sq"=> "Albanian",
26
+ "am_ET"=> "Amharic (Ethiopia)",
27
+ "am"=> "Amharic",
28
+ "ar_DZ"=> "Arabic (Algeria)",
29
+ "ar_BH"=> "Arabic (Bahrain)",
30
+ "ar_EG"=> "Arabic (Egypt)",
31
+ "ar_IQ"=> "Arabic (Iraq)",
32
+ "ar_JO"=> "Arabic (Jordan)",
33
+ "ar_KW"=> "Arabic (Kuwait)",
34
+ "ar_LB"=> "Arabic (Lebanon)",
35
+ "ar_LY"=> "Arabic (Libya)",
36
+ "ar_MA"=> "Arabic (Morocco)",
37
+ "ar_OM"=> "Arabic (Oman)",
38
+ "ar_QA"=> "Arabic (Qatar)",
39
+ "ar_SA"=> "Arabic (Saudi Arabia)",
40
+ "ar_SD"=> "Arabic (Sudan)",
41
+ "ar_SY"=> "Arabic (Syria)",
42
+ "ar_TN"=> "Arabic (Tunisia)",
43
+ "ar_AE"=> "Arabic (United Arab Emirates)",
44
+ "ar_YE"=> "Arabic (Yemen)",
45
+ "ar"=> "Arabic",
46
+ "hy_AM"=> "Armenian (Armenia)",
47
+ "hy"=> "Armenian",
48
+ "as_IN"=> "Assamese (India)",
49
+ "as"=> "Assamese",
50
+ "asa_TZ"=> "Asu (Tanzania)",
51
+ "asa"=> "Asu",
52
+ "az_Cyrl"=> "Azerbaijani (Cyrillic)",
53
+ "az_Cyrl_AZ"=> "Azerbaijani (Cyrillic, Azerbaijan)",
54
+ "az_Latn"=> "Azerbaijani (Latin)",
55
+ "az_Latn_AZ"=> "Azerbaijani (Latin, Azerbaijan)",
56
+ "az"=> "Azerbaijani",
57
+ "bm_ML"=> "Bambara (Mali)",
58
+ "bm"=> "Bambara",
59
+ "eu_ES"=> "Basque (Spain)",
60
+ "eu"=> "Basque",
61
+ "be_BY"=> "Belarusian (Belarus)",
62
+ "be"=> "Belarusian",
63
+ "bem_ZM"=> "Bemba (Zambia)",
64
+ "bem"=> "Bemba",
65
+ "bez_TZ"=> "Bena (Tanzania)",
66
+ "bez"=> "Bena",
67
+ "bn_BD"=> "Bengali (Bangladesh)",
68
+ "bn_IN"=> "Bengali (India)",
69
+ "bn"=> "Bengali",
70
+ "bs_BA"=> "Bosnian (Bosnia and Herzegovina)",
71
+ "bs"=> "Bosnian",
72
+ "bg_BG"=> "Bulgarian (Bulgaria)",
73
+ "bg"=> "Bulgarian",
74
+ "my_MM"=> "Burmese (Myanmar [Burma])",
75
+ "my"=> "Burmese",
76
+ "ca_ES"=> "Catalan (Spain)",
77
+ "ca"=> "Catalan",
78
+ "tzm_Latn"=> "Central Morocco Tamazight (Latin)",
79
+ "tzm_Latn_MA"=> "Central Morocco Tamazight (Latin, Morocco)",
80
+ "tzm"=> "Central Morocco Tamazight",
81
+ "chr_US"=> "Cherokee (United States)",
82
+ "chr"=> "Cherokee",
83
+ "cgg_UG"=> "Chiga (Uganda)",
84
+ "cgg"=> "Chiga",
85
+ "zh_Hans"=> "Chinese (Simplified Han)",
86
+ "zh_Hans_CN"=> "Chinese (Simplified Han, China)",
87
+ "zh_Hans_HK"=> "Chinese (Simplified Han, Hong Kong SAR China)",
88
+ "zh_Hans_MO"=> "Chinese (Simplified Han, Macau SAR China)",
89
+ "zh_Hans_SG"=> "Chinese (Simplified Han, Singapore)",
90
+ "zh_Hant"=> "Chinese (Traditional Han)",
91
+ "zh_Hant_HK"=> "Chinese (Traditional Han, Hong Kong SAR China)",
92
+ "zh_Hant_MO"=> "Chinese (Traditional Han, Macau SAR China)",
93
+ "zh_Hant_TW"=> "Chinese (Traditional Han, Taiwan)",
94
+ "zh"=> "Chinese",
95
+ "kw_GB"=> "Cornish (United Kingdom)",
96
+ "kw"=> "Cornish",
97
+ "hr_HR"=> "Croatian (Croatia)",
98
+ "hr"=> "Croatian",
99
+ "cs_CZ"=> "Czech (Czech Republic)",
100
+ "cs"=> "Czech",
101
+ "da_DK"=> "Danish (Denmark)",
102
+ "da"=> "Danish",
103
+ "nl_BE"=> "Dutch (Belgium)",
104
+ "nl_NL"=> "Dutch (Netherlands)",
105
+ "nl"=> "Dutch",
106
+ "ebu_KE"=> "Embu (Kenya)",
107
+ "ebu"=> "Embu",
108
+ "en_AS"=> "English (American Samoa)",
109
+ "en_AU"=> "English (Australia)",
110
+ "en_BE"=> "English (Belgium)",
111
+ "en_BZ"=> "English (Belize)",
112
+ "en_BW"=> "English (Botswana)",
113
+ "en_CA"=> "English (Canada)",
114
+ "en_GU"=> "English (Guam)",
115
+ "en_HK"=> "English (Hong Kong SAR China)",
116
+ "en_IN"=> "English (India)",
117
+ "en_IE"=> "English (Ireland)",
118
+ "en_JM"=> "English (Jamaica)",
119
+ "en_MT"=> "English (Malta)",
120
+ "en_MH"=> "English (Marshall Islands)",
121
+ "en_MU"=> "English (Mauritius)",
122
+ "en_NA"=> "English (Namibia)",
123
+ "en_NZ"=> "English (New Zealand)",
124
+ "en_MP"=> "English (Northern Mariana Islands)",
125
+ "en_PK"=> "English (Pakistan)",
126
+ "en_PH"=> "English (Philippines)",
127
+ "en_SG"=> "English (Singapore)",
128
+ "en_ZA"=> "English (South Africa)",
129
+ "en_TT"=> "English (Trinidad and Tobago)",
130
+ "en_UM"=> "English (U.S. Minor Outlying Islands)",
131
+ "en_VI"=> "English (U.S. Virgin Islands)",
132
+ "en_GB"=> "English (United Kingdom)",
133
+ "en_US"=> "English (United States)",
134
+ "en_ZW"=> "English (Zimbabwe)",
135
+ "en"=> "English",
136
+ "eo"=> "Esperanto",
137
+ "et_EE"=> "Estonian (Estonia)",
138
+ "et"=> "Estonian",
139
+ "ee_GH"=> "Ewe (Ghana)",
140
+ "ee_TG"=> "Ewe (Togo)",
141
+ "ee"=> "Ewe",
142
+ "fo_FO"=> "Faroese (Faroe Islands)",
143
+ "fo"=> "Faroese",
144
+ "fil_PH"=> "Filipino (Philippines)",
145
+ "fil"=> "Filipino",
146
+ "fi_FI"=> "Finnish (Finland)",
147
+ "fi"=> "Finnish",
148
+ "fr_BE"=> "French (Belgium)",
149
+ "fr_BJ"=> "French (Benin)",
150
+ "fr_BF"=> "French (Burkina Faso)",
151
+ "fr_BI"=> "French (Burundi)",
152
+ "fr_CM"=> "French (Cameroon)",
153
+ "fr_CA"=> "French (Canada)",
154
+ "fr_CF"=> "French (Central African Republic)",
155
+ "fr_TD"=> "French (Chad)",
156
+ "fr_KM"=> "French (Comoros)",
157
+ "fr_CG"=> "French (Congo - Brazzaville)",
158
+ "fr_CD"=> "French (Congo - Kinshasa)",
159
+ "fr_CI"=> "French (Côte d’Ivoire)",
160
+ "fr_DJ"=> "French (Djibouti)",
161
+ "fr_GQ"=> "French (Equatorial Guinea)",
162
+ "fr_FR"=> "French (France)",
163
+ "fr_GA"=> "French (Gabon)",
164
+ "fr_GP"=> "French (Guadeloupe)",
165
+ "fr_GN"=> "French (Guinea)",
166
+ "fr_LU"=> "French (Luxembourg)",
167
+ "fr_MG"=> "French (Madagascar)",
168
+ "fr_ML"=> "French (Mali)",
169
+ "fr_MQ"=> "French (Martinique)",
170
+ "fr_MC"=> "French (Monaco)",
171
+ "fr_NE"=> "French (Niger)",
172
+ "fr_RW"=> "French (Rwanda)",
173
+ "fr_RE"=> "French (Réunion)",
174
+ "fr_BL"=> "French (Saint Barthélemy)",
175
+ "fr_MF"=> "French (Saint Martin)",
176
+ "fr_SN"=> "French (Senegal)",
177
+ "fr_CH"=> "French (Switzerland)",
178
+ "fr_TG"=> "French (Togo)",
179
+ "fr"=> "French",
180
+ "ff_SN"=> "Fulah (Senegal)",
181
+ "ff"=> "Fulah",
182
+ "gl_ES"=> "Galician (Spain)",
183
+ "gl"=> "Galician",
184
+ "lg_UG"=> "Ganda (Uganda)",
185
+ "lg"=> "Ganda",
186
+ "ka_GE"=> "Georgian (Georgia)",
187
+ "ka"=> "Georgian",
188
+ "de_AT"=> "German (Austria)",
189
+ "de_BE"=> "German (Belgium)",
190
+ "de_DE"=> "German (Germany)",
191
+ "de_LI"=> "German (Liechtenstein)",
192
+ "de_LU"=> "German (Luxembourg)",
193
+ "de_CH"=> "German (Switzerland)",
194
+ "de"=> "German",
195
+ "el_CY"=> "Greek (Cyprus)",
196
+ "el_GR"=> "Greek (Greece)",
197
+ "el"=> "Greek",
198
+ "gu_IN"=> "Gujarati (India)",
199
+ "gu"=> "Gujarati",
200
+ "guz_KE"=> "Gusii (Kenya)",
201
+ "guz"=> "Gusii",
202
+ "ha_Latn"=> "Hausa (Latin)",
203
+ "ha_Latn_GH"=> "Hausa (Latin, Ghana)",
204
+ "ha_Latn_NE"=> "Hausa (Latin, Niger)",
205
+ "ha_Latn_NG"=> "Hausa (Latin, Nigeria)",
206
+ "ha"=> "Hausa",
207
+ "haw_US"=> "Hawaiian (United States)",
208
+ "haw"=> "Hawaiian",
209
+ "he_IL"=> "Hebrew (Israel)",
210
+ "he"=> "Hebrew",
211
+ "hi_IN"=> "Hindi (India)",
212
+ "hi"=> "Hindi",
213
+ "hu_HU"=> "Hungarian (Hungary)",
214
+ "hu"=> "Hungarian",
215
+ "is_IS"=> "Icelandic (Iceland)",
216
+ "is"=> "Icelandic",
217
+ "ig_NG"=> "Igbo (Nigeria)",
218
+ "ig"=> "Igbo",
219
+ "id_ID"=> "Indonesian (Indonesia)",
220
+ "id"=> "Indonesian",
221
+ "ga_IE"=> "Irish (Ireland)",
222
+ "ga"=> "Irish",
223
+ "it_IT"=> "Italian (Italy)",
224
+ "it_CH"=> "Italian (Switzerland)",
225
+ "it"=> "Italian",
226
+ "ja_JP"=> "Japanese (Japan)",
227
+ "ja"=> "Japanese",
228
+ "kea_CV"=> "Kabuverdianu (Cape Verde)",
229
+ "kea"=> "Kabuverdianu",
230
+ "kab_DZ"=> "Kabyle (Algeria)",
231
+ "kab"=> "Kabyle",
232
+ "kl_GL"=> "Kalaallisut (Greenland)",
233
+ "kl"=> "Kalaallisut",
234
+ "kln_KE"=> "Kalenjin (Kenya)",
235
+ "kln"=> "Kalenjin",
236
+ "kam_KE"=> "Kamba (Kenya)",
237
+ "kam"=> "Kamba",
238
+ "kn_IN"=> "Kannada (India)",
239
+ "kn"=> "Kannada",
240
+ "kk_Cyrl"=> "Kazakh (Cyrillic)",
241
+ "kk_Cyrl_KZ"=> "Kazakh (Cyrillic, Kazakhstan)",
242
+ "kk"=> "Kazakh",
243
+ "km_KH"=> "Khmer (Cambodia)",
244
+ "km"=> "Khmer",
245
+ "ki_KE"=> "Kikuyu (Kenya)",
246
+ "ki"=> "Kikuyu",
247
+ "rw_RW"=> "Kinyarwanda (Rwanda)",
248
+ "rw"=> "Kinyarwanda",
249
+ "kok_IN"=> "Konkani (India)",
250
+ "kok"=> "Konkani",
251
+ "ko_KR"=> "Korean (South Korea)",
252
+ "ko"=> "Korean",
253
+ "khq_ML"=> "Koyra Chiini (Mali)",
254
+ "khq"=> "Koyra Chiini",
255
+ "ses_ML"=> "Koyraboro Senni (Mali)",
256
+ "ses"=> "Koyraboro Senni",
257
+ "lag_TZ"=> "Langi (Tanzania)",
258
+ "lag"=> "Langi",
259
+ "lv_LV"=> "Latvian (Latvia)",
260
+ "lv"=> "Latvian",
261
+ "lt_LT"=> "Lithuanian (Lithuania)",
262
+ "lt"=> "Lithuanian",
263
+ "luo_KE"=> "Luo (Kenya)",
264
+ "luo"=> "Luo",
265
+ "luy_KE"=> "Luyia (Kenya)",
266
+ "luy"=> "Luyia",
267
+ "mk_MK"=> "Macedonian (Macedonia)",
268
+ "mk"=> "Macedonian",
269
+ "jmc_TZ"=> "Machame (Tanzania)",
270
+ "jmc"=> "Machame",
271
+ "kde_TZ"=> "Makonde (Tanzania)",
272
+ "kde"=> "Makonde",
273
+ "mg_MG"=> "Malagasy (Madagascar)",
274
+ "mg"=> "Malagasy",
275
+ "ms_BN"=> "Malay (Brunei)",
276
+ "ms_MY"=> "Malay (Malaysia)",
277
+ "ms"=> "Malay",
278
+ "ml_IN"=> "Malayalam (India)",
279
+ "ml"=> "Malayalam",
280
+ "mt_MT"=> "Maltese (Malta)",
281
+ "mt"=> "Maltese",
282
+ "gv_GB"=> "Manx (United Kingdom)",
283
+ "gv"=> "Manx",
284
+ "mr_IN"=> "Marathi (India)",
285
+ "mr"=> "Marathi",
286
+ "mas_KE"=> "Masai (Kenya)",
287
+ "mas_TZ"=> "Masai (Tanzania)",
288
+ "mas"=> "Masai",
289
+ "mer_KE"=> "Meru (Kenya)",
290
+ "mer"=> "Meru",
291
+ "mfe_MU"=> "Morisyen (Mauritius)",
292
+ "mfe"=> "Morisyen",
293
+ "naq_NA"=> "Nama (Namibia)",
294
+ "naq"=> "Nama",
295
+ "ne_IN"=> "Nepali (India)",
296
+ "ne_NP"=> "Nepali (Nepal)",
297
+ "ne"=> "Nepali",
298
+ "nd_ZW"=> "North Ndebele (Zimbabwe)",
299
+ "nd"=> "North Ndebele",
300
+ "nb_NO"=> "Norwegian Bokmål (Norway)",
301
+ "nb"=> "Norwegian Bokmål",
302
+ "nn_NO"=> "Norwegian Nynorsk (Norway)",
303
+ "nn"=> "Norwegian Nynorsk",
304
+ "nyn_UG"=> "Nyankole (Uganda)",
305
+ "nyn"=> "Nyankole",
306
+ "or_IN"=> "Oriya (India)",
307
+ "or"=> "Oriya",
308
+ "om_ET"=> "Oromo (Ethiopia)",
309
+ "om_KE"=> "Oromo (Kenya)",
310
+ "om"=> "Oromo",
311
+ "ps_AF"=> "Pashto (Afghanistan)",
312
+ "ps"=> "Pashto",
313
+ "fa_AF"=> "Persian (Afghanistan)",
314
+ "fa_IR"=> "Persian (Iran)",
315
+ "fa"=> "Persian",
316
+ "pl_PL"=> "Polish (Poland)",
317
+ "pl"=> "Polish",
318
+ "pt_BR"=> "Portuguese (Brazil)",
319
+ "pt_GW"=> "Portuguese (Guinea-Bissau)",
320
+ "pt_MZ"=> "Portuguese (Mozambique)",
321
+ "pt_PT"=> "Portuguese (Portugal)",
322
+ "pt"=> "Portuguese",
323
+ "pa_Arab"=> "Punjabi (Arabic)",
324
+ "pa_Arab_PK"=> "Punjabi (Arabic, Pakistan)",
325
+ "pa_Guru"=> "Punjabi (Gurmukhi)",
326
+ "pa_Guru_IN"=> "Punjabi (Gurmukhi, India)",
327
+ "pa"=> "Punjabi",
328
+ "ro_MD"=> "Romanian (Moldova)",
329
+ "ro_RO"=> "Romanian (Romania)",
330
+ "ro"=> "Romanian",
331
+ "rm_CH"=> "Romansh (Switzerland)",
332
+ "rm"=> "Romansh",
333
+ "rof_TZ"=> "Rombo (Tanzania)",
334
+ "rof"=> "Rombo",
335
+ "ru_MD"=> "Russian (Moldova)",
336
+ "ru_RU"=> "Russian (Russia)",
337
+ "ru_UA"=> "Russian (Ukraine)",
338
+ "ru"=> "Russian",
339
+ "rwk_TZ"=> "Rwa (Tanzania)",
340
+ "rwk"=> "Rwa",
341
+ "saq_KE"=> "Samburu (Kenya)",
342
+ "saq"=> "Samburu",
343
+ "sg_CF"=> "Sango (Central African Republic)",
344
+ "sg"=> "Sango",
345
+ "seh_MZ"=> "Sena (Mozambique)",
346
+ "seh"=> "Sena",
347
+ "sr_Cyrl"=> "Serbian (Cyrillic)",
348
+ "sr_Cyrl_BA"=> "Serbian (Cyrillic, Bosnia and Herzegovina)",
349
+ "sr_Cyrl_ME"=> "Serbian (Cyrillic, Montenegro)",
350
+ "sr_Cyrl_RS"=> "Serbian (Cyrillic, Serbia)",
351
+ "sr_Latn"=> "Serbian (Latin)",
352
+ "sr_Latn_BA"=> "Serbian (Latin, Bosnia and Herzegovina)",
353
+ "sr_Latn_ME"=> "Serbian (Latin, Montenegro)",
354
+ "sr_Latn_RS"=> "Serbian (Latin, Serbia)",
355
+ "sr"=> "Serbian",
356
+ "sn_ZW"=> "Shona (Zimbabwe)",
357
+ "sn"=> "Shona",
358
+ "ii_CN"=> "Sichuan Yi (China)",
359
+ "ii"=> "Sichuan Yi",
360
+ "si_LK"=> "Sinhala (Sri Lanka)",
361
+ "si"=> "Sinhala",
362
+ "sk_SK"=> "Slovak (Slovakia)",
363
+ "sk"=> "Slovak",
364
+ "sl_SI"=> "Slovenian (Slovenia)",
365
+ "sl"=> "Slovenian",
366
+ "xog_UG"=> "Soga (Uganda)",
367
+ "xog"=> "Soga",
368
+ "so_DJ"=> "Somali (Djibouti)",
369
+ "so_ET"=> "Somali (Ethiopia)",
370
+ "so_KE"=> "Somali (Kenya)",
371
+ "so_SO"=> "Somali (Somalia)",
372
+ "so"=> "Somali",
373
+ "es_AR"=> "Spanish (Argentina)",
374
+ "es_BO"=> "Spanish (Bolivia)",
375
+ "es_CL"=> "Spanish (Chile)",
376
+ "es_CO"=> "Spanish (Colombia)",
377
+ "es_CR"=> "Spanish (Costa Rica)",
378
+ "es_DO"=> "Spanish (Dominican Republic)",
379
+ "es_EC"=> "Spanish (Ecuador)",
380
+ "es_SV"=> "Spanish (El Salvador)",
381
+ "es_GQ"=> "Spanish (Equatorial Guinea)",
382
+ "es_GT"=> "Spanish (Guatemala)",
383
+ "es_HN"=> "Spanish (Honduras)",
384
+ "es_419"=> "Spanish (Latin America)",
385
+ "es_MX"=> "Spanish (Mexico)",
386
+ "es_NI"=> "Spanish (Nicaragua)",
387
+ "es_PA"=> "Spanish (Panama)",
388
+ "es_PY"=> "Spanish (Paraguay)",
389
+ "es_PE"=> "Spanish (Peru)",
390
+ "es_PR"=> "Spanish (Puerto Rico)",
391
+ "es_ES"=> "Spanish (Spain)",
392
+ "es_US"=> "Spanish (United States)",
393
+ "es_UY"=> "Spanish (Uruguay)",
394
+ "es_VE"=> "Spanish (Venezuela)",
395
+ "es"=> "Spanish",
396
+ "sw_KE"=> "Swahili (Kenya)",
397
+ "sw_TZ"=> "Swahili (Tanzania)",
398
+ "sw"=> "Swahili",
399
+ "sv_FI"=> "Swedish (Finland)",
400
+ "sv_SE"=> "Swedish (Sweden)",
401
+ "sv"=> "Swedish",
402
+ "gsw_CH"=> "Swiss German (Switzerland)",
403
+ "gsw"=> "Swiss German",
404
+ "shi_Latn"=> "Tachelhit (Latin)",
405
+ "shi_Latn_MA"=> "Tachelhit (Latin, Morocco)",
406
+ "shi_Tfng"=> "Tachelhit (Tifinagh)",
407
+ "shi_Tfng_MA"=> "Tachelhit (Tifinagh, Morocco)",
408
+ "shi"=> "Tachelhit",
409
+ "dav_KE"=> "Taita (Kenya)",
410
+ "dav"=> "Taita",
411
+ "ta_IN"=> "Tamil (India)",
412
+ "ta_LK"=> "Tamil (Sri Lanka)",
413
+ "ta"=> "Tamil",
414
+ "te_IN"=> "Telugu (India)",
415
+ "te"=> "Telugu",
416
+ "teo_KE"=> "Teso (Kenya)",
417
+ "teo_UG"=> "Teso (Uganda)",
418
+ "teo"=> "Teso",
419
+ "th_TH"=> "Thai (Thailand)",
420
+ "th"=> "Thai",
421
+ "bo_CN"=> "Tibetan (China)",
422
+ "bo_IN"=> "Tibetan (India)",
423
+ "bo"=> "Tibetan",
424
+ "ti_ER"=> "Tigrinya (Eritrea)",
425
+ "ti_ET"=> "Tigrinya (Ethiopia)",
426
+ "ti"=> "Tigrinya",
427
+ "to_TO"=> "Tonga (Tonga)",
428
+ "to"=> "Tonga",
429
+ "tr_TR"=> "Turkish (Turkey)",
430
+ "tr"=> "Turkish",
431
+ "uk_UA"=> "Ukrainian (Ukraine)",
432
+ "uk"=> "Ukrainian",
433
+ "ur_IN"=> "Urdu (India)",
434
+ "ur_PK"=> "Urdu (Pakistan)",
435
+ "ur"=> "Urdu",
436
+ "uz_Arab"=> "Uzbek (Arabic)",
437
+ "uz_Arab_AF"=> "Uzbek (Arabic, Afghanistan)",
438
+ "uz_Cyrl"=> "Uzbek (Cyrillic)",
439
+ "uz_Cyrl_UZ"=> "Uzbek (Cyrillic, Uzbekistan)",
440
+ "uz_Latn"=> "Uzbek (Latin)",
441
+ "uz_Latn_UZ"=> "Uzbek (Latin, Uzbekistan)",
442
+ "uz"=> "Uzbek",
443
+ "vi_VN"=> "Vietnamese (Vietnam)",
444
+ "vi"=> "Vietnamese",
445
+ "vun_TZ"=> "Vunjo (Tanzania)",
446
+ "vun"=> "Vunjo",
447
+ "cy_GB"=> "Welsh (United Kingdom)",
448
+ "cy"=> "Welsh",
449
+ "yo_NG"=> "Yoruba (Nigeria)",
450
+ "yo"=> "Yoruba",
451
+ "zu_ZA"=> "Zulu (South Africa)",
452
+ "zu"=> "Zulu"
453
+ ];
454
+ }
455
+
456
+ /**
457
+ * @return array
458
+ */
459
+ public static function simple()
460
+ {
461
+ $response = array();
462
+ foreach (static::all() as $key => $value) {
463
+ if (!strpos($key, '_') > 0) {
464
+ $response[$key] = $value;
465
+ }
466
+ }
467
+ return $response;
468
+ }
469
+ }
includes/class-mailchimp-woocommerce-activator.php ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * Fired during plugin activation.
4
+ *
5
+ * This class defines all code necessary to run during the plugin's activation.
6
+ *
7
+ * @since 1.0.1
8
+ * @package MailChimp_Woocommerce
9
+ * @subpackage MailChimp_Woocommerce/includes
10
+ * @author Ryan Hungate <ryan@mailchimp.com>
11
+ */
12
+ class MailChimp_Woocommerce_Activator {
13
+
14
+ /**
15
+ * Short Description. (use period)
16
+ *
17
+ * Long Description.
18
+ *
19
+ * @since 1.0.0
20
+ */
21
+ public static function activate() {
22
+
23
+ add_option('mailchimp_woocommerce_plugin_do_activation_redirect', true);
24
+
25
+ // create the queue tables because we need them for the sync jobs.
26
+ static::create_queue_tables();
27
+
28
+ // create the api keys for MC just in case.
29
+ $data = static::create_keys('MailChimp', 'USER_ID', 'read_write');
30
+
31
+ // update the settings so we have them for use.
32
+ update_option('mailchimp-woocommerce', array(
33
+ 'woo_consumer_key' => $data['consumer_key'],
34
+ 'woo_consumer_secret' => $data['consumer_secret']
35
+ ));
36
+ }
37
+
38
+ /**
39
+ * Create keys.
40
+ *
41
+ * @since 2.4.0
42
+ *
43
+ * @param string $app_name
44
+ * @param string $app_user_id
45
+ * @param string $scope
46
+ *
47
+ * @return array
48
+ */
49
+ public static function create_keys( $app_name, $app_user_id, $scope ) {
50
+ global $wpdb;
51
+
52
+ $description = sprintf( __( '%s - API %s (created on %s at %s).', 'woocommerce' ), wc_clean( $app_name ), __( 'Read/Write', 'woocommerce' ), date_i18n( wc_date_format() ), date_i18n( wc_time_format() ) );
53
+ $user = wp_get_current_user();
54
+
55
+ // Created API keys.
56
+ $permissions = 'read_write';
57
+ $consumer_key = 'ck_' . wc_rand_hash();
58
+ $consumer_secret = 'cs_' . wc_rand_hash();
59
+
60
+ $wpdb->insert(
61
+ $wpdb->prefix . 'woocommerce_api_keys',
62
+ array(
63
+ 'user_id' => $user->ID,
64
+ 'description' => $description,
65
+ 'permissions' => $permissions,
66
+ 'consumer_key' => wc_api_hash( $consumer_key ),
67
+ 'consumer_secret' => $consumer_secret,
68
+ 'truncated_key' => substr( $consumer_key, -7 )
69
+ ),
70
+ array('%d', '%s', '%s', '%s', '%s', '%s')
71
+ );
72
+
73
+ return array(
74
+ 'key_id' => $wpdb->insert_id,
75
+ 'user_id' => $app_user_id,
76
+ 'consumer_key' => $consumer_key,
77
+ 'consumer_secret' => $consumer_secret,
78
+ 'key_permissions' => $permissions
79
+ );
80
+ }
81
+
82
+ /**
83
+ * Create the queue tables in the DB so we can use it for syncing.
84
+ */
85
+ public static function create_queue_tables()
86
+ {
87
+ require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
88
+
89
+ global $wpdb;
90
+
91
+ $wpdb->hide_errors();
92
+
93
+ $charset_collate = $wpdb->get_charset_collate();
94
+
95
+ $sql = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}queue (
96
+ id bigint(20) NOT NULL AUTO_INCREMENT,
97
+ job text NOT NULL,
98
+ attempts tinyint(1) NOT NULL DEFAULT 0,
99
+ locked tinyint(1) NOT NULL DEFAULT 0,
100
+ locked_at datetime DEFAULT NULL,
101
+ available_at datetime NOT NULL,
102
+ created_at datetime NOT NULL,
103
+ PRIMARY KEY (id)
104
+ ) $charset_collate;";
105
+
106
+ dbDelta( $sql );
107
+
108
+ $sql = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}failed_jobs (
109
+ id bigint(20) NOT NULL AUTO_INCREMENT,
110
+ job text NOT NULL,
111
+ failed_at datetime NOT NULL,
112
+ PRIMARY KEY (id)
113
+ ) $charset_collate;";
114
+
115
+ dbDelta( $sql );
116
+ }
117
+ }
includes/class-mailchimp-woocommerce-deactivator.php ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Fired during plugin deactivation
5
+ *
6
+ * @link https://mailchimp.com
7
+ * @since 1.0.1
8
+ *
9
+ * @package MailChimp_Woocommerce
10
+ * @subpackage MailChimp_Woocommerce/includes
11
+ */
12
+
13
+ /**
14
+ * Fired during plugin deactivation.
15
+ *
16
+ * This class defines all code necessary to run during the plugin's deactivation.
17
+ *
18
+ * @since 1.0.1
19
+ * @package MailChimp_Woocommerce
20
+ * @subpackage MailChimp_Woocommerce/includes
21
+ * @author Ryan Hungate <ryan@mailchimp.com>
22
+ */
23
+ class MailChimp_Woocommerce_Deactivator {
24
+
25
+ /**
26
+ * Short Description. (use period)
27
+ *
28
+ * Long Description.
29
+ *
30
+ * @since 1.0.0
31
+ */
32
+ public static function deactivate() {
33
+ // if the api is valid, we need to try to delete the store
34
+ if (($api = mailchimp_get_api())) {
35
+ $api->deleteStore(mailchimp_get_store_id());
36
+ }
37
+ }
38
+
39
+ }
includes/class-mailchimp-woocommerce-i18n.php ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Define the internationalization functionality
5
+ *
6
+ * Loads and defines the internationalization files for this plugin
7
+ * so that it is ready for translation.
8
+ *
9
+ * @link https://mailchimp.com
10
+ * @since 1.0.1
11
+ *
12
+ * @package MailChimp_Woocommerce
13
+ * @subpackage MailChimp_Woocommerce/includes
14
+ */
15
+
16
+ /**
17
+ * Define the internationalization functionality.
18
+ *
19
+ * Loads and defines the internationalization files for this plugin
20
+ * so that it is ready for translation.
21
+ *
22
+ * @since 1.0.1
23
+ * @package MailChimp_Woocommerce
24
+ * @subpackage MailChimp_Woocommerce/includes
25
+ * @author Ryan Hungate <ryan@mailchimp.com>
26
+ */
27
+ class MailChimp_Woocommerce_i18n {
28
+
29
+
30
+ /**
31
+ * Load the plugin text domain for translation.
32
+ *
33
+ * @since 1.0.0
34
+ */
35
+ public function load_plugin_textdomain() {
36
+
37
+ load_plugin_textdomain(
38
+ 'mailchimp-woocommerce',
39
+ false,
40
+ dirname( dirname( plugin_basename( __FILE__ ) ) ) . '/languages/'
41
+ );
42
+
43
+ }
44
+
45
+
46
+
47
+ }
includes/class-mailchimp-woocommerce-loader.php ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Register all actions and filters for the plugin
5
+ *
6
+ * @link https://mailchimp.com
7
+ * @since 1.0.1
8
+ *
9
+ * @package MailChimp_Woocommerce
10
+ * @subpackage MailChimp_Woocommerce/includes
11
+ */
12
+
13
+ /**
14
+ * Register all actions and filters for the plugin.
15
+ *
16
+ * Maintain a list of all hooks that are registered throughout
17
+ * the plugin, and register them with the WordPress API. Call the
18
+ * run function to execute the list of actions and filters.
19
+ *
20
+ * @package MailChimp_Woocommerce
21
+ * @subpackage MailChimp_Woocommerce/includes
22
+ * @author Ryan Hungate <ryan@mailchimp.com>
23
+ */
24
+ class MailChimp_Woocommerce_Loader {
25
+
26
+ /**
27
+ * The array of actions registered with WordPress.
28
+ *
29
+ * @since 1.0.0
30
+ * @access protected
31
+ * @var array $actions The actions registered with WordPress to fire when the plugin loads.
32
+ */
33
+ protected $actions;
34
+
35
+ /**
36
+ * The array of filters registered with WordPress.
37
+ *
38
+ * @since 1.0.0
39
+ * @access protected
40
+ * @var array $filters The filters registered with WordPress to fire when the plugin loads.
41
+ */
42
+ protected $filters;
43
+
44
+ /**
45
+ * Initialize the collections used to maintain the actions and filters.
46
+ *
47
+ * @since 1.0.0
48
+ */
49
+ public function __construct() {
50
+
51
+ $this->actions = array();
52
+ $this->filters = array();
53
+
54
+ }
55
+
56
+ /**
57
+ * Add a new action to the collection to be registered with WordPress.
58
+ *
59
+ * @since 1.0.0
60
+ * @param string $hook The name of the WordPress action that is being registered.
61
+ * @param object $component A reference to the instance of the object on which the action is defined.
62
+ * @param string $callback The name of the function definition on the $component.
63
+ * @param int $priority Optional. he priority at which the function should be fired. Default is 10.
64
+ * @param int $accepted_args Optional. The number of arguments that should be passed to the $callback. Default is 1.
65
+ */
66
+ public function add_action( $hook, $component, $callback, $priority = 10, $accepted_args = 1 ) {
67
+ $this->actions = $this->add( $this->actions, $hook, $component, $callback, $priority, $accepted_args );
68
+ }
69
+
70
+ /**
71
+ * Add a new filter to the collection to be registered with WordPress.
72
+ *
73
+ * @since 1.0.0
74
+ * @param string $hook The name of the WordPress filter that is being registered.
75
+ * @param object $component A reference to the instance of the object on which the filter is defined.
76
+ * @param string $callback The name of the function definition on the $component.
77
+ * @param int $priority Optional. he priority at which the function should be fired. Default is 10.
78
+ * @param int $accepted_args Optional. The number of arguments that should be passed to the $callback. Default is 1
79
+ */
80
+ public function add_filter( $hook, $component, $callback, $priority = 10, $accepted_args = 1 ) {
81
+ $this->filters = $this->add( $this->filters, $hook, $component, $callback, $priority, $accepted_args );
82
+ }
83
+
84
+ /**
85
+ * A utility function that is used to register the actions and hooks into a single
86
+ * collection.
87
+ *
88
+ * @since 1.0.0
89
+ * @access private
90
+ * @param array $hooks The collection of hooks that is being registered (that is, actions or filters).
91
+ * @param string $hook The name of the WordPress filter that is being registered.
92
+ * @param object $component A reference to the instance of the object on which the filter is defined.
93
+ * @param string $callback The name of the function definition on the $component.
94
+ * @param int $priority The priority at which the function should be fired.
95
+ * @param int $accepted_args The number of arguments that should be passed to the $callback.
96
+ * @return array The collection of actions and filters registered with WordPress.
97
+ */
98
+ private function add( $hooks, $hook, $component, $callback, $priority, $accepted_args ) {
99
+
100
+ $hooks[] = array(
101
+ 'hook' => $hook,
102
+ 'component' => $component,
103
+ 'callback' => $callback,
104
+ 'priority' => $priority,
105
+ 'accepted_args' => $accepted_args
106
+ );
107
+
108
+ return $hooks;
109
+
110
+ }
111
+
112
+ /**
113
+ * Register the filters and actions with WordPress.
114
+ *
115
+ * @since 1.0.0
116
+ */
117
+ public function run() {
118
+
119
+ foreach ( $this->filters as $hook ) {
120
+ add_filter( $hook['hook'], array( $hook['component'], $hook['callback'] ), $hook['priority'], $hook['accepted_args'] );
121
+ }
122
+
123
+ foreach ( $this->actions as $hook ) {
124
+ add_action( $hook['hook'], array( $hook['component'], $hook['callback'] ), $hook['priority'], $hook['accepted_args'] );
125
+ }
126
+
127
+ }
128
+
129
+ }
includes/class-mailchimp-woocommerce-newsletter.php ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Created by MailChimp.
5
+ *
6
+ * Name: Ryan Hungate
7
+ * Email: ryan@mailchimp.com
8
+ * Date: 2/22/16
9
+ * Time: 9:09 AM
10
+ */
11
+ class MailChimp_Newsletter extends MailChimp_Woocommerce_Options
12
+ {
13
+ /**
14
+ * @param WC_Checkout $checkout
15
+ */
16
+ public function applyNewsletterField($checkout)
17
+ {
18
+ if (!is_admin()) {
19
+ $status = is_user_logged_in() ? get_user_meta(get_current_user_id(), 'mailchimp_woocommerce_is_subscribed',
20
+ true) : true;
21
+
22
+ $checkbox = '<p class="form-row form-row-wide create-account">';
23
+ $checkbox .= '<input class="input-checkbox" id="mailchimp_woocommerce_newsletter" type="checkbox" name="mailchimp_woocommerce_newsletter" value="1" checked="' . ($status ? 'checked' : '') . '"> ';
24
+ $checkbox .= '<label for="mailchimp_woocommerce_newsletter" class="checkbox">' . $this->getOption('newsletter_label',
25
+ 'Subscribe to our newsletter') . '</label></p>';
26
+ $checkbox .= '<div class="clear"></div>';
27
+
28
+ echo $checkbox;
29
+ }
30
+ }
31
+
32
+ /**
33
+ * @param $order_id
34
+ * @param $posted
35
+ */
36
+ public function processNewsletterField($order_id, $posted)
37
+ {
38
+ $this->handleStatus($order_id);
39
+ }
40
+
41
+ /**
42
+ * @param WC_Order $order
43
+ */
44
+ public function processPayPalNewsletterField($order)
45
+ {
46
+ $this->handleStatus($order->id);
47
+ }
48
+
49
+ /**
50
+ * @param $sanitized_user_login
51
+ * @param $user_email
52
+ * @param $reg_errors
53
+ */
54
+ public function processRegistrationForm($sanitized_user_login, $user_email, $reg_errors)
55
+ {
56
+
57
+ if (defined('WOOCOMMERCE_CHECKOUT')) {
58
+ return; // Ship checkout
59
+ }
60
+ $this->handleStatus();
61
+ }
62
+
63
+ /**
64
+ * @param null $order_id
65
+ * @return bool|int
66
+ */
67
+ protected function handleStatus($order_id = null)
68
+ {
69
+ $status = isset($_POST['mailchimp_woocommerce_newsletter']) ? (int)$_POST['mailchimp_woocommerce_newsletter'] : 0;
70
+
71
+ if ($order_id) {
72
+ update_post_meta($order_id, 'mailchimp_woocommerce_is_subscribed', $status);
73
+ }
74
+
75
+ if (is_user_logged_in()) {
76
+ update_user_meta(get_current_user_id(), 'mailchimp_woocommerce_is_subscribed', $status);
77
+
78
+ return $status;
79
+ }
80
+
81
+ return false;
82
+ }
83
+ }
includes/class-mailchimp-woocommerce-options.php ADDED
@@ -0,0 +1,238 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Created by MailChimp.
5
+ *
6
+ * Name: Ryan Hungate
7
+ * Email: ryan@mailchimp.com
8
+ * Date: 2/22/16
9
+ * Time: 3:45 PM
10
+ */
11
+ abstract class MailChimp_Woocommerce_Options
12
+ {
13
+ /**
14
+ * @var MailChimpApi
15
+ */
16
+ protected $api;
17
+ protected $plugin_name = 'mailchimp-woocommerce';
18
+ protected $environment = 'production';
19
+ protected $version = '1.0.0';
20
+ protected $plugin_options = null;
21
+ protected $is_admin = false;
22
+
23
+ /**
24
+ * hook calls this so that we know the admin is here.
25
+ */
26
+ public function adminReady()
27
+ {
28
+ $this->is_admin = current_user_can('administrator');
29
+ if (get_option('mailchimp_woocommerce_plugin_do_activation_redirect', false)) {
30
+ delete_option('mailchimp_woocommerce_plugin_do_activation_redirect');
31
+ if (!isset($_GET['activate-multi'])) {
32
+ wp_redirect("options-general.php?page=mailchimp-woocommerce");
33
+ }
34
+ }
35
+ }
36
+
37
+ /**
38
+ * @return bool
39
+ */
40
+ public function isAdmin()
41
+ {
42
+ return $this->is_admin;
43
+ }
44
+
45
+ /**
46
+ * @param $version
47
+ */
48
+ public function setVersion($version)
49
+ {
50
+ $this->version = $version;
51
+ }
52
+
53
+ /**
54
+ * @return string
55
+ */
56
+ public function getVersion()
57
+ {
58
+ return $this->version;
59
+ }
60
+
61
+ /**
62
+ * @return string
63
+ */
64
+ public function getUniqueStoreID()
65
+ {
66
+ return md5(get_option('siteurl'));
67
+ }
68
+
69
+ /**
70
+ * @param $env
71
+ * @return $this
72
+ */
73
+ public function setEnvironment($env)
74
+ {
75
+ $this->environment = $env;
76
+ return $this;
77
+ }
78
+
79
+ /**
80
+ * @return string
81
+ */
82
+ public function getEnvironment()
83
+ {
84
+ return $this->environment;
85
+ }
86
+
87
+ /**
88
+ * @param $key
89
+ * @param null $default
90
+ * @return null
91
+ */
92
+ public function getOption($key, $default = null)
93
+ {
94
+ $options = $this->getOptions();
95
+ if (isset($options[$key])) {
96
+ return $options[$key];
97
+ }
98
+ return $default;
99
+ }
100
+
101
+ /**
102
+ * @param $key
103
+ * @param bool $default
104
+ * @return bool
105
+ */
106
+ public function hasOption($key, $default = false)
107
+ {
108
+ return (bool) $this->getOption($key, $default);
109
+ }
110
+
111
+ /**
112
+ * @return array
113
+ */
114
+ public function getOptions()
115
+ {
116
+ if (empty($this->plugin_options)) {
117
+ $this->plugin_options = get_option($this->plugin_name);
118
+ }
119
+ return is_array($this->plugin_options) ? $this->plugin_options : array();
120
+ }
121
+
122
+ /**
123
+ * @param $key
124
+ * @param $value
125
+ * @return $this
126
+ */
127
+ public function setData($key, $value)
128
+ {
129
+ update_option($this->plugin_name.'-'.$key, $value);
130
+ return $this;
131
+ }
132
+
133
+ /**
134
+ * @param $key
135
+ * @param null $default
136
+ * @return mixed|void
137
+ */
138
+ public function getData($key, $default = null)
139
+ {
140
+ return get_option($this->plugin_name.'-'.$key, $default);
141
+ }
142
+
143
+
144
+ /**
145
+ * @param $key
146
+ * @return bool
147
+ */
148
+ public function removeData($key)
149
+ {
150
+ return delete_option($this->plugin_name.'-'.$key);
151
+ }
152
+
153
+ /**
154
+ * @param $key
155
+ * @param null $default
156
+ * @return null|mixed
157
+ */
158
+ public function getCached($key, $default = null)
159
+ {
160
+ $cached = $this->getData("cached-$key", false);
161
+ if (empty($cached) || !($cached = unserialize($cached))) {
162
+ return $default;
163
+ }
164
+
165
+ if (empty($cached['till']) || (time() > $cached['till'])) {
166
+ $this->removeData("cached-$key");
167
+ return $default;
168
+ }
169
+
170
+ return $cached['value'];
171
+ }
172
+
173
+ /**
174
+ * @param $key
175
+ * @param $value
176
+ * @param $seconds
177
+ * @return $this
178
+ */
179
+ public function setCached($key, $value, $seconds = 60)
180
+ {
181
+ $time = time();
182
+ $data = array('at' => $time, 'till' => $time + $seconds, 'value' => $value);
183
+ $this->setData("cached-$key", serialize($data));
184
+
185
+ return $this;
186
+ }
187
+
188
+ /**
189
+ * @param $key
190
+ * @param $callable
191
+ * @param int $seconds
192
+ * @return mixed|null
193
+ */
194
+ public function getCachedWithSetDefault($key, $callable, $seconds = 60)
195
+ {
196
+ if (!($value = $this->getCached($key, false))) {
197
+ $value = call_user_func($callable);
198
+ $this->setCached($key, $value, $seconds);
199
+ }
200
+ return $value;
201
+ }
202
+
203
+ /**
204
+ * @return bool
205
+ */
206
+ public function isConfigured()
207
+ {
208
+ return true;
209
+ //return $this->getOption('public_key', false) && $this->getOption('secret_key', false);
210
+ }
211
+
212
+ /**
213
+ * @return MailChimpApi
214
+ */
215
+ public function api()
216
+ {
217
+ if (empty($this->api)) {
218
+ $this->api = new MailChimpApi($this->getOption('mailchimp_api_key', false));
219
+ }
220
+
221
+ return $this->api;
222
+ }
223
+
224
+ /**
225
+ * @param array $data
226
+ * @param $key
227
+ * @param null $default
228
+ * @return null|mixed
229
+ */
230
+ public function array_get(array $data, $key, $default = null)
231
+ {
232
+ if (isset($data[$key])) {
233
+ return $data[$key];
234
+ }
235
+
236
+ return $default;
237
+ }
238
+ }
includes/class-mailchimp-woocommerce-service.php ADDED
@@ -0,0 +1,492 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Created by MailChimp.
5
+ *
6
+ * Name: Ryan Hungate
7
+ * Email: ryan@mailchimp.com
8
+ * Date: 2/17/16
9
+ * Time: 12:03 PM
10
+ */
11
+ class MailChimp_Service extends MailChimp_Woocommerce_Options
12
+ {
13
+ protected $user_email = null;
14
+ protected $previous_email = null;
15
+ protected $force_cart_post = false;
16
+ protected $pushed_orders = array();
17
+ protected $cart_was_submitted = false;
18
+ protected $cart = array();
19
+
20
+ /**
21
+ * hook fired when we know everything is booted
22
+ */
23
+ public function wooIsRunning()
24
+ {
25
+ $this->handleAdminFunctions();
26
+ $this->is_admin = current_user_can('administrator');
27
+ }
28
+
29
+ /**
30
+ * @param $r
31
+ * @param $url
32
+ * @return mixed
33
+ */
34
+ public function addHttpRequestArgs( $r, $url ) {
35
+ // not sure whether or not we need to implement something like this yet.
36
+ //$r['headers']['Authorization'] = 'Basic ' . base64_encode('username:password');
37
+ return $r;
38
+ }
39
+
40
+ /**
41
+ * @param $key
42
+ * @param $default
43
+ * @return mixed
44
+ */
45
+ protected function cookie($key, $default = null)
46
+ {
47
+ if ($this->is_admin) {
48
+ return $default;
49
+ }
50
+
51
+ return isset($_COOKIE[$key]) ? $_COOKIE[$key] : $default;
52
+ }
53
+
54
+ /**
55
+ * @param $order_id
56
+ */
57
+ public function handleOrderStatusChanged($order_id)
58
+ {
59
+ if ($this->hasOption('mailchimp_api_key') && !array_key_exists($order_id, $this->pushed_orders)) {
60
+
61
+ // register this order is already in process..
62
+ $this->pushed_orders[$order_id] = true;
63
+
64
+ // see if we have a session id and a campaign id, also only do this when this user is not the admin.
65
+ $campaign_id = $this->getCampaignTrackingID();
66
+
67
+ // queue up the single order to be processed.
68
+ $handler = new MailChimp_WooCommerce_Single_Order($order_id, null, $campaign_id);
69
+ wp_queue($handler);
70
+ }
71
+ }
72
+
73
+ /**
74
+ * @return bool|void
75
+ */
76
+ public function handleCartUpdated()
77
+ {
78
+ if ($this->is_admin || $this->cart_was_submitted || !$this->hasOption('mailchimp_api_key')) {
79
+ return false;
80
+ }
81
+
82
+ if (empty($this->cart)) {
83
+ $this->cart = $this->getCartItems();
84
+ }
85
+
86
+ if (($user_email = $this->getCurrentUserEmail())) {
87
+
88
+ $previous = $this->getPreviousEmailFromSession();
89
+
90
+ $uid = md5(trim(strtolower($user_email)));
91
+
92
+ // delete the previous records.
93
+ if (!empty($previous) && $previous !== $user_email) {
94
+ if ($this->api()->deleteCartByID($this->getUniqueStoreID(), $previous_email = md5(trim(strtolower($previous))))) {
95
+ mailchimp_log('ac.cart_swap', "Deleted cart [$previous] :: ID [$previous_email]");
96
+ }
97
+ }
98
+
99
+ if ($this->cart && !empty($this->cart)) {
100
+
101
+ $this->cart_was_submitted = true;
102
+
103
+ // grab the cookie data that could play important roles in the submission
104
+ $campaign = $this->getCampaignTrackingID();
105
+
106
+ // fire up the job handler
107
+ $handler = new MailChimp_WooCommerce_Cart_Update($uid, $user_email, $campaign, $this->cart);
108
+ wp_queue($handler);
109
+ }
110
+
111
+ return true;
112
+ }
113
+
114
+ return false;
115
+ }
116
+
117
+ /**
118
+ * Save post metadata when a post is saved.
119
+ *
120
+ * @param int $post_id The post ID.
121
+ * @param WP_Post $post The post object.
122
+ * @param bool $update Whether this is an existing post being updated or not.
123
+ */
124
+ public function handlePostSaved($post_id, $post, $update) {
125
+ if ('product' == $post->post_type) {
126
+ wp_queue(new MailChimp_WooCommerce_Single_Product($post_id), 5);
127
+ } elseif ('shop_order' == $post->post_type) {
128
+ wp_queue(new MailChimp_WooCommerce_Single_Order($post_id, null, null));
129
+ }
130
+ }
131
+
132
+ /**
133
+ * @return bool|string
134
+ */
135
+ public function getCurrentUserEmail()
136
+ {
137
+ if (isset($this->user_email) && !empty($this->user_email)) {
138
+ return $this->user_email = strtolower($this->user_email);
139
+ }
140
+
141
+ $user = wp_get_current_user();
142
+ $email = ($user->ID > 0 && isset($user->user_email)) ? $user->user_email : $this->getEmailFromSession();
143
+
144
+ return $this->user_email = strtolower($email);
145
+ }
146
+
147
+ /**
148
+ * @return bool|array
149
+ */
150
+ public function getCartItems() {
151
+ if (!($this->cart = $this->getWooSession('cart', false))) {
152
+ $this->cart = WC()->cart->get_cart();
153
+ } else {
154
+ $cart_session = array();
155
+ foreach ( $this->cart as $key => $values ) {
156
+ $cart_session[$key] = $values;
157
+ unset($cart_session[$key]['data']); // Unset product object
158
+ }
159
+ return $this->cart = $cart_session;
160
+ }
161
+
162
+ return is_array($this->cart) ? $this->cart : false;
163
+ }
164
+
165
+ /**
166
+ * Set the cookie of the mailchimp campaigns if we have one.
167
+ */
168
+ public function handleCampaignTracking()
169
+ {
170
+ $cookie_duration = $this->getCookieDuration();
171
+
172
+ if (isset($_REQUEST['mc_cid'])) {
173
+ $this->setCampaignTrackingID($_REQUEST['mc_cid'], $cookie_duration);
174
+ }
175
+
176
+ if (isset($_REQUEST['mc_eid'])) {
177
+ @setcookie('mailchimp_email_id', trim($_REQUEST['mc_eid']), $cookie_duration, '/' );
178
+ }
179
+ }
180
+
181
+ /**
182
+ * @return mixed|null
183
+ */
184
+ public function getCampaignTrackingID()
185
+ {
186
+ $cookie = $this->cookie('mailchimp_campaign_id', false);
187
+ if (empty($cookie)) {
188
+ $cookie = $this->getWooSession('mailchimp_tracking_id', false);
189
+ }
190
+ return $cookie;
191
+ }
192
+
193
+ /**
194
+ * @param $id
195
+ * @param $cookie_duration
196
+ * @return $this
197
+ */
198
+ public function setCampaignTrackingID($id, $cookie_duration)
199
+ {
200
+ $cid = trim($id);
201
+
202
+ @setcookie('mailchimp_campaign_id', $cid, $cookie_duration, '/' );
203
+ $this->setWooSession('mailchimp_campaign_id', $cid);
204
+
205
+ return $this;
206
+ }
207
+
208
+ /**
209
+ * @return bool
210
+ */
211
+ protected function getEmailFromSession()
212
+ {
213
+ return $this->cookie('mailchimp_user_email', false);
214
+ }
215
+
216
+ /**
217
+ * @return bool
218
+ */
219
+ protected function getPreviousEmailFromSession()
220
+ {
221
+ if ($this->previous_email) {
222
+ return $this->previous_email = strtolower($this->previous_email);
223
+ }
224
+ $email = $this->cookie('mailchimp_user_previous_email', false);
225
+ return $email ? strtolower($email) : false;
226
+ }
227
+
228
+ /**
229
+ * @param $key
230
+ * @param null $default
231
+ * @return mixed|null
232
+ */
233
+ public function getWooSession($key, $default = null)
234
+ {
235
+ if (!($woo = WC()) || empty($woo->session)) {
236
+ return $default;
237
+ }
238
+ return $woo->session->get($key, $default);
239
+ }
240
+
241
+ /**
242
+ * @param $key
243
+ * @param $value
244
+ * @return $this
245
+ */
246
+ public function setWooSession($key, $value)
247
+ {
248
+ if (!($woo = WC()) || empty($woo->session)) {
249
+ return $this;
250
+ }
251
+
252
+ $woo->session->set($key, $value);
253
+
254
+ return $this;
255
+ }
256
+
257
+ /**
258
+ * @param $key
259
+ * @return $this
260
+ */
261
+ public function removeWooSession($key)
262
+ {
263
+ if (!($woo = WC()) || empty($woo->session)) {
264
+ return $this;
265
+ }
266
+
267
+ $woo->session->__unset($key);
268
+ return $this;
269
+ }
270
+
271
+ /**
272
+ * @param string $time
273
+ * @return int
274
+ */
275
+ protected function getCookieDuration($time = 'thirty_days')
276
+ {
277
+ $durations = array(
278
+ 'one_day' => 86400, 'seven_days' => 604800, 'fourteen_days' => 1209600, 'thirty_days' => 2419200,
279
+ );
280
+
281
+ if (!array_key_exists($time, $durations)) {
282
+ $time = 'thirty_days';
283
+ }
284
+
285
+ return time() + $durations[$time];
286
+ }
287
+
288
+ /**
289
+ * Just a wrapper to call various methods from MailChimp to the store.
290
+ * Authentication is based on the secret keys being correct or it will fail.
291
+ *
292
+ * The get requests need:
293
+ * 1. mailchimp-woocommerce[action]
294
+ * 2. mailchimp-woocommerce[submission]
295
+ * 3. various other parts based on the api call.
296
+ */
297
+ protected function handleAdminFunctions()
298
+ {
299
+ if (isset($_GET['reset_cookies'])) {
300
+ $buster = time()-300;
301
+
302
+ setcookie('mailchimp_user_previous_email', '', $buster);
303
+ setcookie('mailchimp_user_email', '', $buster);
304
+ setcookie('mailchimp_campaign_id', '', $buster);
305
+ setcookie('mailchimp_email_id', '', $buster);
306
+
307
+ $this->previous_email = null;
308
+ $this->user_email = null;
309
+ }
310
+
311
+ $methods = array(
312
+ 'plugin-version' => 'respondAdminGetPluginVersion',
313
+ 'submit-email' => 'respondAdminSubmitEmail',
314
+ 'track-campaign' => 'respondAdminTrackCampaign',
315
+ 'get-tracking-data' => 'respondAdminGetTrackingData',
316
+ 'verify' => 'respondAdminVerify',
317
+ );
318
+
319
+ if (($action = $this->get('action'))) {
320
+
321
+ if ($action === 'sync') {
322
+ return $this->sync();
323
+ }
324
+
325
+ if (array_key_exists($action, $methods)) {
326
+ if (!in_array($action, array('submit-email', 'track-campaign', 'get-tracking-data'))) {
327
+ $this->authenticate();
328
+ }
329
+ $this->respondJSON($this->{$methods[$action]}());
330
+ }
331
+ }
332
+ }
333
+
334
+ /**
335
+ * Delete all the options pointing to the pages, and re-start the sync process.
336
+ * @return void
337
+ */
338
+ protected function sync()
339
+ {
340
+ // only do this if we're an admin user.
341
+ if ($this->isAdmin()) {
342
+
343
+ delete_option('mailchimp-woocommerce-errors.store_info');
344
+ delete_option('mailchimp-woocommerce-sync.orders.completed_at');
345
+ delete_option('mailchimp-woocommerce-sync.orders.current_page');
346
+ delete_option('mailchimp-woocommerce-sync.products.completed_at');
347
+ delete_option('mailchimp-woocommerce-sync.products.current_page');
348
+ delete_option('mailchimp-woocommerce-sync.syncing');
349
+ delete_option('mailchimp-woocommerce-sync.started_at');
350
+ delete_option('mailchimp-woocommerce-sync.completed_at');
351
+ delete_option('mailchimp-woocommerce-validation.api.ping');
352
+ delete_option('mailchimp-woocommerce-cached-api-lists');
353
+ delete_option('mailchimp-woocommerce-cached-api-ping-check');
354
+
355
+ $job = new MailChimp_WooCommerce_Process_Products();
356
+ $job->flagStartSync();
357
+ wp_queue($job);
358
+
359
+ wp_redirect('/options-general.php?page=mailchimp-woocommerce&tab=api_key&success_notice=re-sync-started');
360
+ }
361
+
362
+ return;
363
+ }
364
+
365
+ /**
366
+ * @return array
367
+ */
368
+ protected function respondAdminGetPluginVersion()
369
+ {
370
+ return array('success' => true, 'version' => $this->getVersion());
371
+ }
372
+
373
+ /**
374
+ * @return array
375
+ */
376
+ protected function respondAdminVerify()
377
+ {
378
+ return array('success' => true);
379
+ }
380
+
381
+ /**
382
+ * @return array
383
+ */
384
+ protected function respondAdminSubmitEmail()
385
+ {
386
+ if ($this->is_admin) {
387
+ return array('success' => false);
388
+ }
389
+
390
+ $submission = $this->get('submission');
391
+
392
+ if (is_array($submission) && isset($submission['email'])) {
393
+
394
+ $cookie_duration = $this->getCookieDuration();
395
+
396
+ $this->user_email = trim(str_replace(' ','+', $submission['email']));
397
+
398
+ if (($current_email = $this->getEmailFromSession()) && $current_email !== $this->user_email) {
399
+ $this->previous_email = $current_email;
400
+ $this->force_cart_post = true;
401
+ @setcookie('mailchimp_user_previous_email',$this->user_email, $cookie_duration, '/' );
402
+ }
403
+
404
+ @setcookie('mailchimp_user_email', $this->user_email, $cookie_duration, '/' );
405
+
406
+ $this->getCartItems();
407
+
408
+ $this->handleCartUpdated();
409
+
410
+ return array(
411
+ 'success' => true,
412
+ 'email' => $this->user_email,
413
+ 'previous' => $this->previous_email,
414
+ 'cart' => $this->cart,
415
+ );
416
+ }
417
+ return array('success' => false);
418
+ }
419
+
420
+ /**
421
+ * @return array
422
+ */
423
+ protected function respondAdminTrackCampaign()
424
+ {
425
+ if ($this->is_admin) {
426
+ return array('success' => false);
427
+ }
428
+
429
+ $submission = $this->get('submission');
430
+
431
+ if (is_array($submission) && isset($submission['campaign_id'])) {
432
+
433
+ $duration = $this->getCookieDuration();
434
+
435
+ $campaign_id = trim($submission['campaign_id']);
436
+ $email_id = trim($submission['email_id']);
437
+
438
+ @setcookie('mailchimp_campaign_id', $campaign_id, $duration, '/');
439
+ @setcookie('mailchimp_email_id', $email_id, $duration, '/');
440
+
441
+ return $this->respondAdminGetTrackingData();
442
+ }
443
+ return array('success' => false);
444
+ }
445
+
446
+ /**
447
+ * @return array
448
+ */
449
+ protected function respondAdminGetTrackingData()
450
+ {
451
+ return array(
452
+ 'success' => true,
453
+ 'campaign_id' => $this->cookie('mailchimp_campaign_id', 'n/a'),
454
+ 'email_id' => $this->cookie('mailchimp_email_id', 'n/a')
455
+ );
456
+ }
457
+
458
+ /**
459
+ * @param $key
460
+ * @param bool $default
461
+ * @return bool
462
+ */
463
+ protected function get($key, $default = false)
464
+ {
465
+ if (!isset($_REQUEST['mailchimp-woocommerce']) || !isset($_REQUEST['mailchimp-woocommerce'][$key])) {
466
+ return $default;
467
+ }
468
+ return $_REQUEST['mailchimp-woocommerce'][$key];
469
+ }
470
+
471
+ /**
472
+ * @return bool
473
+ */
474
+ protected function authenticate()
475
+ {
476
+ if (trim((string) $this->getUniqueStoreID()) !== trim((string) $this->get('store_id'))) {
477
+ $this->respondJSON(array('success' => false, 'message' => 'Not Authorized'));
478
+ }
479
+
480
+ return true;
481
+ }
482
+
483
+ /**
484
+ * @param $data
485
+ */
486
+ protected function respondJSON($data)
487
+ {
488
+ header('Content-Type: application/json');
489
+ echo json_encode($data);
490
+ exit;
491
+ }
492
+ }
includes/class-mailchimp-woocommerce.php ADDED
@@ -0,0 +1,398 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * The file that defines the core plugin class
5
+ *
6
+ * A class definition that includes attributes and functions used across both the
7
+ * public-facing side of the site and the admin area.
8
+ *
9
+ * @link https://mailchimp.com
10
+ * @since 1.0.1
11
+ *
12
+ * @package MailChimp_Woocommerce
13
+ * @subpackage MailChimp_Woocommerce/includes
14
+ */
15
+
16
+ /**
17
+ * The core plugin class.
18
+ *
19
+ * This is used to define internationalization, admin-specific hooks, and
20
+ * public-facing site hooks.
21
+ *
22
+ * Also maintains the unique identifier of this plugin as well as the current
23
+ * version of the plugin.
24
+ *
25
+ * @since 1.0.0
26
+ * @package MailChimp_Woocommerce
27
+ * @subpackage MailChimp_Woocommerce/includes
28
+ * @author Ryan Hungate <ryan@mailchimp.com>
29
+ */
30
+ class MailChimp_Woocommerce {
31
+
32
+ /**
33
+ * The loader that's responsible for maintaining and registering all hooks that power
34
+ * the plugin.
35
+ *
36
+ * @since 1.0.0
37
+ * @access protected
38
+ * @var MailChimp_Woocommerce_Loader $loader Maintains and registers all hooks for the plugin.
39
+ */
40
+ protected $loader;
41
+
42
+ /**
43
+ * The unique identifier of this plugin.
44
+ *
45
+ * @since 1.0.0
46
+ * @access protected
47
+ * @var string $plugin_name The string used to uniquely identify this plugin.
48
+ */
49
+ protected $plugin_name;
50
+
51
+ /**
52
+ * The current version of the plugin.
53
+ *
54
+ * @since 1.0.0
55
+ * @access protected
56
+ * @var string $version The current version of the plugin.
57
+ */
58
+ protected $version;
59
+
60
+ /**
61
+ * @var string
62
+ */
63
+ protected $environment = 'production';
64
+
65
+ protected static $logging_config = null;
66
+
67
+ /**
68
+ * @return object
69
+ */
70
+ public static function getLoggingConfig()
71
+ {
72
+ if (is_object(static::$logging_config)) {
73
+ return static::$logging_config;
74
+ }
75
+
76
+ $plugin_options = get_option('mailchimp-woocommerce');
77
+ $is_options = is_array($plugin_options);
78
+
79
+ $api_key = $is_options && array_key_exists('mailchimp_api_key', $plugin_options) ?
80
+ $plugin_options['mailchimp_api_key'] : false;
81
+
82
+ $enable_logging = $is_options &&
83
+ array_key_exists('mailchimp_debugging', $plugin_options) &&
84
+ $plugin_options['mailchimp_debugging'];
85
+
86
+ $account_id = $is_options && array_key_exists('mailchimp_account_info_id', $plugin_options) ?
87
+ $plugin_options['mailchimp_account_info_id'] : false;
88
+
89
+ $username = $is_options && array_key_exists('mailchimp_account_info_username', $plugin_options) ?
90
+ $plugin_options['mailchimp_account_info_username'] : false;
91
+
92
+ $api_key_parts = str_getcsv($api_key, '-');
93
+ $data_center = isset($api_key_parts[1]) ? $api_key_parts[1] : 'us1';
94
+
95
+ return static::$logging_config = (object) array(
96
+ 'enable_logging' => (bool) $enable_logging,
97
+ 'account_id' => $account_id,
98
+ 'username' => $username,
99
+ 'endpoint' => 'https://ecommerce.'.$data_center.'.list-manage.com/ecommerce/log',
100
+ );
101
+ }
102
+
103
+
104
+ /**
105
+ * Define the core functionality of the plugin.
106
+ *
107
+ * Set the plugin name and the plugin version that can be used throughout the plugin.
108
+ * Load the dependencies, define the locale, and set the hooks for the admin area and
109
+ * the public-facing side of the site.
110
+ *
111
+ * @param string $environment
112
+ * @param string $version
113
+ *
114
+ * @since 1.0.0
115
+ */
116
+ public function __construct($environment = 'production', $version = '1.0.0') {
117
+
118
+ $this->plugin_name = 'mailchimp-woocommerce';
119
+ $this->version = $version;
120
+ $this->environment = $environment;
121
+
122
+ $this->load_dependencies();
123
+ $this->set_locale();
124
+ $this->define_admin_hooks();
125
+ $this->define_public_hooks();
126
+
127
+ $this->activateMailChimpNewsletter();
128
+ $this->activateMailChimpService();
129
+ }
130
+
131
+ /**
132
+ * Load the required dependencies for this plugin.
133
+ *
134
+ * Include the following files that make up the plugin:
135
+ *
136
+ * - MailChimp_Woocommerce_Loader. Orchestrates the hooks of the plugin.
137
+ * - MailChimp_Woocommerce_i18n. Defines internationalization functionality.
138
+ * - MailChimp_Woocommerce_Admin. Defines all hooks for the admin area.
139
+ * - MailChimp_Woocommerce_Public. Defines all hooks for the public side of the site.
140
+ *
141
+ * Create an instance of the loader which will be used to register the hooks
142
+ * with WordPress.
143
+ *
144
+ * @since 1.0.0
145
+ * @access private
146
+ */
147
+ private function load_dependencies() {
148
+
149
+ $path = plugin_dir_path( dirname( __FILE__ ) );
150
+
151
+ $this->slack();
152
+
153
+ /** The abstract options class.*/
154
+ require_once $path . 'includes/class-mailchimp-woocommerce-options.php';
155
+
156
+ /** The class responsible for orchestrating the actions and filters of the core plugin.*/
157
+ require_once $path . 'includes/class-mailchimp-woocommerce-loader.php';
158
+
159
+ /** The class responsible for defining internationalization functionality of the plugin. */
160
+ require_once $path . 'includes/class-mailchimp-woocommerce-i18n.php';
161
+
162
+ /** The service class.*/
163
+ require_once $path . 'includes/class-mailchimp-woocommerce-service.php';
164
+
165
+ /** The newsletter class. */
166
+ require_once $path . 'includes/class-mailchimp-woocommerce-newsletter.php';
167
+
168
+ /** The class responsible for defining all actions that occur in the admin area.*/
169
+ require_once $path . 'admin/class-mailchimp-woocommerce-admin.php';
170
+
171
+ /** The class responsible for defining all actions that occur in the public-facing side of the site. */
172
+ require_once $path . 'public/class-mailchimp-woocommerce-public.php';
173
+
174
+ /** Require all the MailChimp Assets for the API */
175
+ require_once $path . 'includes/api/class-mailchimp-api.php';
176
+ require_once $path . 'includes/api/class-mailchimp-woocommerce-api.php';
177
+ require_once $path . 'includes/api/class-mailchimp-woocommerce-create-list-submission.php';
178
+ require_once $path . 'includes/api/class-mailchimp-woocommerce-transform-products.php';
179
+ require_once $path . 'includes/api/class-mailchimp-woocommerce-transform-orders.php';
180
+
181
+ /** Require all the mailchimp api asset classes */
182
+ require_once $path . 'includes/api/assets/class-mailchimp-address.php';
183
+ require_once $path . 'includes/api/assets/class-mailchimp-cart.php';
184
+ require_once $path . 'includes/api/assets/class-mailchimp-customer.php';
185
+ require_once $path . 'includes/api/assets/class-mailchimp-line-item.php';
186
+ require_once $path . 'includes/api/assets/class-mailchimp-order.php';
187
+ require_once $path . 'includes/api/assets/class-mailchimp-product.php';
188
+ require_once $path . 'includes/api/assets/class-mailchimp-product-variation.php';
189
+ require_once $path . 'includes/api/assets/class-mailchimp-store.php';
190
+
191
+ /** Require all the api error helpers */
192
+ require_once $path . 'includes/api/errors/class-mailchimp-error.php';
193
+ require_once $path . 'includes/api/errors/class-mailchimp-server-error.php';
194
+
195
+ /** Require the various helper scripts */
196
+ require_once $path . 'includes/api/helpers/class-mailchimp-woocommerce-api-currency-codes.php';
197
+ require_once $path . 'includes/api/helpers/class-mailchimp-woocommerce-api-locales.php';
198
+
199
+ /** Background job sync tools */
200
+
201
+ // make sure the queue exists first since the other files depend on it.
202
+ require_once $path . 'includes/vendor/queue.php';
203
+
204
+ // the abstract bulk sync class
205
+ require_once $path.'includes/processes/class-mailchimp-woocommerce-abstract-sync.php';
206
+
207
+ // bulk data sync
208
+ require_once $path.'includes/processes/class-mailchimp-woocommerce-process-orders.php';
209
+ require_once $path.'includes/processes/class-mailchimp-woocommerce-process-products.php';
210
+
211
+ // individual item sync
212
+ require_once $path.'includes/processes/class-mailchimp-woocommerce-cart-update.php';
213
+ require_once $path.'includes/processes/class-mailchimp-woocommerce-single-order.php';
214
+ require_once $path.'includes/processes/class-mailchimp-woocommerce-single-product.php';
215
+
216
+ // fire up the loader
217
+ $this->loader = new MailChimp_Woocommerce_Loader();
218
+ }
219
+
220
+ /**
221
+ *
222
+ */
223
+ private function slack()
224
+ {
225
+ $path = plugin_dir_path( dirname( __FILE__ ) );
226
+
227
+ require_once $path.'includes/slack/Contracts/Http/Interactor.php';
228
+ require_once $path.'includes/slack/Contracts/Http/Response.php';
229
+ require_once $path.'includes/slack/Contracts/Http/ResponseFactory.php';
230
+
231
+ require_once $path.'includes/slack/Core/Commander.php';
232
+
233
+ require_once $path.'includes/slack/Http/CurlInteractor.php';
234
+ require_once $path.'includes/slack/Http/SlackResponse.php';
235
+ require_once $path.'includes/slack/Http/SlackResponseFactory.php';
236
+
237
+ require_once $path.'includes/slack/Logger.php';
238
+ }
239
+
240
+ /**
241
+ * Define the locale for this plugin for internationalization.
242
+ *
243
+ * Uses the MailChimp_Woocommerce_i18n class in order to set the domain and to register the hook
244
+ * with WordPress.
245
+ *
246
+ * @since 1.0.0
247
+ * @access private
248
+ */
249
+ private function set_locale() {
250
+
251
+ $plugin_i18n = new MailChimp_Woocommerce_i18n();
252
+
253
+ $this->loader->add_action( 'plugins_loaded', $plugin_i18n, 'load_plugin_textdomain' );
254
+ }
255
+
256
+ /**
257
+ * Register all of the hooks related to the admin area functionality
258
+ * of the plugin.
259
+ *
260
+ * @since 1.0.0
261
+ * @access private
262
+ */
263
+ private function define_admin_hooks() {
264
+
265
+ $plugin_admin = new MailChimp_Woocommerce_Admin( $this->get_plugin_name(), $this->get_version() );
266
+
267
+ $this->loader->add_action( 'admin_enqueue_scripts', $plugin_admin, 'enqueue_styles' );
268
+ $this->loader->add_action( 'admin_enqueue_scripts', $plugin_admin, 'enqueue_scripts' );
269
+
270
+ // Add menu item
271
+ $this->loader->add_action( 'admin_menu', $plugin_admin, 'add_plugin_admin_menu' );
272
+
273
+ // Add Settings link to the plugin
274
+ $plugin_basename = plugin_basename( plugin_dir_path( __DIR__ ) . $this->plugin_name . '.php' );
275
+ $this->loader->add_filter( 'plugin_action_links_' . $plugin_basename, $plugin_admin, 'add_action_links' );
276
+
277
+ // make sure we're listening for the admin init
278
+ $this->loader->add_action('admin_init', $plugin_admin, 'options_update');
279
+
280
+ // put the menu on the admin top bar.
281
+ //$this->loader->add_action('admin_bar_menu', $plugin_admin, 'admin_bar', 100);
282
+ }
283
+
284
+ /**
285
+ * Register all of the hooks related to the public-facing functionality
286
+ * of the plugin.
287
+ *
288
+ * @since 1.0.0
289
+ * @access private
290
+ */
291
+ private function define_public_hooks() {
292
+
293
+ $plugin_public = new MailChimp_Woocommerce_Public( $this->get_plugin_name(), $this->get_version() );
294
+
295
+ $this->loader->add_action( 'wp_enqueue_scripts', $plugin_public, 'enqueue_styles' );
296
+ $this->loader->add_action( 'wp_enqueue_scripts', $plugin_public, 'enqueue_scripts' );
297
+ }
298
+
299
+ /**
300
+ * Handle the newsletter actions here.
301
+ */
302
+ private function activateMailChimpNewsletter()
303
+ {
304
+ $service = new MailChimp_Newsletter();
305
+
306
+ if ($service->isConfigured()) {
307
+
308
+ $service->setEnvironment($this->environment);
309
+ $service->setVersion($this->version);
310
+
311
+ $this->loader->add_action('woocommerce_after_checkout_billing_form', $service, 'applyNewsletterField', 5);
312
+ $this->loader->add_action('woocommerce_ppe_checkout_order_review', $service, 'applyNewsletterField', 5);
313
+ $this->loader->add_action('woocommerce_register_form', $service, 'applyNewsletterField', 5);
314
+
315
+ $this->loader->add_action('woocommerce_checkout_order_processed', $service, 'processNewsletterField', 5, 2);
316
+ $this->loader->add_action('woocommerce_ppe_do_payaction', $service, 'processPayPalNewsletterField', 5, 1);
317
+ $this->loader->add_action('woocommerce_register_post', $service, 'processRegistrationForm', 5, 3);
318
+ }
319
+ }
320
+
321
+ /**
322
+ * Handle all the service hooks here.
323
+ */
324
+ private function activateMailChimpService()
325
+ {
326
+ $service = new MailChimp_Service();
327
+
328
+ if ($service->isConfigured()) {
329
+
330
+ $service->setEnvironment($this->environment);
331
+ $service->setVersion($this->version);
332
+
333
+ // core hook setup
334
+ $this->loader->add_action('admin_init', $service, 'adminReady');
335
+ $this->loader->add_action('woocommerce_init', $service, 'wooIsRunning');
336
+
337
+ // for the data sync we need to configure basic auth.
338
+ $this->loader->add_filter('http_request_args', $service, 'addHttpRequestArgs', 10, 2);
339
+
340
+ // campaign tracking
341
+ $this->loader->add_action( 'init', $service, 'handleCampaignTracking' );
342
+
343
+ // order hooks
344
+ $this->loader->add_action('woocommerce_api_create_order', $service, 'handleOrderStatusChanged');
345
+ $this->loader->add_action('woocommerce_thankyou', $service, 'handleOrderStatusChanged');
346
+ $this->loader->add_action('woocommerce_order_status_changed', $service, 'handleOrderStatusChanged');
347
+
348
+ // cart hooks
349
+ $this->loader->add_action('woocommerce_cart_updated', $service, 'handleCartUpdated');
350
+ $this->loader->add_action('woocommerce_add_to_cart', $service, 'handleCartUpdated');
351
+ $this->loader->add_action('woocommerce_cart_item_removed', $service, 'handleCartUpdated');
352
+
353
+ // save post hook for products
354
+ $this->loader->add_action('save_post', $service, 'handlePostSaved', 10, 3);
355
+ }
356
+ }
357
+
358
+ /**
359
+ * Run the loader to execute all of the hooks with WordPress.
360
+ *
361
+ * @since 1.0.0
362
+ */
363
+ public function run() {
364
+ $this->loader->run();
365
+ }
366
+
367
+ /**
368
+ * The name of the plugin used to uniquely identify it within the context of
369
+ * WordPress and to define internationalization functionality.
370
+ *
371
+ * @since 1.0.1
372
+ * @return string The name of the plugin.
373
+ */
374
+ public function get_plugin_name() {
375
+ return $this->plugin_name;
376
+ }
377
+
378
+ /**
379
+ * The reference to the class that orchestrates the hooks with the plugin.
380
+ *
381
+ * @since 1.0.1
382
+ * @return MailChimp_Woocommerce_Loader Orchestrates the hooks of the plugin.
383
+ */
384
+ public function get_loader() {
385
+ return $this->loader;
386
+ }
387
+
388
+ /**
389
+ * Retrieve the version number of the plugin.
390
+ *
391
+ * @since 1.0.1
392
+ * @return string The version number of the plugin.
393
+ */
394
+ public function get_version() {
395
+ return $this->version;
396
+ }
397
+
398
+ }
includes/index.php ADDED
@@ -0,0 +1 @@
 
1
+ <?php // Silence is golden
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 ADDED
@@ -0,0 +1,323 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Created by Vextras.
5
+ *
6
+ * Name: Ryan Hungate
7
+ * Email: ryan@mailchimp.com
8
+ * Date: 7/14/16
9
+ * Time: 11:54 AM
10
+ */
11
+ abstract class MailChimp_WooCommerce_Abtstract_Sync extends WP_Job
12
+ {
13
+ /**
14
+ * @var MailChimp_WooCommerce_Api
15
+ */
16
+ private $api;
17
+
18
+ /**
19
+ * @var MailChimpApi
20
+ */
21
+ private $mc;
22
+
23
+ /**
24
+ * @var string
25
+ */
26
+ private $plugin_name = 'mailchimp-woocommerce';
27
+
28
+ /**
29
+ * @var string
30
+ */
31
+ protected $store_id = '';
32
+
33
+ /**
34
+ * @return mixed
35
+ */
36
+ abstract public function getResourceType();
37
+
38
+ /**
39
+ * @param $item
40
+ * @return mixed
41
+ */
42
+ abstract protected function iterate($item);
43
+
44
+ /**
45
+ * @return mixed
46
+ */
47
+ abstract protected function complete();
48
+
49
+ /**
50
+ * @return mixed
51
+ */
52
+ public function go()
53
+ {
54
+ return $this->handle();
55
+ }
56
+
57
+ /**
58
+ * @return string
59
+ */
60
+ public function getStoreID()
61
+ {
62
+ return md5(get_option('siteurl'));
63
+ }
64
+
65
+ /**
66
+ * Task
67
+ *
68
+ * Override this method to perform any actions required on each
69
+ * queue item. Return the modified item for further processing
70
+ * in the next pass through. Or, return false to remove the
71
+ * item from the queue.
72
+ *
73
+ * @param mixed $item Queue item to iterate over
74
+ *
75
+ * @return mixed
76
+ */
77
+ public function handle() {
78
+
79
+ if (!($page = $this->getResources()) || !($this->store_id = $this->getStoreID())) {
80
+ return false;
81
+ }
82
+
83
+ $this->setResourcePagePointer($page->page + 1, $this->getResourceType());
84
+
85
+ // if we've got a 0 count, that means we're done.
86
+ if ($page->count <= 0) {
87
+
88
+ // reset the resource page back to 1
89
+ $this->resourceComplete($this->getResourceType());
90
+
91
+ // call the completed event to process further
92
+ $this->complete();
93
+
94
+ return false;
95
+ }
96
+
97
+ // iterate through the items and send each one through the pipeline based on this class.
98
+ foreach ($page->items as $resource) {
99
+ $this->iterate($resource);
100
+ }
101
+
102
+ // this will paginate through all records for the resource type until they return no records.
103
+ wp_queue(new static());
104
+
105
+ return false;
106
+ }
107
+
108
+ /**
109
+ * @return $this
110
+ */
111
+ public function flagStartSync()
112
+ {
113
+ mailchimp_log('sync.started', "Starting Sync :: ".date('D, M j, Y g:i A'));
114
+
115
+ // this is the last thing we're doing so it's complete as of now.
116
+ $this->setData('sync.syncing', true);
117
+ $this->setData('sync.started_at', time());
118
+
119
+ return $this;
120
+ }
121
+
122
+ /**
123
+ * @return $this
124
+ */
125
+ public function flagStopSync()
126
+ {
127
+ // this is the last thing we're doing so it's complete as of now.
128
+ $this->setData('sync.syncing', false);
129
+ $this->setData('sync.completed_at', time());
130
+
131
+ mailchimp_log('sync.completed', "Finished Sync :: ".date('D, M j, Y g:i A'));
132
+
133
+ return $this;
134
+ }
135
+
136
+ /**
137
+ * @return bool|object|stdClass
138
+ */
139
+ public function getResources()
140
+ {
141
+ $current_page = $this->getResourcePagePointer($this->getResourceType());
142
+
143
+ if ($current_page === 'complete') {
144
+ return false;
145
+ }
146
+
147
+ return $this->api()->paginate($this->getResourceType(), $current_page, 10);
148
+ }
149
+
150
+ /**
151
+ * @param null|string $resource
152
+ * @return $this
153
+ */
154
+ protected function resetResourcePagePointer($resource = null)
155
+ {
156
+ if (empty($resource)) $resource = $this->getResourceType();
157
+
158
+ $this->setData('sync.'.$resource.'.current_page', 1);
159
+
160
+ return $this;
161
+ }
162
+
163
+ /**
164
+ * @param null|string $resource
165
+ * @return null
166
+ */
167
+ protected function getResourcePagePointer($resource = null)
168
+ {
169
+ if (empty($resource)) $resource = $this->getResourceType();
170
+
171
+ return $this->getData('sync.'.$resource.'.current_page', 1);
172
+ }
173
+
174
+ /**
175
+ * @param $page
176
+ * @param null $resource
177
+ * @return MailChimp_WooCommerce_Abtstract_Sync
178
+ */
179
+ protected function setResourcePagePointer($page, $resource = null)
180
+ {
181
+ if (empty($resource)) $resource = $this->getResourceType();
182
+
183
+ return $this->setData('sync.'.$resource.'.current_page', $page);
184
+ }
185
+
186
+ /**
187
+ * @param null|string $resource
188
+ * @return $this
189
+ */
190
+ protected function resourceComplete($resource = null)
191
+ {
192
+ if (empty($resource)) $resource = $this->getResourceType();
193
+
194
+ $this->setData('sync.'.$resource.'.current_page', 'complete');
195
+
196
+ return $this;
197
+ }
198
+
199
+ /**
200
+ * @return null
201
+ */
202
+ protected function setResourceCompleteTime($resource = null)
203
+ {
204
+ if (empty($resource)) $resource = $this->getResourceType();
205
+
206
+ return $this->setData('sync.'.$resource.'.completed_at', time());
207
+ }
208
+
209
+ /**
210
+ * @param null $resource
211
+ * @return bool|DateTime
212
+ */
213
+ protected function getResourceCompleteTime($resource = null)
214
+ {
215
+ if (empty($resource)) $resource = $this->getResourceType();
216
+
217
+ $time = $this->getData('sync.'.$resource.'.completed_at', false);
218
+
219
+ if ($time > 0) {
220
+ return new \DateTime($time);
221
+ }
222
+
223
+ return false;
224
+ }
225
+
226
+ /**
227
+ * @param $key
228
+ * @param null $default
229
+ * @return null
230
+ */
231
+ public function getOption($key, $default = null)
232
+ {
233
+ $options = $this->getOptions();
234
+ if (isset($options[$key])) {
235
+ return $options[$key];
236
+ }
237
+ return $default;
238
+ }
239
+
240
+ /**
241
+ * @param $key
242
+ * @param $value
243
+ * @return $this
244
+ */
245
+ public function setOption($key, $value)
246
+ {
247
+ $options = $this->getOptions();
248
+ $options[$key] = $value;
249
+ update_option($this->plugin_name, $options);
250
+ return $this;
251
+ }
252
+
253
+ /**
254
+ * @param $key
255
+ * @param bool $default
256
+ * @return bool
257
+ */
258
+ public function hasOption($key, $default = false)
259
+ {
260
+ return (bool) $this->getOption($key, $default);
261
+ }
262
+
263
+ /**
264
+ * @return array
265
+ */
266
+ public function getOptions()
267
+ {
268
+ $options = get_option($this->plugin_name);
269
+ return is_array($options) ? $options : array();
270
+ }
271
+
272
+ /**
273
+ * @param $key
274
+ * @param $value
275
+ * @return $this
276
+ */
277
+ public function setData($key, $value)
278
+ {
279
+ update_option($this->plugin_name.'-'.$key, $value);
280
+ return $this;
281
+ }
282
+
283
+ /**
284
+ * @param $key
285
+ * @param null $default
286
+ * @return mixed|void
287
+ */
288
+ public function getData($key, $default = null)
289
+ {
290
+ return get_option($this->plugin_name.'-'.$key, $default);
291
+ }
292
+
293
+ /**
294
+ * @param $key
295
+ * @return bool
296
+ */
297
+ public function removeData($key)
298
+ {
299
+ return delete_option($this->plugin_name.'-'.$key);
300
+ }
301
+
302
+ /**
303
+ * @return MailChimp_WooCommerce_Api
304
+ */
305
+ protected function api()
306
+ {
307
+ if (empty($this->api)) {
308
+ $this->api = new MailChimp_WooCommerce_Api();
309
+ }
310
+ return $this->api;
311
+ }
312
+
313
+ /**
314
+ * @return MailChimpApi
315
+ */
316
+ protected function mailchimp()
317
+ {
318
+ if (empty($this->mc)) {
319
+ $this->mc = new MailChimpApi($this->getOption('mailchimp_api_key'));
320
+ }
321
+ return $this->mc;
322
+ }
323
+ }
includes/processes/class-mailchimp-woocommerce-cart-update.php ADDED
@@ -0,0 +1,176 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Created by Vextras.
5
+ *
6
+ * Name: Ryan Hungate
7
+ * Email: ryan@mailchimp.com
8
+ * Date: 7/15/16
9
+ * Time: 11:42 AM
10
+ */
11
+ class MailChimp_WooCommerce_Cart_Update extends WP_Job
12
+ {
13
+ public $unique_id;
14
+ public $email;
15
+ public $previous_email;
16
+ public $campaign_id;
17
+ public $cart_data;
18
+ public $ip_address;
19
+
20
+ /**
21
+ * MailChimp_WooCommerce_Cart_Update constructor.
22
+ * @param null $uid
23
+ * @param null $email
24
+ * @param null $campaign_id
25
+ * @param array $cart_data
26
+ */
27
+ public function __construct($uid = null, $email = null, $campaign_id = null, array $cart_data = array())
28
+ {
29
+ if ($uid) {
30
+ $this->unique_id = $uid;
31
+ }
32
+ if ($email) {
33
+ $this->email = $email;
34
+ }
35
+ if (!empty($cart_data)) {
36
+ $this->cart_data = json_encode($cart_data);
37
+ }
38
+
39
+ if ($campaign_id) {
40
+ $this->campaign_id = $campaign_id;
41
+ }
42
+
43
+ $this->ip_address = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : null;
44
+ }
45
+
46
+ /**
47
+ * @return bool
48
+ */
49
+ public function handle()
50
+ {
51
+ if (($result = $this->process())) {
52
+ mailchimp_log('ac.success', 'Added', array('api_response' => $result->toArray()));
53
+ }
54
+
55
+ return false;
56
+ }
57
+
58
+ /**
59
+ * @return bool|MailChimp_Cart
60
+ */
61
+ public function process()
62
+ {
63
+ try {
64
+ $options = get_option('mailchimp-woocommerce', array());
65
+ $store_id = mailchimp_get_store_id();
66
+
67
+ if (!empty($store_id) && is_array($options) && isset($options['mailchimp_api_key'])) {
68
+
69
+ $this->cart_data = json_decode($this->cart_data, true);
70
+
71
+ $api = new MailChimpApi($options['mailchimp_api_key']);
72
+
73
+ // delete it and the add it back.
74
+ $api->deleteCartByID($store_id, $this->unique_id);
75
+
76
+ // if they emptied the cart ignore it.
77
+ if (!is_array($this->cart_data) || empty($this->cart_data)) {
78
+ return false;
79
+ }
80
+
81
+ $customer = new MailChimp_Customer();
82
+ $customer->setId($this->unique_id);
83
+ $customer->setEmailAddress($this->email);
84
+ $customer->setOptInStatus(false);
85
+
86
+ $cart = new MailChimp_Cart();
87
+ $cart->setId($this->unique_id);
88
+ $cart->setCampaignID($this->campaign_id);
89
+ $cart->setCheckoutUrl(wc_get_checkout_url());
90
+ $cart->setCurrencyCode(isset($options['store_currency_code']) ? $options['store_currency_code'] : 'USD');
91
+
92
+ $cart->setCustomer($customer);
93
+
94
+ $order_total = 0;
95
+ $products = array();
96
+
97
+ foreach ($this->cart_data as $hash => $item) {
98
+ try {
99
+ $line = $this->transformLineItem($hash, $item);
100
+ $cart->addItem($line);
101
+ $order_total += ($item['quantity'] * $line->getPrice());
102
+ $products[] = $line;
103
+ } catch (\Exception $e) {}
104
+ }
105
+
106
+ if (empty($products)) {
107
+ return false;
108
+ }
109
+
110
+ $cart->setOrderTotal($order_total);
111
+
112
+ try {
113
+ mailchimp_log('abandoned_cart.submitting', "email: {$customer->getEmailAddress()}");
114
+
115
+ // if the post is successful we're all good.
116
+ $api->addCart($store_id, $cart, false);
117
+
118
+ mailchimp_log('abandoned_cart.success', "email: {$customer->getEmailAddress()}");
119
+
120
+ } catch (\Exception $e) {
121
+
122
+ mailchimp_log('abandoned_cart.error', "email: {$customer->getEmailAddress()}");
123
+
124
+ // if we have an error it's most likely due to a product not being found.
125
+ // let's loop through each item, verify that we have the product or not.
126
+ // if not, we will add it.
127
+ foreach ($products as $item) {
128
+ /** @var MailChimp_LineItem $item */
129
+ $transformer = new MailChimp_WooCommerce_Single_Product($item->getProductID());
130
+ if (!$transformer->api()->getStoreProduct($store_id, $item->getProductId())) {
131
+ $transformer->handle();
132
+ }
133
+ }
134
+
135
+ mailchimp_log('abandoned_cart.submitting', "email: {$customer->getEmailAddress()}");
136
+
137
+ // if the post is successful we're all good.
138
+ $api->addCart($store_id, $cart, false);
139
+
140
+ mailchimp_log('abandoned_cart.success', "email: {$customer->getEmailAddress()}");
141
+ }
142
+ }
143
+
144
+ } catch (\Exception $e) {
145
+ update_option('mailchimp-woocommerce-cart-error', $e->getMessage());
146
+ mailchimp_log('abandoned_cart.error', "{$e->getMessage()} on {$e->getLine()} in {$e->getFile()}");
147
+ }
148
+
149
+ return false;
150
+ }
151
+
152
+ /**
153
+ * @param string $hash
154
+ * @param $item
155
+ * @return MailChimp_LineItem
156
+ */
157
+ protected function transformLineItem($hash, $item)
158
+ {
159
+ $product = new WC_Product($item['product_id']);
160
+
161
+ $line = new MailChimp_LineItem();
162
+ $line->setId($hash);
163
+ $line->setProductId($item['product_id']);
164
+
165
+ if (isset($item['variation_id']) && $item['variation_id'] > 0) {
166
+ $line->setProductVariantId($item['variation_id']);
167
+ } else {
168
+ $line->setProductVariantId($item['product_id']);
169
+ }
170
+
171
+ $line->setQuantity($item['quantity']);
172
+ $line->setPrice($product->get_price());
173
+
174
+ return $line;
175
+ }
176
+ }
includes/processes/class-mailchimp-woocommerce-process-orders.php ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Created by Vextras.
5
+ *
6
+ * Name: Ryan Hungate
7
+ * Email: ryan@mailchimp.com
8
+ * Date: 7/14/16
9
+ * Time: 10:57 AM
10
+ */
11
+ class MailChimp_WooCommerce_Process_Orders extends MailChimp_WooCommerce_Abtstract_Sync
12
+ {
13
+ /**
14
+ * @var string
15
+ */
16
+ protected $action = 'mailchimp_woocommerce_process_orders';
17
+ public $items = array();
18
+
19
+ /**
20
+ * @return string
21
+ */
22
+ public function getResourceType()
23
+ {
24
+ return 'orders';
25
+ }
26
+
27
+ /**
28
+ * @param MailChimp_Order $item
29
+ *
30
+ * @return mixed
31
+ */
32
+ protected function iterate($item)
33
+ {
34
+ if ($item instanceof MailChimp_Order) {
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
+ mailchimp_log('sync.orders.success', $log);
56
+
57
+ $this->items[] = array('response' => $response, 'item' => $item);
58
+
59
+ return $response;
60
+
61
+ } catch (MailChimp_Error $e) {
62
+ mailchimp_log('sync.orders.error', "$call :: MailChimp_Error :: {$e->getMessage()}");
63
+ } catch (MailChimp_ServerError $e) {
64
+ mailchimp_log('sync.orders.error', "$call :: MailChimp_ServerError :: {$e->getMessage()}");
65
+ } catch (Exception $e) {
66
+ mailchimp_log('sync.orders.error', "$call :: Uncaught Exception :: {$e->getMessage()}");
67
+ }
68
+ }
69
+
70
+ return false;
71
+ }
72
+
73
+ /**
74
+ * After the resources have been loaded and pushed
75
+ */
76
+ protected function complete()
77
+ {
78
+ mailchimp_log('sync.orders.completed', 'Done with the order sync.');
79
+
80
+ // add a timestamp for the orders sync completion
81
+ $this->setResourceCompleteTime();
82
+
83
+ // this is the last thing we're doing so it's complete as of now.
84
+ $this->flagStopSync();
85
+ }
86
+ }
includes/processes/class-mailchimp-woocommerce-process-products.php ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Created by Vextras.
5
+ *
6
+ * Name: Ryan Hungate
7
+ * Email: ryan@mailchimp.com
8
+ * Date: 7/14/16
9
+ * Time: 10:57 AM
10
+ */
11
+ class MailChimp_WooCommerce_Process_Products extends MailChimp_WooCommerce_Abtstract_Sync
12
+ {
13
+ /**
14
+ * @var string
15
+ */
16
+ protected $action = 'mailchimp_woocommerce_process_products';
17
+
18
+ /**
19
+ * @return string
20
+ */
21
+ public function getResourceType()
22
+ {
23
+ return 'products';
24
+ }
25
+
26
+ /**
27
+ * @param MailChimp_Product $item
28
+ *
29
+ * @return mixed
30
+ */
31
+ protected function iterate($item) {
32
+
33
+ if ($item instanceof MailChimp_Product) {
34
+
35
+ // need to run the delete option on this before submitting because the API does not support PATCH yet.
36
+ $this->mailchimp()->deleteStoreProduct($this->store_id, $item->getId());
37
+
38
+ // add the product.
39
+ try {
40
+ mailchimp_log('sync.products.submitting', "addStoreProduct :: #{$item->getId()}");
41
+
42
+ // make the call
43
+ $response = $this->mailchimp()->addStoreProduct($this->store_id, $item);
44
+
45
+ mailchimp_log('sync.products.success', "addStoreProduct :: #{$item->getId()}");
46
+
47
+ return $response;
48
+
49
+ } catch (MailChimp_Error $e) {
50
+ mailchimp_log('sync.products.error', "addStoreProduct :: MailChimp_Error :: {$e->getMessage()}");
51
+ } catch (MailChimp_ServerError $e) {
52
+ mailchimp_log('sync.products.error', "addStoreProduct :: MailChimp_ServerError :: {$e->getMessage()}");
53
+ } catch (Exception $e) {
54
+ mailchimp_log('sync.products.error', "addStoreProduct :: Uncaught Exception :: {$e->getMessage()}");
55
+ }
56
+ }
57
+
58
+ return false;
59
+ }
60
+
61
+ /**
62
+ * Called after all the products have been iterated and processed into MailChimp
63
+ */
64
+ protected function complete()
65
+ {
66
+ mailchimp_log('sync.products.completed', 'Done with the product sync :: queuing up the orders next!');
67
+
68
+ // add a timestamp for the product sync completion
69
+ $this->setResourceCompleteTime();
70
+
71
+ // since the products are all good, let's sync up the orders now.
72
+ wp_queue(new MailChimp_WooCommerce_Process_Orders());
73
+ }
74
+ }
includes/processes/class-mailchimp-woocommerce-single-order.php ADDED
@@ -0,0 +1,144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Created by Vextras.
5
+ *
6
+ * Name: Ryan Hungate
7
+ * Email: ryan@mailchimp.com
8
+ * Date: 7/15/16
9
+ * Time: 11:42 AM
10
+ */
11
+ class MailChimp_WooCommerce_Single_Order extends WP_Job
12
+ {
13
+ public $order_id;
14
+ public $cart_session_id;
15
+ public $campaign_id;
16
+
17
+ /**
18
+ * MailChimp_WooCommerce_Single_Order constructor.
19
+ * @param null $order_id
20
+ * @param null $cart_session_id
21
+ * @param null $campaign_id
22
+ */
23
+ public function __construct($order_id = null, $cart_session_id = null, $campaign_id = null)
24
+ {
25
+ if (!empty($order_id)) {
26
+ $this->order_id = $order_id;
27
+ }
28
+ if (!empty($cart_session_id)) {
29
+ $this->cart_session_id = $cart_session_id;
30
+ }
31
+ if (!empty($campaign_id)) {
32
+ $this->campaign_id = $campaign_id;
33
+ }
34
+ }
35
+
36
+ /**
37
+ * @return bool
38
+ */
39
+ public function handle()
40
+ {
41
+ $this->process();
42
+
43
+ return false;
44
+ }
45
+
46
+ public function process()
47
+ {
48
+ $options = get_option('mailchimp-woocommerce', array());
49
+ $store_id = mailchimp_get_store_id();
50
+
51
+ // only if we have the right parameters to do the work
52
+ if (!empty($store_id) && is_array($options) && isset($options['mailchimp_api_key'])) {
53
+
54
+ $job = new MailChimp_WooCommerce_Transform_Orders();
55
+ $api = new MailChimpApi($options['mailchimp_api_key']);
56
+
57
+ // set the campaign ID
58
+ $job->campaign_id = $this->campaign_id;
59
+
60
+ $call = $api->getStoreOrder($store_id, $this->order_id) ? 'updateStoreOrder' : 'addStoreOrder';
61
+
62
+ // if we already pushed this order into the system, we need to unset it now just in case there
63
+ // was another campaign that had been sent and this was only an order update.
64
+ if ($call === 'updateStoreOrder') {
65
+ $job->campaign_id = null;
66
+ }
67
+
68
+ // will either add or update the order
69
+ try {
70
+
71
+ // transform the order
72
+ $order = $job->transform(get_post($this->order_id));
73
+
74
+ // will be the same as the customer id. an md5'd hash of a lowercased email.
75
+ $this->cart_session_id = $order->getCustomer()->getId();
76
+
77
+ $log = "$call :: #{$order->getId()} :: email: {$order->getCustomer()->getEmailAddress()}";
78
+
79
+ mailchimp_log('order_submit.submitting', $log);
80
+
81
+ // update or create
82
+ $api_response = $api->$call($store_id, $order, false);
83
+
84
+ if (!empty($job->campaign_id)) {
85
+ $log .= ' :: campaign id '.$job->campaign_id;
86
+ }
87
+
88
+ mailchimp_log('order_submit.success', $log);
89
+
90
+ // if we're adding a new order and the session id is here, we need to delete the AC cart record.
91
+ if (!empty($this->cart_session_id)) {
92
+ $api->deleteCartByID($store_id, $this->cart_session_id);
93
+ }
94
+
95
+ return $api_response;
96
+
97
+ } catch (\Exception $e) {
98
+
99
+ mailchimp_log('order_submit.tracing_error', $message = strtolower($e->getMessage()));
100
+
101
+ if (!isset($order)) {
102
+ // transform the order
103
+ $order = $job->transform(get_post($this->order_id));
104
+ $this->cart_session_id = $order->getCustomer()->getId();
105
+ }
106
+
107
+ // this can happen when a customer changes their email.
108
+ if (isset($order) && strpos($message, 'not be changed')) {
109
+
110
+ try {
111
+
112
+ mailchimp_log('order_submit.deleting_customer', "#{$order->getId()} :: email: {$order->getCustomer()->getEmailAddress()}");
113
+
114
+ // delete the customer before adding it again.
115
+ $api->deleteCustomer($store_id, $order->getCustomer()->getId());
116
+
117
+ // update or create
118
+ $api_response = $api->$call($store_id, $order, false);
119
+
120
+ $log = "Deleted Customer :: $call :: #{$order->getId()} :: email: {$order->getCustomer()->getEmailAddress()}";
121
+
122
+ if (!empty($job->campaign_id)) {
123
+ $log .= ' :: campaign id '.$job->campaign_id;
124
+ }
125
+
126
+ mailchimp_log('order_submit.success', $log);
127
+
128
+ // if we're adding a new order and the session id is here, we need to delete the AC cart record.
129
+ if (!empty($this->cart_session_id)) {
130
+ $api->deleteCartByID($store_id, $this->cart_session_id);
131
+ }
132
+
133
+ return $api_response;
134
+
135
+ } catch (\Exception $e) {
136
+ mailchimp_log('order_submit.error', 'deleting-customer-re-add :: #'.$this->order_id.' :: '.$e->getMessage());
137
+ }
138
+ }
139
+ }
140
+ }
141
+
142
+ return false;
143
+ }
144
+ }
includes/processes/class-mailchimp-woocommerce-single-product.php ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Created by Vextras.
5
+ *
6
+ * Name: Ryan Hungate
7
+ * Email: ryan@mailchimp.com
8
+ * Date: 7/15/16
9
+ * Time: 11:42 AM
10
+ */
11
+ class MailChimp_WooCommerce_Single_Product extends WP_Job
12
+ {
13
+ public $product_id;
14
+ protected $store_id;
15
+ protected $api;
16
+ protected $service;
17
+
18
+ /**
19
+ * MailChimp_WooCommerce_Single_Order constructor.
20
+ * @param null|int $product_id
21
+ */
22
+ public function __construct($product_id = null)
23
+ {
24
+ if (!empty($product_id)) {
25
+ $this->product_id = $product_id instanceof WP_Post ? $product_id->ID : $product_id;
26
+ }
27
+ }
28
+
29
+ /**
30
+ * @return bool
31
+ */
32
+ public function handle()
33
+ {
34
+ $this->process();
35
+
36
+ return false;
37
+ }
38
+
39
+ /**
40
+ * @return MailChimp_Product
41
+ * @throws Exception
42
+ */
43
+ public function process()
44
+ {
45
+ if ($this->api()->getStoreProduct($this->store_id, $this->product_id)) {
46
+ $this->api()->deleteStoreProduct($this->store_id, $this->product_id);
47
+ }
48
+
49
+ try {
50
+
51
+ $product = $this->transformer()->transform(get_post($this->product_id));
52
+
53
+ mailchimp_log('product_submit.submitting', "addStoreProduct :: #{$product->getId()}");
54
+
55
+ $this->api()->addStoreProduct($this->store_id, $product, false);
56
+
57
+ mailchimp_log('product_submit.success', "addStoreProduct :: #{$product->getId()}");
58
+
59
+ update_option('mailchimp-woocommerce-last_product_updated', $product->getId());
60
+
61
+ return $product;
62
+
63
+ } catch (MailChimp_Error $e) {
64
+ mailchimp_log('product_submit.error', "addStoreProduct :: MailChimp_Error :: {$e->getMessage()}");
65
+ } catch (MailChimp_ServerError $e) {
66
+ mailchimp_log('product_submit.error', "addStoreProduct :: MailChimp_ServerError :: {$e->getMessage()}");
67
+ } catch (Exception $e) {
68
+ mailchimp_log('product_submit.error', "addStoreProduct :: Uncaught Exception :: {$e->getMessage()}");
69
+ }
70
+
71
+ return false;
72
+ }
73
+
74
+ /**
75
+ * @return MailChimpApi
76
+ */
77
+ public function api()
78
+ {
79
+ if (is_null($this->api)) {
80
+
81
+ $this->store_id = mailchimp_get_store_id();
82
+ $options = get_option('mailchimp-woocommerce', array());
83
+
84
+ if (!empty($this->store_id) && is_array($options) && isset($options['mailchimp_api_key'])) {
85
+ return $this->api = new MailChimpApi($options['mailchimp_api_key']);
86
+ }
87
+
88
+ throw new \RuntimeException('The MailChimp API is not currently configured!');
89
+ }
90
+
91
+ return $this->api;
92
+ }
93
+
94
+ /**
95
+ * @return MailChimp_WooCommerce_Transform_Products
96
+ */
97
+ public function transformer()
98
+ {
99
+ if (is_null($this->service)) {
100
+ return $this->service = new MailChimp_WooCommerce_Transform_Products();
101
+ }
102
+
103
+ return $this->service;
104
+ }
105
+ }
includes/slack/Contracts/Http/Interactor.php ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php namespace Frlnc\Slack\Contracts\Http;
2
+
3
+ interface Interactor {
4
+
5
+ /**
6
+ * Send a get request to a URL.
7
+ *
8
+ * @param string $url
9
+ * @param array $parameters
10
+ * @param array $headers
11
+ * @return \Frlnc\Slack\Contracts\Http\Response
12
+ */
13
+ public function get($url, array $parameters = [], array $headers = []);
14
+
15
+ /**
16
+ * Send a post request to a URL.
17
+ *
18
+ * @param string $url
19
+ * @param array $urlParameters
20
+ * @param array $postParameters
21
+ * @param array $headers
22
+ * @return \Frlnc\Slack\Contracts\Http\Response
23
+ */
24
+ public function post($url, array $urlParameters = [], array $postParameters = [], array $headers = []);
25
+
26
+ /**
27
+ * Sets the response factory to use.
28
+ *
29
+ * @param \Frlnc\Slack\Contracts\Http\ResponseFactory $factory
30
+ * @return void
31
+ */
32
+ public function setResponseFactory(ResponseFactory $factory);
33
+
34
+ }
includes/slack/Contracts/Http/Response.php ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php namespace Frlnc\Slack\Contracts\Http;
2
+
3
+ interface Response {
4
+
5
+ /**
6
+ * Gets the body of the response.
7
+ *
8
+ * @return string
9
+ */
10
+ public function getBody();
11
+
12
+ /**
13
+ * Gets the headers of the response.
14
+ *
15
+ * @return array
16
+ */
17
+ public function getHeaders();
18
+
19
+ /**
20
+ * Gets the status code of the response.
21
+ *
22
+ * @return integer
23
+ */
24
+ public function getStatusCode();
25
+
26
+ }
includes/slack/Contracts/Http/ResponseFactory.php ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php namespace Frlnc\Slack\Contracts\Http;
2
+
3
+ interface ResponseFactory {
4
+
5
+ /**
6
+ * Builds the response.
7
+ *
8
+ * @param string $body
9
+ * @param array $headers
10
+ * @param integer $statusCode
11
+ * @return \Frlnc\Slack\Contracts\Http\Response
12
+ */
13
+ public function build($body, array $headers, $statusCode);
14
+
15
+ }
includes/slack/Core/Commander.php ADDED
@@ -0,0 +1,508 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php namespace Frlnc\Slack\Core;
2
+
3
+ use InvalidArgumentException;
4
+ use Frlnc\Slack\Contracts\Http\Interactor;
5
+
6
+ class Commander {
7
+
8
+ /**
9
+ * The default command headers.
10
+ *
11
+ * @var array
12
+ */
13
+ protected static $defaultHeaders = [];
14
+
15
+ /**
16
+ * The commands.
17
+ *
18
+ * @var array
19
+ */
20
+ protected static $commands = [
21
+ 'api.test' => [
22
+ 'endpoint' => '/api.test',
23
+ 'token' => false
24
+ ],
25
+ 'auth.test' => [
26
+ 'endpoint' => '/auth.test',
27
+ 'token' => true
28
+ ],
29
+ 'channels.archive' => [
30
+ 'token' => true,
31
+ 'endpoint' => '/channels.archive'
32
+ ],
33
+ 'channels.create' => [
34
+ 'token' => true,
35
+ 'endpoint' => '/channels.create'
36
+ ],
37
+ 'channels.history' => [
38
+ 'token' => true,
39
+ 'endpoint' => '/channels.history'
40
+ ],
41
+ 'channels.info' => [
42
+ 'token' => true,
43
+ 'endpoint' => '/channels.info'
44
+ ],
45
+ 'channels.invite' => [
46
+ 'token' => true,
47
+ 'endpoint' => '/channels.invite'
48
+ ],
49
+ 'channels.join' => [
50
+ 'token' => true,
51
+ 'endpoint' => '/channels.join'
52
+ ],
53
+ 'channels.kick' => [
54
+ 'token' => true,
55
+ 'endpoint' => '/channels.kick'
56
+ ],
57
+ 'channels.leave' => [
58
+ 'token' => true,
59
+ 'endpoint' => '/channels.leave'
60
+ ],
61
+ 'channels.list' => [
62
+ 'token' => true,
63
+ 'endpoint' => '/channels.list'
64
+ ],
65
+ 'channels.mark' => [
66
+ 'token' => true,
67
+ 'endpoint' => '/channels.mark'
68
+ ],
69
+ 'channels.rename' => [
70
+ 'token' => true,
71
+ 'endpoint' => '/channels.rename'
72
+ ],
73
+ 'channels.setPurpose' => [
74
+ 'token' => true,
75
+ 'endpoint' => '/channels.setPurpose',
76
+ 'format' => [
77
+ 'purpose'
78
+ ]
79
+ ],
80
+ 'channels.setTopic' => [
81
+ 'token' => true,
82
+ 'endpoint' => '/channels.setTopic',
83
+ 'format' => [
84
+ 'topic'
85
+ ]
86
+ ],
87
+ 'channels.unarchive' => [
88
+ 'token' => true,
89
+ 'endpoint' => '/channels.unarchive'
90
+ ],
91
+ 'chat.delete' => [
92
+ 'token' => true,
93
+ 'endpoint' => '/chat.delete'
94
+ ],
95
+ 'chat.postMessage' => [
96
+ 'token' => true,
97
+ 'endpoint' => '/chat.postMessage',
98
+ 'format' => [
99
+ 'text',
100
+ 'username'
101
+ ]
102
+ ],
103
+ 'chat.update' => [
104
+ 'token' => true,
105
+ 'endpoint' => '/chat.update',
106
+ 'format' => [
107
+ 'text'
108
+ ]
109
+ ],
110
+ 'dnd.endDnd' => [
111
+ 'token' => true,
112
+ 'endpoint' => '/dnd.endDnd'
113
+ ],
114
+ 'dnd.endSnooze' => [
115
+ 'token' => true,
116
+ 'endpoint' => '/dnd.endSnooze'
117
+ ],
118
+ 'dnd.info' => [
119
+ 'token' => true,
120
+ 'endpoint' => '/dnd.info'
121
+ ],
122
+ 'dnd.setSnooze' => [
123
+ 'token' => true,
124
+ 'endpoint' => '/dnd.setSnooze'
125
+ ],
126
+ 'dnd.teamInfo' => [
127
+ 'token' => true,
128
+ 'endpoint' => '/dnd.teamInfo'
129
+ ],
130
+ 'emoji.list' => [
131
+ 'token' => true,
132
+ 'endpoint' => '/emoji.list'
133
+ ],
134
+ 'files.comments.add' => [
135
+ 'token' => true,
136
+ 'endpoint' => '/files.comments.add'
137
+ ],
138
+ 'files.comments.delete' => [
139
+ 'token' => true,
140
+ 'endpoint' => '/files.comments.delete'
141
+ ],
142
+ 'files.comments.edit' => [
143
+ 'token' => true,
144
+ 'endpoint' => '/files.comments.edit'
145
+ ],
146
+ 'files.delete' => [
147
+ 'token' => true,
148
+ 'endpoint' => '/files.delete'
149
+ ],
150
+ 'files.info' => [
151
+ 'token' => true,
152
+ 'endpoint' => '/files.info'
153
+ ],
154
+ 'files.list' => [
155
+ 'token' => true,
156
+ 'endpoint' => '/files.list'
157
+ ],
158
+ 'files.revokePublicURL' => [
159
+ 'token' => true,
160
+ 'endpoint' => '/files.revokePublicURL'
161
+ ],
162
+ 'files.sharedPublcURL' => [
163
+ 'token' => true,
164
+ 'endpoint' => '/files.sharedPublcURL'
165
+ ],
166
+ 'files.upload' => [
167
+ 'token' => true,
168
+ 'endpoint' => '/files.upload',
169
+ 'post' => true,
170
+ 'headers' => [
171
+ 'Content-Type' => 'multipart/form-data'
172
+ ],
173
+ 'format' => [
174
+ 'filename',
175
+ 'title',
176
+ 'initial_comment'
177
+ ]
178
+ ],
179
+ 'groups.archive' => [
180
+ 'token' => true,
181
+ 'endpoint' => '/groups.archive'
182
+ ],
183
+ 'groups.close' => [
184
+ 'token' => true,
185
+ 'endpoint' => '/groups.close'
186
+ ],
187
+ 'groups.create' => [
188
+ 'token' => true,
189
+ 'endpoint' => '/groups.create',
190
+ 'format' => [
191
+ 'name'
192
+ ]
193
+ ],
194
+ 'groups.createChild' => [
195
+ 'token' => true,
196
+ 'endpoint' => '/groups.createChild'
197
+ ],
198
+ 'groups.history' => [
199
+ 'token' => true,
200
+ 'endpoint' => '/groups.history'
201
+ ],
202
+ 'groups.info' => [
203
+ 'token' => true,
204
+ 'endpoint' => '/groups.info'
205
+ ],
206
+ 'groups.invite' => [
207
+ 'token' => true,
208
+ 'endpoint' => '/groups.invite'
209
+ ],
210
+ 'groups.kick' => [
211
+ 'token' => true,
212
+ 'endpoint' => '/groups.kick'
213
+ ],
214
+ 'groups.leave' => [
215
+ 'token' => true,
216
+ 'endpoint' => '/groups.leave'
217
+ ],
218
+ 'groups.list' => [
219
+ 'token' => true,
220
+ 'endpoint' => '/groups.list'
221
+ ],
222
+ 'groups.mark' => [
223
+ 'token' => true,
224
+ 'endpoint' => '/groups.mark'
225
+ ],
226
+ 'groups.open' => [
227
+ 'token' => true,
228
+ 'endpoint' => '/groups.open'
229
+ ],
230
+ 'groups.rename' => [
231
+ 'token' => true,
232
+ 'endpoint' => '/groups.rename'
233
+ ],
234
+ 'groups.setPurpose' => [
235
+ 'token' => true,
236
+ 'endpoint' => '/groups.setPurpose',
237
+ 'format' => [
238
+ 'purpose'
239
+ ]
240
+ ],
241
+ 'groups.setTopic' => [
242
+ 'token' => true,
243
+ 'endpoint' => '/groups.setTopic',
244
+ 'format' => [
245
+ 'topic'
246
+ ]
247
+ ],
248
+ 'groups.unarchive' => [
249
+ 'token' => true,
250
+ 'endpoint' => '/groups.unarchive'
251
+ ],
252
+ 'im.close' => [
253
+ 'token' => true,
254
+ 'endpoint' => '/im.close'
255
+ ],
256
+ 'im.history' => [
257
+ 'token' => true,
258
+ 'endpoint' => '/im.history'
259
+ ],
260
+ 'im.list' => [
261
+ 'token' => true,
262
+ 'endpoint' => '/im.list'
263
+ ],
264
+ 'im.mark' => [
265
+ 'token' => true,
266
+ 'endpoint' => '/im.mark'
267
+ ],
268
+ 'im.open' => [
269
+ 'token' => true,
270
+ 'endpoint' => '/im.open'
271
+ ],
272
+ 'mpim.close' => [
273
+ 'token' => true,
274
+ 'endpoint' => '/mpim.close'
275
+ ],
276
+ 'mpmim.history' => [
277
+ 'token' => true,
278
+ 'endpoint' => '/mpmim.history'
279
+ ],
280
+ 'mpim.list' => [
281
+ 'token' => true,
282
+ 'endpoint' => '/mpim.list'
283
+ ],
284
+ 'mpim.mark' => [
285
+ 'token' => true,
286
+ 'endpoint' => '/mpim.mark'
287
+ ],
288
+ 'mpim.open' => [
289
+ 'token' => true,
290
+ 'endpoint' => '/mpim.open'
291
+ ],
292
+ 'oauth.access' => [
293
+ 'token' => false,
294
+ 'endpoint' => '/oauth.access'
295
+ ],
296
+ 'pins.add' => [
297
+ 'token' => true,
298
+ 'endpoint' => '/pins.add'
299
+ ],
300
+ 'pins.list' => [
301
+ 'token' => true,
302
+ 'endpoint' => '/pins.list'
303
+ ],
304
+ 'pins.remove' => [
305
+ 'token' => true,
306
+ 'endpoint' => '/pins.remove'
307
+ ],
308
+ 'reactions.add' => [
309
+ 'token' => true,
310
+ 'endpoint' => '/reactions.add'
311
+ ],
312
+ 'reactions.get' => [
313
+ 'token' => true,
314
+ 'endpoint' => '/reactions.get'
315
+ ],
316
+ 'reactions.list' => [
317
+ 'token' => true,
318
+ 'endpoint' => '/reactions.list'
319
+ ],
320
+ 'reactions.remove' => [
321
+ 'token' => true,
322
+ 'endpoint' => '/reactions.remove'
323
+ ],
324
+ 'rtm.start' => [
325
+ 'token' => true,
326
+ 'endpoint' => '/rtm.start'
327
+ ],
328
+ 'search.all' => [
329
+ 'token' => true,
330
+ 'endpoint' => '/search.all'
331
+ ],
332
+ 'search.files' => [
333
+ 'token' => true,
334
+ 'endpoint' => '/search.files'
335
+ ],
336
+ 'search.messages' => [
337
+ 'token' => true,
338
+ 'endpoint' => '/search.messages'
339
+ ],
340
+ 'stars.add' => [
341
+ 'token' => true,
342
+ 'endpoint' => '/stars.add'
343
+ ],
344
+ 'stars.list' => [
345
+ 'token' => true,
346
+ 'endpoint' => '/stars.list'
347
+ ],
348
+ 'stars.remove' => [
349
+ 'token' => true,
350
+ 'endpoint' => '/stars.remove'
351
+ ],
352
+ 'team.accessLogs' => [
353
+ 'token' => true,
354
+ 'endpoint' => '/team.accessLogs'
355
+ ],
356
+ 'team.info' => [
357
+ 'token' => true,
358
+ 'endpoint' => '/team.info'
359
+ ],
360
+ 'team.integrationLogs' => [
361
+ 'token' => true,
362
+ 'endpoint' => '/team.integrationLogs'
363
+ ],
364
+ 'usergroups.create' => [
365
+ 'token' => true,
366
+ 'endpoint' => '/usergroups.create'
367
+ ],
368
+ 'usergroups.disable' => [
369
+ 'token' => true,
370
+ 'endpoint' => '/usergroups.disable'
371
+ ],
372
+ 'usergroups.enable' => [
373
+ 'token' => true,
374
+ 'endpoint' => '/usergroups.enable'
375
+ ],
376
+ 'usergroups.list' => [
377
+ 'token' => true,
378
+ 'endpoint' => '/usergroups.list'
379
+ ],
380
+ 'usergroups.update' => [
381
+ 'token' => true,
382
+ 'endpoint' => '/usergroups.update'
383
+ ],
384
+ 'usergroups.users.list' => [
385
+ 'token' => true,
386
+ 'endpoint' => '/usergroups.users.list'
387
+ ],
388
+ 'usergroups.users.update' => [
389
+ 'token' => true,
390
+ 'endpoint' => '/usergroups.users.update'
391
+ ],
392
+ 'users.getPresence' => [
393
+ 'token' => true,
394
+ 'endpoint' => '/users.getPresence'
395
+ ],
396
+ 'users.info' => [
397
+ 'token' => true,
398
+ 'endpoint' => '/users.info'
399
+ ],
400
+ 'users.list' => [
401
+ 'token' => true,
402
+ 'endpoint' => '/users.list'
403
+ ],
404
+ 'users.setActive' => [
405
+ 'token' => true,
406
+ 'endpoint' => '/users.setActive'
407
+ ],
408
+ 'users.setPresence' => [
409
+ 'token' => true,
410
+ 'endpoint' => '/users.setPresence'
411
+ ],
412
+ 'users.admin.invite' => [
413
+ 'token' => true,
414
+ 'endpoint' => '/users.admin.invite'
415
+ ]
416
+ ];
417
+
418
+ /**
419
+ * The base URL.
420
+ *
421
+ * @var string
422
+ */
423
+ protected static $baseUrl = 'https://slack.com/api';
424
+
425
+ /**
426
+ * The API token.
427
+ *
428
+ * @var string
429
+ */
430
+ protected $token;
431
+
432
+ /**
433
+ * The Http interactor.
434
+ *
435
+ * @var \Frlnc\Slack\Contracts\Http\Interactor
436
+ */
437
+ protected $interactor;
438
+
439
+ /**
440
+ * @param string $token
441
+ * @param \Frlnc\Slack\Contracts\Http\Interactor $interactor
442
+ */
443
+ public function __construct($token, Interactor $interactor)
444
+ {
445
+ $this->token = $token;
446
+ $this->interactor = $interactor;
447
+ }
448
+
449
+ /**
450
+ * Executes a command.
451
+ *
452
+ * @param string $command
453
+ * @param array $parameters
454
+ * @return \Frlnc\Slack\Contracts\Http\Response
455
+ */
456
+ public function execute($command, array $parameters = [])
457
+ {
458
+ if (!isset(self::$commands[$command]))
459
+ throw new InvalidArgumentException("The command '{$command}' is not currently supported");
460
+
461
+ $command = self::$commands[$command];
462
+
463
+ if ($command['token'])
464
+ $parameters = array_merge($parameters, ['token' => $this->token]);
465
+
466
+ if (isset($command['format']))
467
+ foreach ($command['format'] as $format)
468
+ if (isset($parameters[$format]))
469
+ $parameters[$format] = self::format($parameters[$format]);
470
+
471
+ $headers = [];
472
+ if (isset($command['headers']))
473
+ $headers = $command['headers'];
474
+
475
+ $url = self::$baseUrl . $command['endpoint'];
476
+
477
+ if (isset($command['post']) && $command['post'])
478
+ return $this->interactor->post($url, [], $parameters, $headers);
479
+
480
+ return $this->interactor->get($url, $parameters, $headers);
481
+ }
482
+
483
+ /**
484
+ * Sets the token.
485
+ *
486
+ * @param string $token
487
+ */
488
+ public function setToken($token)
489
+ {
490
+ $this->token = $token;
491
+ }
492
+
493
+ /**
494
+ * Formats a string for Slack.
495
+ *
496
+ * @param string $string
497
+ * @return string
498
+ */
499
+ public static function format($string)
500
+ {
501
+ $string = str_replace('&', '&amp;', $string);
502
+ $string = str_replace('<', '&lt;', $string);
503
+ $string = str_replace('>', '&gt;', $string);
504
+
505
+ return $string;
506
+ }
507
+
508
+ }
includes/slack/Http/CurlInteractor.php ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php namespace Frlnc\Slack\Http;
2
+
3
+ use Frlnc\Slack\Contracts\Http\ResponseFactory;
4
+
5
+ class CurlInteractor implements \Frlnc\Slack\Contracts\Http\Interactor {
6
+
7
+ /**
8
+ * The response factory to use.
9
+ *
10
+ * @var \Frlnc\Slack\Contracts\Http\ResponseFactory
11
+ */
12
+ protected $factory;
13
+
14
+ /**
15
+ * {@inheritdoc}
16
+ */
17
+ public function get($url, array $parameters = [], array $headers = [])
18
+ {
19
+ $request = $this->prepareRequest($url, $parameters, $headers);
20
+
21
+ return $this->executeRequest($request);
22
+ }
23
+
24
+ /**
25
+ * {@inheritdoc}
26
+ */
27
+ public function post($url, array $urlParameters = [], array $postParameters = [], array $headers = [])
28
+ {
29
+ $request = $this->prepareRequest($url, $urlParameters, $headers);
30
+
31
+ curl_setopt($request, CURLOPT_POST, count($postParameters));
32
+ curl_setopt($request, CURLOPT_POSTFIELDS, http_build_query($postParameters));
33
+
34
+ return $this->executeRequest($request);
35
+ }
36
+
37
+ /**
38
+ * Prepares a request using curl.
39
+ *
40
+ * @param string $url [description]
41
+ * @param array $parameters [description]
42
+ * @param array $headers [description]
43
+ * @return resource
44
+ */
45
+ protected static function prepareRequest($url, $parameters = [], $headers = [])
46
+ {
47
+ $request = curl_init();
48
+
49
+ if ($query = http_build_query($parameters))
50
+ $url .= '?' . $query;
51
+
52
+ curl_setopt($request, CURLOPT_URL, $url);
53
+ curl_setopt($request, CURLOPT_RETURNTRANSFER, true);
54
+ curl_setopt($request, CURLOPT_HTTPHEADER, $headers);
55
+ curl_setopt($request, CURLINFO_HEADER_OUT, true);
56
+ curl_setopt($request, CURLOPT_SSL_VERIFYPEER, false);
57
+
58
+ return $request;
59
+ }
60
+
61
+ /**
62
+ * Executes a curl request.
63
+ *
64
+ * @param resource $request
65
+ * @return \Frlnc\Slack\Contracts\Http\Response
66
+ */
67
+ public function executeRequest($request)
68
+ {
69
+ $body = curl_exec($request);
70
+ $info = curl_getinfo($request);
71
+
72
+ curl_close($request);
73
+
74
+ $statusCode = $info['http_code'];
75
+ $headers = $info['request_header'];
76
+
77
+ if (function_exists('http_parse_headers'))
78
+ $headers = http_parse_headers($headers);
79
+ else
80
+ {
81
+ $header_text = substr($headers, 0, strpos($headers, "\r\n\r\n"));
82
+ $headers = [];
83
+
84
+ foreach (explode("\r\n", $header_text) as $i => $line)
85
+ if ($i === 0)
86
+ continue;
87
+ else
88
+ {
89
+ list ($key, $value) = explode(': ', $line);
90
+
91
+ $headers[$key] = $value;
92
+ }
93
+ }
94
+
95
+ return $this->factory->build($body, $headers, $statusCode);
96
+ }
97
+
98
+ /**
99
+ * {@inheritdoc}
100
+ */
101
+ public function setResponseFactory(ResponseFactory $factory)
102
+ {
103
+ $this->factory = $factory;
104
+ }
105
+
106
+ }
includes/slack/Http/SlackResponse.php ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php namespace Frlnc\Slack\Http;
2
+
3
+ class SlackResponse implements \Frlnc\Slack\Contracts\Http\Response, \JsonSerializable {
4
+
5
+ /**
6
+ * The response body.
7
+ *
8
+ * @var string
9
+ */
10
+ protected $body;
11
+
12
+ /**
13
+ * The response headers.
14
+ *
15
+ * @var array
16
+ */
17
+ protected $headers;
18
+
19
+ /**
20
+ * The response status code.
21
+ *
22
+ * @var integer
23
+ */
24
+ protected $statusCode;
25
+
26
+ /**
27
+ * @param string $body
28
+ * @param array $headers
29
+ * @param integer $statusCode
30
+ */
31
+ public function __construct($body, array $headers = [], $statusCode = 404)
32
+ {
33
+ $this->body = json_decode($body, true);
34
+ $this->headers = $headers;
35
+ $this->statusCode = $statusCode;
36
+ }
37
+
38
+ /**
39
+ * {@inheritdoc}
40
+ */
41
+ public function getBody()
42
+ {
43
+ return $this->body;
44
+ }
45
+
46
+ /**
47
+ * {@inheritdoc}
48
+ */
49
+ public function getHeaders()
50
+ {
51
+ return $this->headers;
52
+ }
53
+
54
+ /**
55
+ * {@inheritdoc}
56
+ */
57
+ public function getStatusCode()
58
+ {
59
+ return $this->statusCode;
60
+ }
61
+
62
+ /**
63
+ * {@inheritdoc}
64
+ */
65
+ public function jsonSerialize()
66
+ {
67
+ return $this->toArray();
68
+ }
69
+
70
+ /**
71
+ * Converts the response to an array.
72
+ *
73
+ * @return array
74
+ */
75
+ public function toArray()
76
+ {
77
+ return [
78
+ 'status_code' => $this->getStatusCode(),
79
+ 'headers' => $this->getHeaders(),
80
+ 'body' => $this->getBody()
81
+ ];
82
+ }
83
+
84
+ }
includes/slack/Http/SlackResponseFactory.php ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php namespace Frlnc\Slack\Http;
2
+
3
+ class SlackResponseFactory implements \Frlnc\Slack\Contracts\Http\ResponseFactory {
4
+
5
+ /**
6
+ * {@inheritdoc}
7
+ */
8
+ public function build($body, array $headers, $statusCode)
9
+ {
10
+ return new SlackResponse($body, $headers, $statusCode);
11
+ }
12
+
13
+ }
includes/slack/Logger.php ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php namespace Frlnc\Slack;
2
+
3
+ use Frlnc\Slack\Http\CurlInteractor;
4
+ use Frlnc\Slack\Http\SlackResponseFactory;
5
+ use Frlnc\Slack\Core\Commander;
6
+
7
+ /**
8
+ * Created by Vextras.
9
+ *
10
+ * Name: Ryan Hungate
11
+ * Email: ryan@vextras.com
12
+ * Date: 8/12/16
13
+ * Time: 9:36 AM
14
+ */
15
+ class Logger
16
+ {
17
+ private static $instance = null;
18
+
19
+ /**
20
+ * @var null|Commander
21
+ */
22
+ private $commander = null;
23
+ public $api_token = null;
24
+ public $channel = null;
25
+
26
+
27
+ /**
28
+ * @return mixed
29
+ */
30
+ public static function instance()
31
+ {
32
+ if (empty(static::$instance)) {
33
+ $vars = mailchimp_environment_variables();
34
+ static::$instance = new Logger(
35
+ (isset($vars->slack_token) ? $vars->slack_token : null),
36
+ (isset($vars->slack_channel) ? $vars->slack_channel : null)
37
+ );
38
+ }
39
+ return static::$instance;
40
+ }
41
+
42
+ /**
43
+ * Logger constructor.
44
+ * @param string $api_token
45
+ * @param string $channel
46
+ */
47
+ public function __construct($api_token = null, $channel = null)
48
+ {
49
+ if ($api_token && $channel) {
50
+ $this->setup($api_token, $channel);
51
+ }
52
+ }
53
+
54
+ /**
55
+ * @param $api_token
56
+ * @param $channel
57
+ * @return $this
58
+ */
59
+ public function setup($api_token, $channel)
60
+ {
61
+ $this->channel = $channel;
62
+ $this->api_token = $api_token;
63
+
64
+ $curl = new CurlInteractor;
65
+ $curl->setResponseFactory(new SlackResponseFactory);
66
+
67
+ $this->commander = new Commander($this->api_token, $curl);
68
+
69
+ return $this;
70
+ }
71
+
72
+ /**
73
+ * @param $message
74
+ * @return Logger
75
+ */
76
+ public function notice($message)
77
+ {
78
+ if (empty($this->commander) || empty($this->api_token) || empty($this->channel)) {
79
+ return $this;
80
+ }
81
+
82
+ try {
83
+ $this->commander->execute('chat.postMessage', [
84
+ 'channel' => '#'.$this->channel,
85
+ 'text' => $message
86
+ ]);
87
+ } catch (\Exception $e) {
88
+
89
+ }
90
+
91
+ return $this;
92
+ }
93
+ }
includes/vendor/queue.php ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /*
3
+ Plugin Name: WP Background Processing
4
+ Plugin URI: https://github.com/A5hleyRich/wp-background-processing
5
+ Description: Asynchronous requests and background processing in WordPress.
6
+ Author: Delicious Brains Inc.
7
+ Version: 1.0
8
+ Author URI: https://deliciousbrains.com/
9
+ */
10
+
11
+ $queue_folder_path = plugin_dir_path( __FILE__ );
12
+
13
+ require_once $queue_folder_path . 'queue/classes/wp-job.php';
14
+ require_once $queue_folder_path . 'queue/classes/wp-queue.php';
15
+ require_once $queue_folder_path . 'queue/classes/worker/wp-worker.php';
16
+ require_once $queue_folder_path . 'queue/classes/worker/wp-http-worker.php';
17
+
18
+ // Add WP CLI commands
19
+ if ( defined( 'WP_CLI' ) && WP_CLI ) {
20
+ require_once $queue_folder_path . 'queue/classes/cli/queue-command.php';
21
+
22
+ WP_CLI::add_command( 'queue', 'Queue_Command' );
23
+ }
24
+
25
+ global $wp_queue;
26
+ $wp_queue = new WP_Queue();
27
+
28
+ // Instantiate HTTP queue worker
29
+ new WP_Http_Worker($wp_queue);
30
+
31
+ if ( ! function_exists( 'wp_queue' ) ) {
32
+ /**
33
+ * WP queue.
34
+ *
35
+ * @param WP_Job $job
36
+ * @param int $delay
37
+ */
38
+ function wp_queue( WP_Job $job, $delay = 0 ) {
39
+ global $wp_queue;
40
+ $wp_queue->push( $job, $delay );
41
+ do_action( 'wp_queue_job_pushed', $job );
42
+ }
43
+ }
includes/vendor/queue/classes/cli/queue-command.php ADDED
@@ -0,0 +1,120 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Manage queue and jobs.
5
+ *
6
+ * @package wp-cli
7
+ */
8
+ class Queue_Command extends WP_CLI_Command {
9
+
10
+ /**
11
+ * Creates the queue tables.
12
+ *
13
+ * @subcommand create-tables
14
+ */
15
+ public function create_tables( $args, $assoc_args = array() ) {
16
+ require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
17
+
18
+ global $wpdb;
19
+
20
+ $wpdb->hide_errors();
21
+
22
+ $charset_collate = $wpdb->get_charset_collate();
23
+
24
+ $sql = "CREATE TABLE {$wpdb->prefix}queue (
25
+ id bigint(20) NOT NULL AUTO_INCREMENT,
26
+ job text NOT NULL,
27
+ attempts tinyint(1) NOT NULL DEFAULT 0,
28
+ locked tinyint(1) NOT NULL DEFAULT 0,
29
+ locked_at datetime DEFAULT NULL,
30
+ available_at datetime NOT NULL,
31
+ created_at datetime NOT NULL,
32
+ PRIMARY KEY (id)
33
+ ) $charset_collate;";
34
+
35
+ dbDelta( $sql );
36
+
37
+ $sql = "CREATE TABLE {$wpdb->prefix}failed_jobs (
38
+ id bigint(20) NOT NULL AUTO_INCREMENT,
39
+ job text NOT NULL,
40
+ failed_at datetime NOT NULL,
41
+ PRIMARY KEY (id)
42
+ ) $charset_collate;";
43
+
44
+ dbDelta( $sql );
45
+
46
+ WP_CLI::success( "Table {$wpdb->prefix}queue created." );
47
+ }
48
+
49
+ /**
50
+ * Listen to the queue.
51
+ */
52
+ public function listen( $args, $assoc_args = array() ) {
53
+ global $wp_queue;
54
+
55
+ $worker = new WP_Worker( $wp_queue );
56
+
57
+ WP_CLI::log( 'Listening for queue jobs...' );
58
+
59
+ while ( true ) {
60
+ if ( $worker->should_run() ) {
61
+ if ( $worker->process_next_job() ) {
62
+ WP_CLI::success( 'Processed: ' . $worker->get_job_name() );
63
+ } else {
64
+ WP_CLI::warning( 'Failed: ' . $worker->get_job_name() );
65
+ }
66
+ } else {
67
+ sleep( 5 );
68
+ }
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Process the next job in the queue.
74
+ */
75
+ public function work( $args, $assoc_args = array() ) {
76
+ global $wp_queue;
77
+
78
+ $worker = new WP_Worker( $wp_queue );
79
+
80
+ if ( $worker->should_run() ) {
81
+ if ( $worker->process_next_job() ) {
82
+ WP_CLI::success( 'Processed: ' . $worker->get_job_name() );
83
+ } else {
84
+ WP_CLI::warning( 'Failed: ' . $worker->get_job_name() );
85
+ }
86
+ } else {
87
+ WP_CLI::log( 'No jobs to process...' );
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Show queue status.
93
+ */
94
+ public function status( $args, $assoc_args = array() ) {
95
+ global $wp_queue;
96
+
97
+ WP_CLI::log( $wp_queue->available_jobs() . ' jobs in the queue' );
98
+ WP_CLI::log( $wp_queue->failed_jobs() . ' failed jobs' );
99
+ }
100
+
101
+ /**
102
+ * Push failed jobs back onto the queue.
103
+ *
104
+ * @subcommand restart-failed
105
+ */
106
+ public function restart_failed( $args, $assoc_args = array() ) {
107
+ global $wp_queue;
108
+
109
+ if ( ! $wp_queue->failed_jobs() ) {
110
+ WP_CLI::log( 'No failed jobs to restart...' );
111
+
112
+ return;
113
+ }
114
+
115
+ $count = $wp_queue->restart_failed_jobs();
116
+
117
+ WP_CLI::success( $count . ' failed jobs pushed to the queue' );
118
+ }
119
+
120
+ }
includes/vendor/queue/classes/worker/wp-http-worker.php ADDED
@@ -0,0 +1,323 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ if ( ! class_exists( 'WP_Http_Worker' ) ) {
4
+ class WP_Http_Worker extends WP_Worker {
5
+
6
+ /**
7
+ * Has the worker been dispatched in this request?
8
+ *
9
+ * @var bool
10
+ */
11
+ protected $dispatched = false;
12
+
13
+ /**
14
+ * Timestamp of when this worker started processing the queue.
15
+ *
16
+ * @var int
17
+ */
18
+ protected $start_time;
19
+
20
+ /**
21
+ * WP_Http_Worker constructor
22
+ *
23
+ * @param WP_Queue $queue
24
+ */
25
+ public function __construct( $queue ) {
26
+ parent::__construct( $queue );
27
+
28
+ // Cron health check
29
+ add_action( 'http_worker_cron', array( $this, 'handle_cron' ) );
30
+ add_filter( 'cron_schedules', array( $this, 'schedule_cron' ) );
31
+ $this->maybe_schedule_cron();
32
+
33
+ // Dispatch handlers
34
+ add_action( 'wp_ajax_http_worker', array( $this, 'maybe_handle' ) );
35
+ add_action( 'wp_ajax_nopriv_http_worker', array( $this, 'maybe_handle' ) );
36
+
37
+ // Dispatch listener
38
+ add_action( 'wp_queue_job_pushed', array( $this, 'maybe_dispatch_worker' ) );
39
+ }
40
+
41
+ /**
42
+ * Maybe handle
43
+ *
44
+ * Process the queue if no other HTTP worker is running and
45
+ * the current worker is within server memory and time limit constraints.
46
+ * Automatically dispatch another worker and kill the current process if
47
+ * jobs remain in the queue and server limits reached.
48
+ */
49
+ public function maybe_handle() {
50
+ check_ajax_referer( 'http_worker', 'nonce' );
51
+
52
+ if ( $this->is_worker_running() ) {
53
+ // Worker already running, die
54
+ wp_die();
55
+ }
56
+
57
+ // Lock worker to prevent multiple instances spawning
58
+ $this->lock_worker();
59
+
60
+ // Loop over jobs while within server limits
61
+ while ( ! $this->time_exceeded() && ! $this->memory_exceeded() ) {
62
+ if ( $this->should_run() ) {
63
+ $this->process_next_job();
64
+ } else {
65
+ break;
66
+ }
67
+ }
68
+
69
+ // Unlock worker to allow another instance to be spawned
70
+ $this->unlock_worker();
71
+
72
+ if ( $this->queue->available_jobs() ) {
73
+ // Job queue not empty, dispatch async worker request
74
+ $this->dispatch();
75
+ }
76
+
77
+ wp_die();
78
+ }
79
+
80
+ /**
81
+ * Memory exceeded
82
+ *
83
+ * Ensures the worker process never exceeds 80%
84
+ * of the maximum allowed PHP memory.
85
+ *
86
+ * @return bool
87
+ */
88
+ protected function memory_exceeded() {
89
+ $memory_limit = $this->get_memory_limit() * 0.8; // 80% of max memory
90
+ $current_memory = memory_get_usage( true );
91
+ $return = false;
92
+
93
+ if ( $current_memory >= $memory_limit ) {
94
+ $return = true;
95
+ }
96
+
97
+ return apply_filters( 'http_worker_memory_exceeded', $return );
98
+ }
99
+
100
+ /**
101
+ * Get memory limit
102
+ *
103
+ * @return int
104
+ */
105
+ protected function get_memory_limit() {
106
+ if ( function_exists( 'ini_get' ) ) {
107
+ $memory_limit = ini_get( 'memory_limit' );
108
+ } else {
109
+ // Sensible default
110
+ $memory_limit = '128M';
111
+ }
112
+
113
+ if ( ! $memory_limit || -1 == $memory_limit ) {
114
+ // Unlimited, set to 32GB
115
+ $memory_limit = '32000M';
116
+ }
117
+
118
+ return intval( $memory_limit ) * 1024 * 1024;
119
+ }
120
+
121
+ /**
122
+ * Time exceeded
123
+ *
124
+ * Ensures the worker never exceeds a sensible time limit (20s by default).
125
+ * A timeout limit of 30s is common on shared hosting.
126
+ *
127
+ * @return bool
128
+ */
129
+ protected function time_exceeded() {
130
+ $finish = $this->start_time + apply_filters( 'http_worker_default_time_limit', 20 ); // 20 seconds
131
+ $return = false;
132
+
133
+ if ( time() >= $finish ) {
134
+ $return = true;
135
+ }
136
+
137
+ return apply_filters( 'http_worker_time_exceeded', $return );
138
+ }
139
+
140
+ /**
141
+ * Maybe dispatch worker
142
+ *
143
+ * Dispatch a worker process if we haven't already in this request
144
+ * and no other HTTP workers are running.
145
+ *
146
+ * @param WP_Job $job
147
+ */
148
+ public function maybe_dispatch_worker( $job ) {
149
+ if ( $this->is_worker_running() ) {
150
+ // HTTP worker already running, return
151
+ return;
152
+ }
153
+
154
+ // Dispatch async worker request
155
+ $this->dispatch();
156
+ }
157
+
158
+ /**
159
+ * Is worker running
160
+ *
161
+ * Check if another instance of the HTTP worker is running.
162
+ *
163
+ * @return bool
164
+ */
165
+ protected function is_worker_running() {
166
+ if ( get_site_transient( 'http_worker_lock' ) ) {
167
+ // Process already running
168
+ return true;
169
+ }
170
+
171
+ return false;
172
+ }
173
+
174
+ /**
175
+ * Lock worker
176
+ *
177
+ * Lock the HTTP worker to prevent multiple instances running.
178
+ */
179
+ protected function lock_worker() {
180
+ $this->start_time = time(); // Set start time of current worker
181
+
182
+ $lock_duration = apply_filters( 'http_worker_lock_time', 60 ); // 60 seconds
183
+
184
+ set_site_transient( 'http_worker_lock', microtime(), $lock_duration );
185
+ }
186
+
187
+ /**
188
+ * Unlock worker
189
+ *
190
+ * Unlock the HTTP worker to allow other instances to be spawned.
191
+ */
192
+ protected function unlock_worker() {
193
+ delete_site_transient( 'http_worker_lock' );
194
+ }
195
+
196
+ /**
197
+ * Dispatch
198
+ *
199
+ * Fire off a non-blocking async request if we haven't already
200
+ * in this request.
201
+ */
202
+ protected function dispatch() {
203
+ if ( $this->is_http_worker_disabled() ) {
204
+ return;
205
+ }
206
+
207
+ if ( ! $this->dispatched ) {
208
+ $this->async_request();
209
+ }
210
+
211
+ $this->dispatched = true;
212
+ }
213
+
214
+ /**
215
+ * Is HTTP worker disabled
216
+ *
217
+ * @return bool
218
+ */
219
+ protected function is_http_worker_disabled() {
220
+ if ( ! defined( 'DISABLE_WP_HTTP_WORKER' ) || true !== DISABLE_WP_HTTP_WORKER ) {
221
+ return false;
222
+ }
223
+
224
+ return true;
225
+ }
226
+
227
+ /**
228
+ * Async request
229
+ *
230
+ * Fire off a non-blocking request to admin-ajax.php.
231
+ *
232
+ * @return array|WP_Error
233
+ */
234
+ protected function async_request() {
235
+ $action = 'http_worker';
236
+
237
+ $query_args = apply_filters( 'http_worker_query_args', array(
238
+ 'action' => $action,
239
+ 'nonce' => wp_create_nonce( $action ),
240
+ ) );
241
+
242
+ $query_url = apply_filters( 'http_worker_query_url', admin_url( 'admin-ajax.php' ) );
243
+
244
+ $post_args = apply_filters( 'http_worker_post_args', array(
245
+ 'timeout' => 0.01,
246
+ 'blocking' => false,
247
+ 'cookies' => $_COOKIE,
248
+ 'sslverify' => apply_filters( 'https_local_ssl_verify', false ),
249
+ ) );
250
+
251
+ $url = add_query_arg( $query_args, $query_url );
252
+
253
+ return wp_remote_post( esc_url_raw( $url ), $post_args );
254
+ }
255
+
256
+ /**
257
+ * Handle cron
258
+ *
259
+ * Restart the HTTP worker if not already running
260
+ * and data exists in the queue.
261
+ */
262
+ public function handle_cron() {
263
+ if ( $this->is_worker_running() ) {
264
+ // Worker already running, die
265
+ wp_die();
266
+ }
267
+
268
+ if ( ! $this->queue->available_jobs() ) {
269
+ // No jobs on the queue to process, die
270
+ wp_die();
271
+ }
272
+
273
+ $this->dispatch();
274
+
275
+ exit;
276
+ }
277
+
278
+ /**
279
+ * Cron schedules
280
+ *
281
+ * @param $schedules
282
+ *
283
+ * @return mixed
284
+ */
285
+ public function schedule_cron( $schedules ) {
286
+ $interval = apply_filters( 'http_worker_cron_interval', 3 );
287
+
288
+ // Adds every 3 minutes to the existing schedules.
289
+ $schedules[ 'http_worker_cron_interval' ] = array(
290
+ 'interval' => MINUTE_IN_SECONDS * $interval,
291
+ 'display' => sprintf( __( 'Every %d Minutes' ), $interval ),
292
+ );
293
+
294
+ return $schedules;
295
+ }
296
+
297
+ /**
298
+ * Maybe schedule cron
299
+ *
300
+ * Schedule health check cron if not disabled. Remove schedule if
301
+ * disabled and already scheduled.
302
+ */
303
+ public function maybe_schedule_cron() {
304
+ if ( $this->is_http_worker_disabled() ) {
305
+ die('http worker is disabled');
306
+ // Remove health check cron event, if scheduled
307
+ $timestamp = wp_next_scheduled( 'http_worker_cron' );
308
+
309
+ if ( wp_next_scheduled( 'http_worker_cron' ) ) {
310
+ wp_unschedule_event( $timestamp, 'http_worker_cron' );
311
+ }
312
+
313
+ return;
314
+ }
315
+
316
+ if ( ! wp_next_scheduled( 'http_worker_cron' ) ) {
317
+ // Schedule health check
318
+ wp_schedule_event( time(), 'http_worker_cron_interval', 'http_worker_cron' );
319
+ }
320
+ }
321
+
322
+ }
323
+ }
includes/vendor/queue/classes/worker/wp-worker.php ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ if ( ! class_exists( 'WP_Worker' ) ) {
4
+ class WP_Worker {
5
+
6
+ /**
7
+ * @var WP_Queue
8
+ */
9
+ protected $queue;
10
+
11
+ /**
12
+ * @var WP_Job
13
+ */
14
+ protected $payload;
15
+
16
+ /**
17
+ * WP_Worker constructor.
18
+ *
19
+ * @param WP_Queue $queue
20
+ */
21
+ public function __construct( $queue ) {
22
+ $this->queue = $queue;
23
+ }
24
+
25
+ /**
26
+ * Should run
27
+ *
28
+ * @return bool
29
+ */
30
+ public function should_run() {
31
+ if ( $this->queue->available_jobs() ) {
32
+ return true;
33
+ }
34
+
35
+ return false;
36
+ }
37
+
38
+ /**
39
+ * Process next job.
40
+ *
41
+ * @return bool
42
+ */
43
+ public function process_next_job() {
44
+ $job = $this->queue->get_next_job();
45
+ $this->payload = unserialize( $job->job );
46
+
47
+ $this->queue->lock_job( $job );
48
+ $this->payload->set_job( $job );
49
+
50
+ try {
51
+ $this->payload->handle();
52
+
53
+ if ( $this->payload->is_released() ) {
54
+ // Job manually released, release back onto queue
55
+ $this->queue->release( $job, $this->payload->get_delay() );
56
+ }
57
+
58
+ if ( $this->payload->is_deleted() ) {
59
+ // Job manually deleted, delete from queue
60
+ $this->queue->delete( $job );
61
+ }
62
+
63
+ if ( ! $this->payload->is_deleted_or_released() ) {
64
+ // Job completed, delete from queue
65
+ $this->queue->delete( $job );
66
+ }
67
+ } catch ( Exception $e ) {
68
+ $this->queue->release( $job );
69
+
70
+ return false;
71
+ }
72
+
73
+ return true;
74
+ }
75
+
76
+ /**
77
+ * Get job name.
78
+ *
79
+ * @return object
80
+ */
81
+ public function get_job_name() {
82
+ return get_class( $this->payload );
83
+ }
84
+
85
+ }
86
+ }
includes/vendor/queue/classes/wp-job.php ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ if ( ! class_exists( 'WP_Job' ) ) {
4
+ abstract class WP_Job {
5
+
6
+ /**
7
+ * @var stdClass
8
+ */
9
+ private $job;
10
+
11
+ /**
12
+ * @var int
13
+ */
14
+ private $delay = 0;
15
+
16
+ /**
17
+ * @var bool
18
+ */
19
+ private $deleted = false;
20
+
21
+ /**
22
+ * @var bool
23
+ */
24
+ private $released = false;
25
+
26
+ /**
27
+ * Set job
28
+ *
29
+ * @param $job
30
+ */
31
+ public function set_job( $job ) {
32
+ $this->job = $job;
33
+ }
34
+
35
+ /**
36
+ * Delete the job from the queue
37
+ */
38
+ protected function delete() {
39
+ $this->deleted = true;
40
+ }
41
+
42
+ /**
43
+ * Release a job back onto the queue
44
+ *
45
+ * @param int $delay
46
+ */
47
+ protected function release( $delay = 0 ) {
48
+ $this->released = true;
49
+ $this->delay = $delay;
50
+ }
51
+
52
+ /**
53
+ * Attempts
54
+ *
55
+ * @return int
56
+ */
57
+ protected function attempts() {
58
+ return (int) $this->job->attempts;
59
+ }
60
+
61
+ /**
62
+ * Is deleted.
63
+ *
64
+ * @return bool
65
+ */
66
+ public function is_deleted() {
67
+ return $this->deleted;
68
+ }
69
+
70
+ /**
71
+ * Is released.
72
+ *
73
+ * @return bool
74
+ */
75
+ public function is_released() {
76
+ return $this->released;
77
+ }
78
+
79
+ /**
80
+ * Is deleted for released
81
+ *
82
+ * @return bool
83
+ */
84
+ public function is_deleted_or_released() {
85
+ return $this->is_deleted() || $this->is_released();
86
+ }
87
+
88
+ /**
89
+ * Get delay.
90
+ *
91
+ * @return int
92
+ */
93
+ public function get_delay() {
94
+ return $this->delay;
95
+ }
96
+
97
+ /**
98
+ * Handle the job.
99
+ */
100
+ abstract public function handle();
101
+
102
+ }
103
+ }
includes/vendor/queue/classes/wp-queue.php ADDED
@@ -0,0 +1,231 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ if ( ! class_exists( 'WP_Queue' ) ) {
4
+ class WP_Queue {
5
+
6
+ /**
7
+ * @var string
8
+ */
9
+ public $table;
10
+
11
+ /**
12
+ * @var string
13
+ */
14
+ public $failed_table;
15
+
16
+ /**
17
+ * @var int
18
+ */
19
+ public $release_time = 60;
20
+
21
+ /**
22
+ * WP_Queue constructor
23
+ */
24
+ public function __construct() {
25
+ global $wpdb;
26
+
27
+ $this->table = $wpdb->prefix . 'queue';
28
+ $this->failed_table = $wpdb->prefix . 'failed_jobs';
29
+ }
30
+
31
+ /**
32
+ * Push a job onto the queue.
33
+ *
34
+ * @param WP_Job $job
35
+ * @param int $delay
36
+ *
37
+ * @return $this
38
+ */
39
+ public function push( WP_Job $job, $delay = 0 ) {
40
+ global $wpdb;
41
+
42
+ $data = array(
43
+ 'job' => maybe_serialize( $job ),
44
+ 'available_at' => $this->datetime( $delay ),
45
+ 'created_at' => $this->datetime(),
46
+ );
47
+
48
+ $id = $wpdb->insert( $this->table, $data );
49
+
50
+ return $this;
51
+ }
52
+
53
+ /**
54
+ * Release.
55
+ *
56
+ * @param object $job
57
+ * @param int $delay
58
+ */
59
+ public function release( $job, $delay = 0 ) {
60
+ if ( $job->attempts >= 3 ) {
61
+ $this->failed( $job );
62
+
63
+ return;
64
+ }
65
+
66
+ global $wpdb;
67
+
68
+ $data = array(
69
+ 'attempts' => $job->attempts + 1,
70
+ 'locked' => 0,
71
+ 'locked_at' => null,
72
+ 'available_at' => $this->datetime( $delay ),
73
+ );
74
+ $where = array(
75
+ 'id' => $job->id,
76
+ );
77
+
78
+ $wpdb->update( $this->table, $data, $where );
79
+ }
80
+
81
+ /**
82
+ * Failed
83
+ *
84
+ * @param stdClass $job
85
+ */
86
+ protected function failed( $job ) {
87
+ global $wpdb;
88
+
89
+ $wpdb->insert( $this->failed_table, array(
90
+ 'job' => $job->job,
91
+ 'failed_at' => $this->datetime(),
92
+ ) );
93
+
94
+ $payload = unserialize($job->job);
95
+
96
+ if (method_exists($payload, 'failed')) {
97
+ $payload->failed();
98
+ }
99
+
100
+ $this->delete( $job );
101
+ }
102
+
103
+ /**
104
+ * Delete.
105
+ *
106
+ * @param object $job
107
+ */
108
+ public function delete( $job ) {
109
+ global $wpdb;
110
+
111
+ $where = array(
112
+ 'id' => $job->id,
113
+ );
114
+
115
+ $wpdb->delete( $this->table, $where );
116
+ }
117
+
118
+ /**
119
+ * Get MySQL datetime.
120
+ *
121
+ * @param int $offset Seconds, can pass negative int.
122
+ *
123
+ * @return string
124
+ */
125
+ protected function datetime($offset = 0) {
126
+ $timestamp = time() + $offset;
127
+
128
+ return gmdate( 'Y-m-d H:i:s', $timestamp );
129
+ }
130
+
131
+ /**
132
+ * Available jobs.
133
+ */
134
+ public function available_jobs() {
135
+ global $wpdb;
136
+
137
+ $now = $this->datetime();
138
+ $sql = $wpdb->prepare( "
139
+ SELECT COUNT(*) FROM {$this->table}
140
+ WHERE available_at <= %s"
141
+ , $now );
142
+
143
+ return $wpdb->get_var( $sql );
144
+ }
145
+
146
+ /**
147
+ * Available jobs.
148
+ */
149
+ public function failed_jobs() {
150
+ global $wpdb;
151
+
152
+ return $wpdb->get_var( "SELECT COUNT(*) FROM {$this->failed_table}" );
153
+ }
154
+
155
+ /**
156
+ * Restart failed jobs.
157
+ */
158
+ public function restart_failed_jobs() {
159
+ global $wpdb;
160
+
161
+ $count = 0;
162
+ $jobs = $wpdb->get_results( "SELECT * FROM {$this->failed_table}" );
163
+
164
+ foreach ( $jobs as $job ) {
165
+ $this->push( maybe_unserialize( $job->job ) );
166
+ $wpdb->delete( $this->failed_table, array(
167
+ 'id' => $job->id,
168
+ ) );
169
+
170
+ $count++;
171
+ }
172
+
173
+ return $count;
174
+ }
175
+
176
+ /**
177
+ * Get next job.
178
+ */
179
+ public function get_next_job() {
180
+ global $wpdb;
181
+
182
+ $this->maybe_release_locked_jobs();
183
+
184
+ $now = $this->datetime();
185
+ $sql = $wpdb->prepare( "
186
+ SELECT * FROM {$this->table}
187
+ WHERE locked = 0
188
+ AND available_at <= %s"
189
+ , $now );
190
+
191
+ return $wpdb->get_row( $sql );
192
+ }
193
+
194
+ /**
195
+ * Maybe release locked jobs.
196
+ */
197
+ protected function maybe_release_locked_jobs() {
198
+ global $wpdb;
199
+
200
+ $expired = $this->datetime( - $this->release_time );
201
+
202
+ $sql = $wpdb->prepare( "
203
+ UPDATE {$this->table}
204
+ SET attempts = attempts + 1, locked = 0, locked_at = NULL
205
+ WHERE locked = 1
206
+ AND locked_at <= %s"
207
+ , $expired );
208
+
209
+ $wpdb->query( $sql );
210
+ }
211
+
212
+ /**
213
+ * Lock job.
214
+ *
215
+ * @param object $job
216
+ */
217
+ public function lock_job( $job ) {
218
+ global $wpdb;
219
+
220
+ $data = array(
221
+ 'locked' => 1,
222
+ 'locked_at' => $this->datetime(),
223
+ );
224
+ $where = array(
225
+ 'id' => $job->id,
226
+ );
227
+
228
+ $wpdb->update( $this->table, $data, $where );
229
+ }
230
+ }
231
+ }
index.php ADDED
@@ -0,0 +1 @@
 
1
+ <?php // Silence is golden
languages/mailchimp-woocommerce.pot ADDED
File without changes
mailchimp-woocommerce.php ADDED
@@ -0,0 +1,274 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * The plugin bootstrap file
5
+ *
6
+ * This file is read by WordPress to generate the plugin information in the plugin
7
+ * admin area. This file also includes all of the dependencies used by the plugin,
8
+ * registers the activation and deactivation functions, and defines a function
9
+ * that starts the plugin.
10
+ *
11
+ * @link https://mailchimp.com
12
+ * @since 1.0.0
13
+ * @package MailChimp_Woocommerce
14
+ *
15
+ * @wordpress-plugin
16
+ * Plugin Name: MailChimp WooCommerce
17
+ * Plugin URI: https://mailchimp.com/connect-your-store/
18
+ * Description: MailChimp - WooCommerce plugin
19
+ * Version: 1.0.0
20
+ * Author: MailChimp
21
+ * Author URI: https://mailchimp.com
22
+ * License: GPL-2.0+
23
+ * License URI: http://www.gnu.org/licenses/gpl-2.0.txt
24
+ * Text Domain: mailchimp-woocommerce
25
+ * Domain Path: /languages
26
+ */
27
+
28
+ // If this file is called directly, abort.
29
+ if ( ! defined( 'WPINC' ) ) {
30
+ die;
31
+ }
32
+
33
+ /**
34
+ * @return object
35
+ */
36
+ function mailchimp_environment_variables() {
37
+ return (object) array(
38
+ 'repo' => 'master',
39
+ 'environment' => 'production',
40
+ 'version' => '0.1.11',
41
+ 'slack_token' => false,
42
+ 'slack_channel' => 'mc-woo',
43
+ );
44
+ }
45
+
46
+ /**
47
+ * @return string
48
+ */
49
+ function mailchimp_get_store_id() {
50
+ return md5(get_option('siteurl'));
51
+ }
52
+
53
+ /**
54
+ * @return bool|MailChimpApi
55
+ */
56
+ function mailchimp_get_api() {
57
+ if (($options = get_option('mailchimp-woocommerce', false)) && is_array($options)) {
58
+ if (isset($options['mailchimp_api_key'])) {
59
+ return new MailChimpApi($options['mailchimp_api_key']);
60
+ }
61
+ }
62
+ return false;
63
+ }
64
+
65
+ /**
66
+ * @param $key
67
+ * @param null $default
68
+ * @return null
69
+ */
70
+ function mailchimp_get_option($key, $default = null) {
71
+ $options = get_option('mailchimp-woocommerce');
72
+ if (!is_array($options)) {
73
+ return $default;
74
+ }
75
+ if (!array_key_exists($key, $options)) {
76
+ return $default;
77
+ }
78
+ return $options[$key];
79
+ }
80
+
81
+ /**
82
+ * @param $key
83
+ * @param $default
84
+ * @return mixed|void
85
+ */
86
+ function mailchimp_get_data($key, $default) {
87
+ return get_option('mailchimp-woocommerce-'.$key, $default);
88
+ }
89
+
90
+ /**
91
+ * @param $date
92
+ * @return DateTime
93
+ */
94
+ function mailchimp_date_utc($date) {
95
+ $timezone = wc_timezone_string();
96
+ //$timezone = mailchimp_get_option('store_timezone', 'America/New_York');
97
+ $date = new \DateTime($date, new DateTimeZone($timezone));
98
+ $date->setTimezone(new DateTimeZone('UTC'));
99
+ return $date;
100
+ }
101
+
102
+ /**
103
+ * @param $date
104
+ * @return DateTime
105
+ */
106
+ function mailchimp_date_local($date) {
107
+ $timezone = mailchimp_get_option('store_timezone', 'America/New_York');
108
+ $date = new \DateTime($date, new DateTimeZone('UTC'));
109
+ $date->setTimezone(new DateTimeZone($timezone));
110
+ return $date;
111
+ }
112
+
113
+ /**
114
+ * @param array $data
115
+ * @return mixed
116
+ */
117
+ function mailchimp_array_remove_empty($data) {
118
+ if (empty($data) || !is_array($data)) {
119
+ return array();
120
+ }
121
+ foreach ($data as $key => $value) {
122
+ if ($value === null || $value === '') {
123
+ unset($data[$key]);
124
+ }
125
+ }
126
+ return $data;
127
+ }
128
+
129
+ /**
130
+ * @return array
131
+ */
132
+ function mailchimp_get_timezone_list() {
133
+ $zones_array = array();
134
+ $timestamp = time();
135
+ $current = date_default_timezone_get();
136
+
137
+ foreach(timezone_identifiers_list() as $key => $zone) {
138
+ date_default_timezone_set($zone);
139
+ $zones_array[$key]['zone'] = $zone;
140
+ $zones_array[$key]['diff_from_GMT'] = 'UTC/GMT ' . date('P', $timestamp);
141
+ }
142
+
143
+ date_default_timezone_set($current);
144
+
145
+ return $zones_array;
146
+ }
147
+
148
+ /**
149
+ * The code that runs during plugin activation.
150
+ * This action is documented in includes/class-mailchimp-woocommerce-activator.php
151
+ */
152
+ function activate_mailchimp_woocommerce() {
153
+ require_once plugin_dir_path( __FILE__ ) . 'includes/class-mailchimp-woocommerce-activator.php';
154
+
155
+ MailChimp_Woocommerce_Activator::activate();
156
+ }
157
+
158
+ /**
159
+ * Create the queue tables
160
+ */
161
+ function install_mailchimp_queue()
162
+ {
163
+ require_once plugin_dir_path( __FILE__ ) . 'includes/class-mailchimp-woocommerce-activator.php';
164
+ MailChimp_Woocommerce_Activator::create_queue_tables();
165
+ }
166
+
167
+ /**
168
+ * The code that runs during plugin deactivation.
169
+ * This action is documented in includes/class-mailchimp-woocommerce-deactivator.php
170
+ */
171
+ function deactivate_mailchimp_woocommerce() {
172
+ require_once plugin_dir_path( __FILE__ ) . 'includes/class-mailchimp-woocommerce-deactivator.php';
173
+ MailChimp_Woocommerce_Deactivator::deactivate();
174
+ }
175
+
176
+ /**
177
+ * See if we need to run any updates.
178
+ */
179
+ function run_mailchimp_plugin_updater() {
180
+ if (!class_exists('PucFactory')) {
181
+ require plugin_dir_path( __FILE__ ) . 'includes/plugin-update-checker/plugin-update-checker.php';
182
+ }
183
+
184
+ $env = mailchimp_environment_variables();
185
+
186
+ /** @var \PucGitHubChecker_3_1 $checker */
187
+ $updater = PucFactory::getLatestClassVersion('PucGitHubChecker');
188
+ $checker = new $updater('https://github.com/mailchimp/mc-woocommerce/', __FILE__, $env->repo, 1);
189
+ $checker->handleManualCheck();
190
+ }
191
+
192
+ /**
193
+ * @return \Frlnc\Slack\Logger
194
+ */
195
+ function slack()
196
+ {
197
+ return Frlnc\Slack\Logger::instance();
198
+ }
199
+
200
+ /**
201
+ * @param $action
202
+ * @param $message
203
+ * @param array $data
204
+ * @return array|WP_Error
205
+ */
206
+ function mailchimp_log($action, $message, $data = array())
207
+ {
208
+ $options = MailChimp_Woocommerce::getLoggingConfig();
209
+
210
+ if (!$options->enable_logging || !$options->account_id || !$options->username) {
211
+ return false;
212
+ }
213
+
214
+ $data = array(
215
+ 'account_id' => $options->account_id,
216
+ 'username' => $options->username,
217
+ 'store_domain' => site_url(),
218
+ 'platform' => 'woocommerce',
219
+ 'action' => $action,
220
+ 'message' => $message,
221
+ 'data' => $data,
222
+ );
223
+
224
+ $slack_message = "$action :: $message";
225
+
226
+ if (!empty($data['data'])) {
227
+ $slack_message .= "\n\n".(print_r($data['data'], true));
228
+ }
229
+
230
+ slack()->notice($slack_message);
231
+
232
+ return wp_remote_post($options->endpoint, array(
233
+ 'headers' => array(
234
+ 'Accept: application/json',
235
+ 'Content-Type: application/json'
236
+ ),
237
+ 'body' => json_encode($data),
238
+ ));
239
+ }
240
+
241
+ register_activation_hook( __FILE__, 'activate_mailchimp_woocommerce' );
242
+ register_deactivation_hook( __FILE__, 'deactivate_mailchimp_woocommerce' );
243
+
244
+ /**
245
+ * The core plugin class that is used to define internationalization,
246
+ * admin-specific hooks, and public-facing site hooks.
247
+ */
248
+ require plugin_dir_path( __FILE__ ) . 'includes/class-mailchimp-woocommerce.php';
249
+
250
+ /**
251
+ * Begins execution of the plugin.
252
+ *
253
+ * Since everything within the plugin is registered via hooks,
254
+ * then kicking off the plugin from this point in the file does
255
+ * not affect the page life cycle.
256
+ *
257
+ * @since 1.0.0
258
+ */
259
+ function run_mailchimp_woocommerce() {
260
+ $env = mailchimp_environment_variables();
261
+ $plugin = new MailChimp_Woocommerce($env->environment, $env->version);
262
+ $plugin->run();
263
+ }
264
+
265
+ if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
266
+ $forwarded_address = explode(',',$_SERVER['HTTP_X_FORWARDED_FOR']);
267
+ $_SERVER['REMOTE_ADDR'] = $forwarded_address[0];
268
+ }
269
+
270
+ /** Add the plugin updater function ONLY when they are logged in as admin. */
271
+ add_action('admin_init', 'run_mailchimp_plugin_updater');
272
+
273
+ /** Add all the MailChimp hooks. */
274
+ run_mailchimp_woocommerce();
public/class-mailchimp-woocommerce-public.php ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * The public-facing functionality of the plugin.
5
+ *
6
+ * @link https://mailchimp.com
7
+ * @since 1.0.1
8
+ *
9
+ * @package MailChimp_Woocommerce
10
+ * @subpackage MailChimp_Woocommerce/public
11
+ */
12
+
13
+ /**
14
+ * The public-facing functionality of the plugin.
15
+ *
16
+ * Defines the plugin name, version, and two examples hooks for how to
17
+ * enqueue the admin-specific stylesheet and JavaScript.
18
+ *
19
+ * @package MailChimp_Woocommerce
20
+ * @subpackage MailChimp_Woocommerce/public
21
+ * @author Ryan Hungate <ryan@mailchimp.com>
22
+ */
23
+ class MailChimp_Woocommerce_Public {
24
+
25
+ /**
26
+ * The ID of this plugin.
27
+ *
28
+ * @since 1.0.0
29
+ * @access private
30
+ * @var string $plugin_name The ID of this plugin.
31
+ */
32
+ private $plugin_name;
33
+
34
+ /**
35
+ * The version of this plugin.
36
+ *
37
+ * @since 1.0.0
38
+ * @access private
39
+ * @var string $version The current version of this plugin.
40
+ */
41
+ private $version;
42
+
43
+ /**
44
+ * Initialize the class and set its properties.
45
+ *
46
+ * @since 1.0.0
47
+ * @param string $plugin_name The name of the plugin.
48
+ * @param string $version The version of this plugin.
49
+ */
50
+ public function __construct( $plugin_name, $version ) {
51
+
52
+ $this->plugin_name = $plugin_name;
53
+ $this->version = $version;
54
+
55
+ }
56
+
57
+ /**
58
+ * Register the stylesheets for the public-facing side of the site.
59
+ *
60
+ * @since 1.0.0
61
+ */
62
+ public function enqueue_styles() {
63
+
64
+ /**
65
+ * This function is provided for demonstration purposes only.
66
+ *
67
+ * An instance of this class should be passed to the run() function
68
+ * defined in MailChimp_Woocommerce_Loader as all of the hooks are defined
69
+ * in that particular class.
70
+ *
71
+ * The MailChimp_Woocommerce_Loader will then create the relationship
72
+ * between the defined hooks and the functions defined in this
73
+ * class.
74
+ */
75
+
76
+ wp_enqueue_style( $this->plugin_name, plugin_dir_url( __FILE__ ) . 'css/mailchimp-woocommerce-public.css', array(), $this->version, 'all' );
77
+
78
+ }
79
+
80
+ /**
81
+ * Register the JavaScript for the public-facing side of the site.
82
+ *
83
+ * @since 1.0.0
84
+ */
85
+ public function enqueue_scripts() {
86
+
87
+ /**
88
+ * This function is provided for demonstration purposes only.
89
+ *
90
+ * An instance of this class should be passed to the run() function
91
+ * defined in MailChimp_Woocommerce_Loader as all of the hooks are defined
92
+ * in that particular class.
93
+ *
94
+ * The MailChimp_Woocommerce_Loader will then create the relationship
95
+ * between the defined hooks and the functions defined in this
96
+ * class.
97
+ */
98
+
99
+ wp_register_script($this->plugin_name, plugin_dir_url( __FILE__ ) . 'js/mailchimp-woocommerce-public.js', array( 'jquery' ), $this->version, false);
100
+
101
+ wp_localize_script($this->plugin_name, 'public_data', array(
102
+ 'site_url' => site_url(),
103
+ ));
104
+
105
+ // Enqueued script with localized data.
106
+ wp_enqueue_script($this->plugin_name);
107
+
108
+ }
109
+ }
public/css/mailchimp-woocommerce-public.css ADDED
@@ -0,0 +1,4 @@
 
 
 
 
1
+ /**
2
+ * All of the CSS for your public-facing functionality should be
3
+ * included in this file.
4
+ */
public/index.php ADDED
@@ -0,0 +1 @@
 
1
+ <?php // Silence is golden
public/js/mailchimp-woocommerce-public.js ADDED
@@ -0,0 +1,312 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ var mailchimp;
2
+ var mailchimp_cart;
3
+ var mailchimp_public_data;
4
+
5
+ (function( $ ) {
6
+ 'use strict';
7
+
8
+ /**
9
+ * All of the code for your public-facing JavaScript source
10
+ * should reside in this file.
11
+ *
12
+ * Note: It has been assumed you will write jQuery code here, so the
13
+ * $ function reference has been prepared for usage within the scope
14
+ * of this function.
15
+ *
16
+ * This enables you to define handlers, for when the DOM is ready:
17
+ *
18
+ * $(function() {
19
+ *
20
+ * });
21
+ *
22
+ * When the window is loaded:
23
+ *
24
+ * $( window ).load(function() {
25
+ *
26
+ * });
27
+ *
28
+ * ...and/or other possibilities.
29
+ *
30
+ * Ideally, it is not considered best practise to attach more than a
31
+ * single DOM-ready or window-load handler for a particular page.
32
+ * Although scripts in the WordPress core, Plugins and Themes may be
33
+ * practising this, we should strive to set a better example in our own work.
34
+ */
35
+
36
+ var requestTransport = null;
37
+ var scriptTagCounter = 1, head;
38
+ var storageLife = "30";
39
+ var clientIP = null;
40
+ var saved_ip;
41
+ var script;
42
+
43
+ mailchimp_public_data = public_data || {site_url:document.location.origin};
44
+
45
+ function invokeJsonp(fullUrl, cacheOk)
46
+ {
47
+ var c = cacheOk || true;
48
+ script = buildScriptTag(fullUrl, c);
49
+ if (typeof head != 'object') {
50
+ head = document.getElementsByTagName("head").item(0);
51
+ }
52
+ head.appendChild(script);
53
+ return script;
54
+ }
55
+
56
+ function removeTag(tag)
57
+ {
58
+ if (typeof head != 'object') {
59
+ head = document.getElementsByTagName("head").item(0);
60
+ }
61
+ head.removeChild(script);
62
+ }
63
+
64
+ function buildScriptTag(url, cacheOk)
65
+ {
66
+ var element = document.createElement("script"),
67
+ additionalQueryParams, conjunction,
68
+ actualUrl = url,
69
+ elementId = 'jsonp-script-' + scriptTagCounter++;
70
+ if (!cacheOk) {
71
+ additionalQueryParams = '_=' + (new Date()).getTime();
72
+ conjunction = (url.indexOf('?') == -1) ? '?' : '&';
73
+ actualUrl = url + conjunction + additionalQueryParams;
74
+ }
75
+ element.setAttribute("type", "text/javascript");
76
+ element.setAttribute("src", actualUrl);
77
+ element.setAttribute("id", elementId);
78
+ return element;
79
+ }
80
+
81
+ var mailchimpUtils =
82
+ {
83
+ extend:function (e, t) {
84
+ for (var n in t || {}) {
85
+ if (t.hasOwnProperty(n)) {
86
+ e[n] = t[n]
87
+ }
88
+ }
89
+ return e
90
+ },
91
+ getQueryStringVars:function ()
92
+ {
93
+ var e = window.location.search || "";
94
+ var t = [];
95
+ var n = {};
96
+ e = e.substr(1);
97
+ if (e.length) {
98
+ t = e.split("&");
99
+ for (var r in t)
100
+ {
101
+ var i = t[r];
102
+ if(typeof i !== 'string'){continue;}
103
+ var s = i.split("=");
104
+ var o = s[0];
105
+ var u = s[1];
106
+ if (!o.length)continue;
107
+ if (typeof n[o] === "undefined") {
108
+ n[o] = []
109
+ }
110
+ n[o].push(u)
111
+ }
112
+ }
113
+ return n
114
+ },
115
+ unEscape:function (e) {
116
+ return decodeURIComponent(e)
117
+ },
118
+ escape:function (e) {
119
+ return encodeURIComponent(e)
120
+ },
121
+ createDate:function (e, t) {
122
+ if (!e) {
123
+ e = 0
124
+ }
125
+ var n = new Date;
126
+ var r = t ? n.getDate() - e : n.getDate() + e;
127
+ n.setDate(r);
128
+ return n
129
+ },
130
+ arrayUnique:function (e) {
131
+ var t = e.concat();
132
+ for (var n = 0; n < t.length; ++n) {
133
+ for (var r = n + 1; r < t.length; ++r) {
134
+ if (t[n] === t[r]) {
135
+ t.splice(r, 1)
136
+ }
137
+ }
138
+ }
139
+ return t
140
+ },
141
+ objectCombineUnique:function (e) {
142
+ var t = e[0];
143
+ for (var n = 1; n < e.length; n++) {
144
+ var r = e[n];
145
+ for (var i in r) {
146
+ t[i] = r[i]
147
+ }
148
+ }
149
+ return t
150
+ }
151
+ };
152
+
153
+ var mailchimpStorage = function(e, t)
154
+ {
155
+ var n = function (e, t, r) {
156
+ return 1 === arguments.length ? n.get(e) : n.set(e, t, r)
157
+ };
158
+ n.get = function (t, r) {
159
+ e.cookie !== n._cacheString && n._populateCache();
160
+ return n._cache[t] == undefined ? r : n._cache[t]
161
+ };
162
+ n.defaults = {path:"/"};
163
+ n.set = function (r, i, s) {
164
+ s = {path:s && s.path || n.defaults.path, domain:s && s.domain || n.defaults.domain, expires:s && s.expires || n.defaults.expires, secure:s && s.secure !== t ? s.secure : n.defaults.secure};
165
+ i === t && (s.expires = -1);
166
+ switch (typeof s.expires) {
167
+ case"number":
168
+ s.expires = new Date((new Date).getTime() + 1e3 * s.expires);
169
+ break;
170
+ case"string":
171
+ s.expires = new Date(s.expires)
172
+ }
173
+ r = encodeURIComponent(r) + "=" + (i + "").replace(/[^!#-+\--:<-\[\]-~]/g, encodeURIComponent);
174
+ r += s.path ? ";path=" + s.path : "";
175
+ r += s.domain ? ";domain=" + s.domain : "";
176
+ r += s.expires ? ";expires=" + s.expires.toGMTString() : "";
177
+ r += s.secure ? ";secure" : "";
178
+ e.cookie = r;
179
+ return n
180
+ };
181
+ n.expire = function (e, r) {
182
+ return n.set(e, t, r)
183
+ };
184
+ n._populateCache = function () {
185
+ n._cache = {};
186
+ try {
187
+ n._cacheString = e.cookie;
188
+ for (var r = n._cacheString.split("; "), i = 0; i < r.length; i++) {
189
+ var s = r[i].indexOf("="), o = decodeURIComponent(r[i].substr(0, s)), s = decodeURIComponent(r[i].substr(s + 1));
190
+ n._cache[o] === t && (n._cache[o] = s)
191
+ }
192
+ } catch (e) {
193
+ console.log(e);
194
+ }
195
+ };
196
+ n.enabled = function () {
197
+ var e = "1" === n.set("cookies.js", "1").get("cookies.js");
198
+ n.expire("cookies.js");
199
+ return e
200
+ }();
201
+ return n;
202
+ }(document);
203
+
204
+ var Jsonp = {invoke : invokeJsonp, removeTag: removeTag};
205
+
206
+ mailchimp =
207
+ {
208
+ storage : mailchimpStorage,
209
+ utils : mailchimpUtils
210
+ };
211
+
212
+ function MailChimpCart() {
213
+
214
+ this.email_types = "input[type=email]";
215
+ this.regex_email = /^([A-Za-z0-9_+\-\.])+\@([A-Za-z0-9_\-\.])+\.([A-Za-z]{2,4})$/;
216
+ this.current_email = null;
217
+ this.previous_email = null;
218
+
219
+ this.expireUser = function () {
220
+ this.current_email = null;
221
+ mailchimp.storage.expire('mailchimp.cart.current_email');
222
+ };
223
+
224
+ this.expireSaved = function () {
225
+ mailchimp.storage.expire('mailchimp.cart.items');
226
+ };
227
+
228
+ this.setEmail = function (email) {
229
+ if (this.valueEmail(email)) {
230
+ this.setPreviousEmail(this.getEmail());
231
+ this.current_email = email;
232
+ mailchimp.storage.set('mailchimp.cart.current_email', email);
233
+ }
234
+ };
235
+ this.getEmail = function () {
236
+ if (this.current_email) {
237
+ return this.current_email;
238
+ }
239
+ var current_email = mailchimp.storage.get('mailchimp.cart.current_email', false);
240
+ if (!current_email || !this.valueEmail(current_email)) {
241
+ return false;
242
+ }
243
+ this.current_email = current_email;
244
+ return current_email;
245
+ };
246
+ this.setPreviousEmail = function (prev_email) {
247
+ if (this.valueEmail(prev_email)) {
248
+ mailchimp.storage.set('mailchimp.cart.previous_email', prev_email);
249
+ this.previous_email = prev_email;
250
+ }
251
+ };
252
+ this.valueEmail = function (email) {
253
+ return this.regex_email.test(email);
254
+ };
255
+
256
+ $(document).on("blur", "#billing_email",function() {
257
+ var user = $("#billing_email").val();
258
+ if (!mailchimp_cart.valueEmail(user)) {
259
+ return false;
260
+ }
261
+ mailchimp_cart.setEmail(user);
262
+
263
+ $.ajax({
264
+ beforeSend: function (xhrObj) {
265
+ xhrObj.setRequestHeader("Content-Type", "application/json");
266
+ xhrObj.setRequestHeader("Accept", "application/json");
267
+ },
268
+ crossDomain: true,
269
+ dataType: "json",
270
+ type: 'POST',
271
+ url: mailchimp_public_data.site_url+'?mailchimp-woocommerce[action]=submit-email&mailchimp-woocommerce[submission][email]='+user,
272
+ data: {},
273
+ success: function (responseData, textStatus, jqXHR) {
274
+ console.log('email saved', responseData);
275
+ },
276
+ error: function (responseData, textStatus, errorThrown) {
277
+ mailchimp_cart.post_error = errorThrown;
278
+ console.log('error while saving email', responseData);
279
+ }
280
+ });
281
+ });
282
+
283
+ return this;
284
+ }
285
+
286
+ mailchimp_cart = new MailChimpCart();
287
+
288
+ var qsc = mailchimpUtils.getQueryStringVars();
289
+
290
+ // MailChimp Data //
291
+ if (qsc.mc_cid !== undefined && qsc.mc_eid !== undefined) {
292
+ $.ajax({
293
+ beforeSend: function (xhrObj) {
294
+ xhrObj.setRequestHeader("Content-Type", "application/json");
295
+ xhrObj.setRequestHeader("Accept", "application/json");
296
+ },
297
+ crossDomain: true,
298
+ dataType: "json",
299
+ type: 'POST',
300
+ url: mailchimp_public_data.site_url+'?mailchimp-woocommerce[action]=track-campaign&mailchimp-woocommerce[submission][campaign_id]='+qsc.mc_cid[0]+'&mailchimp-woocommerce[submission][email_id]='+qsc.mc_eid[0],
301
+ data: {},
302
+ success: function (responseData, textStatus, jqXHR) {
303
+ console.log('campaign data saved', responseData);
304
+ },
305
+ error: function (responseData, textStatus, errorThrown) {
306
+ mailchimp_cart.post_error = errorThrown;
307
+ console.log('error while saving campaign data', responseData);
308
+ }
309
+ });
310
+ }
311
+
312
+ })( jQuery );
public/partials/mailchimp-woocommerce-public-display.php ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Provide a public-facing view for the plugin
5
+ *
6
+ * This file is used to markup the public-facing aspects of the plugin.
7
+ *
8
+ * @link https://mailchimp.com
9
+ * @since 1.0.1
10
+ *
11
+ * @package MailChimp_Woocommerce
12
+ * @subpackage MailChimp_Woocommerce/public/partials
13
+ */
14
+ ?>
15
+
16
+ <!-- This file should primarily consist of HTML with a little bit of PHP. -->
uninstall.php ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Fired when the plugin is uninstalled.
5
+ *
6
+ * When populating this file, consider the following flow
7
+ * of control:
8
+ *
9
+ * - This method should be static
10
+ * - Check if the $_REQUEST content actually is the plugin name
11
+ * - Run an admin referrer check to make sure it goes through authentication
12
+ * - Verify the output of $_GET makes sense
13
+ * - Repeat with other user roles. Best directly by using the links/query string parameters.
14
+ * - Repeat things for multisite. Once for a single site in the network, once sitewide.
15
+ *
16
+ * This file may be updated more in future version of the Boilerplate; however, this is the
17
+ * general skeleton and outline for how the file should work.
18
+ *
19
+ * For more information, see the following discussion:
20
+ * https://github.com/tommcfarlin/WordPress-Plugin-Boilerplate/pull/123#issuecomment-28541913
21
+ *
22
+ * @link https://mailchimp.com
23
+ * @since 1.0.1
24
+ *
25
+ * @package MailChimp_Woocommerce
26
+ */
27
+
28
+ // If uninstall not called from WordPress, then exit.
29
+ if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
30
+ exit;
31
+ }
32
+
33
+ delete_option('mailchimp-woocommerce');
34
+ delete_option('mailchimp-woocommerce-errors.store_info');
35
+ delete_option('mailchimp-woocommerce-sync.orders.completed_at');
36
+ delete_option('mailchimp-woocommerce-sync.orders.current_page');
37
+ delete_option('mailchimp-woocommerce-sync.products.completed_at');
38
+ delete_option('mailchimp-woocommerce-sync.products.current_page');
39
+ delete_option('mailchimp-woocommerce-sync.syncing');
40
+ delete_option('mailchimp-woocommerce-sync.started_at');
41
+ delete_option('mailchimp-woocommerce-sync.completed_at');
42
+ delete_option('mailchimp-woocommerce-validation.api.ping');
43
+ delete_option('mailchimp-woocommerce-cached-api-lists');
44
+ delete_option('mailchimp-woocommerce-cached-api-ping-check');