Stream - Version 2.0.3

Version Description

  • January 23, 2015 =

  • New: WP-CLI command now available for querying records via the command line (#499)

  • Tweak: Silently disable Stream during content import (#672)

  • Tweak: Search results now ordered by date instead of relevance (#689)

  • Fix: Handle boolean values appropriately during wp_stream_log_data filter (#680)

  • Fix: Hook into external class load methods on init rather than plugins_loaded (#686)

  • Fix: N/A user not working in exclude rules (#688)

  • Fix: Prevent Notification Rule meta from being saved to all post types (#693)

  • Fix: PHP warning shown for some users when deleting plugins (#695)

Props @fjarrett

Download this release

Release Info

Developer lukecarbis
Plugin Icon 128x128 Stream
Version 2.0.3
Comparing to
See all releases

Version 2.0.3

Files changed (69) hide show
  1. .ci-env.sh +2 -0
  2. LICENSE +339 -0
  3. classes/class-wp-stream-admin.php +1279 -0
  4. classes/class-wp-stream-api.php +370 -0
  5. classes/class-wp-stream-author.php +265 -0
  6. classes/class-wp-stream-connector.php +236 -0
  7. classes/class-wp-stream-connectors.php +177 -0
  8. classes/class-wp-stream-dashboard-widget.php +272 -0
  9. classes/class-wp-stream-date-interval.php +112 -0
  10. classes/class-wp-stream-db.php +166 -0
  11. classes/class-wp-stream-feeds.php +264 -0
  12. classes/class-wp-stream-filter-input.php +115 -0
  13. classes/class-wp-stream-list-table.php +881 -0
  14. classes/class-wp-stream-live-update.php +222 -0
  15. classes/class-wp-stream-log.php +309 -0
  16. classes/class-wp-stream-migrate.php +583 -0
  17. classes/class-wp-stream-pointers.php +174 -0
  18. classes/class-wp-stream-query.php +278 -0
  19. classes/class-wp-stream-record.php +69 -0
  20. classes/class-wp-stream-settings.php +927 -0
  21. connectors/class-wp-stream-connector-acf.php +542 -0
  22. connectors/class-wp-stream-connector-bbpress.php +232 -0
  23. connectors/class-wp-stream-connector-buddypress.php +814 -0
  24. connectors/class-wp-stream-connector-comments.php +594 -0
  25. connectors/class-wp-stream-connector-edd.php +490 -0
  26. connectors/class-wp-stream-connector-editor.php +311 -0
  27. connectors/class-wp-stream-connector-gravityforms.php +729 -0
  28. connectors/class-wp-stream-connector-installer.php +374 -0
  29. connectors/class-wp-stream-connector-jetpack.php +672 -0
  30. connectors/class-wp-stream-connector-media.php +215 -0
  31. connectors/class-wp-stream-connector-menus.php +217 -0
  32. connectors/class-wp-stream-connector-posts.php +350 -0
  33. connectors/class-wp-stream-connector-settings.php +702 -0
  34. connectors/class-wp-stream-connector-stream.php +98 -0
  35. connectors/class-wp-stream-connector-taxonomies.php +225 -0
  36. connectors/class-wp-stream-connector-users.php +340 -0
  37. connectors/class-wp-stream-connector-widgets.php +805 -0
  38. connectors/class-wp-stream-connector-woocommerce.php +776 -0
  39. connectors/class-wp-stream-connector-wordpress-seo.php +487 -0
  40. contributing.md +106 -0
  41. extensions/notifications/class-wp-stream-notifications.php +273 -0
  42. extensions/notifications/includes/adapters/class-wp-stream-notifications-adapter-email.php +63 -0
  43. extensions/notifications/includes/adapters/class-wp-stream-notifications-adapter-push.php +190 -0
  44. extensions/notifications/includes/adapters/class-wp-stream-notifications-adapter-sms.php +98 -0
  45. extensions/notifications/includes/class-wp-stream-notifications-adapter.php +119 -0
  46. extensions/notifications/includes/class-wp-stream-notifications-list-table.php +246 -0
  47. extensions/notifications/includes/class-wp-stream-notifications-matcher.php +511 -0
  48. extensions/notifications/includes/class-wp-stream-notifications-post-type.php +850 -0
  49. extensions/notifications/includes/class-wp-stream-notifications-settings.php +107 -0
  50. extensions/notifications/ui/css/form.css +353 -0
  51. extensions/notifications/ui/images/stream-notifications-example.jpg +0 -0
  52. extensions/notifications/ui/js/form.js +571 -0
  53. extensions/notifications/ui/js/list.js +14 -0
  54. extensions/notifications/views/form-templates.php +132 -0
  55. extensions/reports/class-wp-stream-reports.php +331 -0
  56. extensions/reports/includes/class-wp-stream-reports-charts.php +292 -0
  57. extensions/reports/includes/class-wp-stream-reports-date-interval.php +67 -0
  58. extensions/reports/includes/class-wp-stream-reports-meta-boxes.php +895 -0
  59. extensions/reports/includes/class-wp-stream-reports-settings.php +233 -0
  60. extensions/reports/includes/template-tags.php +116 -0
  61. extensions/reports/ui/css/stream-reports.css +288 -0
  62. extensions/reports/ui/images/stream-reports-example.jpg +0 -0
  63. extensions/reports/ui/js/stream-reports.js +778 -0
  64. extensions/reports/ui/lib/d3/d3.min.js +5 -0
  65. extensions/reports/ui/lib/nvd3/nv.d3.min.css +1 -0
  66. extensions/reports/ui/lib/nvd3/nv.d3.min.js +6 -0
  67. extensions/reports/views/all.php +41 -0
  68. extensions/reports/views/error.php +7 -0
  69. extensions/reports/views/examples.php +133 -0
.ci-env.sh ADDED
@@ -0,0 +1,2 @@
 
 
1
+ export WPCS_STANDARD=WordPress-Extra
2
+ export PHPCS_IGNORE='tests/*,includes/lib/*,dev-lib/*'
LICENSE 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., <http://fsf.org/>
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
+ {description}
294
+ Copyright (C) {year} {fullname}
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.
classes/class-wp-stream-admin.php ADDED
@@ -0,0 +1,1279 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class WP_Stream_Admin {
4
+
5
+ /**
6
+ * Menu page screen id
7
+ *
8
+ * @var string
9
+ */
10
+ public static $screen_id = array();
11
+
12
+ /**
13
+ * List table object
14
+ *
15
+ * @var WP_Stream_List_Table
16
+ */
17
+ public static $list_table = null;
18
+
19
+ /**
20
+ * Option to disable access to Stream
21
+ *
22
+ * @var bool
23
+ */
24
+ public static $disable_access = false;
25
+
26
+ /**
27
+ * URL used for authenticating with Stream
28
+ *
29
+ * @var string
30
+ */
31
+ public static $connect_url;
32
+
33
+ const ADMIN_BODY_CLASS = 'wp_stream_screen';
34
+ const RECORDS_PAGE_SLUG = 'wp_stream';
35
+ const SETTINGS_PAGE_SLUG = 'wp_stream_settings';
36
+ const ACCOUNT_PAGE_SLUG = 'wp_stream_account';
37
+ const ADMIN_PARENT_PAGE = 'admin.php';
38
+ const VIEW_CAP = 'view_stream';
39
+ const SETTINGS_CAP = 'manage_options';
40
+ const UNREAD_COUNT_OPTION_KEY = 'stream_unread_count';
41
+ const LAST_READ_OPTION_KEY = 'stream_last_read';
42
+ const PRELOAD_AUTHORS_MAX = 50;
43
+ const PUBLIC_URL = 'https://wp-stream.com';
44
+
45
+ public static function load() {
46
+ // User and role caps
47
+ add_filter( 'user_has_cap', array( __CLASS__, '_filter_user_caps' ), 10, 4 );
48
+ add_filter( 'role_has_cap', array( __CLASS__, '_filter_role_caps' ), 10, 3 );
49
+
50
+ $home_url = str_ireplace( array( 'http://', 'https://' ), '', home_url() );
51
+ $connect_nonce = wp_create_nonce( 'stream_connect_site-' . sanitize_key( $home_url ) );
52
+
53
+ self::$connect_url = add_query_arg(
54
+ array(
55
+ 'auth' => 'true',
56
+ 'action' => 'connect',
57
+ 'home_url' => urlencode( $home_url ),
58
+ 'plugin_url' => urlencode(
59
+ add_query_arg(
60
+ array(
61
+ 'page' => self::RECORDS_PAGE_SLUG,
62
+ 'nonce' => $connect_nonce,
63
+ ),
64
+ admin_url( self::ADMIN_PARENT_PAGE )
65
+ )
66
+ ),
67
+ ),
68
+ esc_url_raw( untrailingslashit( self::PUBLIC_URL ) . '/pricing/' )
69
+ );
70
+
71
+ $api_key = wp_stream_filter_input( INPUT_GET, 'api_key' );
72
+ $site_uuid = wp_stream_filter_input( INPUT_GET, 'site_uuid' );
73
+
74
+ // Connect
75
+ if ( ! empty( $api_key ) && ! empty( $site_uuid ) ) {
76
+ add_action( 'admin_init', array( __CLASS__, 'save_api_authentication' ) );
77
+ }
78
+
79
+ // Disconnect
80
+ if ( self::ACCOUNT_PAGE_SLUG === wp_stream_filter_input( INPUT_GET, 'page' ) && '1' === wp_stream_filter_input( INPUT_GET, 'disconnect' ) ) {
81
+ add_action( 'admin_init', array( __CLASS__, 'remove_api_authentication' ) );
82
+ }
83
+
84
+ self::$disable_access = apply_filters( 'wp_stream_disable_admin_access', false );
85
+
86
+ // Register settings page
87
+ if ( ! self::$disable_access ) {
88
+ add_action( 'admin_menu', array( __CLASS__, 'register_menu' ) );
89
+ }
90
+
91
+ // Admin notices
92
+ add_action( 'admin_notices', array( __CLASS__, 'admin_notices' ) );
93
+
94
+ // Show connect notice on dashboard and plugins pages
95
+ add_action( 'load-index.php', array( __CLASS__, 'prepare_connect_notice' ) );
96
+ add_action( 'load-plugins.php', array( __CLASS__, 'prepare_connect_notice' ) );
97
+
98
+ // Add admin body class
99
+ add_filter( 'admin_body_class', array( __CLASS__, 'admin_body_class' ) );
100
+
101
+ // Plugin action links
102
+ add_filter( 'plugin_action_links', array( __CLASS__, 'plugin_action_links' ), 10, 2 );
103
+
104
+ // Load admin scripts and styles
105
+ add_action( 'admin_enqueue_scripts', array( __CLASS__, 'admin_enqueue_scripts' ) );
106
+ add_action( 'admin_enqueue_scripts', array( __CLASS__, 'admin_menu_css' ) );
107
+
108
+ // Catch list table results
109
+ add_filter( 'wp_stream_query_results', array( __CLASS__, 'mark_as_read' ), 10, 3 );
110
+
111
+ // Add user option for enabling unread counts
112
+ add_action( 'show_user_profile', array( __CLASS__, 'unread_count_user_option' ) );
113
+ add_action( 'edit_user_profile', array( __CLASS__, 'unread_count_user_option' ) );
114
+
115
+ // Save unread counts user option
116
+ add_action( 'personal_options_update', array( __CLASS__, 'save_unread_count_user_option' ) );
117
+ add_action( 'edit_user_profile_update', array( __CLASS__, 'save_unread_count_user_option' ) );
118
+
119
+ // Delete user-specific transient when user is deleted
120
+ add_action( 'delete_user', array( __CLASS__, 'delete_unread_count_transient' ), 10, 1 );
121
+
122
+ // Reset Streams settings
123
+ add_action( 'wp_ajax_wp_stream_defaults', array( __CLASS__, 'wp_ajax_defaults' ) );
124
+
125
+ // Ajax authors list
126
+ add_action( 'wp_ajax_wp_stream_filters', array( __CLASS__, 'ajax_filters' ) );
127
+
128
+ // Ajax author's name by ID
129
+ add_action( 'wp_ajax_wp_stream_get_filter_value_by_id', array( __CLASS__, 'get_filter_value_by_id' ) );
130
+ }
131
+
132
+ /**
133
+ * Prepare the Connect to Stream prompt
134
+ *
135
+ * @return void
136
+ */
137
+ public static function prepare_connect_notice() {
138
+ if ( ! WP_Stream::is_connected() && ! WP_Stream::is_development_mode() ) {
139
+ wp_enqueue_style( 'wp-stream-connect', WP_STREAM_URL . 'ui/css/connect.css', array(), WP_Stream::VERSION );
140
+ wp_enqueue_script( 'wp-stream-connect', WP_STREAM_URL . 'ui/js/connect.js', array(), WP_Stream::VERSION );
141
+ add_action( 'admin_notices', array( __CLASS__, 'admin_connect_notice' ) );
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Prompt the user to connect to Stream
147
+ *
148
+ * @return void
149
+ */
150
+ public static function admin_connect_notice() {
151
+ if ( ! current_user_can( self::SETTINGS_CAP ) ) {
152
+ return;
153
+ }
154
+
155
+ $dismiss_and_deactivate_url = wp_nonce_url( 'plugins.php?action=deactivate&plugin=' . WP_STREAM_PLUGIN, 'deactivate-plugin_' . WP_STREAM_PLUGIN );
156
+ ?>
157
+ <div id="stream-message" class="updated stream-connect" style="display:block !important;">
158
+
159
+ <div class="stream-message-container">
160
+
161
+ <div class="stream-button-container">
162
+ <a href="<?php echo esc_url( self::$connect_url ) ?>" class="stream-button"><i class="stream-icon"></i><?php _e( 'Connect to Stream', 'stream' ) ?></a>
163
+ </div>
164
+
165
+ <div class="stream-message-text">
166
+ <h4><?php esc_html_e( 'Stream is almost ready!', 'stream' ) ?></h4>
167
+ <p>
168
+ <?php
169
+ $tooltip = sprintf(
170
+ esc_html__( 'Stream only uses your WordPress.com ID during sign up to authorize your account. You can sign up for free at %swordpress.com/signup%s.', 'stream' ),
171
+ '<a href="https://signup.wordpress.com/signup/?user=1" target="_blank">',
172
+ '</a>'
173
+ );
174
+ echo wp_kses_post(
175
+ sprintf(
176
+ esc_html__( 'Connect to Stream with your %sWordPress.com ID%s to see every change made to your site in beautifully organized detail.', 'stream' ),
177
+ '<span class="wp-stream-tooltip-text">',
178
+ '</span><span class="wp-stream-tooltip">' . $tooltip . '</span>' // xss ok
179
+ )
180
+ );
181
+ ?>
182
+ </p>
183
+ </div>
184
+
185
+ <div class="clear"></div>
186
+
187
+ </div>
188
+
189
+ </div>
190
+ <?php
191
+ }
192
+
193
+ /**
194
+ * Output specific update
195
+ *
196
+ * @action admin_notices
197
+ * @return string
198
+ */
199
+ public static function admin_notices() {
200
+ $message = wp_stream_filter_input( INPUT_GET, 'message' );
201
+ $notice = false;
202
+
203
+ switch ( $message ) {
204
+ case 'settings_reset':
205
+ $notice = esc_html__( 'All site settings have been successfully reset.', 'stream' );
206
+ break;
207
+ case 'connected':
208
+ if ( ! WP_Stream_Migrate::show_migrate_notice() ) {
209
+ $notice = sprintf(
210
+ '<strong>%s</strong></p><p>%s',
211
+ esc_html__( 'You have successfully connected to Stream!', 'stream' ),
212
+ esc_html__( 'Check back here regularly to see a history of the changes being made to this site.', 'stream' )
213
+ );
214
+ }
215
+ break;
216
+ }
217
+
218
+ if ( $notice ) {
219
+ WP_Stream::notice( $notice, false );
220
+ }
221
+ }
222
+
223
+ /**
224
+ * Return URL used for account level actions
225
+ *
226
+ * @return string
227
+ */
228
+ public static function account_url( $path = '' ) {
229
+ $account_url = add_query_arg(
230
+ array(
231
+ 'auth' => 'true',
232
+ 'plugin_url' => urlencode(
233
+ add_query_arg(
234
+ array(
235
+ 'page' => self::RECORDS_PAGE_SLUG,
236
+ ),
237
+ admin_url( self::ADMIN_PARENT_PAGE )
238
+ )
239
+ ),
240
+ ),
241
+ esc_url_raw( sprintf( '%s/dashboard/%s', untrailingslashit( self::PUBLIC_URL ), untrailingslashit( $path ) ) )
242
+ );
243
+
244
+ return $account_url;
245
+ }
246
+
247
+ /**
248
+ * Register menu page
249
+ *
250
+ * @action admin_menu
251
+ * @return bool|void
252
+ */
253
+ public static function register_menu() {
254
+ if ( WP_Stream::is_connected() || WP_Stream::is_development_mode() ) {
255
+ $unread_count = self::get_unread_count();
256
+ $menu_title = __( 'Stream', 'stream' );
257
+
258
+ if ( self::unread_enabled_for_user() && ! empty( $unread_count ) ) {
259
+ $formatted_count = ( $unread_count > 99 ) ? __( '99 +', 'stream' ) : absint( $unread_count );
260
+ $menu_title = sprintf( '%s <span class="update-plugins count-%d"><span class="plugin-count">%s</span></span>', esc_html( $menu_title ), absint( $unread_count ), esc_html( $formatted_count ) );
261
+ }
262
+
263
+ self::$screen_id['main'] = add_menu_page(
264
+ __( 'Stream', 'stream' ),
265
+ $menu_title,
266
+ self::VIEW_CAP,
267
+ self::RECORDS_PAGE_SLUG,
268
+ array( __CLASS__, 'render_stream_page' ),
269
+ 'div',
270
+ apply_filters( 'wp_stream_menu_position', '2.999999' ) // Using longtail decimal string to reduce the chance of position conflicts, see Codex
271
+ );
272
+
273
+ self::$screen_id['settings'] = add_submenu_page(
274
+ self::RECORDS_PAGE_SLUG,
275
+ __( 'Stream Settings', 'stream' ),
276
+ __( 'Settings', 'stream' ),
277
+ self::SETTINGS_CAP,
278
+ self::SETTINGS_PAGE_SLUG,
279
+ array( __CLASS__, 'render_settings_page' )
280
+ );
281
+
282
+ self::$screen_id['account'] = add_submenu_page(
283
+ self::RECORDS_PAGE_SLUG,
284
+ __( 'Stream Account', 'stream' ),
285
+ __( 'Account', 'stream' ),
286
+ self::SETTINGS_CAP,
287
+ self::ACCOUNT_PAGE_SLUG,
288
+ array( __CLASS__, 'render_account_page' )
289
+ );
290
+ } else {
291
+ self::$screen_id['connect'] = add_menu_page(
292
+ __( 'Connect to Stream', 'stream' ),
293
+ __( 'Stream', 'stream' ),
294
+ self::SETTINGS_CAP,
295
+ self::RECORDS_PAGE_SLUG,
296
+ array( __CLASS__, 'render_connect_page' ),
297
+ 'div',
298
+ apply_filters( 'wp_stream_menu_position', '2.999999' ) // Using longtail decimal string to reduce the chance of position conflicts, see Codex
299
+ );
300
+ }
301
+
302
+ if ( isset( self::$screen_id['main'] ) ) {
303
+ /**
304
+ * Fires just before the Stream list table is registered.
305
+ *
306
+ * @since 1.4.0
307
+ *
308
+ * @return void
309
+ */
310
+ do_action( 'wp_stream_admin_menu_screens' );
311
+
312
+ // Register the list table early, so it associates the column headers with 'Screen settings'
313
+ add_action( 'load-' . self::$screen_id['main'], array( __CLASS__, 'register_list_table' ) );
314
+ }
315
+ }
316
+
317
+ /**
318
+ * Enqueue scripts/styles for admin screen
319
+ *
320
+ * @action admin_enqueue_scripts
321
+ *
322
+ * @param $hook
323
+ *
324
+ * @return void
325
+ */
326
+ public static function admin_enqueue_scripts( $hook ) {
327
+ wp_register_script( 'select2', WP_STREAM_URL . 'ui/lib/select2/select2.js', array( 'jquery' ), '3.5.2', true );
328
+ wp_register_style( 'select2', WP_STREAM_URL . 'ui/lib/select2/select2.css', array(), '3.5.2' );
329
+ wp_register_script( 'timeago', WP_STREAM_URL . 'ui/lib/timeago/jquery.timeago.js', array(), '1.4.1', true );
330
+
331
+ $locale = strtolower( substr( get_locale(), 0, 2 ) );
332
+ $file_tmpl = 'ui/lib/timeago/locales/jquery.timeago.%s.js';
333
+
334
+ if ( file_exists( WP_STREAM_DIR . sprintf( $file_tmpl, $locale ) ) ) {
335
+ wp_register_script( 'timeago-locale', WP_STREAM_URL . sprintf( $file_tmpl, $locale ), array( 'timeago' ), '1' );
336
+ } else {
337
+ wp_register_script( 'timeago-locale', WP_STREAM_URL . sprintf( $file_tmpl, 'en' ), array( 'timeago' ), '1' );
338
+ }
339
+
340
+ wp_enqueue_style( 'wp-stream-admin', WP_STREAM_URL . 'ui/css/admin.css', array(), WP_Stream::VERSION );
341
+
342
+ $script_screens = array( 'plugins.php', 'user-edit.php', 'user-new.php', 'profile.php' );
343
+
344
+ if ( 'index.php' === $hook ) {
345
+ wp_enqueue_script( 'wp-stream-dashboard', WP_STREAM_URL . 'ui/js/dashboard.js', array( 'jquery' ), WP_Stream::VERSION );
346
+ wp_enqueue_script( 'wp-stream-live-updates', WP_STREAM_URL . 'ui/js/live-updates.js', array( 'jquery', 'heartbeat' ), WP_Stream::VERSION );
347
+ } elseif ( in_array( $hook, self::$screen_id ) || in_array( $hook, $script_screens ) ) {
348
+ wp_enqueue_script( 'select2' );
349
+ wp_enqueue_style( 'select2' );
350
+
351
+ wp_enqueue_script( 'timeago' );
352
+ wp_enqueue_script( 'timeago-locale' );
353
+
354
+ wp_enqueue_script( 'wp-stream-admin', WP_STREAM_URL . 'ui/js/admin.js', array( 'jquery', 'select2' ), WP_Stream::VERSION );
355
+ wp_enqueue_script( 'wp-stream-live-updates', WP_STREAM_URL . 'ui/js/live-updates.js', array( 'jquery', 'heartbeat' ), WP_Stream::VERSION );
356
+
357
+ wp_localize_script(
358
+ 'wp-stream-admin',
359
+ 'wp_stream',
360
+ array(
361
+ 'i18n' => array(
362
+ 'confirm_defaults' => __( 'Are you sure you want to reset all site settings to default? This cannot be undone.', 'stream' ),
363
+ ),
364
+ 'locale' => esc_js( $locale ),
365
+ 'gmt_offset' => get_option( 'gmt_offset' ),
366
+ )
367
+ );
368
+ }
369
+
370
+ wp_localize_script(
371
+ 'wp-stream-live-updates',
372
+ 'wp_stream_live_updates',
373
+ array(
374
+ 'current_screen' => $hook,
375
+ 'current_page' => isset( $_GET['paged'] ) ? esc_js( $_GET['paged'] ) : '1',
376
+ 'current_order' => isset( $_GET['order'] ) ? esc_js( $_GET['order'] ) : 'desc',
377
+ 'current_query' => json_encode( $_GET ),
378
+ 'current_query_count' => count( $_GET ),
379
+ )
380
+ );
381
+
382
+ if ( WP_Stream_Migrate::show_migrate_notice() ) {
383
+ $limit = absint( WP_Stream_Migrate::$limit );
384
+ $record_count = absint( WP_Stream_Migrate::$record_count );
385
+ $estimated_time = ( $limit < $record_count ) ? round( ( ( $record_count / $limit ) * ( 0.04 * $limit ) ) / 60 ) : 0;
386
+ $migrate_time_message = ( $estimated_time > 1 ) ? sprintf( __( 'This will take about %d minutes.', 'stream' ), absint( $estimated_time ) ) : __( 'This could take a few minutes.', 'stream' );
387
+ $delete_time_message = ( $estimated_time > 1 && is_multisite() ) ? sprintf( __( 'This will take about %d minutes.', 'stream' ), absint( $estimated_time ) ) : __( 'This could take a few minutes.', 'stream' );
388
+
389
+ wp_enqueue_script( 'wp-stream-migrate', WP_STREAM_URL . 'ui/js/migrate.js', array( 'jquery' ), WP_Stream::VERSION );
390
+ wp_localize_script(
391
+ 'wp-stream-migrate',
392
+ 'wp_stream_migrate',
393
+ array(
394
+ 'i18n' => array(
395
+ 'migrate_process_title' => __( 'Migrating Stream Records', 'stream' ),
396
+ 'delete_process_title' => __( 'Deleting Stream Records', 'stream' ),
397
+ 'error_message' => __( 'An unknown error occurred during migration. Please try again later or contact support.', 'stream' ),
398
+ 'migrate_process_message' => __( 'Please do not exit this page until the process has completed.', 'stream' ) . ' ' . esc_html( $migrate_time_message ),
399
+ 'delete_process_message' => __( 'Please do not exit this page until the process has completed.', 'stream' ) . ' ' . esc_html( $delete_time_message ),
400
+ 'confirm_start_migrate' => ( $estimated_time > 1 ) ? sprintf( __( 'Please note: This process will take about %d minutes to complete.', 'stream' ), absint( $estimated_time ) ) : __( 'Please note: This process could take a few minutes to complete.', 'stream' ),
401
+ 'confirm_migrate_reminder' => __( 'Please note: Your existing records will not appear in Stream until you have migrated them to your account.', 'stream' ),
402
+ 'confirm_delete_records' => sprintf( __( 'Are you sure you want to delete all %s existing Stream records without migrating? This will take %s minutes and cannot be undone.', 'stream' ), number_format( WP_Stream_Migrate::$record_count ), ( $estimated_time > 1 && is_multisite() ) ? sprintf( __( 'about %d', 'stream' ), absint( $estimated_time ) ) : __( 'a few', 'stream' ) ),
403
+ ),
404
+ 'chunk_size' => absint( $limit ),
405
+ 'record_count' => absint( $record_count ),
406
+ 'nonce' => wp_create_nonce( 'wp_stream_migrate-' . absint( get_current_blog_id() ) . absint( get_current_user_id() ) ),
407
+ )
408
+ );
409
+ }
410
+
411
+ /**
412
+ * The maximum number of items that can be updated in bulk without receiving a warning.
413
+ *
414
+ * Stream watches for bulk actions performed in the WordPress Admin (such as updating
415
+ * many posts at once) and warns the user before proceeding if the number of items they
416
+ * are attempting to update exceeds this threshold value. Since Stream will try to save
417
+ * a log for each item, it will take longer than usual to complete the operation.
418
+ *
419
+ * The default threshold is 100 items.
420
+ *
421
+ * @return int
422
+ */
423
+ $bulk_actions_threshold = apply_filters( 'wp_stream_bulk_actions_threshold', 100 );
424
+
425
+ wp_enqueue_script( 'wp-stream-global', WP_STREAM_URL . 'ui/js/global.js', array( 'jquery' ), WP_Stream::VERSION );
426
+ wp_localize_script(
427
+ 'wp-stream-global',
428
+ 'wp_stream_global',
429
+ array(
430
+ 'bulk_actions' => array(
431
+ 'i18n' => array(
432
+ 'confirm_action' => sprintf( __( 'Are you sure you want to perform bulk actions on over %s items? This process could take a while to complete.', 'stream' ), number_format( absint( $bulk_actions_threshold ) ) ),
433
+ ),
434
+ 'threshold' => absint( $bulk_actions_threshold ),
435
+ ),
436
+ 'plugins_screen_url' => self_admin_url( 'plugins.php#stream' ),
437
+ )
438
+ );
439
+ }
440
+
441
+ /**
442
+ * Check whether or not the current admin screen belongs to Stream
443
+ *
444
+ * @return bool
445
+ */
446
+ public static function is_stream_screen() {
447
+ global $typenow;
448
+
449
+ if ( is_admin() && ( false !== strpos( wp_stream_filter_input( INPUT_GET, 'page' ), self::RECORDS_PAGE_SLUG ) || WP_Stream_Notifications_Post_Type::POSTTYPE === $typenow ) ) {
450
+ return true;
451
+ }
452
+
453
+ return false;
454
+ }
455
+
456
+ /**
457
+ * Add a specific body class to all Stream admin screens
458
+ *
459
+ * @filter admin_body_class
460
+ *
461
+ * @param string $classes
462
+ *
463
+ * @return string $classes
464
+ */
465
+ public static function admin_body_class( $classes ) {
466
+ if ( self::is_stream_screen() ) {
467
+ $classes .= sprintf( ' %s ', self::ADMIN_BODY_CLASS );
468
+
469
+ if ( WP_Stream::is_connected() || WP_Stream::is_development_mode() ) {
470
+ $classes .= ' wp_stream_connected ';
471
+ } else {
472
+ $classes .= ' wp_stream_disconnected ';
473
+ }
474
+
475
+ if ( WP_Stream_API::is_restricted() ) {
476
+ $classes .= ' wp_stream_restricted ';
477
+ }
478
+ }
479
+
480
+ $settings_pages = array( self::SETTINGS_PAGE_SLUG );
481
+
482
+ if ( isset( $_GET['page'] ) && in_array( $_GET['page'], $settings_pages ) ) {
483
+ $classes .= sprintf( ' %s ', self::SETTINGS_PAGE_SLUG );
484
+ }
485
+
486
+ return $classes;
487
+ }
488
+
489
+ /**
490
+ * Add menu styles for various WP Admin skins
491
+ *
492
+ * @uses wp_add_inline_style()
493
+ * @action admin_enqueue_scripts
494
+ * @return bool true on success false on failure
495
+ */
496
+ public static function admin_menu_css() {
497
+ wp_register_style( 'jquery-ui', '//ajax.googleapis.com/ajax/libs/jqueryui/1.10.1/themes/base/jquery-ui.css', array(), '1.10.1' );
498
+ wp_register_style( 'wp-stream-datepicker', WP_STREAM_URL . 'ui/css/datepicker.css', array( 'jquery-ui' ), WP_Stream::VERSION );
499
+ wp_register_style( 'wp-stream-icons', WP_STREAM_URL . 'ui/stream-icons/style.css', array(), WP_Stream::VERSION );
500
+
501
+ // Make sure we're working off a clean version
502
+ include( ABSPATH . WPINC . '/version.php' );
503
+
504
+ $body_class = self::ADMIN_BODY_CLASS;
505
+ $records_page = self::RECORDS_PAGE_SLUG;
506
+ $stream_url = WP_STREAM_URL;
507
+
508
+ if ( version_compare( $wp_version, '3.8-alpha', '>=' ) ) {
509
+ wp_enqueue_style( 'wp-stream-icons' );
510
+ $css = "
511
+ #toplevel_page_{$records_page} .wp-menu-image:before {
512
+ font-family: 'WP Stream' !important;
513
+ content: '\\73' !important;
514
+ }
515
+ #toplevel_page_{$records_page} .wp-menu-image {
516
+ background-repeat: no-repeat;
517
+ }
518
+ #menu-posts-feedback .wp-menu-image:before {
519
+ font-family: dashicons !important;
520
+ content: '\\f175';
521
+ }
522
+ #adminmenu #menu-posts-feedback div.wp-menu-image {
523
+ background: none !important;
524
+ background-repeat: no-repeat;
525
+ }
526
+ body.{$body_class} #wpbody-content .wrap h2:nth-child(1):before {
527
+ font-family: 'WP Stream' !important;
528
+ content: '\\73';
529
+ padding: 0 8px 0 0;
530
+ }
531
+ ";
532
+ } else {
533
+ $css = "
534
+ #toplevel_page_{$records_page} .wp-menu-image {
535
+ background: url( {$stream_url}ui/stream-icons/menuicon-sprite.png ) 0 90% no-repeat;
536
+ }
537
+ /* Retina Stream Menu Icon */
538
+ @media only screen and (-moz-min-device-pixel-ratio: 1.5),
539
+ only screen and (-o-min-device-pixel-ratio: 3/2),
540
+ only screen and (-webkit-min-device-pixel-ratio: 1.5),
541
+ only screen and (min-device-pixel-ratio: 1.5) {
542
+ #toplevel_page_{$records_page} .wp-menu-image {
543
+ background: url( {$stream_url}ui/stream-icons/menuicon-sprite-2x.png ) 0 90% no-repeat;
544
+ background-size:30px 64px;
545
+ }
546
+ }
547
+ #toplevel_page_{$records_page}.current .wp-menu-image,
548
+ #toplevel_page_{$records_page}.wp-has-current-submenu .wp-menu-image,
549
+ #toplevel_page_{$records_page}:hover .wp-menu-image {
550
+ background-position: top left;
551
+ }
552
+ ";
553
+ }
554
+
555
+ wp_add_inline_style( 'wp-admin', $css );
556
+ }
557
+
558
+ /**
559
+ * Check whether or not the current user should see the unread counter.
560
+ *
561
+ * Defaults to TRUE if user option does not exist.
562
+ *
563
+ * @param int $user_id (optional)
564
+ *
565
+ * @return bool
566
+ */
567
+ public static function unread_enabled_for_user( $user_id = 0 ) {
568
+ $user_id = empty( $user_id ) ? get_current_user_id() : $user_id;
569
+ $enabled = get_user_meta( $user_id, self::UNREAD_COUNT_OPTION_KEY, true );
570
+ $enabled = ( 'off' !== $enabled );
571
+
572
+ return (bool) $enabled;
573
+ }
574
+
575
+ /**
576
+ * Get the unread count for the current user.
577
+ *
578
+ * Results are cached in transient with a 5 min TTL.
579
+ *
580
+ * @return int
581
+ */
582
+ public static function get_unread_count() {
583
+ if ( ! self::unread_enabled_for_user() ) {
584
+ return false;
585
+ }
586
+
587
+ $user_id = get_current_user_id();
588
+ $cache_key = sprintf( '%s_%d', self::UNREAD_COUNT_OPTION_KEY, $user_id );
589
+
590
+ if ( false === ( $count = get_transient( $cache_key ) ) ) {
591
+ $count = 0;
592
+ $last_read = get_user_meta( $user_id, self::LAST_READ_OPTION_KEY, true );
593
+
594
+ if ( ! empty( $last_read ) ) {
595
+ $args = array(
596
+ 'records_per_page' => 101,
597
+ 'author__not_in' => array( $user_id ), // Ignore changes authored by the current user
598
+ 'date_after' => date( 'c', strtotime( $last_read . ' + 1 second' ) ), // Bump time to bypass gte issue
599
+ 'fields' => array( 'created' ), // We don't need the entire record
600
+ );
601
+
602
+ $unread_records = wp_stream_query( $args );
603
+
604
+ $count = empty( $unread_records ) ? 0 : count( $unread_records );
605
+ }
606
+
607
+ set_transient( $cache_key, $count, 5 * 60 ); // TTL 5 min
608
+ }
609
+
610
+ return absint( $count );
611
+ }
612
+
613
+ /**
614
+ * Mark records as read based on Records screen results
615
+ *
616
+ * @filter wp_stream_query_results
617
+ *
618
+ * @param array $results
619
+ * @param array $query
620
+ * @param array $fields
621
+ *
622
+ * @return array
623
+ */
624
+ public static function mark_as_read( $results, $query, $fields ) {
625
+ if ( ! is_admin() ) {
626
+ return $results;
627
+ }
628
+
629
+ $screen = get_current_screen();
630
+ $is_list_table = ( isset( $screen->id ) && sprintf( 'toplevel_page_%s', self::RECORDS_PAGE_SLUG ) === $screen->id );
631
+ $is_first_page = empty( $query['from'] );
632
+ $is_date_desc = ( isset( $query['sort'][0]['created']['order'] ) && 'desc' === $query['sort'][0]['created']['order'] );
633
+
634
+ if ( $is_list_table && $is_first_page && $is_date_desc ) {
635
+ $user_id = get_current_user_id();
636
+ $cache_key = sprintf( '%s_%d', self::UNREAD_COUNT_OPTION_KEY, $user_id );
637
+
638
+ if ( self::unread_enabled_for_user() && isset( $results[0]->created ) ) {
639
+ update_user_meta( $user_id, self::LAST_READ_OPTION_KEY, date( 'c', strtotime( $results[0]->created ) ) );
640
+ }
641
+
642
+ set_transient( $cache_key, 0 ); // No expiration
643
+ }
644
+
645
+ return $results;
646
+ }
647
+
648
+ /**
649
+ * Output for Stream Unread Count field in user profiles.
650
+ *
651
+ * @action show_user_profile
652
+ * @action edit_user_profile
653
+ *
654
+ * @param WP_User $user
655
+ *
656
+ * @return void
657
+ */
658
+ public static function unread_count_user_option( $user ) {
659
+ if ( ! array_intersect( $user->roles, WP_Stream_Settings::$options['general_role_access'] ) ) {
660
+ return;
661
+ }
662
+
663
+ $unread_enabled = self::unread_enabled_for_user();
664
+ ?>
665
+ <table class="form-table">
666
+ <tr>
667
+ <th scope="row">
668
+ <label for="<?php echo esc_attr( self::UNREAD_COUNT_OPTION_KEY ) ?>">
669
+ <?php esc_html_e( 'Stream Unread Count', 'stream' ) ?>
670
+ </label>
671
+ </th>
672
+ <td>
673
+ <label for="<?php echo esc_attr( self::UNREAD_COUNT_OPTION_KEY ) ?>">
674
+ <input type="checkbox" name="<?php echo esc_attr( self::UNREAD_COUNT_OPTION_KEY ) ?>" id="<?php echo esc_attr( self::UNREAD_COUNT_OPTION_KEY ) ?>" value="1" <?php checked( $unread_enabled ) ?>>
675
+ <?php esc_html_e( 'Enabled', 'stream' ) ?>
676
+ </label>
677
+ </td>
678
+ </tr>
679
+ </table>
680
+ <?php
681
+ }
682
+
683
+ /**
684
+ * Saves unread count user meta option in profiles.
685
+ *
686
+ * @action personal_options_update
687
+ * @action edit_user_profile_update
688
+ *
689
+ * @param $user_id
690
+ *
691
+ * @return void
692
+ */
693
+ public static function save_unread_count_user_option( $user_id ) {
694
+ $enabled = wp_stream_filter_input( INPUT_POST, self::UNREAD_COUNT_OPTION_KEY );
695
+ $enabled = ( '1' === $enabled ) ? 'on' : 'off';
696
+
697
+ update_user_meta( $user_id, self::UNREAD_COUNT_OPTION_KEY, $enabled );
698
+ }
699
+
700
+ /**
701
+ * Delete user-specific transient when a user is deleted
702
+ *
703
+ * @action delete_user
704
+ *
705
+ * @param int $user_id
706
+ *
707
+ * @return void
708
+ */
709
+ public static function delete_unread_count_transient( $user_id ) {
710
+ $cache_key = sprintf( '%s_%d', self::UNREAD_COUNT_OPTION_KEY, $user_id );
711
+
712
+ delete_transient( $cache_key );
713
+ }
714
+
715
+ /**
716
+ * @filter plugin_action_links
717
+ */
718
+ public static function plugin_action_links( $links, $file ) {
719
+ if ( plugin_basename( WP_STREAM_DIR . 'stream.php' ) === $file ) {
720
+ $admin_page_url = add_query_arg( array( 'page' => self::SETTINGS_PAGE_SLUG ), admin_url( self::ADMIN_PARENT_PAGE ) );
721
+
722
+ $links[] = sprintf( '<a href="%s">%s</a>', esc_url( $admin_page_url ), esc_html__( 'Settings', 'stream' ) );
723
+ }
724
+
725
+ return $links;
726
+ }
727
+
728
+ public static function save_api_authentication() {
729
+ $home_url = str_ireplace( array( 'http://', 'https://' ), '', home_url() );
730
+ $connect_nonce_name = 'stream_connect_site-' . sanitize_key( $home_url );
731
+
732
+ if ( ! isset( $_GET['api_key'] ) || ! isset( $_GET['site_uuid'] ) ) {
733
+ wp_die( 'There was a problem connecting to Stream. Please try again later.', 'stream' );
734
+ }
735
+
736
+ if ( ! isset( $_GET['nonce'] ) || ! wp_verify_nonce( $_GET['nonce'], $connect_nonce_name ) ) {
737
+ wp_die( 'Doing it wrong.', 'stream' );
738
+ }
739
+
740
+ WP_Stream::$api->site_uuid = wp_stream_filter_input( INPUT_GET, 'site_uuid' );
741
+ WP_Stream::$api->api_key = wp_stream_filter_input( INPUT_GET, 'api_key' );
742
+
743
+ // Verify the API Key and Site UUID
744
+ $site = WP_Stream::$api->get_site();
745
+
746
+ WP_Stream_API::$restricted = ( ! isset( $site->plan->type ) || 'free' === $site->plan->type ) ? 1 : 0;
747
+
748
+ if ( ! isset( $site->site_id ) ) {
749
+ wp_die( 'There was a problem verifying your site with Stream. Please try again later.', 'stream' );
750
+ }
751
+
752
+ if ( ! WP_Stream_API::$restricted ) {
753
+ WP_Stream_Notifications::$instance->on_activation();
754
+ }
755
+
756
+ update_option( WP_Stream_API::SITE_UUID_OPTION_KEY, WP_Stream::$api->site_uuid );
757
+ update_option( WP_Stream_API::API_KEY_OPTION_KEY, WP_Stream::$api->api_key );
758
+ update_option( WP_Stream_API::RESTRICTED_OPTION_KEY, WP_Stream_API::$restricted );
759
+
760
+ do_action( 'wp_stream_site_connected', WP_Stream::$api->site_uuid, WP_Stream::$api->api_key, get_current_blog_id() );
761
+
762
+ $redirect_url = add_query_arg(
763
+ array(
764
+ 'page' => self::RECORDS_PAGE_SLUG,
765
+ 'message' => 'connected',
766
+ ),
767
+ admin_url( self::ADMIN_PARENT_PAGE )
768
+ );
769
+
770
+ wp_safe_redirect( $redirect_url );
771
+
772
+ exit;
773
+ }
774
+
775
+ public static function remove_api_authentication() {
776
+ delete_option( WP_Stream_API::SITE_UUID_OPTION_KEY );
777
+ delete_option( WP_Stream_API::API_KEY_OPTION_KEY );
778
+
779
+ do_action( 'wp_stream_site_disconnected', WP_Stream::$api->site_uuid, WP_Stream::$api->api_key, get_current_blog_id() );
780
+
781
+ WP_Stream::$api->site_uuid = false;
782
+ WP_Stream::$api->api_key = false;
783
+
784
+ if ( '1' !== wp_stream_filter_input( INPUT_GET, 'disconnect' ) ) {
785
+ return;
786
+ }
787
+
788
+ $redirect_url = add_query_arg(
789
+ array(
790
+ 'page' => self::RECORDS_PAGE_SLUG,
791
+ ),
792
+ admin_url( self::ADMIN_PARENT_PAGE )
793
+ );
794
+
795
+ wp_safe_redirect( $redirect_url );
796
+
797
+ exit;
798
+ }
799
+
800
+ public static function get_testimonials() {
801
+ $testimonials = get_site_transient( 'wp_stream_testimonials' );
802
+
803
+ if ( false !== $testimonials ) {
804
+ return $testimonials;
805
+ }
806
+
807
+ $url = sprintf( '%s/wp-content/themes/wp-stream.com/assets/testimonials.json', untrailingslashit( self::PUBLIC_URL ) );
808
+ $request = wp_remote_request( esc_url_raw( $url ), array( 'sslverify' => false ) );
809
+
810
+ if ( ! is_wp_error( $request ) && $request['response']['code'] === 200 ) {
811
+ $testimonials = json_decode( $request['body'], true );
812
+ } else {
813
+ $testimonials = false;
814
+ }
815
+
816
+ // Cache failed attempts as zero, to distinguish them from expired transients
817
+ if ( ! $testimonials ) {
818
+ $testimonials = 0;
819
+ }
820
+
821
+ set_site_transient( 'wp_stream_testimonials', $testimonials, WEEK_IN_SECONDS );
822
+
823
+ return $testimonials;
824
+ }
825
+
826
+ /**
827
+ * Render settings page
828
+ *
829
+ * @return void
830
+ */
831
+ public static function render_settings_page() {
832
+
833
+ $option_key = WP_Stream_Settings::$option_key;
834
+ $form_action = apply_filters( 'wp_stream_settings_form_action', admin_url( 'options.php' ) );
835
+
836
+ $page_title = apply_filters( 'wp_stream_settings_form_title', get_admin_page_title() );
837
+ $page_description = apply_filters( 'wp_stream_settings_form_description', '' );
838
+
839
+ $sections = WP_Stream_Settings::get_fields();
840
+ $active_tab = wp_stream_filter_input( INPUT_GET, 'tab' );
841
+
842
+ wp_enqueue_script(
843
+ 'stream-settings',
844
+ plugins_url( '../ui/js/settings.js', __FILE__ ),
845
+ array( 'jquery' ),
846
+ WP_Stream::VERSION,
847
+ true
848
+ );
849
+ ?>
850
+ <div class="wrap">
851
+ <h2><?php echo esc_html( $page_title ) ?></h2>
852
+
853
+ <?php if ( ! empty( $page_description ) ) : ?>
854
+ <p><?php echo esc_html( $page_description ) ?></p>
855
+ <?php endif; ?>
856
+
857
+ <?php settings_errors() ?>
858
+
859
+ <?php if ( count( $sections ) > 1 ) : ?>
860
+ <h2 class="nav-tab-wrapper">
861
+ <?php $i = 0 ?>
862
+ <?php foreach ( $sections as $section => $data ) : ?>
863
+ <?php $i ++ ?>
864
+ <?php $is_active = ( ( 1 === $i && ! $active_tab ) || $active_tab === $section ) ?>
865
+ <a href="<?php echo esc_url( add_query_arg( 'tab', $section ) ) ?>" class="nav-tab<?php if ( $is_active ) { echo esc_attr( ' nav-tab-active' ); } ?>">
866
+ <?php echo esc_html( $data['title'] ) ?>
867
+ </a>
868
+ <?php endforeach; ?>
869
+ </h2>
870
+ <?php endif; ?>
871
+
872
+ <div class="nav-tab-content" id="tab-content-settings">
873
+ <form method="post" action="<?php echo esc_attr( $form_action ) ?>" enctype="multipart/form-data">
874
+ <div class="settings-sections">
875
+ <?php
876
+ $i = 0;
877
+ foreach ( $sections as $section => $data ) {
878
+ $i++;
879
+ $is_active = ( ( 1 === $i && ! $active_tab ) || $active_tab === $section );
880
+ if ( $is_active ) {
881
+ settings_fields( $option_key );
882
+ do_settings_sections( $option_key );
883
+ }
884
+ }
885
+ ?>
886
+ </div>
887
+ <?php submit_button() ?>
888
+ </form>
889
+ </div>
890
+ </div>
891
+ <?php
892
+ }
893
+
894
+ /**
895
+ * Render account page
896
+ *
897
+ * @return void
898
+ */
899
+ public static function render_account_page() {
900
+ $page_title = apply_filters( 'wp_stream_account_page_title', get_admin_page_title() );
901
+ $date_format = get_option( 'date_format' );
902
+ $site_details = WP_Stream::$api->get_site();
903
+ ?>
904
+ <div class="wrap">
905
+ <h2><?php echo esc_html( $page_title ) ?></h2>
906
+ <div class="postbox">
907
+ <?php
908
+ if ( ! $site_details ) {
909
+ ?>
910
+ <h3><?php esc_html_e( 'Error retrieving account details.' ); ?></h3>
911
+ <div class="plan-details">
912
+ <p><?php esc_html_e( 'If this problem persists, please disconnect from Stream and try connecting again.', 'stream' ); ?></p>
913
+ </div>
914
+ <div class="plan-actions submitbox">
915
+ <a class="submitdelete disconnect" href="<?php echo esc_url( add_query_arg( 'disconnect', '1' ) ); ?>">Disconnect</a>
916
+ </div>
917
+ <?php
918
+ } else {
919
+ $plan_label = __( 'Free', 'stream' );
920
+
921
+ if ( isset( $site_details->plan ) ) {
922
+ if ( 0 === strpos( $site_details->plan->type, 'pro' ) ) {
923
+ $plan_label = __( 'Pro', 'stream' );
924
+ } elseif ( 0 === strpos( $site_details->plan->type, 'standard' ) ) {
925
+ $plan_label = __( 'Standard', 'stream' );
926
+ }
927
+ }
928
+
929
+ if ( 'free' !== $site_details->plan->type ) {
930
+ $next_billing_label = sprintf(
931
+ _x( '$%1$s on %2$s', '1: Price, 2: Renewal date', 'stream' ),
932
+ isset( $site_details->plan->amount ) ? $site_details->plan->amount : 0,
933
+ isset( $site_details->expiry->date ) ? date_i18n( $date_format, strtotime( $site_details->expiry->date ) ) : __( 'N/A', 'stream' )
934
+ );
935
+ }
936
+
937
+ $retention_label = '';
938
+
939
+ if ( 0 == $site_details->plan->retention ) { // Loose comparison needed
940
+ $retention_label = __( 'Unlimited', 'stream' );
941
+ } else {
942
+ $retention_label = sprintf(
943
+ _n( '1 Day', '%s Days', $site_details->plan->retention, 'stream' ),
944
+ $site_details->plan->retention
945
+ );
946
+ }
947
+ ?>
948
+ <h3><?php echo esc_html( $site_details->site_url ); ?></h3>
949
+ <div class="plan-details">
950
+ <table class="form-table">
951
+ <tbody>
952
+ <tr>
953
+ <th><?php _e( 'Plan', 'stream' ); ?></th>
954
+ <td><?php echo esc_html( $plan_label ); ?></td>
955
+ </tr>
956
+ <tr>
957
+ <th><?php _e( 'Activity History', 'stream' ); ?></th>
958
+ <td><?php echo esc_html( $retention_label ); ?></td>
959
+ </tr>
960
+ <?php if ( 'free' !== $site_details->plan->type ) : ?>
961
+ <tr>
962
+ <th><?php _e( 'Next Billing', 'stream' ); ?></th>
963
+ <td><?php echo esc_html( $next_billing_label ); ?></td>
964
+ </tr>
965
+ <?php endif; ?>
966
+ <tr>
967
+ <th><?php _e( 'Created', 'stream' ); ?></th>
968
+ <td><?php echo esc_html( date_i18n( $date_format, strtotime( $site_details->created ) ) ); ?></td>
969
+ </tr>
970
+ <tr>
971
+ <th><?php _e( 'Site ID', 'stream' ); ?></th>
972
+ <td>
973
+ <code class="site-uuid"><?php echo esc_html( WP_Stream::$api->site_uuid ); ?></code>
974
+ </td>
975
+ </tr>
976
+ <tr>
977
+ <th><?php _e( 'API Key', 'stream' ); ?></th>
978
+ <td>
979
+ <code class="api-key"><?php echo esc_html( WP_Stream::$api->api_key ); ?></code>
980
+ </td>
981
+ </tr>
982
+ </tbody>
983
+ </table>
984
+ </div>
985
+ <div class="plan-actions submitbox">
986
+ <?php if ( 'free' === $site_details->plan->type ) : ?>
987
+ <a href="<?php echo esc_url( WP_Stream_Admin::account_url( sprintf( 'upgrade/?site_uuid=%s', WP_Stream::$api->site_uuid ) ) ); ?>" class="button button-primary button-large"><?php _e( 'Upgrade to Pro', 'stream' ) ?></a>
988
+ <?php else : ?>
989
+ <a href="<?php echo esc_url( sprintf( '%s/dashboard/?site_uuid=%s', self::PUBLIC_URL, WP_Stream::$api->site_uuid ) ); ?>" class="button button-primary button-large" target="_blank"><?php _e( 'Modify This Plan', 'stream' ) ?></a>
990
+ <?php endif; ?>
991
+ <a class="submitdelete disconnect" href="<?php echo esc_url( add_query_arg( 'disconnect', '1' ) ); ?>">Disconnect</a>
992
+ </div>
993
+ <?php
994
+ }
995
+ ?>
996
+ </div>
997
+ </div>
998
+ <?php
999
+ }
1000
+
1001
+ /**
1002
+ * Render connect page
1003
+ *
1004
+ * @return void
1005
+ */
1006
+ public static function render_connect_page() {
1007
+ $page_title = apply_filters( 'wp_stream_connect_page_title', get_admin_page_title() );
1008
+
1009
+ if ( $testimonials = self::get_testimonials() ) {
1010
+ $testimonial = $testimonials[ array_rand( $testimonials ) ];
1011
+ }
1012
+
1013
+ wp_enqueue_style( 'wp-stream-connect', WP_STREAM_URL . 'ui/css/connect.css', array(), WP_Stream::VERSION );
1014
+ wp_enqueue_script( 'wp-stream-connect', WP_STREAM_URL . 'ui/js/connect.js', array(), WP_Stream::VERSION );
1015
+ ?>
1016
+ <div id="wp-stream-connect">
1017
+
1018
+ <div class="wrap">
1019
+
1020
+ <div class="stream-connect-container">
1021
+ <a href="<?php echo esc_url( self::$connect_url ) ?>" class="stream-button"><i class="stream-icon"></i><?php _e( 'Connect to Stream', 'stream' ) ?></a>
1022
+ <p>
1023
+ <?php
1024
+ $tooltip = sprintf(
1025
+ esc_html__( 'Stream only uses your WordPress.com ID during sign up to authorize your account. You can sign up for free at %swordpress.com/signup%s.', 'stream' ),
1026
+ '<a href="https://signup.wordpress.com/signup/?user=1" target="_blank">',
1027
+ '</a>'
1028
+ );
1029
+ wp_kses_post(
1030
+ printf(
1031
+ esc_html__( 'with your %sWordPress.com ID%s', 'stream' ),
1032
+ '<span class="wp-stream-tooltip-text">',
1033
+ '</span><span class="wp-stream-tooltip">' . $tooltip . '</span>' // xss ok
1034
+ )
1035
+ );
1036
+ ?>
1037
+ </p>
1038
+ </div>
1039
+
1040
+ <?php if ( isset( $testimonial ) ) : ?>
1041
+ <div class="stream-quotes-container">
1042
+ <p class="stream-quote">&ldquo;<?php echo esc_html( $testimonial['quote'] ) ?>&rdquo;</p>
1043
+ <p class="stream-quote-author">&dash; <?php echo esc_html( $testimonial['author'] ) ?>, <a class="stream-quote-organization" href="<?php echo esc_url( $testimonial['link'] ) ?>"><?php echo esc_html( $testimonial['organization'] ) ?></a></p>
1044
+ </div>
1045
+ <?php endif; ?>
1046
+
1047
+ </div>
1048
+
1049
+ </div>
1050
+ <?php
1051
+ }
1052
+
1053
+ public static function register_list_table() {
1054
+ self::$list_table = new WP_Stream_List_Table( array( 'screen' => self::$screen_id['main'] ) );
1055
+ }
1056
+
1057
+ public static function render_stream_page() {
1058
+ $page_title = __( 'Stream Records', 'stream' );
1059
+
1060
+ self::$list_table->prepare_items();
1061
+
1062
+ echo '<div class="wrap">';
1063
+
1064
+ printf( '<h2>%s</h2>', __( 'Stream Records', 'stream' ) ); // xss ok
1065
+
1066
+ self::$list_table->display();
1067
+
1068
+ echo '</div>';
1069
+ }
1070
+
1071
+ public static function wp_ajax_defaults() {
1072
+ check_ajax_referer( 'stream_nonce', 'wp_stream_nonce' );
1073
+
1074
+ if ( current_user_can( self::SETTINGS_CAP ) ) {
1075
+ self::reset_stream_settings();
1076
+
1077
+ wp_safe_redirect(
1078
+ add_query_arg(
1079
+ array(
1080
+ 'page' => 'wp_stream_settings',
1081
+ 'message' => 'settings_reset',
1082
+ ),
1083
+ admin_url( self::ADMIN_PARENT_PAGE )
1084
+ )
1085
+ );
1086
+
1087
+ exit;
1088
+ } else {
1089
+ wp_die( "You don't have sufficient privileges to do this action." );
1090
+ }
1091
+ }
1092
+
1093
+ private static function reset_stream_settings() {
1094
+ global $wpdb;
1095
+
1096
+ $blogs = wp_get_sites();
1097
+
1098
+ if ( $blogs ) {
1099
+ foreach ( $blogs as $blog ) {
1100
+ switch_to_blog( $blog['blog_id'] );
1101
+ delete_option( WP_Stream_Settings::OPTION_KEY );
1102
+ }
1103
+ restore_current_blog();
1104
+ }
1105
+ }
1106
+
1107
+ private static function _role_can_view_stream( $role ) {
1108
+ if ( in_array( $role, WP_Stream_Settings::$options['general_role_access'] ) ) {
1109
+ return true;
1110
+ }
1111
+
1112
+ return false;
1113
+ }
1114
+
1115
+ /**
1116
+ * Filter user caps to dynamically grant our view cap based on allowed roles
1117
+ *
1118
+ * @filter user_has_cap
1119
+ *
1120
+ * @param $allcaps
1121
+ * @param $caps
1122
+ * @param $args
1123
+ * @param $user
1124
+ *
1125
+ * @return array
1126
+ */
1127
+ public static function _filter_user_caps( $allcaps, $caps, $args, $user = null ) {
1128
+ global $wp_roles;
1129
+
1130
+ $_wp_roles = isset( $wp_roles ) ? $wp_roles : new WP_Roles();
1131
+
1132
+ $user = is_a( $user, 'WP_User' ) ? $user : wp_get_current_user();
1133
+
1134
+ // @see
1135
+ // https://github.com/WordPress/WordPress/blob/c67c9565f1495255807069fdb39dac914046b1a0/wp-includes/capabilities.php#L758
1136
+ $roles = array_unique(
1137
+ array_merge(
1138
+ $user->roles,
1139
+ array_filter(
1140
+ array_keys( $user->caps ),
1141
+ array( $_wp_roles, 'is_role' )
1142
+ )
1143
+ )
1144
+ );
1145
+
1146
+ $stream_view_caps = array(
1147
+ self::VIEW_CAP,
1148
+ WP_Stream_Notifications::VIEW_CAP,
1149
+ WP_Stream_Reports::VIEW_CAP,
1150
+ );
1151
+
1152
+ foreach ( $caps as $cap ) {
1153
+ if ( in_array( $cap, $stream_view_caps ) ) {
1154
+ foreach ( $roles as $role ) {
1155
+ if ( self::_role_can_view_stream( $role ) ) {
1156
+ $allcaps[ $cap ] = true;
1157
+ break 2;
1158
+ }
1159
+ }
1160
+ }
1161
+ }
1162
+
1163
+ return $allcaps;
1164
+ }
1165
+
1166
+ /**
1167
+ * Filter role caps to dynamically grant our view cap based on allowed roles
1168
+ *
1169
+ * @filter role_has_cap
1170
+ *
1171
+ * @param $allcaps
1172
+ * @param $cap
1173
+ * @param $role
1174
+ *
1175
+ * @return array
1176
+ */
1177
+ public static function _filter_role_caps( $allcaps, $cap, $role ) {
1178
+ $stream_view_caps = array(
1179
+ self::VIEW_CAP,
1180
+ WP_Stream_Notifications::VIEW_CAP,
1181
+ WP_Stream_Reports::VIEW_CAP,
1182
+ );
1183
+
1184
+ if ( in_array( $cap, $stream_view_caps ) && self::_role_can_view_stream( $role ) ) {
1185
+ $allcaps[ $cap ] = true;
1186
+ }
1187
+
1188
+ return $allcaps;
1189
+ }
1190
+
1191
+ /**
1192
+ * @action wp_ajax_wp_stream_filters
1193
+ */
1194
+ public static function ajax_filters() {
1195
+ switch ( wp_stream_filter_input( INPUT_GET, 'filter' ) ) {
1196
+ case 'author':
1197
+ $users = array_merge(
1198
+ array( 0 => (object) array( 'display_name' => 'WP-CLI' ) ),
1199
+ get_users()
1200
+ );
1201
+
1202
+ // `search` arg for get_users() is not enough
1203
+ $users = array_filter(
1204
+ $users,
1205
+ function ( $user ) {
1206
+ return false !== mb_strpos( mb_strtolower( $user->display_name ), mb_strtolower( wp_stream_filter_input( INPUT_GET, 'q' ) ) );
1207
+ }
1208
+ );
1209
+
1210
+ if ( count( $users ) > self::PRELOAD_AUTHORS_MAX ) {
1211
+ $users = array_slice( $users, 0, self::PRELOAD_AUTHORS_MAX );
1212
+ // @todo $extra is not used
1213
+ $extra = array(
1214
+ 'id' => 0,
1215
+ 'disabled' => true,
1216
+ 'text' => sprintf( _n( 'One more result...', '%d more results...', $results_count - self::PRELOAD_AUTHORS_MAX, 'stream' ), $results_count - self::PRELOAD_AUTHORS_MAX ),
1217
+ );
1218
+ }
1219
+
1220
+ // Get gravatar / roles for final result set
1221
+ $results = self::get_authors_record_meta( $users );
1222
+
1223
+ break;
1224
+ }
1225
+ if ( isset( $results ) ) {
1226
+ echo json_encode( array_values( $results ) );
1227
+ }
1228
+ die();
1229
+ }
1230
+
1231
+ /**
1232
+ * @action wp_ajax_wp_stream_get_filter_value_by_id
1233
+ */
1234
+ public static function get_filter_value_by_id() {
1235
+ $filter = wp_stream_filter_input( INPUT_POST, 'filter' );
1236
+
1237
+ switch ( $filter ) {
1238
+ case 'author':
1239
+ $id = wp_stream_filter_input( INPUT_POST, 'id' );
1240
+ if ( '0' === $id ) {
1241
+ $value = 'WP-CLI';
1242
+ break;
1243
+ }
1244
+ $user = get_userdata( $id );
1245
+ if ( ! $user || is_wp_error( $user ) ) {
1246
+ $value = '';
1247
+ } else {
1248
+ $value = $user->display_name;
1249
+ }
1250
+ break;
1251
+ default:
1252
+ $value = '';
1253
+ }
1254
+
1255
+ echo json_encode( $value );
1256
+
1257
+ wp_die();
1258
+ }
1259
+
1260
+ public static function get_authors_record_meta( $authors ) {
1261
+ $authors_records = array();
1262
+
1263
+ foreach ( $authors as $user_id => $args ) {
1264
+ $author = new WP_Stream_Author( $user_id );
1265
+ $disabled = isset( $args['disabled'] ) ? $args['disabled'] : null;
1266
+
1267
+ $authors_records[ $user_id ] = array(
1268
+ 'text' => $author->get_display_name(),
1269
+ 'id' => $user_id,
1270
+ 'label' => $author->get_display_name(),
1271
+ 'icon' => $author->get_avatar_src( 32 ),
1272
+ 'title' => '',
1273
+ 'disabled' => $disabled,
1274
+ );
1275
+ }
1276
+
1277
+ return $authors_records;
1278
+ }
1279
+ }
classes/class-wp-stream-api.php ADDED
@@ -0,0 +1,370 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class WP_Stream_API {
4
+
5
+ /**
6
+ * API Key key/identifier
7
+ */
8
+ const API_KEY_OPTION_KEY = 'wp_stream_site_api_key';
9
+
10
+ /**
11
+ * Site UUID key/identifier
12
+ */
13
+ const SITE_UUID_OPTION_KEY = 'wp_stream_site_uuid';
14
+
15
+ /**
16
+ * Site Retricted key/identifier
17
+ */
18
+ const RESTRICTED_OPTION_KEY = 'wp_stream_site_restricted';
19
+
20
+ /**
21
+ * The site's API Key
22
+ *
23
+ * @var string
24
+ */
25
+ public $api_key = false;
26
+
27
+ /**
28
+ * The site's unique identifier
29
+ *
30
+ * @var string
31
+ */
32
+ public $site_uuid = false;
33
+
34
+ /**
35
+ * The site's restriction status
36
+ *
37
+ * @var bool
38
+ */
39
+ public static $restricted = true;
40
+
41
+ /**
42
+ * The API URL
43
+ *
44
+ * @var string
45
+ */
46
+ public $api_url = 'https://api.wp-stream.com';
47
+
48
+ /**
49
+ * The API Version
50
+ *
51
+ * @var string
52
+ */
53
+ public $api_version = '0.0.2';
54
+
55
+ /**
56
+ * Error messages
57
+ *
58
+ * @var array
59
+ */
60
+ public $errors = array();
61
+
62
+ /**
63
+ * Total API calls made per page load
64
+ * Used for debugging and optimization
65
+ *
66
+ * @var array
67
+ */
68
+ public $count = 0;
69
+
70
+ /**
71
+ * Public constructor
72
+ *
73
+ * @return void
74
+ */
75
+ public function __construct() {
76
+ $this->api_key = get_option( self::API_KEY_OPTION_KEY, 0 );
77
+ $this->site_uuid = get_option( self::SITE_UUID_OPTION_KEY, 0 );
78
+ self::$restricted = get_option( self::RESTRICTED_OPTION_KEY, 1 );
79
+ }
80
+
81
+ /**
82
+ * Check if the current site is restricted
83
+ *
84
+ * @param bool Force the API to send a request to check the site's plan type
85
+ *
86
+ * @return bool
87
+ */
88
+ public static function is_restricted( $force_check = false ) {
89
+ if ( $force_check ) {
90
+ $site = WP_Stream::$api->get_site();
91
+
92
+ self::$restricted = ( ! isset( $site->plan->type ) || 'free' === $site->plan->type );
93
+ }
94
+
95
+ return self::$restricted;
96
+ }
97
+
98
+ /**
99
+ * Used to prioritise the streams transport which support non-blocking
100
+ *
101
+ * @filter http_api_transports
102
+ *
103
+ * @return bool
104
+ */
105
+ public static function http_api_transport_priority( $request_order, $args, $url ) {
106
+ if ( isset( $args['blocking'] ) && false === $args['blocking'] ) {
107
+ $request_order = array( 'streams', 'curl' );
108
+ }
109
+
110
+ return $request_order;
111
+ }
112
+
113
+ /**
114
+ * Get the details for a specific site.
115
+ *
116
+ * @param array Returns specified fields only.
117
+ * @param bool Allow API calls to be cached.
118
+ * @param int Set transient expiration in seconds.
119
+ *
120
+ * @return mixed
121
+ */
122
+ public function get_site( $fields = array(), $allow_cache = true, $expiration = 30 ) {
123
+ if ( ! $this->site_uuid ) {
124
+ return false;
125
+ }
126
+
127
+ $params = array();
128
+
129
+ if ( ! empty( $fields ) ) {
130
+ $params['fields'] = implode( ',', $fields );
131
+ }
132
+
133
+ $url = $this->request_url( sprintf( '/sites/%s', urlencode( $this->site_uuid ) ), $params );
134
+ $args = array( 'method' => 'GET' );
135
+ $site = $this->remote_request( $url, $args, $allow_cache, $expiration );
136
+
137
+ if ( $site && ! is_wp_error( $site ) ) {
138
+ $is_restricted = ( ! isset( $site->plan->type ) || 'free' === $site->plan->type ) ? 1 : 0;
139
+
140
+ if ( self::$restricted !== (bool) $is_restricted ) {
141
+ self::$restricted = $is_restricted;
142
+
143
+ update_option( self::RESTRICTED_OPTION_KEY, $is_restricted );
144
+ }
145
+ }
146
+
147
+ return $site;
148
+ }
149
+
150
+ /**
151
+ * Get a specific record.
152
+ *
153
+ * @param string A record ID.
154
+ * @param array Returns specified fields only.
155
+ * @param bool Allow API calls to be cached.
156
+ * @param int Set transient expiration in seconds.
157
+ *
158
+ * @return mixed
159
+ */
160
+ public function get_record( $record_id = false, $fields = array(), $allow_cache = true, $expiration = 30 ) {
161
+ if ( false === $record_id ) {
162
+ return false;
163
+ }
164
+
165
+ if ( ! $this->site_uuid ) {
166
+ return false;
167
+ }
168
+
169
+ $params = array();
170
+
171
+ if ( ! empty( $fields ) ) {
172
+ $params['fields'] = implode( ',', $fields );
173
+ }
174
+
175
+ $url = $this->request_url( sprintf( '/sites/%s/records/%s', urlencode( $this->site_uuid ), urlencode( $record_id ) ), $params );
176
+ $args = array( 'method' => 'GET' );
177
+
178
+ return $this->remote_request( $url, $args, $allow_cache, $expiration );
179
+ }
180
+
181
+ /**
182
+ * Get all records.
183
+ *
184
+ * @param array Returns specified fields only.
185
+ * @param bool Allow API calls to be cached.
186
+ * @param int Set transient expiration in seconds.
187
+ *
188
+ * @return mixed
189
+ */
190
+ public function get_records( $fields = array(), $allow_cache = true, $expiration = 30 ) {
191
+ if ( ! $this->site_uuid ) {
192
+ return false;
193
+ }
194
+
195
+ $params = array();
196
+
197
+ if ( ! empty( $fields ) ) {
198
+ $params['fields'] = implode( ',', $fields );
199
+ }
200
+
201
+ $url = $this->request_url( sprintf( '/sites/%s/records', urlencode( $this->site_uuid ) ), $params );
202
+ $args = array( 'method' => 'GET' );
203
+
204
+ return $this->remote_request( $url, $args, $allow_cache, $expiration );
205
+ }
206
+
207
+ /**
208
+ * Create new records.
209
+ *
210
+ * @param array $records
211
+ * @param bool $blocking
212
+ *
213
+ * @return mixed
214
+ */
215
+ public function new_records( $records, $blocking = false ) {
216
+ if ( ! $this->site_uuid ) {
217
+ return false;
218
+ }
219
+
220
+ $url = $this->request_url( sprintf( '/sites/%s/records', urlencode( $this->site_uuid ) ) );
221
+ $args = array( 'method' => 'POST', 'body' => json_encode( array( 'records' => $records ) ), 'blocking' => (bool) $blocking );
222
+
223
+ return $this->remote_request( $url, $args );
224
+ }
225
+
226
+ /**
227
+ * Search all records.
228
+ *
229
+ * @param array Elasticsearch's Query DSL query object.
230
+ * @param array Returns specified fields only.
231
+ * @param bool Allow API calls to be cached.
232
+ * @param int Set transient expiration in seconds.
233
+ *
234
+ * @return mixed
235
+ */
236
+ public function search( $query = array(), $fields = array(), $sites = array(), $search_type = '', $allow_cache = false, $expiration = 120 ) {
237
+ if ( ! $this->site_uuid ) {
238
+ return false;
239
+ }
240
+
241
+ $body = array();
242
+
243
+ $body['query'] = ! empty( $query ) ? $query : array();
244
+ $body['fields'] = ! empty( $fields ) ? $fields : array();
245
+ $body['sites'] = ! empty( $sites ) ? $sites : array( $this->site_uuid );
246
+ $body['search_type'] = ! empty( $search_type ) ? $search_type : '';
247
+
248
+ $url = $this->request_url( '/search' );
249
+ $args = array( 'method' => 'POST', 'body' => json_encode( (object) $body ) );
250
+
251
+ return $this->remote_request( $url, $args, $allow_cache, $expiration );
252
+ }
253
+
254
+ /**
255
+ * Helper function to create and escape a URL for an API request.
256
+ *
257
+ * @param string The endpoint path, with a starting slash.
258
+ * @param array The $_GET parameters.
259
+ *
260
+ * @return string A properly escaped URL.
261
+ */
262
+ public function request_url( $path, $params = array() ) {
263
+ return esc_url_raw(
264
+ add_query_arg(
265
+ $params,
266
+ untrailingslashit( $this->api_url ) . $path
267
+ )
268
+ );
269
+ }
270
+
271
+ /**
272
+ * Helper function to query the marketplace API via wp_remote_request.
273
+ *
274
+ * @param string The url to access.
275
+ * @param string The method of the request.
276
+ * @param array The headers sent during the request.
277
+ * @param bool Allow API calls to be cached.
278
+ * @param int Set transient expiration in seconds.
279
+ *
280
+ * @return object The results of the wp_remote_request request.
281
+ */
282
+ protected function remote_request( $url = '', $args = array(), $allow_cache = true, $expiration = 300 ) {
283
+ if ( empty( $url ) || empty( $this->api_key ) ) {
284
+ return false;
285
+ }
286
+
287
+ $defaults = array(
288
+ 'headers' => array(),
289
+ 'method' => 'GET',
290
+ 'body' => '',
291
+ 'sslverify' => true,
292
+ );
293
+
294
+ $this->count++;
295
+
296
+ $args = wp_parse_args( $args, $defaults );
297
+
298
+ $args['headers']['Stream-Site-API-Key'] = $this->api_key;
299
+ $args['headers']['Accept-Version'] = $this->api_version;
300
+ $args['headers']['Content-Type'] = 'application/json';
301
+
302
+ if ( WP_Stream::is_development_mode() ) {
303
+ $args['blocking'] = true;
304
+ }
305
+
306
+ add_filter( 'http_api_transports', array( __CLASS__, 'http_api_transport_priority' ), 10, 3 );
307
+
308
+ $transient = 'wp_stream_' . md5( $url );
309
+
310
+ if ( 'GET' === $args['method'] && $allow_cache ) {
311
+ if ( false === ( $request = get_transient( $transient ) ) ) {
312
+ $request = wp_remote_request( $url, $args );
313
+
314
+ set_transient( $transient, $request, $expiration );
315
+ }
316
+ } else {
317
+ $request = wp_remote_request( $url, $args );
318
+ }
319
+
320
+ remove_filter( 'http_api_transports', array( __CLASS__, 'http_api_transport_priority' ), 10 );
321
+
322
+ // Return early if the request is non blocking
323
+ if ( isset( $args['blocking'] ) && false === $args['blocking'] ) {
324
+ return true;
325
+ }
326
+
327
+ if ( ! is_wp_error( $request ) ) {
328
+ /**
329
+ * Filter the request data of the API response.
330
+ *
331
+ * Does not fire on non-blocking requests.
332
+ *
333
+ * @since 2.0.0
334
+ *
335
+ * @param string $url
336
+ * @param array $args
337
+ *
338
+ * @return array
339
+ */
340
+ $data = apply_filters( 'wp_stream_api_request_data', json_decode( $request['body'] ), $url, $args );
341
+
342
+ // Loose comparison needed
343
+ if ( 200 == $request['response']['code'] || 201 == $request['response']['code'] ) {
344
+ return $data;
345
+ } else {
346
+ // Disconnect if unauthorized or no longer exists, loose comparison needed
347
+ if ( 403 == $request['response']['code'] || 410 == $request['response']['code'] ) {
348
+ WP_Stream_Admin::remove_api_authentication();
349
+ }
350
+
351
+ $this->errors['errors']['http_code'] = $request['response']['code'];
352
+ }
353
+
354
+ if ( isset( $data->error ) ) {
355
+ $this->errors['errors']['api_error'] = $data->error;
356
+ }
357
+ } else {
358
+ $this->errors['errors']['remote_request_error'] = $request->get_error_message();
359
+
360
+ WP_Stream::notice( sprintf( '<strong>%s</strong> %s.', __( 'Stream API Error.', 'stream' ), $this->errors['errors']['remote_request_error'] ) );
361
+ }
362
+
363
+ if ( ! empty( $this->errors ) ) {
364
+ delete_transient( $transient );
365
+ }
366
+
367
+ return false;
368
+ }
369
+
370
+ }
classes/class-wp-stream-author.php ADDED
@@ -0,0 +1,265 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class WP_Stream_Author {
4
+
5
+ /**
6
+ * @var int
7
+ */
8
+ public $id;
9
+
10
+ /**
11
+ * @var array
12
+ */
13
+ public $meta = array();
14
+
15
+ /**
16
+ * @var WP_User
17
+ */
18
+ protected $user;
19
+
20
+ /**
21
+ * @param int $user_id
22
+ * @param array $author_meta
23
+ */
24
+ function __construct( $user_id, $author_meta = array() ) {
25
+ $this->id = $user_id;
26
+ $this->meta = $author_meta;
27
+
28
+ if ( $this->id ) {
29
+ $this->user = new WP_User( $this->id );
30
+ }
31
+ }
32
+
33
+ /**
34
+ * @param string $name
35
+ * @throws Exception
36
+ * @return string|mixed
37
+ */
38
+ function __get( $name ) {
39
+ if ( 'display_name' === $name ) {
40
+ return $this->get_display_name();
41
+ } elseif ( 'avatar_img' === $name ) {
42
+ return $this->get_avatar_img();
43
+ } elseif ( 'avatar_src' === $name ) {
44
+ return $this->get_avatar_src();
45
+ } elseif ( 'role' === $name ) {
46
+ return $this->get_role();
47
+ } elseif ( 'agent' === $name ) {
48
+ return $this->get_agent();
49
+ } elseif ( ! empty( $this->user ) && 0 !== $this->user->ID ) {
50
+ return $this->user->$name;
51
+ } else {
52
+ throw new Exception( "Unrecognized magic '$name'" );
53
+ }
54
+ }
55
+
56
+ /**
57
+ * @return string
58
+ */
59
+ function get_display_name() {
60
+ if ( 0 === $this->id ) {
61
+ if ( isset( $this->meta['system_user_name'] ) ) {
62
+ return esc_html( $this->meta['system_user_name'] );
63
+ }
64
+ return esc_html__( 'N/A', 'stream' );
65
+ } else {
66
+ if ( $this->is_deleted() ) {
67
+ if ( ! empty( $this->meta['display_name'] ) ) {
68
+ return $this->meta['display_name'];
69
+ } elseif ( ! empty( $this->meta['user_login'] ) ) {
70
+ return $this->meta['user_login'];
71
+ } else {
72
+ return esc_html__( 'N/A', 'stream' );
73
+ }
74
+ } elseif ( ! empty( $this->user->display_name ) ) {
75
+ return $this->user->display_name;
76
+ } else {
77
+ return $this->user->user_login;
78
+ }
79
+ }
80
+ }
81
+
82
+ /**
83
+ * @return string
84
+ */
85
+ function get_agent() {
86
+ $agent = '';
87
+
88
+ if ( ! empty( $this->meta['agent'] ) ) {
89
+ $agent = $this->meta['agent'];
90
+ } elseif ( ! empty( $this->meta['is_wp_cli'] ) ) {
91
+ $agent = 'wp_cli'; // legacy
92
+ }
93
+
94
+ return $agent;
95
+ }
96
+
97
+ /**
98
+ * Return a Gravatar image as an HTML element.
99
+ *
100
+ * This function will not return an avatar if "Show Avatars" is unchecked in Settings > Discussion.
101
+ *
102
+ * @param int $size (optional) Size of Gravatar to return (in pixels), max is 512, default is 80
103
+ * @return string An img HTML element
104
+ */
105
+ function get_avatar_img( $size = 80 ) {
106
+ if ( ! get_option( 'show_avatars' ) ) {
107
+ return false;
108
+ }
109
+
110
+ if ( 0 === $this->id ) {
111
+ $url = WP_STREAM_URL . 'ui/stream-icons/wp-cli.png';
112
+ $avatar = sprintf( '<img alt="%1$s" src="%2$s" class="avatar avatar-%3$s photo" height="%3$s" width="%3$s">', esc_attr( $this->get_display_name() ), esc_url( $url ), esc_attr( $size ) );
113
+ } else {
114
+ if ( $this->is_deleted() ) {
115
+ $email = $this->meta['user_email'];
116
+ $avatar = get_avatar( $email, $size );
117
+ } else {
118
+ $avatar = get_avatar( $this->id, $size );
119
+ }
120
+ }
121
+
122
+ return $avatar;
123
+ }
124
+
125
+ /**
126
+ * Return the URL of a Gravatar image.
127
+ *
128
+ * @param int $size (optional) Size of Gravatar to return (in pixels), max is 512, default is 80
129
+ * @return string Gravatar image URL
130
+ */
131
+ function get_avatar_src( $size = 80 ) {
132
+ $img = $this->get_avatar_img( $size );
133
+
134
+ if ( ! $img ) {
135
+ return false;
136
+ }
137
+
138
+ if ( 1 === preg_match( '/src=([\'"])(.*?)\1/', $img, $matches ) ) {
139
+ $src = html_entity_decode( $matches[2] );
140
+ } else {
141
+ return false;
142
+ }
143
+
144
+ return $src;
145
+ }
146
+
147
+ /**
148
+ * Tries to find a label for the record's author_role.
149
+ *
150
+ * If the author_role exists, use the label associated with it.
151
+ *
152
+ * Otherwise, if there is a user role label stored as Stream meta then use that.
153
+ *
154
+ * Otherwise, if the user exists, use the label associated with their current role.
155
+ *
156
+ * Otherwise, use the role slug as the label.
157
+ *
158
+ * @return string
159
+ */
160
+ function get_role() {
161
+ global $wp_roles;
162
+
163
+ if ( ! empty( $this->meta['author_role'] ) && isset( $wp_roles->role_names[ $this->meta['author_role'] ] ) ) {
164
+ $author_role = $wp_roles->role_names[ $this->meta['author_role'] ];
165
+ } elseif ( ! empty( $this->meta['user_role_label'] ) ) {
166
+ $author_role = $this->meta['user_role_label'];
167
+ } elseif ( isset( $this->user->roles[0] ) && isset( $wp_roles->role_names[ $this->user->roles[0] ] ) ) {
168
+ $author_role = $wp_roles->role_names[ $this->user->roles[0] ];
169
+ } else {
170
+ $author_role = '';
171
+ }
172
+
173
+ return $author_role;
174
+ }
175
+
176
+ /**
177
+ * @return string
178
+ */
179
+ function get_records_page_url() {
180
+ $url = add_query_arg(
181
+ array(
182
+ 'page' => WP_Stream_Admin::RECORDS_PAGE_SLUG,
183
+ 'author' => absint( $this->id ),
184
+ ),
185
+ self_admin_url( WP_Stream_Admin::ADMIN_PARENT_PAGE )
186
+ );
187
+
188
+ return $url;
189
+ }
190
+
191
+ /**
192
+ * @return bool
193
+ */
194
+ function is_deleted() {
195
+ return ( 0 !== $this->id && 0 === $this->user->ID );
196
+ }
197
+
198
+ /**
199
+ * @return bool
200
+ */
201
+ function is_wp_cli() {
202
+ return ( 'wp_cli' === $this->get_agent() );
203
+ }
204
+
205
+ /**
206
+ * @return string
207
+ */
208
+ function __toString() {
209
+ return $this->get_display_name();
210
+ }
211
+
212
+ /**
213
+ * Look at the environment to detect if an agent is being used
214
+ *
215
+ * @return string
216
+ */
217
+ static function get_current_agent() {
218
+ $agent = '';
219
+
220
+ if ( defined( 'WP_CLI' ) ) {
221
+ $agent = 'wp_cli';
222
+ } elseif ( defined( 'DOING_CRON' ) && DOING_CRON ) {
223
+ $agent = 'wp_cron';
224
+ }
225
+
226
+ /**
227
+ * Filter the current agent string
228
+ *
229
+ * @since 1.4.4
230
+ *
231
+ * @return string
232
+ */
233
+ $agent = apply_filters( 'wp_stream_current_agent', $agent );
234
+
235
+ return $agent;
236
+ }
237
+
238
+ /**
239
+ * @param string $agent
240
+ * @return string
241
+ */
242
+ static function get_agent_label( $agent ) {
243
+ if ( 'wp_cli' === $agent ) {
244
+ $label = esc_html__( 'via WP-CLI', 'stream' );
245
+ } elseif ( 'wp_cron' === $agent ) {
246
+ $label = esc_html__( 'during WP Cron', 'stream' );
247
+ } else {
248
+ $label = '';
249
+ }
250
+
251
+ /**
252
+ * Filter agent labels
253
+ *
254
+ * @since 1.4.4
255
+ *
256
+ * @param string $agent
257
+ *
258
+ * @return string
259
+ */
260
+ $label = apply_filters( 'wp_stream_agent_label', $label, $agent );
261
+
262
+ return $label;
263
+ }
264
+
265
+ }
classes/class-wp-stream-connector.php ADDED
@@ -0,0 +1,236 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ abstract class WP_Stream_Connector {
4
+
5
+ /**
6
+ * Connector slug
7
+ *
8
+ * @var string
9
+ */
10
+ public static $name = null;
11
+
12
+ /**
13
+ * Actions registered for this connector
14
+ *
15
+ * @var array
16
+ */
17
+ public static $actions = array();
18
+
19
+ /**
20
+ * Previous Stream entry in same request
21
+ *
22
+ * @var int
23
+ */
24
+ public static $prev_stream = null;
25
+
26
+ /**
27
+ * Register all context hooks
28
+ *
29
+ * @return void
30
+ */
31
+ public static function register() {
32
+ $class = get_called_class();
33
+
34
+ foreach ( $class::$actions as $action ) {
35
+ add_action( $action, array( $class, 'callback' ), null, 5 );
36
+ }
37
+
38
+ add_filter( 'wp_stream_action_links_' . $class::$name, array( $class, 'action_links' ), 10, 2 );
39
+ }
40
+
41
+ /**
42
+ * Callback for all registered hooks throughout Stream
43
+ * Looks for a class method with the convention: "callback_{action name}"
44
+ *
45
+ * @return void
46
+ */
47
+ public static function callback() {
48
+ $action = current_filter();
49
+ $class = get_called_class();
50
+ $callback = array( $class, 'callback_' . preg_replace( '/[^a-z0-9_\-]/', '_', $action ) );
51
+
52
+ // For the sake of testing, trigger an action with the name of the callback
53
+ if ( defined( 'STREAM_TESTS' ) ) {
54
+ /**
55
+ * Action fires during testing to test the current callback
56
+ *
57
+ * @param array $callback Callback name
58
+ */
59
+ do_action( 'wp_stream_test_' . $callback[1] );
60
+ }
61
+
62
+ // Call the real function
63
+ if ( is_callable( $callback ) ) {
64
+ return call_user_func_array( $callback, func_get_args() );
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Add action links to Stream drop row in admin list screen
70
+ *
71
+ * @filter wp_stream_action_links_{connector}
72
+ * @param array $links Previous links registered
73
+ * @param object $record Stream record
74
+ * @return array Action links
75
+ */
76
+ public static function action_links( $links, $record ) {
77
+ return $links;
78
+ }
79
+
80
+ /**
81
+ * Log handler
82
+ *
83
+ * @param string $message sprintf-ready error message string
84
+ * @param array $args sprintf (and extra) arguments to use
85
+ * @param int $object_id Target object id
86
+ * @param string $context Context of the event
87
+ * @param string $action Action of the event
88
+ * @param int $user_id User responsible for the event
89
+ *
90
+ * @internal param string $action Action performed (stream_action)
91
+ * @return bool
92
+ */
93
+ public static function log( $message, $args, $object_id, $context, $action, $user_id = null ) {
94
+ $class = get_called_class();
95
+ $connector = $class::$name;
96
+
97
+ $data = apply_filters(
98
+ 'wp_stream_log_data',
99
+ compact( 'connector', 'message', 'args', 'object_id', 'context', 'action', 'user_id' )
100
+ );
101
+
102
+ if ( ! $data ) {
103
+ return false;
104
+ } else {
105
+ $connector = $data['connector'];
106
+ $message = $data['message'];
107
+ $args = $data['args'];
108
+ $object_id = $data['object_id'];
109
+ $context = $data['context'];
110
+ $action = $data['action'];
111
+ $user_id = $data['user_id'];
112
+ }
113
+
114
+ return call_user_func_array( array( WP_Stream_Log::get_instance(), 'log' ), compact( 'connector', 'message', 'args', 'object_id', 'context', 'action', 'user_id' ) );
115
+ }
116
+
117
+ /**
118
+ * Save log data till shutdown, so other callbacks would be able to override
119
+ *
120
+ * @param string $handle Special slug to be shared with other actions
121
+ *
122
+ * @internal param mixed $arg1 Extra arguments to sent to log()
123
+ * @internal param mixed $arg2 , etc..
124
+ * @return void
125
+ */
126
+ public static function delayed_log( $handle ) {
127
+ $args = func_get_args();
128
+
129
+ array_shift( $args );
130
+
131
+ self::$delayed[ $handle ] = $args;
132
+
133
+ add_action( 'shutdown', array( __CLASS__, 'delayed_log_commit' ) );
134
+ }
135
+
136
+ /**
137
+ * Commit delayed logs saved by @delayed_log
138
+ *
139
+ * @return void
140
+ */
141
+ public static function delayed_log_commit() {
142
+ foreach ( self::$delayed as $handle => $args ) {
143
+ call_user_func_array( array( __CLASS__, 'log' ) , $args );
144
+ }
145
+ }
146
+
147
+ /**
148
+ * Compare two values and return changed keys if they are arrays
149
+ *
150
+ * @param mixed $old_value Value before change
151
+ * @param mixed $new_value Value after change
152
+ * @param bool|int $deep Get array children changes keys as well, not just parents
153
+ *
154
+ * @return array
155
+ */
156
+ public static function get_changed_keys( $old_value, $new_value, $deep = false ) {
157
+ if ( ! is_array( $old_value ) && ! is_array( $new_value ) ) {
158
+ return array();
159
+ }
160
+
161
+ if ( ! is_array( $old_value ) ) {
162
+ return array_keys( $new_value );
163
+ }
164
+
165
+ if ( ! is_array( $new_value ) ) {
166
+ return array_keys( $old_value );
167
+ }
168
+
169
+ $diff = array_udiff_assoc(
170
+ $old_value,
171
+ $new_value,
172
+ function( $value1, $value2 ) {
173
+ return maybe_serialize( $value1 ) !== maybe_serialize( $value2 );
174
+ }
175
+ );
176
+
177
+ $result = array_keys( $diff );
178
+
179
+ // find unexisting keys in old or new value
180
+ $common_keys = array_keys( array_intersect_key( $old_value, $new_value ) );
181
+ $unique_keys_old = array_values( array_diff( array_keys( $old_value ), $common_keys ) );
182
+ $unique_keys_new = array_values( array_diff( array_keys( $new_value ), $common_keys ) );
183
+ $result = array_merge( $result, $unique_keys_old, $unique_keys_new );
184
+
185
+ // remove numeric indexes
186
+ $result = array_filter(
187
+ $result,
188
+ function( $value ) {
189
+ // check if is not valid number (is_int, is_numeric and ctype_digit are not enough)
190
+ return (string) (int) $value !== (string) $value;
191
+ }
192
+ );
193
+
194
+ $result = array_values( array_unique( $result ) );
195
+
196
+ if ( false === $deep ) {
197
+ return $result; // Return an numerical based array with changed TOP PARENT keys only
198
+ }
199
+
200
+ $result = array_fill_keys( $result, null );
201
+
202
+ foreach ( $result as $key => $val ) {
203
+ if ( in_array( $key, $unique_keys_old ) ) {
204
+ $result[ $key ] = false; // Removed
205
+ }
206
+ elseif ( in_array( $key, $unique_keys_new ) ) {
207
+ $result[ $key ] = true; // Added
208
+ }
209
+ elseif ( $deep ) { // Changed, find what changed, only if we're allowed to explore a new level
210
+ if ( is_array( $old_value[ $key ] ) && is_array( $new_value[ $key ] ) ) {
211
+ $inner = array();
212
+ $parent = $key;
213
+ $deep--;
214
+ $changed = self::get_changed_keys( $old_value[ $key ], $new_value[ $key ], $deep );
215
+ foreach ( $changed as $child => $change ) {
216
+ $inner[ $parent . '::' . $child ] = $change;
217
+ }
218
+ $result[ $key ] = 0; // Changed parent which has a changed children
219
+ $result = array_merge( $result, $inner );
220
+ }
221
+ }
222
+ }
223
+
224
+ return $result;
225
+ }
226
+
227
+ /**
228
+ * Allow connectors to determine if their dependencies is satisfied or not
229
+ *
230
+ * @return bool
231
+ */
232
+ public static function is_dependency_satisfied() {
233
+ return true;
234
+ }
235
+
236
+ }
classes/class-wp-stream-connectors.php ADDED
@@ -0,0 +1,177 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class WP_Stream_Connectors {
4
+
5
+ /**
6
+ * Connectors registered
7
+ *
8
+ * @var array
9
+ */
10
+ public static $connectors = array();
11
+
12
+ /**
13
+ * Contexts registered to Connectors
14
+ *
15
+ * @var array
16
+ */
17
+ public static $contexts = array();
18
+
19
+ /**
20
+ * Action taxonomy terms
21
+ * Holds slug to localized label association
22
+ *
23
+ * @var array
24
+ */
25
+ public static $term_labels = array(
26
+ 'stream_connector' => array(),
27
+ 'stream_context' => array(),
28
+ 'stream_action' => array(),
29
+ );
30
+
31
+ /**
32
+ * Admin notice messages
33
+ *
34
+ * @since 1.2.3
35
+ * @var array
36
+ */
37
+ protected static $admin_notices = array();
38
+
39
+ /**
40
+ * Load built-in connectors
41
+ */
42
+ public static function load() {
43
+ add_action( 'admin_notices', array( __CLASS__, 'admin_notices' ) );
44
+
45
+ $connectors = array(
46
+ // Core
47
+ 'comments',
48
+ 'editor',
49
+ 'installer',
50
+ 'media',
51
+ 'menus',
52
+ 'posts',
53
+ 'settings',
54
+ 'taxonomies',
55
+ 'users',
56
+ 'widgets',
57
+
58
+ // Extras
59
+ 'acf',
60
+ 'bbpress',
61
+ 'buddypress',
62
+ 'edd',
63
+ 'gravityforms',
64
+ 'jetpack',
65
+ 'stream',
66
+ 'woocommerce',
67
+ 'wordpress-seo',
68
+ );
69
+
70
+ $classes = array();
71
+ foreach ( $connectors as $connector ) {
72
+ include_once WP_STREAM_DIR . '/connectors/class-wp-stream-connector-' . $connector .'.php';
73
+ $class = sprintf( 'WP_Stream_Connector_%s', str_replace( '-', '_', $connector ) );
74
+ if ( $class::is_dependency_satisfied() ) {
75
+ $classes[] = $class;
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Allows for adding additional connectors via classes that extend WP_Stream_Connector.
81
+ *
82
+ * @since 0.0.2
83
+ *
84
+ * @param array $classes An array of connector class names.
85
+ */
86
+ self::$connectors = apply_filters( 'wp_stream_connectors', $classes );
87
+
88
+ foreach ( self::$connectors as $connector ) {
89
+ self::$term_labels['stream_connector'][ $connector::$name ] = $connector::get_label();
90
+ }
91
+
92
+ // Get excluded connectors
93
+ $excluded_connectors = array();
94
+
95
+ foreach ( self::$connectors as $connector ) {
96
+ // Check if the connectors extends the WP_Stream_Connector class, if not skip it
97
+ if ( ! is_subclass_of( $connector, 'WP_Stream_Connector' ) ) {
98
+ self::$admin_notices[] = sprintf(
99
+ __( "%s class wasn't loaded because it doesn't extends the %s class.", 'stream' ),
100
+ $connector,
101
+ 'WP_Stream_Connector'
102
+ );
103
+
104
+ continue;
105
+ }
106
+
107
+ // Store connector label
108
+ if ( ! in_array( $connector::$name, self::$term_labels['stream_connector'] ) ) {
109
+ self::$term_labels['stream_connector'][ $connector::$name ] = $connector::get_label();
110
+ }
111
+
112
+ $connector_name = $connector::$name;
113
+ $is_excluded = in_array( $connector_name, $excluded_connectors );
114
+
115
+ /**
116
+ * Allows excluded connectors to be overridden and registered.
117
+ *
118
+ * @since 1.3.0
119
+ *
120
+ * @param bool $is_excluded True if excluded, otherwise false.
121
+ * @param string $connector The current connector's slug.
122
+ * @param array $excluded_connectors An array of all excluded connector slugs.
123
+ */
124
+ $is_excluded_connector = apply_filters( 'wp_stream_check_connector_is_excluded', $is_excluded, $connector_name, $excluded_connectors );
125
+
126
+ if ( $is_excluded_connector ) {
127
+ continue;
128
+ }
129
+
130
+ $connector::register();
131
+
132
+ // Link context labels to their connector
133
+ self::$contexts[ $connector::$name ] = $connector::get_context_labels();
134
+
135
+ // Add new terms to our label lookup array
136
+ self::$term_labels['stream_action'] = array_merge(
137
+ self::$term_labels['stream_action'],
138
+ $connector::get_action_labels()
139
+ );
140
+ self::$term_labels['stream_context'] = array_merge(
141
+ self::$term_labels['stream_context'],
142
+ $connector::get_context_labels()
143
+ );
144
+ }
145
+
146
+ $connectors = self::$term_labels['stream_connector'];
147
+
148
+ /**
149
+ * Fires after all connectors have been registered.
150
+ *
151
+ * @since 1.3.0
152
+ *
153
+ * @param array all register connectors labels array
154
+ */
155
+ do_action( 'wp_stream_after_connectors_registration', $connectors );
156
+ }
157
+
158
+ /**
159
+ * Print admin notices
160
+ *
161
+ * @since 1.2.3
162
+ *
163
+ * @return void
164
+ */
165
+ public static function admin_notices() {
166
+ if ( ! empty( self::$admin_notices ) ) :
167
+ ?>
168
+ <div class="error">
169
+ <?php foreach ( self::$admin_notices as $message ) : ?>
170
+ <?php echo wpautop( esc_html( $message ) ) // xss ok ?>
171
+ <?php endforeach; ?>
172
+ </div>
173
+ <?php
174
+ endif;
175
+ }
176
+
177
+ }
classes/class-wp-stream-dashboard-widget.php ADDED
@@ -0,0 +1,272 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class WP_Stream_Dashboard_Widget {
4
+
5
+ public static function load() {
6
+ // Load Dashboard widget
7
+ add_action( 'wp_dashboard_setup', array( __CLASS__, 'stream_activity' ) );
8
+
9
+ // Dashboard AJAX pagination
10
+ add_action( 'wp_ajax_stream_activity_dashboard_update', array( __CLASS__, 'stream_activity_update_contents' ) );
11
+ }
12
+
13
+ /**
14
+ * Add Stream Activity widget to the dashboard
15
+ *
16
+ * @action wp_dashboard_setup
17
+ */
18
+ public static function stream_activity() {
19
+ if ( ! current_user_can( WP_Stream_Admin::VIEW_CAP ) ) {
20
+ return;
21
+ }
22
+
23
+ wp_add_dashboard_widget(
24
+ 'dashboard_stream_activity',
25
+ esc_html__( 'Stream Activity', 'stream' ),
26
+ array( __CLASS__, 'stream_activity_initial_contents' ),
27
+ array( __CLASS__, 'stream_activity_options' )
28
+ );
29
+ }
30
+
31
+ public static function stream_activity_initial_contents() {
32
+ self::stream_activity_contents();
33
+ }
34
+
35
+ public static function stream_activity_update_contents() {
36
+ $paged = ! empty( $_POST['stream-paged'] ) ? absint( $_POST['stream-paged'] ) : 1;
37
+ self::stream_activity_contents( $paged );
38
+ die;
39
+ }
40
+
41
+ /**
42
+ * Contents of the Stream Activity dashboard widget
43
+ */
44
+ public static function stream_activity_contents( $paged = 1 ) {
45
+ $options = get_option( 'dashboard_stream_activity_options', array() );
46
+ $records_per_page = isset( $options['records_per_page'] ) ? absint( $options['records_per_page'] ) : 5;
47
+ $args = array(
48
+ 'records_per_page' => $records_per_page,
49
+ 'paged' => $paged,
50
+ );
51
+
52
+ $records = wp_stream_query( $args );
53
+ $total_items = WP_Stream::$db->get_found_rows();
54
+
55
+ if ( ! $records ) {
56
+ ?>
57
+ <p class="no-records"><?php esc_html_e( 'Sorry, no activity records were found.', 'stream' ) ?></p>
58
+ <?php
59
+ return;
60
+ }
61
+
62
+ printf(
63
+ '<ul>%s</ul>',
64
+ implode( '', array_map( array( __CLASS__, 'widget_row' ), $records ) )
65
+ );
66
+
67
+ $args = array(
68
+ 'current' => $paged,
69
+ 'total_pages' => absint( ceil( $total_items / $records_per_page ) ), // Cast as an integer, not a float
70
+ );
71
+
72
+ self::pagination( $args );
73
+ }
74
+
75
+ /*
76
+ * Display pagination links for Dashboard Widget
77
+ * Copied from private class WP_List_Table::pagination()
78
+ */
79
+ public static function pagination( $args = array() ) {
80
+ $args = wp_parse_args(
81
+ $args,
82
+ array(
83
+ 'current' => 1,
84
+ 'total_pages' => 1,
85
+ )
86
+ );
87
+
88
+ $current = $args['current'];
89
+ $total_pages = $args['total_pages'];
90
+
91
+ $records_link = add_query_arg(
92
+ array( 'page' => WP_Stream_Admin::RECORDS_PAGE_SLUG ),
93
+ self_admin_url( WP_Stream_Admin::ADMIN_PARENT_PAGE )
94
+ );
95
+
96
+ $html_view_all = sprintf(
97
+ '<a class="%s" title="%s" href="%s">%s</a>',
98
+ 'view-all',
99
+ esc_attr__( 'View all records', 'stream' ),
100
+ esc_url( $records_link ),
101
+ esc_html__( 'View All', 'stream' )
102
+ );
103
+
104
+ $page_links = array();
105
+ $disable_first = '';
106
+ $disable_last = '';
107
+
108
+ if ( 1 === $current ) {
109
+ $disable_first = ' disabled';
110
+ }
111
+
112
+ if ( $current === $total_pages ) {
113
+ $disable_last = ' disabled';
114
+ }
115
+
116
+ $page_links[] = sprintf(
117
+ '<a class="%s" title="%s" href="%s" data-page="1">%s</a>',
118
+ 'first-page' . $disable_first,
119
+ esc_attr__( 'Go to the first page', 'stream' ),
120
+ esc_url( remove_query_arg( 'paged', $records_link ) ),
121
+ '&laquo;'
122
+ );
123
+
124
+ $page_links[] = sprintf(
125
+ '<a class="%s" title="%s" href="%s" data-page="%s">%s</a>',
126
+ 'prev-page' . $disable_first,
127
+ esc_attr__( 'Go to the previous page', 'stream' ),
128
+ esc_url( add_query_arg( 'paged', max( 1, $current - 1 ), $records_link ) ),
129
+ max( 1, $current - 1 ),
130
+ '&lsaquo;'
131
+ );
132
+
133
+ $html_total_pages = sprintf( '<span class="total-pages">%s</span>', number_format_i18n( $total_pages ) );
134
+ $page_links[] = '<span class="paging-input">' . sprintf( _x( '%1$s of %2$s', 'paging', 'stream' ), number_format_i18n( $current ), $html_total_pages ) . '</span>';
135
+
136
+ $page_links[] = sprintf(
137
+ '<a class="%s" title="%s" href="%s" data-page="%s">%s</a>',
138
+ 'next-page' . $disable_last,
139
+ esc_attr__( 'Go to the next page', 'stream' ),
140
+ esc_url( add_query_arg( 'paged', min( $total_pages, $current + 1 ), $records_link ) ),
141
+ min( $total_pages, $current + 1 ),
142
+ '&rsaquo;'
143
+ );
144
+
145
+ $page_links[] = sprintf(
146
+ '<a class="%s" title="%s" href="%s" data-page="%s">%s</a>',
147
+ 'last-page' . $disable_last,
148
+ esc_attr__( 'Go to the last page', 'stream' ),
149
+ esc_url( add_query_arg( 'paged', $total_pages, $records_link ) ),
150
+ $total_pages,
151
+ '&raquo;'
152
+ );
153
+
154
+ $html_pagination_links = '
155
+ <div class="tablenav">
156
+ <div class="tablenav-pages">
157
+ <span class="pagination-links">' . join( "\n", $page_links ) . '</span>
158
+ </div>
159
+ <div class="clear"></div>
160
+ </div>';
161
+
162
+ echo '<div>' . $html_view_all . $html_pagination_links . '</div>'; // xss ok
163
+ }
164
+
165
+ /**
166
+ * Configurable options for the Stream Activity dashboard widget
167
+ */
168
+ public static function stream_activity_options() {
169
+ $options = get_option( 'dashboard_stream_activity_options', array() );
170
+
171
+ if ( 'POST' === $_SERVER['REQUEST_METHOD'] && isset( $_POST['dashboard_stream_activity_options'] ) ) {
172
+ $options['records_per_page'] = absint( $_POST['dashboard_stream_activity_options']['records_per_page'] );
173
+ $options['live_update'] = isset( $_POST['dashboard_stream_activity_options']['live_update'] ) ? 'on' : 'off';;
174
+ update_option( 'dashboard_stream_activity_options', $options );
175
+ }
176
+
177
+ if ( ! isset( $options['records_per_page'] ) ) {
178
+ $options['records_per_page'] = 5;
179
+ }
180
+
181
+ ?>
182
+ <div id="dashboard-stream-activity-options">
183
+ <p>
184
+ <input type="number" step="1" min="1" max="999" class="screen-per-page" name="dashboard_stream_activity_options[records_per_page]" id="dashboard_stream_activity_options[records_per_page]" value="<?php echo absint( $options['records_per_page'] ) ?>">
185
+ <label for="dashboard_stream_activity_options[records_per_page]"><?php esc_html_e( 'Records per page', 'stream' ) ?></label>
186
+ </p>
187
+ <?php $value = isset( $options['live_update'] ) ? $options['live_update'] : 'on'; ?>
188
+ <p>
189
+ <input type="checkbox" name="dashboard_stream_activity_options[live_update]" id="dashboard_stream_activity_options[live_update]" value='on' <?php checked( $value, 'on' ) ?> />
190
+ <label for="dashboard_stream_activity_options[live_update]"><?php esc_html_e( 'Enable live updates', 'stream' ) ?></label>
191
+ </p>
192
+ </div>
193
+ <?php
194
+ }
195
+
196
+ /**
197
+ * Renders rows for Stream Activity Dashboard Widget
198
+ *
199
+ * @param obj Record to be inserted
200
+ * @param int Row number
201
+ *
202
+ * @return string Contents of new row
203
+ */
204
+ public static function widget_row( $item ) {
205
+ $author = new WP_Stream_Author( (int) $item->author, (array) $item->author_meta );
206
+
207
+ $time_author = sprintf(
208
+ _x(
209
+ '%1$s ago by <a href="%2$s">%3$s</a>',
210
+ '1: Time, 2: User profile URL, 3: User display name',
211
+ 'stream'
212
+ ),
213
+ human_time_diff( strtotime( $item->created ) ),
214
+ esc_url( $author->get_records_page_url() ),
215
+ esc_html( $author->get_display_name() )
216
+ );
217
+
218
+ if ( $author->get_agent() ) {
219
+ $time_author .= sprintf( ' %s', WP_Stream_Author::get_agent_label( $author->get_agent() ) );
220
+ }
221
+
222
+ ob_start()
223
+ ?>
224
+ <li data-datetime="<?php echo wp_stream_get_iso_8601_extended_date( strtotime( $item->created ) ) ?>">
225
+ <div class="record-avatar">
226
+ <a href="<?php echo esc_url( $author->get_records_page_url() ) ?>">
227
+ <?php echo $author->get_avatar_img( 72 ); // xss ok ?>
228
+ </a>
229
+ </div>
230
+ <span class="record-meta"><?php echo $time_author; // xss ok ?></span>
231
+ <br/>
232
+ <?php echo esc_html( $item->summary ) ?>
233
+ </li>
234
+ <?php
235
+
236
+ return ob_get_clean();
237
+ }
238
+
239
+ /**
240
+ * Handles Live Updates for Stream Activity Dashboard Widget.
241
+ *
242
+ * @uses gather_updated_items
243
+ *
244
+ * @param array Response to heartbeat
245
+ * @param array Response from heartbeat
246
+ *
247
+ * @return array Data sent to heartbeat
248
+ */
249
+ public static function live_update( $response, $data ) {
250
+ if ( ! isset( $data['wp-stream-heartbeat-last-time'] ) ) {
251
+ return;
252
+ }
253
+
254
+ $send = array();
255
+
256
+ $last_time = $data['wp-stream-heartbeat-last-time'];
257
+
258
+ $updated_items = WP_Stream_Live_Update::gather_updated_items( $last_time );
259
+
260
+ if ( ! empty( $updated_items ) ) {
261
+ ob_start();
262
+ foreach ( $updated_items as $item ) {
263
+ echo self::widget_row( $item ); //xss okay
264
+ }
265
+
266
+ $send = ob_get_clean();
267
+ }
268
+
269
+ return $send;
270
+ }
271
+
272
+ }
classes/class-wp-stream-date-interval.php ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ // Load Carbon to Handle dates much easier
4
+ if ( ! class_exists( 'Carbon\Carbon' ) ) {
5
+ require_once WP_STREAM_INC_DIR . 'lib/Carbon.php';
6
+ }
7
+
8
+ use Carbon\Carbon;
9
+
10
+ class WP_Stream_Date_Interval {
11
+
12
+ /**
13
+ * Contains an array of all available intervals
14
+ *
15
+ * @var array $intervals
16
+ */
17
+ public $intervals;
18
+
19
+ /**
20
+ * Class constructor
21
+ */
22
+ public function __construct() {
23
+ // Get all default intervals
24
+ $this->intervals = $this->get_predefined_intervals();
25
+ }
26
+
27
+ /**
28
+ * @return mixed|void
29
+ */
30
+ public function get_predefined_intervals() {
31
+ $timezone = get_option( 'timezone_string' );
32
+
33
+ if ( empty( $timezone ) ) {
34
+ $gmt_offset = (int) get_option( 'gmt_offset' );
35
+ $timezone = timezone_name_from_abbr( null, $gmt_offset * 3600, true );
36
+ if ( false === $timezone ) {
37
+ $timezone = timezone_name_from_abbr( null, $gmt_offset * 3600, false );
38
+ }
39
+ if ( false === $timezone ) {
40
+ $timezone = null;
41
+ }
42
+ }
43
+
44
+ return apply_filters(
45
+ 'wp_stream_predefined_date_intervals',
46
+ array(
47
+ 'today' => array(
48
+ 'label' => esc_html__( 'Today', 'stream' ),
49
+ 'start' => Carbon::today( $timezone )->startOfDay(),
50
+ 'end' => Carbon::today( $timezone )->endOfDay(),
51
+ ),
52
+ 'yesterday' => array(
53
+ 'label' => esc_html__( 'Yesterday', 'stream' ),
54
+ 'start' => Carbon::today( $timezone )->startOfDay()->subDay(),
55
+ 'end' => Carbon::today( $timezone )->startOfDay()->subSecond(),
56
+ ),
57
+ 'last-7-days' => array(
58
+ 'label' => sprintf( esc_html__( 'Last %d Days', 'stream' ), 7 ),
59
+ 'start' => Carbon::today( $timezone )->subDays( 7 ),
60
+ 'end' => Carbon::today( $timezone ),
61
+ ),
62
+ 'last-14-days' => array(
63
+ 'label' => sprintf( esc_html__( 'Last %d Days', 'stream' ), 14 ),
64
+ 'start' => Carbon::today( $timezone )->subDays( 14 ),
65
+ 'end' => Carbon::today( $timezone ),
66
+ ),
67
+ 'last-30-days' => array(
68
+ 'label' => sprintf( esc_html__( 'Last %d Days', 'stream' ), 30 ),
69
+ 'start' => Carbon::today( $timezone )->subDays( 30 ),
70
+ 'end' => Carbon::today( $timezone ),
71
+ ),
72
+ 'this-month' => array(
73
+ 'label' => esc_html__( 'This Month', 'stream' ),
74
+ 'start' => Carbon::today( $timezone )->startOfMonth(),
75
+ 'end' => Carbon::today( $timezone )->endOfMonth(),
76
+ ),
77
+ 'last-month' => array(
78
+ 'label' => esc_html__( 'Last Month', 'stream' ),
79
+ 'start' => Carbon::today( $timezone )->startOfMonth()->subMonth(),
80
+ 'end' => Carbon::today( $timezone )->startOfMonth()->subSecond(),
81
+ ),
82
+ 'last-3-months' => array(
83
+ 'label' => sprintf( esc_html__( 'Last %d Months', 'stream' ), 3 ),
84
+ 'start' => Carbon::today( $timezone )->subMonths( 3 ),
85
+ 'end' => Carbon::today( $timezone ),
86
+ ),
87
+ 'last-6-months' => array(
88
+ 'label' => sprintf( esc_html__( 'Last %d Months', 'stream' ), 6 ),
89
+ 'start' => Carbon::today( $timezone )->subMonths( 6 ),
90
+ 'end' => Carbon::today( $timezone ),
91
+ ),
92
+ 'last-12-months' => array(
93
+ 'label' => sprintf( esc_html__( 'Last %d Months', 'stream' ), 12 ),
94
+ 'start' => Carbon::today( $timezone )->subMonths( 12 ),
95
+ 'end' => Carbon::today( $timezone ),
96
+ ),
97
+ 'this-year' => array(
98
+ 'label' => esc_html__( 'This Year', 'stream' ),
99
+ 'start' => Carbon::today( $timezone )->startOfYear(),
100
+ 'end' => Carbon::today( $timezone )->endOfYear(),
101
+ ),
102
+ 'last-year' => array(
103
+ 'label' => esc_html__( 'Last Year', 'stream' ),
104
+ 'start' => Carbon::today( $timezone )->startOfYear()->subYear(),
105
+ 'end' => Carbon::today( $timezone )->startOfYear()->subSecond(),
106
+ ),
107
+ ),
108
+ $timezone
109
+ );
110
+ }
111
+
112
+ }
classes/class-wp-stream-db.php ADDED
@@ -0,0 +1,166 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class WP_Stream_DB {
4
+
5
+ /**
6
+ * Meta information returned in the last query
7
+ *
8
+ * @var mixed
9
+ */
10
+ public $query_meta = false;
11
+
12
+ /**
13
+ * Store records
14
+ *
15
+ * @param array $records
16
+ *
17
+ * @return mixed True if updated, false|WP_Error if not
18
+ */
19
+ public function store( $records ) {
20
+ // Take only what's ours!
21
+ $valid_keys = get_class_vars( 'WP_Stream_Record' );
22
+
23
+ // Fill in defaults
24
+ $defaults = array(
25
+ 'type' => 'stream',
26
+ 'site_id' => 1,
27
+ 'blog_id' => 0,
28
+ 'object_id' => 0,
29
+ 'author' => 0,
30
+ 'author_role' => '',
31
+ 'visibility' => 'publish',
32
+ 'ip' => '',
33
+ );
34
+
35
+ foreach ( $records as $key => $record ) {
36
+ $records[ $key ] = array_intersect_key( $record, $valid_keys );
37
+ $records[ $key ] = array_filter( $record );
38
+ $records[ $key ] = wp_parse_args( $record, $defaults );
39
+ }
40
+
41
+ /**
42
+ * Allows modification of record information just before logging occurs.
43
+ *
44
+ * @since 0.2.0
45
+ *
46
+ * @param array $records An array of record data.
47
+ */
48
+ $records = apply_filters( 'wp_stream_record_array', $records );
49
+
50
+ // Allow extensions to handle the saving process
51
+ if ( empty( $records ) ) {
52
+ return false;
53
+ }
54
+
55
+ // TODO: Check/Validate *required* fields
56
+
57
+ $result = $this->insert( $records );
58
+
59
+ if ( $result && ! is_wp_error( $result ) ) {
60
+
61
+ /**
62
+ * Fires when A Post is inserted
63
+ *
64
+ * @since 2.0.0
65
+ *
66
+ * @param int $record_id Inserted record ID
67
+ * @param array $recordarr Array of information on this record
68
+ */
69
+ do_action( 'wp_stream_records_inserted', $records );
70
+ }
71
+
72
+ return true;
73
+ }
74
+
75
+ /**
76
+ * Insert a new record
77
+ *
78
+ * @internal Used by store()
79
+ *
80
+ * @param array $records
81
+ *
82
+ * @return object $response The inserted records
83
+ */
84
+ private function insert( array $records ) {
85
+ return WP_Stream::$api->new_records( $records );
86
+ }
87
+
88
+ /**
89
+ * Query records
90
+ *
91
+ * @internal Used by WP_Stream_Query, and is not designed to be called explicitly
92
+ *
93
+ * @param array $query Query body.
94
+ * @param array $fields Returns specified fields only.
95
+ *
96
+ * @return array List of records that match query
97
+ */
98
+ public function query( $query, $fields ) {
99
+ $response = WP_Stream::$api->search( $query, $fields );
100
+
101
+ if ( empty( $response ) || ! isset( $response->meta ) || ! isset( $response->records ) ) {
102
+ return false;
103
+ }
104
+
105
+ $this->query_meta = $response->meta;
106
+
107
+ $results = (array) $response->records;
108
+
109
+ /**
110
+ * Allows developers to change the final result set of records
111
+ *
112
+ * @since 2.0.0
113
+ *
114
+ * @param array $results
115
+ * @param array $query
116
+ * @param array $fields
117
+ *
118
+ * @return array Filtered array of record results
119
+ */
120
+ return apply_filters( 'wp_stream_query_results', $results, $query, $fields );
121
+ }
122
+
123
+ /**
124
+ * Get total count of the last query using query() method
125
+ *
126
+ * @return integer Total item count
127
+ */
128
+ public function get_found_rows() {
129
+ if ( ! isset( $this->query_meta->total ) ) {
130
+ return 0;
131
+ }
132
+ return $this->query_meta->total;
133
+ }
134
+
135
+ /**
136
+ * Get meta data for last query using query() method
137
+ *
138
+ * @return array Meta data for query
139
+ */
140
+ public function get_query_meta() {
141
+ return $this->query_meta;
142
+ }
143
+
144
+ /**
145
+ * Returns array of existing values for requested field.
146
+ * Used to fill search filters with only used items, instead of all items.
147
+ *
148
+ * @param string Requested field (i.e., 'context')
149
+ *
150
+ * @return array Array of distinct values
151
+ */
152
+ public function get_distinct_field_values( $field ) {
153
+ $query['aggregations']['fields']['terms']['field'] = $field;
154
+
155
+ $values = array();
156
+ $response = WP_Stream::$api->search( $query, array( $field ) );
157
+
158
+ if ( isset( $response->meta->aggregations->fields->buckets ) ) {
159
+ foreach ( $response->meta->aggregations->fields->buckets as $field ) {
160
+ $values[] = $field->key;
161
+ }
162
+ }
163
+
164
+ return $values;
165
+ }
166
+ }
classes/class-wp-stream-feeds.php ADDED
@@ -0,0 +1,264 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class WP_Stream_Feeds {
4
+
5
+ const FEED_QUERY_VAR = 'stream';
6
+ const FEED_KEY_QUERY_VAR = 'key';
7
+ const FEED_TYPE_QUERY_VAR = 'type';
8
+ const USER_FEED_OPTION_KEY = 'stream_user_feed_key';
9
+ const GENERATE_KEY_QUERY_VAR = 'stream_new_user_feed_key';
10
+
11
+ public static function load() {
12
+ if ( ! is_admin() ) {
13
+ $feed_path = parse_url( $_SERVER['REQUEST_URI'], PHP_URL_PATH );
14
+ }
15
+
16
+ if ( ! isset( WP_Stream_Settings::$options['general_private_feeds'] ) || ! WP_Stream_Settings::$options['general_private_feeds'] ) {
17
+ return;
18
+ }
19
+
20
+ add_action( 'show_user_profile', array( __CLASS__, 'save_user_feed_key' ) );
21
+ add_action( 'edit_user_profile', array( __CLASS__, 'save_user_feed_key' ) );
22
+
23
+ add_action( 'show_user_profile', array( __CLASS__, 'user_feed_key' ) );
24
+ add_action( 'edit_user_profile', array( __CLASS__, 'user_feed_key' ) );
25
+
26
+ // Generate new Stream Feed Key
27
+ add_action( 'wp_ajax_wp_stream_feed_key_generate', array( __CLASS__, 'generate_user_feed_key' ) );
28
+
29
+ add_feed( self::FEED_QUERY_VAR, array( __CLASS__, 'feed_template' ) );
30
+ }
31
+
32
+ /**
33
+ * Sends a new user key when the
34
+ *
35
+ * @return void/json
36
+ */
37
+ public static function generate_user_feed_key() {
38
+ check_ajax_referer( 'wp_stream_generate_key', 'nonce' );
39
+
40
+ $user_id = wp_stream_filter_input( INPUT_POST, 'user', FILTER_SANITIZE_NUMBER_INT );
41
+
42
+ if ( $user_id ) {
43
+ $feed_key = wp_generate_password( 32, false );
44
+ update_user_meta( $user_id, self::USER_FEED_OPTION_KEY, $feed_key );
45
+
46
+ $link = self::get_user_feed_url( $feed_key );
47
+ $xml_feed = add_query_arg( array( 'type' => 'json' ), $link );
48
+ $json_feed = add_query_arg( array( 'type' => 'json' ), $link );
49
+
50
+ wp_send_json_success(
51
+ array(
52
+ 'message' => 'User feed key successfully generated.',
53
+ 'feed_key' => $feed_key,
54
+ 'xml_feed' => $xml_feed,
55
+ 'json_feed' => $json_feed,
56
+ )
57
+ );
58
+ } else {
59
+ wp_send_json_error( 'User ID error' );
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Generates and saves a unique key as user meta if the user does not
65
+ * already have a key, or has requested a new one.
66
+ *
67
+ * @action show_user_profile
68
+ * @action edit_user_profile
69
+ * @param WP_User $user
70
+ * @return void
71
+ */
72
+ public static function save_user_feed_key( $user ) {
73
+ $generate_key = wp_stream_filter_input( INPUT_GET, self::GENERATE_KEY_QUERY_VAR );
74
+ $nonce = wp_stream_filter_input( INPUT_GET, 'wp_stream_nonce' );
75
+
76
+ if ( ! $generate_key && get_user_meta( $user->ID, self::USER_FEED_OPTION_KEY, true ) ) {
77
+ return;
78
+ }
79
+
80
+ if ( $generate_key && ! wp_verify_nonce( $nonce, 'wp_stream_generate_key' ) ) {
81
+ return;
82
+ }
83
+
84
+ $feed_key = wp_generate_password( 32, false );
85
+
86
+ update_user_meta( $user->ID, self::USER_FEED_OPTION_KEY, $feed_key );
87
+ }
88
+
89
+ /**
90
+ * Output for Stream Feed URL field in user profiles.
91
+ *
92
+ * @action show_user_profile
93
+ * @action edit_user_profile
94
+ * @param WP_User $user
95
+ * @return string
96
+ */
97
+ public static function user_feed_key( $user ) {
98
+ if ( ! array_intersect( $user->roles, WP_Stream_Settings::$options['general_role_access'] ) ) {
99
+ return;
100
+ }
101
+
102
+ $key = get_user_meta( $user->ID, self::USER_FEED_OPTION_KEY, true );
103
+ $link = self::get_user_feed_url( $key );
104
+
105
+ $nonce = wp_create_nonce( 'wp_stream_generate_key' );
106
+ ?>
107
+ <table class="form-table">
108
+ <tr>
109
+ <th><label for="<?php echo esc_attr( self::USER_FEED_OPTION_KEY ) ?>"><?php esc_html_e( 'Stream Feeds Key', 'stream' ) ?></label></th>
110
+ <td>
111
+ <p class="wp-stream-feeds-key">
112
+ <?php wp_nonce_field( 'wp_stream_generate_key', 'wp_stream_generate_key_nonce' ) ?>
113
+ <input type="text" name="<?php echo esc_attr( self::USER_FEED_OPTION_KEY ) ?>" id="<?php echo esc_attr( self::USER_FEED_OPTION_KEY ) ?>" class="regular-text code" value="<?php echo esc_attr( $key ) ?>" readonly>
114
+ <small><a href="<?php echo esc_url( add_query_arg( array( self::GENERATE_KEY_QUERY_VAR => true, 'wp_stream_nonce' => $nonce ) ) ) ?>" id="<?php echo esc_attr( self::USER_FEED_OPTION_KEY ) ?>_generate"><?php esc_html_e( 'Generate new key', 'stream' ) ?></a></small>
115
+ <span class="spinner" style="display: none;"></span>
116
+ </p>
117
+ <p class="description"><?php esc_html_e( 'This is your private key used for accessing feeds of Stream Records securely. You can change your key at any time by generating a new one using the link above.', 'stream' ) ?></p>
118
+ <p class="wp-stream-feeds-links">
119
+ <a href="<?php echo esc_url( add_query_arg( array( 'type' => 'rss' ), $link ) ) ?>" class="rss-feed" target="_blank"><?php echo esc_html_e( 'RSS Feed', 'stream' ) ?></a>
120
+ |
121
+ <a href="<?php echo esc_url( add_query_arg( array( 'type' => 'atom' ), $link ) ) ?>" class="atom-feed" target="_blank"><?php echo esc_html_e( 'ATOM Feed', 'stream' ) ?></a>
122
+ |
123
+ <a href="<?php echo esc_url( add_query_arg( array( 'type' => 'json' ), $link ) ) ?>" class="json-feed" target="_blank"><?php echo esc_html_e( 'JSON Feed', 'stream' ) ?></a>
124
+ </p>
125
+ </td>
126
+ </tr>
127
+ </table>
128
+ <?php
129
+ }
130
+
131
+ /**
132
+ * Return Stream Feed URL
133
+ *
134
+ * @return string
135
+ */
136
+ public static function get_user_feed_url( $key ) {
137
+ $pretty_permalinks = get_option( 'permalink_structure' );
138
+ $query_var = self::FEED_QUERY_VAR;
139
+
140
+ if ( empty( $pretty_permalinks ) ) {
141
+ $link = add_query_arg(
142
+ array(
143
+ 'feed' => $query_var,
144
+ self::FEED_KEY_QUERY_VAR => $key,
145
+ ),
146
+ home_url( '/' )
147
+ );
148
+ } else {
149
+ $link = add_query_arg(
150
+ array(
151
+ self::FEED_KEY_QUERY_VAR => $key,
152
+ ),
153
+ home_url(
154
+ sprintf(
155
+ '/feed/%s/',
156
+ $query_var
157
+ )
158
+ )
159
+ );
160
+ }
161
+
162
+ return $link;
163
+ }
164
+
165
+ /**
166
+ * Output for Stream Records as a feed.
167
+ *
168
+ * @return xml
169
+ */
170
+ public static function feed_template() {
171
+ $die_title = esc_html__( 'Access Denied', 'stream' );
172
+ $die_message = sprintf( '<h1>%s</h1><p>%s</p>', $die_title, esc_html__( "You don't have permission to view this feed, please contact your site Administrator.", 'stream' ) );
173
+ $query_var = self::FEED_QUERY_VAR;
174
+
175
+ $args = array(
176
+ 'meta_key' => self::USER_FEED_OPTION_KEY,
177
+ 'meta_value' => wp_stream_filter_input( INPUT_GET, self::FEED_KEY_QUERY_VAR ),
178
+ 'number' => 1,
179
+ );
180
+ $user = get_users( $args );
181
+
182
+ if ( empty( $user ) ) {
183
+ wp_die( $die_message, $die_title );
184
+ }
185
+
186
+ if ( ! is_super_admin( $user[0]->ID ) ) {
187
+ $roles = isset( $user[0]->roles ) ? (array) $user[0]->roles : array();
188
+
189
+ if ( ! $roles || ! array_intersect( $roles, WP_Stream_Settings::$options['general_role_access'] ) ) {
190
+ wp_die( $die_message, $die_title );
191
+ }
192
+ }
193
+
194
+ $args = array(
195
+ 'search' => wp_stream_filter_input( INPUT_GET, 'search' ),
196
+ 'record_after' => wp_stream_filter_input( INPUT_GET, 'record_after' ), // Deprecated, use date_after instead
197
+ 'date' => wp_stream_filter_input( INPUT_GET, 'date' ),
198
+ 'date_from' => wp_stream_filter_input( INPUT_GET, 'date_from' ),
199
+ 'date_to' => wp_stream_filter_input( INPUT_GET, 'date_to' ),
200
+ 'date_after' => wp_stream_filter_input( INPUT_GET, 'date_after' ),
201
+ 'date_before' => wp_stream_filter_input( INPUT_GET, 'date_before' ),
202
+ 'record' => wp_stream_filter_input( INPUT_GET, 'record' ),
203
+ 'record__in' => wp_stream_filter_input( INPUT_GET, 'record__in' ),
204
+ 'record__not_in' => wp_stream_filter_input( INPUT_GET, 'record__not_in' ),
205
+ 'records_per_page' => wp_stream_filter_input( INPUT_GET, 'records_per_page', FILTER_SANITIZE_NUMBER_INT ),
206
+ 'order' => wp_stream_filter_input( INPUT_GET, 'order' ),
207
+ 'orderby' => wp_stream_filter_input( INPUT_GET, 'orderby' ),
208
+ 'meta' => wp_stream_filter_input( INPUT_GET, 'meta' ),
209
+ 'fields' => wp_stream_filter_input( INPUT_GET, 'fields' ),
210
+ );
211
+
212
+ $properties = array(
213
+ 'author',
214
+ 'author_role',
215
+ 'ip',
216
+ 'object_id',
217
+ 'connector',
218
+ 'context',
219
+ 'action',
220
+ );
221
+
222
+ foreach ( $properties as $property ) {
223
+ $args[ $property ] = wp_stream_filter_input( INPUT_GET, $property );
224
+ $args[ "{$property}__in" ] = wp_stream_filter_input( INPUT_GET, "{$property}__in" );
225
+ $args[ "{$property}__not_in" ] = wp_stream_filter_input( INPUT_GET, "{$property}__not_in" );
226
+ }
227
+
228
+ $records = wp_stream_query( $args );
229
+
230
+ $latest_record = isset( $records[0]->created ) ? $records[0]->created : null;
231
+
232
+ $records_admin_url = add_query_arg(
233
+ array(
234
+ 'page' => WP_Stream_Admin::RECORDS_PAGE_SLUG,
235
+ ),
236
+ admin_url( WP_Stream_Admin::ADMIN_PARENT_PAGE )
237
+ );
238
+
239
+ $latest_link = null;
240
+
241
+ if ( isset( $records[0]->ID ) ) {
242
+ $latest_link = add_query_arg(
243
+ array(
244
+ 'record__in' => $records[0]->ID,
245
+ ),
246
+ $records_admin_url
247
+ );
248
+ }
249
+
250
+ $domain = parse_url( $records_admin_url, PHP_URL_HOST );
251
+ $format = wp_stream_filter_input( INPUT_GET, self::FEED_TYPE_QUERY_VAR );
252
+
253
+ if ( 'atom' === $format ) {
254
+ require_once WP_STREAM_INC_DIR . 'feeds/atom.php';
255
+ } elseif ( 'json' === $format ) {
256
+ require_once WP_STREAM_INC_DIR . 'feeds/json.php';
257
+ } else {
258
+ require_once WP_STREAM_INC_DIR . 'feeds/rss-2.0.php';
259
+ }
260
+
261
+ exit;
262
+ }
263
+
264
+ }
classes/class-wp-stream-filter-input.php ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class WP_Stream_Filter_Input {
4
+
5
+ public static $filter_callbacks = array(
6
+ FILTER_DEFAULT => null,
7
+ // Validate
8
+ FILTER_VALIDATE_BOOLEAN => 'is_bool',
9
+ FILTER_VALIDATE_EMAIL => 'is_email',
10
+ FILTER_VALIDATE_FLOAT => 'is_float',
11
+ FILTER_VALIDATE_INT => 'is_int',
12
+ FILTER_VALIDATE_IP => array( 'WP_Stream_Filter_Input', 'is_ip_address' ),
13
+ FILTER_VALIDATE_REGEXP => array( 'WP_Stream_Filter_Input', 'is_regex' ),
14
+ FILTER_VALIDATE_URL => 'wp_http_validate_url',
15
+ // Sanitize
16
+ FILTER_SANITIZE_EMAIL => 'sanitize_email',
17
+ FILTER_SANITIZE_ENCODED => 'esc_url_raw',
18
+ FILTER_SANITIZE_NUMBER_FLOAT => 'floatval',
19
+ FILTER_SANITIZE_NUMBER_INT => 'intval',
20
+ FILTER_SANITIZE_SPECIAL_CHARS => 'htmlspecialchars',
21
+ FILTER_SANITIZE_STRING => 'sanitize_text_field',
22
+ FILTER_SANITIZE_URL => 'esc_url_raw',
23
+ // Other
24
+ FILTER_UNSAFE_RAW => null,
25
+ );
26
+
27
+ public static function super( $type, $variable_name, $filter = null, $options = array() ) {
28
+ $super = null;
29
+
30
+ switch ( $type ) {
31
+ case INPUT_POST :
32
+ $super = $_POST;
33
+ break;
34
+ case INPUT_GET :
35
+ $super = $_GET;
36
+ break;
37
+ case INPUT_COOKIE :
38
+ $super = $_COOKIE;
39
+ break;
40
+ case INPUT_ENV :
41
+ $super = $_ENV;
42
+ break;
43
+ case INPUT_SERVER :
44
+ $super = $_SERVER;
45
+ break;
46
+ }
47
+
48
+ if ( is_null( $super ) ) {
49
+ throw new Exception( __( 'Invalid use, type must be one of INPUT_* family.', 'stream' ) );
50
+ }
51
+
52
+ $var = isset( $super[ $variable_name ] ) ? $super[ $variable_name ] : null;
53
+ $var = self::filter( $var, $filter, $options );
54
+
55
+ return $var;
56
+ }
57
+
58
+ public static function filter( $var, $filter = null, $options = array() ) {
59
+ // Default filter is a sanitizer, not validator
60
+ $filter_type = 'sanitizer';
61
+
62
+ // Only filter value if it is not null
63
+ if ( isset( $var ) && $filter && FILTER_DEFAULT !== $filter ) {
64
+ if ( ! isset( self::$filter_callbacks[ $filter ] ) ) {
65
+ throw new Exception( __( 'Filter not supported.', 'stream' ) );
66
+ }
67
+
68
+ $filter_callback = self::$filter_callbacks[ $filter ];
69
+ $result = call_user_func( $filter_callback, $var );
70
+
71
+ // filter_var / filter_input treats validation/sanitization filters the same
72
+ // they both return output and change the var value, this shouldn't be the case here.
73
+ // We'll do a boolean check on validation function, and let sanitizers change the value
74
+ $filter_type = ( $filter < 500 ) ? 'validator' : 'sanitizer';
75
+ if ( 'validator' === $filter_type ) { // Validation functions
76
+ if ( ! $result ) {
77
+ $var = false;
78
+ }
79
+ } else { // Santization functions
80
+ $var = $result;
81
+ }
82
+ }
83
+
84
+ // Detect FILTER_REQUIRE_ARRAY flag
85
+ if ( isset( $var ) && is_int( $options ) && FILTER_REQUIRE_ARRAY === $options ) {
86
+ if ( ! is_array( $var ) ) {
87
+ $var = ( 'validator' === $filter_type ) ? false : null;
88
+ }
89
+ }
90
+
91
+ // Polyfill the `default` attribute only, for now.
92
+ if ( is_array( $options ) && ! empty( $options['options']['default'] ) ) {
93
+ if ( 'validator' === $filter_type && false === $var ) {
94
+ $var = $options['options']['default'];
95
+ } elseif ( 'sanitizer' === $filter_type && null === $var ) {
96
+ $var = $options['options']['default'];
97
+ }
98
+ }
99
+
100
+ return $var;
101
+ }
102
+
103
+ public static function is_regex( $var ) {
104
+ // @codingStandardsIgnoreStart
105
+ $test = @preg_match( $var, '' );
106
+ // @codingStandardsIgnoreEnd
107
+
108
+ return $test !== false;
109
+ }
110
+
111
+ public static function is_ip_address( $var ) {
112
+ return false !== WP_Http::is_ip_address( $var );
113
+ }
114
+
115
+ }
classes/class-wp-stream-list-table.php ADDED
@@ -0,0 +1,881 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class WP_Stream_List_Table extends WP_List_Table {
4
+
5
+ function __construct( $args = array() ) {
6
+
7
+ $screen_id = isset( $args['screen'] ) ? $args['screen'] : null;
8
+ $screen_id = apply_filters( 'wp_stream_list_table_screen_id', $screen_id );
9
+
10
+ parent::__construct(
11
+ array(
12
+ 'post_type' => 'stream',
13
+ 'plural' => 'records',
14
+ 'screen' => $screen_id,
15
+ )
16
+ );
17
+
18
+ add_screen_option(
19
+ 'per_page',
20
+ array(
21
+ 'default' => 20,
22
+ 'label' => __( 'Records per page', 'stream' ),
23
+ 'option' => 'edit_stream_per_page',
24
+ )
25
+ );
26
+
27
+ // Check for default hidden columns
28
+ $this->get_hidden_columns();
29
+
30
+ add_filter( 'screen_settings', array( $this, 'screen_controls' ), 10, 2 );
31
+ add_filter( 'set-screen-option', array( __CLASS__, 'set_screen_option' ), 10, 3 );
32
+
33
+ set_screen_options();
34
+ }
35
+
36
+ function extra_tablenav( $which ) {
37
+ if ( 'top' === $which ) {
38
+ echo $this->filters_form(); //xss ok
39
+ }
40
+ }
41
+
42
+ function no_items() {
43
+ $site = WP_Stream::$api->get_site();
44
+
45
+ if ( isset( $site->plan->type ) && 'free' === $site->plan->type && 0 !== $this->get_total_found_rows() ) {
46
+ ?>
47
+ <div class="stream-list-table-upgrade">
48
+ <p><?php printf( _n( 'Your free account is limited to viewing 24 hours of activity history.', 'Your free account is limited to viewing <strong>%d days</strong> of activity history.', $site->plan->retention, 'stream' ), absint( $site->plan->retention ) ) ?></p>
49
+ <p><a href="<?php echo esc_url( WP_Stream_Admin::account_url( sprintf( 'upgrade?site_uuid=%s', WP_Stream::$api->site_uuid ) ) ); ?>" class="button button-primary button-large"><?php _e( 'Upgrade to Pro', 'stream' ) ?></a></p>
50
+ </div>
51
+ <?php
52
+ } else {
53
+ _e( 'Sorry, no activity records were found.', 'stream' );
54
+ }
55
+ }
56
+
57
+ function get_columns(){
58
+ /**
59
+ * Allows devs to add new columns to table
60
+ *
61
+ * @param array default columns
62
+ *
63
+ * @return array updated list of columns
64
+ */
65
+ return apply_filters(
66
+ 'wp_stream_list_table_columns',
67
+ array(
68
+ 'date' => __( 'Date', 'stream' ),
69
+ 'summary' => __( 'Summary', 'stream' ),
70
+ 'author' => __( 'Author', 'stream' ),
71
+ 'context' => __( 'Context', 'stream' ),
72
+ 'action' => __( 'Action', 'stream' ),
73
+ 'ip' => __( 'IP Address', 'stream' ),
74
+ )
75
+ );
76
+ }
77
+
78
+ function get_sortable_columns() {
79
+ return array(
80
+ 'date' => array( 'date', false ),
81
+ );
82
+ }
83
+
84
+ function get_hidden_columns() {
85
+ if ( ! $user = wp_get_current_user() ) {
86
+ return array();
87
+ }
88
+
89
+ // Directly checking the user meta; to check whether user has changed screen option or not
90
+ $hidden = get_user_meta( $user->ID, 'manage' . $this->screen->id . 'columnshidden', true );
91
+
92
+ // If user meta is not found; add the default hidden column 'id'
93
+ if ( ! $hidden ) {
94
+ $hidden = array( 'id' );
95
+ update_user_meta( $user->ID, 'manage' . $this->screen->id . 'columnshidden', $hidden );
96
+ }
97
+
98
+ return $hidden;
99
+ }
100
+
101
+ function prepare_items() {
102
+ $columns = $this->get_columns();
103
+ $sortable = $this->get_sortable_columns();
104
+ $hidden = $this->get_hidden_columns();
105
+
106
+ $this->_column_headers = array( $columns, $hidden, $sortable );
107
+
108
+ $this->items = $this->get_records();
109
+
110
+ $total_items = $this->get_total_found_rows();
111
+
112
+ $this->set_pagination_args(
113
+ array(
114
+ 'total_items' => $total_items,
115
+ 'per_page' => $this->get_items_per_page( 'edit_stream_per_page', 20 ),
116
+ )
117
+ );
118
+ }
119
+
120
+ /**
121
+ * Render the checkbox column
122
+ *
123
+ * @param array $item Contains all the data for the checkbox column
124
+ *
125
+ * @return string Displays a checkbox
126
+ */
127
+ function column_cb( $item ) {
128
+ return sprintf(
129
+ '<input type="checkbox" name="%1$s[]" value="%2$s" />',
130
+ /*$1%s*/
131
+ 'wp_stream_checkbox',
132
+ /*$2%s*/
133
+ $item->ID
134
+ );
135
+ }
136
+
137
+ function get_records() {
138
+ $args = array();
139
+
140
+ // Parse sorting params
141
+ if ( $order = wp_stream_filter_input( INPUT_GET, 'order' ) ) {
142
+ $args['order'] = $order;
143
+ }
144
+ if ( $orderby = wp_stream_filter_input( INPUT_GET, 'orderby' ) ) {
145
+ $args['orderby'] = $orderby;
146
+ }
147
+
148
+ // Filters
149
+ $params = array(
150
+ 'search',
151
+ 'date',
152
+ 'date_from',
153
+ 'date_to',
154
+ 'record_after', // Deprecated, use date_after instead
155
+ 'date_after',
156
+ 'date_before',
157
+ );
158
+
159
+ foreach ( $params as $param ) {
160
+ $value = wp_stream_filter_input( INPUT_GET, $param );
161
+ if ( $value ) {
162
+ $args[ $param ] = $value;
163
+ }
164
+ }
165
+
166
+ // Additional filter properties
167
+ $properties = array(
168
+ 'record',
169
+ 'author',
170
+ 'author_role',
171
+ 'ip',
172
+ 'object_id',
173
+ 'site_id',
174
+ 'blog_id',
175
+ 'connector',
176
+ 'context',
177
+ 'action',
178
+ );
179
+
180
+ // Add property fields to defaults, including their __in/__not_in variations
181
+ foreach ( $properties as $property ) {
182
+ $value = wp_stream_filter_input( INPUT_GET, $property );
183
+ if ( $value ) {
184
+ $args[ $property ] = $value;
185
+ }
186
+
187
+ $value_in = wp_stream_filter_input( INPUT_GET, $property . '__in' );
188
+ if ( $value_in ) {
189
+ $args[ $property . '__in' ] = explode( ',', $value_in );
190
+ }
191
+
192
+ $value_not_in = wp_stream_filter_input( INPUT_GET, $property . '__not_in' );
193
+ if ( $value_not_in ) {
194
+ $args[ $property . '__not_in' ] = explode( ',', $value_not_in );
195
+ }
196
+ }
197
+
198
+ $args['paged'] = $this->get_pagenum();
199
+
200
+ if ( isset( $args['context'] ) && 0 === strpos( $args['context'], 'group-' ) ) {
201
+ $args['connector'] = str_replace( 'group-', '', $args['context'] );
202
+ $args['context'] = '';
203
+ }
204
+
205
+ if ( ! isset( $args['records_per_page'] ) ) {
206
+ $args['records_per_page'] = $this->get_items_per_page( 'edit_stream_per_page', 20 );
207
+ }
208
+
209
+ $args['aggregations'] = array( 'author', 'connector', 'context', 'action' );
210
+
211
+ $items = wp_stream_query( $args );
212
+
213
+ return $items;
214
+ }
215
+
216
+ /**
217
+ * Get last query found rows
218
+ *
219
+ * @return integer
220
+ */
221
+ function get_total_found_rows() {
222
+ return WP_Stream::$db->get_found_rows();
223
+ }
224
+
225
+ function column_default( $item, $column_name ) {
226
+ switch ( $column_name ) {
227
+ case 'date' :
228
+ $created = date( 'Y-m-d H:i:s', strtotime( $item->created ) );
229
+ $date_string = sprintf(
230
+ '<time datetime="%s" class="relative-time record-created">%s</time>',
231
+ wp_stream_get_iso_8601_extended_date( strtotime( $item->created ) ),
232
+ get_date_from_gmt( $created, 'Y/m/d' )
233
+ );
234
+ $out = $this->column_link( $date_string, 'date', get_date_from_gmt( $created, 'Y/m/d' ) );
235
+ $out .= '<br />';
236
+ $out .= get_date_from_gmt( $created, 'h:i:s A' );
237
+ break;
238
+
239
+ case 'summary' :
240
+ $out = $item->summary;
241
+ $object_title = wp_stream_get_object_title( $item );
242
+ $view_all_text = $object_title ? sprintf( __( 'View all activity for "%s"', 'stream' ), esc_attr( $object_title ) ) : __( 'View all activity for this object', 'stream' );
243
+ if ( $item->object_id ) {
244
+ $out .= $this->column_link(
245
+ '<span class="dashicons dashicons-search stream-filter-object-id"></span>',
246
+ array(
247
+ 'object_id' => $item->object_id,
248
+ 'context' => $item->context,
249
+ ),
250
+ null,
251
+ esc_attr( $view_all_text )
252
+ );
253
+ }
254
+ $out .= $this->get_action_links( $item );
255
+ break;
256
+
257
+ case 'author' :
258
+ $author = new WP_Stream_Author( (int) $item->author, (array) $item->author_meta );
259
+
260
+ $out = sprintf(
261
+ '<a href="%s">%s <span>%s</span></a>%s%s%s',
262
+ $author->get_records_page_url(),
263
+ $author->get_avatar_img( 80 ),
264
+ $author->get_display_name(),
265
+ $author->is_deleted() ? sprintf( '<br /><small class="deleted">%s</small>', esc_html__( 'Deleted User', 'stream' ) ) : '',
266
+ $author->get_role() ? sprintf( '<br /><small>%s</small>', $author->get_role() ) : '',
267
+ $author->get_agent() ? sprintf( '<br /><small>%s</small>', WP_Stream_Author::get_agent_label( $author->get_agent() ) ) : ''
268
+ );
269
+ break;
270
+
271
+ case 'context':
272
+ $connector_title = $this->get_term_title( $item->{'connector'}, 'connector' );
273
+ $context_title = $this->get_term_title( $item->{'context'}, 'context' );
274
+
275
+ $out = $this->column_link( $connector_title, 'connector', $item->{'connector'} );
276
+ $out .= '<br />&#8627;&nbsp;';
277
+ $out .= $this->column_link(
278
+ $context_title,
279
+ array(
280
+ 'connector' => $item->{'connector'},
281
+ 'context' => $item->{'context'},
282
+ )
283
+ );
284
+ break;
285
+
286
+ case 'action':
287
+ $out = $this->column_link( $this->get_term_title( $item->{$column_name}, $column_name ), $column_name, $item->{$column_name} );
288
+ break;
289
+
290
+ case 'ip' :
291
+ $out = $this->column_link( $item->{$column_name}, 'ip', $item->{$column_name} );
292
+ break;
293
+
294
+ case 'blog_id':
295
+ $blog = get_blog_details( $item->blog_id );
296
+ $out = sprintf(
297
+ '<a href="%s"><span>%s</span></a>',
298
+ add_query_arg( array( 'blog_id' => $blog->blog_id ), admin_url( 'admin.php?page=wp_stream' ) ),
299
+ esc_html( $blog->blogname )
300
+ );
301
+ break;
302
+
303
+ default :
304
+ /**
305
+ * Registers new Columns to be inserted into the table. The cell contents of this column is set
306
+ * below with 'wp_stream_inster_column_default-'
307
+ *
308
+ * @param array $new_columns Array of new column titles to add
309
+ */
310
+ $inserted_columns = apply_filters( 'wp_stream_register_column_defaults', $new_columns = array() );
311
+
312
+ if ( ! empty( $inserted_columns ) && is_array( $inserted_columns ) ) {
313
+ foreach ( $inserted_columns as $column_title ) {
314
+ /**
315
+ * If column title inserted via wp_stream_register_column_defaults ($column_title) exists
316
+ * among columns registered with get_columns ($column_name) and there is an action associated
317
+ * with this column, do the action
318
+ *
319
+ * Also, note that the action name must include the $column_title registered
320
+ * with wp_stream_register_column_defaults
321
+ */
322
+ if ( $column_title == $column_name && has_action( "wp_stream_insert_column_default-{$column_title}" ) ) {
323
+ /**
324
+ * Allows for the addition of content under a specified column.
325
+ *
326
+ * @since 1.0.0
327
+ *
328
+ * @param object $item Contents of the row
329
+ */
330
+ $out = do_action( "wp_stream_insert_column_default-{$column_title}", $item );
331
+ } else {
332
+ $out = $column_name;
333
+ }
334
+ }
335
+ } else {
336
+ $out = $column_name; // xss ok
337
+ }
338
+ }
339
+
340
+ echo $out; // xss ok
341
+ }
342
+
343
+ public static function get_action_links( $record ) {
344
+ $out = '';
345
+
346
+ /**
347
+ * Filter allows modification of action links for a specific connector
348
+ *
349
+ * @param string connector
350
+ * @param array array of action links for this connector
351
+ * @param obj record
352
+ *
353
+ * @return arrray action links for this connector
354
+ */
355
+ $action_links = apply_filters( 'wp_stream_action_links_' . $record->connector, array(), $record );
356
+
357
+ /**
358
+ * Filter allows addition of custom links for a specific connector
359
+ *
360
+ * @param string connector
361
+ * @param array array of custom links for this connector
362
+ * @param obj record
363
+ *
364
+ * @return arrray custom links for this connector
365
+ */
366
+ $custom_links = apply_filters( 'wp_stream_custom_action_links_' . $record->connector, array(), $record );
367
+
368
+ if ( $action_links || $custom_links ) {
369
+ $out .= '<div class="row-actions">';
370
+ }
371
+
372
+ $links = array();
373
+ if ( $action_links && is_array( $action_links ) ) {
374
+ foreach ( $action_links as $al_title => $al_href ) {
375
+ $links[] = sprintf(
376
+ '<span><a href="%s" class="action-link">%s</a></span>',
377
+ $al_href,
378
+ $al_title
379
+ );
380
+ }
381
+ }
382
+
383
+ if ( $custom_links && is_array( $custom_links ) ) {
384
+ foreach ( $custom_links as $key => $link ) {
385
+ $links[] = $link;
386
+ }
387
+ }
388
+
389
+ $out .= implode( ' | ', $links );
390
+
391
+ if ( $action_links || $custom_links ) {
392
+ $out .= '</div>';
393
+ }
394
+
395
+ return $out;
396
+ }
397
+
398
+ function column_link( $display, $key, $value = null, $title = null ) {
399
+ $url = add_query_arg(
400
+ array(
401
+ 'page' => WP_Stream_Admin::RECORDS_PAGE_SLUG,
402
+ ),
403
+ self_admin_url( WP_Stream_Admin::ADMIN_PARENT_PAGE )
404
+ );
405
+
406
+ $args = ! is_array( $key ) ? array( $key => $value ) : $key;
407
+
408
+ foreach ( $args as $k => $v ) {
409
+ $url = add_query_arg( $k, $v, $url );
410
+ }
411
+
412
+ return sprintf(
413
+ '<a href="%s" title="%s">%s</a>',
414
+ esc_url( $url ),
415
+ esc_attr( $title ),
416
+ $display
417
+ );
418
+ }
419
+
420
+ public function get_term_title( $term, $type ) {
421
+ if ( isset( WP_Stream_Connectors::$term_labels[ "stream_$type" ][ $term ] ) ) {
422
+ return WP_Stream_Connectors::$term_labels[ "stream_$type" ][ $term ];
423
+ } else {
424
+ return $term;
425
+ }
426
+ }
427
+
428
+ /**
429
+ * Assembles records for display in search filters
430
+ *
431
+ * Gathers list of all authors/connectors, then compares it to
432
+ * results of existing records. All items that do not exist in records
433
+ * get assigned a disabled value of "true".
434
+ *
435
+ * @uses wp_stream_existing_records (see class-wp-stream-query.php)
436
+ * @since 1.0.4
437
+ *
438
+ * @param string Column requested
439
+ * @param string Table to be queried
440
+ *
441
+ * @return array options to be displayed in search filters
442
+ */
443
+ function assemble_records( $column ) {
444
+ $setting_key = self::get_column_excluded_setting_key( $column );
445
+
446
+ // @todo eliminate special condition for authors, especially using a WP_User object as the value; should use string or stringifiable object
447
+ if ( 'author' === $column ) {
448
+ $all_records = array();
449
+
450
+ // If the number of users exceeds the max authors constant value then return an empty array and use AJAX instead
451
+ $user_count = count_users();
452
+ $total_users = $user_count['total_users'];
453
+
454
+ if ( $total_users > WP_Stream_Admin::PRELOAD_AUTHORS_MAX ) {
455
+ return array();
456
+ }
457
+
458
+ $authors = array_map(
459
+ function ( $user_id ) {
460
+ return new WP_Stream_Author( $user_id );
461
+ },
462
+ get_users( array( 'fields' => 'ID' ) )
463
+ );
464
+
465
+ $authors[] = new WP_Stream_Author( 0, array( 'is_wp_cli' => true ) );
466
+
467
+ foreach ( $authors as $author ) {
468
+ $all_records[ $author->id ] = $author->get_display_name();
469
+ }
470
+ } else {
471
+ $prefixed_column = sprintf( 'stream_%s', $column );
472
+ $all_records = WP_Stream_Connectors::$term_labels[ $prefixed_column ];
473
+ }
474
+
475
+ $query_meta = WP_Stream::$db->get_query_meta();
476
+
477
+ $values = array();
478
+ $existing_records = array();
479
+
480
+ if ( isset( $query_meta->aggregations->$column->buckets ) ) {
481
+ foreach ( $query_meta->aggregations->$column->buckets as $field ) {
482
+ $values[ $field->key ] = $field->key;
483
+ }
484
+
485
+ if ( ! empty( $values ) ) {
486
+ $existing_records = array_combine( $values, $values );
487
+ }
488
+ }
489
+
490
+ $active_records = array();
491
+ $disabled_records = array();
492
+
493
+ foreach ( $all_records as $record => $label ) {
494
+ if ( array_key_exists( $record, $existing_records ) ) {
495
+ $active_records[ $record ] = array( 'label' => $label, 'disabled' => '' );
496
+ } else {
497
+ $disabled_records[ $record ] = array( 'label' => $label, 'disabled' => 'disabled="disabled"' );
498
+ }
499
+ }
500
+
501
+ // Remove WP-CLI pseudo user if no records with user=0 exist
502
+ if ( isset( $disabled_records[0] ) ) {
503
+ unset( $disabled_records[0] );
504
+ }
505
+
506
+ $sort = function ( $a, $b ) use ( $column ) {
507
+ $label_a = (string) $a['label'];
508
+ $label_b = (string) $b['label'];
509
+
510
+ if ( $label_a === $label_b ) {
511
+ return 0;
512
+ }
513
+
514
+ return ( strtolower( $label_a ) < strtolower( $label_b ) ) ? -1 : 1;
515
+ };
516
+
517
+ uasort( $active_records, $sort );
518
+ uasort( $disabled_records, $sort );
519
+
520
+ // Not using array_merge() in order to preserve the array index for the Authors dropdown which uses the user_id as the key
521
+ $all_records = $active_records + $disabled_records;
522
+
523
+ return $all_records;
524
+ }
525
+
526
+ public function get_filters() {
527
+ $filters = array();
528
+
529
+ $date_interval = new WP_Stream_Date_Interval();
530
+
531
+ $filters['date'] = array(
532
+ 'title' => __( 'dates', 'stream' ),
533
+ 'items' => $date_interval->intervals,
534
+ );
535
+
536
+ $authors_records = WP_Stream_Admin::get_authors_record_meta(
537
+ $this->assemble_records( 'author' )
538
+ );
539
+
540
+ $filters['author'] = array(
541
+ 'title' => __( 'authors', 'stream' ),
542
+ 'items' => $authors_records,
543
+ 'ajax' => count( $authors_records ) <= 0,
544
+ );
545
+
546
+ $filters['context'] = array(
547
+ 'title' => __( 'contexts', 'stream' ),
548
+ 'items' => $this->assemble_records( 'context' ),
549
+ );
550
+
551
+ $filters['action'] = array(
552
+ 'title' => __( 'actions', 'stream' ),
553
+ 'items' => $this->assemble_records( 'action' ),
554
+ );
555
+
556
+ /**
557
+ * Filter allows additional filters in the list table dropdowns
558
+ * Note the format of the filters above, with they key and array
559
+ * containing a title and array of items.
560
+ *
561
+ * @param array Array of filters
562
+ *
563
+ * @return array Updated array of filters
564
+ */
565
+
566
+ return apply_filters( 'wp_stream_list_table_filters', $filters );
567
+ }
568
+
569
+ function filters_form() {
570
+ $user_id = get_current_user_id();
571
+ $filters = $this->get_filters();
572
+
573
+ $filters_string = sprintf( '<input type="hidden" name="page" value="%s" />', 'wp_stream' );
574
+ $filters_string .= sprintf( '<span class="filter_info hidden">%s</span>', esc_html__( 'Show filter controls via the screen options tab above.', 'stream' ) );
575
+
576
+ foreach ( $filters as $name => $data ) {
577
+ if ( 'date' === $name ) {
578
+ $filters_string .= $this->filter_date( $data['items'] );
579
+ } else {
580
+ if ( 'context' === $name ) {
581
+ // Add Connectors as parents, and apply the Contexts as children
582
+ $connectors = $this->assemble_records( 'connector' );
583
+
584
+ foreach ( $connectors as $connector => $item ) {
585
+ $context_items[ $connector ]['label'] = $item['label'];
586
+
587
+ foreach ( $data['items'] as $context_value => $context_item ) {
588
+ if ( isset( WP_Stream_Connectors::$contexts[ $connector ] ) && array_key_exists( $context_value, WP_Stream_Connectors::$contexts[ $connector ] ) ) {
589
+ $context_items[ $connector ]['children'][ $context_value ] = $context_item;
590
+ }
591
+ }
592
+ }
593
+
594
+ foreach ( $context_items as $context_value => $context_item ) {
595
+ if ( ! isset( $context_item['children'] ) || empty( $context_item['children'] ) ) {
596
+ unset( $context_items[ $context_value ] );
597
+ }
598
+ }
599
+
600
+ $data['items'] = $context_items;
601
+
602
+ ksort( $data['items'] );
603
+
604
+ // Ouput a hidden input to handle the connector value
605
+ $filters_string .= '<input type="hidden" name="connector" class="record-filter-connector" />';
606
+ }
607
+ $filters_string .= $this->filter_select( $name, $data['title'], $data['items'] );
608
+ }
609
+ }
610
+
611
+ $filters_string .= sprintf( '<input type="submit" id="record-query-submit" class="button" value="%s" />', __( 'Filter', 'stream' ) );
612
+
613
+ $url = self_admin_url( WP_Stream_Admin::ADMIN_PARENT_PAGE );
614
+
615
+ return sprintf( '<div class="alignleft actions">%s</div>', $filters_string ); // xss ok
616
+ }
617
+
618
+ function filter_select( $name, $title, $items, $ajax = false ) {
619
+ if ( $ajax ) {
620
+ $out = sprintf(
621
+ '<input type="hidden" name="%s" class="chosen-select" value="%s" data-placeholder="%s" />',
622
+ esc_attr( $name ),
623
+ esc_attr( wp_stream_filter_input( INPUT_GET, $name ) ),
624
+ esc_attr( $title )
625
+ );
626
+ } else {
627
+ $options = array( '<option value=""></option>' );
628
+ $selected = wp_stream_filter_input( INPUT_GET, $name );
629
+
630
+ foreach ( $items as $key => $item ) {
631
+ $value = isset( $item['children'] ) ? 'group-' . $key : $key;
632
+ $option_args = array(
633
+ 'value' => $value,
634
+ 'selected' => selected( $value, $selected, false ),
635
+ 'disabled' => isset( $item['disabled'] ) ? $item['disabled'] : null,
636
+ 'icon' => isset( $item['icon'] ) ? $item['icon'] : null,
637
+ 'group' => isset( $item['children'] ) ? $key : null,
638
+ 'tooltip' => isset( $item['tooltip'] ) ? $item['tooltip'] : null,
639
+ 'class' => isset( $item['children'] ) ? 'level-1' : null,
640
+ 'label' => isset( $item['label'] ) ? $item['label'] : null,
641
+ );
642
+ $options[] = $this->filter_option( $option_args );
643
+
644
+ if ( isset( $item['children'] ) ) {
645
+ foreach ( $item['children'] as $child_value => $child_item ) {
646
+ $option_args = array(
647
+ 'value' => $child_value,
648
+ 'selected' => selected( $child_value, $selected, false ),
649
+ 'disabled' => isset( $child_item['disabled'] ) ? $child_item['disabled'] : null,
650
+ 'icon' => isset( $child_item['icon'] ) ? $child_item['icon'] : null,
651
+ 'group' => $key,
652
+ 'tooltip' => isset( $child_item['tooltip'] ) ? $child_item['tooltip'] : null,
653
+ 'class' => 'level-2',
654
+ 'label' => isset( $child_item['label'] ) ? '- ' . $child_item['label'] : null,
655
+ );
656
+ $options[] = $this->filter_option( $option_args );
657
+ }
658
+ }
659
+ }
660
+ $out = sprintf(
661
+ '<select name="%s" class="chosen-select" data-placeholder="%s">%s</select>',
662
+ esc_attr( $name ),
663
+ sprintf( esc_attr__( 'Show all %s', 'stream' ), $title ),
664
+ implode( '', $options )
665
+ );
666
+ }
667
+
668
+ return $out;
669
+ }
670
+
671
+ function filter_option( $args ) {
672
+ $defaults = array(
673
+ 'value' => null,
674
+ 'selected' => null,
675
+ 'disabled' => null,
676
+ 'icon' => null,
677
+ 'group' => null,
678
+ 'tooltip' => null,
679
+ 'class' => null,
680
+ 'label' => null,
681
+ );
682
+ wp_parse_args( $args, $defaults );
683
+
684
+ return sprintf(
685
+ '<option value="%s" %s %s %s %s %s class="%s">%s</option>',
686
+ esc_attr( $args['value'] ),
687
+ $args['selected'],
688
+ $args['disabled'],
689
+ $args['icon'] ? sprintf( 'data-icon="%s"', esc_attr( $args['icon'] ) ) : null,
690
+ $args['group'] ? sprintf( 'data-group="%s"', esc_attr( $args['group'] ) ) : null,
691
+ $args['tooltip'] ? sprintf( 'title="%s"', esc_attr( $args['tooltip'] ) ) : null,
692
+ $args['class'] ? esc_attr( $args['class'] ) : null,
693
+ esc_html( $args['label'] )
694
+ );
695
+ }
696
+
697
+ function filter_search() {
698
+ $out = sprintf(
699
+ '<p class="search-box">
700
+ <label class="screen-reader-text" for="record-search-input">%1$s:</label>
701
+ <input type="search" id="record-search-input" name="search" value="%2$s" />
702
+ <input type="submit" name="" id="search-submit" class="button" value="%1$s" />
703
+ </p>',
704
+ esc_attr__( 'Search Records', 'stream' ),
705
+ isset( $_GET['search'] ) ? esc_attr( wp_unslash( $_GET['search'] ) ) : null
706
+ );
707
+
708
+ return $out;
709
+ }
710
+
711
+ function filter_date( $items ) {
712
+ wp_enqueue_style( 'jquery-ui' );
713
+ wp_enqueue_style( 'wp-stream-datepicker' );
714
+ wp_enqueue_script( 'jquery-ui-datepicker' );
715
+
716
+ $date_predefined = wp_stream_filter_input( INPUT_GET, 'date_predefined' );
717
+ $date_from = wp_stream_filter_input( INPUT_GET, 'date_from' );
718
+ $date_to = wp_stream_filter_input( INPUT_GET, 'date_to' );
719
+
720
+ ob_start();
721
+ ?>
722
+ <div class="date-interval">
723
+
724
+ <select class="field-predefined hide-if-no-js" name="date_predefined" data-placeholder="<?php _e( 'All Time', 'stream' ); ?>">
725
+ <option></option>
726
+ <option value="custom" <?php selected( 'custom' === $date_predefined ); ?>><?php esc_attr_e( 'Custom', 'stream' ) ?></option>
727
+ <?php
728
+ foreach ( $items as $key => $interval ) {
729
+ printf(
730
+ '<option value="%s" data-from="%s" data-to="%s" %s>%s</option>',
731
+ esc_attr( $key ),
732
+ esc_attr( $interval['start']->format( 'Y/m/d' ) ),
733
+ isset( $interval['end'] ) ? esc_attr( $interval['end']->format( 'Y/m/d' ) ) : '',
734
+ selected( $key === $date_predefined ),
735
+ esc_html( $interval['label'] )
736
+ ); // xss ok
737
+ }
738
+ ?>
739
+ </select>
740
+
741
+ <div class="date-inputs">
742
+ <div class="box">
743
+ <i class="date-remove dashicons"></i>
744
+ <input type="text" name="date_from" class="date-picker field-from" placeholder="<?php esc_attr_e( 'Start Date', 'stream' ) ?>" value="<?php echo esc_attr( $date_from ) ?>" />
745
+ </div>
746
+ <span class="connector dashicons"></span>
747
+
748
+ <div class="box">
749
+ <i class="date-remove dashicons"></i>
750
+ <input type="text" name="date_to" class="date-picker field-to" placeholder="<?php esc_attr_e( 'End Date', 'stream' ) ?>" value="<?php echo esc_attr( $date_to ) ?>" />
751
+ </div>
752
+ </div>
753
+
754
+ </div>
755
+ <?php
756
+
757
+ return ob_get_clean();
758
+ }
759
+
760
+ function display() {
761
+ $url = self_admin_url( WP_Stream_Admin::ADMIN_PARENT_PAGE );
762
+
763
+ echo '<form method="get" action="' . esc_url( $url ) . '" id="record-filter-form">';
764
+ echo $this->filter_search(); // xss ok
765
+
766
+ parent::display();
767
+ echo '</form>';
768
+ }
769
+
770
+ function display_tablenav( $which ) {
771
+ if ( 'top' === $which ) : ?>
772
+ <div class="tablenav <?php echo esc_attr( $which ); ?>">
773
+ <?php
774
+ $this->pagination( $which );
775
+ $this->extra_tablenav( $which );
776
+ ?>
777
+
778
+ <br class="clear" />
779
+ </div>
780
+ <?php else : ?>
781
+ <div class="tablenav <?php echo esc_attr( $which ); ?>">
782
+ <?php
783
+ /**
784
+ * Fires after the list table is displayed.
785
+ *
786
+ * @since 1.0.0
787
+ */
788
+ do_action( 'wp_stream_after_list_table' );
789
+ $this->pagination( $which );
790
+ $this->extra_tablenav( $which );
791
+ ?>
792
+
793
+ <br class="clear" />
794
+ </div>
795
+ <?php
796
+ endif;
797
+ }
798
+
799
+ static function set_screen_option( $dummy, $option, $value ) {
800
+ if ( 'edit_stream_per_page' === $option ) {
801
+ return $value;
802
+ } else {
803
+ return $dummy;
804
+ }
805
+ }
806
+
807
+ static function set_live_update_option( $dummy, $option, $value ) {
808
+ if ( WP_Stream_Live_Update::USER_META_KEY === $option ) {
809
+ $value = $_POST[ WP_Stream_Live_Update::USER_META_KEY ];
810
+ return $value;
811
+ } else {
812
+ return $dummy;
813
+ }
814
+ }
815
+
816
+ public function screen_controls( $status, $args ) {
817
+ $user_id = get_current_user_id();
818
+ $option = get_user_meta( $user_id, WP_Stream_Live_Update::USER_META_KEY, true );
819
+ $heartbeat = wp_script_is( 'heartbeat', 'done' ) ? 'true' : 'false';
820
+
821
+ if ( 'on' === $option && 'false' === $heartbeat ) {
822
+ $option = 'off';
823
+
824
+ update_user_meta( $user_id, WP_Stream_Live_Update::USER_META_KEY, 'off' );
825
+ }
826
+
827
+ $nonce = wp_create_nonce( WP_Stream_Live_Update::USER_META_KEY . '_nonce' );
828
+
829
+ ob_start();
830
+ ?>
831
+ <fieldset>
832
+ <h5><?php esc_html_e( 'Live updates', 'stream' ) ?></h5>
833
+
834
+ <div>
835
+ <input type="hidden" name="stream_live_update_nonce" id="stream_live_update_nonce" value="<?php echo esc_attr( $nonce ) ?>" />
836
+ </div>
837
+ <div>
838
+ <input type="hidden" name="enable_live_update_user" id="enable_live_update_user" value="<?php echo absint( $user_id ) ?>" />
839
+ </div>
840
+ <div class="metabox-prefs stream-live-update-checkbox">
841
+ <label for="enable_live_update">
842
+ <input type="checkbox" value="on" name="enable_live_update" id="enable_live_update" data-heartbeat="<?php echo esc_attr( $heartbeat ) ?>" <?php checked( $option, 'on' ) ?> />
843
+ <?php esc_html_e( 'Enabled', 'stream' ) ?><span class="spinner"></span>
844
+ </label>
845
+ </div>
846
+ </fieldset>
847
+ <?php
848
+ return ob_get_clean();
849
+ }
850
+
851
+ /**
852
+ * This function is use to map List table column name with excluded setting keys
853
+ *
854
+ * @param $column string list table column name
855
+ *
856
+ * @return string setting name for that column
857
+ */
858
+ function get_column_excluded_setting_key( $column ) {
859
+ switch ( $column ) {
860
+ case 'connector':
861
+ $output = 'connectors';
862
+ break;
863
+ case 'context':
864
+ $output = 'contexts';
865
+ break;
866
+ case 'action':
867
+ $output = 'action';
868
+ break;
869
+ case 'ip':
870
+ $output = 'ip_addresses';
871
+ break;
872
+ case 'author':
873
+ $output = 'authors';
874
+ break;
875
+ default:
876
+ $output = false;
877
+ }
878
+
879
+ return $output;
880
+ }
881
+ }
classes/class-wp-stream-live-update.php ADDED
@@ -0,0 +1,222 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class WP_Stream_Live_Update {
4
+
5
+ /**
6
+ * User meta key/identifier
7
+ *
8
+ * @const string
9
+ */
10
+ const USER_META_KEY = 'stream_live_update_records';
11
+
12
+ /**
13
+ * List table object instance
14
+ *
15
+ * @var object
16
+ */
17
+ public static $list_table = null;
18
+
19
+ /**
20
+ * Load live updates methods
21
+ */
22
+ public static function load() {
23
+ // Heartbeat live update
24
+ add_filter( 'heartbeat_received', array( __CLASS__, 'heartbeat_received' ), 10, 2 );
25
+
26
+ // Enable/Disable live update per user
27
+ add_action( 'wp_ajax_stream_enable_live_update', array( __CLASS__, 'enable_live_update' ) );
28
+ }
29
+
30
+ /**
31
+ * Ajax function to enable/disable live update
32
+ *
33
+ * @return string Ajax respsonse back in JSON format
34
+ */
35
+ public static function enable_live_update() {
36
+ check_ajax_referer( self::USER_META_KEY . '_nonce', 'nonce' );
37
+
38
+ $input = array(
39
+ 'checked' => FILTER_SANITIZE_STRING,
40
+ 'user' => FILTER_SANITIZE_STRING,
41
+ 'heartbeat' => FILTER_SANITIZE_STRING,
42
+ );
43
+
44
+ $input = filter_input_array( INPUT_POST, $input );
45
+
46
+ if ( false === $input ) {
47
+ wp_send_json_error( 'Error in live update checkbox' );
48
+ }
49
+
50
+ $checked = ( 'checked' === $input['checked'] ) ? 'on' : 'off';
51
+
52
+ $user = (int) $input['user'];
53
+
54
+ if ( 'false' === $input['heartbeat'] ) {
55
+ update_user_meta( $user, self::USER_META_KEY, 'off' );
56
+
57
+ wp_send_json_error( esc_html__( "Live updates could not be enabled because Heartbeat is not loaded.\n\nYour hosting provider or another plugin may have disabled it for performance reasons.", 'stream' ) );
58
+
59
+ return;
60
+ }
61
+
62
+ $success = update_user_meta( $user, self::USER_META_KEY, $checked );
63
+
64
+ if ( $success ) {
65
+ wp_send_json_success( ( 'on' === $checked ) ? 'Live Updates enabled' : 'Live Updates disabled' );
66
+ } else {
67
+ wp_send_json_error( 'Live Updates checkbox error' );
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Sends updated actions to the list table view
73
+ *
74
+ * @todo Fix reliability issues with sidebar widgets
75
+ *
76
+ * @uses gather_updated_items
77
+ * @uses generate_row
78
+ *
79
+ * @param array Response to heartbeat
80
+ * @param array Response from heartbeat
81
+ *
82
+ * @return array Data sent to heartbeat
83
+ */
84
+ public static function live_update( $response, $data ) {
85
+ if ( ! isset( $data['wp-stream-heartbeat-last-time'] ) ) {
86
+ return;
87
+ }
88
+
89
+ $last_time = $data['wp-stream-heartbeat-last-time'];
90
+ $query = $data['wp-stream-heartbeat-query'];
91
+
92
+ if ( empty( $query ) ) {
93
+ $query = array();
94
+ }
95
+
96
+ // Decode the query
97
+ $query = json_decode( wp_kses_stripslashes( $query ) );
98
+
99
+ $updated_items = self::gather_updated_items( $last_time, (array) $query );
100
+
101
+ if ( ! empty( $updated_items ) ) {
102
+ ob_start();
103
+
104
+ foreach ( $updated_items as $item ) {
105
+ self::$list_table->single_row( $item );
106
+ }
107
+
108
+ $send = ob_get_clean();
109
+ } else {
110
+ $send = '';
111
+ }
112
+
113
+ return $send;
114
+ }
115
+
116
+ /**
117
+ * Sends Updated Actions to the List Table View
118
+ *
119
+ * @param int Timestamp of last update
120
+ * @param array Query args
121
+ *
122
+ * @return array Array of recently updated items
123
+ */
124
+ public static function gather_updated_items( $last_time, $args = array() ) {
125
+ if ( false === $last_time ) {
126
+ return '';
127
+ }
128
+
129
+ if ( empty( self::$list_table->items ) ) {
130
+ return '';
131
+ }
132
+
133
+ $items = array();
134
+
135
+ foreach ( self::$list_table->items as $item ) {
136
+ if ( strtotime( $item->created ) > strtotime( $last_time ) ) {
137
+ $items[] = $item;
138
+ } else {
139
+ break;
140
+ }
141
+ }
142
+
143
+ return $items;
144
+ }
145
+
146
+ /**
147
+ * Handles live updates for both dashboard widget and Stream Post List
148
+ *
149
+ * @action heartbeat_recieved
150
+ *
151
+ * @param array Response to be sent to heartbeat tick
152
+ * @param array Data from heartbeat send
153
+ *
154
+ * @return array Data sent to heartbeat tick
155
+ */
156
+ public static function heartbeat_received( $response, $data ) {
157
+ // Only fire when Stream is requesting a live update
158
+ if ( ! isset( $data['wp-stream-heartbeat'] ) ) {
159
+ return $response;
160
+ }
161
+
162
+ $option = get_option( 'dashboard_stream_activity_options' );
163
+ $enable_stream_update = ( 'off' !== get_user_meta( get_current_user_id(), self::USER_META_KEY, true ) );
164
+ $enable_dashboard_update = ( 'off' !== ( $option['live_update'] ) );
165
+
166
+ // Register list table
167
+ self::$list_table = new WP_Stream_List_Table( array( 'screen' => 'toplevel_page_' . WP_Stream_Admin::RECORDS_PAGE_SLUG ) );
168
+ self::$list_table->prepare_items();
169
+
170
+ $total_items = isset( self::$list_table->_pagination_args['total_items'] ) ? self::$list_table->_pagination_args['total_items'] : null;
171
+ $total_pages = isset( self::$list_table->_pagination_args['total_pages'] ) ? self::$list_table->_pagination_args['total_pages'] : null;
172
+ $per_page = isset( self::$list_table->_pagination_args['per_page'] ) ? self::$list_table->_pagination_args['per_page'] : null;
173
+
174
+ if ( isset( $data['wp-stream-heartbeat'] ) && isset( $total_items ) ) {
175
+ $response['total_items'] = $total_items;
176
+ $response['total_items_i18n'] = sprintf( _n( '1 item', '%s items', $total_items ), number_format_i18n( $total_items ) );
177
+ }
178
+
179
+ if ( isset( $data['wp-stream-heartbeat'] ) && 'live-update' === $data['wp-stream-heartbeat'] && $enable_stream_update ) {
180
+
181
+ if ( ! empty( $data['wp-stream-heartbeat'] ) ) {
182
+ if ( isset( $total_pages ) ) {
183
+ $response['total_pages'] = $total_pages;
184
+ $response['total_pages_i18n'] = number_format_i18n( $total_pages );
185
+
186
+ $query_args = json_decode( $data['wp-stream-heartbeat-query'], true );
187
+ $query_args['paged'] = $total_pages;
188
+
189
+ $response['last_page_link'] = add_query_arg( $query_args, admin_url( 'admin.php' ) );
190
+ } else {
191
+ $response['total_pages'] = 0;
192
+ }
193
+ }
194
+
195
+ $response['wp-stream-heartbeat'] = self::live_update( $response, $data );
196
+
197
+ } elseif ( isset( $data['wp-stream-heartbeat'] ) && 'dashboard-update' === $data['wp-stream-heartbeat'] && $enable_dashboard_update ) {
198
+
199
+ $per_page = isset( $option['records_per_page'] ) ? absint( $option['records_per_page'] ) : 5;
200
+
201
+ if ( isset( $total_items ) ) {
202
+ $total_pages = ceil( $total_items / $per_page );
203
+ $response['total_pages'] = $total_pages;
204
+ $response['total_pages_i18n'] = number_format_i18n( $total_pages );
205
+
206
+ $query_args['page'] = WP_Stream_Admin::RECORDS_PAGE_SLUG;
207
+ $query_args['paged'] = $total_pages;
208
+
209
+ $response['last_page_link'] = add_query_arg( $query_args, admin_url( 'admin.php' ) );
210
+ }
211
+
212
+ $response['per_page'] = $per_page;
213
+ $response['wp-stream-heartbeat'] = WP_Stream_Dashboard_Widget::live_update( $response, $data );
214
+
215
+ } else {
216
+ $response['log'] = 'fail';
217
+ }
218
+
219
+ return $response;
220
+ }
221
+
222
+ }
classes/class-wp-stream-log.php ADDED
@@ -0,0 +1,309 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class WP_Stream_Log {
4
+
5
+ /**
6
+ * Log handler
7
+ *
8
+ * @var \WP_Stream_Log
9
+ */
10
+ public static $instance = null;
11
+
12
+ /**
13
+ * Load log handler class, filterable by extensions
14
+ *
15
+ * @return void
16
+ */
17
+ public static function load() {
18
+ /**
19
+ * Filter allows developers to change log handler class
20
+ *
21
+ * @since 0.2.0
22
+ *
23
+ * @return string Class name to use for log handling
24
+ */
25
+ $log_handler = apply_filters( 'wp_stream_log_handler', __CLASS__ );
26
+
27
+ self::$instance = new $log_handler;
28
+ }
29
+
30
+ /**
31
+ * Return active instance of this class
32
+ *
33
+ * @return WP_Stream_Log
34
+ */
35
+ public static function get_instance() {
36
+ if ( ! self::$instance ) {
37
+ $class = __CLASS__;
38
+ self::$instance = new $class;
39
+ }
40
+
41
+ return self::$instance;
42
+ }
43
+
44
+ /**
45
+ * Log handler
46
+ *
47
+ * @param $connector
48
+ * @param string $message sprintf-ready error message string
49
+ * @param array $args sprintf (and extra) arguments to use
50
+ * @param int $object_id Target object id
51
+ * @param string $context Context of the event
52
+ * @param string $action Action of the event
53
+ * @param int $user_id User responsible for the event
54
+ *
55
+ * @return void
56
+ */
57
+ public function log( $connector, $message, $args, $object_id, $context, $action, $user_id = null ) {
58
+ global $wpdb;
59
+
60
+ if ( is_null( $user_id ) ) {
61
+ $user_id = get_current_user_id();
62
+ }
63
+
64
+ if ( is_null( $object_id ) ) {
65
+ $object_id = 0;
66
+ }
67
+
68
+ $wp_cron_tracking = isset( WP_Stream_Settings::$options['advanced_wp_cron_tracking'] ) ? WP_Stream_Settings::$options['advanced_wp_cron_tracking'] : false;
69
+ $agent = WP_Stream_Author::get_current_agent();
70
+
71
+ // WP cron tracking requires opt-in
72
+ if ( ! $wp_cron_tracking && 'wp_cron' === $agent ) {
73
+ return;
74
+ }
75
+
76
+ $user = new WP_User( $user_id );
77
+ $roles = get_option( $wpdb->get_blog_prefix() . 'user_roles' );
78
+ $visibility = 'publish';
79
+
80
+ if ( self::is_record_excluded( $connector, $context, $action, $user ) ) {
81
+ $visibility = 'private';
82
+ }
83
+
84
+ if ( defined( 'WP_CLI' ) && empty( $user->display_name ) ) {
85
+ $display_name = 'WP-CLI';
86
+ } elseif ( ! empty( $user->display_name ) ) {
87
+ $display_name = $user->display_name;
88
+ } else {
89
+ $display_name = '';
90
+ }
91
+
92
+ $author_meta = array(
93
+ 'user_email' => (string) ! empty( $user->user_email ) ? $user->user_email : '',
94
+ 'display_name' => (string) $display_name,
95
+ 'user_login' => (string) ! empty( $user->user_login ) ? $user->user_login : '',
96
+ 'user_role_label' => (string) ! empty( $user->roles ) ? $roles[ $user->roles[0] ]['name'] : '',
97
+ 'agent' => (string) $agent,
98
+ );
99
+
100
+ if ( ( defined( 'WP_CLI' ) ) && function_exists( 'posix_getuid' ) ) {
101
+ $uid = posix_getuid();
102
+ $user_info = posix_getpwuid( $uid );
103
+
104
+ $author_meta['system_user_id'] = (int) $uid;
105
+ $author_meta['system_user_name'] = (string) $user_info['name'];
106
+ }
107
+
108
+ // Prevent any meta with null values from being logged
109
+ $stream_meta = array_filter(
110
+ $args,
111
+ function ( $var ) {
112
+ return ! is_null( $var );
113
+ }
114
+ );
115
+
116
+ // All meta must be strings, so we will serialize any array meta values
117
+ array_walk(
118
+ $stream_meta,
119
+ function( &$v ) {
120
+ $v = (string) maybe_serialize( $v );
121
+ }
122
+ );
123
+
124
+ // Get the current time in milliseconds
125
+ $iso_8601_extended_date = wp_stream_get_iso_8601_extended_date();
126
+
127
+ $recordarr = array(
128
+ 'object_id' => (int) $object_id,
129
+ 'site_id' => (int) is_multisite() ? get_current_site()->id : 1,
130
+ 'blog_id' => (int) apply_filters( 'wp_stream_blog_id_logged', get_current_blog_id() ),
131
+ 'author' => (int) $user_id,
132
+ 'author_role' => (string) ! empty( $user->roles ) ? $user->roles[0] : '',
133
+ 'author_meta' => (array) $author_meta,
134
+ 'created' => (string) $iso_8601_extended_date,
135
+ 'visibility' => (string) $visibility,
136
+ 'type' => 'stream',
137
+ 'summary' => (string) vsprintf( $message, $args ),
138
+ 'connector' => (string) $connector,
139
+ 'context' => (string) $context,
140
+ 'action' => (string) $action,
141
+ 'stream_meta' => (array) $stream_meta,
142
+ 'ip' => (string) wp_stream_filter_input( INPUT_SERVER, 'REMOTE_ADDR', FILTER_VALIDATE_IP ),
143
+ );
144
+
145
+ WP_Stream::$db->store( array( $recordarr ) );
146
+
147
+ self::debug_backtrace( $recordarr );
148
+ }
149
+
150
+ /**
151
+ * This function is use to check whether or not a record should be excluded from the log
152
+ *
153
+ * @param $connector string name of the connector being logged
154
+ * @param $context string name of the context being logged
155
+ * @param $action string name of the action being logged
156
+ * @param $user_id int id of the user being logged
157
+ * @param $ip string ip address being logged
158
+ * @return bool
159
+ */
160
+ public function is_record_excluded( $connector, $context, $action, $user = null, $ip = null ) {
161
+ if ( is_null( $user ) ) {
162
+ $user = wp_get_current_user();
163
+ }
164
+
165
+ if ( is_null( $ip ) ) {
166
+ $ip = wp_stream_filter_input( INPUT_SERVER, 'REMOTE_ADDR', FILTER_VALIDATE_IP );
167
+ } else {
168
+ $ip = wp_stream_filter_var( $ip, FILTER_VALIDATE_IP );
169
+ }
170
+
171
+ $user_role = isset( $user->roles[0] ) ? $user->roles[0] : null;
172
+
173
+ $record = array(
174
+ 'connector' => $connector,
175
+ 'context' => $context,
176
+ 'action' => $action,
177
+ 'author' => $user->ID,
178
+ 'role' => $user_role,
179
+ 'ip_address' => $ip,
180
+ );
181
+
182
+ $exclude_settings = isset( WP_Stream_Settings::$options['exclude_rules'] ) ? WP_Stream_Settings::$options['exclude_rules'] : array();
183
+
184
+ if ( isset( $exclude_settings['exclude_row'] ) && ! empty( $exclude_settings['exclude_row'] ) ) {
185
+ foreach ( $exclude_settings['exclude_row'] as $key => $value ) {
186
+ // Prepare values
187
+ $author_or_role = isset( $exclude_settings['author_or_role'][ $key ] ) ? $exclude_settings['author_or_role'][ $key ] : '';
188
+ $connector = isset( $exclude_settings['connector'][ $key ] ) ? $exclude_settings['connector'][ $key ] : '';
189
+ $context = isset( $exclude_settings['context'][ $key ] ) ? $exclude_settings['context'][ $key ] : '';
190
+ $action = isset( $exclude_settings['action'][ $key ] ) ? $exclude_settings['action'][ $key ] : '';
191
+ $ip_address = isset( $exclude_settings['ip_address'][ $key ] ) ? $exclude_settings['ip_address'][ $key ] : '';
192
+
193
+ $exclude = array(
194
+ 'connector' => ! empty( $connector ) ? $connector : null,
195
+ 'context' => ! empty( $context ) ? $context : null,
196
+ 'action' => ! empty( $action ) ? $action : null,
197
+ 'ip_address' => ! empty( $ip_address ) ? $ip_address : null,
198
+ 'author' => is_numeric( $author_or_role ) ? absint( $author_or_role ) : null,
199
+ 'role' => ( ! empty( $author_or_role ) && ! is_numeric( $author_or_role ) ) ? $author_or_role : null,
200
+ );
201
+
202
+ $exclude_rules = array_filter( $exclude, 'strlen' );
203
+
204
+ if ( ! empty( $exclude_rules ) ) {
205
+ $excluded = true;
206
+
207
+ foreach ( $exclude_rules as $exclude_key => $exclude_value ) {
208
+ if ( $record[ $exclude_key ] !== $exclude_value ) {
209
+ $excluded = false;
210
+ break;
211
+ }
212
+ }
213
+
214
+ if ( $excluded ) {
215
+ return true;
216
+ }
217
+ }
218
+ }
219
+ }
220
+
221
+ return false;
222
+ }
223
+
224
+ /**
225
+ * Send a full backtrace of calls to the PHP error log for debugging
226
+ *
227
+ * @param array $recordarr
228
+ *
229
+ * @return void
230
+ */
231
+ public static function debug_backtrace( $recordarr ) {
232
+ /**
233
+ * Enable debug backtrace on records.
234
+ *
235
+ * This filter is for developer use only. When enabled, Stream will send
236
+ * a full debug backtrace of PHP calls for each record. Optionally, you may
237
+ * use the available $recordarr parameter to specify what types of records to
238
+ * create backtrace logs for.
239
+ *
240
+ * @since 2.0.2
241
+ *
242
+ * @param array $recordarr
243
+ *
244
+ * @return bool Set to FALSE by default (backtrace disabled)
245
+ */
246
+ $enabled = apply_filters( 'wp_stream_debug_backtrace', false, $recordarr );
247
+
248
+ if ( ! $enabled ) {
249
+ return;
250
+ }
251
+
252
+ if ( version_compare( PHP_VERSION, '5.3.6', '<' ) ) {
253
+ error_log( 'WP Stream debug backtrace requires at least PHP 5.3.6' );
254
+ return;
255
+ }
256
+
257
+ // Record details
258
+ $summary = isset( $recordarr['summary'] ) ? $recordarr['summary'] : null;
259
+ $author = isset( $recordarr['author'] ) ? $recordarr['author'] : null;
260
+ $connector = isset( $recordarr['connector'] ) ? $recordarr['connector'] : null;
261
+ $context = isset( $recordarr['context'] ) ? $recordarr['context'] : null;
262
+ $action = isset( $recordarr['action'] ) ? $recordarr['action'] : null;
263
+
264
+ // Stream meta
265
+ $stream_meta = isset( $recordarr['stream_meta'] ) ? $recordarr['stream_meta'] : null;
266
+
267
+ if ( $stream_meta ) {
268
+ array_walk( $stream_meta, function( &$value, $key ) {
269
+ $value = sprintf( '%s: %s', $key, ( '' === $value ) ? 'null' : $value );
270
+ });
271
+
272
+ $stream_meta = implode( ', ', $stream_meta );
273
+ }
274
+
275
+ // Author meta
276
+ $author_meta = isset( $recordarr['author_meta'] ) ? $recordarr['author_meta'] : null;
277
+
278
+ if ( $author_meta ) {
279
+ array_walk( $author_meta, function( &$value, $key ) {
280
+ $value = sprintf( '%s: %s', $key, ( '' === $value ) ? 'null' : $value );
281
+ });
282
+
283
+ $author_meta = implode( ', ', $author_meta );
284
+ }
285
+
286
+ // Debug backtrace
287
+ ob_start();
288
+
289
+ debug_print_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS ); // Option to ignore args requires PHP 5.3.6
290
+
291
+ $backtrace = ob_get_clean();
292
+ $backtrace = array_values( array_filter( explode( "\n", $backtrace ) ) );
293
+
294
+ $output = sprintf(
295
+ "WP Stream Debug Backtrace\n\n Summary | %s\n Author | %s\n Connector | %s\n Context | %s\n Action | %s\nStream Meta | %s\nAuthor Meta | %s\n\n%s\n",
296
+ $summary,
297
+ $author,
298
+ $connector,
299
+ $context,
300
+ $action,
301
+ $stream_meta,
302
+ $author_meta,
303
+ implode( "\n", $backtrace )
304
+ );
305
+
306
+ error_log( $output );
307
+ }
308
+
309
+ }
classes/class-wp-stream-migrate.php ADDED
@@ -0,0 +1,583 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class WP_Stream_Migrate {
4
+
5
+ /**
6
+ * Migrate delay transient name/identifier used when user wants to be reminded to migrate later
7
+ */
8
+ const MIGRATE_DELAY_TRANSIENT = 'wp_stream_migrate_delayed';
9
+
10
+ /**
11
+ * Hold the current site ID
12
+ *
13
+ * @var int
14
+ */
15
+ public static $site_id = 1;
16
+
17
+ /**
18
+ * Hold the current blog ID
19
+ *
20
+ * @var int
21
+ */
22
+ public static $blog_id = 1;
23
+
24
+ /**
25
+ * Hold the total number of legacy records found in the DB
26
+ *
27
+ * @var int
28
+ */
29
+ public static $record_count = 0;
30
+
31
+ /**
32
+ * Limit payload chunks to a certain number of records
33
+ *
34
+ * @var int
35
+ */
36
+ public static $limit = 0;
37
+
38
+ /**
39
+ * Hold unformatted records temporarily for deletion
40
+ *
41
+ * @var array
42
+ */
43
+ private static $_records = array();
44
+
45
+ /**
46
+ * Check that legacy data exists before doing anything
47
+ *
48
+ * @return void
49
+ */
50
+ public static function load() {
51
+ // Exit early if there is no option holding the DB version
52
+ if ( false === get_site_option( 'wp_stream_db' ) ) {
53
+ return;
54
+ }
55
+
56
+ global $wpdb;
57
+
58
+ // If there are no legacy tables found, then attempt to clear all legacy data and exit early
59
+ if ( null === $wpdb->get_var( "SHOW TABLES LIKE '{$wpdb->base_prefix}stream'" ) ) {
60
+ self::drop_legacy_data( false );
61
+ return;
62
+ }
63
+
64
+ self::$site_id = is_multisite() ? get_current_site()->id : 1;
65
+ self::$blog_id = get_current_blog_id();
66
+
67
+ self::$record_count = $wpdb->get_var(
68
+ $wpdb->prepare( "
69
+ SELECT COUNT(*)
70
+ FROM {$wpdb->base_prefix}stream AS s, {$wpdb->base_prefix}stream_context AS sc
71
+ WHERE s.site_id = %d
72
+ AND s.blog_id = %d
73
+ AND s.type = 'stream'
74
+ AND sc.record_id = s.ID
75
+ ",
76
+ self::$site_id,
77
+ self::$blog_id
78
+ )
79
+ );
80
+
81
+ // If there are no legacy records for this site/blog, then attempt to clear all legacy data and exit early
82
+ if ( 0 === self::$record_count ) {
83
+ self::drop_legacy_data();
84
+ return;
85
+ }
86
+
87
+ self::$limit = apply_filters( 'wp_stream_migrate_chunk_size', 100 );
88
+
89
+ add_action( 'admin_notices', array( __CLASS__, 'migrate_notice' ), 9 );
90
+
91
+ add_action( 'wp_ajax_wp_stream_migrate_action', array( __CLASS__, 'process_migrate_action' ) );
92
+ }
93
+
94
+ /**
95
+ * Give the user options for how to handle their legacy Stream records
96
+ *
97
+ * @action admin_notices
98
+ * @return void
99
+ */
100
+ public static function show_migrate_notice() {
101
+ if ( ! isset( $_GET['migrate_action'] ) && WP_Stream::is_connected() && WP_Stream_Admin::is_stream_screen() && ! empty( self::$record_count ) && false === get_transient( self::MIGRATE_DELAY_TRANSIENT ) ) {
102
+ return true;
103
+ }
104
+
105
+ return false;
106
+ }
107
+
108
+ /**
109
+ * Give the user options for how to handle their legacy Stream records
110
+ *
111
+ * @action admin_notices
112
+ * @return void
113
+ */
114
+ public static function migrate_notice() {
115
+ if ( ! self::show_migrate_notice() ) {
116
+ return;
117
+ }
118
+
119
+ $notice = sprintf(
120
+ '<strong id="stream-migrate-title">%s</strong></p><p id="stream-migrate-message">%s</p><div id="stream-migrate-progress"><progress value="0" max="100"></progress> <strong>0&#37;</strong> <em></em> <button id="stream-migrate-actions-close" class="button button-secondary">%s</button><div class="clear"></div></div><p id="stream-migrate-actions"><button id="stream-start-migrate" class="button button-primary">%s</button> <button id="stream-migrate-reminder" class="button button-secondary">%s</button> <a href="#" id="stream-delete-records" class="delete">%s</a>',
121
+ __( 'Migrate Stream Records', 'stream' ),
122
+ sprintf( __( 'We found %s existing Stream records that need to be migrated to your Stream account.', 'stream' ), number_format( self::$record_count ) ),
123
+ __( 'Close', 'stream' ),
124
+ __( 'Start Migration Now', 'stream' ),
125
+ __( 'Remind Me Later', 'stream' ),
126
+ __( 'Delete Existing Records', 'stream' )
127
+ );
128
+
129
+ WP_Stream::notice( $notice, false );
130
+ }
131
+
132
+ /**
133
+ * Ajax callback for processing migrate actions
134
+ *
135
+ * Break down the total number of records found into reasonably-sized chunks
136
+ * and send each of those chunks to the Stream API
137
+ *
138
+ * Drops the legacy Stream data from the DB once the API has consumed everything
139
+ *
140
+ * @action wp_ajax_wp_stream_migrate_action
141
+ * @return void
142
+ */
143
+ public static function process_migrate_action() {
144
+ $action = wp_stream_filter_input( INPUT_POST, 'migrate_action' );
145
+ $nonce = wp_stream_filter_input( INPUT_POST, 'nonce' );
146
+
147
+ if ( ! wp_verify_nonce( $nonce, 'wp_stream_migrate-' . absint( get_current_blog_id() ) . absint( get_current_user_id() ) ) ) {
148
+ return;
149
+ }
150
+
151
+ set_time_limit( 0 ); // Just in case, this could take a while for some
152
+
153
+ if ( 'migrate' === $action ) {
154
+ self::migrate_notification_rules();
155
+
156
+ $records = self::get_records( self::$limit );
157
+
158
+ if ( ! $records ) {
159
+ // If all the records are gone, clean everything up
160
+ self::drop_legacy_data();
161
+
162
+ wp_send_json_success( __( 'Migration complete!', 'stream' ) );
163
+ }
164
+
165
+ $response = self::send_records( $records );
166
+
167
+ if ( true === $response ) {
168
+ // Delete the records that were just sent to the API successfully
169
+ self::delete_records( self::$_records );
170
+
171
+ wp_send_json_success( 'migrate' );
172
+ } else {
173
+ if ( isset( $response['body']['message'] ) && ! empty( $response['body']['message'] ) ) {
174
+ $body = json_decode( $response['body'], true );
175
+ $message = $body['message'];
176
+ } elseif ( isset( $response['response']['message'] ) && ! empty( $response['response']['message'] ) ) {
177
+ $message = $response['response']['message'];
178
+ } else {
179
+ $message = __( 'An unknown error occurred during migration.', 'stream' );
180
+ }
181
+
182
+ wp_send_json_error( sprintf( __( '%s Please try again later or contact support.', 'stream' ), esc_html( $message ) ) );
183
+ }
184
+ }
185
+
186
+ if ( 'delay' === $action ) {
187
+ set_transient( self::MIGRATE_DELAY_TRANSIENT, "Don't nag me, bro", HOUR_IN_SECONDS * 3 );
188
+
189
+ wp_send_json_success( __( "OK, we'll remind you again in a few hours.", 'stream' ) );
190
+ }
191
+
192
+ if ( 'delete' === $action ) {
193
+ $success_message = __( 'All existing records have been deleted from the database.', 'stream' );
194
+
195
+ if ( ! is_multisite() ) {
196
+ // If this is a single-site install, force delete everything
197
+ self::drop_legacy_data( true, true );
198
+
199
+ wp_send_json_success( $success_message );
200
+ } else {
201
+ // If multisite, only delete records for this site - this will take longer
202
+ $records = self::get_record_ids( self::$limit );
203
+
204
+ if ( ! $records ) {
205
+ // If all the records are gone, clean everything up
206
+ self::drop_legacy_data();
207
+
208
+ wp_send_json_success( $success_message );
209
+ } else {
210
+ self::delete_records( $records );
211
+
212
+ wp_send_json_success( 'delete' );
213
+ }
214
+ }
215
+ }
216
+
217
+ die();
218
+ }
219
+
220
+ /**
221
+ * Migrate notification_rule records to the new custom post type
222
+ *
223
+ * @return void
224
+ */
225
+ private static function migrate_notification_rules() {
226
+ global $wpdb;
227
+
228
+ // Blog ID is set to 0 on single site installs
229
+ $blog_id = is_multisite() ? self::$blog_id : 0;
230
+
231
+ $rules = $wpdb->get_results(
232
+ $wpdb->prepare( "
233
+ SELECT *
234
+ FROM {$wpdb->base_prefix}stream
235
+ WHERE site_id = %d
236
+ AND blog_id = %d
237
+ AND type = 'notification_rule'
238
+ ORDER BY created DESC
239
+ ",
240
+ self::$site_id,
241
+ $blog_id
242
+ ),
243
+ ARRAY_A
244
+ );
245
+
246
+ if ( empty( $rules ) ) {
247
+ return;
248
+ }
249
+
250
+ foreach ( $rules as $rule => $data ) {
251
+ $rule_post_args = array();
252
+ $rule_post_meta = array();
253
+
254
+ // Set args for the new rule post
255
+ $rule_post_args['post_title'] = $rules[ $rule ]['summary'];
256
+ $rule_post_args['post_type'] = WP_Stream_Notifications_Post_Type::POSTTYPE;
257
+ $rule_post_args['post_status'] = ( 'active' === $rules[ $rule ]['visibility'] ) ? 'publish' : 'draft';
258
+ $rule_post_args['post_date'] = get_date_from_gmt( $rules[ $rule ]['created'] );
259
+ $rule_post_args['post_date_gmt'] = $rules[ $rule ]['created']; // May not work, known bug in WP, see workaround below
260
+ $rule_post_args['comment_status'] = 'closed';
261
+ $rule_post_args['ping_status'] = 'closed';
262
+
263
+ // Get rule meta
264
+ $stream_rule_meta = $wpdb->get_results( $wpdb->prepare( "SELECT meta_key, meta_value FROM {$wpdb->base_prefix}stream_meta WHERE record_id = %d", $rules[ $rule ]['ID'] ), ARRAY_A );
265
+
266
+ // Prepare meta values for rule post meta
267
+ foreach ( $stream_rule_meta as $meta => $value ) {
268
+ $rule_post_meta[ $value['meta_key'] ] = maybe_unserialize( $value['meta_value'] );
269
+ }
270
+
271
+ // Get rule option, which is automatically unserialized
272
+ $stream_rule_option = get_option( 'stream_notifications_' . absint( $rules[ $rule ]['ID'] ) );
273
+
274
+ // Prepare option values for rule post meta
275
+ $rule_post_meta['triggers'] = isset( $stream_rule_option['triggers'] ) ? $stream_rule_option['triggers'] : array();
276
+ $rule_post_meta['groups'] = isset( $stream_rule_option['groups'] ) ? $stream_rule_option['groups'] : array();
277
+ $rule_post_meta['alerts'] = isset( $stream_rule_option['alerts'] ) ? $stream_rule_option['alerts'] : array();
278
+
279
+ // Insert rule as a new post
280
+ $post_id = wp_insert_post( $rule_post_args );
281
+
282
+ // Workaround to fix bug in wp_insert_post() not honoring the `post_date_gmt` arg
283
+ // See: https://core.trac.wordpress.org/ticket/15946
284
+ $wpdb->update( $wpdb->prefix . 'posts', array( 'post_date_gmt' => $rules[ $rule ]['created'] ), array( 'ID' => $post_id ), array( '%s' ), array( '%d' ) );
285
+
286
+ // Save the rule post meta
287
+ foreach ( $rule_post_meta as $key => $value ) {
288
+ update_post_meta( $post_id, $key, $value );
289
+ }
290
+
291
+ // Delete the old option
292
+ delete_option( 'stream_notifications_' . absint( $rules[ $rule ]['ID'] ) );
293
+ }
294
+
295
+ // No need for chunks since there likely won't be more than a few dozen rules
296
+ self::delete_records( $rules );
297
+ }
298
+
299
+ /**
300
+ * Send records to the API
301
+ *
302
+ * @param array $records
303
+ *
304
+ * @return mixed True on success, the full response array on failure.
305
+ */
306
+ private static function send_records( $records ) {
307
+ if ( empty( $records ) || ! WP_Stream::$api->site_uuid ) {
308
+ return false;
309
+ }
310
+
311
+ $url = WP_Stream::$api->request_url( sprintf( '/sites/%s/records', urlencode( WP_Stream::$api->site_uuid ) ) );
312
+ $args = array(
313
+ 'method' => 'POST',
314
+ 'body' => json_encode( array( 'records' => $records ) ),
315
+ 'sslverify' => true,
316
+ 'blocking' => true,
317
+ 'headers' => array(
318
+ 'Content-Type' => 'application/json',
319
+ 'Accept-Version' => WP_Stream::$api->api_version,
320
+ 'Stream-Site-API-Key' => WP_Stream::$api->api_key,
321
+ ),
322
+ );
323
+
324
+ $response = wp_remote_request( $url, $args );
325
+
326
+ // Loose comparison needed
327
+ if ( ! is_wp_error( $response ) && isset( $response['response']['code'] ) && 201 == $response['response']['code'] ) {
328
+ return true;
329
+ } else {
330
+ return (array) $response;
331
+ }
332
+ }
333
+
334
+ /**
335
+ * Get a chunk of records formatted for Stream API ingestion
336
+ *
337
+ * @param int $limit The number of rows to query
338
+ *
339
+ * @return mixed An array of record arrays, or FALSE if no records were found
340
+ */
341
+ private static function get_records( $limit = null ) {
342
+ $limit = is_int( $limit ) ? $limit : self::$limit;
343
+
344
+ global $wpdb;
345
+
346
+ $records = $wpdb->get_results(
347
+ $wpdb->prepare( "
348
+ SELECT s.*, sc.connector, sc.context, sc.action
349
+ FROM {$wpdb->base_prefix}stream AS s, {$wpdb->base_prefix}stream_context AS sc
350
+ WHERE s.site_id = %d
351
+ AND s.blog_id = %d
352
+ AND s.type = 'stream'
353
+ AND sc.record_id = s.ID
354
+ ORDER BY s.created DESC
355
+ LIMIT %d
356
+ ",
357
+ self::$site_id,
358
+ self::$blog_id,
359
+ $limit
360
+ ),
361
+ ARRAY_A
362
+ );
363
+
364
+ if ( empty( $records ) ) {
365
+ return false;
366
+ }
367
+
368
+ self::$_records = array();
369
+
370
+ foreach ( $records as $record => $data ) {
371
+ $stream_meta = $wpdb->get_results( $wpdb->prepare( "SELECT meta_key, meta_value FROM {$wpdb->base_prefix}stream_meta WHERE record_id = %d", $records[ $record ]['ID'] ), ARRAY_A );
372
+ $stream_meta_output = array();
373
+ $author_meta_output = array();
374
+
375
+ foreach ( $stream_meta as $key => $meta ) {
376
+
377
+ if ( 'author_meta' === $meta['meta_key'] && ! empty( $meta['meta_value'] ) ) {
378
+ $author_meta_output = maybe_unserialize( $meta['meta_value'] );
379
+
380
+ unset( $stream_meta[ $key ] );
381
+
382
+ continue;
383
+ }
384
+
385
+ // Unserialize meta first so we can then check for malformed serialized strings
386
+ $stream_meta_output[ $meta['meta_key'] ] = maybe_unserialize( $meta['meta_value'] );
387
+
388
+ // If any serialized data is still lingering in the meta value that means it's malformed and should be removed
389
+ if (
390
+ is_string( $stream_meta_output[ $meta['meta_key'] ] )
391
+ &&
392
+ 1 === preg_match( '/(a|O) ?\x3a ?[0-9]+ ?\x3a ?\x7b/', $stream_meta_output[ $meta['meta_key'] ] )
393
+ ) {
394
+ unset( $stream_meta_output[ $meta['meta_key'] ] );
395
+
396
+ continue;
397
+ }
398
+
399
+ // All meta must be strings, so serialize any array meta values again
400
+ $stream_meta_output[ $meta['meta_key'] ] = (string) maybe_serialize( $stream_meta_output[ $meta['meta_key'] ] );
401
+ }
402
+
403
+ // All author meta must be strings
404
+ array_walk(
405
+ $author_meta_output,
406
+ function( &$v ) {
407
+ $v = (string) $v;
408
+ }
409
+ );
410
+
411
+ $records[ $record ]['stream_meta'] = $stream_meta_output;
412
+ $records[ $record ]['author_meta'] = $author_meta_output;
413
+
414
+ self::$_records[] = $records[ $record ];
415
+
416
+ $records[ $record ]['created'] = wp_stream_get_iso_8601_extended_date( strtotime( $records[ $record ]['created'] ) );
417
+
418
+ unset( $records[ $record ]['ID'] );
419
+ unset( $records[ $record ]['parent'] );
420
+
421
+ // Ensure required fields always exist
422
+ $records[ $record ]['site_id'] = ! empty( $records[ $record ]['site_id'] ) ? $records[ $record ]['site_id'] : 1;
423
+ $records[ $record ]['blog_id'] = ! empty( $records[ $record ]['blog_id'] ) ? $records[ $record ]['blog_id'] : 1;
424
+ $records[ $record ]['object_id'] = ! empty( $records[ $record ]['object_id'] ) ? $records[ $record ]['object_id'] : 0;
425
+ $records[ $record ]['author'] = ! empty( $records[ $record ]['author'] ) ? $records[ $record ]['author'] : 0;
426
+ $records[ $record ]['author_role'] = ! empty( $records[ $record ]['author_role'] ) ? $records[ $record ]['author_role'] : '';
427
+ $records[ $record ]['ip'] = ! empty( $records[ $record ]['ip'] ) ? $records[ $record ]['ip'] : '';
428
+ }
429
+
430
+ return $records;
431
+ }
432
+
433
+ /**
434
+ * Get a chunk of record IDs
435
+ *
436
+ * @param int $limit The number of rows to query
437
+ *
438
+ * @return mixed An array of record IDs, or FALSE if no records were found
439
+ */
440
+ private static function get_record_ids( $limit = null ) {
441
+ $limit = is_int( $limit ) ? $limit : self::$limit;
442
+
443
+ global $wpdb;
444
+
445
+ $records = $wpdb->get_col(
446
+ $wpdb->prepare( "
447
+ SELECT s.ID
448
+ FROM {$wpdb->base_prefix}stream AS s
449
+ WHERE s.site_id = %d
450
+ AND s.blog_id = %d
451
+ AND s.type = 'stream'
452
+ LIMIT %d
453
+ ",
454
+ self::$site_id,
455
+ self::$blog_id,
456
+ $limit
457
+ )
458
+ );
459
+
460
+ if ( empty( $records ) ) {
461
+ return false;
462
+ }
463
+
464
+ return $records;
465
+ }
466
+
467
+ /**
468
+ * Drop the legacy Stream records from the database for the current site/blog
469
+ *
470
+ * @param array $records An array of record arrays.
471
+ *
472
+ * @return void
473
+ */
474
+ private static function delete_records( $records ) {
475
+ if ( empty( $records ) ) {
476
+ return;
477
+ }
478
+
479
+ global $wpdb;
480
+
481
+ // Delete legacy rows from each Stream table for these records only
482
+ foreach ( $records as $record ) {
483
+ // Get the record ID from an array of records, or from an array of IDs
484
+ if ( isset( $record['ID'] ) ) {
485
+ $record_id = $record['ID'];
486
+ } elseif ( is_numeric( $record ) ) {
487
+ $record_id = $record;
488
+ } else {
489
+ $record_id = false;
490
+ }
491
+
492
+ if ( empty( $record_id ) ) {
493
+ continue;
494
+ }
495
+
496
+ $wpdb->delete( $wpdb->base_prefix . 'stream', array( 'ID' => $record_id ), array( '%d' ) );
497
+ $wpdb->delete( $wpdb->base_prefix . 'stream_context', array( 'record_id' => $record_id ), array( '%d' ) );
498
+ $wpdb->delete( $wpdb->base_prefix . 'stream_meta', array( 'record_id' => $record_id ), array( '%d' ) );
499
+ }
500
+ }
501
+
502
+ /**
503
+ * Drop the legacy Stream tables and options from the database
504
+ *
505
+ * @param bool $drop_tables If true, attempt to drop the legacy Stream tables
506
+ * @param bool $force If true, delete tables even if records still exist
507
+ *
508
+ * @return void
509
+ */
510
+ private static function drop_legacy_data( $drop_tables = true, $force = false ) {
511
+ global $wpdb;
512
+
513
+ if ( $drop_tables ) {
514
+ if ( is_multisite() ) {
515
+ $stream_site_blog_pairs = $wpdb->get_results( "SELECT site_id, blog_id FROM {$wpdb->base_prefix}stream WHERE type = 'stream'", ARRAY_A );
516
+ $stream_site_blog_pairs = array_unique( array_map( 'self::implode_key_value', $stream_site_blog_pairs ) );
517
+ $wp_site_blog_pairs = $wpdb->get_results( "SELECT site_id, blog_id FROM {$wpdb->base_prefix}blogs", ARRAY_A );
518
+ $wp_site_blog_pairs = array_unique( array_map( 'self::implode_key_value', $wp_site_blog_pairs ) );
519
+ $records_exist = ( array_intersect( $stream_site_blog_pairs, $wp_site_blog_pairs ) ) ? true : false;
520
+ } else {
521
+ $records_exist = $wpdb->get_var( "SELECT * FROM `{$wpdb->prefix}stream` LIMIT 1" );
522
+ }
523
+
524
+ // If records exist for other sites/blogs then don't proceed, unless we're force deleting or those sites/blogs have been deleted
525
+ if ( $records_exist && ! $force ) {
526
+ return;
527
+ }
528
+
529
+ // Drop legacy tables
530
+ $wpdb->query( "DROP TABLE IF EXISTS {$wpdb->base_prefix}stream, {$wpdb->base_prefix}stream_context, {$wpdb->base_prefix}stream_meta" );
531
+ }
532
+
533
+ // Delete legacy multisite options
534
+ if ( is_multisite() ) {
535
+ $blogs = wp_get_sites();
536
+
537
+ foreach ( $blogs as $blog ) {
538
+ switch_to_blog( $blog['blog_id'] );
539
+ delete_option( plugin_basename( WP_STREAM_DIR ) . '_db' ); // Deprecated option key
540
+ delete_option( 'wp_stream_db' );
541
+ delete_option( 'wp_stream_license' );
542
+ delete_option( 'wp_stream_licensee' );
543
+ }
544
+
545
+ restore_current_blog();
546
+ }
547
+
548
+ // Delete legacy options
549
+ delete_site_option( plugin_basename( WP_STREAM_DIR ) . '_db' ); // Deprecated option key
550
+ delete_site_option( 'wp_stream_db' );
551
+ delete_site_option( 'wp_stream_license' );
552
+ delete_site_option( 'wp_stream_licensee' );
553
+
554
+ // Delete legacy transients
555
+ delete_transient( 'wp_stream_extensions_' );
556
+
557
+ // Delete legacy cron event hooks
558
+ wp_clear_scheduled_hook( 'stream_auto_purge' ); // Deprecated hook
559
+ wp_clear_scheduled_hook( 'wp_stream_auto_purge' );
560
+ }
561
+
562
+ /**
563
+ * Callback to impode key/value pairs from an associative array into a specially-formatted string
564
+ *
565
+ * @param array $array An associate array
566
+ *
567
+ * @return string $output
568
+ */
569
+ public static function implode_key_value( $array ) {
570
+ $output = implode( ', ',
571
+ array_map(
572
+ function ( $v, $k ) {
573
+ return sprintf( '%s:%s', $k, $v );
574
+ },
575
+ $array,
576
+ array_keys( $array )
577
+ )
578
+ );
579
+
580
+ return $output;
581
+ }
582
+
583
+ }
classes/class-wp-stream-pointers.php ADDED
@@ -0,0 +1,174 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * Stream pointers. Based on WP core internal pointers.
4
+ *
5
+ * @since 1.4.4
6
+ */
7
+ class WP_Stream_Pointers {
8
+
9
+ public static $pointers = array();
10
+ public static $caps = array();
11
+
12
+ public static function load() {
13
+ add_action( 'admin_enqueue_scripts', array( 'WP_Stream_Pointers', 'enqueue_scripts' ) );
14
+ add_action( 'user_register', array( 'WP_Stream_Pointers', 'dismiss_pointers_for_new_users' ) );
15
+ }
16
+
17
+ public static function init_core_pointers() {
18
+ self::$pointers = array(
19
+ 'WP_Stream_Pointers' => array(
20
+ 'index.php' => 'wpstream143_extensions',
21
+ 'toplevel_page_' . WP_Stream_Admin::RECORDS_PAGE_SLUG => 'wpstream143_extensions',
22
+ 'stream_page_' . WP_Stream_Admin::SETTINGS_PAGE_SLUG => 'wpstream143_extensions',
23
+ )
24
+ );
25
+
26
+ self::$caps = array(
27
+ 'WP_Stream_Pointers' => array(
28
+ 'wpstream143_extensions' => array( 'install_plugins' ),
29
+ )
30
+ );
31
+ }
32
+
33
+ /**
34
+ * Initializes the pointers.
35
+ *
36
+ * @since 1.4.4
37
+ *
38
+ * All pointers can be disabled using the following:
39
+ * remove_action( 'admin_enqueue_scripts', array( 'WP_Stream_Pointers', 'enqueue_scripts' ) );
40
+ *
41
+ * Individual pointers (e.g. wpstream143_extensions) can be disabled using the following:
42
+ * remove_action( 'admin_print_footer_scripts', array( 'WP_Stream_Pointers', 'pointer_wpstream143_extensions' ) );
43
+ */
44
+ public static function enqueue_scripts( $hook_suffix ) {
45
+ /*
46
+ * Register feature pointers
47
+ * Format: array( hook_suffix => pointer_id )
48
+ */
49
+ self::init_core_pointers();
50
+
51
+ $get_pointers = array_merge( self::$pointers, apply_filters( 'wp_stream_pointers', array() ) );
52
+ $caps = array_merge( self::$caps, apply_filters( 'wp_stream_pointer_caps', array() ) );
53
+
54
+ foreach ( $get_pointers as $context => $registered_pointers ) {
55
+
56
+ // Check if screen related pointer is registered
57
+ if ( empty( $registered_pointers[ $hook_suffix ] ) ) {
58
+ return;
59
+ }
60
+
61
+ $pointers = (array) $registered_pointers[ $hook_suffix ];
62
+
63
+ $caps_required = $caps[ $context ];
64
+
65
+ // Get dismissed pointers
66
+ $dismissed = explode( ',', (string) get_user_meta( get_current_user_id(), 'dismissed_wp_pointers', true ) );
67
+
68
+ $got_pointers = false;
69
+ foreach ( array_diff( $pointers, $dismissed ) as $pointer ) {
70
+ if ( isset( $caps_required[ $pointer ] ) ) {
71
+ foreach ( $caps_required[ $pointer ] as $cap ) {
72
+ if ( ! current_user_can( $cap ) ) {
73
+ continue 2;
74
+ }
75
+ }
76
+ }
77
+
78
+ // Bind pointer print function
79
+ add_action( 'admin_print_footer_scripts', array( $context, 'pointer_' . $pointer ) );
80
+
81
+ $got_pointers = true;
82
+ }
83
+ }
84
+
85
+ if ( ! $got_pointers ) {
86
+ return;
87
+ }
88
+
89
+ // Add pointers script and style to queue
90
+ wp_enqueue_style( 'wp-pointer' );
91
+ wp_enqueue_script( 'wp-pointer' );
92
+ }
93
+
94
+ /**
95
+ * Print the pointer javascript data.
96
+ *
97
+ * @since 1.4.4
98
+ *
99
+ * @param string $pointer_id The pointer ID.
100
+ * @param string $selector The HTML elements, on which the pointer should be attached.
101
+ * @param array $args Arguments to be passed to the pointer JS (see wp-pointer.js).
102
+ */
103
+ public static function print_js( $pointer_id, $selector, $args ) {
104
+ if ( empty( $pointer_id ) || empty( $selector ) || empty( $args ) || empty( $args['content'] ) ) {
105
+ return;
106
+ }
107
+
108
+ ?>
109
+ <script type="text/javascript">
110
+ //<![CDATA[
111
+ (function($){
112
+ var options = <?php echo json_encode( $args ) ?>, setup;
113
+
114
+ if ( ! options ) {
115
+ return;
116
+ }
117
+
118
+ options = $.extend( options, {
119
+ close: function() {
120
+ $.post( ajaxurl, {
121
+ pointer: <?php echo json_encode( $pointer_id ) ?>,
122
+ action: 'dismiss-wp-pointer'
123
+ });
124
+ }
125
+ });
126
+
127
+ setup = function() {
128
+ $(<?php echo json_encode( $selector ) ?>).first().pointer( options ).pointer('open');
129
+ };
130
+
131
+ if ( options.position && options.position.defer_loading ) {
132
+ $(window).bind( 'load.wp-pointers', setup );
133
+ } else {
134
+ $(document).ready( setup );
135
+ }
136
+
137
+ })( jQuery );
138
+ //]]>
139
+ </script>
140
+ <?php
141
+ }
142
+
143
+ public static function pointer_wpstream143_extensions() {
144
+ $content = '<h3>' . esc_html__( 'Stream Extensions', 'stream' ) . '</h3>';
145
+ $content .= '<p>' . esc_html__( 'Extension plugins are now available for Stream!', 'stream' ) . '</p>';
146
+
147
+ if ( 'dashboard' === get_current_screen()->id ) {
148
+ $selector = sprintf( '#toplevel_page_%s', WP_Stream_Admin::RECORDS_PAGE_SLUG );
149
+ $position = array( 'edge' => is_rtl() ? 'right' : 'left', 'align' => 'center' );
150
+ } else {
151
+ $selector = sprintf( 'a[href="%s?page=%s"]', WP_Stream_Admin::ADMIN_PARENT_PAGE, WP_Stream_Admin::ADMIN_PARENT_PAGE );
152
+ $position = array( 'edge' => is_rtl() ? 'right' : 'left', 'align' => 'center' );
153
+ }
154
+
155
+ self::print_js(
156
+ 'wpstream143_extensions',
157
+ $selector,
158
+ array(
159
+ 'content' => $content,
160
+ 'position' => $position,
161
+ )
162
+ );
163
+ }
164
+
165
+ /**
166
+ * Prevents new users from seeing existing 'new feature' pointers.
167
+ *
168
+ * @since 1.4.4
169
+ */
170
+ public static function dismiss_pointers_for_new_users( $user_id ) {
171
+ add_user_meta( $user_id, 'dismissed_wp_pointers', '' );
172
+ }
173
+
174
+ }
classes/class-wp-stream-query.php ADDED
@@ -0,0 +1,278 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class WP_Stream_Query {
4
+
5
+ public static $instance;
6
+
7
+ /**
8
+ * @return WP_Stream_Query
9
+ */
10
+ public static function instance() {
11
+ if ( ! self::$instance ) {
12
+ $class = __CLASS__;
13
+ self::$instance = new $class;
14
+ }
15
+
16
+ return self::$instance;
17
+ }
18
+
19
+ /**
20
+ * Query Stream records
21
+ *
22
+ * @param array Query args
23
+ *
24
+ * @return array Stream Records
25
+ */
26
+ public function query( $args ) {
27
+ global $wpdb;
28
+
29
+ $defaults = array(
30
+ // Search param
31
+ 'search' => null,
32
+ 'search_field' => 'summary',
33
+ 'record_after' => null, // Deprecated, use date_after instead
34
+ // Date-based filters
35
+ 'date' => null, // Ex: 2014-02-17
36
+ 'date_from' => null, // Ex: 2014-02-17
37
+ 'date_to' => null, // Ex: 2014-02-17
38
+ 'date_after' => null, // Ex: 2014-02-17T15:19:21+00:00
39
+ 'date_before' => null, // Ex: 2014-02-17T15:19:21+00:00
40
+ // Record ID filters
41
+ 'record' => null,
42
+ 'record__in' => array(),
43
+ 'record__not_in' => array(),
44
+ // Pagination params
45
+ 'records_per_page' => get_option( 'posts_per_page', 20 ),
46
+ 'paged' => 1,
47
+ // Order
48
+ 'order' => 'desc',
49
+ 'orderby' => 'date',
50
+ // Meta/Taxonomy sub queries
51
+ 'meta' => array(),
52
+ // Data aggregations
53
+ 'aggregations' => array(),
54
+ // Fields selection
55
+ 'fields' => null,
56
+ );
57
+
58
+ // Additional property fields
59
+ $properties = array(
60
+ 'type' => 'stream',
61
+ 'author' => null,
62
+ 'author_role' => null,
63
+ 'ip' => null,
64
+ 'object_id' => null,
65
+ 'site_id' => null,
66
+ 'blog_id' => null,
67
+ 'visibility' => 'publish',
68
+ 'connector' => null,
69
+ 'context' => null,
70
+ 'action' => null,
71
+ );
72
+
73
+ /**
74
+ * Filter allows additional query properties to be added
75
+ *
76
+ * @since 2.0.0
77
+ *
78
+ * @return array Array of query properties
79
+ */
80
+ $properties = apply_filters( 'wp_stream_query_properties', $properties );
81
+
82
+ // Add property fields to defaults, including their __in/__not_in variations
83
+ foreach ( $properties as $property => $default ) {
84
+ if ( ! isset( $defaults[ $property ] ) ) {
85
+ $defaults[ $property ] = $default;
86
+ }
87
+ $defaults[ "{$property}__in" ] = array();
88
+ $defaults[ "{$property}__not_in" ] = array();
89
+ }
90
+
91
+ $args = wp_parse_args( $args, $defaults );
92
+
93
+ /**
94
+ * Filter allows additional arguments to query $args
95
+ *
96
+ * @since 1.4.0
97
+ *
98
+ * @return array Array of query arguments
99
+ */
100
+ $args = apply_filters( 'wp_stream_query_args', $args );
101
+
102
+ $query = array();
103
+ $filters = array();
104
+ $fields = array();
105
+
106
+ // PARSE SEARCH
107
+ if ( $args['search'] ) {
108
+ if ( $args['search_field'] ) {
109
+ $search_field = $args['search_field'];
110
+ $query['query']['match'][ $search_field ] = $args['search'];
111
+ } else {
112
+ $query['query']['match']['summary'] = $args['search'];
113
+ }
114
+ }
115
+
116
+ // PARSE FIELDS
117
+ if ( $args['fields'] ) {
118
+ $fields = is_array( $args['fields'] ) ? $args['fields'] : explode( ',', $args['fields'] );
119
+ }
120
+
121
+ // PARSE DATE
122
+ if ( $args['date_from'] ) {
123
+ $filters[]['range']['created']['gte'] = wp_stream_get_iso_8601_extended_date( strtotime( $args['date_from'] . ' 00:00:00' ), get_option( 'gmt_offset' ) );
124
+ }
125
+
126
+ if ( $args['date_to'] ) {
127
+ $filters[]['range']['created']['lte'] = wp_stream_get_iso_8601_extended_date( strtotime( $args['date_to'] . ' 23:59:59' ), get_option( 'gmt_offset' ) );
128
+ }
129
+
130
+ // Support deprecated argument replaced by date_after
131
+ if ( $args['record_after'] && ! $args['date_after'] ) {
132
+ $args['date_after'] = $args['record_after'];
133
+ }
134
+
135
+ if ( $args['date_after'] ) {
136
+ $filters[]['range']['created']['gt'] = wp_stream_get_iso_8601_extended_date( strtotime( $args['date_after'] ) );
137
+ }
138
+
139
+ if ( $args['date_before'] ) {
140
+ $filters[]['range']['created']['lt'] = wp_stream_get_iso_8601_extended_date( strtotime( $args['date_before'] ) );
141
+ }
142
+
143
+ if ( $args['date'] ) {
144
+ $filters[]['range']['created'] = array(
145
+ 'gte' => wp_stream_get_iso_8601_extended_date( strtotime( $args['date'] . ' 00:00:00' ), get_option( 'gmt_offset' ) ),
146
+ 'lte' => wp_stream_get_iso_8601_extended_date( strtotime( $args['date'] . ' 23:59:59' ), get_option( 'gmt_offset' ) ),
147
+ );
148
+ }
149
+
150
+ // PARSE RECORD
151
+ if ( $args['record'] ) {
152
+ $filters[]['ids']['values'] = array( $args['record'] );
153
+ }
154
+
155
+ if ( $args['record__in'] && is_array( $args['record__in'] ) ) {
156
+ $values = is_array( $args['record__in'] ) ? $args['record__in'] : array_map( 'trim', explode( ',', $args['record__in'] ) );
157
+
158
+ $filters[]['ids']['values'] = $values;
159
+ }
160
+
161
+ if ( $args['record__not_in'] && is_array( $args['record__not_in'] ) ) {
162
+ $values = is_array( $args['record__not_in'] ) ? $args['record__not_in'] : array_map( 'trim', explode( ',', $args['record__not_in'] ) );
163
+
164
+ $filters[]['not']['ids']['values'] = $values;
165
+ }
166
+
167
+ // PARSE PROPERTIES
168
+ foreach ( $properties as $property => $default ) {
169
+ if ( $args[ $property ] ) {
170
+ $filters[]['term'][ $property ] = $args[ $property ];
171
+ }
172
+
173
+ if ( $args[ "{$property}__in" ] ) {
174
+ $values = is_array( $args[ "{$property}__in" ] ) ? $args[ "{$property}__in" ] : array_map( 'trim', explode( ',', $args[ "{$property}__in" ] ) );
175
+ $property_in = array();
176
+ foreach ( $values as $value ) {
177
+ $property_in[]['term'][ $property ] = $value;
178
+ }
179
+ $filters[]['or'] = $property_in;
180
+ }
181
+
182
+ if ( $args[ "{$property}__not_in" ] ) {
183
+ $values = is_array( $args[ "{$property}__not_in" ] ) ? $args[ "{$property}__not_in" ] : array_map( 'trim', explode( ',', $args[ "{$property}__not_in" ] ) );
184
+ $property_not_in = array();
185
+ foreach ( $values as $value ) {
186
+ $property_not_in[]['not']['term'][ $property ] = $value;
187
+ }
188
+ $filters[]['or'] = $property_not_in;
189
+ }
190
+ }
191
+
192
+ // PARSE PAGINATION
193
+ if ( $args['records_per_page'] ) {
194
+ if ( $args['records_per_page'] >= 0 ) {
195
+ $query['size'] = (int) $args['records_per_page'];
196
+ } else {
197
+ $query['size'] = 999999; // Actual limit placed on "unlimited" results
198
+ }
199
+ } else {
200
+ $query['size'] = get_option( 'posts_per_page', 20 );
201
+ }
202
+
203
+ if ( $args['paged'] ) {
204
+ $query['from'] = ( (int) $args['paged'] - 1 ) * $query['size'];
205
+ }
206
+
207
+ // PARSE ORDER
208
+ $query['sort'] = array();
209
+
210
+ $orderby = ! empty( $args['orderby'] ) ? $args['orderby'] : 'created';
211
+ $order = ! empty( $args['order'] ) ? $args['order'] : 'desc';
212
+
213
+ if ( 'date' === $orderby ) {
214
+ $orderby = 'created';
215
+ }
216
+
217
+ $query['sort'][][ $orderby ]['order'] = $order;
218
+
219
+ // PARSE META
220
+ if ( $args['meta'] ) {
221
+ $meta = (array) $args['meta'];
222
+ foreach ( $meta as $key => $values ) {
223
+ if ( ! is_array( $values ) ) {
224
+ $values = (array) $values;
225
+ }
226
+ $filters[]['nested'] = array(
227
+ 'path' => 'stream_meta',
228
+ 'filter' => array(
229
+ 'terms' => array(
230
+ $key => $values,
231
+ ),
232
+ ),
233
+ );
234
+ }
235
+ }
236
+
237
+ // PARSE AGGREGATIONS
238
+ if ( ! empty( $args['aggregations'] ) ) {
239
+ foreach ( $args['aggregations'] as $aggregation_term ) {
240
+ $query['aggregations'][ $aggregation_term ]['terms']['field'] = $aggregation_term;
241
+ }
242
+ }
243
+
244
+ // Add filters to query
245
+ if ( ! empty( $filters ) ) {
246
+ if ( count( $filters ) > 1 ) {
247
+ $query['filter']['and'] = $filters;
248
+ } else {
249
+ $query['filter'] = current( $filters );
250
+ }
251
+ }
252
+
253
+ /**
254
+ * Filter allows the final query args to be modified
255
+ *
256
+ * @since 2.0.0
257
+ *
258
+ * @return array Array of query arguments
259
+ */
260
+ $query = apply_filters( 'wp_stream_db_query', $query );
261
+
262
+ /**
263
+ * Filter allows the final query fields to be modified
264
+ *
265
+ * @since 2.0.0
266
+ *
267
+ * @return array Array of query fields
268
+ */
269
+ $fields = apply_filters( 'wp_stream_db_fields', $fields );
270
+
271
+ /**
272
+ * Query results
273
+ *
274
+ * @var array
275
+ */
276
+ return WP_Stream::$db->query( $query, $fields );
277
+ }
278
+ }
classes/class-wp-stream-record.php ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class WP_Stream_Record {
4
+
5
+ public $ID;
6
+ public $site_id;
7
+ public $blog_id;
8
+ public $object_id;
9
+ public $author;
10
+ public $author_role;
11
+ public $summary;
12
+ public $visibility;
13
+ public $type;
14
+ public $connector;
15
+ public $context;
16
+ public $action;
17
+ public $created;
18
+ public $ip;
19
+
20
+ public $stream_meta;
21
+
22
+ // Deprecated
23
+ public $contexts;
24
+
25
+ public function __construct( $id = null ) {
26
+ if ( $id ) {
27
+ $this->load( $id );
28
+ }
29
+ }
30
+
31
+ public function load( $id ) {
32
+ $records = WP_Stream::$db->query( array( 'id' => $id ) );
33
+
34
+ if ( $record ) {
35
+ $this->populate( $record );
36
+ }
37
+ }
38
+
39
+ public function save() {
40
+ if ( ! $this->validate() ) {
41
+ return new WP_Error( 'validation-error', __( 'Could not validate record data.', 'stream' ) );
42
+ }
43
+
44
+ return WP_Stream::$db->store( (array) $this );
45
+ }
46
+
47
+ public function populate( array $raw ) {
48
+ $keys = get_class_vars( __CLASS__ );
49
+ $data = array_intersect_key( $raw, $keys );
50
+ if ( ! empty( $data['contexts'] ) ) {
51
+ $data['context'] = key( $data['contexts'] );
52
+ $data['action'] = current( $data['contexts'] );
53
+ }
54
+ foreach ( $data as $key => $val ) {
55
+ $this->{$key} = $val;
56
+ }
57
+ }
58
+
59
+ public function validate() {
60
+ return true;
61
+ }
62
+
63
+ public static function instance( array $data ) {
64
+ $object = new self();
65
+ $object->populate( $data );
66
+ return $object;
67
+ }
68
+
69
+ }
classes/class-wp-stream-settings.php ADDED
@@ -0,0 +1,927 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * Settings class
4
+ *
5
+ * @author X-Team <x-team.com>
6
+ * @author Shady Sharaf <shady@x-team.com>
7
+ */
8
+ class WP_Stream_Settings {
9
+
10
+ /**
11
+ * Settings key/identifier
12
+ */
13
+ const OPTION_KEY = 'wp_stream';
14
+
15
+ /**
16
+ * Plugin settings
17
+ *
18
+ * @var array
19
+ */
20
+ public static $options = array();
21
+
22
+ /**
23
+ * Option key for current screen
24
+ *
25
+ * @var array
26
+ */
27
+ public static $option_key = '';
28
+
29
+ /**
30
+ * Settings fields
31
+ *
32
+ * @var array
33
+ */
34
+ public static $fields = array();
35
+
36
+ /**
37
+ * Public constructor
38
+ *
39
+ * @return void
40
+ */
41
+ public static function load() {
42
+ self::$option_key = self::get_option_key();
43
+ self::$options = self::get_options();
44
+
45
+ // Register settings, and fields
46
+ add_action( 'admin_init', array( __CLASS__, 'register_settings' ) );
47
+
48
+ // Check if we need to flush rewrites rules
49
+ add_action( 'update_option_' . self::OPTION_KEY, array( __CLASS__, 'updated_option_trigger_flush_rules' ), 10, 2 );
50
+
51
+ add_filter( 'wp_stream_serialized_labels', array( __CLASS__, 'get_settings_translations' ) );
52
+
53
+ // Ajax callback function to search users
54
+ add_action( 'wp_ajax_stream_get_users', array( __CLASS__, 'get_users' ) );
55
+
56
+ // Ajax callback function to search IPs
57
+ add_action( 'wp_ajax_stream_get_ips', array( __CLASS__, 'get_ips' ) );
58
+ }
59
+
60
+ /**
61
+ * Ajax callback function to search users, used on exclude setting page
62
+ *
63
+ * @uses WP_User_Query WordPress User Query class.
64
+ * @return void
65
+ */
66
+ public static function get_users(){
67
+ if ( ! defined( 'DOING_AJAX' ) || ! current_user_can( WP_Stream_Admin::SETTINGS_CAP ) ) {
68
+ return;
69
+ }
70
+
71
+ check_ajax_referer( 'stream_get_users', 'nonce' );
72
+
73
+ $response = (object) array(
74
+ 'status' => false,
75
+ 'message' => esc_html__( 'There was an error in the request', 'stream' ),
76
+ );
77
+
78
+ $search = ( isset( $_POST['find'] )? wp_unslash( trim( $_POST['find'] ) ) : '' );
79
+ $request = (object) array(
80
+ 'find' => $search,
81
+ );
82
+
83
+ add_filter( 'user_search_columns', array( __CLASS__, 'add_display_name_search_columns' ), 10, 3 );
84
+
85
+ $users = new WP_User_Query(
86
+ array(
87
+ 'search' => "*{$request->find}*",
88
+ 'search_columns' => array(
89
+ 'user_login',
90
+ 'user_nicename',
91
+ 'user_email',
92
+ 'user_url',
93
+ ),
94
+ 'orderby' => 'display_name',
95
+ 'number' => WP_Stream_Admin::PRELOAD_AUTHORS_MAX,
96
+ )
97
+ );
98
+
99
+ remove_filter( 'user_search_columns', array( __CLASS__, 'add_display_name_search_columns' ), 10 );
100
+
101
+ if ( 0 === $users->get_total() ) {
102
+ wp_send_json_error( $response );
103
+ }
104
+
105
+ $response->status = true;
106
+ $response->message = '';
107
+ $response->users = array();
108
+
109
+ foreach ( $users->results as $key => $user ) {
110
+ $author = new WP_Stream_Author( $user->ID );
111
+
112
+ $args = array(
113
+ 'id' => $author->ID,
114
+ 'text' => $author->display_name,
115
+ );
116
+
117
+ $args['tooltip'] = esc_attr(
118
+ sprintf(
119
+ __( "ID: %d\nUser: %s\nEmail: %s\nRole: %s", 'stream' ),
120
+ $author->id,
121
+ $author->user_login,
122
+ $author->user_email,
123
+ ucwords( $author->get_role() )
124
+ )
125
+ );
126
+
127
+ $args['icon'] = $author->get_avatar_src( 32 );
128
+
129
+ $response->users[] = $args;
130
+ }
131
+
132
+ if ( empty( $search ) || preg_match( '/wp|cli|system|unknown/i', $search ) ) {
133
+ $author = new WP_Stream_Author( 0 );
134
+ $response->users[] = array(
135
+ 'id' => $author->id,
136
+ 'text' => $author->get_display_name(),
137
+ 'icon' => $author->get_avatar_src( 32 ),
138
+ 'tooltip' => esc_html__( 'Actions performed by the system when a user is not logged in (e.g. auto site upgrader, or invoking WP-CLI without --user)', 'stream' ),
139
+ );
140
+ }
141
+
142
+ wp_send_json_success( $response );
143
+ }
144
+
145
+ /**
146
+ * Ajax callback function to search IP addresses, used on exclude setting page
147
+ *
148
+ * @uses WP_User_Query WordPress User Query class.
149
+ * @return void
150
+ */
151
+ public static function get_ips(){
152
+ if ( ! defined( 'DOING_AJAX' ) || ! current_user_can( WP_Stream_Admin::SETTINGS_CAP ) ) {
153
+ return;
154
+ }
155
+
156
+ check_ajax_referer( 'stream_get_ips', 'nonce' );
157
+
158
+ $ips = wp_stream_existing_records( 'ip' );
159
+
160
+ if ( $ips ) {
161
+ wp_send_json_success( $ips );
162
+ } else {
163
+ wp_send_json_error();
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Filter the columns to search in a WP_User_Query search.
169
+ *
170
+ * @param array $search_columns Array of column names to be searched.
171
+ * @param string $search Text being searched.
172
+ * @param WP_User_Query $query current WP_User_Query instance.
173
+ *
174
+ * @return array
175
+ */
176
+ public static function add_display_name_search_columns( $search_columns, $search, $query ){
177
+ $search_columns[] = 'display_name';
178
+
179
+ return $search_columns;
180
+ }
181
+
182
+ /**
183
+ * Returns the option key depending on which settings page is being viewed
184
+ *
185
+ * @return string Option key for this page
186
+ */
187
+ public static function get_option_key() {
188
+ $option_key = self::OPTION_KEY;
189
+
190
+ $current_page = wp_stream_filter_input( INPUT_GET, 'page' );
191
+
192
+ if ( ! $current_page ) {
193
+ $current_page = wp_stream_filter_input( INPUT_GET, 'action' );
194
+ }
195
+
196
+ return apply_filters( 'wp_stream_settings_option_key', $option_key );
197
+ }
198
+
199
+ /**
200
+ * Return settings fields
201
+ *
202
+ * @return array Multidimensional array of fields
203
+ */
204
+ public static function get_fields() {
205
+ $fields = array(
206
+ 'general' => array(
207
+ 'title' => esc_html__( 'General', 'stream' ),
208
+ 'fields' => array(
209
+ array(
210
+ 'name' => 'role_access',
211
+ 'title' => esc_html__( 'Role Access', 'stream' ),
212
+ 'type' => 'multi_checkbox',
213
+ 'desc' => esc_html__( 'Users from the selected roles above will have permission to view Stream Records. However, only site Administrators can access Stream Settings.', 'stream' ),
214
+ 'choices' => self::get_roles(),
215
+ 'default' => array( 'administrator' ),
216
+ ),
217
+ array(
218
+ 'name' => 'private_feeds',
219
+ 'title' => esc_html__( 'Private Feeds', 'stream' ),
220
+ 'type' => 'checkbox',
221
+ 'desc' => sprintf(
222
+ __( 'Users from the selected roles above will be given a private key found in their %suser profile%s to access feeds of Stream Records securely. Please %sflush rewrite rules%s on your site after changing this setting.', 'stream' ),
223
+ sprintf(
224
+ '<a href="%s" title="%s">',
225
+ admin_url( sprintf( 'profile.php#wp-stream-highlight:%s', WP_Stream_Feeds::USER_FEED_OPTION_KEY ) ),
226
+ esc_attr__( 'View Profile', 'stream' )
227
+ ),
228
+ '</a>',
229
+ sprintf(
230
+ '<a href="%s" title="%s" target="_blank">',
231
+ esc_url( 'http://codex.wordpress.org/Rewrite_API/flush_rules#What_it_does' ),
232
+ esc_attr__( 'View Codex', 'stream' )
233
+ ),
234
+ '</a>'
235
+ ),
236
+ 'after_field' => esc_html__( 'Enabled', 'stream' ),
237
+ 'default' => 0,
238
+ ),
239
+ ),
240
+ ),
241
+ 'exclude' => array(
242
+ 'title' => esc_html__( 'Exclude', 'stream' ),
243
+ 'fields' => array(
244
+ array(
245
+ 'name' => 'rules',
246
+ 'title' => esc_html__( 'Exclude Rules', 'stream' ),
247
+ 'type' => 'rule_list',
248
+ 'desc' => esc_html__( 'Create rules for excluding certain kinds of records from appearing in Stream.', 'stream' ),
249
+ 'default' => array(),
250
+ 'nonce' => 'stream_get_ips',
251
+ ),
252
+ ),
253
+ ),
254
+ 'advanced' => array(
255
+ 'title' => esc_html__( 'Advanced', 'stream' ),
256
+ 'fields' => array(
257
+ array(
258
+ 'name' => 'comment_flood_tracking',
259
+ 'title' => esc_html__( 'Comment Flood Tracking', 'stream' ),
260
+ 'type' => 'checkbox',
261
+ 'desc' => __( 'WordPress will automatically prevent duplicate comments from flooding the database. By default, Stream does not track these attempts unless you opt-in here. Enabling this is not necessary or recommended for most sites.', 'stream' ),
262
+ 'after_field' => esc_html__( 'Enabled', 'stream' ),
263
+ 'default' => 0,
264
+ ),
265
+ array(
266
+ 'name' => 'wp_cron_tracking',
267
+ 'title' => esc_html__( 'WP Cron Tracking', 'stream' ),
268
+ 'type' => 'checkbox',
269
+ 'desc' => __( 'By default, Stream does not track activity performed by WordPress cron events unless you opt-in here. Enabling this is not necessary or recommended for most sites.', 'stream' ),
270
+ 'after_field' => esc_html__( 'Enabled', 'stream' ),
271
+ 'default' => 0,
272
+ ),
273
+ ),
274
+ ),
275
+ );
276
+
277
+ // If Akismet is active, allow Admins to opt-in to Akismet tracking
278
+ if ( class_exists( 'Akismet' ) ) {
279
+ $akismet_tracking = array(
280
+ 'name' => 'akismet_tracking',
281
+ 'title' => esc_html__( 'Akismet Tracking', 'stream' ),
282
+ 'type' => 'checkbox',
283
+ 'desc' => __( 'Akismet already keeps statistics for comment attempts that it blocks as SPAM. By default, Stream does not track these attempts unless you opt-in here. Enabling this is not necessary or recommended for most sites.', 'stream' ),
284
+ 'after_field' => esc_html__( 'Enabled', 'stream' ),
285
+ 'default' => 0,
286
+ );
287
+
288
+ array_push( $fields['advanced']['fields'], $akismet_tracking );
289
+ }
290
+
291
+ /**
292
+ * Filter allows for modification of options fields
293
+ *
294
+ * @return array Array of option fields
295
+ */
296
+ self::$fields = apply_filters( 'wp_stream_settings_option_fields', $fields );
297
+
298
+ // Sort option fields in each tab by title ASC
299
+ foreach ( self::$fields as $tab => $options ) {
300
+ $titles = wp_list_pluck( self::$fields[ $tab ]['fields'], 'title' );
301
+
302
+ array_multisort( $titles, SORT_ASC, self::$fields[ $tab ]['fields'] );
303
+ }
304
+
305
+ return self::$fields;
306
+ }
307
+
308
+ /**
309
+ * Returns a list of options based on the current screen.
310
+ *
311
+ * @return array Options
312
+ */
313
+ public static function get_options() {
314
+ $option_key = self::$option_key;
315
+ $defaults = self::get_defaults( $option_key );
316
+
317
+ /**
318
+ * Filter allows for modification of options
319
+ *
320
+ * @param array array of options
321
+ * @return array updated array of options
322
+ */
323
+ return apply_filters(
324
+ 'wp_stream_settings_options',
325
+ wp_parse_args(
326
+ (array) get_option( $option_key, array() ),
327
+ $defaults
328
+ ),
329
+ $option_key
330
+ );
331
+ }
332
+
333
+ /**
334
+ * Iterate through registered fields and extract default values
335
+ *
336
+ * @return array Default option values
337
+ */
338
+ public static function get_defaults() {
339
+ $fields = self::get_fields();
340
+ $defaults = array();
341
+
342
+ foreach ( $fields as $section_name => $section ) {
343
+ foreach ( $section['fields'] as $field ) {
344
+ $defaults[ $section_name.'_'.$field['name'] ] = isset( $field['default'] )
345
+ ? $field['default']
346
+ : null;
347
+ }
348
+ }
349
+
350
+ /**
351
+ * Filter allows for modification of options defaults
352
+ *
353
+ * @param array array of options
354
+ * @return array updated array of option defaults
355
+ */
356
+ return apply_filters(
357
+ 'wp_stream_settings_option_defaults',
358
+ $defaults
359
+ );
360
+ }
361
+
362
+ /**
363
+ * Registers settings fields and sections
364
+ *
365
+ * @return void
366
+ */
367
+ public static function register_settings() {
368
+ $sections = self::get_fields();
369
+
370
+ register_setting( self::$option_key, self::$option_key );
371
+
372
+ foreach ( $sections as $section_name => $section ) {
373
+ add_settings_section(
374
+ $section_name,
375
+ null,
376
+ '__return_false',
377
+ self::$option_key
378
+ );
379
+
380
+ foreach ( $section['fields'] as $field_idx => $field ) {
381
+ if ( ! isset( $field['type'] ) ) { // No field type associated, skip, no GUI
382
+ continue;
383
+ }
384
+ add_settings_field(
385
+ $field['name'],
386
+ $field['title'],
387
+ ( isset( $field['callback'] ) ? $field['callback'] : array( __CLASS__, 'output_field' ) ),
388
+ self::$option_key,
389
+ $section_name,
390
+ $field + array(
391
+ 'section' => $section_name,
392
+ 'label_for' => sprintf( '%s_%s_%s', self::$option_key, $section_name, $field['name'] ), // xss ok
393
+ )
394
+ );
395
+ }
396
+ }
397
+ }
398
+
399
+ /**
400
+ * Check if we have updated a settings that requires rewrite rules to be flushed
401
+ *
402
+ * @param array $old_value
403
+ * @param array $new_value
404
+ *
405
+ * @internal param $option
406
+ * @internal param string $option
407
+ * @action updated_option
408
+ * @return void
409
+ */
410
+ public static function updated_option_trigger_flush_rules( $old_value, $new_value ) {
411
+ if ( is_array( $new_value ) && is_array( $old_value ) ) {
412
+ $new_value = ( array_key_exists( 'general_private_feeds', $new_value ) ) ? $new_value['general_private_feeds'] : 0;
413
+ $old_value = ( array_key_exists( 'general_private_feeds', $old_value ) ) ? $old_value['general_private_feeds'] : 0;
414
+
415
+ if ( $new_value !== $old_value ) {
416
+ delete_option( 'rewrite_rules' );
417
+ }
418
+ }
419
+ }
420
+
421
+ /**
422
+ * Compile HTML needed for displaying the field
423
+ *
424
+ * @param array $field Field settings
425
+ * @return string HTML to be displayed
426
+ */
427
+ public static function render_field( $field ) {
428
+ $output = null;
429
+ $type = isset( $field['type'] ) ? $field['type'] : null;
430
+ $section = isset( $field['section'] ) ? $field['section'] : null;
431
+ $name = isset( $field['name'] ) ? $field['name'] : null;
432
+ $class = isset( $field['class'] ) ? $field['class'] : null;
433
+ $placeholder = isset( $field['placeholder'] ) ? $field['placeholder'] : null;
434
+ $description = isset( $field['desc'] ) ? $field['desc'] : null;
435
+ $href = isset( $field['href'] ) ? $field['href'] : null;
436
+ $rows = isset( $field['rows'] ) ? $field['rows'] : 10;
437
+ $cols = isset( $field['cols'] ) ? $field['cols'] : 50;
438
+ $after_field = isset( $field['after_field'] ) ? $field['after_field'] : null;
439
+ $default = isset( $field['default'] ) ? $field['default'] : null;
440
+ $title = isset( $field['title'] ) ? $field['title'] : null;
441
+ $nonce = isset( $field['nonce'] ) ? $field['nonce'] : null;
442
+
443
+ if ( isset( $field['value'] ) ) {
444
+ $current_value = $field['value'];
445
+ } else {
446
+ $current_value = self::$options[ $section . '_' . $name ];
447
+ }
448
+
449
+ $option_key = self::$option_key;
450
+
451
+ if ( is_callable( $current_value ) ) {
452
+ $current_value = call_user_func( $current_value );
453
+ }
454
+
455
+ if ( ! $type || ! $section || ! $name ) {
456
+ return;
457
+ }
458
+
459
+ if ( 'multi_checkbox' === $type
460
+ && ( empty( $field['choices'] ) || ! is_array( $field['choices'] ) )
461
+ ) {
462
+ return;
463
+ }
464
+
465
+ switch ( $type ) {
466
+ case 'text':
467
+ case 'number':
468
+ $output = sprintf(
469
+ '<input type="%1$s" name="%2$s[%3$s_%4$s]" id="%2$s_%3$s_%4$s" class="%5$s" placeholder="%6$s" value="%7$s" /> %8$s',
470
+ esc_attr( $type ),
471
+ esc_attr( $option_key ),
472
+ esc_attr( $section ),
473
+ esc_attr( $name ),
474
+ esc_attr( $class ),
475
+ esc_attr( $placeholder ),
476
+ esc_attr( $current_value ),
477
+ $after_field // xss ok
478
+ );
479
+ break;
480
+ case 'textarea':
481
+ $output = sprintf(
482
+ '<textarea name="%1$s[%2$s_%3$s]" id="%1$s_%2$s_%3$s" class="%4$s" placeholder="%5$s" rows="%6$d" cols="%7$d">%8$s</textarea> %9$s',
483
+ esc_attr( $option_key ),
484
+ esc_attr( $section ),
485
+ esc_attr( $name ),
486
+ esc_attr( $class ),
487
+ esc_attr( $placeholder ),
488
+ absint( $rows ),
489
+ absint( $cols ),
490
+ esc_textarea( $current_value ),
491
+ $after_field // xss ok
492
+ );
493
+ break;
494
+ case 'checkbox':
495
+ $output = sprintf(
496
+ '<label><input type="checkbox" name="%1$s[%2$s_%3$s]" id="%1$s[%2$s_%3$s]" value="1" %4$s /> %5$s</label>',
497
+ esc_attr( $option_key ),
498
+ esc_attr( $section ),
499
+ esc_attr( $name ),
500
+ checked( $current_value, 1, false ),
501
+ $after_field // xss ok
502
+ );
503
+ break;
504
+ case 'multi_checkbox':
505
+ $output = sprintf(
506
+ '<div id="%1$s[%2$s_%3$s]"><fieldset>',
507
+ esc_attr( $option_key ),
508
+ esc_attr( $section ),
509
+ esc_attr( $name )
510
+ );
511
+ // Fallback if nothing is selected
512
+ $output .= sprintf(
513
+ '<input type="hidden" name="%1$s[%2$s_%3$s][]" value="__placeholder__" />',
514
+ esc_attr( $option_key ),
515
+ esc_attr( $section ),
516
+ esc_attr( $name )
517
+ );
518
+ $current_value = (array) $current_value;
519
+ $choices = $field['choices'];
520
+ if ( is_callable( $choices ) ) {
521
+ $choices = call_user_func( $choices );
522
+ }
523
+ foreach ( $choices as $value => $label ) {
524
+ $output .= sprintf(
525
+ '<label>%1$s <span>%2$s</span></label><br />',
526
+ sprintf(
527
+ '<input type="checkbox" name="%1$s[%2$s_%3$s][]" value="%4$s" %5$s />',
528
+ esc_attr( $option_key ),
529
+ esc_attr( $section ),
530
+ esc_attr( $name ),
531
+ esc_attr( $value ),
532
+ checked( in_array( $value, $current_value ), true, false )
533
+ ),
534
+ esc_html( $label )
535
+ );
536
+ }
537
+ $output .= '</fieldset></div>';
538
+ break;
539
+ case 'select':
540
+ $current_value = self::$options[ $section . '_' . $name ];
541
+ $default_value = isset( $default['value'] ) ? $default['value'] : '-1';
542
+ $default_name = isset( $default['name'] ) ? $default['name'] : 'Choose Setting';
543
+
544
+ $output = sprintf(
545
+ '<select name="%1$s[%2$s_%3$s]" class="%1$s_%2$s_%3$s">',
546
+ esc_attr( $option_key ),
547
+ esc_attr( $section ),
548
+ esc_attr( $name )
549
+ );
550
+ $output .= sprintf(
551
+ '<option value="%1$s" %2$s>%3$s</option>',
552
+ esc_attr( $default_value ),
553
+ checked( $default_value === $current_value, true, false ),
554
+ esc_html( $default_name )
555
+ );
556
+ foreach ( $field['choices'] as $value => $label ) {
557
+ $output .= sprintf(
558
+ '<option value="%1$s" %2$s>%3$s</option>',
559
+ esc_attr( $value ),
560
+ checked( $value === $current_value, true, false ),
561
+ esc_html( $label )
562
+ );
563
+ }
564
+ $output .= '</select>';
565
+ break;
566
+ case 'file':
567
+ $output = sprintf(
568
+ '<input type="file" name="%1$s[%2$s_%3$s]" class="%4$s">',
569
+ esc_attr( $option_key ),
570
+ esc_attr( $section ),
571
+ esc_attr( $name ),
572
+ esc_attr( $class )
573
+ );
574
+ break;
575
+ case 'link':
576
+ $output = sprintf(
577
+ '<a id="%1$s_%2$s_%3$s" class="%4$s" href="%5$s">%6$s</a>',
578
+ esc_attr( $option_key ),
579
+ esc_attr( $section ),
580
+ esc_attr( $name ),
581
+ esc_attr( $class ),
582
+ esc_attr( $href ),
583
+ esc_attr( $title )
584
+ );
585
+ break;
586
+ case 'select2' :
587
+ if ( ! isset( $current_value ) ) {
588
+ $current_value = '';
589
+ }
590
+
591
+ $data_values = array();
592
+
593
+ if ( isset( $field['choices'] ) ) {
594
+ $choices = $field['choices'];
595
+ if ( is_callable( $choices ) ) {
596
+ $param = ( isset( $field['param'] ) ) ? $field['param'] : null;
597
+ $choices = call_user_func( $choices, $param );
598
+ }
599
+ foreach ( $choices as $key => $value ) {
600
+ if ( is_array( $value ) ) {
601
+ $child_values = array();
602
+ if ( isset( $value['children'] ) ) {
603
+ $child_values = array();
604
+ foreach ( $value['children'] as $child_key => $child_value ) {
605
+ $child_values[] = array( 'id' => $child_key, 'text' => $child_value );
606
+ }
607
+ }
608
+ if ( isset( $value['label'] ) ) {
609
+ $data_values[] = array( 'id' => $key, 'text' => $value['label'], 'children' => $child_values );
610
+ }
611
+ } else {
612
+ $data_values[] = array( 'id' => $key, 'text' => $value );
613
+ }
614
+ }
615
+ $class .= ' with-source';
616
+ }
617
+
618
+ $input_html = sprintf(
619
+ '<input type="hidden" name="%1$s[%2$s_%3$s]" data-values=\'%4$s\' value="%5$s" class="select2-select %6$s" data-placeholder="%7$s" />',
620
+ esc_attr( $option_key ),
621
+ esc_attr( $section ),
622
+ esc_attr( $name ),
623
+ esc_attr( json_encode( $data_values ) ),
624
+ esc_attr( $current_value ),
625
+ $class,
626
+ sprintf( esc_html__( 'Any %s', 'stream' ), $title )
627
+ );
628
+
629
+ $output = sprintf(
630
+ '<div class="%1$s_%2$s_%3$s">%4$s</div>',
631
+ esc_attr( $option_key ),
632
+ esc_attr( $section ),
633
+ esc_attr( $name ),
634
+ $input_html
635
+ );
636
+
637
+ break;
638
+ case 'rule_list' :
639
+ $output = '<p class="description">' . esc_html( $description ) . '</p>';
640
+
641
+ $actions_top = sprintf( '<input type="button" class="button" id="%1$s_new_rule" value="&#43; %2$s" />', esc_attr( $section . '_' . $name ), __( 'Add New Rule', 'stream' ) );
642
+ $actions_bottom = sprintf( '<input type="button" class="button" id="%1$s_remove_rules" value="%2$s" />', esc_attr( $section . '_' . $name ), __( 'Delete Selected Rules', 'stream' ) );
643
+
644
+ $output .= sprintf( '<div class="tablenav top">%1$s</div>', $actions_top );
645
+ $output .= '<table class="wp-list-table widefat fixed stream-exclude-list">';
646
+
647
+ unset( $description );
648
+
649
+ $heading_row = sprintf(
650
+ '<tr>
651
+ <th scope="col" class="check-column manage-column">%1$s</th>
652
+ <th scope="col" class="manage-column">%2$s</th>
653
+ <th scope="col" class="manage-column">%3$s</th>
654
+ <th scope="col" class="manage-column">%4$s</th>
655
+ <th scope="col" class="manage-column">%5$s</th>
656
+ <th scope="col" class="actions-column manage-column"><span class="hidden">%6$s</span></th>
657
+ </tr>',
658
+ '<input class="cb-select" type="checkbox" />',
659
+ esc_html__( 'Author or Role', 'stream' ),
660
+ esc_html__( 'Context', 'stream' ),
661
+ esc_html__( 'Action', 'stream' ),
662
+ esc_html__( 'IP Address', 'stream' ),
663
+ esc_html__( 'Filters', 'stream' )
664
+ );
665
+
666
+ $exclude_rows = array();
667
+
668
+ // Prepend an empty row
669
+ $current_value['exclude_row'] = array( 'helper' => '' ) + ( isset( $current_value['exclude_row'] ) ? $current_value['exclude_row'] : array() );
670
+
671
+ foreach ( $current_value['exclude_row'] as $key => $value ) {
672
+ // Prepare values
673
+ $author_or_role = isset( $current_value['author_or_role'][ $key ] ) ? $current_value['author_or_role'][ $key ] : '';
674
+ $connector = isset( $current_value['connector'][ $key ] ) ? $current_value['connector'][ $key ] : '';
675
+ $context = isset( $current_value['context'][ $key ] ) ? $current_value['context'][ $key ] : '';
676
+ $action = isset( $current_value['action'][ $key ] ) ? $current_value['action'][ $key ] : '';
677
+ $ip_address = isset( $current_value['ip_address'][ $key ] ) ? $current_value['ip_address'][ $key ] : '';
678
+
679
+ // Author or Role dropdown menu
680
+ $author_or_role_values = array();
681
+ $author_or_role_selected = array();
682
+
683
+ foreach ( self::get_roles() as $role_id => $role ) {
684
+ $args = array( 'id' => $role_id, 'text' => $role );
685
+ $users = get_users( array( 'role' => $role_id ) );
686
+
687
+ if ( count( $users ) ) {
688
+ $args['user_count'] = sprintf( _n( '1 user', '%s users', count( $users ), 'stream' ), count( $users ) );
689
+ }
690
+
691
+ if ( $role_id === $author_or_role ) {
692
+ $author_or_role_selected['id'] = $role_id;
693
+ $author_or_role_selected['text'] = $role;
694
+ }
695
+
696
+ $author_or_role_values[] = $args;
697
+ }
698
+
699
+ if ( empty( $author_or_role_selected ) && is_numeric( $author_or_role ) ) {
700
+ $user = new WP_User( $author_or_role );
701
+ $display_name = ( 0 === $user->ID ) ? __( 'N/A', 'stream' ) : $user->display_name;
702
+ $author_or_role_selected = array( 'id' => $user->ID, 'text' => $display_name );
703
+ }
704
+
705
+ $author_or_role_input = sprintf(
706
+ '<input type="hidden" name="%1$s[%2$s_%3$s][%4$s][]" data-values=\'%5$s\' data-selected-id=\'%6$s\' data-selected-text=\'%7$s\' value="%6$s" class="select2-select %4$s" data-placeholder="%8$s" data-nonce="%9$s" />',
707
+ esc_attr( $option_key ),
708
+ esc_attr( $section ),
709
+ esc_attr( $name ),
710
+ 'author_or_role',
711
+ esc_attr( json_encode( $author_or_role_values ) ),
712
+ isset( $author_or_role_selected['id'] ) ? esc_attr( $author_or_role_selected['id'] ) : '',
713
+ isset( $author_or_role_selected['text'] ) ? esc_attr( $author_or_role_selected['text'] ) : '',
714
+ esc_html__( 'Any Author or Role', 'stream' ),
715
+ esc_attr( wp_create_nonce( 'stream_get_users' ) )
716
+ );
717
+
718
+ // Context dropdown menu
719
+ $context_values = array();
720
+
721
+ foreach ( self::get_terms_labels( 'context' ) as $context_id => $context_data ) {
722
+ if ( is_array( $context_data ) ) {
723
+ $child_values = array();
724
+ if ( isset( $context_data['children'] ) ) {
725
+ $child_values = array();
726
+ foreach ( $context_data['children'] as $child_id => $child_value ) {
727
+ $child_values[] = array( 'id' => $child_id, 'text' => $child_value, 'parent' => $context_id );
728
+ }
729
+ }
730
+ if ( isset( $context_data['label'] ) ) {
731
+ $context_values[] = array( 'id' => $context_id, 'text' => $context_data['label'], 'children' => $child_values );
732
+ }
733
+ } else {
734
+ $context_values[] = array( 'id' => $context_id, 'text' => $context_data );
735
+ }
736
+ }
737
+
738
+ $connector_input = sprintf(
739
+ '<input type="hidden" name="%1$s[%2$s_%3$s][%4$s][]" class="%4$s" value="%5$s">',
740
+ esc_attr( $option_key ),
741
+ esc_attr( $section ),
742
+ esc_attr( $name ),
743
+ esc_attr( 'connector' ),
744
+ esc_attr( $connector )
745
+ );
746
+
747
+ $context_input = sprintf(
748
+ '<input type="hidden" name="%1$s[%2$s_%3$s][%4$s][]" data-values=\'%5$s\' value="%6$s" class="select2-select with-source %4$s" data-placeholder="%7$s" data-group="%8$s" />',
749
+ esc_attr( $option_key ),
750
+ esc_attr( $section ),
751
+ esc_attr( $name ),
752
+ 'context',
753
+ esc_attr( json_encode( $context_values ) ),
754
+ esc_attr( $context ),
755
+ esc_html__( 'Any Context', 'stream' ),
756
+ 'connector'
757
+ );
758
+
759
+ // Action dropdown menu
760
+ $action_values = array();
761
+
762
+ foreach ( self::get_terms_labels( 'action' ) as $action_id => $action_data ) {
763
+ $action_values[] = array( 'id' => $action_id, 'text' => $action_data );
764
+ }
765
+
766
+ $action_input = sprintf(
767
+ '<input type="hidden" name="%1$s[%2$s_%3$s][%4$s][]" data-values=\'%5$s\' value="%6$s" class="select2-select with-source %4$s" data-placeholder="%7$s" />',
768
+ esc_attr( $option_key ),
769
+ esc_attr( $section ),
770
+ esc_attr( $name ),
771
+ 'action',
772
+ esc_attr( json_encode( $action_values ) ),
773
+ esc_attr( $action ),
774
+ esc_html__( 'Any Action', 'stream' )
775
+ );
776
+
777
+ // IP Address input
778
+ $ip_address_input = sprintf(
779
+ '<input type="hidden" name="%1$s[%2$s_%3$s][%4$s][]" value="%5$s" class="select2-select %4$s" data-placeholder="%6$s" data-nonce="%7$s" />',
780
+ esc_attr( $option_key ),
781
+ esc_attr( $section ),
782
+ esc_attr( $name ),
783
+ 'ip_address',
784
+ esc_attr( $ip_address ),
785
+ esc_html__( 'Any IP Address', 'stream' ),
786
+ esc_attr( wp_create_nonce( 'stream_get_ips' ) )
787
+ );
788
+
789
+ // Hidden helper input
790
+ $helper_input = sprintf(
791
+ '<input type="hidden" name="%1$s[%2$s_%3$s][%4$s][]" value="" />',
792
+ esc_attr( $option_key ),
793
+ esc_attr( $section ),
794
+ esc_attr( $name ),
795
+ 'exclude_row'
796
+ );
797
+
798
+ $exclude_rows[] = sprintf(
799
+ '<tr class="%1$s %2$s">
800
+ <th scope="row" class="check-column">%3$s %4$s</th>
801
+ <td>%5$s</td>
802
+ <td>%6$s %7$s</td>
803
+ <td>%8$s</td>
804
+ <td>%9$s</td>
805
+ <th scope="row" class="actions-column">%10$s</th>
806
+ </tr>',
807
+ ( 0 !== $key % 2 ) ? 'alternate' : '',
808
+ ( 'helper' === $key ) ? 'hidden helper' : '',
809
+ '<input class="cb-select" type="checkbox" />',
810
+ $helper_input,
811
+ $author_or_role_input,
812
+ $connector_input,
813
+ $context_input,
814
+ $action_input,
815
+ $ip_address_input,
816
+ '<a href="#" class="exclude_rules_remove_rule_row">Delete</a>'
817
+ );
818
+ }
819
+
820
+ $no_rules_found_row = sprintf(
821
+ '<tr class="no-items hidden"><td class="colspanchange" colspan="5">%1$s</td></tr>',
822
+ esc_html__( 'No rules found.', 'stream' )
823
+ );
824
+
825
+ $output .= '<thead>' . $heading_row . '</thead>';
826
+ $output .= '<tfoot>' . $heading_row . '</tfoot>';
827
+ $output .= '<tbody>' . $no_rules_found_row . implode( '', $exclude_rows ) . '</tbody>';
828
+
829
+ $output .= '</table>';
830
+
831
+ $output .= sprintf( '<div class="tablenav bottom">%1$s</div>', $actions_bottom );
832
+
833
+ break;
834
+ }
835
+ $output .= ! empty( $description ) ? sprintf( '<p class="description">%s</p>', $description /* xss ok */ ) : null;
836
+
837
+ return $output;
838
+ }
839
+
840
+ /**
841
+ * Render Callback for post_types field
842
+ *
843
+ * @param array $field
844
+ *
845
+ * @internal param $args
846
+ * @return void
847
+ */
848
+ public static function output_field( $field ) {
849
+ $method = 'output_' . $field['name'];
850
+
851
+ if ( method_exists( __CLASS__, $method ) ) {
852
+ return call_user_func( array( __CLASS__, $method ), $field );
853
+ }
854
+
855
+ $output = self::render_field( $field );
856
+
857
+ echo $output; // xss okay
858
+ }
859
+
860
+ /**
861
+ * Get an array of user roles
862
+ *
863
+ * @return array
864
+ */
865
+ public static function get_roles() {
866
+ $wp_roles = new WP_Roles();
867
+ $roles = array();
868
+
869
+ foreach ( $wp_roles->get_names() as $role => $label ) {
870
+ $roles[ $role ] = translate_user_role( $label );
871
+ }
872
+
873
+ return $roles;
874
+ }
875
+
876
+ /**
877
+ * Function will return all terms labels of given column
878
+ *
879
+ * @param $column string Name of the column
880
+ * @return array
881
+ */
882
+ public static function get_terms_labels( $column ) {
883
+ $return_labels = array();
884
+
885
+ if ( isset( WP_Stream_Connectors::$term_labels[ 'stream_' . $column ] ) ) {
886
+ if ( 'context' === $column && isset( WP_Stream_Connectors::$term_labels['stream_connector'] ) ) {
887
+ $connectors = WP_Stream_Connectors::$term_labels['stream_connector'];
888
+ $contexts = WP_Stream_Connectors::$term_labels['stream_context'];
889
+
890
+ foreach ( $connectors as $connector => $connector_label ) {
891
+ $return_labels[ $connector ]['label'] = $connector_label;
892
+ foreach ( $contexts as $context => $context_label ) {
893
+ if ( isset( WP_Stream_Connectors::$contexts[ $connector ] ) && array_key_exists( $context, WP_Stream_Connectors::$contexts[ $connector ] ) ) {
894
+ $return_labels[ $connector ]['children'][ $context ] = $context_label;
895
+ }
896
+ }
897
+ }
898
+ } else {
899
+ $return_labels = WP_Stream_Connectors::$term_labels[ 'stream_' . $column ];
900
+ }
901
+
902
+ ksort( $return_labels );
903
+ }
904
+
905
+ return $return_labels;
906
+ }
907
+
908
+ /**
909
+ * Get translations of serialized Stream settings
910
+ *
911
+ * @filter wp_stream_serialized_labels
912
+ * @return array Multidimensional array of fields
913
+ */
914
+ public static function get_settings_translations( $labels ) {
915
+ if ( ! isset( $labels[ self::OPTION_KEY ] ) ) {
916
+ $labels[ self::OPTION_KEY ] = array();
917
+ }
918
+
919
+ foreach ( self::get_fields() as $section_slug => $section ) {
920
+ foreach ( $section['fields'] as $field ) {
921
+ $labels[ self::OPTION_KEY ][ sprintf( '%s_%s', $section_slug, $field['name'] ) ] = $field['title'];
922
+ }
923
+ }
924
+
925
+ return $labels;
926
+ }
927
+ }
connectors/class-wp-stream-connector-acf.php ADDED
@@ -0,0 +1,542 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class WP_Stream_Connector_ACF extends WP_Stream_Connector {
4
+
5
+ /**
6
+ * Connector slug
7
+ *
8
+ * @var string
9
+ */
10
+ public static $name = 'acf';
11
+
12
+ /**
13
+ * Holds tracked plugin minimum version required
14
+ *
15
+ * @const string
16
+ */
17
+ const PLUGIN_MIN_VERSION = '4.3.8';
18
+
19
+ /**
20
+ * Actions registered for this connector
21
+ *
22
+ * @var array
23
+ */
24
+ public static $actions = array(
25
+ 'added_post_meta',
26
+ 'updated_post_meta',
27
+ 'delete_post_meta',
28
+ 'added_user_meta',
29
+ 'updated_user_meta',
30
+ 'delete_user_meta',
31
+ 'added_option',
32
+ 'updated_option',
33
+ 'deleted_option',
34
+ 'pre_post_update',
35
+ );
36
+
37
+ /**
38
+ * Cached location rules, used in shutdown callback to verify changes in meta
39
+ *
40
+ * @var array
41
+ */
42
+ public static $cached_location_rules = array();
43
+
44
+ /**
45
+ * Cached field values updates, used by shutdown callback to verify actual changes
46
+ *
47
+ * @var array
48
+ */
49
+ public static $cached_field_values_updates = array();
50
+
51
+
52
+ /**
53
+ * Check if plugin dependencies are satisfied and add an admin notice if not
54
+ *
55
+ * @return bool
56
+ */
57
+ public static function is_dependency_satisfied() {
58
+ if ( class_exists( 'acf' ) && version_compare( acf()->settings['version'], self::PLUGIN_MIN_VERSION, '>=' ) ) {
59
+ return true;
60
+ }
61
+
62
+ return false;
63
+ }
64
+
65
+ /**
66
+ * Return translated connector label
67
+ *
68
+ * @return string Translated connector label
69
+ */
70
+ public static function get_label() {
71
+ return _x( 'ACF', 'acf', 'stream' );
72
+ }
73
+
74
+ /**
75
+ * Return translated action labels
76
+ *
77
+ * @return array Action label translations
78
+ */
79
+ public static function get_action_labels() {
80
+ return array(
81
+ 'created' => __( 'Created', 'acf', 'stream' ),
82
+ 'updated' => __( 'Updated', 'acf', 'stream' ),
83
+ 'added' => __( 'Added', 'acf', 'stream' ),
84
+ 'deleted' => __( 'Deleted', 'acf', 'stream' ),
85
+ );
86
+ }
87
+
88
+ /**
89
+ * Return translated context labels
90
+ *
91
+ * @return array Context label translations
92
+ */
93
+ public static function get_context_labels() {
94
+ return array(
95
+ 'field_groups' => _x( 'Field Groups', 'acf', 'stream' ),
96
+ 'fields' => _x( 'Fields', 'acf', 'stream' ),
97
+ 'rules' => _x( 'Rules', 'acf', 'stream' ),
98
+ 'options' => _x( 'Options', 'acf', 'stream' ),
99
+ 'values' => _x( 'Values', 'acf', 'stream' ),
100
+ );
101
+ }
102
+
103
+ /**
104
+ * Register the connector
105
+ *
106
+ * @return void
107
+ */
108
+ public static function register() {
109
+ add_filter( 'wp_stream_log_data', array( __CLASS__, 'log_override' ) );
110
+
111
+ /**
112
+ * Allow devs to disable logging values of rendered forms
113
+ *
114
+ * @return bool
115
+ */
116
+ if ( apply_filters( 'wp_stream_acf_enable_value_logging', true ) ) {
117
+ self::$actions[] = 'acf/update_value';
118
+ }
119
+
120
+ parent::register();
121
+ }
122
+
123
+ /**
124
+ * Add action links to Stream drop row in admin list screen
125
+ *
126
+ * @filter wp_stream_action_links_{connector}
127
+ *
128
+ * @param array $links Previous links registered
129
+ * @param object $record Stream record
130
+ *
131
+ * @return array Action links
132
+ */
133
+ public static function action_links( $links, $record ) {
134
+ $links = WP_Stream_Connector_Posts::action_links( $links, $record );
135
+
136
+ return $links;
137
+ }
138
+
139
+ /**
140
+ * Track addition of post meta
141
+ *
142
+ * @action added_post_meta
143
+ */
144
+ public static function callback_added_post_meta() {
145
+ call_user_func_array( array( __CLASS__, 'check_meta' ), array_merge( array( 'post', 'added' ), func_get_args() ) );
146
+ }
147
+
148
+ /**
149
+ * Track updating post meta
150
+ *
151
+ * @action updated_post_meta
152
+ */
153
+ public static function callback_updated_post_meta() {
154
+ call_user_func_array( array( __CLASS__, 'check_meta' ), array_merge( array( 'post', 'updated' ), func_get_args() ) );
155
+ }
156
+
157
+ /**
158
+ * Track deletion of post meta
159
+ *
160
+ * Note: Using delete_post_meta instead of deleted_post_meta to be able to
161
+ * capture old field value
162
+ *
163
+ * @action delete_post_meta
164
+ */
165
+ public static function callback_delete_post_meta() {
166
+ call_user_func_array( array( __CLASS__, 'check_meta' ), array_merge( array( 'post', 'deleted' ), func_get_args() ) );
167
+ }
168
+
169
+ /**
170
+ * Track addition of user meta
171
+ *
172
+ * @action added_user_meta
173
+ */
174
+ public static function callback_added_user_meta() {
175
+ call_user_func_array( array( __CLASS__, 'check_meta' ), array_merge( array( 'user', 'added' ), func_get_args() ) );
176
+ }
177
+
178
+ /**
179
+ * Track updating user meta
180
+ *
181
+ * @action updated_user_meta
182
+ */
183
+ public static function callback_updated_user_meta() {
184
+ call_user_func_array( array( __CLASS__, 'check_meta' ), array_merge( array( 'user', 'updated' ), func_get_args() ) );
185
+ }
186
+
187
+ /**
188
+ * Track deletion of user meta
189
+ *
190
+ * Note: Using delete_user_meta instead of deleted_user_meta to be able to
191
+ * capture old field value
192
+ *
193
+ * @action delete_user_meta
194
+ */
195
+ public static function callback_delete_user_meta() {
196
+ call_user_func_array( array( __CLASS__, 'check_meta' ), array_merge( array( 'user', 'deleted' ), func_get_args() ) );
197
+ }
198
+
199
+ /**
200
+ * Track addition of post/user meta
201
+ *
202
+ * @param string $type Type of object, post or user
203
+ * @param string $action Added, updated, deleted
204
+ * @param integer $meta_id
205
+ * @param integer $object_id
206
+ * @param string $meta_key
207
+ * @param mixed|null $meta_value
208
+ */
209
+ public static function check_meta( $type, $action, $meta_id, $object_id, $meta_key, $meta_value = null ) {
210
+ if ( 'post' !== $type || ! ( $post = get_post( $object_id ) ) || 'acf' !== $post->post_type ) {
211
+ self::check_meta_values( $type, $action, $meta_id, $object_id, $meta_key, $meta_value = null );
212
+ return;
213
+ }
214
+
215
+ $action_labels = self::get_action_labels();
216
+
217
+ // Fields
218
+ if ( 0 === strpos( $meta_key, 'field_' ) ) {
219
+ if ( 'deleted' === $action ) {
220
+ $meta_value = get_post_meta( $object_id, $meta_key, true );
221
+ }
222
+
223
+ self::log(
224
+ _x( '"%1$s" field in "%2$s" %3$s', 'acf', 'stream' ),
225
+ array(
226
+ 'label' => $meta_value['label'],
227
+ 'title' => $post->post_title,
228
+ 'action' => strtolower( $action_labels[ $action ] ),
229
+ 'key' => $meta_value['key'],
230
+ 'name' => $meta_value['name'],
231
+ ),
232
+ $object_id,
233
+ 'fields',
234
+ $action
235
+ );
236
+ }
237
+ // Location rules
238
+ elseif ( 'rule' === $meta_key ) {
239
+ if ( 'deleted' === $action ) {
240
+ self::$cached_location_rules[ $object_id ] = get_post_meta( $object_id, 'rule' );
241
+
242
+ add_action( 'shutdown', array( __CLASS__, 'check_location_rules' ), 9 );
243
+ }
244
+ }
245
+ // Position option
246
+ elseif ( 'position' === $meta_key ) {
247
+ if ( 'deleted' === $action ) {
248
+ return;
249
+ }
250
+
251
+ $options = array(
252
+ 'acf_after_title' => _x( 'High (after title)', 'acf', 'stream' ),
253
+ 'normal' => _x( 'Normal (after content)', 'acf', 'stream' ),
254
+ 'side' => _x( 'Side', 'acf', 'stream' ),
255
+ );
256
+
257
+ self::log(
258
+ _x( 'Position of "%1$s" updated to "%2$s"', 'acf', 'stream' ),
259
+ array(
260
+ 'title' => $post->post_title,
261
+ 'option_label' => $options[ $meta_value ],
262
+ 'option' => $meta_key,
263
+ 'option_value' => $meta_value,
264
+ ),
265
+ $object_id,
266
+ 'options',
267
+ 'updated'
268
+ );
269
+ }
270
+ // Layout option
271
+ elseif ( 'layout' === $meta_key ) {
272
+ if ( 'deleted' === $action ) {
273
+ return;
274
+ }
275
+
276
+ $options = array(
277
+ 'no_box' => _x( 'Seamless (no metabox)', 'acf', 'stream' ),
278
+ 'default' => _x( 'Standard (WP metabox)', 'acf', 'stream' ),
279
+ );
280
+
281
+ self::log(
282
+ _x( 'Style of "%1$s" updated to "%2$s"', 'acf', 'stream' ),
283
+ array(
284
+ 'title' => $post->post_title,
285
+ 'option_label' => $options[ $meta_value ],
286
+ 'option' => $meta_key,
287
+ 'option_value' => $meta_value,
288
+ ),
289
+ $object_id,
290
+ 'options',
291
+ 'updated'
292
+ );
293
+ }
294
+ // Screen exclusion option
295
+ elseif ( 'hide_on_screen' === $meta_key ) {
296
+ if ( 'deleted' === $action ) {
297
+ return;
298
+ }
299
+
300
+ $options = array(
301
+ 'permalink' => _x( 'Permalink', 'acf', 'stream' ),
302
+ 'the_content' => _x( 'Content Editor', 'acf', 'stream' ),
303
+ 'excerpt' => _x( 'Excerpt', 'acf', 'stream' ),
304
+ 'custom_fields' => _x( 'Custom Fields', 'acf', 'stream' ),
305
+ 'discussion' => _x( 'Discussion', 'acf', 'stream' ),
306
+ 'comments' => _x( 'Comments', 'acf', 'stream' ),
307
+ 'revisions' => _x( 'Revisions', 'acf', 'stream' ),
308
+ 'slug' => _x( 'Slug', 'acf', 'stream' ),
309
+ 'author' => _x( 'Author', 'acf', 'stream' ),
310
+ 'format' => _x( 'Format', 'acf', 'stream' ),
311
+ 'featured_image' => _x( 'Featured Image', 'acf', 'stream' ),
312
+ 'categories' => _x( 'Categories', 'acf', 'stream' ),
313
+ 'tags' => _x( 'Tags', 'acf', 'stream' ),
314
+ 'send-trackbacks' => _x( 'Send Trackbacks', 'acf', 'stream' ),
315
+ );
316
+
317
+ if ( count( $options ) === count( $meta_value ) ) {
318
+ $options_label = _x( 'All screens', 'acf', 'stream' );
319
+ } elseif ( empty( $meta_value ) ) {
320
+ $options_label = _x( 'No screens', 'acf', 'stream' );
321
+ } else {
322
+ $options_label = implode( ', ', array_intersect_key( $options, array_flip( $meta_value ) ) );
323
+ }
324
+
325
+ self::log(
326
+ _x( '"%1$s" set to display on "%2$s"', 'acf', 'stream' ),
327
+ array(
328
+ 'title' => $post->post_title,
329
+ 'option_label' => $options_label,
330
+ 'option' => $meta_key,
331
+ 'option_value' => $meta_value,
332
+ ),
333
+ $object_id,
334
+ 'options',
335
+ 'updated'
336
+ );
337
+ }
338
+ }
339
+
340
+ /**
341
+ * Track changes to ACF values within rendered post meta forms
342
+ *
343
+ * @param string $type Type of object, post or user
344
+ * @param string $action Added, updated, deleted
345
+ * @param integer $meta_id
346
+ * @param integer $object_id
347
+ * @param string $key
348
+ * @param mixed|null $value
349
+ *
350
+ * @return bool
351
+ */
352
+ public static function check_meta_values( $type, $action, $meta_id, $object_id, $key, $value = null ) {
353
+ if ( empty( self::$cached_field_values_updates ) ) {
354
+ return false;
355
+ }
356
+
357
+ $object_key = $object_id;
358
+
359
+ if ( 'user' === $type ) {
360
+ $object_key = 'user_' . $object_id;
361
+ } elseif ( 'taxonomy' === $type ) {
362
+ if ( 0 === strpos( $key, '_' ) ) { // Ignore the 'revision' stuff!
363
+ return false;
364
+ }
365
+
366
+ if ( 1 !== preg_match( '#([a-z0-9_-]+)_([\d]+)_([a-z0-9_-]+)#', $key, $matches ) ) {
367
+ return false;
368
+ }
369
+
370
+ list( , $taxonomy, $term_id, $key ) = $matches; // Skips 0 index
371
+
372
+ $object_key = $taxonomy . '_' . $term_id;
373
+ }
374
+
375
+ if ( isset( self::$cached_field_values_updates[ $object_key ][ $key ] ) ) {
376
+ if ( 'post' === $type ) {
377
+ $post = get_post( $object_id );
378
+ $title = $post->post_title;
379
+ $type_name = strtolower( WP_Stream_Connector_Posts::get_post_type_name( $post->post_type ) );
380
+ } elseif ( 'user' === $type ) {
381
+ $user = new WP_User( $object_id );
382
+ $title = $user->get( 'display_name' );
383
+ $type_name = __( 'user', 'stream' );
384
+ } elseif ( 'taxonomy' === $type ) {
385
+ $term = get_term( $term_id, $taxonomy );
386
+ $title = $term->name;
387
+ $tax_obj = get_taxonomy( $taxonomy );
388
+ $type_name = strtolower( get_taxonomy_labels( $tax_obj )->singular_name );
389
+ } else {
390
+ return false;
391
+ }
392
+
393
+ $cache = self::$cached_field_values_updates[ $object_key ][ $key ];
394
+
395
+ self::log(
396
+ _x( '"%1$s" of "%2$s" %3$s updated', 'acf', 'stream' ),
397
+ array(
398
+ 'field_label' => $cache['field']['label'],
399
+ 'title' => $title,
400
+ 'singular_name' => $type_name,
401
+ 'meta_value' => $value,
402
+ 'meta_key' => $key,
403
+ 'meta_type' => $type,
404
+ ),
405
+ $object_id,
406
+ 'values',
407
+ 'updated'
408
+ );
409
+ }
410
+
411
+ return true;
412
+ }
413
+
414
+ /**
415
+ * Track changes to rules, complements post-meta updates
416
+ *
417
+ * @action shutdown
418
+ */
419
+ public static function check_location_rules() {
420
+ foreach ( self::$cached_location_rules as $post_id => $old ) {
421
+ $new = get_post_meta( $post_id, 'rule' );
422
+ $post = get_post( $post_id );
423
+
424
+ if ( $old === $new ) {
425
+ continue;
426
+ }
427
+
428
+ $new = array_map( 'json_encode', $new );
429
+ $old = array_map( 'json_encode', $old );
430
+ $added = array_diff( $new, $old );
431
+ $deleted = array_diff( $old, $new );
432
+
433
+ self::log(
434
+ _x( 'Updated rules of "%1$s" (%2$d added, %3$d deleted)', 'acf', 'stream' ),
435
+ array(
436
+ 'title' => $post->post_title,
437
+ 'no_added' => count( $added ),
438
+ 'no_deleted' => count( $deleted ),
439
+ 'added' => $added,
440
+ 'deleted' => $deleted,
441
+ ),
442
+ $post_id,
443
+ 'rules',
444
+ 'updated'
445
+ );
446
+ }
447
+ }
448
+
449
+ /**
450
+ * Override connector log for our own Settings / Actions
451
+ *
452
+ * @param array $data
453
+ *
454
+ * @return array|bool
455
+ */
456
+ public static function log_override( $data ) {
457
+ if ( ! is_array( $data ) ) {
458
+ return $data;
459
+ }
460
+
461
+ if ( 'posts' === $data['connector'] && 'acf' === $data['context'] ) {
462
+ $data['context'] = 'field_groups';
463
+ $data['connector'] = self::$name;
464
+ $data['args']['singular_name'] = __( 'field group', 'stream' );
465
+ }
466
+
467
+ return $data;
468
+ }
469
+
470
+ /**
471
+ * Track changes to custom field values updates, saves filtered values to be
472
+ * processed by callback_updated_post_meta
473
+ *
474
+ * @param $value
475
+ * @param $post_id
476
+ * @param $field
477
+ */
478
+ public static function callback_acf_update_value( $value, $post_id, $field ) {
479
+ self::$cached_field_values_updates[ $post_id ][ $field['name'] ] = compact( 'field', 'value', 'post_id' );
480
+ return $value;
481
+ }
482
+
483
+ /**
484
+ * Track changes to post main attributes, ie: Order No.
485
+ *
486
+ * @param $post_id
487
+ * @param $data Array with the updated post data
488
+ */
489
+ public static function callback_pre_post_update( $post_id, $data ) {
490
+ $post = get_post( $post_id );
491
+
492
+ if ( 'acf' !== $post->post_type ) {
493
+ return;
494
+ }
495
+
496
+ // menu_order, aka Order No.
497
+ if ( $data['menu_order'] !== $post->menu_order ) {
498
+ self::log(
499
+ _x( 'Updated Order of "%1$s" from %2$d to %3$d', 'acf', 'stream' ),
500
+ array(
501
+ 'title' => $post->post_title,
502
+ 'old_menu_order' => $post->menu_order,
503
+ 'menu_order' => $data['menu_order'],
504
+ ),
505
+ $post_id,
506
+ 'field_groups',
507
+ 'updated'
508
+ );
509
+ }
510
+ }
511
+
512
+ /**
513
+ * Track addition of new options
514
+ *
515
+ * @param $key Option name
516
+ * @param $value Option value
517
+ */
518
+ public static function callback_added_option( $key, $value ) {
519
+ self::check_meta_values( 'taxonomy', 'added', null, null, $key, $value );
520
+ }
521
+
522
+ /**
523
+ * Track addition of new options
524
+ *
525
+ * @param $key
526
+ * @param $old
527
+ * @param $value
528
+ */
529
+ public static function callback_updated_option( $key, $old, $value ) {
530
+ self::check_meta_values( 'taxonomy', 'updated', null, null, $key, $value );
531
+ }
532
+
533
+ /**
534
+ * Track addition of new options
535
+ *
536
+ * @param $key
537
+ */
538
+ public static function callback_deleted_option( $key ) {
539
+ self::check_meta_values( 'taxonomy', 'deleted', null, null, $key, null );
540
+ }
541
+
542
+ }
connectors/class-wp-stream-connector-bbpress.php ADDED
@@ -0,0 +1,232 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class WP_Stream_Connector_bbPress extends WP_Stream_Connector {
4
+
5
+ /**
6
+ * Connector slug
7
+ *
8
+ * @var string
9
+ */
10
+ public static $name = 'bbpress';
11
+
12
+ /**
13
+ * Holds tracked plugin minimum version required
14
+ *
15
+ * @const string
16
+ */
17
+ const PLUGIN_MIN_VERSION = '2.5.4';
18
+
19
+ /**
20
+ * Actions registered for this connector
21
+ *
22
+ * @var array
23
+ */
24
+ public static $actions = array(
25
+ 'bbp_toggle_topic_admin',
26
+ );
27
+
28
+ /**
29
+ * Tracked option keys
30
+ *
31
+ * @var array
32
+ */
33
+ public static $options = array(
34
+ 'bbpress' => null,
35
+ );
36
+
37
+ /**
38
+ * Flag to stop logging update logic twice
39
+ *
40
+ * @var bool
41
+ */
42
+ public static $is_update = false;
43
+
44
+ /**
45
+ * @var bool
46
+ */
47
+ public static $_deleted_activity = false;
48
+
49
+ /**
50
+ * @var array
51
+ */
52
+ public static $_delete_activity_args = array();
53
+
54
+ /**
55
+ * @var bool
56
+ */
57
+ public static $ignore_activity_bulk_deletion = false;
58
+
59
+ /**
60
+ * Check if plugin dependencies are satisfied and add an admin notice if not
61
+ *
62
+ * @return bool
63
+ */
64
+ public static function is_dependency_satisfied() {
65
+ if ( class_exists( 'bbPress' ) && function_exists( 'bbp_get_version' ) && version_compare( bbp_get_version(), self::PLUGIN_MIN_VERSION, '>=' ) ) {
66
+ return true;
67
+ }
68
+
69
+ return false;
70
+ }
71
+
72
+ /**
73
+ * Return translated connector label
74
+ *
75
+ * @return string Translated connector label
76
+ */
77
+ public static function get_label() {
78
+ return _x( 'bbPress', 'bbpress', 'stream' );
79
+ }
80
+
81
+ /**
82
+ * Return translated action labels
83
+ *
84
+ * @return array Action label translations
85
+ */
86
+ public static function get_action_labels() {
87
+ return array(
88
+ 'created' => _x( 'Created', 'bbpress', 'stream' ),
89
+ 'updated' => _x( 'Updated', 'bbpress', 'stream' ),
90
+ 'activated' => _x( 'Activated', 'bbpress', 'stream' ),
91
+ 'deactivated' => _x( 'Deactivated', 'bbpress', 'stream' ),
92
+ 'deleted' => _x( 'Deleted', 'bbpress', 'stream' ),
93
+ 'trashed' => _x( 'Trashed', 'bbpress', 'stream' ),
94
+ 'untrashed' => _x( 'Restored', 'bbpress', 'stream' ),
95
+ 'generated' => _x( 'Generated', 'bbpress', 'stream' ),
96
+ 'imported' => _x( 'Imported', 'bbpress', 'stream' ),
97
+ 'exported' => _x( 'Exported', 'bbpress', 'stream' ),
98
+ 'closed' => _x( 'Closed', 'bbpress', 'stream' ),
99
+ 'opened' => _x( 'Opened', 'bbpress', 'stream' ),
100
+ 'sticked' => _x( 'Sticked', 'bbpress', 'stream' ),
101
+ 'unsticked' => _x( 'Unsticked', 'bbpress', 'stream' ),
102
+ 'spammed' => _x( 'Marked as spam', 'bbpress', 'stream' ),
103
+ 'unspammed' => _x( 'Unmarked as spam', 'bbpress', 'stream' ),
104
+ );
105
+ }
106
+
107
+ /**
108
+ * Return translated context labels
109
+ *
110
+ * @return array Context label translations
111
+ */
112
+ public static function get_context_labels() {
113
+ return array(
114
+ 'settings' => _x( 'Settings', 'bbpress', 'stream' ),
115
+ );
116
+ }
117
+
118
+ /**
119
+ * Add action links to Stream drop row in admin list screen
120
+ *
121
+ * @filter wp_stream_action_links_{connector}
122
+ *
123
+ * @param array $links Previous links registered
124
+ * @param object $record Stream record
125
+ *
126
+ * @return array Action links
127
+ */
128
+ public static function action_links( $links, $record ) {
129
+ if ( 'settings' === $record->context ) {
130
+ $option = wp_stream_get_meta( $record, 'option', true );
131
+ $links[ __( 'Edit', 'stream' ) ] = esc_url( add_query_arg(
132
+ array(
133
+ 'page' => 'bbpress',
134
+ ),
135
+ admin_url( 'options-general.php' )
136
+ ) . esc_url_raw( '#' . $option ) );
137
+ }
138
+ return $links;
139
+ }
140
+
141
+ public static function register() {
142
+ parent::register();
143
+
144
+ add_filter( 'wp_stream_log_data', array( __CLASS__, 'log_override' ) );
145
+ }
146
+
147
+ /**
148
+ * Override connector log for our own Settings / Actions
149
+ *
150
+ * @param array $data
151
+ *
152
+ * @return array|bool
153
+ */
154
+ public static function log_override( $data ) {
155
+ if ( ! is_array( $data ) ) {
156
+ return $data;
157
+ }
158
+
159
+ if ( 'settings' === $data['connector'] && 'bbpress' === $data['args']['context'] ) {
160
+ $settings = bbp_admin_get_settings_fields();
161
+
162
+ /* fix for missing title for this single field */
163
+ $settings['bbp_settings_features']['_bbp_allow_threaded_replies']['title'] = __( 'Reply Threading', 'stream' );
164
+
165
+ $option = $data['args']['option'];
166
+ foreach ( $settings as $section => $fields ) {
167
+ if ( isset( $fields[ $option ] ) ) {
168
+ $field = $fields[ $option ];
169
+ break;
170
+ }
171
+ }
172
+
173
+ if ( ! isset( $field ) ) {
174
+ return $data;
175
+ }
176
+
177
+ $data['args']['label'] = $field['title'];
178
+ $data['connector'] = self::$name;
179
+ $data['context'] = 'settings';
180
+ $data['action'] = 'updated';
181
+ }
182
+ elseif ( 'posts' === $data['connector'] && in_array( $data['context'], array( 'forum', 'topic', 'reply' ) ) ) {
183
+ if ( 'reply' === $data['context'] ) {
184
+ if ( 'updated' === $data['action'] ) {
185
+ $data['message'] = __( 'Replied on "%1$s"', 'stream' );
186
+ $data['args']['post_title'] = get_post( wp_get_post_parent_id( $data['object_id'] ) )->post_title;
187
+ }
188
+ $data['args']['post_title'] = sprintf(
189
+ __( 'Reply to: %s', 'stream' ),
190
+ get_post( wp_get_post_parent_id( $data['object_id'] ) )->post_title
191
+ );
192
+ }
193
+
194
+ $data['connector'] = self::$name;
195
+ }
196
+ elseif ( 'taxonomies' === $data['connector'] && in_array( $data['context'], array( 'topic-tag' ) ) ) {
197
+ $data['connector'] = self::$name;
198
+ }
199
+
200
+ return $data;
201
+ }
202
+
203
+ public static function callback_bbp_toggle_topic_admin( $success, $post_data, $action, $message ) {
204
+
205
+ if ( ! empty( $message['failed'] ) ) {
206
+ return;
207
+ }
208
+
209
+ $action = $message['bbp_topic_toggle_notice'];
210
+ $actions = self::get_action_labels();
211
+
212
+ if ( ! isset( $actions[ $action ] ) ) {
213
+ return;
214
+ }
215
+
216
+ $label = $actions[ $action ];
217
+ $topic = get_post( $message['topic_id'] );
218
+
219
+ self::log(
220
+ _x( '%1$s "%2$s" topic', '1: Action, 2: Topic title', 'stream' ),
221
+ array(
222
+ 'action_title' => $actions[ $action ],
223
+ 'topic_title' => $topic->post_title,
224
+ 'action' => $action,
225
+ ),
226
+ $topic->ID,
227
+ 'topic',
228
+ $action
229
+ );
230
+ }
231
+
232
+ }
connectors/class-wp-stream-connector-buddypress.php ADDED
@@ -0,0 +1,814 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class WP_Stream_Connector_BuddyPress extends WP_Stream_Connector {
4
+
5
+ /**
6
+ * Connector slug
7
+ *
8
+ * @var string
9
+ */
10
+ public static $name = 'buddypress';
11
+
12
+ /**
13
+ * Holds tracked plugin minimum version required
14
+ *
15
+ * @const string
16
+ */
17
+ const PLUGIN_MIN_VERSION = '2.0.1';
18
+
19
+ /**
20
+ * Actions registered for this connector
21
+ *
22
+ * @var array
23
+ */
24
+ public static $actions = array(
25
+ 'update_option',
26
+ 'add_option',
27
+ 'delete_option',
28
+ 'update_site_option',
29
+ 'add_site_option',
30
+ 'delete_site_option',
31
+
32
+ 'bp_before_activity_delete',
33
+ 'bp_activity_deleted_activities',
34
+
35
+ 'bp_activity_mark_as_spam',
36
+ 'bp_activity_mark_as_ham',
37
+ 'bp_activity_admin_edit_after',
38
+
39
+ 'groups_create_group',
40
+ 'groups_update_group',
41
+ 'groups_before_delete_group',
42
+ 'groups_details_updated',
43
+ 'groups_settings_updated',
44
+
45
+ 'groups_leave_group',
46
+ 'groups_join_group',
47
+
48
+ 'groups_promote_member',
49
+ 'groups_demote_member',
50
+ 'groups_ban_member',
51
+ 'groups_unban_member',
52
+ 'groups_remove_member',
53
+
54
+ 'xprofile_field_after_save',
55
+ 'xprofile_fields_deleted_field',
56
+
57
+ 'xprofile_group_after_save',
58
+ 'xprofile_groups_deleted_group',
59
+ );
60
+
61
+ /**
62
+ * Tracked option keys
63
+ *
64
+ * @var array
65
+ */
66
+ public static $options = array(
67
+ 'bp-active-components' => null,
68
+ 'bp-pages' => null,
69
+ 'buddypress' => null,
70
+ );
71
+
72
+ /**
73
+ * Flag to stop logging update logic twice
74
+ *
75
+ * @var bool
76
+ */
77
+ public static $is_update = false;
78
+
79
+ /**
80
+ * @var bool
81
+ */
82
+ public static $_deleted_activity = false;
83
+
84
+ /**
85
+ * @var array
86
+ */
87
+ public static $_delete_activity_args = array();
88
+
89
+ /**
90
+ * @var bool
91
+ */
92
+ public static $ignore_activity_bulk_deletion = false;
93
+
94
+ /**
95
+ * Check if plugin dependencies are satisfied and add an admin notice if not
96
+ *
97
+ * @return bool
98
+ */
99
+ public static function is_dependency_satisfied() {
100
+ if ( class_exists( 'BuddyPress' ) && version_compare( BuddyPress::instance()->version, self::PLUGIN_MIN_VERSION, '>=' ) ) {
101
+ return true;
102
+ }
103
+
104
+ return false;
105
+ }
106
+
107
+ /**
108
+ * Return translated connector label
109
+ *
110
+ * @return string Translated connector label
111
+ */
112
+ public static function get_label() {
113
+ return _x( 'BuddyPress', 'buddypress', 'stream' );
114
+ }
115
+
116
+ /**
117
+ * Return translated action labels
118
+ *
119
+ * @return array Action label translations
120
+ */
121
+ public static function get_action_labels() {
122
+ return array(
123
+ 'created' => _x( 'Created', 'buddypress', 'stream' ),
124
+ 'updated' => _x( 'Updated', 'buddypress', 'stream' ),
125
+ 'activated' => _x( 'Activated', 'buddypress', 'stream' ),
126
+ 'deactivated' => _x( 'Deactivated', 'buddypress', 'stream' ),
127
+ 'deleted' => _x( 'Deleted', 'buddypress', 'stream' ),
128
+ 'spammed' => _x( 'Marked as spam', 'buddypress', 'stream' ),
129
+ 'unspammed' => _x( 'Unmarked as spam', 'buddypress', 'stream' ),
130
+ 'promoted' => _x( 'Promoted', 'buddypress', 'stream' ),
131
+ 'demoted' => _x( 'Demoted', 'buddypress', 'stream' ),
132
+ );
133
+ }
134
+
135
+ /**
136
+ * Return translated context labels
137
+ *
138
+ * @return array Context label translations
139
+ */
140
+ public static function get_context_labels() {
141
+ return array(
142
+ 'components' => _x( 'Components', 'buddypress', 'stream' ),
143
+ 'groups' => _x( 'Groups', 'buddypress', 'stream' ),
144
+ 'activity' => _x( 'Activity', 'buddypress', 'stream' ),
145
+ 'profile_fields' => _x( 'Profile fields', 'buddypress', 'stream' ),
146
+ );
147
+ }
148
+
149
+ /**
150
+ * Add action links to Stream drop row in admin list screen
151
+ *
152
+ * @filter wp_stream_action_links_{connector}
153
+ *
154
+ * @param array $links Previous links registered
155
+ * @param object $record Stream record
156
+ *
157
+ * @return array Action links
158
+ */
159
+ public static function action_links( $links, $record ) {
160
+ if ( in_array( $record->context, array( 'components' ) ) ) {
161
+ $option_key = wp_stream_get_meta( $record, 'option_key', true );
162
+
163
+ if ( 'bp-active-components' === $option_key ) {
164
+ $links[ __( 'Edit', 'stream' ) ] = add_query_arg(
165
+ array(
166
+ 'page' => 'bp-components',
167
+ ),
168
+ admin_url( 'admin.php' )
169
+ );
170
+ } elseif ( 'bp-pages' === $option_key ) {
171
+ $page_id = wp_stream_get_meta( $record, 'page_id', true );
172
+
173
+ $links[ __( 'Edit setting', 'stream' ) ] = add_query_arg(
174
+ array(
175
+ 'page' => 'bp-page-settings',
176
+ ),
177
+ admin_url( 'admin.php' )
178
+ );
179
+
180
+ if ( $page_id ) {
181
+ $links[ __( 'Edit Page', 'stream' ) ] = get_edit_post_link( $page_id );
182
+ $links[ __( 'View', 'stream' ) ] = get_permalink( $page_id );
183
+ }
184
+ }
185
+ } elseif ( in_array( $record->context, array( 'settings' ) ) ) {
186
+ $links[ __( 'Edit setting', 'stream' ) ] = add_query_arg(
187
+ array(
188
+ 'page' => wp_stream_get_meta( $record, 'page', true ),
189
+ ),
190
+ admin_url( 'admin.php' )
191
+ );
192
+ } elseif ( in_array( $record->context, array( 'groups' ) ) ) {
193
+ $group_id = wp_stream_get_meta( $record, 'id', true );
194
+ $group = groups_get_group( array( 'group_id' => $group_id ) );
195
+
196
+ if ( $group ) {
197
+ // Build actions URLs
198
+ $base_url = bp_get_admin_url( 'admin.php?page=bp-groups&amp;gid=' . $group_id );
199
+ $delete_url = wp_nonce_url( $base_url . '&amp;action=delete', 'bp-groups-delete' );
200
+ $edit_url = $base_url . '&amp;action=edit';
201
+ $visit_url = bp_get_group_permalink( $group );
202
+
203
+ $links[ __( 'Edit group', 'stream' ) ] = $edit_url;
204
+ $links[ __( 'View group', 'stream' ) ] = $visit_url;
205
+ $links[ __( 'Delete group', 'stream' ) ] = $delete_url;
206
+ }
207
+ } elseif ( in_array( $record->context, array( 'activity' ) ) ) {
208
+ $activity_id = wp_stream_get_meta( $record, 'id', true );
209
+ $activities = bp_activity_get( array( 'in' => $activity_id, 'spam' => 'all' ) );
210
+ if ( ! empty( $activities['activities'] ) ) {
211
+ $activity = reset( $activities['activities'] );
212
+
213
+ $base_url = bp_get_admin_url( 'admin.php?page=bp-activity&amp;aid=' . $activity->id );
214
+ $spam_nonce = esc_html( '_wpnonce=' . wp_create_nonce( 'spam-activity_' . $activity->id ) );
215
+ $delete_url = $base_url . "&amp;action=delete&amp;$spam_nonce";
216
+ $edit_url = $base_url . '&amp;action=edit';
217
+ $ham_url = $base_url . "&amp;action=ham&amp;$spam_nonce";
218
+ $spam_url = $base_url . "&amp;action=spam&amp;$spam_nonce";
219
+
220
+ if ( $activity->is_spam ) {
221
+ $links[ __( 'Ham', 'stream' ) ] = $ham_url;
222
+ } else {
223
+ $links[ __( 'Edit', 'stream' ) ] = $edit_url;
224
+ $links[ __( 'Spam', 'stream' ) ] = $spam_url;
225
+ }
226
+ $links[ __( 'Delete', 'stream' ) ] = $delete_url;
227
+ }
228
+ } elseif ( in_array( $record->context, array( 'profile_fields' ) ) ) {
229
+ $field_id = wp_stream_get_meta( $record, 'field_id', true );
230
+ $group_id = wp_stream_get_meta( $record, 'group_id', true );
231
+
232
+ if ( empty( $field_id ) ) { // is a group action
233
+ $links[ __( 'Edit', 'stream' ) ] = add_query_arg(
234
+ array(
235
+ 'page' => 'bp-profile-setup',
236
+ 'mode' => 'edit_group',
237
+ 'group_id' => $group_id,
238
+ ),
239
+ admin_url( 'users.php' )
240
+ );
241
+ $links[ __( 'Delete', 'stream' ) ] = add_query_arg(
242
+ array(
243
+ 'page' => 'bp-profile-setup',
244
+ 'mode' => 'delete_group',
245
+ 'group_id' => $group_id,
246
+ ),
247
+ admin_url( 'users.php' )
248
+ );
249
+ } else {
250
+ $field = new BP_XProfile_Field( $field_id );
251
+ if ( empty( $field->type ) ) {
252
+ return $links;
253
+ }
254
+ $links[ __( 'Edit', 'stream' ) ] = add_query_arg(
255
+ array(
256
+ 'page' => 'bp-profile-setup',
257
+ 'mode' => 'edit_field',
258
+ 'group_id' => $group_id,
259
+ 'field_id' => $field_id,
260
+ ),
261
+ admin_url( 'users.php' )
262
+ );
263
+ $links[ __( 'Delete', 'stream' ) ] = add_query_arg(
264
+ array(
265
+ 'page' => 'bp-profile-setup',
266
+ 'mode' => 'delete_field',
267
+ 'field_id' => $field_id,
268
+ ),
269
+ admin_url( 'users.php' )
270
+ );
271
+ }
272
+ }
273
+
274
+ return $links;
275
+ }
276
+
277
+ public static function register() {
278
+ parent::register();
279
+
280
+ self::$options = array_merge(
281
+ self::$options,
282
+ array(
283
+ 'hide-loggedout-adminbar' => array(
284
+ 'label' => _x( 'Toolbar', 'buddypress', 'stream' ),
285
+ 'page' => 'bp-settings',
286
+ ),
287
+ '_bp_force_buddybar' => array(
288
+ 'label' => _x( 'Toolbar', 'buddypress', 'stream' ),
289
+ 'page' => 'bp-settings',
290
+ ),
291
+ 'bp-disable-account-deletion' => array(
292
+ 'label' => _x( 'Account Deletion', 'buddypress', 'stream' ),
293
+ 'page' => 'bp-settings',
294
+ ),
295
+ 'bp-disable-profile-sync' => array(
296
+ 'label' => _x( 'Profile Syncing', 'buddypress', 'stream' ),
297
+ 'page' => 'bp-settings',
298
+ ),
299
+ 'bp_restrict_group_creation' => array(
300
+ 'label' => _x( 'Group Creation', 'buddypress', 'stream' ),
301
+ 'page' => 'bp-settings',
302
+ ),
303
+ 'bb-config-location' => array(
304
+ 'label' => _x( 'bbPress Configuration', 'buddypress', 'stream' ),
305
+ 'page' => 'bp-settings',
306
+ ),
307
+ 'bp-disable-blogforum-comments' => array(
308
+ 'label' => _x( 'Blog &amp; Forum Comments', 'buddypress', 'stream' ),
309
+ 'page' => 'bp-settings',
310
+ ),
311
+ '_bp_enable_heartbeat_refresh' => array(
312
+ 'label' => _x( 'Activity auto-refresh', 'buddypress', 'stream' ),
313
+ 'page' => 'bp-settings',
314
+ ),
315
+ '_bp_enable_akismet' => array(
316
+ 'label' => _x( 'Akismet', 'buddypress', 'stream' ),
317
+ 'page' => 'bp-settings',
318
+ ),
319
+ 'bp-disable-avatar-uploads' => array(
320
+ 'label' => _x( 'Avatar Uploads', 'buddypress', 'stream' ),
321
+ 'page' => 'bp-settings',
322
+ ),
323
+ )
324
+ );
325
+ }
326
+
327
+ public static function callback_update_option( $option, $old, $new ) {
328
+ self::check( $option, $old, $new );
329
+ }
330
+
331
+ public static function callback_add_option( $option, $val ) {
332
+ self::check( $option, null, $val );
333
+ }
334
+
335
+ public static function callback_delete_option( $option ) {
336
+ self::check( $option, null, null );
337
+ }
338
+
339
+ public static function callback_update_site_option( $option, $old, $new ) {
340
+ self::check( $option, $old, $new );
341
+ }
342
+
343
+ public static function callback_add_site_option( $option, $val ) {
344
+ self::check( $option, null, $val );
345
+ }
346
+
347
+ public static function callback_delete_site_option( $option ) {
348
+ self::check( $option, null, null );
349
+ }
350
+
351
+ public static function check( $option, $old_value, $new_value ) {
352
+ if ( ! array_key_exists( $option, self::$options ) ) {
353
+ return;
354
+ }
355
+
356
+ $replacement = str_replace( '-', '_', $option );
357
+
358
+ if ( method_exists( __CLASS__, 'check_' . $replacement ) ) {
359
+ call_user_func( array( __CLASS__, 'check_' . $replacement ), $old_value, $new_value );
360
+ } else {
361
+ $data = self::$options[ $option ];
362
+ $option_title = $data['label'];
363
+ $context = isset( $data['context'] ) ? $data['context'] : 'settings';
364
+ $page = isset( $data['page'] ) ? $data['page'] : null;
365
+
366
+ self::log(
367
+ __( '"%s" setting updated', 'stream' ),
368
+ compact( 'option_title', 'option', 'old_value', 'new_value', 'page' ),
369
+ null,
370
+ $context,
371
+ isset( $data['action'] ) ? $data['action'] : 'updated'
372
+ );
373
+ }
374
+ }
375
+
376
+ public static function check_bp_active_components( $old_value, $new_value ) {
377
+ $options = array();
378
+
379
+ if ( ! is_array( $old_value ) || ! is_array( $new_value ) ) {
380
+ return;
381
+ }
382
+
383
+ foreach ( self::get_changed_keys( $old_value, $new_value, 0 ) as $field_key => $field_value ) {
384
+ $options[ $field_key ] = $field_value;
385
+ }
386
+
387
+ $components = bp_core_admin_get_components();
388
+
389
+ $actions = array(
390
+ true => __( 'activated', 'stream' ),
391
+ false => __( 'deactivated', 'stream' ),
392
+ );
393
+
394
+ foreach ( $options as $option => $option_value ) {
395
+ if ( ! isset( $components[ $option ], $actions[ $option_value ] ) ) {
396
+ continue;
397
+ }
398
+
399
+ self::log(
400
+ sprintf(
401
+ __( '"%1$s" component %2$s', 'stream' ),
402
+ $components[ $option ]['title'],
403
+ $actions[ $option_value ]
404
+ ),
405
+ array(
406
+ 'option' => $option,
407
+ 'option_key' => 'bp-active-components',
408
+ 'old_value' => maybe_serialize( $old_value ),
409
+ 'value' => maybe_serialize( $new_value ),
410
+ ),
411
+ null,
412
+ 'components',
413
+ $option_value ? 'activated' : 'deactivated'
414
+ );
415
+ }
416
+ }
417
+
418
+ public static function check_bp_pages( $old_value, $new_value ) {
419
+ $options = array();
420
+
421
+ if ( ! is_array( $old_value ) || ! is_array( $new_value ) ) {
422
+ return;
423
+ }
424
+
425
+ foreach ( self::get_changed_keys( $old_value, $new_value, 0 ) as $field_key => $field_value ) {
426
+ $options[ $field_key ] = $field_value;
427
+ }
428
+
429
+ $pages = array_merge(
430
+ self::bp_get_directory_pages(),
431
+ array(
432
+ 'register' => _x( 'Register', 'buddypress', 'stream' ),
433
+ 'activate' => _x( 'Activate', 'buddypress', 'stream' ),
434
+ )
435
+ );
436
+
437
+ foreach ( $options as $option => $option_value ) {
438
+ if ( ! isset( $pages[ $option ] ) ) {
439
+ continue;
440
+ }
441
+
442
+ $page = ! empty( $new_value[ $option ] ) ? get_post( $new_value[ $option ] )->post_title : __( 'No page', 'stream' );
443
+
444
+ self::log(
445
+ sprintf(
446
+ __( '"%1$s" page set to "%2$s"', 'stream' ),
447
+ $pages[ $option ],
448
+ $page
449
+ ),
450
+ array(
451
+ 'option' => $option,
452
+ 'option_key' => 'bp-pages',
453
+ 'old_value' => maybe_serialize( $old_value ),
454
+ 'value' => maybe_serialize( $new_value ),
455
+ 'page_id' => empty( $new_value[ $option ] ) ? 0 : $new_value[ $option ],
456
+ ),
457
+ null,
458
+ 'components',
459
+ 'updated'
460
+ );
461
+ }
462
+ }
463
+
464
+ public static function callback_bp_before_activity_delete( $args ) {
465
+ if ( empty( $args['id'] ) ) { // Bail if we're deleting in bulk
466
+ self::$_delete_activity_args = $args;
467
+ return;
468
+ }
469
+
470
+ $activity = new BP_Activity_Activity( $args['id'] );
471
+
472
+ self::$_deleted_activity = $activity;
473
+ }
474
+
475
+ public static function callback_bp_activity_deleted_activities( $activities_ids ) {
476
+ if ( 1 === count( $activities_ids ) && isset( self::$_deleted_activity ) ) { // Single activity deletion
477
+ $activity = self::$_deleted_activity;
478
+ self::log(
479
+ sprintf(
480
+ __( '"%s" activity deleted', 'stream' ),
481
+ strip_tags( $activity->action )
482
+ ),
483
+ array(
484
+ 'id' => $activity->id,
485
+ 'item_id' => $activity->item_id,
486
+ 'type' => $activity->type,
487
+ 'author' => $activity->user_id,
488
+ ),
489
+ $activity->id,
490
+ $activity->component,
491
+ 'deleted'
492
+ );
493
+ } else { // Bulk deletion
494
+ // Sometimes some objects removal are followed by deleting relevant
495
+ // activities, so we probably don't need to track those
496
+ if ( self::$ignore_activity_bulk_deletion ) {
497
+ self::$ignore_activity_bulk_deletion = false;
498
+ return;
499
+ }
500
+ self::log(
501
+ sprintf(
502
+ __( '"%s" activities were deleted', 'stream' ),
503
+ count( $activities_ids )
504
+ ),
505
+ array(
506
+ 'count' => count( $activities_ids ),
507
+ 'args' => self::$_delete_activity_args,
508
+ 'ids' => $activities_ids,
509
+ ),
510
+ null,
511
+ 'activity',
512
+ 'deleted'
513
+ );
514
+ }
515
+ }
516
+
517
+ public static function callback_bp_activity_mark_as_spam( $activity, $by ) {
518
+ self::log(
519
+ sprintf(
520
+ __( 'Marked activity "%s" as spam', 'stream' ),
521
+ strip_tags( $activity->action )
522
+ ),
523
+ array(
524
+ 'id' => $activity->id,
525
+ 'item_id' => $activity->item_id,
526
+ 'type' => $activity->type,
527
+ 'author' => $activity->user_id,
528
+ ),
529
+ $activity->id,
530
+ $activity->component,
531
+ 'spammed'
532
+ );
533
+ }
534
+
535
+ public static function callback_bp_activity_mark_as_ham( $activity, $by ) {
536
+ self::log(
537
+ sprintf(
538
+ __( 'Unmarked activity "%s" as spam', 'stream' ),
539
+ strip_tags( $activity->action )
540
+ ),
541
+ array(
542
+ 'id' => $activity->id,
543
+ 'item_id' => $activity->item_id,
544
+ 'type' => $activity->type,
545
+ 'author' => $activity->user_id,
546
+ ),
547
+ $activity->id,
548
+ $activity->component,
549
+ 'unspammed'
550
+ );
551
+ }
552
+
553
+ public static function callback_bp_activity_admin_edit_after( $activity, $error ) {
554
+ self::log(
555
+ sprintf(
556
+ __( '"%s" activity updated', 'stream' ),
557
+ strip_tags( $activity->action )
558
+ ),
559
+ array(
560
+ 'id' => $activity->id,
561
+ 'item_id' => $activity->item_id,
562
+ 'type' => $activity->type,
563
+ 'author' => $activity->user_id,
564
+ ),
565
+ $activity->id,
566
+ 'activity',
567
+ 'updated'
568
+ );
569
+ }
570
+
571
+ public static function group_action( $group, $action, $meta = array(), $message = null ) {
572
+ if ( is_numeric( $group ) ) {
573
+ $group = groups_get_group( array( 'group_id' => $group ) );
574
+ }
575
+
576
+ $replacements = array(
577
+ $group->name,
578
+ );
579
+
580
+ if ( $message ) {
581
+ // Do nothing
582
+ }
583
+ elseif ( 'created' === $action ) {
584
+ $message = __( '"%s" group created', 'stream' );
585
+ }
586
+ elseif ( 'updated' === $action ) {
587
+ $message = __( '"%s" group updated', 'stream' );
588
+ }
589
+ elseif ( 'deleted' === $action ) {
590
+ $message = __( '"%s" group deleted', 'stream' );
591
+ }
592
+ elseif ( 'joined' === $action ) {
593
+ $message = __( 'Joined group "%s"', 'stream' );
594
+ }
595
+ elseif ( 'left' === $action ) {
596
+ $message = __( 'Left group "%s"', 'stream' );
597
+ }
598
+ elseif ( 'banned' === $action ) {
599
+ $message = __( 'Banned "%2$s" from "%1$s"', 'stream' );
600
+ $replacements[] = get_user_by( 'id', $meta['user_id'] )->display_name;
601
+ }
602
+ elseif ( 'unbanned' === $action ) {
603
+ $message = __( 'Unbanned "%2$s" from "%1$s"', 'stream' );
604
+ $replacements[] = get_user_by( 'id', $meta['user_id'] )->display_name;
605
+ }
606
+ elseif ( 'removed' === $action ) {
607
+ $message = __( 'Removed "%2$s" from "%1$s"', 'stream' );
608
+ $replacements[] = get_user_by( 'id', $meta['user_id'] )->display_name;
609
+ }
610
+ else {
611
+ return;
612
+ }
613
+
614
+ self::log(
615
+ vsprintf(
616
+ $message,
617
+ $replacements
618
+ ),
619
+ array_merge(
620
+ array(
621
+ 'id' => $group->id,
622
+ 'name' => $group->name,
623
+ 'slug' => $group->slug,
624
+ ),
625
+ $meta
626
+ ),
627
+ $group->id,
628
+ 'groups',
629
+ $action
630
+ );
631
+ }
632
+
633
+ public static function callback_groups_create_group( $group_id, $member, $group ) {
634
+ self::group_action( $group, 'created' );
635
+ }
636
+ public static function callback_groups_update_group( $group_id, $group ) {
637
+ self::group_action( $group, 'updated' );
638
+ }
639
+ public static function callback_groups_before_delete_group( $group_id ) {
640
+ self::$ignore_activity_bulk_deletion = true;
641
+ self::group_action( $group_id, 'deleted' );
642
+ }
643
+ public static function callback_groups_details_updated( $group_id ) {
644
+ self::$is_update = true;
645
+ self::group_action( $group_id, 'updated' );
646
+ }
647
+ public static function callback_groups_settings_updated( $group_id ) {
648
+ if ( self::$is_update ) {
649
+ return;
650
+ }
651
+ self::group_action( $group_id, 'updated' );
652
+ }
653
+
654
+ public static function callback_groups_leave_group( $group_id, $user_id ) {
655
+ self::group_action( $group_id, 'left', compact( 'user_id' ) );
656
+ }
657
+ public static function callback_groups_join_group( $group_id, $user_id ) {
658
+ self::group_action( $group_id, 'joined', compact( 'user_id' ) );
659
+ }
660
+
661
+ public static function callback_groups_promote_member( $group_id, $user_id, $status ) {
662
+ $group = groups_get_group( array( 'group_id' => $group_id ) );
663
+ $user = new WP_User( $user_id );
664
+ $roles = array(
665
+ 'admin' => _x( 'Administrator', 'buddypress', 'stream' ),
666
+ 'mod' => _x( 'Moderator', 'buddypress', 'stream' ),
667
+ );
668
+ $message = sprintf(
669
+ __( 'Promoted "%s" to "%s" in "%s"', 'stream' ),
670
+ $user->display_name,
671
+ $roles[ $status ],
672
+ $group->name
673
+ );
674
+ self::group_action( $group_id, 'promoted', compact( 'user_id', 'status' ), $message );
675
+ }
676
+ public static function callback_groups_demote_member( $group_id, $user_id ) {
677
+ $group = groups_get_group( array( 'group_id' => $group_id ) );
678
+ $user = new WP_User( $user_id );
679
+ $message = sprintf(
680
+ __( 'Demoted "%s" to "%s" in "%s"', 'stream' ),
681
+ $user->display_name,
682
+ _x( 'Member', 'buddypress', 'stream' ),
683
+ $group->name
684
+ );
685
+ self::group_action( $group_id, 'demoted', compact( 'user_id' ), $message );
686
+ }
687
+ public static function callback_groups_ban_member( $group_id, $user_id ) {
688
+ self::group_action( $group_id, 'banned', compact( 'user_id' ) );
689
+ }
690
+ public static function callback_groups_unban_member( $group_id, $user_id ) {
691
+ self::group_action( $group_id, 'unbanned', compact( 'user_id' ) );
692
+ }
693
+ public static function callback_groups_remove_member( $group_id, $user_id ) {
694
+ self::group_action( $group_id, 'removed', compact( 'user_id' ) );
695
+ }
696
+
697
+ public static function field_action( $field, $action, $meta = array(), $message = null ) {
698
+ $replacements = array(
699
+ $field->name,
700
+ );
701
+
702
+ if ( $message ) {
703
+ // Do nothing
704
+ }
705
+ elseif ( 'created' === $action ) {
706
+ $message = __( 'Created profile field "%s"', 'stream' );
707
+ }
708
+ elseif ( 'updated' === $action ) {
709
+ $message = __( 'Updated profile field "%s"', 'stream' );
710
+ }
711
+ elseif ( 'deleted' === $action ) {
712
+ $message = __( 'Deleted profile field "%s"', 'stream' );
713
+ }
714
+ else {
715
+ return;
716
+ }
717
+
718
+ self::log(
719
+ vsprintf(
720
+ $message,
721
+ $replacements
722
+ ),
723
+ array_merge(
724
+ array(
725
+ 'field_id' => $field->id,
726
+ 'field_name' => $field->name,
727
+ 'group_id' => $field->group_id,
728
+ ),
729
+ $meta
730
+ ),
731
+ $field->id,
732
+ 'profile_fields',
733
+ $action
734
+ );
735
+ }
736
+
737
+ public static function callback_xprofile_field_after_save( $field ) {
738
+ $action = isset( $field->id ) ? 'updated' : 'created';
739
+ self::field_action( $field, $action );
740
+ }
741
+
742
+ public static function callback_xprofile_fields_deleted_field( $field ){
743
+ self::field_action( $field, 'deleted' );
744
+ }
745
+
746
+ public static function field_group_action( $group, $action, $meta = array(), $message = null ) {
747
+ $replacements = array(
748
+ $group->name,
749
+ );
750
+
751
+ if ( $message ) {
752
+ // Do nothing
753
+ }
754
+ elseif ( 'created' === $action ) {
755
+ $message = __( 'Created profile field group "%s"', 'stream' );
756
+ }
757
+ elseif ( 'updated' === $action ) {
758
+ $message = __( 'Updated profile field group "%s"', 'stream' );
759
+ }
760
+ elseif ( 'deleted' === $action ) {
761
+ $message = __( 'Deleted profile field group "%s"', 'stream' );
762
+ }
763
+ else {
764
+ return;
765
+ }
766
+
767
+ self::log(
768
+ vsprintf(
769
+ $message,
770
+ $replacements
771
+ ),
772
+ array_merge(
773
+ array(
774
+ 'group_id' => $group->id,
775
+ 'group_name' => $group->name,
776
+ ),
777
+ $meta
778
+ ),
779
+ $group->id,
780
+ 'profile_fields',
781
+ $action
782
+ );
783
+ }
784
+
785
+ public static function callback_xprofile_group_after_save( $group ) {
786
+ global $wpdb;
787
+ // a bit hacky, due to inconsistency with BP action scheme, see callback_xprofile_field_after_save for correct behavior
788
+ $action = ( $group->id === $wpdb->insert_id ) ? 'created' : 'updated';
789
+ self::field_group_action( $group, $action );
790
+ }
791
+
792
+ public static function callback_xprofile_groups_deleted_group( $group ){
793
+ self::field_group_action( $group, 'deleted' );
794
+ }
795
+
796
+ private static function bp_get_directory_pages() {
797
+ $bp = buddypress();
798
+ $directory_pages = array();
799
+
800
+ // Loop through loaded components and collect directories
801
+ if ( is_array( $bp->loaded_components ) ) {
802
+ foreach ( $bp->loaded_components as $component_slug => $component_id ) {
803
+ // Only components that need directories should be listed here
804
+ if ( isset( $bp->{$component_id} ) && ! empty( $bp->{$component_id}->has_directory ) ) {
805
+ // component->name was introduced in BP 1.5, so we must provide a fallback
806
+ $directory_pages[ $component_id ] = ! empty( $bp->{ $component_id }->name ) ? $bp->{ $component_id }->name : ucwords( $component_id );
807
+ }
808
+ }
809
+ }
810
+
811
+ return $directory_pages;
812
+ }
813
+
814
+ }
connectors/class-wp-stream-connector-comments.php ADDED
@@ -0,0 +1,594 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class WP_Stream_Connector_Comments extends WP_Stream_Connector {
4
+
5
+ /**
6
+ * Connector slug
7
+ *
8
+ * @var string
9
+ */
10
+ public static $name = 'comments';
11
+
12
+ /**
13
+ * Actions registered for this connector
14
+ *
15
+ * @var array
16
+ */
17
+ public static $actions = array(
18
+ 'comment_flood_trigger',
19
+ 'wp_insert_comment',
20
+ 'edit_comment',
21
+ 'before_delete_post',
22
+ 'deleted_post',
23
+ 'delete_comment',
24
+ 'trash_comment',
25
+ 'untrash_comment',
26
+ 'spam_comment',
27
+ 'unspam_comment',
28
+ 'transition_comment_status',
29
+ 'comment_duplicate_trigger',
30
+ );
31
+
32
+ /**
33
+ * Catch and store the post ID during post deletion
34
+ *
35
+ * @var int
36
+ */
37
+ protected static $delete_post = 0;
38
+
39
+ /**
40
+ * Return translated connector label
41
+ *
42
+ * @return string Translated connector label
43
+ */
44
+ public static function get_label() {
45
+ return __( 'Comments', 'stream' );
46
+ }
47
+
48
+ /**
49
+ * Return translated action labels
50
+ *
51
+ * @return array Action label translations
52
+ */
53
+ public static function get_action_labels() {
54
+ return array(
55
+ 'created' => __( 'Created', 'stream' ),
56
+ 'edited' => __( 'Edited', 'stream' ),
57
+ 'replied' => __( 'Replied', 'stream' ),
58
+ 'approved' => __( 'Approved', 'stream' ),
59
+ 'unapproved' => __( 'Unapproved', 'stream' ),
60
+ 'trashed' => __( 'Trashed', 'stream' ),
61
+ 'untrashed' => __( 'Restored', 'stream' ),
62
+ 'spammed' => __( 'Marked as Spam', 'stream' ),
63
+ 'unspammed' => __( 'Unmarked as Spam', 'stream' ),
64
+ 'deleted' => __( 'Deleted', 'stream' ),
65
+ 'duplicate' => __( 'Duplicate', 'stream' ),
66
+ 'flood' => __( 'Throttled', 'stream' ),
67
+ );
68
+ }
69
+
70
+ /**
71
+ * Return translated context labels
72
+ *
73
+ * @return array Context label translations
74
+ */
75
+ public static function get_context_labels() {
76
+ return array(
77
+ 'comments' => __( 'Comments', 'stream' ),
78
+ );
79
+ }
80
+
81
+ /**
82
+ * Return translated comment type labels
83
+ *
84
+ * @return array Comment type label translations
85
+ */
86
+ public static function get_comment_type_labels() {
87
+ return apply_filters(
88
+ 'wp_stream_comments_comment_type_labels',
89
+ array(
90
+ 'comment' => __( 'Comment', 'stream' ),
91
+ 'trackback' => __( 'Trackback', 'stream' ),
92
+ 'pingback' => __( 'Pingback', 'stream' ),
93
+ )
94
+ );
95
+ }
96
+
97
+ /**
98
+ * Return the comment type label for a given comment ID
99
+ *
100
+ * @param int $comment_id ID of the comment
101
+ * @return string The comment type label
102
+ */
103
+ public static function get_comment_type_label( $comment_id ) {
104
+ $comment_type = get_comment_type( $comment_id );
105
+
106
+ if ( empty( $comment_type ) ) {
107
+ $comment_type = 'comment';
108
+ }
109
+
110
+ $comment_type_labels = self::get_comment_type_labels();
111
+
112
+ $label = isset( $comment_type_labels[ $comment_type ] ) ? $comment_type_labels[ $comment_type ] : $comment_type;
113
+
114
+ return $label;
115
+ }
116
+
117
+ /**
118
+ * Add action links to Stream drop row in admin list screen
119
+ *
120
+ * @filter wp_stream_action_links_{connector}
121
+ *
122
+ * @param array $links Previous links registered
123
+ * @param object $record Stream record
124
+ *
125
+ * @return array Action links
126
+ */
127
+ public static function action_links( $links, $record ) {
128
+ if ( $record->object_id ) {
129
+ if ( $comment = get_comment( $record->object_id ) ) {
130
+ $del_nonce = wp_create_nonce( "delete-comment_$comment->comment_ID" );
131
+ $approve_nonce = wp_create_nonce( "approve-comment_$comment->comment_ID" );
132
+
133
+ $links[ __( 'Edit', 'stream' ) ] = admin_url( "comment.php?action=editcomment&c=$comment->comment_ID" );
134
+
135
+ if ( 1 === $comment->comment_approved ) {
136
+ $links[ __( 'Unapprove', 'stream' ) ] = admin_url(
137
+ sprintf(
138
+ 'comment.php?action=unapprovecomment&c=%s&_wpnonce=%s',
139
+ $record->object_id,
140
+ $approve_nonce
141
+ )
142
+ );
143
+ } elseif ( empty( $comment->comment_approved ) ) {
144
+ $links[ __( 'Approve', 'stream' ) ] = admin_url(
145
+ sprintf(
146
+ 'comment.php?action=approvecomment&c=%s&_wpnonce=%s',
147
+ $record->object_id,
148
+ $approve_nonce
149
+ )
150
+ );
151
+ }
152
+ }
153
+ }
154
+
155
+ return $links;
156
+ }
157
+
158
+ /**
159
+ * Fetches the comment author and returns the specified field.
160
+ *
161
+ * This also takes into consideration whether or not the blog requires only
162
+ * name and e-mail or that users be logged in to comment. In either case it
163
+ * will try to see if the e-mail provided does belong to a registered user.
164
+ *
165
+ * @param object|int $comment A comment object or comment ID
166
+ * @param string $field What field you want to return
167
+ * @return int|string $output User ID or user display name
168
+ */
169
+ public static function get_comment_author( $comment, $field = 'id' ) {
170
+ $comment = is_object( $comment ) ? $comment : get_comment( absint( $comment ) );
171
+
172
+ $req_name_email = get_option( 'require_name_email' );
173
+ $req_user_login = get_option( 'comment_registration' );
174
+
175
+ $user_id = 0;
176
+ $user_name = __( 'Guest', 'stream' );
177
+
178
+ if ( $req_name_email && isset( $comment->comment_author_email ) && isset( $comment->comment_author ) ) {
179
+ $user = get_user_by( 'email', $comment->comment_author_email );
180
+ $user_id = isset( $user->ID ) ? $user->ID : 0;
181
+ $user_name = isset( $user->display_name ) ? $user->display_name : $comment->comment_author;
182
+ }
183
+
184
+ if ( $req_user_login ) {
185
+ $user = wp_get_current_user();
186
+ $user_id = $user->ID;
187
+ $user_name = $user->display_name;
188
+ }
189
+
190
+ if ( 'id' === $field ) {
191
+ $output = $user_id;
192
+ } elseif ( 'name' === $field ) {
193
+ $output = $user_name;
194
+ }
195
+
196
+ return $output;
197
+ }
198
+
199
+ /**
200
+ * Tracks comment flood blocks
201
+ *
202
+ * @action comment_flood_trigger
203
+ */
204
+ public static function callback_comment_flood_trigger( $time_lastcomment, $time_newcomment ) {
205
+ $flood_tracking = isset( WP_Stream_Settings::$options['advanced_comment_flood_tracking'] ) ? WP_Stream_Settings::$options['advanced_comment_flood_tracking'] : false;
206
+
207
+ if ( ! $flood_tracking ) {
208
+ return;
209
+ }
210
+
211
+ $req_user_login = get_option( 'comment_registration' );
212
+
213
+ if ( $req_user_login ) {
214
+ $user = wp_get_current_user();
215
+ $user_id = $user->ID;
216
+ $user_name = $user->display_name;
217
+ } else {
218
+ $user_name = __( 'a logged out user', 'stream' );
219
+ }
220
+
221
+ self::log(
222
+ __( 'Comment flooding by %s detected and prevented', 'stream' ),
223
+ compact( 'user_name', 'user_id', 'time_lastcomment', 'time_newcomment' ),
224
+ null,
225
+ 'comments',
226
+ 'flood'
227
+ );
228
+ }
229
+
230
+ /**
231
+ * Tracks comment creation
232
+ *
233
+ * @action wp_insert_comment
234
+ */
235
+ public static function callback_wp_insert_comment( $comment_id, $comment ) {
236
+ if ( in_array( $comment->comment_type, self::get_ignored_comment_types() ) ) {
237
+ return;
238
+ }
239
+
240
+ $user_id = self::get_comment_author( $comment, 'id' );
241
+ $user_name = self::get_comment_author( $comment, 'name' );
242
+ $post_id = $comment->comment_post_ID;
243
+ $post_type = get_post_type( $post_id );
244
+ $post_title = ( $post = get_post( $post_id ) ) ? "\"$post->post_title\"" : __( 'a post', 'stream' );
245
+ $comment_status = ( 1 == $comment->comment_approved ) ? __( 'approved automatically', 'stream' ) : __( 'pending approval', 'stream' );
246
+ $is_spam = false;
247
+
248
+ // Auto-marked spam comments
249
+ $ak_tracking = isset( WP_Stream_Settings::$options['advanced_akismet_tracking'] ) ? WP_Stream_Settings::$options['advanced_akismet_tracking'] : false;
250
+ if ( class_exists( 'Akismet' ) && $ak_tracking && Akismet::matches_last_comment( $comment ) ) {
251
+ $ak_last_comment = Akismet::get_last_comment();
252
+ if ( 'true' == $ak_last_comment['akismet_result'] ) {
253
+ $is_spam = true;
254
+ $comment_status = __( 'automatically marked as spam by Akismet', 'stream' );
255
+ }
256
+ }
257
+ $comment_type = mb_strtolower( self::get_comment_type_label( $comment_id ) );
258
+
259
+ if ( $comment->comment_parent ) {
260
+ $parent_user_id = get_comment_author( $comment->comment_parent, 'id' );
261
+ $parent_user_name = get_comment_author( $comment->comment_parent, 'name' );
262
+
263
+ self::log(
264
+ _x(
265
+ 'Reply to %1$s\'s %5$s by %2$s on %3$s %4$s',
266
+ "1: Parent comment's author, 2: Comment author, 3: Post title, 4: Comment status, 5: Comment type",
267
+ 'stream'
268
+ ),
269
+ compact( 'parent_user_name', 'user_name', 'post_title', 'comment_status', 'comment_type', 'post_id', 'parent_user_id' ),
270
+ $comment_id,
271
+ $post_type,
272
+ 'replied',
273
+ $user_id
274
+ );
275
+ } else {
276
+ self::log(
277
+ _x(
278
+ 'New %4$s by %1$s on %2$s %3$s',
279
+ '1: Comment author, 2: Post title 3: Comment status, 4: Comment type',
280
+ 'stream'
281
+ ),
282
+ compact( 'user_name', 'post_title', 'comment_status', 'comment_type', 'post_id', 'is_spam' ),
283
+ $comment_id,
284
+ $post_type,
285
+ $is_spam ? 'spammed' : 'created',
286
+ $user_id
287
+ );
288
+ }
289
+ }
290
+
291
+ /**
292
+ * Tracks comment updates
293
+ *
294
+ * @action edit_comment
295
+ */
296
+ public static function callback_edit_comment( $comment_id ) {
297
+ $comment = get_comment( $comment_id );
298
+
299
+ if ( in_array( $comment->comment_type, self::get_ignored_comment_types() ) ) {
300
+ return;
301
+ }
302
+
303
+ $user_id = self::get_comment_author( $comment, 'id' );
304
+ $user_name = self::get_comment_author( $comment, 'name' );
305
+ $post_id = $comment->comment_post_ID;
306
+ $post_type = get_post_type( $post_id );
307
+ $post_title = ( $post = get_post( $post_id ) ) ? "\"$post->post_title\"" : __( 'a post', 'stream' );
308
+ $comment_type = mb_strtolower( self::get_comment_type_label( $comment_id ) );
309
+
310
+ self::log(
311
+ _x(
312
+ '%1$s\'s %3$s on %2$s edited',
313
+ '1: Comment author, 2: Post title, 3: Comment type',
314
+ 'stream'
315
+ ),
316
+ compact( 'user_name', 'post_title', 'comment_type', 'post_id', 'user_id' ),
317
+ $comment_id,
318
+ $post_type,
319
+ 'edited'
320
+ );
321
+ }
322
+
323
+ /**
324
+ * Catch the post ID during deletion
325
+ *
326
+ * @action before_delete_post
327
+ */
328
+ public static function callback_before_delete_post( $post_id ) {
329
+ if ( wp_is_post_revision( $post_id ) ) {
330
+ return;
331
+ }
332
+
333
+ self::$delete_post = $post_id;
334
+ }
335
+
336
+ /**
337
+ * Reset the post ID after deletion
338
+ *
339
+ * @action deleted_post
340
+ */
341
+ public static function callback_deleted_post( $post_id ) {
342
+ if ( wp_is_post_revision( $post_id ) ) {
343
+ return;
344
+ }
345
+
346
+ self::$delete_post = 0;
347
+ }
348
+
349
+ /**
350
+ * Tracks comment delete
351
+ *
352
+ * @action delete_comment
353
+ */
354
+ public static function callback_delete_comment( $comment_id ) {
355
+ $comment = get_comment( $comment_id );
356
+
357
+ if ( in_array( $comment->comment_type, self::get_ignored_comment_types() ) ) {
358
+ return;
359
+ }
360
+
361
+ $user_id = self::get_comment_author( $comment, 'id' );
362
+ $user_name = self::get_comment_author( $comment, 'name' );
363
+ $post_id = absint( $comment->comment_post_ID );
364
+ $post_type = get_post_type( $post_id );
365
+ $post_title = ( $post = get_post( $post_id ) ) ? "\"$post->post_title\"" : __( 'a post', 'stream' );
366
+ $comment_type = mb_strtolower( self::get_comment_type_label( $comment_id ) );
367
+
368
+ if ( self::$delete_post === $post_id ) {
369
+ return;
370
+ }
371
+
372
+ self::log(
373
+ _x(
374
+ '%1$s\'s %3$s on %2$s deleted permanently',
375
+ '1: Comment author, 2: Post title, 3: Comment type',
376
+ 'stream'
377
+ ),
378
+ compact( 'user_name', 'post_title', 'comment_type', 'post_id', 'user_id' ),
379
+ $comment_id,
380
+ $post_type,
381
+ 'deleted'
382
+ );
383
+ }
384
+
385
+ /**
386
+ * Tracks comment trashing
387
+ *
388
+ * @action trash_comment
389
+ */
390
+ public static function callback_trash_comment( $comment_id ) {
391
+ $comment = get_comment( $comment_id );
392
+
393
+ if ( in_array( $comment->comment_type, self::get_ignored_comment_types() ) ) {
394
+ return;
395
+ }
396
+
397
+ $user_id = self::get_comment_author( $comment, 'id' );
398
+ $user_name = self::get_comment_author( $comment, 'name' );
399
+ $post_id = $comment->comment_post_ID;
400
+ $post_type = get_post_type( $post_id );
401
+ $post_title = ( $post = get_post( $post_id ) ) ? "\"$post->post_title\"" : __( 'a post', 'stream' );
402
+ $comment_type = mb_strtolower( self::get_comment_type_label( $comment_id ) );
403
+
404
+ self::log(
405
+ _x(
406
+ '%1$s\'s %3$s on %2$s trashed',
407
+ '1: Comment author, 2: Post title, 3: Comment type',
408
+ 'stream'
409
+ ),
410
+ compact( 'user_name', 'post_title', 'comment_type', 'post_id', 'user_id' ),
411
+ $comment_id,
412
+ $post_type,
413
+ 'trashed'
414
+ );
415
+ }
416
+
417
+ /**
418
+ * Tracks comment trashing
419
+ *
420
+ * @action untrash_comment
421
+ */
422
+ public static function callback_untrash_comment( $comment_id ) {
423
+ $comment = get_comment( $comment_id );
424
+
425
+ if ( in_array( $comment->comment_type, self::get_ignored_comment_types() ) ) {
426
+ return;
427
+ }
428
+
429
+ $user_id = self::get_comment_author( $comment, 'id' );
430
+ $user_name = self::get_comment_author( $comment, 'name' );
431
+ $post_id = $comment->comment_post_ID;
432
+ $post_type = get_post_type( $post_id );
433
+ $post_title = ( $post = get_post( $post_id ) ) ? "\"$post->post_title\"" : __( 'a post', 'stream' );
434
+ $comment_type = mb_strtolower( self::get_comment_type_label( $comment_id ) );
435
+
436
+ self::log(
437
+ _x(
438
+ '%1$s\'s %3$s on %2$s restored',
439
+ '1: Comment author, 2: Post title, 3: Comment type',
440
+ 'stream'
441
+ ),
442
+ compact( 'user_name', 'post_title', 'comment_type', 'post_id', 'user_id' ),
443
+ $comment_id,
444
+ $post_type,
445
+ 'untrashed'
446
+ );
447
+ }
448
+
449
+ /**
450
+ * Tracks comment marking as spam
451
+ *
452
+ * @action spam_comment
453
+ */
454
+ public static function callback_spam_comment( $comment_id ) {
455
+ $comment = get_comment( $comment_id );
456
+
457
+ if ( in_array( $comment->comment_type, self::get_ignored_comment_types() ) ) {
458
+ return;
459
+ }
460
+
461
+ $user_id = self::get_comment_author( $comment, 'id' );
462
+ $user_name = self::get_comment_author( $comment, 'name' );
463
+ $post_id = $comment->comment_post_ID;
464
+ $post_type = get_post_type( $post_id );
465
+ $post_title = ( $post = get_post( $post_id ) ) ? "\"$post->post_title\"" : __( 'a post', 'stream' );
466
+ $comment_type = mb_strtolower( self::get_comment_type_label( $comment_id ) );
467
+
468
+ self::log(
469
+ _x(
470
+ '%1$s\'s %3$s on %2$s marked as spam',
471
+ '1: Comment author, 2: Post title, 3: Comment type',
472
+ 'stream'
473
+ ),
474
+ compact( 'user_name', 'post_title', 'comment_type', 'post_id', 'user_id' ),
475
+ $comment_id,
476
+ $post_type,
477
+ 'spammed'
478
+ );
479
+ }
480
+
481
+ /**
482
+ * Tracks comment unmarking as spam
483
+ *
484
+ * @action unspam_comment
485
+ */
486
+ public static function callback_unspam_comment( $comment_id ) {
487
+ $comment = get_comment( $comment_id );
488
+
489
+ if ( in_array( $comment->comment_type, self::get_ignored_comment_types() ) ) {
490
+ return;
491
+ }
492
+
493
+ $user_id = self::get_comment_author( $comment, 'id' );
494
+ $user_name = self::get_comment_author( $comment, 'name' );
495
+ $post_id = $comment->comment_post_ID;
496
+ $post_type = get_post_type( $post_id );
497
+ $post_title = ( $post = get_post( $post_id ) ) ? "\"$post->post_title\"" : __( 'a post', 'stream' );
498
+ $comment_type = mb_strtolower( self::get_comment_type_label( $comment_id ) );
499
+
500
+ self::log(
501
+ _x(
502
+ '%1$s\'s %3$s on %2$s unmarked as spam',
503
+ '1: Comment author, 2: Post title, 3: Comment type',
504
+ 'stream'
505
+ ),
506
+ compact( 'user_name', 'post_title', 'comment_type', 'post_id', 'user_id' ),
507
+ $comment_id,
508
+ $post_type,
509
+ 'unspammed'
510
+ );
511
+ }
512
+
513
+ /**
514
+ * Track comment status transition
515
+ *
516
+ * @action transition_comment_status
517
+ */
518
+ public static function callback_transition_comment_status( $new_status, $old_status, $comment ) {
519
+ if ( in_array( $comment->comment_type, self::get_ignored_comment_types() ) ) {
520
+ return;
521
+ }
522
+
523
+ if ( 'approved' !== $new_status && 'unapproved' !== $new_status || 'trash' === $old_status || 'spam' === $old_status ) {
524
+ return;
525
+ }
526
+
527
+ $user_id = self::get_comment_author( $comment, 'id' );
528
+ $user_name = self::get_comment_author( $comment, 'name' );
529
+ $post_id = $comment->comment_post_ID;
530
+ $post_type = get_post_type( $post_id );
531
+ $post_title = ( $post = get_post( $post_id ) ) ? "\"$post->post_title\"" : __( 'a post', 'stream' );
532
+ $comment_type = get_comment_type( $comment->comment_ID );
533
+
534
+ self::log(
535
+ _x(
536
+ '%1$s\'s %3$s %2$s',
537
+ 'Comment status transition. 1: Comment author, 2: Post title, 3: Comment type',
538
+ 'stream'
539
+ ),
540
+ compact( 'user_name', 'new_status', 'comment_type', 'old_status', 'post_title', 'post_id', 'user_id' ),
541
+ $comment->comment_ID,
542
+ $post_type,
543
+ $new_status
544
+ );
545
+ }
546
+
547
+ /**
548
+ * Track attempts to add duplicate comments
549
+ *
550
+ * @action comment_duplicate_trigger
551
+ */
552
+ public static function callback_comment_duplicate_trigger( $comment_data ) {
553
+ global $wpdb;
554
+
555
+ $comment_id = $wpdb->last_result[0]->comment_ID;
556
+ $comment = get_comment( $comment_id );
557
+
558
+ if ( in_array( $comment->comment_type, self::get_ignored_comment_types() ) ) {
559
+ return;
560
+ }
561
+
562
+ $user_id = self::get_comment_author( $comment, 'id' );
563
+ $user_name = self::get_comment_author( $comment, 'name' );
564
+ $post_id = $comment->comment_post_ID;
565
+ $post_type = get_post_type( $post_id );
566
+ $post_title = ( $post = get_post( $post_id ) ) ? "\"$post->post_title\"" : __( 'a post', 'stream' );
567
+ $comment_type = mb_strtolower( self::get_comment_type_label( $comment_id ) );
568
+
569
+ self::log(
570
+ _x(
571
+ 'Duplicate %3$s by %1$s prevented on %2$s',
572
+ '1: Comment author, 2: Post title, 3: Comment type',
573
+ 'stream'
574
+ ),
575
+ compact( 'user_name', 'post_title', 'comment_type', 'post_id', 'user_id' ),
576
+ $comment_id,
577
+ $post_type,
578
+ 'duplicate'
579
+ );
580
+ }
581
+
582
+ /**
583
+ * Constructs list of ignored comment types for the comments connector
584
+ *
585
+ * @return array List of ignored comment types
586
+ */
587
+ public static function get_ignored_comment_types() {
588
+ return apply_filters(
589
+ 'wp_stream_comments_exclude_comment_types',
590
+ array()
591
+ );
592
+ }
593
+
594
+ }
connectors/class-wp-stream-connector-edd.php ADDED
@@ -0,0 +1,490 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class WP_Stream_Connector_EDD extends WP_Stream_Connector {
4
+
5
+ /**
6
+ * Connector slug
7
+ *
8
+ * @var string
9
+ */
10
+ public static $name = 'edd';
11
+
12
+ /**
13
+ * Holds tracked plugin minimum version required
14
+ *
15
+ * @const string
16
+ */
17
+ const PLUGIN_MIN_VERSION = '1.8.8';
18
+
19
+ /**
20
+ * Actions registered for this connector
21
+ *
22
+ * @var array
23
+ */
24
+ public static $actions = array(
25
+ 'update_option',
26
+ 'add_option',
27
+ 'delete_option',
28
+ 'update_site_option',
29
+ 'add_site_option',
30
+ 'delete_site_option',
31
+ 'edd_pre_update_discount_status',
32
+ 'edd_generate_pdf',
33
+ 'edd_earnings_export',
34
+ 'edd_payment_export',
35
+ 'edd_email_export',
36
+ 'edd_downloads_history_export',
37
+ 'edd_import_settings',
38
+ 'edd_export_settings',
39
+ 'add_user_meta',
40
+ 'update_user_meta',
41
+ 'delete_user_meta',
42
+ );
43
+
44
+ /**
45
+ * Tracked option keys
46
+ *
47
+ * @var array
48
+ */
49
+ public static $options = array();
50
+
51
+ /**
52
+ * Tracking registered Settings, with overridden data
53
+ *
54
+ * @var array
55
+ */
56
+ public static $options_override = array();
57
+
58
+ /**
59
+ * Tracking user meta updates related to this connector
60
+ *
61
+ * @var array
62
+ */
63
+ public static $user_meta = array(
64
+ 'edd_user_public_key',
65
+ );
66
+
67
+ /**
68
+ * Flag status changes to not create duplicate entries
69
+ * @var bool
70
+ */
71
+ public static $is_discount_status_change = false;
72
+
73
+ /**
74
+ * Flag status changes to not create duplicate entries
75
+ * @var bool
76
+ */
77
+ public static $is_payment_status_change = false;
78
+
79
+ /**
80
+ * Check if plugin dependencies are satisfied and add an admin notice if not
81
+ *
82
+ * @return bool
83
+ */
84
+ public static function is_dependency_satisfied() {
85
+ if ( class_exists( 'Easy_Digital_Downloads' ) && defined( 'EDD_VERSION' ) && version_compare( EDD_VERSION, self::PLUGIN_MIN_VERSION, '>=' ) ) {
86
+ return true;
87
+ }
88
+
89
+ return false;
90
+ }
91
+
92
+ /**
93
+ * Return translated connector label
94
+ *
95
+ * @return string Translated connector label
96
+ */
97
+ public static function get_label() {
98
+ return _x( 'Easy Digital Downloads', 'edd', 'stream' );
99
+ }
100
+
101
+ /**
102
+ * Return translated action labels
103
+ *
104
+ * @return array Action label translations
105
+ */
106
+ public static function get_action_labels() {
107
+ return array(
108
+ 'created' => _x( 'Created', 'edd', 'stream' ),
109
+ 'updated' => _x( 'Updated', 'edd', 'stream' ),
110
+ 'added' => _x( 'Added', 'edd', 'stream' ),
111
+ 'deleted' => _x( 'Deleted', 'edd', 'stream' ),
112
+ 'trashed' => _x( 'Trashed', 'edd', 'stream' ),
113
+ 'untrashed' => _x( 'Restored', 'edd', 'stream' ),
114
+ 'generated' => _x( 'Generated', 'edd', 'stream' ),
115
+ 'imported' => _x( 'Imported', 'edd', 'stream' ),
116
+ 'exported' => _x( 'Exported', 'edd', 'stream' ),
117
+ 'revoked' => _x( 'Revoked', 'edd', 'stream' ),
118
+ );
119
+ }
120
+
121
+ /**
122
+ * Return translated context labels
123
+ *
124
+ * @return array Context label translations
125
+ */
126
+ public static function get_context_labels() {
127
+ return array(
128
+ 'downloads' => _x( 'Downloads', 'edd', 'stream' ),
129
+ 'download_category' => _x( 'Categories', 'edd', 'stream' ),
130
+ 'download_tag' => _x( 'Tags', 'edd', 'stream' ),
131
+ 'discounts' => _x( 'Discounts', 'edd', 'stream' ),
132
+ 'reports' => _x( 'Reports', 'edd', 'stream' ),
133
+ 'api_keys' => _x( 'API Keys', 'edd', 'stream' ),
134
+ //'payments' => _x( 'Payments', 'edd', 'stream' ),
135
+ );
136
+ }
137
+
138
+ /**
139
+ * Add action links to Stream drop row in admin list screen
140
+ *
141
+ * @filter wp_stream_action_links_{connector}
142
+ *
143
+ * @param array $links Previous links registered
144
+ * @param object $record Stream record
145
+ *
146
+ * @return array Action links
147
+ */
148
+ public static function action_links( $links, $record ) {
149
+ if ( in_array( $record->context, array( 'downloads' ) ) ) {
150
+ $links = WP_Stream_Connector_Posts::action_links( $links, $record );
151
+ } elseif ( in_array( $record->context, array( 'discounts' ) ) ) {
152
+ $post_type_label = get_post_type_labels( get_post_type_object( 'edd_discount' ) )->singular_name;
153
+ $base = admin_url( 'edit.php?post_type=download&page=edd-discounts' );
154
+
155
+ $links[ sprintf( __( 'Edit %s', 'stream' ), $post_type_label ) ] = add_query_arg(
156
+ array(
157
+ 'edd-action' => 'edit_discount',
158
+ 'discount' => $record->object_id,
159
+ ),
160
+ $base
161
+ );
162
+
163
+ if ( 'active' === get_post( $record->object_id )->post_status ) {
164
+ $links[ sprintf( __( 'Deactivate %s', 'stream' ), $post_type_label ) ] = add_query_arg(
165
+ array(
166
+ 'edd-action' => 'deactivate_discount',
167
+ 'discount' => $record->object_id,
168
+ ),
169
+ $base
170
+ );
171
+ } else {
172
+ $links[ sprintf( __( 'Activate %s', 'stream' ), $post_type_label ) ] = add_query_arg(
173
+ array(
174
+ 'edd-action' => 'activate_discount',
175
+ 'discount' => $record->object_id,
176
+ ),
177
+ $base
178
+ );
179
+ }
180
+ } elseif ( in_array( $record->context, array( 'download_category', 'download_tag' ) ) ) {
181
+ $tax_label = get_taxonomy_labels( get_taxonomy( $record->context ) )->singular_name;
182
+ $links[ sprintf( __( 'Edit %s', 'stream' ), $tax_label ) ] = get_edit_term_link( $record->object_id, wp_stream_get_meta( $record, 'taxonomy', true ) );
183
+ } elseif ( 'api_keys' === $record->context ) {
184
+ $user = new WP_User( $record->object_id );
185
+
186
+ if ( apply_filters( 'edd_api_log_requests', true ) ) {
187
+ $links[ __( 'View API Log', 'stream' ) ] = add_query_arg( array( 'view' => 'api_requests', 'post_type' => 'download', 'page' => 'edd-reports', 'tab' => 'logs', 's' => $user->user_email ), 'edit.php' );
188
+ }
189
+
190
+ $links[ __( 'Revoke', 'stream' ) ] = add_query_arg( array( 'post_type' => 'download', 'user_id' => $record->object_id, 'edd_action' => 'process_api_key', 'edd_api_process' => 'revoke' ), 'edit.php' );
191
+ $links[ __( 'Reissue', 'stream' ) ] = add_query_arg( array( 'post_type' => 'download', 'user_id' => $record->object_id, 'edd_action' => 'process_api_key', 'edd_api_process' => 'regenerate' ), 'edit.php' );
192
+ }
193
+
194
+ return $links;
195
+ }
196
+
197
+ public static function register() {
198
+ parent::register();
199
+
200
+ add_filter( 'wp_stream_log_data', array( __CLASS__, 'log_override' ) );
201
+
202
+ self::$options = array(
203
+ 'edd_settings' => null,
204
+ );
205
+ }
206
+
207
+ public static function callback_update_option( $option, $old, $new ) {
208
+ self::check( $option, $old, $new );
209
+ }
210
+
211
+ public static function callback_add_option( $option, $val ) {
212
+ self::check( $option, null, $val );
213
+ }
214
+
215
+ public static function callback_delete_option( $option ) {
216
+ self::check( $option, null, null );
217
+ }
218
+
219
+ public static function callback_update_site_option( $option, $old, $new ) {
220
+ self::check( $option, $old, $new );
221
+ }
222
+
223
+ public static function callback_add_site_option( $option, $val ) {
224
+ self::check( $option, null, $val );
225
+ }
226
+
227
+ public static function callback_delete_site_option( $option ) {
228
+ self::check( $option, null, null );
229
+ }
230
+
231
+ public static function check( $option, $old_value, $new_value ) {
232
+ if ( ! array_key_exists( $option, self::$options ) ) {
233
+ return;
234
+ }
235
+
236
+ $replacement = str_replace( '-', '_', $option );
237
+
238
+ if ( method_exists( __CLASS__, 'check_' . $replacement ) ) {
239
+ call_user_func( array( __CLASS__, 'check_' . $replacement ), $old_value, $new_value );
240
+ } else {
241
+ $data = self::$options[ $option ];
242
+ $option_title = $data['label'];
243
+ $context = isset( $data['context'] ) ? $data['context'] : 'settings';
244
+
245
+ self::log(
246
+ __( '"%s" setting updated', 'stream' ),
247
+ compact( 'option_title', 'option', 'old_value', 'new_value' ),
248
+ null,
249
+ $context,
250
+ isset( $data['action'] ) ? $data['action'] : 'updated'
251
+ );
252
+ }
253
+ }
254
+
255
+ public static function check_edd_settings( $old_value, $new_value ) {
256
+ $options = array();
257
+
258
+ if ( ! is_array( $old_value ) || ! is_array( $new_value ) ) {
259
+ return;
260
+ }
261
+
262
+ foreach ( self::get_changed_keys( $old_value, $new_value, 0 ) as $field_key => $field_value ) {
263
+ $options[ $field_key ] = $field_value;
264
+ }
265
+
266
+ $settings = edd_get_registered_settings();
267
+
268
+ foreach ( $options as $option => $option_value ) {
269
+ $field = null;
270
+
271
+ if ( 'banned_email' === $option ) {
272
+ $field = array(
273
+ 'name' => _x( 'Banned emails', 'edd', 'stream' ),
274
+ );
275
+ $page = 'edd-tools';
276
+ $tab = 'general';
277
+ } else {
278
+ $page = 'edd-settings';
279
+
280
+ foreach ( $settings as $tab => $fields ) {
281
+ if ( isset( $fields[ $option ] ) ) {
282
+ $field = $fields[ $option ];
283
+ break;
284
+ }
285
+ }
286
+ }
287
+
288
+ if ( empty( $field ) ) {
289
+ continue;
290
+ }
291
+
292
+ self::log(
293
+ __( '"%s" setting updated', 'stream' ),
294
+ array(
295
+ 'option_title' => $field['name'],
296
+ 'option' => $option,
297
+ 'old_value' => maybe_serialize( $old_value ),
298
+ 'value' => maybe_serialize( $new_value ),
299
+ 'tab' => $tab,
300
+ ),
301
+ null,
302
+ 'settings',
303
+ 'updated'
304
+ );
305
+ }
306
+ }
307
+
308
+ /**
309
+ * Override connector log for our own Settings / Actions
310
+ *
311
+ * @param array $data
312
+ *
313
+ * @return array|bool
314
+ */
315
+ public static function log_override( $data ) {
316
+ if ( ! is_array( $data ) ) {
317
+ return $data;
318
+ }
319
+
320
+ if ( 'posts' === $data['connector'] && 'download' === $data['context'] ) {
321
+ // Download posts operations
322
+ $data['context'] = 'downloads';
323
+ $data['connector'] = self::$name;
324
+ } elseif ( 'posts' === $data['connector'] && 'edd_discount' === $data['context'] ) {
325
+ // Discount posts operations
326
+ if ( self::$is_discount_status_change ) {
327
+ return false;
328
+ }
329
+
330
+ if ( 'deleted' === $data['action'] ) {
331
+ $data['message'] = __( '"%1s" discount deleted', 'stream' );
332
+ }
333
+
334
+ $data['context'] = 'discounts';
335
+ $data['connector'] = self::$name;
336
+ } elseif ( 'posts' === $data['connector'] && 'edd_payment' === $data['context'] ) {
337
+ // Payment posts operations
338
+ return false; // Do not track payments, they're well logged!
339
+ } elseif ( 'posts' === $data['connector'] && 'edd_log' === $data['context'] ) {
340
+ // Logging operations
341
+ return false; // Do not track notes, because they're basically logs
342
+ } elseif ( 'comments' === $data['connector'] && 'edd_payment' === $data['context'] ) {
343
+ // Payment notes ( comments ) operations
344
+ return false; // Do not track notes, because they're basically logs
345
+ } elseif ( 'taxonomies' === $data['connector'] && 'download_category' === $data['context'] ) {
346
+ $data['connector'] = self::$name;
347
+ } elseif ( 'taxonomies' === $data['connector'] && 'download_tag' === $data['contexts'] ) {
348
+ $data['connector'] = self::$name;
349
+ } elseif ( 'taxonomies' === $data['connector'] && 'edd_log_type' === $data['contexts'] ) {
350
+ return false;
351
+ } elseif ( 'settings' === $data['connector'] && 'edd_settings' === $data['args']['option'] ) {
352
+ return false;
353
+ }
354
+
355
+ return $data;
356
+ }
357
+
358
+ public static function callback_edd_pre_update_discount_status( $code_id, $new_status ) {
359
+ self::$is_discount_status_change = true;
360
+
361
+ self::log(
362
+ sprintf(
363
+ __( '"%1$s" discount %2$s', 'stream' ),
364
+ get_post( $code_id )->post_title,
365
+ 'active' === $new_status ? __( 'activated', 'stream' ) : __( 'deactivated', 'stream' )
366
+ ),
367
+ array(
368
+ 'post_id' => $code_id,
369
+ 'status' => $new_status,
370
+ ),
371
+ $code_id,
372
+ 'discounts',
373
+ 'updated'
374
+ );
375
+ }
376
+
377
+ private static function callback_edd_generate_pdf() {
378
+ self::report_generated( 'pdf' );
379
+ }
380
+ public static function callback_edd_earnings_export() {
381
+ self::report_generated( 'earnings' );
382
+ }
383
+ public static function callback_edd_payment_export() {
384
+ self::report_generated( 'payments' );
385
+ }
386
+ public static function callback_edd_email_export() {
387
+ self::report_generated( 'emails' );
388
+ }
389
+ public static function callback_edd_downloads_history_export() {
390
+ self::report_generated( 'download-history' );
391
+ }
392
+
393
+ private static function report_generated( $type ) {
394
+ if ( 'pdf' === $type ) {
395
+ $label = __( 'Sales and Earnings', 'stream' );
396
+ } elseif ( 'earnings' ) {
397
+ $label = __( 'Earnings', 'stream' );
398
+ } elseif ( 'payments' ) {
399
+ $label = __( 'Payments', 'stream' );
400
+ } elseif ( 'emails' ) {
401
+ $label = __( 'Emails', 'stream' );
402
+ } elseif ( 'download-history' ) {
403
+ $label = __( 'Download History', 'stream' );
404
+ }
405
+
406
+ self::log(
407
+ sprintf(
408
+ __( 'Generated %s report', 'stream' ),
409
+ $label
410
+ ),
411
+ array(
412
+ 'type' => $type,
413
+ ),
414
+ null,
415
+ 'reports',
416
+ 'generated'
417
+ );
418
+ }
419
+
420
+ public static function callback_edd_export_settings() {
421
+ self::log(
422
+ __( 'Exported Settings', 'stream' ),
423
+ array(),
424
+ null,
425
+ 'settings',
426
+ 'exported'
427
+ );
428
+ }
429
+
430
+ public static function callback_edd_import_settings() {
431
+ self::log(
432
+ __( 'Imported Settings', 'stream' ),
433
+ array(),
434
+ null,
435
+ 'settings',
436
+ 'imported'
437
+ );
438
+ }
439
+
440
+ public static function callback_update_user_meta( $meta_id, $object_id, $meta_key, $_meta_value ) {
441
+ self::meta( $object_id, $meta_key, $_meta_value );
442
+ }
443
+
444
+ public static function callback_add_user_meta( $object_id, $meta_key, $_meta_value ) {
445
+ self::meta( $object_id, $meta_key, $_meta_value, true );
446
+ }
447
+
448
+ public static function callback_delete_user_meta( $meta_id, $object_id, $meta_key, $_meta_value ) {
449
+ self::meta( $object_id, $meta_key, null );
450
+ }
451
+
452
+ public static function meta( $object_id, $key, $value, $is_add = false ) {
453
+ if ( ! in_array( $key, self::$user_meta ) ) {
454
+ return false;
455
+ }
456
+
457
+ $key = str_replace( '-', '_', $key );
458
+
459
+ if ( method_exists( __CLASS__, 'meta_' . $key ) ) {
460
+ return call_user_func( array( __CLASS__, 'meta_' . $key ), $object_id, $value, $is_add );
461
+ }
462
+ }
463
+
464
+ private static function meta_edd_user_public_key( $user_id, $value, $is_add = false ) {
465
+ if ( is_null( $value ) ) {
466
+ $action = 'revoked';
467
+ $action_title = __( 'revoked', 'stream' );
468
+ } elseif ( $is_add ) {
469
+ $action = 'created';
470
+ $action_title = __( 'created', 'stream' );
471
+ } else {
472
+ $action = 'updated';
473
+ $action_title = __( 'updated', 'stream' );
474
+ }
475
+
476
+ self::log(
477
+ sprintf(
478
+ __( 'User API Key %s', 'stream' ),
479
+ $action_title
480
+ ),
481
+ array(
482
+ 'meta_value' => $value,
483
+ ),
484
+ $user_id,
485
+ 'api_keys',
486
+ $action
487
+ );
488
+ }
489
+
490
+ }
connectors/class-wp-stream-connector-editor.php ADDED
@@ -0,0 +1,311 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class WP_Stream_Connector_Editor extends WP_Stream_Connector {
4
+
5
+ /**
6
+ * Connector slug
7
+ *
8
+ * @var string
9
+ */
10
+ public static $name = 'editor';
11
+
12
+ /**
13
+ * Actions registered for this connector
14
+ *
15
+ * @var array
16
+ */
17
+ public static $actions = array();
18
+
19
+ /**
20
+ * Actions registered for this connector
21
+ *
22
+ * @var array
23
+ */
24
+ private static $edited_file = array();
25
+
26
+ /**
27
+ * Register all context hooks
28
+ *
29
+ * @return void
30
+ */
31
+ public static function register() {
32
+ parent::register();
33
+ add_action( 'load-theme-editor.php', array( __CLASS__, 'get_edition_data' ) );
34
+ add_action( 'load-plugin-editor.php', array( __CLASS__, 'get_edition_data' ) );
35
+ add_filter( 'wp_redirect', array( __CLASS__, 'log_changes' ) );
36
+ }
37
+
38
+ /**
39
+ * Return translated connector label
40
+ *
41
+ * @return string Translated connector label
42
+ */
43
+ public static function get_label() {
44
+ return __( 'Editor', 'stream' );
45
+ }
46
+
47
+ /**
48
+ * Return translated action labels
49
+ *
50
+ * @return array Action label translations
51
+ */
52
+ public static function get_action_labels() {
53
+ return array(
54
+ 'updated' => __( 'Updated', 'stream' ),
55
+ );
56
+ }
57
+
58
+ /**
59
+ * Return translated context labels
60
+ *
61
+ * @return array Context label translations
62
+ */
63
+ public static function get_context_labels() {
64
+ /**
65
+ * Filter available context labels for the Editor connector
66
+ *
67
+ * @return array Array of context slugs and their translated labels
68
+ */
69
+ return apply_filters(
70
+ 'wp_stream_editor_context_labels',
71
+ array(
72
+ 'themes' => __( 'Themes', 'stream' ),
73
+ 'plugins' => __( 'Plugins', 'stream' ),
74
+ )
75
+ );
76
+ }
77
+
78
+ /**
79
+ * Get the context based on wp_redirect location
80
+ *
81
+ * @param string $location The URL of the redirect
82
+ * @return string Context slug
83
+ */
84
+ public static function get_context( $location ) {
85
+ $context = null;
86
+
87
+ if ( false !== strpos( $location, 'theme-editor.php' ) ) {
88
+ $context = 'themes';
89
+ }
90
+
91
+ if ( false !== strpos( $location, 'plugin-editor.php' ) ) {
92
+ $context = 'plugins';
93
+ }
94
+
95
+ /**
96
+ * Filter available contexts for the Editor connector
97
+ *
98
+ * @param string $context Context slug
99
+ * @param string $location The URL of the redirect
100
+ * @return string Context slug
101
+ */
102
+ return apply_filters( 'wp_stream_editor_context', $context, $location );
103
+ }
104
+
105
+ /**
106
+ * Get the message format for file updates
107
+ *
108
+ * @return string Translated string
109
+ */
110
+ public static function get_message() {
111
+ return _x(
112
+ '"%1$s" in "%2$s" updated',
113
+ '1: File name, 2: Theme/plugin name',
114
+ 'stream'
115
+ );
116
+ }
117
+
118
+ /**
119
+ * Add action links to Stream drop row in admin list screen
120
+ *
121
+ * @filter wp_stream_action_links_{connector}
122
+ *
123
+ * @param array $links Previous links registered
124
+ * @param object $record Stream record
125
+ *
126
+ * @return array Action links
127
+ */
128
+ public static function action_links( $links, $record ) {
129
+ if ( current_user_can( 'edit_theme_options' ) ) {
130
+ $file_name = wp_stream_get_meta( $record, 'file', true );
131
+ $file_path = wp_stream_get_meta( $record, 'file_path', true );
132
+
133
+ if ( ! empty( $file_name ) && ! empty( $file_path ) ) {
134
+ $theme_slug = wp_stream_get_meta( $record, 'theme_slug', true );
135
+ $plugin_slug = wp_stream_get_meta( $record, 'plugin_slug', true );
136
+ $theme_exists = ( ! empty( $theme_slug ) && file_exists( $file_path ) );
137
+ $plugin_exists = ( ! empty( $plugin_slug ) && file_exists( $file_path ) );
138
+
139
+ if ( $theme_exists ) {
140
+ $links[ __( 'Edit File', 'stream' ) ] = add_query_arg(
141
+ array(
142
+ 'theme' => urlencode( $theme_slug ),
143
+ 'file' => urlencode( $file_name ),
144
+ ),
145
+ self_admin_url( 'theme-editor.php' )
146
+ );
147
+
148
+ $links[ __( 'Theme Details', 'stream' ) ] = add_query_arg(
149
+ array(
150
+ 'theme' => urlencode( $theme_slug ),
151
+ ),
152
+ self_admin_url( 'themes.php' )
153
+ );
154
+ }
155
+
156
+ if ( $plugin_exists ) {
157
+ $links[ __( 'Edit File', 'stream' ) ] = add_query_arg(
158
+ array(
159
+ 'plugin' => urlencode( $plugin_slug ),
160
+ 'file' => urlencode( str_ireplace( trailingslashit( WP_PLUGIN_DIR ), '', $file_path ) ),
161
+ ),
162
+ self_admin_url( 'plugin-editor.php' )
163
+ );
164
+ }
165
+ }
166
+ }
167
+
168
+ return $links;
169
+ }
170
+
171
+ /**
172
+ * Retrieves data submitted on the screen, and prepares it for the appropriate context type
173
+ *
174
+ * @action load-theme-editor.php
175
+ * @action load-plugin-editor.php
176
+ * @return void
177
+ */
178
+ public static function get_edition_data() {
179
+ if ( 'POST' !== $_SERVER['REQUEST_METHOD'] || 'update' !== wp_stream_filter_input( INPUT_POST, 'action' ) ) {
180
+ return;
181
+ }
182
+
183
+ if ( $slug = wp_stream_filter_input( INPUT_POST, 'theme' ) ) {
184
+ self::$edited_file = self::get_theme_data( $slug );
185
+ }
186
+
187
+ if ( $slug = wp_stream_filter_input( INPUT_POST, 'plugin' ) ) {
188
+ self::$edited_file = self::get_plugin_data( $slug );
189
+ }
190
+ }
191
+
192
+ /**
193
+ * Retrieve theme data needed for the log message
194
+ *
195
+ * @param string $slug The theme slug (e.g. twentyfourteen)
196
+ * @return mixed $output Compacted variables
197
+ */
198
+ public static function get_theme_data( $slug ) {
199
+ $theme = wp_get_theme( $slug );
200
+
201
+ if ( ! $theme->exists() || ( $theme->errors() && 'theme_no_stylesheet' === $theme->errors()->get_error_code() ) ) {
202
+ return;
203
+ }
204
+
205
+ $allowed_files = $theme->get_files( 'php', 1 );
206
+ $style_files = $theme->get_files( 'css' );
207
+ $allowed_files['style.css'] = $style_files['style.css'];
208
+ $file = wp_stream_filter_input( INPUT_POST, 'file' );
209
+
210
+ if ( empty( $file ) ) {
211
+ $file_name = 'style.css';
212
+ $file_path = $allowed_files['style.css'];
213
+ } else {
214
+ $file_name = $file;
215
+ $file_path = sprintf( '%s/%s', $theme->get_stylesheet_directory(), $file_name );
216
+ }
217
+
218
+ $file_contents_before = file_get_contents( $file_path );
219
+
220
+ $name = $theme->get( 'Name' );
221
+
222
+ $output = compact(
223
+ 'file_name',
224
+ 'file_path',
225
+ 'file_contents_before',
226
+ 'slug',
227
+ 'name'
228
+ );
229
+
230
+ return $output;
231
+ }
232
+
233
+ /**
234
+ * Retrieve plugin data needed for the log message
235
+ *
236
+ * @param string $slug The plugin file base name (e.g. akismet/akismet.php)
237
+ * @return mixed $output Compacted variables
238
+ */
239
+ public static function get_plugin_data( $slug ) {
240
+ $base = null;
241
+ $name = null;
242
+ $slug = current( explode( '/', $slug ) );
243
+ $file_name = wp_stream_filter_input( INPUT_POST, 'file' );
244
+ $file_path = WP_PLUGIN_DIR . '/' . $file_name;
245
+ $file_contents_before = file_get_contents( $file_path );
246
+
247
+ $plugins = get_plugins();
248
+
249
+ foreach ( $plugins as $key => $plugin_data ) {
250
+ if ( 0 === strpos( $key, $slug ) ) {
251
+ $base = $key;
252
+ $name = $plugin_data['Name'];
253
+ break;
254
+ }
255
+ }
256
+
257
+ $file_name = str_ireplace( trailingslashit( $slug ), '', $file_name );
258
+ $slug = ! empty( $base ) ? $base : $slug;
259
+
260
+ $output = compact(
261
+ 'file_name',
262
+ 'file_path',
263
+ 'file_contents_before',
264
+ 'slug',
265
+ 'name'
266
+ );
267
+
268
+ return $output;
269
+ }
270
+
271
+ /**
272
+ * @filter wp_redirect
273
+ */
274
+ public static function log_changes( $location ) {
275
+ if ( ! empty( self::$edited_file ) ) {
276
+ if ( file_get_contents( self::$edited_file['file_path'] ) !== self::$edited_file['file_contents_before'] ) {
277
+ $context = self::get_context( $location );
278
+
279
+ switch ( $context ) {
280
+ case 'themes':
281
+ $name_key = 'theme_name';
282
+ $slug_key = 'theme_slug';
283
+ break;
284
+ case 'plugins':
285
+ $name_key = 'plugin_name';
286
+ $slug_key = 'plugin_slug';
287
+ break;
288
+ default:
289
+ $name_key = 'name';
290
+ $slug_key = 'slug';
291
+ }
292
+
293
+ self::log(
294
+ self::get_message(),
295
+ array(
296
+ 'file' => (string) self::$edited_file['file_name'],
297
+ $name_key => (string) self::$edited_file['name'],
298
+ $slug_key => (string) self::$edited_file['slug'],
299
+ 'file_path' => (string) self::$edited_file['file_path'],
300
+ ),
301
+ null,
302
+ $context,
303
+ 'updated'
304
+ );
305
+ }
306
+ }
307
+
308
+ return $location;
309
+ }
310
+
311
+ }
connectors/class-wp-stream-connector-gravityforms.php ADDED
@@ -0,0 +1,729 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class WP_Stream_Connector_GravityForms extends WP_Stream_Connector {
4
+
5
+ /**
6
+ * Connector slug
7
+ *
8
+ * @var string
9
+ */
10
+ public static $name = 'gravityforms';
11
+
12
+ /**
13
+ * Holds tracked plugin minimum version required
14
+ *
15
+ * @const string
16
+ */
17
+ const PLUGIN_MIN_VERSION = '1.8.8';
18
+
19
+ /**
20
+ * Actions registered for this connector
21
+ *
22
+ * @var array
23
+ */
24
+ public static $actions = array(
25
+ 'gform_after_save_form',
26
+ 'gform_pre_confirmation_save',
27
+ 'gform_pre_notification_save',
28
+ 'gform_notification_delete',
29
+ 'gform_confirmation_delete',
30
+ 'gform_notification_status',
31
+ 'gform_confirmation_status',
32
+ 'gform_form_status_change',
33
+ 'gform_form_reset_views',
34
+ 'gform_before_delete_form',
35
+ 'gform_form_trash',
36
+ 'gform_form_restore',
37
+ 'gform_form_duplicate',
38
+ 'gform_export_separator', // Export entries
39
+ 'gform_export_options', // Export forms
40
+ 'gform_import_form_xml_options', // Import
41
+ 'gform_delete_lead',
42
+ 'gform_insert_note',
43
+ 'gform_delete_note',
44
+ 'gform_update_status',
45
+ 'gform_update_is_read',
46
+ 'gform_update_is_starred',
47
+ 'update_option',
48
+ 'add_option',
49
+ 'delete_option',
50
+ 'update_site_option',
51
+ 'add_site_option',
52
+ 'delete_site_option',
53
+ );
54
+
55
+ /**
56
+ * Tracked option keys
57
+ *
58
+ * @var array
59
+ */
60
+ public static $options = array();
61
+
62
+ /**
63
+ * Tracking registered Settings, with overridden data
64
+ *
65
+ * @var array
66
+ */
67
+ public static $options_override = array();
68
+
69
+ /**
70
+ * Check if plugin dependencies are satisfied and add an admin notice if not
71
+ *
72
+ * @return bool
73
+ */
74
+ public static function is_dependency_satisfied() {
75
+ if ( class_exists( 'GFForms' ) && version_compare( GFCommon::$version, self::PLUGIN_MIN_VERSION, '>=' ) ) {
76
+ return true;
77
+ }
78
+
79
+ return false;
80
+ }
81
+
82
+ /**
83
+ * Return translated connector label
84
+ *
85
+ * @return string Translated connector label
86
+ */
87
+ public static function get_label() {
88
+ return _x( 'Gravity Forms', 'gravityforms', 'stream' );
89
+ }
90
+
91
+ /**
92
+ * Return translated action labels
93
+ *
94
+ * @return array Action label translations
95
+ */
96
+ public static function get_action_labels() {
97
+ return array(
98
+ 'created' => _x( 'Created', 'gravityforms', 'stream' ),
99
+ 'updated' => _x( 'Updated', 'gravityforms', 'stream' ),
100
+ 'exported' => _x( 'Exported', 'gravityforms', 'stream' ),
101
+ 'imported' => _x( 'Imported', 'gravityforms', 'stream' ),
102
+ 'added' => _x( 'Added', 'gravityforms', 'stream' ),
103
+ 'deleted' => _x( 'Deleted', 'gravityforms', 'stream' ),
104
+ 'trashed' => _x( 'Trashed', 'gravityforms', 'stream' ),
105
+ 'untrashed' => _x( 'Restored', 'gravityforms', 'stream' ),
106
+ 'duplicated' => _x( 'Duplicated', 'gravityforms', 'stream' ),
107
+ );
108
+ }
109
+
110
+ /**
111
+ * Return translated context labels
112
+ *
113
+ * @return array Context label translations
114
+ */
115
+ public static function get_context_labels() {
116
+ return array(
117
+ 'forms' => _x( 'Forms', 'gravityforms', 'stream' ),
118
+ 'settings' => _x( 'Settings', 'gravityforms', 'stream' ),
119
+ 'export' => _x( 'Import/Export', 'gravityforms', 'stream' ),
120
+ 'entries' => _x( 'Entries', 'gravityforms', 'stream' ),
121
+ 'notes' => _x( 'Notes', 'gravityforms', 'stream' ),
122
+ );
123
+ }
124
+
125
+ /**
126
+ * Add action links to Stream drop row in admin list screen
127
+ *
128
+ * @filter wp_stream_action_links_{connector}
129
+ *
130
+ * @param array $links Previous links registered
131
+ * @param object $record Stream record
132
+ *
133
+ * @return array Action links
134
+ */
135
+ public static function action_links( $links, $record ) {
136
+ if ( 'forms' === $record->context ) {
137
+ $links[ __( 'Edit', 'stream' ) ] = add_query_arg(
138
+ array(
139
+ 'page' => 'gf_edit_forms',
140
+ 'id' => $record->object_id,
141
+ ),
142
+ admin_url( 'admin.php' )
143
+ );
144
+ } elseif ( 'entries' === $record->context ) {
145
+ $links[ __( 'View', 'stream' ) ] = add_query_arg(
146
+ array(
147
+ 'page' => 'gf_entries',
148
+ 'view' => 'entry',
149
+ 'lid' => $record->object_id,
150
+ 'id' => wp_stream_get_meta( $record, 'form_id', true ),
151
+ ),
152
+ admin_url( 'admin.php' )
153
+ );
154
+ } elseif ( 'notes' === $record->context ) {
155
+ $links[ __( 'View', 'stream' ) ] = add_query_arg(
156
+ array(
157
+ 'page' => 'gf_entries',
158
+ 'view' => 'entry',
159
+ 'lid' => wp_stream_get_meta( $record, 'lead_id', true ),
160
+ 'id' => wp_stream_get_meta( $record, 'form_id', true ),
161
+ ),
162
+ admin_url( 'admin.php' )
163
+ );
164
+ } elseif ( 'settings' === $record->context ) {
165
+ $links[ __( 'Edit Settings', 'stream' ) ] = add_query_arg(
166
+ array(
167
+ 'page' => 'gf_settings',
168
+ ),
169
+ admin_url( 'admin.php' )
170
+ );
171
+ }
172
+
173
+ return $links;
174
+ }
175
+
176
+ public static function register() {
177
+ parent::register();
178
+
179
+ self::$options = array(
180
+ 'rg_gforms_disable_css' => array(
181
+ 'label' => _x( 'Output CSS', 'gravityforms', 'stream' ),
182
+ ),
183
+ 'rg_gforms_enable_html5' => array(
184
+ 'label' => _x( 'Output HTML5', 'gravityforms', 'stream' ),
185
+ ),
186
+ 'gform_enable_noconflict' => array(
187
+ 'label' => _x( 'No-Conflict Mode', 'gravityforms', 'stream' ),
188
+ ),
189
+ 'rg_gforms_currency' => array(
190
+ 'label' => _x( 'Currency', 'gravityforms', 'stream' ),
191
+ ),
192
+ 'rg_gforms_captcha_public_key' => array(
193
+ 'label' => _x( 'reCAPTCHA Public Key', 'gravityforms', 'stream' ),
194
+ ),
195
+ 'rg_gforms_captcha_private_key' => array(
196
+ 'label' => _x( 'reCAPTCHA Private Key', 'gravityforms', 'stream' ),
197
+ ),
198
+ 'rg_gforms_key' => null,
199
+ );
200
+ }
201
+
202
+ /**
203
+ * Track Create/Update actions on Forms
204
+ *
205
+ * @param $form
206
+ * @param $is_new
207
+ */
208
+ public static function callback_gform_after_save_form( $form, $is_new ) {
209
+ $title = $form['title'];
210
+ $id = $form['id'];
211
+
212
+ self::log(
213
+ sprintf(
214
+ __( '"%1$s" form %2$s', 'stream' ),
215
+ $title,
216
+ $is_new ? __( 'created', 'stream' ) : __( 'updated', 'stream' )
217
+ ),
218
+ array(
219
+ 'action' => $is_new,
220
+ 'id' => $id,
221
+ 'title' => $title,
222
+ ),
223
+ $id,
224
+ 'forms',
225
+ $is_new ? 'created' : 'updated'
226
+ );
227
+ }
228
+
229
+ /**
230
+ * Track saving form confirmations
231
+ *
232
+ * @param $confirmation
233
+ * @param $form
234
+ * @param bool $is_new
235
+ *
236
+ * @return mixed
237
+ */
238
+ public static function callback_gform_pre_confirmation_save( $confirmation, $form, $is_new = true ) {
239
+ if ( ! isset( $is_new ) ) {
240
+ $is_new = false;
241
+ }
242
+
243
+ self::log(
244
+ sprintf(
245
+ __( '"%1$s" confirmation %2$s for "%3$s"', 'stream' ),
246
+ $confirmation['name'],
247
+ $is_new ? __( 'created', 'stream' ) : __( 'updated', 'stream' ),
248
+ $form['title']
249
+ ),
250
+ array(
251
+ 'is_new' => $is_new,
252
+ 'form_id' => $form['id'],
253
+ ),
254
+ $form['id'],
255
+ 'forms',
256
+ 'updated'
257
+ );
258
+
259
+ return $confirmation;
260
+ }
261
+
262
+ /**
263
+ * Track saving form notifications
264
+ *
265
+ * @param $notification
266
+ * @param $form
267
+ * @param bool $is_new
268
+ *
269
+ * @return mixed
270
+ */
271
+ public static function callback_gform_pre_notification_save( $notification, $form, $is_new = true ) {
272
+ if ( ! isset( $is_new ) ) {
273
+ $is_new = false;
274
+ }
275
+
276
+ self::log(
277
+ sprintf(
278
+ __( '"%1$s" notification %2$s for "%3$s"', 'stream' ),
279
+ $notification['name'],
280
+ $is_new ? __( 'created', 'stream' ) : __( 'updated', 'stream' ),
281
+ $form['title']
282
+ ),
283
+ array(
284
+ 'is_update' => $is_new,
285
+ 'form_id' => $form['id'],
286
+ ),
287
+ $form['id'],
288
+ 'forms',
289
+ 'updated'
290
+ );
291
+
292
+ return $notification;
293
+ }
294
+
295
+ /**
296
+ * Track deletion of notifications
297
+ *
298
+ * @param $notification
299
+ * @param $form
300
+ */
301
+ public static function callback_gform_notification_delete( $notification, $form ) {
302
+ self::log(
303
+ sprintf(
304
+ __( '"%1$s" notification deleted from "%2$s"', 'stream' ),
305
+ $notification['name'],
306
+ $form['title']
307
+ ),
308
+ array(
309
+ 'form_id' => $form['id'],
310
+ 'notification' => $notification,
311
+ ),
312
+ $form['id'],
313
+ 'forms',
314
+ 'updated'
315
+ );
316
+ }
317
+
318
+ /**
319
+ * Track deletion of confirmations
320
+ *
321
+ * @param $confirmation
322
+ * @param $form
323
+ */
324
+ public static function callback_gform_confirmation_delete( $confirmation, $form ) {
325
+ self::log(
326
+ sprintf(
327
+ __( '"%1$s" confirmation deleted from "%2$s"', 'stream' ),
328
+ $confirmation['name'],
329
+ $form['title']
330
+ ),
331
+ array(
332
+ 'form_id' => $form['id'],
333
+ 'confirmation' => $confirmation,
334
+ ),
335
+ $form['id'],
336
+ 'forms',
337
+ 'updated'
338
+ );
339
+ }
340
+
341
+ /**
342
+ * Track status change of confirmations
343
+ *
344
+ * @param $confirmation
345
+ * @param $form
346
+ * @param $is_active
347
+ */
348
+ public static function callback_gform_confirmation_status( $confirmation, $form, $is_active ) {
349
+ self::log(
350
+ sprintf(
351
+ __( '"%1$s" confirmation %2$s from "%3$s"', 'stream' ),
352
+ $confirmation['name'],
353
+ $is_active ? __( 'activated', 'stream' ) : __( 'deactivated', 'stream' ),
354
+ $form['title']
355
+ ),
356
+ array(
357
+ 'form_id' => $form['id'],
358
+ 'confirmation' => $confirmation,
359
+ 'is_active' => $is_active,
360
+ ),
361
+ null,
362
+ 'forms',
363
+ 'updated'
364
+ );
365
+ }
366
+
367
+ /**
368
+ * Track status change of confirmations
369
+ *
370
+ * @param $id
371
+ */
372
+ public static function callback_gform_form_reset_views( $id ) {
373
+ $form = self::get_form( $id );
374
+
375
+ self::log(
376
+ __( '"%s" form views reset', 'stream' ),
377
+ array(
378
+ 'title' => $form['title'],
379
+ 'form_id' => $form['id'],
380
+ ),
381
+ $form['id'],
382
+ 'forms',
383
+ 'updated'
384
+ );
385
+ }
386
+
387
+ /**
388
+ * Track status change of notifications
389
+ *
390
+ * @param $notification
391
+ * @param $form
392
+ * @param $is_active
393
+ */
394
+ public static function callback_gform_notification_status( $notification, $form, $is_active ) {
395
+ self::log(
396
+ sprintf(
397
+ __( '"%1$s" notification %2$s from "%3$s"', 'stream' ),
398
+ $notification['name'],
399
+ $is_active ? __( 'activated', 'stream' ) : __( 'deactivated', 'stream' ),
400
+ $form['title']
401
+ ),
402
+ array(
403
+ 'form_id' => $form['id'],
404
+ 'notification' => $notification,
405
+ 'is_active' => $is_active,
406
+ ),
407
+ $form['id'],
408
+ 'forms',
409
+ 'updated'
410
+ );
411
+ }
412
+
413
+ /**
414
+ * Track status change of forms
415
+ *
416
+ * @param $id
417
+ * @param $action
418
+ */
419
+ public static function callback_gform_form_status_change( $id, $action ) {
420
+ $form = self::get_form( $id );
421
+ $actions = array(
422
+ 'activated' => __( 'Activated', 'stream' ),
423
+ 'deactivated' => __( 'Deactivated', 'stream' ),
424
+ 'trashed' => __( 'Trashed', 'stream' ),
425
+ 'untrashed' => __( 'Restored', 'stream' ),
426
+ );
427
+
428
+ self::log(
429
+ sprintf(
430
+ __( '"%1$s" form %2$s', 'stream' ),
431
+ $form['title'],
432
+ $actions[ $action ]
433
+ ),
434
+ array(
435
+ 'form_title' => $form['title'],
436
+ 'form_id' => $id,
437
+ ),
438
+ $form['id'],
439
+ 'forms',
440
+ $action
441
+ );
442
+ }
443
+
444
+ public static function callback_update_option( $option, $old, $new ) {
445
+ self::check( $option, $old, $new );
446
+ }
447
+
448
+ public static function callback_add_option( $option, $val ) {
449
+ self::check( $option, null, $val );
450
+ }
451
+
452
+ public static function callback_delete_option( $option ) {
453
+ self::check( $option, null, null );
454
+ }
455
+
456
+ public static function callback_update_site_option( $option, $old, $new ) {
457
+ self::check( $option, $old, $new );
458
+ }
459
+
460
+ public static function callback_add_site_option( $option, $val ) {
461
+ self::check( $option, null, $val );
462
+ }
463
+
464
+ public static function callback_delete_site_option( $option ) {
465
+ self::check( $option, null, null );
466
+ }
467
+
468
+ public static function check( $option, $old_value, $new_value ) {
469
+ if ( ! array_key_exists( $option, self::$options ) ) {
470
+ return;
471
+ }
472
+
473
+ if ( is_null( self::$options[ $option ] ) ) {
474
+ call_user_func( array( __CLASS__, 'check_' . str_replace( '-', '_', $option ) ), $old_value, $new_value );
475
+ } else {
476
+ $data = self::$options[ $option ];
477
+ $option_title = $data['label'];
478
+ $context = isset( $data['context'] ) ? $data['context'] : 'settings';
479
+
480
+ self::log(
481
+ __( '"%s" setting updated', 'stream' ),
482
+ compact( 'option_title', 'option', 'old_value', 'new_value' ),
483
+ null,
484
+ $context,
485
+ isset( $data['action'] ) ? $data['action'] : 'updated'
486
+ );
487
+ }
488
+ }
489
+
490
+ public static function check_rg_gforms_key( $old_value, $new_value ) {
491
+ $is_update = ( $new_value && strlen( $new_value ) );
492
+ $option = 'rg_gforms_key';
493
+
494
+ self::log(
495
+ sprintf(
496
+ __( 'Gravity Forms license key %s', 'stream' ),
497
+ $is_update ? __( 'updated', 'stream' ) : __( 'deleted', 'stream' )
498
+ ),
499
+ compact( 'option', 'old_value', 'new_value' ),
500
+ null,
501
+ 'settings',
502
+ $is_update ? 'updated' : 'deleted'
503
+ );
504
+ }
505
+
506
+ public static function callback_gform_export_separator( $dummy, $form_id ) {
507
+ $form = self::get_form( $form_id );
508
+
509
+ self::log(
510
+ __( '"%s" form exported', 'stream' ),
511
+ array(
512
+ 'form_title' => $form['title'],
513
+ 'form_id' => $form_id,
514
+ ),
515
+ $form_id,
516
+ 'export',
517
+ 'exported'
518
+ );
519
+
520
+ return $dummy;
521
+ }
522
+
523
+ public static function callback_gform_import_form_xml_options( $dummy ) {
524
+ self::log(
525
+ __( 'Import process started', 'stream' ),
526
+ array(),
527
+ null,
528
+ 'export',
529
+ 'imported'
530
+ );
531
+
532
+ return $dummy;
533
+ }
534
+
535
+ public static function callback_gform_export_options( $dummy, $forms ) {
536
+ $ids = wp_list_pluck( $forms, 'id' );
537
+ $titles = wp_list_pluck( $forms, 'title' );
538
+
539
+ self::log(
540
+ __( 'Export process started for %d forms', 'stream' ),
541
+ array(
542
+ 'count' => count( $forms ),
543
+ 'ids' => $ids,
544
+ 'titles' => $titles,
545
+ ),
546
+ null,
547
+ 'export',
548
+ 'imported'
549
+ );
550
+
551
+ return $dummy;
552
+ }
553
+
554
+ public static function callback_gform_before_delete_form( $id ) {
555
+ $form = self::get_form( $id );
556
+
557
+ self::log(
558
+ __( '"%s" form deleted', 'stream' ),
559
+ array(
560
+ 'form_title' => $form['title'],
561
+ 'form_id' => $id,
562
+ ),
563
+ $form['id'],
564
+ 'forms',
565
+ 'deleted'
566
+ );
567
+ }
568
+
569
+ public static function callback_gform_form_duplicate( $id, $new_id ) {
570
+ $form = self::get_form( $id );
571
+ $new = self::get_form( $new_id );
572
+
573
+ self::log(
574
+ __( '"%1$s" form created as duplicate from "%2$s"', 'stream' ),
575
+ array(
576
+ 'new_form_title' => $new['title'],
577
+ 'form_title' => $form['title'],
578
+ 'form_id' => $id,
579
+ 'new_id' => $new_id,
580
+ ),
581
+ $new_id,
582
+ 'forms',
583
+ 'duplicated'
584
+ );
585
+ }
586
+
587
+ public static function callback_gform_delete_lead( $lead_id ) {
588
+ $lead = GFFormsModel::get_lead( $lead_id );
589
+ $form = self::get_form( $lead['form_id'] );
590
+
591
+ self::log(
592
+ __( 'Lead #%1$d from "%2$s" deleted', 'stream' ),
593
+ array(
594
+ 'lead_id' => $lead_id,
595
+ 'form_title' => $form['title'],
596
+ 'form_id' => $form['id'],
597
+ ),
598
+ $lead_id,
599
+ 'entries',
600
+ 'deleted'
601
+ );
602
+ }
603
+
604
+ public static function callback_gform_insert_note( $note_id, $lead_id, $user_id, $user_name, $note, $note_type ) {
605
+ $lead = GFFormsModel::get_lead( $lead_id );
606
+ $form = self::get_form( $lead['form_id'] );
607
+
608
+ self::log(
609
+ __( 'Note #%1$d added to lead #%2$d on "%3$s" form', 'stream' ),
610
+ array(
611
+ 'note_id' => $note_id,
612
+ 'lead_id' => $lead_id,
613
+ 'form_title' => $form['title'],
614
+ 'form_id' => $form['id'],
615
+ ),
616
+ $note_id,
617
+ 'notes',
618
+ 'added'
619
+ );
620
+ }
621
+
622
+ public static function callback_gform_delete_note( $note_id, $lead_id ) {
623
+ $lead = GFFormsModel::get_lead( $lead_id );
624
+ $form = self::get_form( $lead['form_id'] );
625
+
626
+ self::log(
627
+ __( 'Note #%1$d deleted from lead #%2$d on "%3$s" form', 'stream' ),
628
+ array(
629
+ 'note_id' => $note_id,
630
+ 'lead_id' => $lead_id,
631
+ 'form_title' => $form['title'],
632
+ 'form_id' => $form['id'],
633
+ ),
634
+ $note_id,
635
+ 'notes',
636
+ 'deleted'
637
+ );
638
+ }
639
+
640
+ public static function callback_gform_update_status( $lead_id, $status, $prev = '' ) {
641
+ $lead = GFFormsModel::get_lead( $lead_id );
642
+ $form = self::get_form( $lead['form_id'] );
643
+
644
+ if ( 'active' === $status && 'trash' === $prev ) {
645
+ $status = 'restore';
646
+ }
647
+
648
+ $actions = array(
649
+ 'active' => __( 'activated', 'stream' ),
650
+ 'spam' => __( 'marked as spam', 'stream' ),
651
+ 'trash' => __( 'trashed', 'stream' ),
652
+ 'restore' => __( 'restored', 'stream' ),
653
+ );
654
+
655
+ if ( ! isset( $actions[ $status ] ) ) {
656
+ return;
657
+ }
658
+
659
+ self::log(
660
+ sprintf(
661
+ __( 'Lead #%1$d %2$s on "%3$s" form', 'stream' ),
662
+ $lead_id,
663
+ $actions[ $status ],
664
+ $form['title']
665
+ ),
666
+ array(
667
+ 'lead_id' => $lead_id,
668
+ 'form_title' => $form['title'],
669
+ 'form_id' => $form['id'],
670
+ 'status' => $status,
671
+ 'prev' => $prev,
672
+ ),
673
+ $lead_id,
674
+ 'entries',
675
+ $status
676
+ );
677
+ }
678
+
679
+ public static function callback_gform_update_is_read( $lead_id, $status ) {
680
+ $lead = GFFormsModel::get_lead( $lead_id );
681
+ $form = self::get_form( $lead['form_id'] );
682
+
683
+ self::log(
684
+ sprintf(
685
+ __( 'Lead #%1$d marked as %2$s on "%3$s" form', 'stream' ),
686
+ $lead_id,
687
+ $status ? __( 'read', 'stream' ) : __( 'unread', 'stream' ),
688
+ $form['title']
689
+ ),
690
+ array(
691
+ 'lead_id' => $lead_id,
692
+ 'form_title' => $form['title'],
693
+ 'form_id' => $form['id'],
694
+ 'status' => $status,
695
+ ),
696
+ $lead_id,
697
+ 'entries',
698
+ 'updated'
699
+ );
700
+ }
701
+
702
+ public static function callback_gform_update_is_starred( $lead_id, $status ) {
703
+ $lead = GFFormsModel::get_lead( $lead_id );
704
+ $form = self::get_form( $lead['form_id'] );
705
+
706
+ self::log(
707
+ sprintf(
708
+ __( 'Lead #%1$d %2$s on "%3$s" form', 'stream' ),
709
+ $lead_id,
710
+ $status ? __( 'starred', 'stream' ) : __( 'unstarred', 'stream' ),
711
+ $form['title']
712
+ ),
713
+ array(
714
+ 'lead_id' => $lead_id,
715
+ 'form_title' => $form['title'],
716
+ 'form_id' => $form['id'],
717
+ 'status' => $status,
718
+ ),
719
+ $lead_id,
720
+ 'entries',
721
+ 'updated'
722
+ );
723
+ }
724
+
725
+ private static function get_form( $form_id ) {
726
+ return reset( GFFormsModel::get_forms_by_id( $form_id ) );
727
+ }
728
+
729
+ }
connectors/class-wp-stream-connector-installer.php ADDED
@@ -0,0 +1,374 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class WP_Stream_Connector_Installer extends WP_Stream_Connector {
4
+
5
+ /**
6
+ * Connector slug
7
+ *
8
+ * @var string
9
+ */
10
+ public static $name = 'installer';
11
+
12
+ /**
13
+ * Actions registered for this connector
14
+ *
15
+ * @var array
16
+ */
17
+ public static $actions = array(
18
+ 'upgrader_process_complete', // plugins::installed | themes::installed
19
+ 'activate_plugin', // plugins::activated
20
+ 'deactivate_plugin', // plugins::deactivated
21
+ 'switch_theme', // themes::activated
22
+ 'delete_site_transient_update_themes', // themes::deleted
23
+ 'pre_option_uninstall_plugins', // plugins::deleted
24
+ 'pre_set_site_transient_update_plugins',
25
+ '_core_updated_successfully',
26
+ );
27
+
28
+ /**
29
+ * Return translated connector label
30
+ *
31
+ * @return string Translated connector label
32
+ */
33
+ public static function get_label() {
34
+ return __( 'Installer', 'stream' );
35
+ }
36
+
37
+ /**
38
+ * Return translated action labels
39
+ *
40
+ * @return array Action label translations
41
+ */
42
+ public static function get_action_labels() {
43
+ return array(
44
+ 'installed' => __( 'Installed', 'stream' ),
45
+ 'activated' => __( 'Activated', 'stream' ),
46
+ 'deactivated' => __( 'Deactivated', 'stream' ),
47
+ 'deleted' => __( 'Deleted', 'stream' ),
48
+ 'updated' => __( 'Updated', 'stream' ),
49
+ );
50
+ }
51
+
52
+ /**
53
+ * Return translated context labels
54
+ *
55
+ * @return array Context label translations
56
+ */
57
+ public static function get_context_labels() {
58
+ return array(
59
+ 'plugins' => __( 'Plugins', 'stream' ),
60
+ 'themes' => __( 'Themes', 'stream' ),
61
+ 'wordpress' => __( 'WordPress', 'stream' ),
62
+ );
63
+ }
64
+
65
+ /**
66
+ * Add action links to Stream drop row in admin list screen
67
+ *
68
+ * @filter wp_stream_action_links_{connector}
69
+ *
70
+ * @param array $links Previous links registered
71
+ * @param object $record Stream record
72
+ *
73
+ * @return array Action links
74
+ */
75
+ public static function action_links( $links, $record ) {
76
+ if ( 'wordpress' === $record->context && 'updated' === $record->action ) {
77
+ global $wp_version;
78
+ $version = wp_stream_get_meta( $record, 'new_version', true );
79
+ if ( $version === $wp_version ) {
80
+ $links[ __( 'About', 'stream' ) ] = admin_url( 'about.php?updated' );
81
+ }
82
+ $links[ __( 'View Release Notes', 'stream' ) ] = esc_url( sprintf( 'http://codex.wordpress.org/Version_%s', $version ) );
83
+ }
84
+ return $links;
85
+ }
86
+
87
+ /**
88
+ * Wrapper method for calling get_plugins()
89
+ *
90
+ * @return array
91
+ */
92
+ public static function get_plugins() {
93
+ if ( ! function_exists( 'get_plugins' ) ) {
94
+ require_once ABSPATH . 'wp-admin/includes/plugin.php';
95
+ }
96
+
97
+ return get_plugins();
98
+ }
99
+
100
+ /**
101
+ * Log plugin installations
102
+ *
103
+ * @action transition_post_status
104
+ */
105
+ public static function callback_upgrader_process_complete( $upgrader, $extra ) {
106
+ $logs = array();
107
+ $success = ! is_wp_error( $upgrader->skin->result );
108
+ $error = null;
109
+
110
+ if ( ! $success ) {
111
+ $errors = $upgrader->skin->result->errors;
112
+ list( $error ) = reset( $errors );
113
+ }
114
+
115
+ // This would have failed down the road anyway
116
+ if ( ! isset( $extra['type'] ) ) {
117
+ return false;
118
+ }
119
+
120
+ $type = $extra['type'];
121
+ $action = $extra['action'];
122
+
123
+ if ( ! in_array( $type, array( 'plugin', 'theme' ) ) ) {
124
+ return;
125
+ }
126
+
127
+ if ( 'install' === $action ) {
128
+ if ( 'plugin' === $type ) {
129
+ $path = $upgrader->plugin_info();
130
+ if ( ! $path ) {
131
+ return;
132
+ }
133
+ $data = get_plugin_data( $upgrader->skin->result['local_destination'] . '/' . $path );
134
+ $slug = $upgrader->result['destination_name'];
135
+ $name = $data['Name'];
136
+ $version = $data['Version'];
137
+ } else { // theme
138
+ $slug = $upgrader->theme_info();
139
+ if ( ! $slug ) {
140
+ return;
141
+ }
142
+ wp_clean_themes_cache();
143
+ $theme = wp_get_theme( $slug );
144
+ $name = $theme->name;
145
+ $version = $theme->version;
146
+ }
147
+ $action = 'installed';
148
+ $message = _x(
149
+ 'Installed %1$s: %2$s %3$s',
150
+ 'Plugin/theme installation. 1: Type (plugin/theme), 2: Plugin/theme name, 3: Plugin/theme version',
151
+ 'stream'
152
+ );
153
+ $logs[] = compact( 'slug', 'name', 'version', 'message', 'action' );
154
+ } elseif ( 'update' === $action ) {
155
+ $action = 'updated';
156
+ $message = _x(
157
+ 'Updated %1$s: %2$s %3$s',
158
+ 'Plugin/theme update. 1: Type (plugin/theme), 2: Plugin/theme name, 3: Plugin/theme version',
159
+ 'stream'
160
+ );
161
+ if ( 'plugin' === $type ) {
162
+ if ( isset( $extra['bulk'] ) && true == $extra['bulk'] ) {
163
+ $slugs = $extra['plugins'];
164
+ } else {
165
+ $slugs = array( $upgrader->skin->plugin );
166
+ }
167
+
168
+ $_plugins = self::get_plugins();
169
+
170
+ foreach ( $slugs as $slug ) {
171
+ $plugin_data = get_plugin_data( WP_PLUGIN_DIR . '/' . $slug );
172
+ $name = $plugin_data['Name'];
173
+ $version = $plugin_data['Version'];
174
+ $old_version = $_plugins[ $slug ]['Version'];
175
+
176
+ $logs[] = compact( 'slug', 'name', 'old_version', 'version', 'message', 'action' );
177
+ }
178
+ } else { // theme
179
+ if ( isset( $extra['bulk'] ) && true == $extra['bulk'] ) {
180
+ $slugs = $extra['themes'];
181
+ } else {
182
+ $slugs = array( $upgrader->skin->theme );
183
+ }
184
+ foreach ( $slugs as $slug ) {
185
+ $theme = wp_get_theme( $slug );
186
+ $stylesheet = $theme['Stylesheet Dir'] . '/style.css';
187
+ $theme_data = get_file_data( $stylesheet, array( 'Version' => 'Version' ) );
188
+ $name = $theme['Name'];
189
+ $old_version = $theme['Version'];
190
+ $version = $theme_data['Version'];
191
+
192
+ $logs[] = compact( 'slug', 'name', 'old_version', 'version', 'message', 'action' );
193
+ }
194
+ }
195
+ } else {
196
+ return false;
197
+ }
198
+
199
+ $context = $type . 's';
200
+
201
+ foreach ( $logs as $log ) {
202
+ $name = isset( $log['name'] ) ? $log['name'] : null;
203
+ $version = isset( $log['version'] ) ? $log['version'] : null;
204
+ $slug = isset( $log['slug'] ) ? $log['slug'] : null;
205
+ $old_version = isset( $log['old_version'] ) ? $log['old_version'] : null;
206
+ $message = isset( $log['message'] ) ? $log['message'] : null;
207
+ $action = isset( $log['action'] ) ? $log['action'] : null;
208
+ self::log(
209
+ $message,
210
+ compact( 'type', 'name', 'version', 'slug', 'success', 'error', 'old_version' ),
211
+ null,
212
+ $context,
213
+ $action
214
+ );
215
+ }
216
+ }
217
+
218
+ public static function callback_activate_plugin( $slug, $network_wide ) {
219
+ $_plugins = self::get_plugins();
220
+ $name = $_plugins[ $slug ]['Name'];
221
+ $network_wide = $network_wide ? __( 'network wide', 'stream' ) : null;
222
+
223
+ self::log(
224
+ _x(
225
+ '"%1$s" plugin activated %2$s',
226
+ '1: Plugin name, 2: Single site or network wide',
227
+ 'stream'
228
+ ),
229
+ compact( 'name', 'network_wide' ),
230
+ null,
231
+ 'plugins',
232
+ 'activated'
233
+ );
234
+ }
235
+
236
+ public static function callback_deactivate_plugin( $slug, $network_wide ) {
237
+ $_plugins = self::get_plugins();
238
+ $name = $_plugins[ $slug ]['Name'];
239
+ $network_wide = $network_wide ? __( 'network wide', 'stream' ) : null;
240
+
241
+ self::log(
242
+ _x(
243
+ '"%1$s" plugin deactivated %2$s',
244
+ '1: Plugin name, 2: Single site or network wide',
245
+ 'stream'
246
+ ),
247
+ compact( 'name', 'network_wide' ),
248
+ null,
249
+ 'plugins',
250
+ 'deactivated'
251
+ );
252
+ }
253
+
254
+ public static function callback_switch_theme( $name, $theme ) {
255
+ $stylesheet = $theme->get_stylesheet();
256
+
257
+ self::log(
258
+ __( '"%s" theme activated', 'stream' ),
259
+ compact( 'name' ),
260
+ null,
261
+ 'themes',
262
+ 'activated'
263
+ );
264
+ }
265
+
266
+ /**
267
+ * @todo Core needs a delete_theme hook
268
+ */
269
+ public static function callback_delete_site_transient_update_themes() {
270
+
271
+ $backtrace = debug_backtrace();
272
+ $delete_theme_call = null;
273
+ foreach ( $backtrace as $call ) {
274
+ if ( isset( $call['function'] ) && 'delete_theme' === $call['function'] ) {
275
+ $delete_theme_call = $call;
276
+ break;
277
+ }
278
+ }
279
+
280
+ if ( empty( $delete_theme_call ) ) {
281
+ return;
282
+ }
283
+
284
+ $name = $delete_theme_call['args'][0];
285
+ // @todo Can we get the name of the theme? Or has it already been eliminated
286
+
287
+ self::log(
288
+ __( '"%s" theme deleted', 'stream' ),
289
+ compact( 'name' ),
290
+ null,
291
+ 'themes',
292
+ 'deleted'
293
+ );
294
+ }
295
+
296
+ /**
297
+ * @todo Core needs an uninstall_plugin hook
298
+ * @todo This does not work in WP-CLI
299
+ */
300
+ public static function callback_pre_option_uninstall_plugins() {
301
+ if (
302
+ 'delete-selected' !== wp_stream_filter_input( INPUT_GET, 'action' )
303
+ &&
304
+ 'delete-selected' !== wp_stream_filter_input( INPUT_POST, 'action2' )
305
+ ) {
306
+ return false;
307
+ }
308
+
309
+ $type = isset( $_POST['action2'] ) ? INPUT_POST : INPUT_GET;
310
+ $plugins = wp_stream_filter_input( $type, 'checked' );
311
+ $_plugins = self::get_plugins();
312
+
313
+ foreach ( (array) $plugins as $plugin ) {
314
+ $plugins_to_delete[ $plugin ] = $_plugins[ $plugin ];
315
+ }
316
+
317
+ update_option( 'wp_stream_plugins_to_delete', $plugins_to_delete );
318
+
319
+ return false;
320
+ }
321
+
322
+ /**
323
+ * @todo Core needs a delete_plugin hook
324
+ * @todo This does not work in WP-CLI
325
+ */
326
+ public static function callback_pre_set_site_transient_update_plugins( $value ) {
327
+ if (
328
+ ! wp_stream_filter_input( INPUT_POST, 'verify-delete' )
329
+ ||
330
+ ! ( $plugins_to_delete = get_option( 'wp_stream_plugins_to_delete' ) )
331
+ ) {
332
+ return $value;
333
+ }
334
+
335
+ foreach ( $plugins_to_delete as $plugin => $data ) {
336
+ $name = $data['Name'];
337
+ $network_wide = $data['Network'] ? __( 'network wide', 'stream' ) : '';
338
+
339
+ self::log(
340
+ __( '"%s" plugin deleted', 'stream' ),
341
+ compact( 'name', 'plugin', 'network_wide' ),
342
+ null,
343
+ 'plugins',
344
+ 'deleted'
345
+ );
346
+ }
347
+
348
+ delete_option( 'wp_stream_plugins_to_delete' );
349
+
350
+ return $value;
351
+ }
352
+
353
+ public static function callback__core_updated_successfully( $new_version ) {
354
+ global $pagenow, $wp_version;
355
+
356
+ $old_version = $wp_version;
357
+ $auto_updated = ( 'update-core.php' !== $pagenow );
358
+
359
+ if ( $auto_updated ) {
360
+ $message = __( 'WordPress auto-updated to %s', 'stream' );
361
+ } else {
362
+ $message = __( 'WordPress updated to %s', 'stream' );
363
+ }
364
+
365
+ self::log(
366
+ $message,
367
+ compact( 'new_version', 'old_version', 'auto_updated' ),
368
+ null,
369
+ 'wordpress',
370
+ 'updated'
371
+ );
372
+ }
373
+
374
+ }
connectors/class-wp-stream-connector-jetpack.php ADDED
@@ -0,0 +1,672 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class WP_Stream_Connector_Jetpack extends WP_Stream_Connector {
4
+
5
+ /**
6
+ * Connector slug
7
+ *
8
+ * @var string
9
+ */
10
+ public static $name = 'jetpack';
11
+
12
+ /**
13
+ * Holds tracked plugin minimum version required
14
+ *
15
+ * @const string
16
+ */
17
+ const PLUGIN_MIN_VERSION = '3.0.2';
18
+
19
+ /**
20
+ * Actions registered for this connector
21
+ *
22
+ * @var array
23
+ */
24
+ public static $actions = array(
25
+ 'jetpack_log_entry',
26
+ 'sharing_get_services_state',
27
+ 'update_option',
28
+ 'add_option',
29
+ 'delete_option',
30
+ 'jetpack_module_configuration_load_monitor',
31
+ 'wp_ajax_jetpack_post_by_email_enable', // @todo These three actions do not verify whether the action has been done or if an error has been raised
32
+ 'wp_ajax_jetpack_post_by_email_regenerate',
33
+ 'wp_ajax_jetpack_post_by_email_disable',
34
+ );
35
+
36
+ /**
37
+ * Tracked option keys
38
+ *
39
+ * @var array
40
+ */
41
+ public static $options = array();
42
+
43
+ /**
44
+ * Tracking registered Settings, with overridden data
45
+ *
46
+ * @var array
47
+ */
48
+ public static $options_override = array();
49
+
50
+ /**
51
+ * Check if plugin dependencies are satisfied and add an admin notice if not
52
+ *
53
+ * @return bool
54
+ */
55
+ public static function is_dependency_satisfied() {
56
+ if ( class_exists( 'Jetpack' ) && defined( 'JETPACK__VERSION' ) && version_compare( JETPACK__VERSION, self::PLUGIN_MIN_VERSION, '>=' ) ) {
57
+ return true;
58
+ }
59
+
60
+ return false;
61
+ }
62
+
63
+ /**
64
+ * Return translated connector label
65
+ *
66
+ * @return string Translated connector label
67
+ */
68
+ public static function get_label() {
69
+ return _x( 'Jetpack', 'jetpack', 'stream' );
70
+ }
71
+
72
+ /**
73
+ * Return translated action labels
74
+ *
75
+ * @return array Action label translations
76
+ */
77
+ public static function get_action_labels() {
78
+ return array(
79
+ 'activated' => _x( 'Activated', 'jetpack', 'stream' ),
80
+ 'deactivated' => _x( 'Dectivated', 'jetpack', 'stream' ),
81
+ 'register' => _x( 'Connected', 'jetpack', 'stream' ),
82
+ 'disconnect' => _x( 'Disconnected', 'jetpack', 'stream' ),
83
+ 'authorize' => _x( 'Link', 'jetpack', 'stream' ),
84
+ 'unlink' => _x( 'Unlink', 'jetpack', 'stream' ),
85
+ 'updated' => _x( 'Updated', 'jetpack', 'stream' ),
86
+ 'added' => _x( 'Added', 'jetpack', 'stream' ),
87
+ 'removed' => _x( 'Removed', 'jetpack', 'stream' ),
88
+ );
89
+ }
90
+
91
+ /**
92
+ * Return translated context labels
93
+ *
94
+ * @return array Context label translations
95
+ */
96
+ public static function get_context_labels() {
97
+ return array(
98
+ 'modules' => _x( 'Modules', 'jetpack', 'stream' ),
99
+ 'blogs' => _x( 'Blogs', 'jetpack', 'stream' ),
100
+ 'users' => _x( 'Users', 'jetpack', 'stream' ),
101
+ 'options' => _x( 'Options', 'jetpack', 'stream' ),
102
+ 'sharedaddy' => _x( 'Sharing', 'jetpack', 'stream' ),
103
+ 'publicize' => _x( 'Publicize', 'jetpack', 'stream' ),
104
+ 'gplus-authorship' => _x( 'Google+ Profile', 'jetpack', 'stream' ),
105
+ 'stats' => _x( 'WordPress.com Stats', 'jetpack', 'stream' ),
106
+ 'carousel' => _x( 'Carousel', 'jetpack', 'stream' ),
107
+ 'custom-css' => _x( 'Custom CSS', 'jetpack', 'stream' ),
108
+ 'subscriptions' => _x( 'Subscriptions', 'jetpack', 'stream' ),
109
+ 'jetpack-comments' => _x( 'Comments', 'jetpack', 'stream' ),
110
+ 'infinite-scroll' => _x( 'Infinite Scroll', 'jetpack', 'stream' ),
111
+ 'sso' => _x( 'SSO', 'jetpack', 'stream' ),
112
+ 'likes' => _x( 'Likes', 'jetpack', 'stream' ),
113
+ 'minileven' => _x( 'Mobile', 'jetpack', 'stream' ),
114
+ 'monitor' => _x( 'Monitor', 'jetpack', 'stream' ),
115
+ 'post-by-email' => _x( 'Post by Email', 'jetpack', 'stream' ),
116
+ 'related-posts' => _x( 'Related Posts', 'jetpack', 'stream' ),
117
+ 'verification-tools' => _x( 'Site Verification', 'jetpack', 'stream' ),
118
+ 'tiled-gallery' => _x( 'Tiled Galleries', 'jetpack', 'stream' ),
119
+ 'videopress' => _x( 'VideoPress', 'jetpack', 'stream' ),
120
+ );
121
+ }
122
+
123
+ /**
124
+ * Add action links to Stream drop row in admin list screen
125
+ *
126
+ * @filter wp_stream_action_links_{connector}
127
+ *
128
+ * @param array $links Previous links registered
129
+ * @param object $record Stream record
130
+ *
131
+ * @return array Action links
132
+ */
133
+ public static function action_links( $links, $record ) {
134
+ // @todo provide proper action links
135
+ if ( 'jetpack' === $record->connector ) {
136
+ if ( 'modules' === $record->context ) {
137
+ $slug = wp_stream_get_meta( $record, 'module_slug', true );
138
+
139
+ if ( is_array( $slug ) ) {
140
+ $slug = current( $slug );
141
+ }
142
+
143
+ if ( Jetpack::is_module_active( $slug ) ) {
144
+ if ( apply_filters( 'jetpack_module_configurable_' . $slug, false ) ) {
145
+ $links[ __( 'Configure', 'stream' ) ] = Jetpack::module_configuration_url( $slug );;
146
+ }
147
+
148
+ $links[ __( 'Deactivate', 'stream' ) ] = wp_nonce_url(
149
+ add_query_arg(
150
+ array(
151
+ 'action' => 'deactivate',
152
+ 'module' => $slug,
153
+ ),
154
+ Jetpack::admin_url()
155
+ ),
156
+ 'jetpack_deactivate-' . sanitize_title( $slug )
157
+ );
158
+ } else {
159
+ $links[ __( 'Activate', 'stream' ) ] = wp_nonce_url(
160
+ add_query_arg(
161
+ array(
162
+ 'action' => 'activate',
163
+ 'module' => $slug,
164
+ ),
165
+ Jetpack::admin_url()
166
+ ),
167
+ 'jetpack_activate-' . sanitize_title( $slug )
168
+ );
169
+ }
170
+ } elseif ( Jetpack::is_module_active( str_replace( 'jetpack-', '', $record->context ) ) ) {
171
+ $slug = str_replace( 'jetpack-', '', $record->context ); // handling jetpack-comment anomaly
172
+
173
+ if ( apply_filters( 'jetpack_module_configurable_' . $slug, false ) ) {
174
+ $links[ __( 'Configure module', 'stream' ) ] = Jetpack::module_configuration_url( $slug );;
175
+ }
176
+ }
177
+ }
178
+
179
+ return $links;
180
+ }
181
+
182
+ public static function register() {
183
+ parent::register();
184
+
185
+ add_filter( 'wp_stream_log_data', array( __CLASS__, 'log_override' ) );
186
+
187
+ self::$options = array(
188
+ 'jetpack_options' => null,
189
+ // Sharing module
190
+ 'hide_gplus' => null,
191
+ 'gplus_authors' => null,
192
+ 'sharing-options' => array(
193
+ 'label' => __( 'Sharing options', 'stream' ),
194
+ 'context' => 'sharedaddy',
195
+ ),
196
+ 'sharedaddy_disable_resources' => null,
197
+ 'jetpack-twitter-cards-site-tag' => array(
198
+ 'label' => __( 'Twitter site tag', 'stream' ),
199
+ 'context' => 'sharedaddy',
200
+ ),
201
+ // Stats module
202
+ 'stats_options' => array(
203
+ 'label' => __( 'WordPress.com Stats', 'stream' ),
204
+ 'context' => 'stats',
205
+ ),
206
+ // Comments
207
+ 'jetpack_comment_form_color_scheme' => array(
208
+ 'label' => __( 'Color Scheme', 'stream' ),
209
+ 'context' => 'jetpack-comments',
210
+ ),
211
+ // Likes
212
+ 'disabled_likes' => array(
213
+ 'label' => __( 'WP.com Site-wide Likes', 'stream' ),
214
+ 'context' => 'likes',
215
+ ),
216
+ // Mobile
217
+ 'wp_mobile_excerpt' => array(
218
+ 'label' => __( 'Excerpts appearance', 'stream' ),
219
+ 'context' => 'minileven',
220
+ ),
221
+ 'wp_mobile_app_promos' => array(
222
+ 'label' => __( 'App promos', 'stream' ),
223
+ 'context' => 'minileven',
224
+ ),
225
+ );
226
+
227
+ self::$options_override = array(
228
+ // Carousel Module
229
+ 'carousel_background_color' => array(
230
+ 'label' => __( 'Background color', 'stream' ),
231
+ 'context' => 'carousel',
232
+ ),
233
+ 'carousel_display_exif' => array(
234
+ 'label' => __( 'Metadata', 'stream' ),
235
+ 'context' => 'carousel',
236
+ ),
237
+ // Subscriptions
238
+ 'stb_enabled' => array(
239
+ 'label' => __( 'Follow blog comment form button', 'stream' ),
240
+ 'context' => 'subscriptions',
241
+ ),
242
+ 'stc_enabled' => array(
243
+ 'label' => __( 'Follow comments form button', 'stream' ),
244
+ 'context' => 'subscriptions',
245
+ ),
246
+ // Jetpack comments
247
+ 'highlander_comment_form_prompt' => array(
248
+ 'label' => __( 'Greeting Text', 'stream' ),
249
+ 'context' => 'jetpack-comments',
250
+ ),
251
+ // Infinite Scroll
252
+ 'infinite_scroll_google_analytics' => array(
253
+ 'label' => __( 'Infinite Scroll Google Analytics', 'stream' ),
254
+ 'context' => 'infinite-scroll',
255
+ ),
256
+ // SSO
257
+ 'jetpack_sso_require_two_step' => array(
258
+ 'label' => __( 'Require Two-Step Authentication', 'stream' ),
259
+ 'context' => 'sso',
260
+ ),
261
+ 'jetpack_sso_match_by_email' => array(
262
+ 'label' => __( 'Match by Email', 'stream' ),
263
+ 'context' => 'sso',
264
+ ),
265
+ // Related posts
266
+ 'jetpack_relatedposts' => array(
267
+ 'show_headline' => array(
268
+ 'label' => __( 'Show Related Posts Headline', 'stream' ),
269
+ 'context' => 'related-posts',
270
+ ),
271
+ 'show_thumbnails' => array(
272
+ 'label' => __( 'Show Related Posts Thumbnails', 'stream' ),
273
+ 'context' => 'related-posts',
274
+ ),
275
+ ),
276
+ // Site verification
277
+ 'verification_services_codes' => array(
278
+ 'google' => array(
279
+ 'label' => __( 'Google Webmaster Tools Token', 'stream' ),
280
+ 'context' => 'verification-tools',
281
+ ),
282
+ 'bing' => array(
283
+ 'label' => __( 'Bing Webmaster Center Token', 'stream' ),
284
+ 'context' => 'verification-tools',
285
+ ),
286
+ 'pinterest' => array(
287
+ 'label' => __( 'Pinterest Site Verification Token', 'stream' ),
288
+ 'context' => 'verification-tools',
289
+ ),
290
+ ),
291
+ // Tiled galleries
292
+ 'tiled_galleries' => array(
293
+ 'label' => __( 'Tiled Galleries', 'stream' ),
294
+ 'context' => 'tiled-gallery',
295
+ ),
296
+ );
297
+ }
298
+
299
+ /**
300
+ * Track Jetpack log entries
301
+ * Includes:
302
+ * - Activation/Deactivation of modules
303
+ * - Registration/Disconnection of blogs
304
+ * - Authorization/unlinking of users
305
+ *
306
+ * @param array $entry
307
+ */
308
+ public static function callback_jetpack_log_entry( array $entry ) {
309
+ $method = $entry['code'];
310
+ $data = $entry['data'];
311
+ $context = null;
312
+ $action = null;
313
+
314
+ if ( in_array( $method, array( 'activate', 'deactivate' ) ) ) {
315
+ $module_slug = $data;
316
+ $module = Jetpack::get_module( $module_slug );
317
+ $module_name = $module['name'];
318
+ $context = 'modules';
319
+ $action = $method . 'd';
320
+ $meta = compact( 'module_slug' );
321
+ $message = sprintf(
322
+ __( '%1$s module %2$s', 'stream' ),
323
+ $module_name,
324
+ ( 'activated' === $action ) ? __( 'activated', 'stream' ) : __( 'deactivated', 'stream' )
325
+ );
326
+ } elseif ( in_array( $method, array( 'authorize', 'unlink' ) ) ) {
327
+ $user_id = intval( $data );
328
+
329
+ if ( empty( $user_id ) ) {
330
+ $user_id = get_current_user_id();
331
+ }
332
+
333
+ $user = new WP_User( $user_id );
334
+ $user_email = $user->user_email;
335
+ $user_login = $user->user_login;
336
+ $context = 'users';
337
+ $action = $method;
338
+ $meta = compact( 'user_id', 'user_email', 'user_login' );
339
+ $message = sprintf(
340
+ __( '%1$s\'s account %2$s %3$s Jetpack', 'stream' ),
341
+ $user->display_name,
342
+ ( 'unlink' === $action ) ? __( 'unlinked', 'stream' ) : __( 'linked', 'stream' ),
343
+ ( 'unlink' === $action ) ? __( 'from', 'stream' ) : __( 'to', 'stream' )
344
+ );
345
+ } elseif ( in_array( $method, array( 'register', 'disconnect', 'subsiteregister', 'subsitedisconnect' ) ) ) {
346
+ $context = 'blogs';
347
+ $action = str_replace( 'subsite', '', $method );
348
+ $is_multisite = ( 0 === strpos( $method, 'subsite' ) );
349
+ $blog_id = $is_multisite ? ( isset( $_GET['site_id'] ) ? intval( $_GET['site_id'] ) : null ) : get_current_blog_id();
350
+
351
+ if ( empty( $blog_id ) ) {
352
+ return;
353
+ }
354
+
355
+ $meta = array();
356
+
357
+ if ( ! $is_multisite ) {
358
+ $message = sprintf(
359
+ __( 'Site %s Jetpack', 'stream' ),
360
+ ( 'register' === $action ) ? __( 'connected to', 'stream' ) : __( 'disconnected from', 'stream' )
361
+ );
362
+ } else {
363
+ $blog_details = get_blog_details( array( 'blog_id' => $blog_id ) );
364
+ $blog_name = $blog_details->blogname;
365
+ $meta += compact( 'blog_id', 'blog_name' );
366
+
367
+ $message = sprintf(
368
+ __( '"%1$s" blog %2$s Jetpack', 'stream' ),
369
+ $blog_name,
370
+ ( 'register' === $action ) ? __( 'connected to', 'stream' ) : __( 'disconnected from', 'stream' )
371
+ );
372
+ }
373
+ }
374
+
375
+ if ( empty( $message ) ) {
376
+ return;
377
+ }
378
+
379
+ self::log(
380
+ $message,
381
+ $meta,
382
+ null,
383
+ $context,
384
+ $action
385
+ );
386
+ }
387
+
388
+ /**
389
+ * Track visible/enabled sharing services ( buttons )
390
+ *
391
+ * @param $state
392
+ */
393
+ public static function callback_sharing_get_services_state( $state ) {
394
+ self::log(
395
+ __( 'Sharing services updated', 'stream' ),
396
+ $state,
397
+ null,
398
+ 'sharedaddy',
399
+ 'updated'
400
+ );
401
+ }
402
+
403
+ public static function callback_update_option( $option, $old, $new ) {
404
+ self::check( $option, $old, $new );
405
+ }
406
+
407
+ public static function callback_add_option( $option, $val ) {
408
+ self::check( $option, null, $val );
409
+ }
410
+
411
+ public static function callback_delete_option( $option ) {
412
+ self::check( $option, null, null );
413
+ }
414
+
415
+ /**
416
+ * Track Monitor module notification status
417
+ */
418
+ public static function callback_jetpack_module_configuration_load_monitor() {
419
+ if ( $_POST ) {
420
+ $active = wp_stream_filter_input( INPUT_POST, 'receive_jetpack_monitor_notification' );
421
+
422
+ self::log(
423
+ __( 'Monitor notifications %s', 'stream' ),
424
+ array(
425
+ 'status' => $active ? __( 'activated', 'stream' ) : __( 'deactivated', 'stream' ),
426
+ 'option' => 'receive_jetpack_monitor_notification',
427
+ 'old_value' => ! $active,
428
+ 'value' => $active,
429
+ ),
430
+ null,
431
+ 'monitor',
432
+ 'updated'
433
+ );
434
+ }
435
+ }
436
+
437
+ public static function callback_wp_ajax_jetpack_post_by_email_enable() {
438
+ self::track_post_by_email( true );
439
+ }
440
+
441
+ public static function callback_wp_ajax_jetpack_post_by_email_regenerate() {
442
+ self::track_post_by_email( null );
443
+ }
444
+
445
+ public static function callback_wp_ajax_jetpack_post_by_email_disable() {
446
+ self::track_post_by_email( false );
447
+ }
448
+
449
+ public static function track_post_by_email( $status ) {
450
+ if ( true === $status ) {
451
+ $action = __( 'enabled', 'stream' );
452
+ } elseif ( false === $status ) {
453
+ $action = __( 'disabled', 'stream' );
454
+ } elseif ( null === $status ) {
455
+ $action = __( 'regenerated', 'stream' );
456
+ }
457
+
458
+ $user = wp_get_current_user();
459
+
460
+ self::log(
461
+ __( '%1$s %2$s Post by Email', 'stream' ),
462
+ array(
463
+ 'user_displayname' => $user->display_name,
464
+ 'action' => $action,
465
+ 'status' => $status,
466
+ ),
467
+ null,
468
+ 'post-by-email',
469
+ 'updated'
470
+ );
471
+ }
472
+
473
+ public static function check( $option, $old_value, $new_value ) {
474
+ if ( ! array_key_exists( $option, self::$options ) ) {
475
+ return;
476
+ }
477
+
478
+ if ( is_null( self::$options[ $option ] ) ) {
479
+ call_user_func( array( __CLASS__, 'check_' . str_replace( '-', '_', $option ) ), $old_value, $new_value );
480
+ } else {
481
+ $data = self::$options[ $option ];
482
+ $option_title = $data['label'];
483
+
484
+ self::log(
485
+ __( '"%s" setting updated', 'stream' ),
486
+ compact( 'option_title', 'option', 'old_value', 'new_value' ),
487
+ null,
488
+ $data['context'],
489
+ isset( $data['action'] ) ? $data['action'] : 'updated'
490
+ );
491
+ }
492
+ }
493
+
494
+ public static function check_jetpack_options( $old_value, $new_value ) {
495
+ $options = array();
496
+
497
+ if ( ! is_array( $old_value ) || ! is_array( $new_value ) ) {
498
+ return;
499
+ }
500
+
501
+ foreach ( self::get_changed_keys( $old_value, $new_value, 1 ) as $field_key => $field_value ) {
502
+ $options[ $field_key ] = $field_value;
503
+ }
504
+
505
+ foreach ( $options as $option => $option_value ) {
506
+ $settings = self::get_settings_def( $option, $option_value );
507
+
508
+ if ( ! $settings ) {
509
+ continue;
510
+ }
511
+
512
+ if ( 0 === $option_value ) { // Skip updated array with updated members, we'll be logging those instead
513
+ continue;
514
+ }
515
+
516
+ $settings['meta'] += array(
517
+ 'option' => $option,
518
+ 'old_value' => maybe_serialize( $old_value ),
519
+ 'value' => maybe_serialize( $new_value ),
520
+ );
521
+
522
+ self::log(
523
+ $settings['message'],
524
+ $settings['meta'],
525
+ isset( $settings['object_id'] ) ? $settings['object_id'] : null,
526
+ $settings['context'],
527
+ $settings['action']
528
+ );
529
+ }
530
+ }
531
+
532
+ public static function check_hide_gplus( $old_value, $new_value ) {
533
+ $status = ! is_null( $new_value );
534
+
535
+ if ( $status && $old_value ) {
536
+ return false;
537
+ }
538
+
539
+ self::log(
540
+ __( 'G+ profile display %s', 'stream' ),
541
+ array(
542
+ 'action' => $status ? __( 'enabled', 'stream' ) : __( 'disabled', 'stream' ),
543
+ ),
544
+ null,
545
+ 'gplus-authorship',
546
+ 'updated'
547
+ );
548
+ }
549
+
550
+ public static function check_gplus_authors( $old_value, $new_value ) {
551
+ $user = wp_get_current_user();
552
+ $connected = is_array( $new_value ) && array_key_exists( $user->ID, $new_value );
553
+
554
+ self::log(
555
+ __( '%1$s\'s Google+ account %2$s', 'stream' ),
556
+ array(
557
+ 'display_name' => $user->display_name,
558
+ 'action' => $connected ? __( 'connected', 'stream' ) : __( 'disconnected', 'stream' ),
559
+ 'user_id' => $user->ID,
560
+ ),
561
+ $user->ID,
562
+ 'gplus-authorship',
563
+ 'updated'
564
+ );
565
+ }
566
+
567
+ public static function check_sharedaddy_disable_resources( $old_value, $new_value ) {
568
+ if ( $old_value == $new_value ) {
569
+ return;
570
+ }
571
+
572
+ $status = ! $new_value ? 'enabled' : 'disabled'; // disabled = 1
573
+
574
+ self::log(
575
+ __( 'Sharing CSS/JS %s', 'stream' ),
576
+ compact( 'status', 'old_value', 'new_value' ),
577
+ null,
578
+ 'sharing',
579
+ 'updated'
580
+ );
581
+ }
582
+
583
+ /**
584
+ * Override connector log for our own Settings / Actions
585
+ *
586
+ * @param array $data
587
+ *
588
+ * @return array|bool
589
+ */
590
+ public static function log_override( $data ) {
591
+ if ( ! is_array( $data ) ) {
592
+ return $data;
593
+ }
594
+
595
+ // Handling our Settings
596
+ if ( 'settings' === $data['connector'] && isset( self::$options_override[ $data['args']['option'] ] ) ) {
597
+ if ( isset( $data['args']['option_key'] ) ) {
598
+ $overrides = self::$options_override[ $data['args']['option'] ][ $data['args']['option_key'] ];
599
+ } else {
600
+ $overrides = self::$options_override[ $data['args']['option'] ];
601
+ }
602
+
603
+ if ( isset( $overrides ) ) {
604
+ $data['args']['label'] = $overrides['label'];
605
+ $data['args']['context'] = $overrides['context'];
606
+ $data['context'] = $overrides['context'];
607
+ $data['connector'] = self::$name;
608
+ }
609
+ } elseif ( 'posts' === $data['connector'] && 'safecss' === $data['context'] ) {
610
+ $data = array_merge(
611
+ $data,
612
+ array(
613
+ 'connector' => self::$name,
614
+ 'message' => __( 'Custom CSS updated', 'stream' ),
615
+ 'args' => array(),
616
+ 'object_id' => null,
617
+ 'context' => 'custom-css',
618
+ 'action' => 'updated',
619
+ )
620
+ );
621
+ }
622
+
623
+ return $data;
624
+ }
625
+
626
+ private static function get_settings_def( $key, $value = null ) {
627
+ // Sharing
628
+ if ( 0 === strpos( $key, 'publicize_connections::' ) ) {
629
+ global $publicize_ui;
630
+
631
+ $name = str_replace( 'publicize_connections::', '', $key );
632
+
633
+ return array(
634
+ 'message' => __( '%1$s connection %2$s', 'stream' ),
635
+ 'meta' => array(
636
+ 'connection' => $publicize_ui->publicize->get_service_label( $name ),
637
+ 'action' => $value ? __( 'added', 'stream' ) : __( 'removed', 'stream' ),
638
+ 'option' => 'jetpack_options',
639
+ 'option_key' => $key,
640
+ ),
641
+ 'action' => $value ? 'added' : 'removed',
642
+ 'context' => 'publicize',
643
+ );
644
+ } elseif ( 0 === strpos( $key, 'videopress::' ) ) {
645
+ $name = str_replace( 'videopress::', '', $key );
646
+ $options = array(
647
+ 'access' => __( 'Video Library Access', 'stream' ),
648
+ 'upload' => __( 'Allow users to upload videos', 'stream' ),
649
+ 'freedom' => __( 'Free formats', 'stream' ),
650
+ 'hd' => __( 'Default quality', 'stream' ),
651
+ );
652
+
653
+ if ( ! isset( $options[ $name ] ) ) {
654
+ return false;
655
+ }
656
+
657
+ return array(
658
+ 'message' => __( '"%s" setting updated', 'stream' ),
659
+ 'meta' => array(
660
+ 'option_name' => $options[ $name ],
661
+ 'option' => 'jetpack_options',
662
+ 'option_key' => $key,
663
+ ),
664
+ 'action' => 'updated',
665
+ 'context' => 'videopress',
666
+ );
667
+ }
668
+
669
+ return false;
670
+ }
671
+
672
+ }
connectors/class-wp-stream-connector-media.php ADDED
@@ -0,0 +1,215 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class WP_Stream_Connector_Media extends WP_Stream_Connector {
4
+
5
+ /**
6
+ * Connector slug
7
+ *
8
+ * @var string
9
+ */
10
+ public static $name = 'media';
11
+
12
+ /**
13
+ * Actions registered for this connector
14
+ *
15
+ * @var array
16
+ */
17
+ public static $actions = array(
18
+ 'add_attachment',
19
+ 'edit_attachment',
20
+ 'delete_attachment',
21
+ 'wp_save_image_editor_file',
22
+ 'wp_save_image_file',
23
+ );
24
+
25
+ /**
26
+ * Return translated connector label
27
+ *
28
+ * @return string Translated connector label
29
+ */
30
+ public static function get_label() {
31
+ return __( 'Media', 'stream' );
32
+ }
33
+
34
+ /**
35
+ * Return translated action labels
36
+ *
37
+ * @return array Action label translations
38
+ */
39
+ public static function get_action_labels() {
40
+ return array(
41
+ 'attached' => __( 'Attached', 'stream' ),
42
+ 'uploaded' => __( 'Uploaded', 'stream' ),
43
+ 'updated' => __( 'Updated', 'stream' ),
44
+ 'deleted' => __( 'Deleted', 'stream' ),
45
+ 'assigned' => __( 'Assigned', 'stream' ),
46
+ 'unassigned' => __( 'Unassigned', 'stream' ),
47
+ );
48
+ }
49
+
50
+ /**
51
+ * Return translated context labels
52
+ *
53
+ * Based on extension types used by wp_ext2type() in wp-includes/functions.php.
54
+ *
55
+ * @return array Context label translations
56
+ */
57
+ public static function get_context_labels() {
58
+ return array(
59
+ 'image' => __( 'Image', 'stream' ),
60
+ 'audio' => __( 'Audio', 'stream' ),
61
+ 'video' => __( 'Video', 'stream' ),
62
+ 'document' => __( 'Document', 'stream' ),
63
+ 'spreadsheet' => __( 'Spreadsheet', 'stream' ),
64
+ 'interactive' => __( 'Interactive', 'stream' ),
65
+ 'text' => __( 'Text', 'stream' ),
66
+ 'archive' => __( 'Archive', 'stream' ),
67
+ 'code' => __( 'Code', 'stream' ),
68
+ );
69
+ }
70
+
71
+ /**
72
+ * Return the file type for an attachment which corresponds with a context label
73
+ *
74
+ * @param object $file_uri URI of the attachment
75
+ * @return string A file type which corresponds with a context label
76
+ */
77
+ public static function get_attachment_type( $file_uri ) {
78
+ $extension = pathinfo( $file_uri, PATHINFO_EXTENSION );
79
+ $extension_type = wp_ext2type( $extension );
80
+
81
+ if ( empty( $extension_type ) ) {
82
+ $extension_type = 'document';
83
+ }
84
+
85
+ $context_labels = self::get_context_labels();
86
+
87
+ if ( ! isset( $context_labels[ $extension_type ] ) ) {
88
+ $extension_type = 'document';
89
+ }
90
+
91
+ return $extension_type;
92
+ }
93
+
94
+ /**
95
+ * Add action links to Stream drop row in admin list screen
96
+ *
97
+ * @filter wp_stream_action_links_{connector}
98
+ *
99
+ * @param array $links Previous links registered
100
+ * @param object $record Stream record
101
+ *
102
+ * @return array Action links
103
+ */
104
+ public static function action_links( $links, $record ) {
105
+ if ( $record->object_id ) {
106
+ if ( $link = get_edit_post_link( $record->object_id ) ) {
107
+ $links[ __( 'Edit Media', 'stream' ) ] = $link;
108
+ }
109
+ if ( $link = get_permalink( $record->object_id ) ) {
110
+ $links[ __( 'View', 'stream' ) ] = $link;
111
+ }
112
+ }
113
+
114
+ return $links;
115
+ }
116
+
117
+ /**
118
+ * Tracks creation of attachments
119
+ *
120
+ * @action add_attachment
121
+ */
122
+ public static function callback_add_attachment( $post_id ) {
123
+ $post = get_post( $post_id );
124
+ if ( $post->post_parent ) {
125
+ $message = _x(
126
+ 'Attached "%1$s" to "%2$s"',
127
+ '1: Attachment title, 2: Parent post title',
128
+ 'stream'
129
+ );
130
+ } else {
131
+ $message = __( 'Added "%s" to Media library', 'stream' );
132
+ }
133
+
134
+ $name = $post->post_title;
135
+ $url = $post->guid;
136
+ $parent_id = $post->post_parent;
137
+ $parent = get_post( $parent_id );
138
+ $parent_title = $parent_id ? $parent->post_title : null;
139
+ $attachment_type = self::get_attachment_type( $post->guid );
140
+
141
+ self::log(
142
+ $message,
143
+ compact( 'name', 'parent_title', 'parent_id', 'url' ),
144
+ $post_id,
145
+ $attachment_type,
146
+ $post->post_parent ? 'attached' : 'uploaded'
147
+ );
148
+ }
149
+
150
+ /**
151
+ * Tracks editing attachments
152
+ *
153
+ * @action edit_attachment
154
+ */
155
+ public static function callback_edit_attachment( $post_id ) {
156
+ $post = get_post( $post_id );
157
+ $message = __( 'Updated "%s"', 'stream' );
158
+ $name = $post->post_title;
159
+ $attachment_type = self::get_attachment_type( $post->guid );
160
+
161
+ self::log(
162
+ $message,
163
+ compact( 'name' ),
164
+ $post_id,
165
+ $attachment_type,
166
+ 'updated'
167
+ );
168
+ }
169
+
170
+ /**
171
+ * Tracks deletion of attachments
172
+ *
173
+ * @action delete_attachment
174
+ */
175
+ public static function callback_delete_attachment( $post_id ) {
176
+ $post = get_post( $post_id );
177
+ $parent = $post->post_parent ? get_post( $post->post_parent ) : null;
178
+ $parent_id = $parent ? $parent->ID : null;
179
+ $message = __( 'Deleted "%s"', 'stream' );
180
+ $name = $post->post_title;
181
+ $url = $post->guid;
182
+ $attachment_type = self::get_attachment_type( $post->guid );
183
+
184
+ self::log(
185
+ $message,
186
+ compact( 'name', 'parent_id', 'url' ),
187
+ $post_id,
188
+ $attachment_type,
189
+ 'deleted'
190
+ );
191
+ }
192
+
193
+ /**
194
+ * Tracks changes made in the image editor
195
+ *
196
+ * @action delete_attachment
197
+ */
198
+ public static function callback_wp_save_image_editor_file( $dummy, $filename, $image, $mime_type, $post_id ) {
199
+ $name = basename( $filename );
200
+ $attachment_type = self::get_attachment_type( $post->guid );
201
+
202
+ self::log(
203
+ __( 'Edited image "%s"', 'stream' ),
204
+ compact( 'name', 'filename', 'post_id' ),
205
+ $post_id,
206
+ $attachment_type,
207
+ 'edited'
208
+ );
209
+ }
210
+
211
+ public static function callback_wp_save_image_file( $dummy, $filename, $image, $mime_type, $post_id ) {
212
+ return self::callback_wp_save_image_editor_file( $dummy, $filename, $image, $mime_type, $post_id );
213
+ }
214
+
215
+ }
connectors/class-wp-stream-connector-menus.php ADDED
@@ -0,0 +1,217 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class WP_Stream_Connector_Menus extends WP_Stream_Connector {
4
+
5
+ /**
6
+ * Connector slug
7
+ *
8
+ * @var string
9
+ */
10
+ public static $name = 'menus';
11
+
12
+ /**
13
+ * Actions registered for this connector
14
+ *
15
+ * @var array
16
+ */
17
+ public static $actions = array(
18
+ 'wp_create_nav_menu',
19
+ 'wp_update_nav_menu',
20
+ 'delete_nav_menu',
21
+ );
22
+
23
+ /**
24
+ * Return translated connector label
25
+ *
26
+ * @return string Translated connector label
27
+ */
28
+ public static function get_label() {
29
+ return __( 'Menus', 'stream' );
30
+ }
31
+
32
+ /**
33
+ * Return translated action labels
34
+ *
35
+ * @return array Action label translations
36
+ */
37
+ public static function get_action_labels() {
38
+ return array(
39
+ 'created' => __( 'Created', 'stream' ),
40
+ 'updated' => __( 'Updated', 'stream' ),
41
+ 'deleted' => __( 'Deleted', 'stream' ),
42
+ 'assigned' => __( 'Assigned', 'stream' ),
43
+ 'unassigned' => __( 'Unassigned', 'stream' ),
44
+ );
45
+ }
46
+
47
+ /**
48
+ * Return translated context labels
49
+ *
50
+ * @return array Context label translations
51
+ */
52
+ public static function get_context_labels() {
53
+ $labels = array();
54
+ $menus = get_terms( 'nav_menu', array( 'hide_empty' => false ) );
55
+
56
+ foreach ( $menus as $menu ) {
57
+ $slug = sanitize_title( $menu->name );
58
+ $labels[ $slug ] = $menu->name;
59
+ }
60
+
61
+ return $labels;
62
+ }
63
+
64
+ public static function register() {
65
+ parent::register();
66
+
67
+ add_action( 'update_option_theme_mods_' . get_option( 'stylesheet' ), array( __CLASS__, 'callback_update_option_theme_mods' ), 10, 2 );
68
+ }
69
+
70
+ /**
71
+ * Add action links to Stream drop row in admin list screen
72
+ *
73
+ * @filter wp_stream_action_links_{connector}
74
+ *
75
+ * @param array $links Previous links registered
76
+ * @param object $record Stream record
77
+ *
78
+ * @return array Action links
79
+ */
80
+ public static function action_links( $links, $record ) {
81
+ if ( $record->object_id ) {
82
+ $menus = wp_get_nav_menus();
83
+ $menu_ids = wp_list_pluck( $menus, 'term_id' );
84
+
85
+ if ( in_array( $record->object_id, $menu_ids ) ) {
86
+ $links[ __( 'Edit Menu', 'stream' ) ] = admin_url( 'nav-menus.php?action=edit&menu=' . $record->object_id ); // xss ok (@todo fix WPCS rule)
87
+ }
88
+ }
89
+
90
+ return $links;
91
+ }
92
+
93
+ /**
94
+ * Tracks creation of menus
95
+ *
96
+ * @action wp_create_nav_menu
97
+ */
98
+ public static function callback_wp_create_nav_menu( $menu_id, $menu_data ) {
99
+ $name = $menu_data['menu-name'];
100
+
101
+ self::log(
102
+ __( 'Created new menu "%s"', 'stream' ),
103
+ compact( 'name', 'menu_id' ),
104
+ $menu_id,
105
+ sanitize_title( $name ),
106
+ 'created'
107
+ );
108
+ }
109
+
110
+ /**
111
+ * Tracks menu updates
112
+ *
113
+ * @action wp_update_nav_menu
114
+ */
115
+ public static function callback_wp_update_nav_menu( $menu_id, $menu_data = array() ) {
116
+ if ( empty( $menu_data ) ) {
117
+ return;
118
+ }
119
+
120
+ $name = $menu_data['menu-name'];
121
+
122
+ self::log(
123
+ _x( 'Updated menu "%s"', 'Menu name', 'stream' ),
124
+ compact( 'name', 'menu_id', 'menu_data' ),
125
+ $menu_id,
126
+ sanitize_title( $name ),
127
+ 'updated'
128
+ );
129
+ }
130
+
131
+ /**
132
+ * Tracks menu deletion
133
+ *
134
+ * @action delete_nav_menu
135
+ */
136
+ public static function callback_delete_nav_menu( $term, $tt_id, $deleted_term ) {
137
+ $name = $deleted_term->name;
138
+ $menu_id = $term;
139
+
140
+ self::log(
141
+ _x( 'Deleted "%s"', 'Menu name', 'stream' ),
142
+ compact( 'name', 'menu_id' ),
143
+ $menu_id,
144
+ sanitize_title( $name ),
145
+ 'deleted'
146
+ );
147
+ }
148
+
149
+ /**
150
+ * Track assignment to menu locations
151
+ *
152
+ * @action update_option_theme_mods_{$stylesheet}
153
+ */
154
+ public static function callback_update_option_theme_mods( $old, $new ) {
155
+ // Disable if we're switching themes
156
+ if ( did_action( 'after_switch_theme' ) ) {
157
+ return;
158
+ }
159
+
160
+ $key = 'nav_menu_locations';
161
+
162
+ if ( ! isset( $new[ $key ] ) ) {
163
+ return; // Switching themes ?
164
+ }
165
+
166
+ if ( $old[ $key ] === $new[ $key ] ) {
167
+ return;
168
+ }
169
+
170
+ $locations = get_registered_nav_menus();
171
+ $old_value = (array) $old[ $key ];
172
+ $new_value = (array) $new[ $key ];
173
+ $changed = array_diff_assoc( $old_value, $new_value ) + array_diff_assoc( $new_value, $old_value );
174
+
175
+ if ( $changed ) {
176
+ foreach ( $changed as $location_id => $menu_id ) {
177
+ $location = $locations[ $location_id ];
178
+
179
+ if ( empty( $new[ $key ][ $location_id ] ) ) {
180
+ $action = 'unassigned';
181
+ $menu_id = isset( $old[ $key ][ $location_id ] ) ? $old[ $key ][ $location_id ] : 0;
182
+ $message = _x(
183
+ '"%1$s" has been unassigned from "%2$s"',
184
+ '1: Menu name, 2: Theme location',
185
+ 'stream'
186
+ );
187
+ } else {
188
+ $action = 'assigned';
189
+ $menu_id = isset( $new[ $key ][ $location_id ] ) ? $new[ $key ][ $location_id ] : 0;
190
+ $message = _x(
191
+ '"%1$s" has been assigned to "%2$s"',
192
+ '1: Menu name, 2: Theme location',
193
+ 'stream'
194
+ );
195
+ }
196
+
197
+ $menu = get_term( $menu_id, 'nav_menu' );
198
+
199
+ if ( ! $menu || is_wp_error( $menu ) ) {
200
+ continue; // This is a deleted menu
201
+ }
202
+
203
+ $name = $menu->name;
204
+
205
+ self::log(
206
+ $message,
207
+ compact( 'name', 'location', 'location_id', 'menu_id' ),
208
+ $menu_id,
209
+ sanitize_title( $name ),
210
+ $action
211
+ );
212
+ }
213
+ }
214
+
215
+ }
216
+
217
+ }
connectors/class-wp-stream-connector-posts.php ADDED
@@ -0,0 +1,350 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class WP_Stream_Connector_Posts extends WP_Stream_Connector {
4
+
5
+ /**
6
+ * Connector slug
7
+ *
8
+ * @var string
9
+ */
10
+ public static $name = 'posts';
11
+
12
+ /**
13
+ * Actions registered for this connector
14
+ *
15
+ * @var array
16
+ */
17
+ public static $actions = array(
18
+ 'transition_post_status',
19
+ 'deleted_post',
20
+ );
21
+
22
+ /**
23
+ * Return translated connector label
24
+ *
25
+ * @return string Translated connector label
26
+ */
27
+ public static function get_label() {
28
+ return __( 'Posts', 'stream' );
29
+ }
30
+
31
+ /**
32
+ * Return translated action labels
33
+ *
34
+ * @return array Action label translations
35
+ */
36
+ public static function get_action_labels() {
37
+ return array(
38
+ 'updated' => __( 'Updated', 'stream' ),
39
+ 'created' => __( 'Created', 'stream' ),
40
+ 'trashed' => __( 'Trashed', 'stream' ),
41
+ 'untrashed' => __( 'Restored', 'stream' ),
42
+ 'deleted' => __( 'Deleted', 'stream' ),
43
+ );
44
+ }
45
+
46
+ /**
47
+ * Return translated context labels
48
+ *
49
+ * @return array Context label translations
50
+ */
51
+ public static function get_context_labels() {
52
+ global $wp_post_types;
53
+
54
+ $post_types = wp_filter_object_list( $wp_post_types, array(), null, 'label' );
55
+ $post_types = array_diff_key( $post_types, array_flip( self::get_excluded_post_types() ) );
56
+
57
+ add_action( 'registered_post_type', array( __CLASS__, '_registered_post_type' ), 10, 2 );
58
+
59
+ return $post_types;
60
+ }
61
+
62
+ /**
63
+ * Add action links to Stream drop row in admin list screen
64
+ *
65
+ * @filter wp_stream_action_links_{connector}
66
+ *
67
+ * @param array $links Previous links registered
68
+ * @param object $record Stream record
69
+ *
70
+ * @return array Action links
71
+ */
72
+ public static function action_links( $links, $record ) {
73
+ $post = get_post( $record->object_id );
74
+
75
+ if ( $post && $post->post_status === wp_stream_get_meta( $record, 'new_status', true ) ) {
76
+ $post_type_name = self::get_post_type_name( get_post_type( $post->ID ) );
77
+
78
+ if ( 'trash' === $post->post_status ) {
79
+ $untrash = wp_nonce_url(
80
+ add_query_arg(
81
+ array(
82
+ 'action' => 'untrash',
83
+ 'post' => $post->ID,
84
+ ),
85
+ admin_url( 'post.php' )
86
+ ),
87
+ sprintf( 'untrash-post_%d', $post->ID )
88
+ );
89
+
90
+ $delete = wp_nonce_url(
91
+ add_query_arg(
92
+ array(
93
+ 'action' => 'delete',
94
+ 'post' => $post->ID,
95
+ ),
96
+ admin_url( 'post.php' )
97
+ ),
98
+ sprintf( 'delete-post_%d', $post->ID )
99
+ );
100
+
101
+ $links[ sprintf( esc_html_x( 'Restore %s', 'Post type singular name', 'stream' ), $post_type_name ) ] = $untrash;
102
+ $links[ sprintf( esc_html_x( 'Delete %s Permenantly', 'Post type singular name', 'stream' ), $post_type_name ) ] = $delete;
103
+ } else {
104
+ $links[ sprintf( esc_html_x( 'Edit %s', 'Post type singular name', 'stream' ), $post_type_name ) ] = get_edit_post_link( $post->ID );
105
+
106
+ if ( $view_link = get_permalink( $post->ID ) ) {
107
+ $links[ esc_html__( 'View', 'stream' ) ] = $view_link;
108
+ }
109
+
110
+ $revision_id = absint( wp_stream_get_meta( $record, 'revision_id', true ) );
111
+ $revision_id = self::get_adjacent_post_revision( $revision_id, false );
112
+
113
+ if ( $revision_id ) {
114
+ $links[ esc_html__( 'Revision', 'stream' ) ] = get_edit_post_link( $revision_id );
115
+ }
116
+ }
117
+ }
118
+
119
+ return $links;
120
+ }
121
+
122
+ /**
123
+ * Catch registeration of post_types after initial loading, to cache its labels
124
+ *
125
+ * @action registered_post_type
126
+ *
127
+ * @param string $post_type Post type slug
128
+ * @param array $args Arguments used to register the post type
129
+ */
130
+ public static function _registered_post_type( $post_type, $args ) {
131
+ $post_type_obj = get_post_type_object( $post_type );
132
+ $label = $post_type_obj->label;
133
+
134
+ WP_Stream_Connectors::$term_labels['stream_context'][ $post_type ] = $label;
135
+ }
136
+
137
+ /**
138
+ * Log all post status changes ( creating / updating / trashing )
139
+ *
140
+ * @action transition_post_status
141
+ */
142
+ public static function callback_transition_post_status( $new, $old, $post ) {
143
+ if ( in_array( $post->post_type, self::get_excluded_post_types() ) ) {
144
+ return;
145
+ }
146
+
147
+ if ( in_array( $new, array( 'auto-draft', 'inherit' ) ) ) {
148
+ return;
149
+ } elseif ( 'auto-draft' === $old && 'draft' === $new ) {
150
+ $summary = _x(
151
+ '"%1$s" %2$s drafted',
152
+ '1: Post title, 2: Post type singular name',
153
+ 'stream'
154
+ );
155
+ $action = 'created';
156
+ } elseif ( 'auto-draft' === $old && ( in_array( $new, array( 'publish', 'private' ) ) ) ) {
157
+ $summary = _x(
158
+ '"%1$s" %2$s published',
159
+ '1: Post title, 2: Post type singular name',
160
+ 'stream'
161
+ );
162
+ $action = 'created';
163
+ } elseif ( 'draft' === $old && ( in_array( $new, array( 'publish', 'private' ) ) ) ) {
164
+ $summary = _x(
165
+ '"%1$s" %2$s published',
166
+ '1: Post title, 2: Post type singular name',
167
+ 'stream'
168
+ );
169
+ } elseif ( 'publish' === $old && ( in_array( $new, array( 'draft' ) ) ) ) {
170
+ $summary = _x(
171
+ '"%1$s" %2$s unpublished',
172
+ '1: Post title, 2: Post type singular name',
173
+ 'stream'
174
+ );
175
+ } elseif ( 'trash' === $new ) {
176
+ $summary = _x(
177
+ '"%1$s" %2$s trashed',
178
+ '1: Post title, 2: Post type singular name',
179
+ 'stream'
180
+ );
181
+ $action = 'trashed';
182
+ } elseif ( 'trash' === $old && 'trash' !== $new ) {
183
+ $summary = _x(
184
+ '"%1$s" %2$s restored from trash',
185
+ '1: Post title, 2: Post type singular name',
186
+ 'stream'
187
+ );
188
+ $action = 'untrashed';
189
+ } else {
190
+ $summary = _x(
191
+ '"%1$s" %2$s updated',
192
+ '1: Post title, 2: Post type singular name',
193
+ 'stream'
194
+ );
195
+ }
196
+
197
+ if ( empty( $action ) ) {
198
+ $action = 'updated';
199
+ }
200
+
201
+ $revision_id = null;
202
+
203
+ if ( wp_revisions_enabled( $post ) ) {
204
+ $revision = get_children(
205
+ array(
206
+ 'post_type' => 'revision',
207
+ 'post_status' => 'inherit',
208
+ 'post_parent' => $post->ID,
209
+ 'posts_per_page' => 1,
210
+ 'orderby' => 'post_date',
211
+ 'order' => 'DESC',
212
+ )
213
+ );
214
+
215
+ if ( $revision ) {
216
+ $revision = array_values( $revision );
217
+ $revision_id = $revision[0]->ID;
218
+ }
219
+ }
220
+
221
+ $post_type_name = strtolower( self::get_post_type_name( $post->post_type ) );
222
+
223
+ self::log(
224
+ $summary,
225
+ array(
226
+ 'post_title' => $post->post_title,
227
+ 'singular_name' => $post_type_name,
228
+ 'new_status' => $new,
229
+ 'old_status' => $old,
230
+ 'revision_id' => $revision_id,
231
+ ),
232
+ $post->ID,
233
+ $post->post_type,
234
+ $action
235
+ );
236
+ }
237
+
238
+ /**
239
+ * Log post deletion
240
+ *
241
+ * @action deleted_post
242
+ */
243
+ public static function callback_deleted_post( $post_id ) {
244
+ $post = get_post( $post_id );
245
+
246
+ // We check if post is an instance of WP_Post as it doesn't always resolve in unit testing
247
+ if ( ! ( $post instanceof WP_Post ) || in_array( $post->post_type, self::get_excluded_post_types() ) ) {
248
+ return;
249
+ }
250
+
251
+ // Ignore auto-drafts that are deleted by the system, see issue-293
252
+ if ( 'auto-draft' === $post->post_status ) {
253
+ return;
254
+ }
255
+
256
+ $post_type_name = strtolower( self::get_post_type_name( $post->post_type ) );
257
+
258
+ self::log(
259
+ _x(
260
+ '"%1$s" %2$s deleted from trash',
261
+ '1: Post title, 2: Post type singular name',
262
+ 'stream'
263
+ ),
264
+ array(
265
+ 'post_title' => $post->post_title,
266
+ 'singular_name' => $post_type_name,
267
+ ),
268
+ $post->ID,
269
+ $post->post_type,
270
+ 'deleted'
271
+ );
272
+ }
273
+
274
+ /**
275
+ * Constructs list of excluded post types for the Posts connector
276
+ *
277
+ * @return array List of excluded post types
278
+ */
279
+ public static function get_excluded_post_types() {
280
+ return apply_filters(
281
+ 'wp_stream_posts_exclude_post_types',
282
+ array(
283
+ 'nav_menu_item',
284
+ 'attachment',
285
+ 'revision',
286
+ )
287
+ );
288
+ }
289
+
290
+ /**
291
+ * Gets the singular post type label
292
+ *
293
+ * @param string $post_type_slug
294
+ * @return string Post type label
295
+ */
296
+ public static function get_post_type_name( $post_type_slug ) {
297
+ $name = __( 'Post', 'stream' ); // Default
298
+
299
+ if ( post_type_exists( $post_type_slug ) ) {
300
+ $post_type = get_post_type_object( $post_type_slug );
301
+ $name = $post_type->labels->singular_name;
302
+ }
303
+
304
+ return $name;
305
+ }
306
+
307
+ /**
308
+ * Get an adjacent post revision ID
309
+ *
310
+ * @param int $revision_id
311
+ * @param bool $previous
312
+ *
313
+ * @return int $revision_id
314
+ */
315
+ public static function get_adjacent_post_revision( $revision_id, $previous = true ) {
316
+ if ( empty( $revision_id ) || ! wp_is_post_revision( $revision_id ) ) {
317
+ return false;
318
+ }
319
+
320
+ $revision = wp_get_post_revision( $revision_id );
321
+ $operator = ( $previous ) ? '<' : '>';
322
+ $order = ( $previous ) ? 'DESC' : 'ASC';
323
+
324
+ global $wpdb;
325
+
326
+ $revision_id = $wpdb->get_var(
327
+ $wpdb->prepare( "
328
+ SELECT p.ID
329
+ FROM $wpdb->posts AS p
330
+ WHERE p.post_date {$operator} %s
331
+ AND p.post_type = 'revision'
332
+ AND p.post_parent = %d
333
+ ORDER BY p.post_date {$order}
334
+ LIMIT 1
335
+ ",
336
+ $revision->post_date,
337
+ $revision->post_parent
338
+ )
339
+ );
340
+
341
+ $revision_id = absint( $revision_id );
342
+
343
+ if ( ! wp_is_post_revision( $revision_id ) ) {
344
+ return false;
345
+ }
346
+
347
+ return $revision_id;
348
+ }
349
+
350
+ }
connectors/class-wp-stream-connector-settings.php ADDED
@@ -0,0 +1,702 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class WP_Stream_Connector_Settings extends WP_Stream_Connector {
4
+
5
+ /**
6
+ * Prefix for the highlight URL hash
7
+ *
8
+ * @const string
9
+ */
10
+ const HIGHLIGHT_FIELD_URL_HASH_PREFIX = 'wp-stream-highlight:';
11
+
12
+ /**
13
+ * Connector slug
14
+ *
15
+ * @var string
16
+ */
17
+ public static $name = 'settings';
18
+
19
+ /**
20
+ * Actions registered for this connector
21
+ *
22
+ * @var array
23
+ */
24
+ public static $actions = array(
25
+ 'whitelist_options',
26
+ 'update_site_option',
27
+ 'update_option_permalink_structure',
28
+ 'update_option_category_base',
29
+ 'update_option_tag_base',
30
+ );
31
+
32
+ /**
33
+ * Option names used in options-permalink.php
34
+ *
35
+ * @var array
36
+ */
37
+ public static $permalink_options = array(
38
+ 'permalink_structure',
39
+ 'category_base',
40
+ 'tag_base',
41
+ );
42
+
43
+ /**
44
+ * Option names used in network/settings.php
45
+ *
46
+ * @var array
47
+ */
48
+ public static $network_options = array(
49
+ 'registrationnotification',
50
+ 'registration',
51
+ 'add_new_users',
52
+ 'menu_items',
53
+ 'upload_space_check_disabled',
54
+ 'blog_upload_space',
55
+ 'upload_filetypes',
56
+ 'site_name',
57
+ 'first_post',
58
+ 'first_page',
59
+ 'first_comment',
60
+ 'first_comment_url',
61
+ 'first_comment_author',
62
+ 'welcome_email',
63
+ 'welcome_user_email',
64
+ 'fileupload_maxk',
65
+ 'global_terms_enabled',
66
+ 'illegal_names',
67
+ 'limited_email_domains',
68
+ 'banned_email_domains',
69
+ 'WPLANG',
70
+ 'admin_email',
71
+ 'user_count',
72
+ );
73
+
74
+ /**
75
+ * Register all context hooks
76
+ *
77
+ * @return void
78
+ */
79
+ public static function register() {
80
+ parent::register();
81
+
82
+ add_action( 'admin_head', array( __CLASS__, 'highlight_field' ) );
83
+ add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_jquery_color' ) );
84
+ add_action( sprintf( 'update_option_theme_mods_%s', get_option( 'stylesheet' ) ), array( __CLASS__, 'log_theme_modification' ), 10, 2 );
85
+ }
86
+
87
+ /**
88
+ * @action update_option_theme_mods_{name}
89
+ */
90
+ public static function log_theme_modification( $old_value, $new_value ) {
91
+ self::callback_updated_option( 'theme_mods', $old_value, $new_value );
92
+ }
93
+
94
+ /**
95
+ * Return translated context label
96
+ *
97
+ * @return string Translated context label
98
+ */
99
+ public static function get_label() {
100
+ return __( 'Settings', 'stream' );
101
+ }
102
+
103
+ /**
104
+ * Return translated action labels
105
+ *
106
+ * @return array Action label translations
107
+ */
108
+ public static function get_action_labels() {
109
+ return array(
110
+ 'updated' => __( 'Updated', 'stream' ),
111
+ );
112
+ }
113
+
114
+ /**
115
+ * Return translated context labels
116
+ *
117
+ * @return array Context label translations
118
+ */
119
+ public static function get_context_labels() {
120
+ $context_labels = array(
121
+ 'settings' => __( 'Settings', 'stream' ),
122
+ 'general' => __( 'General', 'stream' ),
123
+ 'writing' => __( 'Writing', 'stream' ),
124
+ 'reading' => __( 'Reading', 'stream' ),
125
+ 'discussion' => __( 'Discussion', 'stream' ),
126
+ 'media' => __( 'Media', 'stream' ),
127
+ 'permalink' => __( 'Permalinks', 'stream' ),
128
+ 'network' => __( 'Network', 'stream' ),
129
+ 'wp_stream' => __( 'Stream', 'stream' ),
130
+ 'custom_background' => __( 'Custom Background', 'stream' ),
131
+ 'custom_header' => __( 'Custom Header', 'stream' ),
132
+ );
133
+
134
+ if ( is_network_admin() ) {
135
+ $context_labels = array_merge(
136
+ $context_labels,
137
+ array(
138
+ 'wp_stream_network' => __( 'Stream Network', 'stream' ),
139
+ 'wp_stream_defaults' => __( 'Stream Defaults', 'stream' ),
140
+ )
141
+ );
142
+ }
143
+
144
+ return $context_labels;
145
+ }
146
+
147
+ /**
148
+ * Return context by option name and key
149
+ *
150
+ * @return string Context slug
151
+ */
152
+ public static function get_context_by_key( $option_name, $key ) {
153
+ $contexts = array(
154
+ 'theme_mods' => array(
155
+ 'custom_background' => array(
156
+ 'background_image',
157
+ 'background_position_x',
158
+ 'background_repeat',
159
+ 'background_attachment',
160
+ 'background_color',
161
+ ),
162
+ 'custom_header' => array(
163
+ 'header_image',
164
+ 'header_textcolor',
165
+ ),
166
+ ),
167
+ );
168
+
169
+ if ( isset( $contexts[ $option_name ] ) ) {
170
+ foreach ( $contexts[ $option_name ] as $context => $keys ) {
171
+ if ( in_array( $key, $keys ) ) {
172
+ return $context;
173
+ }
174
+ }
175
+ }
176
+
177
+ return false;
178
+ }
179
+
180
+ /**
181
+ * Find out if the option key should be ignored and not logged
182
+ *
183
+ * @return bool Whether option key is ignored or not
184
+ */
185
+ public static function is_key_ignored( $option_name, $key ) {
186
+ $ignored = array(
187
+ 'theme_mods' => array(
188
+ 'background_image_thumb',
189
+ 'header_image_data',
190
+ ),
191
+ );
192
+
193
+ if ( isset( $ignored[ $option_name ] ) ) {
194
+ return in_array( $key, $ignored[ $option_name ] );
195
+ }
196
+
197
+ return false;
198
+ }
199
+
200
+ /**
201
+ * Find out if array keys in the option should be logged separately
202
+ *
203
+ * @return bool Whether the option should be treated as a group
204
+ */
205
+ public static function is_key_option_group( $key, $old_value, $value ) {
206
+ $not_grouped = array(
207
+ 'wp_stream_connected_sites'
208
+ );
209
+
210
+ if ( in_array( $key, $not_grouped ) ) {
211
+ return false;
212
+ }
213
+
214
+ if ( ! is_array( $old_value ) && ! is_array( $value ) ) {
215
+ return false;
216
+ }
217
+
218
+ if ( 0 === count( array_filter( array_keys( $value ), 'is_string' ) ) ) {
219
+ return false;
220
+ }
221
+
222
+ return true;
223
+ }
224
+
225
+ /**
226
+ * Return translated labels for all default Settings fields found in WordPress.
227
+ *
228
+ * @return array Field label translations
229
+ */
230
+ public static function get_field_label( $field_key ) {
231
+ $labels = array(
232
+ // General
233
+ 'blogname' => __( 'Site Title', 'stream' ),
234
+ 'blogdescription' => __( 'Tagline', 'stream' ),
235
+ 'siteurl' => __( 'WordPress Address (URL)', 'stream' ),
236
+ 'home' => __( 'Site Address (URL)', 'stream' ),
237
+ 'admin_email' => __( 'E-mail Address', 'stream' ),
238
+ 'users_can_register' => __( 'Membership', 'stream' ),
239
+ 'default_role' => __( 'New User Default Role', 'stream' ),
240
+ 'timezone_string' => __( 'Timezone', 'stream' ),
241
+ 'date_format' => __( 'Date Format', 'stream' ),
242
+ 'time_format' => __( 'Time Format', 'stream' ),
243
+ 'start_of_week' => __( 'Week Starts On', 'stream' ),
244
+ // Writing
245
+ 'use_smilies' => __( 'Formatting', 'stream' ),
246
+ 'use_balanceTags' => __( 'Formatting', 'stream' ),
247
+ 'default_category' => __( 'Default Post Category', 'stream' ),
248
+ 'default_post_format' => __( 'Default Post Format', 'stream' ),
249
+ 'mailserver_url' => __( 'Mail Server', 'stream' ),
250
+ 'mailserver_login' => __( 'Login Name', 'stream' ),
251
+ 'mailserver_pass' => __( 'Password', 'stream' ),
252
+ 'default_email_category' => __( 'Default Mail Category', 'stream' ),
253
+ 'ping_sites' => __( 'Update Services', 'stream' ),
254
+ // Reading
255
+ 'show_on_front' => __( 'Front page displays', 'stream' ),
256
+ 'page_on_front' => __( 'Front page displays', 'stream' ),
257
+ 'page_for_posts' => __( 'Front page displays', 'stream' ),
258
+ 'posts_per_page' => __( 'Blog pages show at most', 'stream' ),
259
+ 'posts_per_rss' => __( 'Syndication feeds show the most recent', 'stream' ),
260
+ 'rss_use_excerpt' => __( 'For each article in a feed, show', 'stream' ),
261
+ 'blog_public' => __( 'Search Engine Visibility', 'stream' ),
262
+ // Discussion
263
+ 'default_pingback_flag' => __( 'Default article settings', 'stream' ),
264
+ 'default_ping_status' => __( 'Default article settings', 'stream' ),
265
+ 'default_comment_status' => __( 'Default article settings', 'stream' ),
266
+ 'require_name_email' => __( 'Other comment settings', 'stream' ),
267
+ 'comment_registration' => __( 'Other comment settings', 'stream' ),
268
+ 'close_comments_for_old_posts' => __( 'Other comment settings', 'stream' ),
269
+ 'close_comments_days_old' => __( 'Other comment settings', 'stream' ),
270
+ 'thread_comments' => __( 'Other comment settings', 'stream' ),
271
+ 'thread_comments_depth' => __( 'Other comment settings', 'stream' ),
272
+ 'page_comments' => __( 'Other comment settings', 'stream' ),
273
+ 'comments_per_page' => __( 'Other comment settings', 'stream' ),
274
+ 'default_comments_page' => __( 'Other comment settings', 'stream' ),
275
+ 'comment_order' => __( 'Other comment settings', 'stream' ),
276
+ 'comments_notify' => __( 'E-mail me whenever', 'stream' ),
277
+ 'moderation_notify' => __( 'E-mail me whenever', 'stream' ),
278
+ 'comment_moderation' => __( 'Before a comment appears', 'stream' ),
279
+ 'comment_whitelist' => __( 'Before a comment appears', 'stream' ),
280
+ 'comment_max_links' => __( 'Comment Moderation', 'stream' ),
281
+ 'moderation_keys' => __( 'Comment Moderation', 'stream' ),
282
+ 'blacklist_keys' => __( 'Comment Blacklist', 'stream' ),
283
+ 'show_avatars' => __( 'Show Avatars', 'stream' ),
284
+ 'avatar_rating' => __( 'Maximum Rating', 'stream' ),
285
+ 'avatar_default' => __( 'Default Avatar', 'stream' ),
286
+ // Media
287
+ 'thumbnail_size_w' => __( 'Thumbnail size', 'stream' ),
288
+ 'thumbnail_size_h' => __( 'Thumbnail size', 'stream' ),
289
+ 'thumbnail_crop' => __( 'Thumbnail size', 'stream' ),
290
+ 'medium_size_w' => __( 'Medium size', 'stream' ),
291
+ 'medium_size_h' => __( 'Medium size', 'stream' ),
292
+ 'large_size_w' => __( 'Large size', 'stream' ),
293
+ 'large_size_h' => __( 'Large size', 'stream' ),
294
+ 'uploads_use_yearmonth_folders' => __( 'Uploading Files', 'stream' ),
295
+ // Permalinks
296
+ 'permalink_structure' => __( 'Permalink Settings', 'stream' ),
297
+ 'category_base' => __( 'Category base', 'stream' ),
298
+ 'tag_base' => __( 'Tag base', 'stream' ),
299
+ // Network
300
+ 'registrationnotification' => __( 'Registration notification', 'stream' ),
301
+ 'registration' => __( 'Allow new registrations', 'stream' ),
302
+ 'add_new_users' => __( 'Add New Users', 'stream' ),
303
+ 'menu_items' => __( 'Enable administration menus', 'stream' ),
304
+ 'upload_space_check_disabled' => __( 'Site upload space check', 'stream' ),
305
+ 'blog_upload_space' => __( 'Site upload space', 'stream' ),
306
+ 'upload_filetypes' => __( 'Upload file types', 'stream' ),
307
+ 'site_name' => __( 'Network Title', 'stream' ),
308
+ 'first_post' => __( 'First Post', 'stream' ),
309
+ 'first_page' => __( 'First Page', 'stream' ),
310
+ 'first_comment' => __( 'First Comment', 'stream' ),
311
+ 'first_comment_url' => __( 'First Comment URL', 'stream' ),
312
+ 'first_comment_author' => __( 'First Comment Author', 'stream' ),
313
+ 'welcome_email' => __( 'Welcome Email', 'stream' ),
314
+ 'welcome_user_email' => __( 'Welcome User Email', 'stream' ),
315
+ 'fileupload_maxk' => __( 'Max upload file size', 'stream' ),
316
+ 'global_terms_enabled' => __( 'Terms Enabled', 'stream' ),
317
+ 'illegal_names' => __( 'Banned Names', 'stream' ),
318
+ 'limited_email_domains' => __( 'Limited Email Registrations', 'stream' ),
319
+ 'banned_email_domains' => __( 'Banned Email Domains', 'stream' ),
320
+ 'WPLANG' => __( 'Network Language', 'stream' ),
321
+ 'admin_email' => __( 'Network Admin Email', 'stream' ),
322
+ 'user_count' => __( 'User Count', 'stream' ),
323
+ // Other
324
+ 'wp_stream_db' => __( 'Stream Database Version', 'stream' ),
325
+ 'wp_stream_connected_sites' => __( 'Stream Connected Sites', 'stream' ),
326
+ );
327
+
328
+ if ( isset( $labels[ $field_key ] ) ) {
329
+ return $labels[ $field_key ];
330
+ }
331
+
332
+ return $field_key;
333
+ }
334
+
335
+ /**
336
+ * Enqueue jQuery Color plugin
337
+ *
338
+ * @action admin_enqueue_scripts
339
+ * @return void
340
+ */
341
+ public static function enqueue_jquery_color() {
342
+ wp_enqueue_script( 'jquery-color' );
343
+ }
344
+
345
+ /**
346
+ * Return translated labels for all serialized Settings found in WordPress.
347
+ *
348
+ * @return string Field key translation or key itself if not found
349
+ */
350
+ public static function get_serialized_field_label( $option_name, $field_key ) {
351
+ $labels = array(
352
+ 'theme_mods' => array(
353
+ // Custom Background
354
+ 'background_image' => __( 'Background Image', 'stream' ),
355
+ 'background_position_x' => __( 'Background Position', 'stream' ),
356
+ 'background_repeat' => __( 'Background Repeat', 'stream' ),
357
+ 'background_attachment' => __( 'Background Attachment', 'stream' ),
358
+ 'background_color' => __( 'Background Color', 'stream' ),
359
+ // Custom Header
360
+ 'header_image' => __( 'Header Image', 'stream' ),
361
+ 'header_textcolor' => __( 'Text Color', 'stream' ),
362
+ ),
363
+ );
364
+
365
+ /**
366
+ * Filter allows for insertion of serialized labels
367
+ *
368
+ * @param array $lables Serialized labels
369
+ * @return array Updated array of serialzed labels
370
+ */
371
+ $labels = apply_filters( 'wp_stream_serialized_labels', $labels );
372
+
373
+ if ( isset( $labels[ $option_name ] ) && isset( $labels[ $option_name ][ $field_key ] ) ) {
374
+ return $labels[ $option_name ][ $field_key ];
375
+ }
376
+
377
+ return $field_key;
378
+ }
379
+
380
+ /**
381
+ * Add action links to Stream drop row in admin list screen
382
+ *
383
+ * @filter wp_stream_action_links_{connector}
384
+ *
385
+ * @param array $links Previous links registered
386
+ * @param object $record Stream record
387
+ *
388
+ * @return array Action links
389
+ */
390
+ public static function action_links( $links, $record ) {
391
+ $context_labels = self::get_context_labels();
392
+
393
+ $rules = array(
394
+ 'stream' => array(
395
+ 'menu_slug' => 'wp_stream',
396
+ 'submenu_slug' => WP_Stream_Admin::SETTINGS_PAGE_SLUG,
397
+ 'url' => function( $rule, $record ) {
398
+ $option_key = wp_stream_get_meta( $record, 'option_key', true );
399
+ $url_tab = null;
400
+
401
+ if ( '' !== $option_key ) {
402
+ foreach ( WP_Stream_Settings::get_fields() as $tab_name => $tab_properties ) {
403
+ foreach ( $tab_properties['fields'] as $field ) {
404
+ $field_key = sprintf( '%s_%s', $tab_name, $field['name'] );
405
+ if ( $field_key === $option_key ) {
406
+ $url_tab = $tab_name;
407
+ break 2;
408
+ }
409
+ }
410
+ }
411
+ }
412
+
413
+ return add_query_arg(
414
+ array(
415
+ 'page' => $rule['submenu_slug'],
416
+ 'tab' => $url_tab,
417
+ ),
418
+ admin_url( 'admin.php' )
419
+ );
420
+ },
421
+ 'applicable' => function( $submenu, $record ) {
422
+ return $record->context === 'wp_stream';
423
+ }
424
+ ),
425
+ 'background_header' => array(
426
+ 'menu_slug' => 'themes.php',
427
+ 'submenu_slug' => function( $record ) {
428
+ return str_replace( '_', '-', $record->context );
429
+ },
430
+ 'url' => function( $rule, $record ) {
431
+ return add_query_arg( 'page', $rule['submenu_slug']( $record ), admin_url( $rule['menu_slug'] ) );
432
+ },
433
+ 'applicable' => function( $submenu, $record ) {
434
+ return in_array( $record->context, array( 'custom_header', 'custom_background' ) );
435
+ }
436
+ ),
437
+ 'general' => array(
438
+ 'menu_slug' => 'options-general.php',
439
+ 'submenu_slug' => function( $record ) {
440
+ return sprintf( 'options-%s.php', $record->context );
441
+ },
442
+ 'url' => function( $rule, $record ) {
443
+ return admin_url( $rule['submenu_slug']( $record ) );
444
+ },
445
+ 'applicable' => function( $submenu, $record ) {
446
+ return ! empty( $submenu['options-general.php'] );
447
+ },
448
+ ),
449
+ 'network' => array(
450
+ 'menu_slug' => 'settings.php',
451
+ 'submenu_slug' => function( $record ) {
452
+ return 'settings.php';
453
+ },
454
+ 'url' => function( $rule, $record ) {
455
+ return network_admin_url( $rule['menu_slug'] );
456
+ },
457
+ 'applicable' => function( $submenu, $record ) {
458
+ if ( ! $record->blog_id ) {
459
+ return ! empty( $submenu['settings.php'] );
460
+ }
461
+ return false;
462
+ },
463
+ ),
464
+ );
465
+
466
+ if ( 'settings' !== $record->context && in_array( $record->context, array_keys( $context_labels ) ) ) {
467
+ global $submenu;
468
+
469
+ $applicable_rules = array_filter(
470
+ $rules,
471
+ function( $rule ) use ( $submenu, $record ) {
472
+ return call_user_func( $rule['applicable'], $submenu, $record );
473
+ }
474
+ );
475
+
476
+ if ( ! empty( $applicable_rules ) ) {
477
+ // The first applicable rule wins
478
+ $rule = array_shift( $applicable_rules );
479
+ $menu_slug = $rule['menu_slug'];
480
+ $submenu_slug = ( is_object( $rule['submenu_slug'] ) && $rule['submenu_slug'] instanceOf Closure ? $rule['submenu_slug']( $record ) : $rule['submenu_slug'] );
481
+ $url = $rule['url']( $rule, $record );
482
+
483
+ if ( isset( $submenu[ $menu_slug ] ) ) {
484
+ $found_submenus = wp_list_filter(
485
+ $submenu[ $menu_slug ],
486
+ array( 2 => $submenu_slug )
487
+ );
488
+ }
489
+
490
+ if ( ! empty( $found_submenus ) ) {
491
+ $target_submenu = array_pop( $found_submenus );
492
+ list( $menu_title, $capability ) = $target_submenu;
493
+
494
+ if ( current_user_can( $capability ) ) {
495
+ $url = apply_filters( 'wp_stream_action_link_url', $url, $record );
496
+ $text = sprintf( __( 'Edit %s Settings', 'stream' ), $context_labels[ $record->context ] );
497
+ $field_name = wp_stream_get_meta( $record, 'option_key', true );
498
+
499
+ if ( '' === $field_name ) {
500
+ $field_name = wp_stream_get_meta( $record, 'option', true );
501
+ }
502
+
503
+ if ( '' !== $field_name ) {
504
+ $url = sprintf( '%s#%s%s', rtrim( preg_replace( '/#.*/', '', $url ), '/' ), self::HIGHLIGHT_FIELD_URL_HASH_PREFIX, $field_name );
505
+ }
506
+
507
+ $links[ $text ] = $url;
508
+ }
509
+ }
510
+ }
511
+ }
512
+
513
+ return $links;
514
+ }
515
+
516
+ /**
517
+ * Trigger this connector core tracker, only on options.php page
518
+ *
519
+ * @action whitelist_options
520
+ */
521
+ public static function callback_whitelist_options( $options ) {
522
+ add_action( 'updated_option', array( __CLASS__, 'callback' ), 10, 3 );
523
+
524
+ return $options;
525
+ }
526
+
527
+ /**
528
+ * Trigger this connector core tracker, only on options-permalink.php page
529
+ *
530
+ * @action update_option_permalink_structure
531
+ */
532
+ public static function callback_update_option_permalink_structure( $old_value, $value ) {
533
+ self::callback_updated_option( 'permalink_structure', $old_value, $value );
534
+ }
535
+
536
+ /**
537
+ * Trigger this connector core tracker, only on network/settings.php page
538
+ *
539
+ * @action update_site_option
540
+ */
541
+ public static function callback_update_site_option( $option, $value, $old_value ) {
542
+ self::callback_updated_option( $option, $value, $old_value );
543
+ }
544
+
545
+ /**
546
+ * Trigger this connector core tracker, only on options-permalink.php page
547
+ *
548
+ * @action update_option_category_base
549
+ */
550
+ public static function callback_update_option_category_base( $old_value, $value ) {
551
+ self::callback_updated_option( 'category_base', $old_value, $value );
552
+ }
553
+
554
+ /**
555
+ * Trigger this connector core tracker, only on options-permalink.php page
556
+ *
557
+ * @action update_option_tag_base
558
+ */
559
+ public static function callback_update_option_tag_base( $old_value, $value ) {
560
+ self::callback_updated_option( 'tag_base', $old_value, $value );
561
+ }
562
+
563
+ /**
564
+ * Track updated settings
565
+ *
566
+ * @action updated_option
567
+ */
568
+ public static function callback_updated_option( $option, $old_value, $value ) {
569
+ global $whitelist_options, $new_whitelist_options;
570
+
571
+ if ( 0 === strpos( $option, '_transient_' ) || 0 === strpos( $option, '_site_transient_' ) ) {
572
+ return;
573
+ }
574
+
575
+ $options = array_merge(
576
+ (array) $whitelist_options,
577
+ (array) $new_whitelist_options,
578
+ array( 'permalink' => self::$permalink_options ),
579
+ array( 'network' => self::$network_options )
580
+ );
581
+
582
+ foreach ( $options as $key => $opts ) {
583
+ if ( in_array( $option, $opts ) ) {
584
+ $context = $key;
585
+ break;
586
+ }
587
+ }
588
+
589
+ if ( ! isset( $context ) ) {
590
+ $context = 'settings';
591
+ }
592
+
593
+ $changed_options = array();
594
+ $option_group = self::is_key_option_group( $option, $old_value, $value );
595
+
596
+ if ( $option_group ) {
597
+ foreach ( self::get_changed_keys( $old_value, $value ) as $field_key ) {
598
+ if ( ! self::is_key_ignored( $option, $field_key ) ) {
599
+ $key_context = self::get_context_by_key( $option, $field_key );
600
+ $changed_options[] = array(
601
+ 'label' => self::get_serialized_field_label( $option, $field_key ),
602
+ 'option' => $option,
603
+ 'option_key' => $field_key,
604
+ 'context' => ( false !== $key_context ? $key_context : $context ),
605
+ // Prevent fatal error when saving option as array
606
+ 'old_value' => isset( $old_value[ $field_key ] ) ? (string) maybe_serialize( $old_value[ $field_key ] ) : null,
607
+ 'value' => isset( $value[ $field_key ] ) ? (string) maybe_serialize( $value[ $field_key ] ) : null,
608
+ );
609
+ }
610
+ }
611
+ } else {
612
+ $changed_options[] = array(
613
+ 'label' => self::get_field_label( $option ),
614
+ 'option' => $option,
615
+ 'context' => $context,
616
+ // Prevent fatal error when saving option as array
617
+ 'old_value' => (string) maybe_serialize( $old_value ),
618
+ 'value' => (string) maybe_serialize( $value ),
619
+ );
620
+ }
621
+
622
+ foreach ( $changed_options as $properties ) {
623
+ self::log(
624
+ __( '"%s" setting was updated', 'stream' ),
625
+ $properties,
626
+ null,
627
+ $properties['context'],
628
+ 'updated'
629
+ );
630
+ }
631
+ }
632
+
633
+ /**
634
+ * Add class to highlight field by URL param
635
+ *
636
+ * @action admin_head
637
+ */
638
+ public static function highlight_field() {
639
+ ?>
640
+ <script>
641
+ (function ($) {
642
+ $(function () {
643
+ var hashPrefix = <?php echo json_encode( self::HIGHLIGHT_FIELD_URL_HASH_PREFIX ) ?>,
644
+ hashFieldName = "",
645
+ fieldNames = [],
646
+ $select2Choices = {},
647
+ $field = {};
648
+
649
+ if (location.hash.substr(1, hashPrefix.length) === hashPrefix) {
650
+ hashFieldName = location.hash.substr(hashPrefix.length + 1);
651
+ fieldNames = [hashFieldName];
652
+
653
+ $field = $("input, textarea, select").filter(function () {
654
+ return fieldNames.indexOf( $(this).attr("name") ) > -1;
655
+ });
656
+
657
+ // try to find wp_stream field
658
+ if ( $field.length === 0 ) {
659
+ fieldNames = [
660
+ "wp_stream_" + hashFieldName,
661
+ "wp_stream[" + hashFieldName + "]"
662
+ ];
663
+
664
+ $field = $("input, textarea, select, div").filter(function () {
665
+ return fieldNames.indexOf( $(this).attr("id") ) > -1;
666
+ });
667
+
668
+ // if the field has been selectified, the list is the one to be colorized
669
+ $select2Choices = $field.find(".select2-choices");
670
+ if ( $select2Choices.length === 1 ) {
671
+ $field = $select2Choices;
672
+ }
673
+ }
674
+
675
+ $("html, body")
676
+ .animate({
677
+ scrollTop: ($field.closest("tr").length === 1 ? $field.closest("tr") : $field).offset().top - $("#wpadminbar").height()
678
+ }, 1000, function () {
679
+
680
+ $field
681
+ .css("background", $(this).css("background-color"))
682
+ .animate({
683
+ backgroundColor: "#fffedf",
684
+ }, 250);
685
+
686
+ $("label")
687
+ .filter(function () {
688
+ return fieldNames.indexOf( $(this).attr("for") ) > -1;
689
+ })
690
+ .animate({
691
+ color: "#d54e21"
692
+ }, 250);
693
+ }
694
+ );
695
+ }
696
+ });
697
+ }(jQuery));
698
+ </script>
699
+ <?php
700
+ }
701
+
702
+ }
connectors/class-wp-stream-connector-stream.php ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class WP_Stream_Connector_Stream extends WP_Stream_Connector {
4
+
5
+ /**
6
+ * Connector name/slug
7
+ *
8
+ * @var string
9
+ */
10
+ public static $name = 'stream';
11
+
12
+ /**
13
+ * Actions registered for this connector
14
+ *
15
+ * @var array
16
+ */
17
+ public static $actions = array(
18
+ 'wp_stream_site_connected',
19
+ 'wp_stream_site_disconnected',
20
+ );
21
+
22
+ /**
23
+ * Return translated connector label
24
+ *
25
+ * @return string
26
+ */
27
+ public static function get_label() {
28
+ return __( 'Stream', 'stream' );
29
+ }
30
+
31
+ /**
32
+ * Return translated context labels
33
+ *
34
+ * @return array
35
+ */
36
+ public static function get_context_labels() {
37
+ return array(
38
+ 'site' => __( 'Site', 'stream' ),
39
+ );
40
+ }
41
+
42
+ /**
43
+ * Return translated action labels
44
+ *
45
+ * @return array
46
+ */
47
+ public static function get_action_labels() {
48
+ return array(
49
+ 'connected' => __( 'Connected', 'stream' ),
50
+ 'disconnected' => __( 'Disconnected', 'stream' ),
51
+ );
52
+ }
53
+
54
+ /**
55
+ * Site is connected
56
+ *
57
+ * @param string $site_uuid
58
+ * @param string $api_key
59
+ * @param int $blog_id
60
+ *
61
+ * @return void
62
+ */
63
+ public static function callback_wp_stream_site_connected( $site_uuid, $api_key, $blog_id ) {
64
+ self::log(
65
+ __( 'Site connected to Stream', 'stream' ),
66
+ array(
67
+ 'site_uuid' => $site_uuid,
68
+ 'api_key' => $api_key,
69
+ ),
70
+ $blog_id,
71
+ 'site',
72
+ 'connected'
73
+ );
74
+ }
75
+
76
+ /**
77
+ * Site is disconnected
78
+ *
79
+ * @param string $site_uuid
80
+ * @param string $api_key
81
+ * @param int $blog_id
82
+ *
83
+ * @return void
84
+ */
85
+ public static function callback_wp_stream_site_disconnected( $site_uuid, $api_key, $blog_id ) {
86
+ self::log(
87
+ __( 'Site disconnected from Stream', 'stream' ),
88
+ array(
89
+ 'site_uuid' => $site_uuid,
90
+ 'api_key' => $api_key,
91
+ ),
92
+ $blog_id,
93
+ 'site',
94
+ 'disconnected'
95
+ );
96
+ }
97
+
98
+ }
connectors/class-wp-stream-connector-taxonomies.php ADDED
@@ -0,0 +1,225 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class WP_Stream_Connector_Taxonomies extends WP_Stream_Connector {
4
+
5
+ /**
6
+ * Connector slug
7
+ *
8
+ * @var string
9
+ */
10
+ public static $name = 'taxonomies';
11
+
12
+ /**
13
+ * Actions registered for this connector
14
+ *
15
+ * @var array
16
+ */
17
+ public static $actions = array(
18
+ 'created_term',
19
+ 'delete_term',
20
+ 'edit_term',
21
+ 'edited_term',
22
+ );
23
+
24
+ /**
25
+ * Cache term values before update, used by callback_edit_term/callback_edited_term
26
+ *
27
+ * @var Object
28
+ */
29
+ public static $cached_term_before_update;
30
+
31
+ /**
32
+ * Cache taxonomy labels
33
+ *
34
+ * @var array
35
+ */
36
+ public static $context_labels;
37
+
38
+ /**
39
+ * Return translated connector label
40
+ *
41
+ * @return string Translated connector label
42
+ */
43
+ public static function get_label() {
44
+ return __( 'Taxonomies', 'stream' );
45
+ }
46
+
47
+ /**
48
+ * Return translated action labels
49
+ *
50
+ * @return array Action label translations
51
+ */
52
+ public static function get_action_labels() {
53
+ return array(
54
+ 'created' => __( 'Created', 'stream' ),
55
+ 'updated' => __( 'Updated', 'stream' ),
56
+ 'deleted' => __( 'Deleted', 'stream' ),
57
+ );
58
+ }
59
+
60
+ /**
61
+ * Return translated context labels
62
+ *
63
+ * @return array Context label translations
64
+ */
65
+ public static function get_context_labels() {
66
+ global $wp_taxonomies;
67
+
68
+ $labels = wp_list_pluck( $wp_taxonomies, 'labels' );
69
+
70
+ self::$context_labels = wp_list_pluck( $labels, 'singular_name' );
71
+
72
+ add_action( 'registered_taxonomy', array( __CLASS__, '_registered_taxonomy' ), 10, 3 );
73
+
74
+ return self::$context_labels;
75
+ }
76
+
77
+ /**
78
+ * Add action links to Stream drop row in admin list screen
79
+ *
80
+ * @filter wp_stream_action_links_{connector}
81
+ *
82
+ * @param array $links Previous links registered
83
+ * @param object $record Stream record
84
+ *
85
+ * @return array Action links
86
+ */
87
+ public static function action_links( $links, $record ) {
88
+ if ( $record->object_id && 'deleted' !== $record->action && ( $term = get_term_by( 'term_taxonomy_id', $record->object_id, $record->context ) ) ) {
89
+ if ( ! is_wp_error( $term ) ) {
90
+ $tax_obj = get_taxonomy( $term->taxonomy );
91
+ $tax_label = isset( $tax_obj->labels->singular_name ) ? $tax_obj->labels->singular_name : null;
92
+
93
+ $links[ sprintf( _x( 'Edit %s', 'Term singular name', 'stream' ), $tax_label ) ] = get_edit_term_link( $term->term_id, $term->taxonomy );
94
+ $links[ __( 'View', 'stream' ) ] = get_term_link( $term->term_id, $term->taxonomy );
95
+ }
96
+ }
97
+
98
+ return $links;
99
+ }
100
+
101
+ /**
102
+ * Catch registration of taxonomies after inital loading, so we can cache its labels
103
+ *
104
+ * @action registered_taxonomy
105
+ *
106
+ * @param string $taxonomy Taxonomy slug
107
+ * @param array|string $object_type Object type or array of object types
108
+ * @param array|string $args Array or string of taxonomy registration arguments
109
+ */
110
+ public static function _registered_taxonomy( $taxonomy, $object_type, $args ) {
111
+ $taxonomy_obj = (object) $args;
112
+ $label = get_taxonomy_labels( $taxonomy_obj )->name;
113
+
114
+ self::$context_labels[ $taxonomy ] = $label;
115
+
116
+ WP_Stream_Connectors::$term_labels['stream_context'][ $taxonomy ] = $label;
117
+ }
118
+
119
+ /**
120
+ * Tracks creation of terms
121
+ *
122
+ * @action created_term
123
+ */
124
+ public static function callback_created_term( $term_id, $tt_id, $taxonomy ) {
125
+ if ( in_array( $taxonomy, self::get_excluded_taxonomies() ) ) {
126
+ return;
127
+ }
128
+
129
+ $term = get_term( $term_id, $taxonomy );
130
+ $term_name = $term->name;
131
+ $taxonomy_label = strtolower( self::$context_labels[ $taxonomy ] );
132
+ $term_parent = $term->parent;
133
+
134
+ self::log(
135
+ _x(
136
+ '"%1$s" %2$s created',
137
+ '1: Term name, 2: Taxonomy singular label',
138
+ 'stream'
139
+ ),
140
+ compact( 'term_name', 'taxonomy_label', 'term_id', 'taxonomy', 'term_parent' ),
141
+ $tt_id,
142
+ $taxonomy,
143
+ 'created'
144
+ );
145
+ }
146
+
147
+ /**
148
+ * Tracks deletion of taxonomy terms
149
+ *
150
+ * @action delete_term
151
+ */
152
+ public static function callback_delete_term( $term_id, $tt_id, $taxonomy, $deleted_term ) {
153
+ if ( in_array( $taxonomy, self::get_excluded_taxonomies() ) ) {
154
+ return;
155
+ }
156
+
157
+ $term_name = $deleted_term->name;
158
+ $term_parent = $deleted_term->parent;
159
+ $taxonomy_label = strtolower( self::$context_labels[ $taxonomy ] );
160
+
161
+ self::log(
162
+ _x(
163
+ '"%1$s" %2$s deleted',
164
+ '1: Term name, 2: Taxonomy singular label',
165
+ 'stream'
166
+ ),
167
+ compact( 'term_name', 'taxonomy_label', 'term_id', 'taxonomy', 'term_parent' ),
168
+ $tt_id,
169
+ $taxonomy,
170
+ 'deleted'
171
+ );
172
+ }
173
+
174
+ /**
175
+ * Tracks updates of taxonomy terms
176
+ *
177
+ * @action edit_term
178
+ */
179
+ public static function callback_edit_term( $term_id, $tt_id, $taxonomy ) {
180
+ self::$cached_term_before_update = get_term( $term_id, $taxonomy );
181
+ }
182
+
183
+ public static function callback_edited_term( $term_id, $tt_id, $taxonomy ) {
184
+ if ( in_array( $taxonomy, self::get_excluded_taxonomies() ) ) {
185
+ return;
186
+ }
187
+
188
+ $term = self::$cached_term_before_update;
189
+
190
+ if ( ! $term ) { // For some reason!
191
+ $term = get_term( $term_id, $taxonomy );
192
+ }
193
+
194
+ $term_name = $term->name;
195
+ $taxonomy_label = strtolower( self::$context_labels[ $taxonomy ] );
196
+ $term_parent = $term->parent;
197
+
198
+ self::log(
199
+ _x(
200
+ '"%1$s" %2$s updated',
201
+ '1: Term name, 2: Taxonomy singular label',
202
+ 'stream'
203
+ ),
204
+ compact( 'term_name', 'taxonomy_label', 'term_id', 'taxonomy', 'term_parent' ),
205
+ $tt_id,
206
+ $taxonomy,
207
+ 'updated'
208
+ );
209
+ }
210
+
211
+ /**
212
+ * Constructs list of excluded taxonomies for the Taxonomies connector
213
+ *
214
+ * @return array List of excluded taxonomies
215
+ */
216
+ public static function get_excluded_taxonomies() {
217
+ return apply_filters(
218
+ 'wp_stream_taxonomies_exclude_taxonomies',
219
+ array(
220
+ 'nav_menu',
221
+ )
222
+ );
223
+ }
224
+
225
+ }
connectors/class-wp-stream-connector-users.php ADDED
@@ -0,0 +1,340 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class WP_Stream_Connector_Users extends WP_Stream_Connector {
4
+
5
+ /**
6
+ * Connector slug
7
+ *
8
+ * @var string
9
+ */
10
+ public static $name = 'users';
11
+
12
+ /**
13
+ * Stores users object before the user being deleted.
14
+ *
15
+ * @var array
16
+ * @access protected
17
+ */
18
+ protected static $_users_object_pre_deleted = array();
19
+
20
+ /**
21
+ * Actions registered for this connector
22
+ *
23
+ * @var array
24
+ */
25
+ public static $actions = array(
26
+ 'user_register',
27
+ 'profile_update',
28
+ 'password_reset',
29
+ 'retrieve_password',
30
+ 'set_logged_in_cookie',
31
+ 'clear_auth_cookie',
32
+ 'delete_user',
33
+ 'deleted_user',
34
+ 'set_user_role',
35
+ );
36
+
37
+ /**
38
+ * Return translated connector label
39
+ *
40
+ * @return string Translated connector label
41
+ */
42
+ public static function get_label() {
43
+ return __( 'Users', 'stream' );
44
+ }
45
+
46
+ /**
47
+ * Return translated action term labels
48
+ *
49
+ * @return array Action terms label translation
50
+ */
51
+ public static function get_action_labels() {
52
+ return array(
53
+ 'updated' => __( 'Updated', 'stream' ),
54
+ 'created' => __( 'Created', 'stream' ),
55
+ 'deleted' => __( 'Deleted', 'stream' ),
56
+ 'password-reset' => __( 'Password Reset', 'stream' ),
57
+ 'forgot-password' => __( 'Lost Password', 'stream' ),
58
+ 'login' => __( 'Log In', 'stream' ),
59
+ 'logout' => __( 'Log Out', 'stream' ),
60
+ );
61
+ }
62
+
63
+ /**
64
+ * Return translated context labels
65
+ *
66
+ * @return array Context label translations
67
+ */
68
+ public static function get_context_labels() {
69
+ return array(
70
+ 'users' => __( 'Users', 'stream' ),
71
+ 'sessions' => __( 'Sessions', 'stream' ),
72
+ 'profiles' => __( 'Profiles', 'stream' ),
73
+ );
74
+ }
75
+
76
+ /**
77
+ * Add action links to Stream drop row in admin list screen
78
+ *
79
+ * @filter wp_stream_action_links_{connector}
80
+ *
81
+ * @param array $links Previous links registered
82
+ * @param object $record Stream record
83
+ *
84
+ * @return array Action links
85
+ */
86
+ public static function action_links( $links, $record ) {
87
+ if ( $record->object_id ) {
88
+ if ( $link = get_edit_user_link( $record->object_id ) ) {
89
+ $links [ __( 'Edit User', 'stream' ) ] = $link;
90
+ }
91
+ }
92
+
93
+ return $links;
94
+ }
95
+
96
+ /**
97
+ * Get an array of role lables assigned to a specific user.
98
+ *
99
+ * @param object|int $user User object or user ID to get roles for
100
+ * @return array $labels An array of role labels
101
+ */
102
+ public static function get_role_labels( $user ) {
103
+ if ( is_int( $user ) ) {
104
+ $user = get_user_by( 'id', $user );
105
+ }
106
+
107
+ if ( ! is_a( $user, 'WP_User' ) ) {
108
+ return array();
109
+ }
110
+
111
+ global $wp_roles;
112
+
113
+ $roles = $wp_roles->get_names();
114
+ $labels = array();
115
+
116
+ foreach ( $roles as $role => $label ) {
117
+ if ( in_array( $role, (array) $user->roles ) ) {
118
+ $labels[] = translate_user_role( $label );
119
+ }
120
+ }
121
+
122
+ return $labels;
123
+ }
124
+
125
+ /**
126
+ * Log user registrations
127
+ *
128
+ * @action user_register
129
+ * @param int $user_id Newly registered user ID
130
+ */
131
+ public static function callback_user_register( $user_id ) {
132
+ $current_user = wp_get_current_user();
133
+ $registered_user = get_user_by( 'id', $user_id );
134
+
135
+ if ( ! $current_user->ID ) { // Non logged-in user registered themselves
136
+ $message = __( 'New user registration', 'stream' );
137
+ $user_to_log = $registered_user->ID;
138
+ } else { // Current logged-in user created a new user
139
+ $message = _x(
140
+ 'New user account created for %1$s (%2$s)',
141
+ '1: User display name, 2: User role',
142
+ 'stream'
143
+ );
144
+ $user_to_log = $current_user->ID;
145
+ }
146
+
147
+ self::log(
148
+ $message,
149
+ array(
150
+ 'display_name' => ( $registered_user->display_name ) ? $registered_user->display_name : $registered_user->user_login,
151
+ 'roles' => implode( ', ', self::get_role_labels( $user_id ) ),
152
+ ),
153
+ $registered_user->ID,
154
+ 'users',
155
+ 'created',
156
+ $user_to_log
157
+ );
158
+ }
159
+
160
+ /**
161
+ * Log profile update
162
+ *
163
+ * @action profile_update
164
+ */
165
+ public static function callback_profile_update( $user_id, $user ) {
166
+ self::log(
167
+ __( '%s\'s profile was updated', 'stream' ),
168
+ array(
169
+ 'display_name' => $user->display_name,
170
+ ),
171
+ $user->ID,
172
+ 'profiles',
173
+ 'updated'
174
+ );
175
+ }
176
+
177
+ /**
178
+ * Log role transition
179
+ *
180
+ * @action set_user_role
181
+ */
182
+ public static function callback_set_user_role( $user_id, $new_role, $old_roles ) {
183
+ if ( empty( $old_roles ) ) {
184
+ return;
185
+ }
186
+
187
+ global $wp_roles;
188
+
189
+ self::log(
190
+ _x(
191
+ '%1$s\'s role was changed from %2$s to %3$s',
192
+ '1: User display name, 2: Old role, 3: New role',
193
+ 'stream'
194
+ ),
195
+ array(
196
+ 'display_name' => get_user_by( 'id', $user_id )->display_name,
197
+ 'old_role' => translate_user_role( $wp_roles->role_names[ $old_roles[0] ] ),
198
+ 'new_role' => translate_user_role( $wp_roles->role_names[ $new_role ] ),
199
+ ),
200
+ $user_id,
201
+ 'profiles',
202
+ 'updated'
203
+ );
204
+ }
205
+
206
+ /**
207
+ * Log password reset
208
+ *
209
+ * @action password_reset
210
+ */
211
+ public static function callback_password_reset( $user ) {
212
+ self::log(
213
+ __( '%s\'s password was reset', 'stream' ),
214
+ array(
215
+ 'email' => $user->display_name,
216
+ ),
217
+ $user->ID,
218
+ 'profiles',
219
+ 'password-reset',
220
+ $user->ID
221
+ );
222
+ }
223
+
224
+ /**
225
+ * Log user requests to retrieve passwords
226
+ *
227
+ * @action retrieve_password
228
+ */
229
+ public static function callback_retrieve_password( $user_login ) {
230
+ if ( wp_stream_filter_var( $user_login, FILTER_VALIDATE_EMAIL ) ) {
231
+ $user = get_user_by( 'email', $user_login );
232
+ } else {
233
+ $user = get_user_by( 'login', $user_login );
234
+ }
235
+
236
+ self::log(
237
+ __( '%s\'s password was requested to be reset', 'stream' ),
238
+ array( 'display_name' => $user->display_name ),
239
+ $user->ID,
240
+ 'sessions',
241
+ 'forgot-password',
242
+ $user->ID
243
+ );
244
+ }
245
+
246
+ /**
247
+ * Log user login
248
+ *
249
+ * @action set_logged_in_cookie
250
+ */
251
+ public static function callback_set_logged_in_cookie( $logged_in_cookie, $expire, $expiration, $user_id ) {
252
+ $user = get_user_by( 'id', $user_id );
253
+
254
+ self::log(
255
+ __( '%s logged in', 'stream' ),
256
+ array( 'display_name' => $user->display_name ),
257
+ $user->ID,
258
+ 'sessions',
259
+ 'login',
260
+ $user->ID
261
+ );
262
+ }
263
+
264
+ /**
265
+ * Log user logout
266
+ *
267
+ * @action clear_auth_cookie
268
+ */
269
+ public static function callback_clear_auth_cookie() {
270
+ $user = wp_get_current_user();
271
+
272
+ // For some reason, incognito mode calls clear_auth_cookie on failed login attempts
273
+ if ( empty( $user ) || ! $user->exists() ) {
274
+ return;
275
+ }
276
+
277
+ self::log(
278
+ __( '%s logged out', 'stream' ),
279
+ array( 'display_name' => $user->display_name ),
280
+ $user->ID,
281
+ 'sessions',
282
+ 'logout',
283
+ $user->ID
284
+ );
285
+ }
286
+
287
+ /**
288
+ * There's no logging in this callback's action, the reason
289
+ * behind this hook is so that we can store user objects before
290
+ * being deleted. During `deleted_user` hook, our callback
291
+ * receives $user_id param but it's useless as the user record
292
+ * was already removed from DB.
293
+ *
294
+ * @action delete_user
295
+ * @param int $user_id User ID that maybe deleted
296
+ */
297
+ public static function callback_delete_user( $user_id ) {
298
+ if ( ! isset( self::$_users_object_pre_deleted[ $user_id ] ) ) {
299
+ self::$_users_object_pre_deleted[ $user_id ] = get_user_by( 'id', $user_id );
300
+ }
301
+ }
302
+
303
+ /**
304
+ * Log deleted user.
305
+ *
306
+ * @action deleted_user
307
+ * @param int $user_id Deleted user ID
308
+ */
309
+ public static function callback_deleted_user( $user_id ) {
310
+ $user = wp_get_current_user();
311
+
312
+ if ( isset( self::$_users_object_pre_deleted[ $user_id ] ) ) {
313
+ $message = _x(
314
+ '%1$s\'s account was deleted (%2$s)',
315
+ '1: User display name, 2: User roles',
316
+ 'stream'
317
+ );
318
+ $display_name = self::$_users_object_pre_deleted[ $user_id ]->display_name;
319
+ $deleted_user = self::$_users_object_pre_deleted[ $user_id ];
320
+ unset( self::$_users_object_pre_deleted[ $user_id ] );
321
+ } else {
322
+ $message = __( 'User account #%d was deleted', 'stream' );
323
+ $display_name = $user_id;
324
+ $deleted_user = $user_id;
325
+ }
326
+
327
+ self::log(
328
+ $message,
329
+ array(
330
+ 'display_name' => $display_name,
331
+ 'roles' => implode( ', ', self::get_role_labels( $deleted_user ) ),
332
+ ),
333
+ $user_id,
334
+ 'users',
335
+ 'deleted',
336
+ $user->ID
337
+ );
338
+ }
339
+
340
+ }
connectors/class-wp-stream-connector-widgets.php ADDED
@@ -0,0 +1,805 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class WP_Stream_Connector_Widgets extends WP_Stream_Connector {
4
+
5
+ /**
6
+ * Whether or not 'created' and 'deleted' actions should be logged. Normally
7
+ * the sidebar 'added' and 'removed' actions will correspond with these.
8
+ * See note below with usage.
9
+ *
10
+ * @var bool
11
+ */
12
+ public static $verbose_widget_created_deleted_actions = false;
13
+
14
+ /**
15
+ * Connector slug
16
+ *
17
+ * @var string
18
+ */
19
+ public static $name = 'widgets';
20
+
21
+ /**
22
+ * Actions registered for this connector
23
+ *
24
+ * @var array
25
+ */
26
+ public static $actions = array(
27
+ 'update_option_sidebars_widgets',
28
+ 'updated_option',
29
+ );
30
+
31
+ /**
32
+ * Store the initial sidebars_widgets option when the customizer does its
33
+ * multiple rounds of saving to the sidebars_widgets option.
34
+ *
35
+ * @var array
36
+ */
37
+ protected static $customizer_initial_sidebars_widgets = null;
38
+
39
+ /**
40
+ * Return translated connector label
41
+ *
42
+ * @return string Translated connector label
43
+ */
44
+ public static function get_label() {
45
+ return __( 'Widgets', 'stream' );
46
+ }
47
+
48
+ /**
49
+ * Return translated action labels
50
+ *
51
+ * @return array Action label translations
52
+ */
53
+ public static function get_action_labels() {
54
+ return array(
55
+ 'added' => __( 'Added', 'stream' ),
56
+ 'removed' => __( 'Removed', 'stream' ),
57
+ 'moved' => __( 'Moved', 'stream' ),
58
+ 'created' => __( 'Created', 'stream' ),
59
+ 'deleted' => __( 'Deleted', 'stream' ),
60
+ 'deactivated' => __( 'Deactivated', 'stream' ),
61
+ 'reactivated' => __( 'Reactivated', 'stream' ),
62
+ 'updated' => __( 'Updated', 'stream' ),
63
+ 'sorted' => __( 'Sorted', 'stream' ),
64
+ );
65
+ }
66
+
67
+ /**
68
+ * Return translated context labels
69
+ *
70
+ * @return array Context label translations
71
+ */
72
+ public static function get_context_labels() {
73
+ global $wp_registered_sidebars;
74
+
75
+ $labels = array();
76
+
77
+ foreach ( $wp_registered_sidebars as $sidebar ) {
78
+ $labels[ $sidebar['id'] ] = $sidebar['name'];
79
+ }
80
+
81
+ $labels['wp_inactive_widgets'] = __( 'Inactive Widgets', 'stream' );
82
+ $labels['orphaned_widgets'] = __( 'Orphaned Widgets', 'stream' );
83
+
84
+ return $labels;
85
+ }
86
+
87
+ /**
88
+ * Add action links to Stream drop row in admin list screen
89
+ *
90
+ * @filter wp_stream_action_links_{connector}
91
+ *
92
+ * @param array $links Previous links registered
93
+ * @param object $record Stream record
94
+ *
95
+ * @return array Action links
96
+ */
97
+ public static function action_links( $links, $record ) {
98
+ if ( $sidebar = wp_stream_get_meta( $record, 'sidebar_id', true ) ) {
99
+ global $wp_registered_sidebars;
100
+
101
+ if ( array_key_exists( $sidebar, $wp_registered_sidebars ) ) {
102
+ $links[ __( 'Edit Widget Area', 'stream' ) ] = admin_url( 'widgets.php#' . $sidebar ); // xss ok (@todo fix WPCS rule)
103
+ }
104
+ // @todo Also old_sidebar_id and new_sidebar_id
105
+ // @todo Add Edit Widget link
106
+ }
107
+
108
+ return $links;
109
+ }
110
+
111
+ /**
112
+ * Tracks addition/deletion/reordering/deactivation of widgets from sidebars
113
+ *
114
+ * @action update_option_sidebars_widgets
115
+ * @param array $old Old sidebars widgets
116
+ * @param array $new New sidebars widgets
117
+ * @return void
118
+ */
119
+ public static function callback_update_option_sidebars_widgets( $old, $new ) {
120
+ // Disable listener if we're switching themes
121
+ if ( did_action( 'after_switch_theme' ) ) {
122
+ return;
123
+ }
124
+
125
+ if ( did_action( 'customize_save' ) ) {
126
+ if ( is_null( self::$customizer_initial_sidebars_widgets ) ) {
127
+ self::$customizer_initial_sidebars_widgets = $old;
128
+ add_action( 'customize_save_after', array( __CLASS__, '_callback_customize_save_after' ) );
129
+ }
130
+ } else {
131
+ self::handle_sidebars_widgets_changes( $old, $new );
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Since the sidebars_widgets may get updated multiple times when saving
137
+ * changes to Widgets in the Customizer, defer handling the changes until
138
+ * customize_save_after.
139
+ *
140
+ * @see self::callback_update_option_sidebars_widgets()
141
+ */
142
+ public static function _callback_customize_save_after() {
143
+ $old_sidebars_widgets = self::$customizer_initial_sidebars_widgets;
144
+ $new_sidebars_widgets = get_option( 'sidebars_widgets' );
145
+
146
+ self::handle_sidebars_widgets_changes( $old_sidebars_widgets, $new_sidebars_widgets );
147
+ }
148
+
149
+ /**
150
+ * @param array $old
151
+ * @param array $new
152
+ */
153
+ protected static function handle_sidebars_widgets_changes( $old, $new ) {
154
+ unset( $old['array_version'] );
155
+ unset( $new['array_version'] );
156
+
157
+ if ( $old === $new ) {
158
+ return;
159
+ }
160
+
161
+ self::handle_deactivated_widgets( $old, $new );
162
+ self::handle_reactivated_widgets( $old, $new );
163
+ self::handle_widget_removal( $old, $new );
164
+ self::handle_widget_addition( $old, $new );
165
+ self::handle_widget_reordering( $old, $new );
166
+ self::handle_widget_moved( $old, $new );
167
+ }
168
+
169
+ /**
170
+ * Track deactivation of widgets from sidebars
171
+ *
172
+ * @param array $old Old sidebars widgets
173
+ * @param array $new New sidebars widgets
174
+ * @return void
175
+ */
176
+ static protected function handle_deactivated_widgets( $old, $new ) {
177
+ $new_deactivated_widget_ids = array_diff( $new['wp_inactive_widgets'], $old['wp_inactive_widgets'] );
178
+
179
+ foreach ( $new_deactivated_widget_ids as $widget_id ) {
180
+ $sidebar_id = '';
181
+
182
+ foreach ( $old as $old_sidebar_id => $old_widget_ids ) {
183
+ if ( in_array( $widget_id, $old_widget_ids ) ) {
184
+ $sidebar_id = $old_sidebar_id;
185
+ break;
186
+ }
187
+ }
188
+
189
+ $action = 'deactivated';
190
+ $name = self::get_widget_name( $widget_id );
191
+ $title = self::get_widget_title( $widget_id );
192
+ $labels = self::get_context_labels();
193
+ $sidebar_name = isset( $labels[ $sidebar_id ] ) ? $labels[ $sidebar_id ] : $sidebar_id;
194
+
195
+ if ( $name && $title ) {
196
+ $message = _x( '%1$s widget named "%2$s" from "%3$s" deactivated', '1: Name, 2: Title, 3: Sidebar Name', 'stream' );
197
+ } elseif ( $name ) {
198
+ // Empty title, but we have the name
199
+ $message = _x( '%1$s widget from "%3$s" deactivated', '1: Name, 3: Sidebar Name', 'stream' );
200
+ } elseif ( $title ) {
201
+ // Likely a single widget since no name is available
202
+ $message = _x( 'Unknown widget type named "%2$s" from "%3$s" deactivated', '2: Title, 3: Sidebar Name', 'stream' );
203
+ } else {
204
+ // Neither a name nor a title are available, so use the widget ID
205
+ $message = _x( '%4$s widget from "%3$s" deactivated', '4: Widget ID, 3: Sidebar Name', 'stream' );
206
+ }
207
+
208
+ $message = sprintf( $message, $name, $title, $sidebar_name, $widget_id );
209
+
210
+ self::log(
211
+ $message,
212
+ compact( 'title', 'name', 'widget_id', 'sidebar_id' ),
213
+ null,
214
+ 'wp_inactive_widgets',
215
+ $action
216
+ );
217
+ }
218
+ }
219
+
220
+ /**
221
+ * Track reactivation of widgets from sidebars
222
+ *
223
+ * @param array $old Old sidebars widgets
224
+ * @param array $new New sidebars widgets
225
+ * @return void
226
+ */
227
+ static protected function handle_reactivated_widgets( $old, $new ) {
228
+ $new_reactivated_widget_ids = array_diff( $old['wp_inactive_widgets'], $new['wp_inactive_widgets'] );
229
+
230
+ foreach ( $new_reactivated_widget_ids as $widget_id ) {
231
+ $sidebar_id = '';
232
+
233
+ foreach ( $new as $new_sidebar_id => $new_widget_ids ) {
234
+ if ( in_array( $widget_id, $new_widget_ids ) ) {
235
+ $sidebar_id = $new_sidebar_id;
236
+ break;
237
+ }
238
+ }
239
+
240
+ $action = 'reactivated';
241
+ $name = self::get_widget_name( $widget_id );
242
+ $title = self::get_widget_title( $widget_id );
243
+
244
+ if ( $name && $title ) {
245
+ $message = _x( '%1$s widget named "%2$s" reactivated', '1: Name, 2: Title', 'stream' );
246
+ } elseif ( $name ) {
247
+ // Empty title, but we have the name
248
+ $message = _x( '%1$s widget reactivated', '1: Name', 'stream' );
249
+ } elseif ( $title ) {
250
+ // Likely a single widget since no name is available
251
+ $message = _x( 'Unknown widget type named "%2$s" reactivated', '2: Title', 'stream' );
252
+ } else {
253
+ // Neither a name nor a title are available, so use the widget ID
254
+ $message = _x( '%3$s widget reactivated', '3: Widget ID', 'stream' );
255
+ }
256
+
257
+ $message = sprintf( $message, $name, $title, $widget_id );
258
+
259
+ self::log(
260
+ $message,
261
+ compact( 'title', 'name', 'widget_id', 'sidebar_id' ),
262
+ null,
263
+ $sidebar_id,
264
+ $action
265
+ );
266
+ }
267
+ }
268
+
269
+ /**
270
+ * Track deletion of widgets from sidebars
271
+ *
272
+ * @param array $old Old sidebars widgets
273
+ * @param array $new New sidebars widgets
274
+ * @return void
275
+ */
276
+ static protected function handle_widget_removal( $old, $new ) {
277
+ $all_old_widget_ids = array_unique( call_user_func_array( 'array_merge', $old ) );
278
+ $all_new_widget_ids = array_unique( call_user_func_array( 'array_merge', $new ) );
279
+ // @todo In the customizer, moving widgets to other sidebars is problematic because each sidebar is registered as a separate setting; so we need to make sure that all $_POST['customized'] are applied?
280
+ // @todo The widget option is getting updated before the sidebars_widgets are updated, so we need to hook into the option update to try to cache any deletions for future lookup
281
+
282
+ $deleted_widget_ids = array_diff( $all_old_widget_ids, $all_new_widget_ids );
283
+
284
+ foreach ( $deleted_widget_ids as $widget_id ) {
285
+ $sidebar_id = '';
286
+
287
+ foreach ( $old as $old_sidebar_id => $old_widget_ids ) {
288
+ if ( in_array( $widget_id, $old_widget_ids ) ) {
289
+ $sidebar_id = $old_sidebar_id;
290
+ break;
291
+ }
292
+ }
293
+
294
+ $action = 'removed';
295
+ $name = self::get_widget_name( $widget_id );
296
+ $title = self::get_widget_title( $widget_id );
297
+ $labels = self::get_context_labels();
298
+ $sidebar_name = isset( $labels[ $sidebar_id ] ) ? $labels[ $sidebar_id ] : $sidebar_id;
299
+
300
+ if ( $name && $title ) {
301
+ $message = _x( '%1$s widget named "%2$s" removed from "%3$s"', '1: Name, 2: Title, 3: Sidebar Name', 'stream' );
302
+ } elseif ( $name ) {
303
+ // Empty title, but we have the name
304
+ $message = _x( '%1$s widget removed from "%3$s"', '1: Name, 3: Sidebar Name', 'stream' );
305
+ } elseif ( $title ) {
306
+ // Likely a single widget since no name is available
307
+ $message = _x( 'Unknown widget type named "%2$s" removed from "%3$s"', '2: Title, 3: Sidebar Name', 'stream' );
308
+ } else {
309
+ // Neither a name nor a title are available, so use the widget ID
310
+ $message = _x( '%4$s widget removed from "%3$s"', '4: Widget ID, 3: Sidebar Name', 'stream' );
311
+ }
312
+
313
+ $message = sprintf( $message, $name, $title, $sidebar_name, $widget_id );
314
+
315
+ self::log(
316
+ $message,
317
+ compact( 'widget_id', 'sidebar_id' ),
318
+ null,
319
+ $sidebar_id,
320
+ $action
321
+ );
322
+ }
323
+ }
324
+
325
+ /**
326
+ * Track reactivation of widgets from sidebars
327
+ *
328
+ * @param array $old Old sidebars widgets
329
+ * @param array $new New sidebars widgets
330
+ * @return void
331
+ */
332
+ static protected function handle_widget_addition( $old, $new ) {
333
+ $all_old_widget_ids = array_unique( call_user_func_array( 'array_merge', $old ) );
334
+ $all_new_widget_ids = array_unique( call_user_func_array( 'array_merge', $new ) );
335
+ $added_widget_ids = array_diff( $all_new_widget_ids, $all_old_widget_ids );
336
+
337
+ foreach ( $added_widget_ids as $widget_id ) {
338
+ $sidebar_id = '';
339
+
340
+ foreach ( $new as $new_sidebar_id => $new_widget_ids ) {
341
+ if ( in_array( $widget_id, $new_widget_ids ) ) {
342
+ $sidebar_id = $new_sidebar_id;
343
+ break;
344
+ }
345
+ }
346
+
347
+ $action = 'added';
348
+ $name = self::get_widget_name( $widget_id );
349
+ $title = self::get_widget_title( $widget_id );
350
+ $labels = self::get_context_labels();
351
+ $sidebar_name = isset( $labels[ $sidebar_id ] ) ? $labels[ $sidebar_id ] : $sidebar_id;
352
+
353
+ if ( $name && $title ) {
354
+ $message = _x( '%1$s widget named "%2$s" added to "%3$s"', '1: Name, 2: Title, 3: Sidebar Name', 'stream' );
355
+ } elseif ( $name ) {
356
+ // Empty title, but we have the name
357
+ $message = _x( '%1$s widget added to "%3$s"', '1: Name, 3: Sidebar Name', 'stream' );
358
+ } elseif ( $title ) {
359
+ // Likely a single widget since no name is available
360
+ $message = _x( 'Unknown widget type named "%2$s" added to "%3$s"', '2: Title, 3: Sidebar Name', 'stream' );
361
+ } else {
362
+ // Neither a name nor a title are available, so use the widget ID
363
+ $message = _x( '%4$s widget added to "%3$s"', '4: Widget ID, 3: Sidebar Name', 'stream' );
364
+ }
365
+
366
+ $message = sprintf( $message, $name, $title, $sidebar_name, $widget_id );
367
+
368
+ self::log(
369
+ $message,
370
+ compact( 'widget_id', 'sidebar_id' ), // @todo Do we care about sidebar_id in meta if it is already context? But there is no 'context' for what the context signifies
371
+ null,
372
+ $sidebar_id,
373
+ $action
374
+ );
375
+ }
376
+ }
377
+
378
+ /**
379
+ * Track reordering of widgets
380
+ *
381
+ * @param array $old Old sidebars widgets
382
+ * @param array $new New sidebars widgets
383
+ * @return void
384
+ */
385
+ static protected function handle_widget_reordering( $old, $new ) {
386
+ $all_sidebar_ids = array_intersect( array_keys( $old ), array_keys( $new ) );
387
+
388
+ foreach ( $all_sidebar_ids as $sidebar_id ) {
389
+ if ( $old[ $sidebar_id ] === $new[ $sidebar_id ] ) {
390
+ continue;
391
+ }
392
+
393
+ // Use intersect to ignore widget additions and removals
394
+ $all_widget_ids = array_unique( array_merge( $old[ $sidebar_id ], $new[ $sidebar_id ] ) );
395
+ $common_widget_ids = array_intersect( $old[ $sidebar_id ], $new[ $sidebar_id ] );
396
+ $uncommon_widget_ids = array_diff( $all_widget_ids, $common_widget_ids );
397
+ $new_widget_ids = array_values( array_diff( $new[ $sidebar_id ], $uncommon_widget_ids ) );
398
+ $old_widget_ids = array_values( array_diff( $old[ $sidebar_id ], $uncommon_widget_ids ) );
399
+ $widget_order_changed = ( $new_widget_ids !== $old_widget_ids );
400
+
401
+ if ( $widget_order_changed ) {
402
+ $labels = self::get_context_labels();
403
+ $sidebar_name = isset( $labels[ $sidebar_id ] ) ? $labels[ $sidebar_id ] : $sidebar_id;
404
+ $old_widget_ids = $old[ $sidebar_id ];
405
+ $message = _x( 'Widgets reordered in "%s"', 'Sidebar name', 'stream' );
406
+ $message = sprintf( $message, $sidebar_name );
407
+
408
+ self::log(
409
+ $message,
410
+ compact( 'sidebar_id', 'old_widget_ids' ),
411
+ null,
412
+ $sidebar_id,
413
+ 'sorted'
414
+ );
415
+ }
416
+ }
417
+
418
+ }
419
+
420
+ /**
421
+ * Track movement of widgets to other sidebars
422
+ *
423
+ * @param array $old Old sidebars widgets
424
+ * @param array $new New sidebars widgets
425
+ * @return void
426
+ */
427
+ static protected function handle_widget_moved( $old, $new ) {
428
+ $all_sidebar_ids = array_intersect( array_keys( $old ), array_keys( $new ) );
429
+
430
+ foreach ( $all_sidebar_ids as $new_sidebar_id ) {
431
+ if ( $old[ $new_sidebar_id ] === $new[ $new_sidebar_id ] ) {
432
+ continue;
433
+ }
434
+
435
+ $new_widget_ids = array_diff( $new[ $new_sidebar_id ], $old[ $new_sidebar_id ] );
436
+
437
+ foreach ( $new_widget_ids as $widget_id ) {
438
+ // Now find the sidebar that the widget was originally located in, as long it is not wp_inactive_widgets
439
+ $old_sidebar_id = null;
440
+ foreach ( $old as $sidebar_id => $old_widget_ids ) {
441
+ if ( in_array( $widget_id, $old_widget_ids ) ) {
442
+ $old_sidebar_id = $sidebar_id;
443
+ break;
444
+ }
445
+ }
446
+
447
+ if ( ! $old_sidebar_id || 'wp_inactive_widgets' === $old_sidebar_id || 'wp_inactive_widgets' === $new_sidebar_id ) {
448
+ continue;
449
+ }
450
+
451
+ assert( $old_sidebar_id !== $new_sidebar_id );
452
+
453
+ $name = self::get_widget_name( $widget_id );
454
+ $title = self::get_widget_title( $widget_id );
455
+ $labels = self::get_context_labels();
456
+ $old_sidebar_name = isset( $labels[ $old_sidebar_id ] ) ? $labels[ $old_sidebar_id ] : $old_sidebar_id;
457
+ $new_sidebar_name = isset( $labels[ $new_sidebar_id ] ) ? $labels[ $new_sidebar_id ] : $new_sidebar_id;
458
+
459
+ if ( $name && $title ) {
460
+ $message = _x( '%1$s widget named "%2$s" moved from "%4$s" to "%5$s"', '1: Name, 2: Title, 4: Old Sidebar Name, 5: New Sidebar Name', 'stream' );
461
+ } elseif ( $name ) {
462
+ // Empty title, but we have the name
463
+ $message = _x( '%1$s widget moved from "%4$s" to "%5$s"', '1: Name, 4: Old Sidebar Name, 5: New Sidebar Name', 'stream' );
464
+ } elseif ( $title ) {
465
+ // Likely a single widget since no name is available
466
+ $message = _x( 'Unknown widget type named "%2$s" moved from "%4$s" to "%5$s"', '2: Title, 4: Old Sidebar Name, 5: New Sidebar Name', 'stream' );
467
+ } else {
468
+ // Neither a name nor a title are available, so use the widget ID
469
+ $message = _x( '%3$s widget moved from "%4$s" to "%5$s"', '3: Widget ID, 4: Old Sidebar Name, 5: New Sidebar Name', 'stream' );
470
+ }
471
+
472
+ $message = sprintf( $message, $name, $title, $widget_id, $old_sidebar_name, $new_sidebar_name );
473
+ $sidebar_id = $new_sidebar_id;
474
+
475
+ self::log(
476
+ $message,
477
+ compact( 'widget_id', 'sidebar_id', 'old_sidebar_id' ),
478
+ null,
479
+ $sidebar_id,
480
+ 'moved'
481
+ );
482
+ }
483
+ }
484
+
485
+ }
486
+
487
+ /**
488
+ * Track changes to widgets
489
+ *
490
+ * @faction updated_option
491
+ * @param string $option_name
492
+ * @param array $old_value
493
+ * @param array $new_value
494
+ */
495
+ public static function callback_updated_option( $option_name, $old_value, $new_value ) {
496
+ if ( ! preg_match( '/^widget_(.+)$/', $option_name, $matches ) || ! is_array( $new_value ) ) {
497
+ return;
498
+ }
499
+
500
+ $is_multi = ! empty( $new_value['_multiwidget'] );
501
+ $widget_id_base = $matches[1];
502
+
503
+ $creates = array();
504
+ $updates = array();
505
+ $deletes = array();
506
+
507
+ if ( $is_multi ) {
508
+ $widget_id_format = "$widget_id_base-%d";
509
+
510
+ unset( $new_value['_multiwidget'] );
511
+ unset( $old_value['_multiwidget'] );
512
+
513
+ /**
514
+ * Created widgets
515
+ */
516
+ $created_widget_numbers = array_diff( array_keys( $new_value ), array_keys( $old_value ) );
517
+
518
+ foreach ( $created_widget_numbers as $widget_number ) {
519
+ $instance = $new_value[ $widget_number ];
520
+ $widget_id = sprintf( $widget_id_format, $widget_number );
521
+ $name = self::get_widget_name( $widget_id );
522
+ $title = ! empty( $instance['title'] ) ? $instance['title'] : null;
523
+ $sidebar_id = self::get_widget_sidebar_id( $widget_id ); // @todo May not be assigned yet
524
+
525
+ $creates[] = compact( 'name', 'title', 'widget_id', 'sidebar_id', 'instance' );
526
+ }
527
+
528
+ /**
529
+ * Updated widgets
530
+ */
531
+ $updated_widget_numbers = array_intersect( array_keys( $old_value ), array_keys( $new_value ) );
532
+
533
+ foreach ( $updated_widget_numbers as $widget_number ) {
534
+ $new_instance = $new_value[ $widget_number ];
535
+ $old_instance = $old_value[ $widget_number ];
536
+
537
+ if ( $old_instance !== $new_instance ) {
538
+ $widget_id = sprintf( $widget_id_format, $widget_number );
539
+ $name = self::get_widget_name( $widget_id );
540
+ $title = ! empty( $new_instance['title'] ) ? $new_instance['title'] : null;
541
+ $sidebar_id = self::get_widget_sidebar_id( $widget_id );
542
+ $labels = self::get_context_labels();
543
+ $sidebar_name = isset( $labels[ $sidebar_id ] ) ? $labels[ $sidebar_id ] : $sidebar_id;
544
+
545
+ $updates[] = compact( 'name', 'title', 'widget_id', 'sidebar_id', 'old_instance', 'sidebar_name' );
546
+ }
547
+ }
548
+
549
+ /**
550
+ * Deleted widgets
551
+ */
552
+ $deleted_widget_numbers = array_diff( array_keys( $old_value ), array_keys( $new_value ) );
553
+
554
+ foreach ( $deleted_widget_numbers as $widget_number ) {
555
+ $instance = $old_value[ $widget_number ];
556
+ $widget_id = sprintf( $widget_id_format, $widget_number );
557
+ $name = self::get_widget_name( $widget_id );
558
+ $title = ! empty( $instance['title'] ) ? $instance['title'] : null;
559
+ $sidebar_id = self::get_widget_sidebar_id( $widget_id ); // @todo May not be assigned anymore
560
+
561
+ $deletes[] = compact( 'name', 'title', 'widget_id', 'sidebar_id', 'instance' );
562
+ }
563
+ } else {
564
+ // Doing our best guess for tracking changes to old single widgets, assuming their options start with 'widget_'
565
+ $widget_id = $widget_id_base;
566
+ $name = $widget_id; // There aren't names available for single widgets
567
+ $title = ! empty( $new_value['title'] ) ? $new_value['title'] : null;
568
+ $sidebar_id = self::get_widget_sidebar_id( $widget_id );
569
+ $old_instance = $old_value;
570
+ $labels = self::get_context_labels();
571
+ $sidebar_name = isset( $labels[ $sidebar_id ] ) ? $labels[ $sidebar_id ] : $sidebar_id;
572
+
573
+ $updates[] = compact( 'widget_id', 'title', 'name', 'sidebar_id', 'old_instance', 'sidebar_name' );
574
+ }
575
+
576
+ /**
577
+ * Log updated actions
578
+ */
579
+ foreach ( $updates as $update ) {
580
+ if ( $update['name'] && $update['title'] ) {
581
+ $message = _x( '%1$s widget named "%2$s" in "%3$s" updated', '1: Name, 2: Title, 3: Sidebar Name', 'stream' );
582
+ } elseif ( $update['name'] ) {
583
+ // Empty title, but we have the name
584
+ $message = _x( '%1$s widget in "%3$s" updated', '1: Name, 3: Sidebar Name', 'stream' );
585
+ } elseif ( $update['title'] ) {
586
+ // Likely a single widget since no name is available
587
+ $message = _x( 'Unknown widget type named "%2$s" in "%3$s" updated', '2: Title, 3: Sidebar Name', 'stream' );
588
+ } else {
589
+ // Neither a name nor a title are available, so use the widget ID
590
+ $message = _x( '%4$s widget in "%3$s" updated', '4: Widget ID, 3: Sidebar Name', 'stream' );
591
+ }
592
+
593
+ $message = sprintf( $message, $update['name'], $update['title'], $update['sidebar_name'], $update['widget_id'] );
594
+
595
+ unset( $update['title'], $update['name'] );
596
+
597
+ self::log(
598
+ $message,
599
+ $update,
600
+ null,
601
+ $update['sidebar_id'],
602
+ 'updated'
603
+ );
604
+ }
605
+
606
+ /**
607
+ * In the normal case, widgets are never created or deleted in a vacuum.
608
+ * Created widgets are immediately assigned to a sidebar, and deleted
609
+ * widgets are immediately removed from their assigned sidebar. If,
610
+ * however, widget instances get manipulated programmatically, it is
611
+ * possible that they could be orphaned, in which case the following
612
+ * actions would be useful to log.
613
+ */
614
+ if ( self::$verbose_widget_created_deleted_actions ) {
615
+ // We should only do these if not captured by an update to the sidebars_widgets option
616
+ /**
617
+ * Log created actions
618
+ */
619
+ foreach ( $creates as $create ) {
620
+ if ( $create['name'] && $create['title'] ) {
621
+ $message = _x( '%1$s widget named "%2$s" created', '1: Name, 2: Title', 'stream' );
622
+ } elseif ( $create['name'] ) {
623
+ // Empty title, but we have the name
624
+ $message = _x( '%1$s widget created', '1: Name', 'stream' );
625
+ } elseif ( $create['title'] ) {
626
+ // Likely a single widget since no name is available
627
+ $message = _x( 'Unknown widget type named "%2$s" created', '2: Title', 'stream' );
628
+ } else {
629
+ // Neither a name nor a title are available, so use the widget ID
630
+ $message = _x( '%3$s widget created', '3: Widget ID', 'stream' );
631
+ }
632
+
633
+ $message = sprintf( $message, $create['name'], $create['title'], $create['widget_id'] );
634
+
635
+ unset( $create['title'], $create['name'] );
636
+
637
+ self::log(
638
+ $message,
639
+ $create,
640
+ null,
641
+ $create['sidebar_id'],
642
+ 'created'
643
+ );
644
+ }
645
+
646
+ /**
647
+ * Log deleted actions
648
+ */
649
+ foreach ( $deletes as $delete ) {
650
+ if ( $delete['name'] && $delete['title'] ) {
651
+ $message = _x( '%1$s widget named "%2$s" deleted', '1: Name, 2: Title', 'stream' );
652
+ } elseif ( $delete['name'] ) {
653
+ // Empty title, but we have the name
654
+ $message = _x( '%1$s widget deleted', '1: Name', 'stream' );
655
+ } elseif ( $delete['title'] ) {
656
+ // Likely a single widget since no name is available
657
+ $message = _x( 'Unknown widget type named "%2$s" deleted', '2: Title', 'stream' );
658
+ } else {
659
+ // Neither a name nor a title are available, so use the widget ID
660
+ $message = _x( '%3$s widget deleted', '3: Widget ID', 'stream' );
661
+ }
662
+
663
+ $message = sprintf( $message, $delete['name'], $delete['title'], $delete['widget_id'] );
664
+
665
+ unset( $delete['title'], $delete['name'] );
666
+
667
+ self::log(
668
+ $message,
669
+ $delete,
670
+ null,
671
+ $delete['sidebar_id'],
672
+ 'deleted'
673
+ );
674
+ }
675
+ }
676
+ }
677
+
678
+ /**
679
+ * @param string $widget_id
680
+ * @return string
681
+ */
682
+ public static function get_widget_title( $widget_id ) {
683
+ $instance = self::get_widget_instance( $widget_id );
684
+ return ! empty( $instance['title'] ) ? $instance['title'] : null;
685
+ }
686
+
687
+ /**
688
+ * @param string $widget_id
689
+ * @return string|null
690
+ */
691
+ public static function get_widget_name( $widget_id ) {
692
+ $widget_obj = self::get_widget_object( $widget_id );
693
+ return $widget_obj ? $widget_obj->name : null;
694
+ }
695
+
696
+ /**
697
+ * @param $widget_id
698
+ * @return array|null
699
+ */
700
+ public static function parse_widget_id( $widget_id ) {
701
+ if ( preg_match( '/^(.+)-(\d+)$/', $widget_id, $matches ) ) {
702
+ return array(
703
+ 'id_base' => $matches[1],
704
+ 'widget_number' => intval( $matches[2] ),
705
+ );
706
+ } else {
707
+ return null;
708
+ }
709
+ }
710
+
711
+ /**
712
+ * @param string $widget_id
713
+ * @return WP_Widget|null
714
+ */
715
+ public static function get_widget_object( $widget_id ) {
716
+ global $wp_widget_factory;
717
+
718
+ $parsed_widget_id = self::parse_widget_id( $widget_id );
719
+
720
+ if ( ! $parsed_widget_id ) {
721
+ return null;
722
+ }
723
+
724
+ $id_base = $parsed_widget_id['id_base'];
725
+
726
+ $id_base_to_widget_class_map = array_combine(
727
+ wp_list_pluck( $wp_widget_factory->widgets, 'id_base' ),
728
+ array_keys( $wp_widget_factory->widgets )
729
+ );
730
+
731
+ if ( ! isset( $id_base_to_widget_class_map[ $id_base ] ) ) {
732
+ return null;
733
+ }
734
+
735
+ return $wp_widget_factory->widgets[ $id_base_to_widget_class_map[ $id_base ] ];
736
+ }
737
+
738
+ /**
739
+ * Returns widget instance settings
740
+ *
741
+ * @param string $widget_id Widget ID, ex: pages-1
742
+ * @return array|null Widget instance
743
+ */
744
+ public static function get_widget_instance( $widget_id ) {
745
+ $instance = null;
746
+ $parsed_widget_id = self::parse_widget_id( $widget_id );
747
+ $widget_obj = self::get_widget_object( $widget_id );
748
+
749
+ if ( $widget_obj && $parsed_widget_id ) {
750
+ $settings = $widget_obj->get_settings();
751
+ $multi_number = $parsed_widget_id['widget_number'];
752
+
753
+ if ( isset( $settings[ $multi_number ] ) && ! empty( $settings[ $multi_number ]['title'] ) ) {
754
+ $instance = $settings[ $multi_number ];
755
+ }
756
+ } else {
757
+ // Single widgets, try our best guess at the option used
758
+ $potential_instance = get_option( "widget_{$widget_id}" );
759
+
760
+ if ( ! empty( $potential_instance ) && ! empty( $potential_instance['title'] ) ) {
761
+ $instance = $potential_instance;
762
+ }
763
+ }
764
+
765
+ return $instance;
766
+ }
767
+
768
+ /**
769
+ * Get global sidebars widgets
770
+ *
771
+ * @return array
772
+ */
773
+ public static function get_sidebars_widgets() {
774
+ /**
775
+ * Filter allows for insertion of sidebar widgets
776
+ * @todo Do we need this filter?
777
+ *
778
+ * @param array Sidebar Widgets in Options table
779
+ * @param array Inserted Sidebar Widgets
780
+ * @return array Array of updated Sidebar Widgets
781
+ */
782
+ return apply_filters( 'sidebars_widgets', get_option( 'sidebars_widgets', array() ) );
783
+ }
784
+
785
+ /**
786
+ * Return the sidebar of a certain widget, based on widget_id
787
+ *
788
+ * @param string $widget_id Widget id, ex: pages-1
789
+ * @return string Sidebar id
790
+ */
791
+ public static function get_widget_sidebar_id( $widget_id ) {
792
+ $sidebars_widgets = self::get_sidebars_widgets();
793
+
794
+ unset( $sidebars_widgets['array_version'] );
795
+
796
+ foreach ( $sidebars_widgets as $sidebar_id => $widget_ids ) {
797
+ if ( in_array( $widget_id, $widget_ids ) ) {
798
+ return $sidebar_id;
799
+ }
800
+ }
801
+
802
+ return 'orphaned_widgets';
803
+ }
804
+
805
+ }
connectors/class-wp-stream-connector-woocommerce.php ADDED
@@ -0,0 +1,776 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class WP_Stream_Connector_Woocommerce extends WP_Stream_Connector {
4
+
5
+ /**
6
+ * Context name
7
+ * @var string
8
+ */
9
+ public static $name = 'woocommerce';
10
+
11
+ /**
12
+ * Holds tracked plugin minimum version required
13
+ *
14
+ * @const string
15
+ */
16
+ const PLUGIN_MIN_VERSION = '2.1.10';
17
+
18
+ /**
19
+ * Actions registered for this context
20
+ * @var array
21
+ */
22
+ public static $actions = array(
23
+ 'wp_stream_record_array',
24
+ 'updated_option',
25
+ 'transition_post_status',
26
+ 'deleted_post',
27
+ 'woocommerce_order_status_changed',
28
+ 'woocommerce_attribute_added',
29
+ 'woocommerce_attribute_updated',
30
+ 'woocommerce_attribute_deleted',
31
+ 'woocommerce_tax_rate_added',
32
+ 'woocommerce_tax_rate_updated',
33
+ 'woocommerce_tax_rate_deleted',
34
+ );
35
+
36
+ public static $taxonomies = array(
37
+ 'product_type',
38
+ 'product_cat',
39
+ 'product_tag',
40
+ 'product_shipping_class',
41
+ 'shop_order_status',
42
+ );
43
+
44
+ public static $post_types = array(
45
+ 'product',
46
+ 'product_variation',
47
+ 'shop_order',
48
+ 'shop_coupon',
49
+ );
50
+
51
+ private static $order_update_logged = false;
52
+
53
+ private static $settings_pages = array();
54
+
55
+ private static $settings = array();
56
+
57
+ private static $custom_settings = array();
58
+
59
+ public static function register() {
60
+ parent::register();
61
+
62
+ add_filter( 'wp_stream_posts_exclude_post_types', array( __CLASS__, 'exclude_order_post_types' ) );
63
+ add_action( 'wp_stream_comments_exclude_comment_types', array( __CLASS__, 'exclude_order_comment_types' ) );
64
+
65
+ self::get_woocommerce_settings_fields();
66
+ }
67
+
68
+ /**
69
+ * Check if plugin dependencies are satisfied and add an admin notice if not
70
+ *
71
+ * @return bool
72
+ */
73
+ public static function is_dependency_satisfied() {
74
+ global $woocommerce;
75
+
76
+ if ( class_exists( 'WooCommerce' ) && version_compare( $woocommerce->version, self::PLUGIN_MIN_VERSION, '>=' ) ) {
77
+ return true;
78
+ }
79
+
80
+ return false;
81
+ }
82
+
83
+ /**
84
+ * Return translated context label
85
+ *
86
+ * @return string Translated context label
87
+ */
88
+ public static function get_label() {
89
+ return _x( 'WooCommerce', 'woocommerce', 'stream' );
90
+ }
91
+
92
+ /**
93
+ * Return translated action labels
94
+ *
95
+ * @return array Action label translations
96
+ */
97
+ public static function get_action_labels() {
98
+ return array(
99
+ 'updated' => _x( 'Updated', 'woocommerce', 'stream' ),
100
+ 'created' => _x( 'Created', 'woocommerce', 'stream' ),
101
+ 'trashed' => _x( 'Trashed', 'woocommerce', 'stream' ),
102
+ 'deleted' => _x( 'Deleted', 'woocommerce', 'stream' ),
103
+ );
104
+ }
105
+
106
+ /**
107
+ * Return translated context labels
108
+ *
109
+ * @return array Context label translations
110
+ */
111
+ public static function get_context_labels() {
112
+ $context_labels = array();
113
+
114
+ if ( class_exists( 'WP_Stream_Connector_Posts' ) ) {
115
+ $context_labels = array_merge(
116
+ $context_labels,
117
+ WP_Stream_Connector_Posts::get_context_labels()
118
+ );
119
+ }
120
+
121
+ $custom_context_labels = array(
122
+ 'attributes' => _x( 'Attributes', 'woocommerce', 'stream' ),
123
+ );
124
+
125
+ $context_labels = array_merge(
126
+ $context_labels,
127
+ $custom_context_labels,
128
+ self::$settings_pages
129
+ );
130
+
131
+ return apply_filters( 'wp_stream_woocommerce_contexts', $context_labels );
132
+ }
133
+
134
+ /**
135
+ * Return settings used by WooCommerce that aren't registered
136
+ *
137
+ * @return array Custom settings with translated title and page
138
+ */
139
+ public static function get_custom_settings() {
140
+ $custom_settings = array(
141
+ 'woocommerce_frontend_css_colors' => array(
142
+ 'title' => __( 'Frontend Styles', 'stream' ),
143
+ 'page' => 'wc-settings',
144
+ 'tab' => 'general',
145
+ 'section' => '',
146
+ 'type' => __( 'setting', 'stream' ),
147
+ ),
148
+ 'woocommerce_default_gateway' => array(
149
+ 'title' => __( 'Gateway Display Default', 'stream' ),
150
+ 'page' => 'wc-settings',
151
+ 'tab' => 'checkout',
152
+ 'section' => '',
153
+ 'type' => __( 'setting', 'stream' ),
154
+ ),
155
+ 'woocommerce_gateway_order' => array(
156
+ 'title' => __( 'Gateway Display Order', 'stream' ),
157
+ 'page' => 'wc-settings',
158
+ 'tab' => 'checkout',
159
+ 'section' => '',
160
+ 'type' => __( 'setting', 'stream' ),
161
+ ),
162
+ 'woocommerce_default_shipping_method' => array(
163
+ 'title' => __( 'Shipping Methods Default', 'stream' ),
164
+ 'page' => 'wc-settings',
165
+ 'tab' => 'shipping',
166
+ 'section' => '',
167
+ 'type' => __( 'setting', 'stream' ),
168
+ ),
169
+ 'woocommerce_shipping_method_order' => array(
170
+ 'title' => __( 'Shipping Methods Order', 'stream' ),
171
+ 'page' => 'wc-settings',
172
+ 'tab' => 'shipping',
173
+ 'section' => '',
174
+ 'type' => __( 'setting', 'stream' ),
175
+ ),
176
+ 'shipping_debug_mode' => array(
177
+ 'title' => __( 'Shipping Debug Mode', 'stream' ),
178
+ 'page' => 'wc-status',
179
+ 'tab' => 'tools',
180
+ 'section' => '',
181
+ 'type' => __( 'tool', 'stream' ),
182
+ ),
183
+ 'template_debug_mode' => array(
184
+ 'title' => __( 'Template Debug Mode', 'stream' ),
185
+ 'page' => 'wc-status',
186
+ 'tab' => 'tools',
187
+ 'section' => '',
188
+ 'type' => __( 'tool', 'stream' ),
189
+ ),
190
+ 'uninstall_data' => array(
191
+ 'title' => __( 'Remove post types on uninstall', 'stream' ),
192
+ 'page' => 'wc-status',
193
+ 'tab' => 'tools',
194
+ 'section' => '',
195
+ 'type' => __( 'tool', 'stream' ),
196
+ ),
197
+ );
198
+
199
+ return apply_filters( 'wp_stream_woocommerce_custom_settings', $custom_settings );
200
+ }
201
+
202
+ /**
203
+ * Add action links to Stream drop row in admin list screen
204
+ *
205
+ * @filter wp_stream_action_links_{connector}
206
+ *
207
+ * @param array $links Previous links registered
208
+ * @param object $record Stream record
209
+ *
210
+ * @return array Action links
211
+ */
212
+ public static function action_links( $links, $record ) {
213
+ if ( in_array( $record->context, self::$post_types ) && get_post( $record->object_id ) ) {
214
+ if ( $link = get_edit_post_link( $record->object_id ) ) {
215
+ $post_type_name = WP_Stream_Connector_Posts::get_post_type_name( get_post_type( $record->object_id ) );
216
+ $links[ sprintf( _x( 'Edit %s', 'Post type singular name', 'stream' ), $post_type_name ) ] = $link;
217
+ }
218
+
219
+ if ( post_type_exists( get_post_type( $record->object_id ) ) && $link = get_permalink( $record->object_id ) ) {
220
+ $links[ __( 'View', 'stream' ) ] = $link;
221
+ }
222
+ }
223
+
224
+ $context_labels = self::get_context_labels();
225
+ $option_key = wp_stream_get_meta( $record, 'option', true );
226
+ $option_page = wp_stream_get_meta( $record, 'page', true );
227
+ $option_tab = wp_stream_get_meta( $record, 'tab', true );
228
+ $option_section = wp_stream_get_meta( $record, 'section', true );
229
+
230
+ if ( $option_key && $option_tab ) {
231
+ $text = sprintf( __( 'Edit WooCommerce %s', 'stream' ), $context_labels[ $record->context ] );;
232
+ $url = add_query_arg(
233
+ array( 'page' => $option_page, 'tab' => $option_tab, 'section' => $option_section ),
234
+ admin_url( 'admin.php' ) // Not self_admin_url here, as WooCommerce doesn't exist in Network Admin
235
+ );
236
+
237
+ $links[ $text ] = $url . '#wp-stream-highlight:' . $option_key;
238
+ }
239
+
240
+ return $links;
241
+ }
242
+
243
+ /**
244
+ * Prevent the Stream Posts connector from logging orders
245
+ * so that we can handle them differently here
246
+ *
247
+ * @filter wp_stream_posts_exclude_post_types
248
+ * @param array $post_types Ignored post types
249
+ * @return array Filtered post types
250
+ */
251
+ public static function exclude_order_post_types( $post_types ) {
252
+ $post_types[] = 'shop_order';
253
+
254
+ return $post_types;
255
+ }
256
+
257
+ /**
258
+ * Prevent the Stream Comments connector from logging status
259
+ * change comments on orders
260
+ *
261
+ * @filter wp_stream_commnent_exclude_comment_types
262
+ * @param array $comment_types Ignored post types
263
+ * @return array Filtered post types
264
+ */
265
+ public static function exclude_order_comment_types( $comment_types ) {
266
+ $comment_types[] = 'order_note';
267
+
268
+ return $comment_types;
269
+ }
270
+
271
+ /**
272
+ * Log Order major status changes ( creating / updating / trashing )
273
+ *
274
+ * @action transition_post_status
275
+ */
276
+ public static function callback_transition_post_status( $new, $old, $post ) {
277
+ // Only track orders
278
+ if ( 'shop_order' !== $post->post_type ) {
279
+ return;
280
+ }
281
+
282
+ // Don't track customer actions
283
+ if ( ! is_admin() ) {
284
+ return;
285
+ }
286
+
287
+ // Don't track minor status change actions
288
+ if ( in_array( wp_stream_filter_input( INPUT_GET, 'action' ), array( 'mark_processing', 'mark_on-hold', 'mark_completed' ) ) || defined( 'DOING_AJAX' ) ) {
289
+ return;
290
+ }
291
+
292
+ // Don't log updates when more than one happens at the same time
293
+ if ( $post->ID === self::$order_update_logged ) {
294
+ return;
295
+ }
296
+
297
+ if ( in_array( $new, array( 'auto-draft', 'draft', 'inherit' ) ) ) {
298
+ return;
299
+ } elseif ( 'auto-draft' === $old && 'publish' === $new ) {
300
+ $message = _x(
301
+ '%s created',
302
+ 'Order title',
303
+ 'stream'
304
+ );
305
+ $action = 'created';
306
+ } elseif ( 'trash' === $new ) {
307
+ $message = _x(
308
+ '%s trashed',
309
+ 'Order title',
310
+ 'stream'
311
+ );
312
+ $action = 'trashed';
313
+ } elseif ( 'trash' === $old && 'publish' === $new ) {
314
+ $message = _x(
315
+ '%s restored from the trash',
316
+ 'Order title',
317
+ 'stream'
318
+ );
319
+ $action = 'untrashed';
320
+ } else {
321
+ $message = _x(
322
+ '%s updated',
323
+ 'Order title',
324
+ 'stream'
325
+ );
326
+ }
327
+
328
+ if ( empty( $action ) ) {
329
+ $action = 'updated';
330
+ }
331
+
332
+ $order = new WC_Order( $post->ID );
333
+ $order_title = __( 'Order number', 'stream' ) . ' ' . esc_html( $order->get_order_number() );
334
+ $order_type_name = __( 'order', 'stream' );
335
+
336
+ self::log(
337
+ $message,
338
+ array(
339
+ 'post_title' => $order_title,
340
+ 'singular_name' => $order_type_name,
341
+ 'new_status' => $new,
342
+ 'old_status' => $old,
343
+ 'revision_id' => null,
344
+ ),
345
+ $post->ID,
346
+ $post->post_type,
347
+ $action
348
+ );
349
+
350
+ self::$order_update_logged = $post->ID;
351
+ }
352
+
353
+ /**
354
+ * Log order deletion
355
+ *
356
+ * @action deleted_post
357
+ */
358
+ public static function callback_deleted_post( $post_id ) {
359
+ $post = get_post( $post_id );
360
+
361
+ // We check if post is an instance of WP_Post as it doesn't always resolve in unit testing
362
+ if ( ! ( $post instanceof WP_Post ) || 'shop_order' !== $post->post_type ) {
363
+ return;
364
+ }
365
+
366
+ // Ignore auto-drafts that are deleted by the system, see issue-293
367
+ if ( 'auto-draft' === $post->post_status ) {
368
+ return;
369
+ }
370
+
371
+ $order = new WC_Order( $post->ID );
372
+ $order_title = __( 'Order number', 'stream' ) . ' ' . esc_html( $order->get_order_number() );
373
+ $order_type_name = __( 'order', 'stream' );
374
+
375
+ self::log(
376
+ _x(
377
+ '"%s" deleted from trash',
378
+ 'Order title',
379
+ 'stream'
380
+ ),
381
+ array(
382
+ 'post_title' => $order_title,
383
+ 'singular_name' => $order_type_name,
384
+ ),
385
+ $post->ID,
386
+ $post->post_type,
387
+ 'deleted'
388
+ );
389
+ }
390
+
391
+ /**
392
+ * Log Order minor status changes ( pending / on-hold / failed / processing / completed / refunded / cancelled )
393
+ *
394
+ * @action woocommerce_order_status_changed
395
+ */
396
+ public static function callback_woocommerce_order_status_changed( $order_id, $old, $new ) {
397
+ // Don't track customer actions
398
+ if ( ! is_admin() ) {
399
+ return;
400
+ }
401
+
402
+ $old_status = get_term_by( 'slug', $old, 'shop_order_status' );
403
+ $new_status = get_term_by( 'slug', $new, 'shop_order_status' );
404
+
405
+ // Don't track new statuses
406
+ if ( ! $old_status ) {
407
+ return;
408
+ }
409
+
410
+ $message = _x(
411
+ '%1$s status changed from %2$s to %3$s',
412
+ '1. Order title, 2. Old status, 3. New status',
413
+ 'stream'
414
+ );
415
+
416
+ $order = new WC_Order( $order_id );
417
+ $order_title = __( 'Order number', 'stream' ) . ' ' . esc_html( $order->get_order_number() );
418
+ $order_type_name = __( 'order', 'stream' );
419
+ $new_status_name = strtolower( $new_status->name );
420
+ $old_status_name = strtolower( $old_status->name );
421
+
422
+ self::log(
423
+ $message,
424
+ array(
425
+ 'post_title' => $order_title,
426
+ 'old_status_name' => $old_status_name,
427
+ 'new_status_name' => $new_status_name,
428
+ 'singular_name' => $order_type_name,
429
+ 'new_status' => $new,
430
+ 'old_status' => $old,
431
+ 'revision_id' => null,
432
+ ),
433
+ $order_id,
434
+ 'shop_order',
435
+ $new_status_name
436
+ );
437
+ }
438
+
439
+ /**
440
+ * Log adding a product attribute
441
+ *
442
+ * @action woocommerce_attribute_added
443
+ */
444
+ public static function callback_woocommerce_attribute_added( $attribute_id, $attribute ) {
445
+ self::log(
446
+ _x(
447
+ '"%s" product attribute created',
448
+ 'Term name',
449
+ 'stream'
450
+ ),
451
+ $attribute,
452
+ $attribute_id,
453
+ 'attributes',
454
+ 'created'
455
+ );
456
+ }
457
+
458
+ /**
459
+ * Log updating a product attribute
460
+ *
461
+ * @action woocommerce_attribute_updated
462
+ */
463
+ public static function callback_woocommerce_attribute_updated( $attribute_id, $attribute ) {
464
+ self::log(
465
+ _x(
466
+ '"%s" product attribute updated',
467
+ 'Term name',
468
+ 'stream'
469
+ ),
470
+ $attribute,
471
+ $attribute_id,
472
+ 'attributes',
473
+ 'updated'
474
+ );
475
+ }
476
+
477
+ /**
478
+ * Log deleting a product attribute
479
+ *
480
+ * @action woocommerce_attribute_updated
481
+ */
482
+ public static function callback_woocommerce_attribute_deleted( $attribute_id, $attribute_name ) {
483
+ self::log(
484
+ _x(
485
+ '"%s" product attribute deleted',
486
+ 'Term name',
487
+ 'stream'
488
+ ),
489
+ array(
490
+ 'attribute_name' => $attribute_name,
491
+ ),
492
+ $attribute_id,
493
+ 'attributes',
494
+ 'deleted'
495
+ );
496
+ }
497
+
498
+ /**
499
+ * Log adding a tax rate
500
+ *
501
+ * @action woocommerce_tax_rate_added
502
+ */
503
+ public static function callback_woocommerce_tax_rate_added( $tax_rate_id, $tax_rate ) {
504
+ self::log(
505
+ _x(
506
+ '"%4$s" tax rate created',
507
+ 'Tax rate name',
508
+ 'stream'
509
+ ),
510
+ $tax_rate,
511
+ $tax_rate_id,
512
+ 'tax',
513
+ 'created'
514
+ );
515
+ }
516
+
517
+ /**
518
+ * Log updating a tax rate
519
+ *
520
+ * @action woocommerce_tax_rate_updated
521
+ */
522
+ public static function callback_woocommerce_tax_rate_updated( $tax_rate_id, $tax_rate ) {
523
+ self::log(
524
+ _x(
525
+ '"%4$s" tax rate updated',
526
+ 'Tax rate name',
527
+ 'stream'
528
+ ),
529
+ $tax_rate,
530
+ $tax_rate_id,
531
+ 'tax',
532
+ 'updated'
533
+ );
534
+ }
535
+
536
+ /**
537
+ * Log deleting a tax rate
538
+ *
539
+ * @action woocommerce_tax_rate_updated
540
+ */
541
+ public static function callback_woocommerce_tax_rate_deleted( $tax_rate_id ) {
542
+ global $wpdb;
543
+
544
+ $tax_rate_name = $wpdb->get_var(
545
+ $wpdb->prepare(
546
+ "SELECT tax_rate_name FROM {$wpdb->prefix}woocommerce_tax_rates
547
+ WHERE tax_rate_id = %s
548
+ ",
549
+ $tax_rate_id
550
+ )
551
+ );
552
+
553
+ self::log(
554
+ _x(
555
+ '"%s" tax rate deleted',
556
+ 'Tax rate name',
557
+ 'stream'
558
+ ),
559
+ array(
560
+ 'tax_rate_name' => $tax_rate_name,
561
+ ),
562
+ $tax_rate_id,
563
+ 'tax',
564
+ 'deleted'
565
+ );
566
+ }
567
+
568
+ /**
569
+ * Filter records and take-over our precious data
570
+ *
571
+ * @filter wp_stream_record_array
572
+ *
573
+ * @param array $recordarr Record data to be inserted
574
+ *
575
+ * @return array Filtered record data
576
+ */
577
+ public static function callback_wp_stream_record_array( $recordarr ) {
578
+ foreach ( $recordarr as $key => $record ) {
579
+ // Change connector::posts records
580
+ if ( 'posts' === $record['connector'] && in_array( $record['context'], self::$post_types ) ) {
581
+ $recordarr[ $key ]['connector'] = self::$name;
582
+ } elseif ( 'taxonomies' === $record['connector'] && in_array( $record['context'], self::$taxonomies ) ) {
583
+ $recordarr[ $key ]['connector'] = self::$name;
584
+ } elseif ( 'settings' === $record['connector'] ) {
585
+ $option = isset( $record['meta']['option_key'] ) ? $record['meta']['option_key'] : false;
586
+
587
+ if ( $option && isset( self::$settings[ $option ] ) ) {
588
+ return false;
589
+ }
590
+ }
591
+ }
592
+
593
+ return $recordarr;
594
+ }
595
+
596
+ public static function callback_updated_option( $option_key, $old_value, $value ) {
597
+ $options = array( $option_key );
598
+
599
+ if ( is_array( $old_value ) || is_array( $value ) ) {
600
+ foreach ( self::get_changed_keys( $old_value, $value ) as $field_key ) {
601
+ $options[] = $field_key;
602
+ }
603
+ }
604
+
605
+ foreach ( $options as $option ) {
606
+ if ( ! array_key_exists( $option, self::$settings ) ) {
607
+ continue;
608
+ }
609
+
610
+ self::log(
611
+ __( '"%1$s" %2$s updated', 'stream' ),
612
+ array(
613
+ 'label' => self::$settings[ $option ]['title'],
614
+ 'type' => self::$settings[ $option ]['type'],
615
+ 'page' => self::$settings[ $option ]['page'],
616
+ 'tab' => self::$settings[ $option ]['tab'],
617
+ 'section' => self::$settings[ $option ]['section'],
618
+ 'option' => $option,
619
+ // Prevent fatal error when saving option as array
620
+ 'old_value' => maybe_serialize( $old_value ),
621
+ 'value' => maybe_serialize( $value ),
622
+ ),
623
+ null,
624
+ self::$settings[ $option ]['tab'],
625
+ 'updated'
626
+ );
627
+ }
628
+ }
629
+
630
+ public static function get_woocommerce_settings_fields() {
631
+ if ( ! defined( 'WC_VERSION' ) || ! class_exists( 'WC_Admin_Settings' ) ) {
632
+ return;
633
+ }
634
+
635
+ if ( ! empty( self::$settings ) ) {
636
+ return self::$settings;
637
+ }
638
+
639
+ $settings_cache_key = 'stream_connector_woocommerce_settings_' . sanitize_key( WC_VERSION );
640
+
641
+ if ( $settings_transient = get_transient( $settings_cache_key ) ) {
642
+ $settings = $settings_transient['settings'];
643
+ $settings_pages = $settings_transient['settings_pages'];
644
+ } else {
645
+ global $woocommerce;
646
+
647
+ $settings = array();
648
+ $settings_pages = array();
649
+
650
+ foreach ( WC_Admin_Settings::get_settings_pages() as $page ) {
651
+ // Get ID / Label of the page, since they're protected, by hacking into
652
+ // the callback filter for 'woocommerce_settings_tabs_array'
653
+ $info = $page->add_settings_page( array() );
654
+ $page_id = key( $info );
655
+ $page_label = current( $info );
656
+ $sections = $page->get_sections();
657
+
658
+ if ( empty( $sections ) ) {
659
+ $sections[''] = $page_label;
660
+ }
661
+
662
+ $settings_pages[ $page_id ] = $page_label;
663
+
664
+ // Remove non-fields ( sections, titles and whatever )
665
+ $fields = array();
666
+
667
+ foreach ( $sections as $section_key => $section_label ) {
668
+ $_fields = array_filter(
669
+ $page->get_settings( $section_key ),
670
+ function( $item ) {
671
+ return isset( $item['id'] ) && ( ! in_array( $item['type'], array( 'title', 'sectionend' ) ) );
672
+ }
673
+ );
674
+
675
+ foreach ( $_fields as $field ) {
676
+ $title = isset( $field['title'] ) ? $field['title'] : $field['desc'];
677
+ $fields[ $field['id'] ] = array(
678
+ 'title' => $title,
679
+ 'page' => 'wc-settings',
680
+ 'tab' => $page_id,
681
+ 'section' => $section_key,
682
+ 'type' => __( 'setting', 'stream' ),
683
+ );
684
+ }
685
+ }
686
+
687
+ // Store fields in the global array to be searched later
688
+ $settings = array_merge( $settings, $fields );
689
+ }
690
+
691
+ // Provide additional context for each of the settings pages
692
+ array_walk( $settings_pages, function( &$value ) {
693
+ $value .= ' ' . __( 'Settings', 'stream' );
694
+ });
695
+
696
+ // Load Payment Gateway Settings
697
+ $payment_gateway_settings = array();
698
+ $payment_gateways = $woocommerce->payment_gateways();
699
+
700
+ foreach ( $payment_gateways->payment_gateways as $section_key => $payment_gateway ) {
701
+ $title = $payment_gateway->title;
702
+ $key = $payment_gateway->plugin_id . $payment_gateway->id . '_settings';
703
+
704
+ $payment_gateway_settings[ $key ] = array(
705
+ 'title' => $title,
706
+ 'page' => 'wc-settings',
707
+ 'tab' => 'checkout',
708
+ 'section' => strtolower( $section_key ),
709
+ 'type' => __( 'payment gateway', 'stream' ),
710
+ );
711
+ }
712
+
713
+ $settings = array_merge( $settings, $payment_gateway_settings );
714
+
715
+ // Load Shipping Method Settings
716
+ $shipping_method_settings = array();
717
+ $shipping_methods = $woocommerce->shipping();
718
+
719
+ foreach ( $shipping_methods->shipping_methods as $section_key => $shipping_method ) {
720
+ $title = $shipping_method->title;
721
+ $key = $shipping_method->plugin_id . $shipping_method->id . '_settings';
722
+
723
+ $shipping_method_settings[ $key ] = array(
724
+ 'title' => $title,
725
+ 'page' => 'wc-settings',
726
+ 'tab' => 'shipping',
727
+ 'section' => strtolower( $section_key ),
728
+ 'type' => __( 'shipping method', 'stream' ),
729
+ );
730
+ }
731
+
732
+ $settings = array_merge( $settings, $shipping_method_settings );
733
+
734
+ // Load Email Settings
735
+ $email_settings = array();
736
+ $emails = $woocommerce->mailer();
737
+
738
+ foreach ( $emails->emails as $section_key => $email ) {
739
+ $title = $email->title;
740
+ $key = $email->plugin_id . $email->id . '_settings';
741
+
742
+ $email_settings[ $key ] = array(
743
+ 'title' => $title,
744
+ 'page' => 'wc-settings',
745
+ 'tab' => 'email',
746
+ 'section' => strtolower( $section_key ),
747
+ 'type' => __( 'email', 'stream' ),
748
+ );
749
+ }
750
+
751
+ $settings = array_merge( $settings, $email_settings );
752
+
753
+ // Tools page
754
+ $tools_page = array(
755
+ 'tools' => __( 'Tools', 'stream' )
756
+ );
757
+
758
+ $settings_pages = array_merge( $settings_pages, $tools_page );
759
+
760
+ // Cache the results
761
+ $settings_cache = array(
762
+ 'settings' => $settings,
763
+ 'settings_pages' => $settings_pages,
764
+ );
765
+
766
+ set_transient( $settings_cache_key, $settings_cache, MINUTE_IN_SECONDS * 60 * 6 );
767
+ }
768
+
769
+ $custom_settings = self::get_custom_settings();
770
+ self::$settings = array_merge( $settings, $custom_settings );
771
+ self::$settings_pages = $settings_pages;
772
+
773
+ return self::$settings;
774
+ }
775
+
776
+ }
connectors/class-wp-stream-connector-wordpress-seo.php ADDED
@@ -0,0 +1,487 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class WP_Stream_Connector_WordPress_SEO extends WP_Stream_Connector {
4
+
5
+ /**
6
+ * Connector slug
7
+ *
8
+ * @var string
9
+ */
10
+ public static $name = 'wordpress-seo';
11
+
12
+ /**
13
+ * Holds tracked plugin minimum version required
14
+ *
15
+ * @const string
16
+ */
17
+ const PLUGIN_MIN_VERSION = '1.5.3.3';
18
+
19
+ /**
20
+ * Actions registered for this connector
21
+ *
22
+ * @var array
23
+ */
24
+ public static $actions = array(
25
+ 'wpseo_handle_import',
26
+ 'wpseo_import',
27
+ 'seo_page_wpseo_files',
28
+ 'added_post_meta',
29
+ 'updated_post_meta',
30
+ 'deleted_post_meta',
31
+ );
32
+
33
+ /**
34
+ * Tracking registered Settings, with overridden data
35
+ *
36
+ * @var array
37
+ */
38
+ public static $option_groups = array();
39
+
40
+ /**
41
+ * Check if plugin dependencies are satisfied and add an admin notice if not
42
+ *
43
+ * @return bool
44
+ */
45
+ public static function is_dependency_satisfied() {
46
+ if ( defined( 'WPSEO_VERSION' ) && version_compare( WPSEO_VERSION, self::PLUGIN_MIN_VERSION, '>=' ) ) {
47
+ return true;
48
+ }
49
+
50
+ return false;
51
+ }
52
+
53
+ /**
54
+ * Return translated connector label
55
+ *
56
+ * @return string Translated connector label
57
+ */
58
+ public static function get_label() {
59
+ return _x( 'WordPress SEO', 'wordpress-seo', 'stream' );
60
+ }
61
+
62
+ /**
63
+ * Return translated action labels
64
+ *
65
+ * @return array Action label translations
66
+ */
67
+ public static function get_action_labels() {
68
+ return array(
69
+ 'created' => _x( 'Created', 'wordpress-seo', 'stream' ),
70
+ 'updated' => _x( 'Updated', 'wordpress-seo', 'stream' ),
71
+ 'added' => _x( 'Added', 'wordpress-seo', 'stream' ),
72
+ 'deleted' => _x( 'Deleted', 'wordpress-seo', 'stream' ),
73
+ 'exported' => _x( 'Exported', 'wordpress-seo', 'stream' ),
74
+ 'imported' => _x( 'Imported', 'wordpress-seo', 'stream' ),
75
+ );
76
+ }
77
+
78
+ /**
79
+ * Return translated context labels
80
+ *
81
+ * @return array Context label translations
82
+ */
83
+ public static function get_context_labels() {
84
+ return array(
85
+ 'wpseo_dashboard' => _x( 'Dashboard', 'wordpress-seo', 'stream' ),
86
+ 'wpseo_titles' => _x( 'Titles &amp; Metas', 'wordpress-seo', 'stream' ),
87
+ 'wpseo_social' => _x( 'Social', 'wordpress-seo', 'stream' ),
88
+ 'wpseo_xml' => _x( 'XML Sitemaps', 'wordpress-seo', 'stream' ),
89
+ 'wpseo_permalinks' => _x( 'Permalinks', 'wordpress-seo', 'stream' ),
90
+ 'wpseo_internal-links' => _x( 'Internal Links', 'wordpress-seo', 'stream' ),
91
+ 'wpseo_rss' => _x( 'RSS', 'wordpress-seo', 'stream' ),
92
+ 'wpseo_import' => _x( 'Import & Export', 'wordpress-seo', 'stream' ),
93
+ 'wpseo_bulk-title-editor' => _x( 'Bulk Title Editor', 'wordpress-seo', 'stream' ),
94
+ 'wpseo_bulk-description-editor' => _x( 'Bulk Description Editor', 'wordpress-seo', 'stream' ),
95
+ 'wpseo_files' => _x( 'Files', 'wordpress-seo', 'stream' ),
96
+ 'wpseo_meta' => _x( 'Content', 'wordpress-seo', 'stream' ),
97
+ );
98
+ }
99
+
100
+ /**
101
+ * Add action links to Stream drop row in admin list screen
102
+ *
103
+ * @filter wp_stream_action_links_{connector}
104
+ *
105
+ * @param array $links Previous links registered
106
+ * @param object $record Stream record
107
+ *
108
+ * @return array Action links
109
+ */
110
+ public static function action_links( $links, $record ) {
111
+ $contexts = self::get_context_labels();
112
+
113
+ // Options
114
+ if ( $option = wp_stream_get_meta( $record, 'option', true ) ) {
115
+ $key = wp_stream_get_meta( $record, 'option_key', true );
116
+
117
+ $links[ __( 'Edit', 'stream' ) ] = add_query_arg(
118
+ array(
119
+ 'page' => $record->context,
120
+ ),
121
+ admin_url( 'admin.php' )
122
+ ) . '#stream-highlight-' . esc_attr( $key );
123
+ } elseif ( 'wpseo_files' === $record->context ) {
124
+ $links[ __( 'Edit', 'stream' ) ] = add_query_arg(
125
+ array(
126
+ 'page' => $record->context,
127
+ ),
128
+ admin_url( 'admin.php' )
129
+ );
130
+ } elseif ( 'wpseo_meta' === $record->context ) {
131
+ $post = get_post( $record->object_id );
132
+
133
+ if ( $post ) {
134
+ $post_type_name = WP_Stream_Connector_Posts::get_post_type_name( get_post_type( $post->ID ) );
135
+
136
+ if ( 'trash' === $post->post_status ) {
137
+ $untrash = wp_nonce_url(
138
+ add_query_arg(
139
+ array(
140
+ 'action' => 'untrash',
141
+ 'post' => $post->ID,
142
+ ),
143
+ admin_url( 'post.php' )
144
+ ),
145
+ sprintf( 'untrash-post_%d', $post->ID )
146
+ );
147
+
148
+ $delete = wp_nonce_url(
149
+ add_query_arg(
150
+ array(
151
+ 'action' => 'delete',
152
+ 'post' => $post->ID,
153
+ ),
154
+ admin_url( 'post.php' )
155
+ ),
156
+ sprintf( 'delete-post_%d', $post->ID )
157
+ );
158
+
159
+ $links[ sprintf( esc_html_x( 'Restore %s', 'Post type singular name', 'stream' ), $post_type_name ) ] = $untrash;
160
+ $links[ sprintf( esc_html_x( 'Delete %s Permenantly', 'Post type singular name', 'stream' ), $post_type_name ) ] = $delete;
161
+ } else {
162
+ $links[ sprintf( esc_html_x( 'Edit %s', 'Post type singular name', 'stream' ), $post_type_name ) ] = get_edit_post_link( $post->ID );
163
+
164
+ if ( $view_link = get_permalink( $post->ID ) ) {
165
+ $links[ esc_html__( 'View', 'stream' ) ] = $view_link;
166
+ }
167
+
168
+ if ( $revision_id = wp_stream_get_meta( $record, 'revision_id', true ) ) {
169
+ $links[ esc_html__( 'Revision', 'stream' ) ] = get_edit_post_link( $revision_id );
170
+ }
171
+ }
172
+ }
173
+ }
174
+
175
+ return $links;
176
+ }
177
+
178
+ public static function register() {
179
+ parent::register();
180
+
181
+ foreach ( WPSEO_Options::$options as $class ) {
182
+ /* @var $class WPSEO_Options */
183
+ self::$option_groups[ $class::get_instance()->group_name ] = array(
184
+ 'class' => $class,
185
+ 'name' => $class::get_instance()->option_name,
186
+ );
187
+ }
188
+
189
+ add_action( 'admin_enqueue_scripts', array( __CLASS__, 'admin_enqueue_scripts' ) );
190
+ add_filter( 'wp_stream_log_data', array( __CLASS__, 'log_override' ) );
191
+ }
192
+
193
+ public static function admin_enqueue_scripts( $hook ) {
194
+ if ( 0 === strpos( $hook, 'seo_page_' ) ) {
195
+ $src = WP_STREAM_URL . '/ui/js/wpseo-admin.js';
196
+ wp_enqueue_script( 'stream-connector-wpseo', $src, array( 'jquery' ), WP_Stream::VERSION );
197
+ }
198
+ }
199
+
200
+ /**
201
+ * Track importing settings from other plugins
202
+ */
203
+ public static function callback_wpseo_handle_import() {
204
+ $imports = array(
205
+ 'importheadspace' => __( 'HeadSpace2', 'stream' ), # type = checkbox
206
+ 'importaioseo' => __( 'All-in-One SEO', 'stream' ), # type = checkbox
207
+ 'importaioseoold' => __( 'OLD All-in-One SEO', 'stream' ), # type = checkbox
208
+ 'importwoo' => __( 'WooThemes SEO framework', 'stream' ), # type = checkbox
209
+ 'importrobotsmeta' => __( 'Robots Meta (by Yoast)', 'stream' ), # type = checkbox
210
+ 'importrssfooter' => __( 'RSS Footer (by Yoast)', 'stream' ), # type = checkbox
211
+ 'importbreadcrumbs' => __( 'Yoast Breadcrumbs', 'stream' ), # type = checkbox
212
+ );
213
+
214
+ $opts = wp_stream_filter_input( INPUT_POST, 'wpseo' );
215
+
216
+ foreach ( $imports as $key => $name ) {
217
+ if ( isset( $opts[ $key ] ) ) {
218
+ self::log(
219
+ sprintf(
220
+ __( 'Imported settings from %1$s%2$s', 'stream' ),
221
+ $name,
222
+ isset( $opts['deleteolddata'] ) ? __( ', and deleted old data', 'stream' ) : ''
223
+ ),
224
+ array(
225
+ 'key' => $key,
226
+ 'deleteolddata' => isset( $opts['deleteolddata'] ),
227
+ ),
228
+ null,
229
+ 'wpseo_import',
230
+ 'imported'
231
+ );
232
+ }
233
+ }
234
+ }
235
+
236
+ public static function callback_wpseo_import() {
237
+ $opts = wp_stream_filter_input( INPUT_POST, 'wpseo' );
238
+
239
+ if ( wp_stream_filter_input( INPUT_POST, 'wpseo_export' ) ) {
240
+ self::log(
241
+ sprintf(
242
+ __( 'Exported settings%s', 'stream' ),
243
+ isset( $opts['include_taxonomy_meta'] ) ? __( ', including taxonomy meta', 'stream' ) : ''
244
+ ),
245
+ array(
246
+ 'include_taxonomy_meta' => isset( $opts['include_taxonomy_meta'] ),
247
+ ),
248
+ null,
249
+ 'wpseo_import',
250
+ 'exported'
251
+ );
252
+ } elseif ( isset( $_FILES['settings_import_file'] ) ) {
253
+ self::log(
254
+ sprintf(
255
+ __( 'Tried importing settings from "%s"', 'stream' ),
256
+ $_FILES['settings_import_file']['name']
257
+ ),
258
+ array(
259
+ 'file' => $_FILES['settings_import_file']['name'],
260
+ ),
261
+ null,
262
+ 'wpseo_import',
263
+ 'exported'
264
+ );
265
+ }
266
+ }
267
+
268
+ public static function callback_seo_page_wpseo_files() {
269
+ if ( wp_stream_filter_input( INPUT_POST, 'create_robots' ) ) {
270
+ $message = __( 'Tried creating robots.txt file', 'stream' );
271
+ } elseif ( wp_stream_filter_input( INPUT_POST, 'submitrobots' ) ) {
272
+ $message = __( 'Tried updating robots.txt file', 'stream' );
273
+ } elseif ( wp_stream_filter_input( INPUT_POST, 'submithtaccess' ) ) {
274
+ $message = __( 'Tried updating htaccess file', 'stream' );
275
+ }
276
+
277
+ if ( isset( $message ) ) {
278
+ self::log(
279
+ $message,
280
+ array(),
281
+ null,
282
+ 'wpseo_files',
283
+ 'updated'
284
+ );
285
+ }
286
+ }
287
+
288
+ public static function callback_added_post_meta( $meta_id, $object_id, $meta_key, $meta_value ) {
289
+ self::meta( $object_id, $meta_key, $meta_value );
290
+ }
291
+ public static function callback_updated_post_meta( $meta_id, $object_id, $meta_key, $meta_value ) {
292
+ self::meta( $object_id, $meta_key, $meta_value );
293
+ }
294
+ public static function callback_deleted_post_meta( $meta_id, $object_id, $meta_key, $meta_value ) {
295
+ self::meta( $object_id, $meta_key, $meta_value );
296
+ }
297
+
298
+ private static function meta( $object_id, $meta_key, $meta_value ) {
299
+ $prefix = WPSEO_Meta::$meta_prefix;
300
+
301
+ WPSEO_Metabox::translate_meta_boxes();
302
+
303
+ if ( 0 !== strpos( $meta_key, $prefix ) ) {
304
+ return;
305
+ }
306
+
307
+ $key = str_replace( $prefix, '', $meta_key );
308
+
309
+ foreach ( WPSEO_Meta::$meta_fields as $tab => $fields ) {
310
+ if ( isset( $fields[ $key ] ) ) {
311
+ $field = $fields[ $key ];
312
+ break;
313
+ }
314
+ }
315
+
316
+ if ( ! isset( $field, $field['title'], $tab ) || '' === $field['title'] ) {
317
+ return;
318
+ }
319
+
320
+ $post = get_post( $object_id );
321
+ $post_type_label = get_post_type_labels( get_post_type_object( $post->post_type ) )->singular_name;
322
+
323
+ self::log(
324
+ sprintf(
325
+ __( 'Updated "%1$s" of "%2$s" %3$s', 'stream' ),
326
+ $field['title'],
327
+ $post->post_title,
328
+ $post_type_label
329
+ ),
330
+ array(
331
+ 'meta_key' => $meta_key,
332
+ 'meta_value' => $meta_value,
333
+ 'post_type' => $post->post_type,
334
+ ),
335
+ $object_id,
336
+ 'wpseo_meta',
337
+ 'updated'
338
+ );
339
+ }
340
+
341
+ /**
342
+ * Override connector log for our own Settings / Actions
343
+ *
344
+ * @param array $data
345
+ *
346
+ * @return array|bool
347
+ */
348
+ public static function log_override( $data ) {
349
+ if ( ! is_array( $data ) ) {
350
+ return $data;
351
+ }
352
+
353
+ global $pagenow;
354
+
355
+ if ( 'options.php' === $pagenow && 'settings' === $data['connector'] && wp_stream_filter_input( INPUT_POST, '_wp_http_referer' ) ) {
356
+ if ( ! isset( $data['args']['context'] ) || ! isset( self::$option_groups[ $data['args']['context'] ] ) ) {
357
+ return $data;
358
+ }
359
+
360
+ $page = preg_match( '#page=([^&]*)#', wp_stream_filter_input( INPUT_POST, '_wp_http_referer' ), $match ) ? $match[1] : '';
361
+ $labels = self::get_context_labels();
362
+
363
+ if ( ! isset( $labels[ $page ] ) ) {
364
+ return $data;
365
+ }
366
+
367
+ if ( ! ( $label = self::settings_labels( $data['args']['option_key'] ) ) ) {
368
+ $data['message'] = __( '%s settings updated', 'stream' );
369
+ $label = $labels[ $page ];
370
+ }
371
+
372
+ $data['args']['label'] = $label;
373
+ $data['args']['context'] = $page;
374
+ $data['context'] = $page;
375
+ $data['connector'] = self::$name;
376
+ }
377
+
378
+ return $data;
379
+ }
380
+
381
+ private static function settings_labels( $option ) {
382
+ $labels = array(
383
+ // wp-content/plugins/wordpress-seo/admin/pages/dashboard.php:
384
+ 'yoast_tracking' => _x( 'Allow tracking of this WordPress install\'s anonymous data.', 'wordpress-seo', 'stream' ), # type = checkbox
385
+ 'disableadvanced_meta' => _x( 'Disable the Advanced part of the WordPress SEO meta box', 'wordpress-seo', 'stream' ), # type = checkbox
386
+ 'alexaverify' => _x( 'Alexa Verification ID', 'wordpress-seo', 'stream' ), # type = textinput
387
+ 'msverify' => _x( 'Bing Webmaster Tools', 'wordpress-seo', 'stream' ), # type = textinput
388
+ 'googleverify' => _x( 'Google Webmaster Tools', 'wordpress-seo', 'stream' ), # type = textinput
389
+ 'pinterestverify' => _x( 'Pinterest', 'wordpress-seo', 'stream' ), # type = textinput
390
+ 'yandexverify' => _x( 'Yandex Webmaster Tools', 'wordpress-seo', 'stream' ), # type = textinput
391
+
392
+ // wp-content/plugins/wordpress-seo/admin/pages/internal-links.php:
393
+ 'breadcrumbs-enable' => _x( 'Enable Breadcrumbs', 'wordpress-seo', 'stream' ), # type = checkbox
394
+ 'breadcrumbs-sep' => _x( 'Separator between breadcrumbs', 'wordpress-seo', 'stream' ), # type = textinput
395
+ 'breadcrumbs-home' => _x( 'Anchor text for the Homepage', 'wordpress-seo', 'stream' ), # type = textinput
396
+ 'breadcrumbs-prefix' => _x( 'Prefix for the breadcrumb path', 'wordpress-seo', 'stream' ), # type = textinput
397
+ 'breadcrumbs-archiveprefix' => _x( 'Prefix for Archive breadcrumbs', 'wordpress-seo', 'stream' ), # type = textinput
398
+ 'breadcrumbs-searchprefix' => _x( 'Prefix for Search Page breadcrumbs', 'wordpress-seo', 'stream' ), # type = textinput
399
+ 'breadcrumbs-404crumb' => _x( 'Breadcrumb for 404 Page', 'wordpress-seo', 'stream' ), # type = textinput
400
+ 'breadcrumbs-blog-remove' => _x( 'Remove Blog page from Breadcrumbs', 'wordpress-seo', 'stream' ), # type = checkbox
401
+ 'breadcrumbs-boldlast' => _x( 'Bold the last page in the breadcrumb', 'wordpress-seo', 'stream' ), # type = checkbox
402
+
403
+ // wp-content/plugins/wordpress-seo/admin/pages/metas.php:
404
+ 'forcerewritetitle' => _x( 'Force rewrite titles', 'wordpress-seo', 'stream' ), # type = checkbox
405
+ 'noindex-subpages-wpseo' => _x( 'Noindex subpages of archives', 'wordpress-seo', 'stream' ), # type = checkbox
406
+ 'usemetakeywords' => _x( 'Use <code>meta</code> keywords tag?', 'wordpress-seo', 'stream' ), # type = checkbox
407
+ 'noodp' => _x( 'Add <code>noodp</code> meta robots tag sitewide', 'wordpress-seo', 'stream' ), # type = checkbox
408
+ 'noydir' => _x( 'Add <code>noydir</code> meta robots tag sitewide', 'wordpress-seo', 'stream' ), # type = checkbox
409
+ 'hide-rsdlink' => _x( 'Hide RSD Links', 'wordpress-seo', 'stream' ), # type = checkbox
410
+ 'hide-wlwmanifest' => _x( 'Hide WLW Manifest Links', 'wordpress-seo', 'stream' ), # type = checkbox
411
+ 'hide-shortlink' => _x( 'Hide Shortlink for posts', 'wordpress-seo', 'stream' ), # type = checkbox
412
+ 'hide-feedlinks' => _x( 'Hide RSS Links', 'wordpress-seo', 'stream' ), # type = checkbox
413
+ 'disable-author' => _x( 'Disable the author archives', 'wordpress-seo', 'stream' ), # type = checkbox
414
+ 'disable-date' => _x( 'Disable the date-based archives', 'wordpress-seo', 'stream' ), # type = checkbox
415
+
416
+ // wp-content/plugins/wordpress-seo/admin/pages/network.php:
417
+ 'access' => _x( 'Who should have access to the WordPress SEO settings', 'wordpress-seo', 'stream' ), # type = select
418
+ 'defaultblog' => _x( 'New blogs get the SEO settings from this blog', 'wordpress-seo', 'stream' ), # type = textinput
419
+ 'restoreblog' => _x( 'Blog ID', 'wordpress-seo', 'stream' ), # type = textinput
420
+
421
+ // wp-content/plugins/wordpress-seo/admin/pages/permalinks.php:
422
+ 'stripcategorybase' => _x( 'Strip the category base (usually <code>/category/</code>) from the category URL.', 'wordpress-seo', 'stream' ), # type = checkbox
423
+ 'trailingslash' => _x( 'Enforce a trailing slash on all category and tag URL\'s', 'wordpress-seo', 'stream' ), # type = checkbox
424
+ 'cleanslugs' => _x( 'Remove stop words from slugs.', 'wordpress-seo', 'stream' ), # type = checkbox
425
+ 'redirectattachment' => _x( 'Redirect attachment URL\'s to parent post URL.', 'wordpress-seo', 'stream' ), # type = checkbox
426
+ 'cleanreplytocom' => _x( 'Remove the <code>?replytocom</code> variables.', 'wordpress-seo', 'stream' ), # type = checkbox
427
+ 'cleanpermalinks' => _x( 'Redirect ugly URL\'s to clean permalinks. (Not recommended in many cases!)', 'wordpress-seo', 'stream' ), # type = checkbox
428
+ 'force_transport' => _x( 'Force Transport', 'wordpress-seo', 'stream' ), # type = select
429
+ 'cleanpermalink-googlesitesearch' => _x( 'Prevent cleaning out Google Site Search URL\'s.', 'wordpress-seo', 'stream' ), # type = checkbox
430
+ 'cleanpermalink-googlecampaign' => _x( 'Prevent cleaning out Google Analytics Campaign & Google AdWords Parameters.', 'wordpress-seo', 'stream' ), # type = checkbox
431
+ 'cleanpermalink-extravars' => _x( 'Other variables not to clean', 'wordpress-seo', 'stream' ), # type = textinput
432
+
433
+ // wp-content/plugins/wordpress-seo/admin/pages/social.php:
434
+ 'opengraph' => _x( 'Add Open Graph meta data', 'wordpress-seo', 'stream' ), # type = checkbox
435
+ 'facebook_site' => _x( 'Facebook Page URL', 'wordpress-seo', 'stream' ), # type = textinput
436
+ 'og_frontpage_image' => _x( 'Image URL', 'wordpress-seo', 'stream' ), # type = textinput
437
+ 'og_frontpage_desc' => _x( 'Description', 'wordpress-seo', 'stream' ), # type = textinput
438
+ 'og_default_image' => _x( 'Image URL', 'wordpress-seo', 'stream' ), # type = textinput
439
+ 'twitter' => _x( 'Add Twitter card meta data', 'wordpress-seo', 'stream' ), # type = checkbox
440
+ 'twitter_site' => _x( 'Site Twitter Username', 'wordpress-seo', 'stream' ), # type = textinput
441
+ 'twitter_card_type' => _x( 'The default card type to use', 'wordpress-seo', 'stream' ), # type = select
442
+ 'googleplus' => _x( 'Add Google+ specific post meta data (excluding author metadata)', 'wordpress-seo', 'stream' ), # type = checkbox
443
+ 'plus-publisher' => _x( 'Google Publisher Page', 'wordpress-seo', 'stream' ), # type = textinput
444
+
445
+ // wp-content/plugins/wordpress-seo/admin/pages/xml-sitemaps.php:
446
+ 'enablexmlsitemap' => _x( 'Check this box to enable XML sitemap functionality.', 'wordpress-seo', 'stream' ), # type = checkbox
447
+ 'disable_author_sitemap' => _x( 'Disable author/user sitemap', 'wordpress-seo', 'stream' ), # type = checkbox
448
+ 'xml_ping_yahoo' => _x( 'Ping Yahoo!', 'wordpress-seo', 'stream' ), # type = checkbox
449
+ 'xml_ping_ask' => _x( 'Ping Ask.com', 'wordpress-seo', 'stream' ), # type = checkbox
450
+ 'entries-per-page' => _x( 'Max entries per sitemap page', 'wordpress-seo', 'stream' ), # type = textinput
451
+
452
+ // Added manually
453
+ 'rssbefore' => _x( 'Content to put before each post in the feed', 'wordpress-seo', 'stream' ),
454
+ 'rssafter' => _x( 'Content to put after each post', 'wordpress-seo', 'stream' ),
455
+ );
456
+
457
+ $ast_labels = array(
458
+ 'title-' => _x( 'Title template', 'wordpress-seo', 'stream' ), # type = textinput
459
+ 'metadesc-' => _x( 'Meta description template', 'wordpress-seo', 'stream' ), # type = textarea
460
+ 'metakey-' => _x( 'Meta keywords template', 'wordpress-seo', 'stream' ), # type = textinput
461
+ 'noindex-' => _x( 'Meta Robots', 'wordpress-seo', 'stream' ), # type = checkbox
462
+ 'noauthorship-' => _x( 'Authorship', 'wordpress-seo', 'stream' ), # type = checkbox
463
+ 'showdate-' => _x( 'Show date in snippet preview?', 'wordpress-seo', 'stream' ), # type = checkbox
464
+ 'hideeditbox-' => _x( 'WordPress SEO Meta Box', 'wordpress-seo', 'stream' ), # type = checkbox
465
+ 'bctitle-' => _x( 'Breadcrumbs Title', 'wordpress-seo', 'stream' ), # type = textinput
466
+ 'post_types-' => _x( 'Post types', 'wordpress-seo', 'stream' ), # type = checkbox
467
+ 'taxonomies-' => _x( 'Taxonomies', 'wordpress-seo', 'stream' ), # type = checkbox
468
+ );
469
+
470
+ if ( $option ) {
471
+ if ( isset( $labels[ $option ] ) ) {
472
+ return $labels[ $option ];
473
+ } else {
474
+ foreach ( $ast_labels as $key => $trans ) {
475
+ if ( 0 === strpos( $option, $key ) ) {
476
+ return $trans;
477
+ }
478
+ }
479
+
480
+ return false;
481
+ }
482
+ }
483
+
484
+ return $labels;
485
+ }
486
+
487
+ }
contributing.md ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Contributing to this project
2
+
3
+ Please take a moment to review this document in order to make the contribution process easy and effective for everyone involved.
4
+
5
+ Following these guidelines will help us get back to you more quickly, and will show that you care about making Stream better just like we do. In return, we'll do our best to respond to your issue or pull request as soon as possible with the same respect.
6
+
7
+
8
+ ## Use the issue tracker
9
+
10
+ The [issue tracker](https://github.com/x-team/wp-stream/issues) is the preferred channel for [bug reports](#bugs), [features requests](#features) and [submitting pull requests](#pull-requests), but please respect the following restrictions:
11
+
12
+ * Support issues or usage questions that are not bugs should be posted on the [Plugin Support Forum](http://wordpress.org/support/plugin/stream).
13
+ * Please **do not** derail or troll issues. Keep the discussion on topic and respect the opinions of others.
14
+
15
+
16
+ <a name="bugs"></a>
17
+ ## Bug reports
18
+
19
+ A bug is a _demonstrable problem_ that is caused by the code in the repository. Good bug reports with complete error messages, environment details and screenshots are extremely helpful &mdash; thank you!
20
+
21
+ Guidelines for bug reports:
22
+
23
+ 1. **Check if the bug has already been fixed** &mdash; Someone may already be on top of it, so try to reproduce it using the latest from the `master` branch.
24
+
25
+ 2. **Use the [GitHub issue search](https://github.com/x-team/wp-stream/search?type=Issues)** &mdash; Someone might already know about it, so please check if the issue has already been reported.
26
+
27
+ 3. **Isolate the problem** &mdash; The better you can determine exactly what behavior(s) cause the issue, the faster and more effectively it can be resolved. “I’m getting an error message.” is not a good bug report. A good bug report shouldn't leave others needing to contact you for more information.
28
+
29
+ Please try to be as detailed as possible in your report. What is your environment? What steps will reproduce the issue? What browser(s) experience the problem? What outcome did you expect, and how did it differ from what you actually saw? All these details will help people to fix any potential bugs.
30
+
31
+ Example:
32
+
33
+ > Short and descriptive example bug report title
34
+ >
35
+ > A summary of the issue and the environment/browser in which it occurs. If
36
+ > suitable, include the steps required to reproduce the bug.
37
+ >
38
+ > 1. This is the first step
39
+ > 2. This is the second step
40
+ > 3. Further steps, etc.
41
+ >
42
+ > Any other information you want to share that is relevant to the issue being reported. This might include the lines of code that you have identified as causing the bug, and potential solutions (and your opinions on their merits).
43
+
44
+ **Note:** In an effort to keep open issues to a manageable number, we will close any issues that do not provide enough information for us to be able to work on a solution. You will be encouraged to provide the necessary details, after which we will reopen the issue.
45
+
46
+ <a name="features"></a>
47
+ ## Feature requests
48
+
49
+ Feature requests are very welcome! But take a moment to find out whether your idea fits with the scope and aims of the project. It's up to *you* to make a strong case to convince the project's developers of the merits of this feature. Please provide as much detail and context as possible.
50
+
51
+ Building something great means choosing features carefully especially because it is much, much easier to add features than it is to take them away. Additions to Stream will be evaluated on a combination of scope (how well it fits into the project), maintenance burden and general usefulness to users.
52
+
53
+ <a name="pull-requests"></a>
54
+ ## Pull requests
55
+
56
+ Good pull requests &mdash; patches, improvements, new features &mdash; are a fantastic help.
57
+ They should remain focused in scope and avoid containing unrelated commits.
58
+
59
+ **Please ask first** before embarking on any significant pull request (e.g. implementing features, refactoring code), otherwise you risk spending a lot of time working on something that the project's developers might not want to merge into the project. You can solicit feedback and opinions in an open enhancement issue, or [create a new one](https://github.com/x-team/wp-stream/issues/new).
60
+
61
+ Please use the [git flow for pull requests](#git-flow) and follow [WordPress Coding Standards](http://make.wordpress.org/core/handbook/coding-standards/) before submitting your work. Adhering to these guidelines is the best way to get your work included in Stream.
62
+
63
+ <a name="git-flow"></a>
64
+ #### Git Flow for pull requests
65
+
66
+ 1. [Fork](http://help.github.com/fork-a-repo/) the project, clone your fork, and configure the remotes:
67
+
68
+ ```bash
69
+ # Clone your fork of the repo into the current directory
70
+ git clone git@github.com:<YOUR_USERNAME>/wp-stream.git
71
+ # Navigate to the newly cloned directory
72
+ cd wp-stream
73
+ # Assign the original repo to a remote called "upstream"
74
+ git remote add upstream https://github.com/x-team/wp-stream
75
+ ```
76
+
77
+ 2. If you cloned a while ago, get the latest changes from upstream:
78
+
79
+ ```bash
80
+ git checkout master
81
+ git pull upstream master
82
+ ```
83
+
84
+ 3. Create a new topic branch (off the `master` branch) to contain your feature, change, or fix:
85
+
86
+ ```bash
87
+ git checkout -b <topic-branch-name>
88
+ ```
89
+
90
+ 4. Commit your changes in logical chunks. Please adhere to these [git commit message guidelines](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) or your code is unlikely be merged into the main project. Use Git's [interactive rebase](https://help.github.com/articles/interactive-rebase) feature to tidy up your commits before making them public.
91
+
92
+ 5. Locally merge (or rebase) the upstream development branch into your topic branch:
93
+
94
+ ```bash
95
+ git pull [--rebase] upstream master
96
+ ```
97
+
98
+ 6. Push your topic branch up to your fork:
99
+
100
+ ```bash
101
+ git push origin <topic-branch-name>
102
+ ```
103
+
104
+ 7. [Open a Pull Request](https://help.github.com/articles/using-pull-requests/) (with a clear title and description) to the `develop` branch.
105
+
106
+ **IMPORTANT**: By submitting a patch, you agree to allow the project owner to license your work under the [GPL v2 license](http://www.gnu.org/licenses/gpl-2.0.html).
extensions/notifications/class-wp-stream-notifications.php ADDED
@@ -0,0 +1,273 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class WP_Stream_Notifications {
4
+
5
+ /**
6
+ * Hold Stream Notifications instance
7
+ *
8
+ * @var string
9
+ */
10
+ public static $instance;
11
+
12
+ /**
13
+ * Screen ID for my admin page
14
+ * @var string
15
+ */
16
+ public static $screen_id;
17
+
18
+ /**
19
+ * Holds admin notices messages
20
+ *
21
+ * @var array
22
+ */
23
+ public static $messages = array();
24
+
25
+ /*
26
+ * List of registered adapters
27
+ * @var array
28
+ */
29
+ public static $adapters = array();
30
+
31
+ /**
32
+ * Matcher object
33
+ *
34
+ * @var WP_Stream_Notifications_Matcher
35
+ */
36
+ public $matcher;
37
+
38
+ /**
39
+ * Page slug for notifications list table screen
40
+ *
41
+ * @const string
42
+ */
43
+ const NOTIFICATIONS_PAGE_SLUG = 'wp_stream_notifications';
44
+ // Todo: We should probably check whether the current user has caps to
45
+ // view and edit the notifications as this can differ from caps to Stream.
46
+
47
+ /**
48
+ * Capability for the Notifications to be viewed
49
+ *
50
+ * @const string
51
+ */
52
+ const VIEW_CAP = 'view_stream_notifications';
53
+
54
+ /**
55
+ * Return active instance of this class, create one if it doesn't exist
56
+ *
57
+ * @return WP_Stream_Notifications
58
+ */
59
+ public static function get_instance() {
60
+ if ( empty( self::$instance ) ) {
61
+ self::$instance = new self();
62
+ }
63
+ return self::$instance;
64
+ }
65
+
66
+ /**
67
+ * Class constructor
68
+ */
69
+ private function __construct() {
70
+ define( 'WP_STREAM_NOTIFICATIONS_DIR', WP_STREAM_EXTENSIONS_DIR . 'notifications/' ); // Has trailing slash
71
+ define( 'WP_STREAM_NOTIFICATIONS_URL', WP_STREAM_URL . 'extensions/notifications/' ); // Has trailing slash
72
+ define( 'WP_STREAM_NOTIFICATIONS_INC_DIR', WP_STREAM_NOTIFICATIONS_DIR . 'includes/' ); // Has trailing slash
73
+
74
+ if ( ! apply_filters( 'wp_stream_notifications_load', true ) ) {
75
+ return;
76
+ }
77
+
78
+ add_action( 'init', array( $this, 'load' ) );
79
+
80
+ // Register post type
81
+ require_once WP_STREAM_NOTIFICATIONS_INC_DIR . 'class-wp-stream-notifications-post-type.php';
82
+
83
+ WP_Stream_Notifications_Post_Type::get_instance();
84
+ }
85
+
86
+ /**
87
+ * Load our classes, actions/filters, only if our big brother is activated.
88
+ * GO GO GO!
89
+ *
90
+ * @return void
91
+ */
92
+ public function load() {
93
+ // Register new submenu
94
+ if ( ! apply_filters( 'wp_stream_notifications_disallow_site_access', false ) && ! WP_Stream_Admin::$disable_access && ( WP_Stream::is_connected() || WP_Stream::is_development_mode() ) ) {
95
+ add_action( 'admin_menu', array( $this, 'register_menu' ), 11 );
96
+ }
97
+
98
+ // Load settings, enabling extensions to hook in
99
+ require_once WP_STREAM_NOTIFICATIONS_INC_DIR . 'class-wp-stream-notifications-settings.php';
100
+ add_action( 'init', array( 'WP_Stream_Notifications_Settings', 'load' ), 9 );
101
+
102
+ if ( WP_Stream_API::is_restricted() ) {
103
+ add_action( 'in_admin_header', array( __CLASS__, 'in_admin_header' ) );
104
+ return;
105
+ }
106
+
107
+ // Include all adapters
108
+ include_once WP_STREAM_NOTIFICATIONS_INC_DIR . 'class-wp-stream-notifications-adapter.php';
109
+ $adapters = array( 'email', 'push', 'sms' );
110
+
111
+ foreach ( $adapters as $adapter ) {
112
+ include WP_STREAM_NOTIFICATIONS_INC_DIR . 'adapters/class-wp-stream-notifications-adapter-' . $adapter . '.php';
113
+ }
114
+
115
+ // Load Matcher
116
+ include_once WP_STREAM_NOTIFICATIONS_INC_DIR . 'class-wp-stream-notifications-matcher.php';
117
+ $this->matcher = new WP_Stream_Notifications_Matcher();
118
+ }
119
+
120
+ public static function in_admin_header() {
121
+ global $typenow;
122
+
123
+ if ( WP_Stream_Notifications_Post_Type::POSTTYPE !== $typenow ) {
124
+ return;
125
+ }
126
+ ?>
127
+ <div class="stream-example">
128
+ <div class="stream-example-modal">
129
+ <h1><i class="dashicons dashicons-admin-comments"></i> <?php _e( 'Stream Notifications', 'stream' ) ?></h1>
130
+ <p><?php _e( 'Get notified instantly when important changes are made on your site.', 'stream' ) ?></p>
131
+ <ul>
132
+ <li><i class="dashicons dashicons-yes"></i> <?php _e( 'Create notification rules quickly and easily', 'stream' ) ?></li>
133
+ <li><i class="dashicons dashicons-yes"></i> <?php _e( 'Smart and powerful trigger matching', 'stream' ) ?></li>
134
+ <li><i class="dashicons dashicons-yes"></i> <?php _e( 'Fully customized e-mail and SMS alerts', 'stream' ) ?></li>
135
+ <li><i class="dashicons dashicons-yes"></i> <?php _e( 'Push alerts to your smartphone or tablet', 'stream' ) ?></li>
136
+ </ul>
137
+ <a href="<?php echo esc_url( WP_Stream_Admin::account_url( sprintf( 'upgrade?site_uuid=%s', WP_Stream::$api->site_uuid ) ) ); ?>" class="button button-primary button-large"><?php _e( 'Upgrade to Pro', 'stream' ) ?></a>
138
+ </div>
139
+ </div>
140
+ <?php
141
+ }
142
+
143
+ /**
144
+ * Register Notification menu under Stream's main one
145
+ *
146
+ * @action admin_menu
147
+ * @return void
148
+ */
149
+ public function register_menu() {
150
+ self::$screen_id = add_submenu_page(
151
+ WP_Stream_Admin::RECORDS_PAGE_SLUG,
152
+ __( 'Notifications', 'stream' ),
153
+ __( 'Notifications', 'stream' ),
154
+ self::VIEW_CAP,
155
+ sprintf( 'edit.php?post_type=%s', WP_Stream_Notifications_Post_Type::POSTTYPE )
156
+ );
157
+ }
158
+
159
+ public static function register_adapter( $adapter, $name, $title ) {
160
+ self::$adapters[ $name ] = array(
161
+ 'title' => $title,
162
+ 'class' => $adapter,
163
+ );
164
+ }
165
+
166
+ /**
167
+ * Display all messages on admin board
168
+ *
169
+ * @return void
170
+ */
171
+ public static function admin_notices() {
172
+ foreach ( self::$messages as $message ) {
173
+ echo wp_kses_post( $message );
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Plugin activation routine
179
+ * @return void
180
+ */
181
+ public function on_activation() {
182
+ // Add sample rule
183
+ $args = array(
184
+ 'post_type' => WP_Stream_Notifications_Post_Type::POSTTYPE,
185
+ 'post_status' => 'any',
186
+ 'posts_per_page' => 1,
187
+ );
188
+
189
+ if ( ! get_posts( $args ) ) {
190
+ $this->add_sample_rule();
191
+ }
192
+ }
193
+
194
+ /**
195
+ * Add a sample rule, used upon activation
196
+ *
197
+ */
198
+ public function add_sample_rule() {
199
+ $postarr = array(
200
+ 'post_title' => __( 'Sample Rule', 'stream' ),
201
+ 'post_status' => 'draft',
202
+ 'post_type' => WP_Stream_Notifications_Post_Type::POSTTYPE,
203
+ );
204
+
205
+ $meta = array(
206
+ 'triggers' => array(
207
+ array(
208
+ 'group' => 0,
209
+ 'relation' => 'and',
210
+ 'type' => 'author_role',
211
+ 'operator' => '!=',
212
+ 'value' => 'administrator',
213
+ ),
214
+ array(
215
+ 'group' => 0,
216
+ 'relation' => 'and',
217
+ 'type' => 'action',
218
+ 'operator' => '=',
219
+ 'value' => 'updated',
220
+ ),
221
+ array(
222
+ 'group' => 1,
223
+ 'relation' => 'and',
224
+ 'type' => 'author_role',
225
+ 'operator' => '=',
226
+ 'value' => 'administrator',
227
+ ),
228
+ array(
229
+ 'group' => 1,
230
+ 'relation' => 'and',
231
+ 'type' => 'connector',
232
+ 'operator' => '=',
233
+ 'value' => 'widgets',
234
+ ),
235
+ array(
236
+ 'group' => 1,
237
+ 'relation' => 'and',
238
+ 'type' => 'action',
239
+ 'operator' => '=',
240
+ 'value' => 'sorted',
241
+ ),
242
+ ),
243
+ 'groups' => array(
244
+ 1 => array(
245
+ 'group' => 0,
246
+ 'relation' => 'or',
247
+ ),
248
+ ),
249
+ 'alerts' => array(
250
+ array(
251
+ 'type' => 'email',
252
+ 'users' => '1',
253
+ 'emails' => '',
254
+ 'subject' => sprintf( __( '[Site Activity Alert] %s', 'stream' ), get_bloginfo( 'name' ) ),
255
+ 'message' => sprintf( __( 'The following just happened on your site: %s by %s Date of action: %s', 'stream' ), "\r\n\r\n{summary}", "{author.display_name}\r\n\r\n", '{created}' )
256
+ ),
257
+ ),
258
+ );
259
+
260
+ $post_id = wp_insert_post( $postarr );
261
+
262
+ if ( is_a( $post_id, 'WP_Error' ) ) {
263
+ return $post_id;
264
+ }
265
+
266
+ foreach ( $meta as $key => $val ) {
267
+ update_post_meta( $post_id, $key, $val );
268
+ }
269
+ }
270
+
271
+ }
272
+
273
+ $GLOBALS['wp_stream_notifications'] = WP_Stream_Notifications::get_instance();
extensions/notifications/includes/adapters/class-wp-stream-notifications-adapter-email.php ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class WP_Stream_Notifications_Adapter_Email extends WP_Stream_Notifications_Adapter {
4
+
5
+ public static function register( $title = '' ) {
6
+ parent::register( __( 'Email', 'stream' ) );
7
+ }
8
+
9
+ public static function fields() {
10
+ return array(
11
+ 'users' => array(
12
+ 'title' => __( 'Send to Users', 'stream' ),
13
+ 'type' => 'hidden',
14
+ 'multiple' => true,
15
+ 'ajax' => true,
16
+ 'key' => 'author',
17
+ 'hint' => __( 'Alert specific users via email.', 'stream' ),
18
+ ),
19
+ 'emails' => array(
20
+ 'title' => __( 'Send to Emails', 'stream' ),
21
+ 'type' => 'text',
22
+ 'tags' => true,
23
+ 'hint' => __( 'Alert any arbitrary email address not tied to a specific user.', 'stream' ),
24
+ ),
25
+ 'subject' => array(
26
+ 'title' => __( 'Subject', 'stream' ),
27
+ 'type' => 'text',
28
+ 'hint' => __( 'Data tags are allowed.', 'stream' ),
29
+ ),
30
+ 'message' => array(
31
+ 'title' => __( 'Message', 'stream' ),
32
+ 'type' => 'textarea',
33
+ 'hint' => __( 'HTML and data tags are allowed.', 'stream' ),
34
+ ),
35
+ );
36
+ }
37
+
38
+ public function send( $log ) {
39
+ $users = $this->params['users'];
40
+ $user_emails = array();
41
+ if ( $users ) {
42
+ $user_query = new WP_User_Query(
43
+ array(
44
+ 'include' => $users,
45
+ 'fields' => array( 'user_email' ),
46
+ )
47
+ );
48
+ $user_emails = wp_list_pluck( $user_query->results, 'user_email' );
49
+ }
50
+ $emails = $this->replace( $this->params['emails'], $log );
51
+ $emails = explode( ',', $emails );
52
+ if ( ! empty( $user_emails ) ) {
53
+ $emails = array_merge( $emails, $user_emails );
54
+ }
55
+ $emails = array_filter( $emails );
56
+ $subject = $this->replace( $this->params['subject'], $log );
57
+ $message = $this->replace( $this->params['message'], $log );
58
+ wp_mail( $emails, $subject, $message );
59
+ }
60
+
61
+ }
62
+
63
+ WP_Stream_Notifications_Adapter_Email::register();
extensions/notifications/includes/adapters/class-wp-stream-notifications-adapter-push.php ADDED
@@ -0,0 +1,190 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class WP_Stream_Notifications_Adapter_Push extends WP_Stream_Notifications_Adapter {
4
+
5
+ const PUSHOVER_OPTION_NAME = 'ckpn_pushover_notifications_settings';
6
+
7
+ public static function register( $title = '' ) {
8
+ parent::register( __( 'Push', 'stream' ) );
9
+ add_filter( 'wp_stream_serialized_labels', array( __CLASS__, 'pushover_key_labels' ) );
10
+ }
11
+
12
+ public static function get_application_key() {
13
+ $options = get_option( self::PUSHOVER_OPTION_NAME, array() );
14
+ $result = ( isset( $options['application_key'] ) && ! empty( $options['application_key'] ) ) ? $options['application_key'] : false;
15
+
16
+ return $result;
17
+ }
18
+
19
+ public static function fields() {
20
+ include_once ABSPATH . 'wp-admin/includes/plugin.php';
21
+
22
+ $plugin_path = defined( 'CKPN_FILE' ) ? CKPN_FILE : null;
23
+ $is_installed = ( $plugin_path && defined( 'WP_PLUGIN_DIR' ) && file_exists( trailingslashit( WP_PLUGIN_DIR ) . $plugin_path ) );
24
+
25
+ if ( ! $is_installed ) {
26
+ $fields = array(
27
+ 'error' => array(
28
+ 'title' => __( 'Missing Required Plugin', 'stream' ),
29
+ 'type' => 'error',
30
+ 'message' => sprintf(
31
+ __( 'Please install and activate the %1$s plugin to enable push alerts.', 'stream' ),
32
+ sprintf(
33
+ '<a href="%1$s" target="_blank">%2$s</a>',
34
+ esc_url( 'http://wordpress.org/plugins/pushover-notifications/' ),
35
+ __( 'Pushover Notifications', 'stream' )
36
+ )
37
+ ),
38
+ ),
39
+ );
40
+ } elseif ( ! is_plugin_active( $plugin_path ) ) {
41
+ $fields = array(
42
+ 'error' => array(
43
+ 'title' => __( 'Required Plugin Not Activated', 'stream' ),
44
+ 'type' => 'error',
45
+ 'message' => sprintf(
46
+ __( 'Please activate the %1$s plugin to enable push alerts.', 'stream' ),
47
+ sprintf(
48
+ '<a href="%1$s">%2$s</a>',
49
+ self_admin_url( 'plugins.php' ),
50
+ __( 'Pushover Notifications', 'stream' )
51
+ )
52
+ ),
53
+ ),
54
+ );
55
+ } elseif ( false !== self::get_application_key() ) {
56
+ $fields = array(
57
+ 'users' => array(
58
+ 'title' => __( 'Send to Users', 'stream' ),
59
+ 'type' => 'hidden',
60
+ 'multiple' => true,
61
+ 'ajax' => true,
62
+ 'key' => 'author',
63
+ 'args' => array(
64
+ 'push' => true,
65
+ ),
66
+ 'hint' => array(
67
+ // hint 1
68
+ __( 'Alert specific users via push.', 'stream' ),
69
+
70
+ // hint 2
71
+ sprintf(
72
+ __( 'Only those users with a %s in their profile can be selected.', 'stream' ),
73
+ sprintf(
74
+ '<a href="%s" target="_blank">%s</a>',
75
+ self_admin_url( 'profile.php#wp-stream-highlight:ckpn_user_key' ),
76
+ __( 'Pushover User Key', 'stream' )
77
+ )
78
+ ),
79
+ ),
80
+ ),
81
+ 'subject' => array(
82
+ 'title' => __( 'Subject', 'stream' ),
83
+ 'type' => 'text',
84
+ 'hint' => __( 'Data tags are allowed.', 'stream' ),
85
+ ),
86
+ 'message' => array(
87
+ 'title' => __( 'Message', 'stream' ),
88
+ 'type' => 'textarea',
89
+ 'hint' => __( 'Data tags are allowed.', 'stream' ),
90
+ ),
91
+ );
92
+ } else {
93
+ $fields = array(
94
+ 'error' => array(
95
+ 'title' => __( 'Application key is missing', 'stream' ),
96
+ 'type' => 'error',
97
+ 'message' => sprintf(
98
+ __( 'Please provide your Application key on %1$s.', 'stream' ),
99
+ sprintf(
100
+ '<a href="%1$s">%2$s</a>',
101
+ self_admin_url( 'options-general.php?page=pushover-notifications' ),
102
+ __( 'Pushover Notifications settings page', 'stream' )
103
+ )
104
+ ),
105
+ ),
106
+ );
107
+ }
108
+
109
+ return $fields;
110
+ }
111
+
112
+ public function send( $log ) {
113
+ $application_key = self::get_application_key();
114
+
115
+ if ( false === $application_key ) {
116
+ return false;
117
+ }
118
+
119
+ if ( ! empty( $this->params['users'] ) ) {
120
+ $users_ids = explode( ',', $this->params['users'] );
121
+ $users = get_users( array(
122
+ 'include' => $users_ids,
123
+ 'fields' => 'ID',
124
+ 'meta_key' => 'ckpn_user_key',
125
+ ) );
126
+ $users_pushover_keys = array_map(
127
+ function( $user_id ) {
128
+ return get_user_meta( $user_id, 'ckpn_user_key', true );
129
+ },
130
+ $users
131
+ );
132
+ }
133
+
134
+ $subject = isset( $this->params['subject'] ) ? $this->replace( $this->params['subject'], $log ) : null;
135
+ $message = isset( $this->params['message'] ) ? $this->replace( $this->params['message'], $log ) : null;
136
+
137
+ $post_fields = array(
138
+ 'token' => $application_key,
139
+ 'message' => $message,
140
+ 'title' => $subject,
141
+ );
142
+
143
+ $connection = curl_init();
144
+
145
+ if ( ! isset( $users_pushover_keys ) || ! $users_pushover_keys ) {
146
+ return false;
147
+ }
148
+
149
+ foreach ( $users_pushover_keys as $key ) {
150
+ $post_fields['user'] = $key;
151
+ curl_setopt_array(
152
+ $connection,
153
+ array(
154
+ CURLOPT_URL => 'https://api.pushover.net/1/messages.json',
155
+ CURLOPT_POST => true,
156
+ CURLOPT_RETURNTRANSFER => 1,
157
+ CURLOPT_POSTFIELDS => http_build_query( $post_fields ),
158
+ )
159
+ );
160
+ $response = curl_exec( $connection );
161
+ }
162
+ curl_close( $connection );
163
+ }
164
+
165
+ /**
166
+ * @filter wp_stream_serialized_labels
167
+ */
168
+ public static function pushover_key_labels( $labels ) {
169
+ $labels[ self::PUSHOVER_OPTION_NAME ] = array(
170
+ 'application_key' => __( 'Application API Token/Key', 'stream' ),
171
+ 'api_key' => __( 'Your User Key', 'stream' ),
172
+ 'new_user' => __( 'New Users', 'stream' ),
173
+ 'new_post' => __( 'New Posts are Published', 'stream' ),
174
+ 'new_post_roles' => __( 'Roles to Notify', 'stream' ),
175
+ 'new_comment' => __( 'New Comments', 'stream' ),
176
+ 'notify_authors' => __( 'Notify the Post Author (for multi-author blogs)', 'stream' ),
177
+ 'password_reset' => __( 'Notify users when password resets are requested for their accounts', 'stream' ),
178
+ 'core_update' => __( 'WordPress Core Update is Available', 'stream' ),
179
+ 'plugin_updates' => __( 'Plugin & Theme Updates are Available', 'stream' ),
180
+ 'multiple_keys' => __( 'Use Multiple Application Keys', 'stream' ),
181
+ 'sslverify' => __( 'Verify SSL from api.pushover.net', 'stream' ),
182
+ 'logging' => __( 'Enable Logging', 'stream' ),
183
+ );
184
+
185
+ return $labels;
186
+ }
187
+
188
+ }
189
+
190
+ WP_Stream_Notifications_Adapter_Push::register();
extensions/notifications/includes/adapters/class-wp-stream-notifications-adapter-sms.php ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class WP_Stream_Notification_Adapter_SMS extends WP_Stream_Notifications_Adapter {
4
+
5
+ public static function register( $title = '' ) {
6
+ parent::register( __( 'SMS', 'stream' ) );
7
+ }
8
+
9
+ public static function fields() {
10
+ return array(
11
+ 'mobile_number' => array(
12
+ 'title' => __( 'Send to Mobile Number', 'stream' ),
13
+ 'type' => 'text',
14
+ 'multiple' => false,
15
+ 'tags' => true,
16
+ 'hint' => __( 'Enter mobile numbers without dashes (ex: 8885550000)', 'stream' ),
17
+ ),
18
+ 'carrier' => array(
19
+ 'title' => __( 'Carrier', 'stream' ),
20
+ 'type' => 'select',
21
+ 'hint' => __( 'Select your mobile service provider.', 'stream' ),
22
+ 'ajax' => false,
23
+ 'options' => array(
24
+ '@sms.3rivers.net' => esc_html__( '3 River Wireless', 'stream' ),
25
+ '@paging.acswireless.com' => esc_html__( 'ACS Wireless', 'stream' ),
26
+ '@message.alltel.com' => esc_html__( 'Alltel', 'stream' ),
27
+ '@txt.att.net' => esc_html__( 'AT&T, Cingular, Net10 or Tracfone', 'stream' ),
28
+ '@bellmobility.ca' => esc_html__( 'Bell Canada', 'stream' ),
29
+ '@txt.bell.ca' => esc_html__( 'Bell Mobility (Canada), Presidents Choice or Solo Mobile', 'stream' ),
30
+ '@txt.bellmobility.ca' => esc_html__( 'Bell Mobility', 'stream' ),
31
+ '@blueskyfrog.com' => esc_html__( 'Blue Sky Frog', 'stream' ),
32
+ '@sms.bluecell.com' => esc_html__( 'Bluegrass Cellular', 'stream' ),
33
+ '@myboostmobile.com' => esc_html__( 'Boost Mobile', 'stream' ),
34
+ '@bplmobile.com' => esc_html__( 'BPL Mobile', 'stream' ),
35
+ '@cwwsms.com' => esc_html__( 'Carolina West Wireless', 'stream' ),
36
+ '@mobile.celloneusa.com' => esc_html__( 'Cellular One', 'stream' ),
37
+ '@csouth1.com' => esc_html__( 'Cellular South', 'stream' ),
38
+ '@messaging.centurytel.net' => esc_html__( 'CenturyTel', 'stream' ),
39
+ '@msg.clearnet.com' => esc_html__( 'Clearnet', 'stream' ),
40
+ '@comcastpcs.textmsg.com' => esc_html__( 'Comcast', 'stream' ),
41
+ '@corrwireless.net' => esc_html__( 'Corr Wireless Communications', 'stream' ),
42
+ '@sms.mycricket.com' => esc_html__( 'Cricket', 'stream' ),
43
+ '@mobile.dobson.net' => esc_html__( 'Dobson', 'stream' ),
44
+ '@sms.edgewireless.com' => esc_html__( 'Edge Wireless', 'stream' ),
45
+ '@fido.ca' => esc_html__( 'Fido', 'stream' ),
46
+ '@sms.goldentele.com' => esc_html__( 'Golden Telecom', 'stream' ),
47
+ '@text.houstoncellular.net' => esc_html__( 'Houston Cellular', 'stream' ),
48
+ '@ideacellular.net' => esc_html__( 'Idea Cellular', 'stream' ),
49
+ '@ivctext.com' => esc_html__( 'Illinois Valley Cellular', 'stream' ),
50
+ '@inlandlink.com' => esc_html__( 'Inland Cellular Telephone', 'stream' ),
51
+ '@pagemci.com' => esc_html__( 'MCI', 'stream' ),
52
+ '@page.metrocall.com' => esc_html__( 'Metrocall', 'stream' ),
53
+ '@my2way.com' => esc_html__( 'Metrocall 2-way', 'stream' ),
54
+ '@mymetropcs.com' => esc_html__( 'Metro PCS', 'stream' ),
55
+ '@clearlydigital.com' => esc_html__( 'Midwest Wireless', 'stream' ),
56
+ '@mobilecomm.net' => esc_html__( 'Mobilcomm', 'stream' ),
57
+ '@text.mtsmobility.com' => esc_html__( 'MTS', 'stream' ),
58
+ '@messaging.nextel.com' => esc_html__( 'Nextel', 'stream' ),
59
+ '@onlinebeep.net' => esc_html__( 'OnlineBeep', 'stream' ),
60
+ '@pcsone.net' => esc_html__( 'PCS One', 'stream' ),
61
+ '@sms.pscel.com' => esc_html__( 'Public Service Cellular', 'stream' ),
62
+ '@qwestmp.com' => esc_html__( 'Qwest', 'stream' ),
63
+ '@pcs.rogers.com' => esc_html__( 'Rogers AT&T Wireless and Rogers Canada', 'stream' ),
64
+ '@satellink.net' => esc_html__( 'Satellink', 'stream' ),
65
+ '@messaging.sprintpcs.com' => esc_html__( 'Sprint or Helio', 'stream' ),
66
+ '@tms.suncom.com' => esc_html__( 'Suncom and Triton', 'stream' ),
67
+ '@mobile.surewest.com' => esc_html__( 'Surewest Communications', 'stream' ),
68
+ '@tmomail.net' => esc_html__( 'T-Mobile', 'stream' ),
69
+ '@msg.telus.com' => esc_html__( 'Telus', 'stream' ),
70
+ '@utext.com' => esc_html__( 'Unicel', 'stream' ),
71
+ '@email.uscc.net' => esc_html__( 'US Cellular', 'stream' ),
72
+ '@uswestdatamail.com' => esc_html__( 'US West', 'stream' ),
73
+ '@vtext.com' => esc_html__( 'Verizon or Straight Talk', 'stream' ),
74
+ '@vmobl.com' => esc_html__( 'Virgin Mobile', 'stream' ),
75
+ '@vmobile.ca' => esc_html__( 'Virgin Mobile Canada', 'stream' ),
76
+ '@sms.wcc.net' => esc_html__( 'West Central Wireless', 'stream' ),
77
+ '@cellularonewest.com' => esc_html__( 'Western Wireless', 'stream' ),
78
+ ),
79
+ ),
80
+ 'message' => array(
81
+ 'title' => __( 'Message', 'stream' ),
82
+ 'type' => 'textarea',
83
+ 'hint' => __( 'Data tags are allowed. HTML is not allowed.', 'stream' ),
84
+ ),
85
+ );
86
+ }
87
+
88
+ public function send( $log ) {
89
+ $number = preg_replace( '/\D/', '', $this->params['mobile_number'] ); // Removes all non-numeric characters
90
+ $to = sanitize_email( $number . $this->params['carrier'] );
91
+ $message = $this->replace( strip_tags( $this->params['message'] ), $log );
92
+
93
+ wp_mail( $to, null, $message );
94
+ }
95
+
96
+ }
97
+
98
+ WP_Stream_Notification_Adapter_SMS::register();
extensions/notifications/includes/class-wp-stream-notifications-adapter.php ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ abstract class WP_Stream_Notifications_Adapter {
4
+
5
+ public $params = array();
6
+
7
+ public static function register( $title ) {
8
+ $class = get_called_class();
9
+ $name = strtolower( str_replace( 'WP_Stream_Notifications_Adapter_', '', $class ) );
10
+ WP_Stream_Notifications::register_adapter( $class, $name, $title );
11
+ }
12
+
13
+ public static function fields() {
14
+ return array();
15
+ }
16
+
17
+ public static function hints() {
18
+ return '';
19
+ }
20
+
21
+ /**
22
+ * Replace placeholders in alert[field]s with proper info from the log
23
+ * @param string $haystack Text to replace in
24
+ * @param array $log Log array
25
+ * @return string
26
+ */
27
+ public static function replace( $haystack, $log ) {
28
+ if ( preg_match_all( '#{([^}]+)}#', $haystack, $placeholders ) ) {
29
+
30
+ foreach ( $placeholders[1] as $placeholder ) {
31
+ $value = false;
32
+ switch ( $placeholder ) {
33
+ case 'summary':
34
+ case 'object_id':
35
+ case 'author':
36
+ case 'ip':
37
+ $value = $log[ $placeholder ];
38
+ break;
39
+ case 'created':
40
+ $value = get_date_from_gmt( date( 'Y-m-d H:i:s', strtotime( $log[ $placeholder ] ) ) );
41
+ break;
42
+ case 'connector':
43
+ $value = WP_Stream_Connectors::$term_labels['stream_connector'][ $log[ $placeholder ] ];
44
+ break;
45
+ case 'context':
46
+ $value = WP_Stream_Connectors::$term_labels['stream_context'][ $log['context'] ];
47
+ break;
48
+ case 'action':
49
+ $value = WP_Stream_Connectors::$term_labels['stream_action'][ $log['action'] ];
50
+ break;
51
+ case ( false !== strpos( $placeholder, 'meta.' ) ):
52
+ $meta_key = substr( $placeholder, 5 );
53
+ if ( isset( $log['meta'][ $meta_key ] ) ) {
54
+ $value = $log['meta'][ $meta_key ];
55
+ }
56
+ break;
57
+ case ( false !== strpos( $placeholder, 'author.' ) ):
58
+ $meta_key = substr( $placeholder, 7 );
59
+ $author = get_userdata( $log['author'] );
60
+ if ( $author && isset( $author->{$meta_key} ) ) {
61
+ $value = $author->{$meta_key};
62
+ }
63
+ break;
64
+ // TODO Move this part to Stream base, and abstract it
65
+ case ( false !== strpos( $placeholder, 'object.' ) ):
66
+ $meta_key = substr( $placeholder, 7 );
67
+ $context = $log['context'];
68
+ // can only guess the object type, since there is no
69
+ // actual reference here
70
+ switch ( $context ) {
71
+ case 'post':
72
+ case 'page':
73
+ case 'media':
74
+ $object = get_post( $log['object_id'] );
75
+ break;
76
+ case 'users':
77
+ $object = get_userdata( $log['object_id'] );
78
+ break;
79
+ case 'comment':
80
+ $object = get_comment( $log['object_id'] );
81
+ break;
82
+ case 'term':
83
+ case 'category':
84
+ case 'post_tag':
85
+ case 'link_category':
86
+ $object = get_term( $log['object_id'], $log['meta']['taxonomy'] );
87
+ break;
88
+ default:
89
+ $object = apply_filters( 'wp_stream_notifications_record_object', $log['object_id'], $log );
90
+ break;
91
+ }
92
+ if ( is_object( $object ) && isset( $object->{$meta_key} ) ) {
93
+ $value = $object->{$meta_key};
94
+ }
95
+ break;
96
+ }
97
+ if ( $value ) {
98
+ $haystack = str_replace( "{{$placeholder}}", $value, $haystack );
99
+ }
100
+ }
101
+ }
102
+ return $haystack;
103
+ }
104
+
105
+ function load( $alert ) {
106
+ $params = array();
107
+ $fields = $this::fields();
108
+ foreach ( $fields as $field => $options ) {
109
+ $params[ $field ] = isset( $alert[ $field ] )
110
+ ? $alert[ $field ]
111
+ : null;
112
+ }
113
+ $this->params = $params;
114
+ return $this;
115
+ }
116
+
117
+ abstract function send( $log );
118
+
119
+ }
extensions/notifications/includes/class-wp-stream-notifications-list-table.php ADDED
@@ -0,0 +1,246 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class WP_Stream_Notifications_List_Table {
4
+
5
+ /**
6
+ * Hold Singleton instance
7
+ *
8
+ * @var string
9
+ */
10
+ public static $instance;
11
+
12
+ /**
13
+ * Return active instance of this class, create one if it doesn't exist
14
+ *
15
+ * @return WP_Stream_Notifications_List_Table
16
+ */
17
+ public static function get_instance() {
18
+ if ( empty( self::$instance ) ) {
19
+ self::$instance = new self();
20
+ }
21
+
22
+ return self::$instance;
23
+ }
24
+
25
+ private function __construct( $args = array() ) {
26
+ add_filter( 'manage_stream-notification_posts_columns', array( $this, 'column_heads' ) );
27
+ add_action( 'manage_stream-notification_posts_custom_column', array( $this, 'column_content' ), 10, 2 );
28
+ add_action( 'manage_edit-stream-notification_sortable_columns', array( $this, 'column_sortable' ) );
29
+ add_filter( 'request', array( $this, 'column_sortable_request' ) );
30
+
31
+ // Add inline row actions
32
+ add_filter( 'post_row_actions', array( $this, 'row_actions' ), 10, 2 );
33
+
34
+ // Add bulk actions
35
+ add_action( 'admin_head', array( $this, 'scripts' ) );
36
+ // Parse actions
37
+ add_action( 'load-edit.php', array( $this, 'actions' ) );
38
+ }
39
+
40
+ /**
41
+ * @filter manage_posts_columns
42
+ *
43
+ * @param $cols
44
+ *
45
+ * @return array
46
+ */
47
+ public function column_heads( $cols ) {
48
+ $new = array(
49
+ 'type' => esc_html__( 'Type', 'stream' ),
50
+ 'occ' => esc_html__( 'Occurrences', 'stream' ),
51
+ );
52
+
53
+ $cols = array_merge( array_splice( $cols, 0, 2 ), $new, array_splice( $cols, 0 ) );
54
+
55
+ return $cols;
56
+ }
57
+
58
+ /**
59
+ * @action manage_posts_custom_column
60
+ *
61
+ * @param $name
62
+ * @param $post_id
63
+ */
64
+ public function column_content( $name, $post_id ) {
65
+ if ( 'type' === $name ) {
66
+ echo esc_html( $this->get_rule_alert_types( $post_id ) );
67
+ } elseif ( 'occ' === $name ) {
68
+ echo absint( ( $occ = get_post_meta( $post_id, 'occurrences', true ) ) ? $occ : 0 );
69
+ }
70
+ }
71
+
72
+ /**
73
+ * @filter manage_posts_sortable_columns
74
+ *
75
+ * @param $sortable
76
+ *
77
+ * @return array
78
+ */
79
+ public function column_sortable( $sortable ) {
80
+ return $sortable + array(
81
+ 'occ' => array( 'occurrences', true ),
82
+ );
83
+ }
84
+
85
+ /**
86
+ * @filter request
87
+ *
88
+ * @param array $vars
89
+ *
90
+ * @return mixed
91
+ */
92
+ public function column_sortable_request( $vars ) {
93
+ if ( ! is_admin() ) {
94
+ return $vars;
95
+ }
96
+
97
+ if ( isset( $vars['orderby'] ) && 'occurrences' === $vars['orderby'] ) {
98
+ $vars['meta_query'] = array(
99
+ 'relation' => 'OR',
100
+ 0 => array(
101
+ 'key' => 'occurrences',
102
+ 'compare' => 'EXISTS',
103
+ ),
104
+ 1 => array(
105
+ 'key' => 'occurrences',
106
+ 'compare' => 'NOT EXISTS',
107
+ ),
108
+ );
109
+ $vars['meta_key'] = 'occurrences';
110
+ $vars['orderby'] = 'meta_value_num';
111
+ }
112
+
113
+ return $vars;
114
+ }
115
+
116
+ /**
117
+ * Retrieve rule alert types' labels
118
+ *
119
+ * @param $post_id
120
+ *
121
+ * @return string
122
+ */
123
+ function get_rule_alert_types( $post_id ) {
124
+ $alerts = get_post_meta( $post_id, 'alerts', true );
125
+
126
+ if ( empty( $alerts ) ) {
127
+ return esc_html__( 'N/A', 'stream' );
128
+ } else {
129
+ $types = wp_list_pluck( $alerts, 'type' );
130
+ $titles = wp_list_pluck(
131
+ array_intersect_key(
132
+ WP_Stream_Notifications::$adapters,
133
+ array_flip( $types )
134
+ ),
135
+ 'title'
136
+ );
137
+
138
+ return implode( ', ', $titles );
139
+ }
140
+ }
141
+
142
+ /**
143
+ * @filter post_row_actions
144
+ *
145
+ * @param $actions
146
+ *
147
+ * @return array
148
+ */
149
+ public function row_actions( $actions ) {
150
+ global $typenow;
151
+
152
+ if ( WP_Stream_Notifications_Post_Type::POSTTYPE !== $typenow ) {
153
+ return $actions;
154
+ }
155
+
156
+ unset( $actions['view'] );
157
+ unset( $actions['inline hide-if-no-js'] );
158
+
159
+ global $post;
160
+
161
+ $published = ( 'publish' === $post->post_status );
162
+
163
+ $new = array();
164
+ $url = wp_nonce_url(
165
+ add_query_arg(
166
+ array(
167
+ 'post_type' => WP_Stream_Notifications_Post_Type::POSTTYPE,
168
+ 'action' => $published ? 'unpublish' : 'publish',
169
+ 'id' => $post->ID,
170
+ ),
171
+ admin_url( 'edit.php' )
172
+ )
173
+ );
174
+ $new['publish'] = sprintf(
175
+ '<a href="%s">%s</a>',
176
+ $url,
177
+ $published ? __( 'Deactivate', 'stream' ) : __( 'Activate', 'stream' )
178
+ );
179
+
180
+ return array_merge( $new, $actions );
181
+ }
182
+
183
+ /**
184
+ * @action load-edit.php
185
+ */
186
+ public function actions() {
187
+ if ( ! isset( $_REQUEST['action'] ) || ! isset( $_REQUEST['post_type'] ) || WP_Stream_Notifications_Post_Type::POSTTYPE !== wp_stream_filter_input( INPUT_GET, 'post_type' ) ) {
188
+ return;
189
+ }
190
+
191
+ $action = $_REQUEST['action'];
192
+ $request = isset( $_REQUEST['post'] ) ? ( is_array( $_REQUEST['post'] ) ? $_REQUEST['post'] : explode( ',', $_REQUEST['post'] ) ) : isset( $_REQUEST['id'] ) ? array( $_REQUEST['id'] ) : array();
193
+ $ids = array_map( 'absint', $request );
194
+
195
+ if ( empty( $action ) || empty( $ids ) ) {
196
+ return;
197
+ }
198
+
199
+ if ( in_array( $action, array( 'publish', 'unpublish' ) ) ) {
200
+ $status = ( 'publish' === $action ) ? 'publish' : 'draft';
201
+
202
+ foreach ( $ids as $id ) {
203
+ wp_update_post(
204
+ array(
205
+ 'ID' => $id,
206
+ 'post_status' => $status,
207
+ )
208
+ );
209
+ }
210
+
211
+ wp_safe_redirect(
212
+ add_query_arg(
213
+ array(
214
+ 'updated' => count( $ids ),
215
+ ),
216
+ remove_query_arg(
217
+ array( 'action', 'action2', 'id', 'ids', 'post', '_wp_http_referer', 'post_status', 'mode', 'm' )
218
+ )
219
+ )
220
+ );
221
+
222
+ exit; // Without this, the page displays the weird 'Are you sure you want this?'
223
+ }
224
+ }
225
+
226
+ /**
227
+ * @filter admin_head
228
+ */
229
+ public function scripts() {
230
+ if ( 'edit-' . WP_Stream_Notifications_Post_Type::POSTTYPE !== get_current_screen()->id ) {
231
+ return;
232
+ }
233
+
234
+ wp_enqueue_script( 'stream-notifications-list-actions', WP_STREAM_NOTIFICATIONS_URL . 'ui/js/list.js', array( 'jquery', 'underscore' ), WP_STREAM::VERSION );
235
+
236
+ wp_localize_script( 'stream-notifications-list-actions', 'stream_notifications_options',
237
+ array(
238
+ 'bulkActions' => array(
239
+ 'publish' => __( 'Publish', 'stream' ),
240
+ 'unpublish' => __( 'Unpublish', 'stream' ),
241
+ )
242
+ )
243
+ );
244
+ }
245
+
246
+ }
extensions/notifications/includes/class-wp-stream-notifications-matcher.php ADDED
@@ -0,0 +1,511 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class WP_Stream_Notifications_Matcher {
4
+
5
+ const CACHE_KEY = 'stream-notification-rules';
6
+
7
+ /**
8
+ * @todo fix deprecated actions/filters
9
+ */
10
+ public function __construct() {
11
+ // Refresh rules cache on updating/deleting posts
12
+ add_action( 'save_post', array( $this, 'refresh_cache_on_save' ), 10, 2 );
13
+ add_action( 'delete_post', array( $this, 'refresh_cache_on_delete' ), 10, 1 );
14
+
15
+ // Match all new type=stream records
16
+ add_action( 'wp_stream_records_inserted', array( $this, 'match' ), 10, 2 );
17
+ }
18
+
19
+ /**
20
+ * Refresh cache on saving a rule
21
+ *
22
+ * @action save_post
23
+ *
24
+ * @param $post_id
25
+ * @param null $post
26
+ *
27
+ * @return void
28
+ */
29
+ public function refresh_cache_on_save( $post_id, $post = null ) {
30
+ if ( ! isset( $post ) ) {
31
+ $post = get_post( $post_id );
32
+ }
33
+
34
+ if ( WP_Stream_Notifications_Post_Type::POSTTYPE === $post->post_type ) {
35
+ $this->rules( true );
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Refresh cache on deleting a rule
41
+ *
42
+ * @action delete_post
43
+ *
44
+ * @param $post_id
45
+ *
46
+ * @return void
47
+ */
48
+ public function refresh_cache_on_delete( $post_id ) {
49
+ $post = get_post( $post_id );
50
+
51
+ if ( WP_Stream_Notifications_Post_Type::POSTTYPE !== $post->post_type ) {
52
+ return;
53
+ }
54
+
55
+ add_action(
56
+ 'deleted_post', function ( $deleted_id ) use ( $post_id ) {
57
+ if ( $deleted_id === $post_id ) {
58
+ $this->rules( true );
59
+ }
60
+ }
61
+ );
62
+ }
63
+
64
+ /**
65
+ * Generate a proper format of triggers/alerts to be used/cached
66
+ *
67
+ * @param bool $force_refresh Ignore cached version
68
+ *
69
+ * @return array|mixed|void
70
+ */
71
+ public function rules( $force_refresh = false ) {
72
+ if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
73
+ $force_refresh = true;
74
+ }
75
+
76
+ // Check if we have a valid cache
77
+ if ( ! $force_refresh && false !== ( $rules = get_transient( self::CACHE_KEY ) ) ) {
78
+ return $rules;
79
+ }
80
+
81
+ // Get rules
82
+ $args = array(
83
+ 'post_type' => WP_Stream_Notifications_Post_Type::POSTTYPE,
84
+ 'post_status' => 'publish',
85
+ 'posts_per_page' => -1,
86
+ );
87
+ $query = new WP_Query( $args );
88
+ $rules = $query->get_posts();
89
+
90
+ /**
91
+ * Allow developers to add/modify rules
92
+ *
93
+ * @param array $rules Rules for the current blog
94
+ * @param array $args Query args used
95
+ *
96
+ * @return array
97
+ */
98
+ $rules = apply_filters( 'wp_stream_notifications_rules', $rules, $args );
99
+
100
+ $rules = $this->format( $rules );
101
+
102
+ // Cache the new rules
103
+ set_transient( self::CACHE_KEY, $rules );
104
+
105
+ return $rules;
106
+ }
107
+
108
+ public function match( $records ) {
109
+ $rules = $this->rules();
110
+ $rule_match = array();
111
+
112
+ foreach ( $records as $record ) {
113
+ foreach ( $rules as $rule_id => $rule ) {
114
+ $rule_match[ $rule_id ] = $this->match_group( $rule['triggers'], $record );
115
+ }
116
+ }
117
+
118
+ $rule_match = array_keys( array_filter( $rule_match ) );
119
+ $matching_rules = array_intersect_key( $rules, array_flip( $rule_match ) );
120
+
121
+ $this->alert( $matching_rules, $records );
122
+ }
123
+
124
+ /**
125
+ * Match a group of chunked triggers against a log operation
126
+ *
127
+ * @param array $chunks Chunks of triggers, usually from group[triggers]
128
+ * @param array $log Log operation array
129
+ *
130
+ * @return bool Matching result
131
+ */
132
+ private function match_group( $chunks, $record ) {
133
+ // Separate triggers by 'AND'/'OR' relation, to be able to fail early
134
+ // and not have to traverse the whole trigger tree
135
+ foreach ( $chunks as $chunk ) {
136
+ $results = array();
137
+
138
+ foreach ( $chunk as $trigger ) {
139
+ $is_group = isset( $trigger['triggers'] );
140
+
141
+ if ( $is_group ) {
142
+ $results[] = $this->match_group( $trigger['triggers'], $record );
143
+ } else {
144
+ $results[] = $this->match_trigger( $trigger, $record );
145
+ }
146
+ }
147
+
148
+ // If the whole chunk fails, fail the whole group
149
+ if ( 0 === count( array_filter( $results ) ) ) {
150
+ return false;
151
+ }
152
+ }
153
+
154
+ // If nothing fails, group matches
155
+ return true;
156
+ }
157
+
158
+ public function match_trigger( $trigger, $record ) {
159
+ $type = isset( $trigger['type'] ) ? $trigger['type'] : null;
160
+ $needle = isset( $trigger['value'] ) ? $trigger['value'] : null;
161
+ $operator = isset( $trigger['operator'] ) ? $trigger['operator'] : null;
162
+ $negative = ( isset( $operator[0] ) && '!' === $operator[0] );
163
+ $haystack = null;
164
+
165
+ // Post-specific triggers dirty work
166
+ if ( false !== strpos( $trigger['type'], 'post_' ) ) {
167
+ $post = get_post( $record['object_id'] );
168
+
169
+ if ( empty( $post ) ) {
170
+ return false;
171
+ }
172
+ }
173
+
174
+ switch ( $type ) {
175
+ case 'search':
176
+ $haystack = strtolower( $record['summary'] );
177
+ $needle = strtolower( $needle );
178
+ break;
179
+ case 'object_id':
180
+ $haystack = $record['object_id'];
181
+ break;
182
+ case 'author':
183
+ $haystack = $record['author'];
184
+ break;
185
+ case 'author_role':
186
+ $user = get_userdata( $record['author'] );
187
+ $haystack = ( is_object( $user ) && $user->exists() && $user->roles ) ? $user->roles[0] : false;
188
+ break;
189
+ case 'ip':
190
+ $haystack = $record['ip'];
191
+ break;
192
+ case 'date':
193
+ $haystack = get_date_from_gmt( date( 'Y-m-d H:i:s', strtotime( $record['created'] ) ), 'Ymd' );
194
+ $needle = get_date_from_gmt( date( 'Y-m-d H:i:s', strtotime( $needle ) ), 'Ymd' );
195
+ break;
196
+ case 'weekday':
197
+ if ( isset( $needle[0] ) && preg_match( '#\d+#', $needle[0], $weekday_match ) ) {
198
+ $haystack = get_date_from_gmt( date( 'Y-m-d H:i:s', strtotime( $record['created'] ) ), 'w' );
199
+ $needle = $weekday_match[0];
200
+ }
201
+ break;
202
+ case 'connector':
203
+ $haystack = $record['connector'];
204
+ break;
205
+ case 'context':
206
+ $haystack = $record['context'];
207
+ break;
208
+ case 'action':
209
+ $haystack = $record['action'];
210
+ break;
211
+
212
+ /* Context-aware triggers */
213
+ case 'post':
214
+ case 'user':
215
+ case 'term':
216
+ $haystack = $record['object_id'];
217
+ break;
218
+ case 'term_parent':
219
+ $parent = get_term( $record['meta']['term_parent'], $record['meta']['taxonomy'] );
220
+ if ( empty( $parent ) || is_wp_error( $parent ) ) {
221
+ return false;
222
+ } else {
223
+ $haystack = $parent->term_taxonomy_id;
224
+ }
225
+ break;
226
+ case 'tax':
227
+ if ( empty( $record['meta']['taxonomy'] ) ) {
228
+ return false;
229
+ }
230
+ $haystack = $record['meta']['taxonomy'];
231
+ break;
232
+
233
+ case 'post_title':
234
+ $haystack = $post->post_title;
235
+ break;
236
+ case 'post_slug':
237
+ $haystack = $post->post_name;
238
+ break;
239
+ case 'post_content':
240
+ $haystack = $post->post_content;
241
+ break;
242
+ case 'post_excerpt':
243
+ $haystack = $post->post_excerpt;
244
+ break;
245
+ case 'post_status':
246
+ $haystack = get_post_status( $post->ID );
247
+ break;
248
+ case 'post_format':
249
+ $haystack = get_post_format( $post );
250
+ break;
251
+ case 'post_parent':
252
+ $haystack = wp_get_post_parent_id( $post->ID );
253
+ break;
254
+ case 'post_thumbnail':
255
+ if ( ! function_exists( 'get_post_thumbnail_id' ) ) {
256
+ return false;
257
+ }
258
+ $haystack = get_post_thumbnail_id( $post->ID ) > 0;
259
+ break;
260
+ case 'post_comment_status':
261
+ $haystack = $post->comment_status;
262
+ break;
263
+ case 'post_comment_count':
264
+ $haystack = get_comment_count( $post->ID );
265
+ break;
266
+ default:
267
+ return false;
268
+ break;
269
+ }
270
+
271
+ $match = false;
272
+
273
+ switch ( $operator ) {
274
+ case '=':
275
+ case '!=':
276
+ case '>=':
277
+ case '<=':
278
+ $needle = is_array( $needle ) ? $needle : explode( ',', $needle );
279
+ $match = (bool) array_intersect( $needle, (array) $haystack );
280
+ break;
281
+ // string special comparison operators
282
+ case 'contains':
283
+ case '!contains':
284
+ $match = ( false !== strpos( $haystack, $needle ) );
285
+ break;
286
+ case 'starts':
287
+ $match = ( 0 === strpos( $haystack, $needle ) );
288
+ break;
289
+ case 'ends':
290
+ $match = ( strlen( $haystack ) - strlen( $needle ) === strrpos( $haystack, $needle ) );
291
+ break;
292
+ case 'regex':
293
+ $match = preg_match( $needle, $haystack ) > 0;
294
+ break;
295
+ // date operators
296
+ case '<':
297
+ case '<=':
298
+ $match = $match || ( $haystack < $needle );
299
+ break;
300
+ case '>':
301
+ case '>=':
302
+ $match = $match || ( $haystack > $needle );
303
+ break;
304
+ }
305
+
306
+ $result = ( $match == ! $negative ); // Loose comparison needed
307
+
308
+ return $result;
309
+ }
310
+
311
+ /**
312
+ * Format rules to be usable during the matching process
313
+ *
314
+ * @param array $rules Array of rule IDs
315
+ *
316
+ * @return array Reformatted array of groups/triggers
317
+ */
318
+ private function format( $rules ) {
319
+ $output = array();
320
+
321
+ foreach ( $rules as $rule ) {
322
+ $rule_id = $rule->ID;
323
+ $meta = get_post_meta( $rule_id );
324
+ $args = array();
325
+
326
+ foreach ( array( 'triggers', 'groups', 'alerts' ) as $key ) {
327
+ if ( isset( $meta[ $key ] ) ) {
328
+ $args[ $key ] = array_filter( maybe_unserialize( $meta[ $key ][0] ) );
329
+ }
330
+ }
331
+
332
+ // Bail early if no triggers or alerts are defined
333
+ if ( empty( $args['triggers'] ) || empty( $args['alerts'] ) ) {
334
+ continue;
335
+ }
336
+
337
+ $output[ $rule_id ] = array();
338
+
339
+ // Generate an easy-to-parse tree of triggers/groups
340
+ $args['triggers'] = $this->generate_tree(
341
+ $this->generate_flattened_tree(
342
+ $args['triggers'],
343
+ $args['groups']
344
+ )
345
+ );
346
+
347
+ // Chunkify! @see generate_group_chunks
348
+ $args['triggers'] = $this->generate_group_chunks(
349
+ $args['triggers'][0]['triggers']
350
+ );
351
+
352
+ // Add alerts
353
+ $output[ $rule_id ] = $args;
354
+ }
355
+
356
+ return $output;
357
+ }
358
+
359
+ /**
360
+ * Return all of group's ancestors starting with the root
361
+ */
362
+ private function generate_group_chain( $groups, $group_id ) {
363
+ $chain = array();
364
+
365
+ while ( isset( $groups[ $group_id ] ) ) {
366
+ $chain[] = $group_id;
367
+ $group_id = $groups[ $group_id ]['group'];
368
+ }
369
+
370
+ return array_reverse( $chain );
371
+ }
372
+
373
+ /**
374
+ * Takes the groups and triggers and creates a flattened tree,
375
+ * which is an pre-order walkthrough of the tree we want to construct
376
+ * http://en.wikipedia.org/wiki/Tree_traversal#Pre-order
377
+ */
378
+ private function generate_flattened_tree( $triggers, $groups ) {
379
+ // Seed the tree with the universal group
380
+ if ( ! isset( $groups[0] ) ) {
381
+ $groups[0] = array( 'group' => null, 'relation' => 'and' );
382
+ }
383
+
384
+ $flattened_tree = array( array( 'item' => $groups['0'], 'level' => 0, 'type' => 'group' ) );
385
+ $current_group_chain = array( '0' );
386
+ $level = 1;
387
+
388
+ foreach ( $triggers as $key => $trigger ) {
389
+ $active_group = end( $current_group_chain );
390
+
391
+ // If the trigger goes to any other than actually opened group, we need to traverse the tree first
392
+ if ( $trigger['group'] != $active_group ) {
393
+ $trigger_group_chain = $this->generate_group_chain( $groups, $trigger['group'] );
394
+ $common_ancestors = array_intersect( $current_group_chain, $trigger_group_chain );
395
+ $newly_inserted_groups = array_diff( $trigger_group_chain, $current_group_chain );
396
+ $steps_back = $level - count( $common_ancestors );
397
+
398
+ // First take the steps back until we reach a common ancestor
399
+ for ( $i = 0; $i < $steps_back; $i ++ ) {
400
+ array_pop( $current_group_chain );
401
+ $level --;
402
+ }
403
+
404
+ // Then go forward and generate group nodes until the trigger is ready to be inserted
405
+ foreach ( $newly_inserted_groups as $group ) {
406
+ $flattened_tree[] = array( 'item' => $groups[ $group ], 'level' => $level ++, 'type' => 'group' );
407
+ $current_group_chain[] = $group;
408
+ }
409
+ }
410
+
411
+ // Now we're sure the trigger goes to a correct position
412
+ $flattened_tree[] = array( 'item' => $trigger, 'level' => $level, 'type' => 'trigger' );
413
+ }
414
+
415
+ return $flattened_tree;
416
+ }
417
+
418
+ /**
419
+ * Takes the flattened tree and generates a proper tree
420
+ */
421
+ private function generate_tree( $flattened_tree ) {
422
+ // Our recurrent step
423
+ $recurrent_step = function ( $level, $i ) use ( $flattened_tree, &$recurrent_step ) {
424
+ $return = array();
425
+
426
+ for ( $i; $i < count( $flattened_tree ); $i ++ ) {
427
+ // If we're on the correct level, we're going to insert the node
428
+ if ( $flattened_tree[ $i ]['level'] === $level ) {
429
+ if ( 'trigger' === $flattened_tree[ $i ]['type'] ) {
430
+ $return[] = $flattened_tree[ $i ]['item'];
431
+ // If the node is a group, we need to call the recursive function
432
+ // in order to construct the tree for us further
433
+ } else {
434
+ $return[] = array(
435
+ 'relation' => $flattened_tree[ $i ]['item']['relation'],
436
+ 'triggers' => call_user_func( $recurrent_step, $level + 1, $i + 1 ),
437
+ );
438
+ }
439
+ // If we're on a lower level, we came back and we can return this branch
440
+ } elseif ( $flattened_tree[ $i ]['level'] < $level ) {
441
+ return $return;
442
+ }
443
+ }
444
+
445
+ return $return;
446
+ };
447
+
448
+ return call_user_func( $recurrent_step, 0, 0 );
449
+ }
450
+
451
+ /**
452
+ * Split trigger trees by relation, so we can fail trigger trees early if
453
+ * an effective trigger is not matched
454
+ *
455
+ * A chunk would be a bulk of triggers that only matches if ANY of its
456
+ * nested triggers are matched
457
+ *
458
+ * @param $triggers
459
+ *
460
+ * @internal array $group Group array, ex: array(
461
+ * 'relation' => 'and',
462
+ * 'trigger' => array( arr trigger1, arr trigger2 )
463
+ * );
464
+ *
465
+ * @return array Chunks of triggers, split based on their relation
466
+ */
467
+ private function generate_group_chunks( $triggers ) {
468
+ $chunks = array();
469
+ $current_chunk = -1;
470
+
471
+ foreach ( $triggers as $trigger ) {
472
+ // If is a group, chunks its children as well
473
+ if ( isset( $trigger['triggers'] ) ) {
474
+ $trigger['triggers'] = $this->generate_group_chunks( $trigger['triggers'] );
475
+ }
476
+
477
+ // If relation=and, start a new chunk, else join the previous chunk
478
+ if ( 'and' === $trigger['relation'] ) {
479
+ $chunks[] = array( $trigger );
480
+ $current_chunk = count( $chunks ) - 1;
481
+ } else {
482
+ $chunks[ $current_chunk ][] = $trigger;
483
+ }
484
+ }
485
+
486
+ return $chunks;
487
+ }
488
+
489
+ private function alert( $rules, $records ) {
490
+ foreach ( $rules as $rule_id => $rule ) {
491
+ // Update occurrences
492
+ update_post_meta(
493
+ $rule_id,
494
+ 'occurrences',
495
+ ( (int) get_post_meta( $rule_id, 'occurrences', true ) ) + 1
496
+ );
497
+
498
+ foreach ( $records as $record ) {
499
+ foreach ( $rule['alerts'] as $alert ) {
500
+ if ( ! isset( WP_Stream_Notifications::$adapters[ $alert['type'] ] ) ) {
501
+ continue;
502
+ }
503
+
504
+ $adapter = new WP_Stream_Notifications::$adapters[ $alert['type'] ]['class'];
505
+ $adapter->load( $alert )->send( $record );
506
+ }
507
+ }
508
+ }
509
+ }
510
+
511
+ }
extensions/notifications/includes/class-wp-stream-notifications-post-type.php ADDED
@@ -0,0 +1,850 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class WP_Stream_Notifications_Post_Type {
4
+
5
+ private static $instance;
6
+
7
+ const POSTTYPE = 'stream_notification'; // Must be less than 20 chars
8
+
9
+ public static function get_instance() {
10
+ if ( ! self::$instance ) {
11
+ self::$instance = new self();
12
+ }
13
+
14
+ return self::$instance;
15
+ }
16
+
17
+ private function __construct() {
18
+ $this->register_post_type();
19
+
20
+ // AJAX end point for form auto completion
21
+ add_action( 'wp_ajax_stream_notification_endpoint', array( $this, 'form_ajax_ep' ) );
22
+ // Occurance reset
23
+ add_action( 'wp_ajax_stream-notifications-reset-occ', array( $this, 'ajax_reset_occ' ) );
24
+
25
+ // Enqueue our form scripts
26
+ add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_scripts' ), 11 );
27
+
28
+ // define `search_in` arg for WP_User_Query
29
+ add_filter( 'user_search_columns', array( $this, 'define_search_in_arg' ), 10, 3 );
30
+
31
+ // Change title placeholder
32
+ add_filter( 'enter_title_here', array( $this, 'title_placeholder' ), 10, 2 );
33
+
34
+ // Save meta data
35
+ add_action( 'save_post', array( $this, 'save' ), 10, 2 );
36
+
37
+ // Load list-table customizations
38
+ add_action( 'admin_init', array( $this, 'load_list_table' ) );
39
+ }
40
+
41
+ private function register_post_type() {
42
+ register_post_type(
43
+ self::POSTTYPE, array(
44
+ 'label' => __( 'Stream Notification Rule', 'stream' ),
45
+ 'labels' => array(
46
+ 'name' => __( 'Stream Notification Rules', 'stream' ),
47
+ 'singular_name' => __( 'Stream Notification Rule', 'stream' ),
48
+ 'menu_name' => __( 'Notifications', 'stream' ),
49
+ 'add_new' => _x( 'New Rule', 'Stream Notifications', 'stream' ),
50
+ 'add_new_item' => _x( 'Add New Rule', 'Stream Notifications', 'stream' ),
51
+ 'new_item' => _x( 'New Stream Notification Rule', 'Stream Notifications', 'stream' ),
52
+ 'edit_item' => _x( 'Edit Stream Notification Rule', 'Stream Notifications', 'stream' ),
53
+ 'view_item' => _x( 'View Stream Notification Rule', 'Stream Notifications', 'stream' ),
54
+ 'search_items' => _x( 'Search Rules', 'Stream Notifications', 'stream' ),
55
+ 'not_found' => _x( 'No notification rules found.', 'Stream Notifications', 'stream' ),
56
+ 'not_found_in_trash' => _x( 'No notification rules found in Trash.', 'Stream Notifications', 'stream' ),
57
+ ),
58
+ 'public' => false,
59
+ 'show_ui' => true,
60
+ 'show_in_nav_menus' => false,
61
+ 'show_in_menu' => false,
62
+ 'exclude_from_search' => true,
63
+ 'publicly_queryable' => false,
64
+ 'supports' => WP_Stream_API::is_restricted() ? false : array( 'title', 'author' ),
65
+ 'register_meta_box_cb' => WP_Stream_API::is_restricted() ? null : array( $this, 'metaboxes' ),
66
+ 'rewrite' => false,
67
+ )
68
+ );
69
+ }
70
+
71
+ public function metaboxes( $post ) {
72
+ if ( self::POSTTYPE !== $post->post_type ) {
73
+ return;
74
+ }
75
+
76
+ add_meta_box( 'stream-notifications-triggers', __( 'Triggers', 'stream' ), array( $this, 'metabox_triggers' ), self::POSTTYPE );
77
+ add_meta_box( 'stream-notifications-alerts', __( 'Alerts', 'stream' ), array( $this, 'metabox_alerts' ), self::POSTTYPE );
78
+ add_meta_box( 'stream-notifications-data-tags', __( 'Data Tags', 'stream' ), array( $this, 'metabox_data_tags' ), self::POSTTYPE, 'side' );
79
+
80
+ add_action( 'post_submitbox_misc_actions', array( $this, 'metabox_save' ) );
81
+
82
+ add_action(
83
+ 'edit_form_advanced', function () {
84
+ global $post;
85
+ include WP_STREAM_NOTIFICATIONS_DIR . 'views/form-templates.php';
86
+ }
87
+ );
88
+ }
89
+
90
+ /**
91
+ * Enqueue our scripts, in our own page only
92
+ *
93
+ * @action admin_enqueue_scripts
94
+ *
95
+ * @param string $hook Current admin page slug
96
+ *
97
+ * @return void
98
+ */
99
+ public function enqueue_scripts( $hook ) {
100
+ global $typenow;
101
+
102
+ if ( ! in_array( $hook, array( 'post-new.php', 'post.php' ) ) || self::POSTTYPE !== $typenow ) {
103
+ return;
104
+ }
105
+
106
+ wp_enqueue_style( 'select2' );
107
+ wp_enqueue_script( 'select2' );
108
+ wp_enqueue_script( 'underscore' );
109
+ wp_enqueue_style( 'jquery-ui' );
110
+ wp_enqueue_script( 'jquery-ui-datepicker' );
111
+ wp_enqueue_style( 'wp-stream-datepicker' );
112
+ wp_enqueue_script( 'jquery-ui-accordion' );
113
+ wp_enqueue_script( 'accordion' );
114
+ wp_enqueue_style( 'stream-notifications-form', WP_STREAM_NOTIFICATIONS_URL . '/ui/css/form.css' );
115
+ wp_enqueue_script( 'stream-notifications-form', WP_STREAM_NOTIFICATIONS_URL . '/ui/js/form.js', array( 'underscore', 'select2' ) );
116
+ wp_localize_script( 'stream-notifications-form', 'stream_notifications', $this->get_js_options() );
117
+ }
118
+
119
+ public function metabox_triggers() {
120
+ ?>
121
+ <a class="add-trigger button button-secondary" href="#add-trigger" data-group="0"><?php esc_html_e( '+ Add Trigger', 'stream' ) ?></a>
122
+ <a class="add-trigger-group button button-primary" href="#add-trigger-group" data-group="0"><?php esc_html_e( '+ Add Group', 'stream' ) ?></a>
123
+ <div class="group" rel="0"></div>
124
+ <?php
125
+ }
126
+
127
+ public function metabox_alerts() {
128
+ ?>
129
+ <a class="add-alert button button-secondary" href="#add-alert"><?php esc_html_e( '+ Add Alert', 'stream' ) ?></a>
130
+ <?php
131
+ }
132
+
133
+ public function metabox_save() {
134
+ global $post;
135
+ if ( 'auto-draft' === $post->post_status ) {
136
+ return;
137
+ }
138
+
139
+ $reset_link = add_query_arg(
140
+ array(
141
+ 'action' => 'stream-notifications-reset-occ',
142
+ 'id' => absint( $post->ID ),
143
+ 'wp_stream_nonce' => wp_create_nonce( 'reset-occ_' . absint( $post->ID ) ),
144
+ ),
145
+ admin_url( 'admin-ajax.php' )
146
+ );
147
+
148
+ $occ = absint( get_post_meta( $post->ID, 'occurrences', true ) );
149
+
150
+ ?>
151
+ <div class="occurrences misc-pub-section">
152
+ <p>
153
+ <?php
154
+ echo sprintf(
155
+ _n(
156
+ 'This rule has occurred %1$s time.',
157
+ 'This rule has occurred %1$s times.',
158
+ $occ,
159
+ 'stream'
160
+ ),
161
+ sprintf( '<strong>%d</strong>', $occ ? $occ : 0 )
162
+ ) // xss okay
163
+ ?>
164
+ </p>
165
+ <?php if ( 0 !== $occ ) : ?>
166
+ <p>
167
+ <a href="<?php echo esc_url( $reset_link ) ?>" class="button button-secondary reset-occ">
168
+ <?php esc_html_e( 'Reset Count', 'stream' ) ?>
169
+ </a>
170
+ </p>
171
+ <?php endif; ?>
172
+ </div>
173
+ <?php
174
+ }
175
+
176
+ public function metabox_data_tags() {
177
+ $data_tags = array(
178
+ __( 'Basic', 'stream' ) => array(
179
+ 'summary' => __( 'Summary message of the triggered record.', 'stream' ),
180
+ 'author' => __( 'User ID of the triggered record author.', 'stream' ),
181
+ 'connector' => __( 'Connector of the triggered record.', 'stream' ),
182
+ 'context' => __( 'Context of the triggered record.', 'stream' ),
183
+ 'action' => __( 'Action of the triggered record.', 'stream' ),
184
+ 'created' => __( 'Timestamp of triggered record.', 'stream' ),
185
+ 'ip' => __( 'IP of the triggered record author.', 'stream' ),
186
+ 'object_id' => __( 'Object ID of the triggered record.', 'stream' ),
187
+ ),
188
+ __( 'Advanced', 'stream' ) => array(
189
+ 'object.' => __(
190
+ 'Specific object data of the record, relative to what the object type is:
191
+ <br /><br />
192
+ <strong>{object.post_title}</strong>
193
+ <br />
194
+ <strong>{object.post_excerpt}</strong>
195
+ <br />
196
+ <strong>{object.post_status}</strong>
197
+ <br />
198
+ <a href="http://codex.wordpress.org/Function_Reference/get_userdata#Notes" target="_blank">See Codex for more Post values</a>
199
+ <br /><br />
200
+ <strong>{object.name}</strong>
201
+ <br />
202
+ <strong>{object.taxonomy}</strong>
203
+ <br />
204
+ <strong>{object.description}</strong>
205
+ <br />
206
+ <a href="http://codex.wordpress.org/Function_Reference/get_userdata#Notes" target="_blank">See Codex for more Term values</a>', 'stream'
207
+ ),
208
+
209
+ 'author.' => __(
210
+ 'Specific user data of the record author:
211
+ <br /><br />
212
+ <strong>{author.display_name}</strong>
213
+ <br />
214
+ <strong>{author.user_email}</strong>
215
+ <br />
216
+ <strong>{author.user_login}</strong>
217
+ <br />
218
+ <a href="http://codex.wordpress.org/Function_Reference/get_userdata#Notes" target="_blank">See Codex for more User values</a>', 'stream'
219
+ ),
220
+ 'meta.' => __(
221
+ 'Specific meta data of the record, used to display specific meta values created by Connectors.
222
+ <br /><br />
223
+ Example: <strong>{meta.old_theme}</strong> to display the old theme name when a new theme is activated.', 'stream'
224
+ ),
225
+ ),
226
+ );
227
+ $allowed_html = array(
228
+ 'a' => array(
229
+ 'href' => array(),
230
+ 'target' => array(),
231
+ ),
232
+ 'code' => array(),
233
+ 'strong' => array(),
234
+ 'br' => array(),
235
+ );
236
+ ?>
237
+ <div id="data-tag-glossary" class="accordion-container">
238
+ <ul class="outer-border">
239
+ <?php foreach ( $data_tags as $section => $tags ) : ?>
240
+ <li class="control-section accordion-section">
241
+ <h3 class="accordion-section-title hndle" title="<?php echo esc_attr( $section ) ?>"><?php echo esc_html( $section ) ?></h3>
242
+ <div class="accordion-section-content">
243
+ <div class="inside">
244
+ <dl>
245
+ <?php foreach ( $tags as $tag => $desc ) : ?>
246
+ <dt><code>{<?php echo esc_html( $tag ) ?>}</code></dt>
247
+ <dd><?php echo wp_kses( $desc, $allowed_html ) ?></dd>
248
+ <?php endforeach; ?>
249
+ </dl>
250
+ </div>
251
+ </div>
252
+ </li>
253
+ <?php endforeach; ?>
254
+ </ul>
255
+ </div>
256
+ <?php
257
+ }
258
+
259
+ /**
260
+ * Save rule meta data
261
+ *
262
+ * @action save_post
263
+ *
264
+ * @param int $post_id
265
+ * @param object $post
266
+ *
267
+ * @return void
268
+ */
269
+ public function save( $post_id, $post ) {
270
+ if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) {
271
+ return;
272
+ }
273
+
274
+ if ( self::POSTTYPE !== $post->post_type ) {
275
+ return;
276
+ }
277
+
278
+ $defaults = array(
279
+ 'triggers' => array(),
280
+ 'groups' => array(),
281
+ 'alerts' => array(),
282
+ );
283
+
284
+ $args = wp_parse_args( $_POST, $defaults );
285
+ $meta = array_intersect_key( $args, array_flip( array( 'triggers', 'groups', 'alerts' ) ) );
286
+
287
+ foreach ( $meta as $key => $vals ) {
288
+ update_post_meta( $post_id, $key, $vals );
289
+ }
290
+ }
291
+
292
+ /**
293
+ * Callback for form AJAX operations
294
+ *
295
+ * @action wp_ajax_stream_notifications_endpoint
296
+ * @return void
297
+ */
298
+ public function form_ajax_ep() {
299
+ $type = wp_stream_filter_input( INPUT_POST, 'type' );
300
+ $is_single = wp_stream_filter_input( INPUT_POST, 'single' );
301
+ $query = wp_stream_filter_input( INPUT_POST, 'q' );
302
+ $args = json_decode( wp_stream_filter_input( INPUT_POST, 'args' ), true );
303
+
304
+ if ( ! is_array( $args ) ) {
305
+ $args = array();
306
+ }
307
+
308
+ if ( $is_single ) {
309
+ switch ( $type ) {
310
+ case 'author':
311
+ case 'post_author':
312
+ case 'user':
313
+ $user_ids = explode( ',', $query );
314
+ $user_query = new WP_User_Query(
315
+ array(
316
+ 'include' => $user_ids,
317
+ 'fields' => array( 'ID', 'user_email', 'display_name' ),
318
+ )
319
+ );
320
+ if ( $user_query->results ) {
321
+ $data = $this->format_json_for_select2( $user_query->results, 'ID', 'display_name' );
322
+ } else {
323
+ $data = array();
324
+ }
325
+ break;
326
+ case 'post':
327
+ case 'post_parent':
328
+ $args = array(
329
+ 'post_type' => 'any',
330
+ 'post_status' => 'any',
331
+ 'posts_per_page' => - 1,
332
+ 'post__in' => explode( ',', $query ),
333
+ );
334
+ $posts = get_posts( $args );
335
+ $items = array_combine( wp_list_pluck( $posts, 'ID' ), wp_list_pluck( $posts, 'post_title' ) );
336
+ $data = $this->format_json_for_select2( $items );
337
+ break;
338
+ case 'tax':
339
+ $items = get_taxonomies( null, 'objects' );
340
+ $items = wp_list_pluck( $items, 'labels' );
341
+ $items = wp_list_pluck( $items, 'name' );
342
+ $query = explode( ',', $query );
343
+ $chosen = array_intersect_key( $items, array_flip( $query ) );
344
+ $data = $this->format_json_for_select2( $chosen );
345
+ break;
346
+ case 'term':
347
+ case 'term_parent':
348
+ $tax = isset( $args['tax'] ) ? $args['tax'] : null;
349
+ $query = explode( ',', $query );
350
+ $terms = $this->get_terms( $query, $tax );
351
+ $data = $this->format_json_for_select2( $terms );
352
+ break;
353
+ }
354
+ } else {
355
+ switch ( $type ) {
356
+ case 'author':
357
+ case 'post_author':
358
+ case 'user':
359
+ $users = get_users(
360
+ array(
361
+ 'search' => '*' . $query . '*',
362
+ 'search_in' => array(
363
+ 'user_login',
364
+ 'display_name',
365
+ 'user_email',
366
+ 'user_nicename',
367
+ ),
368
+ 'meta_key' => ( isset( $args['push'] ) && $args['push'] ) ? 'ckpn_user_key' : null,
369
+ )
370
+ );
371
+ $data = $this->format_json_for_select2( $users, 'ID', 'display_name' );
372
+ break;
373
+ case 'action':
374
+ case 'context':
375
+ $items = WP_Stream_Connectors::$term_labels[ 'stream_' . $type ];
376
+ $items = preg_grep( sprintf( '/%s/i', $query ), $items );
377
+ $data = $this->format_json_for_select2( $items );
378
+ break;
379
+ case 'post':
380
+ case 'post_parent':
381
+ $posts = get_posts( 'post_type=any&post_status=any&posts_per_page=-1&s=' . $query );
382
+ $items = array_combine( wp_list_pluck( $posts, 'ID' ), wp_list_pluck( $posts, 'post_title' ) );
383
+ $data = $this->format_json_for_select2( $items );
384
+ break;
385
+ case 'tax':
386
+ $items = get_taxonomies( null, 'objects' );
387
+ $items = wp_list_pluck( $items, 'labels' );
388
+ $items = wp_list_pluck( $items, 'name' );
389
+ $items = preg_grep( sprintf( '/%s/i', $query ), $items );
390
+ $data = $this->format_json_for_select2( $items );
391
+ break;
392
+ case 'term':
393
+ case 'term_parent':
394
+ $tax = isset( $args['tax'] ) ? $args['tax'] : null;
395
+ $terms = $this->get_terms( $query, $tax );
396
+ $data = $this->format_json_for_select2( $terms );
397
+ break;
398
+ }
399
+ }
400
+
401
+ // Add gravatar for authors
402
+ if ( 'author' === $type && get_option( 'show_avatars' ) ) {
403
+ foreach ( $data as $i => $item ) {
404
+ if ( $avatar = get_avatar( $item['id'], 20 ) ) {
405
+ $item['avatar'] = $avatar;
406
+ }
407
+ $data[ $i ] = $item;
408
+ }
409
+ }
410
+
411
+ if ( ! empty( $data ) ) {
412
+ wp_send_json_success( $data );
413
+ } else {
414
+ wp_send_json_error();
415
+ }
416
+ }
417
+
418
+ /**
419
+ * Callback for Reset Occurrences count AJAX request
420
+ */
421
+ public function ajax_reset_occ() {
422
+ $id = wp_stream_filter_input( INPUT_GET, 'id' );
423
+ $nonce = wp_stream_filter_input( INPUT_GET, 'wp_stream_nonce' );
424
+
425
+ if ( ! wp_verify_nonce( $nonce, 'reset-occ_' . $id ) ) {
426
+ wp_send_json_error( esc_html__( 'Invalid nonce', 'stream' ) );
427
+ }
428
+
429
+ // Loose comparison needed
430
+ if ( empty( $id ) || (int) $id != $id ) {
431
+ wp_send_json_error( esc_html__( 'Invalid record ID', 'stream' ) );
432
+ }
433
+
434
+ update_post_meta( $id, 'occurrences', 0 );
435
+ wp_send_json_success();
436
+ }
437
+
438
+ /**
439
+ * Format JS options for the form, to be used with wp_localize_script
440
+ *
441
+ * @return array Options for our form JS handling
442
+ */
443
+ public function get_js_options() {
444
+ global $wp_roles;
445
+ $args = array();
446
+
447
+ $connectors = WP_Stream_Connectors::$term_labels['stream_connector'];
448
+ asort( $connectors );
449
+
450
+ $roles = $wp_roles->roles;
451
+ $roles_arr = array_combine( array_keys( $roles ), wp_list_pluck( $roles, 'name' ) );
452
+
453
+ $default_operators = array(
454
+ '=' => esc_html__( 'is', 'stream' ),
455
+ '!=' => esc_html__( 'is not', 'stream' ),
456
+ );
457
+
458
+ $text_operator = array(
459
+ '=' => esc_html__( 'is', 'stream' ),
460
+ '!=' => esc_html__( 'is not', 'stream' ),
461
+ 'contains' => esc_html__( 'contains', 'stream' ),
462
+ '!contains' => esc_html__( 'does not contain', 'stream' ),
463
+ 'starts' => esc_html__( 'starts with', 'stream' ),
464
+ 'ends' => esc_html__( 'ends with', 'stream' ),
465
+ 'regex' => esc_html__( 'regex', 'stream' ),
466
+ );
467
+
468
+ $numeric_operators = array(
469
+ '=' => esc_html__( 'equals', 'stream' ),
470
+ '!=' => esc_html__( 'not equal', 'stream' ),
471
+ '<' => esc_html__( 'less than', 'stream' ),
472
+ '<=' => esc_html__( 'equal or less than', 'stream' ),
473
+ '>' => esc_html__( 'greater than', 'stream' ),
474
+ '>=' => esc_html__( 'equal or greater than', 'stream' ),
475
+ );
476
+
477
+ $args['types'] = array(
478
+ 'search' => array(
479
+ 'title' => esc_html__( 'Summary', 'stream' ),
480
+ 'type' => 'text',
481
+ 'operators' => $text_operator,
482
+ ),
483
+ 'object_id' => array(
484
+ 'title' => esc_html__( 'Object ID', 'stream' ),
485
+ 'type' => 'text',
486
+ 'tags' => true,
487
+ 'operators' => $default_operators,
488
+ ),
489
+
490
+ 'author_role' => array(
491
+ 'title' => esc_html__( 'Author Role', 'stream' ),
492
+ 'type' => 'select',
493
+ 'multiple' => true,
494
+ 'operators' => $default_operators,
495
+ 'options' => $roles_arr,
496
+ ),
497
+
498
+ 'author' => array(
499
+ 'title' => esc_html__( 'Author', 'stream' ),
500
+ 'type' => 'text',
501
+ 'ajax' => true,
502
+ 'operators' => $default_operators,
503
+ ),
504
+
505
+ 'ip' => array(
506
+ 'title' => esc_html__( 'IP', 'stream' ),
507
+ 'type' => 'text',
508
+ 'subtype' => 'ip',
509
+ 'tags' => true,
510
+ 'operators' => $default_operators,
511
+ ),
512
+
513
+ 'date' => array(
514
+ 'title' => esc_html__( 'Date', 'stream' ),
515
+ 'type' => 'date',
516
+ 'operators' => array(
517
+ '=' => esc_html__( 'is on', 'stream' ),
518
+ '!=' => esc_html__( 'is not on', 'stream' ),
519
+ '<' => esc_html__( 'is before', 'stream' ),
520
+ '<=' => esc_html__( 'is on or before', 'stream' ),
521
+ '>' => esc_html__( 'is after', 'stream' ),
522
+ '>=' => esc_html__( 'is on or after', 'stream' ),
523
+ ),
524
+ ),
525
+
526
+ 'weekday' => array(
527
+ 'title' => esc_html__( 'Day of Week', 'stream' ),
528
+ 'type' => 'select',
529
+ 'multiple' => true,
530
+ 'operators' => $default_operators,
531
+ 'options' => array_combine(
532
+ array_map(
533
+ function ( $weekday_index ) {
534
+ return sprintf( 'weekday_%d', $weekday_index % 7 );
535
+ },
536
+ range( get_option( 'start_of_week' ), get_option( 'start_of_week' ) + 6 )
537
+ ),
538
+ array_map(
539
+ function ( $weekday_index ) {
540
+ global $wp_locale;
541
+ return $wp_locale->get_weekday( $weekday_index % 7 );
542
+ },
543
+ range( get_option( 'start_of_week' ), get_option( 'start_of_week' ) + 6 )
544
+ )
545
+ ),
546
+ ),
547
+
548
+ // TODO: find a way to introduce meta to the rules, problem: not translatable since it is
549
+ // generated on run time with no prior definition
550
+ // 'meta_query' => array(),
551
+
552
+ 'connector' => array(
553
+ 'title' => esc_html__( 'Connector', 'stream' ),
554
+ 'type' => 'select',
555
+ 'multiple' => true,
556
+ 'operators' => $default_operators,
557
+ 'options' => $connectors,
558
+ ),
559
+ 'context' => array(
560
+ 'title' => esc_html__( 'Context', 'stream' ),
561
+ 'type' => 'select',
562
+ 'multiple' => true,
563
+ 'operators' => $default_operators,
564
+ 'options' => WP_Stream_Connectors::$term_labels['stream_context'],
565
+ ),
566
+ 'action' => array(
567
+ 'title' => esc_html__( 'Action', 'stream' ),
568
+ 'type' => 'select',
569
+ 'multiple' => true,
570
+ 'operators' => $default_operators,
571
+ 'options' => WP_Stream_Connectors::$term_labels['stream_action'],
572
+ ),
573
+ );
574
+
575
+ // Connector-based triggers
576
+ $args['special_types'] = array(
577
+ 'post' => array(
578
+ 'title' => esc_html__( '- Post', 'stream' ),
579
+ 'type' => 'text',
580
+ 'ajax' => true,
581
+ 'connector' => 'posts',
582
+ 'operators' => $default_operators,
583
+ ),
584
+ 'post_title' => array(
585
+ 'title' => esc_html__( '- Post: Title', 'stream' ),
586
+ 'type' => 'text',
587
+ 'connector' => 'posts',
588
+ 'operators' => $text_operator,
589
+ ),
590
+ 'post_slug' => array(
591
+ 'title' => esc_html__( '- Post: Slug', 'stream' ),
592
+ 'type' => 'text',
593
+ 'connector' => 'posts',
594
+ 'operators' => $text_operator,
595
+ ),
596
+ 'post_content' => array(
597
+ 'title' => esc_html__( '- Post: Content', 'stream' ),
598
+ 'type' => 'text',
599
+ 'connector' => 'posts',
600
+ 'operators' => $text_operator,
601
+ ),
602
+ 'post_excerpt' => array(
603
+ 'title' => esc_html__( '- Post: Excerpt', 'stream' ),
604
+ 'type' => 'text',
605
+ 'connector' => 'posts',
606
+ 'operators' => $text_operator,
607
+ ),
608
+ 'post_author' => array(
609
+ 'title' => esc_html__( '- Post: Author', 'stream' ),
610
+ 'type' => 'text',
611
+ 'ajax' => true,
612
+ 'connector' => 'posts',
613
+ 'operators' => $default_operators,
614
+ ),
615
+ 'post_status' => array(
616
+ 'title' => esc_html__( '- Post: Status', 'stream' ),
617
+ 'type' => 'select',
618
+ 'connector' => 'posts',
619
+ 'options' => wp_list_pluck( $GLOBALS['wp_post_statuses'], 'label' ),
620
+ 'operators' => $default_operators,
621
+ ),
622
+ 'post_format' => array(
623
+ 'title' => esc_html__( '- Post: Format', 'stream' ),
624
+ 'type' => 'select',
625
+ 'connector' => 'posts',
626
+ 'options' => get_post_format_strings(),
627
+ 'operators' => $default_operators,
628
+ ),
629
+ 'post_parent' => array(
630
+ 'title' => esc_html__( '- Post: Parent', 'stream' ),
631
+ 'type' => 'text',
632
+ 'ajax' => true,
633
+ 'connector' => 'posts',
634
+ 'operators' => $default_operators,
635
+ ),
636
+ 'post_thumbnail' => array(
637
+ 'title' => esc_html__( '- Post: Featured Image', 'stream' ),
638
+ 'type' => 'select',
639
+ 'connector' => 'posts',
640
+ 'options' => array(
641
+ '0' => esc_html__( 'None', 'stream' ),
642
+ '1' => esc_html__( 'Has one', 'stream' )
643
+ ),
644
+ 'operators' => $default_operators,
645
+ ),
646
+ 'post_comment_status' => array(
647
+ 'title' => esc_html__( '- Post: Comment Status', 'stream' ),
648
+ 'type' => 'select',
649
+ 'connector' => 'posts',
650
+ 'options' => array(
651
+ 'open' => esc_html__( 'Open', 'stream' ),
652
+ 'closed' => esc_html__( 'Closed', 'stream' )
653
+ ),
654
+ 'operators' => $default_operators,
655
+ ),
656
+ 'post_comment_count' => array(
657
+ 'title' => esc_html__( '- Post: Comment Count', 'stream' ),
658
+ 'type' => 'text',
659
+ 'connector' => 'posts',
660
+ 'operators' => $numeric_operators,
661
+ ),
662
+ 'user' => array(
663
+ 'title' => esc_html__( '- User', 'stream' ),
664
+ 'type' => 'text',
665
+ 'ajax' => true,
666
+ 'connector' => 'users',
667
+ 'operators' => $default_operators,
668
+ ),
669
+ 'user_role' => array(
670
+ 'title' => esc_html__( '- User: Role', 'stream' ),
671
+ 'type' => 'select',
672
+ 'connector' => 'users',
673
+ 'options' => $roles_arr,
674
+ 'operators' => $default_operators,
675
+ ),
676
+ 'tax' => array(
677
+ 'title' => esc_html__( '- Taxonomy', 'stream' ),
678
+ 'type' => 'text',
679
+ 'ajax' => true,
680
+ 'connector' => 'taxonomies',
681
+ 'operators' => $default_operators,
682
+ ),
683
+ 'term' => array(
684
+ 'title' => esc_html__( '- Term', 'stream' ),
685
+ 'type' => 'text',
686
+ 'ajax' => true,
687
+ 'connector' => 'taxonomies',
688
+ 'operators' => $default_operators,
689
+ ),
690
+ 'term_parent' => array(
691
+ 'title' => esc_html__( '- Term: Parent', 'stream' ),
692
+ 'type' => 'text',
693
+ 'ajax' => true,
694
+ 'connector' => 'taxonomies',
695
+ 'operators' => $default_operators,
696
+ ),
697
+ );
698
+
699
+ $args['adapters'] = array();
700
+
701
+ foreach ( WP_Stream_Notifications::$adapters as $name => $options ) {
702
+ $args['adapters'][ $name ] = array(
703
+ 'title' => $options['title'],
704
+ 'fields' => $options['class']::fields(),
705
+ 'hints' => $options['class']::hints(),
706
+ );
707
+ }
708
+
709
+ // Localization
710
+ $args['i18n'] = array(
711
+ 'empty_triggers' => esc_html__( 'You cannot save a rule without any triggers.', 'stream' ),
712
+ 'invalid_first_trigger' => esc_html__( 'You cannot save a rule with an empty first trigger.', 'stream' ),
713
+ 'ajax_error' => esc_html__( 'There was an error submitting your request, please try again.', 'stream' ),
714
+ 'confirm_reset' => esc_html__( 'Are you sure you want to reset occurrences for this rule? This cannot be undone.', 'stream' ),
715
+ );
716
+
717
+ global $post;
718
+
719
+ if ( ( $meta = get_post_meta( $post->ID ) ) && isset( $meta['triggers'] ) ) {
720
+
721
+ $args['meta'] = array(
722
+ 'triggers' => maybe_unserialize( $meta['triggers'][0] ),
723
+ 'groups' => maybe_unserialize( $meta['groups'][0] ),
724
+ 'alerts' => maybe_unserialize( $meta['alerts'][0] ),
725
+ );
726
+ }
727
+
728
+ return apply_filters( 'wp_stream_notifications_js_args', $args );
729
+ }
730
+
731
+ /**
732
+ * Take an (associative) array and format it for select2 AJAX result parser
733
+ *
734
+ * @param array $data (associative) Data array
735
+ * @param string $key Key of the ID column, null if associative array
736
+ * @param string $val Key of the Title column, null if associative array
737
+ *
738
+ * @return array Formatted array, [ { id: %, title: % }, .. ]
739
+ */
740
+ public function format_json_for_select2( $data, $key = null, $val = null ) {
741
+ $return = array();
742
+ if ( is_null( $key ) && is_null( $val ) ) { // for flat associative array
743
+ $keys = array_keys( $data );
744
+ $vals = array_values( $data );
745
+ } else {
746
+ $keys = wp_list_pluck( $data, $key );
747
+ $vals = wp_list_pluck( $data, $val );
748
+ }
749
+ foreach ( $keys as $idx => $key ) {
750
+ $return[] = array(
751
+ 'id' => $key,
752
+ 'text' => $vals[ $idx ],
753
+ );
754
+ }
755
+
756
+ return $return;
757
+ }
758
+
759
+ /**
760
+ * Search for terms in a specific taxonomy
761
+ *
762
+ * @param string $search Search keyword
763
+ * @param array $taxonomies Taxonomies to search in
764
+ *
765
+ * @return array
766
+ */
767
+ public function get_terms( $search, $taxonomies = array() ) {
768
+ global $wpdb;
769
+ $taxonomies = (array) $taxonomies;
770
+
771
+ $sql = "SELECT tt.term_taxonomy_id id, t.name, t.slug, tt.taxonomy, tt.description
772
+ FROM $wpdb->terms t
773
+ JOIN $wpdb->term_taxonomy tt USING ( term_id )
774
+ WHERE
775
+ ";
776
+
777
+ if ( is_array( $search ) ) {
778
+ $search = array_map( 'intval', $search );
779
+ $where = sprintf( 'tt.term_taxonomy_id IN ( %s )', implode( ', ', $search ) );
780
+ } else {
781
+ $where = '
782
+ t.name LIKE %s
783
+ OR
784
+ t.slug LIKE %s
785
+ OR
786
+ tt.taxonomy LIKE %s
787
+ OR
788
+ tt.description LIKE %s
789
+ ';
790
+ $where = $wpdb->prepare( $where, "%$search%", "%$search%", "%$search%", "%$search%" );
791
+ }
792
+
793
+ $sql .= $where;
794
+ $results = $wpdb->get_results( $sql );
795
+
796
+ $return = array();
797
+ foreach ( $results as $result ) {
798
+ $return[ $result->id ] = sprintf( '%s - %s', $result->name, $result->taxonomy );
799
+ }
800
+
801
+ return $return;
802
+ }
803
+
804
+ /**
805
+ * @filter user_search_columns
806
+ */
807
+ public function define_search_in_arg( $search_columns, $search, $query ) {
808
+ $search_in = $query->get( 'search_in' );
809
+ $search_columns = ! is_null( $search_in ) ? (array) $search_in : $search_columns;
810
+
811
+ return $search_columns;
812
+ }
813
+
814
+ /**
815
+ * Change Post Title placeholder in post edit screen
816
+ *
817
+ * @filter enter_title_here
818
+ *
819
+ * @param $text
820
+ * @param $post
821
+ *
822
+ * @return string
823
+ */
824
+ public function title_placeholder( $text, $post ) {
825
+ if ( self::POSTTYPE === $post->post_type ) {
826
+ $text = __( 'Enter Rule Title here', 'stream' );
827
+ }
828
+
829
+ return $text;
830
+ }
831
+
832
+ /**
833
+ * Apply list actions, and load our list-table object
834
+ *
835
+ * @action load-edit.php
836
+ *
837
+ * @return void
838
+ */
839
+ public function load_list_table() {
840
+ global $typenow;
841
+
842
+ if ( self::POSTTYPE !== $typenow || WP_Stream_API::is_restricted( true ) ) {
843
+ return;
844
+ }
845
+
846
+ require_once WP_STREAM_NOTIFICATIONS_INC_DIR . 'class-wp-stream-notifications-list-table.php';
847
+ WP_Stream_Notifications_List_Table::get_instance();
848
+ }
849
+
850
+ }
extensions/notifications/includes/class-wp-stream-notifications-settings.php ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * Settings class for Stream Notifications
4
+ *
5
+ * @author X-Team <x-team.com>
6
+ * @author Shady Sharaf <shady@x-team.com>, Jaroslav Polakovič <dero@x-team.com>
7
+ */
8
+ class WP_Stream_Notifications_Settings {
9
+
10
+ public static $fields = array();
11
+
12
+ /**
13
+ * Public constructor
14
+ */
15
+ public static function load() {
16
+ // User and role caps
17
+ add_filter( 'user_has_cap', array( __CLASS__, '_filter_user_caps' ), 10, 4 );
18
+ add_filter( 'role_has_cap', array( __CLASS__, '_filter_role_caps' ), 10, 3 );
19
+
20
+ if ( WP_Stream_API::is_restricted() ) {
21
+ return;
22
+ }
23
+
24
+ // Add Notifications settings tab to Stream settings
25
+ add_filter( 'wp_stream_settings_option_fields', array( __CLASS__, '_register_settings' ) );
26
+ }
27
+
28
+ public static function get_fields() {
29
+ if ( empty( self::$fields ) ) {
30
+ $fields = array();
31
+
32
+ self::$fields = apply_filters( 'wp_stream_notifications_option_fields', $fields );
33
+ }
34
+
35
+ return self::$fields;
36
+ }
37
+
38
+ /**
39
+ * Appends Notifications settings to Stream settings
40
+ *
41
+ * @filter wp_stream_settings_option_fields
42
+ */
43
+ public static function _register_settings( $stream_fields ) {
44
+ return array_merge( $stream_fields, self::get_fields() );
45
+ }
46
+
47
+ /**
48
+ * Filter user caps to dynamically grant our view cap based on allowed roles
49
+ *
50
+ * @filter user_has_cap
51
+ *
52
+ * @param $allcaps
53
+ * @param $caps
54
+ * @param $args
55
+ * @param $user
56
+ *
57
+ * @return array
58
+ */
59
+ public static function _filter_user_caps( $allcaps, $caps, $args, $user = null ) {
60
+ $user = is_a( $user, 'WP_User' ) ? $user : wp_get_current_user();
61
+
62
+ foreach ( $caps as $cap ) {
63
+ if ( WP_Stream_Notifications::VIEW_CAP === $cap ) {
64
+ foreach ( $user->roles as $role ) {
65
+ if ( self::_role_can_access_notifications( $role ) ) {
66
+ $allcaps[ $cap ] = true;
67
+ break 2;
68
+ }
69
+ }
70
+ }
71
+ }
72
+
73
+ return $allcaps;
74
+ }
75
+
76
+ /**
77
+ * Filter role caps to dynamically grant our view cap based on allowed roles
78
+ *
79
+ * @filter role_has_cap
80
+ *
81
+ * @param $allcaps
82
+ * @param $cap
83
+ * @param $role
84
+ *
85
+ * @return array
86
+ */
87
+ public static function _filter_role_caps( $allcaps, $cap, $role ) {
88
+ if ( WP_Stream_Notifications::VIEW_CAP === $cap && self::_role_can_access_notifications( $role ) ) {
89
+ $allcaps[ $cap ] = true;
90
+ }
91
+
92
+ return $allcaps;
93
+ }
94
+
95
+ private static function _role_can_access_notifications( $role ) {
96
+ if ( ! isset( WP_Stream_Settings::$options['notifications_role_access'] ) ) {
97
+ WP_Stream_Settings::$options['notifications_role_access'] = array( 'administrator' );
98
+ }
99
+
100
+ if ( in_array( $role, WP_Stream_Settings::$options['notifications_role_access'] ) ) {
101
+ return true;
102
+ }
103
+
104
+ return false;
105
+ }
106
+
107
+ }
extensions/notifications/ui/css/form.css ADDED
@@ -0,0 +1,353 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Title */
2
+
3
+ .post-type-stream-notification #titlediv {
4
+ margin-bottom: 20px;
5
+ }
6
+
7
+
8
+ /* Triggers and Alerts */
9
+
10
+ #stream-notifications-triggers .field,
11
+ #stream-notifications-triggers .trigger-type,
12
+ #stream-notifications-triggers .trigger-options,
13
+ #stream-notifications-triggers .trigger-value {
14
+ float: left;
15
+ margin-right: 6px;
16
+ }
17
+
18
+ #stream-notifications-triggers .trigger .form-row {
19
+ clear: both;
20
+ overflow: hidden;
21
+ margin-bottom: 10px;
22
+ background: #eee;
23
+ padding: 10px;
24
+ }
25
+
26
+ #stream-notifications-triggers .trigger-value {
27
+ width: 200px;
28
+ }
29
+
30
+ #stream-notifications-triggers .inside {
31
+ padding-bottom: 3px;
32
+ }
33
+
34
+ #stream-notifications-triggers .inside,
35
+ #stream-notifications-alerts .inside {
36
+ margin-top: 12px;
37
+ }
38
+
39
+ #stream-notifications-alerts .inside {
40
+ padding: 0;
41
+ }
42
+
43
+ #stream-notifications-triggers .group {
44
+ background: rgba(0, 0, 0, 0.08);
45
+ padding: 20px 20px 12px;
46
+ margin-bottom: 10px;
47
+ min-height: 16px;
48
+ clear: both;
49
+ }
50
+
51
+ #stream-notifications-triggers .inside > .group {
52
+ margin: 0;
53
+ min-height: 0;
54
+ background: none;
55
+ padding: 0;
56
+
57
+ -webkit-box-shadow: none;
58
+ box-shadow: none;
59
+ }
60
+
61
+ #stream-notifications-triggers .group .form-row {
62
+ background: rgba(0, 0, 0, 0.03);
63
+ }
64
+
65
+ #stream-notifications-triggers .group,
66
+ #stream-notifications-triggers .group .form-row {
67
+ margin-left: 90px;
68
+
69
+ -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.15);
70
+ box-shadow: 0 1px 1px rgba(0, 0, 0, 0.15);
71
+ }
72
+
73
+ #stream-notifications-triggers .add-trigger,
74
+ #stream-notifications-triggers .add-trigger-group {
75
+ margin-right: 4px !important;
76
+ margin-bottom: 10px !important;
77
+ }
78
+
79
+ #stream-notifications-triggers .group .delete-trigger,
80
+ #stream-notifications-alerts .alert .delete-alert {
81
+ line-height: 29px;
82
+ }
83
+
84
+ #stream-notifications-triggers .group .delete-trigger,
85
+ #stream-notifications-triggers .group .delete-group,
86
+ #stream-notifications-alerts .alert .delete-alert {
87
+ color: #a00;
88
+ text-decoration: none;
89
+ }
90
+
91
+ #stream-notifications-triggers .group .delete-trigger:hover,
92
+ #stream-notifications-triggers .group .delete-group:hover,
93
+ #stream-notifications-alerts .alert .delete-alert:hover {
94
+ color: #f00;
95
+ text-decoration: none;
96
+ }
97
+
98
+ #stream-notifications-triggers .inside > .group,
99
+ #stream-notifications-triggers .inside > .group > .group,
100
+ #stream-notifications-alerts .inside > .alert {
101
+ margin-left: 0;
102
+ }
103
+
104
+ #stream-notifications-triggers .inside > .group > .trigger > .form-row {
105
+ margin-top: 10px;
106
+ }
107
+
108
+ #stream-notifications-triggers .inside > .group > .trigger.first > .form-row {
109
+ margin-top: 0;
110
+ }
111
+
112
+ #stream-notifications-triggers .inside > .group > .trigger > .form-row,
113
+ #stream-notifications-alerts .inside > .alert > .form-row {
114
+ margin-left: 0;
115
+ }
116
+
117
+ #stream-notifications-triggers .group-meta {
118
+ float: left;
119
+ margin-top: -10px;
120
+ margin-left: -10px;
121
+ padding: 0 0 12px;
122
+ }
123
+
124
+ #stream-notifications-triggers .group-meta a.delete-group {
125
+ line-height: 28px;
126
+ font-size: 10px;
127
+ }
128
+
129
+ #stream-notifications-triggers .group .trigger:first-of-type .field.relation,
130
+ #stream-notifications-triggers .trigger.first .field.relation {
131
+ display: none;
132
+ }
133
+
134
+ #stream-notifications-triggers .trigger.first .field.type {
135
+ margin-left: 99px;
136
+ }
137
+
138
+ #stream-notifications-triggers .delete-trigger,
139
+ #stream-notifications-alerts .delete-alert {
140
+ float: right;
141
+ }
142
+
143
+ #stream-notifications-alerts .inside > .alert {
144
+ margin: 0;
145
+ padding: 0;
146
+ border: none;
147
+ }
148
+
149
+ #stream-notifications-alerts .add-alert {
150
+ margin: 0 0 12px 12px;
151
+ }
152
+
153
+ #stream-notifications-alerts .select2-container {
154
+ max-width: 300px;
155
+ }
156
+
157
+ #stream-notifications-alerts .select2-container.alert-type {
158
+ min-width: 150px;
159
+ }
160
+
161
+ #stream-notifications-alerts .alert .form-row .type {
162
+ padding: 12px;
163
+ border: 1px solid #ddd;
164
+ box-shadow: inset #f5f5f5 0 1px 0 0;
165
+ background: #eaeaea;
166
+ }
167
+
168
+ #stream-notifications-alerts .alert .form-row .type .circle {
169
+ display: inline-block;
170
+ height: 22px;
171
+ width: 22px;
172
+ font-size: 12px;
173
+ line-height: 23px;
174
+ border-radius: 12px;
175
+ text-align: center;
176
+ background: #aaa;
177
+ color: #fff;
178
+ margin-right: 10px;
179
+ vertical-align: middle;
180
+ }
181
+
182
+ #stream-notifications-alerts table.alert-options {
183
+ margin-top: 0;
184
+ }
185
+
186
+ #stream-notifications-alerts table.alert-options tbody tr th.label {
187
+ width: 24%;
188
+ vertical-align: top;
189
+ background: #f9f9f9;
190
+ border-right: 1px solid #e1e1e1;
191
+ border-top: 1px solid #f0f0f0;
192
+ }
193
+
194
+ #stream-notifications-alerts table.alert-options tbody tr th.label label {
195
+ display: block;
196
+ font-size: 12px;
197
+ font-weight: bold;
198
+ padding: 0;
199
+ margin: 0 0 3px;
200
+ color: #333;
201
+ }
202
+
203
+ #stream-notifications-alerts table.alert-options tbody tr th .description {
204
+ display: block;
205
+ font-size: 12px;
206
+ line-height: 16px;
207
+ font-weight: normal;
208
+ font-style: normal;
209
+ color: #888;
210
+ }
211
+
212
+ #stream-notifications-alerts table.alert-options tbody tr th .description a {
213
+ color: #0074a2;
214
+ }
215
+
216
+ #stream-notifications-alerts table.alert-options tbody tr th,
217
+ #stream-notifications-alerts table.alert-options tbody tr td {
218
+ padding: 13px 15px;
219
+
220
+ }
221
+
222
+ #stream-notifications-alerts table.alert-options tbody tr td {
223
+ border-top: 1px solid #f5f5f5;
224
+ }
225
+
226
+
227
+ /* Data Tags */
228
+
229
+ #stream-notifications-data-tags .inside {
230
+ margin: 0 !important;
231
+ padding: 0 !important;
232
+ }
233
+
234
+ #stream-notifications-data-tags .accordion-section,
235
+ #stream-notifications-data-tags .accordion-section.bottom h3 {
236
+ border-bottom: none;
237
+ }
238
+
239
+ #stream-notifications-data-tags .accordion-section.open,
240
+ #stream-notifications-data-tags .accordion-section.open.bottom h3 {
241
+ border-bottom: 1px solid #dfdfdf;
242
+ }
243
+
244
+ #stream-notifications-data-tags .accordion-section-content {
245
+ padding-bottom: 5px;
246
+ }
247
+
248
+ #stream-notifications-data-tags .accordion-section-title {
249
+ padding-left: 30px;
250
+ }
251
+
252
+ #stream-notifications-data-tags .accordion-section-title:after {
253
+ display: none;
254
+ }
255
+
256
+ #stream-notifications-data-tags .accordion-section-title:before {
257
+ color: #aaa;
258
+ position: absolute;
259
+ left: 6px;
260
+ float: left;
261
+ content: '\f140';
262
+ font: 400 20px/1 dashicons;
263
+ speak: none;
264
+ display: block;
265
+ padding: 0;
266
+ text-indent: 0;
267
+ text-align: center;
268
+ text-decoration: none !important;
269
+
270
+ -webkit-font-smoothing: antialiased;
271
+ -moz-osx-font-smoothing: grayscale;
272
+ }
273
+
274
+ #stream-notifications-data-tags .accordion-section-title:hover:before {
275
+ color: #777;
276
+ }
277
+
278
+ #stream-notifications-data-tags .accordion-section.open .accordion-section-title:before {
279
+ content: '\f142';
280
+ }
281
+
282
+ #stream-notifications-data-tags dl {
283
+ margin-bottom: 0;
284
+ }
285
+
286
+ #stream-notifications-data-tags dd {
287
+ margin: 0;
288
+ padding-bottom: 15px;
289
+ }
290
+
291
+ .stream_page_wp_stream_notifications .alert {
292
+ background: none;
293
+ margin: 0;
294
+ padding: 0;
295
+ border: none;
296
+ text-shadow: none;
297
+ color: #444;
298
+
299
+ -webkit-border-radius: 0;
300
+ -moz-border-radius: 0;
301
+ border-radius: 0;
302
+ }
303
+
304
+
305
+ /* Select2 */
306
+
307
+ .post-type-stream-notification .select2-container {
308
+ margin-right: 6px;
309
+ }
310
+
311
+ .post-type-stream-notification .select2-results li {
312
+ margin-bottom: 2px;
313
+ }
314
+
315
+ .post-type-stream-notification .select2-container .select2-choice > .select2-chosen {
316
+ font-size: 13px;
317
+ }
318
+
319
+ .post-type-stream-notification .select2-container.trigger-type {
320
+ width: 180px !important;
321
+ }
322
+
323
+ .post-type-stream-notification .select2-container.trigger-operator {
324
+ width: 140px !important;
325
+ }
326
+
327
+ .post-type-stream-notification .select2-choices img.avatar,
328
+ .post-type-stream-notification .select2-results img.avatar {
329
+ vertical-align: middle;
330
+ margin-right: 5px;
331
+ margin-bottom: 2px;
332
+
333
+ -webkit-border-radius: 3px;
334
+ -moz-border-radius: 3px;
335
+ border-radius: 3px;
336
+ }
337
+
338
+ .post-type-stream-notification .select2-search-choice-close {
339
+ -webkit-transition: none;
340
+ -moz-transition: none;
341
+ -o-transition: all 0 none;
342
+ transition: none;
343
+ }
344
+
345
+
346
+ /* Hide Post Publish Elements */
347
+
348
+ #edit-slug-box,
349
+ #preview-action,
350
+ #normal-sortables,
351
+ .misc-pub-section.misc-pub-visibility {
352
+ display: none;
353
+ }
extensions/notifications/ui/images/stream-notifications-example.jpg ADDED
Binary file
extensions/notifications/ui/js/form.js ADDED
@@ -0,0 +1,571 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* globals stream_notifications, ajaxurl, _, alert, confirm */
2
+ jQuery( function( $ ) {
3
+ 'use strict';
4
+
5
+ _.templateSettings.variable = 'vars';
6
+
7
+ $( '#toplevel_page_wp_stream' )
8
+ .add( '#toplevel_page_wp_stream > a.toplevel_page_wp_stream' )
9
+ .removeClass( 'wp-not-current-submenu' )
10
+ .addClass( 'wp-has-current-submenu wp-menu-open' );
11
+
12
+ $.datepicker.setDefaults({
13
+ dateFormat: 'yy/mm/dd',
14
+ minDate: 0
15
+ });
16
+
17
+ var types = stream_notifications.types,
18
+
19
+ divTriggers = $( '#stream-notifications-triggers' ), // Trigger Playground
20
+ divAlerts = $( '#stream-notifications-alerts .inside' ), // Alerts Playground
21
+
22
+ iGroup = 0,
23
+
24
+ btns = {
25
+ add_trigger: '.add-trigger',
26
+ add_alert: '.add-alert',
27
+ add_group: '.add-trigger-group',
28
+ del: '#delete-trigger'
29
+ },
30
+
31
+ tmpl = _.template( $( 'script#trigger-template-row' ).html() ),
32
+ tmpl_options = _.template( $( 'script#trigger-template-options' ).html() ),
33
+ tmpl_group = _.template( $( 'script#trigger-template-group' ).html() ),
34
+ tmpl_alert = _.template( $( 'script#alert-template-row' ).html() ),
35
+ tmpl_alert_options = _.template( $( 'script#alert-template-options' ).html() ),
36
+
37
+ select2_format = function( item ) {
38
+ return item.text;
39
+ },
40
+
41
+ select2_args = {
42
+ allowClear: true,
43
+ minimumResultsForSearch: 8,
44
+ width: '160px',
45
+ format: select2_format,
46
+ formatSelection: select2_format,
47
+ formatResult: select2_format,
48
+
49
+ // Only allow multi items if we have a proper operator
50
+ maximumSelectionSize: function() {
51
+ var item = $( '.select2-container-active.trigger-value' );
52
+ if ( ! item.size() ) {
53
+ return 0;
54
+ }
55
+
56
+ var operator = item.parents( '.form-row' ).first().find( 'select.trigger-operator' ).val();
57
+ if ( ! operator ) {
58
+ return 0;
59
+ }
60
+
61
+ if ( operator.match(/in$/) ) {
62
+ return 0;
63
+ } else {
64
+ return 1;
65
+ }
66
+ }
67
+
68
+ },
69
+
70
+ datify = function( elements ) {
71
+ $( elements ).each( function() {
72
+ $( this ).datepicker();
73
+ });
74
+
75
+ $( '#ui-datepicker-div' ).addClass( 'stream-datepicker' );
76
+ },
77
+
78
+ selectify = function( elements, args ) {
79
+ args = args || {};
80
+ $.extend( args, select2_args );
81
+
82
+ $( elements ).filter( ':not(.select2-offscreen)' ).each( function() {
83
+ var $this = $( this ),
84
+ elementArgs = jQuery.extend( {}, args ),
85
+ tORa = $this.closest( '#stream-notifications-alerts, #stream-notifications-triggers' ).attr( 'id' ).replace( 'stream-notifications-', '' );
86
+
87
+ elementArgs.width = parseInt( $this.css( 'width' ), 10 ) + 30;
88
+ if ( $this.hasClass( 'ip' ) ) {
89
+ elementArgs.ajax = {
90
+ type: 'POST',
91
+ url: ajaxurl,
92
+ dataType: 'json',
93
+ quietMillis: 500,
94
+ data: function( term ) {
95
+ return {
96
+ find: term,
97
+ limit: 10,
98
+ action: 'stream_get_ips'
99
+ };
100
+ },
101
+ results: function( response ) {
102
+ var answer = {
103
+ results: []
104
+ };
105
+
106
+ if ( true !== response.success || undefined === response.data ) {
107
+ return answer;
108
+ }
109
+
110
+ $.each(response.data, function( key, ip ) {
111
+ answer.results.push({
112
+ id: ip,
113
+ text: ip
114
+ });
115
+ });
116
+
117
+ return answer;
118
+ }
119
+ },
120
+ elementArgs.initSelection = function( item, callback ) {
121
+ callback( item.data( 'selected' ) );
122
+ },
123
+ elementArgs.formatNoMatches = function() {
124
+ return '';
125
+ },
126
+ elementArgs.createSearchChoice = function( term ) {
127
+ var ip_chunks = [];
128
+
129
+ ip_chunks = term.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
130
+
131
+ if ( null === ip_chunks ) {
132
+ return;
133
+ }
134
+
135
+ // remove whole match
136
+ ip_chunks.shift();
137
+
138
+ ip_chunks = $.grep(
139
+ ip_chunks,
140
+ function( chunk ) {
141
+ var numeric = parseInt(chunk, 10);
142
+
143
+ return numeric <= 255 && numeric.toString() === chunk;
144
+ }
145
+ );
146
+
147
+ if (ip_chunks.length < 4) {
148
+ return;
149
+ }
150
+
151
+ return {
152
+ id: term,
153
+ text: term
154
+ };
155
+ };
156
+ } else if ( $this.hasClass( 'ajax' ) ) {
157
+ var type = '';
158
+ if ( ! ( type = $this.data( 'ajax-key' ) ) ) {
159
+ if ( 'triggers' === tORa ) {
160
+ type = $this.parents( '.form-row' ).first().find( 'select.trigger-type' ).val();
161
+ } else {
162
+ type = $this.parents( '.form-row' ).eq(1).find( 'select.alert-type' ).val();
163
+ }
164
+ }
165
+ elementArgs.minimumInputLength = 3;
166
+ elementArgs.ajax = {
167
+ url: ajaxurl,
168
+ type: 'post',
169
+ dataType: 'json',
170
+ data: function( term ) {
171
+ return {
172
+ action: 'stream_notification_endpoint',
173
+ type: type,
174
+ q: term,
175
+ args: $( this ).attr( 'data-args' )
176
+ };
177
+ },
178
+ results: function( data ) {
179
+ var r = data.data || [];
180
+ return {results: r};
181
+ }
182
+ };
183
+ elementArgs.initSelection = function( element, callback ) {
184
+ var id = $(element).val();
185
+ if ( '' !== id ) {
186
+ $.ajax({
187
+ url: ajaxurl,
188
+ type: 'post',
189
+ data: {
190
+ action: 'stream_notification_endpoint',
191
+ q: id,
192
+ single: 1,
193
+ type: type,
194
+ args: $( this ).attr( 'data-args' )
195
+ },
196
+ dataType: 'json'
197
+ }).done( function( data ) { callback( data.data ); } );
198
+ }
199
+ };
200
+ elementArgs.formatResult = function( object ) {
201
+ var result = object.text;
202
+
203
+ if ( object.hasOwnProperty( 'avatar' ) ) {
204
+ result = object.avatar + result;
205
+ }
206
+
207
+ return result;
208
+ };
209
+ elementArgs.formatSelection = function( object ) {
210
+ var result = object.text;
211
+
212
+ if ( object.hasOwnProperty( 'avatar' ) ) {
213
+ result += '<i class="icon16 icon-users"></i>';
214
+ }
215
+
216
+ return result;
217
+ };
218
+ }
219
+
220
+ $this.select2( elementArgs );
221
+ $this.on( 'select2_populate', function( e, val ) {
222
+ var $this = $( this );
223
+ if ( ! val ) {
224
+ return;
225
+ }
226
+ if ( $this.hasClass( 'ajax' ) ) {
227
+ $.ajax({
228
+ url: ajaxurl,
229
+ type: 'post',
230
+ data: {
231
+ action: 'stream_notification_endpoint',
232
+ q: val,
233
+ single: 1,
234
+ type: type,
235
+ args: $( this ).attr( 'data-args' )
236
+ },
237
+ dataType: 'json',
238
+ success: function( j ) {
239
+ $this.select2( 'data', j.data );
240
+ }
241
+ });
242
+ } else if ( $this.hasClass( 'tags' ) ) {
243
+ $this.select2( 'data', $.map(
244
+ val.split( ',' ),
245
+ function( ip ) {
246
+ return { id: ip, text: ip };
247
+ }
248
+ ) );
249
+ } else {
250
+ $this.select2( 'val', val.split( ',' ) );
251
+ }
252
+ } );
253
+ });
254
+ },
255
+
256
+ add_trigger = function( group_index ) {
257
+ var index = 0,
258
+ lastItem = null,
259
+ group = divTriggers.find( '.group[rel=' + group_index + ']' ),
260
+ i = null,
261
+ type = null,
262
+ types = {},
263
+ connectors = {}
264
+ ;
265
+
266
+ if ( ( lastItem = divTriggers.find( '.trigger' ).last() ) && lastItem.size() ) {
267
+ index = parseInt( lastItem.attr( 'rel' ), 10 ) + 1;
268
+ }
269
+
270
+ // Get adjacent trigger[type=connector] to filter special trigger types
271
+ connectors = group.find( 'select.trigger-type option:selected[value=connector]' );
272
+ connectors = connectors.map( function() {
273
+ return $( this ).parents( '.trigger' ).first().find( ':input.trigger-value' ).val();
274
+ }).toArray();
275
+ if ( connectors.length ) {
276
+ for ( i in stream_notifications.special_types ) {
277
+ type = stream_notifications.special_types[i];
278
+ if ( -1 !== connectors.indexOf( type.connector ) ) {
279
+ types[i] = type;
280
+ }
281
+ }
282
+ }
283
+
284
+ group.append( tmpl( $.extend(
285
+ { index: index, group: group_index },
286
+ stream_notifications,
287
+ { types: $.extend( {}, stream_notifications.types, types ) }
288
+ ) ) );
289
+ group.find( '.trigger' ).first().addClass( 'first' );
290
+ selectify( group.find( 'select' ) );
291
+ },
292
+
293
+ add_alert = function() {
294
+ var index = divAlerts.find( '.alert' ).size();
295
+
296
+ divAlerts.append( tmpl_alert( $.extend(
297
+ { index: index },
298
+ stream_notifications
299
+ ) ) );
300
+ selectify( divAlerts.find( '.alert select' ) );
301
+ },
302
+
303
+ display_error = function( key ) {
304
+ if ( $( '.error' ).filter( function() { return $( this ).attr( 'data-key' ) === key; } ).length === 0 ) {
305
+ $( 'body,html' ).scrollTop(0);
306
+ $( '.wrap > h2' )
307
+ .after(
308
+ $( '<div></div>' )
309
+ .addClass( 'updated error fade' )
310
+ .attr( 'data-key', key )
311
+ .hide()
312
+ .append(
313
+ $( '<p></p>' ).text(stream_notifications.i18n[key])
314
+ )
315
+ )
316
+ .next( '.updated' )
317
+ .fadeIn( 'normal' )
318
+ .delay( 3000 )
319
+ .fadeOut( 'normal', function() { $( this ).remove(); } );
320
+ }
321
+ };
322
+
323
+ divTriggers
324
+ // Add new rule
325
+ .on( 'click.sn', btns.add_trigger, function( e ) {
326
+ e.preventDefault();
327
+
328
+ add_trigger( $( this ).data( 'group' ) );
329
+ })
330
+
331
+ // Add new group
332
+ .on( 'click.sn', btns.add_group, function( e, groupIndex ) {
333
+ e.preventDefault();
334
+ var $this = $( this ),
335
+ parentGroupIndex = $this.data( 'group' ),
336
+ group = divTriggers.find( '.group[rel=' + $this.data( 'group' ) + ']' );
337
+
338
+ if ( ! groupIndex ) {
339
+ groupIndex = ++iGroup;
340
+ }
341
+
342
+ group.append( tmpl_group( { index: groupIndex, parent: parentGroupIndex } ) );
343
+ selectify( group.find( '.field.relation select' ) );
344
+ })
345
+
346
+ // Delete a trigger
347
+ .on( 'click.sn', '.delete-trigger', function( e ) {
348
+ e.preventDefault();
349
+ var $this = $( this );
350
+
351
+ $this.closest( '.trigger' ).remove();
352
+
353
+ // add `first` class in case the first trigger was removed
354
+ $this.closest( '.group' ).find( '.trigger' ).first().addClass( 'first' );
355
+ })
356
+
357
+ // Delete a group
358
+ .on( 'click.sn', '.delete-group', function( e ) {
359
+ e.preventDefault();
360
+ var $this = $( this );
361
+
362
+ $this.parents( '.group' ).first().remove();
363
+ })
364
+
365
+ // Reveal rule options after choosing rule type
366
+ .on( 'change.sn', '.trigger-type', function() {
367
+ var $this = $( this ),
368
+ options = null,
369
+ index = $this.parents( '.trigger' ).first().attr( 'rel' );
370
+
371
+ if ( ( 'undefined' !== typeof types[ $this.val() ] ) ) {
372
+ options = types[ $this.val() ];
373
+ } else {
374
+ options = stream_notifications.special_types[ $this.val() ];
375
+ }
376
+
377
+ $this.next( '.trigger-options' ).remove();
378
+
379
+ if ( ! options ) { return; }
380
+
381
+ $this.after( tmpl_options( $.extend( options, { index: index } ) ) );
382
+ selectify( $this.parent().find( 'select' ) );
383
+ selectify( $this.parent().find( 'input.tags, input.ajax' ), { tags: [] } );
384
+ datify( $this.parent().find( '.type-date' ) );
385
+ })
386
+ ;
387
+
388
+ divAlerts
389
+ // Add new alert
390
+ .on( 'click.sn', btns.add_alert, function( e ) {
391
+ e.preventDefault();
392
+ add_alert();
393
+ $( 'html, body' ).animate({
394
+ scrollTop: divAlerts.find( '.alert' ).last().offset().top
395
+ }, 400);
396
+ })
397
+
398
+ // Reveal alert options after choosing alert type
399
+ .on( 'change.sn', '.alert-type', function() {
400
+ var $this = $( this ),
401
+ $wrapper = $this.closest( '.alert' ),
402
+ $alert = {},
403
+ $copy = {},
404
+ options = stream_notifications.adapters[ $this.val() ],
405
+ type = $this.val(),
406
+ index = $wrapper.attr( 'rel' );
407
+
408
+ $wrapper.find( '.alert-options' ).hide();
409
+
410
+ if ( ! options ) { return; }
411
+
412
+ $copy = $wrapper
413
+ .find( '.alert-options' )
414
+ .filter( function() {
415
+ return $( this ).attr( 'data-type' ) === type;
416
+ });
417
+ $wrapper.find( '.alert-options' ).hide();
418
+
419
+ if( 0 === $copy.length ) { // render new alert template
420
+ $alert = $( tmpl_alert_options( $.extend( options, { type: type, index: index } ) ) );
421
+ $alert.appendTo( $wrapper );
422
+ selectify( $alert.find( 'select' ) );
423
+ selectify( $alert.find( 'input.tags, input.ajax' ), { tags: [] } );
424
+ } else { // copy found, just show it
425
+ $copy.show();
426
+ }
427
+ })
428
+
429
+ // Delete an alert
430
+ .on( 'click.sn', '.delete-alert', function( e ) {
431
+ e.preventDefault();
432
+ var $this = $( this );
433
+
434
+ $this.parents( '.alert' ).first().remove();
435
+
436
+ $( '.alert .circle' ).each( function( index ) {
437
+ $( this ).text(index + 1);
438
+ });
439
+ })
440
+ ;
441
+
442
+ // Populate form values if it exists
443
+ if ( 'undefined' !== typeof stream_notifications.meta ) {
444
+
445
+ // Triggers
446
+ jQuery.each( stream_notifications.meta.triggers, function( i, trigger ) {
447
+ var groupDiv = divTriggers.find( '.group' ).filter( '[rel='+trigger.group+']' ),
448
+ row,
449
+ valueField;
450
+
451
+ // create the group if it doesn't exist
452
+ if ( ! groupDiv.size() ) {
453
+ var group = stream_notifications.meta.groups[trigger.group];
454
+ $( btns.add_group ).filter( '[data-group='+group.group+']' ).trigger( 'click', trigger.group);
455
+ groupDiv = divTriggers.find( '.group' ).filter( '[rel='+trigger.group+']' );
456
+ groupDiv.find( 'select.group-relation' ).select2( 'val', group.relation );
457
+ }
458
+
459
+ // create the new row, by clicking the add-trigger button in the appropriate group
460
+ divTriggers.find( btns.add_trigger ).filter( '[data-group='+trigger.group+']' ).trigger( 'click' );
461
+ // debugger; # DEBUG
462
+
463
+ // populate values
464
+ row = groupDiv.find( '.trigger:last' );
465
+ row.find( 'select.trigger-relation' ).select2( 'val', trigger.relation ).trigger( 'change' );
466
+ row.find( 'select.trigger-type' ).select2( 'val', trigger.type ).trigger( 'change' );
467
+ row.find( 'select.trigger-operator' ).select2( 'val', trigger.operator ).trigger( 'change' );
468
+
469
+ // populate the trigger value, according to the trigger type
470
+ if ( trigger.value ) {
471
+ valueField = row.find( '.trigger-value:not(.select2-container)' ).eq(0);
472
+ if ( valueField.is( 'select' ) || valueField.is( '.ajax, .ip' ) ) {
473
+ valueField.trigger( 'select2_populate', trigger.value );
474
+ // valueField.select2( 'val', trigger.value ).trigger( 'change' );
475
+ } else {
476
+ valueField.val( trigger.value ).trigger( 'change' );
477
+ }
478
+ }
479
+ } );
480
+
481
+ // Alerts
482
+ jQuery.each( stream_notifications.meta.alerts, function( i, alert ) {
483
+ var row,
484
+ optionFields;
485
+
486
+ // create the new row, by clicking the add-alert button
487
+ add_alert();
488
+
489
+ // populate values
490
+ row = divAlerts.find( '.alert:last' );
491
+ row.find( 'select.alert-type' ).select2( 'val', alert.type ).trigger( 'change' );
492
+ optionFields = row.find( '.alert-options' );
493
+ optionFields.find( ':input[name]' ).each( function( i, el ) {
494
+ var $this = $(el),
495
+ name,
496
+ val;
497
+ name = $this.attr( 'name' ).match(/\[([a-z_\-]+)\]$/)[1];
498
+ if ( 'undefined' !== typeof alert[name] ) {
499
+ val = alert[name];
500
+ if ( $this.hasClass( 'select2-offscreen' ) ) {
501
+ $this.trigger( 'select2_populate', val );
502
+ // $this.select2( 'val', val ).trigger( 'change' );
503
+ } else {
504
+ $this.val( val ).trigger( 'change' );
505
+ }
506
+ }
507
+ });
508
+ });
509
+ }
510
+
511
+ $( '#rule-form' ).submit( function() {
512
+ // Do not submit if no triggers exist
513
+ if ( divTriggers.find( '.trigger' ).size() < 1 ) {
514
+ display_error( 'empty_triggers' );
515
+ return false;
516
+ }
517
+
518
+ // Do not submit if no working triggers exist
519
+ if ( null === $( '.trigger-type:first' ).select2( 'data' ) ) {
520
+ display_error( 'invalid_first_trigger' );
521
+ return false;
522
+ }
523
+
524
+ $( '.alert-options:hidden' ).remove();
525
+ });
526
+
527
+ divAlerts
528
+ .on( 'click', '.toggler', function( e ) {
529
+ e.preventDefault();
530
+ var rel = this.rel,
531
+ toggled = $( rel ),
532
+ toggler = $( this )
533
+ ;
534
+
535
+ if ( ! toggled.is( ':visible' ) ) {
536
+ toggled.slideDown( 'fast' );
537
+ toggler.data( 'text', toggler.text() );
538
+ toggler.text( toggler.data( 'text-toggle' ) );
539
+ } else {
540
+ toggled.slideUp( 'fast' );
541
+ toggler.text( toggler.data( 'text' ) );
542
+ }
543
+ } );
544
+
545
+ // Autofocus for earlier browsers
546
+ $( '[autofocus]' ).focus();
547
+
548
+ // Reset occurrences link
549
+ $( 'a.reset-occ' ).click( function( e ) {
550
+ e.preventDefault();
551
+
552
+ if ( ! confirm( stream_notifications.i18n.confirm_reset ) ) {
553
+ return;
554
+ }
555
+
556
+ $.getJSON( this.href, {}, function( j ) {
557
+ var div = $( '.submitbox .occurrences strong' );
558
+ if ( j.success ) {
559
+ div.html( div.html().replace(/\d+/, 0) );
560
+ } else {
561
+ alert( stream_notifications.i18n.ajax_error );
562
+ }
563
+ } );
564
+ });
565
+
566
+ // Add empty trigger if no triggers are visible
567
+ if ( 0 === $( '.trigger' ).length ) {
568
+ add_trigger( 0 );
569
+ }
570
+
571
+ });
extensions/notifications/ui/js/list.js ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* globals stream_notifications_options, _ */
2
+ jQuery( function( $ ) {
3
+
4
+ if ( stream_notifications_options.bulkActions ) {
5
+ var $bulkSelect = $( '.bulkactions select' ),
6
+ opts = '';
7
+
8
+ _.each( stream_notifications_options.bulkActions, function( el, i ) {
9
+ opts += '<option value="%">%</option>'.replace( '%', i ).replace( '%', el );
10
+ });
11
+
12
+ $bulkSelect.find( 'option:first' ).after( opts );
13
+ }
14
+ });
extensions/notifications/views/form-templates.php ADDED
@@ -0,0 +1,132 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script type="text/template" id="trigger-template-row">
2
+ <div class="trigger" rel="<%- vars.index %>">
3
+ <div class="form-row">
4
+ <input type="hidden" name="triggers[<%- vars.index %>][group]" value="<%- vars.group %>"/>
5
+ <div class="field relation">
6
+ <select name="triggers[<%- vars.index %>][relation]" class="trigger-relation">
7
+ <option value="and"><?php esc_html_e( 'AND', 'stream' ) ?></option>
8
+ <option value="or"><?php esc_html_e( 'OR', 'stream' ) ?></option>
9
+ </select>
10
+ </div>
11
+ <div class="field type">
12
+ <select name="triggers[<%- vars.index %>][type]" class="trigger-type" rel="<%- vars.index %>" placeholder="Choose Rule">
13
+ <option></option>
14
+ <% _.each( vars.types, function( type, name ) { %>
15
+ <option value="<%- name %>"><%- type.title %></option>
16
+ <% }); %>
17
+ </select>
18
+ </div>
19
+ <a href="#" class="delete-trigger"><?php esc_html_e( 'Delete', 'stream' ) ?></a>
20
+ </div>
21
+ </div>
22
+ </script>
23
+
24
+ <script type="text/template" id="trigger-template-group">
25
+ <div class="group" rel="<%- vars.index %>">
26
+ <div class="group-meta">
27
+ <input type="hidden" name="groups[<%- vars.index %>][group]" value="<%- vars.parent %>"/>
28
+ <div class="field relation">
29
+ <select name="groups[<%- vars.index %>][relation]" class="group-relation">
30
+ <option value="and"><?php esc_html_e( 'AND', 'stream' ) ?></option>
31
+ <option value="or"><?php esc_html_e( 'OR', 'stream' ) ?></option>
32
+ </select>
33
+ </div>
34
+ <a href="#add-trigger" class="add-trigger button button-secondary" data-group="<%- vars.index %>"><?php esc_html_e( '+ Add Trigger', 'stream' ) ?></a>
35
+ <a href="#add-trigger-group" class="add-trigger-group button button-primary" data-group="<%- vars.index %>"><?php esc_html_e( '+ Add Group', 'stream' ) ?></a>
36
+ <a href="#" class="delete-group"><?php esc_html_e( 'Delete Group', 'stream' ) ?></a>
37
+ </div>
38
+ </div>
39
+ </script>
40
+
41
+ <script type="text/template" id="trigger-template-options">
42
+ <div class="trigger-options">
43
+ <div class="field operator">
44
+ <select name="triggers[<%- vars.index %>][operator]" class="trigger-operator">
45
+ <% _.each( vars.operators, function( list, name ) { %>
46
+ <option value="<%- name %>"><%- list %></option>
47
+ <% }); %>
48
+ </select>
49
+ </div>
50
+ <div class="field value">
51
+ <% if ( ['select', 'ajax'].indexOf( vars.type ) != -1 ) { %>
52
+ <select name="triggers[<%- vars.index %>][value]<% if ( vars.multiple ) { %>[]<% } %>" class="trigger-value<% if ( vars.subtype ) { %> <%- vars.subtype %><% } %>" data-ajax="<% ( vars.ajax ) %>" <% if ( vars.multiple ) { %> multiple="multiple"<% } %>>
53
+ <option></option>
54
+ <% if ( vars.options ) { %>
55
+ <% _.each( vars.options, function( list, name ) { %>
56
+ <option value="<%- name %>"><%- list %></option>
57
+ <% }); %>
58
+ <% } %>
59
+ </select>
60
+ <% } else { %>
61
+ <input type="text" name="triggers[<%- vars.index %>][value]" class="trigger-value type-<%- vars.type %> <% if ( vars.tags ) { %>tags<% } %> <% if ( vars.ajax ) { %>ajax<% } %><% if ( vars.subtype ) { %> <%- vars.subtype %><% } %>">
62
+ <% } // endif%>
63
+ </div>
64
+ </div>
65
+ </script>
66
+
67
+ <script type="text/template" id="alert-template-row">
68
+ <div class="alert" rel="<%- vars.index %>">
69
+ <div class="form-row">
70
+ <div class="type">
71
+ <span class="circle"><%- vars.index + 1 %></span>
72
+ <select name="alerts[<%- vars.index %>][type]" class="alert-type" rel="<%- vars.index %>" placeholder="Choose Type">
73
+ <option></option>
74
+ <% _.each( vars.adapters, function( type, name ) { %>
75
+ <option value="<%- name %>"><%- type.title %></option>
76
+ <% }); %>
77
+ </select>
78
+ <a href="#" class="delete-alert alignright"><?php esc_html_e( 'Delete', 'stream' ) ?></a>
79
+ <div class="clear"></div>
80
+ </div>
81
+ </div>
82
+ </div>
83
+ </script>
84
+
85
+ <script type="text/template" id="alert-template-options">
86
+ <table class="alert-options form-table" data-type="<%- vars.type %>">
87
+ <% for ( field_name in vars.fields ) { var field = vars.fields[field_name]; %>
88
+ <% var argsHTML = ( typeof field.args === "object" ? "data-args=" + JSON.stringify( field.args ) : "" ); %>
89
+ <tr>
90
+ <th class="label">
91
+ <label><%- field.title %></label>
92
+ <% if ( field.hint ) { %>
93
+ <% var hints = ( typeof field.hint === "object" ? field.hint : [field.hint] ); %>
94
+ <% for ( i in hints ) { var hint = hints[i]; %>
95
+ <p class="description"><%= hint %></p>
96
+ <% } %>
97
+ <% } %>
98
+ </th>
99
+ <td>
100
+ <div class="field value">
101
+ <% if ( ['select'].indexOf( field.type ) != -1 ) { %>
102
+ <select name="alerts[<%- vars.index %>][<%- field_name %>]" class="alert-value widefat" data-ajax="<% ( field.ajax ) %>" <% if ( field.multiple ) { %> multiple="multiple"<% } %> <%- argsHTML %>>
103
+ <option></option>
104
+ <% if ( field.options ) { %>
105
+ <% _.each( field.options, function( list, name ) { %>
106
+ <option value="<%- name %>"><%- list %></option>
107
+ <% }); %>
108
+ <% } %>
109
+ </select>
110
+ <% } else if ( ['textarea'].indexOf( field.type ) != -1 ) { %>
111
+ <textarea name="alerts[<%- vars.index %>][<%- field_name %>]" class="alert-value large-text code" rows="10" cols="80" <%- argsHTML %>></textarea>
112
+ <% } else if ( ['error'].indexOf( field.type ) != -1 ) { %>
113
+ <%= field.message %>
114
+ <% } else { %>
115
+ <input type="text" name="alerts[<%- vars.index %>][<%- field_name %>]" class="alert-value widefat <% if ( field.tags ) { %>tags<% } %> <% if ( field.ajax ) { %>ajax<% } %>" <% if ( field.ajax && field.key ) { %> data-ajax-key="<%- field.key %>"<% } %> <%- argsHTML %>>
116
+ <% } %>
117
+ <% if ( field.after ) { %>
118
+ <%- field.after %>
119
+ <% } %>
120
+ </div>
121
+ </td>
122
+ </tr>
123
+ <% } %>
124
+ <% if ( typeof vars.hints != 'undefined' && vars.hints ) { %>
125
+ <tr>
126
+ <th></th>
127
+ <td><%= vars.hints %></td>
128
+ </tr>
129
+ <% } %>
130
+ </table>
131
+
132
+ </script>
extensions/reports/class-wp-stream-reports.php ADDED
@@ -0,0 +1,331 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class WP_Stream_Reports {
4
+
5
+ /**
6
+ * Hold Stream Reports instance
7
+ *
8
+ * @var string
9
+ */
10
+ public static $instance;
11
+
12
+ /**
13
+ * Screen ID for my admin page
14
+ * @var string
15
+ */
16
+ public static $screen_id;
17
+
18
+ /**
19
+ * Holds admin notices messages
20
+ *
21
+ * @var array
22
+ */
23
+ public static $messages = array();
24
+
25
+ /**
26
+ * Hold the nonce name
27
+ */
28
+ public static $nonce;
29
+
30
+ /**
31
+ * Page slug for notifications list table screen
32
+ *
33
+ * @const string
34
+ */
35
+ const REPORTS_PAGE_SLUG = 'wp_stream_reports';
36
+
37
+ /**
38
+ * Capability for the Notifications to be viewed
39
+ *
40
+ * @const string
41
+ */
42
+ const VIEW_CAP = 'view_stream_reports';
43
+
44
+ /**
45
+ * Class constructor
46
+ */
47
+ private function __construct() {
48
+ define( 'WP_STREAM_REPORTS_DIR', WP_STREAM_EXTENSIONS_DIR . 'reports/' ); // Has trailing slash
49
+ define( 'WP_STREAM_REPORTS_URL', WP_STREAM_URL . 'extensions/reports/' ); // Has trailing slash
50
+ define( 'WP_STREAM_REPORTS_INC_DIR', WP_STREAM_REPORTS_DIR . 'includes/' ); // Has trailing slash
51
+ define( 'WP_STREAM_REPORTS_VIEW_DIR', WP_STREAM_REPORTS_DIR . 'views/' ); // Has trailing slash
52
+
53
+ if ( ! apply_filters( 'wp_stream_reports_load', true ) ) {
54
+ return;
55
+ }
56
+
57
+ add_action( 'init', array( $this, 'load' ) );
58
+ }
59
+
60
+ /**
61
+ * Load our classes, actions/filters, only if our big brother is activated.
62
+ * GO GO GO!
63
+ *
64
+ * @return void
65
+ */
66
+ public function load() {
67
+ // Register new submenu
68
+ if ( ! apply_filters( 'wp_stream_reports_disallow_site_access', false ) && ! WP_Stream_Admin::$disable_access && ( WP_Stream::is_connected() || WP_Stream::is_development_mode() ) ) {
69
+ add_action( 'admin_menu', array( $this, 'register_menu' ), 11 );
70
+ }
71
+
72
+ add_action( 'all_admin_notices', array( $this, 'admin_notices' ) );
73
+
74
+ // Load settings
75
+ require_once WP_STREAM_REPORTS_INC_DIR . 'class-wp-stream-reports-settings.php';
76
+ add_action( 'init', array( 'WP_Stream_Reports_Settings', 'load' ), 9 );
77
+
78
+ // Load date interval
79
+ require_once WP_STREAM_CLASS_DIR . 'class-wp-stream-date-interval.php';
80
+ require_once WP_STREAM_REPORTS_INC_DIR . 'class-wp-stream-reports-date-interval.php';
81
+ add_action( 'init', array( 'WP_Stream_Reports_Date_Interval', 'get_instance' ) );
82
+
83
+ // Load metaboxes and charts
84
+ require_once WP_STREAM_REPORTS_INC_DIR . 'class-wp-stream-reports-meta-boxes.php';
85
+ require_once WP_STREAM_REPORTS_INC_DIR . 'class-wp-stream-reports-charts.php';
86
+ add_action( 'init', array( 'WP_Stream_Reports_Metaboxes', 'get_instance' ), 12 );
87
+
88
+ // Load template tags
89
+ require_once WP_STREAM_REPORTS_INC_DIR . 'template-tags.php';
90
+
91
+ // Register and enqueue the administration scripts
92
+ add_action( 'admin_enqueue_scripts', array( $this, 'register_ui_assets' ), 20 );
93
+ add_action( 'admin_print_scripts', array( $this, 'dequeue_media_conflicts' ), 9999 );
94
+ }
95
+
96
+ /**
97
+ * @param array $ajax_hooks associative array of ajax hooks action to actual functionname
98
+ * @param object $referer Class refering the action
99
+ */
100
+ public static function handle_ajax_request( $ajax_hooks, $referer ) {
101
+ // If we are not in ajax mode, return early
102
+ if ( ! defined( 'DOING_AJAX' ) || ! is_object( $referer ) ) {
103
+ return;
104
+ }
105
+
106
+ foreach ( $ajax_hooks as $hook => $function ) {
107
+ add_action( "wp_ajax_{$hook}", array( $referer, $function ) );
108
+ }
109
+
110
+ // Check referer here so we don't have to check it on every function call
111
+ if ( array_key_exists( $_REQUEST['action'], $ajax_hooks ) ) {
112
+ // Checking permission
113
+ if ( ! current_user_can( WP_Stream_Reports::VIEW_CAP ) ) {
114
+ wp_die( __( 'Cheating huh?', 'stream' ) );
115
+ }
116
+ check_admin_referer( 'stream-reports-page', 'wp_stream_reports_nonce' );
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Register Notification menu under Stream's main one
122
+ *
123
+ * @action admin_menu
124
+ * @return void
125
+ */
126
+ public function register_menu() {
127
+ self::$screen_id = add_submenu_page(
128
+ WP_Stream_Admin::RECORDS_PAGE_SLUG,
129
+ __( 'Reports', 'stream' ),
130
+ __( 'Reports', 'stream' ),
131
+ self::VIEW_CAP,
132
+ self::REPORTS_PAGE_SLUG,
133
+ array( $this, 'page' )
134
+ );
135
+
136
+ // Create nonce right away so it is accessible everywhere
137
+ self::$nonce = array( 'wp_stream_reports_nonce' => wp_create_nonce( 'stream-reports-page' ) );
138
+
139
+ $metabox = WP_Stream_Reports_Metaboxes::get_instance();
140
+ add_action( 'load-' . self::$screen_id, array( $metabox, 'load_page' ) );
141
+ }
142
+
143
+ /**
144
+ * Register and enqueue the scripts related to our plugin.
145
+ *
146
+ * @action admin_enqueue_scripts
147
+ * @uses wp_register_script
148
+ * @uses wp_enqueue_script
149
+ *
150
+ * @param $pagename the actual page name
151
+ *
152
+ * @return void
153
+ */
154
+ public function register_ui_assets( $pagename ) {
155
+ // JavaScript registration
156
+ wp_register_script(
157
+ 'stream-reports-d3',
158
+ WP_STREAM_REPORTS_URL . 'ui/lib/d3/d3.min.js',
159
+ array(),
160
+ '3.5.3',
161
+ true
162
+ );
163
+
164
+ wp_register_script(
165
+ 'stream-reports-nvd3',
166
+ WP_STREAM_REPORTS_URL . 'ui/lib/nvd3/nv.d3.min.js',
167
+ array( 'stream-reports-d3' ),
168
+ '1.1.15b',
169
+ true
170
+ );
171
+
172
+ wp_register_script(
173
+ 'stream-reports',
174
+ WP_STREAM_REPORTS_URL . 'ui/js/stream-reports.js',
175
+ array( 'stream-reports-nvd3', 'jquery', 'underscore', 'jquery-ui-datepicker' ),
176
+ WP_STREAM::VERSION,
177
+ true
178
+ );
179
+
180
+ // CSS registration
181
+ wp_register_style(
182
+ 'stream-reports-nvd3',
183
+ WP_STREAM_REPORTS_URL . 'ui/lib/nvd3/nv.d3.min.css',
184
+ array(),
185
+ '1.1.15b',
186
+ 'screen'
187
+ );
188
+
189
+ wp_register_style(
190
+ 'stream-reports',
191
+ WP_STREAM_REPORTS_URL . 'ui/css/stream-reports.css',
192
+ array( 'stream-reports-nvd3', 'wp-stream-datepicker' ),
193
+ WP_STREAM::VERSION,
194
+ 'screen'
195
+ );
196
+
197
+ // If we are not on the right page we return early
198
+ if ( $pagename !== self::$screen_id ) {
199
+ return;
200
+ }
201
+
202
+ // Localization
203
+ wp_localize_script(
204
+ 'stream-reports',
205
+ 'wp_stream_reports',
206
+ array(
207
+ 'i18n' => array(
208
+ 'configure' => __( 'Configure', 'stream' ),
209
+ 'cancel' => __( 'Cancel', 'stream' ),
210
+ 'deletemsg' => __( 'Do you really want to delete this section? This cannot be undone.', 'stream' ),
211
+ ),
212
+ 'gmt_offset' => get_option( 'gmt_offset' ),
213
+ )
214
+ );
215
+
216
+ // Scripts
217
+ wp_enqueue_script( 'stream-reports' );
218
+ wp_enqueue_script( 'select2' );
219
+ wp_enqueue_script( 'common' );
220
+ wp_enqueue_script( 'dashboard' );
221
+ wp_enqueue_script( 'postbox' );
222
+
223
+ // Styles
224
+ wp_enqueue_style( 'stream-reports' );
225
+ wp_enqueue_style( 'select2' );
226
+ }
227
+
228
+ /**
229
+ * Admin page callback function, redirects to each respective method based
230
+ * on $_GET['view']
231
+ *
232
+ * @return void
233
+ */
234
+ public function page() {
235
+ // Page class
236
+ $class = 'metabox-holder columns-' . get_current_screen()->get_columns();
237
+ $add_url = add_query_arg(
238
+ array_merge(
239
+ array(
240
+ 'action' => 'wp_stream_reports_add_metabox',
241
+ ),
242
+ self::$nonce
243
+ ),
244
+ admin_url( 'admin-ajax.php' )
245
+ );
246
+
247
+ $sections = WP_Stream_Reports_Settings::get_user_options( 'sections' );
248
+ $no_reports = empty( $sections );
249
+ $create_url = add_query_arg(
250
+ array_merge(
251
+ array(
252
+ 'action' => 'wp_stream_reports_default_reports',
253
+ ),
254
+ self::$nonce
255
+ ),
256
+ admin_url( 'admin-ajax.php' )
257
+ );
258
+
259
+ $no_reports_message = __( "There's nothing here! Do you want to <a href=\"%s\">create some reports</a>?", 'stream' );
260
+ $no_reports_message = sprintf( $no_reports_message, $create_url );
261
+
262
+ $view = (object) array(
263
+ 'slug' => 'all',
264
+ 'path' => null,
265
+ );
266
+
267
+ // Avoid throwing Notices by testing the variable
268
+ if ( isset( $_GET['view'] ) && ! empty( $_GET['view'] ) ){
269
+ $view->slug = sanitize_file_name( wp_unslash( $_GET['view'] ) );
270
+ }
271
+
272
+ // First we check if the file exists in our plugin folder, otherwhise give the user an error
273
+ if ( ! file_exists( WP_STREAM_REPORTS_VIEW_DIR . $view->slug . '.php' ) ){
274
+ $view->slug = 'error';
275
+ }
276
+
277
+ // Define the path for the view we
278
+ $view->path = WP_STREAM_REPORTS_VIEW_DIR . $view->slug . '.php';
279
+
280
+ // Execute some actions before including the view, to allow others to hook in here
281
+ // Use these to do stuff related to the view you are working with
282
+ do_action( 'wp_stream_reports_view', $view );
283
+ do_action( "wp_stream_reports_view-{$view->slug}", $view );
284
+
285
+ include_once $view->path;
286
+ }
287
+
288
+ /**
289
+ * Remove conflicting JS libraries caused by other plugins loading on pages not belonging to them
290
+ */
291
+ function dequeue_media_conflicts() {
292
+ if ( 'stream_page_' . self::REPORTS_PAGE_SLUG !== get_current_screen()->id ) {
293
+ return;
294
+ }
295
+
296
+ wp_dequeue_script( 'media-upload' );
297
+ wp_enqueue_script( 'media-editor' );
298
+ wp_dequeue_script( 'media-audiovideo' );
299
+ wp_dequeue_script( 'mce-view' );
300
+ wp_dequeue_script( 'image-edit' );
301
+ wp_dequeue_script( 'media-editor' );
302
+ wp_dequeue_script( 'media-audiovideo' );
303
+ }
304
+
305
+ /**
306
+ * Display all messages on admin board
307
+ *
308
+ * @return void
309
+ */
310
+ public static function admin_notices() {
311
+ foreach ( self::$messages as $message ) {
312
+ echo wp_kses_post( $message );
313
+ }
314
+ }
315
+
316
+ /**
317
+ * Return active instance of WP_Stream_Reports, create one if it doesn't exist
318
+ *
319
+ * @return WP_Stream_Reports
320
+ */
321
+ public static function get_instance() {
322
+ if ( empty( self::$instance ) ) {
323
+ $class = __CLASS__;
324
+ self::$instance = new $class;
325
+ }
326
+ return self::$instance;
327
+ }
328
+
329
+ }
330
+
331
+ $GLOBALS['wp_stream_reports'] = WP_Stream_Reports::get_instance();
extensions/reports/includes/class-wp-stream-reports-charts.php ADDED
@@ -0,0 +1,292 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class WP_Stream_Reports_Charts {
4
+
5
+ public function __construct() {
6
+
7
+ // Load records
8
+ add_filter( 'wp_stream_reports_load_records', array( $this, 'sort_coordinates_by_count' ), 10, 2 );
9
+ add_filter( 'wp_stream_reports_load_records', array( $this, 'limit_coordinates' ), 10, 2 );
10
+
11
+ // Make charts
12
+ add_filter( 'wp_stream_reports_make_chart', array( $this, 'pie_chart_coordinates' ), 10, 2 );
13
+ add_filter( 'wp_stream_reports_make_chart', array( $this, 'bar_chart_coordinates' ), 10, 2 );
14
+ add_filter( 'wp_stream_reports_make_chart', array( $this, 'line_chart_coordinates' ), 10, 2 );
15
+
16
+ // Chart finalization
17
+ add_filter( 'wp_stream_reports_finalize_chart', array( $this, 'apply_chart_settings' ), 10, 2 );
18
+
19
+ }
20
+
21
+ public function get_chart_options( $args, $records ) {
22
+
23
+ $coordinates = apply_filters( 'wp_stream_reports_make_chart', $records, $args );
24
+ $values = apply_filters( 'wp_stream_reports_finalize_chart', $coordinates, $args );
25
+
26
+ $show_controls = count( $values ) > 1;
27
+
28
+ return array(
29
+ 'type' => $args['chart_type'],
30
+ 'guidelines' => true,
31
+ 'tooltip' => array(
32
+ 'show' => true,
33
+ ),
34
+ 'values' => $values,
35
+ 'controls' => $show_controls,
36
+ 'stacked' => (bool) $args['group'],
37
+ 'grouped' => false,
38
+ );
39
+ }
40
+
41
+ /**
42
+ * Sorts each set of data by the number of records in them
43
+ */
44
+ public function sort_coordinates_by_count( $records ) {
45
+ $counts = array();
46
+ foreach ( $records as $field => $data ){
47
+
48
+ $count = count( $data );
49
+ if ( ! array_key_exists( $count, $counts ) ) {
50
+ $counts[ $count ] = array();
51
+ }
52
+
53
+ $counts[ $count ][] = array(
54
+ 'key' => $field,
55
+ 'data' => $data,
56
+ );
57
+ }
58
+
59
+ krsort( $counts );
60
+
61
+ $output = array();
62
+ foreach ( $counts as $count => $element ) {
63
+
64
+ foreach ( $element as $element_data ) {
65
+ $output[ $element_data['key'] ] = $element_data['data'];
66
+ }
67
+ }
68
+
69
+ return $output;
70
+ }
71
+
72
+ /**
73
+ * Merges all records past limit into single record
74
+ */
75
+ public function limit_coordinates( $records, $args ) {
76
+ $limit = apply_filters( 'wp_stream_reports_record_limit', 10 );
77
+ if ( 0 === $limit ) {
78
+ return $records;
79
+ }
80
+
81
+ $top_elements = array_slice( $records, 0, $limit, true );
82
+ $leftover_elements = array_slice( $records, $limit );
83
+
84
+ if ( ! $leftover_elements ) {
85
+ return $top_elements;
86
+ }
87
+
88
+ $other_element = array();
89
+ foreach ( $leftover_elements as $data ) {
90
+ $other_element = array_merge( $other_element, $data );
91
+ }
92
+
93
+ $top_elements['report-others'] = $other_element;
94
+
95
+ return $top_elements;
96
+ }
97
+
98
+ public function line_chart_coordinates( $records, $args ) {
99
+ if ( 'line' !== $args['chart_type'] ) {
100
+ return $records;
101
+ }
102
+
103
+ $sorted = array();
104
+
105
+ // Get date count for each sort
106
+ foreach ( $records as $type => $items ) {
107
+ $sorted[ $type ] = $this->count_by_field( 'created', $items, array( $this, 'collapse_dates' ) );
108
+ }
109
+
110
+ $sorted = $this->pad_fields( $sorted );
111
+
112
+ foreach ( $sorted as $type => &$items ) {
113
+ ksort( $items );
114
+ }
115
+
116
+ $coordinates = array();
117
+
118
+ foreach ( $sorted as $line_name => $points ) {
119
+ $line_data = array(
120
+ 'key' => $line_name,
121
+ 'values' => array(),
122
+ );
123
+
124
+ foreach ( $points as $x => $y ) {
125
+ $line_data['values'][] = array(
126
+ 'x' => $x,
127
+ 'y' => $y,
128
+ );
129
+ }
130
+
131
+ $coordinates[] = $line_data;
132
+ }
133
+
134
+ return $coordinates;
135
+ }
136
+
137
+ public function pie_chart_coordinates( $records, $args ) {
138
+ if ( 'pie' !== $args['chart_type'] ) {
139
+ return $records;
140
+ }
141
+
142
+ $counts = array();
143
+
144
+ foreach ( $records as $type => $items ) {
145
+ $counts[] = array(
146
+ 'key' => $type,
147
+ 'value' => count( $items ),
148
+ );
149
+ }
150
+
151
+ return $counts;
152
+ }
153
+
154
+ public function bar_chart_coordinates( $records, $args ) {
155
+ if ( 'multibar' !== $args['chart_type'] ) {
156
+ return $records;
157
+ }
158
+
159
+ $sorted = array();
160
+
161
+ // Get date count for each sort
162
+ foreach ( $records as $type => $items ) {
163
+ $sorted[ $type ] = $this->count_by_field( 'created', $items, array( $this, 'collapse_dates' ) );
164
+ }
165
+
166
+ $sorted = $this->pad_fields( $sorted );
167
+
168
+ foreach ( $sorted as $type => &$items ) {
169
+ ksort( $items );
170
+ }
171
+
172
+ $coordinates = array();
173
+
174
+ foreach ( $sorted as $line_name => $points ) {
175
+ $line_data = array(
176
+ 'key' => $line_name,
177
+ 'values' => array(),
178
+ );
179
+
180
+ foreach ( $points as $x => $y ) {
181
+ $line_data['values'][] = array(
182
+ 'x' => $x,
183
+ 'y' => $y,
184
+ );
185
+ }
186
+
187
+ $coordinates[] = $line_data;
188
+ }
189
+
190
+ return $coordinates;
191
+ }
192
+
193
+ /**
194
+ * Counts the number of objects with similar field properties in an array
195
+ * @return array
196
+ */
197
+ public function count_by_field( $field, $records, $callback = '' ) {
198
+ $sorted = $this->group_by_field( $field, $records, $callback );
199
+ $counts = array();
200
+
201
+ foreach ( array_keys( $sorted ) as $key ) {
202
+ $counts[ $key ] = count( $sorted[ $key ] );
203
+ }
204
+
205
+ return $counts;
206
+ }
207
+
208
+ /**
209
+ * Groups objects with similar field properties into arrays
210
+ * @return array
211
+ */
212
+ public function group_by_field( $field, $records, $callback = '' ) {
213
+ $sorted = array();
214
+
215
+ foreach ( $records as $record ) {
216
+ $key = $record->$field;
217
+
218
+ if ( is_callable( $callback ) ) {
219
+ $key = call_user_func( $callback, $key );
220
+ }
221
+
222
+ if ( array_key_exists( $key, $sorted ) && is_array( $sorted[ $key ] ) ) {
223
+ $sorted[ $key ][] = $record;
224
+ } else {
225
+ $sorted[ $key ] = array( $record );
226
+ }
227
+ }
228
+
229
+ return $sorted;
230
+ }
231
+
232
+ /**
233
+ * Offsets the record created date by the timezone
234
+ * @return array
235
+ */
236
+ public function offset_record_dates( $records ) {
237
+ $offset = get_option( 'gmt_offset' );
238
+ foreach ( $records as $record => $items ) {
239
+ foreach ( $items as $key => $item ) {
240
+ $records[ $record ][ $key ]->created = wp_stream_get_iso_8601_extended_date( strtotime( $item->created ), $offset );
241
+ }
242
+ }
243
+ return $records;
244
+ }
245
+
246
+ /**
247
+ * Adds blank fields for all keys present in any array
248
+ * @return array
249
+ */
250
+ public function pad_fields( $records ) {
251
+ $keys = array();
252
+
253
+ foreach ( $records as $dataset ) {
254
+ $keys = array_unique( array_merge( $keys, array_keys( $dataset ) ) );
255
+ }
256
+
257
+ $new_records = array();
258
+
259
+ foreach ( $keys as $key ) {
260
+ foreach ( $records as $data_key => $dataset ) {
261
+ if ( ! array_key_exists( $data_key, $new_records ) ) {
262
+ $new_records[ $data_key ] = array();
263
+ }
264
+
265
+ $new_records[ $data_key ][ $key ] = isset( $records[ $data_key ][ $key ] ) ? $records[ $data_key ][ $key ] : 0;
266
+ }
267
+ }
268
+
269
+ return $new_records;
270
+ }
271
+
272
+ /**
273
+ * Used to group data points by day
274
+ */
275
+ protected function collapse_dates( $date ) {
276
+ return strtotime( date( 'Y-m-d', strtotime( $date ) ) );
277
+ }
278
+
279
+ /**
280
+ * Disable coordinate plots that have been disabled by the user
281
+ */
282
+ public function apply_chart_settings( $coordinates, $args ) {
283
+ foreach ( $coordinates as $key => $dataset ) {
284
+ if ( in_array( $key, $args['disabled'] ) ) {
285
+ $coordinates[ $key ]['disabled'] = true;
286
+ }
287
+ }
288
+
289
+ return $coordinates;
290
+ }
291
+
292
+ }
extensions/reports/includes/class-wp-stream-reports-date-interval.php ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class WP_Stream_Reports_Date_Interval extends WP_Stream_Date_Interval {
4
+
5
+ /**
6
+ * Hold WP_Stream_Reports_Date_Interval instance
7
+ *
8
+ * @var string
9
+ */
10
+ public static $instance;
11
+
12
+ /*
13
+ * Handle parent constructor
14
+ */
15
+ public function __construct() {
16
+ // Call parent constructor
17
+ parent::__construct();
18
+
19
+ // Ajax declaration to save time interval
20
+ $ajax_hooks = array(
21
+ 'wp_stream_reports_save_interval' => 'save_interval',
22
+ );
23
+
24
+ // Register all ajax action and check referer for this class
25
+ WP_Stream_Reports::handle_ajax_request( $ajax_hooks, $this );
26
+ }
27
+
28
+ /**
29
+ * Handle ajax saving of time intervals
30
+ */
31
+ public function save_interval() {
32
+ $interval = array(
33
+ 'key' => wp_stream_filter_input( INPUT_GET, 'key', FILTER_SANITIZE_STRING, array( 'default' => '' ) ),
34
+ 'start' => wp_stream_filter_input( INPUT_GET, 'start', FILTER_SANITIZE_STRING, array( 'default' => '' ) ),
35
+ 'end' => wp_stream_filter_input( INPUT_GET, 'end', FILTER_SANITIZE_STRING, array( 'default' => '' ) ),
36
+ );
37
+
38
+ // Get predefined interval for validation
39
+ $avail_intervals = $this->get_predefined_intervals();
40
+
41
+ if ( '' !== $interval['key'] && 'custom' !== $interval['key'] && ! isset( $avail_intervals[ $interval['key'] ] ) ) {
42
+ wp_die( esc_html__( 'That time interval is not available.', 'stream' ) );
43
+ }
44
+
45
+ // Only store dates if we are dealing with custom dates and no relative preset
46
+ if ( 'custom' !== $interval['key'] ) {
47
+ $interval['start'] = '';
48
+ $interval['end'] = '';
49
+ }
50
+
51
+ WP_Stream_Reports_Settings::update_user_option_and_redirect( 'interval', $interval );
52
+ }
53
+
54
+ /**
55
+ * Return active instance of WP_Stream_Reports, create one if it doesn't exist
56
+ *
57
+ * @return WP_Stream_Reports
58
+ */
59
+ public static function get_instance() {
60
+ if ( empty( self::$instance ) ) {
61
+ $class = __CLASS__;
62
+ self::$instance = new $class;
63
+ }
64
+ return self::$instance;
65
+ }
66
+
67
+ }
extensions/reports/includes/class-wp-stream-reports-meta-boxes.php ADDED
@@ -0,0 +1,895 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * Section class for Stream Reports
4
+ *
5
+ * @author X-Team <x-team.com>
6
+ * @author Jonathan Bardo <jonathan.bardo@x-team.com>
7
+ */
8
+ class WP_Stream_Reports_Metaboxes {
9
+
10
+ /**
11
+ * Hold Stream Reports Section instance
12
+ *
13
+ * @var string
14
+ */
15
+ public static $instance;
16
+
17
+ /**
18
+ * Hold all the available sections on the page.
19
+ *
20
+ * @var array
21
+ */
22
+ public static $sections;
23
+
24
+ /**
25
+ * Holds the meta box id prefix
26
+ */
27
+ const META_PREFIX = 'wp-stream-reports-';
28
+
29
+ /**
30
+ * Public constructor
31
+ */
32
+ public function __construct() {
33
+ // Get all sections from the database
34
+ self::$sections = WP_Stream_Reports_Settings::get_user_options( 'sections' );
35
+ $this->charts = new WP_Stream_Reports_Charts();
36
+
37
+ if ( isset( self::$sections[0] ) && isset( self::$sections[0]['data_type'] ) ) {
38
+ $this->migrate_settings();
39
+ }
40
+
41
+ // Finalize charts
42
+ add_filter( 'wp_stream_reports_finalize_chart', array( $this, 'translate_labels' ), 10, 2 );
43
+
44
+ // Get chart labels
45
+ add_filter( 'wp_stream_reports_get_label', array( $this, 'translate_data_type_labels' ), 10, 2 );
46
+
47
+ $ajax_hooks = array(
48
+ 'wp_stream_reports_add_metabox' => 'add_metabox',
49
+ 'wp_stream_reports_delete_metabox' => 'delete_metabox',
50
+ 'wp_stream_reports_default_reports' => 'setup_user',
51
+ 'wp_stream_reports_save_metabox_config' => 'save_metabox_config',
52
+ 'wp_stream_reports_save_chart_height' => 'save_chart_height',
53
+ 'wp_stream_reports_save_chart_options' => 'save_chart_options',
54
+ 'wp_stream_reports_update_metabox_display' => 'update_metabox_display',
55
+ );
56
+
57
+ // Register all ajax action and check referer for this class
58
+ WP_Stream_Reports::handle_ajax_request( $ajax_hooks, $this );
59
+ }
60
+
61
+ /**
62
+ * Runs on a user's first visit to setup sample data
63
+ */
64
+ public function setup_user() {
65
+ $sections = array(
66
+ array(
67
+ 'id' => 0,
68
+ 'title' => __( 'All Activity by Author', 'stream' ),
69
+ 'chart_type' => 'line',
70
+ 'selector_id' => 'author',
71
+ ),
72
+ array(
73
+ 'id' => 1,
74
+ 'title' => __( 'All Activity by Action', 'stream' ),
75
+ 'chart_type' => 'line',
76
+ 'selector_id' => 'action',
77
+ ),
78
+ array(
79
+ 'id' => 2,
80
+ 'title' => __( 'All Activity by Author Role', 'stream' ),
81
+ 'chart_type' => 'multibar',
82
+ 'selector_id' => 'author_role',
83
+ 'group' => true,
84
+ ),
85
+ array(
86
+ 'id' => 3,
87
+ 'title' => __( 'Comments Activity by Action', 'stream' ),
88
+ 'chart_type' => 'pie',
89
+ 'connector_id' => 'comments',
90
+ 'context_id' => 'comments',
91
+ 'selector_id' => 'action',
92
+ ),
93
+ );
94
+
95
+ WP_Stream_Reports_Settings::update_user_option( 'sections', $sections );
96
+
97
+ $order = array(
98
+ 'normal' => sprintf( '%1$s0,%1$s2', self::META_PREFIX ),
99
+ 'side' => sprintf( '%1$s1,%1$s3', self::META_PREFIX ),
100
+ );
101
+
102
+ update_user_option( get_current_user_id(), 'meta-box-order_stream_page_' . WP_Stream_Reports::REPORTS_PAGE_SLUG, $order, true );
103
+
104
+ $interval = array(
105
+ 'key' => 'last-30-days',
106
+ 'start' => '',
107
+ 'end' => '',
108
+ );
109
+
110
+ WP_Stream_Reports_Settings::update_user_option_and_redirect( 'interval', $interval );
111
+ }
112
+
113
+ public static function in_admin_header() {
114
+
115
+ ?>
116
+ <div class="stream-example">
117
+ <div class="stream-example-modal">
118
+ <h1><i class="dashicons dashicons-chart-area"></i> <?php _e( 'Stream Reports', 'stream' ) ?></h1>
119
+ <p><?php _e( 'Generate stunning visuals of logged-in user activity and share them with stakeholders or your clients.', 'stream' ) ?></p>
120
+ <ul>
121
+ <li><i class="dashicons dashicons-yes"></i> <?php _e( 'Fully-interactive charts', 'stream' ) ?></li>
122
+ <li><i class="dashicons dashicons-yes"></i> <?php _e( 'Monitor team contributions', 'stream' ) ?></li>
123
+ <li><i class="dashicons dashicons-yes"></i> <?php _e( 'Responsive for any screen size', 'stream' ) ?></li>
124
+ </ul>
125
+ <a href="<?php echo esc_url( WP_Stream_Admin::account_url( sprintf( 'upgrade?site_uuid=%s', WP_Stream::$api->site_uuid ) ) ); ?>" class="button button-primary button-large"><?php _e( 'Upgrade to Pro', 'stream' ) ?></a>
126
+ </div>
127
+ </div>
128
+ <?php
129
+ }
130
+
131
+ public function load_page() {
132
+ if ( WP_Stream_API::is_restricted() ) {
133
+ add_action( 'in_admin_header', array( __CLASS__, 'in_admin_header' ) );
134
+ return;
135
+ }
136
+
137
+ if ( is_admin() && WP_Stream_Reports_Settings::is_first_visit() ) {
138
+ $this->setup_user();
139
+ }
140
+
141
+ $this->existing_records = $this->get_existing_records();
142
+
143
+ // Add screen option for chart height
144
+ add_filter( 'screen_settings', array( $this, 'chart_height_display' ), 10, 2 );
145
+
146
+ // Enqueue all core scripts required for this page to work
147
+ add_screen_option( 'layout_columns', array( 'max' => 2, 'default' => 2 ) );
148
+
149
+ // Add all metaboxes
150
+ foreach ( self::$sections as $key => $section ) {
151
+ $delete_url = add_query_arg(
152
+ array_merge(
153
+ array(
154
+ 'action' => 'wp_stream_reports_delete_metabox',
155
+ 'key' => $key,
156
+ ),
157
+ WP_Stream_Reports::$nonce
158
+ ),
159
+ admin_url( 'admin-ajax.php' )
160
+ );
161
+
162
+ // Configure button
163
+ $configure = sprintf(
164
+ '<span class="postbox-title-action">
165
+ <a href="javascript:void(0);" class="edit-box open-box">%3$s</a>
166
+ </span>
167
+ <span class="postbox-title-action postbox-delete-action">
168
+ <a href="%1$s">
169
+ %2$s
170
+ </a>
171
+ </span>',
172
+ esc_url( $delete_url ),
173
+ esc_html__( 'Delete', 'stream' ),
174
+ esc_html__( 'Configure', 'stream' )
175
+ );
176
+
177
+ // Parse default argument
178
+ $section = $this->parse_section( $section );
179
+
180
+ // Set the key for template use
181
+ $section['key'] = $key;
182
+ $section['generated_title'] = $this->get_generated_title( $section );
183
+
184
+ // Generate the title automatically if not already set
185
+ $title = empty( $section['title'] ) ? $section['generated_title'] : $section['title'];
186
+
187
+ // Add the actual metabox
188
+ add_meta_box(
189
+ self::META_PREFIX . $key,
190
+ sprintf( '<span class="title">%s</span>%s', esc_html( $title ), $configure ), // xss ok
191
+ array( $this, 'metabox_content' ),
192
+ WP_Stream_Reports::$screen_id,
193
+ $section['context'],
194
+ $section['priority'],
195
+ $section
196
+ );
197
+ }
198
+ }
199
+
200
+ /**
201
+ * Parses the section arguments and provides defaults
202
+ */
203
+ protected function parse_section( $section ) {
204
+ $default = array(
205
+ 'title' => '',
206
+ 'priority' => 'default',
207
+ 'context' => 'normal',
208
+ 'chart_type' => 'line',
209
+ 'connector_id' => '',
210
+ 'context_id' => '',
211
+ 'action_id' => '',
212
+ 'selector_id' => '',
213
+ 'is_new' => false,
214
+ 'disabled' => array(),
215
+ 'group' => false,
216
+ );
217
+
218
+ return wp_parse_args( $section, $default );
219
+ }
220
+
221
+ /**
222
+ * This is the content of the metabox
223
+ *
224
+ * @param $object
225
+ * @param $section
226
+ */
227
+ public function metabox_content( $object, $section ) {
228
+ $args = $section['args'];
229
+
230
+ // Assigning template vars
231
+ $key = $section['args']['key'];
232
+
233
+ $chart_types = $this->get_chart_types();
234
+
235
+ if ( array_key_exists( $args['chart_type'], $chart_types ) ) {
236
+ $chart_types[ $args['chart_type'] ] .= ' active';
237
+ } else {
238
+ $args['chart_type'] = 'line';
239
+ }
240
+
241
+ $configure_class = '';
242
+ if ( $args['is_new'] ) {
243
+ $configure_class = 'stream-reports-expand';
244
+ unset( self::$sections[ $key ]['is_new'] );
245
+ WP_Stream_Reports_Settings::update_user_option( 'sections', self::$sections );
246
+ }
247
+
248
+ $chart_height = WP_Stream_Reports_Settings::get_user_options( 'chart_height' , 300 );
249
+ $data_types = $this->get_contexts();
250
+ $action_types = $this->get_actions();
251
+ $selector_types = $this->get_selector_types();
252
+
253
+ include WP_STREAM_REPORTS_VIEW_DIR . 'meta-box.php';
254
+ }
255
+
256
+ public function translate_labels( $coordinates, $args ) {
257
+ foreach ( $coordinates as $key => $dataset ) {
258
+ $coordinates[ $key ]['key'] = $this->get_label( $dataset['key'], $args['selector_id'] );
259
+ }
260
+
261
+ return $coordinates;
262
+ }
263
+
264
+ protected function get_label( $value, $grouping ) {
265
+ if ( 'report-others' === $value ) {
266
+ return __( 'All Others', 'stream' );
267
+ }
268
+
269
+ switch ( $grouping ) {
270
+ case 'action':
271
+ $output = isset( WP_Stream_Connectors::$term_labels['stream_action'][ $value ] ) ? WP_Stream_Connectors::$term_labels['stream_action'][ $value ] : $value;
272
+ break;
273
+ case 'author':
274
+ if ( $value ) {
275
+ $user_info = get_userdata( $value );
276
+ $output = isset( $user_info->display_name ) ? $user_info->display_name : sprintf( __( 'User ID: %d', 'stream' ), $value );
277
+ } else {
278
+ $output = __( 'N/A', 'stream' );
279
+ }
280
+ break;
281
+ case 'author_role':
282
+ $output = ucfirst( $value );
283
+ break;
284
+ case 'connector':
285
+ $output = isset( WP_Stream_Connectors::$term_labels['stream_connector'][ $value ] ) ? WP_Stream_Connectors::$term_labels['stream_connector'][ $value ] : $value;
286
+ break;
287
+ case 'context':
288
+ $output = isset( WP_Stream_Connectors::$term_labels['stream_context'][ $value ] ) ? WP_Stream_Connectors::$term_labels['stream_context'][ $value ] : $value;
289
+ break;
290
+ default:
291
+ // Allow plugins to translate the label
292
+ $output = apply_filters( 'wp_stream_reports_get_label', $value, $grouping );
293
+ break;
294
+ }
295
+ return $output;
296
+ }
297
+
298
+ public function translate_data_type_labels( $value, $grouping ) {
299
+ return $this->get_data_types( $value ) ? $this->get_data_types( $value ) : $value;
300
+ }
301
+
302
+ /**
303
+ * Returns data type labels, or a single data type's label'
304
+ * @return string
305
+ */
306
+ protected function get_data_types( $key = '' ) {
307
+ $labels = array(
308
+ 'all' => __( 'All Activity', 'stream' ),
309
+ 'connector' => array(
310
+ 'title' => __( 'Connector Activity', 'stream' ),
311
+ 'group' => 'connector',
312
+ 'options' => WP_Stream_Connectors::$term_labels['stream_connector'],
313
+ 'disable' => array(
314
+ 'connector',
315
+ ),
316
+ ),
317
+ 'context' => array(
318
+ 'title' => __( 'Context Activity', 'stream' ),
319
+ 'group' => 'context',
320
+ 'options' => WP_Stream_Connectors::$term_labels['stream_context'],
321
+ 'disable' => array(
322
+ 'context'
323
+ ),
324
+ ),
325
+ 'action' => array(
326
+ 'title' => __( 'Actions Activity', 'stream' ),
327
+ 'group' => 'action',
328
+ 'options' => WP_Stream_Connectors::$term_labels['stream_action'],
329
+ 'disable' => array(
330
+ 'action'
331
+ ),
332
+ ),
333
+ );
334
+
335
+ $labels = apply_filters( 'wp_stream_reports_data_types', $labels );
336
+
337
+ if ( '' === $key ) {
338
+ $output = $labels;
339
+ } elseif ( array_key_exists( $key, $labels ) ) {
340
+ $output = $labels[ $key ];
341
+ } else {
342
+ $output = false;
343
+ }
344
+
345
+ return $output;
346
+ }
347
+
348
+ /**
349
+ * Returns chart types available
350
+ * @return string
351
+ */
352
+ protected function get_chart_types() {
353
+ return array(
354
+ 'line' => 'dashicons-chart-area',
355
+ 'pie' => 'dashicons-chart-pie',
356
+ 'multibar' => 'dashicons-chart-bar',
357
+ );
358
+ }
359
+
360
+ /**
361
+ * Returns selector type labels, or a single selector type's label'
362
+ * @return string
363
+ */
364
+ protected function get_selector_types( $key = '' ) {
365
+ $labels = array(
366
+ 'action' => __( 'Action', 'stream' ),
367
+ 'author' => __( 'Author', 'stream' ),
368
+ 'author_role' => __( 'Author Role', 'stream' ),
369
+ 'connector' => __( 'Connector', 'stream' ),
370
+ 'context' => __( 'Context', 'stream' ),
371
+ 'ip' => __( 'IP Address', 'stream' ),
372
+ );
373
+
374
+ $labels = apply_filters( 'wp_stream_reports_selector_types', $labels );
375
+
376
+ if ( empty( $key ) ) {
377
+ $output = $labels;
378
+ } elseif ( array_key_exists( $key, $labels ) ) {
379
+ $output = $labels[ $key ];
380
+ } else {
381
+ $output = false;
382
+ }
383
+
384
+ return $output;
385
+ }
386
+
387
+ public function load_metabox_records( $args ) {
388
+ $date_interval = $this->get_date_interval();
389
+
390
+ $query_args = array(
391
+ 'records_per_page' => -1,
392
+ 'date_from' => $date_interval['start'],
393
+ 'date_to' => $date_interval['end'],
394
+ );
395
+
396
+ $available_args = array(
397
+ 'action' => 'action_id',
398
+ 'connector' => 'connector_id',
399
+ 'context' => 'context_id',
400
+ );
401
+ foreach ( $available_args as $query_key => $args_key ) {
402
+ if ( isset( $args[ $args_key ] ) ) {
403
+ $query_args[ $query_key ] = $args[ $args_key ];
404
+ }
405
+ }
406
+
407
+ $selector = $args['selector_id'];
408
+ $available_selectors = array( 'author', 'author_role', 'action', 'context', 'connector', 'ip' );
409
+
410
+ if ( ! in_array( $selector, $available_selectors ) ) {
411
+ return array();
412
+ }
413
+
414
+ $query_args = apply_filters( 'wp_stream_reports_query_args', $query_args, $args );
415
+ $records = wp_stream_query( $query_args );
416
+
417
+ if ( 'author_role' === $selector ) {
418
+ foreach ( $records as $key => $record ) {
419
+ $user = get_userdata( $record->author );
420
+ if ( $user ) {
421
+ $record->author_role = join( ',', $user->roles );
422
+ } else if ( 0 === $record->author ) {
423
+ $record->author_role = __( 'N/A', 'stream' );
424
+ } else {
425
+ $record->author_role = __( 'Unknown', 'stream' );
426
+ }
427
+ }
428
+ }
429
+
430
+ $records = $this->charts->group_by_field( $selector, $records );
431
+ $records = $this->charts->offset_record_dates( $records );
432
+
433
+ return apply_filters( 'wp_stream_reports_load_records', $records, $args );
434
+ }
435
+
436
+ protected function get_date_interval(){
437
+ $date = new WP_Stream_Date_Interval();
438
+ $default_interval = array(
439
+ 'key' => 'all-time',
440
+ 'start' => '',
441
+ 'end' => '',
442
+ );
443
+
444
+ $user_interval = WP_Stream_Reports_Settings::get_user_options( 'interval', $default_interval );
445
+ $user_interval_key = $user_interval['key'];
446
+ $available_intervals = $date->get_predefined_intervals();
447
+
448
+ if ( array_key_exists( $user_interval_key, $available_intervals ) ) {
449
+ $interval = $available_intervals[ $user_interval_key ];
450
+ $user_interval['start'] = isset( $interval['start'] ) ? $interval['start']->toDateString() : null;
451
+ $user_interval['end'] = isset( $interval['end'] ) ? $interval['end']->toDateString() : null;
452
+ }
453
+
454
+ return $user_interval;
455
+ }
456
+
457
+ /**
458
+ * Creates a title generated from the arguments for the chart
459
+ */
460
+ protected function get_generated_title( $args ) {
461
+ if ( empty( $args['selector_id'] ) ) {
462
+ return sprintf( esc_html__( 'Report %d', 'stream' ), absint( $args['key'] + 1 ) );
463
+ }
464
+
465
+ if ( ! empty( $args['context_id'] ) ) {
466
+ $dataset = $this->get_label( $args['context_id'], 'context' );
467
+ } else if ( ! empty( $args['connector_id'] ) ) {
468
+ $dataset = $this->get_label( $args['connector_id'], 'connector' );
469
+ } else {
470
+ $dataset = '';
471
+ }
472
+
473
+ $action = ( ! empty( $args['action_id'] ) ) ? $this->get_label( $args['action_id'], 'action' ) : null;
474
+ $selector = ( ! empty( $args['selector_id'] ) ) ? $this->get_selector_types( $args['selector_id'] ) : null;
475
+
476
+ if ( ! empty( $action ) ) {
477
+ if ( ! empty( $dataset ) ) {
478
+ $string = _x(
479
+ '%1$s in %2$s by %3$s',
480
+ '1: Action 2: Dataset 3: Selector',
481
+ 'stream'
482
+ );
483
+ } else {
484
+ $string = _x(
485
+ 'All %1$s by %3$s',
486
+ '1: Action 3: Selector',
487
+ 'stream'
488
+ );
489
+ }
490
+ } else if ( ! empty( $dataset ) ) {
491
+ $string = _x(
492
+ 'All Activity in %2$s by %3$s',
493
+ '2: Dataset 3: Selector',
494
+ 'stream'
495
+ );
496
+ } else {
497
+ $string = _x(
498
+ 'All Activity by %3$s',
499
+ '3: Selector',
500
+ 'stream'
501
+ );
502
+ }
503
+
504
+ $title = sprintf( $string, $action, $dataset, $selector );
505
+ return $title;
506
+ }
507
+
508
+ /**
509
+ * Update configuration array from ajax call and save this to the user option
510
+ */
511
+ public function save_metabox_config() {
512
+ $id = wp_stream_filter_input( INPUT_GET, 'section_id', FILTER_SANITIZE_NUMBER_INT );
513
+
514
+ $input = array(
515
+ 'id' => wp_stream_filter_input( INPUT_GET, 'section_id', FILTER_SANITIZE_NUMBER_INT ),
516
+ 'title' => wp_stream_filter_input( INPUT_GET, 'title', FILTER_SANITIZE_STRING ),
517
+ 'chart_type' => wp_stream_filter_input( INPUT_GET, 'chart_type', FILTER_SANITIZE_STRING ),
518
+ 'connector_id' => wp_stream_filter_input( INPUT_GET, 'data_connector', FILTER_SANITIZE_STRING ),
519
+ 'context_id' => wp_stream_filter_input( INPUT_GET, 'data_context', FILTER_SANITIZE_STRING ),
520
+ 'action_id' => wp_stream_filter_input( INPUT_GET, 'data_action', FILTER_SANITIZE_STRING ),
521
+ 'selector_id' => wp_stream_filter_input( INPUT_GET, 'data_selector', FILTER_SANITIZE_STRING ),
522
+ );
523
+
524
+ $required_fields = array( 'id', 'title', 'chart_type', 'selector_id' );
525
+ foreach ( $required_fields as $key ){
526
+ if ( $input[ $key ] === null ) {
527
+ wp_send_json_error( array( 'missing' => $key, 'value' => $input[ $key ] ) );
528
+ }
529
+ }
530
+
531
+ // Store the chart configuration
532
+ self::$sections[ $id ] = $input;
533
+
534
+ // Update the database option
535
+ WP_Stream_Reports_Settings::ajax_update_user_option( 'sections', self::$sections );
536
+ }
537
+
538
+ /**
539
+ * Instantly update chart based on user configuration
540
+ */
541
+ public function update_metabox_display() {
542
+ $section_id = wp_stream_filter_input( INPUT_GET, 'section_id', FILTER_SANITIZE_NUMBER_INT );
543
+ $section = $this->get_section( $section_id );
544
+
545
+ $args = $this->parse_section( $section );
546
+
547
+ $chart_types = $this->get_chart_types();
548
+
549
+ if ( ! array_key_exists( $args['chart_type'], $chart_types ) ) {
550
+ $args['chart_type'] = 'line';
551
+ }
552
+
553
+ $records = $this->load_metabox_records( $args );
554
+ $chart_options = $this->charts->get_chart_options( $args, $records );
555
+
556
+ wp_send_json_success(
557
+ array(
558
+ 'options' => $chart_options,
559
+ 'title' => $section['title'],
560
+ 'generated_title' => $this->get_generated_title( $args ),
561
+ )
562
+ );
563
+ }
564
+
565
+ /**
566
+ * This function will handle the ajax request to add a metabox to the page.
567
+ */
568
+ public function add_metabox() {
569
+ // Add a new section
570
+ self::$sections[] = array(
571
+ 'is_new' => true,
572
+ );
573
+
574
+ // Push new metabox to top of the display
575
+ $new_section_id = 'wp-stream-reports-' . ( count( self::$sections ) - 1 );
576
+ $order = get_user_option( 'meta-box-order_stream_page_' . WP_Stream_Reports::REPORTS_PAGE_SLUG );
577
+ $normal_order = explode( ',', $order['normal'] );
578
+
579
+ array_unshift( $normal_order, $new_section_id );
580
+ $order['normal'] = join( ',', $normal_order );
581
+ update_user_option( get_current_user_id(), 'meta-box-order_stream_page_' . WP_Stream_Reports::REPORTS_PAGE_SLUG, $order, true );
582
+
583
+ WP_Stream_Reports_Settings::update_user_option_and_redirect( 'sections', self::$sections );
584
+ }
585
+
586
+ /**
587
+ * This function will remove the metabox from the current view.
588
+ */
589
+ public function delete_metabox() {
590
+ $meta_key = wp_stream_filter_input( INPUT_GET, 'key', FILTER_SANITIZE_NUMBER_INT );
591
+
592
+ // Unset the metabox from the array.
593
+ unset( self::$sections[ $meta_key ] );
594
+
595
+ // If there is no more section. We delete the user option.
596
+ if ( empty( self::$sections ) ) {
597
+ delete_user_option(
598
+ get_current_user_id(),
599
+ 'meta-box-order_stream_page_' . WP_Stream_Reports::REPORTS_PAGE_SLUG,
600
+ true
601
+ );
602
+ } else {
603
+ // Delete the metabox from the page ordering as well
604
+ // There might be a better way on handling this I'm sure (stream_page should not be hardcoded)
605
+ $user_options = get_user_option( 'meta-box-order_stream_page_' . WP_Stream_Reports::REPORTS_PAGE_SLUG );
606
+ }
607
+
608
+ if ( ! empty( $user_options ) ) {
609
+ // Remove the one we are deleting from the list
610
+ foreach ( $user_options as $key => &$string ) {
611
+ $order = explode( ',', $string );
612
+ if ( false !== ( $key = array_search( self::META_PREFIX . $meta_key, $order ) ) ) {
613
+ unset( $order[ $key ] );
614
+ $string = implode( ',', $order );
615
+ }
616
+ }
617
+
618
+ // Save the ordering again
619
+ update_user_option(
620
+ get_current_user_id(),
621
+ 'meta-box-order_stream_page_' . WP_Stream_Reports::REPORTS_PAGE_SLUG,
622
+ $user_options,
623
+ true
624
+ );
625
+ }
626
+
627
+ WP_Stream_Reports_Settings::update_user_option_and_redirect( 'sections', self::$sections );
628
+ }
629
+
630
+ public function save_chart_options(){
631
+ $section_id = wp_stream_filter_input( INPUT_GET, 'section_id', FILTER_SANITIZE_NUMBER_INT );
632
+ $section = $this->get_section( $section_id );
633
+ $type = wp_stream_filter_input( INPUT_GET, 'update_type', FILTER_SANITIZE_STRING );
634
+
635
+ if ( 'disable' === $type ) {
636
+ if ( ! isset( $_GET['update_payload'] ) || ! is_array( $_GET['update_payload'] ) ) {
637
+ wp_send_json_error();
638
+ }
639
+
640
+ $payload = array();
641
+ foreach ( $_GET['update_payload'] as $key => $value ) {
642
+ if ( 'true' === $value ) {
643
+ $payload[] = absint( $key );
644
+ }
645
+ }
646
+
647
+ $section['disabled'] = $payload;
648
+ } elseif ( 'group' === $type ) {
649
+ $payload = wp_stream_filter_input( INPUT_GET, 'update_payload', FILTER_SANITIZE_STRING );
650
+ $section['group'] = 'true' === $payload;
651
+ }
652
+
653
+ // Store the chart configuration
654
+ self::$sections[ $section_id ] = $section;
655
+
656
+ // Update the database option
657
+ WP_Stream_Reports_Settings::ajax_update_user_option( 'sections', self::$sections );
658
+ }
659
+
660
+ public function save_chart_height(){
661
+ $chart_height = wp_stream_filter_input( INPUT_GET, 'chart_height', FILTER_SANITIZE_NUMBER_INT );
662
+
663
+ if ( false === $chart_height ) {
664
+ wp_send_json_error();
665
+ }
666
+
667
+ // Update the database option
668
+ WP_Stream_Reports_Settings::ajax_update_user_option( 'chart_height', $chart_height );
669
+ }
670
+
671
+ public function chart_height_display( $status, $args ) {
672
+ $user_id = get_current_user_id();
673
+ $option = WP_Stream_Reports_Settings::get_user_options( 'chart_height', 300 );
674
+ $nonce = wp_create_nonce( 'wp_stream_reports_chart_height_nonce' );
675
+ ob_start();
676
+ ?>
677
+ <fieldset>
678
+ <h5><?php esc_html_e( 'Chart height', 'stream' ); ?></h5>
679
+ <div><input type="hidden" name="update_chart_height_nonce" id="update_chart_height_nonce" value="<?php echo esc_attr( $nonce ); ?>"></div>
680
+ <div><input type="hidden" name="update_chart_height_user" id="update_chart_height_user" value="<?php echo esc_attr( $user_id ); ?>"></div>
681
+ <div class="metabox-prefs stream-reports-chart-height-option">
682
+ <label for="chart-height">
683
+ <input type="number" step="50" min="100" max="950" name="chart_height" id="chart_height" maxlength="3" value="<?php echo esc_attr( $option ); ?>">
684
+ <?php esc_html_e( 'px', 'stream' ); ?>
685
+ </label>
686
+ <input type="submit" id="chart_height_apply" class="button" value="<?php esc_attr_e( 'Apply', 'stream' ); ?>">
687
+ <span class="spinner"></span>
688
+ </div>
689
+ </fieldset>
690
+ <?php
691
+ return ob_get_clean();
692
+ }
693
+
694
+ protected function get_section( $id ) {
695
+ if ( empty( self::$sections ) ) {
696
+ self::$sections = WP_Stream_Reports_Settings::get_user_options( 'sections' );
697
+ }
698
+
699
+ $section = self::$sections[ $id ];
700
+ $section = $this->parse_section( $section );
701
+
702
+ $section['key'] = $id;
703
+
704
+ return $section;
705
+ }
706
+
707
+ public function get_contexts() {
708
+
709
+ // Add Connectors as parents, and apply the Contexts as children
710
+ $contexts = $this->assemble_records( 'context' );
711
+ $connectors = $this->assemble_records( 'connector' );
712
+ foreach ( $connectors as $connector => $item ) {
713
+ $context_items[ $connector ]['label'] = $item['label'];
714
+ $context_items[ $connector ]['connector'] = $connector;
715
+ $context_items[ $connector ]['disabled'] = $item['disabled'];
716
+ foreach ( $contexts as $context_value => $context_item ) {
717
+ if ( isset( WP_Stream_Connectors::$contexts[ $connector ] ) && array_key_exists( $context_value, WP_Stream_Connectors::$contexts[ $connector ] ) ) {
718
+ $context_items[ $connector ]['children'][ $context_value ] = $context_item;
719
+ $context_items[ $connector ]['children'][ $context_value ]['connector'] = $connector;
720
+ $context_items[ $connector ]['children'][ $context_value ]['context'] = $context_value;
721
+ }
722
+ }
723
+ }
724
+
725
+ foreach ( $context_items as $context_value => $context_item ) {
726
+ if ( ! isset( $context_item['children'] ) || empty( $context_item['children'] ) ) {
727
+ unset( $context_items[ $context_value ] );
728
+ }
729
+ }
730
+
731
+ $all_items = array(
732
+ 'label' => __( 'All Contexts', 'stream' )
733
+ );
734
+
735
+ $new_array = apply_filters( 'wp_stream_reports_get_contexts', $context_items );
736
+ return array_merge( array( $all_items ), $new_array );
737
+ }
738
+
739
+ public function get_actions() {
740
+ $actions = $this->assemble_records( 'action' );
741
+ foreach ( $actions as $id => $item ) {
742
+ $actions[ $id ]['action'] = $id;
743
+ }
744
+
745
+ $all_actions = array(
746
+ 'label' => __( 'All Actions', 'stream' )
747
+ );
748
+
749
+ $new_array = array_merge( array( $all_actions ), $actions );
750
+ return apply_filters( 'wp_stream_reports_get_actions', $new_array );
751
+ }
752
+
753
+ /**
754
+ * Assembles records for display
755
+ *
756
+ * Gathers list of all authors/connectors, then compares it to
757
+ * results of existing records. All items that do not exist in records
758
+ * get assigned a disabled value of "true".
759
+ *
760
+ * @param string Column requested
761
+ *
762
+ * @return array options to be displayed in search filters
763
+ */
764
+ function assemble_records( $column ) {
765
+
766
+ $available_columns = array( 'context', 'connector', 'action' );
767
+ if ( ! in_array( $column, $available_columns ) ) {
768
+ return;
769
+ }
770
+
771
+ $prefixed_column = sprintf( 'stream_%s', $column );
772
+ $all_records = WP_Stream_Connectors::$term_labels[ $prefixed_column ];
773
+
774
+ $active_records = array();
775
+ $disabled_records = array();
776
+
777
+ foreach ( $all_records as $record => $label ) {
778
+ if ( isset( $this->existing_records[ $column ] ) && in_array( $record, $this->existing_records[ $column ] ) ) {
779
+ $active_records[ $record ] = array( 'label' => $label, 'disabled' => '' );
780
+ } else {
781
+ $disabled_records[ $record ] = array( 'label' => $label, 'disabled' => 'disabled="disabled"' );
782
+ }
783
+ }
784
+
785
+ // Remove WP-CLI pseudo user if no records with user=0 exist
786
+ if ( isset( $disabled_records[0] ) ) {
787
+ unset( $disabled_records[0] );
788
+ }
789
+
790
+ $sort = function ( $a, $b ) use ( $column ) {
791
+ $label_a = (string) $a['label'];
792
+ $label_b = (string) $b['label'];
793
+ if ( $label_a === $label_b ) {
794
+ return 0;
795
+ }
796
+ return strtolower( $label_a ) < strtolower( $label_b ) ? -1 : 1;
797
+ };
798
+ uasort( $active_records, $sort );
799
+ uasort( $disabled_records, $sort );
800
+
801
+ // Not using array_merge() in order to preserve the array index for the Authors dropdown which uses the user_id as the key
802
+ $all_records = $active_records + $disabled_records;
803
+
804
+ return $all_records;
805
+ }
806
+
807
+ /**
808
+ * Gets existing records for filtering dropdown menus
809
+ *
810
+ * @return array
811
+ */
812
+ function get_existing_records() {
813
+ $existing_records = array();
814
+
815
+ $args = array(
816
+ 'aggregations' => array(
817
+ 'connector',
818
+ 'context',
819
+ 'action',
820
+ ),
821
+ );
822
+
823
+ $query = wp_stream_query( $args );
824
+ $query_meta = WP_Stream::$db->get_query_meta();
825
+
826
+ if ( isset( $query_meta->aggregations ) ) {
827
+ foreach ( $query_meta->aggregations as $field => $aggregation ) {
828
+ $existing_records[ $field ] = array();
829
+ foreach ( $aggregation->buckets as $bucket ) {
830
+ $existing_records[ $field ][] = $bucket->key;
831
+ }
832
+ }
833
+ }
834
+
835
+ return $existing_records;
836
+ }
837
+
838
+ public function migrate_settings() {
839
+ $sections = self::$sections;
840
+ foreach ( $sections as $key => $args ) {
841
+ switch ( $args['data_group'] ) {
842
+ case 'action' :
843
+ $sections[ $key ]['action_id'] = $args['data_type'];
844
+ break;
845
+ case 'connector' :
846
+ $sections[ $key ]['connector_id'] = $args['data_type'];
847
+ break;
848
+ case 'context' :
849
+ $sections[ $key ]['connector_id'] = $this->find_connector_by_context( $args['data_type'] );
850
+ $sections[ $key ]['context_id'] = $args['data_type'];
851
+ break;
852
+ case 'other' :
853
+ break;
854
+ }
855
+
856
+ if ( isset( $args['selector_type'] ) ) {
857
+ $sections[ $key ]['selector_id'] = $args['selector_type'];
858
+ }
859
+
860
+ unset( $sections[ $key ]['data_group'] );
861
+ unset( $sections[ $key ]['data_type'] );
862
+ unset( $sections[ $key ]['selector_type'] );
863
+ }
864
+
865
+ WP_Stream_Reports_Settings::update_user_option_and_redirect( 'sections', $sections );
866
+ }
867
+
868
+ public function find_connector_by_context( $context ) {
869
+ $output = 'unknown';
870
+ $connectors = $this->assemble_records( 'connector' );
871
+ foreach ( $connectors as $connector => $item ) {
872
+ if ( isset( WP_Stream_Connectors::$contexts[ $connector ] ) && array_key_exists( $context, WP_Stream_Connectors::$contexts[ $connector ] ) ) {
873
+ $output = $connector;
874
+ break;
875
+ }
876
+ }
877
+
878
+ return $output;
879
+ }
880
+
881
+ /**
882
+ * Return active instance of WP_Stream_Reports_Metaboxes, create one if it doesn't exist
883
+ *
884
+ * @return WP_Stream_Reports_Metaboxes
885
+ */
886
+ public static function get_instance() {
887
+ if ( empty( self::$instance ) ) {
888
+ $class = __CLASS__;
889
+ self::$instance = new $class;
890
+ }
891
+
892
+ return self::$instance;
893
+ }
894
+
895
+ }
extensions/reports/includes/class-wp-stream-reports-settings.php ADDED
@@ -0,0 +1,233 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * Settings class for Stream Reports
4
+ *
5
+ * @author X-Team <x-team.com>
6
+ * @author Shady Sharaf <shady@x-team.com>
7
+ * @author Jaroslav Polakovič <dero@x-team.com>
8
+ * @author Jonathan Bardo <jonathan.bardo@x-team.com>
9
+ */
10
+ class WP_Stream_Reports_Settings {
11
+
12
+ /**
13
+ * Contains the option fields for the settings
14
+ *
15
+ * @var array $fields
16
+ */
17
+ public static $fields = array();
18
+
19
+ /**
20
+ * Contains the array of user options for the plugin
21
+ *
22
+ * @var array $user_options
23
+ */
24
+ private static $user_options;
25
+
26
+ /**
27
+ * Holds the user option name (key)
28
+ */
29
+ const OPTION_NAME = 'stream_reports_settings';
30
+
31
+ /**
32
+ * Public constructor
33
+ */
34
+ public static function load() {
35
+ // User and role caps
36
+ add_filter( 'user_has_cap', array( __CLASS__, '_filter_user_caps' ), 10, 4 );
37
+ add_filter( 'role_has_cap', array( __CLASS__, '_filter_role_caps' ), 10, 3 );
38
+
39
+ if ( WP_Stream_API::is_restricted() ) {
40
+ return;
41
+ }
42
+
43
+ // Add Reports settings tab to Stream settings
44
+ add_filter( 'wp_stream_settings_option_fields', array( __CLASS__, '_register_settings' ) );
45
+ }
46
+
47
+ public static function get_fields() {
48
+ if ( empty( self::$fields ) ) {
49
+ $fields = array();
50
+
51
+ self::$fields = apply_filters( 'wp_stream_reports_option_fields', $fields );
52
+ }
53
+
54
+ return self::$fields;
55
+ }
56
+
57
+ /**
58
+ * Appends Reports settings to Stream settings
59
+ *
60
+ * @filter wp_stream_settings_option_fields
61
+ */
62
+ public static function _register_settings( $stream_fields ) {
63
+ return array_merge( $stream_fields, self::get_fields() );
64
+ }
65
+
66
+ /**
67
+ * Filter user caps to dynamically grant our view cap based on allowed roles
68
+ *
69
+ * @filter user_has_cap
70
+ *
71
+ * @param $allcaps
72
+ * @param $caps
73
+ * @param $args
74
+ * @param $user
75
+ *
76
+ * @return array
77
+ */
78
+ public static function _filter_user_caps( $allcaps, $caps, $args, $user = null ) {
79
+ $user = is_a( $user, 'WP_User' ) ? $user : wp_get_current_user();
80
+
81
+ foreach ( $caps as $cap ) {
82
+ if ( WP_Stream_Reports::VIEW_CAP === $cap ) {
83
+ foreach ( $user->roles as $role ) {
84
+ if ( self::_role_can_access( $role ) ) {
85
+ $allcaps[ $cap ] = true;
86
+ break 2;
87
+ }
88
+ }
89
+ }
90
+ }
91
+
92
+ return $allcaps;
93
+ }
94
+
95
+ /**
96
+ * Filter role caps to dynamically grant our view cap based on allowed roles
97
+ *
98
+ * @filter role_has_cap
99
+ *
100
+ * @param $allcaps
101
+ * @param $cap
102
+ * @param $role
103
+ *
104
+ * @return array
105
+ */
106
+ public static function _filter_role_caps( $allcaps, $cap, $role ) {
107
+ if ( WP_Stream_Reports::VIEW_CAP === $cap && self::_role_can_access( $role ) ) {
108
+ $allcaps[ $cap ] = true;
109
+ }
110
+
111
+ return $allcaps;
112
+ }
113
+
114
+ private static function _role_can_access( $role ) {
115
+ // Default role if one is not set by default
116
+ if ( ! isset( WP_Stream_Settings::$options['reports_role_access'] ) ) {
117
+ WP_Stream_Settings::$options['reports_role_access'] = array( 'administrator' );
118
+ }
119
+
120
+ if ( in_array( $role, WP_Stream_Settings::$options['reports_role_access'] ) ) {
121
+ return true;
122
+ }
123
+
124
+ return false;
125
+ }
126
+
127
+ /**
128
+ * Returns true if the settings have not been setup for this user
129
+ *
130
+ * @return boolean
131
+ */
132
+ public static function is_first_visit() {
133
+ if ( ! get_user_option( self::get_option_key() ) ) {
134
+ return true;
135
+ }
136
+
137
+ return false;
138
+ }
139
+
140
+ /**
141
+ * Get user option and store it in a static var for easy access
142
+ *
143
+ * @param null $key
144
+ * @param array $default
145
+ *
146
+ * @return array
147
+ */
148
+ public static function get_user_options( $key = null, $default = array() ) {
149
+ if ( empty( self::$user_options ) ) {
150
+ self::$user_options = get_user_option( self::get_option_key() );
151
+ }
152
+
153
+ if ( is_null( $key ) ) {
154
+ // Return empty array if no user option is in DB
155
+ $output = ( self::$user_options ) ?: array();
156
+ } else {
157
+ $output = isset( self::$user_options[ $key ] ) ? self::$user_options[ $key ] : $default;
158
+ }
159
+
160
+ return $output;
161
+ }
162
+
163
+ /**
164
+ * Handle option updating in the database
165
+ *
166
+ * @param string $key
167
+ * @param mixed $option
168
+ * @param bool $redirect If the function must redirect and exit here
169
+ */
170
+ public static function update_user_option( $key, $option ) {
171
+ $user_options = self::get_user_options();
172
+
173
+ if ( ! isset( $user_options[ $key ] ) ) {
174
+ $user_options[ $key ] = array();
175
+ }
176
+
177
+ // Don't re-save if the value hasn't changed
178
+ if ( $user_options[ $key ] != $option ) {
179
+ $user_options[ $key ] = $option;
180
+ $is_saved = update_user_option( get_current_user_id(), self::get_option_key(), $user_options );
181
+ } else {
182
+ $is_saved = true;
183
+ }
184
+
185
+ return $is_saved;
186
+ }
187
+
188
+ /**
189
+ * Handles saving during AJAX requests
190
+ *
191
+ * @param string $key
192
+ * @param mixed $option
193
+ */
194
+ public static function ajax_update_user_option( $key, $option ) {
195
+ check_ajax_referer( 'stream-reports-page', 'wp_stream_reports_nonce' );
196
+
197
+ $is_saved = self::update_user_option( $key, $option );
198
+
199
+ if ( $is_saved ) {
200
+ wp_send_json_success();
201
+ } else {
202
+ wp_send_json_error();
203
+ }
204
+ }
205
+
206
+ /**
207
+ * Updates the user option and redirects back to the main page if successful
208
+ *
209
+ * @param string $key
210
+ * @param mixed $option
211
+ */
212
+ public static function update_user_option_and_redirect( $key, $option ) {
213
+ $is_saved = self::update_user_option( $key, $option );
214
+
215
+ if ( $is_saved ) {
216
+ wp_safe_redirect(
217
+ add_query_arg(
218
+ array( 'page' => WP_Stream_Reports::REPORTS_PAGE_SLUG ),
219
+ self_admin_url( 'admin.php' )
220
+ )
221
+ );
222
+
223
+ exit;
224
+ } else {
225
+ wp_die( __( "Uh no! This wasn't suppose to happen :(", 'stream' ) );
226
+ }
227
+ }
228
+
229
+ public static function get_option_key() {
230
+ return self::OPTION_NAME;
231
+ }
232
+
233
+ }
extensions/reports/includes/template-tags.php ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ function wp_stream_reports_selector( $data_types, $args, $class ) {
4
+ $options = array();
5
+ foreach ( $data_types as $key => $item ) {
6
+ $selected = false;
7
+
8
+ if ( isset( $item['connector'] ) && $item['connector'] == $args['connector_id'] && isset( $item['context'] ) && $item['context'] == null ) {
9
+ $selected = true;
10
+ } else if ( isset( $item['action'] ) && $item['action'] == $args['action_id'] ) {
11
+ $selected = true;
12
+ }
13
+
14
+ $option_args = array(
15
+ 'value' => $key,
16
+ 'label' => isset( $item['label'] ) ? $item['label'] : null,
17
+ 'selected' => selected( $selected, true, false ),
18
+ 'disabled' => isset( $item['disabled'] ) ? $item['disabled'] : null,
19
+ 'class' => isset( $item['children'] ) ? 'level-1' : null,
20
+
21
+ 'connector' => isset( $item['connector'] ) ? $item['connector'] : null,
22
+ 'context' => isset( $item['context'] ) ? $item['context'] : null,
23
+ 'action' => isset( $item['action'] ) ? $item['action'] : null,
24
+ );
25
+ $options[] = wp_stream_reports_filter_option( $option_args );
26
+
27
+ if ( isset( $item['children'] ) ) {
28
+ foreach ( $item['children'] as $child_value => $child_item ) {
29
+ $selected = false;
30
+ if ( isset( $child_item['connector'] ) && $child_item['connector'] == $args['connector_id'] && isset( $child_item['context'] ) && $child_item['context'] == $args['context_id'] ) {
31
+ $selected = true;
32
+ }
33
+
34
+ $option_args = array(
35
+ 'value' => $child_value,
36
+ 'label' => isset( $child_item['label'] ) ? $child_item['label'] : null,
37
+ 'selected' => selected( $selected, true, false ),
38
+ 'disabled' => isset( $child_item['disabled'] ) ? $child_item['disabled'] : null,
39
+ 'class' => 'level-2',
40
+
41
+ 'connector' => isset( $child_item['connector'] ) ? $child_item['connector'] : null,
42
+ 'context' => isset( $child_item['context'] ) ? $child_item['context'] : null,
43
+ 'action' => isset( $child_item['action'] ) ? $child_item['action'] : null,
44
+ );
45
+ $options[] = wp_stream_reports_filter_option( $option_args );
46
+ }
47
+ }
48
+ }
49
+
50
+ $allowed_html = array(
51
+ 'option' => array(
52
+ 'value' => array(),
53
+ 'selected' => array(),
54
+ 'disabled' => array(),
55
+ 'class' => array(),
56
+ 'data-connector' => array(),
57
+ 'data-context' => array(),
58
+ 'data-action' => array(),
59
+ ),
60
+ );
61
+
62
+ printf(
63
+ '<select class="%s">%s</select>',
64
+ esc_attr( $class ),
65
+ wp_kses( implode( '', $options ), $allowed_html )
66
+ );
67
+ }
68
+
69
+ function wp_stream_reports_filter_option( $args ) {
70
+ $defaults = array(
71
+ 'value' => null,
72
+ 'selected' => null,
73
+ 'disabled' => null,
74
+ 'class' => null,
75
+ 'label' => null,
76
+ 'connector' => null,
77
+ 'context' => null,
78
+ 'action' => null,
79
+ );
80
+
81
+ $args = wp_parse_args( $args, $defaults );
82
+ return sprintf(
83
+ '<option value="%s" %s %s %s %s %s class="%s">%s</option>',
84
+ esc_attr( $args['value'] ),
85
+ $args['selected'],
86
+ $args['disabled'],
87
+ $args['connector'] ? sprintf( 'data-connector="%s"', esc_attr( $args['connector'] ) ) : null,
88
+ $args['context'] ? sprintf( 'data-context="%s"', esc_attr( $args['context'] ) ) : null,
89
+ $args['action'] ? sprintf( 'data-action="%s"', esc_attr( $args['action'] ) ) : null,
90
+ $args['class'] ? esc_attr( $args['class'] ) : null,
91
+ esc_html( $args['label'] )
92
+ );
93
+ }
94
+
95
+ function wp_stream_reports_intervals_html() {
96
+ $date = WP_Stream_Reports_Date_Interval::get_instance();
97
+
98
+ // Default interval
99
+ $default = array(
100
+ 'key' => 'all-time',
101
+ 'start' => '',
102
+ 'end' => '',
103
+ );
104
+ $user_interval = WP_Stream_Reports_Settings::get_user_options( 'interval', $default );
105
+ $save_interval_url = add_query_arg(
106
+ array_merge(
107
+ array(
108
+ 'action' => 'wp_stream_reports_save_interval',
109
+ ),
110
+ WP_Stream_Reports::$nonce
111
+ ),
112
+ admin_url( 'admin-ajax.php' )
113
+ );
114
+
115
+ include WP_STREAM_REPORTS_VIEW_DIR . 'intervals.php';
116
+ }
extensions/reports/ui/css/stream-reports.css ADDED
@@ -0,0 +1,288 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Media queries to alternate between 1 columns and 2 columns depending on screen size */
2
+
3
+ @media only screen and (max-width: 850px) {
4
+ #wpbody-content #dashboard-widgets.columns-2 .postbox-container {
5
+ width: 100% !important;
6
+ }
7
+ }
8
+
9
+
10
+ /**
11
+ * Configure Section
12
+ * (There is override of core css declaration)
13
+ */
14
+
15
+ .stream_page_wp_stream_reports #dashboard-widgets h3 input.title {
16
+ width: 220px;
17
+ line-height: 19px;
18
+ }
19
+
20
+ .stream_page_wp_stream_reports #dashboard-widgets h3 .postbox-title-action {
21
+ position: inherit;;
22
+ padding: 0;
23
+ margin-right: 7px;
24
+ float: right;
25
+ }
26
+
27
+ .js.stream_page_wp_stream_reports #dashboard-widgets h3 .postbox-title-action.postbox-delete-action a {
28
+ color : #a00;
29
+ visibility: hidden;
30
+ margin-right: 7px;
31
+ }
32
+
33
+ .js.stream_page_wp_stream_reports #dashboard-widgets h3 .postbox-title-action.postbox-delete-action a.visible {
34
+ visibility: visible;
35
+ }
36
+
37
+ .js.stream_page_wp_stream_reports #dashboard-widgets h3 .postbox-title-action.postbox-delete-action a:hover {
38
+ color : #f00;
39
+ }
40
+
41
+ .stream_page_wp_stream_reports .date-interval {
42
+ margin: 10px 10px 3px;
43
+ }
44
+
45
+ .stream_page_wp_stream_reports .postbox:hover .edit-box {
46
+ display: inline;
47
+ }
48
+
49
+ .stream_page_wp_stream_reports .postbox.configure .handlediv {
50
+ visibility: hidden;
51
+ }
52
+
53
+ .stream_page_wp_stream_reports .postbox > .inside {
54
+ margin: 0;
55
+ }
56
+
57
+ .stream_page_wp_stream_reports .postbox .configure {
58
+ height: 0px;
59
+ overflow: hidden;
60
+ margin: -1px 0 0 0;
61
+ border-bottom: 1px solid #eee;
62
+ }
63
+
64
+ .stream_page_wp_stream_reports .postbox .configure .inside {
65
+ margin: 0;
66
+ padding: 10px 12px 0;
67
+ min-height: 28px;
68
+ }
69
+
70
+ .stream_page_wp_stream_reports .postbox .configure.visible {
71
+ height: auto;
72
+ overflow: hidden;
73
+ }
74
+
75
+ .stream_page_wp_stream_reports .postbox .configure .chart-option,
76
+ .stream_page_wp_stream_reports .postbox .configure .grouping-separator,
77
+ .stream_page_wp_stream_reports .postbox .configure .chart-types {
78
+ float: left;
79
+ margin-bottom: 10px;
80
+ }
81
+
82
+ .stream_page_wp_stream_reports .postbox .configure .chart-option {
83
+ margin-right: 5px;
84
+ width: 20%;
85
+ max-width: 150px;
86
+ min-width: 120px;
87
+ }
88
+
89
+ .stream_page_wp_stream_reports .postbox .configure .chart-dataset {
90
+ width: 30%;
91
+ max-width: 250px;
92
+ min-width: 165px;
93
+ }
94
+
95
+ .stream_page_wp_stream_reports .postbox .configure .grouping-separator {
96
+ margin-right: 5px;
97
+ margin-left: 2px;
98
+ line-height: 28px;
99
+ }
100
+
101
+ .stream_page_wp_stream_reports .postbox .configure input.button {
102
+ float: right;
103
+ }
104
+
105
+ .stream_page_wp_stream_reports .postbox .configure .chart-types {
106
+ display: inline-block;
107
+ line-height: 28px;
108
+ }
109
+
110
+ .stream_page_wp_stream_reports .postbox .configure .chart-types .dashicons {
111
+ padding-left: 5px;
112
+ color: #888888;
113
+ cursor: pointer;
114
+ line-height: 28px;
115
+ }
116
+
117
+ .stream_page_wp_stream_reports .postbox .configure .spinner {
118
+ margin: 4px 8px 0 0;
119
+ }
120
+
121
+ .stream_page_wp_stream_reports .postbox .configure .chart-types .dashicons.active {
122
+ color: #000;
123
+ }
124
+
125
+ .stream_page_wp_stream_reports .hndle {
126
+ position: relative;
127
+ }
128
+
129
+ .stream_page_wp_stream_reports .postbox .clear-title {
130
+ display: inline;
131
+ position: relative;
132
+ cursor: pointer;
133
+ left: -20px;
134
+ top: 2px;
135
+ width: 20px;
136
+ height: 28px;
137
+ font-size: 14px;
138
+ padding-top: 1px;
139
+ padding-right: 2px;
140
+ }
141
+
142
+ .stream_page_wp_stream_reports .postbox .clear-title:before {
143
+ content: '\f158';
144
+ }
145
+
146
+ .stream_page_wp_stream_reports .stream-reports-chart-height-option{
147
+ width: 200px;
148
+ }
149
+
150
+ .stream_page_wp_stream_reports #dashboard-widgets .postbox-container {
151
+ position: relative;
152
+ }
153
+
154
+ .stream_page_wp_stream_reports .no-reports-message {
155
+ position: absolute;
156
+ top: calc(50% - 25px);
157
+ width: calc(100% - 50px);
158
+ padding: 0 25px;
159
+ font-size: 18px;
160
+ line-height: 28px;
161
+ font-weight: 700;
162
+ text-align: center;
163
+ color: #000;
164
+ }
165
+
166
+ @media only screen and (max-width: 799px) {
167
+ .stream_page_wp_stream_reports .no-reports-message {
168
+ margin-top: 50px;
169
+ }
170
+ }
171
+
172
+ .stream_page_wp_stream_reports .no-reports-message a {
173
+ font-weight: normal;
174
+ }
175
+
176
+ .stream_page_wp_stream_reports .no-reports-message span {
177
+ padding: 0 5px;
178
+ text-transform: uppercase;
179
+ }
180
+
181
+ @media only screen and (max-width: 782px) {
182
+ .stream_page_wp_stream_reports .date-interval .field-predefined,
183
+ .stream_page_wp_stream_reports .date-interval .date-inputs {
184
+ margin-bottom: 10px;
185
+ }
186
+
187
+ .stream_page_wp_stream_reports .date-interval .button {
188
+ float: left;
189
+ clear: left;
190
+ }
191
+
192
+ .stream_page_wp_stream_reports .postbox .configure .chart-option {
193
+ width: 46%;
194
+ margin: 0 0 6px;
195
+ max-width: none;
196
+ min-width: 0;
197
+ }
198
+
199
+ .stream_page_wp_stream_reports .postbox .configure .chart-selector {
200
+ float: right;
201
+ }
202
+
203
+ .stream_page_wp_stream_reports .postbox .configure .grouping-separator {
204
+ width: 8%;
205
+ text-align: center;
206
+ margin: 0 0 6px;
207
+ }
208
+
209
+ .stream_page_wp_stream_reports .postbox .configure .chart-types {
210
+ clear: left;
211
+ line-height: 36px;
212
+ margin-bottom: 0;
213
+ }
214
+
215
+ .stream_page_wp_stream_reports .postbox .configure .chart-types .dashicons {
216
+ line-height: 36px;
217
+ }
218
+
219
+ .stream_page_wp_stream_reports .postbox .configure input.button {
220
+ margin-bottom: 6px;
221
+ }
222
+ }
223
+
224
+
225
+ /* Charts */
226
+
227
+ .stream_page_wp_stream_reports .postbox .inside {
228
+ padding: 0;
229
+ }
230
+
231
+ .stream_page_wp_stream_reports .postbox .inside .chart {
232
+ position: relative;
233
+ padding: 0;
234
+ }
235
+
236
+ .stream_page_wp_stream_reports .postbox .inside .chart .chart-loading {
237
+ display: none;
238
+ position: absolute;
239
+ top: calc(50% - 12px);
240
+ width: 100%;
241
+ font-size: 18px;
242
+ font-weight: 700;
243
+ text-align: center;
244
+ color: #000;
245
+ z-index: 1;
246
+ }
247
+
248
+ .stream_page_wp_stream_reports .postbox .inside .chart .chart-loading > span {
249
+ padding: 6px 12px;
250
+ background-color: #f9f9f9;
251
+ }
252
+
253
+ .stream_page_wp_stream_reports .postbox .inside .chart svg {
254
+ padding: 12px;
255
+ }
256
+
257
+ .stream_page_wp_stream_reports .postbox .inside .chart svg g.nv-axisMaxMin:nth-child(3) text {
258
+ text-anchor: end !important;
259
+ }
260
+
261
+ .stream_page_wp_stream_reports .postbox .inside .chart .chart-loading > span,
262
+ .stream_page_wp_stream_reports .nvtooltip.xy-tooltip {
263
+ font-family: "Open Sans", sans-serif !important;
264
+ margin: 20px 0 0 12px;
265
+ border: 1px solid #e5e5e5;
266
+
267
+ -webkit-border-radius: 6px;
268
+ -moz-border-radius: 6px;
269
+ border-radius: 6px;
270
+
271
+ -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
272
+ -moz-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
273
+ box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
274
+ }
275
+
276
+ .stream_page_wp_stream_reports .nvtooltip.xy-tooltip h3 {
277
+ font-weight: bold;
278
+ font-size: 13px;
279
+ }
280
+
281
+ .stream_page_wp_stream_reports svg text {
282
+ font-family: "Open Sans", sans-serif !important;
283
+ }
284
+
285
+ .stream_page_wp_stream_reports svg .nv-axis .tick text,
286
+ .stream_page_wp_stream_reports svg .nv-axis .nv-axisMaxMin text {
287
+ font-size: 10px;
288
+ }
extensions/reports/ui/images/stream-reports-example.jpg ADDED
Binary file
extensions/reports/ui/js/stream-reports.js ADDED
@@ -0,0 +1,778 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*jslint nomen: true */
2
+ /*global jQuery, _, nv, d3, stream, wp_stream_reports, ajaxurl, document, window */
3
+ ( function( window, $, _, nv, d3, wp_stream_reports ) {
4
+ 'use strict';
5
+
6
+ var report = {};
7
+
8
+ report.intervals = {
9
+ init: function( $wrapper ) {
10
+ this.wrapper = $wrapper;
11
+ this.save_interval( this.wrapper.find( '.button-primary' ) );
12
+
13
+ this.$ = this.wrapper.each( function() {
14
+ var container = $( _.last( arguments ) ),
15
+ dateinputs = container.find( '.date-inputs' ),
16
+ from = container.find( '.field-from' ),
17
+ to = container.find( '.field-to' ),
18
+ to_remove = to.prev( '.date-remove' ),
19
+ from_remove = from.prev( '.date-remove' ),
20
+ predefined = container.children( '.field-predefined' ),
21
+ datepickers = $( '' ).add( to ).add( from );
22
+
23
+ if ( _.isFunction( $.fn.datepicker ) ) {
24
+
25
+ // Apply a GMT offset due to Date() using the visitor's local time
26
+ var siteGMTOffsetHours = parseFloat( wp_stream_reports.gmt_offset ),
27
+ localGMTOffsetHours = new Date().getTimezoneOffset() / 60 * -1,
28
+ totalGMTOffsetHours = siteGMTOffsetHours - localGMTOffsetHours,
29
+ localTime = new Date(),
30
+ siteTime = new Date( localTime.getTime() + ( totalGMTOffsetHours * 60 * 60 * 1000 ) ),
31
+ dayOffset = '0';
32
+
33
+ // Check if the site date is different from the local date, and set a day offset
34
+ if ( localTime.getDate() !== siteTime.getDate() || localTime.getMonth() !== siteTime.getMonth() ) {
35
+ if ( localTime.getTime() < siteTime.getTime() ) {
36
+ dayOffset = '+1d';
37
+ } else {
38
+ dayOffset = '-1d';
39
+ }
40
+ }
41
+
42
+ datepickers.datepicker({
43
+ dateFormat: 'yy/mm/dd',
44
+ maxDate: Number( dayOffset ),
45
+ defaultDate: siteTime,
46
+ beforeShow: function() {
47
+ $( this ).prop( 'disabled', true );
48
+ },
49
+ onClose: function() {
50
+ $( this ).prop( 'disabled', false );
51
+ }
52
+ });
53
+
54
+ datepickers.datepicker( 'widget' ).addClass( 'stream-datepicker' );
55
+ }
56
+
57
+ if ( _.isFunction( $.fn.select2 ) ) {
58
+ predefined.select2({
59
+ allowClear: true
60
+ });
61
+ }
62
+
63
+ if ( '' !== from.val() ) {
64
+ from_remove.show();
65
+ }
66
+
67
+ if ( '' !== to.val() ) {
68
+ to_remove.show();
69
+ }
70
+
71
+ predefined.on({
72
+ 'change': function() {
73
+ var value = $( this ).val(),
74
+ option = predefined.find( '[value="' + value + '"]' ),
75
+ to_val = option.data( 'to' ),
76
+ from_val = option.data( 'from' );
77
+
78
+ if ( 'custom' === value ) {
79
+ dateinputs.show();
80
+ return false;
81
+ } else {
82
+ dateinputs.hide();
83
+ datepickers.datepicker( 'hide' );
84
+ }
85
+
86
+ from.val( from_val ).trigger( 'change', [true] );
87
+ to.val( to_val ).trigger( 'change', [true] );
88
+
89
+ if ( _.isFunction( $.fn.datepicker ) && datepickers.datepicker( 'widget' ).is( ':visible' ) ) {
90
+ datepickers.datepicker( 'refresh' ).datepicker( 'hide' );
91
+ }
92
+ },
93
+ 'select2-removed': function() {
94
+ predefined.val( '' ).trigger( 'change' );
95
+ },
96
+ 'check_options': function() {
97
+ if ( '' !== to.val() && '' !== from.val() ) {
98
+ var option = predefined.find( 'option' ).filter( '[data-to="' + to.val() + '"]' ).filter( '[data-from="' + from.val() + '"]' );
99
+ if ( 0 !== option.length ) {
100
+ predefined.val( option.attr( 'value' ) ).trigger( 'change',[true] );
101
+ } else {
102
+ predefined.val( 'custom' ).trigger( 'change',[true] );
103
+ }
104
+ } else if ( '' === to.val() && '' === from.val() ) {
105
+ predefined.val( '' ).trigger( 'change',[true] );
106
+ } else {
107
+ predefined.val( 'custom' ).trigger( 'change',[true] );
108
+ }
109
+ }
110
+ });
111
+
112
+ from.on({
113
+ 'change': function() {
114
+
115
+ if ( '' !== from.val() ) {
116
+ from_remove.show();
117
+ to.datepicker( 'option', 'minDate', from.val() );
118
+ } else {
119
+ from_remove.hide();
120
+ }
121
+
122
+ if ( true === _.last( arguments ) ) {
123
+ return false;
124
+ }
125
+
126
+ predefined.trigger( 'check_options' );
127
+ }
128
+ });
129
+
130
+ to.on({
131
+ 'change': function() {
132
+ if ( '' !== to.val() ) {
133
+ to_remove.show();
134
+ from.datepicker( 'option', 'maxDate', to.val() );
135
+ } else {
136
+ to_remove.hide();
137
+ }
138
+
139
+ if ( true === _.last( arguments ) ) {
140
+ return false;
141
+ }
142
+
143
+ predefined.trigger( 'check_options' );
144
+ }
145
+ });
146
+
147
+ // Trigger change on load
148
+ predefined.trigger( 'change' );
149
+
150
+ $( '' ).add( from_remove ).add( to_remove ).on({
151
+ 'click': function() {
152
+ $( this ).next( 'input' ).val( '' ).trigger( 'change' );
153
+ }
154
+ });
155
+ });
156
+ },
157
+
158
+ save_interval: function( $btn ) {
159
+ var $wrapper = this.wrapper;
160
+ $btn.click( function() {
161
+ var data = {
162
+ key: $wrapper.find( 'select.field-predefined' ).find( ':selected' ).val(),
163
+ start: $wrapper.find( '.date-inputs .field-from' ).val(),
164
+ end: $wrapper.find( '.date-inputs .field-to' ).val()
165
+ };
166
+
167
+ // Add params to URL
168
+ $( this ).attr( 'href', $( this ).attr( 'href' ) + '&' + $.param( data ) );
169
+ });
170
+ }
171
+ };
172
+
173
+ /**
174
+ * Screen options
175
+ */
176
+ report.screen = {
177
+ init: function( chartHeightOption, applyBtn ) {
178
+ this.$chartHeightOption = chartHeightOption;
179
+ this.$applyBtn = applyBtn;
180
+
181
+ this.configureOptions();
182
+ },
183
+
184
+ configureOptions: function() {
185
+ var parent = this;
186
+
187
+ this.$applyBtn.click( function( e ) {
188
+ e.preventDefault();
189
+
190
+ var $spinner = $( this ).siblings( '.spinner' );
191
+ $spinner.show();
192
+
193
+ $.ajax({
194
+ type: 'GET',
195
+ url: ajaxurl,
196
+ data: {
197
+ action: 'wp_stream_reports_save_chart_height',
198
+ wp_stream_reports_nonce: $( '#wp_stream_reports_nonce' ).val(),
199
+ chart_height: parent.$chartHeightOption.val()
200
+ },
201
+ dataType: 'json',
202
+ success: function() {
203
+ location.reload( true );
204
+ }
205
+ });
206
+
207
+ return false;
208
+ });
209
+ }
210
+ };
211
+
212
+ /**
213
+ * Metabox logic logic
214
+ */
215
+ report.metabox = {
216
+ init: function( configureDiv, deleteBtn, configureBtn ) {
217
+ // Variables
218
+ this.$configureDiv = configureDiv;
219
+ this.$deleteBtn = deleteBtn;
220
+ this.$configureBtn = configureBtn;
221
+
222
+ // Let's configure event listener for all sections
223
+ this.configureSection();
224
+ },
225
+
226
+ configureSection: function() {
227
+ var parent = this;
228
+
229
+ // Trigger select2js
230
+ this.$configureDiv.find( 'select.chart-option' ).select2({
231
+ minimumResultsForSearch: 8
232
+ });
233
+
234
+ // Change chart type toggle
235
+ this.$configureDiv.find( '.chart-types .dashicons' ).click( function() {
236
+ var $target = $( this );
237
+ if ( ! $target.hasClass( 'active' ) ) {
238
+ $target.siblings().removeClass( 'active' );
239
+ $target.addClass( 'active' );
240
+ }
241
+ });
242
+
243
+ // Bind handler to save button
244
+ this.$btnSave = this.$configureDiv.find( '.button-primary' ).click( this.configureSave );
245
+
246
+ // Confirmation of deletion
247
+ this.$deleteBtn.click( function() {
248
+ if ( ! window.confirm( wp_stream_reports.i18n.deletemsg ) ) {
249
+ return false;
250
+ }
251
+ });
252
+
253
+ this.$configureDiv.find( '.chart-dataset' ).on( 'change', function() {
254
+ var selectors = parent.$configureDiv.find( '.chart-selector' );
255
+ selectors.find( 'option' ).removeAttr( 'disabled' );
256
+
257
+ var option = parent.$configureDiv.find( '.chart-dataset :selected' );
258
+ if ( 'all' === $( option ).val() ) {
259
+ return;
260
+ }
261
+
262
+ var disable = option.closest( 'optgroup' ).data( 'disable-selectors' );
263
+ var disabled_selectors = disable.split( ',' );
264
+
265
+ for ( var i = 0; i < disabled_selectors.length; i++ ) {
266
+ option = selectors.find( 'option[value="' + disabled_selectors[i] + '"]');
267
+ option.attr( 'disabled', 'disabled' );
268
+ if ( option.is( ':selected' ) ) {
269
+ option.removeAttr( 'selected' );
270
+ selectors.trigger( 'change' );
271
+ }
272
+ }
273
+
274
+ });
275
+
276
+ this.$configureDiv.parents( '.postbox' ).on( 'keyup paste', '.title', function() {
277
+
278
+ var $inputBox = $( this );
279
+
280
+ if ( '' === $( this ).val() && $( this ).siblings( '.clear-title' ).length ) {
281
+ $( this ).siblings( '.clear-title' ).remove();
282
+ }
283
+
284
+ if ( '' !== $( this ).val() && ! $( this ).siblings( '.clear-title' ).length ) {
285
+ $( this ).after(
286
+ $( '<i/>', {
287
+ 'class': 'clear-title dashicons',
288
+ 'click': function() {
289
+ $inputBox.val( '' );
290
+ $inputBox.trigger( 'keyup' );
291
+ }
292
+ })
293
+ );
294
+ }
295
+
296
+ });
297
+
298
+ // Configuration toggle
299
+ this.$configureBtn.on( 'click.streamReports', function() {
300
+ var $target = $( this );
301
+
302
+ var $curPostbox = $target.parents( '.postbox' );
303
+
304
+ var realTitle = $curPostbox.find( '.chart-title' ).val();
305
+ var generatedTitle = $curPostbox.find( '.chart-generated-title' ).val();
306
+ var displayedTitle = realTitle;
307
+ if ( '' === displayedTitle ) {
308
+ displayedTitle = generatedTitle;
309
+ }
310
+
311
+ var $titleText, $inputBox;
312
+
313
+ // Remove event handler added by core and add it back when user click cancel or save
314
+ if ( $target.text() === wp_stream_reports.i18n.configure ) {
315
+ $titleText = $curPostbox.find( '.hndle .title' );
316
+ $inputBox = $( '<input/>', {
317
+ 'type': 'text',
318
+ 'class': 'title',
319
+ 'value': realTitle,
320
+ 'placeholder': generatedTitle
321
+ });
322
+ $titleText.replaceWith( $inputBox );
323
+ $inputBox.trigger( 'keyup' );
324
+
325
+ // Add class to container
326
+ $curPostbox.addClass( 'configure' );
327
+
328
+ // Click function management
329
+ parent.$clickFunction = $._data( $curPostbox.find( 'h3' ).get( 0 ) ).events.click[0].handler;
330
+ $curPostbox.find( 'h3' ).off( 'click.postboxes' );
331
+
332
+ // Switch configure button text
333
+ $target.text( wp_stream_reports.i18n.cancel );
334
+ } else {
335
+ $inputBox = $curPostbox.find( '.hndle .title' );
336
+ $titleText = $( '<span/>', {
337
+ 'class': 'title',
338
+ 'text': displayedTitle
339
+ });
340
+
341
+ if ( '' === $titleText.text() ) {
342
+ $titleText.text( $inputBox.attr( 'placeholder' ) );
343
+ }
344
+
345
+ $inputBox.replaceWith( $titleText );
346
+ $titleText.siblings( '.clear-title' ).remove();
347
+
348
+ // Remove class from container
349
+ $curPostbox.removeClass( 'configure' );
350
+
351
+ // Click function management
352
+ $curPostbox.find( 'h3' ).on( 'click.postboxes', parent.$clickFunction );
353
+
354
+ // Switch cancel button text
355
+ $target.text( wp_stream_reports.i18n.configure );
356
+ }
357
+
358
+ // Always show the cancel button
359
+ $target.toggleClass( 'edit-box' );
360
+
361
+ // Show the delete button
362
+ $target.parent().next().find( 'a' ).toggleClass( 'visible' );
363
+
364
+ //Open the section if it's hidden
365
+ $curPostbox.removeClass( 'closed' );
366
+
367
+ // Show the configure div
368
+ $curPostbox.find( '.inside .configure' ).toggleClass( 'visible' );
369
+ });
370
+
371
+ this.$configureDiv.filter( '.stream-reports-expand' ).closest( '.postbox' ).find( '.postbox-title-action a.open-box').click();
372
+ },
373
+
374
+ configureSave: function() {
375
+ var parent = $( this ).parents( '.configure' ), $spinner, $postbox, $cancelBtn;
376
+
377
+ // Postbox container
378
+ $postbox = $( this ).parents( '.postbox' );
379
+
380
+ // Show the spinner
381
+ $spinner = $postbox.find( '.configure .spinner' );
382
+ $spinner.show();
383
+
384
+ // Cancel button
385
+ $cancelBtn = $postbox.find( '.hndle .open-box' );
386
+
387
+ var id = $( this ).data( 'id' );
388
+ var option = parent.find( '.chart-dataset' ).select2( 'data' ).element[0].dataset;
389
+ var connector = option.connector;
390
+ var context = option.context;
391
+ var action = parent.find( '.chart-action' ).select2( 'data' ).element[0].dataset.action;
392
+ var selector = parent.find( '.chart-selector' ).select2( 'data' ).id;
393
+
394
+ // Send the new
395
+ $.ajax({
396
+ type: 'GET',
397
+ url: ajaxurl,
398
+ data: {
399
+ action: 'wp_stream_reports_save_metabox_config',
400
+ wp_stream_reports_nonce: $( '#wp_stream_reports_nonce' ).val(),
401
+ section_id: id,
402
+ title: $postbox.find( '.title' ).val(),
403
+ chart_type: parent.find( '.chart-types .active' ).data( 'type' ),
404
+ data_connector: connector,
405
+ data_context: context,
406
+ data_action: action,
407
+ data_selector: selector
408
+ },
409
+ dataType: 'json',
410
+ success: function( data ) {
411
+ $spinner.hide(0,function() {
412
+ $cancelBtn.trigger( 'click' );
413
+ });
414
+
415
+ if ( true === data.success ) {
416
+
417
+ $.ajax({
418
+ type: 'GET',
419
+ url: ajaxurl,
420
+ data: {
421
+ action: 'wp_stream_reports_update_metabox_display',
422
+ wp_stream_reports_nonce: $( '#wp_stream_reports_nonce' ).val(),
423
+ section_id: id
424
+ },
425
+ dataType: 'json',
426
+ success: stream.report.chart.loadSectionCallback( $postbox )
427
+ });
428
+
429
+ }
430
+
431
+
432
+ }
433
+ });
434
+ }
435
+ };
436
+
437
+ /**
438
+ * Chart logic
439
+ */
440
+ report.chart = {
441
+ _: {
442
+ // Default Options
443
+ 'opts': {
444
+ // Store the jQuery elements we are using
445
+ '$': null,
446
+
447
+ // Margin on the sides of the chart
448
+ 'margin': {
449
+ 'left': 30,
450
+ 'right': 25
451
+ },
452
+
453
+ // Width of the canvas
454
+ 'width': 'this',
455
+
456
+ // Height of the Canvas
457
+ 'height': 'this',
458
+
459
+ // Actual data that will be plotted
460
+ 'values': {},
461
+
462
+ // Global Label options
463
+ 'label': {
464
+ 'show': true,
465
+ 'threshold': 0.05,
466
+ 'type': 'percent'
467
+ },
468
+
469
+ // Global legend options
470
+ 'legend': {
471
+ 'show': true
472
+ },
473
+
474
+ 'tooltip': {
475
+ 'show': true
476
+ },
477
+
478
+ // y Axis information
479
+ 'yAxis': {
480
+ 'show': true,
481
+ 'label': null,
482
+ 'format': ',r',
483
+ 'reduceTicks': false
484
+ },
485
+
486
+ // x Axis information
487
+ 'xAxis': {
488
+ 'show': true,
489
+ 'label': null,
490
+ 'format': ',r',
491
+ 'reduceTicks': true,
492
+ 'rotateLabels': 0
493
+ },
494
+
495
+ // Group opts
496
+ 'group': {
497
+ 'spacing': 0.1
498
+ },
499
+
500
+ // Use interactive guidelines
501
+ 'guidelines': false,
502
+
503
+ // Use interactive guidelines
504
+ 'showValues': false,
505
+
506
+ // Show Controls
507
+ 'controls': true,
508
+
509
+ // Type of the Chart
510
+ 'type': false,
511
+
512
+ // Miliseconds on the animation, or false to deactivate
513
+ 'animate': 350,
514
+
515
+ // Check if a graph need to be draw
516
+ 'draw': null,
517
+
518
+ // Whether to stack the chart by default
519
+ 'stacked': false
520
+ }
521
+ },
522
+
523
+ stateChangeCallback: function( section_id ) {
524
+ var id = section_id;
525
+ return function( e ) {
526
+ var data = {
527
+ 'type': 'none'
528
+ };
529
+
530
+ if ( undefined !== e.stacked ) {
531
+ data.type = 'group';
532
+ data.payload = e.stacked;
533
+ } else if ( undefined !== e.disabled ) {
534
+ data.type = 'disable';
535
+ data.payload = e.disabled;
536
+ }
537
+
538
+ if ( 'none' !== data.type ) {
539
+ $.ajax({
540
+ type: 'GET',
541
+ url: ajaxurl,
542
+ data: {
543
+ 'action': 'wp_stream_reports_save_chart_options',
544
+ 'wp_stream_reports_nonce': $( '#wp_stream_reports_nonce' ).val(),
545
+ 'section_id': id,
546
+ 'update_type': data.type,
547
+ 'update_payload': data.payload
548
+ },
549
+ dataType: 'json'
550
+ });
551
+ }
552
+ };
553
+ },
554
+
555
+ // Build all the opts to be drawn later
556
+ init: function( elements, $columns ) {
557
+ this.elements = elements;
558
+ this.$columns = $columns;
559
+ },
560
+
561
+ // Loads and redraws a section's chart and configuration options
562
+ loadSection: function( $section ) {
563
+
564
+ var section_id = $section.data( 'section-id' );
565
+ var $chart = $section.find( '.chart' );
566
+
567
+ $chart.find( '.chart-loading' ).show();
568
+
569
+ $.ajax({
570
+ type: 'GET',
571
+ url: ajaxurl,
572
+ data: {
573
+ action: 'wp_stream_reports_update_metabox_display',
574
+ wp_stream_reports_nonce: $( '#wp_stream_reports_nonce' ).val(),
575
+ section_id: section_id
576
+ },
577
+ dataType: 'json',
578
+ success: stream.report.chart.refreshSectionCallback( $section, $chart )
579
+ });
580
+
581
+ },
582
+
583
+ loadSectionCallback: function( $section ) {
584
+ return function() {
585
+ stream.report.chart.loadSection( $section );
586
+ };
587
+ },
588
+
589
+ refreshSectionCallback: function( $section, $chart ) {
590
+ return function( data ) {
591
+
592
+ // Update chart data
593
+ $chart.data( 'report', data.data.options );
594
+ $chart.find( 'svg' ).html( '<svg></svg>' );
595
+ var opts = $.extend( true, {}, report.chart._.opts, { '$': this.elements }, ( 'undefined' !== typeof opts ? opts : {} ) );
596
+ stream.report.chart.drawChart( $section.find( '.section-id' ).val(), $chart, opts, stream.report.chart.$columns );
597
+
598
+ // Update title values
599
+ $section.find( '.chart-title' ).val( data.data.title );
600
+ $section.find( '.chart-generated-title' ).val( data.data.generated_title );
601
+
602
+ // Update Title Text
603
+ var newTitle = data.data.title;
604
+ if ( '' === newTitle || null === newTitle ) {
605
+ newTitle = data.data.generated_title;
606
+ }
607
+ $section.find( '.hndle .title' ).text( newTitle );
608
+
609
+ $chart.find( '.chart-loading' ).hide();
610
+
611
+ };
612
+ },
613
+
614
+ // Grab all the opts and draw the chart on the screen
615
+ drawChart: function( k, el, opts, $columns ) {
616
+ var $el = $( el ),
617
+ data = $el.data( 'report', $.extend( true, {}, opts, { 'id': _.uniqueId( '__stream-report-chart-' ) }, $el.data( 'report' ) ) ).data( 'report' );
618
+
619
+ if ( $( el ).parents( '.postbox.closed' ).length ) {
620
+ return;
621
+ }
622
+
623
+ var section_id = $( el ).parents( '.postbox' ).find( '.section-id' ).val();
624
+
625
+ if ( 'parent' === data.width || 'parent' === data._width ) {
626
+ data.width = $el.parent().innerWidth();
627
+ data._width = 'parent';
628
+ } else if ( 'this' === data.width || 'this' === data._width ) {
629
+ data.width = $el.innerWidth();
630
+ data._width = 'this';
631
+ }
632
+ if ( 'parent' === data.height || 'parent' === data._height ) {
633
+ data.height = $el.parent().innerHeight();
634
+ data._height = 'parent';
635
+ } else if ( 'this' === data.height || 'this' === data._height ) {
636
+ data.height = $el.innerHeight();
637
+ data._height = 'this';
638
+ }
639
+
640
+ // This is very important, if you build the SVG live it was bugging...
641
+ data.svg = $el.find( 'svg' );
642
+ data.d3 = d3.select( data.svg[0] );
643
+ var dateFormat = function( d ) {
644
+ var milliseconds = d * 1000;
645
+
646
+ return d3.time.format( '%Y/%m/%d' )( new Date( milliseconds ) );
647
+ };
648
+
649
+ nv.addGraph( function() {
650
+ switch ( data.type ) {
651
+ case 'donut':
652
+ case 'pie':
653
+ data.chart = nv.models.pieChart();
654
+ data.chart.valueFormat( d3.format( ',f' ) );
655
+ data.chart.x( function( d ) { return d.key; } );
656
+ data.chart.y( function( d ) { return d.value; } );
657
+ if ( 'donut' === data.type ) {
658
+ data.chart.donut( true );
659
+ }
660
+ break;
661
+ case 'line':
662
+ data.chart = nv.models.lineChart();
663
+ data.chart.xAxis.tickFormat( dateFormat );
664
+ break;
665
+ case 'multibar':
666
+ data.chart = nv.models.multiBarChart();
667
+ data.chart.yAxis.tickFormat( d3.format( ',f' ) );
668
+ data.chart.xAxis.tickFormat( dateFormat );
669
+ break;
670
+
671
+ case 'multibar-horizontal':
672
+ data.chart = nv.models.multiBarHorizontalChart();
673
+ data.chart.xAxis.tickFormat( dateFormat );
674
+ break;
675
+
676
+ default: // If we don't have a type of chart defined it gets out...
677
+ return;
678
+ }
679
+
680
+ var mapValidation = [
681
+ { data: data.donutRatio, _function: data.chart.donutRatio },
682
+ { data: data.label.show, _function: data.chart.showLabels },
683
+ { data: data.showValues, _function: data.chart.showValues },
684
+ { data: data.label.threshold, _function: data.chart.labelThreshold },
685
+ { data: data.label.type, _function: data.chart.labelType },
686
+ { data: data.group.spacing, _function: data.chart.groupSpacing },
687
+ { data: data.guidelines, _function: data.chart.useInteractiveGuideline },
688
+ { data: data.animate, _function: data.chart.transitionDuration },
689
+ { data: data.legend.show, _function: data.chart.showLegend },
690
+ { data: data.yAxis.show, _function: data.chart.showYAxis },
691
+ { data: data.yAxis.reduceTicks, _function: data.chart.reduceYTicks },
692
+ { data: data.xAxis.show, _function: data.chart.showXAxis },
693
+ { data: data.xAxis.reduceTicks, _function: data.chart.reduceXTicks },
694
+ { data: data.controls, _function: data.chart.showControls },
695
+ { data: data.margin, _function: data.chart.margin },
696
+ { data: data.tooltip.show, _function: data.chart.tooltips },
697
+ { data: data.stacked, _function: data.chart.stacked }
698
+ ];
699
+
700
+ _.map( mapValidation, function( value ) {
701
+ if ( null !== value.data && _.isFunction( value._function ) ) {
702
+ value._function( value.data );
703
+ }
704
+ });
705
+
706
+ mapValidation = [
707
+ {data: data.yAxis.label, object: data.chart.yAxis, _function: 'data.chart.yAxis.axisLabel'},
708
+ {data: data.yAxis.format, object: data.chart.yAxis, _function: 'data.chart.yAxistickFormat', format: true},
709
+ {data: data.xAxis.label, object: data.chart.xAxis, _function: 'data.chart.xAxis.axisLabel'},
710
+ {data: data.xAxis.format, object: data.chart.xAxis, _function: 'data.chart.xAxis.tickFormat', format: true}
711
+ ];
712
+
713
+ _.map( mapValidation, function( value ) {
714
+ if ( null !== value.data && _.isObject( value.object ) && _.isFunction( value._function ) ) {
715
+ if ( ! _.isUndefined( value.format ) ) {
716
+ value._function( d3.format( value.data ) );
717
+ } else {
718
+ value._function( value.data );
719
+ }
720
+ }
721
+ });
722
+
723
+ data.d3.datum( data.values ).call( data.chart );
724
+
725
+ //Update the chart when window resizes.
726
+ nv.utils.windowResize( data.chart.update );
727
+ $columns.click( data.chart.update );
728
+
729
+ data.chart.dispatch.on( 'stateChange', stream.report.chart.stateChangeCallback( section_id ) );
730
+
731
+ return data.chart;
732
+ });
733
+ }
734
+
735
+ };
736
+
737
+ window.stream = $.extend( true, ( !_.isObject( window.stream ) ? {} : window.stream ), { 'report': report } );
738
+
739
+ /**
740
+ * Document Ready actions
741
+ */
742
+ $( document ).ready( function() {
743
+ stream.report.intervals.init(
744
+ $( '.date-interval' )
745
+ );
746
+
747
+ stream.report.screen.init(
748
+ $( '#chart_height' ),
749
+ $( '#chart_height_apply' )
750
+ );
751
+
752
+ $( '.stream_page_wp_stream_reports .postbox' ).each( function() {
753
+ var id = $( this ).find( '.section-id' ).val();
754
+ $( this ).data( 'section-id', id );
755
+ });
756
+
757
+ stream.report.chart.init(
758
+ $( '.stream_page_wp_stream_reports .chart' ),
759
+ $( '.columns-prefs input[type="radio"]' )
760
+ );
761
+
762
+ $( '.stream_page_wp_stream_reports .chart' ).each( function() {
763
+ stream.report.chart.loadSection( $( this ).parents( '.postbox' ) );
764
+ });
765
+
766
+ $( '.postbox.closed' ).bind( 'click.initOpen', function() {
767
+ stream.report.chart.loadSection( $( this ) );
768
+ $( this ).unbind( 'click.initOpen' );
769
+ });
770
+
771
+ stream.report.metabox.init(
772
+ $( '.postbox .inside .configure' ),
773
+ $( '.postbox-delete-action a' ),
774
+ $( '.postbox-title-action .edit-box' )
775
+ );
776
+ });
777
+
778
+ } ( window, jQuery.noConflict(), _.noConflict(), nv, d3, wp_stream_reports ) );
extensions/reports/ui/lib/d3/d3.min.js ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
1
+ !function(){function n(n){return null!=n&&!isNaN(n)}function t(n){return n.length}function e(n){for(var t=1;n*t%1;)t*=10;return t}function r(n,t){try{for(var e in t)Object.defineProperty(n.prototype,e,{value:t[e],enumerable:!1})}catch(r){n.prototype=t}}function u(){}function i(n){return aa+n in this}function o(n){return n=aa+n,n in this&&delete this[n]}function a(){var n=[];return this.forEach(function(t){n.push(t)}),n}function c(){var n=0;for(var t in this)t.charCodeAt(0)===ca&&++n;return n}function s(){for(var n in this)if(n.charCodeAt(0)===ca)return!1;return!0}function l(){}function f(n,t,e){return function(){var r=e.apply(t,arguments);return r===t?n:r}}function h(n,t){if(t in n)return t;t=t.charAt(0).toUpperCase()+t.substring(1);for(var e=0,r=sa.length;r>e;++e){var u=sa[e]+t;if(u in n)return u}}function g(){}function p(){}function v(n){function t(){for(var t,r=e,u=-1,i=r.length;++u<i;)(t=r[u].on)&&t.apply(this,arguments);return n}var e=[],r=new u;return t.on=function(t,u){var i,o=r.get(t);return arguments.length<2?o&&o.on:(o&&(o.on=null,e=e.slice(0,i=e.indexOf(o)).concat(e.slice(i+1)),r.remove(t)),u&&e.push(r.set(t,{on:u})),n)},t}function d(){Xo.event.preventDefault()}function m(){for(var n,t=Xo.event;n=t.sourceEvent;)t=n;return t}function y(n){for(var t=new p,e=0,r=arguments.length;++e<r;)t[arguments[e]]=v(t);return t.of=function(e,r){return function(u){try{var i=u.sourceEvent=Xo.event;u.target=n,Xo.event=u,t[u.type].apply(e,r)}finally{Xo.event=i}}},t}function x(n){return fa(n,da),n}function M(n){return"function"==typeof n?n:function(){return ha(n,this)}}function _(n){return"function"==typeof n?n:function(){return ga(n,this)}}function b(n,t){function e(){this.removeAttribute(n)}function r(){this.removeAttributeNS(n.space,n.local)}function u(){this.setAttribute(n,t)}function i(){this.setAttributeNS(n.space,n.local,t)}function o(){var e=t.apply(this,arguments);null==e?this.removeAttribute(n):this.setAttribute(n,e)}function a(){var e=t.apply(this,arguments);null==e?this.removeAttributeNS(n.space,n.local):this.setAttributeNS(n.space,n.local,e)}return n=Xo.ns.qualify(n),null==t?n.local?r:e:"function"==typeof t?n.local?a:o:n.local?i:u}function w(n){return n.trim().replace(/\s+/g," ")}function S(n){return new RegExp("(?:^|\\s+)"+Xo.requote(n)+"(?:\\s+|$)","g")}function k(n){return n.trim().split(/^|\s+/)}function E(n,t){function e(){for(var e=-1;++e<u;)n[e](this,t)}function r(){for(var e=-1,r=t.apply(this,arguments);++e<u;)n[e](this,r)}n=k(n).map(A);var u=n.length;return"function"==typeof t?r:e}function A(n){var t=S(n);return function(e,r){if(u=e.classList)return r?u.add(n):u.remove(n);var u=e.getAttribute("class")||"";r?(t.lastIndex=0,t.test(u)||e.setAttribute("class",w(u+" "+n))):e.setAttribute("class",w(u.replace(t," ")))}}function C(n,t,e){function r(){this.style.removeProperty(n)}function u(){this.style.setProperty(n,t,e)}function i(){var r=t.apply(this,arguments);null==r?this.style.removeProperty(n):this.style.setProperty(n,r,e)}return null==t?r:"function"==typeof t?i:u}function N(n,t){function e(){delete this[n]}function r(){this[n]=t}function u(){var e=t.apply(this,arguments);null==e?delete this[n]:this[n]=e}return null==t?e:"function"==typeof t?u:r}function L(n){return"function"==typeof n?n:(n=Xo.ns.qualify(n)).local?function(){return this.ownerDocument.createElementNS(n.space,n.local)}:function(){return this.ownerDocument.createElementNS(this.namespaceURI,n)}}function z(n){return{__data__:n}}function q(n){return function(){return va(this,n)}}function T(n){return arguments.length||(n=Xo.ascending),function(t,e){return t&&e?n(t.__data__,e.__data__):!t-!e}}function R(n,t){for(var e=0,r=n.length;r>e;e++)for(var u,i=n[e],o=0,a=i.length;a>o;o++)(u=i[o])&&t(u,o,e);return n}function D(n){return fa(n,ya),n}function P(n){var t,e;return function(r,u,i){var o,a=n[i].update,c=a.length;for(i!=e&&(e=i,t=0),u>=t&&(t=u+1);!(o=a[t])&&++t<c;);return o}}function U(){var n=this.__transition__;n&&++n.active}function j(n,t,e){function r(){var t=this[o];t&&(this.removeEventListener(n,t,t.$),delete this[o])}function u(){var u=c(t,Bo(arguments));r.call(this),this.addEventListener(n,this[o]=u,u.$=e),u._=t}function i(){var t,e=new RegExp("^__on([^.]+)"+Xo.requote(n)+"$");for(var r in this)if(t=r.match(e)){var u=this[r];this.removeEventListener(t[1],u,u.$),delete this[r]}}var o="__on"+n,a=n.indexOf("."),c=H;a>0&&(n=n.substring(0,a));var s=Ma.get(n);return s&&(n=s,c=F),a?t?u:r:t?g:i}function H(n,t){return function(e){var r=Xo.event;Xo.event=e,t[0]=this.__data__;try{n.apply(this,t)}finally{Xo.event=r}}}function F(n,t){var e=H(n,t);return function(n){var t=this,r=n.relatedTarget;r&&(r===t||8&r.compareDocumentPosition(t))||e.call(t,n)}}function O(){var n=".dragsuppress-"+ ++ba,t="click"+n,e=Xo.select(Go).on("touchmove"+n,d).on("dragstart"+n,d).on("selectstart"+n,d);if(_a){var r=Jo.style,u=r[_a];r[_a]="none"}return function(i){function o(){e.on(t,null)}e.on(n,null),_a&&(r[_a]=u),i&&(e.on(t,function(){d(),o()},!0),setTimeout(o,0))}}function Y(n,t){t.changedTouches&&(t=t.changedTouches[0]);var e=n.ownerSVGElement||n;if(e.createSVGPoint){var r=e.createSVGPoint();if(0>wa&&(Go.scrollX||Go.scrollY)){e=Xo.select("body").append("svg").style({position:"absolute",top:0,left:0,margin:0,padding:0,border:"none"},"important");var u=e[0][0].getScreenCTM();wa=!(u.f||u.e),e.remove()}return wa?(r.x=t.pageX,r.y=t.pageY):(r.x=t.clientX,r.y=t.clientY),r=r.matrixTransform(n.getScreenCTM().inverse()),[r.x,r.y]}var i=n.getBoundingClientRect();return[t.clientX-i.left-n.clientLeft,t.clientY-i.top-n.clientTop]}function I(n){return n>0?1:0>n?-1:0}function Z(n,t,e){return(t[0]-n[0])*(e[1]-n[1])-(t[1]-n[1])*(e[0]-n[0])}function V(n){return n>1?0:-1>n?Sa:Math.acos(n)}function X(n){return n>1?Ea:-1>n?-Ea:Math.asin(n)}function $(n){return((n=Math.exp(n))-1/n)/2}function B(n){return((n=Math.exp(n))+1/n)/2}function W(n){return((n=Math.exp(2*n))-1)/(n+1)}function J(n){return(n=Math.sin(n/2))*n}function G(){}function K(n,t,e){return new Q(n,t,e)}function Q(n,t,e){this.h=n,this.s=t,this.l=e}function nt(n,t,e){function r(n){return n>360?n-=360:0>n&&(n+=360),60>n?i+(o-i)*n/60:180>n?o:240>n?i+(o-i)*(240-n)/60:i}function u(n){return Math.round(255*r(n))}var i,o;return n=isNaN(n)?0:(n%=360)<0?n+360:n,t=isNaN(t)?0:0>t?0:t>1?1:t,e=0>e?0:e>1?1:e,o=.5>=e?e*(1+t):e+t-e*t,i=2*e-o,gt(u(n+120),u(n),u(n-120))}function tt(n,t,e){return new et(n,t,e)}function et(n,t,e){this.h=n,this.c=t,this.l=e}function rt(n,t,e){return isNaN(n)&&(n=0),isNaN(t)&&(t=0),ut(e,Math.cos(n*=Na)*t,Math.sin(n)*t)}function ut(n,t,e){return new it(n,t,e)}function it(n,t,e){this.l=n,this.a=t,this.b=e}function ot(n,t,e){var r=(n+16)/116,u=r+t/500,i=r-e/200;return u=ct(u)*Fa,r=ct(r)*Oa,i=ct(i)*Ya,gt(lt(3.2404542*u-1.5371385*r-.4985314*i),lt(-.969266*u+1.8760108*r+.041556*i),lt(.0556434*u-.2040259*r+1.0572252*i))}function at(n,t,e){return n>0?tt(Math.atan2(e,t)*La,Math.sqrt(t*t+e*e),n):tt(0/0,0/0,n)}function ct(n){return n>.206893034?n*n*n:(n-4/29)/7.787037}function st(n){return n>.008856?Math.pow(n,1/3):7.787037*n+4/29}function lt(n){return Math.round(255*(.00304>=n?12.92*n:1.055*Math.pow(n,1/2.4)-.055))}function ft(n){return gt(n>>16,255&n>>8,255&n)}function ht(n){return ft(n)+""}function gt(n,t,e){return new pt(n,t,e)}function pt(n,t,e){this.r=n,this.g=t,this.b=e}function vt(n){return 16>n?"0"+Math.max(0,n).toString(16):Math.min(255,n).toString(16)}function dt(n,t,e){var r,u,i,o=0,a=0,c=0;if(r=/([a-z]+)\((.*)\)/i.exec(n))switch(u=r[2].split(","),r[1]){case"hsl":return e(parseFloat(u[0]),parseFloat(u[1])/100,parseFloat(u[2])/100);case"rgb":return t(Mt(u[0]),Mt(u[1]),Mt(u[2]))}return(i=Va.get(n))?t(i.r,i.g,i.b):(null!=n&&"#"===n.charAt(0)&&(4===n.length?(o=n.charAt(1),o+=o,a=n.charAt(2),a+=a,c=n.charAt(3),c+=c):7===n.length&&(o=n.substring(1,3),a=n.substring(3,5),c=n.substring(5,7)),o=parseInt(o,16),a=parseInt(a,16),c=parseInt(c,16)),t(o,a,c))}function mt(n,t,e){var r,u,i=Math.min(n/=255,t/=255,e/=255),o=Math.max(n,t,e),a=o-i,c=(o+i)/2;return a?(u=.5>c?a/(o+i):a/(2-o-i),r=n==o?(t-e)/a+(e>t?6:0):t==o?(e-n)/a+2:(n-t)/a+4,r*=60):(r=0/0,u=c>0&&1>c?0:r),K(r,u,c)}function yt(n,t,e){n=xt(n),t=xt(t),e=xt(e);var r=st((.4124564*n+.3575761*t+.1804375*e)/Fa),u=st((.2126729*n+.7151522*t+.072175*e)/Oa),i=st((.0193339*n+.119192*t+.9503041*e)/Ya);return ut(116*u-16,500*(r-u),200*(u-i))}function xt(n){return(n/=255)<=.04045?n/12.92:Math.pow((n+.055)/1.055,2.4)}function Mt(n){var t=parseFloat(n);return"%"===n.charAt(n.length-1)?Math.round(2.55*t):t}function _t(n){return"function"==typeof n?n:function(){return n}}function bt(n){return n}function wt(n){return function(t,e,r){return 2===arguments.length&&"function"==typeof e&&(r=e,e=null),St(t,e,n,r)}}function St(n,t,e,r){function u(){var n,t=c.status;if(!t&&c.responseText||t>=200&&300>t||304===t){try{n=e.call(i,c)}catch(r){return o.error.call(i,r),void 0}o.load.call(i,n)}else o.error.call(i,c)}var i={},o=Xo.dispatch("beforesend","progress","load","error"),a={},c=new XMLHttpRequest,s=null;return!Go.XDomainRequest||"withCredentials"in c||!/^(http(s)?:)?\/\//.test(n)||(c=new XDomainRequest),"onload"in c?c.onload=c.onerror=u:c.onreadystatechange=function(){c.readyState>3&&u()},c.onprogress=function(n){var t=Xo.event;Xo.event=n;try{o.progress.call(i,c)}finally{Xo.event=t}},i.header=function(n,t){return n=(n+"").toLowerCase(),arguments.length<2?a[n]:(null==t?delete a[n]:a[n]=t+"",i)},i.mimeType=function(n){return arguments.length?(t=null==n?null:n+"",i):t},i.responseType=function(n){return arguments.length?(s=n,i):s},i.response=function(n){return e=n,i},["get","post"].forEach(function(n){i[n]=function(){return i.send.apply(i,[n].concat(Bo(arguments)))}}),i.send=function(e,r,u){if(2===arguments.length&&"function"==typeof r&&(u=r,r=null),c.open(e,n,!0),null==t||"accept"in a||(a.accept=t+",*/*"),c.setRequestHeader)for(var l in a)c.setRequestHeader(l,a[l]);return null!=t&&c.overrideMimeType&&c.overrideMimeType(t),null!=s&&(c.responseType=s),null!=u&&i.on("error",u).on("load",function(n){u(null,n)}),o.beforesend.call(i,c),c.send(null==r?null:r),i},i.abort=function(){return c.abort(),i},Xo.rebind(i,o,"on"),null==r?i:i.get(kt(r))}function kt(n){return 1===n.length?function(t,e){n(null==t?e:null)}:n}function Et(){var n=At(),t=Ct()-n;t>24?(isFinite(t)&&(clearTimeout(Wa),Wa=setTimeout(Et,t)),Ba=0):(Ba=1,Ga(Et))}function At(){var n=Date.now();for(Ja=Xa;Ja;)n>=Ja.t&&(Ja.f=Ja.c(n-Ja.t)),Ja=Ja.n;return n}function Ct(){for(var n,t=Xa,e=1/0;t;)t.f?t=n?n.n=t.n:Xa=t.n:(t.t<e&&(e=t.t),t=(n=t).n);return $a=n,e}function Nt(n,t){return t-(n?Math.ceil(Math.log(n)/Math.LN10):1)}function Lt(n,t){var e=Math.pow(10,3*oa(8-t));return{scale:t>8?function(n){return n/e}:function(n){return n*e},symbol:n}}function zt(n){var t=n.decimal,e=n.thousands,r=n.grouping,u=n.currency,i=r?function(n){for(var t=n.length,u=[],i=0,o=r[0];t>0&&o>0;)u.push(n.substring(t-=o,t+o)),o=r[i=(i+1)%r.length];return u.reverse().join(e)}:bt;return function(n){var e=Qa.exec(n),r=e[1]||" ",o=e[2]||">",a=e[3]||"",c=e[4]||"",s=e[5],l=+e[6],f=e[7],h=e[8],g=e[9],p=1,v="",d="",m=!1;switch(h&&(h=+h.substring(1)),(s||"0"===r&&"="===o)&&(s=r="0",o="=",f&&(l-=Math.floor((l-1)/4))),g){case"n":f=!0,g="g";break;case"%":p=100,d="%",g="f";break;case"p":p=100,d="%",g="r";break;case"b":case"o":case"x":case"X":"#"===c&&(v="0"+g.toLowerCase());case"c":case"d":m=!0,h=0;break;case"s":p=-1,g="r"}"$"===c&&(v=u[0],d=u[1]),"r"!=g||h||(g="g"),null!=h&&("g"==g?h=Math.max(1,Math.min(21,h)):("e"==g||"f"==g)&&(h=Math.max(0,Math.min(20,h)))),g=nc.get(g)||qt;var y=s&&f;return function(n){var e=d;if(m&&n%1)return"";var u=0>n||0===n&&0>1/n?(n=-n,"-"):a;if(0>p){var c=Xo.formatPrefix(n,h);n=c.scale(n),e=c.symbol+d}else n*=p;n=g(n,h);var x=n.lastIndexOf("."),M=0>x?n:n.substring(0,x),_=0>x?"":t+n.substring(x+1);!s&&f&&(M=i(M));var b=v.length+M.length+_.length+(y?0:u.length),w=l>b?new Array(b=l-b+1).join(r):"";return y&&(M=i(w+M)),u+=v,n=M+_,("<"===o?u+n+w:">"===o?w+u+n:"^"===o?w.substring(0,b>>=1)+u+n+w.substring(b):u+(y?n:w+n))+e}}}function qt(n){return n+""}function Tt(){this._=new Date(arguments.length>1?Date.UTC.apply(this,arguments):arguments[0])}function Rt(n,t,e){function r(t){var e=n(t),r=i(e,1);return r-t>t-e?e:r}function u(e){return t(e=n(new ec(e-1)),1),e}function i(n,e){return t(n=new ec(+n),e),n}function o(n,r,i){var o=u(n),a=[];if(i>1)for(;r>o;)e(o)%i||a.push(new Date(+o)),t(o,1);else for(;r>o;)a.push(new Date(+o)),t(o,1);return a}function a(n,t,e){try{ec=Tt;var r=new Tt;return r._=n,o(r,t,e)}finally{ec=Date}}n.floor=n,n.round=r,n.ceil=u,n.offset=i,n.range=o;var c=n.utc=Dt(n);return c.floor=c,c.round=Dt(r),c.ceil=Dt(u),c.offset=Dt(i),c.range=a,n}function Dt(n){return function(t,e){try{ec=Tt;var r=new Tt;return r._=t,n(r,e)._}finally{ec=Date}}}function Pt(n){function t(n){function t(t){for(var e,u,i,o=[],a=-1,c=0;++a<r;)37===n.charCodeAt(a)&&(o.push(n.substring(c,a)),null!=(u=uc[e=n.charAt(++a)])&&(e=n.charAt(++a)),(i=C[e])&&(e=i(t,null==u?"e"===e?" ":"0":u)),o.push(e),c=a+1);return o.push(n.substring(c,a)),o.join("")}var r=n.length;return t.parse=function(t){var r={y:1900,m:0,d:1,H:0,M:0,S:0,L:0,Z:null},u=e(r,n,t,0);if(u!=t.length)return null;"p"in r&&(r.H=r.H%12+12*r.p);var i=null!=r.Z&&ec!==Tt,o=new(i?Tt:ec);return"j"in r?o.setFullYear(r.y,0,r.j):"w"in r&&("W"in r||"U"in r)?(o.setFullYear(r.y,0,1),o.setFullYear(r.y,0,"W"in r?(r.w+6)%7+7*r.W-(o.getDay()+5)%7:r.w+7*r.U-(o.getDay()+6)%7)):o.setFullYear(r.y,r.m,r.d),o.setHours(r.H+Math.floor(r.Z/100),r.M+r.Z%100,r.S,r.L),i?o._:o},t.toString=function(){return n},t}function e(n,t,e,r){for(var u,i,o,a=0,c=t.length,s=e.length;c>a;){if(r>=s)return-1;if(u=t.charCodeAt(a++),37===u){if(o=t.charAt(a++),i=N[o in uc?t.charAt(a++):o],!i||(r=i(n,e,r))<0)return-1}else if(u!=e.charCodeAt(r++))return-1}return r}function r(n,t,e){b.lastIndex=0;var r=b.exec(t.substring(e));return r?(n.w=w.get(r[0].toLowerCase()),e+r[0].length):-1}function u(n,t,e){M.lastIndex=0;var r=M.exec(t.substring(e));return r?(n.w=_.get(r[0].toLowerCase()),e+r[0].length):-1}function i(n,t,e){E.lastIndex=0;var r=E.exec(t.substring(e));return r?(n.m=A.get(r[0].toLowerCase()),e+r[0].length):-1}function o(n,t,e){S.lastIndex=0;var r=S.exec(t.substring(e));return r?(n.m=k.get(r[0].toLowerCase()),e+r[0].length):-1}function a(n,t,r){return e(n,C.c.toString(),t,r)}function c(n,t,r){return e(n,C.x.toString(),t,r)}function s(n,t,r){return e(n,C.X.toString(),t,r)}function l(n,t,e){var r=x.get(t.substring(e,e+=2).toLowerCase());return null==r?-1:(n.p=r,e)}var f=n.dateTime,h=n.date,g=n.time,p=n.periods,v=n.days,d=n.shortDays,m=n.months,y=n.shortMonths;t.utc=function(n){function e(n){try{ec=Tt;var t=new ec;return t._=n,r(t)}finally{ec=Date}}var r=t(n);return e.parse=function(n){try{ec=Tt;var t=r.parse(n);return t&&t._}finally{ec=Date}},e.toString=r.toString,e},t.multi=t.utc.multi=ee;var x=Xo.map(),M=jt(v),_=Ht(v),b=jt(d),w=Ht(d),S=jt(m),k=Ht(m),E=jt(y),A=Ht(y);p.forEach(function(n,t){x.set(n.toLowerCase(),t)});var C={a:function(n){return d[n.getDay()]},A:function(n){return v[n.getDay()]},b:function(n){return y[n.getMonth()]},B:function(n){return m[n.getMonth()]},c:t(f),d:function(n,t){return Ut(n.getDate(),t,2)},e:function(n,t){return Ut(n.getDate(),t,2)},H:function(n,t){return Ut(n.getHours(),t,2)},I:function(n,t){return Ut(n.getHours()%12||12,t,2)},j:function(n,t){return Ut(1+tc.dayOfYear(n),t,3)},L:function(n,t){return Ut(n.getMilliseconds(),t,3)},m:function(n,t){return Ut(n.getMonth()+1,t,2)},M:function(n,t){return Ut(n.getMinutes(),t,2)},p:function(n){return p[+(n.getHours()>=12)]},S:function(n,t){return Ut(n.getSeconds(),t,2)},U:function(n,t){return Ut(tc.sundayOfYear(n),t,2)},w:function(n){return n.getDay()},W:function(n,t){return Ut(tc.mondayOfYear(n),t,2)},x:t(h),X:t(g),y:function(n,t){return Ut(n.getFullYear()%100,t,2)},Y:function(n,t){return Ut(n.getFullYear()%1e4,t,4)},Z:ne,"%":function(){return"%"}},N={a:r,A:u,b:i,B:o,c:a,d:Bt,e:Bt,H:Jt,I:Jt,j:Wt,L:Qt,m:$t,M:Gt,p:l,S:Kt,U:Ot,w:Ft,W:Yt,x:c,X:s,y:Zt,Y:It,Z:Vt,"%":te};return t}function Ut(n,t,e){var r=0>n?"-":"",u=(r?-n:n)+"",i=u.length;return r+(e>i?new Array(e-i+1).join(t)+u:u)}function jt(n){return new RegExp("^(?:"+n.map(Xo.requote).join("|")+")","i")}function Ht(n){for(var t=new u,e=-1,r=n.length;++e<r;)t.set(n[e].toLowerCase(),e);return t}function Ft(n,t,e){ic.lastIndex=0;var r=ic.exec(t.substring(e,e+1));return r?(n.w=+r[0],e+r[0].length):-1}function Ot(n,t,e){ic.lastIndex=0;var r=ic.exec(t.substring(e));return r?(n.U=+r[0],e+r[0].length):-1}function Yt(n,t,e){ic.lastIndex=0;var r=ic.exec(t.substring(e));return r?(n.W=+r[0],e+r[0].length):-1}function It(n,t,e){ic.lastIndex=0;var r=ic.exec(t.substring(e,e+4));return r?(n.y=+r[0],e+r[0].length):-1}function Zt(n,t,e){ic.lastIndex=0;var r=ic.exec(t.substring(e,e+2));return r?(n.y=Xt(+r[0]),e+r[0].length):-1}function Vt(n,t,e){return/^[+-]\d{4}$/.test(t=t.substring(e,e+5))?(n.Z=+t,e+5):-1}function Xt(n){return n+(n>68?1900:2e3)}function $t(n,t,e){ic.lastIndex=0;var r=ic.exec(t.substring(e,e+2));return r?(n.m=r[0]-1,e+r[0].length):-1}function Bt(n,t,e){ic.lastIndex=0;var r=ic.exec(t.substring(e,e+2));return r?(n.d=+r[0],e+r[0].length):-1}function Wt(n,t,e){ic.lastIndex=0;var r=ic.exec(t.substring(e,e+3));return r?(n.j=+r[0],e+r[0].length):-1}function Jt(n,t,e){ic.lastIndex=0;var r=ic.exec(t.substring(e,e+2));return r?(n.H=+r[0],e+r[0].length):-1}function Gt(n,t,e){ic.lastIndex=0;var r=ic.exec(t.substring(e,e+2));return r?(n.M=+r[0],e+r[0].length):-1}function Kt(n,t,e){ic.lastIndex=0;var r=ic.exec(t.substring(e,e+2));return r?(n.S=+r[0],e+r[0].length):-1}function Qt(n,t,e){ic.lastIndex=0;var r=ic.exec(t.substring(e,e+3));return r?(n.L=+r[0],e+r[0].length):-1}function ne(n){var t=n.getTimezoneOffset(),e=t>0?"-":"+",r=~~(oa(t)/60),u=oa(t)%60;return e+Ut(r,"0",2)+Ut(u,"0",2)}function te(n,t,e){oc.lastIndex=0;var r=oc.exec(t.substring(e,e+1));return r?e+r[0].length:-1}function ee(n){for(var t=n.length,e=-1;++e<t;)n[e][0]=this(n[e][0]);return function(t){for(var e=0,r=n[e];!r[1](t);)r=n[++e];return r[0](t)}}function re(){}function ue(n,t,e){var r=e.s=n+t,u=r-n,i=r-u;e.t=n-i+(t-u)}function ie(n,t){n&&lc.hasOwnProperty(n.type)&&lc[n.type](n,t)}function oe(n,t,e){var r,u=-1,i=n.length-e;for(t.lineStart();++u<i;)r=n[u],t.point(r[0],r[1],r[2]);t.lineEnd()}function ae(n,t){var e=-1,r=n.length;for(t.polygonStart();++e<r;)oe(n[e],t,1);t.polygonEnd()}function ce(){function n(n,t){n*=Na,t=t*Na/2+Sa/4;var e=n-r,o=Math.cos(t),a=Math.sin(t),c=i*a,s=u*o+c*Math.cos(e),l=c*Math.sin(e);hc.add(Math.atan2(l,s)),r=n,u=o,i=a}var t,e,r,u,i;gc.point=function(o,a){gc.point=n,r=(t=o)*Na,u=Math.cos(a=(e=a)*Na/2+Sa/4),i=Math.sin(a)},gc.lineEnd=function(){n(t,e)}}function se(n){var t=n[0],e=n[1],r=Math.cos(e);return[r*Math.cos(t),r*Math.sin(t),Math.sin(e)]}function le(n,t){return n[0]*t[0]+n[1]*t[1]+n[2]*t[2]}function fe(n,t){return[n[1]*t[2]-n[2]*t[1],n[2]*t[0]-n[0]*t[2],n[0]*t[1]-n[1]*t[0]]}function he(n,t){n[0]+=t[0],n[1]+=t[1],n[2]+=t[2]}function ge(n,t){return[n[0]*t,n[1]*t,n[2]*t]}function pe(n){var t=Math.sqrt(n[0]*n[0]+n[1]*n[1]+n[2]*n[2]);n[0]/=t,n[1]/=t,n[2]/=t}function ve(n){return[Math.atan2(n[1],n[0]),X(n[2])]}function de(n,t){return oa(n[0]-t[0])<Aa&&oa(n[1]-t[1])<Aa}function me(n,t){n*=Na;var e=Math.cos(t*=Na);ye(e*Math.cos(n),e*Math.sin(n),Math.sin(t))}function ye(n,t,e){++pc,dc+=(n-dc)/pc,mc+=(t-mc)/pc,yc+=(e-yc)/pc}function xe(){function n(n,u){n*=Na;var i=Math.cos(u*=Na),o=i*Math.cos(n),a=i*Math.sin(n),c=Math.sin(u),s=Math.atan2(Math.sqrt((s=e*c-r*a)*s+(s=r*o-t*c)*s+(s=t*a-e*o)*s),t*o+e*a+r*c);vc+=s,xc+=s*(t+(t=o)),Mc+=s*(e+(e=a)),_c+=s*(r+(r=c)),ye(t,e,r)}var t,e,r;kc.point=function(u,i){u*=Na;var o=Math.cos(i*=Na);t=o*Math.cos(u),e=o*Math.sin(u),r=Math.sin(i),kc.point=n,ye(t,e,r)}}function Me(){kc.point=me}function _e(){function n(n,t){n*=Na;var e=Math.cos(t*=Na),o=e*Math.cos(n),a=e*Math.sin(n),c=Math.sin(t),s=u*c-i*a,l=i*o-r*c,f=r*a-u*o,h=Math.sqrt(s*s+l*l+f*f),g=r*o+u*a+i*c,p=h&&-V(g)/h,v=Math.atan2(h,g);bc+=p*s,wc+=p*l,Sc+=p*f,vc+=v,xc+=v*(r+(r=o)),Mc+=v*(u+(u=a)),_c+=v*(i+(i=c)),ye(r,u,i)}var t,e,r,u,i;kc.point=function(o,a){t=o,e=a,kc.point=n,o*=Na;var c=Math.cos(a*=Na);r=c*Math.cos(o),u=c*Math.sin(o),i=Math.sin(a),ye(r,u,i)},kc.lineEnd=function(){n(t,e),kc.lineEnd=Me,kc.point=me}}function be(){return!0}function we(n,t,e,r,u){var i=[],o=[];if(n.forEach(function(n){if(!((t=n.length-1)<=0)){var t,e=n[0],r=n[t];if(de(e,r)){u.lineStart();for(var a=0;t>a;++a)u.point((e=n[a])[0],e[1]);return u.lineEnd(),void 0}var c=new ke(e,n,null,!0),s=new ke(e,null,c,!1);c.o=s,i.push(c),o.push(s),c=new ke(r,n,null,!1),s=new ke(r,null,c,!0),c.o=s,i.push(c),o.push(s)}}),o.sort(t),Se(i),Se(o),i.length){for(var a=0,c=e,s=o.length;s>a;++a)o[a].e=c=!c;for(var l,f,h=i[0];;){for(var g=h,p=!0;g.v;)if((g=g.n)===h)return;l=g.z,u.lineStart();do{if(g.v=g.o.v=!0,g.e){if(p)for(var a=0,s=l.length;s>a;++a)u.point((f=l[a])[0],f[1]);else r(g.x,g.n.x,1,u);g=g.n}else{if(p){l=g.p.z;for(var a=l.length-1;a>=0;--a)u.point((f=l[a])[0],f[1])}else r(g.x,g.p.x,-1,u);g=g.p}g=g.o,l=g.z,p=!p}while(!g.v);u.lineEnd()}}}function Se(n){if(t=n.length){for(var t,e,r=0,u=n[0];++r<t;)u.n=e=n[r],e.p=u,u=e;u.n=e=n[0],e.p=u}}function ke(n,t,e,r){this.x=n,this.z=t,this.o=e,this.e=r,this.v=!1,this.n=this.p=null}function Ee(n,t,e,r){return function(u,i){function o(t,e){var r=u(t,e);n(t=r[0],e=r[1])&&i.point(t,e)}function a(n,t){var e=u(n,t);d.point(e[0],e[1])}function c(){y.point=a,d.lineStart()}function s(){y.point=o,d.lineEnd()}function l(n,t){v.push([n,t]);var e=u(n,t);M.point(e[0],e[1])}function f(){M.lineStart(),v=[]}function h(){l(v[0][0],v[0][1]),M.lineEnd();var n,t=M.clean(),e=x.buffer(),r=e.length;if(v.pop(),p.push(v),v=null,r){if(1&t){n=e[0];var u,r=n.length-1,o=-1;for(i.lineStart();++o<r;)i.point((u=n[o])[0],u[1]);return i.lineEnd(),void 0}r>1&&2&t&&e.push(e.pop().concat(e.shift())),g.push(e.filter(Ae))}}var g,p,v,d=t(i),m=u.invert(r[0],r[1]),y={point:o,lineStart:c,lineEnd:s,polygonStart:function(){y.point=l,y.lineStart=f,y.lineEnd=h,g=[],p=[],i.polygonStart()},polygonEnd:function(){y.point=o,y.lineStart=c,y.lineEnd=s,g=Xo.merge(g);var n=Le(m,p);g.length?we(g,Ne,n,e,i):n&&(i.lineStart(),e(null,null,1,i),i.lineEnd()),i.polygonEnd(),g=p=null},sphere:function(){i.polygonStart(),i.lineStart(),e(null,null,1,i),i.lineEnd(),i.polygonEnd()}},x=Ce(),M=t(x);return y}}function Ae(n){return n.length>1}function Ce(){var n,t=[];return{lineStart:function(){t.push(n=[])},point:function(t,e){n.push([t,e])},lineEnd:g,buffer:function(){var e=t;return t=[],n=null,e},rejoin:function(){t.length>1&&t.push(t.pop().concat(t.shift()))}}}function Ne(n,t){return((n=n.x)[0]<0?n[1]-Ea-Aa:Ea-n[1])-((t=t.x)[0]<0?t[1]-Ea-Aa:Ea-t[1])}function Le(n,t){var e=n[0],r=n[1],u=[Math.sin(e),-Math.cos(e),0],i=0,o=0;hc.reset();for(var a=0,c=t.length;c>a;++a){var s=t[a],l=s.length;if(l)for(var f=s[0],h=f[0],g=f[1]/2+Sa/4,p=Math.sin(g),v=Math.cos(g),d=1;;){d===l&&(d=0),n=s[d];var m=n[0],y=n[1]/2+Sa/4,x=Math.sin(y),M=Math.cos(y),_=m-h,b=oa(_)>Sa,w=p*x;if(hc.add(Math.atan2(w*Math.sin(_),v*M+w*Math.cos(_))),i+=b?_+(_>=0?ka:-ka):_,b^h>=e^m>=e){var S=fe(se(f),se(n));pe(S);var k=fe(u,S);pe(k);var E=(b^_>=0?-1:1)*X(k[2]);(r>E||r===E&&(S[0]||S[1]))&&(o+=b^_>=0?1:-1)}if(!d++)break;h=m,p=x,v=M,f=n}}return(-Aa>i||Aa>i&&0>hc)^1&o}function ze(n){var t,e=0/0,r=0/0,u=0/0;return{lineStart:function(){n.lineStart(),t=1},point:function(i,o){var a=i>0?Sa:-Sa,c=oa(i-e);oa(c-Sa)<Aa?(n.point(e,r=(r+o)/2>0?Ea:-Ea),n.point(u,r),n.lineEnd(),n.lineStart(),n.point(a,r),n.point(i,r),t=0):u!==a&&c>=Sa&&(oa(e-u)<Aa&&(e-=u*Aa),oa(i-a)<Aa&&(i-=a*Aa),r=qe(e,r,i,o),n.point(u,r),n.lineEnd(),n.lineStart(),n.point(a,r),t=0),n.point(e=i,r=o),u=a},lineEnd:function(){n.lineEnd(),e=r=0/0},clean:function(){return 2-t}}}function qe(n,t,e,r){var u,i,o=Math.sin(n-e);return oa(o)>Aa?Math.atan((Math.sin(t)*(i=Math.cos(r))*Math.sin(e)-Math.sin(r)*(u=Math.cos(t))*Math.sin(n))/(u*i*o)):(t+r)/2}function Te(n,t,e,r){var u;if(null==n)u=e*Ea,r.point(-Sa,u),r.point(0,u),r.point(Sa,u),r.point(Sa,0),r.point(Sa,-u),r.point(0,-u),r.point(-Sa,-u),r.point(-Sa,0),r.point(-Sa,u);else if(oa(n[0]-t[0])>Aa){var i=n[0]<t[0]?Sa:-Sa;u=e*i/2,r.point(-i,u),r.point(0,u),r.point(i,u)}else r.point(t[0],t[1])}function Re(n){function t(n,t){return Math.cos(n)*Math.cos(t)>i}function e(n){var e,i,c,s,l;return{lineStart:function(){s=c=!1,l=1},point:function(f,h){var g,p=[f,h],v=t(f,h),d=o?v?0:u(f,h):v?u(f+(0>f?Sa:-Sa),h):0;if(!e&&(s=c=v)&&n.lineStart(),v!==c&&(g=r(e,p),(de(e,g)||de(p,g))&&(p[0]+=Aa,p[1]+=Aa,v=t(p[0],p[1]))),v!==c)l=0,v?(n.lineStart(),g=r(p,e),n.point(g[0],g[1])):(g=r(e,p),n.point(g[0],g[1]),n.lineEnd()),e=g;else if(a&&e&&o^v){var m;d&i||!(m=r(p,e,!0))||(l=0,o?(n.lineStart(),n.point(m[0][0],m[0][1]),n.point(m[1][0],m[1][1]),n.lineEnd()):(n.point(m[1][0],m[1][1]),n.lineEnd(),n.lineStart(),n.point(m[0][0],m[0][1])))}!v||e&&de(e,p)||n.point(p[0],p[1]),e=p,c=v,i=d},lineEnd:function(){c&&n.lineEnd(),e=null},clean:function(){return l|(s&&c)<<1}}}function r(n,t,e){var r=se(n),u=se(t),o=[1,0,0],a=fe(r,u),c=le(a,a),s=a[0],l=c-s*s;if(!l)return!e&&n;var f=i*c/l,h=-i*s/l,g=fe(o,a),p=ge(o,f),v=ge(a,h);he(p,v);var d=g,m=le(p,d),y=le(d,d),x=m*m-y*(le(p,p)-1);if(!(0>x)){var M=Math.sqrt(x),_=ge(d,(-m-M)/y);if(he(_,p),_=ve(_),!e)return _;var b,w=n[0],S=t[0],k=n[1],E=t[1];w>S&&(b=w,w=S,S=b);var A=S-w,C=oa(A-Sa)<Aa,N=C||Aa>A;if(!C&&k>E&&(b=k,k=E,E=b),N?C?k+E>0^_[1]<(oa(_[0]-w)<Aa?k:E):k<=_[1]&&_[1]<=E:A>Sa^(w<=_[0]&&_[0]<=S)){var L=ge(d,(-m+M)/y);return he(L,p),[_,ve(L)]}}}function u(t,e){var r=o?n:Sa-n,u=0;return-r>t?u|=1:t>r&&(u|=2),-r>e?u|=4:e>r&&(u|=8),u}var i=Math.cos(n),o=i>0,a=oa(i)>Aa,c=cr(n,6*Na);return Ee(t,e,c,o?[0,-n]:[-Sa,n-Sa])}function De(n,t,e,r){return function(u){var i,o=u.a,a=u.b,c=o.x,s=o.y,l=a.x,f=a.y,h=0,g=1,p=l-c,v=f-s;if(i=n-c,p||!(i>0)){if(i/=p,0>p){if(h>i)return;g>i&&(g=i)}else if(p>0){if(i>g)return;i>h&&(h=i)}if(i=e-c,p||!(0>i)){if(i/=p,0>p){if(i>g)return;i>h&&(h=i)}else if(p>0){if(h>i)return;g>i&&(g=i)}if(i=t-s,v||!(i>0)){if(i/=v,0>v){if(h>i)return;g>i&&(g=i)}else if(v>0){if(i>g)return;i>h&&(h=i)}if(i=r-s,v||!(0>i)){if(i/=v,0>v){if(i>g)return;i>h&&(h=i)}else if(v>0){if(h>i)return;g>i&&(g=i)}return h>0&&(u.a={x:c+h*p,y:s+h*v}),1>g&&(u.b={x:c+g*p,y:s+g*v}),u}}}}}}function Pe(n,t,e,r){function u(r,u){return oa(r[0]-n)<Aa?u>0?0:3:oa(r[0]-e)<Aa?u>0?2:1:oa(r[1]-t)<Aa?u>0?1:0:u>0?3:2}function i(n,t){return o(n.x,t.x)}function o(n,t){var e=u(n,1),r=u(t,1);return e!==r?e-r:0===e?t[1]-n[1]:1===e?n[0]-t[0]:2===e?n[1]-t[1]:t[0]-n[0]}return function(a){function c(n){for(var t=0,e=d.length,r=n[1],u=0;e>u;++u)for(var i,o=1,a=d[u],c=a.length,s=a[0];c>o;++o)i=a[o],s[1]<=r?i[1]>r&&Z(s,i,n)>0&&++t:i[1]<=r&&Z(s,i,n)<0&&--t,s=i;return 0!==t}function s(i,a,c,s){var l=0,f=0;if(null==i||(l=u(i,c))!==(f=u(a,c))||o(i,a)<0^c>0){do s.point(0===l||3===l?n:e,l>1?r:t);while((l=(l+c+4)%4)!==f)}else s.point(a[0],a[1])}function l(u,i){return u>=n&&e>=u&&i>=t&&r>=i}function f(n,t){l(n,t)&&a.point(n,t)}function h(){N.point=p,d&&d.push(m=[]),S=!0,w=!1,_=b=0/0}function g(){v&&(p(y,x),M&&w&&A.rejoin(),v.push(A.buffer())),N.point=f,w&&a.lineEnd()}function p(n,t){n=Math.max(-Ac,Math.min(Ac,n)),t=Math.max(-Ac,Math.min(Ac,t));var e=l(n,t);if(d&&m.push([n,t]),S)y=n,x=t,M=e,S=!1,e&&(a.lineStart(),a.point(n,t));else if(e&&w)a.point(n,t);else{var r={a:{x:_,y:b},b:{x:n,y:t}};C(r)?(w||(a.lineStart(),a.point(r.a.x,r.a.y)),a.point(r.b.x,r.b.y),e||a.lineEnd(),k=!1):e&&(a.lineStart(),a.point(n,t),k=!1)}_=n,b=t,w=e}var v,d,m,y,x,M,_,b,w,S,k,E=a,A=Ce(),C=De(n,t,e,r),N={point:f,lineStart:h,lineEnd:g,polygonStart:function(){a=A,v=[],d=[],k=!0},polygonEnd:function(){a=E,v=Xo.merge(v);var t=c([n,r]),e=k&&t,u=v.length;(e||u)&&(a.polygonStart(),e&&(a.lineStart(),s(null,null,1,a),a.lineEnd()),u&&we(v,i,t,s,a),a.polygonEnd()),v=d=m=null}};return N}}function Ue(n,t){function e(e,r){return e=n(e,r),t(e[0],e[1])}return n.invert&&t.invert&&(e.invert=function(e,r){return e=t.invert(e,r),e&&n.invert(e[0],e[1])}),e}function je(n){var t=0,e=Sa/3,r=nr(n),u=r(t,e);return u.parallels=function(n){return arguments.length?r(t=n[0]*Sa/180,e=n[1]*Sa/180):[180*(t/Sa),180*(e/Sa)]},u}function He(n,t){function e(n,t){var e=Math.sqrt(i-2*u*Math.sin(t))/u;return[e*Math.sin(n*=u),o-e*Math.cos(n)]}var r=Math.sin(n),u=(r+Math.sin(t))/2,i=1+r*(2*u-r),o=Math.sqrt(i)/u;return e.invert=function(n,t){var e=o-t;return[Math.atan2(n,e)/u,X((i-(n*n+e*e)*u*u)/(2*u))]},e}function Fe(){function n(n,t){Nc+=u*n-r*t,r=n,u=t}var t,e,r,u;Rc.point=function(i,o){Rc.point=n,t=r=i,e=u=o},Rc.lineEnd=function(){n(t,e)}}function Oe(n,t){Lc>n&&(Lc=n),n>qc&&(qc=n),zc>t&&(zc=t),t>Tc&&(Tc=t)}function Ye(){function n(n,t){o.push("M",n,",",t,i)}function t(n,t){o.push("M",n,",",t),a.point=e}function e(n,t){o.push("L",n,",",t)}function r(){a.point=n}function u(){o.push("Z")}var i=Ie(4.5),o=[],a={point:n,lineStart:function(){a.point=t},lineEnd:r,polygonStart:function(){a.lineEnd=u},polygonEnd:function(){a.lineEnd=r,a.point=n},pointRadius:function(n){return i=Ie(n),a},result:function(){if(o.length){var n=o.join("");return o=[],n}}};return a}function Ie(n){return"m0,"+n+"a"+n+","+n+" 0 1,1 0,"+-2*n+"a"+n+","+n+" 0 1,1 0,"+2*n+"z"}function Ze(n,t){dc+=n,mc+=t,++yc}function Ve(){function n(n,r){var u=n-t,i=r-e,o=Math.sqrt(u*u+i*i);xc+=o*(t+n)/2,Mc+=o*(e+r)/2,_c+=o,Ze(t=n,e=r)}var t,e;Pc.point=function(r,u){Pc.point=n,Ze(t=r,e=u)}}function Xe(){Pc.point=Ze}function $e(){function n(n,t){var e=n-r,i=t-u,o=Math.sqrt(e*e+i*i);xc+=o*(r+n)/2,Mc+=o*(u+t)/2,_c+=o,o=u*n-r*t,bc+=o*(r+n),wc+=o*(u+t),Sc+=3*o,Ze(r=n,u=t)}var t,e,r,u;Pc.point=function(i,o){Pc.point=n,Ze(t=r=i,e=u=o)},Pc.lineEnd=function(){n(t,e)}}function Be(n){function t(t,e){n.moveTo(t,e),n.arc(t,e,o,0,ka)}function e(t,e){n.moveTo(t,e),a.point=r}function r(t,e){n.lineTo(t,e)}function u(){a.point=t}function i(){n.closePath()}var o=4.5,a={point:t,lineStart:function(){a.point=e},lineEnd:u,polygonStart:function(){a.lineEnd=i},polygonEnd:function(){a.lineEnd=u,a.point=t},pointRadius:function(n){return o=n,a},result:g};return a}function We(n){function t(n){return(a?r:e)(n)}function e(t){return Ke(t,function(e,r){e=n(e,r),t.point(e[0],e[1])})}function r(t){function e(e,r){e=n(e,r),t.point(e[0],e[1])}function r(){x=0/0,S.point=i,t.lineStart()}function i(e,r){var i=se([e,r]),o=n(e,r);u(x,M,y,_,b,w,x=o[0],M=o[1],y=e,_=i[0],b=i[1],w=i[2],a,t),t.point(x,M)}function o(){S.point=e,t.lineEnd()}function c(){r(),S.point=s,S.lineEnd=l}function s(n,t){i(f=n,h=t),g=x,p=M,v=_,d=b,m=w,S.point=i}function l(){u(x,M,y,_,b,w,g,p,f,v,d,m,a,t),S.lineEnd=o,o()}var f,h,g,p,v,d,m,y,x,M,_,b,w,S={point:e,lineStart:r,lineEnd:o,polygonStart:function(){t.polygonStart(),S.lineStart=c},polygonEnd:function(){t.polygonEnd(),S.lineStart=r}};return S}function u(t,e,r,a,c,s,l,f,h,g,p,v,d,m){var y=l-t,x=f-e,M=y*y+x*x;if(M>4*i&&d--){var _=a+g,b=c+p,w=s+v,S=Math.sqrt(_*_+b*b+w*w),k=Math.asin(w/=S),E=oa(oa(w)-1)<Aa||oa(r-h)<Aa?(r+h)/2:Math.atan2(b,_),A=n(E,k),C=A[0],N=A[1],L=C-t,z=N-e,q=x*L-y*z;(q*q/M>i||oa((y*L+x*z)/M-.5)>.3||o>a*g+c*p+s*v)&&(u(t,e,r,a,c,s,C,N,E,_/=S,b/=S,w,d,m),m.point(C,N),u(C,N,E,_,b,w,l,f,h,g,p,v,d,m))}}var i=.5,o=Math.cos(30*Na),a=16;return t.precision=function(n){return arguments.length?(a=(i=n*n)>0&&16,t):Math.sqrt(i)},t}function Je(n){var t=We(function(t,e){return n([t*La,e*La])});return function(n){return tr(t(n))}}function Ge(n){this.stream=n}function Ke(n,t){return{point:t,sphere:function(){n.sphere()},lineStart:function(){n.lineStart()},lineEnd:function(){n.lineEnd()},polygonStart:function(){n.polygonStart()},polygonEnd:function(){n.polygonEnd()}}}function Qe(n){return nr(function(){return n})()}function nr(n){function t(n){return n=a(n[0]*Na,n[1]*Na),[n[0]*h+c,s-n[1]*h]}function e(n){return n=a.invert((n[0]-c)/h,(s-n[1])/h),n&&[n[0]*La,n[1]*La]}function r(){a=Ue(o=ur(m,y,x),i);var n=i(v,d);return c=g-n[0]*h,s=p+n[1]*h,u()}function u(){return l&&(l.valid=!1,l=null),t}var i,o,a,c,s,l,f=We(function(n,t){return n=i(n,t),[n[0]*h+c,s-n[1]*h]}),h=150,g=480,p=250,v=0,d=0,m=0,y=0,x=0,M=Ec,_=bt,b=null,w=null;return t.stream=function(n){return l&&(l.valid=!1),l=tr(M(o,f(_(n)))),l.valid=!0,l},t.clipAngle=function(n){return arguments.length?(M=null==n?(b=n,Ec):Re((b=+n)*Na),u()):b
2
+ },t.clipExtent=function(n){return arguments.length?(w=n,_=n?Pe(n[0][0],n[0][1],n[1][0],n[1][1]):bt,u()):w},t.scale=function(n){return arguments.length?(h=+n,r()):h},t.translate=function(n){return arguments.length?(g=+n[0],p=+n[1],r()):[g,p]},t.center=function(n){return arguments.length?(v=n[0]%360*Na,d=n[1]%360*Na,r()):[v*La,d*La]},t.rotate=function(n){return arguments.length?(m=n[0]%360*Na,y=n[1]%360*Na,x=n.length>2?n[2]%360*Na:0,r()):[m*La,y*La,x*La]},Xo.rebind(t,f,"precision"),function(){return i=n.apply(this,arguments),t.invert=i.invert&&e,r()}}function tr(n){return Ke(n,function(t,e){n.point(t*Na,e*Na)})}function er(n,t){return[n,t]}function rr(n,t){return[n>Sa?n-ka:-Sa>n?n+ka:n,t]}function ur(n,t,e){return n?t||e?Ue(or(n),ar(t,e)):or(n):t||e?ar(t,e):rr}function ir(n){return function(t,e){return t+=n,[t>Sa?t-ka:-Sa>t?t+ka:t,e]}}function or(n){var t=ir(n);return t.invert=ir(-n),t}function ar(n,t){function e(n,t){var e=Math.cos(t),a=Math.cos(n)*e,c=Math.sin(n)*e,s=Math.sin(t),l=s*r+a*u;return[Math.atan2(c*i-l*o,a*r-s*u),X(l*i+c*o)]}var r=Math.cos(n),u=Math.sin(n),i=Math.cos(t),o=Math.sin(t);return e.invert=function(n,t){var e=Math.cos(t),a=Math.cos(n)*e,c=Math.sin(n)*e,s=Math.sin(t),l=s*i-c*o;return[Math.atan2(c*i+s*o,a*r+l*u),X(l*r-a*u)]},e}function cr(n,t){var e=Math.cos(n),r=Math.sin(n);return function(u,i,o,a){var c=o*t;null!=u?(u=sr(e,u),i=sr(e,i),(o>0?i>u:u>i)&&(u+=o*ka)):(u=n+o*ka,i=n-.5*c);for(var s,l=u;o>0?l>i:i>l;l-=c)a.point((s=ve([e,-r*Math.cos(l),-r*Math.sin(l)]))[0],s[1])}}function sr(n,t){var e=se(t);e[0]-=n,pe(e);var r=V(-e[1]);return((-e[2]<0?-r:r)+2*Math.PI-Aa)%(2*Math.PI)}function lr(n,t,e){var r=Xo.range(n,t-Aa,e).concat(t);return function(n){return r.map(function(t){return[n,t]})}}function fr(n,t,e){var r=Xo.range(n,t-Aa,e).concat(t);return function(n){return r.map(function(t){return[t,n]})}}function hr(n){return n.source}function gr(n){return n.target}function pr(n,t,e,r){var u=Math.cos(t),i=Math.sin(t),o=Math.cos(r),a=Math.sin(r),c=u*Math.cos(n),s=u*Math.sin(n),l=o*Math.cos(e),f=o*Math.sin(e),h=2*Math.asin(Math.sqrt(J(r-t)+u*o*J(e-n))),g=1/Math.sin(h),p=h?function(n){var t=Math.sin(n*=h)*g,e=Math.sin(h-n)*g,r=e*c+t*l,u=e*s+t*f,o=e*i+t*a;return[Math.atan2(u,r)*La,Math.atan2(o,Math.sqrt(r*r+u*u))*La]}:function(){return[n*La,t*La]};return p.distance=h,p}function vr(){function n(n,u){var i=Math.sin(u*=Na),o=Math.cos(u),a=oa((n*=Na)-t),c=Math.cos(a);Uc+=Math.atan2(Math.sqrt((a=o*Math.sin(a))*a+(a=r*i-e*o*c)*a),e*i+r*o*c),t=n,e=i,r=o}var t,e,r;jc.point=function(u,i){t=u*Na,e=Math.sin(i*=Na),r=Math.cos(i),jc.point=n},jc.lineEnd=function(){jc.point=jc.lineEnd=g}}function dr(n,t){function e(t,e){var r=Math.cos(t),u=Math.cos(e),i=n(r*u);return[i*u*Math.sin(t),i*Math.sin(e)]}return e.invert=function(n,e){var r=Math.sqrt(n*n+e*e),u=t(r),i=Math.sin(u),o=Math.cos(u);return[Math.atan2(n*i,r*o),Math.asin(r&&e*i/r)]},e}function mr(n,t){function e(n,t){var e=oa(oa(t)-Ea)<Aa?0:o/Math.pow(u(t),i);return[e*Math.sin(i*n),o-e*Math.cos(i*n)]}var r=Math.cos(n),u=function(n){return Math.tan(Sa/4+n/2)},i=n===t?Math.sin(n):Math.log(r/Math.cos(t))/Math.log(u(t)/u(n)),o=r*Math.pow(u(n),i)/i;return i?(e.invert=function(n,t){var e=o-t,r=I(i)*Math.sqrt(n*n+e*e);return[Math.atan2(n,e)/i,2*Math.atan(Math.pow(o/r,1/i))-Ea]},e):xr}function yr(n,t){function e(n,t){var e=i-t;return[e*Math.sin(u*n),i-e*Math.cos(u*n)]}var r=Math.cos(n),u=n===t?Math.sin(n):(r-Math.cos(t))/(t-n),i=r/u+n;return oa(u)<Aa?er:(e.invert=function(n,t){var e=i-t;return[Math.atan2(n,e)/u,i-I(u)*Math.sqrt(n*n+e*e)]},e)}function xr(n,t){return[n,Math.log(Math.tan(Sa/4+t/2))]}function Mr(n){var t,e=Qe(n),r=e.scale,u=e.translate,i=e.clipExtent;return e.scale=function(){var n=r.apply(e,arguments);return n===e?t?e.clipExtent(null):e:n},e.translate=function(){var n=u.apply(e,arguments);return n===e?t?e.clipExtent(null):e:n},e.clipExtent=function(n){var o=i.apply(e,arguments);if(o===e){if(t=null==n){var a=Sa*r(),c=u();i([[c[0]-a,c[1]-a],[c[0]+a,c[1]+a]])}}else t&&(o=null);return o},e.clipExtent(null)}function _r(n,t){return[Math.log(Math.tan(Sa/4+t/2)),-n]}function br(n){return n[0]}function wr(n){return n[1]}function Sr(n){for(var t=n.length,e=[0,1],r=2,u=2;t>u;u++){for(;r>1&&Z(n[e[r-2]],n[e[r-1]],n[u])<=0;)--r;e[r++]=u}return e.slice(0,r)}function kr(n,t){return n[0]-t[0]||n[1]-t[1]}function Er(n,t,e){return(e[0]-t[0])*(n[1]-t[1])<(e[1]-t[1])*(n[0]-t[0])}function Ar(n,t,e,r){var u=n[0],i=e[0],o=t[0]-u,a=r[0]-i,c=n[1],s=e[1],l=t[1]-c,f=r[1]-s,h=(a*(c-s)-f*(u-i))/(f*o-a*l);return[u+h*o,c+h*l]}function Cr(n){var t=n[0],e=n[n.length-1];return!(t[0]-e[0]||t[1]-e[1])}function Nr(){Jr(this),this.edge=this.site=this.circle=null}function Lr(n){var t=Jc.pop()||new Nr;return t.site=n,t}function zr(n){Or(n),$c.remove(n),Jc.push(n),Jr(n)}function qr(n){var t=n.circle,e=t.x,r=t.cy,u={x:e,y:r},i=n.P,o=n.N,a=[n];zr(n);for(var c=i;c.circle&&oa(e-c.circle.x)<Aa&&oa(r-c.circle.cy)<Aa;)i=c.P,a.unshift(c),zr(c),c=i;a.unshift(c),Or(c);for(var s=o;s.circle&&oa(e-s.circle.x)<Aa&&oa(r-s.circle.cy)<Aa;)o=s.N,a.push(s),zr(s),s=o;a.push(s),Or(s);var l,f=a.length;for(l=1;f>l;++l)s=a[l],c=a[l-1],$r(s.edge,c.site,s.site,u);c=a[0],s=a[f-1],s.edge=Vr(c.site,s.site,null,u),Fr(c),Fr(s)}function Tr(n){for(var t,e,r,u,i=n.x,o=n.y,a=$c._;a;)if(r=Rr(a,o)-i,r>Aa)a=a.L;else{if(u=i-Dr(a,o),!(u>Aa)){r>-Aa?(t=a.P,e=a):u>-Aa?(t=a,e=a.N):t=e=a;break}if(!a.R){t=a;break}a=a.R}var c=Lr(n);if($c.insert(t,c),t||e){if(t===e)return Or(t),e=Lr(t.site),$c.insert(c,e),c.edge=e.edge=Vr(t.site,c.site),Fr(t),Fr(e),void 0;if(!e)return c.edge=Vr(t.site,c.site),void 0;Or(t),Or(e);var s=t.site,l=s.x,f=s.y,h=n.x-l,g=n.y-f,p=e.site,v=p.x-l,d=p.y-f,m=2*(h*d-g*v),y=h*h+g*g,x=v*v+d*d,M={x:(d*y-g*x)/m+l,y:(h*x-v*y)/m+f};$r(e.edge,s,p,M),c.edge=Vr(s,n,null,M),e.edge=Vr(n,p,null,M),Fr(t),Fr(e)}}function Rr(n,t){var e=n.site,r=e.x,u=e.y,i=u-t;if(!i)return r;var o=n.P;if(!o)return-1/0;e=o.site;var a=e.x,c=e.y,s=c-t;if(!s)return a;var l=a-r,f=1/i-1/s,h=l/s;return f?(-h+Math.sqrt(h*h-2*f*(l*l/(-2*s)-c+s/2+u-i/2)))/f+r:(r+a)/2}function Dr(n,t){var e=n.N;if(e)return Rr(e,t);var r=n.site;return r.y===t?r.x:1/0}function Pr(n){this.site=n,this.edges=[]}function Ur(n){for(var t,e,r,u,i,o,a,c,s,l,f=n[0][0],h=n[1][0],g=n[0][1],p=n[1][1],v=Xc,d=v.length;d--;)if(i=v[d],i&&i.prepare())for(a=i.edges,c=a.length,o=0;c>o;)l=a[o].end(),r=l.x,u=l.y,s=a[++o%c].start(),t=s.x,e=s.y,(oa(r-t)>Aa||oa(u-e)>Aa)&&(a.splice(o,0,new Br(Xr(i.site,l,oa(r-f)<Aa&&p-u>Aa?{x:f,y:oa(t-f)<Aa?e:p}:oa(u-p)<Aa&&h-r>Aa?{x:oa(e-p)<Aa?t:h,y:p}:oa(r-h)<Aa&&u-g>Aa?{x:h,y:oa(t-h)<Aa?e:g}:oa(u-g)<Aa&&r-f>Aa?{x:oa(e-g)<Aa?t:f,y:g}:null),i.site,null)),++c)}function jr(n,t){return t.angle-n.angle}function Hr(){Jr(this),this.x=this.y=this.arc=this.site=this.cy=null}function Fr(n){var t=n.P,e=n.N;if(t&&e){var r=t.site,u=n.site,i=e.site;if(r!==i){var o=u.x,a=u.y,c=r.x-o,s=r.y-a,l=i.x-o,f=i.y-a,h=2*(c*f-s*l);if(!(h>=-Ca)){var g=c*c+s*s,p=l*l+f*f,v=(f*g-s*p)/h,d=(c*p-l*g)/h,f=d+a,m=Gc.pop()||new Hr;m.arc=n,m.site=u,m.x=v+o,m.y=f+Math.sqrt(v*v+d*d),m.cy=f,n.circle=m;for(var y=null,x=Wc._;x;)if(m.y<x.y||m.y===x.y&&m.x<=x.x){if(!x.L){y=x.P;break}x=x.L}else{if(!x.R){y=x;break}x=x.R}Wc.insert(y,m),y||(Bc=m)}}}}function Or(n){var t=n.circle;t&&(t.P||(Bc=t.N),Wc.remove(t),Gc.push(t),Jr(t),n.circle=null)}function Yr(n){for(var t,e=Vc,r=De(n[0][0],n[0][1],n[1][0],n[1][1]),u=e.length;u--;)t=e[u],(!Ir(t,n)||!r(t)||oa(t.a.x-t.b.x)<Aa&&oa(t.a.y-t.b.y)<Aa)&&(t.a=t.b=null,e.splice(u,1))}function Ir(n,t){var e=n.b;if(e)return!0;var r,u,i=n.a,o=t[0][0],a=t[1][0],c=t[0][1],s=t[1][1],l=n.l,f=n.r,h=l.x,g=l.y,p=f.x,v=f.y,d=(h+p)/2,m=(g+v)/2;if(v===g){if(o>d||d>=a)return;if(h>p){if(i){if(i.y>=s)return}else i={x:d,y:c};e={x:d,y:s}}else{if(i){if(i.y<c)return}else i={x:d,y:s};e={x:d,y:c}}}else if(r=(h-p)/(v-g),u=m-r*d,-1>r||r>1)if(h>p){if(i){if(i.y>=s)return}else i={x:(c-u)/r,y:c};e={x:(s-u)/r,y:s}}else{if(i){if(i.y<c)return}else i={x:(s-u)/r,y:s};e={x:(c-u)/r,y:c}}else if(v>g){if(i){if(i.x>=a)return}else i={x:o,y:r*o+u};e={x:a,y:r*a+u}}else{if(i){if(i.x<o)return}else i={x:a,y:r*a+u};e={x:o,y:r*o+u}}return n.a=i,n.b=e,!0}function Zr(n,t){this.l=n,this.r=t,this.a=this.b=null}function Vr(n,t,e,r){var u=new Zr(n,t);return Vc.push(u),e&&$r(u,n,t,e),r&&$r(u,t,n,r),Xc[n.i].edges.push(new Br(u,n,t)),Xc[t.i].edges.push(new Br(u,t,n)),u}function Xr(n,t,e){var r=new Zr(n,null);return r.a=t,r.b=e,Vc.push(r),r}function $r(n,t,e,r){n.a||n.b?n.l===e?n.b=r:n.a=r:(n.a=r,n.l=t,n.r=e)}function Br(n,t,e){var r=n.a,u=n.b;this.edge=n,this.site=t,this.angle=e?Math.atan2(e.y-t.y,e.x-t.x):n.l===t?Math.atan2(u.x-r.x,r.y-u.y):Math.atan2(r.x-u.x,u.y-r.y)}function Wr(){this._=null}function Jr(n){n.U=n.C=n.L=n.R=n.P=n.N=null}function Gr(n,t){var e=t,r=t.R,u=e.U;u?u.L===e?u.L=r:u.R=r:n._=r,r.U=u,e.U=r,e.R=r.L,e.R&&(e.R.U=e),r.L=e}function Kr(n,t){var e=t,r=t.L,u=e.U;u?u.L===e?u.L=r:u.R=r:n._=r,r.U=u,e.U=r,e.L=r.R,e.L&&(e.L.U=e),r.R=e}function Qr(n){for(;n.L;)n=n.L;return n}function nu(n,t){var e,r,u,i=n.sort(tu).pop();for(Vc=[],Xc=new Array(n.length),$c=new Wr,Wc=new Wr;;)if(u=Bc,i&&(!u||i.y<u.y||i.y===u.y&&i.x<u.x))(i.x!==e||i.y!==r)&&(Xc[i.i]=new Pr(i),Tr(i),e=i.x,r=i.y),i=n.pop();else{if(!u)break;qr(u.arc)}t&&(Yr(t),Ur(t));var o={cells:Xc,edges:Vc};return $c=Wc=Vc=Xc=null,o}function tu(n,t){return t.y-n.y||t.x-n.x}function eu(n,t,e){return(n.x-e.x)*(t.y-n.y)-(n.x-t.x)*(e.y-n.y)}function ru(n){return n.x}function uu(n){return n.y}function iu(){return{leaf:!0,nodes:[],point:null,x:null,y:null}}function ou(n,t,e,r,u,i){if(!n(t,e,r,u,i)){var o=.5*(e+u),a=.5*(r+i),c=t.nodes;c[0]&&ou(n,c[0],e,r,o,a),c[1]&&ou(n,c[1],o,r,u,a),c[2]&&ou(n,c[2],e,a,o,i),c[3]&&ou(n,c[3],o,a,u,i)}}function au(n,t){n=Xo.rgb(n),t=Xo.rgb(t);var e=n.r,r=n.g,u=n.b,i=t.r-e,o=t.g-r,a=t.b-u;return function(n){return"#"+vt(Math.round(e+i*n))+vt(Math.round(r+o*n))+vt(Math.round(u+a*n))}}function cu(n,t){var e,r={},u={};for(e in n)e in t?r[e]=fu(n[e],t[e]):u[e]=n[e];for(e in t)e in n||(u[e]=t[e]);return function(n){for(e in r)u[e]=r[e](n);return u}}function su(n,t){return t-=n=+n,function(e){return n+t*e}}function lu(n,t){var e,r,u,i,o,a=0,c=0,s=[],l=[];for(n+="",t+="",Qc.lastIndex=0,r=0;e=Qc.exec(t);++r)e.index&&s.push(t.substring(a,c=e.index)),l.push({i:s.length,x:e[0]}),s.push(null),a=Qc.lastIndex;for(a<t.length&&s.push(t.substring(a)),r=0,i=l.length;(e=Qc.exec(n))&&i>r;++r)if(o=l[r],o.x==e[0]){if(o.i)if(null==s[o.i+1])for(s[o.i-1]+=o.x,s.splice(o.i,1),u=r+1;i>u;++u)l[u].i--;else for(s[o.i-1]+=o.x+s[o.i+1],s.splice(o.i,2),u=r+1;i>u;++u)l[u].i-=2;else if(null==s[o.i+1])s[o.i]=o.x;else for(s[o.i]=o.x+s[o.i+1],s.splice(o.i+1,1),u=r+1;i>u;++u)l[u].i--;l.splice(r,1),i--,r--}else o.x=su(parseFloat(e[0]),parseFloat(o.x));for(;i>r;)o=l.pop(),null==s[o.i+1]?s[o.i]=o.x:(s[o.i]=o.x+s[o.i+1],s.splice(o.i+1,1)),i--;return 1===s.length?null==s[0]?(o=l[0].x,function(n){return o(n)+""}):function(){return t}:function(n){for(r=0;i>r;++r)s[(o=l[r]).i]=o.x(n);return s.join("")}}function fu(n,t){for(var e,r=Xo.interpolators.length;--r>=0&&!(e=Xo.interpolators[r](n,t)););return e}function hu(n,t){var e,r=[],u=[],i=n.length,o=t.length,a=Math.min(n.length,t.length);for(e=0;a>e;++e)r.push(fu(n[e],t[e]));for(;i>e;++e)u[e]=n[e];for(;o>e;++e)u[e]=t[e];return function(n){for(e=0;a>e;++e)u[e]=r[e](n);return u}}function gu(n){return function(t){return 0>=t?0:t>=1?1:n(t)}}function pu(n){return function(t){return 1-n(1-t)}}function vu(n){return function(t){return.5*(.5>t?n(2*t):2-n(2-2*t))}}function du(n){return n*n}function mu(n){return n*n*n}function yu(n){if(0>=n)return 0;if(n>=1)return 1;var t=n*n,e=t*n;return 4*(.5>n?e:3*(n-t)+e-.75)}function xu(n){return function(t){return Math.pow(t,n)}}function Mu(n){return 1-Math.cos(n*Ea)}function _u(n){return Math.pow(2,10*(n-1))}function bu(n){return 1-Math.sqrt(1-n*n)}function wu(n,t){var e;return arguments.length<2&&(t=.45),arguments.length?e=t/ka*Math.asin(1/n):(n=1,e=t/4),function(r){return 1+n*Math.pow(2,-10*r)*Math.sin((r-e)*ka/t)}}function Su(n){return n||(n=1.70158),function(t){return t*t*((n+1)*t-n)}}function ku(n){return 1/2.75>n?7.5625*n*n:2/2.75>n?7.5625*(n-=1.5/2.75)*n+.75:2.5/2.75>n?7.5625*(n-=2.25/2.75)*n+.9375:7.5625*(n-=2.625/2.75)*n+.984375}function Eu(n,t){n=Xo.hcl(n),t=Xo.hcl(t);var e=n.h,r=n.c,u=n.l,i=t.h-e,o=t.c-r,a=t.l-u;return isNaN(o)&&(o=0,r=isNaN(r)?t.c:r),isNaN(i)?(i=0,e=isNaN(e)?t.h:e):i>180?i-=360:-180>i&&(i+=360),function(n){return rt(e+i*n,r+o*n,u+a*n)+""}}function Au(n,t){n=Xo.hsl(n),t=Xo.hsl(t);var e=n.h,r=n.s,u=n.l,i=t.h-e,o=t.s-r,a=t.l-u;return isNaN(o)&&(o=0,r=isNaN(r)?t.s:r),isNaN(i)?(i=0,e=isNaN(e)?t.h:e):i>180?i-=360:-180>i&&(i+=360),function(n){return nt(e+i*n,r+o*n,u+a*n)+""}}function Cu(n,t){n=Xo.lab(n),t=Xo.lab(t);var e=n.l,r=n.a,u=n.b,i=t.l-e,o=t.a-r,a=t.b-u;return function(n){return ot(e+i*n,r+o*n,u+a*n)+""}}function Nu(n,t){return t-=n,function(e){return Math.round(n+t*e)}}function Lu(n){var t=[n.a,n.b],e=[n.c,n.d],r=qu(t),u=zu(t,e),i=qu(Tu(e,t,-u))||0;t[0]*e[1]<e[0]*t[1]&&(t[0]*=-1,t[1]*=-1,r*=-1,u*=-1),this.rotate=(r?Math.atan2(t[1],t[0]):Math.atan2(-e[0],e[1]))*La,this.translate=[n.e,n.f],this.scale=[r,i],this.skew=i?Math.atan2(u,i)*La:0}function zu(n,t){return n[0]*t[0]+n[1]*t[1]}function qu(n){var t=Math.sqrt(zu(n,n));return t&&(n[0]/=t,n[1]/=t),t}function Tu(n,t,e){return n[0]+=e*t[0],n[1]+=e*t[1],n}function Ru(n,t){var e,r=[],u=[],i=Xo.transform(n),o=Xo.transform(t),a=i.translate,c=o.translate,s=i.rotate,l=o.rotate,f=i.skew,h=o.skew,g=i.scale,p=o.scale;return a[0]!=c[0]||a[1]!=c[1]?(r.push("translate(",null,",",null,")"),u.push({i:1,x:su(a[0],c[0])},{i:3,x:su(a[1],c[1])})):c[0]||c[1]?r.push("translate("+c+")"):r.push(""),s!=l?(s-l>180?l+=360:l-s>180&&(s+=360),u.push({i:r.push(r.pop()+"rotate(",null,")")-2,x:su(s,l)})):l&&r.push(r.pop()+"rotate("+l+")"),f!=h?u.push({i:r.push(r.pop()+"skewX(",null,")")-2,x:su(f,h)}):h&&r.push(r.pop()+"skewX("+h+")"),g[0]!=p[0]||g[1]!=p[1]?(e=r.push(r.pop()+"scale(",null,",",null,")"),u.push({i:e-4,x:su(g[0],p[0])},{i:e-2,x:su(g[1],p[1])})):(1!=p[0]||1!=p[1])&&r.push(r.pop()+"scale("+p+")"),e=u.length,function(n){for(var t,i=-1;++i<e;)r[(t=u[i]).i]=t.x(n);return r.join("")}}function Du(n,t){return t=t-(n=+n)?1/(t-n):0,function(e){return(e-n)*t}}function Pu(n,t){return t=t-(n=+n)?1/(t-n):0,function(e){return Math.max(0,Math.min(1,(e-n)*t))}}function Uu(n){for(var t=n.source,e=n.target,r=Hu(t,e),u=[t];t!==r;)t=t.parent,u.push(t);for(var i=u.length;e!==r;)u.splice(i,0,e),e=e.parent;return u}function ju(n){for(var t=[],e=n.parent;null!=e;)t.push(n),n=e,e=e.parent;return t.push(n),t}function Hu(n,t){if(n===t)return n;for(var e=ju(n),r=ju(t),u=e.pop(),i=r.pop(),o=null;u===i;)o=u,u=e.pop(),i=r.pop();return o}function Fu(n){n.fixed|=2}function Ou(n){n.fixed&=-7}function Yu(n){n.fixed|=4,n.px=n.x,n.py=n.y}function Iu(n){n.fixed&=-5}function Zu(n,t,e){var r=0,u=0;if(n.charge=0,!n.leaf)for(var i,o=n.nodes,a=o.length,c=-1;++c<a;)i=o[c],null!=i&&(Zu(i,t,e),n.charge+=i.charge,r+=i.charge*i.cx,u+=i.charge*i.cy);if(n.point){n.leaf||(n.point.x+=Math.random()-.5,n.point.y+=Math.random()-.5);var s=t*e[n.point.index];n.charge+=n.pointCharge=s,r+=s*n.point.x,u+=s*n.point.y}n.cx=r/n.charge,n.cy=u/n.charge}function Vu(n,t){return Xo.rebind(n,t,"sort","children","value"),n.nodes=n,n.links=Wu,n}function Xu(n){return n.children}function $u(n){return n.value}function Bu(n,t){return t.value-n.value}function Wu(n){return Xo.merge(n.map(function(n){return(n.children||[]).map(function(t){return{source:n,target:t}})}))}function Ju(n){return n.x}function Gu(n){return n.y}function Ku(n,t,e){n.y0=t,n.y=e}function Qu(n){return Xo.range(n.length)}function ni(n){for(var t=-1,e=n[0].length,r=[];++t<e;)r[t]=0;return r}function ti(n){for(var t,e=1,r=0,u=n[0][1],i=n.length;i>e;++e)(t=n[e][1])>u&&(r=e,u=t);return r}function ei(n){return n.reduce(ri,0)}function ri(n,t){return n+t[1]}function ui(n,t){return ii(n,Math.ceil(Math.log(t.length)/Math.LN2+1))}function ii(n,t){for(var e=-1,r=+n[0],u=(n[1]-r)/t,i=[];++e<=t;)i[e]=u*e+r;return i}function oi(n){return[Xo.min(n),Xo.max(n)]}function ai(n,t){return n.parent==t.parent?1:2}function ci(n){var t=n.children;return t&&t.length?t[0]:n._tree.thread}function si(n){var t,e=n.children;return e&&(t=e.length)?e[t-1]:n._tree.thread}function li(n,t){var e=n.children;if(e&&(u=e.length))for(var r,u,i=-1;++i<u;)t(r=li(e[i],t),n)>0&&(n=r);return n}function fi(n,t){return n.x-t.x}function hi(n,t){return t.x-n.x}function gi(n,t){return n.depth-t.depth}function pi(n,t){function e(n,r){var u=n.children;if(u&&(o=u.length))for(var i,o,a=null,c=-1;++c<o;)i=u[c],e(i,a),a=i;t(n,r)}e(n,null)}function vi(n){for(var t,e=0,r=0,u=n.children,i=u.length;--i>=0;)t=u[i]._tree,t.prelim+=e,t.mod+=e,e+=t.shift+(r+=t.change)}function di(n,t,e){n=n._tree,t=t._tree;var r=e/(t.number-n.number);n.change+=r,t.change-=r,t.shift+=e,t.prelim+=e,t.mod+=e}function mi(n,t,e){return n._tree.ancestor.parent==t.parent?n._tree.ancestor:e}function yi(n,t){return n.value-t.value}function xi(n,t){var e=n._pack_next;n._pack_next=t,t._pack_prev=n,t._pack_next=e,e._pack_prev=t}function Mi(n,t){n._pack_next=t,t._pack_prev=n}function _i(n,t){var e=t.x-n.x,r=t.y-n.y,u=n.r+t.r;return.999*u*u>e*e+r*r}function bi(n){function t(n){l=Math.min(n.x-n.r,l),f=Math.max(n.x+n.r,f),h=Math.min(n.y-n.r,h),g=Math.max(n.y+n.r,g)}if((e=n.children)&&(s=e.length)){var e,r,u,i,o,a,c,s,l=1/0,f=-1/0,h=1/0,g=-1/0;if(e.forEach(wi),r=e[0],r.x=-r.r,r.y=0,t(r),s>1&&(u=e[1],u.x=u.r,u.y=0,t(u),s>2))for(i=e[2],Ei(r,u,i),t(i),xi(r,i),r._pack_prev=i,xi(i,u),u=r._pack_next,o=3;s>o;o++){Ei(r,u,i=e[o]);var p=0,v=1,d=1;for(a=u._pack_next;a!==u;a=a._pack_next,v++)if(_i(a,i)){p=1;break}if(1==p)for(c=r._pack_prev;c!==a._pack_prev&&!_i(c,i);c=c._pack_prev,d++);p?(d>v||v==d&&u.r<r.r?Mi(r,u=a):Mi(r=c,u),o--):(xi(r,i),u=i,t(i))}var m=(l+f)/2,y=(h+g)/2,x=0;for(o=0;s>o;o++)i=e[o],i.x-=m,i.y-=y,x=Math.max(x,i.r+Math.sqrt(i.x*i.x+i.y*i.y));n.r=x,e.forEach(Si)}}function wi(n){n._pack_next=n._pack_prev=n}function Si(n){delete n._pack_next,delete n._pack_prev}function ki(n,t,e,r){var u=n.children;if(n.x=t+=r*n.x,n.y=e+=r*n.y,n.r*=r,u)for(var i=-1,o=u.length;++i<o;)ki(u[i],t,e,r)}function Ei(n,t,e){var r=n.r+e.r,u=t.x-n.x,i=t.y-n.y;if(r&&(u||i)){var o=t.r+e.r,a=u*u+i*i;o*=o,r*=r;var c=.5+(r-o)/(2*a),s=Math.sqrt(Math.max(0,2*o*(r+a)-(r-=a)*r-o*o))/(2*a);e.x=n.x+c*u+s*i,e.y=n.y+c*i-s*u}else e.x=n.x+r,e.y=n.y}function Ai(n){return 1+Xo.max(n,function(n){return n.y})}function Ci(n){return n.reduce(function(n,t){return n+t.x},0)/n.length}function Ni(n){var t=n.children;return t&&t.length?Ni(t[0]):n}function Li(n){var t,e=n.children;return e&&(t=e.length)?Li(e[t-1]):n}function zi(n){return{x:n.x,y:n.y,dx:n.dx,dy:n.dy}}function qi(n,t){var e=n.x+t[3],r=n.y+t[0],u=n.dx-t[1]-t[3],i=n.dy-t[0]-t[2];return 0>u&&(e+=u/2,u=0),0>i&&(r+=i/2,i=0),{x:e,y:r,dx:u,dy:i}}function Ti(n){var t=n[0],e=n[n.length-1];return e>t?[t,e]:[e,t]}function Ri(n){return n.rangeExtent?n.rangeExtent():Ti(n.range())}function Di(n,t,e,r){var u=e(n[0],n[1]),i=r(t[0],t[1]);return function(n){return i(u(n))}}function Pi(n,t){var e,r=0,u=n.length-1,i=n[r],o=n[u];return i>o&&(e=r,r=u,u=e,e=i,i=o,o=e),n[r]=t.floor(i),n[u]=t.ceil(o),n}function Ui(n){return n?{floor:function(t){return Math.floor(t/n)*n},ceil:function(t){return Math.ceil(t/n)*n}}:ls}function ji(n,t,e,r){var u=[],i=[],o=0,a=Math.min(n.length,t.length)-1;for(n[a]<n[0]&&(n=n.slice().reverse(),t=t.slice().reverse());++o<=a;)u.push(e(n[o-1],n[o])),i.push(r(t[o-1],t[o]));return function(t){var e=Xo.bisect(n,t,1,a)-1;return i[e](u[e](t))}}function Hi(n,t,e,r){function u(){var u=Math.min(n.length,t.length)>2?ji:Di,c=r?Pu:Du;return o=u(n,t,c,e),a=u(t,n,c,fu),i}function i(n){return o(n)}var o,a;return i.invert=function(n){return a(n)},i.domain=function(t){return arguments.length?(n=t.map(Number),u()):n},i.range=function(n){return arguments.length?(t=n,u()):t},i.rangeRound=function(n){return i.range(n).interpolate(Nu)},i.clamp=function(n){return arguments.length?(r=n,u()):r},i.interpolate=function(n){return arguments.length?(e=n,u()):e},i.ticks=function(t){return Ii(n,t)},i.tickFormat=function(t,e){return Zi(n,t,e)},i.nice=function(t){return Oi(n,t),u()},i.copy=function(){return Hi(n,t,e,r)},u()}function Fi(n,t){return Xo.rebind(n,t,"range","rangeRound","interpolate","clamp")}function Oi(n,t){return Pi(n,Ui(Yi(n,t)[2]))}function Yi(n,t){null==t&&(t=10);var e=Ti(n),r=e[1]-e[0],u=Math.pow(10,Math.floor(Math.log(r/t)/Math.LN10)),i=t/r*u;return.15>=i?u*=10:.35>=i?u*=5:.75>=i&&(u*=2),e[0]=Math.ceil(e[0]/u)*u,e[1]=Math.floor(e[1]/u)*u+.5*u,e[2]=u,e}function Ii(n,t){return Xo.range.apply(Xo,Yi(n,t))}function Zi(n,t,e){var r=Yi(n,t);return Xo.format(e?e.replace(Qa,function(n,t,e,u,i,o,a,c,s,l){return[t,e,u,i,o,a,c,s||"."+Xi(l,r),l].join("")}):",."+Vi(r[2])+"f")}function Vi(n){return-Math.floor(Math.log(n)/Math.LN10+.01)}function Xi(n,t){var e=Vi(t[2]);return n in fs?Math.abs(e-Vi(Math.max(Math.abs(t[0]),Math.abs(t[1]))))+ +("e"!==n):e-2*("%"===n)}function $i(n,t,e,r){function u(n){return(e?Math.log(0>n?0:n):-Math.log(n>0?0:-n))/Math.log(t)}function i(n){return e?Math.pow(t,n):-Math.pow(t,-n)}function o(t){return n(u(t))}return o.invert=function(t){return i(n.invert(t))},o.domain=function(t){return arguments.length?(e=t[0]>=0,n.domain((r=t.map(Number)).map(u)),o):r},o.base=function(e){return arguments.length?(t=+e,n.domain(r.map(u)),o):t},o.nice=function(){var t=Pi(r.map(u),e?Math:gs);return n.domain(t),r=t.map(i),o},o.ticks=function(){var n=Ti(r),o=[],a=n[0],c=n[1],s=Math.floor(u(a)),l=Math.ceil(u(c)),f=t%1?2:t;if(isFinite(l-s)){if(e){for(;l>s;s++)for(var h=1;f>h;h++)o.push(i(s)*h);o.push(i(s))}else for(o.push(i(s));s++<l;)for(var h=f-1;h>0;h--)o.push(i(s)*h);for(s=0;o[s]<a;s++);for(l=o.length;o[l-1]>c;l--);o=o.slice(s,l)}return o},o.tickFormat=function(n,t){if(!arguments.length)return hs;arguments.length<2?t=hs:"function"!=typeof t&&(t=Xo.format(t));var r,a=Math.max(.1,n/o.ticks().length),c=e?(r=1e-12,Math.ceil):(r=-1e-12,Math.floor);return function(n){return n/i(c(u(n)+r))<=a?t(n):""}},o.copy=function(){return $i(n.copy(),t,e,r)},Fi(o,n)}function Bi(n,t,e){function r(t){return n(u(t))}var u=Wi(t),i=Wi(1/t);return r.invert=function(t){return i(n.invert(t))},r.domain=function(t){return arguments.length?(n.domain((e=t.map(Number)).map(u)),r):e},r.ticks=function(n){return Ii(e,n)},r.tickFormat=function(n,t){return Zi(e,n,t)},r.nice=function(n){return r.domain(Oi(e,n))},r.exponent=function(o){return arguments.length?(u=Wi(t=o),i=Wi(1/t),n.domain(e.map(u)),r):t},r.copy=function(){return Bi(n.copy(),t,e)},Fi(r,n)}function Wi(n){return function(t){return 0>t?-Math.pow(-t,n):Math.pow(t,n)}}function Ji(n,t){function e(e){return o[((i.get(e)||"range"===t.t&&i.set(e,n.push(e)))-1)%o.length]}function r(t,e){return Xo.range(n.length).map(function(n){return t+e*n})}var i,o,a;return e.domain=function(r){if(!arguments.length)return n;n=[],i=new u;for(var o,a=-1,c=r.length;++a<c;)i.has(o=r[a])||i.set(o,n.push(o));return e[t.t].apply(e,t.a)},e.range=function(n){return arguments.length?(o=n,a=0,t={t:"range",a:arguments},e):o},e.rangePoints=function(u,i){arguments.length<2&&(i=0);var c=u[0],s=u[1],l=(s-c)/(Math.max(1,n.length-1)+i);return o=r(n.length<2?(c+s)/2:c+l*i/2,l),a=0,t={t:"rangePoints",a:arguments},e},e.rangeBands=function(u,i,c){arguments.length<2&&(i=0),arguments.length<3&&(c=i);var s=u[1]<u[0],l=u[s-0],f=u[1-s],h=(f-l)/(n.length-i+2*c);return o=r(l+h*c,h),s&&o.reverse(),a=h*(1-i),t={t:"rangeBands",a:arguments},e},e.rangeRoundBands=function(u,i,c){arguments.length<2&&(i=0),arguments.length<3&&(c=i);var s=u[1]<u[0],l=u[s-0],f=u[1-s],h=Math.floor((f-l)/(n.length-i+2*c)),g=f-l-(n.length-i)*h;return o=r(l+Math.round(g/2),h),s&&o.reverse(),a=Math.round(h*(1-i)),t={t:"rangeRoundBands",a:arguments},e},e.rangeBand=function(){return a},e.rangeExtent=function(){return Ti(t.a[0])},e.copy=function(){return Ji(n,t)},e.domain(n)}function Gi(n,t){function e(){var e=0,i=t.length;for(u=[];++e<i;)u[e-1]=Xo.quantile(n,e/i);return r}function r(n){return isNaN(n=+n)?void 0:t[Xo.bisect(u,n)]}var u;return r.domain=function(t){return arguments.length?(n=t.filter(function(n){return!isNaN(n)}).sort(Xo.ascending),e()):n},r.range=function(n){return arguments.length?(t=n,e()):t},r.quantiles=function(){return u},r.invertExtent=function(e){return e=t.indexOf(e),0>e?[0/0,0/0]:[e>0?u[e-1]:n[0],e<u.length?u[e]:n[n.length-1]]},r.copy=function(){return Gi(n,t)},e()}function Ki(n,t,e){function r(t){return e[Math.max(0,Math.min(o,Math.floor(i*(t-n))))]}function u(){return i=e.length/(t-n),o=e.length-1,r}var i,o;return r.domain=function(e){return arguments.length?(n=+e[0],t=+e[e.length-1],u()):[n,t]},r.range=function(n){return arguments.length?(e=n,u()):e},r.invertExtent=function(t){return t=e.indexOf(t),t=0>t?0/0:t/i+n,[t,t+1/i]},r.copy=function(){return Ki(n,t,e)},u()}function Qi(n,t){function e(e){return e>=e?t[Xo.bisect(n,e)]:void 0}return e.domain=function(t){return arguments.length?(n=t,e):n},e.range=function(n){return arguments.length?(t=n,e):t},e.invertExtent=function(e){return e=t.indexOf(e),[n[e-1],n[e]]},e.copy=function(){return Qi(n,t)},e}function no(n){function t(n){return+n}return t.invert=t,t.domain=t.range=function(e){return arguments.length?(n=e.map(t),t):n},t.ticks=function(t){return Ii(n,t)},t.tickFormat=function(t,e){return Zi(n,t,e)},t.copy=function(){return no(n)},t}function to(n){return n.innerRadius}function eo(n){return n.outerRadius}function ro(n){return n.startAngle}function uo(n){return n.endAngle}function io(n){function t(t){function o(){s.push("M",i(n(l),a))}for(var c,s=[],l=[],f=-1,h=t.length,g=_t(e),p=_t(r);++f<h;)u.call(this,c=t[f],f)?l.push([+g.call(this,c,f),+p.call(this,c,f)]):l.length&&(o(),l=[]);return l.length&&o(),s.length?s.join(""):null}var e=br,r=wr,u=be,i=oo,o=i.key,a=.7;return t.x=function(n){return arguments.length?(e=n,t):e},t.y=function(n){return arguments.length?(r=n,t):r},t.defined=function(n){return arguments.length?(u=n,t):u},t.interpolate=function(n){return arguments.length?(o="function"==typeof n?i=n:(i=Ms.get(n)||oo).key,t):o},t.tension=function(n){return arguments.length?(a=n,t):a},t}function oo(n){return n.join("L")}function ao(n){return oo(n)+"Z"}function co(n){for(var t=0,e=n.length,r=n[0],u=[r[0],",",r[1]];++t<e;)u.push("H",(r[0]+(r=n[t])[0])/2,"V",r[1]);return e>1&&u.push("H",r[0]),u.join("")}function so(n){for(var t=0,e=n.length,r=n[0],u=[r[0],",",r[1]];++t<e;)u.push("V",(r=n[t])[1],"H",r[0]);return u.join("")}function lo(n){for(var t=0,e=n.length,r=n[0],u=[r[0],",",r[1]];++t<e;)u.push("H",(r=n[t])[0],"V",r[1]);return u.join("")}function fo(n,t){return n.length<4?oo(n):n[1]+po(n.slice(1,n.length-1),vo(n,t))}function ho(n,t){return n.length<3?oo(n):n[0]+po((n.push(n[0]),n),vo([n[n.length-2]].concat(n,[n[1]]),t))}function go(n,t){return n.length<3?oo(n):n[0]+po(n,vo(n,t))}function po(n,t){if(t.length<1||n.length!=t.length&&n.length!=t.length+2)return oo(n);var e=n.length!=t.length,r="",u=n[0],i=n[1],o=t[0],a=o,c=1;if(e&&(r+="Q"+(i[0]-2*o[0]/3)+","+(i[1]-2*o[1]/3)+","+i[0]+","+i[1],u=n[1],c=2),t.length>1){a=t[1],i=n[c],c++,r+="C"+(u[0]+o[0])+","+(u[1]+o[1])+","+(i[0]-a[0])+","+(i[1]-a[1])+","+i[0]+","+i[1];for(var s=2;s<t.length;s++,c++)i=n[c],a=t[s],r+="S"+(i[0]-a[0])+","+(i[1]-a[1])+","+i[0]+","+i[1]}if(e){var l=n[c];r+="Q"+(i[0]+2*a[0]/3)+","+(i[1]+2*a[1]/3)+","+l[0]+","+l[1]}return r}function vo(n,t){for(var e,r=[],u=(1-t)/2,i=n[0],o=n[1],a=1,c=n.length;++a<c;)e=i,i=o,o=n[a],r.push([u*(o[0]-e[0]),u*(o[1]-e[1])]);return r}function mo(n){if(n.length<3)return oo(n);var t=1,e=n.length,r=n[0],u=r[0],i=r[1],o=[u,u,u,(r=n[1])[0]],a=[i,i,i,r[1]],c=[u,",",i,"L",_o(ws,o),",",_o(ws,a)];for(n.push(n[e-1]);++t<=e;)r=n[t],o.shift(),o.push(r[0]),a.shift(),a.push(r[1]),bo(c,o,a);return n.pop(),c.push("L",r),c.join("")}function yo(n){if(n.length<4)return oo(n);for(var t,e=[],r=-1,u=n.length,i=[0],o=[0];++r<3;)t=n[r],i.push(t[0]),o.push(t[1]);for(e.push(_o(ws,i)+","+_o(ws,o)),--r;++r<u;)t=n[r],i.shift(),i.push(t[0]),o.shift(),o.push(t[1]),bo(e,i,o);return e.join("")}function xo(n){for(var t,e,r=-1,u=n.length,i=u+4,o=[],a=[];++r<4;)e=n[r%u],o.push(e[0]),a.push(e[1]);for(t=[_o(ws,o),",",_o(ws,a)],--r;++r<i;)e=n[r%u],o.shift(),o.push(e[0]),a.shift(),a.push(e[1]),bo(t,o,a);return t.join("")}function Mo(n,t){var e=n.length-1;if(e)for(var r,u,i=n[0][0],o=n[0][1],a=n[e][0]-i,c=n[e][1]-o,s=-1;++s<=e;)r=n[s],u=s/e,r[0]=t*r[0]+(1-t)*(i+u*a),r[1]=t*r[1]+(1-t)*(o+u*c);return mo(n)}function _o(n,t){return n[0]*t[0]+n[1]*t[1]+n[2]*t[2]+n[3]*t[3]}function bo(n,t,e){n.push("C",_o(_s,t),",",_o(_s,e),",",_o(bs,t),",",_o(bs,e),",",_o(ws,t),",",_o(ws,e))}function wo(n,t){return(t[1]-n[1])/(t[0]-n[0])}function So(n){for(var t=0,e=n.length-1,r=[],u=n[0],i=n[1],o=r[0]=wo(u,i);++t<e;)r[t]=(o+(o=wo(u=i,i=n[t+1])))/2;return r[t]=o,r}function ko(n){for(var t,e,r,u,i=[],o=So(n),a=-1,c=n.length-1;++a<c;)t=wo(n[a],n[a+1]),oa(t)<Aa?o[a]=o[a+1]=0:(e=o[a]/t,r=o[a+1]/t,u=e*e+r*r,u>9&&(u=3*t/Math.sqrt(u),o[a]=u*e,o[a+1]=u*r));for(a=-1;++a<=c;)u=(n[Math.min(c,a+1)][0]-n[Math.max(0,a-1)][0])/(6*(1+o[a]*o[a])),i.push([u||0,o[a]*u||0]);return i}function Eo(n){return n.length<3?oo(n):n[0]+po(n,ko(n))}function Ao(n){for(var t,e,r,u=-1,i=n.length;++u<i;)t=n[u],e=t[0],r=t[1]+ys,t[0]=e*Math.cos(r),t[1]=e*Math.sin(r);return n}function Co(n){function t(t){function c(){v.push("M",a(n(m),f),l,s(n(d.reverse()),f),"Z")}for(var h,g,p,v=[],d=[],m=[],y=-1,x=t.length,M=_t(e),_=_t(u),b=e===r?function(){return g}:_t(r),w=u===i?function(){return p}:_t(i);++y<x;)o.call(this,h=t[y],y)?(d.push([g=+M.call(this,h,y),p=+_.call(this,h,y)]),m.push([+b.call(this,h,y),+w.call(this,h,y)])):d.length&&(c(),d=[],m=[]);return d.length&&c(),v.length?v.join(""):null}var e=br,r=br,u=0,i=wr,o=be,a=oo,c=a.key,s=a,l="L",f=.7;return t.x=function(n){return arguments.length?(e=r=n,t):r},t.x0=function(n){return arguments.length?(e=n,t):e},t.x1=function(n){return arguments.length?(r=n,t):r},t.y=function(n){return arguments.length?(u=i=n,t):i},t.y0=function(n){return arguments.length?(u=n,t):u},t.y1=function(n){return arguments.length?(i=n,t):i},t.defined=function(n){return arguments.length?(o=n,t):o},t.interpolate=function(n){return arguments.length?(c="function"==typeof n?a=n:(a=Ms.get(n)||oo).key,s=a.reverse||a,l=a.closed?"M":"L",t):c},t.tension=function(n){return arguments.length?(f=n,t):f},t}function No(n){return n.radius}function Lo(n){return[n.x,n.y]}function zo(n){return function(){var t=n.apply(this,arguments),e=t[0],r=t[1]+ys;return[e*Math.cos(r),e*Math.sin(r)]}}function qo(){return 64}function To(){return"circle"}function Ro(n){var t=Math.sqrt(n/Sa);return"M0,"+t+"A"+t+","+t+" 0 1,1 0,"+-t+"A"+t+","+t+" 0 1,1 0,"+t+"Z"}function Do(n,t){return fa(n,Ns),n.id=t,n}function Po(n,t,e,r){var u=n.id;return R(n,"function"==typeof e?function(n,i,o){n.__transition__[u].tween.set(t,r(e.call(n,n.__data__,i,o)))}:(e=r(e),function(n){n.__transition__[u].tween.set(t,e)}))}function Uo(n){return null==n&&(n=""),function(){this.textContent=n}}function jo(n,t,e,r){var i=n.__transition__||(n.__transition__={active:0,count:0}),o=i[e];if(!o){var a=r.time;o=i[e]={tween:new u,time:a,ease:r.ease,delay:r.delay,duration:r.duration},++i.count,Xo.timer(function(r){function u(r){return i.active>e?s():(i.active=e,o.event&&o.event.start.call(n,l,t),o.tween.forEach(function(e,r){(r=r.call(n,l,t))&&v.push(r)}),Xo.timer(function(){return p.c=c(r||1)?be:c,1},0,a),void 0)}function c(r){if(i.active!==e)return s();for(var u=r/g,a=f(u),c=v.length;c>0;)v[--c].call(n,a);return u>=1?(o.event&&o.event.end.call(n,l,t),s()):void 0}function s(){return--i.count?delete i[e]:delete n.__transition__,1}var l=n.__data__,f=o.ease,h=o.delay,g=o.duration,p=Ja,v=[];return p.t=h+a,r>=h?u(r-h):(p.c=u,void 0)},0,a)}}function Ho(n,t){n.attr("transform",function(n){return"translate("+t(n)+",0)"})}function Fo(n,t){n.attr("transform",function(n){return"translate(0,"+t(n)+")"})}function Oo(n){return n.toISOString()}function Yo(n,t,e){function r(t){return n(t)}function u(n,e){var r=n[1]-n[0],u=r/e,i=Xo.bisect(js,u);return i==js.length?[t.year,Yi(n.map(function(n){return n/31536e6}),e)[2]]:i?t[u/js[i-1]<js[i]/u?i-1:i]:[Os,Yi(n,e)[2]]
3
+ }return r.invert=function(t){return Io(n.invert(t))},r.domain=function(t){return arguments.length?(n.domain(t),r):n.domain().map(Io)},r.nice=function(n,t){function e(e){return!isNaN(e)&&!n.range(e,Io(+e+1),t).length}var i=r.domain(),o=Ti(i),a=null==n?u(o,10):"number"==typeof n&&u(o,n);return a&&(n=a[0],t=a[1]),r.domain(Pi(i,t>1?{floor:function(t){for(;e(t=n.floor(t));)t=Io(t-1);return t},ceil:function(t){for(;e(t=n.ceil(t));)t=Io(+t+1);return t}}:n))},r.ticks=function(n,t){var e=Ti(r.domain()),i=null==n?u(e,10):"number"==typeof n?u(e,n):!n.range&&[{range:n},t];return i&&(n=i[0],t=i[1]),n.range(e[0],Io(+e[1]+1),1>t?1:t)},r.tickFormat=function(){return e},r.copy=function(){return Yo(n.copy(),t,e)},Fi(r,n)}function Io(n){return new Date(n)}function Zo(n){return JSON.parse(n.responseText)}function Vo(n){var t=Wo.createRange();return t.selectNode(Wo.body),t.createContextualFragment(n.responseText)}var Xo={version:"3.4.2"};Date.now||(Date.now=function(){return+new Date});var $o=[].slice,Bo=function(n){return $o.call(n)},Wo=document,Jo=Wo.documentElement,Go=window;try{Bo(Jo.childNodes)[0].nodeType}catch(Ko){Bo=function(n){for(var t=n.length,e=new Array(t);t--;)e[t]=n[t];return e}}try{Wo.createElement("div").style.setProperty("opacity",0,"")}catch(Qo){var na=Go.Element.prototype,ta=na.setAttribute,ea=na.setAttributeNS,ra=Go.CSSStyleDeclaration.prototype,ua=ra.setProperty;na.setAttribute=function(n,t){ta.call(this,n,t+"")},na.setAttributeNS=function(n,t,e){ea.call(this,n,t,e+"")},ra.setProperty=function(n,t,e){ua.call(this,n,t+"",e)}}Xo.ascending=function(n,t){return t>n?-1:n>t?1:n>=t?0:0/0},Xo.descending=function(n,t){return n>t?-1:t>n?1:t>=n?0:0/0},Xo.min=function(n,t){var e,r,u=-1,i=n.length;if(1===arguments.length){for(;++u<i&&!(null!=(e=n[u])&&e>=e);)e=void 0;for(;++u<i;)null!=(r=n[u])&&e>r&&(e=r)}else{for(;++u<i&&!(null!=(e=t.call(n,n[u],u))&&e>=e);)e=void 0;for(;++u<i;)null!=(r=t.call(n,n[u],u))&&e>r&&(e=r)}return e},Xo.max=function(n,t){var e,r,u=-1,i=n.length;if(1===arguments.length){for(;++u<i&&!(null!=(e=n[u])&&e>=e);)e=void 0;for(;++u<i;)null!=(r=n[u])&&r>e&&(e=r)}else{for(;++u<i&&!(null!=(e=t.call(n,n[u],u))&&e>=e);)e=void 0;for(;++u<i;)null!=(r=t.call(n,n[u],u))&&r>e&&(e=r)}return e},Xo.extent=function(n,t){var e,r,u,i=-1,o=n.length;if(1===arguments.length){for(;++i<o&&!(null!=(e=u=n[i])&&e>=e);)e=u=void 0;for(;++i<o;)null!=(r=n[i])&&(e>r&&(e=r),r>u&&(u=r))}else{for(;++i<o&&!(null!=(e=u=t.call(n,n[i],i))&&e>=e);)e=void 0;for(;++i<o;)null!=(r=t.call(n,n[i],i))&&(e>r&&(e=r),r>u&&(u=r))}return[e,u]},Xo.sum=function(n,t){var e,r=0,u=n.length,i=-1;if(1===arguments.length)for(;++i<u;)isNaN(e=+n[i])||(r+=e);else for(;++i<u;)isNaN(e=+t.call(n,n[i],i))||(r+=e);return r},Xo.mean=function(t,e){var r,u=t.length,i=0,o=-1,a=0;if(1===arguments.length)for(;++o<u;)n(r=t[o])&&(i+=(r-i)/++a);else for(;++o<u;)n(r=e.call(t,t[o],o))&&(i+=(r-i)/++a);return a?i:void 0},Xo.quantile=function(n,t){var e=(n.length-1)*t+1,r=Math.floor(e),u=+n[r-1],i=e-r;return i?u+i*(n[r]-u):u},Xo.median=function(t,e){return arguments.length>1&&(t=t.map(e)),t=t.filter(n),t.length?Xo.quantile(t.sort(Xo.ascending),.5):void 0},Xo.bisector=function(n){return{left:function(t,e,r,u){for(arguments.length<3&&(r=0),arguments.length<4&&(u=t.length);u>r;){var i=r+u>>>1;n.call(t,t[i],i)<e?r=i+1:u=i}return r},right:function(t,e,r,u){for(arguments.length<3&&(r=0),arguments.length<4&&(u=t.length);u>r;){var i=r+u>>>1;e<n.call(t,t[i],i)?u=i:r=i+1}return r}}};var ia=Xo.bisector(function(n){return n});Xo.bisectLeft=ia.left,Xo.bisect=Xo.bisectRight=ia.right,Xo.shuffle=function(n){for(var t,e,r=n.length;r;)e=0|Math.random()*r--,t=n[r],n[r]=n[e],n[e]=t;return n},Xo.permute=function(n,t){for(var e=t.length,r=new Array(e);e--;)r[e]=n[t[e]];return r},Xo.pairs=function(n){for(var t,e=0,r=n.length-1,u=n[0],i=new Array(0>r?0:r);r>e;)i[e]=[t=u,u=n[++e]];return i},Xo.zip=function(){if(!(u=arguments.length))return[];for(var n=-1,e=Xo.min(arguments,t),r=new Array(e);++n<e;)for(var u,i=-1,o=r[n]=new Array(u);++i<u;)o[i]=arguments[i][n];return r},Xo.transpose=function(n){return Xo.zip.apply(Xo,n)},Xo.keys=function(n){var t=[];for(var e in n)t.push(e);return t},Xo.values=function(n){var t=[];for(var e in n)t.push(n[e]);return t},Xo.entries=function(n){var t=[];for(var e in n)t.push({key:e,value:n[e]});return t},Xo.merge=function(n){for(var t,e,r,u=n.length,i=-1,o=0;++i<u;)o+=n[i].length;for(e=new Array(o);--u>=0;)for(r=n[u],t=r.length;--t>=0;)e[--o]=r[t];return e};var oa=Math.abs;Xo.range=function(n,t,r){if(arguments.length<3&&(r=1,arguments.length<2&&(t=n,n=0)),1/0===(t-n)/r)throw new Error("infinite range");var u,i=[],o=e(oa(r)),a=-1;if(n*=o,t*=o,r*=o,0>r)for(;(u=n+r*++a)>t;)i.push(u/o);else for(;(u=n+r*++a)<t;)i.push(u/o);return i},Xo.map=function(n){var t=new u;if(n instanceof u)n.forEach(function(n,e){t.set(n,e)});else for(var e in n)t.set(e,n[e]);return t},r(u,{has:i,get:function(n){return this[aa+n]},set:function(n,t){return this[aa+n]=t},remove:o,keys:a,values:function(){var n=[];return this.forEach(function(t,e){n.push(e)}),n},entries:function(){var n=[];return this.forEach(function(t,e){n.push({key:t,value:e})}),n},size:c,empty:s,forEach:function(n){for(var t in this)t.charCodeAt(0)===ca&&n.call(this,t.substring(1),this[t])}});var aa="\x00",ca=aa.charCodeAt(0);Xo.nest=function(){function n(t,a,c){if(c>=o.length)return r?r.call(i,a):e?a.sort(e):a;for(var s,l,f,h,g=-1,p=a.length,v=o[c++],d=new u;++g<p;)(h=d.get(s=v(l=a[g])))?h.push(l):d.set(s,[l]);return t?(l=t(),f=function(e,r){l.set(e,n(t,r,c))}):(l={},f=function(e,r){l[e]=n(t,r,c)}),d.forEach(f),l}function t(n,e){if(e>=o.length)return n;var r=[],u=a[e++];return n.forEach(function(n,u){r.push({key:n,values:t(u,e)})}),u?r.sort(function(n,t){return u(n.key,t.key)}):r}var e,r,i={},o=[],a=[];return i.map=function(t,e){return n(e,t,0)},i.entries=function(e){return t(n(Xo.map,e,0),0)},i.key=function(n){return o.push(n),i},i.sortKeys=function(n){return a[o.length-1]=n,i},i.sortValues=function(n){return e=n,i},i.rollup=function(n){return r=n,i},i},Xo.set=function(n){var t=new l;if(n)for(var e=0,r=n.length;r>e;++e)t.add(n[e]);return t},r(l,{has:i,add:function(n){return this[aa+n]=!0,n},remove:function(n){return n=aa+n,n in this&&delete this[n]},values:a,size:c,empty:s,forEach:function(n){for(var t in this)t.charCodeAt(0)===ca&&n.call(this,t.substring(1))}}),Xo.behavior={},Xo.rebind=function(n,t){for(var e,r=1,u=arguments.length;++r<u;)n[e=arguments[r]]=f(n,t,t[e]);return n};var sa=["webkit","ms","moz","Moz","o","O"];Xo.dispatch=function(){for(var n=new p,t=-1,e=arguments.length;++t<e;)n[arguments[t]]=v(n);return n},p.prototype.on=function(n,t){var e=n.indexOf("."),r="";if(e>=0&&(r=n.substring(e+1),n=n.substring(0,e)),n)return arguments.length<2?this[n].on(r):this[n].on(r,t);if(2===arguments.length){if(null==t)for(n in this)this.hasOwnProperty(n)&&this[n].on(r,null);return this}},Xo.event=null,Xo.requote=function(n){return n.replace(la,"\\$&")};var la=/[\\\^\$\*\+\?\|\[\]\(\)\.\{\}]/g,fa={}.__proto__?function(n,t){n.__proto__=t}:function(n,t){for(var e in t)n[e]=t[e]},ha=function(n,t){return t.querySelector(n)},ga=function(n,t){return t.querySelectorAll(n)},pa=Jo[h(Jo,"matchesSelector")],va=function(n,t){return pa.call(n,t)};"function"==typeof Sizzle&&(ha=function(n,t){return Sizzle(n,t)[0]||null},ga=function(n,t){return Sizzle.uniqueSort(Sizzle(n,t))},va=Sizzle.matchesSelector),Xo.selection=function(){return xa};var da=Xo.selection.prototype=[];da.select=function(n){var t,e,r,u,i=[];n=M(n);for(var o=-1,a=this.length;++o<a;){i.push(t=[]),t.parentNode=(r=this[o]).parentNode;for(var c=-1,s=r.length;++c<s;)(u=r[c])?(t.push(e=n.call(u,u.__data__,c,o)),e&&"__data__"in u&&(e.__data__=u.__data__)):t.push(null)}return x(i)},da.selectAll=function(n){var t,e,r=[];n=_(n);for(var u=-1,i=this.length;++u<i;)for(var o=this[u],a=-1,c=o.length;++a<c;)(e=o[a])&&(r.push(t=Bo(n.call(e,e.__data__,a,u))),t.parentNode=e);return x(r)};var ma={svg:"http://www.w3.org/2000/svg",xhtml:"http://www.w3.org/1999/xhtml",xlink:"http://www.w3.org/1999/xlink",xml:"http://www.w3.org/XML/1998/namespace",xmlns:"http://www.w3.org/2000/xmlns/"};Xo.ns={prefix:ma,qualify:function(n){var t=n.indexOf(":"),e=n;return t>=0&&(e=n.substring(0,t),n=n.substring(t+1)),ma.hasOwnProperty(e)?{space:ma[e],local:n}:n}},da.attr=function(n,t){if(arguments.length<2){if("string"==typeof n){var e=this.node();return n=Xo.ns.qualify(n),n.local?e.getAttributeNS(n.space,n.local):e.getAttribute(n)}for(t in n)this.each(b(t,n[t]));return this}return this.each(b(n,t))},da.classed=function(n,t){if(arguments.length<2){if("string"==typeof n){var e=this.node(),r=(n=k(n)).length,u=-1;if(t=e.classList){for(;++u<r;)if(!t.contains(n[u]))return!1}else for(t=e.getAttribute("class");++u<r;)if(!S(n[u]).test(t))return!1;return!0}for(t in n)this.each(E(t,n[t]));return this}return this.each(E(n,t))},da.style=function(n,t,e){var r=arguments.length;if(3>r){if("string"!=typeof n){2>r&&(t="");for(e in n)this.each(C(e,n[e],t));return this}if(2>r)return Go.getComputedStyle(this.node(),null).getPropertyValue(n);e=""}return this.each(C(n,t,e))},da.property=function(n,t){if(arguments.length<2){if("string"==typeof n)return this.node()[n];for(t in n)this.each(N(t,n[t]));return this}return this.each(N(n,t))},da.text=function(n){return arguments.length?this.each("function"==typeof n?function(){var t=n.apply(this,arguments);this.textContent=null==t?"":t}:null==n?function(){this.textContent=""}:function(){this.textContent=n}):this.node().textContent},da.html=function(n){return arguments.length?this.each("function"==typeof n?function(){var t=n.apply(this,arguments);this.innerHTML=null==t?"":t}:null==n?function(){this.innerHTML=""}:function(){this.innerHTML=n}):this.node().innerHTML},da.append=function(n){return n=L(n),this.select(function(){return this.appendChild(n.apply(this,arguments))})},da.insert=function(n,t){return n=L(n),t=M(t),this.select(function(){return this.insertBefore(n.apply(this,arguments),t.apply(this,arguments)||null)})},da.remove=function(){return this.each(function(){var n=this.parentNode;n&&n.removeChild(this)})},da.data=function(n,t){function e(n,e){var r,i,o,a=n.length,f=e.length,h=Math.min(a,f),g=new Array(f),p=new Array(f),v=new Array(a);if(t){var d,m=new u,y=new u,x=[];for(r=-1;++r<a;)d=t.call(i=n[r],i.__data__,r),m.has(d)?v[r]=i:m.set(d,i),x.push(d);for(r=-1;++r<f;)d=t.call(e,o=e[r],r),(i=m.get(d))?(g[r]=i,i.__data__=o):y.has(d)||(p[r]=z(o)),y.set(d,o),m.remove(d);for(r=-1;++r<a;)m.has(x[r])&&(v[r]=n[r])}else{for(r=-1;++r<h;)i=n[r],o=e[r],i?(i.__data__=o,g[r]=i):p[r]=z(o);for(;f>r;++r)p[r]=z(e[r]);for(;a>r;++r)v[r]=n[r]}p.update=g,p.parentNode=g.parentNode=v.parentNode=n.parentNode,c.push(p),s.push(g),l.push(v)}var r,i,o=-1,a=this.length;if(!arguments.length){for(n=new Array(a=(r=this[0]).length);++o<a;)(i=r[o])&&(n[o]=i.__data__);return n}var c=D([]),s=x([]),l=x([]);if("function"==typeof n)for(;++o<a;)e(r=this[o],n.call(r,r.parentNode.__data__,o));else for(;++o<a;)e(r=this[o],n);return s.enter=function(){return c},s.exit=function(){return l},s},da.datum=function(n){return arguments.length?this.property("__data__",n):this.property("__data__")},da.filter=function(n){var t,e,r,u=[];"function"!=typeof n&&(n=q(n));for(var i=0,o=this.length;o>i;i++){u.push(t=[]),t.parentNode=(e=this[i]).parentNode;for(var a=0,c=e.length;c>a;a++)(r=e[a])&&n.call(r,r.__data__,a,i)&&t.push(r)}return x(u)},da.order=function(){for(var n=-1,t=this.length;++n<t;)for(var e,r=this[n],u=r.length-1,i=r[u];--u>=0;)(e=r[u])&&(i&&i!==e.nextSibling&&i.parentNode.insertBefore(e,i),i=e);return this},da.sort=function(n){n=T.apply(this,arguments);for(var t=-1,e=this.length;++t<e;)this[t].sort(n);return this.order()},da.each=function(n){return R(this,function(t,e,r){n.call(t,t.__data__,e,r)})},da.call=function(n){var t=Bo(arguments);return n.apply(t[0]=this,t),this},da.empty=function(){return!this.node()},da.node=function(){for(var n=0,t=this.length;t>n;n++)for(var e=this[n],r=0,u=e.length;u>r;r++){var i=e[r];if(i)return i}return null},da.size=function(){var n=0;return this.each(function(){++n}),n};var ya=[];Xo.selection.enter=D,Xo.selection.enter.prototype=ya,ya.append=da.append,ya.empty=da.empty,ya.node=da.node,ya.call=da.call,ya.size=da.size,ya.select=function(n){for(var t,e,r,u,i,o=[],a=-1,c=this.length;++a<c;){r=(u=this[a]).update,o.push(t=[]),t.parentNode=u.parentNode;for(var s=-1,l=u.length;++s<l;)(i=u[s])?(t.push(r[s]=e=n.call(u.parentNode,i.__data__,s,a)),e.__data__=i.__data__):t.push(null)}return x(o)},ya.insert=function(n,t){return arguments.length<2&&(t=P(this)),da.insert.call(this,n,t)},da.transition=function(){for(var n,t,e=ks||++Ls,r=[],u=Es||{time:Date.now(),ease:yu,delay:0,duration:250},i=-1,o=this.length;++i<o;){r.push(n=[]);for(var a=this[i],c=-1,s=a.length;++c<s;)(t=a[c])&&jo(t,c,e,u),n.push(t)}return Do(r,e)},da.interrupt=function(){return this.each(U)},Xo.select=function(n){var t=["string"==typeof n?ha(n,Wo):n];return t.parentNode=Jo,x([t])},Xo.selectAll=function(n){var t=Bo("string"==typeof n?ga(n,Wo):n);return t.parentNode=Jo,x([t])};var xa=Xo.select(Jo);da.on=function(n,t,e){var r=arguments.length;if(3>r){if("string"!=typeof n){2>r&&(t=!1);for(e in n)this.each(j(e,n[e],t));return this}if(2>r)return(r=this.node()["__on"+n])&&r._;e=!1}return this.each(j(n,t,e))};var Ma=Xo.map({mouseenter:"mouseover",mouseleave:"mouseout"});Ma.forEach(function(n){"on"+n in Wo&&Ma.remove(n)});var _a="onselectstart"in Wo?null:h(Jo.style,"userSelect"),ba=0;Xo.mouse=function(n){return Y(n,m())};var wa=/WebKit/.test(Go.navigator.userAgent)?-1:0;Xo.touches=function(n,t){return arguments.length<2&&(t=m().touches),t?Bo(t).map(function(t){var e=Y(n,t);return e.identifier=t.identifier,e}):[]},Xo.behavior.drag=function(){function n(){this.on("mousedown.drag",o).on("touchstart.drag",a)}function t(){return Xo.event.changedTouches[0].identifier}function e(n,t){return Xo.touches(n).filter(function(n){return n.identifier===t})[0]}function r(n,t,e,r){return function(){function o(){var n=t(l,g),e=n[0]-v[0],r=n[1]-v[1];d|=e|r,v=n,f({type:"drag",x:n[0]+c[0],y:n[1]+c[1],dx:e,dy:r})}function a(){m.on(e+"."+p,null).on(r+"."+p,null),y(d&&Xo.event.target===h),f({type:"dragend"})}var c,s=this,l=s.parentNode,f=u.of(s,arguments),h=Xo.event.target,g=n(),p=null==g?"drag":"drag-"+g,v=t(l,g),d=0,m=Xo.select(Go).on(e+"."+p,o).on(r+"."+p,a),y=O();i?(c=i.apply(s,arguments),c=[c.x-v[0],c.y-v[1]]):c=[0,0],f({type:"dragstart"})}}var u=y(n,"drag","dragstart","dragend"),i=null,o=r(g,Xo.mouse,"mousemove","mouseup"),a=r(t,e,"touchmove","touchend");return n.origin=function(t){return arguments.length?(i=t,n):i},Xo.rebind(n,u,"on")};var Sa=Math.PI,ka=2*Sa,Ea=Sa/2,Aa=1e-6,Ca=Aa*Aa,Na=Sa/180,La=180/Sa,za=Math.SQRT2,qa=2,Ta=4;Xo.interpolateZoom=function(n,t){function e(n){var t=n*y;if(m){var e=B(v),o=i/(qa*h)*(e*W(za*t+v)-$(v));return[r+o*s,u+o*l,i*e/B(za*t+v)]}return[r+n*s,u+n*l,i*Math.exp(za*t)]}var r=n[0],u=n[1],i=n[2],o=t[0],a=t[1],c=t[2],s=o-r,l=a-u,f=s*s+l*l,h=Math.sqrt(f),g=(c*c-i*i+Ta*f)/(2*i*qa*h),p=(c*c-i*i-Ta*f)/(2*c*qa*h),v=Math.log(Math.sqrt(g*g+1)-g),d=Math.log(Math.sqrt(p*p+1)-p),m=d-v,y=(m||Math.log(c/i))/za;return e.duration=1e3*y,e},Xo.behavior.zoom=function(){function n(n){n.on(A,s).on(Pa+".zoom",f).on(C,h).on("dblclick.zoom",g).on(L,l)}function t(n){return[(n[0]-S.x)/S.k,(n[1]-S.y)/S.k]}function e(n){return[n[0]*S.k+S.x,n[1]*S.k+S.y]}function r(n){S.k=Math.max(E[0],Math.min(E[1],n))}function u(n,t){t=e(t),S.x+=n[0]-t[0],S.y+=n[1]-t[1]}function i(){_&&_.domain(M.range().map(function(n){return(n-S.x)/S.k}).map(M.invert)),w&&w.domain(b.range().map(function(n){return(n-S.y)/S.k}).map(b.invert))}function o(n){n({type:"zoomstart"})}function a(n){i(),n({type:"zoom",scale:S.k,translate:[S.x,S.y]})}function c(n){n({type:"zoomend"})}function s(){function n(){l=1,u(Xo.mouse(r),g),a(i)}function e(){f.on(C,Go===r?h:null).on(N,null),p(l&&Xo.event.target===s),c(i)}var r=this,i=z.of(r,arguments),s=Xo.event.target,l=0,f=Xo.select(Go).on(C,n).on(N,e),g=t(Xo.mouse(r)),p=O();U.call(r),o(i)}function l(){function n(){var n=Xo.touches(g);return h=S.k,n.forEach(function(n){n.identifier in v&&(v[n.identifier]=t(n))}),n}function e(){for(var t=Xo.event.changedTouches,e=0,i=t.length;i>e;++e)v[t[e].identifier]=null;var o=n(),c=Date.now();if(1===o.length){if(500>c-x){var s=o[0],l=v[s.identifier];r(2*S.k),u(s,l),d(),a(p)}x=c}else if(o.length>1){var s=o[0],f=o[1],h=s[0]-f[0],g=s[1]-f[1];m=h*h+g*g}}function i(){for(var n,t,e,i,o=Xo.touches(g),c=0,s=o.length;s>c;++c,i=null)if(e=o[c],i=v[e.identifier]){if(t)break;n=e,t=i}if(i){var l=(l=e[0]-n[0])*l+(l=e[1]-n[1])*l,f=m&&Math.sqrt(l/m);n=[(n[0]+e[0])/2,(n[1]+e[1])/2],t=[(t[0]+i[0])/2,(t[1]+i[1])/2],r(f*h)}x=null,u(n,t),a(p)}function f(){if(Xo.event.touches.length){for(var t=Xo.event.changedTouches,e=0,r=t.length;r>e;++e)delete v[t[e].identifier];for(var u in v)return void n()}b.on(M,null).on(_,null),w.on(A,s).on(L,l),k(),c(p)}var h,g=this,p=z.of(g,arguments),v={},m=0,y=Xo.event.changedTouches[0].identifier,M="touchmove.zoom-"+y,_="touchend.zoom-"+y,b=Xo.select(Go).on(M,i).on(_,f),w=Xo.select(g).on(A,null).on(L,e),k=O();U.call(g),e(),o(p)}function f(){var n=z.of(this,arguments);m?clearTimeout(m):(U.call(this),o(n)),m=setTimeout(function(){m=null,c(n)},50),d();var e=v||Xo.mouse(this);p||(p=t(e)),r(Math.pow(2,.002*Ra())*S.k),u(e,p),a(n)}function h(){p=null}function g(){var n=z.of(this,arguments),e=Xo.mouse(this),i=t(e),s=Math.log(S.k)/Math.LN2;o(n),r(Math.pow(2,Xo.event.shiftKey?Math.ceil(s)-1:Math.floor(s)+1)),u(e,i),a(n),c(n)}var p,v,m,x,M,_,b,w,S={x:0,y:0,k:1},k=[960,500],E=Da,A="mousedown.zoom",C="mousemove.zoom",N="mouseup.zoom",L="touchstart.zoom",z=y(n,"zoomstart","zoom","zoomend");return n.event=function(n){n.each(function(){var n=z.of(this,arguments),t=S;ks?Xo.select(this).transition().each("start.zoom",function(){S=this.__chart__||{x:0,y:0,k:1},o(n)}).tween("zoom:zoom",function(){var e=k[0],r=k[1],u=e/2,i=r/2,o=Xo.interpolateZoom([(u-S.x)/S.k,(i-S.y)/S.k,e/S.k],[(u-t.x)/t.k,(i-t.y)/t.k,e/t.k]);return function(t){var r=o(t),c=e/r[2];this.__chart__=S={x:u-r[0]*c,y:i-r[1]*c,k:c},a(n)}}).each("end.zoom",function(){c(n)}):(this.__chart__=S,o(n),a(n),c(n))})},n.translate=function(t){return arguments.length?(S={x:+t[0],y:+t[1],k:S.k},i(),n):[S.x,S.y]},n.scale=function(t){return arguments.length?(S={x:S.x,y:S.y,k:+t},i(),n):S.k},n.scaleExtent=function(t){return arguments.length?(E=null==t?Da:[+t[0],+t[1]],n):E},n.center=function(t){return arguments.length?(v=t&&[+t[0],+t[1]],n):v},n.size=function(t){return arguments.length?(k=t&&[+t[0],+t[1]],n):k},n.x=function(t){return arguments.length?(_=t,M=t.copy(),S={x:0,y:0,k:1},n):_},n.y=function(t){return arguments.length?(w=t,b=t.copy(),S={x:0,y:0,k:1},n):w},Xo.rebind(n,z,"on")};var Ra,Da=[0,1/0],Pa="onwheel"in Wo?(Ra=function(){return-Xo.event.deltaY*(Xo.event.deltaMode?120:1)},"wheel"):"onmousewheel"in Wo?(Ra=function(){return Xo.event.wheelDelta},"mousewheel"):(Ra=function(){return-Xo.event.detail},"MozMousePixelScroll");G.prototype.toString=function(){return this.rgb()+""},Xo.hsl=function(n,t,e){return 1===arguments.length?n instanceof Q?K(n.h,n.s,n.l):dt(""+n,mt,K):K(+n,+t,+e)};var Ua=Q.prototype=new G;Ua.brighter=function(n){return n=Math.pow(.7,arguments.length?n:1),K(this.h,this.s,this.l/n)},Ua.darker=function(n){return n=Math.pow(.7,arguments.length?n:1),K(this.h,this.s,n*this.l)},Ua.rgb=function(){return nt(this.h,this.s,this.l)},Xo.hcl=function(n,t,e){return 1===arguments.length?n instanceof et?tt(n.h,n.c,n.l):n instanceof it?at(n.l,n.a,n.b):at((n=yt((n=Xo.rgb(n)).r,n.g,n.b)).l,n.a,n.b):tt(+n,+t,+e)};var ja=et.prototype=new G;ja.brighter=function(n){return tt(this.h,this.c,Math.min(100,this.l+Ha*(arguments.length?n:1)))},ja.darker=function(n){return tt(this.h,this.c,Math.max(0,this.l-Ha*(arguments.length?n:1)))},ja.rgb=function(){return rt(this.h,this.c,this.l).rgb()},Xo.lab=function(n,t,e){return 1===arguments.length?n instanceof it?ut(n.l,n.a,n.b):n instanceof et?rt(n.l,n.c,n.h):yt((n=Xo.rgb(n)).r,n.g,n.b):ut(+n,+t,+e)};var Ha=18,Fa=.95047,Oa=1,Ya=1.08883,Ia=it.prototype=new G;Ia.brighter=function(n){return ut(Math.min(100,this.l+Ha*(arguments.length?n:1)),this.a,this.b)},Ia.darker=function(n){return ut(Math.max(0,this.l-Ha*(arguments.length?n:1)),this.a,this.b)},Ia.rgb=function(){return ot(this.l,this.a,this.b)},Xo.rgb=function(n,t,e){return 1===arguments.length?n instanceof pt?gt(n.r,n.g,n.b):dt(""+n,gt,nt):gt(~~n,~~t,~~e)};var Za=pt.prototype=new G;Za.brighter=function(n){n=Math.pow(.7,arguments.length?n:1);var t=this.r,e=this.g,r=this.b,u=30;return t||e||r?(t&&u>t&&(t=u),e&&u>e&&(e=u),r&&u>r&&(r=u),gt(Math.min(255,~~(t/n)),Math.min(255,~~(e/n)),Math.min(255,~~(r/n)))):gt(u,u,u)},Za.darker=function(n){return n=Math.pow(.7,arguments.length?n:1),gt(~~(n*this.r),~~(n*this.g),~~(n*this.b))},Za.hsl=function(){return mt(this.r,this.g,this.b)},Za.toString=function(){return"#"+vt(this.r)+vt(this.g)+vt(this.b)};var Va=Xo.map({aliceblue:15792383,antiquewhite:16444375,aqua:65535,aquamarine:8388564,azure:15794175,beige:16119260,bisque:16770244,black:0,blanchedalmond:16772045,blue:255,blueviolet:9055202,brown:10824234,burlywood:14596231,cadetblue:6266528,chartreuse:8388352,chocolate:13789470,coral:16744272,cornflowerblue:6591981,cornsilk:16775388,crimson:14423100,cyan:65535,darkblue:139,darkcyan:35723,darkgoldenrod:12092939,darkgray:11119017,darkgreen:25600,darkgrey:11119017,darkkhaki:12433259,darkmagenta:9109643,darkolivegreen:5597999,darkorange:16747520,darkorchid:10040012,darkred:9109504,darksalmon:15308410,darkseagreen:9419919,darkslateblue:4734347,darkslategray:3100495,darkslategrey:3100495,darkturquoise:52945,darkviolet:9699539,deeppink:16716947,deepskyblue:49151,dimgray:6908265,dimgrey:6908265,dodgerblue:2003199,firebrick:11674146,floralwhite:16775920,forestgreen:2263842,fuchsia:16711935,gainsboro:14474460,ghostwhite:16316671,gold:16766720,goldenrod:14329120,gray:8421504,green:32768,greenyellow:11403055,grey:8421504,honeydew:15794160,hotpink:16738740,indianred:13458524,indigo:4915330,ivory:16777200,khaki:15787660,lavender:15132410,lavenderblush:16773365,lawngreen:8190976,lemonchiffon:16775885,lightblue:11393254,lightcoral:15761536,lightcyan:14745599,lightgoldenrodyellow:16448210,lightgray:13882323,lightgreen:9498256,lightgrey:13882323,lightpink:16758465,lightsalmon:16752762,lightseagreen:2142890,lightskyblue:8900346,lightslategray:7833753,lightslategrey:7833753,lightsteelblue:11584734,lightyellow:16777184,lime:65280,limegreen:3329330,linen:16445670,magenta:16711935,maroon:8388608,mediumaquamarine:6737322,mediumblue:205,mediumorchid:12211667,mediumpurple:9662683,mediumseagreen:3978097,mediumslateblue:8087790,mediumspringgreen:64154,mediumturquoise:4772300,mediumvioletred:13047173,midnightblue:1644912,mintcream:16121850,mistyrose:16770273,moccasin:16770229,navajowhite:16768685,navy:128,oldlace:16643558,olive:8421376,olivedrab:7048739,orange:16753920,orangered:16729344,orchid:14315734,palegoldenrod:15657130,palegreen:10025880,paleturquoise:11529966,palevioletred:14381203,papayawhip:16773077,peachpuff:16767673,peru:13468991,pink:16761035,plum:14524637,powderblue:11591910,purple:8388736,red:16711680,rosybrown:12357519,royalblue:4286945,saddlebrown:9127187,salmon:16416882,sandybrown:16032864,seagreen:3050327,seashell:16774638,sienna:10506797,silver:12632256,skyblue:8900331,slateblue:6970061,slategray:7372944,slategrey:7372944,snow:16775930,springgreen:65407,steelblue:4620980,tan:13808780,teal:32896,thistle:14204888,tomato:16737095,turquoise:4251856,violet:15631086,wheat:16113331,white:16777215,whitesmoke:16119285,yellow:16776960,yellowgreen:10145074});Va.forEach(function(n,t){Va.set(n,ft(t))}),Xo.functor=_t,Xo.xhr=wt(bt),Xo.dsv=function(n,t){function e(n,e,i){arguments.length<3&&(i=e,e=null);var o=St(n,t,null==e?r:u(e),i);return o.row=function(n){return arguments.length?o.response(null==(e=n)?r:u(n)):e},o}function r(n){return e.parse(n.responseText)}function u(n){return function(t){return e.parse(t.responseText,n)}}function i(t){return t.map(o).join(n)}function o(n){return a.test(n)?'"'+n.replace(/\"/g,'""')+'"':n}var a=new RegExp('["'+n+"\n]"),c=n.charCodeAt(0);return e.parse=function(n,t){var r;return e.parseRows(n,function(n,e){if(r)return r(n,e-1);var u=new Function("d","return {"+n.map(function(n,t){return JSON.stringify(n)+": d["+t+"]"}).join(",")+"}");r=t?function(n,e){return t(u(n),e)}:u})},e.parseRows=function(n,t){function e(){if(l>=s)return o;if(u)return u=!1,i;var t=l;if(34===n.charCodeAt(t)){for(var e=t;e++<s;)if(34===n.charCodeAt(e)){if(34!==n.charCodeAt(e+1))break;++e}l=e+2;var r=n.charCodeAt(e+1);return 13===r?(u=!0,10===n.charCodeAt(e+2)&&++l):10===r&&(u=!0),n.substring(t+1,e).replace(/""/g,'"')}for(;s>l;){var r=n.charCodeAt(l++),a=1;if(10===r)u=!0;else if(13===r)u=!0,10===n.charCodeAt(l)&&(++l,++a);else if(r!==c)continue;return n.substring(t,l-a)}return n.substring(t)}for(var r,u,i={},o={},a=[],s=n.length,l=0,f=0;(r=e())!==o;){for(var h=[];r!==i&&r!==o;)h.push(r),r=e();(!t||(h=t(h,f++)))&&a.push(h)}return a},e.format=function(t){if(Array.isArray(t[0]))return e.formatRows(t);var r=new l,u=[];return t.forEach(function(n){for(var t in n)r.has(t)||u.push(r.add(t))}),[u.map(o).join(n)].concat(t.map(function(t){return u.map(function(n){return o(t[n])}).join(n)})).join("\n")},e.formatRows=function(n){return n.map(i).join("\n")},e},Xo.csv=Xo.dsv(",","text/csv"),Xo.tsv=Xo.dsv(" ","text/tab-separated-values");var Xa,$a,Ba,Wa,Ja,Ga=Go[h(Go,"requestAnimationFrame")]||function(n){setTimeout(n,17)};Xo.timer=function(n,t,e){var r=arguments.length;2>r&&(t=0),3>r&&(e=Date.now());var u=e+t,i={c:n,t:u,f:!1,n:null};$a?$a.n=i:Xa=i,$a=i,Ba||(Wa=clearTimeout(Wa),Ba=1,Ga(Et))},Xo.timer.flush=function(){At(),Ct()},Xo.round=function(n,t){return t?Math.round(n*(t=Math.pow(10,t)))/t:Math.round(n)};var Ka=["y","z","a","f","p","n","\xb5","m","","k","M","G","T","P","E","Z","Y"].map(Lt);Xo.formatPrefix=function(n,t){var e=0;return n&&(0>n&&(n*=-1),t&&(n=Xo.round(n,Nt(n,t))),e=1+Math.floor(1e-12+Math.log(n)/Math.LN10),e=Math.max(-24,Math.min(24,3*Math.floor((0>=e?e+1:e-1)/3)))),Ka[8+e/3]};var Qa=/(?:([^{])?([<>=^]))?([+\- ])?([$#])?(0)?(\d+)?(,)?(\.-?\d+)?([a-z%])?/i,nc=Xo.map({b:function(n){return n.toString(2)},c:function(n){return String.fromCharCode(n)},o:function(n){return n.toString(8)},x:function(n){return n.toString(16)},X:function(n){return n.toString(16).toUpperCase()},g:function(n,t){return n.toPrecision(t)},e:function(n,t){return n.toExponential(t)},f:function(n,t){return n.toFixed(t)},r:function(n,t){return(n=Xo.round(n,Nt(n,t))).toFixed(Math.max(0,Math.min(20,Nt(n*(1+1e-15),t))))}}),tc=Xo.time={},ec=Date;Tt.prototype={getDate:function(){return this._.getUTCDate()},getDay:function(){return this._.getUTCDay()},getFullYear:function(){return this._.getUTCFullYear()},getHours:function(){return this._.getUTCHours()},getMilliseconds:function(){return this._.getUTCMilliseconds()},getMinutes:function(){return this._.getUTCMinutes()},getMonth:function(){return this._.getUTCMonth()},getSeconds:function(){return this._.getUTCSeconds()},getTime:function(){return this._.getTime()},getTimezoneOffset:function(){return 0},valueOf:function(){return this._.valueOf()},setDate:function(){rc.setUTCDate.apply(this._,arguments)},setDay:function(){rc.setUTCDay.apply(this._,arguments)},setFullYear:function(){rc.setUTCFullYear.apply(this._,arguments)},setHours:function(){rc.setUTCHours.apply(this._,arguments)},setMilliseconds:function(){rc.setUTCMilliseconds.apply(this._,arguments)},setMinutes:function(){rc.setUTCMinutes.apply(this._,arguments)},setMonth:function(){rc.setUTCMonth.apply(this._,arguments)},setSeconds:function(){rc.setUTCSeconds.apply(this._,arguments)},setTime:function(){rc.setTime.apply(this._,arguments)}};var rc=Date.prototype;tc.year=Rt(function(n){return n=tc.day(n),n.setMonth(0,1),n},function(n,t){n.setFullYear(n.getFullYear()+t)},function(n){return n.getFullYear()}),tc.years=tc.year.range,tc.years.utc=tc.year.utc.range,tc.day=Rt(function(n){var t=new ec(2e3,0);return t.setFullYear(n.getFullYear(),n.getMonth(),n.getDate()),t},function(n,t){n.setDate(n.getDate()+t)},function(n){return n.getDate()-1}),tc.days=tc.day.range,tc.days.utc=tc.day.utc.range,tc.dayOfYear=function(n){var t=tc.year(n);return Math.floor((n-t-6e4*(n.getTimezoneOffset()-t.getTimezoneOffset()))/864e5)},["sunday","monday","tuesday","wednesday","thursday","friday","saturday"].forEach(function(n,t){t=7-t;var e=tc[n]=Rt(function(n){return(n=tc.day(n)).setDate(n.getDate()-(n.getDay()+t)%7),n},function(n,t){n.setDate(n.getDate()+7*Math.floor(t))},function(n){var e=tc.year(n).getDay();return Math.floor((tc.dayOfYear(n)+(e+t)%7)/7)-(e!==t)});tc[n+"s"]=e.range,tc[n+"s"].utc=e.utc.range,tc[n+"OfYear"]=function(n){var e=tc.year(n).getDay();return Math.floor((tc.dayOfYear(n)+(e+t)%7)/7)}}),tc.week=tc.sunday,tc.weeks=tc.sunday.range,tc.weeks.utc=tc.sunday.utc.range,tc.weekOfYear=tc.sundayOfYear;var uc={"-":"",_:" ",0:"0"},ic=/^\s*\d+/,oc=/^%/;Xo.locale=function(n){return{numberFormat:zt(n),timeFormat:Pt(n)}};var ac=Xo.locale({decimal:".",thousands:",",grouping:[3],currency:["$",""],dateTime:"%a %b %e %X %Y",date:"%m/%d/%Y",time:"%H:%M:%S",periods:["AM","PM"],days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],shortDays:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],shortMonths:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]});Xo.format=ac.numberFormat,Xo.geo={},re.prototype={s:0,t:0,add:function(n){ue(n,this.t,cc),ue(cc.s,this.s,this),this.s?this.t+=cc.t:this.s=cc.t},reset:function(){this.s=this.t=0},valueOf:function(){return this.s}};var cc=new re;Xo.geo.stream=function(n,t){n&&sc.hasOwnProperty(n.type)?sc[n.type](n,t):ie(n,t)};var sc={Feature:function(n,t){ie(n.geometry,t)},FeatureCollection:function(n,t){for(var e=n.features,r=-1,u=e.length;++r<u;)ie(e[r].geometry,t)}},lc={Sphere:function(n,t){t.sphere()},Point:function(n,t){n=n.coordinates,t.point(n[0],n[1],n[2])},MultiPoint:function(n,t){for(var e=n.coordinates,r=-1,u=e.length;++r<u;)n=e[r],t.point(n[0],n[1],n[2])},LineString:function(n,t){oe(n.coordinates,t,0)},MultiLineString:function(n,t){for(var e=n.coordinates,r=-1,u=e.length;++r<u;)oe(e[r],t,0)},Polygon:function(n,t){ae(n.coordinates,t)},MultiPolygon:function(n,t){for(var e=n.coordinates,r=-1,u=e.length;++r<u;)ae(e[r],t)},GeometryCollection:function(n,t){for(var e=n.geometries,r=-1,u=e.length;++r<u;)ie(e[r],t)}};Xo.geo.area=function(n){return fc=0,Xo.geo.stream(n,gc),fc};var fc,hc=new re,gc={sphere:function(){fc+=4*Sa},point:g,lineStart:g,lineEnd:g,polygonStart:function(){hc.reset(),gc.lineStart=ce},polygonEnd:function(){var n=2*hc;fc+=0>n?4*Sa+n:n,gc.lineStart=gc.lineEnd=gc.point=g}};Xo.geo.bounds=function(){function n(n,t){x.push(M=[l=n,h=n]),f>t&&(f=t),t>g&&(g=t)}function t(t,e){var r=se([t*Na,e*Na]);if(m){var u=fe(m,r),i=[u[1],-u[0],0],o=fe(i,u);pe(o),o=ve(o);var c=t-p,s=c>0?1:-1,v=o[0]*La*s,d=oa(c)>180;if(d^(v>s*p&&s*t>v)){var y=o[1]*La;y>g&&(g=y)}else if(v=(v+360)%360-180,d^(v>s*p&&s*t>v)){var y=-o[1]*La;f>y&&(f=y)}else f>e&&(f=e),e>g&&(g=e);d?p>t?a(l,t)>a(l,h)&&(h=t):a(t,h)>a(l,h)&&(l=t):h>=l?(l>t&&(l=t),t>h&&(h=t)):t>p?a(l,t)>a(l,h)&&(h=t):a(t,h)>a(l,h)&&(l=t)}else n(t,e);m=r,p=t}function e(){_.point=t}function r(){M[0]=l,M[1]=h,_.point=n,m=null}function u(n,e){if(m){var r=n-p;y+=oa(r)>180?r+(r>0?360:-360):r}else v=n,d=e;gc.point(n,e),t(n,e)}function i(){gc.lineStart()}function o(){u(v,d),gc.lineEnd(),oa(y)>Aa&&(l=-(h=180)),M[0]=l,M[1]=h,m=null}function a(n,t){return(t-=n)<0?t+360:t}function c(n,t){return n[0]-t[0]}function s(n,t){return t[0]<=t[1]?t[0]<=n&&n<=t[1]:n<t[0]||t[1]<n}var l,f,h,g,p,v,d,m,y,x,M,_={point:n,lineStart:e,lineEnd:r,polygonStart:function(){_.point=u,_.lineStart=i,_.lineEnd=o,y=0,gc.polygonStart()},polygonEnd:function(){gc.polygonEnd(),_.point=n,_.lineStart=e,_.lineEnd=r,0>hc?(l=-(h=180),f=-(g=90)):y>Aa?g=90:-Aa>y&&(f=-90),M[0]=l,M[1]=h
4
+ }};return function(n){g=h=-(l=f=1/0),x=[],Xo.geo.stream(n,_);var t=x.length;if(t){x.sort(c);for(var e,r=1,u=x[0],i=[u];t>r;++r)e=x[r],s(e[0],u)||s(e[1],u)?(a(u[0],e[1])>a(u[0],u[1])&&(u[1]=e[1]),a(e[0],u[1])>a(u[0],u[1])&&(u[0]=e[0])):i.push(u=e);for(var o,e,p=-1/0,t=i.length-1,r=0,u=i[t];t>=r;u=e,++r)e=i[r],(o=a(u[1],e[0]))>p&&(p=o,l=e[0],h=u[1])}return x=M=null,1/0===l||1/0===f?[[0/0,0/0],[0/0,0/0]]:[[l,f],[h,g]]}}(),Xo.geo.centroid=function(n){pc=vc=dc=mc=yc=xc=Mc=_c=bc=wc=Sc=0,Xo.geo.stream(n,kc);var t=bc,e=wc,r=Sc,u=t*t+e*e+r*r;return Ca>u&&(t=xc,e=Mc,r=_c,Aa>vc&&(t=dc,e=mc,r=yc),u=t*t+e*e+r*r,Ca>u)?[0/0,0/0]:[Math.atan2(e,t)*La,X(r/Math.sqrt(u))*La]};var pc,vc,dc,mc,yc,xc,Mc,_c,bc,wc,Sc,kc={sphere:g,point:me,lineStart:xe,lineEnd:Me,polygonStart:function(){kc.lineStart=_e},polygonEnd:function(){kc.lineStart=xe}},Ec=Ee(be,ze,Te,[-Sa,-Sa/2]),Ac=1e9;Xo.geo.clipExtent=function(){var n,t,e,r,u,i,o={stream:function(n){return u&&(u.valid=!1),u=i(n),u.valid=!0,u},extent:function(a){return arguments.length?(i=Pe(n=+a[0][0],t=+a[0][1],e=+a[1][0],r=+a[1][1]),u&&(u.valid=!1,u=null),o):[[n,t],[e,r]]}};return o.extent([[0,0],[960,500]])},(Xo.geo.conicEqualArea=function(){return je(He)}).raw=He,Xo.geo.albers=function(){return Xo.geo.conicEqualArea().rotate([96,0]).center([-.6,38.7]).parallels([29.5,45.5]).scale(1070)},Xo.geo.albersUsa=function(){function n(n){var i=n[0],o=n[1];return t=null,e(i,o),t||(r(i,o),t)||u(i,o),t}var t,e,r,u,i=Xo.geo.albers(),o=Xo.geo.conicEqualArea().rotate([154,0]).center([-2,58.5]).parallels([55,65]),a=Xo.geo.conicEqualArea().rotate([157,0]).center([-3,19.9]).parallels([8,18]),c={point:function(n,e){t=[n,e]}};return n.invert=function(n){var t=i.scale(),e=i.translate(),r=(n[0]-e[0])/t,u=(n[1]-e[1])/t;return(u>=.12&&.234>u&&r>=-.425&&-.214>r?o:u>=.166&&.234>u&&r>=-.214&&-.115>r?a:i).invert(n)},n.stream=function(n){var t=i.stream(n),e=o.stream(n),r=a.stream(n);return{point:function(n,u){t.point(n,u),e.point(n,u),r.point(n,u)},sphere:function(){t.sphere(),e.sphere(),r.sphere()},lineStart:function(){t.lineStart(),e.lineStart(),r.lineStart()},lineEnd:function(){t.lineEnd(),e.lineEnd(),r.lineEnd()},polygonStart:function(){t.polygonStart(),e.polygonStart(),r.polygonStart()},polygonEnd:function(){t.polygonEnd(),e.polygonEnd(),r.polygonEnd()}}},n.precision=function(t){return arguments.length?(i.precision(t),o.precision(t),a.precision(t),n):i.precision()},n.scale=function(t){return arguments.length?(i.scale(t),o.scale(.35*t),a.scale(t),n.translate(i.translate())):i.scale()},n.translate=function(t){if(!arguments.length)return i.translate();var s=i.scale(),l=+t[0],f=+t[1];return e=i.translate(t).clipExtent([[l-.455*s,f-.238*s],[l+.455*s,f+.238*s]]).stream(c).point,r=o.translate([l-.307*s,f+.201*s]).clipExtent([[l-.425*s+Aa,f+.12*s+Aa],[l-.214*s-Aa,f+.234*s-Aa]]).stream(c).point,u=a.translate([l-.205*s,f+.212*s]).clipExtent([[l-.214*s+Aa,f+.166*s+Aa],[l-.115*s-Aa,f+.234*s-Aa]]).stream(c).point,n},n.scale(1070)};var Cc,Nc,Lc,zc,qc,Tc,Rc={point:g,lineStart:g,lineEnd:g,polygonStart:function(){Nc=0,Rc.lineStart=Fe},polygonEnd:function(){Rc.lineStart=Rc.lineEnd=Rc.point=g,Cc+=oa(Nc/2)}},Dc={point:Oe,lineStart:g,lineEnd:g,polygonStart:g,polygonEnd:g},Pc={point:Ze,lineStart:Ve,lineEnd:Xe,polygonStart:function(){Pc.lineStart=$e},polygonEnd:function(){Pc.point=Ze,Pc.lineStart=Ve,Pc.lineEnd=Xe}};Xo.geo.path=function(){function n(n){return n&&("function"==typeof a&&i.pointRadius(+a.apply(this,arguments)),o&&o.valid||(o=u(i)),Xo.geo.stream(n,o)),i.result()}function t(){return o=null,n}var e,r,u,i,o,a=4.5;return n.area=function(n){return Cc=0,Xo.geo.stream(n,u(Rc)),Cc},n.centroid=function(n){return dc=mc=yc=xc=Mc=_c=bc=wc=Sc=0,Xo.geo.stream(n,u(Pc)),Sc?[bc/Sc,wc/Sc]:_c?[xc/_c,Mc/_c]:yc?[dc/yc,mc/yc]:[0/0,0/0]},n.bounds=function(n){return qc=Tc=-(Lc=zc=1/0),Xo.geo.stream(n,u(Dc)),[[Lc,zc],[qc,Tc]]},n.projection=function(n){return arguments.length?(u=(e=n)?n.stream||Je(n):bt,t()):e},n.context=function(n){return arguments.length?(i=null==(r=n)?new Ye:new Be(n),"function"!=typeof a&&i.pointRadius(a),t()):r},n.pointRadius=function(t){return arguments.length?(a="function"==typeof t?t:(i.pointRadius(+t),+t),n):a},n.projection(Xo.geo.albersUsa()).context(null)},Xo.geo.transform=function(n){return{stream:function(t){var e=new Ge(t);for(var r in n)e[r]=n[r];return e}}},Ge.prototype={point:function(n,t){this.stream.point(n,t)},sphere:function(){this.stream.sphere()},lineStart:function(){this.stream.lineStart()},lineEnd:function(){this.stream.lineEnd()},polygonStart:function(){this.stream.polygonStart()},polygonEnd:function(){this.stream.polygonEnd()}},Xo.geo.projection=Qe,Xo.geo.projectionMutator=nr,(Xo.geo.equirectangular=function(){return Qe(er)}).raw=er.invert=er,Xo.geo.rotation=function(n){function t(t){return t=n(t[0]*Na,t[1]*Na),t[0]*=La,t[1]*=La,t}return n=ur(n[0]%360*Na,n[1]*Na,n.length>2?n[2]*Na:0),t.invert=function(t){return t=n.invert(t[0]*Na,t[1]*Na),t[0]*=La,t[1]*=La,t},t},rr.invert=er,Xo.geo.circle=function(){function n(){var n="function"==typeof r?r.apply(this,arguments):r,t=ur(-n[0]*Na,-n[1]*Na,0).invert,u=[];return e(null,null,1,{point:function(n,e){u.push(n=t(n,e)),n[0]*=La,n[1]*=La}}),{type:"Polygon",coordinates:[u]}}var t,e,r=[0,0],u=6;return n.origin=function(t){return arguments.length?(r=t,n):r},n.angle=function(r){return arguments.length?(e=cr((t=+r)*Na,u*Na),n):t},n.precision=function(r){return arguments.length?(e=cr(t*Na,(u=+r)*Na),n):u},n.angle(90)},Xo.geo.distance=function(n,t){var e,r=(t[0]-n[0])*Na,u=n[1]*Na,i=t[1]*Na,o=Math.sin(r),a=Math.cos(r),c=Math.sin(u),s=Math.cos(u),l=Math.sin(i),f=Math.cos(i);return Math.atan2(Math.sqrt((e=f*o)*e+(e=s*l-c*f*a)*e),c*l+s*f*a)},Xo.geo.graticule=function(){function n(){return{type:"MultiLineString",coordinates:t()}}function t(){return Xo.range(Math.ceil(i/d)*d,u,d).map(h).concat(Xo.range(Math.ceil(s/m)*m,c,m).map(g)).concat(Xo.range(Math.ceil(r/p)*p,e,p).filter(function(n){return oa(n%d)>Aa}).map(l)).concat(Xo.range(Math.ceil(a/v)*v,o,v).filter(function(n){return oa(n%m)>Aa}).map(f))}var e,r,u,i,o,a,c,s,l,f,h,g,p=10,v=p,d=90,m=360,y=2.5;return n.lines=function(){return t().map(function(n){return{type:"LineString",coordinates:n}})},n.outline=function(){return{type:"Polygon",coordinates:[h(i).concat(g(c).slice(1),h(u).reverse().slice(1),g(s).reverse().slice(1))]}},n.extent=function(t){return arguments.length?n.majorExtent(t).minorExtent(t):n.minorExtent()},n.majorExtent=function(t){return arguments.length?(i=+t[0][0],u=+t[1][0],s=+t[0][1],c=+t[1][1],i>u&&(t=i,i=u,u=t),s>c&&(t=s,s=c,c=t),n.precision(y)):[[i,s],[u,c]]},n.minorExtent=function(t){return arguments.length?(r=+t[0][0],e=+t[1][0],a=+t[0][1],o=+t[1][1],r>e&&(t=r,r=e,e=t),a>o&&(t=a,a=o,o=t),n.precision(y)):[[r,a],[e,o]]},n.step=function(t){return arguments.length?n.majorStep(t).minorStep(t):n.minorStep()},n.majorStep=function(t){return arguments.length?(d=+t[0],m=+t[1],n):[d,m]},n.minorStep=function(t){return arguments.length?(p=+t[0],v=+t[1],n):[p,v]},n.precision=function(t){return arguments.length?(y=+t,l=lr(a,o,90),f=fr(r,e,y),h=lr(s,c,90),g=fr(i,u,y),n):y},n.majorExtent([[-180,-90+Aa],[180,90-Aa]]).minorExtent([[-180,-80-Aa],[180,80+Aa]])},Xo.geo.greatArc=function(){function n(){return{type:"LineString",coordinates:[t||r.apply(this,arguments),e||u.apply(this,arguments)]}}var t,e,r=hr,u=gr;return n.distance=function(){return Xo.geo.distance(t||r.apply(this,arguments),e||u.apply(this,arguments))},n.source=function(e){return arguments.length?(r=e,t="function"==typeof e?null:e,n):r},n.target=function(t){return arguments.length?(u=t,e="function"==typeof t?null:t,n):u},n.precision=function(){return arguments.length?n:0},n},Xo.geo.interpolate=function(n,t){return pr(n[0]*Na,n[1]*Na,t[0]*Na,t[1]*Na)},Xo.geo.length=function(n){return Uc=0,Xo.geo.stream(n,jc),Uc};var Uc,jc={sphere:g,point:g,lineStart:vr,lineEnd:g,polygonStart:g,polygonEnd:g},Hc=dr(function(n){return Math.sqrt(2/(1+n))},function(n){return 2*Math.asin(n/2)});(Xo.geo.azimuthalEqualArea=function(){return Qe(Hc)}).raw=Hc;var Fc=dr(function(n){var t=Math.acos(n);return t&&t/Math.sin(t)},bt);(Xo.geo.azimuthalEquidistant=function(){return Qe(Fc)}).raw=Fc,(Xo.geo.conicConformal=function(){return je(mr)}).raw=mr,(Xo.geo.conicEquidistant=function(){return je(yr)}).raw=yr;var Oc=dr(function(n){return 1/n},Math.atan);(Xo.geo.gnomonic=function(){return Qe(Oc)}).raw=Oc,xr.invert=function(n,t){return[n,2*Math.atan(Math.exp(t))-Ea]},(Xo.geo.mercator=function(){return Mr(xr)}).raw=xr;var Yc=dr(function(){return 1},Math.asin);(Xo.geo.orthographic=function(){return Qe(Yc)}).raw=Yc;var Ic=dr(function(n){return 1/(1+n)},function(n){return 2*Math.atan(n)});(Xo.geo.stereographic=function(){return Qe(Ic)}).raw=Ic,_r.invert=function(n,t){return[-t,2*Math.atan(Math.exp(n))-Ea]},(Xo.geo.transverseMercator=function(){var n=Mr(_r),t=n.center,e=n.rotate;return n.center=function(n){return n?t([-n[1],n[0]]):(n=t(),[-n[1],n[0]])},n.rotate=function(n){return n?e([n[0],n[1],n.length>2?n[2]+90:90]):(n=e(),[n[0],n[1],n[2]-90])},n.rotate([0,0])}).raw=_r,Xo.geom={},Xo.geom.hull=function(n){function t(n){if(n.length<3)return[];var t,u=_t(e),i=_t(r),o=n.length,a=[],c=[];for(t=0;o>t;t++)a.push([+u.call(this,n[t],t),+i.call(this,n[t],t),t]);for(a.sort(kr),t=0;o>t;t++)c.push([a[t][0],-a[t][1]]);var s=Sr(a),l=Sr(c),f=l[0]===s[0],h=l[l.length-1]===s[s.length-1],g=[];for(t=s.length-1;t>=0;--t)g.push(n[a[s[t]][2]]);for(t=+f;t<l.length-h;++t)g.push(n[a[l[t]][2]]);return g}var e=br,r=wr;return arguments.length?t(n):(t.x=function(n){return arguments.length?(e=n,t):e},t.y=function(n){return arguments.length?(r=n,t):r},t)},Xo.geom.polygon=function(n){return fa(n,Zc),n};var Zc=Xo.geom.polygon.prototype=[];Zc.area=function(){for(var n,t=-1,e=this.length,r=this[e-1],u=0;++t<e;)n=r,r=this[t],u+=n[1]*r[0]-n[0]*r[1];return.5*u},Zc.centroid=function(n){var t,e,r=-1,u=this.length,i=0,o=0,a=this[u-1];for(arguments.length||(n=-1/(6*this.area()));++r<u;)t=a,a=this[r],e=t[0]*a[1]-a[0]*t[1],i+=(t[0]+a[0])*e,o+=(t[1]+a[1])*e;return[i*n,o*n]},Zc.clip=function(n){for(var t,e,r,u,i,o,a=Cr(n),c=-1,s=this.length-Cr(this),l=this[s-1];++c<s;){for(t=n.slice(),n.length=0,u=this[c],i=t[(r=t.length-a)-1],e=-1;++e<r;)o=t[e],Er(o,l,u)?(Er(i,l,u)||n.push(Ar(i,o,l,u)),n.push(o)):Er(i,l,u)&&n.push(Ar(i,o,l,u)),i=o;a&&n.push(n[0]),l=u}return n};var Vc,Xc,$c,Bc,Wc,Jc=[],Gc=[];Pr.prototype.prepare=function(){for(var n,t=this.edges,e=t.length;e--;)n=t[e].edge,n.b&&n.a||t.splice(e,1);return t.sort(jr),t.length},Br.prototype={start:function(){return this.edge.l===this.site?this.edge.a:this.edge.b},end:function(){return this.edge.l===this.site?this.edge.b:this.edge.a}},Wr.prototype={insert:function(n,t){var e,r,u;if(n){if(t.P=n,t.N=n.N,n.N&&(n.N.P=t),n.N=t,n.R){for(n=n.R;n.L;)n=n.L;n.L=t}else n.R=t;e=n}else this._?(n=Qr(this._),t.P=null,t.N=n,n.P=n.L=t,e=n):(t.P=t.N=null,this._=t,e=null);for(t.L=t.R=null,t.U=e,t.C=!0,n=t;e&&e.C;)r=e.U,e===r.L?(u=r.R,u&&u.C?(e.C=u.C=!1,r.C=!0,n=r):(n===e.R&&(Gr(this,e),n=e,e=n.U),e.C=!1,r.C=!0,Kr(this,r))):(u=r.L,u&&u.C?(e.C=u.C=!1,r.C=!0,n=r):(n===e.L&&(Kr(this,e),n=e,e=n.U),e.C=!1,r.C=!0,Gr(this,r))),e=n.U;this._.C=!1},remove:function(n){n.N&&(n.N.P=n.P),n.P&&(n.P.N=n.N),n.N=n.P=null;var t,e,r,u=n.U,i=n.L,o=n.R;if(e=i?o?Qr(o):i:o,u?u.L===n?u.L=e:u.R=e:this._=e,i&&o?(r=e.C,e.C=n.C,e.L=i,i.U=e,e!==o?(u=e.U,e.U=n.U,n=e.R,u.L=n,e.R=o,o.U=e):(e.U=u,u=e,n=e.R)):(r=n.C,n=e),n&&(n.U=u),!r){if(n&&n.C)return n.C=!1,void 0;do{if(n===this._)break;if(n===u.L){if(t=u.R,t.C&&(t.C=!1,u.C=!0,Gr(this,u),t=u.R),t.L&&t.L.C||t.R&&t.R.C){t.R&&t.R.C||(t.L.C=!1,t.C=!0,Kr(this,t),t=u.R),t.C=u.C,u.C=t.R.C=!1,Gr(this,u),n=this._;break}}else if(t=u.L,t.C&&(t.C=!1,u.C=!0,Kr(this,u),t=u.L),t.L&&t.L.C||t.R&&t.R.C){t.L&&t.L.C||(t.R.C=!1,t.C=!0,Gr(this,t),t=u.L),t.C=u.C,u.C=t.L.C=!1,Kr(this,u),n=this._;break}t.C=!0,n=u,u=u.U}while(!n.C);n&&(n.C=!1)}}},Xo.geom.voronoi=function(n){function t(n){var t=new Array(n.length),r=a[0][0],u=a[0][1],i=a[1][0],o=a[1][1];return nu(e(n),a).cells.forEach(function(e,a){var c=e.edges,s=e.site,l=t[a]=c.length?c.map(function(n){var t=n.start();return[t.x,t.y]}):s.x>=r&&s.x<=i&&s.y>=u&&s.y<=o?[[r,o],[i,o],[i,u],[r,u]]:[];l.point=n[a]}),t}function e(n){return n.map(function(n,t){return{x:Math.round(i(n,t)/Aa)*Aa,y:Math.round(o(n,t)/Aa)*Aa,i:t}})}var r=br,u=wr,i=r,o=u,a=Kc;return n?t(n):(t.links=function(n){return nu(e(n)).edges.filter(function(n){return n.l&&n.r}).map(function(t){return{source:n[t.l.i],target:n[t.r.i]}})},t.triangles=function(n){var t=[];return nu(e(n)).cells.forEach(function(e,r){for(var u,i,o=e.site,a=e.edges.sort(jr),c=-1,s=a.length,l=a[s-1].edge,f=l.l===o?l.r:l.l;++c<s;)u=l,i=f,l=a[c].edge,f=l.l===o?l.r:l.l,r<i.i&&r<f.i&&eu(o,i,f)<0&&t.push([n[r],n[i.i],n[f.i]])}),t},t.x=function(n){return arguments.length?(i=_t(r=n),t):r},t.y=function(n){return arguments.length?(o=_t(u=n),t):u},t.clipExtent=function(n){return arguments.length?(a=null==n?Kc:n,t):a===Kc?null:a},t.size=function(n){return arguments.length?t.clipExtent(n&&[[0,0],n]):a===Kc?null:a&&a[1]},t)};var Kc=[[-1e6,-1e6],[1e6,1e6]];Xo.geom.delaunay=function(n){return Xo.geom.voronoi().triangles(n)},Xo.geom.quadtree=function(n,t,e,r,u){function i(n){function i(n,t,e,r,u,i,o,a){if(!isNaN(e)&&!isNaN(r))if(n.leaf){var c=n.x,l=n.y;if(null!=c)if(oa(c-e)+oa(l-r)<.01)s(n,t,e,r,u,i,o,a);else{var f=n.point;n.x=n.y=n.point=null,s(n,f,c,l,u,i,o,a),s(n,t,e,r,u,i,o,a)}else n.x=e,n.y=r,n.point=t}else s(n,t,e,r,u,i,o,a)}function s(n,t,e,r,u,o,a,c){var s=.5*(u+a),l=.5*(o+c),f=e>=s,h=r>=l,g=(h<<1)+f;n.leaf=!1,n=n.nodes[g]||(n.nodes[g]=iu()),f?u=s:a=s,h?o=l:c=l,i(n,t,e,r,u,o,a,c)}var l,f,h,g,p,v,d,m,y,x=_t(a),M=_t(c);if(null!=t)v=t,d=e,m=r,y=u;else if(m=y=-(v=d=1/0),f=[],h=[],p=n.length,o)for(g=0;p>g;++g)l=n[g],l.x<v&&(v=l.x),l.y<d&&(d=l.y),l.x>m&&(m=l.x),l.y>y&&(y=l.y),f.push(l.x),h.push(l.y);else for(g=0;p>g;++g){var _=+x(l=n[g],g),b=+M(l,g);v>_&&(v=_),d>b&&(d=b),_>m&&(m=_),b>y&&(y=b),f.push(_),h.push(b)}var w=m-v,S=y-d;w>S?y=d+w:m=v+S;var k=iu();if(k.add=function(n){i(k,n,+x(n,++g),+M(n,g),v,d,m,y)},k.visit=function(n){ou(n,k,v,d,m,y)},g=-1,null==t){for(;++g<p;)i(k,n[g],f[g],h[g],v,d,m,y);--g}else n.forEach(k.add);return f=h=n=l=null,k}var o,a=br,c=wr;return(o=arguments.length)?(a=ru,c=uu,3===o&&(u=e,r=t,e=t=0),i(n)):(i.x=function(n){return arguments.length?(a=n,i):a},i.y=function(n){return arguments.length?(c=n,i):c},i.extent=function(n){return arguments.length?(null==n?t=e=r=u=null:(t=+n[0][0],e=+n[0][1],r=+n[1][0],u=+n[1][1]),i):null==t?null:[[t,e],[r,u]]},i.size=function(n){return arguments.length?(null==n?t=e=r=u=null:(t=e=0,r=+n[0],u=+n[1]),i):null==t?null:[r-t,u-e]},i)},Xo.interpolateRgb=au,Xo.interpolateObject=cu,Xo.interpolateNumber=su,Xo.interpolateString=lu;var Qc=/[-+]?(?:\d+\.?\d*|\.?\d+)(?:[eE][-+]?\d+)?/g;Xo.interpolate=fu,Xo.interpolators=[function(n,t){var e=typeof t;return("string"===e?Va.has(t)||/^(#|rgb\(|hsl\()/.test(t)?au:lu:t instanceof G?au:"object"===e?Array.isArray(t)?hu:cu:su)(n,t)}],Xo.interpolateArray=hu;var ns=function(){return bt},ts=Xo.map({linear:ns,poly:xu,quad:function(){return du},cubic:function(){return mu},sin:function(){return Mu},exp:function(){return _u},circle:function(){return bu},elastic:wu,back:Su,bounce:function(){return ku}}),es=Xo.map({"in":bt,out:pu,"in-out":vu,"out-in":function(n){return vu(pu(n))}});Xo.ease=function(n){var t=n.indexOf("-"),e=t>=0?n.substring(0,t):n,r=t>=0?n.substring(t+1):"in";return e=ts.get(e)||ns,r=es.get(r)||bt,gu(r(e.apply(null,$o.call(arguments,1))))},Xo.interpolateHcl=Eu,Xo.interpolateHsl=Au,Xo.interpolateLab=Cu,Xo.interpolateRound=Nu,Xo.transform=function(n){var t=Wo.createElementNS(Xo.ns.prefix.svg,"g");return(Xo.transform=function(n){if(null!=n){t.setAttribute("transform",n);var e=t.transform.baseVal.consolidate()}return new Lu(e?e.matrix:rs)})(n)},Lu.prototype.toString=function(){return"translate("+this.translate+")rotate("+this.rotate+")skewX("+this.skew+")scale("+this.scale+")"};var rs={a:1,b:0,c:0,d:1,e:0,f:0};Xo.interpolateTransform=Ru,Xo.layout={},Xo.layout.bundle=function(){return function(n){for(var t=[],e=-1,r=n.length;++e<r;)t.push(Uu(n[e]));return t}},Xo.layout.chord=function(){function n(){var n,s,f,h,g,p={},v=[],d=Xo.range(i),m=[];for(e=[],r=[],n=0,h=-1;++h<i;){for(s=0,g=-1;++g<i;)s+=u[h][g];v.push(s),m.push(Xo.range(i)),n+=s}for(o&&d.sort(function(n,t){return o(v[n],v[t])}),a&&m.forEach(function(n,t){n.sort(function(n,e){return a(u[t][n],u[t][e])})}),n=(ka-l*i)/n,s=0,h=-1;++h<i;){for(f=s,g=-1;++g<i;){var y=d[h],x=m[y][g],M=u[y][x],_=s,b=s+=M*n;p[y+"-"+x]={index:y,subindex:x,startAngle:_,endAngle:b,value:M}}r[y]={index:y,startAngle:f,endAngle:s,value:(s-f)/n},s+=l}for(h=-1;++h<i;)for(g=h-1;++g<i;){var w=p[h+"-"+g],S=p[g+"-"+h];(w.value||S.value)&&e.push(w.value<S.value?{source:S,target:w}:{source:w,target:S})}c&&t()}function t(){e.sort(function(n,t){return c((n.source.value+n.target.value)/2,(t.source.value+t.target.value)/2)})}var e,r,u,i,o,a,c,s={},l=0;return s.matrix=function(n){return arguments.length?(i=(u=n)&&u.length,e=r=null,s):u},s.padding=function(n){return arguments.length?(l=n,e=r=null,s):l},s.sortGroups=function(n){return arguments.length?(o=n,e=r=null,s):o},s.sortSubgroups=function(n){return arguments.length?(a=n,e=null,s):a},s.sortChords=function(n){return arguments.length?(c=n,e&&t(),s):c},s.chords=function(){return e||n(),e},s.groups=function(){return r||n(),r},s},Xo.layout.force=function(){function n(n){return function(t,e,r,u){if(t.point!==n){var i=t.cx-n.x,o=t.cy-n.y,a=u-e,c=i*i+o*o;if(c>a*a/d){if(p>c){var s=t.charge/c;n.px-=i*s,n.py-=o*s}return!0}if(t.point&&c&&p>c){var s=t.pointCharge/c;n.px-=i*s,n.py-=o*s}}return!t.charge}}function t(n){n.px=Xo.event.x,n.py=Xo.event.y,a.resume()}var e,r,u,i,o,a={},c=Xo.dispatch("start","tick","end"),s=[1,1],l=.9,f=us,h=is,g=-30,p=os,v=.1,d=.64,m=[],y=[];return a.tick=function(){if((r*=.99)<.005)return c.end({type:"end",alpha:r=0}),!0;var t,e,a,f,h,p,d,x,M,_=m.length,b=y.length;for(e=0;b>e;++e)a=y[e],f=a.source,h=a.target,x=h.x-f.x,M=h.y-f.y,(p=x*x+M*M)&&(p=r*i[e]*((p=Math.sqrt(p))-u[e])/p,x*=p,M*=p,h.x-=x*(d=f.weight/(h.weight+f.weight)),h.y-=M*d,f.x+=x*(d=1-d),f.y+=M*d);if((d=r*v)&&(x=s[0]/2,M=s[1]/2,e=-1,d))for(;++e<_;)a=m[e],a.x+=(x-a.x)*d,a.y+=(M-a.y)*d;if(g)for(Zu(t=Xo.geom.quadtree(m),r,o),e=-1;++e<_;)(a=m[e]).fixed||t.visit(n(a));for(e=-1;++e<_;)a=m[e],a.fixed?(a.x=a.px,a.y=a.py):(a.x-=(a.px-(a.px=a.x))*l,a.y-=(a.py-(a.py=a.y))*l);c.tick({type:"tick",alpha:r})},a.nodes=function(n){return arguments.length?(m=n,a):m},a.links=function(n){return arguments.length?(y=n,a):y},a.size=function(n){return arguments.length?(s=n,a):s},a.linkDistance=function(n){return arguments.length?(f="function"==typeof n?n:+n,a):f},a.distance=a.linkDistance,a.linkStrength=function(n){return arguments.length?(h="function"==typeof n?n:+n,a):h},a.friction=function(n){return arguments.length?(l=+n,a):l},a.charge=function(n){return arguments.length?(g="function"==typeof n?n:+n,a):g},a.chargeDistance=function(n){return arguments.length?(p=n*n,a):Math.sqrt(p)},a.gravity=function(n){return arguments.length?(v=+n,a):v},a.theta=function(n){return arguments.length?(d=n*n,a):Math.sqrt(d)},a.alpha=function(n){return arguments.length?(n=+n,r?r=n>0?n:0:n>0&&(c.start({type:"start",alpha:r=n}),Xo.timer(a.tick)),a):r},a.start=function(){function n(n,r){if(!e){for(e=new Array(c),a=0;c>a;++a)e[a]=[];for(a=0;s>a;++a){var u=y[a];e[u.source.index].push(u.target),e[u.target.index].push(u.source)}}for(var i,o=e[t],a=-1,s=o.length;++a<s;)if(!isNaN(i=o[a][n]))return i;return Math.random()*r}var t,e,r,c=m.length,l=y.length,p=s[0],v=s[1];for(t=0;c>t;++t)(r=m[t]).index=t,r.weight=0;for(t=0;l>t;++t)r=y[t],"number"==typeof r.source&&(r.source=m[r.source]),"number"==typeof r.target&&(r.target=m[r.target]),++r.source.weight,++r.target.weight;for(t=0;c>t;++t)r=m[t],isNaN(r.x)&&(r.x=n("x",p)),isNaN(r.y)&&(r.y=n("y",v)),isNaN(r.px)&&(r.px=r.x),isNaN(r.py)&&(r.py=r.y);if(u=[],"function"==typeof f)for(t=0;l>t;++t)u[t]=+f.call(this,y[t],t);else for(t=0;l>t;++t)u[t]=f;if(i=[],"function"==typeof h)for(t=0;l>t;++t)i[t]=+h.call(this,y[t],t);else for(t=0;l>t;++t)i[t]=h;if(o=[],"function"==typeof g)for(t=0;c>t;++t)o[t]=+g.call(this,m[t],t);else for(t=0;c>t;++t)o[t]=g;return a.resume()},a.resume=function(){return a.alpha(.1)},a.stop=function(){return a.alpha(0)},a.drag=function(){return e||(e=Xo.behavior.drag().origin(bt).on("dragstart.force",Fu).on("drag.force",t).on("dragend.force",Ou)),arguments.length?(this.on("mouseover.force",Yu).on("mouseout.force",Iu).call(e),void 0):e},Xo.rebind(a,c,"on")};var us=20,is=1,os=1/0;Xo.layout.hierarchy=function(){function n(t,o,a){var c=u.call(e,t,o);if(t.depth=o,a.push(t),c&&(s=c.length)){for(var s,l,f=-1,h=t.children=new Array(s),g=0,p=o+1;++f<s;)l=h[f]=n(c[f],p,a),l.parent=t,g+=l.value;r&&h.sort(r),i&&(t.value=g)}else delete t.children,i&&(t.value=+i.call(e,t,o)||0);return t}function t(n,r){var u=n.children,o=0;if(u&&(a=u.length))for(var a,c=-1,s=r+1;++c<a;)o+=t(u[c],s);else i&&(o=+i.call(e,n,r)||0);return i&&(n.value=o),o}function e(t){var e=[];return n(t,0,e),e}var r=Bu,u=Xu,i=$u;return e.sort=function(n){return arguments.length?(r=n,e):r},e.children=function(n){return arguments.length?(u=n,e):u},e.value=function(n){return arguments.length?(i=n,e):i},e.revalue=function(n){return t(n,0),n},e},Xo.layout.partition=function(){function n(t,e,r,u){var i=t.children;if(t.x=e,t.y=t.depth*u,t.dx=r,t.dy=u,i&&(o=i.length)){var o,a,c,s=-1;for(r=t.value?r/t.value:0;++s<o;)n(a=i[s],e,c=a.value*r,u),e+=c}}function t(n){var e=n.children,r=0;if(e&&(u=e.length))for(var u,i=-1;++i<u;)r=Math.max(r,t(e[i]));return 1+r}function e(e,i){var o=r.call(this,e,i);return n(o[0],0,u[0],u[1]/t(o[0])),o}var r=Xo.layout.hierarchy(),u=[1,1];return e.size=function(n){return arguments.length?(u=n,e):u},Vu(e,r)},Xo.layout.pie=function(){function n(i){var o=i.map(function(e,r){return+t.call(n,e,r)}),a=+("function"==typeof r?r.apply(this,arguments):r),c=(("function"==typeof u?u.apply(this,arguments):u)-a)/Xo.sum(o),s=Xo.range(i.length);null!=e&&s.sort(e===as?function(n,t){return o[t]-o[n]}:function(n,t){return e(i[n],i[t])});var l=[];return s.forEach(function(n){var t;l[n]={data:i[n],value:t=o[n],startAngle:a,endAngle:a+=t*c}}),l}var t=Number,e=as,r=0,u=ka;return n.value=function(e){return arguments.length?(t=e,n):t},n.sort=function(t){return arguments.length?(e=t,n):e},n.startAngle=function(t){return arguments.length?(r=t,n):r},n.endAngle=function(t){return arguments.length?(u=t,n):u},n};var as={};Xo.layout.stack=function(){function n(a,c){var s=a.map(function(e,r){return t.call(n,e,r)}),l=s.map(function(t){return t.map(function(t,e){return[i.call(n,t,e),o.call(n,t,e)]})}),f=e.call(n,l,c);s=Xo.permute(s,f),l=Xo.permute(l,f);var h,g,p,v=r.call(n,l,c),d=s.length,m=s[0].length;for(g=0;m>g;++g)for(u.call(n,s[0][g],p=v[g],l[0][g][1]),h=1;d>h;++h)u.call(n,s[h][g],p+=l[h-1][g][1],l[h][g][1]);return a}var t=bt,e=Qu,r=ni,u=Ku,i=Ju,o=Gu;return n.values=function(e){return arguments.length?(t=e,n):t},n.order=function(t){return arguments.length?(e="function"==typeof t?t:cs.get(t)||Qu,n):e},n.offset=function(t){return arguments.length?(r="function"==typeof t?t:ss.get(t)||ni,n):r},n.x=function(t){return arguments.length?(i=t,n):i},n.y=function(t){return arguments.length?(o=t,n):o},n.out=function(t){return arguments.length?(u=t,n):u},n};var cs=Xo.map({"inside-out":function(n){var t,e,r=n.length,u=n.map(ti),i=n.map(ei),o=Xo.range(r).sort(function(n,t){return u[n]-u[t]}),a=0,c=0,s=[],l=[];for(t=0;r>t;++t)e=o[t],c>a?(a+=i[e],s.push(e)):(c+=i[e],l.push(e));return l.reverse().concat(s)},reverse:function(n){return Xo.range(n.length).reverse()},"default":Qu}),ss=Xo.map({silhouette:function(n){var t,e,r,u=n.length,i=n[0].length,o=[],a=0,c=[];for(e=0;i>e;++e){for(t=0,r=0;u>t;t++)r+=n[t][e][1];r>a&&(a=r),o.push(r)}for(e=0;i>e;++e)c[e]=(a-o[e])/2;return c},wiggle:function(n){var t,e,r,u,i,o,a,c,s,l=n.length,f=n[0],h=f.length,g=[];for(g[0]=c=s=0,e=1;h>e;++e){for(t=0,u=0;l>t;++t)u+=n[t][e][1];for(t=0,i=0,a=f[e][0]-f[e-1][0];l>t;++t){for(r=0,o=(n[t][e][1]-n[t][e-1][1])/(2*a);t>r;++r)o+=(n[r][e][1]-n[r][e-1][1])/a;i+=o*n[t][e][1]}g[e]=c-=u?i/u*a:0,s>c&&(s=c)}for(e=0;h>e;++e)g[e]-=s;return g},expand:function(n){var t,e,r,u=n.length,i=n[0].length,o=1/u,a=[];for(e=0;i>e;++e){for(t=0,r=0;u>t;t++)r+=n[t][e][1];if(r)for(t=0;u>t;t++)n[t][e][1]/=r;else for(t=0;u>t;t++)n[t][e][1]=o}for(e=0;i>e;++e)a[e]=0;return a},zero:ni});Xo.layout.histogram=function(){function n(n,i){for(var o,a,c=[],s=n.map(e,this),l=r.call(this,s,i),f=u.call(this,l,s,i),i=-1,h=s.length,g=f.length-1,p=t?1:1/h;++i<g;)o=c[i]=[],o.dx=f[i+1]-(o.x=f[i]),o.y=0;if(g>0)for(i=-1;++i<h;)a=s[i],a>=l[0]&&a<=l[1]&&(o=c[Xo.bisect(f,a,1,g)-1],o.y+=p,o.push(n[i]));return c}var t=!0,e=Number,r=oi,u=ui;return n.value=function(t){return arguments.length?(e=t,n):e},n.range=function(t){return arguments.length?(r=_t(t),n):r},n.bins=function(t){return arguments.length?(u="number"==typeof t?function(n){return ii(n,t)}:_t(t),n):u},n.frequency=function(e){return arguments.length?(t=!!e,n):t},n},Xo.layout.tree=function(){function n(n,i){function o(n,t){var r=n.children,u=n._tree;if(r&&(i=r.length)){for(var i,a,s,l=r[0],f=l,h=-1;++h<i;)s=r[h],o(s,a),f=c(s,a,f),a=s;vi(n);var g=.5*(l._tree.prelim+s._tree.prelim);t?(u.prelim=t._tree.prelim+e(n,t),u.mod=u.prelim-g):u.prelim=g}else t&&(u.prelim=t._tree.prelim+e(n,t))}function a(n,t){n.x=n._tree.prelim+t;var e=n.children;if(e&&(r=e.length)){var r,u=-1;for(t+=n._tree.mod;++u<r;)a(e[u],t)}}function c(n,t,r){if(t){for(var u,i=n,o=n,a=t,c=n.parent.children[0],s=i._tree.mod,l=o._tree.mod,f=a._tree.mod,h=c._tree.mod;a=si(a),i=ci(i),a&&i;)c=ci(c),o=si(o),o._tree.ancestor=n,u=a._tree.prelim+f-i._tree.prelim-s+e(a,i),u>0&&(di(mi(a,n,r),n,u),s+=u,l+=u),f+=a._tree.mod,s+=i._tree.mod,h+=c._tree.mod,l+=o._tree.mod;a&&!si(o)&&(o._tree.thread=a,o._tree.mod+=f-l),i&&!ci(c)&&(c._tree.thread=i,c._tree.mod+=s-h,r=n)}return r}var s=t.call(this,n,i),l=s[0];pi(l,function(n,t){n._tree={ancestor:n,prelim:0,mod:0,change:0,shift:0,number:t?t._tree.number+1:0}}),o(l),a(l,-l._tree.prelim);var f=li(l,hi),h=li(l,fi),g=li(l,gi),p=f.x-e(f,h)/2,v=h.x+e(h,f)/2,d=g.depth||1;return pi(l,u?function(n){n.x*=r[0],n.y=n.depth*r[1],delete n._tree}:function(n){n.x=(n.x-p)/(v-p)*r[0],n.y=n.depth/d*r[1],delete n._tree}),s}var t=Xo.layout.hierarchy().sort(null).value(null),e=ai,r=[1,1],u=!1;return n.separation=function(t){return arguments.length?(e=t,n):e},n.size=function(t){return arguments.length?(u=null==(r=t),n):u?null:r},n.nodeSize=function(t){return arguments.length?(u=null!=(r=t),n):u?r:null},Vu(n,t)},Xo.layout.pack=function(){function n(n,i){var o=e.call(this,n,i),a=o[0],c=u[0],s=u[1],l=null==t?Math.sqrt:"function"==typeof t?t:function(){return t};if(a.x=a.y=0,pi(a,function(n){n.r=+l(n.value)}),pi(a,bi),r){var f=r*(t?1:Math.max(2*a.r/c,2*a.r/s))/2;pi(a,function(n){n.r+=f}),pi(a,bi),pi(a,function(n){n.r-=f})}return ki(a,c/2,s/2,t?1:1/Math.max(2*a.r/c,2*a.r/s)),o}var t,e=Xo.layout.hierarchy().sort(yi),r=0,u=[1,1];return n.size=function(t){return arguments.length?(u=t,n):u},n.radius=function(e){return arguments.length?(t=null==e||"function"==typeof e?e:+e,n):t},n.padding=function(t){return arguments.length?(r=+t,n):r},Vu(n,e)},Xo.layout.cluster=function(){function n(n,i){var o,a=t.call(this,n,i),c=a[0],s=0;pi(c,function(n){var t=n.children;t&&t.length?(n.x=Ci(t),n.y=Ai(t)):(n.x=o?s+=e(n,o):0,n.y=0,o=n)});var l=Ni(c),f=Li(c),h=l.x-e(l,f)/2,g=f.x+e(f,l)/2;return pi(c,u?function(n){n.x=(n.x-c.x)*r[0],n.y=(c.y-n.y)*r[1]}:function(n){n.x=(n.x-h)/(g-h)*r[0],n.y=(1-(c.y?n.y/c.y:1))*r[1]}),a}var t=Xo.layout.hierarchy().sort(null).value(null),e=ai,r=[1,1],u=!1;return n.separation=function(t){return arguments.length?(e=t,n):e},n.size=function(t){return arguments.length?(u=null==(r=t),n):u?null:r},n.nodeSize=function(t){return arguments.length?(u=null!=(r=t),n):u?r:null},Vu(n,t)},Xo.layout.treemap=function(){function n(n,t){for(var e,r,u=-1,i=n.length;++u<i;)r=(e=n[u]).value*(0>t?0:t),e.area=isNaN(r)||0>=r?0:r}function t(e){var i=e.children;if(i&&i.length){var o,a,c,s=f(e),l=[],h=i.slice(),p=1/0,v="slice"===g?s.dx:"dice"===g?s.dy:"slice-dice"===g?1&e.depth?s.dy:s.dx:Math.min(s.dx,s.dy);for(n(h,s.dx*s.dy/e.value),l.area=0;(c=h.length)>0;)l.push(o=h[c-1]),l.area+=o.area,"squarify"!==g||(a=r(l,v))<=p?(h.pop(),p=a):(l.area-=l.pop().area,u(l,v,s,!1),v=Math.min(s.dx,s.dy),l.length=l.area=0,p=1/0);l.length&&(u(l,v,s,!0),l.length=l.area=0),i.forEach(t)}}function e(t){var r=t.children;if(r&&r.length){var i,o=f(t),a=r.slice(),c=[];for(n(a,o.dx*o.dy/t.value),c.area=0;i=a.pop();)c.push(i),c.area+=i.area,null!=i.z&&(u(c,i.z?o.dx:o.dy,o,!a.length),c.length=c.area=0);r.forEach(e)}}function r(n,t){for(var e,r=n.area,u=0,i=1/0,o=-1,a=n.length;++o<a;)(e=n[o].area)&&(i>e&&(i=e),e>u&&(u=e));return r*=r,t*=t,r?Math.max(t*u*p/r,r/(t*i*p)):1/0}function u(n,t,e,r){var u,i=-1,o=n.length,a=e.x,s=e.y,l=t?c(n.area/t):0;if(t==e.dx){for((r||l>e.dy)&&(l=e.dy);++i<o;)u=n[i],u.x=a,u.y=s,u.dy=l,a+=u.dx=Math.min(e.x+e.dx-a,l?c(u.area/l):0);u.z=!0,u.dx+=e.x+e.dx-a,e.y+=l,e.dy-=l}else{for((r||l>e.dx)&&(l=e.dx);++i<o;)u=n[i],u.x=a,u.y=s,u.dx=l,s+=u.dy=Math.min(e.y+e.dy-s,l?c(u.area/l):0);u.z=!1,u.dy+=e.y+e.dy-s,e.x+=l,e.dx-=l}}function i(r){var u=o||a(r),i=u[0];return i.x=0,i.y=0,i.dx=s[0],i.dy=s[1],o&&a.revalue(i),n([i],i.dx*i.dy/i.value),(o?e:t)(i),h&&(o=u),u}var o,a=Xo.layout.hierarchy(),c=Math.round,s=[1,1],l=null,f=zi,h=!1,g="squarify",p=.5*(1+Math.sqrt(5));return i.size=function(n){return arguments.length?(s=n,i):s},i.padding=function(n){function t(t){var e=n.call(i,t,t.depth);return null==e?zi(t):qi(t,"number"==typeof e?[e,e,e,e]:e)}function e(t){return qi(t,n)}if(!arguments.length)return l;var r;return f=null==(l=n)?zi:"function"==(r=typeof n)?t:"number"===r?(n=[n,n,n,n],e):e,i},i.round=function(n){return arguments.length?(c=n?Math.round:Number,i):c!=Number},i.sticky=function(n){return arguments.length?(h=n,o=null,i):h},i.ratio=function(n){return arguments.length?(p=n,i):p},i.mode=function(n){return arguments.length?(g=n+"",i):g},Vu(i,a)},Xo.random={normal:function(n,t){var e=arguments.length;return 2>e&&(t=1),1>e&&(n=0),function(){var e,r,u;do e=2*Math.random()-1,r=2*Math.random()-1,u=e*e+r*r;while(!u||u>1);return n+t*e*Math.sqrt(-2*Math.log(u)/u)}},logNormal:function(){var n=Xo.random.normal.apply(Xo,arguments);return function(){return Math.exp(n())}},bates:function(n){var t=Xo.random.irwinHall(n);return function(){return t()/n}},irwinHall:function(n){return function(){for(var t=0,e=0;n>e;e++)t+=Math.random();return t}}},Xo.scale={};var ls={floor:bt,ceil:bt};Xo.scale.linear=function(){return Hi([0,1],[0,1],fu,!1)};var fs={s:1,g:1,p:1,r:1,e:1};Xo.scale.log=function(){return $i(Xo.scale.linear().domain([0,1]),10,!0,[1,10])};var hs=Xo.format(".0e"),gs={floor:function(n){return-Math.ceil(-n)},ceil:function(n){return-Math.floor(-n)}};Xo.scale.pow=function(){return Bi(Xo.scale.linear(),1,[0,1])},Xo.scale.sqrt=function(){return Xo.scale.pow().exponent(.5)},Xo.scale.ordinal=function(){return Ji([],{t:"range",a:[[]]})},Xo.scale.category10=function(){return Xo.scale.ordinal().range(ps)},Xo.scale.category20=function(){return Xo.scale.ordinal().range(vs)},Xo.scale.category20b=function(){return Xo.scale.ordinal().range(ds)},Xo.scale.category20c=function(){return Xo.scale.ordinal().range(ms)};var ps=[2062260,16744206,2924588,14034728,9725885,9197131,14907330,8355711,12369186,1556175].map(ht),vs=[2062260,11454440,16744206,16759672,2924588,10018698,14034728,16750742,9725885,12955861,9197131,12885140,14907330,16234194,8355711,13092807,12369186,14408589,1556175,10410725].map(ht),ds=[3750777,5395619,7040719,10264286,6519097,9216594,11915115,13556636,9202993,12426809,15186514,15190932,8666169,11356490,14049643,15177372,8077683,10834324,13528509,14589654].map(ht),ms=[3244733,7057110,10406625,13032431,15095053,16616764,16625259,16634018,3253076,7652470,10607003,13101504,7695281,10394312,12369372,14342891,6513507,9868950,12434877,14277081].map(ht);Xo.scale.quantile=function(){return Gi([],[])
5
+ },Xo.scale.quantize=function(){return Ki(0,1,[0,1])},Xo.scale.threshold=function(){return Qi([.5],[0,1])},Xo.scale.identity=function(){return no([0,1])},Xo.svg={},Xo.svg.arc=function(){function n(){var n=t.apply(this,arguments),i=e.apply(this,arguments),o=r.apply(this,arguments)+ys,a=u.apply(this,arguments)+ys,c=(o>a&&(c=o,o=a,a=c),a-o),s=Sa>c?"0":"1",l=Math.cos(o),f=Math.sin(o),h=Math.cos(a),g=Math.sin(a);return c>=xs?n?"M0,"+i+"A"+i+","+i+" 0 1,1 0,"+-i+"A"+i+","+i+" 0 1,1 0,"+i+"M0,"+n+"A"+n+","+n+" 0 1,0 0,"+-n+"A"+n+","+n+" 0 1,0 0,"+n+"Z":"M0,"+i+"A"+i+","+i+" 0 1,1 0,"+-i+"A"+i+","+i+" 0 1,1 0,"+i+"Z":n?"M"+i*l+","+i*f+"A"+i+","+i+" 0 "+s+",1 "+i*h+","+i*g+"L"+n*h+","+n*g+"A"+n+","+n+" 0 "+s+",0 "+n*l+","+n*f+"Z":"M"+i*l+","+i*f+"A"+i+","+i+" 0 "+s+",1 "+i*h+","+i*g+"L0,0"+"Z"}var t=to,e=eo,r=ro,u=uo;return n.innerRadius=function(e){return arguments.length?(t=_t(e),n):t},n.outerRadius=function(t){return arguments.length?(e=_t(t),n):e},n.startAngle=function(t){return arguments.length?(r=_t(t),n):r},n.endAngle=function(t){return arguments.length?(u=_t(t),n):u},n.centroid=function(){var n=(t.apply(this,arguments)+e.apply(this,arguments))/2,i=(r.apply(this,arguments)+u.apply(this,arguments))/2+ys;return[Math.cos(i)*n,Math.sin(i)*n]},n};var ys=-Ea,xs=ka-Aa;Xo.svg.line=function(){return io(bt)};var Ms=Xo.map({linear:oo,"linear-closed":ao,step:co,"step-before":so,"step-after":lo,basis:mo,"basis-open":yo,"basis-closed":xo,bundle:Mo,cardinal:go,"cardinal-open":fo,"cardinal-closed":ho,monotone:Eo});Ms.forEach(function(n,t){t.key=n,t.closed=/-closed$/.test(n)});var _s=[0,2/3,1/3,0],bs=[0,1/3,2/3,0],ws=[0,1/6,2/3,1/6];Xo.svg.line.radial=function(){var n=io(Ao);return n.radius=n.x,delete n.x,n.angle=n.y,delete n.y,n},so.reverse=lo,lo.reverse=so,Xo.svg.area=function(){return Co(bt)},Xo.svg.area.radial=function(){var n=Co(Ao);return n.radius=n.x,delete n.x,n.innerRadius=n.x0,delete n.x0,n.outerRadius=n.x1,delete n.x1,n.angle=n.y,delete n.y,n.startAngle=n.y0,delete n.y0,n.endAngle=n.y1,delete n.y1,n},Xo.svg.chord=function(){function n(n,a){var c=t(this,i,n,a),s=t(this,o,n,a);return"M"+c.p0+r(c.r,c.p1,c.a1-c.a0)+(e(c,s)?u(c.r,c.p1,c.r,c.p0):u(c.r,c.p1,s.r,s.p0)+r(s.r,s.p1,s.a1-s.a0)+u(s.r,s.p1,c.r,c.p0))+"Z"}function t(n,t,e,r){var u=t.call(n,e,r),i=a.call(n,u,r),o=c.call(n,u,r)+ys,l=s.call(n,u,r)+ys;return{r:i,a0:o,a1:l,p0:[i*Math.cos(o),i*Math.sin(o)],p1:[i*Math.cos(l),i*Math.sin(l)]}}function e(n,t){return n.a0==t.a0&&n.a1==t.a1}function r(n,t,e){return"A"+n+","+n+" 0 "+ +(e>Sa)+",1 "+t}function u(n,t,e,r){return"Q 0,0 "+r}var i=hr,o=gr,a=No,c=ro,s=uo;return n.radius=function(t){return arguments.length?(a=_t(t),n):a},n.source=function(t){return arguments.length?(i=_t(t),n):i},n.target=function(t){return arguments.length?(o=_t(t),n):o},n.startAngle=function(t){return arguments.length?(c=_t(t),n):c},n.endAngle=function(t){return arguments.length?(s=_t(t),n):s},n},Xo.svg.diagonal=function(){function n(n,u){var i=t.call(this,n,u),o=e.call(this,n,u),a=(i.y+o.y)/2,c=[i,{x:i.x,y:a},{x:o.x,y:a},o];return c=c.map(r),"M"+c[0]+"C"+c[1]+" "+c[2]+" "+c[3]}var t=hr,e=gr,r=Lo;return n.source=function(e){return arguments.length?(t=_t(e),n):t},n.target=function(t){return arguments.length?(e=_t(t),n):e},n.projection=function(t){return arguments.length?(r=t,n):r},n},Xo.svg.diagonal.radial=function(){var n=Xo.svg.diagonal(),t=Lo,e=n.projection;return n.projection=function(n){return arguments.length?e(zo(t=n)):t},n},Xo.svg.symbol=function(){function n(n,r){return(Ss.get(t.call(this,n,r))||Ro)(e.call(this,n,r))}var t=To,e=qo;return n.type=function(e){return arguments.length?(t=_t(e),n):t},n.size=function(t){return arguments.length?(e=_t(t),n):e},n};var Ss=Xo.map({circle:Ro,cross:function(n){var t=Math.sqrt(n/5)/2;return"M"+-3*t+","+-t+"H"+-t+"V"+-3*t+"H"+t+"V"+-t+"H"+3*t+"V"+t+"H"+t+"V"+3*t+"H"+-t+"V"+t+"H"+-3*t+"Z"},diamond:function(n){var t=Math.sqrt(n/(2*Cs)),e=t*Cs;return"M0,"+-t+"L"+e+",0"+" 0,"+t+" "+-e+",0"+"Z"},square:function(n){var t=Math.sqrt(n)/2;return"M"+-t+","+-t+"L"+t+","+-t+" "+t+","+t+" "+-t+","+t+"Z"},"triangle-down":function(n){var t=Math.sqrt(n/As),e=t*As/2;return"M0,"+e+"L"+t+","+-e+" "+-t+","+-e+"Z"},"triangle-up":function(n){var t=Math.sqrt(n/As),e=t*As/2;return"M0,"+-e+"L"+t+","+e+" "+-t+","+e+"Z"}});Xo.svg.symbolTypes=Ss.keys();var ks,Es,As=Math.sqrt(3),Cs=Math.tan(30*Na),Ns=[],Ls=0;Ns.call=da.call,Ns.empty=da.empty,Ns.node=da.node,Ns.size=da.size,Xo.transition=function(n){return arguments.length?ks?n.transition():n:xa.transition()},Xo.transition.prototype=Ns,Ns.select=function(n){var t,e,r,u=this.id,i=[];n=M(n);for(var o=-1,a=this.length;++o<a;){i.push(t=[]);for(var c=this[o],s=-1,l=c.length;++s<l;)(r=c[s])&&(e=n.call(r,r.__data__,s,o))?("__data__"in r&&(e.__data__=r.__data__),jo(e,s,u,r.__transition__[u]),t.push(e)):t.push(null)}return Do(i,u)},Ns.selectAll=function(n){var t,e,r,u,i,o=this.id,a=[];n=_(n);for(var c=-1,s=this.length;++c<s;)for(var l=this[c],f=-1,h=l.length;++f<h;)if(r=l[f]){i=r.__transition__[o],e=n.call(r,r.__data__,f,c),a.push(t=[]);for(var g=-1,p=e.length;++g<p;)(u=e[g])&&jo(u,g,o,i),t.push(u)}return Do(a,o)},Ns.filter=function(n){var t,e,r,u=[];"function"!=typeof n&&(n=q(n));for(var i=0,o=this.length;o>i;i++){u.push(t=[]);for(var e=this[i],a=0,c=e.length;c>a;a++)(r=e[a])&&n.call(r,r.__data__,a,i)&&t.push(r)}return Do(u,this.id)},Ns.tween=function(n,t){var e=this.id;return arguments.length<2?this.node().__transition__[e].tween.get(n):R(this,null==t?function(t){t.__transition__[e].tween.remove(n)}:function(r){r.__transition__[e].tween.set(n,t)})},Ns.attr=function(n,t){function e(){this.removeAttribute(a)}function r(){this.removeAttributeNS(a.space,a.local)}function u(n){return null==n?e:(n+="",function(){var t,e=this.getAttribute(a);return e!==n&&(t=o(e,n),function(n){this.setAttribute(a,t(n))})})}function i(n){return null==n?r:(n+="",function(){var t,e=this.getAttributeNS(a.space,a.local);return e!==n&&(t=o(e,n),function(n){this.setAttributeNS(a.space,a.local,t(n))})})}if(arguments.length<2){for(t in n)this.attr(t,n[t]);return this}var o="transform"==n?Ru:fu,a=Xo.ns.qualify(n);return Po(this,"attr."+n,t,a.local?i:u)},Ns.attrTween=function(n,t){function e(n,e){var r=t.call(this,n,e,this.getAttribute(u));return r&&function(n){this.setAttribute(u,r(n))}}function r(n,e){var r=t.call(this,n,e,this.getAttributeNS(u.space,u.local));return r&&function(n){this.setAttributeNS(u.space,u.local,r(n))}}var u=Xo.ns.qualify(n);return this.tween("attr."+n,u.local?r:e)},Ns.style=function(n,t,e){function r(){this.style.removeProperty(n)}function u(t){return null==t?r:(t+="",function(){var r,u=Go.getComputedStyle(this,null).getPropertyValue(n);return u!==t&&(r=fu(u,t),function(t){this.style.setProperty(n,r(t),e)})})}var i=arguments.length;if(3>i){if("string"!=typeof n){2>i&&(t="");for(e in n)this.style(e,n[e],t);return this}e=""}return Po(this,"style."+n,t,u)},Ns.styleTween=function(n,t,e){function r(r,u){var i=t.call(this,r,u,Go.getComputedStyle(this,null).getPropertyValue(n));return i&&function(t){this.style.setProperty(n,i(t),e)}}return arguments.length<3&&(e=""),this.tween("style."+n,r)},Ns.text=function(n){return Po(this,"text",n,Uo)},Ns.remove=function(){return this.each("end.transition",function(){var n;this.__transition__.count<2&&(n=this.parentNode)&&n.removeChild(this)})},Ns.ease=function(n){var t=this.id;return arguments.length<1?this.node().__transition__[t].ease:("function"!=typeof n&&(n=Xo.ease.apply(Xo,arguments)),R(this,function(e){e.__transition__[t].ease=n}))},Ns.delay=function(n){var t=this.id;return R(this,"function"==typeof n?function(e,r,u){e.__transition__[t].delay=+n.call(e,e.__data__,r,u)}:(n=+n,function(e){e.__transition__[t].delay=n}))},Ns.duration=function(n){var t=this.id;return R(this,"function"==typeof n?function(e,r,u){e.__transition__[t].duration=Math.max(1,n.call(e,e.__data__,r,u))}:(n=Math.max(1,n),function(e){e.__transition__[t].duration=n}))},Ns.each=function(n,t){var e=this.id;if(arguments.length<2){var r=Es,u=ks;ks=e,R(this,function(t,r,u){Es=t.__transition__[e],n.call(t,t.__data__,r,u)}),Es=r,ks=u}else R(this,function(r){var u=r.__transition__[e];(u.event||(u.event=Xo.dispatch("start","end"))).on(n,t)});return this},Ns.transition=function(){for(var n,t,e,r,u=this.id,i=++Ls,o=[],a=0,c=this.length;c>a;a++){o.push(n=[]);for(var t=this[a],s=0,l=t.length;l>s;s++)(e=t[s])&&(r=Object.create(e.__transition__[u]),r.delay+=r.duration,jo(e,s,i,r)),n.push(e)}return Do(o,i)},Xo.svg.axis=function(){function n(n){n.each(function(){var n,s=Xo.select(this),l=this.__chart__||e,f=this.__chart__=e.copy(),h=null==c?f.ticks?f.ticks.apply(f,a):f.domain():c,g=null==t?f.tickFormat?f.tickFormat.apply(f,a):bt:t,p=s.selectAll(".tick").data(h,f),v=p.enter().insert("g",".domain").attr("class","tick").style("opacity",Aa),d=Xo.transition(p.exit()).style("opacity",Aa).remove(),m=Xo.transition(p).style("opacity",1),y=Ri(f),x=s.selectAll(".domain").data([0]),M=(x.enter().append("path").attr("class","domain"),Xo.transition(x));v.append("line"),v.append("text");var _=v.select("line"),b=m.select("line"),w=p.select("text").text(g),S=v.select("text"),k=m.select("text");switch(r){case"bottom":n=Ho,_.attr("y2",u),S.attr("y",Math.max(u,0)+o),b.attr("x2",0).attr("y2",u),k.attr("x",0).attr("y",Math.max(u,0)+o),w.attr("dy",".71em").style("text-anchor","middle"),M.attr("d","M"+y[0]+","+i+"V0H"+y[1]+"V"+i);break;case"top":n=Ho,_.attr("y2",-u),S.attr("y",-(Math.max(u,0)+o)),b.attr("x2",0).attr("y2",-u),k.attr("x",0).attr("y",-(Math.max(u,0)+o)),w.attr("dy","0em").style("text-anchor","middle"),M.attr("d","M"+y[0]+","+-i+"V0H"+y[1]+"V"+-i);break;case"left":n=Fo,_.attr("x2",-u),S.attr("x",-(Math.max(u,0)+o)),b.attr("x2",-u).attr("y2",0),k.attr("x",-(Math.max(u,0)+o)).attr("y",0),w.attr("dy",".32em").style("text-anchor","end"),M.attr("d","M"+-i+","+y[0]+"H0V"+y[1]+"H"+-i);break;case"right":n=Fo,_.attr("x2",u),S.attr("x",Math.max(u,0)+o),b.attr("x2",u).attr("y2",0),k.attr("x",Math.max(u,0)+o).attr("y",0),w.attr("dy",".32em").style("text-anchor","start"),M.attr("d","M"+i+","+y[0]+"H0V"+y[1]+"H"+i)}if(f.rangeBand){var E=f,A=E.rangeBand()/2;l=f=function(n){return E(n)+A}}else l.rangeBand?l=f:d.call(n,f);v.call(n,l),m.call(n,f)})}var t,e=Xo.scale.linear(),r=zs,u=6,i=6,o=3,a=[10],c=null;return n.scale=function(t){return arguments.length?(e=t,n):e},n.orient=function(t){return arguments.length?(r=t in qs?t+"":zs,n):r},n.ticks=function(){return arguments.length?(a=arguments,n):a},n.tickValues=function(t){return arguments.length?(c=t,n):c},n.tickFormat=function(e){return arguments.length?(t=e,n):t},n.tickSize=function(t){var e=arguments.length;return e?(u=+t,i=+arguments[e-1],n):u},n.innerTickSize=function(t){return arguments.length?(u=+t,n):u},n.outerTickSize=function(t){return arguments.length?(i=+t,n):i},n.tickPadding=function(t){return arguments.length?(o=+t,n):o},n.tickSubdivide=function(){return arguments.length&&n},n};var zs="bottom",qs={top:1,right:1,bottom:1,left:1};Xo.svg.brush=function(){function n(i){i.each(function(){var i=Xo.select(this).style("pointer-events","all").style("-webkit-tap-highlight-color","rgba(0,0,0,0)").on("mousedown.brush",u).on("touchstart.brush",u),o=i.selectAll(".background").data([0]);o.enter().append("rect").attr("class","background").style("visibility","hidden").style("cursor","crosshair"),i.selectAll(".extent").data([0]).enter().append("rect").attr("class","extent").style("cursor","move");var a=i.selectAll(".resize").data(p,bt);a.exit().remove(),a.enter().append("g").attr("class",function(n){return"resize "+n}).style("cursor",function(n){return Ts[n]}).append("rect").attr("x",function(n){return/[ew]$/.test(n)?-3:null}).attr("y",function(n){return/^[ns]/.test(n)?-3:null}).attr("width",6).attr("height",6).style("visibility","hidden"),a.style("display",n.empty()?"none":null);var l,f=Xo.transition(i),h=Xo.transition(o);c&&(l=Ri(c),h.attr("x",l[0]).attr("width",l[1]-l[0]),e(f)),s&&(l=Ri(s),h.attr("y",l[0]).attr("height",l[1]-l[0]),r(f)),t(f)})}function t(n){n.selectAll(".resize").attr("transform",function(n){return"translate("+l[+/e$/.test(n)]+","+f[+/^s/.test(n)]+")"})}function e(n){n.select(".extent").attr("x",l[0]),n.selectAll(".extent,.n>rect,.s>rect").attr("width",l[1]-l[0])}function r(n){n.select(".extent").attr("y",f[0]),n.selectAll(".extent,.e>rect,.w>rect").attr("height",f[1]-f[0])}function u(){function u(){32==Xo.event.keyCode&&(C||(x=null,L[0]-=l[1],L[1]-=f[1],C=2),d())}function p(){32==Xo.event.keyCode&&2==C&&(L[0]+=l[1],L[1]+=f[1],C=0,d())}function v(){var n=Xo.mouse(_),u=!1;M&&(n[0]+=M[0],n[1]+=M[1]),C||(Xo.event.altKey?(x||(x=[(l[0]+l[1])/2,(f[0]+f[1])/2]),L[0]=l[+(n[0]<x[0])],L[1]=f[+(n[1]<x[1])]):x=null),E&&m(n,c,0)&&(e(S),u=!0),A&&m(n,s,1)&&(r(S),u=!0),u&&(t(S),w({type:"brush",mode:C?"move":"resize"}))}function m(n,t,e){var r,u,a=Ri(t),c=a[0],s=a[1],p=L[e],v=e?f:l,d=v[1]-v[0];return C&&(c-=p,s-=d+p),r=(e?g:h)?Math.max(c,Math.min(s,n[e])):n[e],C?u=(r+=p)+d:(x&&(p=Math.max(c,Math.min(s,2*x[e]-r))),r>p?(u=r,r=p):u=p),v[0]!=r||v[1]!=u?(e?o=null:i=null,v[0]=r,v[1]=u,!0):void 0}function y(){v(),S.style("pointer-events","all").selectAll(".resize").style("display",n.empty()?"none":null),Xo.select("body").style("cursor",null),z.on("mousemove.brush",null).on("mouseup.brush",null).on("touchmove.brush",null).on("touchend.brush",null).on("keydown.brush",null).on("keyup.brush",null),N(),w({type:"brushend"})}var x,M,_=this,b=Xo.select(Xo.event.target),w=a.of(_,arguments),S=Xo.select(_),k=b.datum(),E=!/^(n|s)$/.test(k)&&c,A=!/^(e|w)$/.test(k)&&s,C=b.classed("extent"),N=O(),L=Xo.mouse(_),z=Xo.select(Go).on("keydown.brush",u).on("keyup.brush",p);if(Xo.event.changedTouches?z.on("touchmove.brush",v).on("touchend.brush",y):z.on("mousemove.brush",v).on("mouseup.brush",y),S.interrupt().selectAll("*").interrupt(),C)L[0]=l[0]-L[0],L[1]=f[0]-L[1];else if(k){var q=+/w$/.test(k),T=+/^n/.test(k);M=[l[1-q]-L[0],f[1-T]-L[1]],L[0]=l[q],L[1]=f[T]}else Xo.event.altKey&&(x=L.slice());S.style("pointer-events","none").selectAll(".resize").style("display",null),Xo.select("body").style("cursor",b.style("cursor")),w({type:"brushstart"}),v()}var i,o,a=y(n,"brushstart","brush","brushend"),c=null,s=null,l=[0,0],f=[0,0],h=!0,g=!0,p=Rs[0];return n.event=function(n){n.each(function(){var n=a.of(this,arguments),t={x:l,y:f,i:i,j:o},e=this.__chart__||t;this.__chart__=t,ks?Xo.select(this).transition().each("start.brush",function(){i=e.i,o=e.j,l=e.x,f=e.y,n({type:"brushstart"})}).tween("brush:brush",function(){var e=hu(l,t.x),r=hu(f,t.y);return i=o=null,function(u){l=t.x=e(u),f=t.y=r(u),n({type:"brush",mode:"resize"})}}).each("end.brush",function(){i=t.i,o=t.j,n({type:"brush",mode:"resize"}),n({type:"brushend"})}):(n({type:"brushstart"}),n({type:"brush",mode:"resize"}),n({type:"brushend"}))})},n.x=function(t){return arguments.length?(c=t,p=Rs[!c<<1|!s],n):c},n.y=function(t){return arguments.length?(s=t,p=Rs[!c<<1|!s],n):s},n.clamp=function(t){return arguments.length?(c&&s?(h=!!t[0],g=!!t[1]):c?h=!!t:s&&(g=!!t),n):c&&s?[h,g]:c?h:s?g:null},n.extent=function(t){var e,r,u,a,h;return arguments.length?(c&&(e=t[0],r=t[1],s&&(e=e[0],r=r[0]),i=[e,r],c.invert&&(e=c(e),r=c(r)),e>r&&(h=e,e=r,r=h),(e!=l[0]||r!=l[1])&&(l=[e,r])),s&&(u=t[0],a=t[1],c&&(u=u[1],a=a[1]),o=[u,a],s.invert&&(u=s(u),a=s(a)),u>a&&(h=u,u=a,a=h),(u!=f[0]||a!=f[1])&&(f=[u,a])),n):(c&&(i?(e=i[0],r=i[1]):(e=l[0],r=l[1],c.invert&&(e=c.invert(e),r=c.invert(r)),e>r&&(h=e,e=r,r=h))),s&&(o?(u=o[0],a=o[1]):(u=f[0],a=f[1],s.invert&&(u=s.invert(u),a=s.invert(a)),u>a&&(h=u,u=a,a=h))),c&&s?[[e,u],[r,a]]:c?[e,r]:s&&[u,a])},n.clear=function(){return n.empty()||(l=[0,0],f=[0,0],i=o=null),n},n.empty=function(){return!!c&&l[0]==l[1]||!!s&&f[0]==f[1]},Xo.rebind(n,a,"on")};var Ts={n:"ns-resize",e:"ew-resize",s:"ns-resize",w:"ew-resize",nw:"nwse-resize",ne:"nesw-resize",se:"nwse-resize",sw:"nesw-resize"},Rs=[["n","e","s","w","nw","ne","se","sw"],["e","w"],["n","s"],[]],Ds=tc.format=ac.timeFormat,Ps=Ds.utc,Us=Ps("%Y-%m-%dT%H:%M:%S.%LZ");Ds.iso=Date.prototype.toISOString&&+new Date("2000-01-01T00:00:00.000Z")?Oo:Us,Oo.parse=function(n){var t=new Date(n);return isNaN(t)?null:t},Oo.toString=Us.toString,tc.second=Rt(function(n){return new ec(1e3*Math.floor(n/1e3))},function(n,t){n.setTime(n.getTime()+1e3*Math.floor(t))},function(n){return n.getSeconds()}),tc.seconds=tc.second.range,tc.seconds.utc=tc.second.utc.range,tc.minute=Rt(function(n){return new ec(6e4*Math.floor(n/6e4))},function(n,t){n.setTime(n.getTime()+6e4*Math.floor(t))},function(n){return n.getMinutes()}),tc.minutes=tc.minute.range,tc.minutes.utc=tc.minute.utc.range,tc.hour=Rt(function(n){var t=n.getTimezoneOffset()/60;return new ec(36e5*(Math.floor(n/36e5-t)+t))},function(n,t){n.setTime(n.getTime()+36e5*Math.floor(t))},function(n){return n.getHours()}),tc.hours=tc.hour.range,tc.hours.utc=tc.hour.utc.range,tc.month=Rt(function(n){return n=tc.day(n),n.setDate(1),n},function(n,t){n.setMonth(n.getMonth()+t)},function(n){return n.getMonth()}),tc.months=tc.month.range,tc.months.utc=tc.month.utc.range;var js=[1e3,5e3,15e3,3e4,6e4,3e5,9e5,18e5,36e5,108e5,216e5,432e5,864e5,1728e5,6048e5,2592e6,7776e6,31536e6],Hs=[[tc.second,1],[tc.second,5],[tc.second,15],[tc.second,30],[tc.minute,1],[tc.minute,5],[tc.minute,15],[tc.minute,30],[tc.hour,1],[tc.hour,3],[tc.hour,6],[tc.hour,12],[tc.day,1],[tc.day,2],[tc.week,1],[tc.month,1],[tc.month,3],[tc.year,1]],Fs=Ds.multi([[".%L",function(n){return n.getMilliseconds()}],[":%S",function(n){return n.getSeconds()}],["%I:%M",function(n){return n.getMinutes()}],["%I %p",function(n){return n.getHours()}],["%a %d",function(n){return n.getDay()&&1!=n.getDate()}],["%b %d",function(n){return 1!=n.getDate()}],["%B",function(n){return n.getMonth()}],["%Y",be]]),Os={range:function(n,t,e){return Xo.range(+n,+t,e).map(Io)},floor:bt,ceil:bt};Hs.year=tc.year,tc.scale=function(){return Yo(Xo.scale.linear(),Hs,Fs)};var Ys=Hs.map(function(n){return[n[0].utc,n[1]]}),Is=Ps.multi([[".%L",function(n){return n.getUTCMilliseconds()}],[":%S",function(n){return n.getUTCSeconds()}],["%I:%M",function(n){return n.getUTCMinutes()}],["%I %p",function(n){return n.getUTCHours()}],["%a %d",function(n){return n.getUTCDay()&&1!=n.getUTCDate()}],["%b %d",function(n){return 1!=n.getUTCDate()}],["%B",function(n){return n.getUTCMonth()}],["%Y",be]]);Ys.year=tc.year.utc,tc.scale.utc=function(){return Yo(Xo.scale.linear(),Ys,Is)},Xo.text=wt(function(n){return n.responseText}),Xo.json=function(n,t){return St(n,"application/json",Zo,t)},Xo.html=function(n,t){return St(n,"text/html",Vo,t)},Xo.xml=wt(function(n){return n.responseXML}),"function"==typeof define&&define.amd?define(Xo):"object"==typeof module&&module.exports?module.exports=Xo:this.d3=Xo}();
extensions/reports/ui/lib/nvd3/nv.d3.min.css ADDED
@@ -0,0 +1 @@
 
1
+ .chartWrap{margin:0;padding:0;overflow:hidden}.nvtooltip.with-3d-shadow,.with-3d-shadow .nvtooltip{-moz-box-shadow:0 5px 10px rgba(0,0,0,.2);-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2);-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.nvtooltip{position:absolute;background-color:rgba(255,255,255,1);padding:1px;border:1px solid rgba(0,0,0,.2);z-index:10000;font-family:Arial;font-size:13px;text-align:left;pointer-events:none;white-space:nowrap;-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.nvtooltip.with-transitions,.with-transitions .nvtooltip{transition:opacity 250ms linear;-moz-transition:opacity 250ms linear;-webkit-transition:opacity 250ms linear;transition-delay:250ms;-moz-transition-delay:250ms;-webkit-transition-delay:250ms}.nvtooltip.x-nvtooltip,.nvtooltip.y-nvtooltip{padding:8px}.nvtooltip h3{margin:0;padding:4px 14px;line-height:18px;font-weight:400;background-color:rgba(247,247,247,.75);text-align:center;border-bottom:1px solid #ebebeb;-webkit-border-radius:5px 5px 0 0;-moz-border-radius:5px 5px 0 0;border-radius:5px 5px 0 0}.nvtooltip p{margin:0;padding:5px 14px;text-align:center}.nvtooltip span{display:inline-block;margin:2px 0}.nvtooltip table{margin:6px;border-spacing:0}.nvtooltip table td{padding:2px 9px 2px 0;vertical-align:middle}.nvtooltip table td.key{font-weight:400}.nvtooltip table td.value{text-align:right;font-weight:700}.nvtooltip table tr.highlight td{padding:1px 9px 1px 0;border-bottom-style:solid;border-bottom-width:1px;border-top-style:solid;border-top-width:1px}.nvtooltip table td.legend-color-guide div{width:8px;height:8px;vertical-align:middle}.nvtooltip .footer{padding:3px;text-align:center}.nvtooltip-pending-removal{position:absolute;pointer-events:none}svg{-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;display:block;width:100%;height:100%}svg text{font:400 12px Arial}svg .title{font:700 14px Arial}.nvd3 .nv-background{fill:#fff;fill-opacity:0}.nvd3.nv-noData{font-size:18px;font-weight:700}.nv-brush .extent{fill-opacity:.125;shape-rendering:crispEdges}.nvd3 .nv-legend .nv-series{cursor:pointer}.nvd3 .nv-legend .disabled circle{fill-opacity:0}.nvd3 .nv-axis{pointer-events:none}.nvd3 .nv-axis path{fill:none;stroke:#000;stroke-opacity:.75;shape-rendering:crispEdges}.nvd3 .nv-axis path.domain{stroke-opacity:.75}.nvd3 .nv-axis.nv-x path.domain{stroke-opacity:0}.nvd3 .nv-axis line{fill:none;stroke:#e5e5e5;shape-rendering:crispEdges}.nvd3 .nv-axis .zero line,.nvd3 .nv-axis line.zero{stroke-opacity:.75}.nvd3 .nv-axis .nv-axisMaxMin text{font-weight:700}.nvd3 .x .nv-axis .nv-axisMaxMin text,.nvd3 .x2 .nv-axis .nv-axisMaxMin text,.nvd3 .x3 .nv-axis .nv-axisMaxMin text{text-anchor:middle}.nv-brush .resize path{fill:#eee;stroke:#666}.nvd3 .nv-bars .negative rect{zfill:brown}.nvd3 .nv-bars rect{zfill:#4682b4;fill-opacity:.75;transition:fill-opacity 250ms linear;-moz-transition:fill-opacity 250ms linear;-webkit-transition:fill-opacity 250ms linear}.nvd3 .nv-bars rect.hover{fill-opacity:1}.nvd3 .nv-bars .hover rect{fill:#add8e6}.nvd3 .nv-bars text{fill:rgba(0,0,0,0)}.nvd3 .nv-bars .hover text{fill:rgba(0,0,0,1)}.nvd3 .nv-multibar .nv-groups rect,.nvd3 .nv-multibarHorizontal .nv-groups rect,.nvd3 .nv-discretebar .nv-groups rect{stroke-opacity:0;transition:fill-opacity 250ms linear;-moz-transition:fill-opacity 250ms linear;-webkit-transition:fill-opacity 250ms linear}.nvd3 .nv-multibar .nv-groups rect:hover,.nvd3 .nv-multibarHorizontal .nv-groups rect:hover,.nvd3 .nv-discretebar .nv-groups rect:hover{fill-opacity:1}.nvd3 .nv-discretebar .nv-groups text,.nvd3 .nv-multibarHorizontal .nv-groups text{font-weight:700;fill:rgba(0,0,0,1);stroke:rgba(0,0,0,0)}.nvd3.nv-pie path{stroke-opacity:0;transition:fill-opacity 250ms linear,stroke-width 250ms linear,stroke-opacity 250ms linear;-moz-transition:fill-opacity 250ms linear,stroke-width 250ms linear,stroke-opacity 250ms linear;-webkit-transition:fill-opacity 250ms linear,stroke-width 250ms linear,stroke-opacity 250ms linear}.nvd3.nv-pie .nv-slice text{stroke:#000;stroke-width:0}.nvd3.nv-pie path{stroke:#fff;stroke-width:1px;stroke-opacity:1}.nvd3.nv-pie .hover path{fill-opacity:.7}.nvd3.nv-pie .nv-label{pointer-events:none}.nvd3.nv-pie .nv-label rect{fill-opacity:0;stroke-opacity:0}.nvd3 .nv-groups path.nv-line{fill:none;stroke-width:1.5px}.nvd3 .nv-groups path.nv-line.nv-thin-line{stroke-width:1px}.nvd3 .nv-groups path.nv-area{stroke:none}.nvd3 .nv-line.hover path{stroke-width:6px}.nvd3.nv-line .nvd3.nv-scatter .nv-groups .nv-point{fill-opacity:0;stroke-opacity:0}.nvd3.nv-scatter.nv-single-point .nv-groups .nv-point{fill-opacity:.5!important;stroke-opacity:.5!important}.with-transitions .nvd3 .nv-groups .nv-point{transition:stroke-width 250ms linear,stroke-opacity 250ms linear;-moz-transition:stroke-width 250ms linear,stroke-opacity 250ms linear;-webkit-transition:stroke-width 250ms linear,stroke-opacity 250ms linear}.nvd3.nv-scatter .nv-groups .nv-point.hover,.nvd3 .nv-groups .nv-point.hover{stroke-width:7px;fill-opacity:.95!important;stroke-opacity:.95!important}.nvd3 .nv-point-paths path{stroke:#aaa;stroke-opacity:0;fill:#eee;fill-opacity:0}.nvd3 .nv-indexLine{cursor:ew-resize}.nvd3 .nv-distribution{pointer-events:none}.nvd3 .nv-groups .nv-point.hover{stroke-width:20px;stroke-opacity:.5}.nvd3 .nv-scatter .nv-point.hover{fill-opacity:1}.nvd3.nv-stackedarea path.nv-area{fill-opacity:.7;stroke-opacity:0;transition:fill-opacity 250ms linear,stroke-opacity 250ms linear;-moz-transition:fill-opacity 250ms linear,stroke-opacity 250ms linear;-webkit-transition:fill-opacity 250ms linear,stroke-opacity 250ms linear}.nvd3.nv-stackedarea path.nv-area.hover{fill-opacity:.9}.nvd3.nv-stackedarea .nv-groups .nv-point{stroke-opacity:0;fill-opacity:0}.nvd3.nv-linePlusBar .nv-bar rect{fill-opacity:.75}.nvd3.nv-linePlusBar .nv-bar rect:hover{fill-opacity:1}.nvd3.nv-bullet{font:10px sans-serif}.nvd3.nv-bullet .nv-measure{fill-opacity:.8}.nvd3.nv-bullet .nv-measure:hover{fill-opacity:1}.nvd3.nv-bullet .nv-marker{stroke:#000;stroke-width:2px}.nvd3.nv-bullet .nv-markerTriangle{stroke:#000;fill:#fff;stroke-width:1.5px}.nvd3.nv-bullet .nv-tick line{stroke:#666;stroke-width:.5px}.nvd3.nv-bullet .nv-range.nv-s0{fill:#eee}.nvd3.nv-bullet .nv-range.nv-s1{fill:#ddd}.nvd3.nv-bullet .nv-range.nv-s2{fill:#ccc}.nvd3.nv-bullet .nv-title{font-size:14px;font-weight:700}.nvd3.nv-bullet .nv-subtitle{fill:#999}.nvd3.nv-bullet .nv-range{fill:#bababa;fill-opacity:.4}.nvd3.nv-bullet .nv-range:hover{fill-opacity:.7}.nvd3.nv-sparkline path{fill:none}.nvd3.nv-sparklineplus g.nv-hoverValue{pointer-events:none}.nvd3.nv-sparklineplus .nv-hoverValue line{stroke:#333;stroke-width:1.5px}.nvd3.nv-sparklineplus,.nvd3.nv-sparklineplus g{pointer-events:all}.nvd3 .nv-hoverArea{fill-opacity:0;stroke-opacity:0}.nvd3.nv-sparklineplus .nv-xValue,.nvd3.nv-sparklineplus .nv-yValue{stroke-width:0;font-size:.9em;font-weight:400}.nvd3.nv-sparklineplus .nv-yValue{stroke:#f66}.nvd3.nv-sparklineplus .nv-maxValue{stroke:#2ca02c;fill:#2ca02c}.nvd3.nv-sparklineplus .nv-minValue{stroke:#d62728;fill:#d62728}.nvd3.nv-sparklineplus .nv-currentValue{font-weight:700;font-size:1.1em}.nvd3.nv-ohlcBar .nv-ticks .nv-tick{stroke-width:2px}.nvd3.nv-ohlcBar .nv-ticks .nv-tick.hover{stroke-width:4px}.nvd3.nv-ohlcBar .nv-ticks .nv-tick.positive{stroke:#2ca02c}.nvd3.nv-ohlcBar .nv-ticks .nv-tick.negative{stroke:#d62728}.nvd3.nv-historicalStockChart .nv-axis .nv-axislabel{font-weight:700}.nvd3.nv-historicalStockChart .nv-dragTarget{fill-opacity:0;stroke:none;cursor:move}.nvd3 .nv-brush .extent{fill-opacity:0!important}.nvd3 .nv-brushBackground rect{stroke:#000;stroke-width:.4;fill:#fff;fill-opacity:.7}.nvd3.nv-indentedtree .name{margin-left:5px}.nvd3.nv-indentedtree .clickable{color:#08C;cursor:pointer}.nvd3.nv-indentedtree span.clickable:hover{color:#005580;text-decoration:underline}.nvd3.nv-indentedtree .nv-childrenCount{display:inline-block;margin-left:5px}.nvd3.nv-indentedtree .nv-treeicon{cursor:pointer}.nvd3.nv-indentedtree .nv-treeicon.nv-folded{cursor:pointer}.nvd3 .background path{fill:none;stroke:#ccc;stroke-opacity:.4;shape-rendering:crispEdges}.nvd3 .foreground path{fill:none;stroke:#4682b4;stroke-opacity:.7}.nvd3 .brush .extent{fill-opacity:.3;stroke:#fff;shape-rendering:crispEdges}.nvd3 .axis line,.axis path{fill:none;stroke:#000;shape-rendering:crispEdges}.nvd3 .axis text{text-shadow:0 1px 0 #fff}.nvd3 .nv-interactiveGuideLine{pointer-events:none}.nvd3 line.nv-guideline{stroke:#ccc}
extensions/reports/ui/lib/nvd3/nv.d3.min.js ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
1
+ (function(){function t(e,t){return(new Date(t,e+1,0)).getDate()}function n(e,t,n){return function(r,i,s){var o=e(r),u=[];o<r&&t(o);if(s>1)while(o<i){var a=new Date(+o);n(a)%s===0&&u.push(a),t(o)}else while(o<i)u.push(new Date(+o)),t(o);return u}}var e=window.nv||{};e.version="1.1.15b",e.dev=!0,window.nv=e,e.tooltip=e.tooltip||{},e.utils=e.utils||{},e.models=e.models||{},e.charts={},e.graphs=[],e.logs={},e.dispatch=d3.dispatch("render_start","render_end"),e.dev&&(e.dispatch.on("render_start",function(t){e.logs.startTime=+(new Date)}),e.dispatch.on("render_end",function(t){e.logs.endTime=+(new Date),e.logs.totalTime=e.logs.endTime-e.logs.startTime,e.log("total",e.logs.totalTime)})),e.log=function(){if(e.dev&&console.log&&console.log.apply)console.log.apply(console,arguments);else if(e.dev&&typeof console.log=="function"&&Function.prototype.bind){var t=Function.prototype.bind.call(console.log,console);t.apply(console,arguments)}return arguments[arguments.length-1]},e.render=function(n){n=n||1,e.render.active=!0,e.dispatch.render_start(),setTimeout(function(){var t,r;for(var i=0;i<n&&(r=e.render.queue[i]);i++)t=r.generate(),typeof r.callback==typeof Function&&r.callback(t),e.graphs.push(t);e.render.queue.splice(0,i),e.render.queue.length?setTimeout(arguments.callee,0):(e.dispatch.render_end(),e.render.active=!1)},0)},e.render.active=!1,e.render.queue=[],e.addGraph=function(t){typeof arguments[0]==typeof Function&&(t={generate:arguments[0],callback:arguments[1]}),e.render.queue.push(t),e.render.active||e.render()},e.identity=function(e){return e},e.strip=function(e){return e.replace(/(\s|&)/g,"")},d3.time.monthEnd=function(e){return new Date(e.getFullYear(),e.getMonth(),0)},d3.time.monthEnds=n(d3.time.monthEnd,function(e){e.setUTCDate(e.getUTCDate()+1),e.setDate(t(e.getMonth()+1,e.getFullYear()))},function(e){return e.getMonth()}),e.interactiveGuideline=function(){"use strict";function c(o){o.each(function(o){function g(){var e=d3.mouse(this),n=e[0],r=e[1],o=!0,a=!1;l&&(n=d3.event.offsetX,r=d3.event.offsetY,d3.event.target.tagName!=="svg"&&(o=!1),d3.event.target.className.baseVal.match("nv-legend")&&(a=!0)),o&&(n-=i.left,r-=i.top);if(n<0||r<0||n>p||r>d||d3.event.relatedTarget&&d3.event.relatedTarget.ownerSVGElement===undefined||a){if(l&&d3.event.relatedTarget&&d3.event.relatedTarget.ownerSVGElement===undefined&&d3.event.relatedTarget.className.match(t.nvPointerEventsClass))return;u.elementMouseout({mouseX:n,mouseY:r}),c.renderGuideLine(null);return}var f=s.invert(n);u.elementMousemove({mouseX:n,mouseY:r,pointXValue:f}),d3.event.type==="dblclick"&&u.elementDblclick({mouseX:n,mouseY:r,pointXValue:f})}var h=d3.select(this),p=n||960,d=r||400,v=h.selectAll("g.nv-wrap.nv-interactiveLineLayer").data([o]),m=v.enter().append("g").attr("class"," nv-wrap nv-interactiveLineLayer");m.append("g").attr("class","nv-interactiveGuideLine");if(!f)return;f.on("mousemove",g,!0).on("mouseout",g,!0).on("dblclick",g),c.renderGuideLine=function(t){if(!a)return;var n=v.select(".nv-interactiveGuideLine").selectAll("line").data(t!=null?[e.utils.NaNtoZero(t)]:[],String);n.enter().append("line").attr("class","nv-guideline").attr("x1",function(e){return e}).attr("x2",function(e){return e}).attr("y1",d).attr("y2",0),n.exit().remove()}})}var t=e.models.tooltip(),n=null,r=null,i={left:0,top:0},s=d3.scale.linear(),o=d3.scale.linear(),u=d3.dispatch("elementMousemove","elementMouseout","elementDblclick"),a=!0,f=null,l=navigator.userAgent.indexOf("MSIE")!==-1;return c.dispatch=u,c.tooltip=t,c.margin=function(e){return arguments.length?(i.top=typeof e.top!="undefined"?e.top:i.top,i.left=typeof e.left!="undefined"?e.left:i.left,c):i},c.width=function(e){return arguments.length?(n=e,c):n},c.height=function(e){return arguments.length?(r=e,c):r},c.xScale=function(e){return arguments.length?(s=e,c):s},c.showGuideLine=function(e){return arguments.length?(a=e,c):a},c.svgContainer=function(e){return arguments.length?(f=e,c):f},c},e.interactiveBisect=function(e,t,n){"use strict";if(!e instanceof Array)return null;typeof n!="function"&&(n=function(e,t){return e.x});var r=d3.bisector(n).left,i=d3.max([0,r(e,t)-1]),s=n(e[i],i);typeof s=="undefined"&&(s=i);if(s===t)return i;var o=d3.min([i+1,e.length-1]),u=n(e[o],o);return typeof u=="undefined"&&(u=o),Math.abs(u-t)>=Math.abs(s-t)?i:o},e.nearestValueIndex=function(e,t,n){"use strict";var r=Infinity,i=null;return e.forEach(function(e,s){var o=Math.abs(t-e);o<=r&&o<n&&(r=o,i=s)}),i},function(){"use strict";window.nv.tooltip={},window.nv.models.tooltip=function(){function y(){if(a){var e=d3.select(a);e.node().tagName!=="svg"&&(e=e.select("svg"));var t=e.node()?e.attr("viewBox"):null;if(t){t=t.split(" ");var n=parseInt(e.style("width"))/t[2];l.left=l.left*n,l.top=l.top*n}}}function b(e){var t;a?t=d3.select(a):t=d3.select("body");var n=t.select(".nvtooltip");return n.node()===null&&(n=t.append("div").attr("class","nvtooltip "+(u?u:"xy-tooltip")).attr("id",h)),n.node().innerHTML=e,n.style("top",0).style("left",0).style("opacity",0),n.selectAll("div, table, td, tr").classed(p,!0),n.classed(p,!0),n.node()}function w(){if(!c)return;if(!g(n))return;y();var t=l.left,u=o!=null?o:l.top,h=b(m(n));f=h;if(a){var p=a.getElementsByTagName("svg")[0],d=p?p.getBoundingClientRect():a.getBoundingClientRect(),v={left:0,top:0};if(p){var E=p.getBoundingClientRect(),S=a.getBoundingClientRect(),x=E.top;if(x<0){var T=a.getBoundingClientRect();x=Math.abs(x)>T.height?0:x}v.top=Math.abs(x-S.top),v.left=Math.abs(E.left-S.left)}t+=a.offsetLeft+v.left-2*a.scrollLeft,u+=a.offsetTop+v.top-2*a.scrollTop}return s&&s>0&&(u=Math.floor(u/s)*s),e.tooltip.calcTooltipPosition([t,u],r,i,h),w}var t=null,n=null,r="w",i=50,s=25,o=null,u=null,a=null,f=null,l={left:null,top:null},c=!0,h="nvtooltip-"+Math.floor(Math.random()*1e5),p="nv-pointer-events-none",d=function(e,t){return e},v=function(e){return e},m=function(e){if(t!=null)return t;if(e==null)return"";var n=d3.select(document.createElement("table")),r=n.selectAll("thead").data([e]).enter().append("thead");r.append("tr").append("td").attr("colspan",3).append("strong").classed("x-value",!0).html(v(e.value));var i=n.selectAll("tbody").data([e]).enter().append("tbody"),s=i.selectAll("tr").data(function(e){return e.series}).enter().append("tr").classed("highlight",function(e){return e.highlight});s.append("td").classed("legend-color-guide",!0).append("div").style("background-color",function(e){return e.color}),s.append("td").classed("key",!0).html(function(e){return e.key}),s.append("td").classed("value",!0).html(function(e,t){return d(e.value,t)}),s.selectAll("td").each(function(e){if(e.highlight){var t=d3.scale.linear().domain([0,1]).range(["#fff",e.color]),n=.6;d3.select(this).style("border-bottom-color",t(n)).style("border-top-color",t(n))}});var o=n.node().outerHTML;return e.footer!==undefined&&(o+="<div class='footer'>"+e.footer+"</div>"),o},g=function(e){return e&&e.series&&e.series.length>0?!0:!1};return w.nvPointerEventsClass=p,w.content=function(e){return arguments.length?(t=e,w):t},w.tooltipElem=function(){return f},w.contentGenerator=function(e){return arguments.length?(typeof e=="function"&&(m=e),w):m},w.data=function(e){return arguments.length?(n=e,w):n},w.gravity=function(e){return arguments.length?(r=e,w):r},w.distance=function(e){return arguments.length?(i=e,w):i},w.snapDistance=function(e){return arguments.length?(s=e,w):s},w.classes=function(e){return arguments.length?(u=e,w):u},w.chartContainer=function(e){return arguments.length?(a=e,w):a},w.position=function(e){return arguments.length?(l.left=typeof e.left!="undefined"?e.left:l.left,l.top=typeof e.top!="undefined"?e.top:l.top,w):l},w.fixedTop=function(e){return arguments.length?(o=e,w):o},w.enabled=function(e){return arguments.length?(c=e,w):c},w.valueFormatter=function(e){return arguments.length?(typeof e=="function"&&(d=e),w):d},w.headerFormatter=function(e){return arguments.length?(typeof e=="function"&&(v=e),w):v},w.id=function(){return h},w},e.tooltip.show=function(t,n,r,i,s,o){var u=document.createElement("div");u.className="nvtooltip "+(o?o:"xy-tooltip");var a=s;if(!s||s.tagName.match(/g|svg/i))a=document.getElementsByTagName("body")[0];u.style.left=0,u.style.top=0,u.style.opacity=0,u.innerHTML=n,a.appendChild(u),s&&(t[0]=t[0]-s.scrollLeft,t[1]=t[1]-s.scrollTop),e.tooltip.calcTooltipPosition(t,r,i,u)},e.tooltip.findFirstNonSVGParent=function(e){while(e.tagName.match(/^g|svg$/i)!==null)e=e.parentNode;return e},e.tooltip.findTotalOffsetTop=function(e,t){var n=t;do isNaN(e.offsetTop)||(n+=e.offsetTop);while(e=e.offsetParent);return n},e.tooltip.findTotalOffsetLeft=function(e,t){var n=t;do isNaN(e.offsetLeft)||(n+=e.offsetLeft);while(e=e.offsetParent);return n},e.tooltip.calcTooltipPosition=function(t,n,r,i){var s=parseInt(i.offsetHeight),o=parseInt(i.offsetWidth),u=e.utils.windowSize().width,a=e.utils.windowSize().height,f=window.pageYOffset,l=window.pageXOffset,c,h;a=window.innerWidth>=document.body.scrollWidth?a:a-16,u=window.innerHeight>=document.body.scrollHeight?u:u-16,n=n||"s",r=r||20;var p=function(t){return e.tooltip.findTotalOffsetTop(t,h)},d=function(t){return e.tooltip.findTotalOffsetLeft(t,c)};switch(n){case"e":c=t[0]-o-r,h=t[1]-s/2;var v=d(i),m=p(i);v<l&&(c=t[0]+r>l?t[0]+r:l-v+c),m<f&&(h=f-m+h),m+s>f+a&&(h=f+a-m+h-s);break;case"w":c=t[0]+r,h=t[1]-s/2;var v=d(i),m=p(i);v+o>u&&(c=t[0]-o-r),m<f&&(h=f+5),m+s>f+a&&(h=f+a-m+h-s);break;case"n":c=t[0]-o/2-5,h=t[1]+r;var v=d(i),m=p(i);v<l&&(c=l+5),v+o>u&&(c=c-o/2+5),m+s>f+a&&(h=f+a-m+h-s);break;case"s":c=t[0]-o/2,h=t[1]-s-r;var v=d(i),m=p(i);v<l&&(c=l+5),v+o>u&&(c=c-o/2+5),f>m&&(h=f);break;case"none":c=t[0],h=t[1]-r;var v=d(i),m=p(i)}return i.style.left=c+"px",i.style.top=h+"px",i.style.opacity=1,i.style.position="absolute",i},e.tooltip.cleanup=function(){var e=document.getElementsByClassName("nvtooltip"),t=[];while(e.length)t.push(e[0]),e[0].style.transitionDelay="0 !important",e[0].style.opacity=0,e[0].className="nvtooltip-pending-removal";setTimeout(function(){while(t.length){var e=t.pop();e.parentNode.removeChild(e)}},500)}}(),e.utils.windowSize=function(){var e={width:640,height:480};return document.body&&document.body.offsetWidth&&(e.width=document.body.offsetWidth,e.height=document.body.offsetHeight),document.compatMode=="CSS1Compat"&&document.documentElement&&document.documentElement.offsetWidth&&(e.width=document.documentElement.offsetWidth,e.height=document.documentElement.offsetHeight),window.innerWidth&&window.innerHeight&&(e.width=window.innerWidth,e.height=window.innerHeight),e},e.utils.windowResize=function(e){if(e===undefined)return;var t=window.onresize;window.onresize=function(n){typeof t=="function"&&t(n),e(n)}},e.utils.getColor=function(t){return arguments.length?Object.prototype.toString.call(t)==="[object Array]"?function(e,n){return e.color||t[n%t.length]}:t:e.utils.defaultColor()},e.utils.defaultColor=function(){var e=d3.scale.category20().range();return function(t,n){return t.color||e[n%e.length]}},e.utils.customTheme=function(e,t,n){t=t||function(e){return e.key},n=n||d3.scale.category20().range();var r=n.length;return function(i,s){var o=t(i);return r||(r=n.length),typeof e[o]!="undefined"?typeof e[o]=="function"?e[o]():e[o]:n[--r]}},e.utils.pjax=function(t,n){function r(r){d3.html(r,function(r){var i=d3.select(n).node();i.parentNode.replaceChild(d3.select(r).select(n).node(),i),e.utils.pjax(t,n)})}d3.selectAll(t).on("click",function(){history.pushState(this.href,this.textContent,this.href),r(this.href),d3.event.preventDefault()}),d3.select(window).on("popstate",function(){d3.event.state&&r(d3.event.state)})},e.utils.calcApproxTextWidth=function(e){if(typeof e.style=="function"&&typeof e.text=="function"){var t=parseInt(e.style("font-size").replace("px","")),n=e.text().length;return n*t*.5}return 0},e.utils.NaNtoZero=function(e){return typeof e!="number"||isNaN(e)||e===null||e===Infinity?0:e},e.utils.optionsFunc=function(e){return e&&d3.map(e).forEach(function(e,t){typeof this[e]=="function"&&this[e](t)}.bind(this)),this},e.models.axis=function(){"use strict";function m(e){return e.each(function(e){var i=d3.select(this),m=i.selectAll("g.nv-wrap.nv-axis").data([e]),g=m.enter().append("g").attr("class","nvd3 nv-wrap nv-axis"),y=g.append("g"),b=m.select("g");p!==null?t.ticks(p):(t.orient()=="top"||t.orient()=="bottom")&&t.ticks(Math.abs(s.range()[1]-s.range()[0])/100),b.transition().call(t),v=v||t.scale();var w=t.tickFormat();w==null&&(w=v.tickFormat());var E=b.selectAll("text.nv-axislabel").data([o||null]);E.exit().remove();switch(t.orient()){case"top":E.enter().append("text").attr("class","nv-axislabel");var S=s.range().length==2?s.range()[1]:s.range()[s.range().length-1]+(s.range()[1]-s.range()[0]);E.attr("text-anchor","middle").attr("y",0).attr("x",S/2);if(u){var x=m.selectAll("g.nv-axisMaxMin").data(s.domain());x.enter().append("g").attr("class","nv-axisMaxMin").append("text"),x.exit().remove(),x.attr("transform",function(e,t){return"translate("+s(e)+",0)"}).select("text").attr("dy","-0.5em").attr("y",-t.tickPadding()).attr("text-anchor","middle").text(function(e,t){var n=w(e);return(""+n).match("NaN")?"":n}),x.transition().attr("transform",function(e,t){return"translate("+s.range()[t]+",0)"})}break;case"bottom":var T=36,N=30,C=b.selectAll("g").select("text");if(f%360){C.each(function(e,t){var n=this.getBBox().width;n>N&&(N=n)});var k=Math.abs(Math.sin(f*Math.PI/180)),T=(k?k*N:N)+30;C.attr("transform",function(e,t,n){return"rotate("+f+" 0,0)"}).style("text-anchor",f%360>0?"start":"end")}E.enter().append("text").attr("class","nv-axislabel");var S=s.range().length==2?s.range()[1]:s.range()[s.range().length-1]+(s.range()[1]-s.range()[0]);E.attr("text-anchor","middle").attr("y",T).attr("x",S/2);if(u){var x=m.selectAll("g.nv-axisMaxMin").data([s.domain()[0],s.domain()[s.domain().length-1]]);x.enter().append("g").attr("class","nv-axisMaxMin").append("text"),x.exit().remove(),x.attr("transform",function(e,t){return"translate("+(s(e)+(h?s.rangeBand()/2:0))+",0)"}).select("text").attr("dy",".71em").attr("y",t.tickPadding()).attr("transform",function(e,t,n){return"rotate("+f+" 0,0)"}).style("text-anchor",f?f%360>0?"start":"end":"middle").text(function(e,t){var n=w(e);return(""+n).match("NaN")?"":n}),x.transition().attr("transform",function(e,t){return"translate("+(s(e)+(h?s.rangeBand()/2:0))+",0)"})}c&&C.attr("transform",function(e,t){return"translate(0,"+(t%2==0?"0":"12")+")"});break;case"right":E.enter().append("text").attr("class","nv-axislabel"),E.style("text-anchor",l?"middle":"begin").attr("transform",l?"rotate(90)":"").attr("y",l?-Math.max(n.right,r)+12:-10).attr("x",l?s.range()[0]/2:t.tickPadding());if(u){var x=m.selectAll("g.nv-axisMaxMin").data(s.domain());x.enter().append("g").attr("class","nv-axisMaxMin").append("text").style("opacity",0),x.exit().remove(),x.attr("transform",function(e,t){return"translate(0,"+s(e)+")"}).select("text").attr("dy",".32em").attr("y",0).attr("x",t.tickPadding()).style("text-anchor","start").text(function(e,t){var n=w(e);return(""+n).match("NaN")?"":n}),x.transition().attr("transform",function(e,t){return"translate(0,"+s.range()[t]+")"}).select("text").style("opacity",1)}break;case"left":E.enter().append("text").attr("class","nv-axislabel"),E.style("text-anchor",l?"middle":"end").attr("transform",l?"rotate(-90)":"").attr("y",l?-Math.max(n.left,r)+d:-10).attr("x",l?-s.range()[0]/2:-t.tickPadding());if(u){var x=m.selectAll("g.nv-axisMaxMin").data(s.domain());x.enter().append("g").attr("class","nv-axisMaxMin").append("text").style("opacity",0),x.exit().remove(),x.attr("transform",function(e,t){return"translate(0,"+v(e)+")"}).select("text").attr("dy",".32em").attr("y",0).attr("x",-t.tickPadding()).attr("text-anchor","end").text(function(e,t){var n=w(e);return(""+n).match("NaN")?"":n}),x.transition().attr("transform",function(e,t){return"translate(0,"+s.range()[t]+")"}).select("text").style("opacity",1)}}E.text(function(e){return e}),u&&(t.orient()==="left"||t.orient()==="right")&&(b.selectAll("g").each(function(e,t){d3.select(this).select("text").attr("opacity",1);if(s(e)<s.range()[1]+10||s(e)>s.range()[0]-10)(e>1e-10||e<-1e-10)&&d3.select(this).attr("opacity",0),d3.select(this).select("text").attr("opacity",0)}),s.domain()[0]==s.domain()[1]&&s.domain()[0]==0&&m.selectAll("g.nv-axisMaxMin").style("opacity",function(e,t){return t?0:1}));if(u&&(t.orient()==="top"||t.orient()==="bottom")){var L=[];m.selectAll("g.nv-axisMaxMin").each(function(e,t){try{t?L.push(s(e)-this.getBBox().width-4):L.push(s(e)+this.getBBox().width+4)}catch(n){t?L.push(s(e)-4):L.push(s(e)+4)}}),b.selectAll("g").each(function(e,t){if(s(e)<L[0]||s(e)>L[1])e>1e-10||e<-1e-10?d3.select(this).remove():d3.select(this).select("text").remove()})}a&&b.selectAll(".tick").filter(function(e){return!parseFloat(Math.round(e.__data__*1e5)/1e6)&&e.__data__!==undefined}).classed("zero",!0),v=s.copy()}),m}var t=d3.svg.axis(),n={top:0,right:0,bottom:0,left:0},r=75,i=60,s=d3.scale.linear(),o=null,u=!0,a=!0,f=0,l=!0,c=!1,h=!1,p=null,d=12;t.scale(s).orient("bottom").tickFormat(function(e){return e});var v;return m.axis=t,d3.rebind(m,t,"orient","tickValues","tickSubdivide","tickSize","tickPadding","tickFormat"),d3.rebind(m,s,"domain","range","rangeBand","rangeBands"),m.options=e.utils.optionsFunc.bind(m),m.margin=function(e){return arguments.length?(n.top=typeof e.top!="undefined"?e.top:n.top,n.right=typeof e.right!="undefined"?e.right:n.right,n.bottom=typeof e.bottom!="undefined"?e.bottom:n.bottom,n.left=typeof e.left!="undefined"?e.left:n.left,m):n},m.width=function(e){return arguments.length?(r=e,m):r},m.ticks=function(e){return arguments.length?(p=e,m):p},m.height=function(e){return arguments.length?(i=e,m):i},m.axisLabel=function(e){return arguments.length?(o=e,m):o},m.showMaxMin=function(e){return arguments.length?(u=e,m):u},m.highlightZero=function(e){return arguments.length?(a=e,m):a},m.scale=function(e){return arguments.length?(s=e,t.scale(s),h=typeof s.rangeBands=="function",d3.rebind(m,s,"domain","range","rangeBand","rangeBands"),m):s},m.rotateYLabel=function(e){return arguments.length?(l=e,m):l},m.rotateLabels=function(e){return arguments.length?(f=e,m):f},m.staggerLabels=function(e){return arguments.length?(c=e,m):c},m.axisLabelDistance=function(e){return arguments.length?(d=e,m):d},m},e.models.bullet=function(){"use strict";function m(e){return e.each(function(e,n){var p=c-t.left-t.right,m=h-t.top-t.bottom,g=d3.select(this),y=i.call(this,e,n).slice().sort(d3.descending),b=s.call(this,e,n).slice().sort(d3.descending),w=o.call(this,e,n).slice().sort(d3.descending),E=u.call(this,e,n).slice(),S=a.call(this,e,n).slice(),x=f.call(this,e,n).slice(),T=d3.scale.linear().domain(d3.extent(d3.merge([l,y]))).range(r?[p,0]:[0,p]),N=this.__chart__||d3.scale.linear().domain([0,Infinity]).range(T.range());this.__chart__=T;var C=d3.min(y),k=d3.max(y),L=y[1],A=g.selectAll("g.nv-wrap.nv-bullet").data([e]),O=A.enter().append("g").attr("class","nvd3 nv-wrap nv-bullet"),M=O.append("g"),_=A.select("g");M.append("rect").attr("class","nv-range nv-rangeMax"),M.append("rect").attr("class","nv-range nv-rangeAvg"),M.append("rect").attr("class","nv-range nv-rangeMin"),M.append("rect").attr("class","nv-measure"),M.append("path").attr("class","nv-markerTriangle"),A.attr("transform","translate("+t.left+","+t.top+")");var D=function(e){return Math.abs(N(e)-N(0))},P=function(e){return Math.abs(T(e)-T(0))},H=function(e){return e<0?N(e):N(0)},B=function(e){return e<0?T(e):T(0)};_.select("rect.nv-rangeMax").attr("height",m).attr("width",P(k>0?k:C)).attr("x",B(k>0?k:C)).datum(k>0?k:C),_.select("rect.nv-rangeAvg").attr("height",m).attr("width",P(L)).attr("x",B(L)).datum(L),_.select("rect.nv-rangeMin").attr("height",m).attr("width",P(k)).attr("x",B(k)).attr("width",P(k>0?C:k)).attr("x",B(k>0?C:k)).datum(k>0?C:k),_.select("rect.nv-measure").style("fill",d).attr("height",m/3).attr("y",m/3).attr("width",w<0?T(0)-T(w[0]):T(w[0])-T(0)).attr("x",B(w)).on("mouseover",function(){v.elementMouseover({value:w[0],label:x[0]||"Current",pos:[T(w[0]),m/2]})}).on("mouseout",function(){v.elementMouseout({value:w[0],label:x[0]||"Current"})});var j=m/6;b[0]?_.selectAll("path.nv-markerTriangle").attr("transform",function(e){return"translate("+T(b[0])+","+m/2+")"}).attr("d","M0,"+j+"L"+j+","+ -j+" "+ -j+","+ -j+"Z").on("mouseover",function(){v.elementMouseover({value:b[0],label:S[0]||"Previous",pos:[T(b[0]),m/2]})}).on("mouseout",function(){v.elementMouseout({value:b[0],label:S[0]||"Previous"})}):_.selectAll("path.nv-markerTriangle").remove(),A.selectAll(".nv-range").on("mouseover",function(e,t){var n=E[t]||(t?t==1?"Mean":"Minimum":"Maximum");v.elementMouseover({value:e,label:n,pos:[T(e),m/2]})}).on("mouseout",function(e,t){var n=E[t]||(t?t==1?"Mean":"Minimum":"Maximum");v.elementMouseout({value:e,label:n})})}),m}var t={top:0,right:0,bottom:0,left:0},n="left",r=!1,i=function(e){return e.ranges},s=function(e){return e.markers},o=function(e){return e.measures},u=function(e){return e.rangeLabels?e.rangeLabels:[]},a=function(e){return e.markerLabels?e.markerLabels:[]},f=function(e){return e.measureLabels?e.measureLabels:[]},l=[0],c=380,h=30,p=null,d=e.utils.getColor(["#1f77b4"]),v=d3.dispatch("elementMouseover","elementMouseout");return m.dispatch=v,m.options=e.utils.optionsFunc.bind(m),m.orient=function(e){return arguments.length?(n=e,r=n=="right"||n=="bottom",m):n},m.ranges=function(e){return arguments.length?(i=e,m):i},m.markers=function(e){return arguments.length?(s=e,m):s},m.measures=function(e){return arguments.length?(o=e,m):o},m.forceX=function(e){return arguments.length?(l=e,m):l},m.width=function(e){return arguments.length?(c=e,m):c},m.height=function(e){return arguments.length?(h=e,m):h},m.margin=function(e){return arguments.length?(t.top=typeof e.top!="undefined"?e.top:t.top,t.right=typeof e.right!="undefined"?e.right:t.right,t.bottom=typeof e.bottom!="undefined"?e.bottom:t.bottom,t.left=typeof e.left!="undefined"?e.left:t.left,m):t},m.tickFormat=function(e){return arguments.length?(p=e,m):p},m.color=function(t){return arguments.length?(d=e.utils.getColor(t),m):d},m},e.models.bulletChart=function(){"use strict";function m(e){return e.each(function(n,h){var g=d3.select(this),y=(a||parseInt(g.style("width"))||960)-i.left-i.right,b=f-i.top-i.bottom,w=this;m.update=function(){m(e)},m.container=this;if(!n||!s.call(this,n,h)){var E=g.selectAll(".nv-noData").data([p]);return E.enter().append("text").attr("class","nvd3 nv-noData").attr("dy","-.7em").style("text-anchor","middle"),E.attr("x",i.left+y/2).attr("y",18+i.top+b/2).text(function(e){return e}),m}g.selectAll(".nv-noData").remove();var S=s.call(this,n,h).slice().sort(d3.descending),x=o.call(this,n,h).slice().sort(d3.descending),T=u.call(this,n,h).slice().sort(d3.descending),N=g.selectAll("g.nv-wrap.nv-bulletChart").data([n]),C=N.enter().append("g").attr("class","nvd3 nv-wrap nv-bulletChart"),k=C.append("g"),L=N.select("g");k.append("g").attr("class","nv-bulletWrap"),k.append("g").attr("class","nv-titles"),N.attr("transform","translate("+i.left+","+i.top+")");var A=d3.scale.linear().domain([0,Math.max(S[0],x[0],T[0])]).range(r?[y,0]:[0,y]),O=this.__chart__||d3.scale.linear().domain([0,Infinity]).range(A.range());this.__chart__=A;var M=function(e){return Math.abs(O(e)-O(0))},_=function(e){return Math.abs(A(e)-A(0))},D=k.select(".nv-titles").append("g").attr("text-anchor","end").attr("transform","translate(-6,"+(f-i.top-i.bottom)/2+")");D.append("text").attr("class","nv-title").text(function(e){return e.title}),D.append("text").attr("class","nv-subtitle").attr("dy","1em").text(function(e){return e.subtitle}),t.width(y).height(b);var P=L.select(".nv-bulletWrap");d3.transition(P).call(t);var H=l||A.tickFormat(y/100),B=L.selectAll("g.nv-tick").data(A.ticks(y/50),function(e){return this.textContent||H(e)}),j=B.enter().append("g").attr("class","nv-tick").attr("transform",function(e){return"translate("+O(e)+",0)"}).style("opacity",1e-6);j.append("line").attr("y1",b).attr("y2",b*7/6),j.append("text").attr("text-anchor","middle").attr("dy","1em").attr("y",b*7/6).text(H);var F=d3.transition(B).attr("transform",function(e){return"translate("+A(e)+",0)"}).style("opacity",1);F.select("line").attr("y1",b).attr("y2",b*7/6),F.select("text").attr("y",b*7/6),d3.transition(B.exit()).attr("transform",function(e){return"translate("+A(e)+",0)"}).style("opacity",1e-6).remove(),d.on("tooltipShow",function(e){e.key=n.title,c&&v(e,w.parentNode)})}),d3.timer.flush(),m}var t=e.models.bullet(),n="left",r=!1,i={top:5,right:40,bottom:20,left:120},s=function(e){return e.ranges},o=function(e){return e.markers},u=function(e){return e.measures},a=null,f=55,l=null,c=!0,h=function(e,t,n,r,i){return"<h3>"+t+"</h3>"+"<p>"+n+"</p>"},p="No Data Available.",d=d3.dispatch("tooltipShow","tooltipHide"),v=function(t,n){var r=t.pos[0]+(n.offsetLeft||0)+i.left,s=t.pos[1]+(n.offsetTop||0)+i.top,o=h(t.key,t.label,t.value,t,m);e.tooltip.show([r,s],o,t.value<0?"e":"w",null,n)};return t.dispatch.on("elementMouseover.tooltip",function(e){d.tooltipShow(e)}),t.dispatch.on("elementMouseout.tooltip",function(e){d.tooltipHide(e)}),d.on("tooltipHide",function(){c&&e.tooltip.cleanup()}),m.dispatch=d,m.bullet=t,d3.rebind(m,t,"color"),m.options=e.utils.optionsFunc.bind(m),m.orient=function(e){return arguments.length?(n=e,r=n=="right"||n=="bottom",m):n},m.ranges=function(e){return arguments.length?(s=e,m):s},m.markers=function(e){return arguments.length?(o=e,m):o},m.measures=function(e){return arguments.length?(u=e,m):u},m.width=function(e){return arguments.length?(a=e,m):a},m.height=function(e){return arguments.length?(f=e,m):f},m.margin=function(e){return arguments.length?(i.top=typeof e.top!="undefined"?e.top:i.top,i.right=typeof e.right!="undefined"?e.right:i.right,i.bottom=typeof e.bottom!="undefined"?e.bottom:i.bottom,i.left=typeof e.left!="undefined"?e.left:i.left,m):i},m.tickFormat=function(e){return arguments.length?(l=e,m):l},m.tooltips=function(e){return arguments.length?(c=e,m):c},m.tooltipContent=function(e){return arguments.length?(h=e,m):h},m.noData=function(e){return arguments.length?(p=e,m):p},m},e.models.cumulativeLineChart=function(){"use strict";function D(b){return b.each(function(b){function q(e,t){d3.select(D.container).style("cursor","ew-resize")}function R(e,t){M.x=d3.event.x,M.i=Math.round(O.invert(M.x)),rt()}function U(e,t){d3.select(D.container).style("cursor","auto"),x.index=M.i,k.stateChange(x)}function rt(){nt.data([M]);var e=D.transitionDuration();D.transitionDuration(0),D.update(),D.transitionDuration(e)}var A=d3.select(this).classed("nv-chart-"+S,!0),H=this,B=(f||parseInt(A.style("width"))||960)-u.left-u.right,j=(l||parseInt(A.style("height"))||400)-u.top-u.bottom;D.update=function(){A.transition().duration(L).call(D)},D.container=this,x.disabled=b.map(function(e){return!!e.disabled});if(!T){var F;T={};for(F in x)x[F]instanceof Array?T[F]=x[F].slice(0):T[F]=x[F]}var I=d3.behavior.drag().on("dragstart",q).on("drag",R).on("dragend",U);if(!b||!b.length||!b.filter(function(e){return e.values.length}).length){var z=A.selectAll(".nv-noData").data([N]);return z.enter().append("text").attr("class","nvd3 nv-noData").attr("dy","-.7em").style("text-anchor","middle"),z.attr("x",u.left+B/2).attr("y",u.top+j/2).text(function(e){return e}),D}A.selectAll(".nv-noData").remove(),w=t.xScale(),E=t.yScale();if(!y){var W=b.filter(function(e){return!e.disabled}).map(function(e,n){var r=d3.extent(e.values,t.y());return r[0]<-0.95&&(r[0]=-0.95),[(r[0]-r[1])/(1+r[1]),(r[1]-r[0])/(1+r[0])]}),X=[d3.min(W,function(e){return e[0]}),d3.max(W,function(e){return e[1]})];t.yDomain(X)}else t.yDomain(null);O.domain([0,b[0].values.length-1]).range([0,B]).clamp(!0);var b=P(M.i,b),V=g?"none":"all",$=A.selectAll("g.nv-wrap.nv-cumulativeLine").data([b]),J=$.enter().append("g").attr("class","nvd3 nv-wrap nv-cumulativeLine").append("g"),K=$.select("g");J.append("g").attr("class","nv-interactive"),J.append("g").attr("class","nv-x nv-axis").style("pointer-events","none"),J.append("g").attr("class","nv-y nv-axis"),J.append("g").attr("class","nv-background"),J.append("g").attr("class","nv-linesWrap").style("pointer-events",V),J.append("g").attr("class","nv-avgLinesWrap").style("pointer-events","none"),J.append("g").attr("class","nv-legendWrap"),J.append("g").attr("class","nv-controlsWrap"),c&&(i.width(B),K.select(".nv-legendWrap").datum(b).call(i),u.top!=i.height()&&(u.top=i.height(),j=(l||parseInt(A.style("height"))||400)-u.top-u.bottom),K.select(".nv-legendWrap").attr("transform","translate(0,"+ -u.top+")"));if(m){var Q=[{key:"Re-scale y-axis",disabled:!y}];s.width(140).color(["#444","#444","#444"]).rightAlign(!1).margin({top:5,right:0,bottom:5,left:20}),K.select(".nv-controlsWrap").datum(Q).attr("transform","translate(0,"+ -u.top+")").call(s)}$.attr("transform","translate("+u.left+","+u.top+")"),d&&K.select(".nv-y.nv-axis").attr("transform","translate("+B+",0)");var G=b.filter(function(e){return e.tempDisabled});$.select(".tempDisabled").remove(),G.length&&$.append("text").attr("class","tempDisabled").attr("x",B/2).attr("y","-.71em").style("text-anchor","end").text(G.map(function(e){return e.key}).join(", ")+" values cannot be calculated for this time period."),g&&(o.width(B).height(j).margin({left:u.left,top:u.top}).svgContainer(A).xScale(w),$.select(".nv-interactive").call(o)),J.select(".nv-background").append("rect"),K.select(".nv-background rect").attr("width",B).attr("height",j),t.y(function(e){return e.display.y}).width(B).height(j).color(b.map(function(e,t){return e.color||a(e,t)}).filter(function(e,t){return!b[t].disabled&&!b[t].tempDisabled}));var Y=K.select(".nv-linesWrap").datum(b.filter(function(e){return!e.disabled&&!e.tempDisabled}));Y.call(t),b.forEach(function(e,t){e.seriesIndex=t});var Z=b.filter(function(e){return!e.disabled&&!!C(e)}),et=K.select(".nv-avgLinesWrap").selectAll("line").data(Z,function(e){return e.key}),tt=function(e){var t=E(C(e));return t<0?0:t>j?j:t};et.enter().append("line").style("stroke-width",2).style("stroke-dasharray","10,10").style("stroke",function(e,n){return t.color()(e,e.seriesIndex)}).attr("x1",0).attr("x2",B).attr("y1",tt).attr("y2",tt),et.style("stroke-opacity",function(e){var t=E(C(e));return t<0||t>j?0:1}).attr("x1",0).attr("x2",B).attr("y1",tt).attr("y2",tt),et.exit().remove();var nt=Y.selectAll(".nv-indexLine").data([M]);nt.enter().append("rect").attr("class","nv-indexLine").attr("width",3).attr("x",-2).attr("fill","red").attr("fill-opacity",.5).style("pointer-events","all").call(I),nt.attr("transform",function(e){return"translate("+O(e.i)+",0)"}).attr("height",j),h&&(n.scale(w).ticks(Math.min(b[0].values.length,B/70)).tickSize(-j,0),K.select(".nv-x.nv-axis").attr("transform","translate(0,"+E.range()[0]+")"),d3.transition(K.select(".nv-x.nv-axis")).call(n)),p&&(r.scale(E).ticks(j/36).tickSize(-B,0),d3.transition(K.select(".nv-y.nv-axis")).call(r)),K.select(".nv-background rect").on("click",function(){M.x=d3.mouse(this)[0],M.i=Math.round(O.invert(M.x)),x.index=M.i,k.stateChange(x),rt()}),t.dispatch.on("elementClick",function(e){M.i=e.pointIndex,M.x=O(M.i),x.index=M.i,k.stateChange(x),rt()}),s.dispatch.on("legendClick",function(e,t){e.disabled=!e.disabled,y=!e.disabled,x.rescaleY=y,k.stateChange(x),D.update()}),i.dispatch.on("stateChange",function(e){x.disabled=e.disabled,k.stateChange(x),D.update()}),o.dispatch.on("elementMousemove",function(i){t.clearHighlights();var s,f,l,c=[];b.filter(function(e,t){return e.seriesIndex=t,!e.disabled}).forEach(function(n,r){f=e.interactiveBisect(n.values,i.pointXValue,D.x()),t.highlightPoint(r,f,!0);var o=n.values[f];if(typeof o=="undefined")return;typeof s=="undefined"&&(s=o),typeof l=="undefined"&&(l=D.xScale()(D.x()(o,f))),c.push({key:n.key,value:D.y()(o,f),color:a(n,n.seriesIndex)})});if(c.length>2){var h=D.yScale().invert(i.mouseY),p=Math.abs(D.yScale().domain()[0]-D.yScale().domain()[1]),d=.03*p,m=e.nearestValueIndex(c.map(function(e){return e.value}),h,d);m!==null&&(c[m].highlight=!0)}var g=n.tickFormat()(D.x()(s,f),f);o.tooltip.position({left:l+u.left,top:i.mouseY+u.top}).chartContainer(H.parentNode).enabled(v).valueFormatter(function(e,t){return r.tickFormat()(e)}).data({value:g,series:c})(),o.renderGuideLine(l)}),o.dispatch.on("elementMouseout",function(e){k.tooltipHide(),t.clearHighlights()}),k.on("tooltipShow",function(e){v&&_(e,H.parentNode)}),k.on("changeState",function(e){typeof e.disabled!="undefined"&&(b.forEach(function(t,n){t.disabled=e.disabled[n]}),x.disabled=e.disabled),typeof e.index!="undefined"&&(M.i=e.index,M.x=O(M.i),x.index=e.index,nt.data([M])),typeof e.rescaleY!="undefined"&&(y=e.rescaleY),D.update()})}),D}function P(e,n){return n.map(function(n,r){if(!n.values)return n;var i=t.y()(n.values[e],e);return i<-0.95&&!A?(n.tempDisabled=!0,n):(n.tempDisabled=!1,n.values=
2
+ n.values.map(function(e,n){return e.display={y:(t.y()(e,n)-i)/(1+i)},e}),n)})}var t=e.models.line(),n=e.models.axis(),r=e.models.axis(),i=e.models.legend(),s=e.models.legend(),o=e.interactiveGuideline(),u={top:30,right:30,bottom:50,left:60},a=e.utils.defaultColor(),f=null,l=null,c=!0,h=!0,p=!0,d=!1,v=!0,m=!0,g=!1,y=!0,b=function(e,t,n,r,i){return"<h3>"+e+"</h3>"+"<p>"+n+" at "+t+"</p>"},w,E,S=t.id(),x={index:0,rescaleY:y},T=null,N="No Data Available.",C=function(e){return e.average},k=d3.dispatch("tooltipShow","tooltipHide","stateChange","changeState"),L=250,A=!1;n.orient("bottom").tickPadding(7),r.orient(d?"right":"left"),s.updateState(!1);var O=d3.scale.linear(),M={i:0,x:0},_=function(i,s){var o=i.pos[0]+(s.offsetLeft||0),u=i.pos[1]+(s.offsetTop||0),a=n.tickFormat()(t.x()(i.point,i.pointIndex)),f=r.tickFormat()(t.y()(i.point,i.pointIndex)),l=b(i.series.key,a,f,i,D);e.tooltip.show([o,u],l,null,null,s)};return t.dispatch.on("elementMouseover.tooltip",function(e){e.pos=[e.pos[0]+u.left,e.pos[1]+u.top],k.tooltipShow(e)}),t.dispatch.on("elementMouseout.tooltip",function(e){k.tooltipHide(e)}),k.on("tooltipHide",function(){v&&e.tooltip.cleanup()}),D.dispatch=k,D.lines=t,D.legend=i,D.xAxis=n,D.yAxis=r,D.interactiveLayer=o,d3.rebind(D,t,"defined","isArea","x","y","xScale","yScale","size","xDomain","yDomain","xRange","yRange","forceX","forceY","interactive","clipEdge","clipVoronoi","useVoronoi","id"),D.options=e.utils.optionsFunc.bind(D),D.margin=function(e){return arguments.length?(u.top=typeof e.top!="undefined"?e.top:u.top,u.right=typeof e.right!="undefined"?e.right:u.right,u.bottom=typeof e.bottom!="undefined"?e.bottom:u.bottom,u.left=typeof e.left!="undefined"?e.left:u.left,D):u},D.width=function(e){return arguments.length?(f=e,D):f},D.height=function(e){return arguments.length?(l=e,D):l},D.color=function(t){return arguments.length?(a=e.utils.getColor(t),i.color(a),D):a},D.rescaleY=function(e){return arguments.length?(y=e,D):y},D.showControls=function(e){return arguments.length?(m=e,D):m},D.useInteractiveGuideline=function(e){return arguments.length?(g=e,e===!0&&(D.interactive(!1),D.useVoronoi(!1)),D):g},D.showLegend=function(e){return arguments.length?(c=e,D):c},D.showXAxis=function(e){return arguments.length?(h=e,D):h},D.showYAxis=function(e){return arguments.length?(p=e,D):p},D.rightAlignYAxis=function(e){return arguments.length?(d=e,r.orient(e?"right":"left"),D):d},D.tooltips=function(e){return arguments.length?(v=e,D):v},D.tooltipContent=function(e){return arguments.length?(b=e,D):b},D.state=function(e){return arguments.length?(x=e,D):x},D.defaultState=function(e){return arguments.length?(T=e,D):T},D.noData=function(e){return arguments.length?(N=e,D):N},D.average=function(e){return arguments.length?(C=e,D):C},D.transitionDuration=function(e){return arguments.length?(L=e,D):L},D.noErrorCheck=function(e){return arguments.length?(A=e,D):A},D},e.models.discreteBar=function(){"use strict";function E(e){return e.each(function(e){var i=n-t.left-t.right,E=r-t.top-t.bottom,S=d3.select(this);e.forEach(function(e,t){e.values.forEach(function(e){e.series=t})});var T=p&&d?[]:e.map(function(e){return e.values.map(function(e,t){return{x:u(e,t),y:a(e,t),y0:e.y0}})});s.domain(p||d3.merge(T).map(function(e){return e.x})).rangeBands(v||[0,i],.1),o.domain(d||d3.extent(d3.merge(T).map(function(e){return e.y}).concat(f))),c?o.range(m||[E-(o.domain()[0]<0?12:0),o.domain()[1]>0?12:0]):o.range(m||[E,0]),b=b||s,w=w||o.copy().range([o(0),o(0)]);var N=S.selectAll("g.nv-wrap.nv-discretebar").data([e]),C=N.enter().append("g").attr("class","nvd3 nv-wrap nv-discretebar"),k=C.append("g"),L=N.select("g");k.append("g").attr("class","nv-groups"),N.attr("transform","translate("+t.left+","+t.top+")");var A=N.select(".nv-groups").selectAll(".nv-group").data(function(e){return e},function(e){return e.key});A.enter().append("g").style("stroke-opacity",1e-6).style("fill-opacity",1e-6),A.exit().transition().style("stroke-opacity",1e-6).style("fill-opacity",1e-6).remove(),A.attr("class",function(e,t){return"nv-group nv-series-"+t}).classed("hover",function(e){return e.hover}),A.transition().style("stroke-opacity",1).style("fill-opacity",.75);var O=A.selectAll("g.nv-bar").data(function(e){return e.values});O.exit().remove();var M=O.enter().append("g").attr("transform",function(e,t,n){return"translate("+(s(u(e,t))+s.rangeBand()*.05)+", "+o(0)+")"}).on("mouseover",function(t,n){d3.select(this).classed("hover",!0),g.elementMouseover({value:a(t,n),point:t,series:e[t.series],pos:[s(u(t,n))+s.rangeBand()*(t.series+.5)/e.length,o(a(t,n))],pointIndex:n,seriesIndex:t.series,e:d3.event})}).on("mouseout",function(t,n){d3.select(this).classed("hover",!1),g.elementMouseout({value:a(t,n),point:t,series:e[t.series],pointIndex:n,seriesIndex:t.series,e:d3.event})}).on("click",function(t,n){g.elementClick({value:a(t,n),point:t,series:e[t.series],pos:[s(u(t,n))+s.rangeBand()*(t.series+.5)/e.length,o(a(t,n))],pointIndex:n,seriesIndex:t.series,e:d3.event}),d3.event.stopPropagation()}).on("dblclick",function(t,n){g.elementDblClick({value:a(t,n),point:t,series:e[t.series],pos:[s(u(t,n))+s.rangeBand()*(t.series+.5)/e.length,o(a(t,n))],pointIndex:n,seriesIndex:t.series,e:d3.event}),d3.event.stopPropagation()});M.append("rect").attr("height",0).attr("width",s.rangeBand()*.9/e.length),c?(M.append("text").attr("text-anchor","middle"),O.select("text").text(function(e,t){return h(a(e,t))}).transition().attr("x",s.rangeBand()*.9/2).attr("y",function(e,t){return a(e,t)<0?o(a(e,t))-o(0)+12:-4})):O.selectAll("text").remove(),O.attr("class",function(e,t){return a(e,t)<0?"nv-bar negative":"nv-bar positive"}).style("fill",function(e,t){return e.color||l(e,t)}).style("stroke",function(e,t){return e.color||l(e,t)}).select("rect").attr("class",y).transition().attr("width",s.rangeBand()*.9/e.length),O.transition().attr("transform",function(e,t){var n=s(u(e,t))+s.rangeBand()*.05,r=a(e,t)<0?o(0):o(0)-o(a(e,t))<1?o(0)-1:o(a(e,t));return"translate("+n+", "+r+")"}).select("rect").attr("height",function(e,t){return Math.max(Math.abs(o(a(e,t))-o(d&&d[0]||0))||1)}),b=s.copy(),w=o.copy()}),E}var t={top:0,right:0,bottom:0,left:0},n=960,r=500,i=Math.floor(Math.random()*1e4),s=d3.scale.ordinal(),o=d3.scale.linear(),u=function(e){return e.x},a=function(e){return e.y},f=[0],l=e.utils.defaultColor(),c=!1,h=d3.format(",.2f"),p,d,v,m,g=d3.dispatch("chartClick","elementClick","elementDblClick","elementMouseover","elementMouseout"),y="discreteBar",b,w;return E.dispatch=g,E.options=e.utils.optionsFunc.bind(E),E.x=function(e){return arguments.length?(u=e,E):u},E.y=function(e){return arguments.length?(a=e,E):a},E.margin=function(e){return arguments.length?(t.top=typeof e.top!="undefined"?e.top:t.top,t.right=typeof e.right!="undefined"?e.right:t.right,t.bottom=typeof e.bottom!="undefined"?e.bottom:t.bottom,t.left=typeof e.left!="undefined"?e.left:t.left,E):t},E.width=function(e){return arguments.length?(n=e,E):n},E.height=function(e){return arguments.length?(r=e,E):r},E.xScale=function(e){return arguments.length?(s=e,E):s},E.yScale=function(e){return arguments.length?(o=e,E):o},E.xDomain=function(e){return arguments.length?(p=e,E):p},E.yDomain=function(e){return arguments.length?(d=e,E):d},E.xRange=function(e){return arguments.length?(v=e,E):v},E.yRange=function(e){return arguments.length?(m=e,E):m},E.forceY=function(e){return arguments.length?(f=e,E):f},E.color=function(t){return arguments.length?(l=e.utils.getColor(t),E):l},E.id=function(e){return arguments.length?(i=e,E):i},E.showValues=function(e){return arguments.length?(c=e,E):c},E.valueFormat=function(e){return arguments.length?(h=e,E):h},E.rectClass=function(e){return arguments.length?(y=e,E):y},E},e.models.discreteBarChart=function(){"use strict";function w(e){return e.each(function(e){var u=d3.select(this),p=this,E=(s||parseInt(u.style("width"))||960)-i.left-i.right,S=(o||parseInt(u.style("height"))||400)-i.top-i.bottom;w.update=function(){g.beforeUpdate(),u.transition().duration(y).call(w)},w.container=this;if(!e||!e.length||!e.filter(function(e){return e.values.length}).length){var T=u.selectAll(".nv-noData").data([m]);return T.enter().append("text").attr("class","nvd3 nv-noData").attr("dy","-.7em").style("text-anchor","middle"),T.attr("x",i.left+E/2).attr("y",i.top+S/2).text(function(e){return e}),w}u.selectAll(".nv-noData").remove(),d=t.xScale(),v=t.yScale().clamp(!0);var N=u.selectAll("g.nv-wrap.nv-discreteBarWithAxes").data([e]),C=N.enter().append("g").attr("class","nvd3 nv-wrap nv-discreteBarWithAxes").append("g"),k=C.append("defs"),L=N.select("g");C.append("g").attr("class","nv-x nv-axis"),C.append("g").attr("class","nv-y nv-axis").append("g").attr("class","nv-zeroLine").append("line"),C.append("g").attr("class","nv-barsWrap"),L.attr("transform","translate("+i.left+","+i.top+")"),l&&L.select(".nv-y.nv-axis").attr("transform","translate("+E+",0)"),t.width(E).height(S);var A=L.select(".nv-barsWrap").datum(e.filter(function(e){return!e.disabled}));A.transition().call(t),k.append("clipPath").attr("id","nv-x-label-clip-"+t.id()).append("rect"),L.select("#nv-x-label-clip-"+t.id()+" rect").attr("width",d.rangeBand()*(c?2:1)).attr("height",16).attr("x",-d.rangeBand()/(c?1:2));if(a){n.scale(d).ticks(E/100).tickSize(-S,0),L.select(".nv-x.nv-axis").attr("transform","translate(0,"+(v.range()[0]+(t.showValues()&&v.domain()[0]<0?16:0))+")"),L.select(".nv-x.nv-axis").transition().call(n);var O=L.select(".nv-x.nv-axis").selectAll("g");c&&O.selectAll("text").attr("transform",function(e,t,n){return"translate(0,"+(n%2==0?"5":"17")+")"})}f&&(r.scale(v).ticks(S/36).tickSize(-E,0),L.select(".nv-y.nv-axis").transition().call(r)),L.select(".nv-zeroLine line").attr("x1",0).attr("x2",E).attr("y1",v(0)).attr("y2",v(0)),g.on("tooltipShow",function(e){h&&b(e,p.parentNode)})}),w}var t=e.models.discreteBar(),n=e.models.axis(),r=e.models.axis(),i={top:15,right:10,bottom:50,left:60},s=null,o=null,u=e.utils.getColor(),a=!0,f=!0,l=!1,c=!1,h=!0,p=function(e,t,n,r,i){return"<h3>"+t+"</h3>"+"<p>"+n+"</p>"},d,v,m="No Data Available.",g=d3.dispatch("tooltipShow","tooltipHide","beforeUpdate"),y=250;n.orient("bottom").highlightZero(!1).showMaxMin(!1).tickFormat(function(e){return e}),r.orient(l?"right":"left").tickFormat(d3.format(",.1f"));var b=function(i,s){var o=i.pos[0]+(s.offsetLeft||0),u=i.pos[1]+(s.offsetTop||0),a=n.tickFormat()(t.x()(i.point,i.pointIndex)),f=r.tickFormat()(t.y()(i.point,i.pointIndex)),l=p(i.series.key,a,f,i,w);e.tooltip.show([o,u],l,i.value<0?"n":"s",null,s)};return t.dispatch.on("elementMouseover.tooltip",function(e){e.pos=[e.pos[0]+i.left,e.pos[1]+i.top],g.tooltipShow(e)}),t.dispatch.on("elementMouseout.tooltip",function(e){g.tooltipHide(e)}),g.on("tooltipHide",function(){h&&e.tooltip.cleanup()}),w.dispatch=g,w.discretebar=t,w.xAxis=n,w.yAxis=r,d3.rebind(w,t,"x","y","xDomain","yDomain","xRange","yRange","forceX","forceY","id","showValues","valueFormat"),w.options=e.utils.optionsFunc.bind(w),w.margin=function(e){return arguments.length?(i.top=typeof e.top!="undefined"?e.top:i.top,i.right=typeof e.right!="undefined"?e.right:i.right,i.bottom=typeof e.bottom!="undefined"?e.bottom:i.bottom,i.left=typeof e.left!="undefined"?e.left:i.left,w):i},w.width=function(e){return arguments.length?(s=e,w):s},w.height=function(e){return arguments.length?(o=e,w):o},w.color=function(n){return arguments.length?(u=e.utils.getColor(n),t.color(u),w):u},w.showXAxis=function(e){return arguments.length?(a=e,w):a},w.showYAxis=function(e){return arguments.length?(f=e,w):f},w.rightAlignYAxis=function(e){return arguments.length?(l=e,r.orient(e?"right":"left"),w):l},w.staggerLabels=function(e){return arguments.length?(c=e,w):c},w.tooltips=function(e){return arguments.length?(h=e,w):h},w.tooltipContent=function(e){return arguments.length?(p=e,w):p},w.noData=function(e){return arguments.length?(m=e,w):m},w.transitionDuration=function(e){return arguments.length?(y=e,w):y},w},e.models.distribution=function(){"use strict";function l(e){return e.each(function(e){var a=n-(i==="x"?t.left+t.right:t.top+t.bottom),l=i=="x"?"y":"x",c=d3.select(this);f=f||u;var h=c.selectAll("g.nv-distribution").data([e]),p=h.enter().append("g").attr("class","nvd3 nv-distribution"),d=p.append("g"),v=h.select("g");h.attr("transform","translate("+t.left+","+t.top+")");var m=v.selectAll("g.nv-dist").data(function(e){return e},function(e){return e.key});m.enter().append("g"),m.attr("class",function(e,t){return"nv-dist nv-series-"+t}).style("stroke",function(e,t){return o(e,t)});var g=m.selectAll("line.nv-dist"+i).data(function(e){return e.values});g.enter().append("line").attr(i+"1",function(e,t){return f(s(e,t))}).attr(i+"2",function(e,t){return f(s(e,t))}),m.exit().selectAll("line.nv-dist"+i).transition().attr(i+"1",function(e,t){return u(s(e,t))}).attr(i+"2",function(e,t){return u(s(e,t))}).style("stroke-opacity",0).remove(),g.attr("class",function(e,t){return"nv-dist"+i+" nv-dist"+i+"-"+t}).attr(l+"1",0).attr(l+"2",r),g.transition().attr(i+"1",function(e,t){return u(s(e,t))}).attr(i+"2",function(e,t){return u(s(e,t))}),f=u.copy()}),l}var t={top:0,right:0,bottom:0,left:0},n=400,r=8,i="x",s=function(e){return e[i]},o=e.utils.defaultColor(),u=d3.scale.linear(),a,f;return l.options=e.utils.optionsFunc.bind(l),l.margin=function(e){return arguments.length?(t.top=typeof e.top!="undefined"?e.top:t.top,t.right=typeof e.right!="undefined"?e.right:t.right,t.bottom=typeof e.bottom!="undefined"?e.bottom:t.bottom,t.left=typeof e.left!="undefined"?e.left:t.left,l):t},l.width=function(e){return arguments.length?(n=e,l):n},l.axis=function(e){return arguments.length?(i=e,l):i},l.size=function(e){return arguments.length?(r=e,l):r},l.getData=function(e){return arguments.length?(s=d3.functor(e),l):s},l.scale=function(e){return arguments.length?(u=e,l):u},l.color=function(t){return arguments.length?(o=e.utils.getColor(t),l):o},l},e.models.historicalBar=function(){"use strict";function w(E){return E.each(function(w){var E=n-t.left-t.right,S=r-t.top-t.bottom,T=d3.select(this);s.domain(d||d3.extent(w[0].values.map(u).concat(f))),c?s.range(m||[E*.5/w[0].values.length,E*(w[0].values.length-.5)/w[0].values.length]):s.range(m||[0,E]),o.domain(v||d3.extent(w[0].values.map(a).concat(l))).range(g||[S,0]),s.domain()[0]===s.domain()[1]&&(s.domain()[0]?s.domain([s.domain()[0]-s.domain()[0]*.01,s.domain()[1]+s.domain()[1]*.01]):s.domain([-1,1])),o.domain()[0]===o.domain()[1]&&(o.domain()[0]?o.domain([o.domain()[0]+o.domain()[0]*.01,o.domain()[1]-o.domain()[1]*.01]):o.domain([-1,1]));var N=T.selectAll("g.nv-wrap.nv-historicalBar-"+i).data([w[0].values]),C=N.enter().append("g").attr("class","nvd3 nv-wrap nv-historicalBar-"+i),k=C.append("defs"),L=C.append("g"),A=N.select("g");L.append("g").attr("class","nv-bars"),N.attr("transform","translate("+t.left+","+t.top+")"),T.on("click",function(e,t){y.chartClick({data:e,index:t,pos:d3.event,id:i})}),k.append("clipPath").attr("id","nv-chart-clip-path-"+i).append("rect"),N.select("#nv-chart-clip-path-"+i+" rect").attr("width",E).attr("height",S),A.attr("clip-path",h?"url(#nv-chart-clip-path-"+i+")":"");var O=N.select(".nv-bars").selectAll(".nv-bar").data(function(e){return e},function(e,t){return u(e,t)});O.exit().remove();var M=O.enter().append("rect").attr("x",0).attr("y",function(t,n){return e.utils.NaNtoZero(o(Math.max(0,a(t,n))))}).attr("height",function(t,n){return e.utils.NaNtoZero(Math.abs(o(a(t,n))-o(0)))}).attr("transform",function(e,t){return"translate("+(s(u(e,t))-E/w[0].values.length*.45)+",0)"}).on("mouseover",function(e,t){if(!b)return;d3.select(this).classed("hover",!0),y.elementMouseover({point:e,series:w[0],pos:[s(u(e,t)),o(a(e,t))],pointIndex:t,seriesIndex:0,e:d3.event})}).on("mouseout",function(e,t){if(!b)return;d3.select(this).classed("hover",!1),y.elementMouseout({point:e,series:w[0],pointIndex:t,seriesIndex:0,e:d3.event})}).on("click",function(e,t){if(!b)return;y.elementClick({value:a(e,t),data:e,index:t,pos:[s(u(e,t)),o(a(e,t))],e:d3.event,id:i}),d3.event.stopPropagation()}).on("dblclick",function(e,t){if(!b)return;y.elementDblClick({value:a(e,t),data:e,index:t,pos:[s(u(e,t)),o(a(e,t))],e:d3.event,id:i}),d3.event.stopPropagation()});O.attr("fill",function(e,t){return p(e,t)}).attr("class",function(e,t,n){return(a(e,t)<0?"nv-bar negative":"nv-bar positive")+" nv-bar-"+n+"-"+t}).transition().attr("transform",function(e,t){return"translate("+(s(u(e,t))-E/w[0].values.length*.45)+",0)"}).attr("width",E/w[0].values.length*.9),O.transition().attr("y",function(t,n){var r=a(t,n)<0?o(0):o(0)-o(a(t,n))<1?o(0)-1:o(a(t,n));return e.utils.NaNtoZero(r)}).attr("height",function(t,n){return e.utils.NaNtoZero(Math.max(Math.abs(o(a(t,n))-o(0)),1))})}),w}var t={top:0,right:0,bottom:0,left:0},n=960,r=500,i=Math.floor(Math.random()*1e4),s=d3.scale.linear(),o=d3.scale.linear(),u=function(e){return e.x},a=function(e){return e.y},f=[],l=[0],c=!1,h=!0,p=e.utils.defaultColor(),d,v,m,g,y=d3.dispatch("chartClick","elementClick","elementDblClick","elementMouseover","elementMouseout"),b=!0;return w.highlightPoint=function(e,t){d3.select(".nv-historicalBar-"+i).select(".nv-bars .nv-bar-0-"+e).classed("hover",t)},w.clearHighlights=function(){d3.select(".nv-historicalBar-"+i).select(".nv-bars .nv-bar.hover").classed("hover",!1)},w.dispatch=y,w.options=e.utils.optionsFunc.bind(w),w.x=function(e){return arguments.length?(u=e,w):u},w.y=function(e){return arguments.length?(a=e,w):a},w.margin=function(e){return arguments.length?(t.top=typeof e.top!="undefined"?e.top:t.top,t.right=typeof e.right!="undefined"?e.right:t.right,t.bottom=typeof e.bottom!="undefined"?e.bottom:t.bottom,t.left=typeof e.left!="undefined"?e.left:t.left,w):t},w.width=function(e){return arguments.length?(n=e,w):n},w.height=function(e){return arguments.length?(r=e,w):r},w.xScale=function(e){return arguments.length?(s=e,w):s},w.yScale=function(e){return arguments.length?(o=e,w):o},w.xDomain=function(e){return arguments.length?(d=e,w):d},w.yDomain=function(e){return arguments.length?(v=e,w):v},w.xRange=function(e){return arguments.length?(m=e,w):m},w.yRange=function(e){return arguments.length?(g=e,w):g},w.forceX=function(e){return arguments.length?(f=e,w):f},w.forceY=function(e){return arguments.length?(l=e,w):l},w.padData=function(e){return arguments.length?(c=e,w):c},w.clipEdge=function(e){return arguments.length?(h=e,w):h},w.color=function(t){return arguments.length?(p=e.utils.getColor(t),w):p},w.id=function(e){return arguments.length?(i=e,w):i},w.interactive=function(e){return arguments.length?(b=!1,w):b},w},e.models.historicalBarChart=function(){"use strict";function x(e){return e.each(function(d){var T=d3.select(this),N=this,C=(u||parseInt(T.style("width"))||960)-s.left-s.right,k=(a||parseInt(T.style("height"))||400)-s.top-s.bottom;x.update=function(){T.transition().duration(E).call(x)},x.container=this,g.disabled=d.map(function(e){return!!e.disabled});if(!y){var L;y={};for(L in g)g[L]instanceof Array?y[L]=g[L].slice(0):y[L]=g[L]}if(!d||!d.length||!d.filter(function(e){return e.values.length}).length){var A=T.selectAll(".nv-noData").data([b]);return A.enter().append("text").attr("class","nvd3 nv-noData").attr("dy","-.7em").style("text-anchor","middle"),A.attr("x",s.left+C/2).attr("y",s.top+k/2).text(function(e){return e}),x}T.selectAll(".nv-noData").remove(),v=t.xScale(),m=t.yScale();var O=T.selectAll("g.nv-wrap.nv-historicalBarChart").data([d]),M=O.enter().append("g").attr("class","nvd3 nv-wrap nv-historicalBarChart").append("g"),_=O.select("g");M.append("g").attr("class","nv-x nv-axis"),M.append("g").attr("class","nv-y nv-axis"),M.append("g").attr("class","nv-barsWrap"),M.append("g").attr("class","nv-legendWrap"),f&&(i.width(C),_.select(".nv-legendWrap").datum(d).call(i),s.top!=i.height()&&(s.top=i.height(),k=(a||parseInt(T.style("height"))||400)-s.top-s.bottom),O.select(".nv-legendWrap").attr("transform","translate(0,"+ -s.top+")")),O.attr("transform","translate("+s.left+","+s.top+")"),h&&_.select(".nv-y.nv-axis").attr("transform","translate("+C+",0)"),t.width(C).height(k).color(d.map(function(e,t){return e.color||o(e,t)}).filter(function(e,t){return!d[t].disabled}));var D=_.select(".nv-barsWrap").datum(d.filter(function(e){return!e.disabled}));D.transition().call(t),l&&(n.scale(v).tickSize(-k,0),_.select(".nv-x.nv-axis").attr("transform","translate(0,"+m.range()[0]+")"),_.select(".nv-x.nv-axis").transition().call(n)),c&&(r.scale(m).ticks(k/36).tickSize(-C,0),_.select(".nv-y.nv-axis").transition().call(r)),i.dispatch.on("legendClick",function(t,n){t.disabled=!t.disabled,d.filter(function(e){return!e.disabled}).length||d.map(function(e){return e.disabled=!1,O.selectAll(".nv-series").classed("disabled",!1),e}),g.disabled=d.map(function(e){return!!e.disabled}),w.stateChange(g),e.transition().call(x)}),i.dispatch.on("legendDblclick",function(e){d.forEach(function(e){e.disabled=!0}),e.disabled=!1,g.disabled=d.map(function(e){return!!e.disabled}),w.stateChange(g),x.update()}),w.on("tooltipShow",function(e){p&&S(e,N.parentNode)}),w.on("changeState",function(e){typeof e.disabled!="undefined"&&(d.forEach(function(t,n){t.disabled=e.disabled[n]}),g.disabled=e.disabled),x.update()})}),x}var t=e.models.historicalBar(),n=e.models.axis(),r=e.models.axis(),i=e.models.legend(),s={top:30,right:90,bottom:50,left:90},o=e.utils.defaultColor(),u=null,a=null,f=!1,l=!0,c=!0,h=!1,p=!0,d=function(e,t,n,r,i){return"<h3>"+e+"</h3>"+"<p>"+n+" at "+t+"</p>"},v,m,g={},y=null,b="No Data Available.",w=d3.dispatch("tooltipShow","tooltipHide","stateChange","changeState"),E=250;n.orient("bottom").tickPadding(7),r.orient(h?"right":"left");var S=function(i,s){if(s){var o=d3.select(s).select("svg"),u=o.node()?o.attr("viewBox"):null;if(u){u=u.split(" ");var a=parseInt(o.style("width"))/u[2];i.pos[0]=i.pos[0]*a,i.pos[1]=i.pos[1]*a}}var f=i.pos[0]+(s.offsetLeft||0),l=i.pos[1]+(s.offsetTop||0),c=n.tickFormat()(t.x()(i.point,i.pointIndex)),h=r.tickFormat()(t.y()(i.point,i.pointIndex)),p=d(i.series.key,c,h,i,x);e.tooltip.show([f,l],p,null,null,s)};return t.dispatch.on("elementMouseover.tooltip",function(e){e.pos=[e.pos[0]+s.left,e.pos[1]+s.top],w.tooltipShow(e)}),t.dispatch.on("elementMouseout.tooltip",function(e){w.tooltipHide(e)}),w.on("tooltipHide",function(){p&&e.tooltip.cleanup()}),x.dispatch=w,x.bars=t,x.legend=i,x.xAxis=n,x.yAxis=r,d3.rebind(x,t,"defined","isArea","x","y","size","xScale","yScale","xDomain","yDomain","xRange","yRange","forceX","forceY","interactive","clipEdge","clipVoronoi","id","interpolate","highlightPoint","clearHighlights","interactive"),x.options=e.utils.optionsFunc.bind(x),x.margin=function(e){return arguments.length?(s.top=typeof e.top!="undefined"?e.top:s.top,s.right=typeof e.right!="undefined"?e.right:s.right,s.bottom=typeof e.bottom!="undefined"?e.bottom:s.bottom,s.left=typeof e.left!="undefined"?e.left:s.left,x):s},x.width=function(e){return arguments.length?(u=e,x):u},x.height=function(e){return arguments.length?(a=e,x):a},x.color=function(t){return arguments.length?(o=e.utils.getColor(t),i.color(o),x):o},x.showLegend=function(e){return arguments.length?(f=e,x):f},x.showXAxis=function(e){return arguments.length?(l=e,x):l},x.showYAxis=function(e){return arguments.length?(c=e,x):c},x.rightAlignYAxis=function(e){return arguments.length?(h=e,r.orient(e?"right":"left"),x):h},x.tooltips=function(e){return arguments.length?(p=e,x):p},x.tooltipContent=function(e){return arguments.length?(d=e,x):d},x.state=function(e){return arguments.length?(g=e,x):g},x.defaultState=function(e){return arguments.length?(y=e,x):y},x.noData=function(e){return arguments.length?(b=e,x):b},x.transitionDuration=function(e){return arguments.length?(E=e,x):E},x},e.models.indentedTree=function(){"use strict";function g(e){return e.each(function(e){function k(e,t,n){d3.event.stopPropagation();if(d3.event.shiftKey&&!n)return d3.event.shiftKey=!1,e.values&&e.values.forEach(function(e){(e.values||e._values)&&k(e,0,!0)}),!0;if(!O(e))return!0;e.values?(e._values=e.values,e.values=null):(e.values=e._values,e._values=null),g.update()}function L(e){return e._values&&e._values.length?h:e.values&&e.values.length?p:""}function A(e){return e._values&&e._values.length}function O(e){var t=e.values||e._values;return t&&t.length}var t=1,n=d3.select(this),i=d3.layout.tree().children(function(e){return e.values}).size([r,f]);g.update=function(){n.transition().duration(600).call(g)},e[0]||(e[0]={key:a});var s=i.nodes(e[0]),y=d3.select(this).selectAll("div").data([[s]]),b=y.enter().append("div").attr("class","nvd3 nv-wrap nv-indentedtree"),w=b.append("table"),E=y.select("table").attr("width","100%").attr("class",c);if(o){var S=w.append("thead"),x=S.append("tr");l.forEach(function(e){x.append("th").attr("width",e.width?e.width:"10%").style("text-align",e.type=="numeric"?"right":"left").append("span").text(e.label)})}var T=E.selectAll("tbody").data(function(e){return e});T.enter().append("tbody"),t=d3.max(s,function(e){return e.depth}),i.size([r,t*f]);var N=T.selectAll("tr").data(function(e){return e.filter(function(e){return u&&!e.children?u(e):!0})},function(e,t){return e.id||e.id||++m});N.exit().remove(),N.select("img.nv-treeicon").attr("src",L).classed("folded",A);var C=N.enter().append("tr");l.forEach(function(e,t){var n=C.append("td").style("padding-left",function(e){return(t?0:e.depth*f+12+(L(e)?0:16))+"px"},"important").style("text-align",e.type=="numeric"?"right":"left");t==0&&n.append("img").classed("nv-treeicon",!0).classed("nv-folded",A).attr("src",L).style("width","14px").style("height","14px").style("padding","0 1px").style("display",function(e){return L(e)?"inline-block":"none"}).on("click",k),n.each(function(n){!t&&v(n)?d3.select(this).append("a").attr("href",v).attr("class",d3.functor(e.classes)).append("span"):d3.select(this).append("span"),d3.select(this).select("span").attr("class",d3.functor(e.classes)).text(function(t){return e.format?e.format(t):t[e.key]||"-"})}),e.showCount&&(n.append("span").attr("class","nv-childrenCount"),N.selectAll("span.nv-childrenCount").text(function(e){return e.values&&e.values.length||e._values&&e._values.length?"("+(e.values&&e.values.filter(function(e){return u?u(e):!0}).length||e._values&&e._values.filter(function(e){return u?u(e):!0}).length||0)+")":""}))}),N.order().on("click",function(e){d.elementClick({row:this,data:e,pos:[e.x,e.y]})}).on("dblclick",function(e){d.elementDblclick({row:this,data:e,pos:[e.x,e.y]})}).on("mouseover",function(e){d.elementMouseover({row:this,data:e,pos:[e.x,e.y]})}).on("mouseout",function(e){d.elementMouseout({row:this,data:e,pos:[e.x,e.y]})})}),g}var t={top:0,right:0,bottom:0,left:0},n=960,r=500,i=e.utils.defaultColor(),s=Math.floor(Math.random()*1e4),o=!0,u=!1,a="No Data Available.",f=20,l=[{key:"key",label:"Name",type:"text"}],c=null,h="images/grey-plus.png",p="images/grey-minus.png",d=d3.dispatch("elementClick","elementDblclick","elementMouseover","elementMouseout"),v=function(e){return e.url},m=0;return g.options=e.utils.optionsFunc.bind(g),g.margin=function(e){return arguments.length?(t.top=typeof e.top!="undefined"?e.top:t.top,t.right=typeof e.right!="undefined"?e.right:t.right,t.bottom=typeof e.bottom!="undefined"?e.bottom:t.bottom,t.left=typeof e.left!="undefined"?e.left:t.left,g):t},g.width=function(e){return arguments.length?(n=e,g):n},g.height=function(e){return arguments.length?(r=e,g):r},g.color=function(t){return arguments.length?(i=e.utils.getColor(t),scatter.color(i),g):i},g.id=function(e){return arguments.length?(s=e,g):s},g.header=function(e){return arguments.length?(o=e,g):o},g.noData=function(e){return arguments.length?(a=e,g):a},g.filterZero=function(e){return arguments.length?(u=e,g):u},g.columns=function(e){return arguments.length?(l=e,g):l},g.tableClass=function(e){return arguments.length?(c=e,g):c},g.iconOpen=function(e){return arguments.length?(h=e,g):h},g.iconClose=function(e){return arguments.length?(p=e,g):p},g.getUrl=function(e){return arguments.length?(v=e,g):v},g},e.models.legend=function(){"use strict";function c(h){return h.each(function(c){var h=n-t.left-t.right,p=d3.select(this),d=p.selectAll("g.nv-legend").data([c]),v=d.enter().append("g").attr("class","nvd3 nv-legend").append("g"),m=d.select("g");d.attr("transform","translate("+t.left+","+t.top+")");var g=m.selectAll(".nv-series").data(function(e){return e}),y=g.enter().append("g").attr("class","nv-series").on("mouseover",function(e,t){l.legendMouseover(e,t)}).on("mouseout",function(e,t){l.legendMouseout(e,t)}).on("click",function(e,t){l.legendClick(e,t),a&&(f?(c.forEach(function(e){e.disabled=!0}),e.disabled=!1):(e.disabled=!e.disabled,c.every(function(e){return e.disabled})&&c.forEach(function(e){e.disabled=!1})),l.stateChange({disabled:c.map(function(e){return!!e.disabled})}))}).on("dblclick",function(e,t){l.legendDblclick(e,t),a&&(c.forEach(function(e){e.disabled=!0}),e.disabled=!1,l.stateChange({disabled:c.map(function(e){return!!e.disabled})}))});y.append("circle").style("stroke-width",2).attr("class","nv-legend-symbol").attr("r",5),y.append("text").attr("text-anchor","start").attr("class","nv-legend-text").attr("dy",".32em").attr("dx","8"),g.classed("disabled",function(e){return e.disabled}),g.exit().remove(),g.select("circle").style("fill",function(e,t){return e.color||s(e,t)}).style("stroke",function(e,t){return e.color||s(e,t)}),g.select("text").text(i);if(o){var b=[];g.each(function(t,n){var r=d3.select(this).select("text"),i;try{i=r.getComputedTextLength();if(i<=0)throw Error()}catch(s){i=e.utils.calcApproxTextWidth(r)}b.push(i+28)});var w=0,E=0,S=[];while(E<h&&w<b.length)S[w]=b[w],E+=b[w++];w===0&&(w=1);while(E>h&&w>1){S=[],w--;for(var x=0;x<b.length;x++)b[x]>(S[x%w]||0)&&(S[x%w]=b[x]);E=S.reduce(function(e,t,n,r){return e+t})}var T=[];for(var N=0,C=0;N<w;N++)T[N]=C,C+=S[N];g.attr("transform",function(e,t){return"translate("+T[t%w]+","+(5+Math.floor(t/w)*20)+")"}),u?m.attr("transform","translate("+(n-t.right-E)+","+t.top+")"):m.attr("transform","translate(0,"+t.top+")"),r=t.top+t.bottom+Math.ceil(b.length/w)*20}else{var k=5,L=5,A=0,O;g.attr("transform",function(e,r){var i=d3.select(this).select("text").node().getComputedTextLength()+28;return O=L,n<t.left+t.right+O+i&&(L=O=5,k+=20),L+=i,L>A&&(A=L),"translate("+O+","+k+")"}),m.attr("transform","translate("+(n-t.right-A)+","+t.top+")"),r=t.top+t.bottom+k+15}}),c}var t={top:5,right:0,bottom:5,left:0},n=400,r=20,i=function(e){return e.key},s=e.utils.defaultColor(),o=!0,u=!0,a=!0,f=!1,l=d3.dispatch("legendClick","legendDblclick","legendMouseover","legendMouseout","stateChange");return c.dispatch=l,c.options=e.utils.optionsFunc.bind(c),c.margin=function(e){return arguments.length?(t.top=typeof e.top!="undefined"?e.top:t.top,t.right=typeof e.right!="undefined"?e.right:t.right,t.bottom=typeof e.bottom!="undefined"?e.bottom:t.bottom,t.left=typeof e.left!="undefined"?e.left:t.left,c):t},c.width=function(e){return arguments.length?(n=e,c):n},c.height=function(e){return arguments.length?(r=e,c):r},c.key=function(e){return arguments.length?(i=e,c):i},c.color=function(t){return arguments.length?(s=e.utils.getColor(t),c):s},c.align=function(e){return arguments.length?(o=e,c):o},c.rightAlign=function(e){return arguments.length?(u=e,c):u},c.updateState=function(e){return arguments.length?(a=e,c):a},c.radioButtonMode=function(e){return arguments.length?(f=e,c):f},c},e.models.line=function(){"use strict";function m(g){return g.each(function(m){var g=r-n.left-n.right,b=i-n.top-n.bottom,w=d3.select(this);c=t.xScale(),h=t.yScale(),d=d||c,v=v||h;var E=w.selectAll("g.nv-wrap.nv-line").data([m]),S=E.enter().append("g").attr("class","nvd3 nv-wrap nv-line"),T=S.append("defs"),N=S.append("g"),C=E.select("g");N.append("g").attr("class","nv-groups"),N.append("g").attr("class","nv-scatterWrap"),E.attr("transform","translate("+n.left+","+n.top+")"),t.width(g).height(b);var k=E.select(".nv-scatterWrap");k.transition().call(t),T.append("clipPath").attr("id","nv-edge-clip-"+t.id()).append("rect"),E.select("#nv-edge-clip-"+t.id()+" rect").attr("width",g).attr("height",b),C.attr("clip-path",l?"url(#nv-edge-clip-"+t.id()+")":""),k.attr("clip-path",l?"url(#nv-edge-clip-"+t.id()+")":"");var L=E.select(".nv-groups").selectAll(".nv-group").data(function(e){return e},function(e){return e.key});L.enter().append("g").style("stroke-opacity",1e-6).style("fill-opacity",1e-6),L.exit().remove(),L.attr("class",function(e,t){return"nv-group nv-series-"+t}).classed("hover",function(e){return e.hover}).style("fill",function(e,t){return s(e,t)}).style("stroke",function(e,t){return s(e,t)}),L.transition().style("stroke-opacity",1).style("fill-opacity",.5);var A=L.selectAll("path.nv-area").data(function(e){return f(e)?[e]:[]});A.enter().append("path").attr("class","nv-area").attr("d",function(t){return d3.svg.area().interpolate(p).defined(a).x(function(t,n){return e.
3
+ utils.NaNtoZero(d(o(t,n)))}).y0(function(t,n){return e.utils.NaNtoZero(v(u(t,n)))}).y1(function(e,t){return v(h.domain()[0]<=0?h.domain()[1]>=0?0:h.domain()[1]:h.domain()[0])}).apply(this,[t.values])}),L.exit().selectAll("path.nv-area").remove(),A.transition().attr("d",function(t){return d3.svg.area().interpolate(p).defined(a).x(function(t,n){return e.utils.NaNtoZero(c(o(t,n)))}).y0(function(t,n){return e.utils.NaNtoZero(h(u(t,n)))}).y1(function(e,t){return h(h.domain()[0]<=0?h.domain()[1]>=0?0:h.domain()[1]:h.domain()[0])}).apply(this,[t.values])});var O=L.selectAll("path.nv-line").data(function(e){return[e.values]});O.enter().append("path").attr("class","nv-line").attr("d",d3.svg.line().interpolate(p).defined(a).x(function(t,n){return e.utils.NaNtoZero(d(o(t,n)))}).y(function(t,n){return e.utils.NaNtoZero(v(u(t,n)))})),O.transition().attr("d",d3.svg.line().interpolate(p).defined(a).x(function(t,n){return e.utils.NaNtoZero(c(o(t,n)))}).y(function(t,n){return e.utils.NaNtoZero(h(u(t,n)))})),d=c.copy(),v=h.copy()}),m}var t=e.models.scatter(),n={top:0,right:0,bottom:0,left:0},r=960,i=500,s=e.utils.defaultColor(),o=function(e){return e.x},u=function(e){return e.y},a=function(e,t){return!isNaN(u(e,t))&&u(e,t)!==null},f=function(e){return e.area},l=!1,c,h,p="linear";t.size(16).sizeDomain([16,256]);var d,v;return m.dispatch=t.dispatch,m.scatter=t,d3.rebind(m,t,"id","interactive","size","xScale","yScale","zScale","xDomain","yDomain","xRange","yRange","sizeDomain","forceX","forceY","forceSize","clipVoronoi","useVoronoi","clipRadius","padData","highlightPoint","clearHighlights"),m.options=e.utils.optionsFunc.bind(m),m.margin=function(e){return arguments.length?(n.top=typeof e.top!="undefined"?e.top:n.top,n.right=typeof e.right!="undefined"?e.right:n.right,n.bottom=typeof e.bottom!="undefined"?e.bottom:n.bottom,n.left=typeof e.left!="undefined"?e.left:n.left,m):n},m.width=function(e){return arguments.length?(r=e,m):r},m.height=function(e){return arguments.length?(i=e,m):i},m.x=function(e){return arguments.length?(o=e,t.x(e),m):o},m.y=function(e){return arguments.length?(u=e,t.y(e),m):u},m.clipEdge=function(e){return arguments.length?(l=e,m):l},m.color=function(n){return arguments.length?(s=e.utils.getColor(n),t.color(s),m):s},m.interpolate=function(e){return arguments.length?(p=e,m):p},m.defined=function(e){return arguments.length?(a=e,m):a},m.isArea=function(e){return arguments.length?(f=d3.functor(e),m):f},m},e.models.lineChart=function(){"use strict";function N(m){return m.each(function(m){var C=d3.select(this),k=this,L=(a||parseInt(C.style("width"))||960)-o.left-o.right,A=(f||parseInt(C.style("height"))||400)-o.top-o.bottom;N.update=function(){C.transition().duration(x).call(N)},N.container=this,b.disabled=m.map(function(e){return!!e.disabled});if(!w){var O;w={};for(O in b)b[O]instanceof Array?w[O]=b[O].slice(0):w[O]=b[O]}if(!m||!m.length||!m.filter(function(e){return e.values.length}).length){var M=C.selectAll(".nv-noData").data([E]);return M.enter().append("text").attr("class","nvd3 nv-noData").attr("dy","-.7em").style("text-anchor","middle"),M.attr("x",o.left+L/2).attr("y",o.top+A/2).text(function(e){return e}),N}C.selectAll(".nv-noData").remove(),g=t.xScale(),y=t.yScale();var _=C.selectAll("g.nv-wrap.nv-lineChart").data([m]),D=_.enter().append("g").attr("class","nvd3 nv-wrap nv-lineChart").append("g"),P=_.select("g");D.append("rect").style("opacity",0),D.append("g").attr("class","nv-x nv-axis"),D.append("g").attr("class","nv-y nv-axis"),D.append("g").attr("class","nv-linesWrap"),D.append("g").attr("class","nv-legendWrap"),D.append("g").attr("class","nv-interactive"),P.select("rect").attr("width",L).attr("height",A>0?A:0),l&&(i.width(L),P.select(".nv-legendWrap").datum(m).call(i),o.top!=i.height()&&(o.top=i.height(),A=(f||parseInt(C.style("height"))||400)-o.top-o.bottom),_.select(".nv-legendWrap").attr("transform","translate(0,"+ -o.top+")")),_.attr("transform","translate("+o.left+","+o.top+")"),p&&P.select(".nv-y.nv-axis").attr("transform","translate("+L+",0)"),d&&(s.width(L).height(A).margin({left:o.left,top:o.top}).svgContainer(C).xScale(g),_.select(".nv-interactive").call(s)),t.width(L).height(A).color(m.map(function(e,t){return e.color||u(e,t)}).filter(function(e,t){return!m[t].disabled}));var H=P.select(".nv-linesWrap").datum(m.filter(function(e){return!e.disabled}));H.transition().call(t),c&&(n.scale(g).ticks(L/100).tickSize(-A,0),P.select(".nv-x.nv-axis").attr("transform","translate(0,"+y.range()[0]+")"),P.select(".nv-x.nv-axis").transition().call(n)),h&&(r.scale(y).ticks(A/36).tickSize(-L,0),P.select(".nv-y.nv-axis").transition().call(r)),i.dispatch.on("stateChange",function(e){b=e,S.stateChange(b),N.update()}),s.dispatch.on("elementMousemove",function(i){t.clearHighlights();var a,f,l,c=[];m.filter(function(e,t){return e.seriesIndex=t,!e.disabled}).forEach(function(n,r){f=e.interactiveBisect(n.values,i.pointXValue,N.x()),t.highlightPoint(r,f,!0);var s=n.values[f];if(typeof s=="undefined")return;typeof a=="undefined"&&(a=s),typeof l=="undefined"&&(l=N.xScale()(N.x()(s,f))),c.push({key:n.key,value:N.y()(s,f),color:u(n,n.seriesIndex)})});if(c.length>2){var h=N.yScale().invert(i.mouseY),p=Math.abs(N.yScale().domain()[0]-N.yScale().domain()[1]),d=.03*p,g=e.nearestValueIndex(c.map(function(e){return e.value}),h,d);g!==null&&(c[g].highlight=!0)}var y=n.tickFormat()(N.x()(a,f));s.tooltip.position({left:l+o.left,top:i.mouseY+o.top}).chartContainer(k.parentNode).enabled(v).valueFormatter(function(e,t){return r.tickFormat()(e)}).data({value:y,series:c})(),s.renderGuideLine(l)}),s.dispatch.on("elementMouseout",function(e){S.tooltipHide(),t.clearHighlights()}),S.on("tooltipShow",function(e){v&&T(e,k.parentNode)}),S.on("changeState",function(e){typeof e.disabled!="undefined"&&m.length===e.disabled.length&&(m.forEach(function(t,n){t.disabled=e.disabled[n]}),b.disabled=e.disabled),N.update()})}),N}var t=e.models.line(),n=e.models.axis(),r=e.models.axis(),i=e.models.legend(),s=e.interactiveGuideline(),o={top:30,right:20,bottom:50,left:60},u=e.utils.defaultColor(),a=null,f=null,l=!0,c=!0,h=!0,p=!1,d=!1,v=!0,m=function(e,t,n,r,i){return"<h3>"+e+"</h3>"+"<p>"+n+" at "+t+"</p>"},g,y,b={},w=null,E="No Data Available.",S=d3.dispatch("tooltipShow","tooltipHide","stateChange","changeState"),x=250;n.orient("bottom").tickPadding(7),r.orient(p?"right":"left");var T=function(i,s){var o=i.pos[0]+(s.offsetLeft||0),u=i.pos[1]+(s.offsetTop||0),a=n.tickFormat()(t.x()(i.point,i.pointIndex)),f=r.tickFormat()(t.y()(i.point,i.pointIndex)),l=m(i.series.key,a,f,i,N);e.tooltip.show([o,u],l,null,null,s)};return t.dispatch.on("elementMouseover.tooltip",function(e){e.pos=[e.pos[0]+o.left,e.pos[1]+o.top],S.tooltipShow(e)}),t.dispatch.on("elementMouseout.tooltip",function(e){S.tooltipHide(e)}),S.on("tooltipHide",function(){v&&e.tooltip.cleanup()}),N.dispatch=S,N.lines=t,N.legend=i,N.xAxis=n,N.yAxis=r,N.interactiveLayer=s,d3.rebind(N,t,"defined","isArea","x","y","size","xScale","yScale","xDomain","yDomain","xRange","yRange","forceX","forceY","interactive","clipEdge","clipVoronoi","useVoronoi","id","interpolate"),N.options=e.utils.optionsFunc.bind(N),N.margin=function(e){return arguments.length?(o.top=typeof e.top!="undefined"?e.top:o.top,o.right=typeof e.right!="undefined"?e.right:o.right,o.bottom=typeof e.bottom!="undefined"?e.bottom:o.bottom,o.left=typeof e.left!="undefined"?e.left:o.left,N):o},N.width=function(e){return arguments.length?(a=e,N):a},N.height=function(e){return arguments.length?(f=e,N):f},N.color=function(t){return arguments.length?(u=e.utils.getColor(t),i.color(u),N):u},N.showLegend=function(e){return arguments.length?(l=e,N):l},N.showXAxis=function(e){return arguments.length?(c=e,N):c},N.showYAxis=function(e){return arguments.length?(h=e,N):h},N.rightAlignYAxis=function(e){return arguments.length?(p=e,r.orient(e?"right":"left"),N):p},N.useInteractiveGuideline=function(e){return arguments.length?(d=e,e===!0&&(N.interactive(!1),N.useVoronoi(!1)),N):d},N.tooltips=function(e){return arguments.length?(v=e,N):v},N.tooltipContent=function(e){return arguments.length?(m=e,N):m},N.state=function(e){return arguments.length?(b=e,N):b},N.defaultState=function(e){return arguments.length?(w=e,N):w},N.noData=function(e){return arguments.length?(E=e,N):E},N.transitionDuration=function(e){return arguments.length?(x=e,N):x},N},e.models.linePlusBarChart=function(){"use strict";function T(e){return e.each(function(e){var l=d3.select(this),c=this,v=(a||parseInt(l.style("width"))||960)-u.left-u.right,N=(f||parseInt(l.style("height"))||400)-u.top-u.bottom;T.update=function(){l.transition().call(T)},b.disabled=e.map(function(e){return!!e.disabled});if(!w){var C;w={};for(C in b)b[C]instanceof Array?w[C]=b[C].slice(0):w[C]=b[C]}if(!e||!e.length||!e.filter(function(e){return e.values.length}).length){var k=l.selectAll(".nv-noData").data([E]);return k.enter().append("text").attr("class","nvd3 nv-noData").attr("dy","-.7em").style("text-anchor","middle"),k.attr("x",u.left+v/2).attr("y",u.top+N/2).text(function(e){return e}),T}l.selectAll(".nv-noData").remove();var L=e.filter(function(e){return!e.disabled&&e.bar}),A=e.filter(function(e){return!e.bar});m=A.filter(function(e){return!e.disabled}).length&&A.filter(function(e){return!e.disabled})[0].values.length?t.xScale():n.xScale(),g=n.yScale(),y=t.yScale();var O=d3.select(this).selectAll("g.nv-wrap.nv-linePlusBar").data([e]),M=O.enter().append("g").attr("class","nvd3 nv-wrap nv-linePlusBar").append("g"),_=O.select("g");M.append("g").attr("class","nv-x nv-axis"),M.append("g").attr("class","nv-y1 nv-axis"),M.append("g").attr("class","nv-y2 nv-axis"),M.append("g").attr("class","nv-barsWrap"),M.append("g").attr("class","nv-linesWrap"),M.append("g").attr("class","nv-legendWrap"),p&&(o.width(v/2),_.select(".nv-legendWrap").datum(e.map(function(e){return e.originalKey=e.originalKey===undefined?e.key:e.originalKey,e.key=e.originalKey+(e.bar?" (left axis)":" (right axis)"),e})).call(o),u.top!=o.height()&&(u.top=o.height(),N=(f||parseInt(l.style("height"))||400)-u.top-u.bottom),_.select(".nv-legendWrap").attr("transform","translate("+v/2+","+ -u.top+")")),O.attr("transform","translate("+u.left+","+u.top+")"),t.width(v).height(N).color(e.map(function(e,t){return e.color||h(e,t)}).filter(function(t,n){return!e[n].disabled&&!e[n].bar})),n.width(v).height(N).color(e.map(function(e,t){return e.color||h(e,t)}).filter(function(t,n){return!e[n].disabled&&e[n].bar}));var D=_.select(".nv-barsWrap").datum(L.length?L:[{values:[]}]),P=_.select(".nv-linesWrap").datum(A[0]&&!A[0].disabled?A:[{values:[]}]);d3.transition(D).call(n),d3.transition(P).call(t),r.scale(m).ticks(v/100).tickSize(-N,0),_.select(".nv-x.nv-axis").attr("transform","translate(0,"+g.range()[0]+")"),d3.transition(_.select(".nv-x.nv-axis")).call(r),i.scale(g).ticks(N/36).tickSize(-v,0),d3.transition(_.select(".nv-y1.nv-axis")).style("opacity",L.length?1:0).call(i),s.scale(y).ticks(N/36).tickSize(L.length?0:-v,0),_.select(".nv-y2.nv-axis").style("opacity",A.length?1:0).attr("transform","translate("+v+",0)"),d3.transition(_.select(".nv-y2.nv-axis")).call(s),o.dispatch.on("stateChange",function(e){b=e,S.stateChange(b),T.update()}),S.on("tooltipShow",function(e){d&&x(e,c.parentNode)}),S.on("changeState",function(t){typeof t.disabled!="undefined"&&(e.forEach(function(e,n){e.disabled=t.disabled[n]}),b.disabled=t.disabled),T.update()})}),T}var t=e.models.line(),n=e.models.historicalBar(),r=e.models.axis(),i=e.models.axis(),s=e.models.axis(),o=e.models.legend(),u={top:30,right:60,bottom:50,left:60},a=null,f=null,l=function(e){return e.x},c=function(e){return e.y},h=e.utils.defaultColor(),p=!0,d=!0,v=function(e,t,n,r,i){return"<h3>"+e+"</h3>"+"<p>"+n+" at "+t+"</p>"},m,g,y,b={},w=null,E="No Data Available.",S=d3.dispatch("tooltipShow","tooltipHide","stateChange","changeState");n.padData(!0),t.clipEdge(!1).padData(!0),r.orient("bottom").tickPadding(7).highlightZero(!1),i.orient("left"),s.orient("right");var x=function(n,o){var u=n.pos[0]+(o.offsetLeft||0),a=n.pos[1]+(o.offsetTop||0),f=r.tickFormat()(t.x()(n.point,n.pointIndex)),l=(n.series.bar?i:s).tickFormat()(t.y()(n.point,n.pointIndex)),c=v(n.series.key,f,l,n,T);e.tooltip.show([u,a],c,n.value<0?"n":"s",null,o)};return t.dispatch.on("elementMouseover.tooltip",function(e){e.pos=[e.pos[0]+u.left,e.pos[1]+u.top],S.tooltipShow(e)}),t.dispatch.on("elementMouseout.tooltip",function(e){S.tooltipHide(e)}),n.dispatch.on("elementMouseover.tooltip",function(e){e.pos=[e.pos[0]+u.left,e.pos[1]+u.top],S.tooltipShow(e)}),n.dispatch.on("elementMouseout.tooltip",function(e){S.tooltipHide(e)}),S.on("tooltipHide",function(){d&&e.tooltip.cleanup()}),T.dispatch=S,T.legend=o,T.lines=t,T.bars=n,T.xAxis=r,T.y1Axis=i,T.y2Axis=s,d3.rebind(T,t,"defined","size","clipVoronoi","interpolate"),T.options=e.utils.optionsFunc.bind(T),T.x=function(e){return arguments.length?(l=e,t.x(e),n.x(e),T):l},T.y=function(e){return arguments.length?(c=e,t.y(e),n.y(e),T):c},T.margin=function(e){return arguments.length?(u.top=typeof e.top!="undefined"?e.top:u.top,u.right=typeof e.right!="undefined"?e.right:u.right,u.bottom=typeof e.bottom!="undefined"?e.bottom:u.bottom,u.left=typeof e.left!="undefined"?e.left:u.left,T):u},T.width=function(e){return arguments.length?(a=e,T):a},T.height=function(e){return arguments.length?(f=e,T):f},T.color=function(t){return arguments.length?(h=e.utils.getColor(t),o.color(h),T):h},T.showLegend=function(e){return arguments.length?(p=e,T):p},T.tooltips=function(e){return arguments.length?(d=e,T):d},T.tooltipContent=function(e){return arguments.length?(v=e,T):v},T.state=function(e){return arguments.length?(b=e,T):b},T.defaultState=function(e){return arguments.length?(w=e,T):w},T.noData=function(e){return arguments.length?(E=e,T):E},T},e.models.lineWithFocusChart=function(){"use strict";function k(e){return e.each(function(e){function U(e){var t=+(e=="e"),n=t?1:-1,r=M/3;return"M"+.5*n+","+r+"A6,6 0 0 "+t+" "+6.5*n+","+(r+6)+"V"+(2*r-6)+"A6,6 0 0 "+t+" "+.5*n+","+2*r+"Z"+"M"+2.5*n+","+(r+8)+"V"+(2*r-8)+"M"+4.5*n+","+(r+8)+"V"+(2*r-8)}function z(){a.empty()||a.extent(w),I.data([a.empty()?g.domain():w]).each(function(e,t){var n=g(e[0])-v.range()[0],r=v.range()[1]-g(e[1]);d3.select(this).select(".left").attr("width",n<0?0:n),d3.select(this).select(".right").attr("x",g(e[1])).attr("width",r<0?0:r)})}function W(){w=a.empty()?null:a.extent();var n=a.empty()?g.domain():a.extent();if(Math.abs(n[0]-n[1])<=1)return;T.brush({extent:n,brush:a}),z();var s=H.select(".nv-focus .nv-linesWrap").datum(e.filter(function(e){return!e.disabled}).map(function(e,r){return{key:e.key,values:e.values.filter(function(e,r){return t.x()(e,r)>=n[0]&&t.x()(e,r)<=n[1]})}}));s.transition().duration(N).call(t),H.select(".nv-focus .nv-x.nv-axis").transition().duration(N).call(r),H.select(".nv-focus .nv-y.nv-axis").transition().duration(N).call(i)}var S=d3.select(this),L=this,A=(h||parseInt(S.style("width"))||960)-f.left-f.right,O=(p||parseInt(S.style("height"))||400)-f.top-f.bottom-d,M=d-l.top-l.bottom;k.update=function(){S.transition().duration(N).call(k)},k.container=this;if(!e||!e.length||!e.filter(function(e){return e.values.length}).length){var _=S.selectAll(".nv-noData").data([x]);return _.enter().append("text").attr("class","nvd3 nv-noData").attr("dy","-.7em").style("text-anchor","middle"),_.attr("x",f.left+A/2).attr("y",f.top+O/2).text(function(e){return e}),k}S.selectAll(".nv-noData").remove(),v=t.xScale(),m=t.yScale(),g=n.xScale(),y=n.yScale();var D=S.selectAll("g.nv-wrap.nv-lineWithFocusChart").data([e]),P=D.enter().append("g").attr("class","nvd3 nv-wrap nv-lineWithFocusChart").append("g"),H=D.select("g");P.append("g").attr("class","nv-legendWrap");var B=P.append("g").attr("class","nv-focus");B.append("g").attr("class","nv-x nv-axis"),B.append("g").attr("class","nv-y nv-axis"),B.append("g").attr("class","nv-linesWrap");var j=P.append("g").attr("class","nv-context");j.append("g").attr("class","nv-x nv-axis"),j.append("g").attr("class","nv-y nv-axis"),j.append("g").attr("class","nv-linesWrap"),j.append("g").attr("class","nv-brushBackground"),j.append("g").attr("class","nv-x nv-brush"),b&&(u.width(A),H.select(".nv-legendWrap").datum(e).call(u),f.top!=u.height()&&(f.top=u.height(),O=(p||parseInt(S.style("height"))||400)-f.top-f.bottom-d),H.select(".nv-legendWrap").attr("transform","translate(0,"+ -f.top+")")),D.attr("transform","translate("+f.left+","+f.top+")"),t.width(A).height(O).color(e.map(function(e,t){return e.color||c(e,t)}).filter(function(t,n){return!e[n].disabled})),n.defined(t.defined()).width(A).height(M).color(e.map(function(e,t){return e.color||c(e,t)}).filter(function(t,n){return!e[n].disabled})),H.select(".nv-context").attr("transform","translate(0,"+(O+f.bottom+l.top)+")");var F=H.select(".nv-context .nv-linesWrap").datum(e.filter(function(e){return!e.disabled}));d3.transition(F).call(n),r.scale(v).ticks(A/100).tickSize(-O,0),i.scale(m).ticks(O/36).tickSize(-A,0),H.select(".nv-focus .nv-x.nv-axis").attr("transform","translate(0,"+O+")"),a.x(g).on("brush",function(){var e=k.transitionDuration();k.transitionDuration(0),W(),k.transitionDuration(e)}),w&&a.extent(w);var I=H.select(".nv-brushBackground").selectAll("g").data([w||a.extent()]),q=I.enter().append("g");q.append("rect").attr("class","left").attr("x",0).attr("y",0).attr("height",M),q.append("rect").attr("class","right").attr("x",0).attr("y",0).attr("height",M);var R=H.select(".nv-x.nv-brush").call(a);R.selectAll("rect").attr("height",M),R.selectAll(".resize").append("path").attr("d",U),W(),s.scale(g).ticks(A/100).tickSize(-M,0),H.select(".nv-context .nv-x.nv-axis").attr("transform","translate(0,"+y.range()[0]+")"),d3.transition(H.select(".nv-context .nv-x.nv-axis")).call(s),o.scale(y).ticks(M/36).tickSize(-A,0),d3.transition(H.select(".nv-context .nv-y.nv-axis")).call(o),H.select(".nv-context .nv-x.nv-axis").attr("transform","translate(0,"+y.range()[0]+")"),u.dispatch.on("stateChange",function(e){k.update()}),T.on("tooltipShow",function(e){E&&C(e,L.parentNode)})}),k}var t=e.models.line(),n=e.models.line(),r=e.models.axis(),i=e.models.axis(),s=e.models.axis(),o=e.models.axis(),u=e.models.legend(),a=d3.svg.brush(),f={top:30,right:30,bottom:30,left:60},l={top:0,right:30,bottom:20,left:60},c=e.utils.defaultColor(),h=null,p=null,d=100,v,m,g,y,b=!0,w=null,E=!0,S=function(e,t,n,r,i){return"<h3>"+e+"</h3>"+"<p>"+n+" at "+t+"</p>"},x="No Data Available.",T=d3.dispatch("tooltipShow","tooltipHide","brush"),N=250;t.clipEdge(!0),n.interactive(!1),r.orient("bottom").tickPadding(5),i.orient("left"),s.orient("bottom").tickPadding(5),o.orient("left");var C=function(n,s){var o=n.pos[0]+(s.offsetLeft||0),u=n.pos[1]+(s.offsetTop||0),a=r.tickFormat()(t.x()(n.point,n.pointIndex)),f=i.tickFormat()(t.y()(n.point,n.pointIndex)),l=S(n.series.key,a,f,n,k);e.tooltip.show([o,u],l,null,null,s)};return t.dispatch.on("elementMouseover.tooltip",function(e){e.pos=[e.pos[0]+f.left,e.pos[1]+f.top],T.tooltipShow(e)}),t.dispatch.on("elementMouseout.tooltip",function(e){T.tooltipHide(e)}),T.on("tooltipHide",function(){E&&e.tooltip.cleanup()}),k.dispatch=T,k.legend=u,k.lines=t,k.lines2=n,k.xAxis=r,k.yAxis=i,k.x2Axis=s,k.y2Axis=o,d3.rebind(k,t,"defined","isArea","size","xDomain","yDomain","xRange","yRange","forceX","forceY","interactive","clipEdge","clipVoronoi","id"),k.options=e.utils.optionsFunc.bind(k),k.x=function(e){return arguments.length?(t.x(e),n.x(e),k):t.x},k.y=function(e){return arguments.length?(t.y(e),n.y(e),k):t.y},k.margin=function(e){return arguments.length?(f.top=typeof e.top!="undefined"?e.top:f.top,f.right=typeof e.right!="undefined"?e.right:f.right,f.bottom=typeof e.bottom!="undefined"?e.bottom:f.bottom,f.left=typeof e.left!="undefined"?e.left:f.left,k):f},k.margin2=function(e){return arguments.length?(l=e,k):l},k.width=function(e){return arguments.length?(h=e,k):h},k.height=function(e){return arguments.length?(p=e,k):p},k.height2=function(e){return arguments.length?(d=e,k):d},k.color=function(t){return arguments.length?(c=e.utils.getColor(t),u.color(c),k):c},k.showLegend=function(e){return arguments.length?(b=e,k):b},k.tooltips=function(e){return arguments.length?(E=e,k):E},k.tooltipContent=function(e){return arguments.length?(S=e,k):S},k.interpolate=function(e){return arguments.length?(t.interpolate(e),n.interpolate(e),k):t.interpolate()},k.noData=function(e){return arguments.length?(x=e,k):x},k.xTickFormat=function(e){return arguments.length?(r.tickFormat(e),s.tickFormat(e),k):r.tickFormat()},k.yTickFormat=function(e){return arguments.length?(i.tickFormat(e),o.tickFormat(e),k):i.tickFormat()},k.brushExtent=function(e){return arguments.length?(w=e,k):w},k.transitionDuration=function(e){return arguments.length?(N=e,k):N},k},e.models.linePlusBarWithFocusChart=function(){"use strict";function B(e){return e.each(function(e){function nt(e){var t=+(e=="e"),n=t?1:-1,r=q/3;return"M"+.5*n+","+r+"A6,6 0 0 "+t+" "+6.5*n+","+(r+6)+"V"+(2*r-6)+"A6,6 0 0 "+t+" "+.5*n+","+2*r+"Z"+"M"+2.5*n+","+(r+8)+"V"+(2*r-8)+"M"+4.5*n+","+(r+8)+"V"+(2*r-8)}function rt(){h.empty()||h.extent(x),Z.data([h.empty()?k.domain():x]).each(function(e,t){var n=k(e[0])-k.range()[0],r=k.range()[1]-k(e[1]);d3.select(this).select(".left").attr("width",n<0?0:n),d3.select(this).select(".right").attr("x",k(e[1])).attr("width",r<0?0:r)})}function it(){x=h.empty()?null:h.extent(),S=h.empty()?k.domain():h.extent(),D.brush({extent:S,brush:h}),rt(),r.width(F).height(I).color(e.map(function(e,t){return e.color||w(e,t)}).filter(function(t,n){return!e[n].disabled&&e[n].bar})),t.width(F).height(I).color(e.map(function(e,t){return e.color||w(e,t)}).filter(function(t,n){return!e[n].disabled&&!e[n].bar}));var n=J.select(".nv-focus .nv-barsWrap").datum(U.length?U.map(function(e,t){return{key:e.key,values:e.values.filter(function(e,t){return r.x()(e,t)>=S[0]&&r.x()(e,t)<=S[1]})}}):[{values:[]}]),i=J.select(".nv-focus .nv-linesWrap").datum(z[0].disabled?[{values:[]}]:z.map(function(e,n){return{key:e.key,values:e.values.filter(function(e,n){return t.x()(e,n)>=S[0]&&t.x()(e,n)<=S[1]})}}));U.length?C=r.xScale():C=t.xScale(),s.scale(C).ticks(F/100).tickSize(-I,0),s.domain([Math.ceil(S[0]),Math.floor(S[1])]),J.select(".nv-x.nv-axis").transition().duration(P).call(s),n.transition().duration(P).call(r),i.transition().duration(P).call(t),J.select(".nv-focus .nv-x.nv-axis").attr("transform","translate(0,"+L.range()[0]+")"),u.scale(L).ticks(I/36).tickSize(-F,0),J.select(".nv-focus .nv-y1.nv-axis").style("opacity",U.length?1:0),a.scale(A).ticks(I/36).tickSize(U.length?0:-F,0),J.select(".nv-focus .nv-y2.nv-axis").style("opacity",z.length?1:0).attr("transform","translate("+C.range()[1]+",0)"),J.select(".nv-focus .nv-y1.nv-axis").transition().duration(P).call(u),J.select(".nv-focus .nv-y2.nv-axis").transition().duration(P).call(a)}var N=d3.select(this),j=this,F=(v||parseInt(N.style("width"))||960)-p.left-p.right,I=(m||parseInt(N.style("height"))||400)-p.top-p.bottom-g,q=g-d.top-d.bottom;B.update=function(){N.transition().duration(P).call(B)},B.container=this;if(!e||!e.length||!e.filter(function(e){return e.values.length}).length){var R=N.selectAll(".nv-noData").data([_]);return R.enter().append("text").attr("class","nvd3 nv-noData").attr("dy","-.7em").style("text-anchor","middle"),R.attr("x",p.left+F/2).attr("y",p.top+I/2).text(function(e){return e}),B}N.selectAll(".nv-noData").remove();var U=e.filter(function(e){return!e.disabled&&e.bar}),z=e.filter(function(e){return!e.bar});C=r.xScale(),k=o.scale(),L=r.yScale(),A=t.yScale(),O=i.yScale(),M=n.yScale();var W=e.filter(function(e){return!e.disabled&&e.bar}).map(function(e){return e.values.map(function(e,t){return{x:y(e,t),y:b(e,t)}})}),X=e.filter(function(e){return!e.disabled&&!e.bar}).map(function(e){return e.values.map(function(e,t){return{x:y(e,t),y:b(e,t)}})});C.range([0,F]),k.domain(d3.extent(d3.merge(W.concat(X)),function(e){return e.x})).range([0,F]);var V=N.selectAll("g.nv-wrap.nv-linePlusBar").data([e]),$=V.enter().append("g").attr("class","nvd3 nv-wrap nv-linePlusBar").append("g"),J=V.select("g");$.append("g").attr("class","nv-legendWrap");var K=$.append("g").attr("class","nv-focus");K.append("g").attr("class","nv-x nv-axis"),K.append("g").attr("class","nv-y1 nv-axis"),K.append("g").attr("class","nv-y2 nv-axis"),K.append("g").attr("class","nv-barsWrap"),K.append("g").attr("class","nv-linesWrap");var Q=$.append("g").attr("class","nv-context");Q.append("g").attr("class","nv-x nv-axis"),Q.append("g").attr("class","nv-y1 nv-axis"),Q.append("g").attr("class","nv-y2 nv-axis"),Q.append("g").attr("class","nv-barsWrap"),Q.append("g").attr("class","nv-linesWrap"),Q.append("g").attr("class","nv-brushBackground"),Q.append("g").attr("class","nv-x nv-brush"),E&&(c.width(F/2),J.select(".nv-legendWrap").datum(e.map(function(e){return e.originalKey=e.originalKey===undefined?e.key:e.originalKey,e.key=e.originalKey+(e.bar?" (left axis)":" (right axis)"),e})).call(c),p.top!=c.height()&&(p.top=c.height(),I=(m||parseInt(N.style("height"))||400)-p.top-p.bottom-g),J.select(".nv-legendWrap").attr("transform","translate("+F/2+","+ -p.top+")")),V.attr("transform","translate("+p.left+","+p.top+")"),i.width(F).height(q).color(e.map(function(e,t){return e.color||w(e,t)}).filter(function(t,n){return!e[n].disabled&&e[n].bar})),n.width(F).height(q).color(e.map(function(e,t){return e.color||w(e,t)}).filter(function(t,n){return!e[n].disabled&&!e[n].bar}));var G=J.select(".nv-context .nv-barsWrap").datum(U.length?U:[{values:[]}]),Y=J.select(".nv-context .nv-linesWrap").datum(z[0].disabled?[{values:[]}]:z);J.select(".nv-context").attr("transform","translate(0,"+(I+p.bottom+d.top)+")"),G.transition().call(i),Y.transition().call(n),h.x(k).on("brush",it),x&&h.extent(x);var Z=J.select(".nv-brushBackground").selectAll("g").data([x||h.extent()]),et=Z.enter().append("g");et.append("rect").attr("class","left").attr("x",0).attr("y",0).attr("height",q),et.append("rect").attr("class","right").attr("x",0).attr("y",0).attr("height",q);var tt=J.select(".nv-x.nv-brush").call(h);tt.selectAll("rect").attr("height",q),tt.selectAll(".resize").append("path").attr("d",nt),o.ticks(F/100).tickSize(-q,0),J.select(".nv-context .nv-x.nv-axis").attr("transform","translate(0,"+O.range()[0]+")"),J.select(".nv-context .nv-x.nv-axis").transition().call(o),f.scale(O).ticks(q/36).tickSize(-F,0),J.select(".nv-context .nv-y1.nv-axis").style("opacity",U.length?1:0).attr("transform","translate(0,"+k.range()[0]+")"),J.select(".nv-context .nv-y1.nv-axis").transition().call(f),l.scale(M).ticks(q/36).tickSize(U.length?0:-F,0),J.select(".nv-context .nv-y2.nv-axis").style("opacity",z.length?1:0).attr("transform","translate("+k.range()[1]+",0)"),J.select(".nv-context .nv-y2.nv-axis").transition().call(l),c.dispatch.on("stateChange",function(e){B.update()}),D.on("tooltipShow",function(e){T&&H(e,j.parentNode)}),it()}),B}var t=e.models.line(),n=e.models.line(),r=e.models.historicalBar(),i=e.models.historicalBar(),s=e.models.axis(),o=e.models.axis(),u=e.models.axis(),a=e.models.axis(),f=e.models.axis(),l=e.models.axis(),c=e.models.legend(),h=d3.svg.brush(),p={top:30,right:30,bottom:30,left:60},d={top:0,right:30,bottom:20,left:60},v=null,m=null,g=100,y=function(e){return e.x},b=function(e){return e.y},w=e.utils.defaultColor(),E=!0,S,x=null,T=!0,N=function(e,t,n,r,i){return"<h3>"+e+"</h3>"+"<p>"+n+" at "+t+"</p>"},C,k,L,A,O,M,_="No Data Available.",D=d3.dispatch("tooltipShow","tooltipHide","brush"),P=0;t.clipEdge(!0),n.interactive(!1),s.orient("bottom").tickPadding(5),u.orient("left"),a.orient("right"),o.orient("bottom").tickPadding(5),f.orient("left"),l.orient("right");var H=function(n,r){S&&(n.pointIndex+=Math.ceil(S[0]));var i=n.pos[0]+(r.offsetLeft||0),o=n.pos[1]+(r.offsetTop||0),f=s.tickFormat()(t.x()(n.point,n.pointIndex)),l=(n.series.bar?u:a).tickFormat()(t.y()(n.point,n.pointIndex)),c=N(n.series.key,f,l,n,B);e.tooltip.show([i,o],c,n.value<0?"n":"s",null,r)};return t.dispatch.on("elementMouseover.tooltip",function(e){e.pos=[e.pos[0]+p.left,e.pos[1]+p.top],D.tooltipShow(e)}),t.dispatch.on("elementMouseout.tooltip",function(e){D.tooltipHide(e)}),r.dispatch.on("elementMouseover.tooltip",function(e){e.pos=[e.pos[0]+p.left,e.pos[1]+p.top],D.tooltipShow(e)}),r.dispatch.on("elementMouseout.tooltip",function(e){D.tooltipHide(e)}),D.on("tooltipHide",function(){T&&e.tooltip.cleanup()}),B.dispatch=D,B.legend=c,B.lines=t,B.lines2=n,B.bars=r,B.bars2=i,B.xAxis=s,B.x2Axis=o,B.y1Axis=u,B.y2Axis=a,B.y3Axis=f,B.y4Axis=l,d3.rebind(B,t,"defined","size","clipVoronoi","interpolate"),B.options=e.utils.optionsFunc.bind(B),B.x=function(e){return arguments.length?(y=e,t.x(e),r.x(e),B):y},B.y=function(e){return arguments.length?(b=e,t.y(e),r.y(e),B):b},B.margin=function(e){return arguments.length?(p.top=typeof e.top!="undefined"?e.top:p.top,p.right=typeof e.right!="undefined"?e.right:p.right,p.bottom=typeof e.bottom!="undefined"?e.bottom:p.bottom,p.left=typeof e.left!="undefined"?e.left:p.left,B):p},B.width=function(e){return arguments.length?(v=e,B):v},B.height=function(e){return arguments.length?(m=e,B):m},B.color=function(t){return arguments.length?(w=e.utils.getColor(t),c.color(w),B):w},B.showLegend=function(e){return arguments.length?(E=e,B):E},B.tooltips=function(e){return arguments.length?(T=e,B):T},B.tooltipContent=function(e){return arguments.length?(N=e,B):N},B.noData=function(e){return arguments.length?(_=e,B):_},B.brushExtent=function(e){return arguments.length?(x=e,B):x},B},e.models.multiBar=function(){"use strict";function C(e){return e.each(function(e){var C=n-t.left-t.right,k=r-t.top-t.bottom,L=d3.select(this);d&&e.length&&(d=[{values:e[0].values.map(function(e){return{x:e.x,y:0,series:e.series,size:.01}})}]),c&&(e=d3.layout.stack().offset(h).values(function(e){return e.values}).y(a)(!e.length&&d?d:e)),e.forEach(function(e,t){e.values.forEach(function(e){e.series=t})}),c&&e[0].values.map(function(t,n){var r=0,i=0;e.map(function(e){var t=e.values[n];t.size=Math.abs(t.y),t.y<0?(t.y1=i,i-=t.size):(t.y1=t.size+r,r+=t.size)})});var A=y&&b?[]:e.map(function(e){return e.values.map(function(e,t){return{x:u(e,t),y:a(e,t),y0:e.y0,y1:e.y1}})});i.domain(y||d3.merge(A).map(function(e){return e.x})).rangeBands(w||[0,C],S),s.domain(b||d3.extent(d3.merge(A).map(function(e){return c?e.y>0?e.y1:e.y1+e.y:e.y}).concat(f))).range(E||[k,0]),i.domain()[0]===i.domain()[1]&&(i.domain()[0]?i.domain([i.domain()[0]-i.domain()[0]*.01,i.domain()[1]+i.domain()[1]*.01]):i.domain([-1,1])),s.domain()[0]===s.domain()[1]&&(s.domain()[0]?s.domain([s.domain()[0]+s.domain()[0]*.01,s.domain()[1]-s.domain()[1]*.01]):s.domain([-1,1])),T=T||i,N=N||s;var O=L.selectAll("g.nv-wrap.nv-multibar").data([e]),M=O.enter().append("g").attr("class","nvd3 nv-wrap nv-multibar"),_=M.append("defs"),D=M.append("g"),P=O.select("g");D.append("g").attr("class","nv-groups"),O.attr("transform","translate("+t.left+","+t.top+")"),_.append("clipPath").attr("id","nv-edge-clip-"+o).append("rect"),O.select("#nv-edge-clip-"+o+" rect").attr("width",C).attr("height",k),P.attr("clip-path",l?"url(#nv-edge-clip-"+o+")":"");var H=O.select(".nv-groups").selectAll(".nv-group").data(function(e){return e},function(e,t){return t});H.enter().append("g").style("stroke-opacity",1e-6).style("fill-opacity",1e-6),H.exit().transition().selectAll("rect.nv-bar").delay(function(t,n){return n*g/e[0].values.length}).attr("y",function(e){return c?N(e.y0):N(0)}).attr("height",0).remove(),H.attr("class",function(e,t){return"nv-group nv-series-"+t}).classed("hover",function(e){return e.hover}).style("fill",function(e,t){return p(e,t)}).style("stroke",function(e,t){return p(e,t)}),H.transition().style("stroke-opacity",1).style("fill-opacity",.75);var B=H.selectAll("rect.nv-bar").data(function(t){return d&&!e.length?d.values:t.values});B.exit().remove();var j=B.enter().append("rect").attr("class",function(e,t){return a(e,t)<0?"nv-bar negative":"nv-bar positive"}).attr("x",function(t,n,r){return c?0:r*i.rangeBand()/e.length}).attr("y",function(e){return N(c?e.y0:0)}).attr("height",0).attr("width",i.rangeBand()/(c?1:e.length)).attr("transform",function(e,t){return"translate("+i(u(e,t))+",0)"});B.style("fill",function(e,t,n){return p(e,n,t)}).style("stroke",function(e,t,n){return p(e,n,t)}).on("mouseover",function(t,n){d3.select(this).classed("hover",!0),x.elementMouseover({value:a(t,n),point:t,series:e[t.series],pos:[i(u(t,n))+i.rangeBand()*(c?e.length/2:t.series+.5)/e.length,s(a(t,n)+(c?t.y0:0))],pointIndex:n,seriesIndex:t.series,e:d3.event})}).on("mouseout",function(t,n){d3.select(this).classed("hover",!1),x.elementMouseout({value:a(t,n),point:t,series:e[t.series],pointIndex:n,seriesIndex:t.series,e:d3.event})}).on("click",function(t,n){x.elementClick({value:a(t,n),point:t,series:e[t.series],pos:[i(u(t,n))+i.rangeBand()*(c?e.length/2:t.series+.5)/e.length
4
+ ,s(a(t,n)+(c?t.y0:0))],pointIndex:n,seriesIndex:t.series,e:d3.event}),d3.event.stopPropagation()}).on("dblclick",function(t,n){x.elementDblClick({value:a(t,n),point:t,series:e[t.series],pos:[i(u(t,n))+i.rangeBand()*(c?e.length/2:t.series+.5)/e.length,s(a(t,n)+(c?t.y0:0))],pointIndex:n,seriesIndex:t.series,e:d3.event}),d3.event.stopPropagation()}),B.attr("class",function(e,t){return a(e,t)<0?"nv-bar negative":"nv-bar positive"}).transition().attr("transform",function(e,t){return"translate("+i(u(e,t))+",0)"}),v&&(m||(m=e.map(function(){return!0})),B.style("fill",function(e,t,n){return d3.rgb(v(e,t)).darker(m.map(function(e,t){return t}).filter(function(e,t){return!m[t]})[n]).toString()}).style("stroke",function(e,t,n){return d3.rgb(v(e,t)).darker(m.map(function(e,t){return t}).filter(function(e,t){return!m[t]})[n]).toString()})),c?B.transition().delay(function(t,n){return n*g/e[0].values.length}).attr("y",function(e,t){return s(c?e.y1:0)}).attr("height",function(e,t){return Math.max(Math.abs(s(e.y+(c?e.y0:0))-s(c?e.y0:0)),1)}).attr("x",function(t,n){return c?0:t.series*i.rangeBand()/e.length}).attr("width",i.rangeBand()/(c?1:e.length)):B.transition().delay(function(t,n){return n*g/e[0].values.length}).attr("x",function(t,n){return t.series*i.rangeBand()/e.length}).attr("width",i.rangeBand()/e.length).attr("y",function(e,t){return a(e,t)<0?s(0):s(0)-s(a(e,t))<1?s(0)-1:s(a(e,t))||0}).attr("height",function(e,t){return Math.max(Math.abs(s(a(e,t))-s(0)),1)||0}),T=i.copy(),N=s.copy()}),C}var t={top:0,right:0,bottom:0,left:0},n=960,r=500,i=d3.scale.ordinal(),s=d3.scale.linear(),o=Math.floor(Math.random()*1e4),u=function(e){return e.x},a=function(e){return e.y},f=[0],l=!0,c=!1,h="zero",p=e.utils.defaultColor(),d=!1,v=null,m,g=1200,y,b,w,E,S=.1,x=d3.dispatch("chartClick","elementClick","elementDblClick","elementMouseover","elementMouseout"),T,N;return C.dispatch=x,C.options=e.utils.optionsFunc.bind(C),C.x=function(e){return arguments.length?(u=e,C):u},C.y=function(e){return arguments.length?(a=e,C):a},C.margin=function(e){return arguments.length?(t.top=typeof e.top!="undefined"?e.top:t.top,t.right=typeof e.right!="undefined"?e.right:t.right,t.bottom=typeof e.bottom!="undefined"?e.bottom:t.bottom,t.left=typeof e.left!="undefined"?e.left:t.left,C):t},C.width=function(e){return arguments.length?(n=e,C):n},C.height=function(e){return arguments.length?(r=e,C):r},C.xScale=function(e){return arguments.length?(i=e,C):i},C.yScale=function(e){return arguments.length?(s=e,C):s},C.xDomain=function(e){return arguments.length?(y=e,C):y},C.yDomain=function(e){return arguments.length?(b=e,C):b},C.xRange=function(e){return arguments.length?(w=e,C):w},C.yRange=function(e){return arguments.length?(E=e,C):E},C.forceY=function(e){return arguments.length?(f=e,C):f},C.stacked=function(e){return arguments.length?(c=e,C):c},C.stackOffset=function(e){return arguments.length?(h=e,C):h},C.clipEdge=function(e){return arguments.length?(l=e,C):l},C.color=function(t){return arguments.length?(p=e.utils.getColor(t),C):p},C.barColor=function(t){return arguments.length?(v=e.utils.getColor(t),C):v},C.disabled=function(e){return arguments.length?(m=e,C):m},C.id=function(e){return arguments.length?(o=e,C):o},C.hideable=function(e){return arguments.length?(d=e,C):d},C.delay=function(e){return arguments.length?(g=e,C):g},C.groupSpacing=function(e){return arguments.length?(S=e,C):S},C},e.models.multiBarChart=function(){"use strict";function A(e){return e.each(function(e){var b=d3.select(this),O=this,M=(u||parseInt(b.style("width"))||960)-o.left-o.right,_=(a||parseInt(b.style("height"))||400)-o.top-o.bottom;A.update=function(){b.transition().duration(k).call(A)},A.container=this,S.disabled=e.map(function(e){return!!e.disabled});if(!x){var D;x={};for(D in S)S[D]instanceof Array?x[D]=S[D].slice(0):x[D]=S[D]}if(!e||!e.length||!e.filter(function(e){return e.values.length}).length){var P=b.selectAll(".nv-noData").data([T]);return P.enter().append("text").attr("class","nvd3 nv-noData").attr("dy","-.7em").style("text-anchor","middle"),P.attr("x",o.left+M/2).attr("y",o.top+_/2).text(function(e){return e}),A}b.selectAll(".nv-noData").remove(),w=t.xScale(),E=t.yScale();var H=b.selectAll("g.nv-wrap.nv-multiBarWithLegend").data([e]),B=H.enter().append("g").attr("class","nvd3 nv-wrap nv-multiBarWithLegend").append("g"),j=H.select("g");B.append("g").attr("class","nv-x nv-axis"),B.append("g").attr("class","nv-y nv-axis"),B.append("g").attr("class","nv-barsWrap"),B.append("g").attr("class","nv-legendWrap"),B.append("g").attr("class","nv-controlsWrap"),c&&(i.width(M-C()),t.barColor()&&e.forEach(function(e,t){e.color=d3.rgb("#ccc").darker(t*1.5).toString()}),j.select(".nv-legendWrap").datum(e).call(i),o.top!=i.height()&&(o.top=i.height(),_=(a||parseInt(b.style("height"))||400)-o.top-o.bottom),j.select(".nv-legendWrap").attr("transform","translate("+C()+","+ -o.top+")"));if(l){var F=[{key:"Grouped",disabled:t.stacked()},{key:"Stacked",disabled:!t.stacked()}];s.width(C()).color(["#444","#444","#444"]),j.select(".nv-controlsWrap").datum(F).attr("transform","translate(0,"+ -o.top+")").call(s)}H.attr("transform","translate("+o.left+","+o.top+")"),d&&j.select(".nv-y.nv-axis").attr("transform","translate("+M+",0)"),t.disabled(e.map(function(e){return e.disabled})).width(M).height(_).color(e.map(function(e,t){return e.color||f(e,t)}).filter(function(t,n){return!e[n].disabled}));var I=j.select(".nv-barsWrap").datum(e.filter(function(e){return!e.disabled}));I.transition().call(t);if(h){n.scale(w).ticks(M/100).tickSize(-_,0),j.select(".nv-x.nv-axis").attr("transform","translate(0,"+E.range()[0]+")"),j.select(".nv-x.nv-axis").transition().call(n);var q=j.select(".nv-x.nv-axis > g").selectAll("g");q.selectAll("line, text").style("opacity",1);if(m){var R=function(e,t){return"translate("+e+","+t+")"},U=5,z=17;q.selectAll("text").attr("transform",function(e,t,n){return R(0,n%2==0?U:z)});var W=d3.selectAll(".nv-x.nv-axis .nv-wrap g g text")[0].length;j.selectAll(".nv-x.nv-axis .nv-axisMaxMin text").attr("transform",function(e,t){return R(0,t===0||W%2!==0?z:U)})}v&&q.filter(function(t,n){return n%Math.ceil(e[0].values.length/(M/100))!==0}).selectAll("text, line").style("opacity",0),g&&q.selectAll(".tick text").attr("transform","rotate("+g+" 0,0)").style("text-anchor",g>0?"start":"end"),j.select(".nv-x.nv-axis").selectAll("g.nv-axisMaxMin text").style("opacity",1)}p&&(r.scale(E).ticks(_/36).tickSize(-M,0),j.select(".nv-y.nv-axis").transition().call(r)),i.dispatch.on("stateChange",function(e){S=e,N.stateChange(S),A.update()}),s.dispatch.on("legendClick",function(e,n){if(!e.disabled)return;F=F.map(function(e){return e.disabled=!0,e}),e.disabled=!1;switch(e.key){case"Grouped":t.stacked(!1);break;case"Stacked":t.stacked(!0)}S.stacked=t.stacked(),N.stateChange(S),A.update()}),N.on("tooltipShow",function(e){y&&L(e,O.parentNode)}),N.on("changeState",function(n){typeof n.disabled!="undefined"&&(e.forEach(function(e,t){e.disabled=n.disabled[t]}),S.disabled=n.disabled),typeof n.stacked!="undefined"&&(t.stacked(n.stacked),S.stacked=n.stacked),A.update()})}),A}var t=e.models.multiBar(),n=e.models.axis(),r=e.models.axis(),i=e.models.legend(),s=e.models.legend(),o={top:30,right:20,bottom:50,left:60},u=null,a=null,f=e.utils.defaultColor(),l=!0,c=!0,h=!0,p=!0,d=!1,v=!0,m=!1,g=0,y=!0,b=function(e,t,n,r,i){return"<h3>"+e+"</h3>"+"<p>"+n+" on "+t+"</p>"},w,E,S={stacked:!1},x=null,T="No Data Available.",N=d3.dispatch("tooltipShow","tooltipHide","stateChange","changeState"),C=function(){return l?180:0},k=250;t.stacked(!1),n.orient("bottom").tickPadding(7).highlightZero(!0).showMaxMin(!1).tickFormat(function(e){return e}),r.orient(d?"right":"left").tickFormat(d3.format(",.1f")),s.updateState(!1);var L=function(i,s){var o=i.pos[0]+(s.offsetLeft||0),u=i.pos[1]+(s.offsetTop||0),a=n.tickFormat()(t.x()(i.point,i.pointIndex)),f=r.tickFormat()(t.y()(i.point,i.pointIndex)),l=b(i.series.key,a,f,i,A);e.tooltip.show([o,u],l,i.value<0?"n":"s",null,s)};return t.dispatch.on("elementMouseover.tooltip",function(e){e.pos=[e.pos[0]+o.left,e.pos[1]+o.top],N.tooltipShow(e)}),t.dispatch.on("elementMouseout.tooltip",function(e){N.tooltipHide(e)}),N.on("tooltipHide",function(){y&&e.tooltip.cleanup()}),A.dispatch=N,A.multibar=t,A.legend=i,A.xAxis=n,A.yAxis=r,d3.rebind(A,t,"x","y","xDomain","yDomain","xRange","yRange","forceX","forceY","clipEdge","id","stacked","stackOffset","delay","barColor","groupSpacing"),A.options=e.utils.optionsFunc.bind(A),A.margin=function(e){return arguments.length?(o.top=typeof e.top!="undefined"?e.top:o.top,o.right=typeof e.right!="undefined"?e.right:o.right,o.bottom=typeof e.bottom!="undefined"?e.bottom:o.bottom,o.left=typeof e.left!="undefined"?e.left:o.left,A):o},A.width=function(e){return arguments.length?(u=e,A):u},A.height=function(e){return arguments.length?(a=e,A):a},A.color=function(t){return arguments.length?(f=e.utils.getColor(t),i.color(f),A):f},A.showControls=function(e){return arguments.length?(l=e,A):l},A.showLegend=function(e){return arguments.length?(c=e,A):c},A.showXAxis=function(e){return arguments.length?(h=e,A):h},A.showYAxis=function(e){return arguments.length?(p=e,A):p},A.rightAlignYAxis=function(e){return arguments.length?(d=e,r.orient(e?"right":"left"),A):d},A.reduceXTicks=function(e){return arguments.length?(v=e,A):v},A.rotateLabels=function(e){return arguments.length?(g=e,A):g},A.staggerLabels=function(e){return arguments.length?(m=e,A):m},A.tooltip=function(e){return arguments.length?(b=e,A):b},A.tooltips=function(e){return arguments.length?(y=e,A):y},A.tooltipContent=function(e){return arguments.length?(b=e,A):b},A.state=function(e){return arguments.length?(S=e,A):S},A.defaultState=function(e){return arguments.length?(x=e,A):x},A.noData=function(e){return arguments.length?(T=e,A):T},A.transitionDuration=function(e){return arguments.length?(k=e,A):k},A},e.models.multiBarHorizontal=function(){"use strict";function C(e){return e.each(function(e){var i=n-t.left-t.right,y=r-t.top-t.bottom,C=d3.select(this);p&&(e=d3.layout.stack().offset("zero").values(function(e){return e.values}).y(a)(e)),e.forEach(function(e,t){e.values.forEach(function(e){e.series=t})}),p&&e[0].values.map(function(t,n){var r=0,i=0;e.map(function(e){var t=e.values[n];t.size=Math.abs(t.y),t.y<0?(t.y1=i-t.size,i-=t.size):(t.y1=r,r+=t.size)})});var k=b&&w?[]:e.map(function(e){return e.values.map(function(e,t){return{x:u(e,t),y:a(e,t),y0:e.y0,y1:e.y1}})});s.domain(b||d3.merge(k).map(function(e){return e.x})).rangeBands(E||[0,y],.1),o.domain(w||d3.extent(d3.merge(k).map(function(e){return p?e.y>0?e.y1+e.y:e.y1:e.y}).concat(f))),d&&!p?o.range(S||[o.domain()[0]<0?m:0,i-(o.domain()[1]>0?m:0)]):o.range(S||[0,i]),T=T||s,N=N||d3.scale.linear().domain(o.domain()).range([o(0),o(0)]);var L=d3.select(this).selectAll("g.nv-wrap.nv-multibarHorizontal").data([e]),A=L.enter().append("g").attr("class","nvd3 nv-wrap nv-multibarHorizontal"),O=A.append("defs"),M=A.append("g"),_=L.select("g");M.append("g").attr("class","nv-groups"),L.attr("transform","translate("+t.left+","+t.top+")");var D=L.select(".nv-groups").selectAll(".nv-group").data(function(e){return e},function(e,t){return t});D.enter().append("g").style("stroke-opacity",1e-6).style("fill-opacity",1e-6),D.exit().transition().style("stroke-opacity",1e-6).style("fill-opacity",1e-6).remove(),D.attr("class",function(e,t){return"nv-group nv-series-"+t}).classed("hover",function(e){return e.hover}).style("fill",function(e,t){return l(e,t)}).style("stroke",function(e,t){return l(e,t)}),D.transition().style("stroke-opacity",1).style("fill-opacity",.75);var P=D.selectAll("g.nv-bar").data(function(e){return e.values});P.exit().remove();var H=P.enter().append("g").attr("transform",function(t,n,r){return"translate("+N(p?t.y0:0)+","+(p?0:r*s.rangeBand()/e.length+s(u(t,n)))+")"});H.append("rect").attr("width",0).attr("height",s.rangeBand()/(p?1:e.length)),P.on("mouseover",function(t,n){d3.select(this).classed("hover",!0),x.elementMouseover({value:a(t,n),point:t,series:e[t.series],pos:[o(a(t,n)+(p?t.y0:0)),s(u(t,n))+s.rangeBand()*(p?e.length/2:t.series+.5)/e.length],pointIndex:n,seriesIndex:t.series,e:d3.event})}).on("mouseout",function(t,n){d3.select(this).classed("hover",!1),x.elementMouseout({value:a(t,n),point:t,series:e[t.series],pointIndex:n,seriesIndex:t.series,e:d3.event})}).on("click",function(t,n){x.elementClick({value:a(t,n),point:t,series:e[t.series],pos:[s(u(t,n))+s.rangeBand()*(p?e.length/2:t.series+.5)/e.length,o(a(t,n)+(p?t.y0:0))],pointIndex:n,seriesIndex:t.series,e:d3.event}),d3.event.stopPropagation()}).on("dblclick",function(t,n){x.elementDblClick({value:a(t,n),point:t,series:e[t.series],pos:[s(u(t,n))+s.rangeBand()*(p?e.length/2:t.series+.5)/e.length,o(a(t,n)+(p?t.y0:0))],pointIndex:n,seriesIndex:t.series,e:d3.event}),d3.event.stopPropagation()}),H.append("text"),d&&!p?(P.select("text").attr("text-anchor",function(e,t){return a(e,t)<0?"end":"start"}).attr("y",s.rangeBand()/(e.length*2)).attr("dy",".32em").text(function(e,t){return g(a(e,t))}),P.transition().select("text").attr("x",function(e,t){return a(e,t)<0?-4:o(a(e,t))-o(0)+4})):P.selectAll("text").text(""),v&&!p?(H.append("text").classed("nv-bar-label",!0),P.select("text.nv-bar-label").attr("text-anchor",function(e,t){return a(e,t)<0?"start":"end"}).attr("y",s.rangeBand()/(e.length*2)).attr("dy",".32em").text(function(e,t){return u(e,t)}),P.transition().select("text.nv-bar-label").attr("x",function(e,t){return a(e,t)<0?o(0)-o(a(e,t))+4:-4})):P.selectAll("text.nv-bar-label").text(""),P.attr("class",function(e,t){return a(e,t)<0?"nv-bar negative":"nv-bar positive"}),c&&(h||(h=e.map(function(){return!0})),P.style("fill",function(e,t,n){return d3.rgb(c(e,t)).darker(h.map(function(e,t){return t}).filter(function(e,t){return!h[t]})[n]).toString()}).style("stroke",function(e,t,n){return d3.rgb(c(e,t)).darker(h.map(function(e,t){return t}).filter(function(e,t){return!h[t]})[n]).toString()})),p?P.transition().attr("transform",function(e,t){return"translate("+o(e.y1)+","+s(u(e,t))+")"}).select("rect").attr("width",function(e,t){return Math.abs(o(a(e,t)+e.y0)-o(e.y0))}).attr("height",s.rangeBand()):P.transition().attr("transform",function(t,n){return"translate("+(a(t,n)<0?o(a(t,n)):o(0))+","+(t.series*s.rangeBand()/e.length+s(u(t,n)))+")"}).select("rect").attr("height",s.rangeBand()/e.length).attr("width",function(e,t){return Math.max(Math.abs(o(a(e,t))-o(0)),1)}),T=s.copy(),N=o.copy()}),C}var t={top:0,right:0,bottom:0,left:0},n=960,r=500,i=Math.floor(Math.random()*1e4),s=d3.scale.ordinal(),o=d3.scale.linear(),u=function(e){return e.x},a=function(e){return e.y},f=[0],l=e.utils.defaultColor(),c=null,h,p=!1,d=!1,v=!1,m=60,g=d3.format(",.2f"),y=1200,b,w,E,S,x=d3.dispatch("chartClick","elementClick","elementDblClick","elementMouseover","elementMouseout"),T,N;return C.dispatch=x,C.options=e.utils.optionsFunc.bind(C),C.x=function(e){return arguments.length?(u=e,C):u},C.y=function(e){return arguments.length?(a=e,C):a},C.margin=function(e){return arguments.length?(t.top=typeof e.top!="undefined"?e.top:t.top,t.right=typeof e.right!="undefined"?e.right:t.right,t.bottom=typeof e.bottom!="undefined"?e.bottom:t.bottom,t.left=typeof e.left!="undefined"?e.left:t.left,C):t},C.width=function(e){return arguments.length?(n=e,C):n},C.height=function(e){return arguments.length?(r=e,C):r},C.xScale=function(e){return arguments.length?(s=e,C):s},C.yScale=function(e){return arguments.length?(o=e,C):o},C.xDomain=function(e){return arguments.length?(b=e,C):b},C.yDomain=function(e){return arguments.length?(w=e,C):w},C.xRange=function(e){return arguments.length?(E=e,C):E},C.yRange=function(e){return arguments.length?(S=e,C):S},C.forceY=function(e){return arguments.length?(f=e,C):f},C.stacked=function(e){return arguments.length?(p=e,C):p},C.color=function(t){return arguments.length?(l=e.utils.getColor(t),C):l},C.barColor=function(t){return arguments.length?(c=e.utils.getColor(t),C):c},C.disabled=function(e){return arguments.length?(h=e,C):h},C.id=function(e){return arguments.length?(i=e,C):i},C.delay=function(e){return arguments.length?(y=e,C):y},C.showValues=function(e){return arguments.length?(d=e,C):d},C.showBarLabels=function(e){return arguments.length?(v=e,C):v},C.valueFormat=function(e){return arguments.length?(g=e,C):g},C.valuePadding=function(e){return arguments.length?(m=e,C):m},C},e.models.multiBarHorizontalChart=function(){"use strict";function C(e){return e.each(function(e){var d=d3.select(this),m=this,k=(u||parseInt(d.style("width"))||960)-o.left-o.right,L=(a||parseInt(d.style("height"))||400)-o.top-o.bottom;C.update=function(){d.transition().duration(T).call(C)},C.container=this,b.disabled=e.map(function(e){return!!e.disabled});if(!w){var A;w={};for(A in b)b[A]instanceof Array?w[A]=b[A].slice(0):w[A]=b[A]}if(!e||!e.length||!e.filter(function(e){return e.values.length}).length){var O=d.selectAll(".nv-noData").data([E]);return O.enter().append("text").attr("class","nvd3 nv-noData").attr("dy","-.7em").style("text-anchor","middle"),O.attr("x",o.left+k/2).attr("y",o.top+L/2).text(function(e){return e}),C}d.selectAll(".nv-noData").remove(),g=t.xScale(),y=t.yScale();var M=d.selectAll("g.nv-wrap.nv-multiBarHorizontalChart").data([e]),_=M.enter().append("g").attr("class","nvd3 nv-wrap nv-multiBarHorizontalChart").append("g"),D=M.select("g");_.append("g").attr("class","nv-x nv-axis"),_.append("g").attr("class","nv-y nv-axis").append("g").attr("class","nv-zeroLine").append("line"),_.append("g").attr("class","nv-barsWrap"),_.append("g").attr("class","nv-legendWrap"),_.append("g").attr("class","nv-controlsWrap"),c&&(i.width(k-x()),t.barColor()&&e.forEach(function(e,t){e.color=d3.rgb("#ccc").darker(t*1.5).toString()}),D.select(".nv-legendWrap").datum(e).call(i),o.top!=i.height()&&(o.top=i.height(),L=(a||parseInt(d.style("height"))||400)-o.top-o.bottom),D.select(".nv-legendWrap").attr("transform","translate("+x()+","+ -o.top+")"));if(l){var P=[{key:"Grouped",disabled:t.stacked()},{key:"Stacked",disabled:!t.stacked()}];s.width(x()).color(["#444","#444","#444"]),D.select(".nv-controlsWrap").datum(P).attr("transform","translate(0,"+ -o.top+")").call(s)}M.attr("transform","translate("+o.left+","+o.top+")"),t.disabled(e.map(function(e){return e.disabled})).width(k).height(L).color(e.map(function(e,t){return e.color||f(e,t)}).filter(function(t,n){return!e[n].disabled}));var H=D.select(".nv-barsWrap").datum(e.filter(function(e){return!e.disabled}));H.transition().call(t);if(h){n.scale(g).ticks(L/24).tickSize(-k,0),D.select(".nv-x.nv-axis").transition().call(n);var B=D.select(".nv-x.nv-axis").selectAll("g");B.selectAll("line, text")}p&&(r.scale(y).ticks(k/100).tickSize(-L,0),D.select(".nv-y.nv-axis").attr("transform","translate(0,"+L+")"),D.select(".nv-y.nv-axis").transition().call(r)),D.select(".nv-zeroLine line").attr("x1",y(0)).attr("x2",y(0)).attr("y1",0).attr("y2",-L),i.dispatch.on("stateChange",function(e){b=e,S.stateChange(b),C.update()}),s.dispatch.on("legendClick",function(e,n){if(!e.disabled)return;P=P.map(function(e){return e.disabled=!0,e}),e.disabled=!1;switch(e.key){case"Grouped":t.stacked(!1);break;case"Stacked":t.stacked(!0)}b.stacked=t.stacked(),S.stateChange(b),C.update()}),S.on("tooltipShow",function(e){v&&N(e,m.parentNode)}),S.on("changeState",function(n){typeof n.disabled!="undefined"&&(e.forEach(function(e,t){e.disabled=n.disabled[t]}),b.disabled=n.disabled),typeof n.stacked!="undefined"&&(t.stacked(n.stacked),b.stacked=n.stacked),C.update()})}),C}var t=e.models.multiBarHorizontal(),n=e.models.axis(),r=e.models.axis(),i=e.models.legend().height(30),s=e.models.legend().height(30),o={top:30,right:20,bottom:50,left:60},u=null,a=null,f=e.utils.defaultColor(),l=!0,c=!0,h=!0,p=!0,d=!1,v=!0,m=function(e,t,n,r,i){return"<h3>"+e+" - "+t+"</h3>"+"<p>"+n+"</p>"},g,y,b={stacked:d},w=null,E="No Data Available.",S=d3.dispatch("tooltipShow","tooltipHide","stateChange","changeState"),x=function(){return l?180:0},T=250;t.stacked(d),n.orient("left").tickPadding(5).highlightZero(!1).showMaxMin(!1).tickFormat(function(e){return e}),r.orient("bottom").tickFormat(d3.format(",.1f")),s.updateState(!1);var N=function(i,s){var o=i.pos[0]+(s.offsetLeft||0),u=i.pos[1]+(s.offsetTop||0),a=n.tickFormat()(t.x()(i.point,i.pointIndex)),f=r.tickFormat()(t.y()(i.point,i.pointIndex)),l=m(i.series.key,a,f,i,C);e.tooltip.show([o,u],l,i.value<0?"e":"w",null,s)};return t.dispatch.on("elementMouseover.tooltip",function(e){e.pos=[e.pos[0]+o.left,e.pos[1]+o.top],S.tooltipShow(e)}),t.dispatch.on("elementMouseout.tooltip",function(e){S.tooltipHide(e)}),S.on("tooltipHide",function(){v&&e.tooltip.cleanup()}),C.dispatch=S,C.multibar=t,C.legend=i,C.xAxis=n,C.yAxis=r,d3.rebind(C,t,"x","y","xDomain","yDomain","xRange","yRange","forceX","forceY","clipEdge","id","delay","showValues","showBarLabels","valueFormat","stacked","barColor"),C.options=e.utils.optionsFunc.bind(C),C.margin=function(e){return arguments.length?(o.top=typeof e.top!="undefined"?e.top:o.top,o.right=typeof e.right!="undefined"?e.right:o.right,o.bottom=typeof e.bottom!="undefined"?e.bottom:o.bottom,o.left=typeof e.left!="undefined"?e.left:o.left,C):o},C.width=function(e){return arguments.length?(u=e,C):u},C.height=function(e){return arguments.length?(a=e,C):a},C.color=function(t){return arguments.length?(f=e.utils.getColor(t),i.color(f),C):f},C.showControls=function(e){return arguments.length?(l=e,C):l},C.showLegend=function(e){return arguments.length?(c=e,C):c},C.showXAxis=function(e){return arguments.length?(h=e,C):h},C.showYAxis=function(e){return arguments.length?(p=e,C):p},C.tooltip=function(e){return arguments.length?(m=e,C):m},C.tooltips=function(e){return arguments.length?(v=e,C):v},C.tooltipContent=function(e){return arguments.length?(m=e,C):m},C.state=function(e){return arguments.length?(b=e,C):b},C.defaultState=function(e){return arguments.length?(w=e,C):w},C.noData=function(e){return arguments.length?(E=e,C):E},C.transitionDuration=function(e){return arguments.length?(T=e,C):T},C},e.models.multiChart=function(){"use strict";function C(e){return e.each(function(e){var u=d3.select(this),f=this;C.update=function(){u.transition().call(C)},C.container=this;var k=(r||parseInt(u.style("width"))||960)-t.left-t.right,L=(i||parseInt(u.style("height"))||400)-t.top-t.bottom,A=e.filter(function(e){return!e.disabled&&e.type=="line"&&e.yAxis==1}),O=e.filter(function(e){return!e.disabled&&e.type=="line"&&e.yAxis==2}),M=e.filter(function(e){return!e.disabled&&e.type=="bar"&&e.yAxis==1}),_=e.filter(function(e){return!e.disabled&&e.type=="bar"&&e.yAxis==2}),D=e.filter(function(e){return!e.disabled&&e.type=="area"&&e.yAxis==1}),P=e.filter(function(e){return!e.disabled&&e.type=="area"&&e.yAxis==2}),H=e.filter(function(e){return!e.disabled&&e.yAxis==1}).map(function(e){return e.values.map(function(e,t){return{x:e.x,y:e.y}})}),B=e.filter(function(e){return!e.disabled&&e.yAxis==2}).map(function(e){return e.values.map(function(e,t){return{x:e.x,y:e.y}})});a.domain(d3.extent(d3.merge(H.concat(B)),function(e){return e.x})).range([0,k]);var j=u.selectAll("g.wrap.multiChart").data([e]),F=j.enter().append("g").attr("class","wrap nvd3 multiChart").append("g");F.append("g").attr("class","x axis"),F.append("g").attr("class","y1 axis"),F.append("g").attr("class","y2 axis"),F.append("g").attr("class","lines1Wrap"),F.append("g").attr("class","lines2Wrap"),F.append("g").attr("class","bars1Wrap"),F.append("g").attr("class","bars2Wrap"),F.append("g").attr("class","stack1Wrap"),F.append("g").attr("class","stack2Wrap"),F.append("g").attr("class","legendWrap");var I=j.select("g");s&&(x.width(k/2),I.select(".legendWrap").datum(e.map(function(e){return e.originalKey=e.originalKey===undefined?e.key:e.originalKey,e.key=e.originalKey+(e.yAxis==1?"":" (right axis)"),e})).call(x),t.top!=x.height()&&(t.top=x.height(),L=(i||parseInt(u.style("height"))||400)-t.top-t.bottom),I.select(".legendWrap").attr("transform","translate("+k/2+","+ -t.top+")")),d.width(k).height(L).interpolate("monotone").color(e.map(function(e,t){return e.color||n[t%n.length]}).filter(function(t,n){return!e[n].disabled&&e[n].yAxis==1&&e[n].type=="line"})),v.width(k).height(L).interpolate("monotone").color(e.map(function(e,t){return e.color||n[t%n.length]}).filter(function(t,n){return!e[n].disabled&&e[n].yAxis==2&&e[n].type=="line"})),m.width(k).height(L).color(e.map(function(e,t){return e.color||n[t%n.length]}).filter(function(t,n){return!e[n].disabled&&e[n].yAxis==1&&e[n].type=="bar"})),g.width(k).height(L).color(e.map(function(e,t){return e.color||n[t%n.length]}).filter(function(t,n){return!e[n].disabled&&e[n].yAxis==2&&e[n].type=="bar"})),y.width(k).height(L).color(e.map(function(e,t){return e.color||n[t%n.length]}).filter(function(t,n){return!e[n].disabled&&e[n].yAxis==1&&e[n].type=="area"})),b.width(k).height(L).color(e.map(function(e,t){return e.color||n[t%n.length]}).filter(function(t,n){return!e[n].disabled&&e[n].yAxis==2&&e[n].type=="area"})),I.attr("transform","translate("+t.left+","+t.top+")");var q=I.select(".lines1Wrap").datum(A),R=I.select(".bars1Wrap").datum(M),U=I.select(".stack1Wrap").datum(D),z=I.select(".lines2Wrap").datum(O),W=I.select(".bars2Wrap").datum(_),X=I.select(".stack2Wrap").datum(P),V=D.length?D.map(function(e){return e.values}).reduce(function(e,t){return e.map(function(e,n){return{x:e.x,y:e.y+t[n].y}})}).concat([{x:0,y:0}]):[],$=P.length?P.map(function(e){return e.values}).reduce(function(e,t){return e.map(function(e,n){return{x:e.x,y:e.y+t[n].y}})}).concat([{x:0,y:0}]):[];h.domain(l||d3.extent(d3.merge(H).concat(V),function(e){return e.y})).range([0,L]),p.domain(c||d3.extent(d3.merge(B).concat($),function(e){return e.y})).range([0,L]),d.yDomain(h.domain()),m.yDomain(h.domain()),y.yDomain(h.domain()),v.yDomain(p.domain()),g.yDomain(p.domain()),b.yDomain(p.domain()),D.length&&d3.transition(U).call(y),P.length&&d3.transition(X).call(b),M.length&&d3.transition(R).call(m),_.length&&d3.transition(W).call(g),A.length&&d3.transition(q).call(d),O.length&&d3.transition(z).call(v),w.ticks(k/100).tickSize(-L,0),I.select(".x.axis").attr("transform","translate(0,"+L+")"),d3.transition(I.select(".x.axis")).call(w),E.ticks(L/36).tickSize(-k,0),d3.transition(I.select(".y1.axis")).call(E),S.ticks(L/36).tickSize(-k,0),d3.transition(I.select(".y2.axis")).call(S),I.select(".y2.axis").style("opacity",B.length?1:0).attr("transform","translate("+a.range()[1]+",0)"),x.dispatch.on("stateChange",function(e){C.update()}),T.on("tooltipShow",function(e){o&&N(e,f.parentNode)})}),C}var t={top:30,right:20,bottom:50,left:60},n=d3.scale.category20().range(),r=null,i=null,s=!0,o=!0,u=function(e,t,n,r,i){return"<h3>"+e+"</h3>"+"<p>"+n+" at "+t+"</p>"},a,f,l,c,a=d3.scale.linear(),h=d3.scale.linear(),p=d3.scale.linear(),d=e.models.line().yScale(h),v=e.models.line().yScale(p),m=e.models.multiBar().stacked(!1).yScale(h),g=e.models.multiBar().stacked(!1).yScale(p),y=e.models.stackedArea().yScale(h),b=e.models.stackedArea().yScale(p),w=e.models.axis().scale(a).orient("bottom").tickPadding(5),E=e.models.axis().scale(h).orient("left"),S=e.models.axis().scale(p).orient("right"),x=e.models.legend().height(30),T=d3.dispatch("tooltipShow","tooltipHide"),N=function(t,n){var r=t.pos[0]+(n.offsetLeft||0),i=t.pos[1]+(n.offsetTop||0),s=w.tickFormat()(d.x()(t.point,t.pointIndex)),o=(t.series.yAxis==2?S:E).tickFormat()(d.y()(t.point,t.pointIndex)),a=u(t.series.key,s,o,t,C);e.tooltip.show([r,i],a,undefined,undefined,n.offsetParent)};return d.dispatch.on("elementMouseover.tooltip",function(e){e.pos=[e.pos[0]+t.left,e.pos[1]+t.top],T.tooltipShow(e)}),d.dispatch.on("elementMouseout.tooltip",function(e){T.tooltipHide(e)}),v.dispatch.on("elementMouseover.tooltip",function(e){e.pos=[e.pos[0]+t.left,e.pos[1]+t.top],T.tooltipShow(e)}),v.dispatch.on("elementMouseout.tooltip",function(e){T.tooltipHide(e)}),m.dispatch.on("elementMouseover.tooltip",function(e){e.pos=[e.pos[0]+t.left,e.pos[1]+t.top],T.tooltipShow(e)}),m.dispatch.on("elementMouseout.tooltip",function(e){T.tooltipHide(e)}),g.dispatch.on("elementMouseover.tooltip",function(e){e.pos=[e.pos[0]+t.left,e.pos[1]+t.top],T.tooltipShow(e)}),g.dispatch.on("elementMouseout.tooltip",function(e){T.tooltipHide(e)}),y.dispatch.on("tooltipShow",function(e){if(!Math.round(y.y()(e.point)*100))return setTimeout(function(){d3.selectAll(".point.hover").classed("hover",!1)},0),!1;e.pos=[e.pos[0]+t.left,e.pos[1]+t.top],T.tooltipShow(e)}),y.dispatch.on("tooltipHide",function(e){T.tooltipHide(e)}),b.dispatch.on("tooltipShow",function(e){if(!Math.round(b.y()(e.point)*100))return setTimeout(function(){d3.selectAll(".point.hover").classed("hover",!1)},0),!1;e.pos=[e.pos[0]+t.left,e.pos[1]+t.top],T.tooltipShow(e)}),b.dispatch.on("tooltipHide",function(e){T.tooltipHide(e)}),d.dispatch.on("elementMouseover.tooltip",function(e){e.pos=[e.pos[0]+t.left,e.pos[1]+t.top],T.tooltipShow(e)}),d.dispatch.on("elementMouseout.tooltip",function(e){T.tooltipHide(e)}),v.dispatch.on("elementMouseover.tooltip",function(e){e.pos=[e.pos[0]+t.left,e.pos[1]+t.top],T.tooltipShow(e)}),v.dispatch.on("elementMouseout.tooltip",function(e){T.tooltipHide(e)}),T.on("tooltipHide",function(){o&&e.tooltip.cleanup()}),C.dispatch=T,C.lines1=d,C.lines2=v,C.bars1=m,C.bars2=g,C.stack1=y,C.stack2=b,C.xAxis=w,C.yAxis1=E,C.yAxis2=S,C.options=e.utils.optionsFunc.bind(C),C.x=function(e){return arguments.length?(getX=e,d.x(e),m.x(e),C):getX},C.y=function(e){return arguments.length?(getY=e,d.y(e),m.y(e),C):getY},C.yDomain1=function(e){return arguments.length?(l=e,C):l},C.yDomain2=function(e){return arguments.length?(c=e,C):c},C.margin=function(e){return arguments.length?(t=e,C):t},C.width=function(e){return arguments.length?(r=e,C):r},C.height=function(e){return arguments.length?(i=e,C):i},C.color=function(e){return arguments.length?(n=e,x.color(e),C):n},C.showLegend=function(e){return arguments.length?(s=e,C):s},C.tooltips=function(e){return arguments.length?(o=e,C):o},C.tooltipContent=function(e){return arguments.length?(u=e,C):u},C},e.models.ohlcBar=function(){"use strict";function x(e){return e.each(function(e){var g=n-t.left-t.right,x=r-t.top-t.bottom,T=d3.select(this);s.domain(y||d3.extent(e[0].values.map(u).concat(p))),v?s.range(w||[g*.5/e[0].values.length,g*(e[0].values.length-.5)/e[0].values.length]):s.range(w||[0,g]),o.domain(b||[d3.min(e[0].values.map(h).concat(d)),d3.max(e[0].values.map(c).concat(d))]).range(E||[x,0]),s.domain()[0]===s.domain()[1]&&(s.domain()[0]?s.domain([s.domain()[0]-s.domain()[0]*.01,s.domain()[1]+s.domain()[1]*.01]):s.domain([-1,1])),o.domain()[0]===o.domain()[1]&&(o.domain()[0]?o.domain([o.domain()[0]+o.domain()[0]*.01,o.domain()[1]-o.domain()[1]*.01]):o.domain([-1,1]));var N=d3.select(this).selectAll("g.nv-wrap.nv-ohlcBar").data([e[0].values]),C=N.enter().append("g").attr("class","nvd3 nv-wrap nv-ohlcBar"),k=C.append("defs"),L=C.append("g"),A=N.select("g");L.append("g").attr("class","nv-ticks"),N.attr("transform","translate("+t.left+","+t.top+")"),T.on("click",function(e,t){S.chartClick({data:e,index:t,pos:d3.event,id:i})}),k.append("clipPath").attr("id","nv-chart-clip-path-"+i).append("rect"),N.select("#nv-chart-clip-path-"+i+" rect").attr("width",g).attr("height",x),A.attr("clip-path",m?"url(#nv-chart-clip-path-"+i+")":"");var O=N.select(".nv-ticks").selectAll(".nv-tick").data(function(e){return e});O.exit().remove();var M=O.enter().append("path").attr("class",function(e,t,n){return(f(e,t)>l(e,t)?"nv-tick negative":"nv-tick positive")+" nv-tick-"+n+"-"+t}).attr("d",function(t,n){var r=g/e[0].values.length*.9;return"m0,0l0,"+(o(f(t,n))-o(c(t,n)))+"l"+ -r/2+",0l"+r/2+",0l0,"+(o(h(t,n))-o(f(t,n)))+"l0,"+(o(l(t,n))-o(h(t,n)))+"l"+r/2+",0l"+ -r/2+",0z"}).attr("transform",function(e,t){return"translate("+s(u(e,t))+","+o(c(e,t))+")"}).on("mouseover",function(t,n){d3.select(this).classed("hover",!0),S.elementMouseover({point:t,series:e[0],pos:[s(u(t,n)),o(a(t,n))],pointIndex:n,seriesIndex:0,e:d3.event})}).on("mouseout",function(t,n){d3.select(this).classed("hover",!1),S.elementMouseout({point:t,series:e[0],pointIndex:n,seriesIndex:0,e:d3.event})}).on("click",function(e,t){S.elementClick({value:a(e,t),data:e,index:t,pos:[s(u(e,t)),o(a(e,t))],e:d3.event,id:i}),d3.event.stopPropagation()}).on("dblclick",function(e,t){S.elementDblClick({value:a(e,t),data:e,index:t,pos:[s(u(e,t)),o(a(e,t))],e:d3.event,id:i}),d3.event.stopPropagation()});O.attr("class",function(e,t,n){return(f(e,t)>l(e,t)?"nv-tick negative":"nv-tick positive")+" nv-tick-"+n+"-"+t}),d3.transition(O).attr("transform",function(e,t){return"translate("+s(u(e,t))+","+o(c(e,t))+")"}).attr("d",function(t,n){var r=g/e[0].values.length*.9;return"m0,0l0,"+(o(f(t,n))-o(c(t,n)))+"l"+ -r/2+",0l"+r/2+",0l0,"+(o(h(t,n))-o(f(t,n)))+"l0,"+(o(l(t,n))-o(h(t,n)))+"l"+r/2+",0l"+ -r/2+",0z"})}),x}var t={top:0
5
+ ,right:0,bottom:0,left:0},n=960,r=500,i=Math.floor(Math.random()*1e4),s=d3.scale.linear(),o=d3.scale.linear(),u=function(e){return e.x},a=function(e){return e.y},f=function(e){return e.open},l=function(e){return e.close},c=function(e){return e.high},h=function(e){return e.low},p=[],d=[],v=!1,m=!0,g=e.utils.defaultColor(),y,b,w,E,S=d3.dispatch("chartClick","elementClick","elementDblClick","elementMouseover","elementMouseout");return x.dispatch=S,x.options=e.utils.optionsFunc.bind(x),x.x=function(e){return arguments.length?(u=e,x):u},x.y=function(e){return arguments.length?(a=e,x):a},x.open=function(e){return arguments.length?(f=e,x):f},x.close=function(e){return arguments.length?(l=e,x):l},x.high=function(e){return arguments.length?(c=e,x):c},x.low=function(e){return arguments.length?(h=e,x):h},x.margin=function(e){return arguments.length?(t.top=typeof e.top!="undefined"?e.top:t.top,t.right=typeof e.right!="undefined"?e.right:t.right,t.bottom=typeof e.bottom!="undefined"?e.bottom:t.bottom,t.left=typeof e.left!="undefined"?e.left:t.left,x):t},x.width=function(e){return arguments.length?(n=e,x):n},x.height=function(e){return arguments.length?(r=e,x):r},x.xScale=function(e){return arguments.length?(s=e,x):s},x.yScale=function(e){return arguments.length?(o=e,x):o},x.xDomain=function(e){return arguments.length?(y=e,x):y},x.yDomain=function(e){return arguments.length?(b=e,x):b},x.xRange=function(e){return arguments.length?(w=e,x):w},x.yRange=function(e){return arguments.length?(E=e,x):E},x.forceX=function(e){return arguments.length?(p=e,x):p},x.forceY=function(e){return arguments.length?(d=e,x):d},x.padData=function(e){return arguments.length?(v=e,x):v},x.clipEdge=function(e){return arguments.length?(m=e,x):m},x.color=function(t){return arguments.length?(g=e.utils.getColor(t),x):g},x.id=function(e){return arguments.length?(i=e,x):i},x},e.models.pie=function(){"use strict";function S(e){return e.each(function(e){function q(e){var t=(e.startAngle+e.endAngle)*90/Math.PI-90;return t>90?t-180:t}function R(e){e.endAngle=isNaN(e.endAngle)?0:e.endAngle,e.startAngle=isNaN(e.startAngle)?0:e.startAngle,m||(e.innerRadius=0);var t=d3.interpolate(this._current,e);return this._current=t(0),function(e){return A(t(e))}}function U(e){e.innerRadius=0;var t=d3.interpolate({startAngle:0,endAngle:0},e);return function(e){return A(t(e))}}var o=n-t.left-t.right,f=r-t.top-t.bottom,S=Math.min(o,f)/2,x=S-S/5,T=d3.select(this),N=T.selectAll(".nv-wrap.nv-pie").data(e),C=N.enter().append("g").attr("class","nvd3 nv-wrap nv-pie nv-chart-"+u),k=C.append("g"),L=N.select("g");k.append("g").attr("class","nv-pie"),k.append("g").attr("class","nv-pieLabels"),N.attr("transform","translate("+t.left+","+t.top+")"),L.select(".nv-pie").attr("transform","translate("+o/2+","+f/2+")"),L.select(".nv-pieLabels").attr("transform","translate("+o/2+","+f/2+")"),T.on("click",function(e,t){E.chartClick({data:e,index:t,pos:d3.event,id:u})});var A=d3.svg.arc().outerRadius(x);y&&A.startAngle(y),b&&A.endAngle(b),m&&A.innerRadius(S*w);var O=d3.layout.pie().sort(null).value(function(e){return e.disabled?0:s(e)}),M=N.select(".nv-pie").selectAll(".nv-slice").data(O),_=N.select(".nv-pieLabels").selectAll(".nv-label").data(O);M.exit().remove(),_.exit().remove();var D=M.enter().append("g").attr("class","nv-slice").on("mouseover",function(e,t){d3.select(this).classed("hover",!0),E.elementMouseover({label:i(e.data),value:s(e.data),point:e.data,pointIndex:t,pos:[d3.event.pageX,d3.event.pageY],id:u})}).on("mouseout",function(e,t){d3.select(this).classed("hover",!1),E.elementMouseout({label:i(e.data),value:s(e.data),point:e.data,index:t,id:u})}).on("click",function(e,t){E.elementClick({label:i(e.data),value:s(e.data),point:e.data,index:t,pos:d3.event,id:u}),d3.event.stopPropagation()}).on("dblclick",function(e,t){E.elementDblClick({label:i(e.data),value:s(e.data),point:e.data,index:t,pos:d3.event,id:u}),d3.event.stopPropagation()});M.attr("fill",function(e,t){return a(e,t)}).attr("stroke",function(e,t){return a(e,t)});var P=D.append("path").each(function(e){this._current=e});M.select("path").transition().attr("d",A).attrTween("d",R);if(l){var H=d3.svg.arc().innerRadius(0);c&&(H=A),h&&(H=d3.svg.arc().outerRadius(A.outerRadius())),_.enter().append("g").classed("nv-label",!0).each(function(e,t){var n=d3.select(this);n.attr("transform",function(e){if(g){e.outerRadius=x+10,e.innerRadius=x+15;var t=(e.startAngle+e.endAngle)/2*(180/Math.PI);return(e.startAngle+e.endAngle)/2<Math.PI?t-=90:t+=90,"translate("+H.centroid(e)+") rotate("+t+")"}return e.outerRadius=S+10,e.innerRadius=S+15,"translate("+H.centroid(e)+")"}),n.append("rect").style("stroke","#fff").style("fill","#fff").attr("rx",3).attr("ry",3),n.append("text").style("text-anchor",g?(e.startAngle+e.endAngle)/2<Math.PI?"start":"end":"middle").style("fill","#000")});var B={},j=14,F=140,I=function(e){return Math.floor(e[0]/F)*F+","+Math.floor(e[1]/j)*j};_.transition().attr("transform",function(e){if(g){e.outerRadius=x+10,e.innerRadius=x+15;var t=(e.startAngle+e.endAngle)/2*(180/Math.PI);return(e.startAngle+e.endAngle)/2<Math.PI?t-=90:t+=90,"translate("+H.centroid(e)+") rotate("+t+")"}e.outerRadius=S+10,e.innerRadius=S+15;var n=H.centroid(e),r=I(n);return B[r]&&(n[1]-=j),B[I(n)]=!0,"translate("+n+")"}),_.select(".nv-label text").style("text-anchor",g?(d.startAngle+d.endAngle)/2<Math.PI?"start":"end":"middle").text(function(e,t){var n=(e.endAngle-e.startAngle)/(2*Math.PI),r={key:i(e.data),value:s(e.data),percent:d3.format("%")(n)};return e.value&&n>v?r[p]:""})}}),S}var t={top:0,right:0,bottom:0,left:0},n=500,r=500,i=function(e){return e.x},s=function(e){return e.y},o=function(e){return e.description},u=Math.floor(Math.random()*1e4),a=e.utils.defaultColor(),f=d3.format(",.2f"),l=!0,c=!0,h=!1,p="key",v=.02,m=!1,g=!1,y=!1,b=!1,w=.5,E=d3.dispatch("chartClick","elementClick","elementDblClick","elementMouseover","elementMouseout");return S.dispatch=E,S.options=e.utils.optionsFunc.bind(S),S.margin=function(e){return arguments.length?(t.top=typeof e.top!="undefined"?e.top:t.top,t.right=typeof e.right!="undefined"?e.right:t.right,t.bottom=typeof e.bottom!="undefined"?e.bottom:t.bottom,t.left=typeof e.left!="undefined"?e.left:t.left,S):t},S.width=function(e){return arguments.length?(n=e,S):n},S.height=function(e){return arguments.length?(r=e,S):r},S.values=function(t){return e.log("pie.values() is no longer supported."),S},S.x=function(e){return arguments.length?(i=e,S):i},S.y=function(e){return arguments.length?(s=d3.functor(e),S):s},S.description=function(e){return arguments.length?(o=e,S):o},S.showLabels=function(e){return arguments.length?(l=e,S):l},S.labelSunbeamLayout=function(e){return arguments.length?(g=e,S):g},S.donutLabelsOutside=function(e){return arguments.length?(h=e,S):h},S.pieLabelsOutside=function(e){return arguments.length?(c=e,S):c},S.labelType=function(e){return arguments.length?(p=e,p=p||"key",S):p},S.donut=function(e){return arguments.length?(m=e,S):m},S.donutRatio=function(e){return arguments.length?(w=e,S):w},S.startAngle=function(e){return arguments.length?(y=e,S):y},S.endAngle=function(e){return arguments.length?(b=e,S):b},S.id=function(e){return arguments.length?(u=e,S):u},S.color=function(t){return arguments.length?(a=e.utils.getColor(t),S):a},S.valueFormat=function(e){return arguments.length?(f=e,S):f},S.labelThreshold=function(e){return arguments.length?(v=e,S):v},S},e.models.pieChart=function(){"use strict";function v(e){return e.each(function(e){var u=d3.select(this),a=this,f=(i||parseInt(u.style("width"))||960)-r.left-r.right,d=(s||parseInt(u.style("height"))||400)-r.top-r.bottom;v.update=function(){u.transition().call(v)},v.container=this,l.disabled=e.map(function(e){return!!e.disabled});if(!c){var m;c={};for(m in l)l[m]instanceof Array?c[m]=l[m].slice(0):c[m]=l[m]}if(!e||!e.length){var g=u.selectAll(".nv-noData").data([h]);return g.enter().append("text").attr("class","nvd3 nv-noData").attr("dy","-.7em").style("text-anchor","middle"),g.attr("x",r.left+f/2).attr("y",r.top+d/2).text(function(e){return e}),v}u.selectAll(".nv-noData").remove();var y=u.selectAll("g.nv-wrap.nv-pieChart").data([e]),b=y.enter().append("g").attr("class","nvd3 nv-wrap nv-pieChart").append("g"),w=y.select("g");b.append("g").attr("class","nv-pieWrap"),b.append("g").attr("class","nv-legendWrap"),o&&(n.width(f).key(t.x()),y.select(".nv-legendWrap").datum(e).call(n),r.top!=n.height()&&(r.top=n.height(),d=(s||parseInt(u.style("height"))||400)-r.top-r.bottom),y.select(".nv-legendWrap").attr("transform","translate(0,"+ -r.top+")")),y.attr("transform","translate("+r.left+","+r.top+")"),t.width(f).height(d);var E=w.select(".nv-pieWrap").datum([e]);d3.transition(E).call(t),n.dispatch.on("stateChange",function(e){l=e,p.stateChange(l),v.update()}),t.dispatch.on("elementMouseout.tooltip",function(e){p.tooltipHide(e)}),p.on("changeState",function(t){typeof t.disabled!="undefined"&&(e.forEach(function(e,n){e.disabled=t.disabled[n]}),l.disabled=t.disabled),v.update()})}),v}var t=e.models.pie(),n=e.models.legend(),r={top:30,right:20,bottom:20,left:20},i=null,s=null,o=!0,u=e.utils.defaultColor(),a=!0,f=function(e,t,n,r){return"<h3>"+e+"</h3>"+"<p>"+t+"</p>"},l={},c=null,h="No Data Available.",p=d3.dispatch("tooltipShow","tooltipHide","stateChange","changeState"),d=function(n,r){var i=t.description()(n.point)||t.x()(n.point),s=n.pos[0]+(r&&r.offsetLeft||0),o=n.pos[1]+(r&&r.offsetTop||0),u=t.valueFormat()(t.y()(n.point)),a=f(i,u,n,v);e.tooltip.show([s,o],a,n.value<0?"n":"s",null,r)};return t.dispatch.on("elementMouseover.tooltip",function(e){e.pos=[e.pos[0]+r.left,e.pos[1]+r.top],p.tooltipShow(e)}),p.on("tooltipShow",function(e){a&&d(e)}),p.on("tooltipHide",function(){a&&e.tooltip.cleanup()}),v.legend=n,v.dispatch=p,v.pie=t,d3.rebind(v,t,"valueFormat","values","x","y","description","id","showLabels","donutLabelsOutside","pieLabelsOutside","labelType","donut","donutRatio","labelThreshold"),v.options=e.utils.optionsFunc.bind(v),v.margin=function(e){return arguments.length?(r.top=typeof e.top!="undefined"?e.top:r.top,r.right=typeof e.right!="undefined"?e.right:r.right,r.bottom=typeof e.bottom!="undefined"?e.bottom:r.bottom,r.left=typeof e.left!="undefined"?e.left:r.left,v):r},v.width=function(e){return arguments.length?(i=e,v):i},v.height=function(e){return arguments.length?(s=e,v):s},v.color=function(r){return arguments.length?(u=e.utils.getColor(r),n.color(u),t.color(u),v):u},v.showLegend=function(e){return arguments.length?(o=e,v):o},v.tooltips=function(e){return arguments.length?(a=e,v):a},v.tooltipContent=function(e){return arguments.length?(f=e,v):f},v.state=function(e){return arguments.length?(l=e,v):l},v.defaultState=function(e){return arguments.length?(c=e,v):c},v.noData=function(e){return arguments.length?(h=e,v):h},v},e.models.scatter=function(){"use strict";function I(q){return q.each(function(I){function Q(){if(!g)return!1;var e,i=d3.merge(I.map(function(e,t){return e.values.map(function(e,n){var r=f(e,n),i=l(e,n);return[o(r)+Math.random()*1e-7,u(i)+Math.random()*1e-7,t,n,e]}).filter(function(e,t){return b(e[4],t)})}));if(D===!0){if(x){var a=X.select("defs").selectAll(".nv-point-clips").data([s]).enter();a.append("clipPath").attr("class","nv-point-clips").attr("id","nv-points-clip-"+s);var c=X.select("#nv-points-clip-"+s).selectAll("circle").data(i);c.enter().append("circle").attr("r",T),c.exit().remove(),c.attr("cx",function(e){return e[0]}).attr("cy",function(e){return e[1]}),X.select(".nv-point-paths").attr("clip-path","url(#nv-points-clip-"+s+")")}i.length&&(i.push([o.range()[0]-20,u.range()[0]-20,null,null]),i.push([o.range()[1]+20,u.range()[1]+20,null,null]),i.push([o.range()[0]-20,u.range()[0]+20,null,null]),i.push([o.range()[1]+20,u.range()[1]-20,null,null]));var h=d3.geom.polygon([[-10,-10],[-10,r+10],[n+10,r+10],[n+10,-10]]),p=d3.geom.voronoi(i).map(function(e,t){return{data:h.clip(e),series:i[t][2],point:i[t][3]}}),d=X.select(".nv-point-paths").selectAll("path").data(p);d.enter().append("path").attr("class",function(e,t){return"nv-path-"+t}),d.exit().remove(),d.attr("d",function(e){return e.data.length===0?"M 0 0":"M"+e.data.join("L")+"Z"});var v=function(e,n){if(F)return 0;var r=I[e.series];if(typeof r=="undefined")return;var i=r.values[e.point];n({point:i,series:r,pos:[o(f(i,e.point))+t.left,u(l(i,e.point))+t.top],seriesIndex:e.series,pointIndex:e.point})};d.on("click",function(e){v(e,_.elementClick)}).on("mouseover",function(e){v(e,_.elementMouseover)}).on("mouseout",function(e,t){v(e,_.elementMouseout)})}else X.select(".nv-groups").selectAll(".nv-group").selectAll(".nv-point").on("click",function(e,n){if(F||!I[e.series])return 0;var r=I[e.series],i=r.values[n];_.elementClick({point:i,series:r,pos:[o(f(i,n))+t.left,u(l(i,n))+t.top],seriesIndex:e.series,pointIndex:n})}).on("mouseover",function(e,n){if(F||!I[e.series])return 0;var r=I[e.series],i=r.values[n];_.elementMouseover({point:i,series:r,pos:[o(f(i,n))+t.left,u(l(i,n))+t.top],seriesIndex:e.series,pointIndex:n})}).on("mouseout",function(e,t){if(F||!I[e.series])return 0;var n=I[e.series],r=n.values[t];_.elementMouseout({point:r,series:n,seriesIndex:e.series,pointIndex:t})});F=!1}var q=n-t.left-t.right,R=r-t.top-t.bottom,U=d3.select(this);I.forEach(function(e,t){e.values.forEach(function(e){e.series=t})});var W=N&&C&&A?[]:d3.merge(I.map(function(e){return e.values.map(function(e,t){return{x:f(e,t),y:l(e,t),size:c(e,t)}})}));o.domain(N||d3.extent(W.map(function(e){return e.x}).concat(d))),w&&I[0]?o.range(k||[(q*E+q)/(2*I[0].values.length),q-q*(1+E)/(2*I[0].values.length)]):o.range(k||[0,q]),u.domain(C||d3.extent(W.map(function(e){return e.y}).concat(v))).range(L||[R,0]),a.domain(A||d3.extent(W.map(function(e){return e.size}).concat(m))).range(O||[16,256]);if(o.domain()[0]===o.domain()[1]||u.domain()[0]===u.domain()[1])M=!0;o.domain()[0]===o.domain()[1]&&(o.domain()[0]?o.domain([o.domain()[0]-o.domain()[0]*.01,o.domain()[1]+o.domain()[1]*.01]):o.domain([-1,1])),u.domain()[0]===u.domain()[1]&&(u.domain()[0]?u.domain([u.domain()[0]-u.domain()[0]*.01,u.domain()[1]+u.domain()[1]*.01]):u.domain([-1,1])),isNaN(o.domain()[0])&&o.domain([-1,1]),isNaN(u.domain()[0])&&u.domain([-1,1]),P=P||o,H=H||u,B=B||a;var X=U.selectAll("g.nv-wrap.nv-scatter").data([I]),V=X.enter().append("g").attr("class","nvd3 nv-wrap nv-scatter nv-chart-"+s+(M?" nv-single-point":"")),$=V.append("defs"),J=V.append("g"),K=X.select("g");J.append("g").attr("class","nv-groups"),J.append("g").attr("class","nv-point-paths"),X.attr("transform","translate("+t.left+","+t.top+")"),$.append("clipPath").attr("id","nv-edge-clip-"+s).append("rect"),X.select("#nv-edge-clip-"+s+" rect").attr("width",q).attr("height",R>0?R:0),K.attr("clip-path",S?"url(#nv-edge-clip-"+s+")":""),F=!0;var G=X.select(".nv-groups").selectAll(".nv-group").data(function(e){return e},function(e){return e.key});G.enter().append("g").style("stroke-opacity",1e-6).style("fill-opacity",1e-6),G.exit().remove(),G.attr("class",function(e,t){return"nv-group nv-series-"+t}).classed("hover",function(e){return e.hover}),G.transition().style("fill",function(e,t){return i(e,t)}).style("stroke",function(e,t){return i(e,t)}).style("stroke-opacity",1).style("fill-opacity",.5);if(p){var Y=G.selectAll("circle.nv-point").data(function(e){return e.values},y);Y.enter().append("circle").style("fill",function(e,t){return e.color}).style("stroke",function(e,t){return e.color}).attr("cx",function(t,n){return e.utils.NaNtoZero(P(f(t,n)))}).attr("cy",function(t,n){return e.utils.NaNtoZero(H(l(t,n)))}).attr("r",function(e,t){return Math.sqrt(a(c(e,t))/Math.PI)}),Y.exit().remove(),G.exit().selectAll("path.nv-point").transition().attr("cx",function(t,n){return e.utils.NaNtoZero(o(f(t,n)))}).attr("cy",function(t,n){return e.utils.NaNtoZero(u(l(t,n)))}).remove(),Y.each(function(e,t){d3.select(this).classed("nv-point",!0).classed("nv-point-"+t,!0).classed("hover",!1)}),Y.transition().attr("cx",function(t,n){return e.utils.NaNtoZero(o(f(t,n)))}).attr("cy",function(t,n){return e.utils.NaNtoZero(u(l(t,n)))}).attr("r",function(e,t){return Math.sqrt(a(c(e,t))/Math.PI)})}else{var Y=G.selectAll("path.nv-point").data(function(e){return e.values});Y.enter().append("path").style("fill",function(e,t){return e.color}).style("stroke",function(e,t){return e.color}).attr("transform",function(e,t){return"translate("+P(f(e,t))+","+H(l(e,t))+")"}).attr("d",d3.svg.symbol().type(h).size(function(e,t){return a(c(e,t))})),Y.exit().remove(),G.exit().selectAll("path.nv-point").transition().attr("transform",function(e,t){return"translate("+o(f(e,t))+","+u(l(e,t))+")"}).remove(),Y.each(function(e,t){d3.select(this).classed("nv-point",!0).classed("nv-point-"+t,!0).classed("hover",!1)}),Y.transition().attr("transform",function(e,t){return"translate("+o(f(e,t))+","+u(l(e,t))+")"}).attr("d",d3.svg.symbol().type(h).size(function(e,t){return a(c(e,t))}))}clearTimeout(j),j=setTimeout(Q,300),P=o.copy(),H=u.copy(),B=a.copy()}),I}var t={top:0,right:0,bottom:0,left:0},n=960,r=500,i=e.utils.defaultColor(),s=Math.floor(Math.random()*1e5),o=d3.scale.linear(),u=d3.scale.linear(),a=d3.scale.linear(),f=function(e){return e.x},l=function(e){return e.y},c=function(e){return e.size||1},h=function(e){return e.shape||"circle"},p=!0,d=[],v=[],m=[],g=!0,y=null,b=function(e){return!e.notActive},w=!1,E=.1,S=!1,x=!0,T=function(){return 25},N=null,C=null,k=null,L=null,A=null,O=null,M=!1,_=d3.dispatch("elementClick","elementMouseover","elementMouseout"),D=!0,P,H,B,j,F=!1;return I.clearHighlights=function(){d3.selectAll(".nv-chart-"+s+" .nv-point.hover").classed("hover",!1)},I.highlightPoint=function(e,t,n){d3.select(".nv-chart-"+s+" .nv-series-"+e+" .nv-point-"+t).classed("hover",n)},_.on("elementMouseover.point",function(e){g&&I.highlightPoint(e.seriesIndex,e.pointIndex,!0)}),_.on("elementMouseout.point",function(e){g&&I.highlightPoint(e.seriesIndex,e.pointIndex,!1)}),I.dispatch=_,I.options=e.utils.optionsFunc.bind(I),I.x=function(e){return arguments.length?(f=d3.functor(e),I):f},I.y=function(e){return arguments.length?(l=d3.functor(e),I):l},I.size=function(e){return arguments.length?(c=d3.functor(e),I):c},I.margin=function(e){return arguments.length?(t.top=typeof e.top!="undefined"?e.top:t.top,t.right=typeof e.right!="undefined"?e.right:t.right,t.bottom=typeof e.bottom!="undefined"?e.bottom:t.bottom,t.left=typeof e.left!="undefined"?e.left:t.left,I):t},I.width=function(e){return arguments.length?(n=e,I):n},I.height=function(e){return arguments.length?(r=e,I):r},I.xScale=function(e){return arguments.length?(o=e,I):o},I.yScale=function(e){return arguments.length?(u=e,I):u},I.zScale=function(e){return arguments.length?(a=e,I):a},I.xDomain=function(e){return arguments.length?(N=e,I):N},I.yDomain=function(e){return arguments.length?(C=e,I):C},I.sizeDomain=function(e){return arguments.length?(A=e,I):A},I.xRange=function(e){return arguments.length?(k=e,I):k},I.yRange=function(e){return arguments.length?(L=e,I):L},I.sizeRange=function(e){return arguments.length?(O=e,I):O},I.forceX=function(e){return arguments.length?(d=e,I):d},I.forceY=function(e){return arguments.length?(v=e,I):v},I.forceSize=function(e){return arguments.length?(m=e,I):m},I.interactive=function(e){return arguments.length?(g=e,I):g},I.pointKey=function(e){return arguments.length?(y=e,I):y},I.pointActive=function(e){return arguments.length?(b=e,I):b},I.padData=function(e){return arguments.length?(w=e,I):w},I.padDataOuter=function(e){return arguments.length?(E=e,I):E},I.clipEdge=function(e){return arguments.length?(S=e,I):S},I.clipVoronoi=function(e){return arguments.length?(x=e,I):x},I.useVoronoi=function(e){return arguments.length?(D=e,D===!1&&(x=!1),I):D},I.clipRadius=function(e){return arguments.length?(T=e,I):T},I.color=function(t){return arguments.length?(i=e.utils.getColor(t),I):i},I.shape=function(e){return arguments.length?(h=e,I):h},I.onlyCircles=function(e){return arguments.length?(p=e,I):p},I.id=function(e){return arguments.length?(s=e,I):s},I.singlePoint=function(e){return arguments.length?(M=e,I):M},I},e.models.scatterChart=function(){"use strict";function F(e){return e.each(function(e){function K(){if(T)return X.select(".nv-point-paths").style("pointer-events","all"),!1;X.select(".nv-point-paths").style("pointer-events","none");var i=d3.mouse(this);h.distortion(x).focus(i[0]),p.distortion(x).focus(i[1]),X.select(".nv-scatterWrap").call(t),b&&X.select(".nv-x.nv-axis").call(n),w&&X.select(".nv-y.nv-axis").call(r),X.select(".nv-distributionX").datum(e.filter(function(e){return!e.disabled})).call(o),X.select(".nv-distributionY").datum(e.filter(function(e){return!e.disabled})).call(u)}var C=d3.select(this),k=this,L=(f||parseInt(C.style("width"))||960)-a.left-a.right,I=(l||parseInt(C.style("height"))||400)-a.top-a.bottom;F.update=function(){C.transition().duration(D).call(F)},F.container=this,A.disabled=e.map(function(e){return!!e.disabled});if(!O){var q;O={};for(q in A)A[q]instanceof Array?O[q]=A[q].slice(0):O[q]=A[q]}if(!e||!e.length||!e.filter(function(e){return e.values.length}).length){var R=C.selectAll(".nv-noData").data([_]);return R.enter().append("text").attr("class","nvd3 nv-noData").attr("dy","-.7em").style("text-anchor","middle"),R.attr("x",a.left+L/2).attr("y",a.top+I/2).text(function(e){return e}),F}C.selectAll(".nv-noData").remove(),P=P||h,H=H||p;var U=C.selectAll("g.nv-wrap.nv-scatterChart").data([e]),z=U.enter().append("g").attr("class","nvd3 nv-wrap nv-scatterChart nv-chart-"+t.id()),W=z.append("g"),X=U.select("g");W.append("rect").attr("class","nvd3 nv-background"),W.append("g").attr("class","nv-x nv-axis"),W.append("g").attr("class","nv-y nv-axis"),W.append("g").attr("class","nv-scatterWrap"),W.append("g").attr("class","nv-distWrap"),W.append("g").attr("class","nv-legendWrap"),W.append("g").attr("class","nv-controlsWrap");if(y){var V=S?L/2:L;i.width(V),U.select(".nv-legendWrap").datum(e).call(i),a.top!=i.height()&&(a.top=i.height(),I=(l||parseInt(C.style("height"))||400)-a.top-a.bottom),U.select(".nv-legendWrap").attr("transform","translate("+(L-V)+","+ -a.top+")")}S&&(s.width(180).color(["#444"]),X.select(".nv-controlsWrap").datum(j).attr("transform","translate(0,"+ -a.top+")").call(s)),U.attr("transform","translate("+a.left+","+a.top+")"),E&&X.select(".nv-y.nv-axis").attr("transform","translate("+L+",0)"),t.width(L).height(I).color(e.map(function(e,t){return e.color||c(e,t)}).filter(function(t,n){return!e[n].disabled})),d!==0&&t.xDomain(null),v!==0&&t.yDomain(null),U.select(".nv-scatterWrap").datum(e.filter(function(e){return!e.disabled})).call(t);if(d!==0){var $=h.domain()[1]-h.domain()[0];t.xDomain([h.domain()[0]-d*$,h.domain()[1]+d*$])}if(v!==0){var J=p.domain()[1]-p.domain()[0];t.yDomain([p.domain()[0]-v*J,p.domain()[1]+v*J])}(v!==0||d!==0)&&U.select(".nv-scatterWrap").datum(e.filter(function(e){return!e.disabled})).call(t),b&&(n.scale(h).ticks(n.ticks()&&n.ticks().length?n.ticks():L/100).tickSize(-I,0),X.select(".nv-x.nv-axis").attr("transform","translate(0,"+p.range()[0]+")").call(n)),w&&(r.scale(p).ticks(r.ticks()&&r.ticks().length?r.ticks():I/36).tickSize(-L,0),X.select(".nv-y.nv-axis").call(r)),m&&(o.getData(t.x()).scale(h).width(L).color(e.map(function(e,t){return e.color||c(e,t)}).filter(function(t,n){return!e[n].disabled})),W.select(".nv-distWrap").append("g").attr("class","nv-distributionX"),X.select(".nv-distributionX").attr("transform","translate(0,"+p.range()[0]+")").datum(e.filter(function(e){return!e.disabled})).call(o)),g&&(u.getData(t.y()).scale(p).width(I).color(e.map(function(e,t){return e.color||c(e,t)}).filter(function(t,n){return!e[n].disabled})),W.select(".nv-distWrap").append("g").attr("class","nv-distributionY"),X.select(".nv-distributionY").attr("transform","translate("+(E?L:-u.size())+",0)").datum(e.filter(function(e){return!e.disabled})).call(u)),d3.fisheye&&(X.select(".nv-background").attr("width",L).attr("height",I),X.select(".nv-background").on("mousemove",K),X.select(".nv-background").on("click",function(){T=!T}),t.dispatch.on("elementClick.freezeFisheye",function(){T=!T})),s.dispatch.on("legendClick",function(e,i){e.disabled=!e.disabled,x=e.disabled?0:2.5,X.select(".nv-background").style("pointer-events",e.disabled?"none":"all"),X.select(".nv-point-paths").style("pointer-events",e.disabled?"all":"none"),e.disabled?(h.distortion(x).focus(0),p.distortion(x).focus(0),X.select(".nv-scatterWrap").call(t),X.select(".nv-x.nv-axis").call(n),X.select(".nv-y.nv-axis").call(r)):T=!1,F.update()}),i.dispatch.on("stateChange",function(e){A.disabled=e.disabled,M.stateChange(A),F.update()}),t.dispatch.on("elementMouseover.tooltip",function(e){d3.select(".nv-chart-"+t.id()+" .nv-series-"+e.seriesIndex+" .nv-distx-"+e.pointIndex).attr("y1",function(t,n){return e.pos[1]-I}),d3.select(".nv-chart-"+t.id()+" .nv-series-"+e.seriesIndex+" .nv-disty-"+e.pointIndex).attr("x2",e.pos[0]+o.size()),e.pos=[e.pos[0]+a.left,e.pos[1]+a.top],M.tooltipShow(e)}),M.on("tooltipShow",function(e){N&&B(e,k.parentNode)}),M.on("changeState",function(t){typeof t.disabled!="undefined"&&(e.forEach(function(e,n){e.disabled=t.disabled[n]}),A.disabled=t.disabled),F.update()}),P=h.copy(),H=p.copy()}),F}var t=e.models.scatter(),n=e.models.axis(),r=e.models.axis(),i=e.models.legend(),s=e.models.legend(),o=e.models.distribution(),u=e.models.distribution(),a={top:30,right:20,bottom:50,left:75},f=null,l=null,c=e.utils.defaultColor(),h=d3.fisheye?d3.fisheye.scale(d3.scale.linear).distortion(0):t.xScale(),p=d3.fisheye?d3.fisheye.scale(d3.scale.linear).distortion(0):t.yScale(),d=0,v=0,m=!1,g=!1,y=!0,b=!0,w=!0,E=!1,S=!!d3.fisheye,x=0,T=!1,N=!0,C=function(e,t,n){return"<strong>"+t+"</strong>"},k=function(e,t,n){return"<strong>"+n+"</strong>"},L=null,A={},O=null,M=d3.dispatch("tooltipShow","tooltipHide","stateChange","changeState"),_="No Data Available.",D=250;t.xScale(h).yScale(p),n.orient("bottom").tickPadding(10),r.orient(E?"right":"left").tickPadding(10),o.axis("x"),u.axis("y"),s.updateState(!1);var P,H,B=function(i,s){var o=i.pos[0]+(s.offsetLeft||0),u=i.pos[1]+(s.offsetTop||0),f=i.pos[0]+(s.offsetLeft||0),l=p.range()[0]+a.top+(s.offsetTop||0),c=h.range()[0]+a.left+(s.offsetLeft||0),d=i.pos[1]+(s.offsetTop||0),v=n.tickFormat()(t.x()(i.point,i.pointIndex)),m=r.tickFormat()(t.y()(i.point,i.pointIndex));C!=null&&e.tooltip.show([f,l],C(i.series.key,v,m,i,F),"n",1,s,"x-nvtooltip"),k!=null&&e.tooltip.show([c,d],k(i.series.key,v,m,i,F),"e",1,s,"y-nvtooltip"),L!=null&&e.tooltip.show([o,u],L(i.series.key,v,m,i,F),i.value<0?"n":"s",null,s)},j=[{key:"Magnify",disabled:!0}];return t.dispatch.on("elementMouseout.tooltip",function(e){M.tooltipHide(e),d3.select(".nv-chart-"+t.id()+" .nv-series-"+e.seriesIndex+" .nv-distx-"+e.pointIndex).attr("y1",0),d3.select(".nv-chart-"+t.id()+" .nv-series-"+e.seriesIndex+" .nv-disty-"+e.pointIndex).attr("x2",u.size())}),M.on("tooltipHide",function(){N&&e.tooltip.cleanup()}),F.dispatch=M,F.scatter=t,F.legend=i,F.controls=s,F.xAxis=n,F.yAxis=r,F.distX=o,F.distY=u,d3.rebind(F,t,"id","interactive","pointActive","x","y","shape","size","xScale","yScale","zScale","xDomain","yDomain","xRange","yRange","sizeDomain","sizeRange","forceX","forceY","forceSize","clipVoronoi","clipRadius","useVoronoi"),F.options=e.utils.optionsFunc.bind(F),F.margin=function(e){return arguments.length?(a.top=typeof e.top!="undefined"?e.top:a.top,a.right=typeof e.right!="undefined"?e.right:a.right,a.bottom=typeof e.bottom!="undefined"?e.bottom:a.bottom,a.left=typeof e.left!="undefined"?e.left:a.left,F):a},F.width=function(e){return arguments.length?(f=e,F):f},F.height=function(e){return arguments.length?(l=e,F):l},F.color=function(t){return arguments.length?(c=e.utils.getColor(t),i.color(c),o.color(c),u.color(c),F):c},F.showDistX=function(e){return arguments.length?(m=e,F):m},F.showDistY=function(e){return arguments.length?(g=e,F):g},F.showControls=function(e){return arguments.length?(S=e,F):S},F.showLegend=function(e){return arguments.length?(y=e,F):y},F.showXAxis=function(e){return arguments.length?(b=e,F):b},F.showYAxis=function(e){return arguments.length?(w=e,F):w},F.rightAlignYAxis=function(e){return arguments.length?(E=e,r.orient(e?"right":"left"),F):E},F.fisheye=function(e){return arguments.length?(x=e,F):x},F.xPadding=function(e){return arguments.length?(d=e,F):d},F.yPadding=function(e){return arguments.length?(v=e,F):v},F.tooltips=function(e){return arguments.length?(N=e,F):N},F.tooltipContent=function(e){return arguments.length?(L=e,F):L},F.tooltipXContent=function(e){return arguments.length?(C=e,F):C},F.tooltipYContent=function(e){return arguments.length?(k=e,F):k},F.state=function(e){return arguments.length?(A=e,F):A},F.defaultState=function(e){return arguments.length?(O=e,F):O},F.noData=function(e){return arguments.length?(_=e,F):_},F.transitionDuration=function(e){return arguments.length?(D=e,F):D},F},e.models.scatterPlusLineChart=function(){"use strict";function B(e){return e.each(function(e){function $(){if(S)return z.select(".nv-point-paths").style("pointer-events","all"),!1;z.select(".nv-point-paths").style("pointer-events","none");var i=d3.mouse(this);h.distortion(E).focus(i[0]),p.distortion(E).focus(i[1]),z.select(".nv-scatterWrap").datum(e.filter(function(e){return!e.disabled})).call(t),g&&z.select(".nv-x.nv-axis").call(n),y&&z.select(".nv-y.nv-axis").call(r),z.select(".nv-distributionX").datum(e.filter(function(e){return!e.disabled})).call(o),z.select(".nv-distributionY").datum(e.filter(function(e){return!e.disabled})).call(u)}var T=d3.select(this),N=this,C=(f||parseInt(T.style("width"))||960)-a.left-a.right,j=(l||parseInt(T.style("height"))||400)-a.top-a.bottom;B.update=function(){T.transition().duration(M).call(B)},B.container=this,k.disabled=e.map(function(e){return!!e.disabled});if(!L){var F;L={};for(F in k)k[F]instanceof Array?L[F]=k[F].slice(0):L[F]=k[F]}if(!e||!e.length||!e.filter(function(e){return e.values.length}).length){var I=T.selectAll(".nv-noData").data([O]);return I.enter().append("text").attr("class","nvd3 nv-noData").attr("dy","-.7em").style("text-anchor","middle"),I.attr("x",a.left+C/2).attr("y",a.top+j/2).text(function(e){return e}),B}T.selectAll(".nv-noData").remove(),h=t.xScale(),p=t.yScale(),_=_||h,D=D||p;var q=T.selectAll("g.nv-wrap.nv-scatterChart").data([e]),R=q.enter().append("g").attr("class","nvd3 nv-wrap nv-scatterChart nv-chart-"+t.id()),U=R.append("g"),z=q.select("g");U.append("rect").attr("class","nvd3 nv-background").style("pointer-events","none"),U.append("g").attr("class","nv-x nv-axis"),U.append("g").attr("class","nv-y nv-axis"),U.append("g").attr("class","nv-scatterWrap"),U.append("g").attr("class","nv-regressionLinesWrap"),U.append("g").attr("class","nv-distWrap"),U.append("g").attr("class","nv-legendWrap"),U.append("g").attr("class","nv-controlsWrap"),q.attr("transform","translate("+a.left+","+a.top+")"),b&&z.select(".nv-y.nv-axis").attr("transform","translate("+C+",0)"),m&&(i.width(C/2),q.select(".nv-legendWrap").datum(e).call(i),a.top!=i.height()&&(a.top=i.height(),j=(l||parseInt(T.style("height"))||400)-a.top-a.bottom),q.select(".nv-legendWrap").attr("transform","translate("+C/2+","+ -a.top+")")),w&&(s.width(180).color(["#444"]),z.select(".nv-controlsWrap").datum(H).attr("transform","translate(0,"+ -a.top+")").call(s)),t.width(C).height(j).color(e.map(function(e,t){return e.color||c(e,t)}).filter(function(t,n){return!e[n].disabled})),q.select(".nv-scatterWrap").datum(e.filter(function(e){return!e.disabled})).call(t),q.select(".nv-regressionLinesWrap").attr("clip-path","url(#nv-edge-clip-"+t.id()+")");var W=q.select(".nv-regressionLinesWrap").selectAll(".nv-regLines").data(function(e){return e});W.enter().append("g").attr("class","nv-regLines");var X=W.selectAll(".nv-regLine").data(function(e){return[e]}),V=X.enter().append("line").attr("class","nv-regLine").style("stroke-opacity",0);X.transition().attr("x1",h.range()[0]).attr("x2",h.range()[1]).attr("y1",function(e,t){return p(h.domain()[0]*e.slope+e.intercept)}).attr("y2",function(e,t){return p(h.domain()[1]*e.slope+e.intercept)}).style("stroke",function(e,t,n){return c(e,n)}).style("stroke-opacity",function(e,t){return e.disabled||typeof e.slope=="undefined"||typeof e.intercept=="undefined"?0:1}),g&&(n.scale(h).ticks(n.ticks()?n.ticks():C/100).tickSize(-j,0),z.select(".nv-x.nv-axis").attr("transform","translate(0,"+p.range()[0]+")").call(n)),y&&(r.scale(p).ticks(r.ticks()?r.ticks():j/36).tickSize(-C,0),z.select(".nv-y.nv-axis").call(r)),d&&(o.getData(t.x()).scale(h).width(C).color(e.map(function(e,t){return e.color||c(e,t)}).filter(function(t,n){return!e[n].disabled})),U.select(".nv-distWrap").append("g").attr("class","nv-distributionX"),z.select(".nv-distributionX").attr("transform","translate(0,"+p.range()[0]+")").datum(e.filter(function(e){return!e.disabled})).call(o)),v&&(u.getData(t.y()).scale(p).width(
6
+ j).color(e.map(function(e,t){return e.color||c(e,t)}).filter(function(t,n){return!e[n].disabled})),U.select(".nv-distWrap").append("g").attr("class","nv-distributionY"),z.select(".nv-distributionY").attr("transform","translate("+(b?C:-u.size())+",0)").datum(e.filter(function(e){return!e.disabled})).call(u)),d3.fisheye&&(z.select(".nv-background").attr("width",C).attr("height",j),z.select(".nv-background").on("mousemove",$),z.select(".nv-background").on("click",function(){S=!S}),t.dispatch.on("elementClick.freezeFisheye",function(){S=!S})),s.dispatch.on("legendClick",function(e,i){e.disabled=!e.disabled,E=e.disabled?0:2.5,z.select(".nv-background").style("pointer-events",e.disabled?"none":"all"),z.select(".nv-point-paths").style("pointer-events",e.disabled?"all":"none"),e.disabled?(h.distortion(E).focus(0),p.distortion(E).focus(0),z.select(".nv-scatterWrap").call(t),z.select(".nv-x.nv-axis").call(n),z.select(".nv-y.nv-axis").call(r)):S=!1,B.update()}),i.dispatch.on("stateChange",function(e){k=e,A.stateChange(k),B.update()}),t.dispatch.on("elementMouseover.tooltip",function(e){d3.select(".nv-chart-"+t.id()+" .nv-series-"+e.seriesIndex+" .nv-distx-"+e.pointIndex).attr("y1",e.pos[1]-j),d3.select(".nv-chart-"+t.id()+" .nv-series-"+e.seriesIndex+" .nv-disty-"+e.pointIndex).attr("x2",e.pos[0]+o.size()),e.pos=[e.pos[0]+a.left,e.pos[1]+a.top],A.tooltipShow(e)}),A.on("tooltipShow",function(e){x&&P(e,N.parentNode)}),A.on("changeState",function(t){typeof t.disabled!="undefined"&&(e.forEach(function(e,n){e.disabled=t.disabled[n]}),k.disabled=t.disabled),B.update()}),_=h.copy(),D=p.copy()}),B}var t=e.models.scatter(),n=e.models.axis(),r=e.models.axis(),i=e.models.legend(),s=e.models.legend(),o=e.models.distribution(),u=e.models.distribution(),a={top:30,right:20,bottom:50,left:75},f=null,l=null,c=e.utils.defaultColor(),h=d3.fisheye?d3.fisheye.scale(d3.scale.linear).distortion(0):t.xScale(),p=d3.fisheye?d3.fisheye.scale(d3.scale.linear).distortion(0):t.yScale(),d=!1,v=!1,m=!0,g=!0,y=!0,b=!1,w=!!d3.fisheye,E=0,S=!1,x=!0,T=function(e,t,n){return"<strong>"+t+"</strong>"},N=function(e,t,n){return"<strong>"+n+"</strong>"},C=function(e,t,n,r){return"<h3>"+e+"</h3>"+"<p>"+r+"</p>"},k={},L=null,A=d3.dispatch("tooltipShow","tooltipHide","stateChange","changeState"),O="No Data Available.",M=250;t.xScale(h).yScale(p),n.orient("bottom").tickPadding(10),r.orient(b?"right":"left").tickPadding(10),o.axis("x"),u.axis("y"),s.updateState(!1);var _,D,P=function(i,s){var o=i.pos[0]+(s.offsetLeft||0),u=i.pos[1]+(s.offsetTop||0),f=i.pos[0]+(s.offsetLeft||0),l=p.range()[0]+a.top+(s.offsetTop||0),c=h.range()[0]+a.left+(s.offsetLeft||0),d=i.pos[1]+(s.offsetTop||0),v=n.tickFormat()(t.x()(i.point,i.pointIndex)),m=r.tickFormat()(t.y()(i.point,i.pointIndex));T!=null&&e.tooltip.show([f,l],T(i.series.key,v,m,i,B),"n",1,s,"x-nvtooltip"),N!=null&&e.tooltip.show([c,d],N(i.series.key,v,m,i,B),"e",1,s,"y-nvtooltip"),C!=null&&e.tooltip.show([o,u],C(i.series.key,v,m,i.point.tooltip,i,B),i.value<0?"n":"s",null,s)},H=[{key:"Magnify",disabled:!0}];return t.dispatch.on("elementMouseout.tooltip",function(e){A.tooltipHide(e),d3.select(".nv-chart-"+t.id()+" .nv-series-"+e.seriesIndex+" .nv-distx-"+e.pointIndex).attr("y1",0),d3.select(".nv-chart-"+t.id()+" .nv-series-"+e.seriesIndex+" .nv-disty-"+e.pointIndex).attr("x2",u.size())}),A.on("tooltipHide",function(){x&&e.tooltip.cleanup()}),B.dispatch=A,B.scatter=t,B.legend=i,B.controls=s,B.xAxis=n,B.yAxis=r,B.distX=o,B.distY=u,d3.rebind(B,t,"id","interactive","pointActive","x","y","shape","size","xScale","yScale","zScale","xDomain","yDomain","xRange","yRange","sizeDomain","sizeRange","forceX","forceY","forceSize","clipVoronoi","clipRadius","useVoronoi"),B.options=e.utils.optionsFunc.bind(B),B.margin=function(e){return arguments.length?(a.top=typeof e.top!="undefined"?e.top:a.top,a.right=typeof e.right!="undefined"?e.right:a.right,a.bottom=typeof e.bottom!="undefined"?e.bottom:a.bottom,a.left=typeof e.left!="undefined"?e.left:a.left,B):a},B.width=function(e){return arguments.length?(f=e,B):f},B.height=function(e){return arguments.length?(l=e,B):l},B.color=function(t){return arguments.length?(c=e.utils.getColor(t),i.color(c),o.color(c),u.color(c),B):c},B.showDistX=function(e){return arguments.length?(d=e,B):d},B.showDistY=function(e){return arguments.length?(v=e,B):v},B.showControls=function(e){return arguments.length?(w=e,B):w},B.showLegend=function(e){return arguments.length?(m=e,B):m},B.showXAxis=function(e){return arguments.length?(g=e,B):g},B.showYAxis=function(e){return arguments.length?(y=e,B):y},B.rightAlignYAxis=function(e){return arguments.length?(b=e,r.orient(e?"right":"left"),B):b},B.fisheye=function(e){return arguments.length?(E=e,B):E},B.tooltips=function(e){return arguments.length?(x=e,B):x},B.tooltipContent=function(e){return arguments.length?(C=e,B):C},B.tooltipXContent=function(e){return arguments.length?(T=e,B):T},B.tooltipYContent=function(e){return arguments.length?(N=e,B):N},B.state=function(e){return arguments.length?(k=e,B):k},B.defaultState=function(e){return arguments.length?(L=e,B):L},B.noData=function(e){return arguments.length?(O=e,B):O},B.transitionDuration=function(e){return arguments.length?(M=e,B):M},B},e.models.sparkline=function(){"use strict";function d(e){return e.each(function(e){var i=n-t.left-t.right,d=r-t.top-t.bottom,v=d3.select(this);s.domain(l||d3.extent(e,u)).range(h||[0,i]),o.domain(c||d3.extent(e,a)).range(p||[d,0]);var m=v.selectAll("g.nv-wrap.nv-sparkline").data([e]),g=m.enter().append("g").attr("class","nvd3 nv-wrap nv-sparkline"),b=g.append("g"),w=m.select("g");m.attr("transform","translate("+t.left+","+t.top+")");var E=m.selectAll("path").data(function(e){return[e]});E.enter().append("path"),E.exit().remove(),E.style("stroke",function(e,t){return e.color||f(e,t)}).attr("d",d3.svg.line().x(function(e,t){return s(u(e,t))}).y(function(e,t){return o(a(e,t))}));var S=m.selectAll("circle.nv-point").data(function(e){function n(t){if(t!=-1){var n=e[t];return n.pointIndex=t,n}return null}var t=e.map(function(e,t){return a(e,t)}),r=n(t.lastIndexOf(o.domain()[1])),i=n(t.indexOf(o.domain()[0])),s=n(t.length-1);return[i,r,s].filter(function(e){return e!=null})});S.enter().append("circle"),S.exit().remove(),S.attr("cx",function(e,t){return s(u(e,e.pointIndex))}).attr("cy",function(e,t){return o(a(e,e.pointIndex))}).attr("r",2).attr("class",function(e,t){return u(e,e.pointIndex)==s.domain()[1]?"nv-point nv-currentValue":a(e,e.pointIndex)==o.domain()[0]?"nv-point nv-minValue":"nv-point nv-maxValue"})}),d}var t={top:2,right:0,bottom:2,left:0},n=400,r=32,i=!0,s=d3.scale.linear(),o=d3.scale.linear(),u=function(e){return e.x},a=function(e){return e.y},f=e.utils.getColor(["#000"]),l,c,h,p;return d.options=e.utils.optionsFunc.bind(d),d.margin=function(e){return arguments.length?(t.top=typeof e.top!="undefined"?e.top:t.top,t.right=typeof e.right!="undefined"?e.right:t.right,t.bottom=typeof e.bottom!="undefined"?e.bottom:t.bottom,t.left=typeof e.left!="undefined"?e.left:t.left,d):t},d.width=function(e){return arguments.length?(n=e,d):n},d.height=function(e){return arguments.length?(r=e,d):r},d.x=function(e){return arguments.length?(u=d3.functor(e),d):u},d.y=function(e){return arguments.length?(a=d3.functor(e),d):a},d.xScale=function(e){return arguments.length?(s=e,d):s},d.yScale=function(e){return arguments.length?(o=e,d):o},d.xDomain=function(e){return arguments.length?(l=e,d):l},d.yDomain=function(e){return arguments.length?(c=e,d):c},d.xRange=function(e){return arguments.length?(h=e,d):h},d.yRange=function(e){return arguments.length?(p=e,d):p},d.animate=function(e){return arguments.length?(i=e,d):i},d.color=function(t){return arguments.length?(f=e.utils.getColor(t),d):f},d},e.models.sparklinePlus=function(){"use strict";function v(e){return e.each(function(c){function O(){if(a)return;var e=C.selectAll(".nv-hoverValue").data(u),r=e.enter().append("g").attr("class","nv-hoverValue").style("stroke-opacity",0).style("fill-opacity",0);e.exit().transition().duration(250).style("stroke-opacity",0).style("fill-opacity",0).remove(),e.attr("transform",function(e){return"translate("+s(t.x()(c[e],e))+",0)"}).transition().duration(250).style("stroke-opacity",1).style("fill-opacity",1);if(!u.length)return;r.append("line").attr("x1",0).attr("y1",-n.top).attr("x2",0).attr("y2",b),r.append("text").attr("class","nv-xValue").attr("x",-6).attr("y",-n.top).attr("text-anchor","end").attr("dy",".9em"),C.select(".nv-hoverValue .nv-xValue").text(f(t.x()(c[u[0]],u[0]))),r.append("text").attr("class","nv-yValue").attr("x",6).attr("y",-n.top).attr("text-anchor","start").attr("dy",".9em"),C.select(".nv-hoverValue .nv-yValue").text(l(t.y()(c[u[0]],u[0])))}function M(){function r(e,n){var r=Math.abs(t.x()(e[0],0)-n),i=0;for(var s=0;s<e.length;s++)Math.abs(t.x()(e[s],s)-n)<r&&(r=Math.abs(t.x()(e[s],s)-n),i=s);return i}if(a)return;var e=d3.mouse(this)[0]-n.left;u=[r(c,Math.round(s.invert(e)))],O()}var m=d3.select(this),g=(r||parseInt(m.style("width"))||960)-n.left-n.right,b=(i||parseInt(m.style("height"))||400)-n.top-n.bottom;v.update=function(){v(e)},v.container=this;if(!c||!c.length){var w=m.selectAll(".nv-noData").data([d]);return w.enter().append("text").attr("class","nvd3 nv-noData").attr("dy","-.7em").style("text-anchor","middle"),w.attr("x",n.left+g/2).attr("y",n.top+b/2).text(function(e){return e}),v}m.selectAll(".nv-noData").remove();var E=t.y()(c[c.length-1],c.length-1);s=t.xScale(),o=t.yScale();var S=m.selectAll("g.nv-wrap.nv-sparklineplus").data([c]),T=S.enter().append("g").attr("class","nvd3 nv-wrap nv-sparklineplus"),N=T.append("g"),C=S.select("g");N.append("g").attr("class","nv-sparklineWrap"),N.append("g").attr("class","nv-valueWrap"),N.append("g").attr("class","nv-hoverArea"),S.attr("transform","translate("+n.left+","+n.top+")");var k=C.select(".nv-sparklineWrap");t.width(g).height(b),k.call(t);var L=C.select(".nv-valueWrap"),A=L.selectAll(".nv-currentValue").data([E]);A.enter().append("text").attr("class","nv-currentValue").attr("dx",p?-8:8).attr("dy",".9em").style("text-anchor",p?"end":"start"),A.attr("x",g+(p?n.right:0)).attr("y",h?function(e){return o(e)}:0).style("fill",t.color()(c[c.length-1],c.length-1)).text(l(E)),N.select(".nv-hoverArea").append("rect").on("mousemove",M).on("click",function(){a=!a}).on("mouseout",function(){u=[],O()}),C.select(".nv-hoverArea rect").attr("transform",function(e){return"translate("+ -n.left+","+ -n.top+")"}).attr("width",g+n.left+n.right).attr("height",b+n.top)}),v}var t=e.models.sparkline(),n={top:15,right:100,bottom:10,left:50},r=null,i=null,s,o,u=[],a=!1,f=d3.format(",r"),l=d3.format(",.2f"),c=!0,h=!0,p=!1,d="No Data Available.";return v.sparkline=t,d3.rebind(v,t,"x","y","xScale","yScale","color"),v.options=e.utils.optionsFunc.bind(v),v.margin=function(e){return arguments.length?(n.top=typeof e.top!="undefined"?e.top:n.top,n.right=typeof e.right!="undefined"?e.right:n.right,n.bottom=typeof e.bottom!="undefined"?e.bottom:n.bottom,n.left=typeof e.left!="undefined"?e.left:n.left,v):n},v.width=function(e){return arguments.length?(r=e,v):r},v.height=function(e){return arguments.length?(i=e,v):i},v.xTickFormat=function(e){return arguments.length?(f=e,v):f},v.yTickFormat=function(e){return arguments.length?(l=e,v):l},v.showValue=function(e){return arguments.length?(c=e,v):c},v.alignValue=function(e){return arguments.length?(h=e,v):h},v.rightAlignValue=function(e){return arguments.length?(p=e,v):p},v.noData=function(e){return arguments.length?(d=e,v):d},v},e.models.stackedArea=function(){"use strict";function g(e){return e.each(function(e){var a=n-t.left-t.right,b=r-t.top-t.bottom,w=d3.select(this);p=v.xScale(),d=v.yScale();var E=e;e.forEach(function(e,t){e.seriesIndex=t,e.values=e.values.map(function(e,n){return e.index=n,e.seriesIndex=t,e})});var S=e.filter(function(e){return!e.disabled});e=d3.layout.stack().order(l).offset(f).values(function(e){return e.values}).x(o).y(u).out(function(e,t,n){var r=u(e)===0?0:n;e.display={y:r,y0:t}})(S);var T=w.selectAll("g.nv-wrap.nv-stackedarea").data([e]),N=T.enter().append("g").attr("class","nvd3 nv-wrap nv-stackedarea"),C=N.append("defs"),k=N.append("g"),L=T.select("g");k.append("g").attr("class","nv-areaWrap"),k.append("g").attr("class","nv-scatterWrap"),T.attr("transform","translate("+t.left+","+t.top+")"),v.width(a).height(b).x(o).y(function(e){return e.display.y+e.display.y0}).forceY([0]).color(e.map(function(e,t){return e.color||i(e,e.seriesIndex)}));var A=L.select(".nv-scatterWrap").datum(e);A.call(v),C.append("clipPath").attr("id","nv-edge-clip-"+s).append("rect"),T.select("#nv-edge-clip-"+s+" rect").attr("width",a).attr("height",b),L.attr("clip-path",h?"url(#nv-edge-clip-"+s+")":"");var O=d3.svg.area().x(function(e,t){return p(o(e,t))}).y0(function(e){return d(e.display.y0)}).y1(function(e){return d(e.display.y+e.display.y0)}).interpolate(c),M=d3.svg.area().x(function(e,t){return p(o(e,t))}).y0(function(e){return d(e.display.y0)}).y1(function(e){return d(e.display.y0)}),_=L.select(".nv-areaWrap").selectAll("path.nv-area").data(function(e){return e});_.enter().append("path").attr("class",function(e,t){return"nv-area nv-area-"+t}).attr("d",function(e,t){return M(e.values,e.seriesIndex)}).on("mouseover",function(e,t){d3.select(this).classed("hover",!0),m.areaMouseover({point:e,series:e.key,pos:[d3.event.pageX,d3.event.pageY],seriesIndex:e.seriesIndex})}).on("mouseout",function(e,t){d3.select(this).classed("hover",!1),m.areaMouseout({point:e,series:e.key,pos:[d3.event.pageX,d3.event.pageY],seriesIndex:e.seriesIndex})}).on("click",function(e,t){d3.select(this).classed("hover",!1),m.areaClick({point:e,series:e.key,pos:[d3.event.pageX,d3.event.pageY],seriesIndex:e.seriesIndex})}),_.exit().remove(),_.style("fill",function(e,t){return e.color||i(e,e.seriesIndex)}).style("stroke",function(e,t){return e.color||i(e,e.seriesIndex)}),_.transition().attr("d",function(e,t){return O(e.values,t)}),v.dispatch.on("elementMouseover.area",function(e){L.select(".nv-chart-"+s+" .nv-area-"+e.seriesIndex).classed("hover",!0)}),v.dispatch.on("elementMouseout.area",function(e){L.select(".nv-chart-"+s+" .nv-area-"+e.seriesIndex).classed("hover",!1)}),g.d3_stackedOffset_stackPercent=function(e){var t=e.length,n=e[0].length,r=1/t,i,s,o,a=[];for(s=0;s<n;++s){for(i=0,o=0;i<E.length;i++)o+=u(E[i].values[s]);if(o)for(i=0;i<t;i++)e[i][s][1]/=o;else for(i=0;i<t;i++)e[i][s][1]=r}for(s=0;s<n;++s)a[s]=0;return a}}),g}var t={top:0,right:0,bottom:0,left:0},n=960,r=500,i=e.utils.defaultColor(),s=Math.floor(Math.random()*1e5),o=function(e){return e.x},u=function(e){return e.y},a="stack",f="zero",l="default",c="linear",h=!1,p,d,v=e.models.scatter(),m=d3.dispatch("tooltipShow","tooltipHide","areaClick","areaMouseover","areaMouseout");return v.size(2.2).sizeDomain([2.2,2.2]),v.dispatch.on("elementClick.area",function(e){m.areaClick(e)}),v.dispatch.on("elementMouseover.tooltip",function(e){e.pos=[e.pos[0]+t.left,e.pos[1]+t.top],m.tooltipShow(e)}),v.dispatch.on("elementMouseout.tooltip",function(e){m.tooltipHide(e)}),g.dispatch=m,g.scatter=v,d3.rebind(g,v,"interactive","size","xScale","yScale","zScale","xDomain","yDomain","xRange","yRange","sizeDomain","forceX","forceY","forceSize","clipVoronoi","useVoronoi","clipRadius","highlightPoint","clearHighlights"),g.options=e.utils.optionsFunc.bind(g),g.x=function(e){return arguments.length?(o=d3.functor(e),g):o},g.y=function(e){return arguments.length?(u=d3.functor(e),g):u},g.margin=function(e){return arguments.length?(t.top=typeof e.top!="undefined"?e.top:t.top,t.right=typeof e.right!="undefined"?e.right:t.right,t.bottom=typeof e.bottom!="undefined"?e.bottom:t.bottom,t.left=typeof e.left!="undefined"?e.left:t.left,g):t},g.width=function(e){return arguments.length?(n=e,g):n},g.height=function(e){return arguments.length?(r=e,g):r},g.clipEdge=function(e){return arguments.length?(h=e,g):h},g.color=function(t){return arguments.length?(i=e.utils.getColor(t),g):i},g.offset=function(e){return arguments.length?(f=e,g):f},g.order=function(e){return arguments.length?(l=e,g):l},g.style=function(e){if(!arguments.length)return a;a=e;switch(a){case"stack":g.offset("zero"),g.order("default");break;case"stream":g.offset("wiggle"),g.order("inside-out");break;case"stream-center":g.offset("silhouette"),g.order("inside-out");break;case"expand":g.offset("expand"),g.order("default");break;case"stack_percent":g.offset(g.d3_stackedOffset_stackPercent),g.order("default")}return g},g.interpolate=function(e){return arguments.length?(c=e,g):c},g},e.models.stackedAreaChart=function(){"use strict";function M(y){return y.each(function(y){var _=d3.select(this),D=this,P=(a||parseInt(_.style("width"))||960)-u.left-u.right,H=(f||parseInt(_.style("height"))||400)-u.top-u.bottom;M.update=function(){_.transition().duration(A).call(M)},M.container=this,S.disabled=y.map(function(e){return!!e.disabled});if(!x){var B;x={};for(B in S)S[B]instanceof Array?x[B]=S[B].slice(0):x[B]=S[B]}if(!y||!y.length||!y.filter(function(e){return e.values.length}).length){var j=_.selectAll(".nv-noData").data([T]);return j.enter().append("text").attr("class","nvd3 nv-noData").attr("dy","-.7em").style("text-anchor","middle"),j.attr("x",u.left+P/2).attr("y",u.top+H/2).text(function(e){return e}),M}_.selectAll(".nv-noData").remove(),b=t.xScale(),w=t.yScale();var F=_.selectAll("g.nv-wrap.nv-stackedAreaChart").data([y]),I=F.enter().append("g").attr("class","nvd3 nv-wrap nv-stackedAreaChart").append("g"),q=F.select("g");I.append("rect").style("opacity",0),I.append("g").attr("class","nv-x nv-axis"),I.append("g").attr("class","nv-y nv-axis"),I.append("g").attr("class","nv-stackedWrap"),I.append("g").attr("class","nv-legendWrap"),I.append("g").attr("class","nv-controlsWrap"),I.append("g").attr("class","nv-interactive"),q.select("rect").attr("width",P).attr("height",H);if(h){var R=c?P-C:P;i.width(R),q.select(".nv-legendWrap").datum(y).call(i),u.top!=i.height()&&(u.top=i.height(),H=(f||parseInt(_.style("height"))||400)-u.top-u.bottom),q.select(".nv-legendWrap").attr("transform","translate("+(P-R)+","+ -u.top+")")}if(c){var U=[{key:L.stacked||"Stacked",metaKey:"Stacked",disabled:t.style()!="stack",style:"stack"},{key:L.stream||"Stream",metaKey:"Stream",disabled:t.style()!="stream",style:"stream"},{key:L.expanded||"Expanded",metaKey:"Expanded",disabled:t.style()!="expand",style:"expand"},{key:L.stack_percent||"Stack %",metaKey:"Stack_Percent",disabled:t.style()!="stack_percent",style:"stack_percent"}];C=k.length/3*260,U=U.filter(function(e){return k.indexOf(e.metaKey)!==-1}),s.width(C).color(["#444","#444","#444"]),q.select(".nv-controlsWrap").datum(U).call(s),u.top!=Math.max(s.height(),i.height())&&(u.top=Math.max(s.height(),i.height()),H=(f||parseInt(_.style("height"))||400)-u.top-u.bottom),q.select(".nv-controlsWrap").attr("transform","translate(0,"+ -u.top+")")}F.attr("transform","translate("+u.left+","+u.top+")"),v&&q.select(".nv-y.nv-axis").attr("transform","translate("+P+",0)"),m&&(o.width(P).height(H).margin({left:u.left,top:u.top}).svgContainer(_).xScale(b),F.select(".nv-interactive").call(o)),t.width(P).height(H);var z=q.select(".nv-stackedWrap").datum(y);z.transition().call(t),p&&(n.scale(b).ticks(P/100).tickSize(-H,0),q.select(".nv-x.nv-axis").attr("transform","translate(0,"+H+")"),q.select(".nv-x.nv-axis").transition().duration(0).call(n)),d&&(r.scale(w).ticks(t.offset()=="wiggle"?0:H/36).tickSize(-P,0).setTickFormat(t.style()=="expand"||t.style()=="stack_percent"?d3.format("%"):E),q.select(".nv-y.nv-axis").transition().duration(0).call(r)),t.dispatch.on("areaClick.toggle",function(e){y.filter(function(e){return!e.disabled}).length===1?y.forEach(function(e){e.disabled=!1}):y.forEach(function(t,n){t.disabled=n!=e.seriesIndex}),S.disabled=y.map(function(e){return!!e.disabled}),N.stateChange(S),M.update()}),i.dispatch.on("stateChange",function(e){S.disabled=e.disabled,N.stateChange(S),M.update()}),s.dispatch.on("legendClick",function(e,n){if(!e.disabled)return;U=U.map(function(e){return e.disabled=!0,e}),e.disabled=!1,t.style(e.style),S.style=t.style(),N.stateChange(S),M.update()}),o.dispatch.on("elementMousemove",function(i){t.clearHighlights();var s,a,f,c=[];y.filter(function(e,t){return e.seriesIndex=t,!e.disabled}).forEach(function(n,r){a=e.interactiveBisect(n.values,i.pointXValue,M.x()),t.highlightPoint(r,a,!0);var o=n.values[a];if(typeof o=="undefined")return;typeof s=="undefined"&&(s=o),typeof f=="undefined"&&(f=M.xScale()(M.x()(o,a)));var u=t.style()=="expand"?o.display.y:M.y()(o,a);c.push({key:n.key,value:u,color:l(n,n.seriesIndex),stackedValue:o.display})}),c.reverse();if(c.length>2){var h=M.yScale().invert(i.mouseY),p=Infinity,d=null;c.forEach(function(e,t){h=Math.abs(h);var n=Math.abs(e.stackedValue.y0),r=Math.abs(e.stackedValue.y);if(h>=n&&h<=r+n){d=t;return}}),d!=null&&(c[d].highlight=!0)}var v=n.tickFormat()(M.x()(s,a)),m=t.style()=="expand"?function(e,t){return d3.format(".1%")(e)}:function(e,t){return r.tickFormat()(e)};o.tooltip.position({left:f+u.left,top:i.mouseY+u.top}).chartContainer(D.parentNode).enabled(g).valueFormatter(m).data({value:v,series:c})(),o.renderGuideLine(f)}),o.dispatch.on("elementMouseout",function(e){N.tooltipHide(),t.clearHighlights()}),N.on("tooltipShow",function(e){g&&O(e,D.parentNode)}),N.on("changeState",function(e){typeof e.disabled!="undefined"&&y.length===e.disabled.length&&(y.forEach(function(t,n){t.disabled=e.disabled[n]}),S.disabled=e.disabled),typeof e.style!="undefined"&&t.style(e.style),M.update()})}),M}var t=e.models.stackedArea(),n=e.models.axis(),r=e.models.axis(),i=e.models.legend(),s=e.models.legend(),o=e.interactiveGuideline(),u={top:30,right:25,bottom:50,left:60},a=null,f=null,l=e.utils.defaultColor(),c=!0,h=!0,p=!0,d=!0,v=!1,m=!1,g=!0,y=function(e,t,n,r,i){return"<h3>"+e+"</h3>"+"<p>"+n+" on "+t+"</p>"},b,w,E=d3.format(",.2f"),S={style:t.style()},x=null,T="No Data Available.",N=d3.dispatch("tooltipShow","tooltipHide","stateChange","changeState"),C=250,k=["Stacked","Stream","Expanded"],L={},A=250;n.orient("bottom").tickPadding(7),r.orient(v?"right":"left"),s.updateState(!1);var O=function(i,s){var o=i.pos[0]+(s.offsetLeft||0),u=i.pos[1]+(s.offsetTop||0),a=n.tickFormat()(t.x()(i.point,i.pointIndex)),f=r.tickFormat()(t.y()(i.point,i.pointIndex)),l=y(i.series.key,a,f,i,M);e.tooltip.show([o,u],l,i.value<0?"n":"s",null,s)};return t.dispatch.on("tooltipShow",function(e){e.pos=[e.pos[0]+u.left,e.pos[1]+u.top],N.tooltipShow(e)}),t.dispatch.on("tooltipHide",function(e){N.tooltipHide(e)}),N.on("tooltipHide",function(){g&&e.tooltip.cleanup()}),M.dispatch=N,M.stacked=t,M.legend=i,M.controls=s,M.xAxis=n,M.yAxis=r,M.interactiveLayer=o,d3.rebind(M,t,"x","y","size","xScale","yScale","xDomain","yDomain","xRange","yRange","sizeDomain","interactive","useVoronoi","offset","order","style","clipEdge","forceX","forceY","forceSize","interpolate"),M.options=e.utils.optionsFunc.bind(M),M.margin=function(e){return arguments.length?(u.top=typeof e.top!="undefined"?e.top:u.top,u.right=typeof e.right!="undefined"?e.right:u.right,u.bottom=typeof e.bottom!="undefined"?e.bottom:u.bottom,u.left=typeof e.left!="undefined"?e.left:u.left,M):u},M.width=function(e){return arguments.length?(a=e,M):a},M.height=function(e){return arguments.length?(f=e,M):f},M.color=function(n){return arguments.length?(l=e.utils.getColor(n),i.color(l),t.color(l),M):l},M.showControls=function(e){return arguments.length?(c=e,M):c},M.showLegend=function(e){return arguments.length?(h=e,M):h},M.showXAxis=function(e){return arguments.length?(p=e,M):p},M.showYAxis=function(e){return arguments.length?(d=e,M):d},M.rightAlignYAxis=function(e){return arguments.length?(v=e,r.orient(e?"right":"left"),M):v},M.useInteractiveGuideline=function(e){return arguments.length?(m=e,e===!0&&(M.interactive(!1),M.useVoronoi(!1)),M):m},M.tooltip=function(e){return arguments.length?(y=e,M):y},M.tooltips=function(e){return arguments.length?(g=e,M):g},M.tooltipContent=function(e){return arguments.length?(y=e,M):y},M.state=function(e){return arguments.length?(S=e,M):S},M.defaultState=function(e){return arguments.length?(x=e,M):x},M.noData=function(e){return arguments.length?(T=e,M):T},M.transitionDuration=function(e){return arguments.length?(A=e,M):A},M.controlsData=function(e){return arguments.length?(k=e,M):k},M.controlLabels=function(e){return arguments.length?typeof e!="object"?L:(L=e,M):L},r.setTickFormat=r.tickFormat,r.tickFormat=function(e){return arguments.length?(E=e,r):E},M}})();
extensions/reports/views/all.php ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <div class="wrap">
2
+
3
+ <h2><?php esc_html_e( 'Stream Reports', 'stream' ) ?>
4
+ <a href="<?php echo esc_url( $add_url ) ?>" class="add-new-h2">
5
+ <?php esc_html_e( 'New Report', 'stream' ) ?>
6
+ </a>
7
+ </h2>
8
+
9
+ <?php wp_nonce_field( 'stream-reports-page', 'wp_stream_reports_nonce', false ) ?>
10
+ <?php wp_nonce_field( 'closedpostboxes', 'closedpostboxesnonce', false ) ?>
11
+ <?php wp_nonce_field( 'meta-box-order', 'meta-box-order-nonce', false ) ?>
12
+
13
+ <?php wp_stream_reports_intervals_html() ?>
14
+
15
+ <div id="dashboard-widgets" class="<?php echo esc_attr( $class ) ?>">
16
+
17
+ <div class="postbox-container">
18
+ <?php if ( $no_reports ) : ?>
19
+ <div class="no-reports-message">
20
+ <?php esc_html_e( 'Well, this is embarrassing. There are no reports yet!', 'stream' ) ?>
21
+ <p>
22
+ <a href="<?php echo esc_url( $add_url ) ?>" class="button button-secondary">
23
+ <?php esc_html_e( 'Add a new one', 'stream' ) ?>
24
+ </a>
25
+ <span><?php esc_html_e( 'or', 'stream' ) ?></span>
26
+ <a href="<?php echo esc_url( $create_url ) ?>" class="button button-primary">
27
+ <?php esc_html_e( 'Generate some for me', 'stream' ) ?>
28
+ </a>
29
+ </p>
30
+ </div>
31
+ <?php endif; ?>
32
+ <?php do_meta_boxes( WP_Stream_Reports::$screen_id, 'normal', 'normal' ) ?>
33
+ </div>
34
+
35
+ <div class="postbox-container">
36
+ <?php do_meta_boxes( WP_Stream_Reports::$screen_id, 'side', 'side' ) ?>
37
+ </div>
38
+
39
+ </div>
40
+
41
+ </div>
extensions/reports/views/error.php ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
1
+ <div class='wrap about-wrap'>
2
+ <h1><?php esc_html_e( "Oops, that's an error!", 'stream' ) ?></h1>
3
+
4
+ <div class='about-text'>
5
+ <?php esc_html_e( 'Yeah... Something went very wrong somewhere along your last actions.', 'stream' ) ?>
6
+ </div>
7
+ </div>
extensions/reports/views/examples.php ADDED
@@ -0,0 +1,249 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <div class="wrap">
2
+ <h2><?php esc_html_e( 'Stream Reports', 'stream' ); ?></h2>
3
+ <?php
4
+ $args = array(
5
+ 'type' => 'multibar-horizontal',
6
+ 'showValues' => true,
7
+ 'values' => array(
8
+ array(
9
+ 'key' => 'One',
10
+ 'values' => array(
11
+ array(
12
+ 'x' => 0,
13
+ 'y' => -3,
14
+ ),
15
+ array(
16
+ 'x' => 1,
17
+ 'y' => -2,
18
+ ),
19
+ array(
20
+ 'x' => 2,
21
+ 'y' => -1,
22
+ ),
23
+ array(
24
+ 'x' => 3,
25
+ 'y' => 0,
26
+ ),
27
+ ),
28
+ ),
29
+ array(
30
+ 'key' => 'Three',
31
+ 'values' => array(
32
+ array(
33
+ 'x' => 0,
34
+ 'y' => 0,
35
+ ),
36
+ array(
37
+ 'x' => 1,
38
+ 'y' => 4,
39
+ ),
40
+ array(
41
+ 'x' => 2,
42
+ 'y' => 1,
43
+ ),
44
+ array(
45
+ 'x' => 3,
46
+ 'y' => 2,
47
+ ),
48
+ ),
49
+ ),
50
+ array(
51
+ 'key' => 'Two',
52
+ 'values' => array(
53
+ array(
54
+ 'x' => 0,
55
+ 'y' => 1,
56
+ ),
57
+ array(
58
+ 'x' => 1,
59
+ 'y' => 2,
60
+ ),
61
+ array(
62
+ 'x' => 2,
63
+ 'y' => 3,
64
+ ),
65
+ array(
66
+ 'x' => 3,
67
+ 'y' => 4,
68
+ ),
69
+ ),
70
+ ),
71
+ ),
72
+ );
73
+ ?>
74
+ <div class="report-chart" style='height: 300px; width: 500px;' data-report='<?php echo json_encode( $args ) ?>'><svg></svg></div>
75
+
76
+ <?php
77
+ $args = array(
78
+ 'type' => 'multibar',
79
+ 'values' => array(
80
+ array(
81
+ 'key' => 'One',
82
+ 'values' => array(
83
+ array(
84
+ 'x' => -2,
85
+ 'y' => 0,
86
+ ),
87
+ array(
88
+ 'x' => -1,
89
+ 'y' => 1,
90
+ ),
91
+ array(
92
+ 'x' => 0,
93
+ 'y' => 2,
94
+ ),
95
+ array(
96
+ 'x' => 1,
97
+ 'y' => 3,
98
+ ),
99
+ ),
100
+ ),
101
+ array(
102
+ 'key' => 'Three',
103
+ 'values' => array(
104
+ array(
105
+ 'x' => 0,
106
+ 'y' => 0,
107
+ ),
108
+ array(
109
+ 'x' => 1,
110
+ 'y' => 4,
111
+ ),
112
+ array(
113
+ 'x' => 2,
114
+ 'y' => 1,
115
+ ),
116
+ array(
117
+ 'x' => 3,
118
+ 'y' => 2,
119
+ ),
120
+ ),
121
+ ),
122
+ array(
123
+ 'key' => 'Two',
124
+ 'values' => array(
125
+ array(
126
+ 'x' => 0,
127
+ 'y' => 1,
128
+ ),
129
+ array(
130
+ 'x' => 1,
131
+ 'y' => 2,
132
+ ),
133
+