Stream - Version 3.0.0

Version Description

  • August 25, 2015 =

  • New: Activity logs are now stored locally in WordPress. No data is sent externally and no registration required.

  • New: Migration process for Stream 2 users to move records out of the cloud, and into your local database.

  • New: Various measures and database schema changes to improve Stream's performance.

  • Removed: Notifications and Reports have been removed to be reworked for an upcoming release.

Props @fjarrett, @lukecarbis

Download this release

Release Info

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

Version 3.0.0

Files changed (87) hide show
  1. LICENSE +339 -0
  2. classes/class-admin.php +1018 -0
  3. classes/class-author.php +308 -0
  4. classes/class-cli.php +221 -0
  5. classes/class-connector.php +237 -0
  6. classes/class-connectors.php +190 -0
  7. classes/class-date-interval.php +111 -0
  8. classes/class-db.php +201 -0
  9. classes/class-filter-input.php +116 -0
  10. classes/class-install.php +495 -0
  11. classes/class-list-table.php +933 -0
  12. classes/class-live-update.php +214 -0
  13. classes/class-log.php +285 -0
  14. classes/class-migrate.php +353 -0
  15. classes/class-network.php +524 -0
  16. classes/class-plugin.php +234 -0
  17. classes/class-query.php +349 -0
  18. classes/class-record.php +109 -0
  19. classes/class-settings.php +1019 -0
  20. classes/class-uninstall.php +229 -0
  21. connectors/class-connector-acf.php +543 -0
  22. connectors/class-connector-bbpress.php +244 -0
  23. connectors/class-connector-blogs.php +364 -0
  24. connectors/class-connector-buddypress.php +816 -0
  25. connectors/class-connector-comments.php +629 -0
  26. connectors/class-connector-edd.php +492 -0
  27. connectors/class-connector-editor.php +323 -0
  28. connectors/class-connector-gravityforms.php +728 -0
  29. connectors/class-connector-installer.php +403 -0
  30. connectors/class-connector-jetpack.php +682 -0
  31. connectors/class-connector-media.php +233 -0
  32. connectors/class-connector-menus.php +232 -0
  33. connectors/class-connector-posts.php +380 -0
  34. connectors/class-connector-settings.php +742 -0
  35. connectors/class-connector-taxonomies.php +253 -0
  36. connectors/class-connector-users.php +360 -0
  37. connectors/class-connector-widgets.php +814 -0
  38. connectors/class-connector-woocommerce.php +806 -0
  39. connectors/class-connector-wordpress-seo.php +492 -0
  40. contributing.md +106 -0
  41. includes/db-updates.php +20 -0
  42. includes/feeds/atom.php +54 -0
  43. includes/feeds/json.php +7 -0
  44. includes/feeds/rss-2.0.php +65 -0
  45. includes/functions.php +106 -0
  46. includes/lib/Carbon.php +2213 -0
  47. languages/stream-en_US.mo +0 -0
  48. languages/stream-en_US.po +4154 -0
  49. phpcs.ruleset.xml +11 -0
  50. readme.md +485 -0
  51. readme.txt +484 -0
  52. stream.php +60 -0
  53. tests/bootstrap.php +23 -0
  54. tests/testcase.php +74 -0
  55. tests/tests/connectors/test-class-connector-posts.php +6 -0
  56. tests/tests/test-class-admin.php +470 -0
  57. tests/tests/test-class-author.php +113 -0
  58. tests/tests/test-class-connector.php +225 -0
  59. tests/tests/test-class-connectors.php +38 -0
  60. tests/tests/test-class-date-interval.php +51 -0
  61. tests/tests/test-class-db.php +119 -0
  62. tests/tests/test-class-filter-input.php +48 -0
  63. tests/tests/test-class-plugin.php +66 -0
  64. ui/css/admin.css +515 -0
  65. ui/css/datepicker.css +346 -0
  66. ui/js/admin.js +501 -0
  67. ui/js/global.js +40 -0
  68. ui/js/live-updates.js +111 -0
  69. ui/js/migrate.js +114 -0
  70. ui/js/settings.js +378 -0
  71. ui/js/wpseo-admin.js +30 -0
  72. ui/lib/select2/CONTRIBUTING.md +107 -0
  73. ui/lib/select2/LICENSE +18 -0
  74. ui/lib/select2/README.md +115 -0
  75. ui/lib/select2/bower.json +8 -0
  76. ui/lib/select2/component.json +66 -0
  77. ui/lib/select2/composer.json +29 -0
  78. ui/lib/select2/package.json +20 -0
  79. ui/lib/select2/release.sh +79 -0
  80. ui/lib/select2/select2-bootstrap.css +87 -0
  81. ui/lib/select2/select2-spinner.gif +0 -0
  82. ui/lib/select2/select2.css +692 -0
  83. ui/lib/select2/select2.jquery.json +36 -0
  84. ui/lib/select2/select2.js +3558 -0
  85. ui/lib/select2/select2.png +0 -0
  86. ui/lib/select2/select2_locale_ar.js +19 -0
  87. ui/lib/select2/select2_locale_az.js +12 -0
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-admin.php ADDED
@@ -0,0 +1,1018 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace WP_Stream;
3
+
4
+ use DateTime;
5
+ use DateTimeZone;
6
+ use DateInterval;
7
+ use \WP_CLI;
8
+ use \WP_Roles;
9
+
10
+ class Admin {
11
+ /**
12
+ * Hold Plugin class
13
+ * @var Plugin
14
+ */
15
+ public $plugin;
16
+
17
+ /**
18
+ * @var Network
19
+ */
20
+ public $network;
21
+
22
+ /**
23
+ * @var Live_Update
24
+ */
25
+ public $live_update;
26
+
27
+ /**
28
+ * @var Migrate
29
+ */
30
+ public $migrate;
31
+
32
+ /**
33
+ * Menu page screen id
34
+ *
35
+ * @var string
36
+ */
37
+ public $screen_id = array();
38
+
39
+ /**
40
+ * List table object
41
+ *
42
+ * @var List_Table
43
+ */
44
+ public $list_table = null;
45
+
46
+ /**
47
+ * Option to disable access to Stream
48
+ *
49
+ * @var bool
50
+ */
51
+ public $disable_access = false;
52
+
53
+ /**
54
+ * Class applied to the body of the admin screen
55
+ *
56
+ * @var string
57
+ */
58
+ public $admin_body_class = 'wp_stream_screen';
59
+
60
+ /**
61
+ * Slug of the records page
62
+ *
63
+ * @var string
64
+ */
65
+ public $records_page_slug = 'wp_stream';
66
+
67
+ /**
68
+ * Slug of the settings page
69
+ *
70
+ * @var string
71
+ */
72
+ public $settings_page_slug = 'wp_stream_settings';
73
+
74
+ /**
75
+ * Parent page of the records and settings pages
76
+ *
77
+ * @var string
78
+ */
79
+ public $admin_parent_page = 'admin.php';
80
+
81
+ /**
82
+ * Capability name for viewing records
83
+ *
84
+ * @var string
85
+ */
86
+ public $view_cap = 'view_stream';
87
+
88
+ /**
89
+ * Capability name for viewing settings
90
+ *
91
+ * @var string
92
+ */
93
+ public $settings_cap = 'manage_options';
94
+
95
+ /**
96
+ * Total amount of authors to pre-load
97
+ *
98
+ * @var int
99
+ */
100
+ public $preload_users_max = 50;
101
+
102
+ /**
103
+ * Admin notices, collected and displayed on proper action
104
+ *
105
+ * @var array
106
+ */
107
+ public $notices = array();
108
+
109
+ /**
110
+ * Class constructor.
111
+ *
112
+ * @param Plugin $plugin The main Plugin class.
113
+ */
114
+ public function __construct( $plugin ) {
115
+ $this->plugin = $plugin;
116
+
117
+ add_action( 'init', array( $this, 'init' ) );
118
+
119
+ // Ensure function used in various methods is pre-loaded
120
+ if ( ! function_exists( 'is_plugin_active_for_network' ) ) {
121
+ require_once( ABSPATH . '/wp-admin/includes/plugin.php' );
122
+ }
123
+
124
+ // User and role caps
125
+ add_filter( 'user_has_cap', array( $this, 'filter_user_caps' ), 10, 4 );
126
+ add_filter( 'role_has_cap', array( $this, 'filter_role_caps' ), 10, 3 );
127
+
128
+ if ( is_multisite() && is_plugin_active_for_network( $this->plugin->locations['plugin'] ) && ! is_network_admin() ) {
129
+ $options = (array) get_site_option( 'wp_stream_network', array() );
130
+ $option = isset( $options['general_site_access'] ) ? absint( $options['general_site_access'] ) : 1;
131
+
132
+ $this->disable_access = ( $option ) ? false : true;
133
+ }
134
+
135
+ // Register settings page
136
+ if ( ! $this->disable_access ) {
137
+ add_action( 'admin_menu', array( $this, 'register_menu' ) );
138
+ }
139
+
140
+ // Admin notices
141
+ add_action( 'admin_notices', array( $this, 'prepare_admin_notices' ) );
142
+ add_action( 'shutdown', array( $this, 'admin_notices' ) );
143
+
144
+ // Add admin body class
145
+ add_filter( 'admin_body_class', array( $this, 'admin_body_class' ) );
146
+
147
+ // Plugin action links
148
+ add_filter( 'plugin_action_links', array( $this, 'plugin_action_links' ), 10, 2 );
149
+
150
+ // Load admin scripts and styles
151
+ add_action( 'admin_enqueue_scripts', array( $this, 'admin_enqueue_scripts' ) );
152
+ add_action( 'admin_enqueue_scripts', array( $this, 'admin_menu_css' ) );
153
+
154
+ // Reset Streams database
155
+ add_action( 'wp_ajax_wp_stream_reset', array( $this, 'wp_ajax_reset' ) );
156
+
157
+ // Uninstall Streams and Deactivate plugin
158
+ $uninstall = new Uninstall( $this->plugin );
159
+ add_action( 'wp_ajax_wp_stream_uninstall', array( $uninstall, 'uninstall' ) );
160
+
161
+ // Auto purge setup
162
+ add_action( 'wp_loaded', array( $this, 'purge_schedule_setup' ) );
163
+ add_action( 'wp_stream_auto_purge', array( $this, 'purge_scheduled_action' ) );
164
+
165
+ // Ajax users list
166
+ add_action( 'wp_ajax_wp_stream_filters', array( $this, 'ajax_filters' ) );
167
+
168
+ // Ajax user's name by ID
169
+ add_action( 'wp_ajax_wp_stream_get_filter_value_by_id', array( $this, 'get_filter_value_by_id' ) );
170
+
171
+ // Ajax users list
172
+ add_action( 'wp_ajax_wp_stream_filters', array( $this, 'ajax_filters' ) );
173
+
174
+ // Ajax user's name by ID
175
+ add_action( 'wp_ajax_wp_stream_get_filter_value_by_id', array( $this, 'get_filter_value_by_id' ) );
176
+ }
177
+
178
+ /**
179
+ * Load admin classes
180
+ *
181
+ * @action init
182
+ */
183
+ public function init() {
184
+ $this->network = new Network( $this->plugin );
185
+ $this->live_update = new Live_Update( $this->plugin );
186
+ $this->migrate = new Migrate( $this->plugin );
187
+ }
188
+
189
+ /**
190
+ * Output specific updates passed as URL parameters
191
+ *
192
+ * @action admin_notices
193
+ *
194
+ * @return string
195
+ */
196
+ public function prepare_admin_notices() {
197
+ $message = wp_stream_filter_input( INPUT_GET, 'message' );
198
+
199
+ switch ( $message ) {
200
+ case 'settings_reset':
201
+ $this->notice( esc_html__( 'All site settings have been successfully reset.', 'stream' ) );
202
+ break;
203
+ }
204
+ }
205
+
206
+ /**
207
+ * Handle notice messages according to the appropriate context (WP-CLI or the WP Admin)
208
+ *
209
+ * @param string $message
210
+ * @param bool $is_error
211
+ */
212
+ public function notice( $message, $is_error = true ) {
213
+ if ( defined( 'WP_CLI' ) && WP_CLI ) {
214
+ $message = strip_tags( $message );
215
+
216
+ if ( $is_error ) {
217
+ WP_CLI::warning( $message );
218
+ } else {
219
+ WP_CLI::success( $message );
220
+ }
221
+ } else {
222
+ // Trigger admin notices late, so that any notices which occur during page load are displayed
223
+ add_action( 'shutdown', array( $this, 'admin_notices' ) );
224
+
225
+ $notice = compact( 'message', 'is_error' );
226
+
227
+ if ( ! in_array( $notice, $this->notices ) ) {
228
+ $this->notices[] = $notice;
229
+ }
230
+ }
231
+ }
232
+
233
+ /**
234
+ * Show an error or other message in the WP Admin
235
+ *
236
+ * @action shutdown
237
+ */
238
+ public function admin_notices() {
239
+ global $allowedposttags;
240
+
241
+ $custom = array(
242
+ 'progress' => array(
243
+ 'class' => true,
244
+ 'id' => true,
245
+ 'max' => true,
246
+ 'style' => true,
247
+ 'value' => true,
248
+ ),
249
+ );
250
+
251
+ $allowed_html = array_merge( $allowedposttags, $custom );
252
+
253
+ ksort( $allowed_html );
254
+
255
+ foreach ( $this->notices as $notice ) {
256
+ $class_name = empty( $notice['is_error'] ) ? 'updated' : 'error';
257
+ $html_message = sprintf( '<div class="%s">%s</div>', esc_attr( $class_name ), wpautop( $notice['message'] ) );
258
+
259
+ echo wp_kses( $html_message, $allowed_html );
260
+ }
261
+ }
262
+
263
+ /**
264
+ * Register menu page
265
+ *
266
+ * @action admin_menu
267
+ *
268
+ * @return bool|void
269
+ */
270
+ public function register_menu() {
271
+ /**
272
+ * Filter the main admin menu title
273
+ *
274
+ * @return string
275
+ */
276
+ $main_menu_title = apply_filters( 'wp_stream_admin_menu_title', esc_html__( 'Stream', 'stream' ) );
277
+
278
+ /**
279
+ * Filter the main admin menu position
280
+ *
281
+ * Note: Using longtail decimal string to reduce the chance of position conflicts, see Codex
282
+ *
283
+ * @return string
284
+ */
285
+ $main_menu_position = apply_filters( 'wp_stream_menu_position', '2.999999' );
286
+
287
+ /**
288
+ * Filter the main admin page title
289
+ *
290
+ * @return string
291
+ */
292
+ $main_page_title = apply_filters( 'wp_stream_admin_page_title', esc_html__( 'Stream Records', 'stream' ) );
293
+
294
+ $this->screen_id['main'] = add_menu_page(
295
+ $main_page_title,
296
+ $main_menu_title,
297
+ $this->view_cap,
298
+ $this->records_page_slug,
299
+ array( $this, 'render_list_table' ),
300
+ 'div',
301
+ $main_menu_position
302
+ );
303
+
304
+ /**
305
+ * Filter the Settings admin page title
306
+ *
307
+ * @return string
308
+ */
309
+ $settings_page_title = apply_filters( 'wp_stream_settings_form_title', esc_html__( 'Stream Settings', 'stream' ) );
310
+
311
+ $this->screen_id['settings'] = add_submenu_page(
312
+ $this->records_page_slug,
313
+ $settings_page_title,
314
+ esc_html__( 'Settings', 'stream' ),
315
+ $this->settings_cap,
316
+ $this->settings_page_slug,
317
+ array( $this, 'render_settings_page' )
318
+ );
319
+
320
+ if ( isset( $this->screen_id['main'] ) ) {
321
+ /**
322
+ * Fires just before the Stream list table is registered.
323
+ *
324
+ * @return void
325
+ */
326
+ do_action( 'wp_stream_admin_menu_screens' );
327
+
328
+ // Register the list table early, so it associates the column headers with 'Screen settings'
329
+ add_action( 'load-' . $this->screen_id['main'], array( $this, 'register_list_table' ) );
330
+ }
331
+ }
332
+
333
+ /**
334
+ * Enqueue scripts/styles for admin screen
335
+ *
336
+ * @action admin_enqueue_scripts
337
+ *
338
+ * @param string $hook
339
+ *
340
+ * @return void
341
+ */
342
+ public function admin_enqueue_scripts( $hook ) {
343
+ wp_register_script( 'select2', $this->plugin->locations['url'] . 'ui/lib/select2/select2.js', array( 'jquery' ), '3.5.2', true );
344
+ wp_register_style( 'select2', $this->plugin->locations['url'] . 'ui/lib/select2/select2.css', array(), '3.5.2' );
345
+ wp_register_script( 'timeago', $this->plugin->locations['url'] . 'ui/lib/timeago/jquery.timeago.js', array(), '1.4.1', true );
346
+
347
+ $locale = strtolower( substr( get_locale(), 0, 2 ) );
348
+ $file_tmpl = 'ui/lib/timeago/locales/jquery.timeago.%s.js';
349
+
350
+ if ( file_exists( $this->plugin->locations['dir'] . sprintf( $file_tmpl, $locale ) ) ) {
351
+ wp_register_script( 'timeago-locale', $this->plugin->locations['url'] . sprintf( $file_tmpl, $locale ), array( 'timeago' ), '1' );
352
+ } else {
353
+ wp_register_script( 'timeago-locale', $this->plugin->locations['url'] . sprintf( $file_tmpl, 'en' ), array( 'timeago' ), '1' );
354
+ }
355
+
356
+ wp_enqueue_style( 'wp-stream-admin', $this->plugin->locations['url'] . 'ui/css/admin.css', array(), $this->plugin->get_version() );
357
+
358
+ $script_screens = array( 'plugins.php', 'user-edit.php', 'user-new.php', 'profile.php' );
359
+
360
+ if ( in_array( $hook, $this->screen_id ) || in_array( $hook, $script_screens ) ) {
361
+ wp_enqueue_script( 'select2' );
362
+ wp_enqueue_style( 'select2' );
363
+
364
+ wp_enqueue_script( 'timeago' );
365
+ wp_enqueue_script( 'timeago-locale' );
366
+
367
+ wp_enqueue_script( 'wp-stream-admin', $this->plugin->locations['url'] . 'ui/js/admin.js', array( 'jquery', 'select2' ), $this->plugin->get_version() );
368
+ wp_enqueue_script( 'wp-stream-live-updates', $this->plugin->locations['url'] . 'ui/js/live-updates.js', array( 'jquery', 'heartbeat' ), $this->plugin->get_version() );
369
+
370
+ wp_localize_script(
371
+ 'wp-stream-admin',
372
+ 'wp_stream',
373
+ array(
374
+ 'i18n' => array(
375
+ 'confirm_purge' => esc_html__( 'Are you sure you want to delete all Stream activity records from the database? This cannot be undone.', 'stream' ),
376
+ 'confirm_defaults' => esc_html__( 'Are you sure you want to reset all site settings to default? This cannot be undone.', 'stream' ),
377
+ 'confirm_uninstall' => esc_html__( 'Are you sure you want to uninstall and deactivate Stream? This will delete all Stream tables from the database and cannot be undone.', 'stream' ),
378
+ ),
379
+ 'locale' => esc_js( $locale ),
380
+ 'gmt_offset' => get_option( 'gmt_offset' ),
381
+ )
382
+ );
383
+
384
+ wp_localize_script(
385
+ 'wp-stream-live-updates',
386
+ 'wp_stream_live_updates',
387
+ array(
388
+ 'current_screen' => $hook,
389
+ 'current_page' => isset( $_GET['paged'] ) ? esc_js( $_GET['paged'] ) : '1', // input var okay
390
+ 'current_order' => isset( $_GET['order'] ) ? esc_js( $_GET['order'] ) : 'desc', // input var okay
391
+ 'current_query' => wp_stream_json_encode( $_GET ), // input var okay
392
+ 'current_query_count' => count( $_GET ), // input var okay
393
+ )
394
+ );
395
+ }
396
+
397
+ if ( $this->migrate->show_migrate_notice() ) {
398
+ $limit = absint( $this->migrate->limit );
399
+ $record_count = absint( $this->migrate->record_count );
400
+ $chunks = ceil( $record_count / $limit );
401
+ $estimated_time = ( $chunks > 1 ) ? round( ( $chunks * 5 ) / 60 ) : 0;
402
+ $migrate_time_message = ( $estimated_time > 1 ) ? sprintf( esc_html__( 'This will take about %d minutes.', 'stream' ), absint( $estimated_time ) ) : esc_html__( 'This could take a few minutes.', 'stream' );
403
+
404
+ wp_enqueue_script( 'wp-stream-migrate', $this->plugin->locations['url'] . 'ui/js/migrate.js', array( 'jquery' ), $this->plugin->get_version() );
405
+ wp_localize_script(
406
+ 'wp-stream-migrate',
407
+ 'wp_stream_migrate',
408
+ array(
409
+ 'i18n' => array(
410
+ 'migrate_process_title' => esc_html__( 'Migrating Stream Records', 'stream' ),
411
+ 'ignore_migrate_title' => esc_html__( 'No Records Were Migrated', 'stream' ),
412
+ 'migrate_process_message' => esc_html__( 'Please do not exit this page until the process has completed.', 'stream' ) . ' ' . esc_html( $migrate_time_message ),
413
+ 'confirm_start_migrate' => ( $estimated_time > 1 ) ? sprintf( esc_html__( 'Please note: This process will take about %d minutes to complete.', 'stream' ), absint( $estimated_time ) ) : esc_html__( 'Please note: This process could take a few minutes to complete.', 'stream' ),
414
+ 'confirm_migrate_reminder' => esc_html__( 'Please note: Your existing records will not appear in Stream until you have migrated them to your local database.', 'stream' ),
415
+ 'confirm_ignore_migrate' => sprintf( esc_html__( 'Are you sure you want to lose all %s existing Stream records without migrating?', 'stream' ), number_format( $record_count ), ( $estimated_time > 1 && is_multisite() ) ? sprintf( esc_html__( 'about %d', 'stream' ), absint( $estimated_time ) ) : esc_html__( 'a few', 'stream' ) ),
416
+ ),
417
+ 'chunks' => absint( $chunks ),
418
+ 'nonce' => wp_create_nonce( 'wp_stream_migrate-' . absint( get_current_blog_id() ) . absint( get_current_user_id() ) ),
419
+ )
420
+ );
421
+ }
422
+
423
+ /**
424
+ * The maximum number of items that can be updated in bulk without receiving a warning.
425
+ *
426
+ * Stream watches for bulk actions performed in the WordPress Admin (such as updating
427
+ * many posts at once) and warns the user before proceeding if the number of items they
428
+ * are attempting to update exceeds this threshold value. Since Stream will try to save
429
+ * a log for each item, it will take longer than usual to complete the operation.
430
+ *
431
+ * The default threshold is 100 items.
432
+ *
433
+ * @return int
434
+ */
435
+ $bulk_actions_threshold = apply_filters( 'wp_stream_bulk_actions_threshold', 100 );
436
+
437
+ wp_enqueue_script( 'wp-stream-global', $this->plugin->locations['url'] . 'ui/js/global.js', array( 'jquery' ), $this->plugin->get_version() );
438
+ wp_localize_script(
439
+ 'wp-stream-global',
440
+ 'wp_stream_global',
441
+ array(
442
+ 'bulk_actions' => array(
443
+ 'i18n' => array(
444
+ 'confirm_action' => sprintf( esc_html__( '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 ) ) ),
445
+ ),
446
+ 'threshold' => absint( $bulk_actions_threshold ),
447
+ ),
448
+ 'plugins_screen_url' => self_admin_url( 'plugins.php#stream' ),
449
+ )
450
+ );
451
+ }
452
+
453
+ /**
454
+ * Check whether or not the current admin screen belongs to Stream
455
+ *
456
+ * @return bool
457
+ */
458
+ public function is_stream_screen() {
459
+ if ( is_admin() && false !== strpos( wp_stream_filter_input( INPUT_GET, 'page' ), $this->records_page_slug ) ) {
460
+ return true;
461
+ }
462
+
463
+ return false;
464
+ }
465
+
466
+ /**
467
+ * Add a specific body class to all Stream admin screens
468
+ *
469
+ * @param string $classes
470
+ *
471
+ * @filter admin_body_class
472
+ *
473
+ * @return string
474
+ */
475
+ public function admin_body_class( $classes ) {
476
+ $stream_classes = array();
477
+
478
+ if ( $this->is_stream_screen() ) {
479
+ $stream_classes[] = $this->admin_body_class;
480
+
481
+ if ( isset( $_GET['page'] ) ) {
482
+ $stream_classes[] = sanitize_key( $_GET['page'] ); // input var okay
483
+ }
484
+ }
485
+
486
+ /**
487
+ * Filter the Stream admin body classes
488
+ *
489
+ * @return array
490
+ */
491
+ $stream_classes = apply_filters( 'wp_stream_admin_body_classes', $stream_classes );
492
+ $stream_classes = implode( ' ', array_map( 'trim', $stream_classes ) );
493
+
494
+ return sprintf( '%s %s ', $classes, $stream_classes );
495
+ }
496
+
497
+ /**
498
+ * Add menu styles for various WP Admin skins
499
+ *
500
+ * @uses \wp_add_inline_style()
501
+ *
502
+ * @action admin_enqueue_scripts
503
+ */
504
+ public function admin_menu_css() {
505
+ wp_register_style( 'jquery-ui', '//ajax.googleapis.com/ajax/libs/jqueryui/1.10.1/themes/base/jquery-ui.css', array(), '1.10.1' );
506
+ wp_register_style( 'wp-stream-datepicker', $this->plugin->locations['url'] . 'ui/css/datepicker.css', array( 'jquery-ui' ), $this->plugin->get_version() );
507
+ wp_register_style( 'wp-stream-icons', $this->plugin->locations['url'] . 'ui/stream-icons/style.css', array(), $this->plugin->get_version() );
508
+
509
+ // Make sure we're working off a clean version
510
+ if ( ! file_exists( ABSPATH . WPINC . '/version.php' ) ) {
511
+ return;
512
+ }
513
+ include( ABSPATH . WPINC . '/version.php' );
514
+
515
+ if ( ! isset( $wp_version ) ) {
516
+ return;
517
+ }
518
+
519
+ $body_class = $this->admin_body_class;
520
+ $records_page = $this->records_page_slug;
521
+ $stream_url = $this->plugin->locations['url'];
522
+
523
+ if ( version_compare( $wp_version, '3.8-alpha', '>=' ) ) {
524
+ wp_enqueue_style( 'wp-stream-icons' );
525
+
526
+ $css = "
527
+ #toplevel_page_{$records_page} .wp-menu-image:before {
528
+ font-family: 'WP Stream' !important;
529
+ content: '\\73' !important;
530
+ }
531
+ #toplevel_page_{$records_page} .wp-menu-image {
532
+ background-repeat: no-repeat;
533
+ }
534
+ #menu-posts-feedback .wp-menu-image:before {
535
+ font-family: dashicons !important;
536
+ content: '\\f175';
537
+ }
538
+ #adminmenu #menu-posts-feedback div.wp-menu-image {
539
+ background: none !important;
540
+ background-repeat: no-repeat;
541
+ }
542
+ body.{$body_class} #wpbody-content .wrap h2:nth-child(1):before {
543
+ font-family: 'WP Stream' !important;
544
+ content: '\\73';
545
+ padding: 0 8px 0 0;
546
+ }
547
+ ";
548
+ } else {
549
+ $css = "
550
+ #toplevel_page_{$records_page} .wp-menu-image {
551
+ background: url( {$stream_url}ui/stream-icons/menuicon-sprite.png ) 0 90% no-repeat;
552
+ }
553
+ /* Retina Stream Menu Icon */
554
+ @media only screen and (-moz-min-device-pixel-ratio: 1.5),
555
+ only screen and (-o-min-device-pixel-ratio: 3/2),
556
+ only screen and (-webkit-min-device-pixel-ratio: 1.5),
557
+ only screen and (min-device-pixel-ratio: 1.5) {
558
+ #toplevel_page_{$records_page} .wp-menu-image {
559
+ background: url( {$stream_url}ui/stream-icons/menuicon-sprite-2x.png ) 0 90% no-repeat;
560
+ background-size:30px 64px;
561
+ }
562
+ }
563
+ #toplevel_page_{$records_page}.current .wp-menu-image,
564
+ #toplevel_page_{$records_page}.wp-has-current-submenu .wp-menu-image,
565
+ #toplevel_page_{$records_page}:hover .wp-menu-image {
566
+ background-position: top left;
567
+ }
568
+ ";
569
+ }
570
+
571
+ \wp_add_inline_style( 'wp-admin', $css );
572
+ }
573
+
574
+ public function wp_ajax_reset() {
575
+ check_ajax_referer( 'stream_nonce', 'wp_stream_nonce' );
576
+
577
+ if ( ! current_user_can( $this->settings_cap ) ) {
578
+ wp_die(
579
+ esc_html__( "You don't have sufficient privileges to do this action.", 'stream' )
580
+ );
581
+ }
582
+
583
+ $this->erase_stream_records();
584
+
585
+ if ( defined( 'WP_STREAM_TESTS' ) && WP_STREAM_TESTS ) {
586
+ return true;
587
+ }
588
+
589
+ wp_redirect(
590
+ add_query_arg(
591
+ array(
592
+ 'page' => is_network_admin() ? $this->network->network_settings_page_slug : $this->settings_page_slug,
593
+ 'message' => 'data_erased',
594
+ ),
595
+ self_admin_url( $this->admin_parent_page )
596
+ )
597
+ );
598
+
599
+ exit;
600
+ }
601
+
602
+ private function erase_stream_records() {
603
+ global $wpdb;
604
+
605
+ $where = '';
606
+
607
+ if ( is_multisite() && ! is_plugin_active_for_network( $this->plugin->locations['plugin'] ) ) {
608
+ $where .= $wpdb->prepare( ' AND `blog_id` = %d', get_current_blog_id() );
609
+ }
610
+
611
+ $wpdb->query(
612
+ "DELETE `stream`, `meta`
613
+ FROM {$wpdb->stream} AS `stream`
614
+ LEFT JOIN {$wpdb->streammeta} AS `meta`
615
+ ON `meta`.`record_id` = `stream`.`ID`
616
+ WHERE 1=1 {$where};"
617
+ );
618
+ }
619
+
620
+ public function purge_schedule_setup() {
621
+ if ( ! wp_next_scheduled( 'wp_stream_auto_purge' ) ) {
622
+ wp_schedule_event( time(), 'twicedaily', 'wp_stream_auto_purge' );
623
+ }
624
+ }
625
+
626
+ public function purge_scheduled_action() {
627
+ global $wpdb;
628
+
629
+ // Don't purge when in Network Admin unless Stream is network activated
630
+ if (
631
+ is_multisite()
632
+ &&
633
+ is_network_admin()
634
+ &&
635
+ ! is_plugin_active_for_network( $this->plugin->locations['plugin'] )
636
+ ) {
637
+ return;
638
+ }
639
+
640
+ if ( is_multisite() && is_plugin_active_for_network( $this->plugin->locations['plugin'] ) ) {
641
+ $options = (array) get_site_option( 'wp_stream_network', array() );
642
+ } else {
643
+ $options = (array) get_option( 'wp_stream', array() );
644
+ }
645
+
646
+ $days = $options['general_records_ttl'];
647
+ $date = new DateTime( 'now', $timezone = new DateTimeZone( 'UTC' ) );
648
+
649
+ $date->sub( DateInterval::createFromDateString( "$days days" ) );
650
+
651
+ $where = $wpdb->prepare( ' AND `stream`.`created` < %s', $date->format( 'Y-m-d H:i:s' ) );
652
+
653
+ // Multisite but NOT network activated, only purge the current blog
654
+ if ( is_multisite() && ! is_plugin_active_for_network( $this->plugin->locations['plugin'] ) ) {
655
+ $where .= $wpdb->prepare( ' AND `blog_id` = %d', get_current_blog_id() );
656
+ }
657
+
658
+ $wpdb->query(
659
+ "DELETE `stream`, `meta`
660
+ FROM {$wpdb->stream} AS `stream`
661
+ LEFT JOIN {$wpdb->streammeta} AS `meta`
662
+ ON `meta`.`record_id` = `stream`.`ID`
663
+ WHERE 1=1 {$where};"
664
+ );
665
+ }
666
+
667
+ /**
668
+ * @param array $links
669
+ * @param string $file
670
+ *
671
+ * @filter plugin_action_links
672
+ *
673
+ * @return array
674
+ */
675
+ public function plugin_action_links( $links, $file ) {
676
+ if ( plugin_basename( $this->plugin->locations['dir'] . 'stream.php' ) !== $file ) {
677
+ return $links;
678
+ }
679
+
680
+ // Also don't show links in Network Admin if Stream isn't network enabled
681
+ if ( is_network_admin() && is_multisite() && ! is_plugin_active_for_network( $this->plugin->locations['plugin'] ) ) {
682
+ return $links;
683
+ }
684
+
685
+ if ( is_network_admin() ) {
686
+ $admin_page_url = add_query_arg( array( 'page' => $this->network->network_settings_page_slug ), network_admin_url( $this->admin_parent_page ) );
687
+ } else {
688
+ $admin_page_url = add_query_arg( array( 'page' => $this->settings_page_slug ), admin_url( $this->admin_parent_page ) );
689
+ }
690
+
691
+ $links[] = sprintf( '<a href="%s">%s</a>', esc_url( $admin_page_url ), esc_html__( 'Settings', 'default' ) );
692
+
693
+ $url = add_query_arg(
694
+ array(
695
+ 'action' => 'wp_stream_uninstall',
696
+ 'wp_stream_nonce' => wp_create_nonce( 'stream_nonce' ),
697
+ ),
698
+ admin_url( 'admin-ajax.php' )
699
+ );
700
+
701
+ $links[] = sprintf( '<span id="wp_stream_uninstall" class="delete"><a href="%s">%s</a></span>', esc_url( $url ), esc_html__( 'Uninstall', 'stream' ) );
702
+
703
+ return $links;
704
+ }
705
+
706
+ /**
707
+ * Render main page
708
+ */
709
+ public function render_list_table() {
710
+ $this->list_table->prepare_items();
711
+ ?>
712
+ <div class="wrap">
713
+ <h2><?php echo esc_html( get_admin_page_title() ) ?></h2>
714
+ <?php $this->list_table->display() ?>
715
+ </div>
716
+ <?php
717
+ }
718
+
719
+ /**
720
+ * Render settings page
721
+ */
722
+ public function render_settings_page() {
723
+ $option_key = $this->plugin->settings->option_key;
724
+ $form_action = apply_filters( 'wp_stream_settings_form_action', admin_url( 'options.php' ) );
725
+
726
+ $page_description = apply_filters( 'wp_stream_settings_form_description', '' );
727
+
728
+ $sections = $this->plugin->settings->get_fields();
729
+ $active_tab = wp_stream_filter_input( INPUT_GET, 'tab' );
730
+
731
+ wp_enqueue_script( 'wp-stream-settings', $this->plugin->locations['url'] . 'ui/js/settings.js', array( 'jquery' ), $this->plugin->get_version(), true );
732
+ ?>
733
+ <div class="wrap">
734
+ <h2><?php echo esc_html( get_admin_page_title() ) ?></h2>
735
+
736
+ <?php if ( ! empty( $page_description ) ) : ?>
737
+ <p><?php echo esc_html( $page_description ) ?></p>
738
+ <?php endif; ?>
739
+
740
+ <?php settings_errors() ?>
741
+
742
+ <?php if ( count( $sections ) > 1 ) : ?>
743
+ <h2 class="nav-tab-wrapper">
744
+ <?php $i = 0 ?>
745
+ <?php foreach ( $sections as $section => $data ) : ?>
746
+ <?php $i ++ ?>
747
+ <?php $is_active = ( ( 1 === $i && ! $active_tab ) || $active_tab === $section ) ?>
748
+ <a href="<?php echo esc_url( add_query_arg( 'tab', $section ) ) ?>" class="nav-tab<?php if ( $is_active ) { echo esc_attr( ' nav-tab-active' ); } ?>">
749
+ <?php echo esc_html( $data['title'] ) ?>
750
+ </a>
751
+ <?php endforeach; ?>
752
+ </h2>
753
+ <?php endif; ?>
754
+
755
+ <div class="nav-tab-content" id="tab-content-settings">
756
+ <form method="post" action="<?php echo esc_attr( $form_action ) ?>" enctype="multipart/form-data">
757
+ <div class="settings-sections">
758
+ <?php
759
+ $i = 0;
760
+ foreach ( $sections as $section => $data ) {
761
+ $i++;
762
+
763
+ $is_active = ( ( 1 === $i && ! $active_tab ) || $active_tab === $section );
764
+
765
+ if ( $is_active ) {
766
+ settings_fields( $option_key );
767
+ do_settings_sections( $option_key );
768
+ }
769
+ }
770
+ ?>
771
+ </div>
772
+ <?php submit_button() ?>
773
+ </form>
774
+ </div>
775
+ </div>
776
+ <?php
777
+ }
778
+
779
+ /**
780
+ * Instantiate the list table
781
+ */
782
+ public function register_list_table() {
783
+ $this->list_table = new List_Table( $this->plugin, array( 'screen' => $this->screen_id['main'] ) );
784
+ }
785
+
786
+ /**
787
+ * Check if a particular role has access
788
+ *
789
+ * @param string $role
790
+ *
791
+ * @return bool
792
+ */
793
+ private function role_can_view( $role ) {
794
+ if ( in_array( $role, $this->plugin->settings->options['general_role_access'] ) ) {
795
+ return true;
796
+ }
797
+
798
+ return false;
799
+ }
800
+
801
+ /**
802
+ * Filter user caps to dynamically grant our view cap based on allowed roles
803
+ *
804
+ * @param $allcaps
805
+ * @param $caps
806
+ * @param $args
807
+ * @param $user
808
+ *
809
+ * @filter user_has_cap
810
+ *
811
+ * @return array
812
+ */
813
+ public function filter_user_caps( $allcaps, $caps, $args, $user = null ) {
814
+ global $wp_roles;
815
+
816
+ $_wp_roles = isset( $wp_roles ) ? $wp_roles : new WP_Roles();
817
+
818
+ $user = is_a( $user, 'WP_User' ) ? $user : wp_get_current_user();
819
+
820
+ // @see
821
+ // https://github.com/WordPress/WordPress/blob/c67c9565f1495255807069fdb39dac914046b1a0/wp-includes/capabilities.php#L758
822
+ $roles = array_unique(
823
+ array_merge(
824
+ $user->roles,
825
+ array_filter(
826
+ array_keys( $user->caps ),
827
+ array( $_wp_roles, 'is_role' )
828
+ )
829
+ )
830
+ );
831
+
832
+ $stream_view_caps = array( $this->view_cap );
833
+
834
+ foreach ( $caps as $cap ) {
835
+ if ( in_array( $cap, $stream_view_caps ) ) {
836
+ foreach ( $roles as $role ) {
837
+ if ( $this->role_can_view( $role ) ) {
838
+ $allcaps[ $cap ] = true;
839
+
840
+ break 2;
841
+ }
842
+ }
843
+ }
844
+ }
845
+
846
+ return $allcaps;
847
+ }
848
+
849
+ /**
850
+ * Filter role caps to dynamically grant our view cap based on allowed roles
851
+ *
852
+ * @filter role_has_cap
853
+ *
854
+ * @param $allcaps
855
+ * @param $cap
856
+ * @param $role
857
+ *
858
+ * @return array
859
+ */
860
+ public function filter_role_caps( $allcaps, $cap, $role ) {
861
+ $stream_view_caps = array( $this->view_cap );
862
+
863
+ if ( in_array( $cap, $stream_view_caps ) && $this->role_can_view( $role ) ) {
864
+ $allcaps[ $cap ] = true;
865
+ }
866
+
867
+ return $allcaps;
868
+ }
869
+
870
+ /**
871
+ * @action wp_ajax_wp_stream_filters
872
+ */
873
+ public function ajax_filters() {
874
+ switch ( wp_stream_filter_input( INPUT_GET, 'filter' ) ) {
875
+ case 'user_id':
876
+ $users = array_merge(
877
+ array( 0 => (object) array( 'display_name' => 'WP-CLI' ) ),
878
+ get_users()
879
+ );
880
+
881
+ $search = wp_stream_filter_input( INPUT_GET, 'q' );
882
+ if ( $search ) {
883
+ // `search` arg for get_users() is not enough
884
+ $users = array_filter(
885
+ $users,
886
+ function ( $user ) use ( $search ) {
887
+ return false !== mb_strpos( mb_strtolower( $user->display_name ), mb_strtolower( $search ) );
888
+ }
889
+ );
890
+ }
891
+
892
+ if ( count( $users ) > $this->preload_users_max ) {
893
+ $users = array_slice( $users, 0, $this->preload_users_max );
894
+ }
895
+
896
+ // Get gravatar / roles for final result set
897
+ $results = $this->get_users_record_meta( $users );
898
+
899
+ break;
900
+ }
901
+
902
+ if ( isset( $results ) ) {
903
+ echo wp_stream_json_encode( array_values( $results ) ); // xss ok
904
+ }
905
+
906
+ if ( defined( 'WP_STREAM_TESTS' ) && WP_STREAM_TESTS ) {
907
+ return;
908
+ }
909
+
910
+ die();
911
+ }
912
+
913
+ /**
914
+ * @action wp_ajax_wp_stream_get_filter_value_by_id
915
+ */
916
+ public function get_filter_value_by_id() {
917
+ $filter = wp_stream_filter_input( INPUT_POST, 'filter' );
918
+
919
+ switch ( $filter ) {
920
+ case 'user_id':
921
+ $id = wp_stream_filter_input( INPUT_POST, 'id' );
922
+
923
+ if ( '0' === $id ) {
924
+ $value = 'WP-CLI';
925
+
926
+ break;
927
+ }
928
+
929
+ $user = get_userdata( $id );
930
+
931
+ if ( ! $user || is_wp_error( $user ) ) {
932
+ $value = '';
933
+ } else {
934
+ $value = $user->display_name;
935
+ }
936
+
937
+ break;
938
+ default:
939
+ $value = '';
940
+ }
941
+
942
+ echo wp_stream_json_encode( $value ); // xss ok
943
+
944
+ if ( defined( 'WP_STREAM_TESTS' ) && WP_STREAM_TESTS ) {
945
+ return;
946
+ }
947
+
948
+ die();
949
+ }
950
+
951
+ public function get_users_record_meta( $authors ) {
952
+ $authors_records = array();
953
+
954
+ foreach ( $authors as $user_id => $args ) {
955
+ $author = new Author( $user_id );
956
+
957
+ $authors_records[ $user_id ] = array(
958
+ 'text' => $author->get_display_name(),
959
+ 'id' => $user_id,
960
+ 'label' => $author->get_display_name(),
961
+ 'icon' => $author->get_avatar_src( 32 ),
962
+ 'title' => '',
963
+ );
964
+ }
965
+
966
+ return $authors_records;
967
+ }
968
+
969
+ /**
970
+ * Get user meta in a way that is also safe for VIP
971
+ *
972
+ * @param int $user_id
973
+ * @param string $meta_key
974
+ * @param bool $single (optional)
975
+ *
976
+ * @return mixed
977
+ */
978
+ function get_user_meta( $user_id, $meta_key, $single = true ) {
979
+ if ( wp_stream_is_vip() && function_exists( 'get_user_attribute' ) ) {
980
+ return get_user_attribute( $user_id, $meta_key );
981
+ }
982
+ return get_user_meta( $user_id, $meta_key, $single );
983
+ }
984
+
985
+ /**
986
+ * Update user meta in a way that is also safe for VIP
987
+ *
988
+ * @param int $user_id
989
+ * @param string $meta_key
990
+ * @param mixed $meta_value
991
+ * @param mixed $prev_value (optional)
992
+ *
993
+ * @return int|bool
994
+ */
995
+ function update_user_meta( $user_id, $meta_key, $meta_value, $prev_value = '' ) {
996
+ if ( wp_stream_is_vip() && function_exists( 'update_user_attribute' ) ) {
997
+ return update_user_attribute( $user_id, $meta_key, $meta_value );
998
+ }
999
+ return update_user_meta( $user_id, $meta_key, $meta_value, $prev_value );
1000
+ }
1001
+
1002
+ /**
1003
+ * Delete user meta in a way that is also safe for VIP
1004
+ *
1005
+ * @param int $user_id
1006
+ * @param string $meta_key
1007
+ * @param mixed $meta_value (optional)
1008
+ *
1009
+ * @return bool
1010
+ */
1011
+ function delete_user_meta( $user_id, $meta_key, $meta_value = '' ) {
1012
+ if ( wp_stream_is_vip() && function_exists( 'delete_user_attribute' ) ) {
1013
+ return delete_user_attribute( $user_id, $meta_key, $meta_value );
1014
+ }
1015
+ return delete_user_meta( $user_id, $meta_key, $meta_value );
1016
+ }
1017
+
1018
+ }
classes/class-author.php ADDED
@@ -0,0 +1,308 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace WP_Stream;
3
+
4
+ class Author {
5
+ /**
6
+ * Hold Plugin class
7
+ * @var Plugin
8
+ */
9
+ public $plugin;
10
+
11
+ /**
12
+ * @var int
13
+ */
14
+ public $id;
15
+
16
+ /**
17
+ * @var array
18
+ */
19
+ public $meta = array();
20
+
21
+ /**
22
+ * @var \WP_User
23
+ */
24
+ protected $user;
25
+
26
+ /**
27
+ * Class constructor.
28
+ *
29
+ * @param int $user_id The user ID.
30
+ * @param array|string $user_meta The user meta array, or a serialized string of user meta.
31
+ */
32
+ function __construct( $user_id, $user_meta = array() ) {
33
+ $this->id = absint( $user_id );
34
+ $this->meta = maybe_unserialize( $user_meta );
35
+
36
+ if ( $this->id ) {
37
+ $this->user = new \WP_User( $this->id );
38
+ }
39
+
40
+ $this->plugin = wp_stream_get_instance();
41
+ }
42
+
43
+ /**
44
+ * Get various user meta data
45
+ *
46
+ * @param string $name
47
+ *
48
+ * @throws \Exception
49
+ *
50
+ * @return string
51
+ */
52
+ function __get( $name ) {
53
+ if ( 'display_name' === $name ) {
54
+ return $this->get_display_name();
55
+ } elseif ( 'avatar_img' === $name ) {
56
+ return $this->get_avatar_img();
57
+ } elseif ( 'avatar_src' === $name ) {
58
+ return $this->get_avatar_src();
59
+ } elseif ( 'role' === $name ) {
60
+ return $this->get_role();
61
+ } elseif ( 'agent' === $name ) {
62
+ return $this->get_agent();
63
+ } elseif ( ! empty( $this->user ) && 0 !== $this->user->ID ) {
64
+ return $this->user->$name;
65
+ } else {
66
+ throw new \Exception( "Unrecognized magic '$name'" );
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Get the display name of the user
72
+ *
73
+ * @return string
74
+ */
75
+ function get_display_name() {
76
+ if ( 0 === $this->id ) {
77
+ if ( isset( $this->meta['system_user_name'] ) ) {
78
+ return esc_html( $this->meta['system_user_name'] );
79
+ } elseif ( 'wp_cli' === $this->get_current_agent() ) {
80
+ return 'WP-CLI'; // No translation needed
81
+ }
82
+ return esc_html__( 'N/A', 'stream' );
83
+ } else {
84
+ if ( $this->is_deleted() ) {
85
+ if ( ! empty( $this->meta['display_name'] ) ) {
86
+ return $this->meta['display_name'];
87
+ } elseif ( ! empty( $this->meta['user_login'] ) ) {
88
+ return $this->meta['user_login'];
89
+ } else {
90
+ return esc_html__( 'N/A', 'stream' );
91
+ }
92
+ } elseif ( ! empty( $this->user->display_name ) ) {
93
+ return $this->user->display_name;
94
+ } else {
95
+ return $this->user->user_login;
96
+ }
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Get the agent of the user
102
+ *
103
+ * @return string
104
+ */
105
+ function get_agent() {
106
+ $agent = '';
107
+
108
+ if ( ! empty( $this->meta['agent'] ) ) {
109
+ $agent = $this->meta['agent'];
110
+ } elseif ( ! empty( $this->meta['is_wp_cli'] ) ) {
111
+ $agent = 'wp_cli'; // legacy
112
+ }
113
+
114
+ return $agent;
115
+ }
116
+
117
+ /**
118
+ * Return a Gravatar image as an HTML element.
119
+ *
120
+ * This function will not return an avatar if "Show Avatars" is unchecked in Settings > Discussion.
121
+ *
122
+ * @param int $size (optional) Size of Gravatar to return (in pixels), max is 512, default is 80
123
+ *
124
+ * @return string|bool An img HTML element, or false if avatars are disabled
125
+ */
126
+ function get_avatar_img( $size = 80 ) {
127
+ if ( ! get_option( 'show_avatars' ) ) {
128
+ return false;
129
+ }
130
+
131
+ if ( 0 === $this->id ) {
132
+ $url = $this->plugin->locations['url'] . 'ui/stream-icons/wp-cli.png';
133
+ $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 ) );
134
+ } else {
135
+ if ( $this->is_deleted() && isset( $this->meta['user_email'] ) ) {
136
+ $email = $this->meta['user_email'];
137
+ $avatar = get_avatar( $email, $size );
138
+ } else {
139
+ $avatar = get_avatar( $this->id, $size );
140
+ }
141
+ }
142
+
143
+ return $avatar;
144
+ }
145
+
146
+ /**
147
+ * Return the URL of a Gravatar image.
148
+ *
149
+ * @param int $size (optional) Size of Gravatar to return (in pixels), max is 512, default is 80
150
+ *
151
+ * @return string|bool Gravatar image URL, or false on failure
152
+ */
153
+ function get_avatar_src( $size = 80 ) {
154
+ $img = $this->get_avatar_img( $size );
155
+
156
+ if ( ! $img ) {
157
+ return false;
158
+ }
159
+
160
+ if ( 1 === preg_match( '/src=([\'"])(.*?)\1/', $img, $matches ) ) {
161
+ $src = html_entity_decode( $matches[2] );
162
+ } else {
163
+ return false;
164
+ }
165
+
166
+ return $src;
167
+ }
168
+
169
+ /**
170
+ * Tries to find a label for the record's user_role.
171
+ *
172
+ * If the user_role exists, use the label associated with it.
173
+ *
174
+ * Otherwise, if there is a user role label stored as Stream meta then use that.
175
+ * Otherwise, if the user exists, use the label associated with their current role.
176
+ * Otherwise, use the role slug as the label.
177
+ *
178
+ * @return string
179
+ */
180
+ function get_role() {
181
+ global $wp_roles;
182
+
183
+ if ( ! empty( $this->meta['user_role'] ) && isset( $wp_roles->role_names[ $this->meta['user_role'] ] ) ) {
184
+ $user_role = $wp_roles->role_names[ $this->meta['user_role'] ];
185
+ } elseif ( ! empty( $this->meta['user_role_label'] ) ) {
186
+ $user_role = $this->meta['user_role_label'];
187
+ } elseif ( isset( $this->user->roles[0] ) && isset( $wp_roles->role_names[ $this->user->roles[0] ] ) ) {
188
+ $user_role = $wp_roles->role_names[ $this->user->roles[0] ];
189
+ } else {
190
+ $user_role = '';
191
+ }
192
+
193
+ return $user_role;
194
+ }
195
+
196
+ /**
197
+ * Construct a URL for viewing user-specific records
198
+ *
199
+ * @return string
200
+ */
201
+ function get_records_page_url() {
202
+ $url = add_query_arg(
203
+ array(
204
+ 'page' => $this->plugin->admin->records_page_slug,
205
+ 'user_id' => absint( $this->id ),
206
+ ),
207
+ self_admin_url( $this->plugin->admin->admin_parent_page )
208
+ );
209
+
210
+ return $url;
211
+ }
212
+
213
+ /**
214
+ * True if user no longer exists, otherwise false
215
+ *
216
+ * @return bool
217
+ */
218
+ function is_deleted() {
219
+ return ( 0 !== $this->id && 0 === $this->user->ID );
220
+ }
221
+
222
+ /**
223
+ * True if user is WP-CLI, otherwise false
224
+ *
225
+ * @return bool
226
+ */
227
+ function is_wp_cli() {
228
+ return ( 'wp_cli' === $this->get_agent() );
229
+ }
230
+
231
+ /**
232
+ * True if doing WP Cron, otherwise false
233
+ *
234
+ * Note: If native WP Cron has been disabled and you are
235
+ * hitting the cron endpoint with a system cron job, this
236
+ * method will always return false.
237
+ *
238
+ * @return bool
239
+ */
240
+ function is_doing_wp_cron() {
241
+ return (
242
+ wp_stream_is_cron_enabled()
243
+ &&
244
+ defined( 'DOING_CRON' )
245
+ &&
246
+ DOING_CRON
247
+ );
248
+ }
249
+
250
+ /**
251
+ * @return string
252
+ */
253
+ function __toString() {
254
+ return $this->get_display_name();
255
+ }
256
+
257
+ /**
258
+ * Look at the environment to detect if an agent is being used
259
+ *
260
+ * @return string
261
+ */
262
+ function get_current_agent() {
263
+ $agent = '';
264
+
265
+ if ( defined( '\WP_CLI' ) && \WP_CLI ) {
266
+ $agent = 'wp_cli';
267
+ } elseif ( $this->is_doing_wp_cron() ) {
268
+ $agent = 'wp_cron';
269
+ }
270
+
271
+ /**
272
+ * Filter the current agent string
273
+ *
274
+ * @return string
275
+ */
276
+ $agent = apply_filters( 'wp_stream_current_agent', $agent );
277
+
278
+ return $agent;
279
+ }
280
+
281
+ /**
282
+ * Get the agent label
283
+ *
284
+ * @param string $agent
285
+ *
286
+ * @return string
287
+ */
288
+ function get_agent_label( $agent ) {
289
+ if ( 'wp_cli' === $agent ) {
290
+ $label = esc_html__( 'via WP-CLI', 'stream' );
291
+ } elseif ( 'wp_cron' === $agent ) {
292
+ $label = esc_html__( 'during WP Cron', 'stream' );
293
+ } else {
294
+ $label = '';
295
+ }
296
+
297
+ /**
298
+ * Filter agent labels
299
+ *
300
+ * @param string $agent
301
+ *
302
+ * @return string
303
+ */
304
+ $label = apply_filters( 'wp_stream_agent_label', $label, $agent );
305
+
306
+ return $label;
307
+ }
308
+ }
classes/class-cli.php ADDED
@@ -0,0 +1,221 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * Stream command for WP-CLI
4
+ *
5
+ * @see https://github.com/wp-cli/wp-cli
6
+ */
7
+ namespace WP_Stream;
8
+
9
+ class CLI extends \WP_CLI_Command {
10
+ /**
11
+ * Query a set of Stream records.
12
+ *
13
+ * ## OPTIONS
14
+ *
15
+ * [--fields=<fields>]
16
+ * : Limit the output to specific object fields.
17
+ *
18
+ * [--<field>=<value>]
19
+ * : One or more args to pass to WP_Stream_Query.
20
+ *
21
+ * [--format=<format>]
22
+ * : Accepted values: table, count, json, json_pretty, csv. Default: table
23
+ *
24
+ * ## AVAILABLE FIELDS TO QUERY
25
+ *
26
+ * You can build a query from these fields:
27
+ *
28
+ * * user_id
29
+ * * user_id__in
30
+ * * user_id__not_in
31
+ * * user_role
32
+ * * user_role__in
33
+ * * user_role__not_in
34
+ * * date
35
+ * * date_from
36
+ * * date_to
37
+ * * date_after
38
+ * * date_before
39
+ * * ip
40
+ * * ip__in
41
+ * * ip__not_in
42
+ * * connector
43
+ * * connector__in
44
+ * * connector__not_in
45
+ * * context
46
+ * * context__in
47
+ * * context__not_in
48
+ * * action
49
+ * * action__in
50
+ * * action__not_in
51
+ * * search
52
+ * * search_field
53
+ * * record
54
+ * * record__in
55
+ * * record__not_in
56
+ * * records_per_page
57
+ * * paged
58
+ * * order
59
+ * * orderby
60
+ *
61
+ * ## AVAILABLE FIELDS
62
+ *
63
+ * These fields will be displayed by default for each post:
64
+ *
65
+ * * created
66
+ * * ip
67
+ * * user_id
68
+ * * user_role
69
+ * * summary
70
+ *
71
+ * These fields are optionally available:
72
+ *
73
+ * * ID
74
+ * * site_id
75
+ * * blog_id
76
+ * * object_id
77
+ * * connector
78
+ * * context
79
+ * * action
80
+ *
81
+ * ## EXAMPLES
82
+ *
83
+ * wp stream query --user_role__not_in=administrator --date_after=2015-01-01T12:00:00
84
+ * wp stream query --user_id=1 --action=login --records_per_page=50 --fields=created
85
+ *
86
+ * @see WP_Stream_Query
87
+ * @see https://github.com/wp-stream/stream/wiki/WP-CLI-Command
88
+ * @see https://github.com/wp-stream/stream/wiki/Query-Reference
89
+ */
90
+ public function query( $args, $assoc_args ) {
91
+ unset( $args );
92
+
93
+ $query_args = array();
94
+ $formatted_records = array();
95
+
96
+ $this->connection();
97
+
98
+ if ( empty( $assoc_args['fields'] ) ) {
99
+ $fields = array( 'created', 'ip', 'user_id', 'user_role', 'summary' );
100
+ } else {
101
+ $fields = explode( ',', $assoc_args['fields'] );
102
+ }
103
+
104
+ foreach ( $assoc_args as $key => $value ) {
105
+ if ( 'format' === $key ) {
106
+ continue;
107
+ }
108
+
109
+ $query_args[ $key ] = $value;
110
+ }
111
+
112
+ $query_args['fields'] = implode( ',', $fields );
113
+
114
+ $records = wp_stream_get_instance()->db->query->query( $query_args );
115
+
116
+ // Make structure Formatter compatible
117
+ foreach ( (array) $records as $key => $record ) {
118
+ $formatted_records[ $key ] = array();
119
+
120
+ // Catch any fields missing in records
121
+ foreach ( $fields as $field ) {
122
+ if ( ! array_key_exists( $field, $record ) ) {
123
+ $record->$field = null;
124
+ }
125
+ }
126
+
127
+ foreach ( $record as $field_name => $field ) {
128
+
129
+ $formatted_records[ $key ] = array_merge(
130
+ $formatted_records[ $key ],
131
+ $this->format_field( $field_name, $field )
132
+ );
133
+ }
134
+ }
135
+
136
+ if ( isset( $assoc_args['format'] ) && 'table' !== $assoc_args['format'] ) {
137
+ if ( 'count' === $assoc_args['format'] ) {
138
+ WP_CLI::line( count( $records ) );
139
+ }
140
+
141
+ if ( 'json' === $assoc_args['format'] ) {
142
+ WP_CLI::line( wp_stream_json_encode( $formatted_records ) );
143
+ }
144
+
145
+ if ( 'json_pretty' === $assoc_args['format'] ) {
146
+ if ( version_compare( PHP_VERSION, '5.4', '<' ) ) {
147
+ WP_CLI::line( wp_stream_json_encode( $formatted_records ) ); // xss ok
148
+ } else {
149
+ WP_CLI::line( wp_stream_json_encode( $formatted_records, JSON_PRETTY_PRINT ) ); // xss ok
150
+ }
151
+ }
152
+
153
+ if ( 'csv' === $assoc_args['format'] ) {
154
+ WP_CLI::line( $this->csv_format( $formatted_records ) );
155
+ }
156
+
157
+ return;
158
+ }
159
+
160
+ $formatter = new \WP_CLI\Formatter(
161
+ $assoc_args,
162
+ $fields
163
+ );
164
+
165
+ $formatter->display_items( $formatted_records );
166
+ }
167
+
168
+ /**
169
+ * Convert any field to a flat array.
170
+ *
171
+ * @param string $name The output array element name
172
+ * @param mixed $object Any value to be converted to an array
173
+ *
174
+ * @return array The flat array
175
+ */
176
+ private function format_field( $name, $object ) {
177
+ $array = array();
178
+
179
+ if ( is_object( $object ) ) {
180
+ foreach ( $object as $key => $property ) {
181
+ $array = array_merge( $array, $this->format_field( $name . '.' . $key, $property ) );
182
+ }
183
+ } elseif ( is_array( $object ) ) {
184
+ $array[ $name ] = $object[0];
185
+ } else {
186
+ $array[ $name ] = $object;
187
+ }
188
+
189
+ return $array;
190
+ }
191
+
192
+ /**
193
+ * Convert an array of flat records to CSV
194
+ *
195
+ * @param array $array The input array of records
196
+ *
197
+ * @return string The CSV output
198
+ */
199
+ private function csv_format( $array ) {
200
+ $output = fopen( 'php://output', 'w' );
201
+
202
+ foreach ( $array as $line ) {
203
+ fputcsv( $output, $line );
204
+ }
205
+
206
+ fclose( $output );
207
+ }
208
+
209
+ /**
210
+ * Checks for a Stream connection and displays an error or success message.
211
+ *
212
+ * @return void
213
+ */
214
+ private function connection() {
215
+ $query = wp_stream_get_instance()->db->query->query( array( 'records_per_page' => 1, 'fields' => 'created' ) );
216
+
217
+ if ( ! $query ) {
218
+ WP_CLI::error( esc_html__( 'SITE IS DISCONNECTED', 'stream' ) );
219
+ }
220
+ }
221
+ }
classes/class-connector.php ADDED
@@ -0,0 +1,237 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace WP_Stream;
3
+
4
+ abstract class Connector {
5
+ /**
6
+ * Connector slug
7
+ *
8
+ * @var string
9
+ */
10
+ public $name = null;
11
+
12
+ /**
13
+ * Actions registered for this connector
14
+ *
15
+ * @var array
16
+ */
17
+ public $actions = array();
18
+
19
+ /**
20
+ * Store delayed logs
21
+ *
22
+ * @var array
23
+ */
24
+ public $delayed = array();
25
+
26
+ /**
27
+ * Previous Stream entry in same request
28
+ *
29
+ * @var int
30
+ */
31
+ public $prev_stream = null;
32
+
33
+ /**
34
+ * Register all context hooks
35
+ */
36
+ public function register() {
37
+ $class_name = get_called_class();
38
+ $class = new $class_name;
39
+
40
+ foreach ( $class->actions as $action ) {
41
+ add_action( $action, array( $class, 'callback' ), null, 5 );
42
+ }
43
+
44
+ add_filter( 'wp_stream_action_links_' . $class->name, array( $class, 'action_links' ), 10, 2 );
45
+ }
46
+
47
+ /**
48
+ * Callback for all registered hooks throughout Stream
49
+ * Looks for a class method with the convention: "callback_{action name}"
50
+ */
51
+ public function callback() {
52
+ $action = current_filter();
53
+ $callback = array( $this, 'callback_' . preg_replace( '/[^a-z0-9_\-]/', '_', $action ) );
54
+
55
+ // For the sake of testing, trigger an action with the name of the callback
56
+ if ( defined( 'WP_STREAM_TESTS' ) && WP_STREAM_TESTS ) {
57
+ /**
58
+ * Action fires during testing to test the current callback
59
+ *
60
+ * @param array $callback Callback name
61
+ */
62
+ do_action( 'wp_stream_test_' . $callback[1] );
63
+ }
64
+
65
+ // Call the real function
66
+ if ( is_callable( $callback ) ) {
67
+ return call_user_func_array( $callback, func_get_args() );
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Add action links to Stream drop row in admin list screen
73
+ *
74
+ * @param array $links Previous links registered
75
+ * @param object $record Stream record
76
+ *
77
+ * @filter wp_stream_action_links_{connector}
78
+ *
79
+ * @return array Action links
80
+ */
81
+ public function action_links( $links, $record ) {
82
+ unset( $record );
83
+ return $links;
84
+ }
85
+
86
+ /**
87
+ * Log handler
88
+ *
89
+ * @param string $message sprintf-ready error message string
90
+ * @param array $args sprintf (and extra) arguments to use
91
+ * @param int $object_id Target object id
92
+ * @param string $context Context of the event
93
+ * @param string $action Action of the event
94
+ * @param int $user_id User responsible for the event
95
+ *
96
+ * @return bool
97
+ */
98
+ public function log( $message, $args, $object_id, $context, $action, $user_id = null ) {
99
+ $class = get_called_class();
100
+ $connector = str_replace( array( 'WP_Stream\\', 'Connector_' ), array( '', '' ), $class );
101
+
102
+ $data = apply_filters(
103
+ 'wp_stream_log_data',
104
+ compact( 'connector', 'message', 'args', 'object_id', 'context', 'action', 'user_id' )
105
+ );
106
+
107
+ if ( ! $data ) {
108
+ return false;
109
+ } else {
110
+ $connector = $data['connector'];
111
+ $message = $data['message'];
112
+ $args = $data['args'];
113
+ $object_id = $data['object_id'];
114
+ $context = $data['context'];
115
+ $action = $data['action'];
116
+ $user_id = $data['user_id'];
117
+ }
118
+
119
+ return call_user_func_array( array( wp_stream_get_instance()->log, 'log' ), compact( 'connector', 'message', 'args', 'object_id', 'context', 'action', 'user_id' ) );
120
+ }
121
+
122
+ /**
123
+ * Save log data till shutdown, so other callbacks would be able to override
124
+ *
125
+ * @param string $handle Special slug to be shared with other actions
126
+ * @note param mixed $arg1 Extra arguments to sent to log()
127
+ * @note param param mixed $arg2, etc..
128
+ */
129
+ public function delayed_log( $handle ) {
130
+ $args = func_get_args();
131
+
132
+ array_shift( $args );
133
+
134
+ $this->delayed[ $handle ] = $args;
135
+
136
+ add_action( 'shutdown', array( $this, 'delayed_log_commit' ) );
137
+ }
138
+
139
+ /**
140
+ * Commit delayed logs saved by @delayed_log
141
+ */
142
+ public function delayed_log_commit() {
143
+ foreach ( $this->delayed as $handle => $args ) {
144
+ call_user_func_array( array( $this, 'log' ), $args );
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Compare two values and return changed keys if they are arrays
150
+ *
151
+ * @param mixed $old_value Value before change
152
+ * @param mixed $new_value Value after change
153
+ * @param bool|int $deep Get array children changes keys as well, not just parents
154
+ *
155
+ * @return array
156
+ */
157
+ public function get_changed_keys( $old_value, $new_value, $deep = false ) {
158
+ if ( ! is_array( $old_value ) && ! is_array( $new_value ) ) {
159
+ return array();
160
+ }
161
+
162
+ if ( ! is_array( $old_value ) ) {
163
+ return array_keys( $new_value );
164
+ }
165
+
166
+ if ( ! is_array( $new_value ) ) {
167
+ return array_keys( $old_value );
168
+ }
169
+
170
+ $diff = array_udiff_assoc(
171
+ $old_value,
172
+ $new_value,
173
+ function( $value1, $value2 ) {
174
+ return maybe_serialize( $value1 ) !== maybe_serialize( $value2 );
175
+ }
176
+ );
177
+
178
+ $result = array_keys( $diff );
179
+
180
+ // find unexisting keys in old or new value
181
+ $common_keys = array_keys( array_intersect_key( $old_value, $new_value ) );
182
+ $unique_keys_old = array_values( array_diff( array_keys( $old_value ), $common_keys ) );
183
+ $unique_keys_new = array_values( array_diff( array_keys( $new_value ), $common_keys ) );
184
+
185
+ $result = array_merge( $result, $unique_keys_old, $unique_keys_new );
186
+
187
+ // remove numeric indexes
188
+ $result = array_filter(
189
+ $result,
190
+ function( $value ) {
191
+ // @codingStandardsIgnoreStart
192
+ // check if is not valid number (is_int, is_numeric and ctype_digit are not enough)
193
+ return (string) (int) $value !== (string) $value;
194
+ // @codingStandardsIgnoreEnd
195
+ }
196
+ );
197
+
198
+ $result = array_values( array_unique( $result ) );
199
+
200
+ if ( false === $deep ) {
201
+ return $result; // Return an numerical based array with changed TOP PARENT keys only
202
+ }
203
+
204
+ $result = array_fill_keys( $result, null );
205
+
206
+ foreach ( $result as $key => $val ) {
207
+ if ( in_array( $key, $unique_keys_old ) ) {
208
+ $result[ $key ] = false; // Removed
209
+ } elseif ( in_array( $key, $unique_keys_new ) ) {
210
+ $result[ $key ] = true; // Added
211
+ } elseif ( $deep ) { // Changed, find what changed, only if we're allowed to explore a new level
212
+ if ( is_array( $old_value[ $key ] ) && is_array( $new_value[ $key ] ) ) {
213
+ $inner = array();
214
+ $parent = $key;
215
+ $deep--;
216
+ $changed = $this->get_changed_keys( $old_value[ $key ], $new_value[ $key ], $deep );
217
+ foreach ( $changed as $child => $change ) {
218
+ $inner[ $parent . '::' . $child ] = $change;
219
+ }
220
+ $result[ $key ] = 0; // Changed parent which has a changed children
221
+ $result = array_merge( $result, $inner );
222
+ }
223
+ }
224
+ }
225
+
226
+ return $result;
227
+ }
228
+
229
+ /**
230
+ * Allow connectors to determine if their dependencies is satisfied or not
231
+ *
232
+ * @return bool
233
+ */
234
+ public function is_dependency_satisfied() {
235
+ return true;
236
+ }
237
+ }
classes/class-connectors.php ADDED
@@ -0,0 +1,190 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace WP_Stream;
3
+
4
+ class Connectors {
5
+ /**
6
+ * Hold Plugin class
7
+ * @var Plugin
8
+ */
9
+ public $plugin;
10
+
11
+ /**
12
+ * Connectors registered
13
+ *
14
+ * @var array
15
+ */
16
+ public $connectors = array();
17
+
18
+ /**
19
+ * Contexts registered to Connectors
20
+ *
21
+ * @var array
22
+ */
23
+ public $contexts = array();
24
+
25
+ /**
26
+ * Action taxonomy terms
27
+ * Holds slug to localized label association
28
+ *
29
+ * @var array
30
+ */
31
+ public $term_labels = array(
32
+ 'stream_connector' => array(),
33
+ 'stream_context' => array(),
34
+ 'stream_action' => array(),
35
+ );
36
+
37
+ /**
38
+ * Admin notice messages
39
+ *
40
+ * @var array
41
+ */
42
+ protected $admin_notices = array();
43
+
44
+ /**
45
+ * Class constructor.
46
+ *
47
+ * @param Plugin $plugin The main Plugin class.
48
+ */
49
+ public function __construct( $plugin ) {
50
+ $this->plugin = $plugin;
51
+ $this->load_connectors();
52
+ }
53
+
54
+ /**
55
+ * Load built-in connectors
56
+ */
57
+ public function load_connectors() {
58
+ $connectors = array(
59
+ // Core
60
+ 'blogs',
61
+ 'comments',
62
+ 'editor',
63
+ 'installer',
64
+ 'media',
65
+ 'menus',
66
+ 'posts',
67
+ 'settings',
68
+ 'taxonomies',
69
+ 'users',
70
+ 'widgets',
71
+
72
+ // Extras
73
+ 'acf',
74
+ 'bbpress',
75
+ 'buddypress',
76
+ 'edd',
77
+ 'gravityforms',
78
+ 'jetpack',
79
+ 'woocommerce',
80
+ 'wordpress-seo',
81
+ );
82
+
83
+ $classes = array();
84
+ foreach ( $connectors as $connector ) {
85
+ include_once $this->plugin->locations['dir'] . '/connectors/class-connector-' . $connector .'.php';
86
+ $class_name = sprintf( '\WP_Stream\Connector_%s', str_replace( '-', '_', $connector ) );
87
+ if ( ! class_exists( $class_name ) ) {
88
+ continue;
89
+ }
90
+ $class = new $class_name( $this->plugin->log );
91
+ if ( ! method_exists( $class, 'is_dependency_satisfied' ) ) {
92
+ continue;
93
+ }
94
+ if ( $class->is_dependency_satisfied() ) {
95
+ $classes[] = $class;
96
+ }
97
+ }
98
+
99
+ if ( empty( $classes ) ) {
100
+ return;
101
+ }
102
+
103
+ /**
104
+ * Allows for adding additional connectors via classes that extend Connector.
105
+ *
106
+ * @param array $classes An array of connector class names.
107
+ */
108
+ $this->connectors = apply_filters( 'wp_stream_connectors', $classes );
109
+
110
+ foreach ( $this->connectors as $connector ) {
111
+ if ( ! method_exists( $connector, 'get_label' ) ) {
112
+ continue;
113
+ }
114
+ $this->term_labels['stream_connector'][ $connector->name ] = $connector->get_label();
115
+ }
116
+
117
+ // Get excluded connectors
118
+ $excluded_connectors = array();
119
+
120
+ foreach ( $this->connectors as $connector ) {
121
+ if ( ! method_exists( $connector, 'get_label' ) ) {
122
+ $this->plugin->admin->notice( sprintf( __( "%s class wasn't loaded because it doesn't implement the get_label method.", 'stream' ), $connector->name, 'Connector' ), true );
123
+ continue;
124
+ }
125
+ if ( ! method_exists( $connector, 'register' ) ) {
126
+ $this->plugin->admin->notice( sprintf( __( "%s class wasn't loaded because it doesn't implement the register method.", 'stream' ), $connector->name, 'Connector' ), true );
127
+ continue;
128
+ }
129
+ if ( ! method_exists( $connector, 'get_context_labels' ) ) {
130
+ $this->plugin->admin->notice( sprintf( __( "%s class wasn't loaded because it doesn't implement the get_context_labels method.", 'stream' ), $connector->name, 'Connector' ), true );
131
+ continue;
132
+ }
133
+ if ( ! method_exists( $connector, 'get_action_labels' ) ) {
134
+ $this->plugin->admin->notice( sprintf( __( "%s class wasn't loaded because it doesn't implement the get_action_labels method.", 'stream' ), $connector->name, 'Connector' ), true );
135
+ continue;
136
+ }
137
+
138
+ // Check if the connectors extends the Connector class, if not skip it
139
+ if ( ! is_subclass_of( $connector, '\WP_Stream\Connector' ) ) {
140
+ $this->plugin->admin->notice( sprintf( __( "%s class wasn't loaded because it doesn't extends the %s class.", 'stream' ), $connector->name, 'Connector' ), true );
141
+ continue;
142
+ }
143
+
144
+ // Store connector label
145
+ if ( ! in_array( $connector->name, $this->term_labels['stream_connector'] ) ) {
146
+ $this->term_labels['stream_connector'][ $connector->name ] = $connector->get_label();
147
+ }
148
+
149
+ $connector_name = $connector->name;
150
+ $is_excluded = in_array( $connector_name, $excluded_connectors );
151
+
152
+ /**
153
+ * Allows excluded connectors to be overridden and registered.
154
+ *
155
+ * @param bool $is_excluded True if excluded, otherwise false.
156
+ * @param string $connector The current connector's slug.
157
+ * @param array $excluded_connectors An array of all excluded connector slugs.
158
+ */
159
+ $is_excluded_connector = apply_filters( 'wp_stream_check_connector_is_excluded', $is_excluded, $connector_name, $excluded_connectors );
160
+
161
+ if ( $is_excluded_connector ) {
162
+ continue;
163
+ }
164
+
165
+ $connector->register();
166
+
167
+ // Link context labels to their connector
168
+ $this->contexts[ $connector->name ] = $connector->get_context_labels();
169
+
170
+ // Add new terms to our label lookup array
171
+ $this->term_labels['stream_action'] = array_merge(
172
+ $this->term_labels['stream_action'],
173
+ $connector->get_action_labels()
174
+ );
175
+ $this->term_labels['stream_context'] = array_merge(
176
+ $this->term_labels['stream_context'],
177
+ $connector->get_context_labels()
178
+ );
179
+ }
180
+
181
+ $connectors = $this->term_labels['stream_connector'];
182
+
183
+ /**
184
+ * Fires after all connectors have been registered.
185
+ *
186
+ * @param array $connectors All register connectors labels array
187
+ */
188
+ do_action( 'wp_stream_after_connectors_registration', $connectors );
189
+ }
190
+ }
classes/class-date-interval.php ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace WP_Stream;
3
+
4
+ // Load Carbon to Handle dates much easier
5
+ if ( ! class_exists( 'Carbon\Carbon' ) ) {
6
+ require_once wp_stream_get_instance()->locations['inc_dir'] . 'lib/Carbon.php';
7
+ }
8
+
9
+ use Carbon\Carbon;
10
+
11
+ class Date_Interval {
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
+ }
classes/class-db.php ADDED
@@ -0,0 +1,201 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace WP_Stream;
3
+
4
+ class DB {
5
+ /**
6
+ * Hold Plugin class
7
+ * @var Plugin
8
+ */
9
+ public $plugin;
10
+
11
+ /**
12
+ * Hold Query class
13
+ * @var Query
14
+ */
15
+ public $query;
16
+
17
+ /**
18
+ * Hold records table name
19
+ *
20
+ * @var string
21
+ */
22
+ public $table;
23
+
24
+ /**
25
+ * Hold meta table name
26
+ *
27
+ * @var string
28
+ */
29
+ public $table_meta;
30
+
31
+ /**
32
+ * Class constructor.
33
+ *
34
+ * @param Plugin $plugin The main Plugin class.
35
+ */
36
+ public function __construct( $plugin ) {
37
+ $this->plugin = $plugin;
38
+ $this->query = new Query( $this );
39
+
40
+ global $wpdb;
41
+
42
+ /**
43
+ * Allows devs to alter the tables prefix, default to base_prefix
44
+ *
45
+ * @param string $prefix
46
+ *
47
+ * @return string
48
+ */
49
+ $prefix = apply_filters( 'wp_stream_db_tables_prefix', $wpdb->base_prefix );
50
+
51
+ $this->table = $prefix . 'stream';
52
+ $this->table_meta = $prefix . 'stream_meta';
53
+
54
+ $wpdb->stream = $this->table;
55
+ $wpdb->streammeta = $this->table_meta;
56
+
57
+ // Hack for get_metadata
58
+ $wpdb->recordmeta = $this->table_meta;
59
+ }
60
+
61
+ /**
62
+ * Public getter to return table names
63
+ *
64
+ * @return array
65
+ */
66
+ public function get_table_names() {
67
+ return array(
68
+ $this->table,
69
+ $this->table_meta,
70
+ );
71
+ }
72
+
73
+ /**
74
+ * Insert a record
75
+ *
76
+ * @param array $recordarr
77
+ *
78
+ * @return int
79
+ */
80
+ public function insert( $recordarr ) {
81
+ if ( defined( 'WP_IMPORTING' ) && WP_IMPORTING ) {
82
+ return false;
83
+ }
84
+
85
+ /**
86
+ * Filter allows modification of record information
87
+ *
88
+ * @param array $recordarr
89
+ *
90
+ * @return array
91
+ */
92
+ $recordarr = apply_filters( 'wp_stream_record_array', $recordarr );
93
+
94
+ if ( empty( $recordarr ) ) {
95
+ return false;
96
+ }
97
+
98
+ global $wpdb;
99
+
100
+ $fields = array( 'object_id', 'site_id', 'blog_id', 'user_id', 'user_role', 'created', 'summary', 'ip', 'connector', 'context', 'action' );
101
+ $data = array_intersect_key( $recordarr, array_flip( $fields ) );
102
+ $data = array_filter( $data );
103
+ $result = $wpdb->insert( $this->table, $data );
104
+
105
+ if ( 1 !== $result ) {
106
+ /**
107
+ * Fires on a record insertion error
108
+ *
109
+ * @param array $recordarr
110
+ * @param mixed $result
111
+ */
112
+ do_action( 'wp_stream_record_insert_error', $recordarr, $result );
113
+
114
+ return $result;
115
+ }
116
+
117
+ $record_id = $wpdb->insert_id;
118
+
119
+ // Insert record meta
120
+ foreach ( (array) $recordarr['meta'] as $key => $vals ) {
121
+ // If associative array, serialize it, otherwise loop on its members
122
+ $vals = ( is_array( $vals ) && 0 !== key( $vals ) ) ? array( $vals ) : $vals;
123
+
124
+ foreach ( (array) $vals as $val ) {
125
+ $val = maybe_serialize( $val );
126
+
127
+ $this->insert_meta( $record_id, $key, $val );
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Fires after a record has been inserted
133
+ *
134
+ * @param int $record_id
135
+ * @param array $recordarr
136
+ */
137
+ do_action( 'wp_stream_record_inserted', $record_id, $recordarr );
138
+
139
+ return absint( $record_id );
140
+ }
141
+
142
+ /**
143
+ * Insert record meta
144
+ *
145
+ * @param int $record_id
146
+ * @param string $key
147
+ * @param string $val
148
+ *
149
+ * @return array
150
+ */
151
+ public function insert_meta( $record_id, $key, $val ) {
152
+ global $wpdb;
153
+
154
+ $result = $wpdb->insert(
155
+ $this->table_meta,
156
+ array(
157
+ 'record_id' => $record_id,
158
+ 'meta_key' => $key,
159
+ 'meta_value' => $val,
160
+ )
161
+ );
162
+
163
+ return $result;
164
+ }
165
+
166
+ /**
167
+ * Returns array of existing values for requested column.
168
+ * Used to fill search filters with only used items, instead of all items.
169
+ *
170
+ * GROUP BY allows query to find just the first occurance of each value in the column,
171
+ * increasing the efficiency of the query.
172
+ *
173
+ * @see assemble_records
174
+ * @since 1.0.4
175
+ *
176
+ * @param string $column
177
+ *
178
+ * @return array
179
+ */
180
+ function existing_records( $column ) {
181
+ global $wpdb;
182
+
183
+ $rows = $wpdb->get_results( "SELECT {$column} FROM $wpdb->stream GROUP BY {$column}", 'ARRAY_A' );
184
+
185
+ if ( is_array( $rows ) && ! empty( $rows ) ) {
186
+ $output_array = array();
187
+
188
+ foreach ( $rows as $row ) {
189
+ foreach ( $row as $cell => $value ) {
190
+ $output_array[ $value ] = $value;
191
+ }
192
+ }
193
+
194
+ return (array) $output_array;
195
+ }
196
+
197
+ $column = sprintf( 'stream_%s', $column );
198
+
199
+ return isset( $this->plugin->connectors->term_labels[ $column ] ) ? $this->plugin->connectors->term_labels[ $column ] : array();
200
+ }
201
+ }
classes/class-filter-input.php ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace WP_Stream;
3
+
4
+ class Filter_Input {
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( __CLASS__, 'is_ip_address' ),
13
+ FILTER_VALIDATE_REGEXP => array( __CLASS__, '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
+ // @codingStandardsIgnoreStart
31
+ switch ( $type ) {
32
+ case INPUT_POST :
33
+ $super = $_POST;
34
+ break;
35
+ case INPUT_GET :
36
+ $super = $_GET;
37
+ break;
38
+ case INPUT_COOKIE :
39
+ $super = $_COOKIE;
40
+ break;
41
+ case INPUT_ENV :
42
+ $super = $_ENV;
43
+ break;
44
+ case INPUT_SERVER :
45
+ $super = $_SERVER;
46
+ break;
47
+ }
48
+ // @codingStandardsIgnoreEnd
49
+
50
+ if ( is_null( $super ) ) {
51
+ throw new \Exception( esc_html__( 'Invalid use, type must be one of INPUT_* family.', 'stream' ) );
52
+ }
53
+
54
+ $var = isset( $super[ $variable_name ] ) ? $super[ $variable_name ] : null;
55
+ $var = self::filter( $var, $filter, $options );
56
+
57
+ return $var;
58
+ }
59
+
60
+ public static function filter( $var, $filter = null, $options = array() ) {
61
+ // Default filter is a sanitizer, not validator
62
+ $filter_type = 'sanitizer';
63
+
64
+ // Only filter value if it is not null
65
+ if ( isset( $var ) && $filter && FILTER_DEFAULT !== $filter ) {
66
+ if ( ! isset( self::$filter_callbacks[ $filter ] ) ) {
67
+ throw new \Exception( esc_html__( 'Filter not supported.', 'stream' ) );
68
+ }
69
+
70
+ $filter_callback = self::$filter_callbacks[ $filter ];
71
+ $result = call_user_func( $filter_callback, $var );
72
+
73
+ // filter_var / filter_input treats validation/sanitization filters the same
74
+ // they both return output and change the var value, this shouldn't be the case here.
75
+ // We'll do a boolean check on validation function, and let sanitizers change the value
76
+ $filter_type = ( $filter < 500 ) ? 'validator' : 'sanitizer';
77
+ if ( 'validator' === $filter_type ) { // Validation functions
78
+ if ( ! $result ) {
79
+ $var = false;
80
+ }
81
+ } else { // Santization functions
82
+ $var = $result;
83
+ }
84
+ }
85
+
86
+ // Detect FILTER_REQUIRE_ARRAY flag
87
+ if ( isset( $var ) && is_int( $options ) && FILTER_REQUIRE_ARRAY === $options ) {
88
+ if ( ! is_array( $var ) ) {
89
+ $var = ( 'validator' === $filter_type ) ? false : null;
90
+ }
91
+ }
92
+
93
+ // Polyfill the `default` attribute only, for now.
94
+ if ( is_array( $options ) && ! empty( $options['options']['default'] ) ) {
95
+ if ( 'validator' === $filter_type && false === $var ) {
96
+ $var = $options['options']['default'];
97
+ } elseif ( 'sanitizer' === $filter_type && null === $var ) {
98
+ $var = $options['options']['default'];
99
+ }
100
+ }
101
+
102
+ return $var;
103
+ }
104
+
105
+ public static function is_regex( $var ) {
106
+ // @codingStandardsIgnoreStart
107
+ $test = @preg_match( $var, '' );
108
+ // @codingStandardsIgnoreEnd
109
+
110
+ return false !== $test;
111
+ }
112
+
113
+ public static function is_ip_address( $var ) {
114
+ return false !== \WP_Http::is_ip_address( $var );
115
+ }
116
+ }
classes/class-install.php ADDED
@@ -0,0 +1,495 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace WP_Stream;
3
+
4
+ class Install {
5
+ /**
6
+ * Hold Plugin class
7
+ * @var Plugin
8
+ */
9
+ public $plugin;
10
+
11
+ /**
12
+ * Option key to store database version
13
+ *
14
+ * @var string
15
+ */
16
+ public $option_key = 'wp_stream_db';
17
+
18
+ /**
19
+ * Holds the database table prefix
20
+ *
21
+ * @var string
22
+ */
23
+ public $table_prefix;
24
+
25
+ /**
26
+ * Holds version of database at last update
27
+ ] *
28
+ * @var string
29
+ */
30
+ public $db_version;
31
+
32
+ /**
33
+ * URL to the Stream Admin settings page.
34
+ *
35
+ * @var string
36
+ */
37
+ public $stream_url;
38
+
39
+ /**
40
+ * Array of version numbers that require database update
41
+ *
42
+ * @var array
43
+ */
44
+ public $update_versions;
45
+
46
+ /**
47
+ * Holds status of whether it's safe to run Stream or not
48
+ *
49
+ * @var bool
50
+ */
51
+ public $update_required = false;
52
+
53
+ /**
54
+ * Holds status of whether the database update worked
55
+ *
56
+ * @var bool
57
+ */
58
+ public $success_db;
59
+
60
+ /**
61
+ * Class constructor
62
+ */
63
+ public function __construct( $plugin ) {
64
+ $this->plugin = $plugin;
65
+
66
+ $this->db_version = $this->get_db_version();
67
+ $this->stream_url = self_admin_url( $this->plugin->admin->admin_parent_page . '&page=' . $this->plugin->admin->settings_page_slug );
68
+
69
+ // Check DB and display an admin notice if there are tables missing
70
+ add_action( 'init', array( $this, 'verify_db' ) );
71
+
72
+ // Install the plugin
73
+ add_action( 'wp_stream_before_db_notices', array( $this, 'check' ) );
74
+
75
+ register_activation_hook( $this->plugin->locations['plugin'], array( $this, 'check' ) );
76
+ }
77
+
78
+ /**
79
+ * Check db version, create/update table schema accordingly
80
+ * If database update required admin notice will be given
81
+ * on the plugin update screen
82
+ *
83
+ * @return void
84
+ */
85
+ public function check() {
86
+ global $wpdb;
87
+
88
+ if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) {
89
+ return;
90
+ }
91
+
92
+ /**
93
+ * Allows devs to alter the tables prefix, default to base_prefix
94
+ *
95
+ * @param string $prefix
96
+ *
97
+ * @return string
98
+ */
99
+ $this->table_prefix = apply_filters( 'wp_stream_db_tables_prefix', $wpdb->base_prefix );
100
+
101
+ if ( empty( $this->db_version ) ) {
102
+ $this->install( $this->plugin->get_version() );
103
+
104
+ return;
105
+ }
106
+
107
+ if ( $this->plugin->get_version() === $this->db_version ) {
108
+ return;
109
+ }
110
+
111
+ $update = isset( $_REQUEST['wp_stream_update'] ) ? $_REQUEST['wp_stream_update'] : null;
112
+
113
+ if ( ! $update ) {
114
+ $this->update_required = true;
115
+ $this->success_db = $this->update( $this->db_version, $this->plugin->get_version(), array( 'type' => 'auto' ) );
116
+
117
+ return;
118
+ }
119
+
120
+ if ( 'update_and_continue' === $update ) {
121
+ $this->$success_db = $this->update( $this->db_version, $this->plugin->get_version(), array( 'type' => 'user' ) );
122
+ }
123
+
124
+ $versions = $this->db_update_versions();
125
+
126
+ if ( version_compare( end( $versions ), $this->db_version, '>' ) ) {
127
+ add_action( 'all_admin_notices', array( $this, 'update_notice_hook' ) );
128
+
129
+ return;
130
+ }
131
+
132
+ $this->update_db_option();
133
+ }
134
+
135
+ /**
136
+ * Verify that the required DB tables exists
137
+ *
138
+ * @return void
139
+ */
140
+ public function verify_db() {
141
+ /**
142
+ * Filter will halt install() if set to true
143
+ *
144
+ * @param bool
145
+ *
146
+ * @return bool
147
+ */
148
+ if ( apply_filters( 'wp_stream_no_tables', false ) ) {
149
+ return;
150
+ }
151
+
152
+ if ( ! function_exists( 'is_plugin_active_for_network' ) ) {
153
+ require_once( ABSPATH . '/wp-admin/includes/plugin.php' );
154
+ }
155
+
156
+ /**
157
+ * Fires before admin notices are triggered for missing database tables.
158
+ */
159
+ do_action( 'wp_stream_before_db_notices' );
160
+
161
+ global $wpdb;
162
+
163
+ $database_message = '';
164
+ $uninstall_message = '';
165
+
166
+ // Check if all needed DB is present
167
+ $missing_tables = array();
168
+
169
+ foreach ( $this->plugin->db->get_table_names() as $table_name ) {
170
+ if ( $wpdb->get_var( "SHOW TABLES LIKE '$table_name'" ) !== $table_name ) {
171
+ $missing_tables[] = $table_name;
172
+ }
173
+ }
174
+
175
+ if ( $missing_tables ) {
176
+ $database_message .= sprintf(
177
+ '%s <strong>%s</strong>',
178
+ _n(
179
+ 'The following table is not present in the WordPress database:',
180
+ 'The following tables are not present in the WordPress database:',
181
+ count( $missing_tables ),
182
+ 'stream'
183
+ ),
184
+ esc_html( implode( ', ', $missing_tables ) )
185
+ );
186
+ }
187
+
188
+ if ( is_plugin_active_for_network( $this->plugin->locations['plugin'] ) && current_user_can( 'manage_network_plugins' ) ) {
189
+ $uninstall_message = sprintf( __( 'Please <a href="%s">uninstall</a> the Stream plugin and activate it again.', 'stream' ), network_admin_url( 'plugins.php#stream' ) );
190
+ } elseif ( current_user_can( 'activate_plugins' ) ) {
191
+ $uninstall_message = sprintf( __( 'Please <a href="%s">uninstall</a> the Stream plugin and activate it again.', 'stream' ), admin_url( 'plugins.php#stream' ) );
192
+ }
193
+
194
+ if ( ! empty( $database_message ) ) {
195
+ $this->plugin->admin->notice( $database_message );
196
+
197
+ if ( ! empty( $uninstall_message ) ) {
198
+ $this->plugin->admin->notice( $uninstall_message );
199
+ }
200
+ }
201
+ }
202
+
203
+ /**
204
+ * Register a routine to be called when stream or a stream connector has been updated
205
+ * It works by comparing the current version with the version previously stored in the database.
206
+ *
207
+ * @param string $file A reference to the main plugin file
208
+ * @param string $callback The function to run when the hook is called.
209
+ * @param string $version The version to which the plugin is updating.
210
+ *
211
+ * @return void
212
+ */
213
+ public function register_update_hook( $file, $callback, $version ) {
214
+ if ( ! is_admin() ) {
215
+ return;
216
+ }
217
+
218
+ $plugin = plugin_basename( $file );
219
+
220
+ if ( is_plugin_active_for_network( $plugin ) ) {
221
+ $current_versions = get_site_option( $this->option_key . '_connectors', array() );
222
+ $network = true;
223
+ } elseif ( is_plugin_active( $plugin ) ) {
224
+ $current_versions = get_option( $this->option_key . '_connectors', array() );
225
+ $network = false;
226
+ } else {
227
+ return;
228
+ }
229
+
230
+ if ( version_compare( $version, $current_versions[ $plugin ], '>' ) ) {
231
+ call_user_func( $callback, $current_versions[ $plugin ], $network );
232
+
233
+ $current_versions[ $plugin ] = $version;
234
+ }
235
+
236
+ if ( $network ) {
237
+ update_site_option( $this->option_key . '_registered_connectors', $current_versions );
238
+ } else {
239
+ update_option( $this->option_key . '_registered_connectors', $current_versions );
240
+ }
241
+
242
+ return;
243
+ }
244
+
245
+ /**
246
+ * @return string
247
+ */
248
+ public function get_db_version() {
249
+ return get_site_option( $this->option_key );
250
+ }
251
+
252
+ /**
253
+ * @return void
254
+ */
255
+ public function update_db_option() {
256
+ if ( $this->success_db ) {
257
+ $success_op = update_site_option( $this->option_key, $this->plugin->get_version() );
258
+ }
259
+
260
+ if ( ! empty( $this->success_db ) && ! empty( $success_op ) ) {
261
+ return;
262
+ }
263
+
264
+ wp_die(
265
+ esc_html__( 'There was an error updating the Stream database. Please try again.', 'stream' ),
266
+ esc_html__( 'Database Update Error', 'stream' ),
267
+ array(
268
+ 'response' => 200,
269
+ 'back_link' => 1,
270
+ )
271
+ );
272
+ }
273
+
274
+ /**
275
+ * Added to the admin_notices hook when file plugin version is higher than database plugin version
276
+ *
277
+ * @action admin_notices
278
+ *
279
+ * @return void
280
+ */
281
+ public function update_notice_hook() {
282
+ if ( ! current_user_can( $this->plugin->admin->view_cap ) ) {
283
+ return;
284
+ }
285
+
286
+ $update = isset( $_REQUEST['wp_stream_update'] ) ? $_REQUEST['wp_stream_update'] : null;
287
+
288
+ if ( ! $update ) {
289
+ $this->prompt_update();
290
+
291
+ return;
292
+ }
293
+
294
+ if ( 'update_and_continue' === $update ) {
295
+ $this->prompt_update_status();
296
+ }
297
+ }
298
+
299
+ /**
300
+ * Action hook callback function
301
+ *
302
+ * Adds the user controlled database upgrade routine to the plugins updated page.
303
+ * When database update is complete page will refresh with dismissible message to user.
304
+ *
305
+ * @return void
306
+ */
307
+ public function prompt_update() {
308
+ ?>
309
+ <div class="error">
310
+ <form method="post" action="<?php echo esc_url( remove_query_arg( 'wp_stream_update' ) ) ?>">
311
+ <?php wp_nonce_field( 'wp_stream_update_db' ) ?>
312
+ <input type="hidden" name="wp_stream_update" value="update_and_continue"/>
313
+ <p><strong><?php esc_html_e( 'Stream Database Update Required', 'stream' ) ?></strong></p>
314
+ <p><?php esc_html_e( 'Stream has updated! Before we send you on your way, we need to update your database to the newest version.', 'stream' ) ?></p>
315
+ <p><?php esc_html_e( 'This process could take a little while, so please be patient.', 'stream' ) ?></p>
316
+ <?php submit_button( esc_html__( 'Update Database', 'stream' ), 'primary', 'stream-update-db-submit' ) ?>
317
+ </form>
318
+ </div>
319
+ <?php
320
+ }
321
+
322
+ /**
323
+ * When user initiates a database update this function calls the update methods, checks for success
324
+ * updates the stream_db version number in the database and outputs a success and continue message
325
+ *
326
+ * @return void
327
+ */
328
+ public function prompt_update_status() {
329
+ check_admin_referer( 'wp_stream_update_db' );
330
+
331
+ $this->update_db_option();
332
+ ?>
333
+ <div class="updated">
334
+ <form method="post" action="<?php echo esc_url( remove_query_arg( 'wp_stream_update' ) ) ?>" style="display:inline;">
335
+ <p><strong><?php esc_html_e( 'Update Complete', 'stream' ) ?></strong></p>
336
+ <p><?php esc_html_e( sprintf( 'Your Stream database has been successfully updated from %1$s to %2$s!', esc_html( $this->db_version ), esc_html( WP_Stream::VERSION ) ), 'stream' ) ?></p>
337
+ <?php submit_button( esc_html__( 'Continue', 'stream' ), 'secondary', false ) ?>
338
+ </form>
339
+ </div>
340
+ <?php
341
+ }
342
+
343
+ /**
344
+ * Array of database versions that require and updates
345
+ *
346
+ * To add your own stream extension database update routine
347
+ * use the filter and return the version that requires an update
348
+ * You must also make the callback function available in the global namespace on plugins loaded
349
+ * use the wp_stream_update_{version_number} version number must be a string of characters that represent the version with no periods
350
+ *
351
+ * @return array
352
+ */
353
+ public function db_update_versions() {
354
+ $db_update_versions = array(
355
+ '1.1.4' /* @version 1.1.4 Fix mysql character set issues */,
356
+ '1.1.7' /* @version 1.1.7 Modified the ip column to varchar(39) */,
357
+ '1.2.8' /* @version 1.2.8 Change the context for Media connectors to the attachment type */,
358
+ '1.3.0' /* @version 1.3.0 Backward settings compatibility for old version plugins */,
359
+ '1.3.1' /* @version 1.3.1 Update records of Installer to Theme Editor connector */,
360
+ '1.4.0' /* @version 1.4.0 Add the author_role column and prepare tables for multisite support */,
361
+ '1.4.2' /* @version 1.4.2 Patch to fix rare multisite upgrade not triggering */,
362
+ '1.4.5' /* @version 1.4.5 Patch to fix author_meta broken values */,
363
+ );
364
+
365
+ /**
366
+ * Filter to alter the DB update versions array
367
+ *
368
+ * @param array $db_update_versions
369
+ *
370
+ * @return array
371
+ */
372
+ return apply_filters( 'wp_stream_db_update_versions', $db_update_versions );
373
+ }
374
+
375
+ /**
376
+ * Database user controlled update routine
377
+ *
378
+ * @param int $db_version
379
+ * @param int $current_version
380
+ * @param array $update_args
381
+ *
382
+ * @return mixed Version number on success, true on no update needed, mysql error message on error
383
+ */
384
+ public function update( $db_version, $current_version, $update_args ) {
385
+ $versions = $this->db_update_versions();
386
+
387
+ foreach ( $versions as $version ) {
388
+ if ( ! isset( $update_args['type'] ) ) {
389
+ $update_args['type'] = 'user';
390
+ }
391
+
392
+ $function = 'wp_stream_update_' . ( 'user' === $update_args['type'] ? '' : $update_args['type'] . '_' ) . str_ireplace( '.', '', $version );
393
+
394
+ if ( version_compare( $db_version, $version, '<' ) ) {
395
+ $result = function_exists( $function ) ? call_user_func( $function, $db_version, $current_version ) : $current_version;
396
+
397
+ if ( $current_version !== $result ) {
398
+ return false;
399
+ }
400
+ }
401
+ }
402
+
403
+ return $current_version;
404
+ }
405
+
406
+ /**
407
+ * Initial database install routine
408
+ *
409
+ * @param string $current_version
410
+ *
411
+ * @return string
412
+ */
413
+ private function install( $current_version ) {
414
+ global $wpdb;
415
+
416
+ require_once ABSPATH . 'wp-admin/includes/upgrade.php';
417
+
418
+ $prefix = $this->table_prefix;
419
+
420
+ $sql = "CREATE TABLE {$prefix}stream (
421
+ ID bigint(20) unsigned NOT NULL AUTO_INCREMENT,
422
+ site_id bigint(20) unsigned NOT NULL DEFAULT '1',
423
+ blog_id bigint(20) unsigned NOT NULL DEFAULT '1',
424
+ object_id bigint(20) unsigned NULL,
425
+ user_id bigint(20) unsigned NOT NULL DEFAULT '0',
426
+ user_role varchar(20) NOT NULL DEFAULT '',
427
+ summary longtext NOT NULL,
428
+ created datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
429
+ connector varchar(100) NOT NULL,
430
+ context varchar(100) NOT NULL,
431
+ action varchar(100) NOT NULL,
432
+ ip varchar(39) NULL,
433
+ PRIMARY KEY (ID),
434
+ KEY site_id (site_id),
435
+ KEY blog_id (blog_id),
436
+ KEY object_id (object_id),
437
+ KEY user_id (user_id),
438
+ KEY created (created),
439
+ KEY connector (connector),
440
+ KEY context (context),
441
+ KEY action (action)
442
+ )";
443
+
444
+ if ( ! empty( $wpdb->charset ) ) {
445
+ $sql .= " CHARACTER SET $wpdb->charset";
446
+ }
447
+
448
+ if ( ! empty( $wpdb->collate ) ) {
449
+ $sql .= " COLLATE $wpdb->collate";
450
+ }
451
+
452
+ $sql .= ';';
453
+
454
+ \dbDelta( $sql );
455
+
456
+ if ( ! empty( $wpdb->charset ) ) {
457
+ $sql .= " CHARACTER SET $wpdb->charset";
458
+ }
459
+
460
+ if ( ! empty( $wpdb->collate ) ) {
461
+ $sql .= " COLLATE $wpdb->collate";
462
+ }
463
+
464
+ $sql .= ';';
465
+
466
+ \dbDelta( $sql );
467
+
468
+ $sql = "CREATE TABLE {$prefix}stream_meta (
469
+ meta_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
470
+ record_id bigint(20) unsigned NOT NULL,
471
+ meta_key varchar(200) NOT NULL,
472
+ meta_value varchar(200) NOT NULL,
473
+ PRIMARY KEY (meta_id),
474
+ KEY record_id (record_id),
475
+ KEY meta_key (meta_key),
476
+ KEY meta_value (meta_value)
477
+ )";
478
+
479
+ if ( ! empty( $wpdb->charset ) ) {
480
+ $sql .= " CHARACTER SET $wpdb->charset";
481
+ }
482
+
483
+ if ( ! empty( $wpdb->collate ) ) {
484
+ $sql .= " COLLATE $wpdb->collate";
485
+ }
486
+
487
+ $sql .= ';';
488
+
489
+ \dbDelta( $sql );
490
+
491
+ update_site_option( $this->option_key, $this->plugin->get_version() );
492
+
493
+ return $current_version;
494
+ }
495
+ }
classes/class-list-table.php ADDED
@@ -0,0 +1,933 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace WP_Stream;
3
+
4
+ class List_Table extends \WP_List_Table {
5
+ /**
6
+ * Hold Plugin class
7
+ * @var Plugin
8
+ */
9
+ public $plugin;
10
+
11
+ /**
12
+ * Class constructor.
13
+ *
14
+ * @param Plugin $plugin The main Plugin class.
15
+ * @param array $args
16
+ */
17
+ function __construct( $plugin, $args = array() ) {
18
+ $this->plugin = $plugin;
19
+
20
+ $screen_id = isset( $args['screen'] ) ? $args['screen'] : null;
21
+
22
+ /**
23
+ * Filter the list table screen ID
24
+ *
25
+ * @return string
26
+ */
27
+ $screen_id = apply_filters( 'wp_stream_list_table_screen_id', $screen_id );
28
+
29
+ parent::__construct(
30
+ array(
31
+ 'post_type' => 'stream',
32
+ 'plural' => 'records',
33
+ 'screen' => $screen_id,
34
+ )
35
+ );
36
+
37
+ add_screen_option(
38
+ 'per_page',
39
+ array(
40
+ 'default' => 20,
41
+ 'label' => __( 'Records per page', 'stream' ),
42
+ 'option' => 'edit_stream_per_page',
43
+ )
44
+ );
45
+
46
+ // Check for default hidden columns
47
+ $this->get_hidden_columns();
48
+
49
+ add_filter( 'screen_settings', array( $this, 'screen_controls' ), 10, 2 );
50
+ add_filter( 'set-screen-option', array( $this, 'set_screen_option' ), 10, 3 );
51
+
52
+ set_screen_options();
53
+ }
54
+
55
+ function extra_tablenav( $which ) {
56
+ if ( 'top' === $which ) {
57
+ echo $this->filters_form(); //xss ok
58
+ }
59
+ }
60
+
61
+ function no_items() {
62
+ ?>
63
+ <div class="stream-list-table-no-items">
64
+ <p><?php esc_html_e( 'Sorry, no activity records were found.', 'stream' ) ?></p>
65
+ </div>
66
+ <?php
67
+ }
68
+
69
+ function get_columns() {
70
+ /**
71
+ * Allows devs to add new columns to table
72
+ *
73
+ * @return array
74
+ */
75
+ return apply_filters(
76
+ 'wp_stream_list_table_columns',
77
+ array(
78
+ 'date' => __( 'Date', 'stream' ),
79
+ 'summary' => __( 'Summary', 'stream' ),
80
+ 'user_id' => __( 'User', 'stream' ),
81
+ 'context' => __( 'Context', 'stream' ),
82
+ 'action' => __( 'Action', 'stream' ),
83
+ 'ip' => __( 'IP Address', 'stream' ),
84
+ )
85
+ );
86
+ }
87
+
88
+ function get_sortable_columns() {
89
+ return array(
90
+ 'date' => array( 'date', false ),
91
+ );
92
+ }
93
+
94
+ function get_hidden_columns() {
95
+ if ( ! $user = wp_get_current_user() ) {
96
+ return array();
97
+ }
98
+
99
+ // Directly checking the user meta; to check whether user has changed screen option or not
100
+ $hidden = $this->plugin->admin->get_user_meta( $user->ID, 'manage' . $this->screen->id . 'columnshidden', true );
101
+
102
+ // If user meta is not found; add the default hidden column 'id'
103
+ if ( ! $hidden ) {
104
+ $hidden = array( 'id' );
105
+ $this->plugin->admin->update_user_meta( $user->ID, 'manage' . $this->screen->id . 'columnshidden', $hidden );
106
+ }
107
+
108
+ return $hidden;
109
+ }
110
+
111
+ function prepare_items() {
112
+ $columns = $this->get_columns();
113
+ $sortable = $this->get_sortable_columns();
114
+ $hidden = $this->get_hidden_columns();
115
+ $primary = $columns['summary'];
116
+
117
+ $this->_column_headers = array( $columns, $hidden, $sortable, $primary );
118
+
119
+ $this->items = $this->get_records();
120
+
121
+ $total_items = $this->get_total_found_rows();
122
+
123
+ $this->set_pagination_args(
124
+ array(
125
+ 'total_items' => $total_items,
126
+ 'per_page' => $this->get_items_per_page( 'edit_stream_per_page', 20 ),
127
+ )
128
+ );
129
+ }
130
+
131
+ function get_records() {
132
+ $args = array();
133
+
134
+ // Parse sorting params
135
+ if ( $order = wp_stream_filter_input( INPUT_GET, 'order' ) ) {
136
+ $args['order'] = $order;
137
+ }
138
+
139
+ if ( $orderby = wp_stream_filter_input( INPUT_GET, 'orderby' ) ) {
140
+ $args['orderby'] = $orderby;
141
+ }
142
+
143
+ $params = array(
144
+ 'search',
145
+ 'date',
146
+ 'date_from',
147
+ 'date_to',
148
+ 'date_after',
149
+ 'date_before',
150
+ );
151
+
152
+ foreach ( $params as $param ) {
153
+ $value = wp_stream_filter_input( INPUT_GET, $param );
154
+
155
+ if ( $value ) {
156
+ $args[ $param ] = $value;
157
+ }
158
+ }
159
+
160
+ // Additional filter properties
161
+ $properties = array(
162
+ 'record',
163
+ 'site_id',
164
+ 'blog_id',
165
+ 'object_id',
166
+ 'user_id',
167
+ 'user_role',
168
+ 'ip',
169
+ 'connector',
170
+ 'context',
171
+ 'action',
172
+ );
173
+
174
+ // Add property fields to defaults, including their __in/__not_in variations
175
+ foreach ( $properties as $property ) {
176
+ $value = wp_stream_filter_input( INPUT_GET, $property );
177
+
178
+ // Allow 0 values
179
+ if ( isset( $value ) && '' !== $value && false !== $value ) {
180
+ $args[ $property ] = $value;
181
+ }
182
+
183
+ $value_in = wp_stream_filter_input( INPUT_GET, $property . '__in' );
184
+
185
+ if ( $value_in ) {
186
+ $args[ $property . '__in' ] = explode( ',', $value_in );
187
+ }
188
+
189
+ $value_not_in = wp_stream_filter_input( INPUT_GET, $property . '__not_in' );
190
+
191
+ if ( $value_not_in ) {
192
+ $args[ $property . '__not_in' ] = explode( ',', $value_not_in );
193
+ }
194
+ }
195
+
196
+ $args['paged'] = $this->get_pagenum();
197
+
198
+ if ( isset( $args['context'] ) && 0 === strpos( $args['context'], 'group-' ) ) {
199
+ $args['connector'] = str_replace( 'group-', '', $args['context'] );
200
+ $args['context'] = '';
201
+ }
202
+
203
+ if ( ! isset( $args['records_per_page'] ) ) {
204
+ $args['records_per_page'] = $this->get_items_per_page( 'edit_stream_per_page', 20 );
205
+ }
206
+
207
+ $items = $this->plugin->db->query->query( $args );
208
+
209
+ return $items;
210
+ }
211
+
212
+ /**
213
+ * Get last query found rows
214
+ *
215
+ * @return integer
216
+ */
217
+ public function get_total_found_rows() {
218
+ return $this->plugin->db->query->found_records;
219
+ }
220
+
221
+ function column_default( $item, $column_name ) {
222
+ $out = '';
223
+ $record = new Record( $item );
224
+
225
+ switch ( $column_name ) {
226
+ case 'date' :
227
+ $created = date( 'Y-m-d H:i:s', strtotime( $record->created ) );
228
+ $date_string = sprintf(
229
+ '<time datetime="%s" class="relative-time record-created">%s</time>',
230
+ wp_stream_get_iso_8601_extended_date( strtotime( $record->created ) ),
231
+ get_date_from_gmt( $created, 'Y/m/d' )
232
+ );
233
+ $out = $this->column_link( $date_string, 'date', get_date_from_gmt( $created, 'Y/m/d' ) );
234
+ $out .= '<br />';
235
+ $out .= get_date_from_gmt( $created, 'h:i:s A' );
236
+ break;
237
+
238
+ case 'summary' :
239
+ $out = $record->summary;
240
+ $object_title = $record->get_object_title();
241
+ $view_all_text = $object_title ? sprintf( esc_html__( 'View all activity for "%s"', 'stream' ), esc_attr( $object_title ) ) : esc_html__( 'View all activity for this object', 'stream' );
242
+
243
+ if ( $record->object_id ) {
244
+ $out .= $this->column_link(
245
+ '<span class="dashicons dashicons-search stream-filter-object-id"></span>',
246
+ array(
247
+ 'object_id' => $record->object_id,
248
+ 'context' => $record->context,
249
+ ),
250
+ null,
251
+ esc_attr( $view_all_text )
252
+ );
253
+ }
254
+ $out .= $this->get_action_links( $record );
255
+ break;
256
+
257
+ case 'user_id' :
258
+ $user = new Author( (int) $record->user_id, (array) maybe_unserialize( $record->user_meta ) );
259
+
260
+ $out = sprintf(
261
+ '<a href="%s">%s <span>%s</span></a>%s%s%s',
262
+ $user->get_records_page_url(),
263
+ $user->get_avatar_img( 80 ),
264
+ $user->get_display_name(),
265
+ $user->is_deleted() ? sprintf( '<br /><small class="deleted">%s</small>', esc_html__( 'Deleted User', 'stream' ) ) : '',
266
+ $user->get_role() ? sprintf( '<br /><small>%s</small>', $user->get_role() ) : '',
267
+ $user->get_agent() ? sprintf( '<br /><small>%s</small>', $user->get_agent_label( $user->get_agent() ) ) : ''
268
+ );
269
+ break;
270
+
271
+ case 'context':
272
+ $connector_title = $this->get_term_title( $record->{'connector'}, 'connector' );
273
+ $context_title = $this->get_term_title( $record->{'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' => $record->{'connector'},
281
+ 'context' => $record->{'context'},
282
+ )
283
+ );
284
+ break;
285
+
286
+ case 'action':
287
+ $out = $this->column_link( $this->get_term_title( $record->{$column_name}, $column_name ), $column_name, $record->{$column_name} );
288
+ break;
289
+
290
+ case 'blog_id':
291
+ $blog = ( $record->blog_id && is_multisite() ) ? get_blog_details( $record->blog_id ) : $this->plugin->admin->network->get_network_blog();
292
+ $out = $this->column_link( $blog->blogname, 'blog_id', $blog->blog_id );
293
+ break;
294
+
295
+ case 'ip' :
296
+ $out = $this->column_link( $record->{$column_name}, 'ip', $record->{$column_name} );
297
+ break;
298
+
299
+ default :
300
+ /**
301
+ * Registers new Columns to be inserted into the table. The cell contents of this column is set
302
+ * below with 'wp_stream_inster_column_default-'
303
+ *
304
+ * @return array
305
+ */
306
+ $inserted_columns = apply_filters( 'wp_stream_register_column_defaults', $new_columns = array() );
307
+
308
+ if ( ! empty( $inserted_columns ) && is_array( $inserted_columns ) ) {
309
+ foreach ( $inserted_columns as $column_title ) {
310
+ /**
311
+ * If column title inserted via wp_stream_register_column_defaults ($column_title) exists
312
+ * among columns registered with get_columns ($column_name) and there is an action associated
313
+ * with this column, do the action
314
+ *
315
+ * Also, note that the action name must include the $column_title registered
316
+ * with wp_stream_register_column_defaults
317
+ */
318
+ if ( $column_title === $column_name && has_filter( "wp_stream_insert_column_default-{$column_title}" ) ) {
319
+ /**
320
+ * Allows for the addition of content under a specified column.
321
+ *
322
+ * @param object $record Contents of the row
323
+ *
324
+ * @return string
325
+ */
326
+ $out = apply_filters( "wp_stream_insert_column_default-{$column_title}", $column_name, $record );
327
+ } else {
328
+ $out = $column_name;
329
+ }
330
+ }
331
+ } else {
332
+ $out = $column_name;
333
+ }
334
+ }
335
+
336
+ echo $out; // xss ok
337
+ }
338
+
339
+ public function get_action_links( $record ) {
340
+ $out = '';
341
+
342
+ /**
343
+ * Filter allows modification of action links for a specific connector
344
+ *
345
+ * @param array
346
+ * @param Record
347
+ *
348
+ * @return array Action links for this connector
349
+ */
350
+ $action_links = apply_filters( 'wp_stream_action_links_' . $record->connector, array(), $record );
351
+
352
+ /**
353
+ * Filter allows addition of custom links for a specific connector
354
+ *
355
+ * @param array
356
+ * @param Record
357
+ *
358
+ * @return array Custom links for this connector
359
+ */
360
+ $custom_links = apply_filters( 'wp_stream_custom_action_links_' . $record->connector, array(), $record );
361
+
362
+ if ( $action_links || $custom_links ) {
363
+ $out .= '<div class="row-actions">';
364
+ }
365
+
366
+ $links = array();
367
+ if ( $action_links && is_array( $action_links ) ) {
368
+ foreach ( $action_links as $al_title => $al_href ) {
369
+ $links[] = sprintf(
370
+ '<span><a href="%s" class="action-link">%s</a></span>',
371
+ $al_href,
372
+ $al_title
373
+ );
374
+ }
375
+ }
376
+
377
+ if ( $custom_links && is_array( $custom_links ) ) {
378
+ foreach ( $custom_links as $key => $link ) {
379
+ $links[] = $link;
380
+ }
381
+ }
382
+
383
+ $out .= implode( ' | ', $links );
384
+
385
+ if ( $action_links || $custom_links ) {
386
+ $out .= '</div>';
387
+ }
388
+
389
+ return $out;
390
+ }
391
+
392
+ function column_link( $display, $key, $value = null, $title = null ) {
393
+ $url = add_query_arg(
394
+ array(
395
+ 'page' => $this->plugin->admin->records_page_slug,
396
+ ),
397
+ self_admin_url( $this->plugin->admin->admin_parent_page )
398
+ );
399
+
400
+ $args = ! is_array( $key ) ? array( $key => $value ) : $key;
401
+
402
+ foreach ( $args as $k => $v ) {
403
+ $url = add_query_arg( $k, $v, $url );
404
+ }
405
+
406
+ return sprintf(
407
+ '<a href="%s" title="%s">%s</a>',
408
+ esc_url( $url ),
409
+ esc_attr( $title ),
410
+ $display
411
+ );
412
+ }
413
+
414
+ public function get_term_title( $term, $type ) {
415
+ if ( ! isset( $this->plugin->connectors->term_labels[ "stream_$type" ][ $term ] ) ) {
416
+ return $term;
417
+ }
418
+
419
+ return $this->plugin->connectors->term_labels[ "stream_$type" ][ $term ];
420
+ }
421
+
422
+ /**
423
+ * Assembles records for display in search filters
424
+ *
425
+ * Gathers list of all users/connectors, then compares it to
426
+ * results of existing records. All items that do not exist in records
427
+ * get assigned a disabled value of "true".
428
+ *
429
+ * @param string $column List table column name
430
+ *
431
+ * @return array Options to be displayed in search filters
432
+ */
433
+ function assemble_records( $column ) {
434
+ // @todo eliminate special condition for authors, especially using a WP_User object as the value; should use string or stringifiable object
435
+ if ( 'user_id' === $column ) {
436
+ $all_records = array();
437
+
438
+ // If the number of users exceeds the max users constant value then return an empty array and use AJAX instead
439
+ $user_count = count_users();
440
+ $total_users = $user_count['total_users'];
441
+
442
+ if ( $total_users > $this->plugin->admin->preload_users_max ) {
443
+ return array();
444
+ }
445
+
446
+ $users = array_map(
447
+ function ( $user_id ) {
448
+ return new Author( $user_id );
449
+ },
450
+ get_users( array( 'fields' => 'ID' ) )
451
+ );
452
+
453
+ $users[] = new Author( 0, array( 'is_wp_cli' => true ) );
454
+
455
+ foreach ( $users as $user ) {
456
+ $all_records[ $user->id ] = $user->get_display_name();
457
+ }
458
+ } else {
459
+ $prefixed_column = sprintf( 'stream_%s', $column );
460
+ $all_records = $this->plugin->connectors->term_labels[ $prefixed_column ];
461
+ }
462
+
463
+ $existing_records = $this->plugin->db->existing_records( $column );
464
+ $active_records = array();
465
+ $disabled_records = array();
466
+
467
+ foreach ( $all_records as $record => $label ) {
468
+ if ( array_key_exists( $record, $existing_records ) ) {
469
+ $active_records[ $record ] = array( 'label' => $label, 'disabled' => '' );
470
+ } else {
471
+ $disabled_records[ $record ] = array( 'label' => $label, 'disabled' => 'disabled="disabled"' );
472
+ }
473
+ }
474
+
475
+ // Remove WP-CLI pseudo user if no records with user=0 exist
476
+ if ( isset( $disabled_records[0] ) ) {
477
+ unset( $disabled_records[0] );
478
+ }
479
+
480
+ $sort = function ( $a, $b ) use ( $column ) {
481
+ $label_a = (string) $a['label'];
482
+ $label_b = (string) $b['label'];
483
+
484
+ if ( $label_a === $label_b ) {
485
+ return 0;
486
+ }
487
+
488
+ return ( strtolower( $label_a ) < strtolower( $label_b ) ) ? -1 : 1;
489
+ };
490
+
491
+ uasort( $active_records, $sort );
492
+ uasort( $disabled_records, $sort );
493
+
494
+ // Not using array_merge() in order to preserve the array index for the users dropdown which uses the user_id as the key
495
+ $all_records = $active_records + $disabled_records;
496
+
497
+ return $all_records;
498
+ }
499
+
500
+ public function get_filters() {
501
+ $filters = array();
502
+
503
+ $date_interval = new Date_Interval();
504
+
505
+ $filters['date'] = array(
506
+ 'title' => __( 'dates', 'stream' ),
507
+ 'items' => $date_interval->intervals,
508
+ );
509
+
510
+ $users = $this->get_users_dropdown_items(
511
+ $this->assemble_records( 'user_id' )
512
+ );
513
+
514
+ $filters['user_id'] = array(
515
+ 'title' => __( 'users', 'stream' ),
516
+ 'items' => $users,
517
+ 'ajax' => count( $users ) <= 0,
518
+ );
519
+
520
+ $filters['context'] = array(
521
+ 'title' => __( 'contexts', 'stream' ),
522
+ 'items' => $this->assemble_records( 'context' ),
523
+ );
524
+
525
+ $filters['action'] = array(
526
+ 'title' => __( 'actions', 'stream' ),
527
+ 'items' => $this->assemble_records( 'action' ),
528
+ );
529
+
530
+ /**
531
+ * Filter allows additional filters in the list table dropdowns
532
+ * Note the format of the filters above, with they key and array
533
+ * containing a title and array of items.
534
+ *
535
+ * @return array
536
+ */
537
+ return apply_filters( 'wp_stream_list_table_filters', $filters );
538
+ }
539
+
540
+ function filters_form() {
541
+ $user_id = get_current_user_id();
542
+ $filters = $this->get_filters();
543
+
544
+ $filters_string = sprintf( '<input type="hidden" name="page" value="%s" />', 'wp_stream' );
545
+ $filters_string .= sprintf( '<span class="filter_info hidden">%s</span>', esc_html__( 'Show filter controls via the screen options tab above.', 'stream' ) );
546
+
547
+ foreach ( $filters as $name => $data ) {
548
+ if ( 'date' === $name ) {
549
+ $filters_string .= $this->filter_date( $data['items'] );
550
+ } else {
551
+ if ( 'context' === $name ) {
552
+ // Add Connectors as parents, and apply the Contexts as children
553
+ $connectors = $this->assemble_records( 'connector' );
554
+ $context_items = array();
555
+
556
+ foreach ( $connectors as $connector => $item ) {
557
+ $context_items[ $connector ]['label'] = $item['label'];
558
+
559
+ foreach ( $data['items'] as $context_value => $context_item ) {
560
+ if ( isset( $this->plugin->connectors->contexts[ $connector ] ) && array_key_exists( $context_value, $this->plugin->connectors->contexts[ $connector ] ) ) {
561
+ $context_items[ $connector ]['children'][ $context_value ] = $context_item;
562
+ }
563
+ }
564
+
565
+ if ( isset( $context_items[ $connector ]['children'] ) ) {
566
+ $labels = wp_list_pluck( $context_items[ $connector ]['children'], 'label' );
567
+
568
+ // Sort child items by label
569
+ array_multisort( $labels, SORT_ASC, $context_items[ $connector ]['children'] );
570
+ }
571
+ }
572
+
573
+ foreach ( $context_items as $context_value => $context_item ) {
574
+ if ( ! isset( $context_item['children'] ) || empty( $context_item['children'] ) ) {
575
+ unset( $context_items[ $context_value ] );
576
+ }
577
+ }
578
+
579
+ $data['items'] = $context_items;
580
+
581
+ $labels = wp_list_pluck( $data['items'], 'label' );
582
+
583
+ // Sort top-level items by label
584
+ array_multisort( $labels, SORT_ASC, $data['items'] );
585
+
586
+ // Ouput a hidden input to handle the connector value
587
+ $filters_string .= '<input type="hidden" name="connector" class="record-filter-connector" />';
588
+ }
589
+
590
+ $filters_string .= $this->filter_select( $name, $data['title'], $data['items'] );
591
+ }
592
+ }
593
+
594
+ $filters_string .= sprintf( '<input type="submit" id="record-query-submit" class="button" value="%s" />', __( 'Filter', 'stream' ) );
595
+
596
+ // Parse all query vars into an array
597
+ $query_vars = array();
598
+
599
+ if ( isset( $_SERVER['QUERY_STRING'] ) ) {
600
+ parse_str( urldecode( $_SERVER['QUERY_STRING'] ), $query_vars );
601
+ }
602
+
603
+ // Ignore certain query vars and query vars that are empty
604
+ foreach ( $query_vars as $query_var => $value ) {
605
+ if ( '' === $value || 'page' === $query_var || 'paged' === $query_var ) {
606
+ unset( $query_vars[ $query_var ] );
607
+ }
608
+ }
609
+
610
+ $url = add_query_arg(
611
+ array(
612
+ 'page' => $this->plugin->admin->records_page_slug,
613
+ ),
614
+ self_admin_url( $this->plugin->admin->admin_parent_page )
615
+ );
616
+
617
+ // Display reset action if records are being filtered
618
+ if ( ! empty( $query_vars ) ) {
619
+ $filters_string .= sprintf( '<a href="%s" id="record-query-reset"><span class="dashicons dashicons-dismiss"></span> <span class="record-query-reset-text">%s</span></a>', esc_url( $url ), __( 'Reset filters', 'stream' ) );
620
+ }
621
+
622
+ return sprintf( '<div class="alignleft actions">%s</div>', $filters_string ); // xss ok
623
+ }
624
+
625
+ function filter_select( $name, $title, $items, $ajax = false ) {
626
+ if ( $ajax ) {
627
+ $out = sprintf(
628
+ '<input type="hidden" name="%s" class="chosen-select" value="%s" data-placeholder="%s" />',
629
+ esc_attr( $name ),
630
+ esc_attr( wp_stream_filter_input( INPUT_GET, $name ) ),
631
+ esc_attr( $title )
632
+ );
633
+ } else {
634
+ $options = array( '<option value=""></option>' );
635
+ $selected = wp_stream_filter_input( INPUT_GET, $name );
636
+
637
+ foreach ( $items as $key => $item ) {
638
+ $value = isset( $item['children'] ) ? 'group-' . $key : $key;
639
+ $option_args = array(
640
+ 'value' => $value,
641
+ 'selected' => selected( $value, $selected, false ),
642
+ 'disabled' => isset( $item['disabled'] ) ? $item['disabled'] : null,
643
+ 'icon' => isset( $item['icon'] ) ? $item['icon'] : null,
644
+ 'group' => isset( $item['children'] ) ? $key : null,
645
+ 'tooltip' => isset( $item['tooltip'] ) ? $item['tooltip'] : null,
646
+ 'class' => isset( $item['children'] ) ? 'level-1' : null,
647
+ 'label' => isset( $item['label'] ) ? $item['label'] : null,
648
+ );
649
+ $options[] = $this->filter_option( $option_args );
650
+
651
+ if ( isset( $item['children'] ) ) {
652
+ foreach ( $item['children'] as $child_value => $child_item ) {
653
+ $option_args = array(
654
+ 'value' => $child_value,
655
+ 'selected' => selected( $child_value, $selected, false ),
656
+ 'disabled' => isset( $child_item['disabled'] ) ? $child_item['disabled'] : null,
657
+ 'icon' => isset( $child_item['icon'] ) ? $child_item['icon'] : null,
658
+ 'group' => $key,
659
+ 'tooltip' => isset( $child_item['tooltip'] ) ? $child_item['tooltip'] : null,
660
+ 'class' => 'level-2',
661
+ 'label' => isset( $child_item['label'] ) ? '- ' . $child_item['label'] : null,
662
+ );
663
+ $options[] = $this->filter_option( $option_args );
664
+ }
665
+ }
666
+ }
667
+ $out = sprintf(
668
+ '<select name="%s" class="chosen-select" data-placeholder="%s">%s</select>',
669
+ esc_attr( $name ),
670
+ sprintf( esc_attr__( 'Show all %s', 'stream' ), $title ),
671
+ implode( '', $options )
672
+ );
673
+ }
674
+
675
+ return $out;
676
+ }
677
+
678
+ function filter_option( $args ) {
679
+ $defaults = array(
680
+ 'value' => null,
681
+ 'selected' => null,
682
+ 'disabled' => null,
683
+ 'icon' => null,
684
+ 'group' => null,
685
+ 'tooltip' => null,
686
+ 'class' => null,
687
+ 'label' => null,
688
+ );
689
+ wp_parse_args( $args, $defaults );
690
+
691
+ return sprintf(
692
+ '<option value="%s" %s %s %s %s %s class="%s">%s</option>',
693
+ esc_attr( $args['value'] ),
694
+ $args['selected'],
695
+ $args['disabled'],
696
+ $args['icon'] ? sprintf( 'data-icon="%s"', esc_attr( $args['icon'] ) ) : null,
697
+ $args['group'] ? sprintf( 'data-group="%s"', esc_attr( $args['group'] ) ) : null,
698
+ $args['tooltip'] ? sprintf( 'title="%s"', esc_attr( $args['tooltip'] ) ) : null,
699
+ $args['class'] ? esc_attr( $args['class'] ) : null,
700
+ esc_html( $args['label'] )
701
+ );
702
+ }
703
+
704
+ function filter_search() {
705
+ $search = null;
706
+ if ( isset( $_GET['search'] ) ) {
707
+ // @TODO: Make this pass phpcs
708
+ $search = esc_attr( wp_unslash( $_GET['search'] ) ); // input var okay
709
+ }
710
+ $out = sprintf(
711
+ '<p class="search-box">
712
+ <label class="screen-reader-text" for="record-search-input">%1$s:</label>
713
+ <input type="search" id="record-search-input" name="search" value="%2$s" />
714
+ <input type="submit" name="" id="search-submit" class="button" value="%1$s" />
715
+ </p>',
716
+ esc_attr__( 'Search Records', 'stream' ),
717
+ $search
718
+ );
719
+
720
+ return $out;
721
+ }
722
+
723
+ function filter_date( $items ) {
724
+ wp_enqueue_style( 'jquery-ui' );
725
+ wp_enqueue_style( 'wp-stream-datepicker' );
726
+ wp_enqueue_script( 'jquery-ui-datepicker' );
727
+
728
+ $date_predefined = wp_stream_filter_input( INPUT_GET, 'date_predefined' );
729
+ $date_from = wp_stream_filter_input( INPUT_GET, 'date_from' );
730
+ $date_to = wp_stream_filter_input( INPUT_GET, 'date_to' );
731
+
732
+ ob_start();
733
+ ?>
734
+ <div class="date-interval">
735
+
736
+ <select class="field-predefined hide-if-no-js" name="date_predefined" data-placeholder="<?php esc_attr_e( 'All Time', 'stream' ) ?>">
737
+ <option></option>
738
+ <option value="custom" <?php selected( 'custom' === $date_predefined ); ?>><?php esc_attr_e( 'Custom', 'stream' ) ?></option>
739
+ <?php
740
+ foreach ( $items as $key => $interval ) {
741
+ $end = isset( $interval['end'] ) ? $interval['end']->format( 'Y/m/d' ) : null;
742
+
743
+ printf(
744
+ '<option value="%s" data-from="%s" data-to="%s" %s>%s</option>',
745
+ esc_attr( $key ),
746
+ esc_attr( $interval['start']->format( 'Y/m/d' ) ),
747
+ esc_attr( $end ),
748
+ selected( $key === $date_predefined ),
749
+ esc_html( $interval['label'] )
750
+ );
751
+ }
752
+ ?>
753
+ </select>
754
+
755
+ <div class="date-inputs">
756
+ <div class="box">
757
+ <i class="date-remove dashicons"></i>
758
+ <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 ) ?>" />
759
+ </div>
760
+ <span class="connector dashicons"></span>
761
+
762
+ <div class="box">
763
+ <i class="date-remove dashicons"></i>
764
+ <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 ) ?>" />
765
+ </div>
766
+ </div>
767
+
768
+ </div>
769
+ <?php
770
+
771
+ return ob_get_clean();
772
+ }
773
+
774
+ function display() {
775
+ $url = self_admin_url( $this->plugin->admin->admin_parent_page );
776
+
777
+ echo '<form method="get" action="' . esc_url( $url ) . '" id="record-filter-form">';
778
+ echo $this->filter_search(); // xss ok
779
+
780
+ parent::display();
781
+ echo '</form>';
782
+ }
783
+
784
+ function display_tablenav( $which ) {
785
+ if ( 'top' === $which ) : ?>
786
+ <div class="tablenav <?php echo esc_attr( $which ); ?>">
787
+ <?php
788
+ $this->pagination( $which );
789
+ $this->extra_tablenav( $which );
790
+ ?>
791
+
792
+ <br class="clear" />
793
+ </div>
794
+ <?php else : ?>
795
+ <div class="tablenav <?php echo esc_attr( $which ); ?>">
796
+ <?php
797
+ /**
798
+ * Fires after the list table is displayed.
799
+ */
800
+ do_action( 'wp_stream_after_list_table' );
801
+ $this->pagination( $which );
802
+ $this->extra_tablenav( $which );
803
+ ?>
804
+
805
+ <br class="clear" />
806
+ </div>
807
+ <?php
808
+ endif;
809
+ }
810
+
811
+ function set_screen_option( $dummy, $option, $value ) {
812
+ if ( 'edit_stream_per_page' === $option ) {
813
+ return $value;
814
+ } else {
815
+ return $dummy;
816
+ }
817
+ }
818
+
819
+ function set_live_update_option( $dummy, $option, $value ) {
820
+ unset( $value );
821
+
822
+ // @codingStandardsIgnoreStart
823
+ if (
824
+ $this->plugin->admin->live_update->user_meta_key === $option
825
+ &&
826
+ isset( $_POST[ $this->plugin->admin->live_update->user_meta_key ] )
827
+ ) {
828
+ $value = esc_attr( $_POST[ $this->plugin->admin->live_update->user_meta_key ] ); //input var okay
829
+
830
+ return $value;
831
+ }
832
+ // @codingStandardsIgnoreEnd
833
+
834
+ return $dummy;
835
+ }
836
+
837
+ public function screen_controls( $status, $args ) {
838
+ unset( $status );
839
+ unset( $args );
840
+
841
+ $user_id = get_current_user_id();
842
+ $option = $this->plugin->admin->get_user_meta( $user_id, $this->plugin->admin->live_update->user_meta_key, true );
843
+ $heartbeat = wp_script_is( 'heartbeat', 'done' ) ? 'true' : 'false';
844
+
845
+ if ( 'on' === $option && 'false' === $heartbeat ) {
846
+ $option = 'off';
847
+
848
+ $this->plugin->admin->update_user_meta( $user_id, $this->plugin->admin->live_update->user_meta_key, 'off' );
849
+ }
850
+
851
+ $nonce = wp_create_nonce( $this->plugin->admin->live_update->user_meta_key . '_nonce' );
852
+
853
+ ob_start();
854
+ ?>
855
+ <fieldset>
856
+ <h5><?php esc_html_e( 'Live updates', 'stream' ) ?></h5>
857
+
858
+ <div>
859
+ <input type="hidden" name="stream_live_update_nonce" id="stream_live_update_nonce" value="<?php echo esc_attr( $nonce ) ?>" />
860
+ </div>
861
+ <div>
862
+ <input type="hidden" name="enable_live_update_user" id="enable_live_update_user" value="<?php echo absint( $user_id ) ?>" />
863
+ </div>
864
+ <div class="metabox-prefs stream-live-update-checkbox">
865
+ <label for="enable_live_update">
866
+ <input type="checkbox" value="on" name="enable_live_update" id="enable_live_update" data-heartbeat="<?php echo esc_attr( $heartbeat ) ?>" <?php checked( $option, 'on' ) ?> />
867
+ <?php esc_html_e( 'Enabled', 'stream' ) ?><span class="spinner"></span>
868
+ </label>
869
+ </div>
870
+ </fieldset>
871
+ <?php
872
+ return ob_get_clean();
873
+ }
874
+
875
+ /**
876
+ * This function is use to map List table column name with excluded setting keys
877
+ *
878
+ * @param string $column List table column name
879
+ *
880
+ * @return string setting name for that column
881
+ */
882
+ function get_column_excluded_setting_key( $column ) {
883
+ switch ( $column ) {
884
+ case 'connector':
885
+ $output = 'connectors';
886
+ break;
887
+ case 'context':
888
+ $output = 'contexts';
889
+ break;
890
+ case 'action':
891
+ $output = 'action';
892
+ break;
893
+ case 'ip':
894
+ $output = 'ip_addresses';
895
+ break;
896
+ case 'user_id':
897
+ $output = 'users';
898
+ break;
899
+ default:
900
+ $output = false;
901
+ }
902
+
903
+ return $output;
904
+ }
905
+
906
+ /**
907
+ * Get users as dropdown items
908
+ *
909
+ * @param array $users
910
+ *
911
+ * @return array
912
+ */
913
+ public function get_users_dropdown_items( $users ) {
914
+ $record_meta = array();
915
+
916
+ foreach ( $users as $user_id => $args ) {
917
+ $user = new Author( $user_id );
918
+ $disabled = isset( $args['disabled'] ) ? $args['disabled'] : null;
919
+
920
+ $record_meta[ $user_id ] = array(
921
+ 'text' => $user->get_display_name(),
922
+ 'id' => $user_id,
923
+ 'label' => $user->get_display_name(),
924
+ 'icon' => $user->get_avatar_src( 32 ),
925
+ 'title' => '',
926
+ 'disabled' => $disabled,
927
+ );
928
+ }
929
+
930
+ return $record_meta;
931
+ }
932
+
933
+ }
classes/class-live-update.php ADDED
@@ -0,0 +1,214 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace WP_Stream;
3
+
4
+ class Live_Update {
5
+ /**
6
+ * Hold Plugin class
7
+ * @var Plugin
8
+ */
9
+ public $plugin;
10
+
11
+ /**
12
+ * User meta key/identifier
13
+ *
14
+ * @var string
15
+ */
16
+ public $user_meta_key = 'stream_live_update_records';
17
+
18
+ /**
19
+ * List table object instance
20
+ *
21
+ * @var List_Table
22
+ */
23
+ public $list_table = null;
24
+
25
+ /**
26
+ * Class constructor.
27
+ *
28
+ * @param Plugin $plugin The main Plugin class.
29
+ */
30
+ public function __construct( $plugin ) {
31
+ $this->plugin = $plugin;
32
+
33
+ // Heartbeat live update
34
+ add_filter( 'heartbeat_received', array( $this, 'heartbeat_received' ), 10, 2 );
35
+
36
+ // Enable / Disable live update per user
37
+ add_action( 'wp_ajax_stream_enable_live_update', array( $this, 'enable_live_update' ) );
38
+ }
39
+
40
+ /**
41
+ * Ajax function to enable/disable live update
42
+ *
43
+ * @return string Ajax respsonse back in JSON format
44
+ */
45
+ public function enable_live_update() {
46
+ check_ajax_referer( $this->user_meta_key . '_nonce', 'nonce' );
47
+
48
+ $input = array(
49
+ 'checked' => FILTER_SANITIZE_STRING,
50
+ 'user' => FILTER_SANITIZE_STRING,
51
+ 'heartbeat' => FILTER_SANITIZE_STRING,
52
+ );
53
+
54
+ $input = filter_input_array( INPUT_POST, $input );
55
+
56
+ if ( false === $input ) {
57
+ wp_send_json_error( 'Error in live update checkbox' );
58
+ }
59
+
60
+ $checked = ( 'checked' === $input['checked'] ) ? 'on' : 'off';
61
+
62
+ $user = (int) $input['user'];
63
+
64
+ if ( 'false' === $input['heartbeat'] ) {
65
+ $this->plugin->admin->update_user_meta( $user, $this->user_meta_key, 'off' );
66
+
67
+ 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' ) );
68
+
69
+ return;
70
+ }
71
+
72
+ $success = $this->plugin->admin->update_user_meta( $user, $this->user_meta_key, $checked );
73
+
74
+ if ( $success ) {
75
+ wp_send_json_success( ( 'on' === $checked ) ? 'Live Updates enabled' : 'Live Updates disabled' );
76
+ } else {
77
+ wp_send_json_error( 'Live Updates checkbox error' );
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Sends updated actions to the list table view
83
+ *
84
+ * @todo Fix reliability issues with sidebar widgets
85
+ *
86
+ * @uses gather_updated_items
87
+ * @uses generate_row
88
+ *
89
+ * @param array $response Response to heartbeat
90
+ * @param array $data Data from heartbeat
91
+ *
92
+ * @return array Data sent to heartbeat
93
+ */
94
+ public function live_update( $response, $data ) {
95
+ unset( $response );
96
+
97
+ if ( ! isset( $data['wp-stream-heartbeat-last-time'] ) ) {
98
+ return array();
99
+ }
100
+
101
+ $last_time = $data['wp-stream-heartbeat-last-time'];
102
+ $query = $data['wp-stream-heartbeat-query'];
103
+
104
+ if ( empty( $query ) ) {
105
+ $query = array();
106
+ }
107
+
108
+ // Decode the query
109
+ $query = json_decode( wp_kses_stripslashes( $query ) );
110
+
111
+ $updated_items = $this->gather_updated_items( $last_time, (array) $query );
112
+
113
+ if ( ! empty( $updated_items ) ) {
114
+ ob_start();
115
+
116
+ foreach ( $updated_items as $item ) {
117
+ $this->list_table->single_row( $item );
118
+ }
119
+
120
+ $send = ob_get_clean();
121
+ } else {
122
+ $send = '';
123
+ }
124
+
125
+ return $send;
126
+ }
127
+
128
+ /**
129
+ * Sends Updated Actions to the List Table View
130
+ *
131
+ * @param int $last_time Timestamp of last update
132
+ * @param array $args Query args
133
+ *
134
+ * @return array Array of recently updated items
135
+ */
136
+ public function gather_updated_items( $last_time, $args = array() ) {
137
+ unset( $args );
138
+
139
+ if ( false === $last_time ) {
140
+ return '';
141
+ }
142
+
143
+ if ( empty( $this->list_table->items ) ) {
144
+ return '';
145
+ }
146
+
147
+ $items = array();
148
+
149
+ foreach ( $this->list_table->items as $item ) {
150
+ if ( strtotime( $item->created ) > strtotime( $last_time ) ) {
151
+ $items[] = $item;
152
+ } else {
153
+ break;
154
+ }
155
+ }
156
+
157
+ return $items;
158
+ }
159
+
160
+ /**
161
+ * Handles live updates for Stream Post List
162
+ *
163
+ * @action heartbeat_recieved
164
+ *
165
+ * @param array $response Response to be sent to heartbeat tick
166
+ * @param array $data Data from heartbeat send
167
+ *
168
+ * @return array Data sent to heartbeat tick
169
+ */
170
+ public function heartbeat_received( $response, $data ) {
171
+ // Only fire when Stream is requesting a live update
172
+ if ( ! isset( $data['wp-stream-heartbeat'] ) ) {
173
+ return $response;
174
+ }
175
+
176
+ $enable_stream_update = ( 'off' !== $this->plugin->admin->get_user_meta( get_current_user_id(), $this->user_meta_key ) );
177
+
178
+ // Register list table
179
+ $this->list_table = new WP_Stream_List_Table( array( 'screen' => 'toplevel_page_' . $this->plugin->admin->records_page_slug ) );
180
+ $this->list_table->prepare_items();
181
+
182
+ $total_items = isset( $this->list_table->_pagination_args['total_items'] ) ? $this->list_table->_pagination_args['total_items'] : null;
183
+ $total_pages = isset( $this->list_table->_pagination_args['total_pages'] ) ? $this->list_table->_pagination_args['total_pages'] : null;
184
+
185
+ if ( isset( $data['wp-stream-heartbeat'] ) && isset( $total_items ) ) {
186
+ $response['total_items'] = $total_items;
187
+ $response['total_items_i18n'] = sprintf( _n( '1 item', '%s items', $total_items ), number_format_i18n( $total_items ) );
188
+ }
189
+
190
+ if ( isset( $data['wp-stream-heartbeat'] ) && 'live-update' === $data['wp-stream-heartbeat'] && $enable_stream_update ) {
191
+
192
+ if ( ! empty( $data['wp-stream-heartbeat'] ) ) {
193
+ if ( isset( $total_pages ) ) {
194
+ $response['total_pages'] = $total_pages;
195
+ $response['total_pages_i18n'] = number_format_i18n( $total_pages );
196
+
197
+ $query_args = json_decode( $data['wp-stream-heartbeat-query'], true );
198
+ $query_args['paged'] = $total_pages;
199
+
200
+ $response['last_page_link'] = add_query_arg( $query_args, admin_url( 'admin.php' ) );
201
+ } else {
202
+ $response['total_pages'] = 0;
203
+ }
204
+ }
205
+
206
+ $response['wp-stream-heartbeat'] = $this->live_update( $response, $data );
207
+
208
+ } else {
209
+ $response['log'] = 'fail';
210
+ }
211
+
212
+ return $response;
213
+ }
214
+ }
classes/class-log.php ADDED
@@ -0,0 +1,285 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace WP_Stream;
3
+
4
+ class Log {
5
+ /**
6
+ * Hold Plugin class
7
+ * @var Plugin
8
+ */
9
+ public $plugin;
10
+
11
+ /**
12
+ * Previous Stream record ID, used for chaining same-session records
13
+ *
14
+ * @var int
15
+ */
16
+ private $prev_record;
17
+
18
+ /**
19
+ * Class constructor.
20
+ *
21
+ * @param Plugin $plugin The main Plugin class.
22
+ */
23
+ public function __construct( $plugin ) {
24
+ $this->plugin = $plugin;
25
+ }
26
+
27
+ /**
28
+ * Log handler
29
+ *
30
+ * @param Connector $connector Connector responsible for logging the event
31
+ * @param string $message sprintf-ready error message string
32
+ * @param array $args sprintf (and extra) arguments to use
33
+ * @param int $object_id Target object id
34
+ * @param string $context Context of the event
35
+ * @param string $action Action of the event
36
+ * @param int $user_id User responsible for the event
37
+ *
38
+ * @return mixed True if updated, otherwise false|WP_Error
39
+ */
40
+ public function log( $connector, $message, $args, $object_id, $context, $action, $user_id = null ) {
41
+ if ( is_null( $user_id ) ) {
42
+ $user_id = get_current_user_id();
43
+ }
44
+
45
+ if ( is_null( $object_id ) ) {
46
+ $object_id = 0;
47
+ }
48
+
49
+ $wp_cron_tracking = isset( $this->plugin->settings->options['advanced_wp_cron_tracking'] ) ? $this->plugin->settings->options['advanced_wp_cron_tracking'] : false;
50
+ $author = new Author( $user_id );
51
+ $agent = $author->get_current_agent();
52
+
53
+ // WP Cron tracking requires opt-in and WP Cron to be enabled
54
+ if ( ! $wp_cron_tracking && 'wp_cron' === $agent ) {
55
+ return false;
56
+ }
57
+
58
+ $user = new \WP_User( $user_id );
59
+
60
+ if ( $this->is_record_excluded( $connector, $context, $action, $user ) ) {
61
+ return false;
62
+ }
63
+
64
+ $user_meta = array(
65
+ 'user_email' => (string) ! empty( $user->user_email ) ? $user->user_email : '',
66
+ 'display_name' => (string) $author->get_display_name(),
67
+ 'user_login' => (string) ! empty( $user->user_login ) ? $user->user_login : '',
68
+ 'user_role_label' => (string) $author->get_role(),
69
+ 'agent' => (string) $agent,
70
+ );
71
+
72
+ if ( 'wp_cli' === $agent && function_exists( 'posix_getuid' ) ) {
73
+ $uid = posix_getuid();
74
+ $user_info = posix_getpwuid( $uid );
75
+
76
+ $user_meta['system_user_id'] = (int) $uid;
77
+ $user_meta['system_user_name'] = (string) $user_info['name'];
78
+ }
79
+
80
+ // Prevent any meta with null values from being logged
81
+ $stream_meta = array_filter(
82
+ $args,
83
+ function ( $var ) {
84
+ return ! is_null( $var );
85
+ }
86
+ );
87
+
88
+ // Add user meta to Stream meta
89
+ $stream_meta['user_meta'] = $user_meta;
90
+
91
+ // All meta must be strings, so we will serialize any array meta values
92
+ array_walk(
93
+ $stream_meta,
94
+ function( &$v ) {
95
+ $v = (string) maybe_serialize( $v );
96
+ }
97
+ );
98
+
99
+ // Get the current time in milliseconds
100
+ $iso_8601_extended_date = wp_stream_get_iso_8601_extended_date();
101
+
102
+ $recordarr = array(
103
+ 'object_id' => (int) $object_id,
104
+ 'site_id' => (int) is_multisite() ? get_current_site()->id : 1,
105
+ 'blog_id' => (int) apply_filters( 'wp_stream_blog_id_logged', get_current_blog_id() ),
106
+ 'user_id' => (int) $user_id,
107
+ 'user_role' => (string) ! empty( $user->roles ) ? $user->roles[0] : '',
108
+ 'created' => (string) $iso_8601_extended_date,
109
+ 'summary' => (string) vsprintf( $message, $args ),
110
+ 'connector' => (string) $connector,
111
+ 'context' => (string) $context,
112
+ 'action' => (string) $action,
113
+ 'ip' => (string) wp_stream_filter_input( INPUT_SERVER, 'REMOTE_ADDR', FILTER_VALIDATE_IP ),
114
+ 'meta' => (array) $stream_meta,
115
+ );
116
+
117
+ $result = $this->plugin->db->insert( $recordarr );
118
+
119
+ $this->debug_backtrace( $recordarr );
120
+
121
+ return $result;
122
+ }
123
+
124
+ /**
125
+ * This function is use to check whether or not a record should be excluded from the log
126
+ *
127
+ * @param string $connector Name of the connector being logged
128
+ * @param string $context Name of the context being logged
129
+ * @param string $action Name of the action being logged
130
+ * @param \WP_User $user The user being logged
131
+ * @param string $ip IP address being logged
132
+ *
133
+ * @return bool
134
+ */
135
+ public function is_record_excluded( $connector, $context, $action, $user = null, $ip = null ) {
136
+ if ( is_null( $user ) ) {
137
+ $user = wp_get_current_user();
138
+ }
139
+
140
+ if ( is_null( $ip ) ) {
141
+ $ip = wp_stream_filter_input( INPUT_SERVER, 'REMOTE_ADDR', FILTER_VALIDATE_IP );
142
+ } else {
143
+ $ip = wp_stream_filter_var( $ip, FILTER_VALIDATE_IP );
144
+ }
145
+
146
+ $user_role = isset( $user->roles[0] ) ? $user->roles[0] : null;
147
+
148
+ $record = array(
149
+ 'connector' => $connector,
150
+ 'context' => $context,
151
+ 'action' => $action,
152
+ 'author' => $user->ID,
153
+ 'role' => $user_role,
154
+ 'ip_address' => $ip,
155
+ );
156
+
157
+ $exclude_settings = isset( $this->plugin->settings->options['exclude_rules'] ) ? $this->plugin->settings->options['exclude_rules'] : array();
158
+
159
+ if ( isset( $exclude_settings['exclude_row'] ) && ! empty( $exclude_settings['exclude_row'] ) ) {
160
+ foreach ( $exclude_settings['exclude_row'] as $key => $value ) {
161
+ // Prepare values
162
+ $author_or_role = isset( $exclude_settings['author_or_role'][ $key ] ) ? $exclude_settings['author_or_role'][ $key ] : '';
163
+ $connector = isset( $exclude_settings['connector'][ $key ] ) ? $exclude_settings['connector'][ $key ] : '';
164
+ $context = isset( $exclude_settings['context'][ $key ] ) ? $exclude_settings['context'][ $key ] : '';
165
+ $action = isset( $exclude_settings['action'][ $key ] ) ? $exclude_settings['action'][ $key ] : '';
166
+ $ip_address = isset( $exclude_settings['ip_address'][ $key ] ) ? $exclude_settings['ip_address'][ $key ] : '';
167
+
168
+ $exclude = array(
169
+ 'connector' => ! empty( $connector ) ? $connector : null,
170
+ 'context' => ! empty( $context ) ? $context : null,
171
+ 'action' => ! empty( $action ) ? $action : null,
172
+ 'ip_address' => ! empty( $ip_address ) ? $ip_address : null,
173
+ 'author' => is_numeric( $author_or_role ) ? absint( $author_or_role ) : null,
174
+ 'role' => ( ! empty( $author_or_role ) && ! is_numeric( $author_or_role ) ) ? $author_or_role : null,
175
+ );
176
+
177
+ $exclude_rules = array_filter( $exclude, 'strlen' );
178
+
179
+ if ( ! empty( $exclude_rules ) ) {
180
+ $excluded = true;
181
+
182
+ foreach ( $exclude_rules as $exclude_key => $exclude_value ) {
183
+ if ( $record[ $exclude_key ] !== $exclude_value ) {
184
+ $excluded = false;
185
+ break;
186
+ }
187
+ }
188
+
189
+ if ( $excluded ) {
190
+ return true;
191
+ }
192
+ }
193
+ }
194
+ }
195
+
196
+ return false;
197
+ }
198
+
199
+ /**
200
+ * Send a full backtrace of calls to the PHP error log for debugging
201
+ *
202
+ * @param array $recordarr
203
+ *
204
+ * @return void
205
+ */
206
+ public function debug_backtrace( $recordarr ) {
207
+ /**
208
+ * Enable debug backtrace on records.
209
+ *
210
+ * This filter is for developer use only. When enabled, Stream will send
211
+ * a full debug backtrace of PHP calls for each record. Optionally, you may
212
+ * use the available $recordarr parameter to specify what types of records to
213
+ * create backtrace logs for.
214
+ *
215
+ * @param array $recordarr
216
+ *
217
+ * @return bool Set to FALSE by default (backtrace disabled)
218
+ */
219
+ $enabled = apply_filters( 'wp_stream_debug_backtrace', false, $recordarr );
220
+
221
+ if ( ! $enabled ) {
222
+ return;
223
+ }
224
+
225
+ if ( version_compare( PHP_VERSION, '5.3.6', '<' ) ) {
226
+ error_log( 'WP Stream debug backtrace requires at least PHP 5.3.6' );
227
+ return;
228
+ }
229
+
230
+ // Record details
231
+ $summary = isset( $recordarr['summary'] ) ? $recordarr['summary'] : null;
232
+ $author = isset( $recordarr['author'] ) ? $recordarr['author'] : null;
233
+ $connector = isset( $recordarr['connector'] ) ? $recordarr['connector'] : null;
234
+ $context = isset( $recordarr['context'] ) ? $recordarr['context'] : null;
235
+ $action = isset( $recordarr['action'] ) ? $recordarr['action'] : null;
236
+
237
+ // Stream meta
238
+ $stream_meta = isset( $recordarr['meta'] ) ? $recordarr['meta'] : null;
239
+
240
+ unset( $stream_meta['user_meta'] );
241
+
242
+ if ( $stream_meta ) {
243
+ array_walk( $stream_meta, function( &$value, $key ) {
244
+ $value = sprintf( '%s: %s', $key, ( '' === $value ) ? 'null' : $value );
245
+ });
246
+
247
+ $stream_meta = implode( ', ', $stream_meta );
248
+ }
249
+
250
+ // User meta
251
+ $user_meta = isset( $recordarr['meta']['user_meta'] ) ? $recordarr['meta']['user_meta'] : null;
252
+
253
+ if ( $user_meta ) {
254
+ array_walk( $user_meta, function( &$value, $key ) {
255
+ $value = sprintf( '%s: %s', $key, ( '' === $value ) ? 'null' : $value );
256
+ });
257
+
258
+ $user_meta = implode( ', ', $user_meta );
259
+ }
260
+
261
+ // Debug backtrace
262
+ ob_start();
263
+
264
+ // @codingStandardsIgnoreStart
265
+ debug_print_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS ); // Option to ignore args requires PHP 5.3.6
266
+ // @codingStandardsIgnoreEnd
267
+
268
+ $backtrace = ob_get_clean();
269
+ $backtrace = array_values( array_filter( explode( "\n", $backtrace ) ) );
270
+
271
+ $output = sprintf(
272
+ "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",
273
+ $summary,
274
+ $author,
275
+ $connector,
276
+ $context,
277
+ $action,
278
+ $stream_meta,
279
+ $user_meta,
280
+ implode( "\n", $backtrace )
281
+ );
282
+
283
+ error_log( $output );
284
+ }
285
+ }
classes/class-migrate.php ADDED
@@ -0,0 +1,353 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace WP_Stream;
3
+
4
+ class Migrate {
5
+ /**
6
+ * Hold Plugin class
7
+ * @var Plugin
8
+ */
9
+ public $plugin;
10
+
11
+ /**
12
+ * Hold site API Key
13
+ *
14
+ * @var string
15
+ */
16
+ private $api_key;
17
+
18
+ /**
19
+ * Hold site UUID
20
+ *
21
+ * @var string
22
+ */
23
+ private $site_uuid;
24
+
25
+ /**
26
+ * Hold the total number of legacy records found in the cloud
27
+ *
28
+ * @var int
29
+ */
30
+ public $record_count;
31
+
32
+ /**
33
+ * Limit payload chunks to a certain number of records
34
+ *
35
+ * @var int
36
+ */
37
+ public $limit;
38
+
39
+ /**
40
+ * Class constructor.
41
+ *
42
+ * @param Plugin $plugin The main Plugin class.
43
+ */
44
+ public function __construct( $plugin ) {
45
+ $this->plugin = $plugin;
46
+
47
+ $this->api_key = get_option( 'wp_stream_site_api_key' );
48
+ $this->site_uuid = get_option( 'wp_stream_site_uuid' );
49
+
50
+ // Exit early if disconnected
51
+ if ( ! $this->is_connected() ) {
52
+ return;
53
+ }
54
+
55
+ $this->limit = absint( apply_filters( 'wp_stream_migrate_chunk_size', 100 ) );
56
+
57
+ $this->record_count = $this->get_record_count();
58
+
59
+ // Display admin notice
60
+ add_action( 'admin_notices', array( $this, 'migrate_notice' ), 9 );
61
+
62
+ // AJAX callback for migrate action
63
+ add_action( 'wp_ajax_wp_stream_migrate_action', array( $this, 'migrate_action_callback' ) );
64
+ }
65
+
66
+ /**
67
+ * Are we currently connected to WP Stream?
68
+ *
69
+ * @return bool
70
+ */
71
+ private function is_connected() {
72
+ return ( ! empty( $this->api_key ) && ! empty( $this->site_uuid ) );
73
+ }
74
+
75
+ /**
76
+ * Disconnect from WP Stream
77
+ */
78
+ private function disconnect() {
79
+ delete_option( 'wp_stream_site_api_key' );
80
+ delete_option( 'wp_stream_site_uuid' );
81
+ delete_option( 'wp_stream_delay_migration' );
82
+ delete_option( 'wp_stream_site_restricted' );
83
+
84
+ $this->api_key = false;
85
+ $this->site_uuid = false;
86
+ }
87
+
88
+ /**
89
+ * Search for records
90
+ *
91
+ * @param array $query
92
+ *
93
+ * @return mixed Response body on success, or FALSE on failure
94
+ */
95
+ private function search( $query = array() ) {
96
+ if ( ! $this->is_connected() ) {
97
+ return false;
98
+ }
99
+
100
+ $defaults = array(
101
+ 'sort' => array(
102
+ array(
103
+ 'created' => array(
104
+ 'order' => 'asc',
105
+ ),
106
+ ),
107
+ ),
108
+ );
109
+
110
+ $last = get_option( 'wp_stream_last_migrated' );
111
+
112
+ if ( $last ) {
113
+ $defaults['filter'] = array(
114
+ 'and' => array(
115
+ array(
116
+ 'range' => array(
117
+ 'created' => array(
118
+ 'gt' => $last,
119
+ ),
120
+ ),
121
+ ),
122
+ ),
123
+ );
124
+ }
125
+
126
+ $body['sites'] = array( $this->site_uuid );
127
+ $body['query'] = array_merge( $defaults, (array) $query );
128
+
129
+ $args = array(
130
+ 'headers' => array(
131
+ 'Stream-Site-API-Key' => $this->api_key,
132
+ 'Content-Type' => 'application/json',
133
+ ),
134
+ 'method' => 'POST',
135
+ 'body' => wp_stream_json_encode( $body ),
136
+ 'sslverify' => true,
137
+ );
138
+
139
+ $response = wp_safe_remote_request( 'https://api.wp-stream.com/search', $args );
140
+
141
+ if ( is_wp_error( $response ) ) {
142
+ return false;
143
+ }
144
+
145
+ return json_decode( wp_remote_retrieve_body( $response ) );
146
+ }
147
+
148
+ /**
149
+ * Get the total number of records found
150
+ *
151
+ * @return int
152
+ */
153
+ private function get_record_count() {
154
+ $response = $this->search( array( 'size' => 0 ) );
155
+
156
+ if ( empty( $response->meta->total ) ) {
157
+ return 0;
158
+ }
159
+
160
+ return absint( $response->meta->total );
161
+ }
162
+
163
+ /**
164
+ * Get a chunk of records
165
+ *
166
+ * @param int $limit (optional)
167
+ * @param int $offset (optional)
168
+ *
169
+ * @return array|bool An array of record arrays, or FALSE if no records were found
170
+ */
171
+ private function get_records( $limit = 100, $offset = 0 ) {
172
+ $limit = is_int( $limit ) ? $limit : $this->limit;
173
+
174
+ $query = array(
175
+ 'size' => absint( $limit ),
176
+ 'from' => absint( $offset ),
177
+ );
178
+
179
+ $response = $this->search( $query );
180
+
181
+ if ( empty( $response->records ) ) {
182
+ return false;
183
+ }
184
+
185
+ return $response->records;
186
+ }
187
+
188
+ /**
189
+ * Determine where and when the migrate notice should be displayed
190
+ *
191
+ * @see Admin->admin_enqueue_scripts()
192
+ *
193
+ * @return bool
194
+ */
195
+ public function show_migrate_notice() {
196
+ if (
197
+ ! isset( $_GET['migrate_action'] ) // input var okay
198
+ &&
199
+ $this->is_connected()
200
+ &&
201
+ ! empty( $this->record_count )
202
+ &&
203
+ false === get_transient( 'wp_stream_delay_migration' )
204
+ ) {
205
+ return true;
206
+ }
207
+
208
+ return false;
209
+ }
210
+
211
+ /**
212
+ * Give the user options for how to handle their records
213
+ *
214
+ * @action admin_notices
215
+ */
216
+ public function migrate_notice() {
217
+ if ( ! $this->show_migrate_notice() ) {
218
+ return;
219
+ }
220
+
221
+ $notice = sprintf(
222
+ '<h3>%s</h3><strong id="stream-migrate-title">%s</strong></p><p id="stream-migrate-blog-link"><a href="https://wp-stream.com/introducing-stream-3/" target="_blank">%s</a></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-ignore-migrate" class="delete">%s</a>',
223
+ __( 'Stream Records Update' ),
224
+ __( 'Our cloud storage services will be shutting down permanently on September 1, 2015', 'stream' ),
225
+ __( 'Read the announcement post', 'stream' ),
226
+ sprintf( esc_html__( 'We found %s activity records in the cloud that need to be migrated to your local database.', 'stream' ), number_format( $this->record_count ) ),
227
+ __( 'Close', 'stream' ),
228
+ __( 'Start Migration Now', 'stream' ),
229
+ __( 'Remind Me Later', 'stream' ),
230
+ __( "No thanks, I don't want to migrate", 'stream' )
231
+ );
232
+
233
+ $this->plugin->admin->notice( $notice, true );
234
+ }
235
+
236
+ /**
237
+ * Ajax callback for processing migrate actions
238
+ *
239
+ * Break down the total number of records found into reasonably-sized
240
+ * chunks and save records from each of those chunks to the local DB.
241
+ *
242
+ * Disconnects from WP Stream once the migration is complete.
243
+ *
244
+ * @action wp_ajax_wp_stream_migrate_action
245
+ */
246
+ public function migrate_action_callback() {
247
+ $action = wp_stream_filter_input( INPUT_POST, 'migrate_action' );
248
+ $nonce = wp_stream_filter_input( INPUT_POST, 'nonce' );
249
+
250
+ if ( ! wp_verify_nonce( $nonce, 'wp_stream_migrate-' . absint( get_current_blog_id() ) . absint( get_current_user_id() ) ) ) {
251
+ return;
252
+ }
253
+
254
+ set_time_limit( 0 ); // Just in case, this could take a while for some
255
+
256
+ switch ( $action ) {
257
+ case 'migrate':
258
+ case 'continue':
259
+ $this->migrate();
260
+ break;
261
+ case 'delay':
262
+ $this->delay();
263
+ break;
264
+ case 'ignore':
265
+ $this->ignore();
266
+ break;
267
+ }
268
+
269
+ die();
270
+ }
271
+
272
+ /**
273
+ * Migrate a chunk of records
274
+ *
275
+ * @return string JSON data
276
+ */
277
+ private function migrate() {
278
+ $records = $this->get_records( $this->limit );
279
+
280
+ // Disconnect when there are no records left
281
+ if ( ! $records ) {
282
+ $this->disconnect();
283
+
284
+ wp_send_json_success( esc_html__( 'Migration complete!', 'stream' ) );
285
+ }
286
+
287
+ $records_saved = $this->save_records( $records );
288
+
289
+ if ( true !== $records_saved ) {
290
+ wp_send_json_error( esc_html__( 'An unknown error occurred during migration. Please try again later or contact support.', 'stream' ) );
291
+ }
292
+
293
+ wp_send_json_success( 'continue' );
294
+ }
295
+
296
+ /**
297
+ * Delay the migration of records for 3 hours
298
+ *
299
+ * @return string JSON data
300
+ */
301
+ private function delay() {
302
+ set_transient( 'wp_stream_delay_migration', "Don't nag me, bro", HOUR_IN_SECONDS * 3 );
303
+
304
+ wp_send_json_success( esc_html__( "OK, we'll remind you again in a few hours.", 'stream' ) );
305
+ }
306
+
307
+ /**
308
+ * Don't migrate any records
309
+ *
310
+ * @return string JSON data
311
+ */
312
+ private function ignore() {
313
+ $this->disconnect();
314
+
315
+ wp_send_json_success( esc_html__( 'All new activity will be stored in the local database.', 'stream' ) );
316
+ }
317
+
318
+ /**
319
+ * Save records to the database
320
+ *
321
+ * @param array $records
322
+ *
323
+ * @return bool
324
+ */
325
+ private function save_records( $records ) {
326
+ foreach ( $records as $record ) {
327
+ // Remove existing meta field
328
+ unset( $record->meta );
329
+
330
+ // Map fields to the newer data model
331
+ $record->user_id = $record->author;
332
+ $record->user_role = $record->author_role;
333
+ $record->meta = $record->stream_meta;
334
+ $record->meta->user_meta = $record->author_meta;
335
+
336
+ // Convert the record object to a record array
337
+ // @codingStandardsIgnoreStart
338
+ $record = json_decode( json_encode( $record ), true );
339
+ // @codingStandardsIgnoreEnd
340
+
341
+ // Save the record
342
+ $inserted = $this->plugin->db->insert( $record );
343
+
344
+ // Save the date of the last known migrated record
345
+ if ( false !== $inserted ) {
346
+ update_option( 'wp_stream_last_migrated', $record['created'] );
347
+ }
348
+ }
349
+
350
+ return true;
351
+ }
352
+
353
+ }
classes/class-network.php ADDED
@@ -0,0 +1,524 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace WP_Stream;
3
+
4
+ class Network {
5
+ /**
6
+ * Hold Plugin class
7
+ * @var Plugin
8
+ */
9
+ public $plugin;
10
+
11
+ public $network_settings_page_slug = 'wp_stream_network_settings';
12
+
13
+ public $default_settings_page_slug = 'wp_stream_default_settings';
14
+
15
+ function __construct( $plugin ) {
16
+ $this->plugin = $plugin;
17
+
18
+ // Always add default site_id/blog_id params when multisite
19
+ if ( is_multisite() ) {
20
+ add_filter( 'wp_stream_query_args', array( $this, 'network_query_args' ) );
21
+ }
22
+
23
+ // Bail early if not network-activated
24
+ if ( ! $this->is_network_activated() ) {
25
+ return;
26
+ }
27
+
28
+ // Actions
29
+ add_action( 'init', array( $this, 'ajax_network_admin' ) );
30
+ add_action( 'network_admin_menu', array( $this->plugin->admin, 'register_menu' ) );
31
+ add_action( 'network_admin_menu', array( $this, 'admin_menu_screens' ) );
32
+ add_action( 'admin_menu', array( $this, 'admin_menu_screens' ) );
33
+ add_action( 'admin_bar_menu', array( $this, 'network_admin_bar_menu' ), 99 );
34
+ add_action( 'network_admin_notices', array( $this->plugin->admin, 'admin_notices' ) );
35
+ add_action( 'wpmuadminedit', array( $this, 'network_options_action' ) );
36
+ add_action( 'update_site_option_' . $this->plugin->settings->network_options_key, array( $this, 'updated_option_ttl_remove_records' ), 10, 3 );
37
+
38
+ // Filters
39
+ add_filter( 'wp_stream_blog_id_logged', array( $this, 'blog_id_logged' ) );
40
+ add_filter( 'wp_stream_admin_page_title', array( $this, 'network_admin_page_title' ) );
41
+ add_filter( 'wp_stream_list_table_screen_id', array( $this, 'list_table_screen_id' ) );
42
+ add_filter( 'wp_stream_list_table_filters', array( $this, 'list_table_filters' ) );
43
+ add_filter( 'wp_stream_list_table_columns', array( $this, 'network_admin_columns' ) );
44
+ add_filter( 'wp_stream_settings_form_action', array( $this, 'settings_form_action' ) );
45
+ add_filter( 'wp_stream_settings_form_description', array( $this, 'settings_form_description' ) );
46
+ add_filter( 'wp_stream_settings_option_fields', array( $this, 'get_network_admin_fields' ) );
47
+ add_filter( 'wp_stream_serialized_labels', array( $this, 'get_settings_translations' ) );
48
+ add_filter( 'wp_stream_connectors', array( $this, 'hide_blogs_connector' ) );
49
+ }
50
+
51
+ /**
52
+ * Workaround to get admin-ajax.php to know when the request is from the Network Admin
53
+ *
54
+ * @action init
55
+ *
56
+ * @see https://core.trac.wordpress.org/ticket/22589
57
+ */
58
+ public function ajax_network_admin() {
59
+ if (
60
+ defined( 'DOING_AJAX' )
61
+ &&
62
+ DOING_AJAX
63
+ &&
64
+ preg_match( '#^' . network_admin_url() . '#i', $_SERVER['HTTP_REFERER'] )
65
+ ) {
66
+ define( 'WP_NETWORK_ADMIN', true );
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Builds a stdClass object used when displaying actions done in network administration
72
+ *
73
+ * @return object
74
+ */
75
+ public function get_network_blog() {
76
+ $blog = new \stdClass;
77
+ $blog->blog_id = 0;
78
+ $blog->blogname = esc_html__( 'Network Admin', 'stream' );
79
+
80
+ return $blog;
81
+ }
82
+
83
+ /**
84
+ * Returns true if Stream is network activated, otherwise false
85
+ *
86
+ * @return bool
87
+ */
88
+ public function is_network_activated() {
89
+ if ( ! function_exists( 'is_plugin_active_for_network' ) ) {
90
+ require_once( ABSPATH . '/wp-admin/includes/plugin.php' );
91
+ }
92
+
93
+ return is_plugin_active_for_network( $this->plugin->locations['plugin'] );
94
+ }
95
+
96
+ /**
97
+ * Adds Stream to the admin bar under the "My Sites > Network Admin" menu
98
+ * if Stream has been network-activated.
99
+ *
100
+ * @action admin_bar_menu
101
+ *
102
+ * @param object $admin_bar
103
+ *
104
+ * @return void
105
+ */
106
+ public function network_admin_bar_menu( $admin_bar ) {
107
+ if ( ! $this->is_network_activated() ) {
108
+ return;
109
+ }
110
+
111
+ $href = add_query_arg(
112
+ array(
113
+ 'page' => $this->plugin->admin->records_page_slug,
114
+ ),
115
+ network_admin_url( $this->plugin->admin->admin_parent_page )
116
+ );
117
+
118
+ $admin_bar->add_menu(
119
+ array(
120
+ 'id' => 'network-admin-stream',
121
+ 'parent' => 'network-admin',
122
+ 'title' => esc_html__( 'Stream', 'stream' ),
123
+ 'href' => esc_url( $href ),
124
+ )
125
+ );
126
+ }
127
+
128
+ /**
129
+ * Add Network Settings and Default Settings menu items
130
+ *
131
+ * @return array
132
+ */
133
+ public function admin_menu_screens() {
134
+ if ( ! is_network_admin() ) {
135
+ return;
136
+ }
137
+
138
+ remove_submenu_page( $this->plugin->admin->records_page_slug, 'wp_stream_settings' );
139
+
140
+ $this->plugin->admin->screen_id['network_settings'] = add_submenu_page(
141
+ $this->plugin->admin->records_page_slug,
142
+ __( 'Stream Network Settings', 'stream' ),
143
+ __( 'Network Settings', 'stream' ),
144
+ $this->plugin->admin->settings_cap,
145
+ $this->network_settings_page_slug,
146
+ array( $this->plugin->admin, 'render_settings_page' )
147
+ );
148
+ }
149
+
150
+ /**
151
+ * Remove records when records TTL is shortened
152
+ *
153
+ * @param string $option_key
154
+ * @param array $old_value
155
+ * @param array $new_value
156
+ *
157
+ * @action update_option_wp_stream
158
+ * @return void
159
+ */
160
+ public function updated_option_ttl_remove_records( $option_key, $new_value, $old_value ) {
161
+ unset( $option_key );
162
+ $this->plugin->settings->updated_option_ttl_remove_records( $old_value, $new_value );
163
+ }
164
+
165
+ /**
166
+ * Adjust the action of the settings form when in the Network Admin
167
+ *
168
+ * @param $action
169
+ *
170
+ * @return string
171
+ */
172
+ public function settings_form_action( $action ) {
173
+ if ( is_network_admin() ) {
174
+ $current_page = wp_stream_filter_input( INPUT_GET, 'page' );
175
+ $action = add_query_arg( array( 'action' => $current_page ), 'edit.php' );
176
+ }
177
+
178
+ return $action;
179
+ }
180
+
181
+ /**
182
+ * Add a description to each of the Settings pages in the Network Admin
183
+ *
184
+ * @param $description
185
+ *
186
+ * @return string
187
+ */
188
+ public function settings_form_description( $description ) {
189
+ if ( ! is_network_admin() ) {
190
+ return '';
191
+ }
192
+
193
+ $current_page = wp_stream_filter_input( INPUT_GET, 'page' );
194
+
195
+ switch ( $current_page ) {
196
+ case $this->network_settings_page_slug :
197
+ $description = __( 'These settings apply to all sites on the network.', 'stream' );
198
+ break;
199
+ case $this->default_settings_page_slug :
200
+ $description = __( 'These default settings will apply to new sites created on the network. These settings do not alter existing sites.', 'stream' );
201
+ break;
202
+ }
203
+
204
+ return $description;
205
+ }
206
+
207
+ /**
208
+ * Adjusts the settings fields displayed in various network admin screens
209
+ *
210
+ * @param $fields
211
+ *
212
+ * @return mixed
213
+ */
214
+ public function get_network_admin_fields( $fields ) {
215
+ if ( ! $this->is_network_activated() ) {
216
+ return $fields;
217
+ }
218
+
219
+ $stream_hidden_options = apply_filters(
220
+ 'wp_stream_hidden_option_fields',
221
+ array(
222
+ 'general' => array(
223
+ 'delete_all_records',
224
+ 'records_ttl',
225
+ ),
226
+ )
227
+ );
228
+
229
+ $network_hidden_options = apply_filters(
230
+ 'wp_stream_network_option_fields',
231
+ array(
232
+ 'general' => array(
233
+ 'role_access',
234
+ ),
235
+ 'exclude' => array(
236
+ 'authors',
237
+ 'roles',
238
+ 'connectors',
239
+ 'contexts',
240
+ 'actions',
241
+ 'ip_addresses',
242
+ 'hide_previous_records',
243
+ ),
244
+ )
245
+ );
246
+
247
+ // Remove settings based on context
248
+ if ( $this->plugin->settings->network_options_key === $this->plugin->settings->option_key ) {
249
+ $hidden_options = $network_hidden_options;
250
+ } else {
251
+ $hidden_options = $stream_hidden_options;
252
+ }
253
+
254
+ foreach ( $fields as $section_key => $section ) {
255
+ foreach ( $section['fields'] as $key => $field ) {
256
+ if ( ! isset( $hidden_options[ $section_key ] ) ) {
257
+ continue;
258
+ }
259
+
260
+ if ( in_array( $field['name'], $hidden_options[ $section_key ] ) ) {
261
+ unset( $fields[ $section_key ]['fields'][ $key ] );
262
+ }
263
+ }
264
+ }
265
+
266
+ // Add settings based on context
267
+ if ( $this->plugin->settings->network_options_key === $this->plugin->settings->option_key ) {
268
+ $new_fields['general']['fields'][] = array(
269
+ 'name' => 'site_access',
270
+ 'title' => __( 'Site Access', 'stream' ),
271
+ 'after_field' => __( 'Enabled', 'stream' ),
272
+ 'default' => 1,
273
+ 'desc' => __( 'Allow sites on this network to view their Stream activity. Leave unchecked to only allow Stream to be viewed in the Network Admin.', 'stream' ),
274
+ 'type' => 'checkbox',
275
+ );
276
+
277
+ $fields = array_merge_recursive( $new_fields, $fields );
278
+ }
279
+
280
+ // Remove empty settings sections
281
+ foreach ( $fields as $section_key => $section ) {
282
+ if ( empty( $section['fields'] ) ) {
283
+ unset( $fields[ $section_key ] );
284
+ }
285
+ }
286
+
287
+ return $fields;
288
+ }
289
+
290
+ /**
291
+ * Get translations of serialized Stream Network settings
292
+ *
293
+ * @filter wp_stream_serialized_labels
294
+ *
295
+ * @return array Multidimensional array of fields
296
+ */
297
+ public function get_settings_translations( $labels ) {
298
+ $network_key = $this->plugin->settings->network_options_key;
299
+
300
+ if ( ! isset( $labels[ $network_key ] ) ) {
301
+ $labels[ $network_key ] = array();
302
+ }
303
+
304
+ foreach ( $this->plugin->settings->get_fields() as $section_slug => $section ) {
305
+ foreach ( $section['fields'] as $field ) {
306
+ $labels[ $network_key ][ sprintf( '%s_%s', $section_slug, $field['name'] ) ] = $field['title'];
307
+ }
308
+ }
309
+
310
+ return $labels;
311
+ }
312
+
313
+ /**
314
+ * Wrapper for the settings API to work on the network settings page
315
+ */
316
+ public function network_options_action() {
317
+ $allowed_referers = array(
318
+ $this->network_settings_page_slug,
319
+ $this->default_settings_page_slug,
320
+ );
321
+
322
+ if ( ! isset( $_GET['action'] ) || ! in_array( $_GET['action'], $allowed_referers ) ) {
323
+ return;
324
+ }
325
+
326
+ // @codingStandardsIgnoreStart
327
+ $options = isset( $_POST['option_page'] ) ? explode( ',', stripslashes( $_POST['option_page'] ) ) : null;
328
+ // @codingStandardsIgnoreEnd
329
+
330
+ if ( $options ) {
331
+
332
+ foreach ( $options as $option ) {
333
+ $option = trim( $option );
334
+ $value = null;
335
+ $sections = $this->plugin->settings->get_fields();
336
+
337
+ foreach ( $sections as $section_name => $section ) {
338
+ foreach ( $section['fields'] as $field_idx => $field ) {
339
+ $option_key = $section_name . '_' . $field['name'];
340
+
341
+ // @codingStandardsIgnoreStart
342
+ if ( isset( $_POST[ $option ][ $option_key ] ) ) {
343
+ $value[ $option_key ] = $_POST[ $option ][ $option_key ];
344
+ } else {
345
+ $value[ $option_key ] = false;
346
+ }
347
+ // @codingStandardsIgnoreEnd
348
+ }
349
+ }
350
+
351
+ if ( ! is_array( $value ) ) {
352
+ $value = trim( $value );
353
+ }
354
+
355
+ update_site_option( $option, $value );
356
+ }
357
+ }
358
+
359
+ if ( ! count( get_settings_errors() ) ) {
360
+ add_settings_error( 'general', 'settings_updated', __( 'Settings saved.', 'stream' ), 'updated' );
361
+ }
362
+
363
+ set_transient( 'settings_errors', get_settings_errors(), 30 );
364
+
365
+ $go_back = add_query_arg( 'settings-updated', 'true', wp_get_referer() );
366
+
367
+ wp_redirect( $go_back );
368
+
369
+ exit;
370
+ }
371
+
372
+ /**
373
+ * Add the Site filter to the Network records screen
374
+ *
375
+ * @filter wp_stream_list_table_filters
376
+ *
377
+ * @param $filters
378
+ *
379
+ * @return array
380
+ */
381
+ public function list_table_filters( $filters ) {
382
+ if ( ! is_network_admin() || wp_is_large_network() ) {
383
+ return $filters;
384
+ }
385
+
386
+ $blogs = array();
387
+
388
+ // Display network blog as the first option
389
+ $network_blog = $this->get_network_blog();
390
+
391
+ $blogs[ $network_blog->blog_id ] = array(
392
+ 'label' => $network_blog->blogname,
393
+ 'disabled' => '',
394
+ );
395
+
396
+ // add all sites
397
+ foreach ( wp_get_sites() as $blog ) {
398
+ $blog_data = get_blog_details( $blog );
399
+
400
+ $blogs[ $blog['blog_id'] ] = array(
401
+ 'label' => $blog_data->blogname,
402
+ 'disabled' => '',
403
+ );
404
+ }
405
+
406
+ $filters['blog_id'] = array(
407
+ 'title' => __( 'sites', 'stream' ),
408
+ 'items' => $blogs,
409
+ );
410
+
411
+ return $filters;
412
+ }
413
+
414
+ /**
415
+ * Add the Site toggle to screen options in network admin
416
+ *
417
+ * @param $filters
418
+ *
419
+ * @return array
420
+ */
421
+ public function toggle_filters( $filters ) {
422
+ if ( is_network_admin() ) {
423
+ $filters['blog_id'] = esc_html__( 'Site', 'stream' );
424
+ }
425
+
426
+ return $filters;
427
+ }
428
+
429
+ /**
430
+ * Add the network suffix to the $screen_id when in the network admin
431
+ *
432
+ * @param $screen_id
433
+ *
434
+ * @return string
435
+ */
436
+ public function list_table_screen_id( $screen_id ) {
437
+ if ( $screen_id && is_network_admin() ) {
438
+ if ( '-network' !== substr( $screen_id, -8 ) ) {
439
+ $screen_id .= '-network';
440
+ }
441
+ }
442
+
443
+ return $screen_id;
444
+ }
445
+
446
+ /**
447
+ * Set blog_id for network admin activity
448
+ *
449
+ * @return int
450
+ */
451
+ public function blog_id_logged( $blog_id ) {
452
+ return is_network_admin() ? 0 : $blog_id;
453
+ }
454
+
455
+ /**
456
+ * Customize query args on multisite installs
457
+ *
458
+ * @filter wp_stream_query_args
459
+ *
460
+ * @param array $args
461
+ *
462
+ * @return array
463
+ */
464
+ public function network_query_args( $args ) {
465
+ $args['site_id'] = is_numeric( $args['site_id'] ) ? $args['site_id'] : get_current_site()->id;
466
+ $args['blog_id'] = is_numeric( $args['blog_id'] ) ? $args['blog_id'] : ( is_network_admin() ? null : get_current_blog_id() );
467
+
468
+ return $args;
469
+ }
470
+
471
+ /**
472
+ * Add site count to the page title in the network admin
473
+ *
474
+ * @filter wp_stream_admin_page_title
475
+ *
476
+ * @param string $page_title
477
+ *
478
+ * @return string
479
+ */
480
+ public function network_admin_page_title( $page_title ) {
481
+ if ( is_network_admin() ) {
482
+ $site_count = sprintf( _n( '1 site', '%s sites', get_blog_count(), 'stream' ), number_format( get_blog_count() ) );
483
+ $page_title = sprintf( '%s (%s)', $page_title, $site_count );
484
+ }
485
+
486
+ return $page_title;
487
+ }
488
+
489
+ /**
490
+ * Add the Site column to the network stream records
491
+ *
492
+ * @param $columns
493
+ *
494
+ * @return mixed
495
+ */
496
+ public function network_admin_columns( $columns ) {
497
+ if ( is_network_admin() ) {
498
+ $columns = array_merge(
499
+ array_slice( $columns, 0, -1 ),
500
+ array(
501
+ 'blog_id' => esc_html__( 'Site', 'stream' ),
502
+ ),
503
+ array_slice( $columns, -1 )
504
+ );
505
+ }
506
+
507
+ return $columns;
508
+ }
509
+
510
+ /**
511
+ * Prevent the Blogs connector from loading when not in Network Admin
512
+ *
513
+ * @param $connectors
514
+ *
515
+ * @return mixed
516
+ */
517
+ public function hide_blogs_connector( $connectors ) {
518
+ if ( ! is_network_admin() ) {
519
+ return array_diff( $connectors, array( 'Connector_Blogs' ) );
520
+ }
521
+
522
+ return $connectors;
523
+ }
524
+ }
classes/class-plugin.php ADDED
@@ -0,0 +1,234 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace WP_Stream;
3
+
4
+ class Plugin {
5
+ /**
6
+ * Plugin version number
7
+ *
8
+ * @const string
9
+ */
10
+ const VERSION = '3.0.0';
11
+
12
+ /**
13
+ * WP-CLI command
14
+ *
15
+ * @const string
16
+ */
17
+ const WP_CLI_COMMAND = 'stream';
18
+
19
+ /**
20
+ * @var Admin
21
+ */
22
+ public $admin;
23
+
24
+ /**
25
+ * @var Connectors
26
+ */
27
+ public $connectors;
28
+
29
+ /**
30
+ * @var DB
31
+ */
32
+ public $db;
33
+
34
+ /**
35
+ * @var Log
36
+ */
37
+ public $log;
38
+
39
+ /**
40
+ * @var Settings
41
+ */
42
+ public $settings;
43
+
44
+ /**
45
+ * @var Install
46
+ */
47
+ public $install;
48
+
49
+ /**
50
+ * URLs and Paths used by the plugin
51
+ *
52
+ * @var array
53
+ */
54
+ public $locations = array();
55
+
56
+ /**
57
+ * Class constructor
58
+ */
59
+ public function __construct() {
60
+ $locate = $this->locate_plugin();
61
+
62
+ $this->locations = array(
63
+ 'plugin' => $locate['plugin_basename'],
64
+ 'dir' => $locate['dir_path'],
65
+ 'url' => $locate['dir_url'],
66
+ 'inc_dir' => $locate['dir_path'] . 'includes/',
67
+ 'class_dir' => $locate['dir_path'] . 'classes/',
68
+ );
69
+
70
+ spl_autoload_register( array( $this, 'autoload' ) );
71
+
72
+ // Load helper functions
73
+ require_once $this->locations['inc_dir'] . 'functions.php';
74
+
75
+ // Load DB helper interface/class
76
+ $driver = '\WP_Stream\DB';
77
+ if ( class_exists( $driver ) ) {
78
+ $this->db = new DB( $this );
79
+ }
80
+
81
+ if ( ! $this->db ) {
82
+ wp_die(
83
+ esc_html__( 'Stream: Could not load chosen DB driver.', 'stream' ),
84
+ esc_html__( 'Stream DB Error', 'stream' )
85
+ );
86
+ }
87
+
88
+ // Load languages
89
+ add_action( 'plugins_loaded', array( $this, 'i18n' ) );
90
+
91
+ // Load logger class
92
+ $this->log = apply_filters( 'wp_stream_log_handler', new Log( $this ) );
93
+
94
+ // Load settings and connectors after widgets_init and before the default init priority
95
+ add_action( 'init', array( $this, 'init' ), 9 );
96
+
97
+ // Add frontend indicator
98
+ add_action( 'wp_head', array( $this, 'frontend_indicator' ) );
99
+
100
+ // Load admin area classes
101
+ if ( is_admin() || ( defined( 'WP_STREAM_DEV_DEBUG' ) && WP_STREAM_DEV_DEBUG ) ) {
102
+ $this->admin = new Admin( $this );
103
+ $this->install = new Install( $this );
104
+ }
105
+
106
+ // Load WP-CLI command
107
+ if ( defined( 'WP_CLI' ) && WP_CLI ) {
108
+ \WP_CLI::add_command( self::WP_CLI_COMMAND, 'WP_Stream\CLI' );
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Autoloader for classes
114
+ *
115
+ * @param string $class
116
+ */
117
+ function autoload( $class ) {
118
+ if ( ! preg_match( '/^(?P<namespace>.+)\\\\(?P<autoload>[^\\\\]+)$/', $class, $matches ) ) {
119
+ return;
120
+ }
121
+
122
+ static $reflection;
123
+
124
+ if ( empty( $reflection ) ) {
125
+ $reflection = new \ReflectionObject( $this );
126
+ }
127
+
128
+ if ( $reflection->getNamespaceName() !== $matches['namespace'] ) {
129
+ return;
130
+ }
131
+
132
+ $autoload_name = $matches['autoload'];
133
+ $autoload_dir = \trailingslashit( $this->locations['class_dir'] );
134
+ $autoload_path = sprintf( '%sclass-%s.php', $autoload_dir, strtolower( str_replace( '_', '-', $autoload_name ) ) );
135
+
136
+ if ( is_readable( $autoload_path ) ) {
137
+ require_once $autoload_path;
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Loads the translation files.
143
+ *
144
+ * @action plugins_loaded
145
+ */
146
+ public function i18n() {
147
+ load_plugin_textdomain( 'stream', false, dirname( $this->locations['plugin'] ) . '/languages/' );
148
+ }
149
+
150
+ /*
151
+ * Load Settings and Connectors
152
+ *
153
+ * @action init
154
+ */
155
+ public function init() {
156
+ $this->settings = new Settings( $this );
157
+ $this->connectors = new Connectors( $this );
158
+ }
159
+
160
+ /**
161
+ * Displays an HTML comment in the frontend head to indicate that Stream is activated,
162
+ * and which version of Stream is currently in use.
163
+ *
164
+ * @action wp_head
165
+ *
166
+ * @return string|void An HTML comment, or nothing if the value is filtered out.
167
+ */
168
+ public function frontend_indicator() {
169
+ $comment = sprintf( 'Stream WordPress user activity plugin v%s', esc_html( $this->get_version() ) ); // Localization not needed
170
+
171
+ /**
172
+ * Filter allows the HTML output of the frontend indicator comment
173
+ * to be altered or removed, if desired.
174
+ *
175
+ * @return string The content of the HTML comment
176
+ */
177
+ $comment = apply_filters( 'wp_stream_frontend_indicator', $comment );
178
+
179
+ if ( ! empty( $comment ) ) {
180
+ echo sprintf( "<!-- %s -->\n", esc_html( $comment ) ); // xss ok
181
+ }
182
+ }
183
+
184
+ /**
185
+ * Version of plugin_dir_url() which works for plugins installed in the plugins directory,
186
+ * and for plugins bundled with themes.
187
+ *
188
+ * @throws \Exception
189
+ *
190
+ * @return array
191
+ */
192
+ private function locate_plugin() {
193
+ $reflection = new \ReflectionObject( $this );
194
+ $file_name = $reflection->getFileName();
195
+
196
+ if ( '/' !== \DIRECTORY_SEPARATOR ) {
197
+ $file_name = str_replace( \DIRECTORY_SEPARATOR, '/', $file_name ); // Windows compat
198
+ }
199
+
200
+ $plugin_dir = preg_replace( '#(.*plugins[^/]*/[^/]+)(/.*)?#', '$1', $file_name, 1, $count );
201
+
202
+ if ( 0 === $count ) {
203
+ throw new \Exception( "Class not located within a directory tree containing 'plugins': $file_name" );
204
+ }
205
+
206
+ // Make sure that we can reliably get the relative path inside of the content directory
207
+ $content_dir = trailingslashit( WP_CONTENT_DIR );
208
+
209
+ if ( '/' !== \DIRECTORY_SEPARATOR ) {
210
+ $content_dir = str_replace( \DIRECTORY_SEPARATOR, '/', $content_dir ); // Windows compat
211
+ }
212
+
213
+ if ( 0 !== strpos( $plugin_dir, $content_dir ) ) {
214
+ throw new \Exception( 'Plugin dir is not inside of WP_CONTENT_DIR' );
215
+ }
216
+
217
+ $content_sub_path = substr( $plugin_dir, strlen( $content_dir ) );
218
+ $dir_url = content_url( trailingslashit( $content_sub_path ) );
219
+ $dir_path = trailingslashit( $plugin_dir );
220
+ $dir_basename = basename( $plugin_dir );
221
+ $plugin_basename = trailingslashit( $dir_basename ) . $dir_basename. '.php';
222
+
223
+ return compact( 'dir_url', 'dir_path', 'dir_basename', 'plugin_basename' );
224
+ }
225
+
226
+ /**
227
+ * Getter for the version number.
228
+ *
229
+ * @return string
230
+ */
231
+ public function get_version() {
232
+ return self::VERSION;
233
+ }
234
+ }
classes/class-query.php ADDED
@@ -0,0 +1,349 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace WP_Stream;
3
+
4
+ class Query {
5
+ /**
6
+ * @var DB
7
+ */
8
+ public $db;
9
+
10
+ /**
11
+ * Hold the number of records found
12
+ *
13
+ * @var int
14
+ */
15
+ public $found_records = 0;
16
+
17
+ /**
18
+ * Class constructor.
19
+ *
20
+ * @param DB $db The parent database class.
21
+ */
22
+ public function __construct( $db ) {
23
+ $this->db = $db;
24
+ }
25
+
26
+ /**
27
+ * Query records
28
+ *
29
+ * @param array Query args
30
+ *
31
+ * @return array Stream Records
32
+ */
33
+ public function query( $args ) {
34
+ global $wpdb;
35
+
36
+ $defaults = array(
37
+ // Search param
38
+ 'search' => null,
39
+ 'search_field' => 'summary',
40
+ 'record_after' => null, // Deprecated, use date_after instead
41
+ // Date-based filters
42
+ 'date' => null, // Ex: 2015-07-01
43
+ 'date_from' => null, // Ex: 2015-07-01
44
+ 'date_to' => null, // Ex: 2015-07-01
45
+ 'date_after' => null, // Ex: 2015-07-01T15:19:21+00:00
46
+ 'date_before' => null, // Ex: 2015-07-01T15:19:21+00:00
47
+ // Record ID filters
48
+ 'record' => null,
49
+ 'record__in' => array(),
50
+ 'record__not_in' => array(),
51
+ // Pagination params
52
+ 'records_per_page' => get_option( 'posts_per_page', 20 ),
53
+ 'paged' => 1,
54
+ // Order
55
+ 'order' => 'desc',
56
+ 'orderby' => 'date',
57
+ // Fields selection
58
+ 'fields' => array(),
59
+ );
60
+
61
+ // Additional property fields
62
+ $properties = array(
63
+ 'user_id' => null,
64
+ 'user_role' => null,
65
+ 'ip' => null,
66
+ 'object_id' => null,
67
+ 'site_id' => null,
68
+ 'blog_id' => null,
69
+ 'connector' => null,
70
+ 'context' => null,
71
+ 'action' => null,
72
+ );
73
+
74
+ /**
75
+ * Filter allows additional query properties to be added
76
+ *
77
+ * @return array Array of query properties
78
+ */
79
+ $properties = apply_filters( 'wp_stream_query_properties', $properties );
80
+
81
+ // Add property fields to defaults, including their __in/__not_in variations
82
+ foreach ( $properties as $property => $default ) {
83
+ if ( ! isset( $defaults[ $property ] ) ) {
84
+ $defaults[ $property ] = $default;
85
+ }
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
+ * @return array Array of query arguments
97
+ */
98
+ $args = apply_filters( 'wp_stream_query_args', $args );
99
+
100
+ $join = '';
101
+ $where = '';
102
+
103
+ /**
104
+ * PARSE CORE PARAMS
105
+ */
106
+ if ( is_numeric( $args['site_id'] ) ) {
107
+ $where .= $wpdb->prepare( " AND $wpdb->stream.site_id = %d", $args['site_id'] );
108
+ }
109
+
110
+ if ( is_numeric( $args['blog_id'] ) ) {
111
+ $where .= $wpdb->prepare( " AND $wpdb->stream.blog_id = %d", $args['blog_id'] );
112
+ }
113
+
114
+ if ( is_numeric( $args['object_id'] ) ) {
115
+ $where .= $wpdb->prepare( " AND $wpdb->stream.object_id = %d", $args['object_id'] );
116
+ }
117
+
118
+ if ( is_numeric( $args['user_id'] ) ) {
119
+ $where .= $wpdb->prepare( " AND $wpdb->stream.user_id = %d", $args['user_id'] );
120
+ }
121
+
122
+ if ( ! empty( $args['user_role'] ) ) {
123
+ $where .= $wpdb->prepare( " AND $wpdb->stream.user_role = %s", $args['user_role'] );
124
+ }
125
+
126
+ if ( ! empty( $args['search'] ) ) {
127
+ $field = ! empty( $args['search_field'] ) ? $args['search_field'] : 'summary';
128
+ $where .= $wpdb->prepare( " AND $wpdb->stream.{$field} LIKE %s", "%{$args['search']}%" );
129
+ }
130
+
131
+ if ( ! empty( $args['connector'] ) ) {
132
+ $where .= $wpdb->prepare( " AND $wpdb->stream.connector = %s", $args['connector'] );
133
+ }
134
+
135
+ if ( ! empty( $args['context'] ) ) {
136
+ $where .= $wpdb->prepare( " AND $wpdb->stream.context = %s", $args['context'] );
137
+ }
138
+
139
+ if ( ! empty( $args['action'] ) ) {
140
+ $where .= $wpdb->prepare( " AND $wpdb->stream.action = %s", $args['action'] );
141
+ }
142
+
143
+ if ( ! empty( $args['ip'] ) ) {
144
+ $where .= $wpdb->prepare( " AND $wpdb->stream.ip = %s", wp_stream_filter_var( $args['ip'], FILTER_VALIDATE_IP ) );
145
+ }
146
+
147
+ /**
148
+ * PARSE DATE PARAM FAMILY
149
+ */
150
+ if ( ! empty( $args['date_from'] ) ) {
151
+ $date = get_gmt_from_date( date( 'Y-m-d H:i:s', strtotime( $args['date_from'] . ' 00:00:00' ) ) );
152
+ $where .= $wpdb->prepare( " AND DATE($wpdb->stream.created) >= %s", $date );
153
+ }
154
+
155
+ if ( ! empty( $args['date_to'] ) ) {
156
+ $date = get_gmt_from_date( date( 'Y-m-d H:i:s', strtotime( $args['date_to'] . ' 23:59:59' ) ) );
157
+ $where .= $wpdb->prepare( " AND DATE($wpdb->stream.created) <= %s", $date );
158
+ }
159
+
160
+ if ( ! empty( $args['date_after'] ) ) {
161
+ $date = get_gmt_from_date( date( 'Y-m-d H:i:s', strtotime( $args['date_after'] ) ) );
162
+ $where .= $wpdb->prepare( " AND DATE($wpdb->stream.created) > %s", $date );
163
+ }
164
+
165
+ if ( ! empty( $args['date_before'] ) ) {
166
+ $date = get_gmt_from_date( date( 'Y-m-d H:i:s', strtotime( $args['date_before'] ) ) );
167
+ $where .= $wpdb->prepare( " AND DATE($wpdb->stream.created) < %s", $date );
168
+ }
169
+
170
+ if ( ! empty( $args['date'] ) ) {
171
+ $args['date_from'] = date( 'Y-m-d', strtotime( $args['date'] ) ) . ' 00:00:00';
172
+ $args['date_to'] = date( 'Y-m-d', strtotime( $args['date'] ) ) . ' 23:59:59';
173
+ }
174
+
175
+ /**
176
+ * PARSE __IN PARAM FAMILY
177
+ */
178
+ $ins = array();
179
+
180
+ foreach ( $args as $arg => $value ) {
181
+ if ( '__in' === substr( $arg, -4 ) ) {
182
+ $ins[ $arg ] = $value;
183
+ }
184
+ }
185
+
186
+ if ( ! empty( $ins ) ) {
187
+ foreach ( $ins as $key => $value ) {
188
+ if ( empty( $value ) || ! is_array( $value ) ) {
189
+ continue;
190
+ }
191
+
192
+ $field = str_replace( array( 'record_', '__in' ), '', $key );
193
+ $field = empty( $field ) ? 'ID' : $field;
194
+ $type = is_numeric( array_shift( $value ) ) ? '%d' : '%s';
195
+
196
+ if ( ! empty( $value ) ) {
197
+ $format = '(' . join( ',', array_fill( 0, count( $value ), $type ) ) . ')';
198
+ $where .= $wpdb->prepare( " AND $wpdb->stream.%s IN {$format}", $field, $value );
199
+ }
200
+ }
201
+ }
202
+
203
+ /**
204
+ * PARSE __NOT_IN PARAM FAMILY
205
+ */
206
+ $not_ins = array();
207
+
208
+ foreach ( $args as $arg => $value ) {
209
+ if ( '__not_in' === substr( $arg, -8 ) ) {
210
+ $not_ins[ $arg ] = $value;
211
+ }
212
+ }
213
+
214
+ if ( ! empty( $not_ins ) ) {
215
+ foreach ( $not_ins as $key => $value ) {
216
+ if ( empty( $value ) || ! is_array( $value ) ) {
217
+ continue;
218
+ }
219
+
220
+ $field = str_replace( array( 'record_', '__not_in' ), '', $key );
221
+ $field = empty( $field ) ? 'ID' : $field;
222
+ $type = is_numeric( array_shift( $value ) ) ? '%d' : '%s';
223
+
224
+ if ( ! empty( $value ) ) {
225
+ $format = '(' . join( ',', array_fill( 0, count( $value ), $type ) ) . ')';
226
+ $where .= $wpdb->prepare( " AND $wpdb->stream.%s NOT IN {$format}", $field, $value );
227
+ }
228
+ }
229
+ }
230
+
231
+ /**
232
+ * PARSE PAGINATION PARAMS
233
+ */
234
+ $limits = '';
235
+ $page = absint( $args['paged'] );
236
+ $per_page = absint( $args['records_per_page'] );
237
+
238
+ if ( $per_page >= 0 ) {
239
+ $offset = absint( ( $page - 1 ) * $per_page );
240
+ $limits = "LIMIT {$offset}, {$per_page}";
241
+ }
242
+
243
+ /**
244
+ * PARSE ORDER PARAMS
245
+ */
246
+ $order = esc_sql( $args['order'] );
247
+ $orderby = esc_sql( $args['orderby'] );
248
+ $orderable = array( 'ID', 'site_id', 'blog_id', 'object_id', 'user_id', 'user_role', 'summary', 'created', 'connector', 'context', 'action' );
249
+
250
+ if ( in_array( $orderby, $orderable ) ) {
251
+ $orderby = sprintf( '%s.%s', $wpdb->stream, $orderby );
252
+ } elseif ( 'meta_value_num' === $orderby && ! empty( $args['meta_key'] ) ) {
253
+ $orderby = "CAST($wpdb->streammeta.meta_value AS SIGNED)";
254
+ } elseif ( 'meta_value' === $orderby && ! empty( $args['meta_key'] ) ) {
255
+ $orderby = "$wpdb->streammeta.meta_value";
256
+ } else {
257
+ $orderby = "$wpdb->stream.ID";
258
+ }
259
+
260
+ $orderby = "ORDER BY {$orderby} {$order}";
261
+
262
+ /**
263
+ * PARSE FIELDS PARAMETER
264
+ */
265
+ $fields = (array) $args['fields'];
266
+ $selects = array();
267
+
268
+ if ( ! empty( $fields ) ) {
269
+ foreach ( $fields as $field ) {
270
+ // We'll query the meta table later
271
+ if ( 'meta' === $field ) {
272
+ continue;
273
+ }
274
+
275
+ $selects[] = sprintf( "$wpdb->stream.%s", $field );
276
+ }
277
+ } else {
278
+ $selects[] = "$wpdb->stream.*";
279
+ }
280
+
281
+ $select = implode( ', ', $selects );
282
+
283
+ /**
284
+ * BUILD THE FINAL QUERY
285
+ */
286
+ $query = "SELECT SQL_CALC_FOUND_ROWS {$select}
287
+ FROM $wpdb->stream
288
+ {$join}
289
+ WHERE 1=1 {$where}
290
+ {$orderby}
291
+ {$limits}";
292
+
293
+ /**
294
+ * Filter allows the final query to be modified before execution
295
+ *
296
+ * @param string $query
297
+ * @param array $args
298
+ *
299
+ * @return string
300
+ */
301
+ $query = apply_filters( 'wp_stream_db_query', $query, $args );
302
+
303
+ /**
304
+ * QUERY THE DATABASE FOR RESULTS
305
+ */
306
+ $results = $wpdb->get_results( $query );
307
+
308
+ // Hold the number of records found
309
+ $this->found_records = absint( $wpdb->get_var( 'SELECT FOUND_ROWS()' ) );
310
+
311
+ // Add meta to the records, when applicable
312
+ if ( empty( $fields ) || in_array( 'meta', $fields ) ) {
313
+ $results = $this->add_record_meta( $results );
314
+ }
315
+
316
+ return (array) $results;
317
+ }
318
+
319
+ /**
320
+ * Add meta to a set of records
321
+ *
322
+ * @param array $records
323
+ *
324
+ * @return array
325
+ */
326
+ public function add_record_meta( $records ) {
327
+ global $wpdb;
328
+
329
+ $record_ids = array_map( 'absint', wp_list_pluck( $records, 'ID' ) );
330
+
331
+ if ( empty( $record_ids ) ) {
332
+ return (array) $records;
333
+ }
334
+
335
+ $sql_meta = sprintf(
336
+ "SELECT * FROM $wpdb->streammeta WHERE record_id IN ( %s )",
337
+ implode( ',', $record_ids )
338
+ );
339
+
340
+ $meta = $wpdb->get_results( $sql_meta );
341
+ $ids_f = array_flip( $record_ids );
342
+
343
+ foreach ( $meta as $meta_record ) {
344
+ $records[ $ids_f[ $meta_record->record_id ] ]->meta[ $meta_record->meta_key ] = maybe_unserialize( $meta_record->meta_value );
345
+ }
346
+
347
+ return (array) $records;
348
+ }
349
+ }
classes/class-record.php ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace WP_Stream;
3
+
4
+ class Record {
5
+ public $ID;
6
+ public $created;
7
+ public $site_id;
8
+ public $blog_id;
9
+ public $object_id;
10
+ public $user_id;
11
+ public $user_role;
12
+ public $user_meta;
13
+ public $summary;
14
+ public $connector;
15
+ public $context;
16
+ public $action;
17
+ public $ip;
18
+ public $meta;
19
+
20
+ public function __construct( $item ) {
21
+ $this->ID = isset( $item->ID ) ? $item->ID : null;
22
+ $this->created = isset( $item->created ) ? $item->created : null;
23
+ $this->site_id = isset( $item->site_id ) ? $item->site_id : null;
24
+ $this->blog_id = isset( $item->blog_id ) ? $item->blog_id : null;
25
+ $this->object_id = isset( $item->object_id ) ? $item->object_id : null;
26
+ $this->user_id = isset( $item->user_id ) ? $item->user_id : null;
27
+ $this->user_role = isset( $item->user_role ) ? $item->user_role : null;
28
+ $this->user_meta = isset( $item->meta['user_meta'] ) ? $item->meta['user_meta'] : null;
29
+ $this->summary = isset( $item->summary ) ? $item->summary : null;
30
+ $this->connector = isset( $item->connector ) ? $item->connector : null;
31
+ $this->context = isset( $item->context ) ? $item->context : null;
32
+ $this->action = isset( $item->action ) ? $item->action : null;
33
+ $this->ip = isset( $item->ip ) ? $item->ip : null;
34
+ $this->meta = isset( $item->meta ) ? $item->meta : null;
35
+
36
+ if ( isset( $this->meta['user_meta'] ) ) {
37
+ unset( $this->meta['user_meta'] );
38
+ }
39
+ }
40
+
41
+ public function save() {
42
+ if ( ! $this->validate() ) {
43
+ return new \WP_Error( 'validation-error', esc_html__( 'Could not validate record data.', 'stream' ) );
44
+ }
45
+
46
+ return wp_stream_get_instance()->db->insert( (array) $this );
47
+ }
48
+
49
+ public function populate( array $raw ) {
50
+ $keys = get_class_vars( $this );
51
+ $data = array_intersect_key( $raw, $keys );
52
+ foreach ( $data as $key => $val ) {
53
+ $this->{$key} = $val;
54
+ }
55
+ }
56
+
57
+ public function validate() {
58
+ return true;
59
+ }
60
+
61
+ /**
62
+ * Query record meta
63
+ *
64
+ * @param string $meta_key (optional)
65
+ * @param bool $single (optional)
66
+ *
67
+ * @return array
68
+ */
69
+ public function get_meta( $meta_key = '', $single = false ) {
70
+ return maybe_unserialize( get_metadata( 'record', $this->ID, $meta_key, $single ) );
71
+ }
72
+
73
+ /**
74
+ * Update record meta
75
+ *
76
+ * @param string $meta_key
77
+ * @param string $meta_value
78
+ * @param string $prev_value (optional)
79
+ *
80
+ * @return array
81
+ */
82
+ public function update_meta( $meta_key, $meta_value, $prev_value = '' ) {
83
+ return update_metadata( 'record', $this->ID, $meta_key, $meta_value, $prev_value );
84
+ }
85
+
86
+ /**
87
+ * Determine the title of an object that a record is for.
88
+ *
89
+ * @param object Record object
90
+ * @return mixed The title of the object as a string, otherwise false
91
+ */
92
+ function get_object_title() {
93
+ if ( ! isset( $this->object_id ) || empty( $this->object_id ) ) {
94
+ return false;
95
+ }
96
+
97
+ $output = false;
98
+
99
+ if ( isset( $this->meta->post_title ) && ! empty( $this->meta->post_title ) ) {
100
+ $output = (string) $this->meta->post_title;
101
+ } elseif ( isset( $this->meta->display_name ) && ! empty( $this->meta->display_name ) ) {
102
+ $output = (string) $this->meta->display_name;
103
+ } elseif ( isset( $this->meta->name ) && ! empty( $this->meta->name ) ) {
104
+ $output = (string) $this->meta->name;
105
+ }
106
+
107
+ return $output;
108
+ }
109
+ }
classes/class-settings.php ADDED
@@ -0,0 +1,1019 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace WP_Stream;
3
+
4
+ use \WP_Roles;
5
+ use \WP_User;
6
+ use \WP_User_Query;
7
+
8
+ class Settings {
9
+ /**
10
+ * Hold Plugin class
11
+ * @var Plugin
12
+ */
13
+ public $plugin;
14
+
15
+ /**
16
+ * Settings key/identifier
17
+ *
18
+ * @var string
19
+ */
20
+ public $option_key = 'wp_stream';
21
+
22
+ /**
23
+ * Network settings key/identifier
24
+ *
25
+ * @var string
26
+ */
27
+ public $network_options_key = 'wp_stream_network';
28
+
29
+ /**
30
+ * Plugin settings
31
+ *
32
+ * @var array
33
+ */
34
+ public $options = array();
35
+
36
+ /**
37
+ * Settings fields
38
+ *
39
+ * @var array
40
+ */
41
+ public $fields = array();
42
+
43
+ /**
44
+ * Class constructor.
45
+ *
46
+ * @param Plugin $plugin The main Plugin class.
47
+ */
48
+ public function __construct( $plugin ) {
49
+ $this->plugin = $plugin;
50
+
51
+ $this->option_key = $this->get_option_key();
52
+ $this->options = $this->get_options();
53
+
54
+ // Register settings, and fields
55
+ add_action( 'admin_init', array( $this, 'register_settings' ) );
56
+
57
+ // Remove records when records TTL is shortened
58
+ add_action( 'update_option_' . $this->option_key, array( $this, 'updated_option_ttl_remove_records' ), 10, 2 );
59
+
60
+ // Apply label translations for settings
61
+ add_filter( 'wp_stream_serialized_labels', array( $this, 'get_settings_translations' ) );
62
+
63
+ // Ajax callback function to search users
64
+ add_action( 'wp_ajax_stream_get_users', array( $this, 'get_users' ) );
65
+
66
+ // Ajax callback function to search IPs
67
+ add_action( 'wp_ajax_stream_get_ips', array( $this, 'get_ips' ) );
68
+ }
69
+
70
+ /**
71
+ * Ajax callback function to search users, used on exclude setting page
72
+ *
73
+ * @uses \WP_User_Query
74
+ */
75
+ public function get_users() {
76
+ if ( ! defined( 'DOING_AJAX' ) || ! current_user_can( $this->plugin->admin->settings_cap ) ) {
77
+ return;
78
+ }
79
+
80
+ check_ajax_referer( 'stream_get_users', 'nonce' );
81
+
82
+ $response = (object) array(
83
+ 'status' => false,
84
+ 'message' => esc_html__( 'There was an error in the request', 'stream' ),
85
+ );
86
+
87
+ $search = '';
88
+ if ( isset( $_POST['find'] ) ) {
89
+ // @TODO: Make this pass phpcs
90
+ $search = esc_html( wp_unslash( trim( $_POST['find'] ) ) ); // input var okay
91
+ }
92
+ $request = (object) array(
93
+ 'find' => $search,
94
+ );
95
+
96
+ add_filter( 'user_search_columns', array( $this, 'add_display_name_search_columns' ), 10, 3 );
97
+
98
+ $users = new WP_User_Query(
99
+ array(
100
+ 'search' => "*{$request->find}*",
101
+ 'search_columns' => array(
102
+ 'user_login',
103
+ 'user_nicename',
104
+ 'user_email',
105
+ 'user_url',
106
+ ),
107
+ 'orderby' => 'display_name',
108
+ 'number' => $this->plugin->admin->preload_users_max,
109
+ )
110
+ );
111
+
112
+ remove_filter( 'user_search_columns', array( $this, 'add_display_name_search_columns' ), 10 );
113
+
114
+ if ( 0 === $users->get_total() ) {
115
+ wp_send_json_error( $response );
116
+ }
117
+
118
+ $response->status = true;
119
+ $response->message = '';
120
+ $response->users = array();
121
+
122
+ foreach ( $users->results as $key => $user ) {
123
+ $author = new Author( $user->ID );
124
+
125
+ $args = array(
126
+ 'id' => $author->ID,
127
+ 'text' => $author->display_name,
128
+ );
129
+
130
+ $args['tooltip'] = esc_attr(
131
+ sprintf(
132
+ __( "ID: %d\nUser: %s\nEmail: %s\nRole: %s", 'stream' ),
133
+ $author->id,
134
+ $author->user_login,
135
+ $author->user_email,
136
+ ucwords( $author->get_role() )
137
+ )
138
+ );
139
+
140
+ $args['icon'] = $author->get_avatar_src( 32 );
141
+
142
+ $response->users[] = $args;
143
+ }
144
+
145
+ if ( empty( $search ) || preg_match( '/wp|cli|system|unknown/i', $search ) ) {
146
+ $author = new Author( 0 );
147
+ $response->users[] = array(
148
+ 'id' => $author->id,
149
+ 'text' => $author->get_display_name(),
150
+ 'icon' => $author->get_avatar_src( 32 ),
151
+ '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' ),
152
+ );
153
+ }
154
+
155
+ wp_send_json_success( $response );
156
+ }
157
+
158
+ /**
159
+ * Ajax callback function to search IP addresses, used on exclude setting page
160
+ */
161
+ public function get_ips() {
162
+ if ( ! defined( 'DOING_AJAX' ) || ! current_user_can( $this->plugin->admin->settings_cap ) ) {
163
+ return;
164
+ }
165
+
166
+ check_ajax_referer( 'stream_get_ips', 'nonce' );
167
+
168
+ $ips = $this->plugin->db->existing_records( 'ip' );
169
+
170
+ if ( $ips ) {
171
+ wp_send_json_success( $ips );
172
+ } else {
173
+ wp_send_json_error();
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Filter the columns to search in a WP_User_Query search.
179
+ *
180
+ * @param array $search_columns Array of column names to be searched.
181
+ * @param string $search Text being searched.
182
+ * @param \WP_User_Query $query current WP_User_Query instance.
183
+ *
184
+ * @return array
185
+ */
186
+ public function add_display_name_search_columns( $search_columns, $search, $query ) {
187
+ unset( $search );
188
+ unset( $query );
189
+
190
+ $search_columns[] = 'display_name';
191
+
192
+ return $search_columns;
193
+ }
194
+
195
+ /**
196
+ * Returns the option key
197
+ *
198
+ * @return string
199
+ */
200
+ public function get_option_key() {
201
+ $option_key = $this->option_key;
202
+
203
+ $current_page = wp_stream_filter_input( INPUT_GET, 'page' );
204
+
205
+ if ( ! $current_page ) {
206
+ $current_page = wp_stream_filter_input( INPUT_GET, 'action' );
207
+ }
208
+
209
+ if ( 'wp_stream_network_settings' === $current_page ) {
210
+ $option_key = $this->network_options_key;
211
+ }
212
+
213
+ return apply_filters( 'wp_stream_settings_option_key', $option_key );
214
+ }
215
+
216
+ /**
217
+ * Return settings fields
218
+ *
219
+ * @return array
220
+ */
221
+ public function get_fields() {
222
+ $fields = array(
223
+ 'general' => array(
224
+ 'title' => esc_html__( 'General', 'stream' ),
225
+ 'fields' => array(
226
+ array(
227
+ 'name' => 'role_access',
228
+ 'title' => esc_html__( 'Role Access', 'stream' ),
229
+ 'type' => 'multi_checkbox',
230
+ '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' ),
231
+ 'choices' => $this->get_roles(),
232
+ 'default' => array( 'administrator' ),
233
+ ),
234
+ array(
235
+ 'name' => 'records_ttl',
236
+ 'title' => esc_html__( 'Keep Records for', 'stream' ),
237
+ 'type' => 'number',
238
+ 'class' => 'small-text',
239
+ 'desc' => esc_html__( 'Maximum number of days to keep activity records. Leave blank to keep records forever.', 'stream' ),
240
+ 'default' => 30,
241
+ 'min' => 0,
242
+ 'max' => 999,
243
+ 'step' => 1,
244
+ 'after_field' => esc_html__( 'days', 'stream' ),
245
+ ),
246
+ ),
247
+ ),
248
+ 'exclude' => array(
249
+ 'title' => esc_html__( 'Exclude', 'stream' ),
250
+ 'fields' => array(
251
+ array(
252
+ 'name' => 'rules',
253
+ 'title' => esc_html__( 'Exclude Rules', 'stream' ),
254
+ 'type' => 'rule_list',
255
+ 'desc' => esc_html__( 'Create rules to exclude certain kinds of activity from being recorded by Stream.', 'stream' ),
256
+ 'default' => array(),
257
+ 'nonce' => 'stream_get_ips',
258
+ ),
259
+ ),
260
+ ),
261
+ 'advanced' => array(
262
+ 'title' => esc_html__( 'Advanced', 'stream' ),
263
+ 'fields' => array(
264
+ array(
265
+ 'name' => 'comment_flood_tracking',
266
+ 'title' => esc_html__( 'Comment Flood Tracking', 'stream' ),
267
+ 'type' => 'checkbox',
268
+ 'desc' => esc_html__( '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' ),
269
+ 'after_field' => esc_html__( 'Enabled', 'stream' ),
270
+ 'default' => 0,
271
+ ),
272
+ array(
273
+ 'name' => 'delete_all_records',
274
+ 'title' => esc_html__( 'Reset Stream Database', 'stream' ),
275
+ 'type' => 'link',
276
+ 'href' => add_query_arg(
277
+ array(
278
+ 'action' => 'wp_stream_reset',
279
+ 'wp_stream_nonce' => wp_create_nonce( 'stream_nonce' ),
280
+ ),
281
+ admin_url( 'admin-ajax.php' )
282
+ ),
283
+ 'class' => 'warning',
284
+ 'desc' => esc_html__( 'Warning: This will delete all activity records from the database.', 'stream' ),
285
+ 'default' => 0,
286
+ 'sticky' => 'bottom',
287
+ ),
288
+ ),
289
+ ),
290
+ );
291
+
292
+ // If Akismet is active, allow Admins to opt-in to Akismet tracking
293
+ if ( class_exists( 'Akismet' ) ) {
294
+ $akismet_tracking = array(
295
+ 'name' => 'akismet_tracking',
296
+ 'title' => esc_html__( 'Akismet Tracking', 'stream' ),
297
+ 'type' => 'checkbox',
298
+ 'desc' => esc_html__( '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' ),
299
+ 'after_field' => esc_html__( 'Enabled', 'stream' ),
300
+ 'default' => 0,
301
+ );
302
+
303
+ array_push( $fields['advanced']['fields'], $akismet_tracking );
304
+ }
305
+
306
+ // If WP Cron is enabled, allow Admins to opt-in to WP Cron tracking
307
+ if ( wp_stream_is_cron_enabled() ) {
308
+ $wp_cron_tracking = array(
309
+ 'name' => 'wp_cron_tracking',
310
+ 'title' => esc_html__( 'WP Cron Tracking', 'stream' ),
311
+ 'type' => 'checkbox',
312
+ 'desc' => esc_html__( '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' ),
313
+ 'after_field' => esc_html__( 'Enabled', 'stream' ),
314
+ 'default' => 0,
315
+ );
316
+
317
+ array_push( $fields['advanced']['fields'], $wp_cron_tracking );
318
+ }
319
+
320
+ /**
321
+ * Filter allows for modification of options fields
322
+ *
323
+ * @return array Array of option fields
324
+ */
325
+ $this->fields = apply_filters( 'wp_stream_settings_option_fields', $fields );
326
+
327
+ // Sort option fields in each tab by title ASC
328
+ foreach ( $this->fields as $tab => $options ) {
329
+ $titles = array();
330
+
331
+ foreach ( $options['fields'] as $field ) {
332
+ $prefix = null;
333
+
334
+ if ( ! empty( $field['sticky'] ) ) {
335
+ $prefix = ( 'bottom' === $field['sticky'] ) ? 'ZZZ' : 'AAA';
336
+ }
337
+
338
+ $titles[] = $prefix . $field['title'];
339
+ }
340
+
341
+ array_multisort( $titles, SORT_ASC, $this->fields[ $tab ]['fields'] );
342
+ }
343
+
344
+ return $this->fields;
345
+ }
346
+
347
+ /**
348
+ * Returns a list of options based on the current screen.
349
+ *
350
+ * @return array
351
+ */
352
+ public function get_options() {
353
+ $option_key = $this->option_key;
354
+ $defaults = $this->get_defaults( $option_key );
355
+
356
+ /**
357
+ * Filter allows for modification of options
358
+ *
359
+ * @param array
360
+ *
361
+ * @return array Updated array of options
362
+ */
363
+ return apply_filters(
364
+ 'wp_stream_settings_options',
365
+ wp_parse_args(
366
+ is_network_admin() ? (array) get_site_option( $option_key, array() ) : (array) get_option( $option_key, array() ),
367
+ $defaults
368
+ ),
369
+ $option_key
370
+ );
371
+ }
372
+
373
+ /**
374
+ * Iterate through registered fields and extract default values
375
+ *
376
+ * @return array
377
+ */
378
+ public function get_defaults() {
379
+ $fields = $this->get_fields();
380
+ $defaults = array();
381
+
382
+ foreach ( $fields as $section_name => $section ) {
383
+ foreach ( $section['fields'] as $field ) {
384
+ $defaults[ $section_name . '_' . $field['name'] ] = isset( $field['default'] ) ? $field['default'] : null;
385
+ }
386
+ }
387
+
388
+ return (array) $defaults;
389
+ }
390
+
391
+ /**
392
+ * Registers settings fields and sections
393
+ *
394
+ * @return void
395
+ */
396
+ public function register_settings() {
397
+ $sections = $this->get_fields();
398
+
399
+ register_setting( $this->option_key, $this->option_key, array( $this, 'sanitize_settings' ) );
400
+
401
+ foreach ( $sections as $section_name => $section ) {
402
+ add_settings_section(
403
+ $section_name,
404
+ null,
405
+ '__return_false',
406
+ $this->option_key
407
+ );
408
+
409
+ foreach ( $section['fields'] as $field_idx => $field ) {
410
+ if ( ! isset( $field['type'] ) ) { // No field type associated, skip, no GUI
411
+ continue;
412
+ }
413
+
414
+ add_settings_field(
415
+ $field['name'],
416
+ $field['title'],
417
+ ( isset( $field['callback'] ) ? $field['callback'] : array( $this, 'output_field' ) ),
418
+ $this->option_key,
419
+ $section_name,
420
+ $field + array(
421
+ 'section' => $section_name,
422
+ 'label_for' => sprintf( '%s_%s_%s', $this->option_key, $section_name, $field['name'] ), // xss ok
423
+ )
424
+ );
425
+ }
426
+ }
427
+ }
428
+
429
+ /**
430
+ * Sanitization callback for settings field values before save
431
+ *
432
+ * @param array $input
433
+ *
434
+ * @return array
435
+ */
436
+ public function sanitize_settings( $input ) {
437
+ $output = array();
438
+ $sections = $this->get_fields();
439
+
440
+ foreach ( $sections as $section => $data ) {
441
+ if ( empty( $data['fields'] ) || ! is_array( $data['fields'] ) ) {
442
+ continue;
443
+ }
444
+
445
+ foreach ( $data['fields'] as $field ) {
446
+ $type = ! empty( $field['type'] ) ? $field['type'] : null;
447
+ $name = ! empty( $field['name'] ) ? sprintf( '%s_%s', $section, $field['name'] ) : null;
448
+
449
+ if ( empty( $type ) || ! isset( $input[ $name ] ) || '' === $input[ $name ] ) {
450
+ continue;
451
+ }
452
+
453
+ // Sanitize depending on the type of field
454
+ switch ( $type ) {
455
+ case 'number':
456
+ $output[ $name ] = is_numeric( $input[ $name ] ) ? intval( trim( $input[ $name ] ) ) : '';
457
+ break;
458
+ case 'checkbox':
459
+ $output[ $name ] = is_numeric( $input[ $name ] ) ? absint( trim( $input[ $name ] ) ) : '';
460
+ break;
461
+ default:
462
+ if ( is_array( $input[ $name ] ) ) {
463
+ $output[ $name ] = $input[ $name ];
464
+
465
+ // Support all values in multidimentional arrays too
466
+ array_walk_recursive( $output[ $name ], function( &$v, $k ) {
467
+ $v = trim( $v );
468
+ } );
469
+ } else {
470
+ $output[ $name ] = trim( $input[ $name ] );
471
+ }
472
+ }
473
+ }
474
+ }
475
+
476
+ return $output;
477
+ }
478
+
479
+ /**
480
+ * Compile HTML needed for displaying the field
481
+ *
482
+ * @param array $field Field settings
483
+ *
484
+ * @return string HTML to be displayed
485
+ */
486
+ public function render_field( $field ) {
487
+ $output = null;
488
+ $type = isset( $field['type'] ) ? $field['type'] : null;
489
+ $section = isset( $field['section'] ) ? $field['section'] : null;
490
+ $name = isset( $field['name'] ) ? $field['name'] : null;
491
+ $class = isset( $field['class'] ) ? $field['class'] : null;
492
+ $placeholder = isset( $field['placeholder'] ) ? $field['placeholder'] : null;
493
+ $description = isset( $field['desc'] ) ? $field['desc'] : null;
494
+ $href = isset( $field['href'] ) ? $field['href'] : null;
495
+ $rows = isset( $field['rows'] ) ? $field['rows'] : 10;
496
+ $cols = isset( $field['cols'] ) ? $field['cols'] : 50;
497
+ $after_field = isset( $field['after_field'] ) ? $field['after_field'] : null;
498
+ $default = isset( $field['default'] ) ? $field['default'] : null;
499
+ $min = isset( $field['min'] ) ? $field['min'] : 0;
500
+ $max = isset( $field['max'] ) ? $field['max'] : 999;
501
+ $step = isset( $field['step'] ) ? $field['step'] : 1;
502
+ $title = isset( $field['title'] ) ? $field['title'] : null;
503
+ $nonce = isset( $field['nonce'] ) ? $field['nonce'] : null;
504
+
505
+ if ( isset( $field['value'] ) ) {
506
+ $current_value = $field['value'];
507
+ } else {
508
+ if ( isset( $this->options[ $section . '_' . $name ] ) ) {
509
+ $current_value = $this->options[ $section . '_' . $name ];
510
+ } else {
511
+ $current_value = null;
512
+ }
513
+ }
514
+
515
+ $option_key = $this->option_key;
516
+
517
+ if ( is_callable( $current_value ) ) {
518
+ $current_value = call_user_func( $current_value );
519
+ }
520
+
521
+ if ( ! $type || ! $section || ! $name ) {
522
+ return '';
523
+ }
524
+
525
+ if ( 'multi_checkbox' === $type
526
+ && ( empty( $field['choices'] ) || ! is_array( $field['choices'] ) )
527
+ ) {
528
+ return '';
529
+ }
530
+
531
+ switch ( $type ) {
532
+ case 'text':
533
+ case 'number':
534
+ $output = sprintf(
535
+ '<input type="%1$s" name="%2$s[%3$s_%4$s]" id="%2$s_%3$s_%4$s" class="%5$s" placeholder="%6$s" min="%7$d" max="%8$d" step="%9$d" value="%10$s" /> %11$s',
536
+ esc_attr( $type ),
537
+ esc_attr( $option_key ),
538
+ esc_attr( $section ),
539
+ esc_attr( $name ),
540
+ esc_attr( $class ),
541
+ esc_attr( $placeholder ),
542
+ esc_attr( $min ),
543
+ esc_attr( $max ),
544
+ esc_attr( $step ),
545
+ esc_attr( $current_value ),
546
+ wp_kses_post( $after_field )
547
+ );
548
+ break;
549
+ case 'textarea':
550
+ $output = sprintf(
551
+ '<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',
552
+ esc_attr( $option_key ),
553
+ esc_attr( $section ),
554
+ esc_attr( $name ),
555
+ esc_attr( $class ),
556
+ esc_attr( $placeholder ),
557
+ absint( $rows ),
558
+ absint( $cols ),
559
+ esc_textarea( $current_value ),
560
+ wp_kses_post( $after_field )
561
+ );
562
+ break;
563
+ case 'checkbox':
564
+ $output = sprintf(
565
+ '<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>',
566
+ esc_attr( $option_key ),
567
+ esc_attr( $section ),
568
+ esc_attr( $name ),
569
+ checked( $current_value, 1, false ),
570
+ wp_kses_post( $after_field )
571
+ );
572
+ break;
573
+ case 'multi_checkbox':
574
+ $output = sprintf(
575
+ '<div id="%1$s[%2$s_%3$s]"><fieldset>',
576
+ esc_attr( $option_key ),
577
+ esc_attr( $section ),
578
+ esc_attr( $name )
579
+ );
580
+ // Fallback if nothing is selected
581
+ $output .= sprintf(
582
+ '<input type="hidden" name="%1$s[%2$s_%3$s][]" value="__placeholder__" />',
583
+ esc_attr( $option_key ),
584
+ esc_attr( $section ),
585
+ esc_attr( $name )
586
+ );
587
+ $current_value = (array) $current_value;
588
+ $choices = $field['choices'];
589
+ if ( is_callable( $choices ) ) {
590
+ $choices = call_user_func( $choices );
591
+ }
592
+ foreach ( $choices as $value => $label ) {
593
+ $output .= sprintf(
594
+ '<label>%1$s <span>%2$s</span></label><br />',
595
+ sprintf(
596
+ '<input type="checkbox" name="%1$s[%2$s_%3$s][]" value="%4$s" %5$s />',
597
+ esc_attr( $option_key ),
598
+ esc_attr( $section ),
599
+ esc_attr( $name ),
600
+ esc_attr( $value ),
601
+ checked( in_array( $value, $current_value ), true, false )
602
+ ),
603
+ esc_html( $label )
604
+ );
605
+ }
606
+ $output .= '</fieldset></div>';
607
+ break;
608
+ case 'select':
609
+ $current_value = $this->options[ $section . '_' . $name ];
610
+ $default_value = isset( $default['value'] ) ? $default['value'] : '-1';
611
+ $default_name = isset( $default['name'] ) ? $default['name'] : 'Choose Setting';
612
+
613
+ $output = sprintf(
614
+ '<select name="%1$s[%2$s_%3$s]" class="%1$s_%2$s_%3$s">',
615
+ esc_attr( $option_key ),
616
+ esc_attr( $section ),
617
+ esc_attr( $name )
618
+ );
619
+ $output .= sprintf(
620
+ '<option value="%1$s" %2$s>%3$s</option>',
621
+ esc_attr( $default_value ),
622
+ checked( $default_value === $current_value, true, false ),
623
+ esc_html( $default_name )
624
+ );
625
+ foreach ( $field['choices'] as $value => $label ) {
626
+ $output .= sprintf(
627
+ '<option value="%1$s" %2$s>%3$s</option>',
628
+ esc_attr( $value ),
629
+ checked( $value === $current_value, true, false ),
630
+ esc_html( $label )
631
+ );
632
+ }
633
+ $output .= '</select>';
634
+ break;
635
+ case 'file':
636
+ $output = sprintf(
637
+ '<input type="file" name="%1$s[%2$s_%3$s]" class="%4$s">',
638
+ esc_attr( $option_key ),
639
+ esc_attr( $section ),
640
+ esc_attr( $name ),
641
+ esc_attr( $class )
642
+ );
643
+ break;
644
+ case 'link':
645
+ $output = sprintf(
646
+ '<a id="%1$s_%2$s_%3$s" class="%4$s" href="%5$s">%6$s</a>',
647
+ esc_attr( $option_key ),
648
+ esc_attr( $section ),
649
+ esc_attr( $name ),
650
+ esc_attr( $class ),
651
+ esc_attr( $href ),
652
+ esc_attr( $title )
653
+ );
654
+ break;
655
+ case 'select2' :
656
+ if ( ! isset( $current_value ) ) {
657
+ $current_value = '';
658
+ }
659
+
660
+ $data_values = array();
661
+
662
+ if ( isset( $field['choices'] ) ) {
663
+ $choices = $field['choices'];
664
+ if ( is_callable( $choices ) ) {
665
+ $param = ( isset( $field['param'] ) ) ? $field['param'] : null;
666
+ $choices = call_user_func( $choices, $param );
667
+ }
668
+ foreach ( $choices as $key => $value ) {
669
+ if ( is_array( $value ) ) {
670
+ $child_values = array();
671
+ if ( isset( $value['children'] ) ) {
672
+ $child_values = array();
673
+ foreach ( $value['children'] as $child_key => $child_value ) {
674
+ $child_values[] = array( 'id' => $child_key, 'text' => $child_value );
675
+ }
676
+ }
677
+ if ( isset( $value['label'] ) ) {
678
+ $data_values[] = array( 'id' => $key, 'text' => $value['label'], 'children' => $child_values );
679
+ }
680
+ } else {
681
+ $data_values[] = array( 'id' => $key, 'text' => $value );
682
+ }
683
+ }
684
+ $class .= ' with-source';
685
+ }
686
+
687
+ $input_html = sprintf(
688
+ '<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" />',
689
+ esc_attr( $option_key ),
690
+ esc_attr( $section ),
691
+ esc_attr( $name ),
692
+ esc_attr( wp_stream_json_encode( $data_values ) ),
693
+ esc_attr( $current_value ),
694
+ esc_attr( $class ),
695
+ sprintf( esc_html__( 'Any %s', 'stream' ), $title )
696
+ );
697
+
698
+ $output = sprintf(
699
+ '<div class="%1$s_%2$s_%3$s">%4$s</div>',
700
+ esc_attr( $option_key ),
701
+ esc_attr( $section ),
702
+ esc_attr( $name ),
703
+ $input_html
704
+ );
705
+
706
+ break;
707
+ case 'rule_list' :
708
+ $output = '<p class="description">' . esc_html( $description ) . '</p>';
709
+
710
+ $actions_top = sprintf( '<input type="button" class="button" id="%1$s_new_rule" value="&#43; %2$s" />', esc_attr( $section . '_' . $name ), esc_html__( 'Add New Rule', 'stream' ) );
711
+ $actions_bottom = sprintf( '<input type="button" class="button" id="%1$s_remove_rules" value="%2$s" />', esc_attr( $section . '_' . $name ), esc_html__( 'Delete Selected Rules', 'stream' ) );
712
+
713
+ $output .= sprintf( '<div class="tablenav top">%1$s</div>', $actions_top );
714
+ $output .= '<table class="wp-list-table widefat fixed stream-exclude-list">';
715
+
716
+ unset( $description );
717
+
718
+ $heading_row = sprintf(
719
+ '<tr>
720
+ <td scope="col" class="manage-column column-cb check-column">%1$s</td>
721
+ <th scope="col" class="manage-column">%2$s</th>
722
+ <th scope="col" class="manage-column">%3$s</th>
723
+ <th scope="col" class="manage-column">%4$s</th>
724
+ <th scope="col" class="manage-column">%5$s</th>
725
+ <th scope="col" class="actions-column manage-column"><span class="hidden">%6$s</span></th>
726
+ </tr>',
727
+ '<input class="cb-select" type="checkbox" />',
728
+ esc_html__( 'Author or Role', 'stream' ),
729
+ esc_html__( 'Context', 'stream' ),
730
+ esc_html__( 'Action', 'stream' ),
731
+ esc_html__( 'IP Address', 'stream' ),
732
+ esc_html__( 'Filters', 'stream' )
733
+ );
734
+
735
+ $exclude_rows = array();
736
+
737
+ // Prepend an empty row
738
+ $current_value['exclude_row'] = array( 'helper' => '' ) + ( isset( $current_value['exclude_row'] ) ? $current_value['exclude_row'] : array() );
739
+
740
+ foreach ( $current_value['exclude_row'] as $key => $value ) {
741
+ // Prepare values
742
+ $author_or_role = isset( $current_value['author_or_role'][ $key ] ) ? $current_value['author_or_role'][ $key ] : '';
743
+ $connector = isset( $current_value['connector'][ $key ] ) ? $current_value['connector'][ $key ] : '';
744
+ $context = isset( $current_value['context'][ $key ] ) ? $current_value['context'][ $key ] : '';
745
+ $action = isset( $current_value['action'][ $key ] ) ? $current_value['action'][ $key ] : '';
746
+ $ip_address = isset( $current_value['ip_address'][ $key ] ) ? $current_value['ip_address'][ $key ] : '';
747
+
748
+ // Author or Role dropdown menu
749
+ $author_or_role_values = array();
750
+ $author_or_role_selected = array();
751
+
752
+ foreach ( $this->get_roles() as $role_id => $role ) {
753
+ $args = array( 'id' => $role_id, 'text' => $role );
754
+ $users = count_users();
755
+ $count = isset( $users['avail_roles'][ $role_id ] ) ? $users['avail_roles'][ $role_id ] : 0;
756
+
757
+ if ( ! empty( $count ) ) {
758
+ $args['user_count'] = sprintf( _n( '1 user', '%s users', absint( $count ), 'stream' ), absint( $count ) );
759
+ }
760
+
761
+ if ( $role_id === $author_or_role ) {
762
+ $author_or_role_selected['id'] = $role_id;
763
+ $author_or_role_selected['text'] = $role;
764
+ }
765
+
766
+ $author_or_role_values[] = $args;
767
+ }
768
+
769
+ if ( empty( $author_or_role_selected ) && is_numeric( $author_or_role ) ) {
770
+ $user = new WP_User( $author_or_role );
771
+ $display_name = ( 0 === $user->ID ) ? esc_html__( 'N/A', 'stream' ) : $user->display_name;
772
+ $author_or_role_selected = array( 'id' => $user->ID, 'text' => $display_name );
773
+ }
774
+
775
+ $author_or_role_input = sprintf(
776
+ '<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" />',
777
+ esc_attr( $option_key ),
778
+ esc_attr( $section ),
779
+ esc_attr( $name ),
780
+ 'author_or_role',
781
+ esc_attr( wp_stream_json_encode( $author_or_role_values ) ),
782
+ isset( $author_or_role_selected['id'] ) ? esc_attr( $author_or_role_selected['id'] ) : '',
783
+ isset( $author_or_role_selected['text'] ) ? esc_attr( $author_or_role_selected['text'] ) : '',
784
+ esc_html__( 'Any Author or Role', 'stream' ),
785
+ esc_attr( wp_create_nonce( 'stream_get_users' ) )
786
+ );
787
+
788
+ // Context dropdown menu
789
+ $context_values = array();
790
+
791
+ foreach ( $this->get_terms_labels( 'context' ) as $context_id => $context_data ) {
792
+ if ( is_array( $context_data ) ) {
793
+ $child_values = array();
794
+ if ( isset( $context_data['children'] ) ) {
795
+ $child_values = array();
796
+ foreach ( $context_data['children'] as $child_id => $child_value ) {
797
+ $child_values[] = array( 'id' => $child_id, 'text' => $child_value, 'parent' => $context_id );
798
+ }
799
+ }
800
+ if ( isset( $context_data['label'] ) ) {
801
+ $context_values[] = array( 'id' => $context_id, 'text' => $context_data['label'], 'children' => $child_values );
802
+ }
803
+ } else {
804
+ $context_values[] = array( 'id' => $context_id, 'text' => $context_data );
805
+ }
806
+ }
807
+
808
+ $connector_input = sprintf(
809
+ '<input type="hidden" name="%1$s[%2$s_%3$s][%4$s][]" class="%4$s" value="%5$s">',
810
+ esc_attr( $option_key ),
811
+ esc_attr( $section ),
812
+ esc_attr( $name ),
813
+ esc_attr( 'connector' ),
814
+ esc_attr( $connector )
815
+ );
816
+
817
+ $context_input = sprintf(
818
+ '<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" />',
819
+ esc_attr( $option_key ),
820
+ esc_attr( $section ),
821
+ esc_attr( $name ),
822
+ 'context',
823
+ esc_attr( wp_stream_json_encode( $context_values ) ),
824
+ esc_attr( $context ),
825
+ esc_html__( 'Any Context', 'stream' ),
826
+ 'connector'
827
+ );
828
+
829
+ // Action dropdown menu
830
+ $action_values = array();
831
+
832
+ foreach ( $this->get_terms_labels( 'action' ) as $action_id => $action_data ) {
833
+ $action_values[] = array( 'id' => $action_id, 'text' => $action_data );
834
+ }
835
+
836
+ $action_input = sprintf(
837
+ '<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" />',
838
+ esc_attr( $option_key ),
839
+ esc_attr( $section ),
840
+ esc_attr( $name ),
841
+ 'action',
842
+ esc_attr( wp_stream_json_encode( $action_values ) ),
843
+ esc_attr( $action ),
844
+ esc_html__( 'Any Action', 'stream' )
845
+ );
846
+
847
+ // IP Address input
848
+ $ip_address_input = sprintf(
849
+ '<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" />',
850
+ esc_attr( $option_key ),
851
+ esc_attr( $section ),
852
+ esc_attr( $name ),
853
+ 'ip_address',
854
+ esc_attr( $ip_address ),
855
+ esc_html__( 'Any IP Address', 'stream' ),
856
+ esc_attr( wp_create_nonce( 'stream_get_ips' ) )
857
+ );
858
+
859
+ // Hidden helper input
860
+ $helper_input = sprintf(
861
+ '<input type="hidden" name="%1$s[%2$s_%3$s][%4$s][]" value="" />',
862
+ esc_attr( $option_key ),
863
+ esc_attr( $section ),
864
+ esc_attr( $name ),
865
+ 'exclude_row'
866
+ );
867
+
868
+ $exclude_rows[] = sprintf(
869
+ '<tr class="%1$s %2$s">
870
+ <th scope="row" class="check-column">%3$s %4$s</th>
871
+ <td>%5$s</td>
872
+ <td>%6$s %7$s</td>
873
+ <td>%8$s</td>
874
+ <td>%9$s</td>
875
+ <th scope="row" class="actions-column">%10$s</th>
876
+ </tr>',
877
+ ( 0 !== $key % 2 ) ? 'alternate' : '',
878
+ ( 'helper' === $key ) ? 'hidden helper' : '',
879
+ '<input class="cb-select" type="checkbox" />',
880
+ $helper_input,
881
+ $author_or_role_input,
882
+ $connector_input,
883
+ $context_input,
884
+ $action_input,
885
+ $ip_address_input,
886
+ '<a href="#" class="exclude_rules_remove_rule_row">Delete</a>'
887
+ );
888
+ }
889
+
890
+ $no_rules_found_row = sprintf(
891
+ '<tr class="no-items hidden"><td class="colspanchange" colspan="5">%1$s</td></tr>',
892
+ esc_html__( 'No rules found.', 'stream' )
893
+ );
894
+
895
+ $output .= '<thead>' . $heading_row . '</thead>';
896
+ $output .= '<tfoot>' . $heading_row . '</tfoot>';
897
+ $output .= '<tbody>' . $no_rules_found_row . implode( '', $exclude_rows ) . '</tbody>';
898
+
899
+ $output .= '</table>';
900
+
901
+ $output .= sprintf( '<div class="tablenav bottom">%1$s</div>', $actions_bottom );
902
+
903
+ break;
904
+ }
905
+ $output .= ! empty( $description ) ? wp_kses_post( sprintf( '<p class="description">%s</p>', $description ) ) : null;
906
+
907
+ return $output;
908
+ }
909
+
910
+ /**
911
+ * Render Callback for post_types field
912
+ *
913
+ * @param array $field
914
+ *
915
+ * @return string
916
+ */
917
+ public function output_field( $field ) {
918
+ $method = 'output_' . $field['name'];
919
+
920
+ if ( method_exists( $this, $method ) ) {
921
+ return call_user_func( array( $this, $method ), $field );
922
+ }
923
+
924
+ $output = $this->render_field( $field );
925
+
926
+ echo $output; // xss ok
927
+ }
928
+
929
+ /**
930
+ * Get an array of user roles
931
+ *
932
+ * @return array
933
+ */
934
+ public function get_roles() {
935
+ $wp_roles = new WP_Roles();
936
+ $roles = array();
937
+
938
+ foreach ( $wp_roles->get_names() as $role => $label ) {
939
+ $roles[ $role ] = translate_user_role( $label );
940
+ }
941
+
942
+ return $roles;
943
+ }
944
+
945
+ /**
946
+ * Function will return all terms labels of given column
947
+ *
948
+ * @param string $column string Name of the column
949
+ *
950
+ * @return array
951
+ */
952
+ public function get_terms_labels( $column ) {
953
+ $return_labels = array();
954
+
955
+ if ( isset( $this->plugin->connectors->term_labels[ 'stream_' . $column ] ) ) {
956
+ if ( 'context' === $column && isset( $this->plugin->connectors->term_labels['stream_connector'] ) ) {
957
+ $connectors = $this->plugin->connectors->term_labels['stream_connector'];
958
+ $contexts = $this->plugin->connectors->term_labels['stream_context'];
959
+
960
+ foreach ( $connectors as $connector => $connector_label ) {
961
+ $return_labels[ $connector ]['label'] = $connector_label;
962
+ foreach ( $contexts as $context => $context_label ) {
963
+ if ( isset( $this->plugin->connectors->contexts[ $connector ] ) && array_key_exists( $context, $this->plugin->connectors->contexts[ $connector ] ) ) {
964
+ $return_labels[ $connector ]['children'][ $context ] = $context_label;
965
+ }
966
+ }
967
+ }
968
+ } else {
969
+ $return_labels = $this->plugin->connectors->term_labels[ 'stream_' . $column ];
970
+ }
971
+
972
+ ksort( $return_labels );
973
+ }
974
+
975
+ return $return_labels;
976
+ }
977
+
978
+ /**
979
+ * Remove records when records TTL is shortened
980
+ *
981
+ * @action update_option_wp_stream
982
+ *
983
+ * @param array $old_value
984
+ * @param array $new_value
985
+ */
986
+ public function updated_option_ttl_remove_records( $old_value, $new_value ) {
987
+ $ttl_before = isset( $old_value['general_records_ttl'] ) ? (int) $old_value['general_records_ttl'] : -1;
988
+ $ttl_after = isset( $new_value['general_records_ttl'] ) ? (int) $new_value['general_records_ttl'] : -1;
989
+
990
+ if ( $ttl_after < $ttl_before ) {
991
+ /**
992
+ * Action assists in purging when TTL is shortened
993
+ */
994
+ do_action( 'wp_stream_auto_purge' );
995
+ }
996
+ }
997
+
998
+ /**
999
+ * Get translations of serialized Stream settings
1000
+ *
1001
+ * @filter wp_stream_serialized_labels
1002
+ *
1003
+ * @return array Multidimensional array of fields
1004
+ */
1005
+ public function get_settings_translations( $labels ) {
1006
+ if ( ! isset( $labels[ $this->option_key ] ) ) {
1007
+ $labels[ $this->option_key ] = array();
1008
+ }
1009
+
1010
+ foreach ( $this->get_fields() as $section_slug => $section ) {
1011
+ foreach ( $section['fields'] as $field ) {
1012
+ $labels[ $this->option_key ][ sprintf( '%s_%s', $section_slug, $field['name'] ) ] = $field['title'];
1013
+ }
1014
+ }
1015
+
1016
+ return $labels;
1017
+ }
1018
+
1019
+ }
classes/class-uninstall.php ADDED
@@ -0,0 +1,229 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace WP_Stream;
3
+
4
+ class Uninstall {
5
+ /**
6
+ * Hold Plugin class
7
+ * @var Plugin
8
+ */
9
+ public $plugin;
10
+
11
+ /**
12
+ * Hold the array of option keys to uninstall
13
+ *
14
+ * @var array
15
+ */
16
+ public $options;
17
+
18
+ /**
19
+ * Hold the array of user meta keys to uninstall
20
+ *
21
+ * @var array
22
+ */
23
+ public $user_meta;
24
+
25
+ function __construct( $plugin ) {
26
+ $this->plugin = $plugin;
27
+
28
+ $this->user_meta = array(
29
+ 'edit_stream_per_page',
30
+ 'stream_last_read', // Deprecated
31
+ 'stream_unread_count', // Deprecated
32
+ 'stream_user_feed_key', // Deprecated
33
+ );
34
+ }
35
+
36
+ /**
37
+ * Uninstall Stream by deleting its data
38
+ */
39
+ public function uninstall() {
40
+ //check_ajax_referer( 'stream_nonce', 'wp_stream_nonce' );
41
+
42
+ $this->options = array(
43
+ $this->plugin->install->option_key,
44
+ $this->plugin->settings->option_key,
45
+ $this->plugin->settings->network_options_key,
46
+ );
47
+
48
+ // Verify current user's permissions before proceeding
49
+ if ( ! current_user_can( $this->plugin->admin->settings_cap ) ) {
50
+ wp_die(
51
+ esc_html__( "You don't have sufficient privileges to do this action.", 'stream' )
52
+ );
53
+ }
54
+
55
+ // Prevent this action from firing
56
+ remove_action( 'deactivate_plugin', array( 'Connector_Installer', 'callback' ), null );
57
+
58
+ // Just in case
59
+ if ( ! function_exists( 'is_plugin_active_for_network' ) ) {
60
+ require_once ABSPATH . '/wp-admin/includes/plugin.php';
61
+ }
62
+
63
+ // Drop everything on single site installs or when network activated
64
+ // Otherwise only delete data relative to the current blog
65
+ if ( ! is_multisite() || is_plugin_active_for_network( $this->plugin->locations['plugin'] ) ) {
66
+ $this->delete_all_records();
67
+ $this->delete_all_options();
68
+ $this->delete_all_user_meta();
69
+ } else {
70
+ $blog_id = get_current_blog_id();
71
+
72
+ $this->delete_blog_records( $blog_id );
73
+ $this->delete_blog_options( $blog_id );
74
+ $this->delete_blog_user_meta( $blog_id );
75
+ }
76
+
77
+ $this->delete_all_cron_events();
78
+
79
+ $this->deactivate();
80
+ }
81
+
82
+ /**
83
+ * Delete the Stream database tables
84
+ */
85
+ private function delete_all_records() {
86
+ global $wpdb;
87
+
88
+ $wpdb->query( "DROP TABLE {$wpdb->stream}" );
89
+ $wpdb->query( "DROP TABLE {$wpdb->streammeta}" );
90
+ }
91
+
92
+ /**
93
+ * Delete records and record meta from a specific blog
94
+ *
95
+ * @param int $blog_id (optional)
96
+ */
97
+ private function delete_blog_records( $blog_id = 1 ) {
98
+ if ( empty( $blog_id ) || ! is_int( $blog_id ) ) {
99
+ return;
100
+ }
101
+
102
+ global $wpdb;
103
+
104
+ $wpdb->query(
105
+ $wpdb->prepare(
106
+ "DELETE `records`, `meta`
107
+ FROM {$wpdb->stream} AS `records`
108
+ LEFT JOIN {$wpdb->streammeta} AS `meta`
109
+ ON `meta`.`record_id` = `records`.`ID`
110
+ WHERE blog_id = %d;",
111
+ $blog_id
112
+ )
113
+ );
114
+ }
115
+
116
+ /**
117
+ * Delete all options
118
+ */
119
+ private function delete_all_options() {
120
+ global $wpdb;
121
+
122
+ // Wildcard matches
123
+ $wpdb->query( "DELETE FROM {$wpdb->options} WHERE option_name LIKE '%wp_stream%';" );
124
+
125
+ // Specific options
126
+ foreach ( $this->options as $option ) {
127
+ delete_site_option( $option ); // Supports both multisite and single site installs
128
+ }
129
+
130
+ // Single site installs can stop here
131
+ if ( ! is_multisite() ) {
132
+ return;
133
+ }
134
+
135
+ // Wildcard matches on network options
136
+ $wpdb->query( "DELETE FROM {$wpdb->sitemeta} WHERE meta_key LIKE '%wp_stream%';" );
137
+
138
+ // Delete options from each blog on network
139
+ foreach ( wp_get_sites() as $blog ) {
140
+ $this->delete_blog_options( absint( $blog['blog_id'] ) );
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Delete options from a specific blog
146
+ *
147
+ * @param int $blog_id (optional)
148
+ */
149
+ private function delete_blog_options( $blog_id = 1 ) {
150
+ if ( empty( $blog_id ) || ! is_int( $blog_id ) ) {
151
+ return;
152
+ }
153
+
154
+ global $wpdb;
155
+
156
+ $blog_prefix = $wpdb->get_blog_prefix( $blog_id );
157
+
158
+ // Wildcard matches
159
+ $wpdb->query( "DELETE FROM {$blog_prefix}options WHERE option_name LIKE '%wp_stream%';" );
160
+
161
+ // Specific options
162
+ foreach ( $this->options as $option ) {
163
+ delete_blog_option( $blog_id, $option );
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Delete all user meta
169
+ */
170
+ private function delete_all_user_meta() {
171
+ global $wpdb;
172
+
173
+ // Wildcard matches
174
+ $wpdb->query( "DELETE FROM {$wpdb->usermeta} WHERE meta_key LIKE '%wp_stream%';" );
175
+
176
+ // Specific user meta
177
+ foreach ( $this->user_meta as $meta_key ) {
178
+ $wpdb->query( "DELETE FROM {$wpdb->usermeta} WHERE meta_key = '{$meta_key}';" );
179
+ }
180
+ }
181
+
182
+ /**
183
+ * Delete user meta from a specific blog
184
+ *
185
+ * @param int $blog_id (optional)
186
+ */
187
+ private function delete_blog_user_meta( $blog_id = 1 ) {
188
+ if ( empty( $blog_id ) || ! is_int( $blog_id ) ) {
189
+ return;
190
+ }
191
+
192
+ global $wpdb;
193
+
194
+ $blog_prefix = $wpdb->get_blog_prefix( $blog_id );
195
+
196
+ // Wildcard matches
197
+ $wpdb->query( "DELETE FROM {$wpdb->usermeta} WHERE meta_key LIKE '{$blog_prefix}%wp_stream%';" );
198
+
199
+ // Specific user meta
200
+ foreach ( $this->user_meta as $meta_key ) {
201
+ $wpdb->query( "DELETE FROM {$wpdb->usermeta} WHERE meta_key = '{$blog_prefix}{$meta_key}';" );
202
+ }
203
+ }
204
+
205
+ /**
206
+ * Delete scheduled cron event hooks
207
+ */
208
+ private function delete_all_cron_events() {
209
+ wp_clear_scheduled_hook( 'wp_stream_auto_purge' );
210
+ }
211
+
212
+ /**
213
+ * Deactivate the plugin and redirect to the plugins screen
214
+ */
215
+ private function deactivate() {
216
+ deactivate_plugins( $this->plugin->locations['plugin'] );
217
+
218
+ wp_safe_redirect(
219
+ add_query_arg(
220
+ array(
221
+ 'deactivate' => true,
222
+ ),
223
+ self_admin_url( 'plugins.php' )
224
+ )
225
+ );
226
+
227
+ exit;
228
+ }
229
+ }
connectors/class-connector-acf.php ADDED
@@ -0,0 +1,543 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace WP_Stream;
3
+
4
+ class Connector_ACF extends Connector {
5
+ /**
6
+ * Connector slug
7
+ *
8
+ * @var string
9
+ */
10
+ public $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 $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 $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 $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 function is_dependency_satisfied() {
58
+ if ( class_exists( 'acf' ) ) { //TODO: Should this be function_exists?
59
+ $acf = \acf();
60
+ if ( version_compare( $acf->settings['version'], self::PLUGIN_MIN_VERSION, '>=' ) ) {
61
+ return true;
62
+ }
63
+ }
64
+
65
+ return false;
66
+ }
67
+
68
+ /**
69
+ * Return translated connector label
70
+ *
71
+ * @return string Translated connector label
72
+ */
73
+ public function get_label() {
74
+ return esc_html_x( 'ACF', 'acf', 'stream' );
75
+ }
76
+
77
+ /**
78
+ * Return translated action labels
79
+ *
80
+ * @return array Action label translations
81
+ */
82
+ public function get_action_labels() {
83
+ return array(
84
+ 'created' => esc_html__( 'Created', 'acf', 'stream' ),
85
+ 'updated' => esc_html__( 'Updated', 'acf', 'stream' ),
86
+ 'added' => esc_html__( 'Added', 'acf', 'stream' ),
87
+ 'deleted' => esc_html__( 'Deleted', 'acf', 'stream' ),
88
+ );
89
+ }
90
+
91
+ /**
92
+ * Return translated context labels
93
+ *
94
+ * @return array Context label translations
95
+ */
96
+ public function get_context_labels() {
97
+ return array(
98
+ 'field_groups' => esc_html_x( 'Field Groups', 'acf', 'stream' ),
99
+ 'fields' => esc_html_x( 'Fields', 'acf', 'stream' ),
100
+ 'rules' => esc_html_x( 'Rules', 'acf', 'stream' ),
101
+ 'options' => esc_html_x( 'Options', 'acf', 'stream' ),
102
+ 'values' => esc_html_x( 'Values', 'acf', 'stream' ),
103
+ );
104
+ }
105
+
106
+ /**
107
+ * Register the connector
108
+ */
109
+ public function register() {
110
+ add_filter( 'wp_stream_log_data', array( $this, 'log_override' ) );
111
+
112
+ /**
113
+ * Allow devs to disable logging values of rendered forms
114
+ *
115
+ * @return bool
116
+ */
117
+ if ( apply_filters( 'wp_stream_acf_enable_value_logging', true ) ) {
118
+ $this->actions[] = 'acf/update_value';
119
+ }
120
+
121
+ parent::register();
122
+ }
123
+
124
+ /**
125
+ * Add action links to Stream drop row in admin list screen
126
+ *
127
+ * @filter wp_stream_action_links_{connector}
128
+ *
129
+ * @param array $links Previous links registered
130
+ * @param object $record Stream record
131
+ *
132
+ * @return array Action links
133
+ */
134
+ public function action_links( $links, $record ) {
135
+ $posts_connector = new Connector_Posts();
136
+ $links = $posts_connector->action_links( $links, $record );
137
+
138
+ return $links;
139
+ }
140
+
141
+ /**
142
+ * Track addition of post meta
143
+ *
144
+ * @action added_post_meta
145
+ */
146
+ public function callback_added_post_meta() {
147
+ call_user_func_array( array( $this, 'check_meta' ), array_merge( array( 'post', 'added' ), func_get_args() ) );
148
+ }
149
+
150
+ /**
151
+ * Track updating post meta
152
+ *
153
+ * @action updated_post_meta
154
+ */
155
+ public function callback_updated_post_meta() {
156
+ call_user_func_array( array( $this, 'check_meta' ), array_merge( array( 'post', 'updated' ), func_get_args() ) );
157
+ }
158
+
159
+ /**
160
+ * Track deletion of post meta
161
+ *
162
+ * Note: Using delete_post_meta instead of deleted_post_meta to be able to
163
+ * capture old field value
164
+ *
165
+ * @action delete_post_meta
166
+ */
167
+ public function callback_delete_post_meta() {
168
+ call_user_func_array( array( $this, 'check_meta' ), array_merge( array( 'post', 'deleted' ), func_get_args() ) );
169
+ }
170
+
171
+ /**
172
+ * Track addition of user meta
173
+ *
174
+ * @action added_user_meta
175
+ */
176
+ public function callback_added_user_meta() {
177
+ call_user_func_array( array( $this, 'check_meta' ), array_merge( array( 'user', 'added' ), func_get_args() ) );
178
+ }
179
+
180
+ /**
181
+ * Track updating user meta
182
+ *
183
+ * @action updated_user_meta
184
+ */
185
+ public function callback_updated_user_meta() {
186
+ call_user_func_array( array( $this, 'check_meta' ), array_merge( array( 'user', 'updated' ), func_get_args() ) );
187
+ }
188
+
189
+ /**
190
+ * Track deletion of user meta
191
+ *
192
+ * Note: Using delete_user_meta instead of deleted_user_meta to be able to
193
+ * capture old field value
194
+ *
195
+ * @action delete_user_meta
196
+ */
197
+ public function callback_delete_user_meta() {
198
+ call_user_func_array( array( $this, 'check_meta' ), array_merge( array( 'user', 'deleted' ), func_get_args() ) );
199
+ }
200
+
201
+ /**
202
+ * Track addition of post/user meta
203
+ *
204
+ * @param string $type Type of object, post or user
205
+ * @param string $action Added, updated, deleted
206
+ * @param integer $meta_id
207
+ * @param integer $object_id
208
+ * @param string $meta_key
209
+ * @param mixed|null $meta_value
210
+ */
211
+ public function check_meta( $type, $action, $meta_id, $object_id, $meta_key, $meta_value = null ) {
212
+ if ( 'post' !== $type || ! ( $post = get_post( $object_id ) ) || 'acf' !== $post->post_type ) {
213
+ $this->check_meta_values( $type, $action, $meta_id, $object_id, $meta_key, $meta_value = null );
214
+ return;
215
+ }
216
+
217
+ $action_labels = $this->get_action_labels();
218
+
219
+ // Fields
220
+ if ( 0 === strpos( $meta_key, 'field_' ) ) {
221
+ if ( 'deleted' === $action ) {
222
+ $meta_value = get_post_meta( $object_id, $meta_key, true );
223
+ }
224
+
225
+ $this->log(
226
+ esc_html_x( '"%1$s" field in "%2$s" %3$s', 'acf', 'stream' ),
227
+ array(
228
+ 'label' => $meta_value['label'],
229
+ 'title' => $post->post_title,
230
+ 'action' => strtolower( $action_labels[ $action ] ),
231
+ 'key' => $meta_value['key'],
232
+ 'name' => $meta_value['name'],
233
+ ),
234
+ $object_id,
235
+ 'fields',
236
+ $action
237
+ );
238
+ } elseif ( 'rule' === $meta_key ) {
239
+ if ( 'deleted' === $action ) {
240
+ $this->cached_location_rules[ $object_id ] = get_post_meta( $object_id, 'rule' );
241
+
242
+ add_action( 'shutdown', array( $this, 'check_location_rules' ), 9 );
243
+ }
244
+ } elseif ( 'position' === $meta_key ) {
245
+ if ( 'deleted' === $action ) {
246
+ return;
247
+ }
248
+
249
+ $options = array(
250
+ 'acf_after_title' => esc_html_x( 'High (after title)', 'acf', 'stream' ),
251
+ 'normal' => esc_html_x( 'Normal (after content)', 'acf', 'stream' ),
252
+ 'side' => esc_html_x( 'Side', 'acf', 'stream' ),
253
+ );
254
+
255
+ $this->log(
256
+ esc_html_x( 'Position of "%1$s" updated to "%2$s"', 'acf', 'stream' ),
257
+ array(
258
+ 'title' => $post->post_title,
259
+ 'option_label' => $options[ $meta_value ],
260
+ 'option' => $meta_key,
261
+ 'option_value' => $meta_value,
262
+ ),
263
+ $object_id,
264
+ 'options',
265
+ 'updated'
266
+ );
267
+ } elseif ( 'layout' === $meta_key ) {
268
+ if ( 'deleted' === $action ) {
269
+ return;
270
+ }
271
+
272
+ $options = array(
273
+ 'no_box' => esc_html_x( 'Seamless (no metabox)', 'acf', 'stream' ),
274
+ 'default' => esc_html_x( 'Standard (WP metabox)', 'acf', 'stream' ),
275
+ );
276
+
277
+ $this->log(
278
+ esc_html_x( 'Style of "%1$s" updated to "%2$s"', 'acf', 'stream' ),
279
+ array(
280
+ 'title' => $post->post_title,
281
+ 'option_label' => $options[ $meta_value ],
282
+ 'option' => $meta_key,
283
+ 'option_value' => $meta_value,
284
+ ),
285
+ $object_id,
286
+ 'options',
287
+ 'updated'
288
+ );
289
+ } elseif ( 'hide_on_screen' === $meta_key ) {
290
+ if ( 'deleted' === $action ) {
291
+ return;
292
+ }
293
+
294
+ $options = array(
295
+ 'permalink' => esc_html_x( 'Permalink', 'acf', 'stream' ),
296
+ 'the_content' => esc_html_x( 'Content Editor', 'acf', 'stream' ),
297
+ 'excerpt' => esc_html_x( 'Excerpt', 'acf', 'stream' ),
298
+ 'custom_fields' => esc_html_x( 'Custom Fields', 'acf', 'stream' ),
299
+ 'discussion' => esc_html_x( 'Discussion', 'acf', 'stream' ),
300
+ 'comments' => esc_html_x( 'Comments', 'acf', 'stream' ),
301
+ 'revisions' => esc_html_x( 'Revisions', 'acf', 'stream' ),
302
+ 'slug' => esc_html_x( 'Slug', 'acf', 'stream' ),
303
+ 'author' => esc_html_x( 'Author', 'acf', 'stream' ),
304
+ 'format' => esc_html_x( 'Format', 'acf', 'stream' ),
305
+ 'featured_image' => esc_html_x( 'Featured Image', 'acf', 'stream' ),
306
+ 'categories' => esc_html_x( 'Categories', 'acf', 'stream' ),
307
+ 'tags' => esc_html_x( 'Tags', 'acf', 'stream' ),
308
+ 'send-trackbacks' => esc_html_x( 'Send Trackbacks', 'acf', 'stream' ),
309
+ );
310
+
311
+ if ( count( $options ) === count( $meta_value ) ) {
312
+ $options_label = esc_html_x( 'All screens', 'acf', 'stream' );
313
+ } elseif ( empty( $meta_value ) ) {
314
+ $options_label = esc_html_x( 'No screens', 'acf', 'stream' );
315
+ } else {
316
+ $options_label = implode( ', ', array_intersect_key( $options, array_flip( $meta_value ) ) );
317
+ }
318
+
319
+ $this->log(
320
+ esc_html_x( '"%1$s" set to display on "%2$s"', 'acf', 'stream' ),
321
+ array(
322
+ 'title' => $post->post_title,
323
+ 'option_label' => $options_label,
324
+ 'option' => $meta_key,
325
+ 'option_value' => $meta_value,
326
+ ),
327
+ $object_id,
328
+ 'options',
329
+ 'updated'
330
+ );
331
+ }
332
+ }
333
+
334
+ /**
335
+ * Track changes to ACF values within rendered post meta forms
336
+ *
337
+ * @param string $type Type of object, post or user
338
+ * @param string $action Added, updated, deleted
339
+ * @param integer $meta_id
340
+ * @param integer $object_id
341
+ * @param string $key
342
+ * @param mixed|null $value
343
+ *
344
+ * @return bool
345
+ */
346
+ public function check_meta_values( $type, $action, $meta_id, $object_id, $key, $value = null ) {
347
+ unset( $action );
348
+ unset( $meta_id );
349
+
350
+ if ( empty( $this->cached_field_values_updates ) ) {
351
+ return false;
352
+ }
353
+
354
+ $object_key = $object_id;
355
+
356
+ if ( 'user' === $type ) {
357
+ $object_key = 'user_' . $object_id;
358
+ } elseif ( 'taxonomy' === $type ) {
359
+ if ( 0 === strpos( $key, '_' ) ) { // Ignore the 'revision' stuff!
360
+ return false;
361
+ }
362
+
363
+ if ( 1 !== preg_match( '#([a-z0-9_-]+)_([\d]+)_([a-z0-9_-]+)#', $key, $matches ) ) {
364
+ return false;
365
+ }
366
+
367
+ list( , $taxonomy, $term_id, $key ) = $matches; // Skips 0 index
368
+
369
+ $object_key = $taxonomy . '_' . $term_id;
370
+ }
371
+
372
+ if ( isset( $this->cached_field_values_updates[ $object_key ][ $key ] ) ) {
373
+ if ( 'post' === $type ) {
374
+ $posts_connector = new Connector_Posts();
375
+
376
+ $post = get_post( $object_id );
377
+ $title = $post->post_title;
378
+ $type_name = strtolower( $posts_connector->get_post_type_name( $post->post_type ) );
379
+ } elseif ( 'user' === $type ) {
380
+ $user = new \WP_User( $object_id );
381
+ $title = $user->get( 'display_name' );
382
+ $type_name = esc_html__( 'user', 'stream' );
383
+ } elseif ( 'taxonomy' === $type && isset( $term_id ) && isset( $taxonomy ) ) {
384
+ $term = get_term( $term_id, $taxonomy );
385
+ $title = $term->name;
386
+ $tax_obj = get_taxonomy( $taxonomy );
387
+ $type_name = strtolower( get_taxonomy_labels( $tax_obj )->singular_name );
388
+ } else {
389
+ return false;
390
+ }
391
+
392
+ $cache = $this->cached_field_values_updates[ $object_key ][ $key ];
393
+
394
+ $this->log(
395
+ esc_html_x( '"%1$s" of "%2$s" %3$s updated', 'acf', 'stream' ),
396
+ array(
397
+ 'field_label' => $cache['field']['label'],
398
+ 'title' => $title,
399
+ 'singular_name' => $type_name,
400
+ 'meta_value' => $value,
401
+ 'meta_key' => $key,
402
+ 'meta_type' => $type,
403
+ ),
404
+ $object_id,
405
+ 'values',
406
+ 'updated'
407
+ );
408
+ }
409
+
410
+ return true;
411
+ }
412
+
413
+ /**
414
+ * Track changes to rules, complements post-meta updates
415
+ *
416
+ * @action shutdown
417
+ */
418
+ public function check_location_rules() {
419
+ foreach ( $this->cached_location_rules as $post_id => $old ) {
420
+ $new = get_post_meta( $post_id, 'rule' );
421
+ $post = get_post( $post_id );
422
+
423
+ if ( $old === $new ) {
424
+ continue;
425
+ }
426
+
427
+ $new = array_map( 'wp_stream_json_encode', $new );
428
+ $old = array_map( 'wp_stream_json_encode', $old );
429
+ $added = array_diff( $new, $old );
430
+ $deleted = array_diff( $old, $new );
431
+
432
+ $this->log(
433
+ esc_html_x( 'Updated rules of "%1$s" (%2$d added, %3$d deleted)', 'acf', 'stream' ),
434
+ array(
435
+ 'title' => $post->post_title,
436
+ 'no_added' => count( $added ),
437
+ 'no_deleted' => count( $deleted ),
438
+ 'added' => $added,
439
+ 'deleted' => $deleted,
440
+ ),
441
+ $post_id,
442
+ 'rules',
443
+ 'updated'
444
+ );
445
+ }
446
+ }
447
+
448
+ /**
449
+ * Override connector log for our own Settings / Actions
450
+ *
451
+ * @param array $data
452
+ *
453
+ * @return array|bool
454
+ */
455
+ public function log_override( $data ) {
456
+ if ( ! is_array( $data ) ) {
457
+ return $data;
458
+ }
459
+
460
+ if ( 'posts' === $data['connector'] && 'acf' === $data['context'] ) {
461
+ $data['context'] = 'field_groups';
462
+ $data['connector'] = $this->name;
463
+ $data['args']['singular_name'] = esc_html__( 'field group', 'stream' );
464
+ }
465
+
466
+ return $data;
467
+ }
468
+
469
+ /**
470
+ * Track changes to custom field values updates, saves filtered values to be
471
+ * processed by callback_updated_post_meta
472
+ *
473
+ * @param string $value
474
+ * @param int $post_id
475
+ * @param string $field
476
+ *
477
+ * @return string
478
+ */
479
+ public function callback_acf_update_value( $value, $post_id, $field ) {
480
+ $this->cached_field_values_updates[ $post_id ][ $field['name'] ] = compact( 'field', 'value', 'post_id' );
481
+ return $value;
482
+ }
483
+
484
+ /**
485
+ * Track changes to post main attributes, ie: Order No.
486
+ *
487
+ * @param int $post_id
488
+ * @param array $data Array with the updated post data
489
+ */
490
+ public function callback_pre_post_update( $post_id, $data ) {
491
+ $post = get_post( $post_id );
492
+
493
+ if ( 'acf' !== $post->post_type ) {
494
+ return;
495
+ }
496
+
497
+ // menu_order, aka Order No.
498
+ if ( $data['menu_order'] !== $post->menu_order ) {
499
+ $this->log(
500
+ esc_html_x( '"%1$s" reordered from %2$d to %3$d', 'acf', 'stream' ),
501
+ array(
502
+ 'title' => $post->post_title,
503
+ 'old_menu_order' => $post->menu_order,
504
+ 'menu_order' => $data['menu_order'],
505
+ ),
506
+ $post_id,
507
+ 'field_groups',
508
+ 'updated'
509
+ );
510
+ }
511
+ }
512
+
513
+ /**
514
+ * Track addition of new options
515
+ *
516
+ * @param string $key Option name
517
+ * @param string $value Option value
518
+ */
519
+ public function callback_added_option( $key, $value ) {
520
+ $this->check_meta_values( 'taxonomy', 'added', null, null, $key, $value );
521
+ }
522
+
523
+ /**
524
+ * Track addition of new options
525
+ *
526
+ * @param $key
527
+ * @param $old
528
+ * @param $value
529
+ */
530
+ public function callback_updated_option( $key, $old, $value ) {
531
+ unset( $old );
532
+ $this->check_meta_values( 'taxonomy', 'updated', null, null, $key, $value );
533
+ }
534
+
535
+ /**
536
+ * Track addition of new options
537
+ *
538
+ * @param $key
539
+ */
540
+ public function callback_deleted_option( $key ) {
541
+ $this->check_meta_values( 'taxonomy', 'deleted', null, null, $key, null );
542
+ }
543
+ }
connectors/class-connector-bbpress.php ADDED
@@ -0,0 +1,244 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace WP_Stream;
3
+
4
+ class Connector_bbPress extends Connector {
5
+ /**
6
+ * Connector slug
7
+ *
8
+ * @var string
9
+ */
10
+ public $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 $actions = array(
25
+ 'bbp_toggle_topic_admin',
26
+ );
27
+
28
+ /**
29
+ * Tracked option keys
30
+ *
31
+ * @var array
32
+ */
33
+ public $options = array(
34
+ 'bbpress' => null,
35
+ );
36
+
37
+ /**
38
+ * Flag to stop logging update logic twice
39
+ *
40
+ * @var bool
41
+ */
42
+ public $is_update = false;
43
+
44
+ /**
45
+ * @var bool
46
+ */
47
+ public $_deleted_activity = false;
48
+
49
+ /**
50
+ * @var array
51
+ */
52
+ public $_delete_activity_args = array();
53
+
54
+ /**
55
+ * @var bool
56
+ */
57
+ public $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 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 function get_label() {
78
+ return esc_html_x( 'bbPress', 'bbpress', 'stream' );
79
+ }
80
+
81
+ /**
82
+ * Return translated action labels
83
+ *
84
+ * @return array Action label translations
85
+ */
86
+ public function get_action_labels() {
87
+ return array(
88
+ 'created' => esc_html_x( 'Created', 'bbpress', 'stream' ),
89
+ 'updated' => esc_html_x( 'Updated', 'bbpress', 'stream' ),
90
+ 'activated' => esc_html_x( 'Activated', 'bbpress', 'stream' ),
91
+ 'deactivated' => esc_html_x( 'Deactivated', 'bbpress', 'stream' ),
92
+ 'deleted' => esc_html_x( 'Deleted', 'bbpress', 'stream' ),
93
+ 'trashed' => esc_html_x( 'Trashed', 'bbpress', 'stream' ),
94
+ 'untrashed' => esc_html_x( 'Restored', 'bbpress', 'stream' ),
95
+ 'generated' => esc_html_x( 'Generated', 'bbpress', 'stream' ),
96
+ 'imported' => esc_html_x( 'Imported', 'bbpress', 'stream' ),
97
+ 'exported' => esc_html_x( 'Exported', 'bbpress', 'stream' ),
98
+ 'closed' => esc_html_x( 'Closed', 'bbpress', 'stream' ),
99
+ 'opened' => esc_html_x( 'Opened', 'bbpress', 'stream' ),
100
+ 'sticked' => esc_html_x( 'Sticked', 'bbpress', 'stream' ),
101
+ 'unsticked' => esc_html_x( 'Unsticked', 'bbpress', 'stream' ),
102
+ 'spammed' => esc_html_x( 'Marked as spam', 'bbpress', 'stream' ),
103
+ 'unspammed' => esc_html_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 function get_context_labels() {
113
+ return array(
114
+ 'settings' => esc_html_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 function action_links( $links, $record ) {
129
+ if ( 'settings' === $record->context ) {
130
+ $option = $record->get_meta( 'option', true );
131
+ $links[ esc_html__( 'Edit', 'stream' ) ] = esc_url(
132
+ add_query_arg(
133
+ array(
134
+ 'page' => 'bbpress',
135
+ ),
136
+ admin_url( 'options-general.php' )
137
+ ) . esc_url_raw( '#' . $option )
138
+ );
139
+ }
140
+ return $links;
141
+ }
142
+
143
+ public function register() {
144
+ parent::register();
145
+
146
+ add_filter( 'wp_stream_log_data', array( $this, 'log_override' ) );
147
+ }
148
+
149
+ /**
150
+ * Override connector log for our own Settings / Actions
151
+ *
152
+ * @param array $data
153
+ *
154
+ * @return array|bool
155
+ */
156
+ public function log_override( $data ) {
157
+ if ( ! is_array( $data ) ) {
158
+ return $data;
159
+ }
160
+
161
+ if ( 'settings' === $data['connector'] && 'bbpress' === $data['args']['context'] ) {
162
+ $settings = \bbp_admin_get_settings_fields();
163
+
164
+ /* fix for missing title for this single field */
165
+ $settings['bbp_settings_features']['_bbp_allow_threaded_replies']['title'] = esc_html__( 'Reply Threading', 'stream' );
166
+
167
+ $option = $data['args']['option'];
168
+
169
+ foreach ( $settings as $section => $fields ) {
170
+ if ( isset( $fields[ $option ] ) ) {
171
+ $field = $fields[ $option ];
172
+ break;
173
+ }
174
+ }
175
+
176
+ if ( ! isset( $field ) ) {
177
+ return $data;
178
+ }
179
+
180
+ $data['args']['label'] = $field['title'];
181
+ $data['connector'] = $this->name;
182
+ $data['context'] = 'settings';
183
+ $data['action'] = 'updated';
184
+ } elseif ( 'posts' === $data['connector'] && in_array( $data['context'], array( 'forum', 'topic', 'reply' ) ) ) {
185
+ if ( 'reply' === $data['context'] ) {
186
+ if ( 'updated' === $data['action'] ) {
187
+ $data['message'] = esc_html__( 'Replied on "%1$s"', 'stream' );
188
+ $data['args']['post_title'] = get_post( wp_get_post_parent_id( $data['object_id'] ) )->post_title;
189
+ }
190
+ $data['args']['post_title'] = sprintf(
191
+ __( 'Reply to: %s', 'stream' ),
192
+ get_post( wp_get_post_parent_id( $data['object_id'] ) )->post_title
193
+ );
194
+ }
195
+
196
+ $data['connector'] = $this->name;
197
+ } elseif ( 'taxonomies' === $data['connector'] && in_array( $data['context'], array( 'topic-tag' ) ) ) {
198
+ $data['connector'] = $this->name;
199
+ }
200
+
201
+ return $data;
202
+ }
203
+
204
+ /**
205
+ * Tracks togging the forum topics
206
+ *
207
+ * @param bool $success
208
+ * @param \WP_Post $post_data
209
+ * @param string $action
210
+ * @param string $message
211
+ *
212
+ * @return array|bool
213
+ */
214
+ public function callback_bbp_toggle_topic_admin( $success, $post_data, $action, $message ) {
215
+ unset( $success );
216
+ unset( $post_data );
217
+ unset( $action );
218
+
219
+ if ( ! empty( $message['failed'] ) ) {
220
+ return;
221
+ }
222
+
223
+ $action = $message['bbp_topic_toggle_notice'];
224
+ $actions = $this->get_action_labels();
225
+
226
+ if ( ! isset( $actions[ $action ] ) ) {
227
+ return;
228
+ }
229
+
230
+ $topic = get_post( $message['topic_id'] );
231
+
232
+ $this->log(
233
+ _x( '%1$s "%2$s" topic', '1: Action, 2: Topic title', 'stream' ),
234
+ array(
235
+ 'action_title' => $actions[ $action ],
236
+ 'topic_title' => $topic->post_title,
237
+ 'action' => $action,
238
+ ),
239
+ $topic->ID,
240
+ 'topic',
241
+ $action
242
+ );
243
+ }
244
+ }
connectors/class-connector-blogs.php ADDED
@@ -0,0 +1,364 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace WP_Stream;
3
+
4
+ class Connector_Blogs extends Connector {
5
+ /**
6
+ * Connector slug
7
+ *
8
+ * @var string
9
+ */
10
+ public $name = 'blogs';
11
+
12
+ /**
13
+ * Actions registered for this connector
14
+ *
15
+ * @var array
16
+ */
17
+ public $actions = array(
18
+ 'wpmu_new_blog',
19
+ 'wpmu_activate_blog',
20
+ 'wpmu_new_user',
21
+ 'add_user_to_blog',
22
+ 'remove_user_from_blog',
23
+ 'make_spam_blog',
24
+ 'make_ham_blog',
25
+ 'mature_blog',
26
+ 'unmature_blog',
27
+ 'archive_blog',
28
+ 'unarchive_blog',
29
+ 'make_delete_blog',
30
+ 'make_undelete_blog',
31
+ 'update_blog_public',
32
+ );
33
+
34
+ /**
35
+ * Return translated connector label
36
+ *
37
+ * @return string
38
+ */
39
+ public function get_label() {
40
+ return esc_html__( 'Sites' );
41
+ }
42
+
43
+ /**
44
+ * Return translated action labels
45
+ *
46
+ * @return array
47
+ */
48
+ public function get_action_labels() {
49
+ return array(
50
+ 'archive_blog' => esc_html__( 'Archived', 'stream' ),
51
+ 'created' => esc_html__( 'Created', 'stream' ),
52
+ 'deleted' => esc_html__( 'Deleted', 'stream' ),
53
+ 'updated' => esc_html__( 'Updated', 'stream' ),
54
+ );
55
+ }
56
+
57
+ /**
58
+ * Return translated context labels
59
+ *
60
+ * @return array
61
+ */
62
+ public function get_context_labels() {
63
+ $labels = array();
64
+
65
+ if ( is_multisite() && ! wp_is_large_network() ) {
66
+ $blogs = wp_get_sites();
67
+
68
+ foreach ( $blogs as $blog ) {
69
+ $blog_details = get_blog_details( $blog['blog_id'] );
70
+ $key = sanitize_key( $blog_details->blogname );
71
+ $labels[ $key ] = $blog_details->blogname;
72
+ }
73
+ }
74
+
75
+ return $labels;
76
+ }
77
+
78
+ /**
79
+ * Add action links to Stream drop row in admin list screen
80
+ *
81
+ * @filter wp_stream_action_links_{connector}
82
+ *
83
+ * @param array $links
84
+ * @param Record $record
85
+ *
86
+ * @return array
87
+ */
88
+ public function action_links( $links, $record ) {
89
+ $links [ esc_html__( 'Site Admin' ) ] = get_admin_url( $record->object_id );
90
+
91
+ if ( $record->object_id ) {
92
+ $site_admin_link = get_admin_url( $record->object_id );
93
+
94
+ if ( $site_admin_link ) {
95
+ $links [ esc_html__( 'Site Admin' ) ] = $site_admin_link;
96
+ }
97
+
98
+ $site_settings_link = add_query_arg(
99
+ array(
100
+ 'id' => $record->object_id,
101
+ ),
102
+ network_admin_url( 'site-info.php' )
103
+ );
104
+
105
+ if ( $site_settings_link ) {
106
+ $links [ esc_html__( 'Site Settings', 'stream' ) ] = $site_settings_link;
107
+ }
108
+ }
109
+
110
+ return $links;
111
+ }
112
+
113
+ /**
114
+ * Blog created
115
+ *
116
+ * @action wpmu_new_blog
117
+ *
118
+ * @param int $blog_id
119
+ */
120
+ public function callback_wpmu_new_blog( $blog_id ) {
121
+ $blog = get_blog_details( $blog_id );
122
+
123
+ $this->log(
124
+ _x(
125
+ '"%1$s" site was created',
126
+ '1. Site name',
127
+ 'stream'
128
+ ),
129
+ array(
130
+ 'site_name' => $blog->blogname,
131
+ ),
132
+ $blog_id,
133
+ sanitize_key( $blog->blogname ),
134
+ 'created'
135
+ );
136
+ }
137
+
138
+ /**
139
+ * Blog registered
140
+ *
141
+ * @action wpmu_activate_blog
142
+ *
143
+ * @param int $blog_id
144
+ * @param int $user_id
145
+ */
146
+ public function callback_wpmu_activate_blog( $blog_id, $user_id ) {
147
+ $blog = get_blog_details( $blog_id );
148
+
149
+ $this->log(
150
+ _x(
151
+ '"%1$s" site was registered',
152
+ '1. Site name',
153
+ 'stream'
154
+ ),
155
+ array(
156
+ 'site_name' => $blog->blogname,
157
+ ),
158
+ $blog_id,
159
+ sanitize_key( $blog->blogname ),
160
+ 'created',
161
+ $user_id
162
+ );
163
+ }
164
+
165
+ /**
166
+ * User added to a blog
167
+ *
168
+ * @action add_user_to_blog
169
+ *
170
+ * @param int $user_id
171
+ * @param string $role
172
+ * @param int $blog_id
173
+ */
174
+ public function callback_add_user_to_blog( $user_id, $role, $blog_id ) {
175
+ $blog = get_blog_details( $blog_id );
176
+ $user = get_user_by( 'id', $user_id );
177
+
178
+ if ( ! is_a( $user, 'WP_User' ) ) {
179
+ return;
180
+ }
181
+
182
+ $this->log(
183
+ _x(
184
+ '%1$s was added to the "%2$s" site with %3$s capabilities',
185
+ '1. User\'s name, 2. Site name, 3. Role',
186
+ 'stream'
187
+ ),
188
+ array(
189
+ 'user_name' => $user->display_name,
190
+ 'site_name' => $blog->blogname,
191
+ 'role_name' => $role,
192
+ ),
193
+ $blog_id,
194
+ sanitize_key( $blog->blogname ),
195
+ 'updated'
196
+ );
197
+ }
198
+
199
+ /**
200
+ * User removed from a blog
201
+ *
202
+ * @action remove_user_from_blog
203
+ *
204
+ * @param int $user_id
205
+ * @param int $blog_id
206
+ */
207
+ public function callback_remove_user_from_blog( $user_id, $blog_id ) {
208
+ $blog = get_blog_details( $blog_id );
209
+ $user = get_user_by( 'id', $user_id );
210
+
211
+ if ( ! is_a( $user, 'WP_User' ) ) {
212
+ return;
213
+ }
214
+
215
+ $this->log(
216
+ _x(
217
+ '%1$s was removed from the "%2$s" site',
218
+ '1. User\'s name, 2. Site name',
219
+ 'stream'
220
+ ),
221
+ array(
222
+ 'user_name' => $user->display_name,
223
+ 'site_name' => $blog->blogname,
224
+ ),
225
+ $blog_id,
226
+ sanitize_key( $blog->blogname ),
227
+ 'updated'
228
+ );
229
+ }
230
+
231
+ /**
232
+ * Blog marked as spam
233
+ *
234
+ * @action make_spam_blog
235
+ *
236
+ * @param int $blog_id
237
+ */
238
+ public function callback_make_spam_blog( $blog_id ) {
239
+ $this->callback_update_blog_status( $blog_id, esc_html__( 'marked as spam', 'stream' ), 'updated' );
240
+ }
241
+
242
+ /**
243
+ * Blog not marked as spam
244
+ *
245
+ * @action make_ham_blog
246
+ *
247
+ * @param int $blog_id
248
+ */
249
+ public function callback_make_ham_blog( $blog_id ) {
250
+ $this->callback_update_blog_status( $blog_id, esc_html__( 'marked as not spam', 'stream' ), 'updated' );
251
+ }
252
+
253
+ /**
254
+ * Blog marked as mature
255
+ *
256
+ * @action mature_blog
257
+ *
258
+ * @param int $blog_id
259
+ */
260
+ public function callback_mature_blog( $blog_id ) {
261
+ $this->callback_update_blog_status( $blog_id, esc_html__( 'marked as mature', 'stream' ), 'updated' );
262
+ }
263
+
264
+ /**
265
+ * Blog not marked as mature
266
+ *
267
+ * @action unmature_blog
268
+ *
269
+ * @param int $blog_id
270
+ */
271
+ public function callback_unmature_blog( $blog_id ) {
272
+ $this->callback_update_blog_status( $blog_id, esc_html__( 'marked as not mature', 'stream' ), 'updated' );
273
+ }
274
+
275
+ /**
276
+ * Blog marked as archived
277
+ *
278
+ * @action archive_blog
279
+ *
280
+ * @param int $blog_id
281
+ */
282
+ public function callback_archive_blog( $blog_id ) {
283
+ $this->callback_update_blog_status( $blog_id, esc_html__( 'archived', 'stream' ), 'archive_blog' );
284
+ }
285
+
286
+ /**
287
+ * Blog not marked as archived
288
+ *
289
+ * @action unarchive_blog
290
+ *
291
+ * @param int $blog_id
292
+ */
293
+ public function callback_unarchive_blog( $blog_id ) {
294
+ $this->callback_update_blog_status( $blog_id, esc_html__( 'restored from archive', 'stream' ), 'updated' );
295
+ }
296
+
297
+ /**
298
+ * Blog marked as deleted
299
+ *
300
+ * @action make_delete_blog
301
+ *
302
+ * @param int $blog_id
303
+ */
304
+ public function callback_make_delete_blog( $blog_id ) {
305
+ $this->callback_update_blog_status( $blog_id, esc_html__( 'deleted', 'stream' ), 'deleted' );
306
+ }
307
+
308
+ /**
309
+ * Blog not marked as deleted
310
+ *
311
+ * @action undelete_blog
312
+ *
313
+ * @param int $blog_id
314
+ */
315
+ public function callback_make_undelete_blog( $blog_id ) {
316
+ $this->callback_update_blog_status( $blog_id, esc_html__( 'restored', 'stream' ), 'updated' );
317
+ }
318
+
319
+ /**
320
+ * Blog marked as public or private
321
+ *
322
+ * @action update_blog_public
323
+ *
324
+ * @param int $blog_id
325
+ * @param string $value
326
+ */
327
+ public function callback_update_blog_public( $blog_id, $value ) {
328
+ if ( $value ) {
329
+ $status = esc_html__( 'marked as public', 'stream' );
330
+ } else {
331
+ $status = esc_html__( 'marked as private', 'stream' );
332
+ }
333
+
334
+ $this->callback_update_blog_status( $blog_id, $status, 'updated' );
335
+ }
336
+
337
+ /**
338
+ * Blog updated
339
+ *
340
+ * @action update_blog_status
341
+ *
342
+ * @param int $blog_id
343
+ * @param string $status
344
+ * @param string $action
345
+ */
346
+ public function callback_update_blog_status( $blog_id, $status, $action ) {
347
+ $blog = get_blog_details( $blog_id );
348
+
349
+ $this->log(
350
+ _x(
351
+ '"%1$s" site was %2$s',
352
+ '1. Site name, 2. Status',
353
+ 'stream'
354
+ ),
355
+ array(
356
+ 'site_name' => $blog->blogname,
357
+ 'status' => $status,
358
+ ),
359
+ $blog_id,
360
+ sanitize_key( $blog->blogname ),
361
+ $action
362
+ );
363
+ }
364
+ }
connectors/class-connector-buddypress.php ADDED
@@ -0,0 +1,816 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace WP_Stream;
3
+
4
+ class Connector_BuddyPress extends Connector {
5
+ /**
6
+ * Connector slug
7
+ *
8
+ * @var string
9
+ */
10
+ public $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 $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 $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 $is_update = false;
78
+
79
+ /**
80
+ * @var bool
81
+ */
82
+ public $_deleted_activity = false;
83
+
84
+ /**
85
+ * @var array
86
+ */
87
+ public $_delete_activity_args = array();
88
+
89
+ /**
90
+ * @var bool
91
+ */
92
+ public $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 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 function get_label() {
113
+ return esc_html_x( 'BuddyPress', 'buddypress', 'stream' );
114
+ }
115
+
116
+ /**
117
+ * Return translated action labels
118
+ *
119
+ * @return array Action label translations
120
+ */
121
+ public function get_action_labels() {
122
+ return array(
123
+ 'created' => esc_html_x( 'Created', 'buddypress', 'stream' ),
124
+ 'updated' => esc_html_x( 'Updated', 'buddypress', 'stream' ),
125
+ 'activated' => esc_html_x( 'Activated', 'buddypress', 'stream' ),
126
+ 'deactivated' => esc_html_x( 'Deactivated', 'buddypress', 'stream' ),
127
+ 'deleted' => esc_html_x( 'Deleted', 'buddypress', 'stream' ),
128
+ 'spammed' => esc_html_x( 'Marked as spam', 'buddypress', 'stream' ),
129
+ 'unspammed' => esc_html_x( 'Unmarked as spam', 'buddypress', 'stream' ),
130
+ 'promoted' => esc_html_x( 'Promoted', 'buddypress', 'stream' ),
131
+ 'demoted' => esc_html_x( 'Demoted', 'buddypress', 'stream' ),
132
+ );
133
+ }
134
+
135
+ /**
136
+ * Return translated context labels
137
+ *
138
+ * @return array Context label translations
139
+ */
140
+ public function get_context_labels() {
141
+ return array(
142
+ 'components' => esc_html_x( 'Components', 'buddypress', 'stream' ),
143
+ 'groups' => esc_html_x( 'Groups', 'buddypress', 'stream' ),
144
+ 'activity' => esc_html_x( 'Activity', 'buddypress', 'stream' ),
145
+ 'profile_fields' => esc_html_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 function action_links( $links, $record ) {
160
+ if ( in_array( $record->context, array( 'components' ) ) ) {
161
+ $option_key = $record->get_meta( 'option_key', true );
162
+
163
+ if ( 'bp-active-components' === $option_key ) {
164
+ $links[ esc_html__( '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 = $record->get_meta( 'page_id', true );
172
+
173
+ $links[ esc_html__( '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[ esc_html__( 'Edit Page', 'stream' ) ] = get_edit_post_link( $page_id );
182
+ $links[ esc_html__( 'View', 'stream' ) ] = get_permalink( $page_id );
183
+ }
184
+ }
185
+ } elseif ( in_array( $record->context, array( 'settings' ) ) ) {
186
+ $links[ esc_html__( 'Edit setting', 'stream' ) ] = add_query_arg(
187
+ array(
188
+ 'page' => $record->get_meta( 'page', true ),
189
+ ),
190
+ admin_url( 'admin.php' )
191
+ );
192
+ } elseif ( in_array( $record->context, array( 'groups' ) ) ) {
193
+ $group_id = $record->get_meta( '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[ esc_html__( 'Edit group', 'stream' ) ] = $edit_url;
204
+ $links[ esc_html__( 'View group', 'stream' ) ] = $visit_url;
205
+ $links[ esc_html__( 'Delete group', 'stream' ) ] = $delete_url;
206
+ }
207
+ } elseif ( in_array( $record->context, array( 'activity' ) ) ) {
208
+ $activity_id = $record->get_meta( '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[ esc_html__( 'Ham', 'stream' ) ] = $ham_url;
222
+ } else {
223
+ $links[ esc_html__( 'Edit', 'stream' ) ] = $edit_url;
224
+ $links[ esc_html__( 'Spam', 'stream' ) ] = $spam_url;
225
+ }
226
+ $links[ esc_html__( 'Delete', 'stream' ) ] = $delete_url;
227
+ }
228
+ } elseif ( in_array( $record->context, array( 'profile_fields' ) ) ) {
229
+ $field_id = $record->get_meta( 'field_id', true );
230
+ $group_id = $record->get_meta( 'group_id', true );
231
+
232
+ if ( empty( $field_id ) ) { // is a group action
233
+ $links[ esc_html__( '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[ esc_html__( '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[ esc_html__( '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[ esc_html__( '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 function register() {
278
+ parent::register();
279
+
280
+ $this->options = array_merge(
281
+ $this->options,
282
+ array(
283
+ 'hide-loggedout-adminbar' => array(
284
+ 'label' => esc_html_x( 'Toolbar', 'buddypress', 'stream' ),
285
+ 'page' => 'bp-settings',
286
+ ),
287
+ '_bp_force_buddybar' => array(
288
+ 'label' => esc_html_x( 'Toolbar', 'buddypress', 'stream' ),
289
+ 'page' => 'bp-settings',
290
+ ),
291
+ 'bp-disable-account-deletion' => array(
292
+ 'label' => esc_html_x( 'Account Deletion', 'buddypress', 'stream' ),
293
+ 'page' => 'bp-settings',
294
+ ),
295
+ 'bp-disable-profile-sync' => array(
296
+ 'label' => esc_html_x( 'Profile Syncing', 'buddypress', 'stream' ),
297
+ 'page' => 'bp-settings',
298
+ ),
299
+ 'bp_restrict_group_creation' => array(
300
+ 'label' => esc_html_x( 'Group Creation', 'buddypress', 'stream' ),
301
+ 'page' => 'bp-settings',
302
+ ),
303
+ 'bb-config-location' => array(
304
+ 'label' => esc_html_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' => esc_html_x( 'Activity auto-refresh', 'buddypress', 'stream' ),
313
+ 'page' => 'bp-settings',
314
+ ),
315
+ '_bp_enable_akismet' => array(
316
+ 'label' => esc_html_x( 'Akismet', 'buddypress', 'stream' ),
317
+ 'page' => 'bp-settings',
318
+ ),
319
+ 'bp-disable-avatar-uploads' => array(
320
+ 'label' => esc_html_x( 'Avatar Uploads', 'buddypress', 'stream' ),
321
+ 'page' => 'bp-settings',
322
+ ),
323
+ )
324
+ );
325
+ }
326
+
327
+ public function callback_update_option( $option, $old, $new ) {
328
+ $this->check( $option, $old, $new );
329
+ }
330
+
331
+ public function callback_add_option( $option, $val ) {
332
+ $this->check( $option, null, $val );
333
+ }
334
+
335
+ public function callback_delete_option( $option ) {
336
+ $this->check( $option, null, null );
337
+ }
338
+
339
+ public function callback_update_site_option( $option, $old, $new ) {
340
+ $this->check( $option, $old, $new );
341
+ }
342
+
343
+ public function callback_add_site_option( $option, $val ) {
344
+ $this->check( $option, null, $val );
345
+ }
346
+
347
+ public function callback_delete_site_option( $option ) {
348
+ $this->check( $option, null, null );
349
+ }
350
+
351
+ public function check( $option, $old_value, $new_value ) {
352
+ if ( ! array_key_exists( $option, $this->options ) ) {
353
+ return;
354
+ }
355
+
356
+ $replacement = str_replace( '-', '_', $option );
357
+
358
+ if ( method_exists( $this, 'check_' . $replacement ) ) {
359
+ call_user_func( array( $this, 'check_' . $replacement ), $old_value, $new_value );
360
+ } else {
361
+ $data = $this->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
+ $this->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 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 ( $this->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 => esc_html__( 'activated', 'stream' ),
391
+ false => esc_html__( 'deactivated', 'stream' ),
392
+ );
393
+
394
+ foreach ( $options as $option => $option_value ) {
395
+ if ( ! isset( $components[ $option ], $actions[ $option_value ] ) ) {
396
+ continue;
397
+ }
398
+
399
+ $this->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 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 ( $this->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
+ $this->bp_get_directory_pages(),
431
+ array(
432
+ 'register' => esc_html_x( 'Register', 'buddypress', 'stream' ),
433
+ 'activate' => esc_html_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 : esc_html__( 'No page', 'stream' );
443
+
444
+ $this->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 function callback_bp_before_activity_delete( $args ) {
465
+ if ( empty( $args['id'] ) ) { // Bail if we're deleting in bulk
466
+ $this->_delete_activity_args = $args;
467
+ return;
468
+ }
469
+
470
+ $activity = new \BP_Activity_Activity( $args['id'] );
471
+
472
+ $this->_deleted_activity = $activity;
473
+ }
474
+
475
+ public function callback_bp_activity_deleted_activities( $activities_ids ) {
476
+ if ( 1 === count( $activities_ids ) && isset( $this->_deleted_activity ) ) { // Single activity deletion
477
+ $activity = $this->_deleted_activity;
478
+ $this->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 ( $this->ignore_activity_bulk_deletion ) {
497
+ $this->ignore_activity_bulk_deletion = false;
498
+ return;
499
+ }
500
+ $this->log(
501
+ sprintf(
502
+ __( '"%s" activities were deleted', 'stream' ),
503
+ count( $activities_ids )
504
+ ),
505
+ array(
506
+ 'count' => count( $activities_ids ),
507
+ 'args' => $this->_delete_activity_args,
508
+ 'ids' => $activities_ids,
509
+ ),
510
+ null,
511
+ 'activity',
512
+ 'deleted'
513
+ );
514
+ }
515
+ }
516
+
517
+ public function callback_bp_activity_mark_as_spam( $activity, $by ) {
518
+ unset( $by );
519
+
520
+ $this->log(
521
+ sprintf(
522
+ __( 'Marked activity "%s" as spam', 'stream' ),
523
+ strip_tags( $activity->action )
524
+ ),
525
+ array(
526
+ 'id' => $activity->id,
527
+ 'item_id' => $activity->item_id,
528
+ 'type' => $activity->type,
529
+ 'author' => $activity->user_id,
530
+ ),
531
+ $activity->id,
532
+ $activity->component,
533
+ 'spammed'
534
+ );
535
+ }
536
+
537
+ public function callback_bp_activity_mark_as_ham( $activity, $by ) {
538
+ unset( $by );
539
+
540
+ $this->log(
541
+ sprintf(
542
+ __( 'Unmarked activity "%s" as spam', 'stream' ),
543
+ strip_tags( $activity->action )
544
+ ),
545
+ array(
546
+ 'id' => $activity->id,
547
+ 'item_id' => $activity->item_id,
548
+ 'type' => $activity->type,
549
+ 'author' => $activity->user_id,
550
+ ),
551
+ $activity->id,
552
+ $activity->component,
553
+ 'unspammed'
554
+ );
555
+ }
556
+
557
+ public function callback_bp_activity_admin_edit_after( $activity, $error ) {
558
+ unset( $error );
559
+
560
+ $this->log(
561
+ sprintf(
562
+ __( '"%s" activity updated', 'stream' ),
563
+ strip_tags( $activity->action )
564
+ ),
565
+ array(
566
+ 'id' => $activity->id,
567
+ 'item_id' => $activity->item_id,
568
+ 'type' => $activity->type,
569
+ 'author' => $activity->user_id,
570
+ ),
571
+ $activity->id,
572
+ 'activity',
573
+ 'updated'
574
+ );
575
+ }
576
+
577
+ public function group_action( $group, $action, $meta = array(), $message = null ) {
578
+ if ( is_numeric( $group ) ) {
579
+ $group = \groups_get_group( array( 'group_id' => $group ) );
580
+ }
581
+
582
+ $replacements = array(
583
+ $group->name,
584
+ );
585
+
586
+ if ( ! $message ) {
587
+ if ( 'created' === $action ) {
588
+ $message = esc_html__( '"%s" group created', 'stream' );
589
+ } elseif ( 'updated' === $action ) {
590
+ $message = esc_html__( '"%s" group updated', 'stream' );
591
+ } elseif ( 'deleted' === $action ) {
592
+ $message = esc_html__( '"%s" group deleted', 'stream' );
593
+ } elseif ( 'joined' === $action ) {
594
+ $message = esc_html__( 'Joined group "%s"', 'stream' );
595
+ } elseif ( 'left' === $action ) {
596
+ $message = esc_html__( 'Left group "%s"', 'stream' );
597
+ } elseif ( 'banned' === $action ) {
598
+ $message = esc_html__( 'Banned "%2$s" from "%1$s"', 'stream' );
599
+ $replacements[] = get_user_by( 'id', $meta['user_id'] )->display_name;
600
+ } elseif ( 'unbanned' === $action ) {
601
+ $message = esc_html__( 'Unbanned "%2$s" from "%1$s"', 'stream' );
602
+ $replacements[] = get_user_by( 'id', $meta['user_id'] )->display_name;
603
+ } elseif ( 'removed' === $action ) {
604
+ $message = esc_html__( 'Removed "%2$s" from "%1$s"', 'stream' );
605
+ $replacements[] = get_user_by( 'id', $meta['user_id'] )->display_name;
606
+ } else {
607
+ return;
608
+ }
609
+ }
610
+
611
+ $this->log(
612
+ vsprintf(
613
+ $message,
614
+ $replacements
615
+ ),
616
+ array_merge(
617
+ array(
618
+ 'id' => $group->id,
619
+ 'name' => $group->name,
620
+ 'slug' => $group->slug,
621
+ ),
622
+ $meta
623
+ ),
624
+ $group->id,
625
+ 'groups',
626
+ $action
627
+ );
628
+ }
629
+
630
+ public function callback_groups_create_group( $group_id, $member, $group ) {
631
+ unset( $group_id );
632
+ unset( $member );
633
+
634
+ $this->group_action( $group, 'created' );
635
+ }
636
+
637
+ public function callback_groups_update_group( $group_id, $group ) {
638
+ unset( $group_id );
639
+
640
+ $this->group_action( $group, 'updated' );
641
+ }
642
+
643
+ public function callback_groups_before_delete_group( $group_id ) {
644
+ $this->ignore_activity_bulk_deletion = true;
645
+ $this->group_action( $group_id, 'deleted' );
646
+ }
647
+
648
+ public function callback_groups_details_updated( $group_id ) {
649
+ $this->is_update = true;
650
+ $this->group_action( $group_id, 'updated' );
651
+ }
652
+
653
+ public function callback_groups_settings_updated( $group_id ) {
654
+ if ( $this->is_update ) {
655
+ return;
656
+ }
657
+ $this->group_action( $group_id, 'updated' );
658
+ }
659
+
660
+ public function callback_groups_leave_group( $group_id, $user_id ) {
661
+ $this->group_action( $group_id, 'left', compact( 'user_id' ) );
662
+ }
663
+
664
+ public function callback_groups_join_group( $group_id, $user_id ) {
665
+ $this->group_action( $group_id, 'joined', compact( 'user_id' ) );
666
+ }
667
+
668
+ public function callback_groups_promote_member( $group_id, $user_id, $status ) {
669
+ $group = \groups_get_group( array( 'group_id' => $group_id ) );
670
+ $user = new \WP_User( $user_id );
671
+ $roles = array(
672
+ 'admin' => esc_html_x( 'Administrator', 'buddypress', 'stream' ),
673
+ 'mod' => esc_html_x( 'Moderator', 'buddypress', 'stream' ),
674
+ );
675
+ $message = sprintf(
676
+ __( 'Promoted "%s" to "%s" in "%s"', 'stream' ),
677
+ $user->display_name,
678
+ $roles[ $status ],
679
+ $group->name
680
+ );
681
+ $this->group_action( $group_id, 'promoted', compact( 'user_id', 'status' ), $message );
682
+ }
683
+
684
+ public function callback_groups_demote_member( $group_id, $user_id ) {
685
+ $group = \groups_get_group( array( 'group_id' => $group_id ) );
686
+ $user = new \WP_User( $user_id );
687
+ $message = sprintf(
688
+ __( 'Demoted "%s" to "%s" in "%s"', 'stream' ),
689
+ $user->display_name,
690
+ _x( 'Member', 'buddypress', 'stream' ),
691
+ $group->name
692
+ );
693
+ $this->group_action( $group_id, 'demoted', compact( 'user_id' ), $message );
694
+ }
695
+
696
+ public function callback_groups_ban_member( $group_id, $user_id ) {
697
+ $this->group_action( $group_id, 'banned', compact( 'user_id' ) );
698
+ }
699
+
700
+ public function callback_groups_unban_member( $group_id, $user_id ) {
701
+ $this->group_action( $group_id, 'unbanned', compact( 'user_id' ) );
702
+ }
703
+
704
+ public function callback_groups_remove_member( $group_id, $user_id ) {
705
+ $this->group_action( $group_id, 'removed', compact( 'user_id' ) );
706
+ }
707
+
708
+ public function field_action( $field, $action, $meta = array(), $message = null ) {
709
+ $replacements = array(
710
+ $field->name,
711
+ );
712
+
713
+ if ( ! $message ) {
714
+ if ( 'created' === $action ) {
715
+ $message = esc_html__( 'Created profile field "%s"', 'stream' );
716
+ } elseif ( 'updated' === $action ) {
717
+ $message = esc_html__( 'Updated profile field "%s"', 'stream' );
718
+ } elseif ( 'deleted' === $action ) {
719
+ $message = esc_html__( 'Deleted profile field "%s"', 'stream' );
720
+ } else {
721
+ return;
722
+ }
723
+ }
724
+
725
+ $this->log(
726
+ vsprintf(
727
+ $message,
728
+ $replacements
729
+ ),
730
+ array_merge(
731
+ array(
732
+ 'field_id' => $field->id,
733
+ 'field_name' => $field->name,
734
+ 'group_id' => $field->group_id,
735
+ ),
736
+ $meta
737
+ ),
738
+ $field->id,
739
+ 'profile_fields',
740
+ $action
741
+ );
742
+ }
743
+
744
+ public function callback_xprofile_field_after_save( $field ) {
745
+ $action = isset( $field->id ) ? 'updated' : 'created';
746
+ $this->field_action( $field, $action );
747
+ }
748
+
749
+ public function callback_xprofile_fields_deleted_field( $field ) {
750
+ $this->field_action( $field, 'deleted' );
751
+ }
752
+
753
+ public function field_group_action( $group, $action, $meta = array(), $message = null ) {
754
+ $replacements = array(
755
+ $group->name,
756
+ );
757
+
758
+ if ( ! $message ) {
759
+ if ( 'created' === $action ) {
760
+ $message = esc_html__( 'Created profile field group "%s"', 'stream' );
761
+ } elseif ( 'updated' === $action ) {
762
+ $message = esc_html__( 'Updated profile field group "%s"', 'stream' );
763
+ } elseif ( 'deleted' === $action ) {
764
+ $message = esc_html__( 'Deleted profile field group "%s"', 'stream' );
765
+ } else {
766
+ return;
767
+ }
768
+ }
769
+
770
+ $this->log(
771
+ vsprintf(
772
+ $message,
773
+ $replacements
774
+ ),
775
+ array_merge(
776
+ array(
777
+ 'group_id' => $group->id,
778
+ 'group_name' => $group->name,
779
+ ),
780
+ $meta
781
+ ),
782
+ $group->id,
783
+ 'profile_fields',
784
+ $action
785
+ );
786
+ }
787
+
788
+ public function callback_xprofile_group_after_save( $group ) {
789
+ global $wpdb;
790
+ // a bit hacky, due to inconsistency with BP action scheme, see callback_xprofile_field_after_save for correct behavior
791
+ $action = ( $group->id === $wpdb->insert_id ) ? 'created' : 'updated';
792
+ $this->field_group_action( $group, $action );
793
+ }
794
+
795
+ public function callback_xprofile_groups_deleted_group( $group ) {
796
+ $this->field_group_action( $group, 'deleted' );
797
+ }
798
+
799
+ private function bp_get_directory_pages() {
800
+ $bp = \buddypress();
801
+ $directory_pages = array();
802
+
803
+ // Loop through loaded components and collect directories
804
+ if ( is_array( $bp->loaded_components ) ) {
805
+ foreach ( $bp->loaded_components as $component_slug => $component_id ) {
806
+ // Only components that need directories should be listed here
807
+ if ( isset( $bp->{$component_id} ) && ! empty( $bp->{$component_id}->has_directory ) ) {
808
+ // component->name was introduced in BP 1.5, so we must provide a fallback
809
+ $directory_pages[ $component_id ] = ! empty( $bp->{ $component_id }->name ) ? $bp->{ $component_id }->name : ucwords( $component_id );
810
+ }
811
+ }
812
+ }
813
+
814
+ return $directory_pages;
815
+ }
816
+ }
connectors/class-connector-comments.php ADDED
@@ -0,0 +1,629 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace WP_Stream;
3
+
4
+ class Connector_Comments extends Connector {
5
+ /**
6
+ * Connector slug
7
+ *
8
+ * @var string
9
+ */
10
+ public $name = 'comments';
11
+
12
+ /**
13
+ * Actions registered for this connector
14
+ *
15
+ * @var array
16
+ */
17
+ public $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 $delete_post = 0;
38
+
39
+ /**
40
+ * Return translated connector label
41
+ *
42
+ * @return string Translated connector label
43
+ */
44
+ public function get_label() {
45
+ return esc_html__( 'Comments', 'stream' );
46
+ }
47
+
48
+ /**
49
+ * Return translated action labels
50
+ *
51
+ * @return array Action label translations
52
+ */
53
+ public function get_action_labels() {
54
+ return array(
55
+ 'created' => esc_html__( 'Created', 'stream' ),
56
+ 'edited' => esc_html__( 'Edited', 'stream' ),
57
+ 'replied' => esc_html__( 'Replied', 'stream' ),
58
+ 'approved' => esc_html__( 'Approved', 'stream' ),
59
+ 'unapproved' => esc_html__( 'Unapproved', 'stream' ),
60
+ 'trashed' => esc_html__( 'Trashed', 'stream' ),
61
+ 'untrashed' => esc_html__( 'Restored', 'stream' ),
62
+ 'spammed' => esc_html__( 'Marked as Spam', 'stream' ),
63
+ 'unspammed' => esc_html__( 'Unmarked as Spam', 'stream' ),
64
+ 'deleted' => esc_html__( 'Deleted', 'stream' ),
65
+ 'duplicate' => esc_html__( 'Duplicate', 'stream' ),
66
+ 'flood' => esc_html__( 'Throttled', 'stream' ),
67
+ );
68
+ }
69
+
70
+ /**
71
+ * Return translated context labels
72
+ *
73
+ * @return array Context label translations
74
+ */
75
+ public function get_context_labels() {
76
+ return array(
77
+ 'comments' => esc_html__( 'Comments', 'stream' ),
78
+ );
79
+ }
80
+
81
+ /**
82
+ * Return translated comment type labels
83
+ *
84
+ * @return array Comment type label translations
85
+ */
86
+ public function get_comment_type_labels() {
87
+ return apply_filters(
88
+ 'wp_stream_comments_comment_type_labels',
89
+ array(
90
+ 'comment' => esc_html__( 'Comment', 'stream' ),
91
+ 'trackback' => esc_html__( 'Trackback', 'stream' ),
92
+ 'pingback' => esc_html__( '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
+ *
102
+ * @return string The comment type label
103
+ */
104
+ public function get_comment_type_label( $comment_id ) {
105
+ $comment_type = get_comment_type( $comment_id );
106
+
107
+ if ( empty( $comment_type ) ) {
108
+ $comment_type = 'comment';
109
+ }
110
+
111
+ $comment_type_labels = $this->get_comment_type_labels();
112
+
113
+ $label = isset( $comment_type_labels[ $comment_type ] ) ? $comment_type_labels[ $comment_type ] : $comment_type;
114
+
115
+ return $label;
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 function action_links( $links, $record ) {
129
+ if ( $record->object_id ) {
130
+ if ( $comment = get_comment( $record->object_id ) ) {
131
+ $approve_nonce = wp_create_nonce( "approve-comment_$comment->comment_ID" );
132
+
133
+ $links[ esc_html__( 'Edit', 'stream' ) ] = admin_url( "comment.php?action=editcomment&c=$comment->comment_ID" );
134
+
135
+ if ( 1 === $comment->comment_approved ) {
136
+ $links[ esc_html__( '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[ esc_html__( '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
+ *
168
+ * @return int|string $output User ID or user display name
169
+ */
170
+ public function get_comment_author( $comment, $field = 'id' ) {
171
+ $comment = is_object( $comment ) ? $comment : get_comment( absint( $comment ) );
172
+
173
+ $req_name_email = get_option( 'require_name_email' );
174
+ $req_user_login = get_option( 'comment_registration' );
175
+
176
+ $user_id = 0;
177
+ $user_name = esc_html__( 'Guest', 'stream' );
178
+
179
+ $output = '';
180
+
181
+ if ( $req_name_email && isset( $comment->comment_author_email ) && isset( $comment->comment_author ) ) {
182
+ $user = get_user_by( 'email', $comment->comment_author_email );
183
+ $user_id = isset( $user->ID ) ? $user->ID : 0;
184
+ $user_name = isset( $user->display_name ) ? $user->display_name : $comment->comment_author;
185
+ }
186
+
187
+ if ( $req_user_login ) {
188
+ $user = wp_get_current_user();
189
+ $user_id = $user->ID;
190
+ $user_name = $user->display_name;
191
+ }
192
+
193
+ if ( 'id' === $field ) {
194
+ $output = $user_id;
195
+ } elseif ( 'name' === $field ) {
196
+ $output = $user_name;
197
+ }
198
+
199
+ return $output;
200
+ }
201
+
202
+ /**
203
+ * Tracks comment flood blocks
204
+ *
205
+ * @action comment_flood_trigger
206
+ *
207
+ * @param string $time_lastcomment
208
+ * @param string $time_newcomment
209
+ */
210
+ public function callback_comment_flood_trigger( $time_lastcomment, $time_newcomment ) {
211
+ $options = wp_stream_get_instance()->settings->options;
212
+ $flood_tracking = isset( $options['advanced_comment_flood_tracking'] ) ? $options['advanced_comment_flood_tracking'] : false;
213
+
214
+ if ( ! $flood_tracking ) {
215
+ return;
216
+ }
217
+
218
+ $req_user_login = get_option( 'comment_registration' );
219
+
220
+ if ( $req_user_login ) {
221
+ $user = wp_get_current_user();
222
+ $user_id = $user->ID;
223
+ $user_name = $user->display_name;
224
+ } else {
225
+ $user_name = esc_html__( 'a logged out user', 'stream' );
226
+ }
227
+
228
+ $this->log(
229
+ __( 'Comment flooding by %s detected and prevented', 'stream' ),
230
+ compact( 'user_name', 'user_id', 'time_lastcomment', 'time_newcomment' ),
231
+ null,
232
+ 'comments',
233
+ 'flood'
234
+ );
235
+ }
236
+
237
+ /**
238
+ * Tracks comment creation
239
+ *
240
+ * @action wp_insert_comment
241
+ *
242
+ * @param int $comment_id
243
+ * @param object $comment
244
+ */
245
+ public function callback_wp_insert_comment( $comment_id, $comment ) {
246
+ if ( in_array( $comment->comment_type, $this->get_ignored_comment_types() ) ) {
247
+ return;
248
+ }
249
+
250
+ $user_id = $this->get_comment_author( $comment, 'id' );
251
+ $user_name = $this->get_comment_author( $comment, 'name' );
252
+ $post_id = $comment->comment_post_ID;
253
+ $post_type = get_post_type( $post_id );
254
+ $post_title = ( $post = get_post( $post_id ) ) ? "\"$post->post_title\"" : esc_html__( 'a post', 'stream' );
255
+ $comment_status = ( 1 === $comment->comment_approved ) ? esc_html__( 'approved automatically', 'stream' ) : esc_html__( 'pending approval', 'stream' );
256
+ $is_spam = false;
257
+
258
+ // Auto-marked spam comments
259
+ $options = wp_stream_get_instance()->settings->options;
260
+ $ak_tracking = isset( $options['advanced_akismet_tracking'] ) ? $options['advanced_akismet_tracking'] : false;
261
+
262
+ if ( class_exists( 'Akismet' ) && $ak_tracking && \Akismet::matches_last_comment( $comment ) ) {
263
+ $ak_last_comment = \Akismet::get_last_comment();
264
+ if ( 'true' === $ak_last_comment['akismet_result'] ) {
265
+ $is_spam = true;
266
+ $comment_status = esc_html__( 'automatically marked as spam by Akismet', 'stream' );
267
+ }
268
+ }
269
+
270
+ $comment_type = mb_strtolower( $this->get_comment_type_label( $comment_id ) );
271
+
272
+ if ( $comment->comment_parent ) {
273
+ $parent_user_id = get_comment_author( $comment->comment_parent, 'id' );
274
+ $parent_user_name = get_comment_author( $comment->comment_parent, 'name' );
275
+
276
+ $this->log(
277
+ _x(
278
+ 'Reply to %1$s\'s %5$s by %2$s on %3$s %4$s',
279
+ "1: Parent comment's author, 2: Comment author, 3: Post title, 4: Comment status, 5: Comment type",
280
+ 'stream'
281
+ ),
282
+ compact( 'parent_user_name', 'user_name', 'post_title', 'comment_status', 'comment_type', 'post_id', 'parent_user_id' ),
283
+ $comment_id,
284
+ $post_type,
285
+ 'replied',
286
+ $user_id
287
+ );
288
+ } else {
289
+ $this->log(
290
+ _x(
291
+ 'New %4$s by %1$s on %2$s %3$s',
292
+ '1: Comment author, 2: Post title 3: Comment status, 4: Comment type',
293
+ 'stream'
294
+ ),
295
+ compact( 'user_name', 'post_title', 'comment_status', 'comment_type', 'post_id', 'is_spam' ),
296
+ $comment_id,
297
+ $post_type,
298
+ $is_spam ? 'spammed' : 'created',
299
+ $user_id
300
+ );
301
+ }
302
+ }
303
+
304
+ /**
305
+ * Tracks comment updates
306
+ *
307
+ * @action edit_comment
308
+ *
309
+ * @param int $comment_id
310
+ */
311
+ public function callback_edit_comment( $comment_id ) {
312
+ $comment = get_comment( $comment_id );
313
+
314
+ if ( in_array( $comment->comment_type, $this->get_ignored_comment_types() ) ) {
315
+ return;
316
+ }
317
+
318
+ $user_id = $this->get_comment_author( $comment, 'id' );
319
+ $user_name = $this->get_comment_author( $comment, 'name' );
320
+ $post_id = $comment->comment_post_ID;
321
+ $post_type = get_post_type( $post_id );
322
+ $post_title = ( $post = get_post( $post_id ) ) ? "\"$post->post_title\"" : esc_html__( 'a post', 'stream' );
323
+ $comment_type = mb_strtolower( $this->get_comment_type_label( $comment_id ) );
324
+
325
+ $this->log(
326
+ _x(
327
+ '%1$s\'s %3$s on %2$s edited',
328
+ '1: Comment author, 2: Post title, 3: Comment type',
329
+ 'stream'
330
+ ),
331
+ compact( 'user_name', 'post_title', 'comment_type', 'post_id', 'user_id' ),
332
+ $comment_id,
333
+ $post_type,
334
+ 'edited'
335
+ );
336
+ }
337
+
338
+ /**
339
+ * Catch the post ID during deletion
340
+ *
341
+ * @action before_delete_post
342
+ *
343
+ * @param int $post_id
344
+ */
345
+ public function callback_before_delete_post( $post_id ) {
346
+ if ( wp_is_post_revision( $post_id ) ) {
347
+ return;
348
+ }
349
+
350
+ $this->delete_post = $post_id;
351
+ }
352
+
353
+ /**
354
+ * Reset the post ID after deletion
355
+ *
356
+ * @action deleted_post
357
+ *
358
+ * @param int $post_id
359
+ */
360
+ public function callback_deleted_post( $post_id ) {
361
+ if ( wp_is_post_revision( $post_id ) ) {
362
+ return;
363
+ }
364
+
365
+ $this->delete_post = 0;
366
+ }
367
+
368
+ /**
369
+ * Tracks comment delete
370
+ *
371
+ * @action delete_comment
372
+ *
373
+ * @param int $comment_id
374
+ */
375
+ public function callback_delete_comment( $comment_id ) {
376
+ $comment = get_comment( $comment_id );
377
+
378
+ if ( in_array( $comment->comment_type, $this->get_ignored_comment_types() ) ) {
379
+ return;
380
+ }
381
+
382
+ $user_id = $this->get_comment_author( $comment, 'id' );
383
+ $user_name = $this->get_comment_author( $comment, 'name' );
384
+ $post_id = absint( $comment->comment_post_ID );
385
+ $post_type = get_post_type( $post_id );
386
+ $post_title = ( $post = get_post( $post_id ) ) ? "\"$post->post_title\"" : esc_html__( 'a post', 'stream' );
387
+ $comment_type = mb_strtolower( $this->get_comment_type_label( $comment_id ) );
388
+
389
+ if ( $this->delete_post === $post_id ) {
390
+ return;
391
+ }
392
+
393
+ $this->log(
394
+ _x(
395
+ '%1$s\'s %3$s on %2$s deleted permanently',
396
+ '1: Comment author, 2: Post title, 3: Comment type',
397
+ 'stream'
398
+ ),
399
+ compact( 'user_name', 'post_title', 'comment_type', 'post_id', 'user_id' ),
400
+ $comment_id,
401
+ $post_type,
402
+ 'deleted'
403
+ );
404
+ }
405
+
406
+ /**
407
+ * Tracks comment trashing
408
+ *
409
+ * @action trash_comment
410
+ *
411
+ * @param int $comment_id
412
+ */
413
+ public function callback_trash_comment( $comment_id ) {
414
+ $comment = get_comment( $comment_id );
415
+
416
+ if ( in_array( $comment->comment_type, $this->get_ignored_comment_types() ) ) {
417
+ return;
418
+ }
419
+
420
+ $user_id = $this->get_comment_author( $comment, 'id' );
421
+ $user_name = $this->get_comment_author( $comment, 'name' );
422
+ $post_id = $comment->comment_post_ID;
423
+ $post_type = get_post_type( $post_id );
424
+ $post_title = ( $post = get_post( $post_id ) ) ? "\"$post->post_title\"" : esc_html__( 'a post', 'stream' );
425
+ $comment_type = mb_strtolower( $this->get_comment_type_label( $comment_id ) );
426
+
427
+ $this->log(
428
+ _x(
429
+ '%1$s\'s %3$s on %2$s trashed',
430
+ '1: Comment author, 2: Post title, 3: Comment type',
431
+ 'stream'
432
+ ),
433
+ compact( 'user_name', 'post_title', 'comment_type', 'post_id', 'user_id' ),
434
+ $comment_id,
435
+ $post_type,
436
+ 'trashed'
437
+ );
438
+ }
439
+
440
+ /**
441
+ * Tracks comment trashing
442
+ *
443
+ * @action untrash_comment
444
+ *
445
+ * @param int $comment_id
446
+ */
447
+ public function callback_untrash_comment( $comment_id ) {
448
+ $comment = get_comment( $comment_id );
449
+
450
+ if ( in_array( $comment->comment_type, $this->get_ignored_comment_types() ) ) {
451
+ return;
452
+ }
453
+
454
+ $user_id = $this->get_comment_author( $comment, 'id' );
455
+ $user_name = $this->get_comment_author( $comment, 'name' );
456
+ $post_id = $comment->comment_post_ID;
457
+ $post_type = get_post_type( $post_id );
458
+ $post_title = ( $post = get_post( $post_id ) ) ? "\"$post->post_title\"" : esc_html__( 'a post', 'stream' );
459
+ $comment_type = mb_strtolower( $this->get_comment_type_label( $comment_id ) );
460
+
461
+ $this->log(
462
+ _x(
463
+ '%1$s\'s %3$s on %2$s restored',
464
+ '1: Comment author, 2: Post title, 3: Comment type',
465
+ 'stream'
466
+ ),
467
+ compact( 'user_name', 'post_title', 'comment_type', 'post_id', 'user_id' ),
468
+ $comment_id,
469
+ $post_type,
470
+ 'untrashed'
471
+ );
472
+ }
473
+
474
+ /**
475
+ * Tracks comment marking as spam
476
+ *
477
+ * @action spam_comment
478
+ *
479
+ * @param int $comment_id
480
+ */
481
+ public function callback_spam_comment( $comment_id ) {
482
+ $comment = get_comment( $comment_id );
483
+
484
+ if ( in_array( $comment->comment_type, $this->get_ignored_comment_types() ) ) {
485
+ return;
486
+ }
487
+
488
+ $user_id = $this->get_comment_author( $comment, 'id' );
489
+ $user_name = $this->get_comment_author( $comment, 'name' );
490
+ $post_id = $comment->comment_post_ID;
491
+ $post_type = get_post_type( $post_id );
492
+ $post_title = ( $post = get_post( $post_id ) ) ? "\"$post->post_title\"" : esc_html__( 'a post', 'stream' );
493
+ $comment_type = mb_strtolower( $this->get_comment_type_label( $comment_id ) );
494
+
495
+ $this->log(
496
+ _x(
497
+ '%1$s\'s %3$s on %2$s marked as spam',
498
+ '1: Comment author, 2: Post title, 3: Comment type',
499
+ 'stream'
500
+ ),
501
+ compact( 'user_name', 'post_title', 'comment_type', 'post_id', 'user_id' ),
502
+ $comment_id,
503
+ $post_type,
504
+ 'spammed'
505
+ );
506
+ }
507
+
508
+ /**
509
+ * Tracks comment unmarking as spam
510
+ *
511
+ * @action unspam_comment
512
+ *
513
+ * @param int $comment_id
514
+ */
515
+ public function callback_unspam_comment( $comment_id ) {
516
+ $comment = get_comment( $comment_id );
517
+
518
+ if ( in_array( $comment->comment_type, $this->get_ignored_comment_types() ) ) {
519
+ return;
520
+ }
521
+
522
+ $user_id = $this->get_comment_author( $comment, 'id' );
523
+ $user_name = $this->get_comment_author( $comment, 'name' );
524
+ $post_id = $comment->comment_post_ID;
525
+ $post_type = get_post_type( $post_id );
526
+ $post_title = ( $post = get_post( $post_id ) ) ? "\"$post->post_title\"" : esc_html__( 'a post', 'stream' );
527
+ $comment_type = mb_strtolower( $this->get_comment_type_label( $comment_id ) );
528
+
529
+ $this->log(
530
+ _x(
531
+ '%1$s\'s %3$s on %2$s unmarked as spam',
532
+ '1: Comment author, 2: Post title, 3: Comment type',
533
+ 'stream'
534
+ ),
535
+ compact( 'user_name', 'post_title', 'comment_type', 'post_id', 'user_id' ),
536
+ $comment_id,
537
+ $post_type,
538
+ 'unspammed'
539
+ );
540
+ }
541
+
542
+ /**
543
+ * Track comment status transition
544
+ *
545
+ * @action transition_comment_status
546
+ *
547
+ * @param string $new_status
548
+ * @param string $old_status
549
+ * @param object $comment
550
+ */
551
+ public function callback_transition_comment_status( $new_status, $old_status, $comment ) {
552
+ if ( in_array( $comment->comment_type, $this->get_ignored_comment_types() ) ) {
553
+ return;
554
+ }
555
+
556
+ if ( 'approved' !== $new_status && 'unapproved' !== $new_status || 'trash' === $old_status || 'spam' === $old_status ) {
557
+ return;
558
+ }
559
+
560
+ $user_id = $this->get_comment_author( $comment, 'id' );
561
+ $user_name = $this->get_comment_author( $comment, 'name' );
562
+ $post_id = $comment->comment_post_ID;
563
+ $post_type = get_post_type( $post_id );
564
+ $post_title = ( $post = get_post( $post_id ) ) ? "\"$post->post_title\"" : esc_html__( 'a post', 'stream' );
565
+ $comment_type = get_comment_type( $comment->comment_ID );
566
+
567
+ $this->log(
568
+ _x(
569
+ '%1$s\'s %3$s %2$s',
570
+ 'Comment status transition. 1: Comment author, 2: Post title, 3: Comment type',
571
+ 'stream'
572
+ ),
573
+ compact( 'user_name', 'new_status', 'comment_type', 'old_status', 'post_title', 'post_id', 'user_id' ),
574
+ $comment->comment_ID,
575
+ $post_type,
576
+ $new_status
577
+ );
578
+ }
579
+
580
+ /**
581
+ * Track attempts to add duplicate comments
582
+ *
583
+ * @action comment_duplicate_trigger
584
+ *
585
+ * @param array $comment_data
586
+ */
587
+ public function callback_comment_duplicate_trigger( $comment_data ) {
588
+ global $wpdb;
589
+ unset( $comment_data );
590
+
591
+ $comment_id = $wpdb->last_result[0]->comment_ID;
592
+ $comment = get_comment( $comment_id );
593
+
594
+ if ( in_array( $comment->comment_type, $this->get_ignored_comment_types() ) ) {
595
+ return;
596
+ }
597
+
598
+ $user_id = $this->get_comment_author( $comment, 'id' );
599
+ $user_name = $this->get_comment_author( $comment, 'name' );
600
+ $post_id = $comment->comment_post_ID;
601
+ $post_type = get_post_type( $post_id );
602
+ $post_title = ( $post = get_post( $post_id ) ) ? "\"$post->post_title\"" : esc_html__( 'a post', 'stream' );
603
+ $comment_type = mb_strtolower( $this->get_comment_type_label( $comment_id ) );
604
+
605
+ $this->log(
606
+ _x(
607
+ 'Duplicate %3$s by %1$s prevented on %2$s',
608
+ '1: Comment author, 2: Post title, 3: Comment type',
609
+ 'stream'
610
+ ),
611
+ compact( 'user_name', 'post_title', 'comment_type', 'post_id', 'user_id' ),
612
+ $comment_id,
613
+ $post_type,
614
+ 'duplicate'
615
+ );
616
+ }
617
+
618
+ /**
619
+ * Constructs list of ignored comment types for the comments connector
620
+ *
621
+ * @return array List of ignored comment types
622
+ */
623
+ public function get_ignored_comment_types() {
624
+ return apply_filters(
625
+ 'wp_stream_comments_exclude_comment_types',
626
+ array()
627
+ );
628
+ }
629
+ }
connectors/class-connector-edd.php ADDED
@@ -0,0 +1,492 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace WP_Stream;
3
+
4
+ class Connector_EDD extends Connector {
5
+
6
+ /**
7
+ * Connector slug
8
+ *
9
+ * @var string
10
+ */
11
+ public $name = 'edd';
12
+
13
+ /**
14
+ * Holds tracked plugin minimum version required
15
+ *
16
+ * @const string
17
+ */
18
+ const PLUGIN_MIN_VERSION = '1.8.8';
19
+
20
+ /**
21
+ * Actions registered for this connector
22
+ *
23
+ * @var array
24
+ */
25
+ public $actions = array(
26
+ 'update_option',
27
+ 'add_option',
28
+ 'delete_option',
29
+ 'update_site_option',
30
+ 'add_site_option',
31
+ 'delete_site_option',
32
+ 'edd_pre_update_discount_status',
33
+ 'edd_generate_pdf',
34
+ 'edd_earnings_export',
35
+ 'edd_payment_export',
36
+ 'edd_email_export',
37
+ 'edd_downloads_history_export',
38
+ 'edd_import_settings',
39
+ 'edd_export_settings',
40
+ 'add_user_meta',
41
+ 'update_user_meta',
42
+ 'delete_user_meta',
43
+ );
44
+
45
+ /**
46
+ * Tracked option keys
47
+ *
48
+ * @var array
49
+ */
50
+ public $options = array();
51
+
52
+ /**
53
+ * Tracking registered Settings, with overridden data
54
+ *
55
+ * @var array
56
+ */
57
+ public $options_override = array();
58
+
59
+ /**
60
+ * Tracking user meta updates related to this connector
61
+ *
62
+ * @var array
63
+ */
64
+ public $user_meta = array(
65
+ 'edd_user_public_key',
66
+ );
67
+
68
+ /**
69
+ * Flag status changes to not create duplicate entries
70
+ * @var bool
71
+ */
72
+ public $is_discount_status_change = false;
73
+
74
+ /**
75
+ * Flag status changes to not create duplicate entries
76
+ * @var bool
77
+ */
78
+ public $is_payment_status_change = false;
79
+
80
+ /**
81
+ * Check if plugin dependencies are satisfied and add an admin notice if not
82
+ *
83
+ * @return bool
84
+ */
85
+ public function is_dependency_satisfied() {
86
+ if ( class_exists( 'Easy_Digital_Downloads' ) && defined( 'EDD_VERSION' ) && version_compare( EDD_VERSION, self::PLUGIN_MIN_VERSION, '>=' ) ) {
87
+ return true;
88
+ }
89
+
90
+ return false;
91
+ }
92
+
93
+ /**
94
+ * Return translated connector label
95
+ *
96
+ * @return string Translated connector label
97
+ */
98
+ public function get_label() {
99
+ return esc_html_x( 'Easy Digital Downloads', 'edd', 'stream' );
100
+ }
101
+
102
+ /**
103
+ * Return translated action labels
104
+ *
105
+ * @return array Action label translations
106
+ */
107
+ public function get_action_labels() {
108
+ return array(
109
+ 'created' => esc_html_x( 'Created', 'edd', 'stream' ),
110
+ 'updated' => esc_html_x( 'Updated', 'edd', 'stream' ),
111
+ 'added' => esc_html_x( 'Added', 'edd', 'stream' ),
112
+ 'deleted' => esc_html_x( 'Deleted', 'edd', 'stream' ),
113
+ 'trashed' => esc_html_x( 'Trashed', 'edd', 'stream' ),
114
+ 'untrashed' => esc_html_x( 'Restored', 'edd', 'stream' ),
115
+ 'generated' => esc_html_x( 'Generated', 'edd', 'stream' ),
116
+ 'imported' => esc_html_x( 'Imported', 'edd', 'stream' ),
117
+ 'exported' => esc_html_x( 'Exported', 'edd', 'stream' ),
118
+ 'revoked' => esc_html_x( 'Revoked', 'edd', 'stream' ),
119
+ );
120
+ }
121
+
122
+ /**
123
+ * Return translated context labels
124
+ *
125
+ * @return array Context label translations
126
+ */
127
+ public function get_context_labels() {
128
+ return array(
129
+ 'downloads' => esc_html_x( 'Downloads', 'edd', 'stream' ),
130
+ 'download_category' => esc_html_x( 'Categories', 'edd', 'stream' ),
131
+ 'download_tag' => esc_html_x( 'Tags', 'edd', 'stream' ),
132
+ 'discounts' => esc_html_x( 'Discounts', 'edd', 'stream' ),
133
+ 'reports' => esc_html_x( 'Reports', 'edd', 'stream' ),
134
+ 'api_keys' => esc_html_x( 'API Keys', 'edd', 'stream' ),
135
+ //'payments' => esc_html_x( 'Payments', 'edd', 'stream' ),
136
+ );
137
+ }
138
+
139
+ /**
140
+ * Add action links to Stream drop row in admin list screen
141
+ *
142
+ * @filter wp_stream_action_links_{connector}
143
+ *
144
+ * @param array $links Previous links registered
145
+ * @param object $record Stream record
146
+ *
147
+ * @return array Action links
148
+ */
149
+ public function action_links( $links, $record ) {
150
+ if ( in_array( $record->context, array( 'downloads' ) ) ) {
151
+ $posts_connector = new Connector_Posts();
152
+ $links = $posts_connector->action_links( $links, $record );
153
+ } elseif ( in_array( $record->context, array( 'discounts' ) ) ) {
154
+ $post_type_label = get_post_type_labels( get_post_type_object( 'edd_discount' ) )->singular_name;
155
+ $base = admin_url( 'edit.php?post_type=download&page=edd-discounts' );
156
+
157
+ $links[ sprintf( esc_html__( 'Edit %s', 'stream' ), $post_type_label ) ] = add_query_arg(
158
+ array(
159
+ 'edd-action' => 'edit_discount',
160
+ 'discount' => $record->object_id,
161
+ ),
162
+ $base
163
+ );
164
+
165
+ if ( 'active' === get_post( $record->object_id )->post_status ) {
166
+ $links[ sprintf( esc_html__( 'Deactivate %s', 'stream' ), $post_type_label ) ] = add_query_arg(
167
+ array(
168
+ 'edd-action' => 'deactivate_discount',
169
+ 'discount' => $record->object_id,
170
+ ),
171
+ $base
172
+ );
173
+ } else {
174
+ $links[ sprintf( esc_html__( 'Activate %s', 'stream' ), $post_type_label ) ] = add_query_arg(
175
+ array(
176
+ 'edd-action' => 'activate_discount',
177
+ 'discount' => $record->object_id,
178
+ ),
179
+ $base
180
+ );
181
+ }
182
+ } elseif ( in_array( $record->context, array( 'download_category', 'download_tag' ) ) ) {
183
+ $tax_label = get_taxonomy_labels( get_taxonomy( $record->context ) )->singular_name;
184
+ $links[ sprintf( esc_html__( 'Edit %s', 'stream' ), $tax_label ) ] = get_edit_term_link( $record->object_id, $record->get_meta( 'taxonomy', true ) );
185
+ } elseif ( 'api_keys' === $record->context ) {
186
+ $user = new \WP_User( $record->object_id );
187
+
188
+ if ( apply_filters( 'edd_api_log_requests', true ) ) {
189
+ $links[ esc_html__( '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' );
190
+ }
191
+
192
+ $links[ esc_html__( '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' );
193
+ $links[ esc_html__( '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' );
194
+ }
195
+
196
+ return $links;
197
+ }
198
+
199
+ public function register() {
200
+ parent::register();
201
+
202
+ add_filter( 'wp_stream_log_data', array( $this, 'log_override' ) );
203
+
204
+ $this->options = array(
205
+ 'edd_settings' => null,
206
+ );
207
+ }
208
+
209
+ public function callback_update_option( $option, $old, $new ) {
210
+ $this->check( $option, $old, $new );
211
+ }
212
+
213
+ public function callback_add_option( $option, $val ) {
214
+ $this->check( $option, null, $val );
215
+ }
216
+
217
+ public function callback_delete_option( $option ) {
218
+ $this->check( $option, null, null );
219
+ }
220
+
221
+ public function callback_update_site_option( $option, $old, $new ) {
222
+ $this->check( $option, $old, $new );
223
+ }
224
+
225
+ public function callback_add_site_option( $option, $val ) {
226
+ $this->check( $option, null, $val );
227
+ }
228
+
229
+ public function callback_delete_site_option( $option ) {
230
+ $this->check( $option, null, null );
231
+ }
232
+
233
+ public function check( $option, $old_value, $new_value ) {
234
+ if ( ! array_key_exists( $option, $this->options ) ) {
235
+ return;
236
+ }
237
+
238
+ $replacement = str_replace( '-', '_', $option );
239
+
240
+ if ( method_exists( $this, 'check_' . $replacement ) ) {
241
+ call_user_func( array( $this, 'check_' . $replacement ), $old_value, $new_value );
242
+ } else {
243
+ $data = $this->options[ $option ];
244
+ $option_title = $data['label'];
245
+ $context = isset( $data['context'] ) ? $data['context'] : 'settings';
246
+
247
+ $this->log(
248
+ __( '"%s" setting updated', 'stream' ),
249
+ compact( 'option_title', 'option', 'old_value', 'new_value' ),
250
+ null,
251
+ $context,
252
+ isset( $data['action'] ) ? $data['action'] : 'updated'
253
+ );
254
+ }
255
+ }
256
+
257
+ public function check_edd_settings( $old_value, $new_value ) {
258
+ $options = array();
259
+
260
+ if ( ! is_array( $old_value ) || ! is_array( $new_value ) ) {
261
+ return;
262
+ }
263
+
264
+ foreach ( $this->get_changed_keys( $old_value, $new_value, 0 ) as $field_key => $field_value ) {
265
+ $options[ $field_key ] = $field_value;
266
+ }
267
+
268
+ //TODO: Check this exists first
269
+ $settings = \edd_get_registered_settings();
270
+
271
+ foreach ( $options as $option => $option_value ) {
272
+ $field = null;
273
+
274
+ if ( 'banned_email' === $option ) {
275
+ $field = array(
276
+ 'name' => esc_html_x( 'Banned emails', 'edd', 'stream' ),
277
+ );
278
+ $tab = 'general';
279
+ } else {
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
+ $this->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 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'] = $this->name;
324
+ } elseif ( 'posts' === $data['connector'] && 'edd_discount' === $data['context'] ) {
325
+ // Discount posts operations
326
+ if ( $this->is_discount_status_change ) {
327
+ return false;
328
+ }
329
+
330
+ if ( 'deleted' === $data['action'] ) {
331
+ $data['message'] = esc_html__( '"%1s" discount deleted', 'stream' );
332
+ }
333
+
334
+ $data['context'] = 'discounts';
335
+ $data['connector'] = $this->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'] = $this->name;
347
+ } elseif ( 'taxonomies' === $data['connector'] && 'download_tag' === $data['contexts'] ) {
348
+ $data['connector'] = $this->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 function callback_edd_pre_update_discount_status( $code_id, $new_status ) {
359
+ $this->is_discount_status_change = true;
360
+
361
+ $this->log(
362
+ sprintf(
363
+ __( '"%1$s" discount %2$s', 'stream' ),
364
+ get_post( $code_id )->post_title,
365
+ 'active' === $new_status ? esc_html__( 'activated', 'stream' ) : esc_html__( '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 function callback_edd_generate_pdf() {
378
+ $this->report_generated( 'pdf' );
379
+ }
380
+ public function callback_edd_earnings_export() {
381
+ $this->report_generated( 'earnings' );
382
+ }
383
+ public function callback_edd_payment_export() {
384
+ $this->report_generated( 'payments' );
385
+ }
386
+ public function callback_edd_email_export() {
387
+ $this->report_generated( 'emails' );
388
+ }
389
+ public function callback_edd_downloads_history_export() {
390
+ $this->report_generated( 'download-history' );
391
+ }
392
+
393
+ private function report_generated( $type ) {
394
+ if ( 'pdf' === $type ) {
395
+ $label = esc_html__( 'Sales and Earnings', 'stream' );
396
+ } elseif ( 'earnings' ) {
397
+ $label = esc_html__( 'Earnings', 'stream' );
398
+ } elseif ( 'payments' ) {
399
+ $label = esc_html__( 'Payments', 'stream' );
400
+ } elseif ( 'emails' ) {
401
+ $label = esc_html__( 'Emails', 'stream' );
402
+ } elseif ( 'download-history' ) {
403
+ $label = esc_html__( 'Download History', 'stream' );
404
+ }
405
+
406
+ $this->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 function callback_edd_export_settings() {
421
+ $this->log(
422
+ __( 'Exported Settings', 'stream' ),
423
+ array(),
424
+ null,
425
+ 'settings',
426
+ 'exported'
427
+ );
428
+ }
429
+
430
+ public function callback_edd_import_settings() {
431
+ $this->log(
432
+ __( 'Imported Settings', 'stream' ),
433
+ array(),
434
+ null,
435
+ 'settings',
436
+ 'imported'
437
+ );
438
+ }
439
+
440
+ public function callback_update_user_meta( $meta_id, $object_id, $meta_key, $_meta_value ) {
441
+ unset( $meta_id );
442
+ $this->meta( $object_id, $meta_key, $_meta_value );
443
+ }
444
+
445
+ public function callback_add_user_meta( $object_id, $meta_key, $_meta_value ) {
446
+ $this->meta( $object_id, $meta_key, $_meta_value, true );
447
+ }
448
+
449
+ public function callback_delete_user_meta( $meta_id, $object_id, $meta_key, $_meta_value ) {
450
+ $this->meta( $object_id, $meta_key, null );
451
+ }
452
+
453
+ public function meta( $object_id, $key, $value, $is_add = false ) {
454
+ if ( ! in_array( $key, $this->user_meta ) ) {
455
+ return false;
456
+ }
457
+
458
+ $key = str_replace( '-', '_', $key );
459
+
460
+ if ( ! method_exists( $this, 'meta_' . $key ) ) {
461
+ return false;
462
+ }
463
+
464
+ return call_user_func( array( $this, 'meta_' . $key ), $object_id, $value, $is_add );
465
+ }
466
+
467
+ private function meta_edd_user_public_key( $user_id, $value, $is_add = false ) {
468
+ if ( is_null( $value ) ) {
469
+ $action = 'revoked';
470
+ $action_title = esc_html__( 'revoked', 'stream' );
471
+ } elseif ( $is_add ) {
472
+ $action = 'created';
473
+ $action_title = esc_html__( 'created', 'stream' );
474
+ } else {
475
+ $action = 'updated';
476
+ $action_title = esc_html__( 'updated', 'stream' );
477
+ }
478
+
479
+ $this->log(
480
+ sprintf(
481
+ __( 'User API Key %s', 'stream' ),
482
+ $action_title
483
+ ),
484
+ array(
485
+ 'meta_value' => $value,
486
+ ),
487
+ $user_id,
488
+ 'api_keys',
489
+ $action
490
+ );
491
+ }
492
+ }
connectors/class-connector-editor.php ADDED
@@ -0,0 +1,323 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace WP_Stream;
3
+
4
+ class Connector_Editor extends Connector {
5
+ /**
6
+ * Connector slug
7
+ *
8
+ * @var string
9
+ */
10
+ public $name = 'editor';
11
+
12
+ /**
13
+ * Actions registered for this connector
14
+ *
15
+ * @var array
16
+ */
17
+ public $actions = array();
18
+
19
+ /**
20
+ * Actions registered for this connector
21
+ *
22
+ * @var array
23
+ */
24
+ private $edited_file = array();
25
+
26
+ /**
27
+ * Register all context hooks
28
+ *
29
+ * @return void
30
+ */
31
+ public function register() {
32
+ parent::register();
33
+ add_action( 'load-theme-editor.php', array( $this, 'get_edition_data' ) );
34
+ add_action( 'load-plugin-editor.php', array( $this, 'get_edition_data' ) );
35
+ add_filter( 'wp_redirect', array( $this, 'log_changes' ) );
36
+ }
37
+
38
+ /**
39
+ * Return translated connector label
40
+ *
41
+ * @return string Translated connector label
42
+ */
43
+ public function get_label() {
44
+ return esc_html__( 'Editor', 'stream' );
45
+ }
46
+
47
+ /**
48
+ * Return translated action labels
49
+ *
50
+ * @return array Action label translations
51
+ */
52
+ public function get_action_labels() {
53
+ return array(
54
+ 'updated' => esc_html__( 'Updated', 'stream' ),
55
+ );
56
+ }
57
+
58
+ /**
59
+ * Return translated context labels
60
+ *
61
+ * @return array Context label translations
62
+ */
63
+ public 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' => esc_html__( 'Themes', 'stream' ),
73
+ 'plugins' => esc_html__( '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 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 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 function action_links( $links, $record ) {
129
+ if ( current_user_can( 'edit_theme_options' ) ) {
130
+ $file_name = $record->get_meta( 'file', true );
131
+ $file_path = $record->get_meta( 'file_path', true );
132
+
133
+ if ( ! empty( $file_name ) && ! empty( $file_path ) ) {
134
+ $theme_slug = $record->get_meta( 'theme_slug', true );
135
+ $plugin_slug = $record->get_meta( '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[ esc_html__( '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[ esc_html__( '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[ esc_html__( '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
+ */
177
+ public function get_edition_data() {
178
+ if (
179
+ (
180
+ isset( $_SERVER['REQUEST_METHOD'] )
181
+ &&
182
+ 'POST' !== esc_attr( $_SERVER['REQUEST_METHOD'] )
183
+ )
184
+ ||
185
+ 'update' !== wp_stream_filter_input( INPUT_POST, 'action' )
186
+ ) {
187
+ return;
188
+ }
189
+
190
+ if ( $slug = wp_stream_filter_input( INPUT_POST, 'theme' ) ) {
191
+ $this->edited_file = $this->get_theme_data( $slug );
192
+ }
193
+
194
+ if ( $slug = wp_stream_filter_input( INPUT_POST, 'plugin' ) ) {
195
+ $this->edited_file = $this->get_plugin_data( $slug );
196
+ }
197
+ }
198
+
199
+ /**
200
+ * Retrieve theme data needed for the log message
201
+ *
202
+ * @param string $slug The theme slug (e.g. twentyfourteen)
203
+ *
204
+ * @return mixed $output Compacted variables
205
+ */
206
+ public function get_theme_data( $slug ) {
207
+ $theme = wp_get_theme( $slug );
208
+
209
+ if ( ! $theme->exists() || ( $theme->errors() && 'theme_no_stylesheet' === $theme->errors()->get_error_code() ) ) {
210
+ return false;
211
+ }
212
+
213
+ $allowed_files = $theme->get_files( 'php', 1 );
214
+ $style_files = $theme->get_files( 'css' );
215
+ $file = wp_stream_filter_input( INPUT_POST, 'file' );
216
+
217
+ $allowed_files['style.css'] = $style_files['style.css'];
218
+
219
+ if ( empty( $file ) ) {
220
+ $file_name = 'style.css';
221
+ $file_path = $allowed_files['style.css'];
222
+ } else {
223
+ $file_name = $file;
224
+ $file_path = sprintf( '%s/%s', $theme->get_stylesheet_directory(), $file_name );
225
+ }
226
+
227
+ //TODO: phpcs fix
228
+ $file_contents_before = file_get_contents( $file_path );
229
+
230
+ $name = $theme->get( 'Name' );
231
+
232
+ $output = compact(
233
+ 'file_name',
234
+ 'file_path',
235
+ 'file_contents_before',
236
+ 'slug',
237
+ 'name'
238
+ );
239
+
240
+ return $output;
241
+ }
242
+
243
+ /**
244
+ * Retrieve plugin data needed for the log message
245
+ *
246
+ * @param string $slug The plugin file base name (e.g. akismet/akismet.php)
247
+ * @return mixed $output Compacted variables
248
+ */
249
+ public function get_plugin_data( $slug ) {
250
+ $base = null;
251
+ $name = null;
252
+ $slug = current( explode( '/', $slug ) );
253
+ $file_name = wp_stream_filter_input( INPUT_POST, 'file' );
254
+ $file_path = WP_PLUGIN_DIR . '/' . $file_name;
255
+
256
+ //TODO: phpcs fix
257
+ $file_contents_before = file_get_contents( $file_path );
258
+
259
+ $plugins = get_plugins();
260
+
261
+ foreach ( $plugins as $key => $plugin_data ) {
262
+ if ( 0 === strpos( $key, $slug ) ) {
263
+ $base = $key;
264
+ $name = $plugin_data['Name'];
265
+ break;
266
+ }
267
+ }
268
+
269
+ $file_name = str_ireplace( trailingslashit( $slug ), '', $file_name );
270
+ $slug = ! empty( $base ) ? $base : $slug;
271
+
272
+ $output = compact(
273
+ 'file_name',
274
+ 'file_path',
275
+ 'file_contents_before',
276
+ 'slug',
277
+ 'name'
278
+ );
279
+
280
+ return $output;
281
+ }
282
+
283
+ /**
284
+ * @filter wp_redirect
285
+ */
286
+ public function log_changes( $location ) {
287
+ if ( ! empty( $this->edited_file ) ) {
288
+ // TODO: phpcs fix
289
+ if ( file_get_contents( $this->edited_file['file_path'] ) !== $this->edited_file['file_contents_before'] ) {
290
+ $context = $this->get_context( $location );
291
+
292
+ switch ( $context ) {
293
+ case 'themes':
294
+ $name_key = 'theme_name';
295
+ $slug_key = 'theme_slug';
296
+ break;
297
+ case 'plugins':
298
+ $name_key = 'plugin_name';
299
+ $slug_key = 'plugin_slug';
300
+ break;
301
+ default:
302
+ $name_key = 'name';
303
+ $slug_key = 'slug';
304
+ }
305
+
306
+ $this->log(
307
+ $this->get_message(),
308
+ array(
309
+ 'file' => (string) $this->edited_file['file_name'],
310
+ $name_key => (string) $this->edited_file['name'],
311
+ $slug_key => (string) $this->edited_file['slug'],
312
+ 'file_path' => (string) $this->edited_file['file_path'],
313
+ ),
314
+ null,
315
+ $context,
316
+ 'updated'
317
+ );
318
+ }
319
+ }
320
+
321
+ return $location;
322
+ }
323
+ }
connectors/class-connector-gravityforms.php ADDED
@@ -0,0 +1,728 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace WP_Stream;
3
+
4
+ class Connector_GravityForms extends Connector {
5
+ /**
6
+ * Connector slug
7
+ *
8
+ * @var string
9
+ */
10
+ public $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 $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 $options = array();
61
+
62
+ /**
63
+ * Tracking registered Settings, with overridden data
64
+ *
65
+ * @var array
66
+ */
67
+ public $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 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 function get_label() {
88
+ return esc_html_x( 'Gravity Forms', 'gravityforms', 'stream' );
89
+ }
90
+
91
+ /**
92
+ * Return translated action labels
93
+ *
94
+ * @return array Action label translations
95
+ */
96
+ public function get_action_labels() {
97
+ return array(
98
+ 'created' => esc_html_x( 'Created', 'gravityforms', 'stream' ),
99
+ 'updated' => esc_html_x( 'Updated', 'gravityforms', 'stream' ),
100
+ 'exported' => esc_html_x( 'Exported', 'gravityforms', 'stream' ),
101
+ 'imported' => esc_html_x( 'Imported', 'gravityforms', 'stream' ),
102
+ 'added' => esc_html_x( 'Added', 'gravityforms', 'stream' ),
103
+ 'deleted' => esc_html_x( 'Deleted', 'gravityforms', 'stream' ),
104
+ 'trashed' => esc_html_x( 'Trashed', 'gravityforms', 'stream' ),
105
+ 'untrashed' => esc_html_x( 'Restored', 'gravityforms', 'stream' ),
106
+ 'duplicated' => esc_html_x( 'Duplicated', 'gravityforms', 'stream' ),
107
+ );
108
+ }
109
+
110
+ /**
111
+ * Return translated context labels
112
+ *
113
+ * @return array Context label translations
114
+ */
115
+ public function get_context_labels() {
116
+ return array(
117
+ 'forms' => esc_html_x( 'Forms', 'gravityforms', 'stream' ),
118
+ 'settings' => esc_html_x( 'Settings', 'gravityforms', 'stream' ),
119
+ 'export' => esc_html_x( 'Import/Export', 'gravityforms', 'stream' ),
120
+ 'entries' => esc_html_x( 'Entries', 'gravityforms', 'stream' ),
121
+ 'notes' => esc_html_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 function action_links( $links, $record ) {
136
+ if ( 'forms' === $record->context ) {
137
+ $links[ esc_html__( '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[ esc_html__( 'View', 'stream' ) ] = add_query_arg(
146
+ array(
147
+ 'page' => 'gf_entries',
148
+ 'view' => 'entry',
149
+ 'lid' => $record->object_id,
150
+ 'id' => $record->get_meta( 'form_id', true ),
151
+ ),
152
+ admin_url( 'admin.php' )
153
+ );
154
+ } elseif ( 'notes' === $record->context ) {
155
+ $links[ esc_html__( 'View', 'stream' ) ] = add_query_arg(
156
+ array(
157
+ 'page' => 'gf_entries',
158
+ 'view' => 'entry',
159
+ 'lid' => $record->get_meta( 'lead_id', true ),
160
+ 'id' => $record->get_meta( 'form_id', true ),
161
+ ),
162
+ admin_url( 'admin.php' )
163
+ );
164
+ } elseif ( 'settings' === $record->context ) {
165
+ $links[ esc_html__( '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 function register() {
177
+ parent::register();
178
+
179
+ $this->options = array(
180
+ 'rg_gforms_disable_css' => array(
181
+ 'label' => esc_html_x( 'Output CSS', 'gravityforms', 'stream' ),
182
+ ),
183
+ 'rg_gforms_enable_html5' => array(
184
+ 'label' => esc_html_x( 'Output HTML5', 'gravityforms', 'stream' ),
185
+ ),
186
+ 'gform_enable_noconflict' => array(
187
+ 'label' => esc_html_x( 'No-Conflict Mode', 'gravityforms', 'stream' ),
188
+ ),
189
+ 'rg_gforms_currency' => array(
190
+ 'label' => esc_html_x( 'Currency', 'gravityforms', 'stream' ),
191
+ ),
192
+ 'rg_gforms_captcha_public_key' => array(
193
+ 'label' => esc_html_x( 'reCAPTCHA Public Key', 'gravityforms', 'stream' ),
194
+ ),
195
+ 'rg_gforms_captcha_private_key' => array(
196
+ 'label' => esc_html_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 array $form
206
+ * @param bool $is_new
207
+ */
208
+ public function callback_gform_after_save_form( $form, $is_new ) {
209
+ $title = $form['title'];
210
+ $id = $form['id'];
211
+
212
+ $this->log(
213
+ sprintf(
214
+ __( '"%1$s" form %2$s', 'stream' ),
215
+ $title,
216
+ $is_new ? esc_html__( 'created', 'stream' ) : esc_html__( '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 array $confirmation
233
+ * @param array $form
234
+ * @param bool $is_new
235
+ *
236
+ * @return array
237
+ */
238
+ public function callback_gform_pre_confirmation_save( $confirmation, $form, $is_new = true ) {
239
+ if ( ! isset( $is_new ) ) {
240
+ $is_new = false;
241
+ }
242
+
243
+ $this->log(
244
+ sprintf(
245
+ __( '"%1$s" confirmation %2$s for "%3$s"', 'stream' ),
246
+ $confirmation['name'],
247
+ $is_new ? esc_html__( 'created', 'stream' ) : esc_html__( '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 array $notification
266
+ * @param array $form
267
+ * @param bool $is_new
268
+ *
269
+ * @return array
270
+ */
271
+ public function callback_gform_pre_notification_save( $notification, $form, $is_new = true ) {
272
+ if ( ! isset( $is_new ) ) {
273
+ $is_new = false;
274
+ }
275
+
276
+ $this->log(
277
+ sprintf(
278
+ __( '"%1$s" notification %2$s for "%3$s"', 'stream' ),
279
+ $notification['name'],
280
+ $is_new ? esc_html__( 'created', 'stream' ) : esc_html__( '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 array $notification
299
+ * @param array $form
300
+ */
301
+ public function callback_gform_notification_delete( $notification, $form ) {
302
+ $this->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 array $confirmation
322
+ * @param array $form
323
+ */
324
+ public function callback_gform_confirmation_delete( $confirmation, $form ) {
325
+ $this->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 array $confirmation
345
+ * @param array $form
346
+ * @param bool $is_active
347
+ */
348
+ public function callback_gform_confirmation_status( $confirmation, $form, $is_active ) {
349
+ $this->log(
350
+ sprintf(
351
+ __( '"%1$s" confirmation %2$s from "%3$s"', 'stream' ),
352
+ $confirmation['name'],
353
+ $is_active ? esc_html__( 'activated', 'stream' ) : esc_html__( '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 integer $id
371
+ */
372
+ public function callback_gform_form_reset_views( $id ) {
373
+ $form = $this->get_form( $id );
374
+
375
+ $this->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 array $notification
391
+ * @param array $form
392
+ * @param bool $is_active
393
+ */
394
+ public function callback_gform_notification_status( $notification, $form, $is_active ) {
395
+ $this->log(
396
+ sprintf(
397
+ __( '"%1$s" notification %2$s from "%3$s"', 'stream' ),
398
+ $notification['name'],
399
+ $is_active ? esc_html__( 'activated', 'stream' ) : esc_html__( '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 integer $id
417
+ * @param string $action
418
+ */
419
+ public function callback_gform_form_status_change( $id, $action ) {
420
+ $form = $this->get_form( $id );
421
+ $actions = array(
422
+ 'activated' => esc_html__( 'Activated', 'stream' ),
423
+ 'deactivated' => esc_html__( 'Deactivated', 'stream' ),
424
+ 'trashed' => esc_html__( 'Trashed', 'stream' ),
425
+ 'untrashed' => esc_html__( 'Restored', 'stream' ),
426
+ );
427
+
428
+ $this->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 function callback_update_option( $option, $old, $new ) {
445
+ $this->check( $option, $old, $new );
446
+ }
447
+
448
+ public function callback_add_option( $option, $val ) {
449
+ $this->check( $option, null, $val );
450
+ }
451
+
452
+ public function callback_delete_option( $option ) {
453
+ $this->check( $option, null, null );
454
+ }
455
+
456
+ public function callback_update_site_option( $option, $old, $new ) {
457
+ $this->check( $option, $old, $new );
458
+ }
459
+
460
+ public function callback_add_site_option( $option, $val ) {
461
+ $this->check( $option, null, $val );
462
+ }
463
+
464
+ public function callback_delete_site_option( $option ) {
465
+ $this->check( $option, null, null );
466
+ }
467
+
468
+ public function check( $option, $old_value, $new_value ) {
469
+ if ( ! array_key_exists( $option, $this->options ) ) {
470
+ return;
471
+ }
472
+
473
+ if ( is_null( $this->options[ $option ] ) ) {
474
+ call_user_func( array( $this, 'check_' . str_replace( '-', '_', $option ) ), $old_value, $new_value );
475
+ } else {
476
+ $data = $this->options[ $option ];
477
+ $option_title = $data['label'];
478
+ $context = isset( $data['context'] ) ? $data['context'] : 'settings';
479
+
480
+ $this->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 function check_rg_gforms_key( $old_value, $new_value ) {
491
+ $is_update = ( $new_value && strlen( $new_value ) );
492
+ $option = 'rg_gforms_key';
493
+
494
+ $this->log(
495
+ sprintf(
496
+ __( 'Gravity Forms license key %s', 'stream' ),
497
+ $is_update ? esc_html__( 'updated', 'stream' ) : esc_html__( 'deleted', 'stream' )
498
+ ),
499
+ compact( 'option', 'old_value', 'new_value' ),
500
+ null,
501
+ 'settings',
502
+ $is_update ? 'updated' : 'deleted'
503
+ );
504
+ }
505
+
506
+ public function callback_gform_export_separator( $dummy, $form_id ) {
507
+ $form = $this->get_form( $form_id );
508
+
509
+ $this->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 function callback_gform_import_form_xml_options( $dummy ) {
524
+ $this->log(
525
+ __( 'Import process started', 'stream' ),
526
+ array(),
527
+ null,
528
+ 'export',
529
+ 'imported'
530
+ );
531
+
532
+ return $dummy;
533
+ }
534
+
535
+ public function callback_gform_export_options( $dummy, $forms ) {
536
+ $ids = wp_list_pluck( $forms, 'id' );
537
+ $titles = wp_list_pluck( $forms, 'title' );
538
+
539
+ $this->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 function callback_gform_before_delete_form( $id ) {
555
+ $form = $this->get_form( $id );
556
+
557
+ $this->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 function callback_gform_form_duplicate( $id, $new_id ) {
570
+ $form = $this->get_form( $id );
571
+ $new = $this->get_form( $new_id );
572
+
573
+ $this->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 function callback_gform_delete_lead( $lead_id ) {
588
+ $lead = \GFFormsModel::get_lead( $lead_id );
589
+ $form = $this->get_form( $lead['form_id'] );
590
+
591
+ $this->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 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 = $this->get_form( $lead['form_id'] );
607
+
608
+ $this->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 function callback_gform_delete_note( $note_id, $lead_id ) {
623
+ $lead = \GFFormsModel::get_lead( $lead_id );
624
+ $form = $this->get_form( $lead['form_id'] );
625
+
626
+ $this->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 function callback_gform_update_status( $lead_id, $status, $prev = '' ) {
641
+ $lead = \GFFormsModel::get_lead( $lead_id );
642
+ $form = $this->get_form( $lead['form_id'] );
643
+
644
+ if ( 'active' === $status && 'trash' === $prev ) {
645
+ $status = 'restore';
646
+ }
647
+
648
+ $actions = array(
649
+ 'active' => esc_html__( 'activated', 'stream' ),
650
+ 'spam' => esc_html__( 'marked as spam', 'stream' ),
651
+ 'trash' => esc_html__( 'trashed', 'stream' ),
652
+ 'restore' => esc_html__( 'restored', 'stream' ),
653
+ );
654
+
655
+ if ( ! isset( $actions[ $status ] ) ) {
656
+ return;
657
+ }
658
+
659
+ $this->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 function callback_gform_update_is_read( $lead_id, $status ) {
680
+ $lead = \GFFormsModel::get_lead( $lead_id );
681
+ $form = $this->get_form( $lead['form_id'] );
682
+
683
+ $this->log(
684
+ sprintf(
685
+ __( 'Lead #%1$d marked as %2$s on "%3$s" form', 'stream' ),
686
+ $lead_id,
687
+ $status ? esc_html__( 'read', 'stream' ) : esc_html__( '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 function callback_gform_update_is_starred( $lead_id, $status ) {
703
+ $lead = \GFFormsModel::get_lead( $lead_id );
704
+ $form = $this->get_form( $lead['form_id'] );
705
+
706
+ $this->log(
707
+ sprintf(
708
+ __( 'Lead #%1$d %2$s on "%3$s" form', 'stream' ),
709
+ $lead_id,
710
+ $status ? esc_html__( 'starred', 'stream' ) : esc_html__( '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 function get_form( $form_id ) {
726
+ return reset( \GFFormsModel::get_forms_by_id( $form_id ) );
727
+ }
728
+ }
connectors/class-connector-installer.php ADDED
@@ -0,0 +1,403 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace WP_Stream;
3
+
4
+ class Connector_Installer extends Connector {
5
+
6
+ /**
7
+ * Connector slug
8
+ *
9
+ * @var string
10
+ */
11
+ public $name = 'installer';
12
+
13
+ /**
14
+ * Actions registered for this connector
15
+ *
16
+ * @var array
17
+ */
18
+ public $actions = array(
19
+ 'upgrader_process_complete', // plugins::installed | themes::installed
20
+ 'activate_plugin', // plugins::activated
21
+ 'deactivate_plugin', // plugins::deactivated
22
+ 'switch_theme', // themes::activated
23
+ 'delete_site_transient_update_themes', // themes::deleted
24
+ 'pre_option_uninstall_plugins', // plugins::deleted
25
+ 'pre_set_site_transient_update_plugins',
26
+ '_core_updated_successfully',
27
+ );
28
+
29
+ /**
30
+ * Return translated connector label
31
+ *
32
+ * @return string Translated connector label
33
+ */
34
+ public function get_label() {
35
+ return esc_html__( 'Installer', 'stream' );
36
+ }
37
+
38
+ /**
39
+ * Return translated action labels
40
+ *
41
+ * @return array Action label translations
42
+ */
43
+ public function get_action_labels() {
44
+ return array(
45
+ 'installed' => esc_html__( 'Installed', 'stream' ),
46
+ 'activated' => esc_html__( 'Activated', 'stream' ),
47
+ 'deactivated' => esc_html__( 'Deactivated', 'stream' ),
48
+ 'deleted' => esc_html__( 'Deleted', 'stream' ),
49
+ 'updated' => esc_html__( 'Updated', 'stream' ),
50
+ );
51
+ }
52
+
53
+ /**
54
+ * Return translated context labels
55
+ *
56
+ * @return array Context label translations
57
+ */
58
+ public function get_context_labels() {
59
+ return array(
60
+ 'plugins' => esc_html__( 'Plugins', 'stream' ),
61
+ 'themes' => esc_html__( 'Themes', 'stream' ),
62
+ 'wordpress' => esc_html__( 'WordPress', 'stream' ),
63
+ );
64
+ }
65
+
66
+ /**
67
+ * Add action links to Stream drop row in admin list screen
68
+ *
69
+ * @filter wp_stream_action_links_{connector}
70
+ *
71
+ * @param array $links Previous links registered
72
+ * @param object $record Stream record
73
+ *
74
+ * @return array Action links
75
+ */
76
+ public function action_links( $links, $record ) {
77
+ if ( 'wordpress' === $record->context && 'updated' === $record->action ) {
78
+ global $wp_version;
79
+
80
+ $version = $record->get_meta( 'new_version', true );
81
+
82
+ if ( $version === $wp_version ) {
83
+ $links[ esc_html__( 'About', 'stream' ) ] = admin_url( 'about.php?updated' );
84
+ }
85
+
86
+ $links[ esc_html__( 'View Release Notes', 'stream' ) ] = esc_url( sprintf( 'http://codex.wordpress.org/Version_%s', $version ) );
87
+ }
88
+
89
+ return $links;
90
+ }
91
+
92
+ /**
93
+ * Wrapper method for calling get_plugins()
94
+ *
95
+ * @return array
96
+ */
97
+ public function get_plugins() {
98
+ if ( ! function_exists( 'get_plugins' ) ) {
99
+ require_once ABSPATH . 'wp-admin/includes/plugin.php';
100
+ }
101
+
102
+ return get_plugins();
103
+ }
104
+
105
+ /**
106
+ * Log plugin installations
107
+ *
108
+ * @action transition_post_status
109
+ *
110
+ * @param \WP_Upgrader $upgrader
111
+ * @param array $extra
112
+ *
113
+ * @return bool
114
+ */
115
+ public function callback_upgrader_process_complete( $upgrader, $extra ) {
116
+ $logs = array();
117
+ $success = ! is_wp_error( $upgrader->skin->result );
118
+ $error = null;
119
+
120
+ if ( ! $success ) {
121
+ $errors = $upgrader->skin->result->errors;
122
+
123
+ list( $error ) = reset( $errors );
124
+ }
125
+
126
+ // This would have failed down the road anyway
127
+ if ( ! isset( $extra['type'] ) ) {
128
+ return false;
129
+ }
130
+
131
+ $type = $extra['type'];
132
+ $action = $extra['action'];
133
+
134
+ if ( ! in_array( $type, array( 'plugin', 'theme' ) ) ) {
135
+ return false;
136
+ }
137
+
138
+ if ( 'install' === $action ) {
139
+ if ( 'plugin' === $type ) {
140
+ $path = $upgrader->plugin_info();
141
+
142
+ if ( ! $path ) {
143
+ return false;
144
+ }
145
+
146
+ $data = get_plugin_data( $upgrader->skin->result['local_destination'] . '/' . $path );
147
+ $slug = $upgrader->result['destination_name'];
148
+ $name = $data['Name'];
149
+ $version = $data['Version'];
150
+ } else { // theme
151
+ $slug = $upgrader->theme_info();
152
+
153
+ if ( ! $slug ) {
154
+ return false;
155
+ }
156
+
157
+ wp_clean_themes_cache();
158
+
159
+ $theme = wp_get_theme( $slug );
160
+ $name = $theme->name;
161
+ $version = $theme->version;
162
+ }
163
+
164
+ $action = 'installed';
165
+ $message = _x(
166
+ 'Installed %1$s: %2$s %3$s',
167
+ 'Plugin/theme installation. 1: Type (plugin/theme), 2: Plugin/theme name, 3: Plugin/theme version',
168
+ 'stream'
169
+ );
170
+
171
+ $logs[] = compact( 'slug', 'name', 'version', 'message', 'action' );
172
+ } elseif ( 'update' === $action ) {
173
+ $action = 'updated';
174
+ $message = _x(
175
+ 'Updated %1$s: %2$s %3$s',
176
+ 'Plugin/theme update. 1: Type (plugin/theme), 2: Plugin/theme name, 3: Plugin/theme version',
177
+ 'stream'
178
+ );
179
+
180
+ if ( 'plugin' === $type ) {
181
+ if ( isset( $extra['bulk'] ) && true === $extra['bulk'] ) {
182
+ $slugs = $extra['plugins'];
183
+ } else {
184
+ $slugs = array( $upgrader->skin->plugin );
185
+ }
186
+
187
+ $_plugins = $this->get_plugins();
188
+
189
+ foreach ( $slugs as $slug ) {
190
+ $plugin_data = get_plugin_data( WP_PLUGIN_DIR . '/' . $slug );
191
+ $name = $plugin_data['Name'];
192
+ $version = $plugin_data['Version'];
193
+ $old_version = $_plugins[ $slug ]['Version'];
194
+
195
+ $logs[] = compact( 'slug', 'name', 'old_version', 'version', 'message', 'action' );
196
+ }
197
+ } else { // theme
198
+ if ( isset( $extra['bulk'] ) && true === $extra['bulk'] ) {
199
+ $slugs = $extra['themes'];
200
+ } else {
201
+ $slugs = array( $upgrader->skin->theme );
202
+ }
203
+
204
+ foreach ( $slugs as $slug ) {
205
+ $theme = wp_get_theme( $slug );
206
+ $stylesheet = $theme['Stylesheet Dir'] . '/style.css';
207
+ $theme_data = get_file_data( $stylesheet, array( 'Version' => 'Version' ) );
208
+ $name = $theme['Name'];
209
+ $old_version = $theme['Version'];
210
+ $version = $theme_data['Version'];
211
+
212
+ $logs[] = compact( 'slug', 'name', 'old_version', 'version', 'message', 'action' );
213
+ }
214
+ }
215
+ } else {
216
+ return false;
217
+ }
218
+
219
+ $context = $type . 's';
220
+
221
+ foreach ( $logs as $log ) {
222
+ $name = isset( $log['name'] ) ? $log['name'] : null;
223
+ $version = isset( $log['version'] ) ? $log['version'] : null;
224
+ $slug = isset( $log['slug'] ) ? $log['slug'] : null;
225
+ $old_version = isset( $log['old_version'] ) ? $log['old_version'] : null;
226
+ $message = isset( $log['message'] ) ? $log['message'] : null;
227
+ $action = isset( $log['action'] ) ? $log['action'] : null;
228
+
229
+ $this->log(
230
+ $message,
231
+ compact( 'type', 'name', 'version', 'slug', 'success', 'error', 'old_version' ),
232
+ null,
233
+ $context,
234
+ $action
235
+ );
236
+ }
237
+
238
+ return true;
239
+ }
240
+
241
+ public function callback_activate_plugin( $slug, $network_wide ) {
242
+ $_plugins = $this->get_plugins();
243
+ $name = $_plugins[ $slug ]['Name'];
244
+ $network_wide = $network_wide ? esc_html__( 'network wide', 'stream' ) : null;
245
+
246
+ $this->log(
247
+ _x(
248
+ '"%1$s" plugin activated %2$s',
249
+ '1: Plugin name, 2: Single site or network wide',
250
+ 'stream'
251
+ ),
252
+ compact( 'name', 'network_wide' ),
253
+ null,
254
+ 'plugins',
255
+ 'activated'
256
+ );
257
+ }
258
+
259
+ public function callback_deactivate_plugin( $slug, $network_wide ) {
260
+ $_plugins = $this->get_plugins();
261
+ $name = $_plugins[ $slug ]['Name'];
262
+ $network_wide = $network_wide ? esc_html__( 'network wide', 'stream' ) : null;
263
+
264
+ $this->log(
265
+ _x(
266
+ '"%1$s" plugin deactivated %2$s',
267
+ '1: Plugin name, 2: Single site or network wide',
268
+ 'stream'
269
+ ),
270
+ compact( 'name', 'network_wide' ),
271
+ null,
272
+ 'plugins',
273
+ 'deactivated'
274
+ );
275
+ }
276
+
277
+ public function callback_switch_theme( $name, $theme ) {
278
+ unset( $theme );
279
+ $this->log(
280
+ __( '"%s" theme activated', 'stream' ),
281
+ compact( 'name' ),
282
+ null,
283
+ 'themes',
284
+ 'activated'
285
+ );
286
+ }
287
+
288
+ /**
289
+ * @todo Core needs a delete_theme hook
290
+ */
291
+ public function callback_delete_site_transient_update_themes() {
292
+ $backtrace = debug_backtrace();
293
+ $delete_theme_call = null;
294
+
295
+ foreach ( $backtrace as $call ) {
296
+ if ( isset( $call['function'] ) && 'delete_theme' === $call['function'] ) {
297
+ $delete_theme_call = $call;
298
+ break;
299
+ }
300
+ }
301
+
302
+ if ( empty( $delete_theme_call ) ) {
303
+ return;
304
+ }
305
+
306
+ $name = $delete_theme_call['args'][0];
307
+ // @todo Can we get the name of the theme? Or has it already been eliminated
308
+
309
+ $this->log(
310
+ __( '"%s" theme deleted', 'stream' ),
311
+ compact( 'name' ),
312
+ null,
313
+ 'themes',
314
+ 'deleted'
315
+ );
316
+ }
317
+
318
+ /**
319
+ * @todo Core needs an uninstall_plugin hook
320
+ * @todo This does not work in WP-CLI
321
+ */
322
+ public function callback_pre_option_uninstall_plugins() {
323
+ if (
324
+ 'delete-selected' !== wp_stream_filter_input( INPUT_GET, 'action' )
325
+ &&
326
+ 'delete-selected' !== wp_stream_filter_input( INPUT_POST, 'action2' )
327
+ ) {
328
+ return false;
329
+ }
330
+
331
+ // @codingStandardsIgnoreStart
332
+ $type = isset( $_POST['action2'] ) ? INPUT_POST : INPUT_GET;
333
+ // @codingStandardsIgnoreEnd
334
+
335
+ $plugins = wp_stream_filter_input( $type, 'checked' );
336
+ $_plugins = $this->get_plugins();
337
+
338
+ $plugins_to_delete = array();
339
+
340
+ foreach ( (array) $plugins as $plugin ) {
341
+ $plugins_to_delete[ $plugin ] = $_plugins[ $plugin ];
342
+ }
343
+
344
+ update_option( 'wp_stream_plugins_to_delete', $plugins_to_delete );
345
+
346
+ return false;
347
+ }
348
+
349
+ /**
350
+ * @param mixed $value
351
+ *
352
+ * @return mixed
353
+ * @todo Core needs a delete_plugin hook
354
+ * @todo This does not work in WP-CLI
355
+ */
356
+ public function callback_pre_set_site_transient_update_plugins( $value ) {
357
+ if (
358
+ ! wp_stream_filter_input( INPUT_POST, 'verify-delete' )
359
+ ||
360
+ ! ( $plugins_to_delete = get_option( 'wp_stream_plugins_to_delete' ) )
361
+ ) {
362
+ return $value;
363
+ }
364
+
365
+ foreach ( $plugins_to_delete as $plugin => $data ) {
366
+ $name = $data['Name'];
367
+ $network_wide = $data['Network'] ? esc_html__( 'network wide', 'stream' ) : '';
368
+
369
+ $this->log(
370
+ __( '"%s" plugin deleted', 'stream' ),
371
+ compact( 'name', 'plugin', 'network_wide' ),
372
+ null,
373
+ 'plugins',
374
+ 'deleted'
375
+ );
376
+ }
377
+
378
+ delete_option( 'wp_stream_plugins_to_delete' );
379
+
380
+ return $value;
381
+ }
382
+
383
+ public function callback__core_updated_successfully( $new_version ) {
384
+ global $pagenow, $wp_version;
385
+
386
+ $old_version = $wp_version;
387
+ $auto_updated = ( 'update-core.php' !== $pagenow );
388
+
389
+ if ( $auto_updated ) {
390
+ $message = esc_html__( 'WordPress auto-updated to %s', 'stream' );
391
+ } else {
392
+ $message = esc_html__( 'WordPress updated to %s', 'stream' );
393
+ }
394
+
395
+ $this->log(
396
+ $message,
397
+ compact( 'new_version', 'old_version', 'auto_updated' ),
398
+ null,
399
+ 'wordpress',
400
+ 'updated'
401
+ );
402
+ }
403
+ }
connectors/class-connector-jetpack.php ADDED
@@ -0,0 +1,682 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace WP_Stream;
3
+
4
+ class Connector_Jetpack extends Connector {
5
+ /**
6
+ * Connector slug
7
+ *
8
+ * @var string
9
+ */
10
+ public $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 $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 $options = array();
42
+
43
+ /**
44
+ * Tracking registered Settings, with overridden data
45
+ *
46
+ * @var array
47
+ */
48
+ public $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 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 function get_label() {
69
+ return esc_html_x( 'Jetpack', 'jetpack', 'stream' );
70
+ }
71
+
72
+ /**
73
+ * Return translated action labels
74
+ *
75
+ * @return array Action label translations
76
+ */
77
+ public function get_action_labels() {
78
+ return array(
79
+ 'activated' => esc_html_x( 'Activated', 'jetpack', 'stream' ),
80
+ 'deactivated' => esc_html_x( 'Dectivated', 'jetpack', 'stream' ),
81
+ 'register' => esc_html_x( 'Connected', 'jetpack', 'stream' ),
82
+ 'disconnect' => esc_html_x( 'Disconnected', 'jetpack', 'stream' ),
83
+ 'authorize' => esc_html_x( 'Link', 'jetpack', 'stream' ),
84
+ 'unlink' => esc_html_x( 'Unlink', 'jetpack', 'stream' ),
85
+ 'updated' => esc_html_x( 'Updated', 'jetpack', 'stream' ),
86
+ 'added' => esc_html_x( 'Added', 'jetpack', 'stream' ),
87
+ 'removed' => esc_html_x( 'Removed', 'jetpack', 'stream' ),
88
+ );
89
+ }
90
+
91
+ /**
92
+ * Return translated context labels
93
+ *
94
+ * @return array Context label translations
95
+ */
96
+ public function get_context_labels() {
97
+ return array(
98
+ 'blogs' => esc_html_x( 'Blogs', 'jetpack', 'stream' ),
99
+ 'carousel' => esc_html_x( 'Carousel', 'jetpack', 'stream' ),
100
+ 'custom-css' => esc_html_x( 'Custom CSS', 'jetpack', 'stream' ),
101
+ 'gplus-authorship' => esc_html_x( 'Google+ Profile', 'jetpack', 'stream' ),
102
+ 'infinite-scroll' => esc_html_x( 'Infinite Scroll', 'jetpack', 'stream' ),
103
+ 'jetpack-comments' => esc_html_x( 'Comments', 'jetpack', 'stream' ),
104
+ 'likes' => esc_html_x( 'Likes', 'jetpack', 'stream' ),
105
+ 'minileven' => esc_html_x( 'Mobile', 'jetpack', 'stream' ),
106
+ 'modules' => esc_html_x( 'Modules', 'jetpack', 'stream' ),
107
+ 'monitor' => esc_html_x( 'Monitor', 'jetpack', 'stream' ),
108
+ 'options' => esc_html_x( 'Options', 'jetpack', 'stream' ),
109
+ 'post-by-email' => esc_html_x( 'Post by Email', 'jetpack', 'stream' ),
110
+ 'protect' => esc_html_x( 'Protect', 'jetpack', 'stream' ),
111
+ 'publicize' => esc_html_x( 'Publicize', 'jetpack', 'stream' ),
112
+ 'related-posts' => esc_html_x( 'Related Posts', 'jetpack', 'stream' ),
113
+ 'sharedaddy' => esc_html_x( 'Sharing', 'jetpack', 'stream' ),
114
+ 'subscriptions' => esc_html_x( 'Subscriptions', 'jetpack', 'stream' ),
115
+ 'sso' => esc_html_x( 'SSO', 'jetpack', 'stream' ),
116
+ 'stats' => esc_html_x( 'WordPress.com Stats', 'jetpack', 'stream' ),
117
+ 'tiled-gallery' => esc_html_x( 'Tiled Galleries', 'jetpack', 'stream' ),
118
+ 'users' => esc_html_x( 'Users', 'jetpack', 'stream' ),
119
+ 'verification-tools' => esc_html_x( 'Site Verification', 'jetpack', 'stream' ),
120
+ 'videopress' => esc_html_x( 'VideoPress', 'jetpack', 'stream' ),
121
+ );
122
+ }
123
+
124
+ /**
125
+ * Add action links to Stream drop row in admin list screen
126
+ *
127
+ * @filter wp_stream_action_links_{connector}
128
+ *
129
+ * @param array $links Previous links registered
130
+ * @param object $record Stream record
131
+ *
132
+ * @return array Action links
133
+ */
134
+ public function action_links( $links, $record ) {
135
+ // @todo provide proper action links
136
+ if ( 'jetpack' === $record->connector ) {
137
+ if ( 'modules' === $record->context ) {
138
+ $slug = $record->get_meta( 'module_slug', true );
139
+
140
+ if ( is_array( $slug ) ) {
141
+ $slug = current( $slug );
142
+ }
143
+
144
+ if ( Jetpack::is_module_active( $slug ) ) {
145
+ if ( apply_filters( 'jetpack_module_configurable_' . $slug, false ) ) {
146
+ $links[ esc_html__( 'Configure', 'stream' ) ] = Jetpack::module_configuration_url( $slug );
147
+ }
148
+
149
+ $links[ esc_html__( 'Deactivate', 'stream' ) ] = wp_nonce_url(
150
+ add_query_arg(
151
+ array(
152
+ 'action' => 'deactivate',
153
+ 'module' => $slug,
154
+ ),
155
+ Jetpack::admin_url()
156
+ ),
157
+ 'jetpack_deactivate-' . sanitize_title( $slug )
158
+ );
159
+ } else {
160
+ $links[ esc_html__( 'Activate', 'stream' ) ] = wp_nonce_url(
161
+ add_query_arg(
162
+ array(
163
+ 'action' => 'activate',
164
+ 'module' => $slug,
165
+ ),
166
+ Jetpack::admin_url()
167
+ ),
168
+ 'jetpack_activate-' . sanitize_title( $slug )
169
+ );
170
+ }
171
+ } elseif ( Jetpack::is_module_active( str_replace( 'jetpack-', '', $record->context ) ) ) {
172
+ $slug = str_replace( 'jetpack-', '', $record->context ); // handling jetpack-comment anomaly
173
+
174
+ if ( apply_filters( 'jetpack_module_configurable_' . $slug, false ) ) {
175
+ $links[ esc_html__( 'Configure module', 'stream' ) ] = Jetpack::module_configuration_url( $slug );
176
+ }
177
+ }
178
+ }
179
+
180
+ return $links;
181
+ }
182
+
183
+ public function register() {
184
+ parent::register();
185
+
186
+ add_filter( 'wp_stream_log_data', array( $this, 'log_override' ) );
187
+
188
+ $this->options = array(
189
+ 'jetpack_options' => null,
190
+ // Sharing module
191
+ 'hide_gplus' => null,
192
+ 'gplus_authors' => null,
193
+ 'sharing-options' => array(
194
+ 'label' => esc_html__( 'Sharing options', 'stream' ),
195
+ 'context' => 'sharedaddy',
196
+ ),
197
+ 'sharedaddy_disable_resources' => null,
198
+ 'jetpack-twitter-cards-site-tag' => array(
199
+ 'label' => esc_html__( 'Twitter site tag', 'stream' ),
200
+ 'context' => 'sharedaddy',
201
+ ),
202
+ // Stats module
203
+ 'stats_options' => array(
204
+ 'label' => esc_html__( 'WordPress.com Stats', 'stream' ),
205
+ 'context' => 'stats',
206
+ ),
207
+ // Comments
208
+ 'jetpack_comment_form_color_scheme' => array(
209
+ 'label' => esc_html__( 'Color Scheme', 'stream' ),
210
+ 'context' => 'jetpack-comments',
211
+ ),
212
+ // Likes
213
+ 'disabled_likes' => array(
214
+ 'label' => esc_html__( 'WP.com Site-wide Likes', 'stream' ),
215
+ 'context' => 'likes',
216
+ ),
217
+ // Mobile
218
+ 'wp_mobile_excerpt' => array(
219
+ 'label' => esc_html__( 'Excerpts appearance', 'stream' ),
220
+ 'context' => 'minileven',
221
+ ),
222
+ 'wp_mobile_app_promos' => array(
223
+ 'label' => esc_html__( 'App promos', 'stream' ),
224
+ 'context' => 'minileven',
225
+ ),
226
+ );
227
+
228
+ $this->options_override = array(
229
+ // Carousel Module
230
+ 'carousel_background_color' => array(
231
+ 'label' => esc_html__( 'Background color', 'stream' ),
232
+ 'context' => 'carousel',
233
+ ),
234
+ 'carousel_display_exif' => array(
235
+ 'label' => esc_html__( 'Metadata', 'stream' ),
236
+ 'context' => 'carousel',
237
+ ),
238
+ // Subscriptions
239
+ 'stb_enabled' => array(
240
+ 'label' => esc_html__( 'Follow blog comment form button', 'stream' ),
241
+ 'context' => 'subscriptions',
242
+ ),
243
+ 'stc_enabled' => array(
244
+ 'label' => esc_html__( 'Follow comments form button', 'stream' ),
245
+ 'context' => 'subscriptions',
246
+ ),
247
+ // Jetpack comments
248
+ 'highlander_comment_form_prompt' => array(
249
+ 'label' => esc_html__( 'Greeting Text', 'stream' ),
250
+ 'context' => 'jetpack-comments',
251
+ ),
252
+ // Infinite Scroll
253
+ 'infinite_scroll_google_analytics' => array(
254
+ 'label' => esc_html__( 'Infinite Scroll Google Analytics', 'stream' ),
255
+ 'context' => 'infinite-scroll',
256
+ ),
257
+ // Protect
258
+ 'jetpack_protect_blocked_attempts' => array(
259
+ 'label' => esc_html__( 'Blocked Attempts', 'stream' ),
260
+ 'context' => 'protect',
261
+ ),
262
+ // SSO
263
+ 'jetpack_sso_require_two_step' => array(
264
+ 'label' => esc_html__( 'Require Two-Step Authentication', 'stream' ),
265
+ 'context' => 'sso',
266
+ ),
267
+ 'jetpack_sso_match_by_email' => array(
268
+ 'label' => esc_html__( 'Match by Email', 'stream' ),
269
+ 'context' => 'sso',
270
+ ),
271
+ // Related posts
272
+ 'jetpack_relatedposts' => array(
273
+ 'show_headline' => array(
274
+ 'label' => esc_html__( 'Show Related Posts Headline', 'stream' ),
275
+ 'context' => 'related-posts',
276
+ ),
277
+ 'show_thumbnails' => array(
278
+ 'label' => esc_html__( 'Show Related Posts Thumbnails', 'stream' ),
279
+ 'context' => 'related-posts',
280
+ ),
281
+ ),
282
+ // Site verification
283
+ 'verification_services_codes' => array(
284
+ 'google' => array(
285
+ 'label' => esc_html__( 'Google Webmaster Tools Token', 'stream' ),
286
+ 'context' => 'verification-tools',
287
+ ),
288
+ 'bing' => array(
289
+ 'label' => esc_html__( 'Bing Webmaster Center Token', 'stream' ),
290
+ 'context' => 'verification-tools',
291
+ ),
292
+ 'pinterest' => array(
293
+ 'label' => esc_html__( 'Pinterest Site Verification Token', 'stream' ),
294
+ 'context' => 'verification-tools',
295
+ ),
296
+ ),
297
+ // Tiled galleries
298
+ 'tiled_galleries' => array(
299
+ 'label' => esc_html__( 'Tiled Galleries', 'stream' ),
300
+ 'context' => 'tiled-gallery',
301
+ ),
302
+ );
303
+ }
304
+
305
+ /**
306
+ * Track Jetpack log entries
307
+ * Includes:
308
+ * - Activation/Deactivation of modules
309
+ * - Registration/Disconnection of blogs
310
+ * - Authorization/unlinking of users
311
+ *
312
+ * @param array $entry
313
+ */
314
+ public function callback_jetpack_log_entry( array $entry ) {
315
+ $method = $entry['code'];
316
+ $data = $entry['data'];
317
+ $context = null;
318
+ $action = null;
319
+
320
+ if ( in_array( $method, array( 'activate', 'deactivate' ) ) ) {
321
+ $module_slug = $data;
322
+ $module = \Jetpack::get_module( $module_slug );
323
+ $module_name = $module['name'];
324
+ $context = 'modules';
325
+ $action = $method . 'd';
326
+ $meta = compact( 'module_slug' );
327
+ $message = sprintf(
328
+ __( '%1$s module %2$s', 'stream' ),
329
+ $module_name,
330
+ ( 'activated' === $action ) ? esc_html__( 'activated', 'stream' ) : esc_html__( 'deactivated', 'stream' )
331
+ );
332
+ } elseif ( in_array( $method, array( 'authorize', 'unlink' ) ) ) {
333
+ $user_id = intval( $data );
334
+
335
+ if ( empty( $user_id ) ) {
336
+ $user_id = get_current_user_id();
337
+ }
338
+
339
+ $user = new \WP_User( $user_id );
340
+ $user_email = $user->user_email;
341
+ $user_login = $user->user_login;
342
+ $context = 'users';
343
+ $action = $method;
344
+ $meta = compact( 'user_id', 'user_email', 'user_login' );
345
+ $message = sprintf(
346
+ __( '%1$s\'s account %2$s %3$s Jetpack', 'stream' ),
347
+ $user->display_name,
348
+ ( 'unlink' === $action ) ? esc_html__( 'unlinked', 'stream' ) : esc_html__( 'linked', 'stream' ),
349
+ ( 'unlink' === $action ) ? esc_html__( 'from', 'stream' ) : esc_html__( 'to', 'stream' )
350
+ );
351
+ } elseif ( in_array( $method, array( 'register', 'disconnect', 'subsiteregister', 'subsitedisconnect' ) ) ) {
352
+ $context = 'blogs';
353
+ $action = str_replace( 'subsite', '', $method );
354
+ $is_multisite = ( 0 === strpos( $method, 'subsite' ) );
355
+ $blog_id = $is_multisite ? ( isset( $_GET['site_id'] ) ? intval( wp_unslash( $_GET['site_id'] ) ) : null ) : get_current_blog_id(); // phpcs: input var okay
356
+
357
+ if ( empty( $blog_id ) ) {
358
+ return;
359
+ }
360
+
361
+ $meta = array();
362
+
363
+ if ( ! $is_multisite ) {
364
+ $message = sprintf(
365
+ __( 'Site %s Jetpack', 'stream' ),
366
+ ( 'register' === $action ) ? esc_html__( 'connected to', 'stream' ) : esc_html__( 'disconnected from', 'stream' )
367
+ );
368
+ } else {
369
+ $blog_details = get_blog_details( array( 'blog_id' => $blog_id ) );
370
+ $blog_name = $blog_details->blogname;
371
+ $meta += compact( 'blog_id', 'blog_name' );
372
+
373
+ $message = sprintf(
374
+ __( '"%1$s" blog %2$s Jetpack', 'stream' ),
375
+ $blog_name,
376
+ ( 'register' === $action ) ? esc_html__( 'connected to', 'stream' ) : esc_html__( 'disconnected from', 'stream' )
377
+ );
378
+ }
379
+ }
380
+
381
+ if ( empty( $message ) ) {
382
+ return;
383
+ }
384
+
385
+ $this->log(
386
+ $message,
387
+ $meta,
388
+ null,
389
+ $context,
390
+ $action
391
+ );
392
+ }
393
+
394
+ /**
395
+ * Track visible/enabled sharing services ( buttons )
396
+ *
397
+ * @param string $state
398
+ */
399
+ public function callback_sharing_get_services_state( $state ) {
400
+ $this->log(
401
+ __( 'Sharing services updated', 'stream' ),
402
+ $state,
403
+ null,
404
+ 'sharedaddy',
405
+ 'updated'
406
+ );
407
+ }
408
+
409
+ public function callback_update_option( $option, $old, $new ) {
410
+ $this->check( $option, $old, $new );
411
+ }
412
+
413
+ public function callback_add_option( $option, $val ) {
414
+ $this->check( $option, null, $val );
415
+ }
416
+
417
+ public function callback_delete_option( $option ) {
418
+ $this->check( $option, null, null );
419
+ }
420
+
421
+ /**
422
+ * Track Monitor module notification status
423
+ */
424
+ public function callback_jetpack_module_configuration_load_monitor() {
425
+ $active = wp_stream_filter_input( INPUT_POST, 'receive_jetpack_monitor_notification' );
426
+
427
+ if ( ! $active ) {
428
+ return;
429
+ }
430
+
431
+ $this->log(
432
+ __( 'Monitor notifications %s', 'stream' ),
433
+ array(
434
+ 'status' => $active ? esc_html__( 'activated', 'stream' ) : esc_html__( 'deactivated', 'stream' ),
435
+ 'option' => 'receive_jetpack_monitor_notification',
436
+ 'old_value' => ! $active,
437
+ 'value' => $active,
438
+ ),
439
+ null,
440
+ 'monitor',
441
+ 'updated'
442
+ );
443
+ }
444
+
445
+ public function callback_wp_ajax_jetpack_post_by_email_enable() {
446
+ $this->track_post_by_email( true );
447
+ }
448
+
449
+ public function callback_wp_ajax_jetpack_post_by_email_regenerate() {
450
+ $this->track_post_by_email( null );
451
+ }
452
+
453
+ public function callback_wp_ajax_jetpack_post_by_email_disable() {
454
+ $this->track_post_by_email( false );
455
+ }
456
+
457
+ public function track_post_by_email( $status ) {
458
+ if ( true === $status ) {
459
+ $action = esc_html__( 'enabled', 'stream' );
460
+ } elseif ( false === $status ) {
461
+ $action = esc_html__( 'disabled', 'stream' );
462
+ } elseif ( null === $status ) {
463
+ $action = esc_html__( 'regenerated', 'stream' );
464
+ }
465
+
466
+ $user = wp_get_current_user();
467
+
468
+ $this->log(
469
+ __( '%1$s %2$s Post by Email', 'stream' ),
470
+ array(
471
+ 'user_displayname' => $user->display_name,
472
+ 'action' => $action,
473
+ 'status' => $status,
474
+ ),
475
+ null,
476
+ 'post-by-email',
477
+ 'updated'
478
+ );
479
+ }
480
+
481
+ public function check( $option, $old_value, $new_value ) {
482
+ if ( ! array_key_exists( $option, $this->options ) ) {
483
+ return;
484
+ }
485
+
486
+ if ( is_null( $this->options[ $option ] ) ) {
487
+ call_user_func( array( $this, 'check_' . str_replace( '-', '_', $option ) ), $old_value, $new_value );
488
+ } else {
489
+ $data = $this->options[ $option ];
490
+ $option_title = $data['label'];
491
+
492
+ $this->log(
493
+ __( '"%s" setting updated', 'stream' ),
494
+ compact( 'option_title', 'option', 'old_value', 'new_value' ),
495
+ null,
496
+ $data['context'],
497
+ isset( $data['action'] ) ? $data['action'] : 'updated'
498
+ );
499
+ }
500
+ }
501
+
502
+ public function check_jetpack_options( $old_value, $new_value ) {
503
+ $options = array();
504
+
505
+ if ( ! is_array( $old_value ) || ! is_array( $new_value ) ) {
506
+ return;
507
+ }
508
+
509
+ foreach ( $this->get_changed_keys( $old_value, $new_value, 1 ) as $field_key => $field_value ) {
510
+ $options[ $field_key ] = $field_value;
511
+ }
512
+
513
+ foreach ( $options as $option => $option_value ) {
514
+ $settings = $this->get_settings_def( $option, $option_value );
515
+
516
+ if ( ! $settings ) {
517
+ continue;
518
+ }
519
+
520
+ if ( 0 === $option_value ) { // Skip updated array with updated members, we'll be logging those instead
521
+ continue;
522
+ }
523
+
524
+ $settings['meta'] += array(
525
+ 'option' => $option,
526
+ 'old_value' => maybe_serialize( $old_value ),
527
+ 'value' => maybe_serialize( $new_value ),
528
+ );
529
+
530
+ $this->log(
531
+ $settings['message'],
532
+ $settings['meta'],
533
+ isset( $settings['object_id'] ) ? $settings['object_id'] : null,
534
+ $settings['context'],
535
+ $settings['action']
536
+ );
537
+ }
538
+ }
539
+
540
+ public function check_hide_gplus( $old_value, $new_value ) {
541
+ $status = ! is_null( $new_value );
542
+
543
+ if ( $status && $old_value ) {
544
+ return false;
545
+ }
546
+
547
+ $this->log(
548
+ __( 'G+ profile display %s', 'stream' ),
549
+ array(
550
+ 'action' => $status ? esc_html__( 'enabled', 'stream' ) : esc_html__( 'disabled', 'stream' ),
551
+ ),
552
+ null,
553
+ 'gplus-authorship',
554
+ 'updated'
555
+ );
556
+ }
557
+
558
+ public function check_gplus_authors( $old_value, $new_value ) {
559
+ unset( $old_value );
560
+
561
+ $user = wp_get_current_user();
562
+ $connected = is_array( $new_value ) && array_key_exists( $user->ID, $new_value );
563
+
564
+ $this->log(
565
+ __( '%1$s\'s Google+ account %2$s', 'stream' ),
566
+ array(
567
+ 'display_name' => $user->display_name,
568
+ 'action' => $connected ? esc_html__( 'connected', 'stream' ) : esc_html__( 'disconnected', 'stream' ),
569
+ 'user_id' => $user->ID,
570
+ ),
571
+ $user->ID,
572
+ 'gplus-authorship',
573
+ 'updated'
574
+ );
575
+ }
576
+
577
+ public function check_sharedaddy_disable_resources( $old_value, $new_value ) {
578
+ if ( $old_value === $new_value ) {
579
+ return;
580
+ }
581
+
582
+ $status = ! $new_value ? 'enabled' : 'disabled'; // disabled = 1
583
+
584
+ $this->log(
585
+ __( 'Sharing CSS/JS %s', 'stream' ),
586
+ compact( 'status', 'old_value', 'new_value' ),
587
+ null,
588
+ 'sharing',
589
+ 'updated'
590
+ );
591
+ }
592
+
593
+ /**
594
+ * Override connector log for our own Settings / Actions
595
+ *
596
+ * @param array $data
597
+ *
598
+ * @return array|bool
599
+ */
600
+ public function log_override( $data ) {
601
+ if ( ! is_array( $data ) ) {
602
+ return $data;
603
+ }
604
+
605
+ // Handling our Settings
606
+ if ( 'settings' === $data['connector'] && isset( $this->options_override[ $data['args']['option'] ] ) ) {
607
+ if ( isset( $data['args']['option_key'] ) ) {
608
+ $overrides = $this->options_override[ $data['args']['option'] ][ $data['args']['option_key'] ];
609
+ } else {
610
+ $overrides = $this->options_override[ $data['args']['option'] ];
611
+ }
612
+
613
+ if ( isset( $overrides ) ) {
614
+ $data['args']['label'] = $overrides['label'];
615
+ $data['args']['context'] = $overrides['context'];
616
+ $data['context'] = $overrides['context'];
617
+ $data['connector'] = $this->name;
618
+ }
619
+ } elseif ( 'posts' === $data['connector'] && 'safecss' === $data['context'] ) {
620
+ $data = array_merge(
621
+ $data,
622
+ array(
623
+ 'connector' => $this->name,
624
+ 'message' => esc_html__( 'Custom CSS updated', 'stream' ),
625
+ 'args' => array(),
626
+ 'object_id' => null,
627
+ 'context' => 'custom-css',
628
+ 'action' => 'updated',
629
+ )
630
+ );
631
+ }
632
+
633
+ return $data;
634
+ }
635
+
636
+ private function get_settings_def( $key, $value = null ) {
637
+ // Sharing
638
+ if ( 0 === strpos( $key, 'publicize_connections::' ) ) {
639
+ global $publicize_ui;
640
+
641
+ $name = str_replace( 'publicize_connections::', '', $key );
642
+
643
+ return array(
644
+ 'message' => esc_html__( '%1$s connection %2$s', 'stream' ),
645
+ 'meta' => array(
646
+ 'connection' => $publicize_ui->publicize->get_service_label( $name ),
647
+ 'action' => $value ? esc_html__( 'added', 'stream' ) : esc_html__( 'removed', 'stream' ),
648
+ 'option' => 'jetpack_options',
649
+ 'option_key' => $key,
650
+ ),
651
+ 'action' => $value ? 'added' : 'removed',
652
+ 'context' => 'publicize',
653
+ );
654
+ } elseif ( 0 === strpos( $key, 'videopress::' ) ) {
655
+ $name = str_replace( 'videopress::', '', $key );
656
+ $options = array(
657
+ 'access' => esc_html__( 'Video Library Access', 'stream' ),
658
+ 'upload' => esc_html__( 'Allow users to upload videos', 'stream' ),
659
+ 'freedom' => esc_html__( 'Free formats', 'stream' ),
660
+ 'hd' => esc_html__( 'Default quality', 'stream' ),
661
+ );
662
+
663
+ if ( ! isset( $options[ $name ] ) ) {
664
+ return false;
665
+ }
666
+
667
+ return array(
668
+ 'message' => esc_html__( '"%s" setting updated', 'stream' ),
669
+ 'meta' => array(
670
+ 'option_name' => $options[ $name ],
671
+ 'option' => 'jetpack_options',
672
+ 'option_key' => $key,
673
+ ),
674
+ 'action' => 'updated',
675
+ 'context' => 'videopress',
676
+ );
677
+ }
678
+
679
+ return false;
680
+ }
681
+
682
+ }
connectors/class-connector-media.php ADDED
@@ -0,0 +1,233 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace WP_Stream;
3
+
4
+ class Connector_Media extends Connector {
5
+ /**
6
+ * Connector slug
7
+ *
8
+ * @var string
9
+ */
10
+ public $name = 'media';
11
+
12
+ /**
13
+ * Actions registered for this connector
14
+ *
15
+ * @var array
16
+ */
17
+ public $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 function get_label() {
31
+ return esc_html__( 'Media', 'stream' );
32
+ }
33
+
34
+ /**
35
+ * Return translated action labels
36
+ *
37
+ * @return array Action label translations
38
+ */
39
+ public function get_action_labels() {
40
+ return array(
41
+ 'attached' => esc_html__( 'Attached', 'stream' ),
42
+ 'uploaded' => esc_html__( 'Uploaded', 'stream' ),
43
+ 'updated' => esc_html__( 'Updated', 'stream' ),
44
+ 'deleted' => esc_html__( 'Deleted', 'stream' ),
45
+ 'assigned' => esc_html__( 'Assigned', 'stream' ),
46
+ 'unassigned' => esc_html__( '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 function get_context_labels() {
58
+ return array(
59
+ 'image' => esc_html__( 'Image', 'stream' ),
60
+ 'audio' => esc_html__( 'Audio', 'stream' ),
61
+ 'video' => esc_html__( 'Video', 'stream' ),
62
+ 'document' => esc_html__( 'Document', 'stream' ),
63
+ 'spreadsheet' => esc_html__( 'Spreadsheet', 'stream' ),
64
+ 'interactive' => esc_html__( 'Interactive', 'stream' ),
65
+ 'text' => esc_html__( 'Text', 'stream' ),
66
+ 'archive' => esc_html__( 'Archive', 'stream' ),
67
+ 'code' => esc_html__( '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
+ *
76
+ * @return string A file type which corresponds with a context label
77
+ */
78
+ public function get_attachment_type( $file_uri ) {
79
+ $extension = pathinfo( $file_uri, PATHINFO_EXTENSION );
80
+ $extension_type = wp_ext2type( $extension );
81
+
82
+ if ( empty( $extension_type ) ) {
83
+ $extension_type = 'document';
84
+ }
85
+
86
+ $context_labels = $this->get_context_labels();
87
+
88
+ if ( ! isset( $context_labels[ $extension_type ] ) ) {
89
+ $extension_type = 'document';
90
+ }
91
+
92
+ return $extension_type;
93
+ }
94
+
95
+ /**
96
+ * Add action links to Stream drop row in admin list screen
97
+ *
98
+ * @filter wp_stream_action_links_{connector}
99
+ *
100
+ * @param array $links Previous links registered
101
+ * @param object $record Stream record
102
+ *
103
+ * @return array Action links
104
+ */
105
+ public function action_links( $links, $record ) {
106
+ if ( $record->object_id ) {
107
+ if ( $link = get_edit_post_link( $record->object_id ) ) {
108
+ $links[ esc_html__( 'Edit Media', 'stream' ) ] = $link;
109
+ }
110
+ if ( $link = get_permalink( $record->object_id ) ) {
111
+ $links[ esc_html__( 'View', 'stream' ) ] = $link;
112
+ }
113
+ }
114
+
115
+ return $links;
116
+ }
117
+
118
+ /**
119
+ * Tracks creation of attachments
120
+ *
121
+ * @action add_attachment
122
+ *
123
+ * @param int $post_id
124
+ */
125
+ public function callback_add_attachment( $post_id ) {
126
+ $post = get_post( $post_id );
127
+ if ( $post->post_parent ) {
128
+ $message = _x(
129
+ 'Attached "%1$s" to "%2$s"',
130
+ '1: Attachment title, 2: Parent post title',
131
+ 'stream'
132
+ );
133
+ } else {
134
+ $message = esc_html__( 'Added "%s" to Media library', 'stream' );
135
+ }
136
+
137
+ $name = $post->post_title;
138
+ $url = $post->guid;
139
+ $parent_id = $post->post_parent;
140
+ $parent = get_post( $parent_id );
141
+ $parent_title = $parent_id ? $parent->post_title : null;
142
+ $attachment_type = $this->get_attachment_type( $post->guid );
143
+
144
+ $this->log(
145
+ $message,
146
+ compact( 'name', 'parent_title', 'parent_id', 'url' ),
147
+ $post_id,
148
+ $attachment_type,
149
+ $post->post_parent ? 'attached' : 'uploaded'
150
+ );
151
+ }
152
+
153
+ /**
154
+ * Tracks editing attachments
155
+ *
156
+ * @action edit_attachment
157
+ *
158
+ * @param int $post_id
159
+ */
160
+ public function callback_edit_attachment( $post_id ) {
161
+ $post = get_post( $post_id );
162
+ $message = esc_html__( 'Updated "%s"', 'stream' );
163
+ $name = $post->post_title;
164
+ $attachment_type = $this->get_attachment_type( $post->guid );
165
+
166
+ $this->log(
167
+ $message,
168
+ compact( 'name' ),
169
+ $post_id,
170
+ $attachment_type,
171
+ 'updated'
172
+ );
173
+ }
174
+
175
+ /**
176
+ * Tracks deletion of attachments
177
+ *
178
+ * @action delete_attachment
179
+ *
180
+ * @param int $post_id
181
+ */
182
+ public function callback_delete_attachment( $post_id ) {
183
+ $post = get_post( $post_id );
184
+ $parent = $post->post_parent ? get_post( $post->post_parent ) : null;
185
+ $parent_id = $parent ? $parent->ID : null;
186
+ $message = esc_html__( 'Deleted "%s"', 'stream' );
187
+ $name = $post->post_title;
188
+ $url = $post->guid;
189
+ $attachment_type = $this->get_attachment_type( $post->guid );
190
+
191
+ $this->log(
192
+ $message,
193
+ compact( 'name', 'parent_id', 'url' ),
194
+ $post_id,
195
+ $attachment_type,
196
+ 'deleted'
197
+ );
198
+ }
199
+
200
+ /**
201
+ * Tracks changes made in the image editor
202
+ *
203
+ * @action delete_attachment
204
+ *
205
+ * @param string $dummy
206
+ * @param string $filename
207
+ * @param string $image
208
+ * @param string $mime_type
209
+ * @param int $post_id
210
+ */
211
+ public function callback_wp_save_image_editor_file( $dummy, $filename, $image, $mime_type, $post_id ) {
212
+ unset( $dummy );
213
+ unset( $image );
214
+ unset( $mime_type );
215
+
216
+ $name = basename( $filename );
217
+ $post = get_post( $post_id );
218
+
219
+ $attachment_type = $this->get_attachment_type( $post->guid );
220
+
221
+ $this->log(
222
+ __( 'Edited image "%s"', 'stream' ),
223
+ compact( 'name', 'filename', 'post_id' ),
224
+ $post_id,
225
+ $attachment_type,
226
+ 'edited'
227
+ );
228
+ }
229
+
230
+ public function callback_wp_save_image_file( $dummy, $filename, $image, $mime_type, $post_id ) {
231
+ return $this->callback_wp_save_image_editor_file( $dummy, $filename, $image, $mime_type, $post_id );
232
+ }
233
+ }
connectors/class-connector-menus.php ADDED
@@ -0,0 +1,232 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace WP_Stream;
3
+
4
+ class Connector_Menus extends Connector {
5
+ /**
6
+ * Connector slug
7
+ *
8
+ * @var string
9
+ */
10
+ public $name = 'menus';
11
+
12
+ /**
13
+ * Actions registered for this connector
14
+ *
15
+ * @var array
16
+ */
17
+ public $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 function get_label() {
29
+ return esc_html__( 'Menus', 'stream' );
30
+ }
31
+
32
+ /**
33
+ * Return translated action labels
34
+ *
35
+ * @return array Action label translations
36
+ */
37
+ public function get_action_labels() {
38
+ return array(
39
+ 'created' => esc_html__( 'Created', 'stream' ),
40
+ 'updated' => esc_html__( 'Updated', 'stream' ),
41
+ 'deleted' => esc_html__( 'Deleted', 'stream' ),
42
+ 'assigned' => esc_html__( 'Assigned', 'stream' ),
43
+ 'unassigned' => esc_html__( 'Unassigned', 'stream' ),
44
+ );
45
+ }
46
+
47
+ /**
48
+ * Return translated context labels
49
+ *
50
+ * @return array Context label translations
51
+ */
52
+ public 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 function register() {
65
+ parent::register();
66
+
67
+ add_action( 'update_option_theme_mods_' . get_option( 'stylesheet' ), array( $this, '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 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[ esc_html__( 'Edit Menu', 'stream' ) ] = admin_url( 'nav-menus.php?action=edit&menu=' . $record->object_id );
87
+ }
88
+ }
89
+
90
+ return $links;
91
+ }
92
+
93
+ /**
94
+ * Tracks creation of menus
95
+ *
96
+ * @action wp_create_nav_menu
97
+ *
98
+ * @param int $menu_id
99
+ * @param array $menu_data
100
+ */
101
+ public function callback_wp_create_nav_menu( $menu_id, $menu_data ) {
102
+ $name = $menu_data['menu-name'];
103
+
104
+ $this->log(
105
+ __( 'Created new menu "%s"', 'stream' ),
106
+ compact( 'name', 'menu_id' ),
107
+ $menu_id,
108
+ sanitize_title( $name ),
109
+ 'created'
110
+ );
111
+ }
112
+
113
+ /**
114
+ * Tracks menu updates
115
+ *
116
+ * @action wp_update_nav_menu
117
+ *
118
+ * @param int $menu_id
119
+ * @param array $menu_data
120
+ */
121
+ public function callback_wp_update_nav_menu( $menu_id, $menu_data = array() ) {
122
+ if ( empty( $menu_data ) ) {
123
+ return;
124
+ }
125
+
126
+ $name = $menu_data['menu-name'];
127
+
128
+ $this->log(
129
+ _x( 'Updated menu "%s"', 'Menu name', 'stream' ),
130
+ compact( 'name', 'menu_id', 'menu_data' ),
131
+ $menu_id,
132
+ sanitize_title( $name ),
133
+ 'updated'
134
+ );
135
+ }
136
+
137
+ /**
138
+ * Tracks menu deletion
139
+ *
140
+ * @action delete_nav_menu
141
+ *
142
+ * @param object $term
143
+ * @param int $tt_id
144
+ * @param object $deleted_term
145
+ */
146
+ public function callback_delete_nav_menu( $term, $tt_id, $deleted_term ) {
147
+ unset( $tt_id );
148
+
149
+ $name = $deleted_term->name;
150
+ $menu_id = $term->term_id;
151
+
152
+ $this->log(
153
+ _x( 'Deleted "%s"', 'Menu name', 'stream' ),
154
+ compact( 'name', 'menu_id' ),
155
+ $menu_id,
156
+ sanitize_title( $name ),
157
+ 'deleted'
158
+ );
159
+ }
160
+
161
+ /**
162
+ * Track assignment to menu locations
163
+ *
164
+ * @action update_option_theme_mods_{$stylesheet}
165
+ *
166
+ * @param array $old
167
+ * @param array $new
168
+ */
169
+ public function callback_update_option_theme_mods( $old, $new ) {
170
+ // Disable if we're switching themes
171
+ if ( did_action( 'after_switch_theme' ) ) {
172
+ return;
173
+ }
174
+
175
+ $key = 'nav_menu_locations';
176
+
177
+ if ( ! isset( $new[ $key ] ) ) {
178
+ return; // Switching themes ?
179
+ }
180
+
181
+ if ( $old[ $key ] === $new[ $key ] ) {
182
+ return;
183
+ }
184
+
185
+ $locations = get_registered_nav_menus();
186
+ $old_value = (array) $old[ $key ];
187
+ $new_value = (array) $new[ $key ];
188
+ $changed = array_diff_assoc( $old_value, $new_value ) + array_diff_assoc( $new_value, $old_value );
189
+
190
+ if ( ! $changed ) {
191
+ return;
192
+ }
193
+
194
+ foreach ( $changed as $location_id => $menu_id ) {
195
+ $location = $locations[ $location_id ];
196
+
197
+ if ( empty( $new[ $key ][ $location_id ] ) ) {
198
+ $action = 'unassigned';
199
+ $menu_id = isset( $old[ $key ][ $location_id ] ) ? $old[ $key ][ $location_id ] : 0;
200
+ $message = _x(
201
+ '"%1$s" has been unassigned from "%2$s"',
202
+ '1: Menu name, 2: Theme location',
203
+ 'stream'
204
+ );
205
+ } else {
206
+ $action = 'assigned';
207
+ $menu_id = isset( $new[ $key ][ $location_id ] ) ? $new[ $key ][ $location_id ] : 0;
208
+ $message = _x(
209
+ '"%1$s" has been assigned to "%2$s"',
210
+ '1: Menu name, 2: Theme location',
211
+ 'stream'
212
+ );
213
+ }
214
+
215
+ $menu = get_term( $menu_id, 'nav_menu' );
216
+
217
+ if ( ! $menu || is_wp_error( $menu ) ) {
218
+ continue; // This is a deleted menu
219
+ }
220
+
221
+ $name = $menu->name;
222
+
223
+ $this->log(
224
+ $message,
225
+ compact( 'name', 'location', 'location_id', 'menu_id' ),
226
+ $menu_id,
227
+ sanitize_title( $name ),
228
+ $action
229
+ );
230
+ }
231
+ }
232
+ }
connectors/class-connector-posts.php ADDED
@@ -0,0 +1,380 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace WP_Stream;
3
+
4
+ class Connector_Posts extends Connector {
5
+ /**
6
+ * Connector slug
7
+ *
8
+ * @var string
9
+ */
10
+ public $name = 'posts';
11
+
12
+ /**
13
+ * Actions registered for this connector
14
+ *
15
+ * @var array
16
+ */
17
+ public $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 function get_label() {
28
+ return esc_html__( 'Posts', 'stream' );
29
+ }
30
+
31
+ /**
32
+ * Return translated action labels
33
+ *
34
+ * @return array Action label translations
35
+ */
36
+ public function get_action_labels() {
37
+ return array(
38
+ 'updated' => esc_html__( 'Updated', 'stream' ),
39
+ 'created' => esc_html__( 'Created', 'stream' ),
40
+ 'trashed' => esc_html__( 'Trashed', 'stream' ),
41
+ 'untrashed' => esc_html__( 'Restored', 'stream' ),
42
+ 'deleted' => esc_html__( 'Deleted', 'stream' ),
43
+ );
44
+ }
45
+
46
+ /**
47
+ * Return translated context labels
48
+ *
49
+ * @return array Context label translations
50
+ */
51
+ public 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( $this->get_excluded_post_types() ) );
56
+
57
+ add_action( 'registered_post_type', array( $this, '_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 Record $record Stream record
69
+ *
70
+ * @return array Action links
71
+ */
72
+ public function action_links( $links, $record ) {
73
+ $post = get_post( $record->object_id );
74
+
75
+ if ( $post && $post->post_status === $record->get_meta( 'new_status', true ) ) {
76
+ $post_type_name = $this->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( $record->get_meta( 'revision_id', true ) );
111
+ $revision_id = $this->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 function _registered_post_type( $post_type, $args ) {
131
+ unset( $args );
132
+
133
+ $post_type_obj = get_post_type_object( $post_type );
134
+ $label = $post_type_obj->label;
135
+
136
+ wp_stream_get_instance()->connectors->term_labels['stream_context'][ $post_type ] = $label;
137
+ }
138
+
139
+ /**
140
+ * Log all post status changes ( creating / updating / trashing )
141
+ *
142
+ * @action transition_post_status
143
+ *
144
+ * @param mixed $new
145
+ * @param mixed $old
146
+ * @param \WP_Post $post
147
+ */
148
+ public function callback_transition_post_status( $new, $old, $post ) {
149
+ if ( in_array( $post->post_type, $this->get_excluded_post_types() ) ) {
150
+ return;
151
+ }
152
+
153
+ if ( in_array( $new, array( 'auto-draft', 'inherit' ) ) ) {
154
+ return;
155
+ } elseif ( 'draft' === $new && 'publish' === $old ) {
156
+ $summary = _x(
157
+ '"%1$s" %2$s unpublished',
158
+ '1: Post title, 2: Post type singular name',
159
+ 'stream'
160
+ );
161
+ } elseif ( 'trash' === $old && 'trash' !== $new ) {
162
+ $summary = _x(
163
+ '"%1$s" %2$s restored from trash',
164
+ '1: Post title, 2: Post type singular name',
165
+ 'stream'
166
+ );
167
+ $action = 'untrashed';
168
+ } elseif ( 'draft' === $new ) {
169
+ $summary = _x(
170
+ '"%1$s" %2$s drafted',
171
+ '1: Post title, 2: Post type singular name',
172
+ 'stream'
173
+ );
174
+ } elseif ( 'pending' === $new ) {
175
+ $summary = _x(
176
+ '"%1$s" %2$s pending review',
177
+ '1: Post title, 2: Post type singular name',
178
+ 'stream'
179
+ );
180
+ } elseif ( 'future' === $new ) {
181
+ $summary = _x(
182
+ '"%1$s" %2$s scheduled for %3$s',
183
+ '1: Post title, 2: Post type singular name, 3: Scheduled post date',
184
+ 'stream'
185
+ );
186
+ } elseif ( 'future' === $old && 'publish' === $new ) {
187
+ $summary = _x(
188
+ '"%1$s" scheduled %2$s published',
189
+ '1: Post title, 2: Post type singular name',
190
+ 'stream'
191
+ );
192
+ } elseif ( 'publish' === $new ) {
193
+ $summary = _x(
194
+ '"%1$s" %2$s published',
195
+ '1: Post title, 2: Post type singular name',
196
+ 'stream'
197
+ );
198
+ } elseif ( 'private' === $new ) {
199
+ $summary = _x(
200
+ '"%1$s" %2$s privately published',
201
+ '1: Post title, 2: Post type singular name',
202
+ 'stream'
203
+ );
204
+ } elseif ( 'trash' === $new ) {
205
+ $summary = _x(
206
+ '"%1$s" %2$s trashed',
207
+ '1: Post title, 2: Post type singular name',
208
+ 'stream'
209
+ );
210
+ $action = 'trashed';
211
+ } else {
212
+ $summary = _x(
213
+ '"%1$s" %2$s updated',
214
+ '1: Post title, 2: Post type singular name',
215
+ 'stream'
216
+ );
217
+ }
218
+
219
+ if ( 'auto-draft' === $old && 'auto-draft' !== $new ) {
220
+ $action = 'created';
221
+ }
222
+
223
+ if ( empty( $action ) ) {
224
+ $action = 'updated';
225
+ }
226
+
227
+ $revision_id = null;
228
+
229
+ if ( wp_revisions_enabled( $post ) ) {
230
+ $revision = get_children(
231
+ array(
232
+ 'post_type' => 'revision',
233
+ 'post_status' => 'inherit',
234
+ 'post_parent' => $post->ID,
235
+ 'posts_per_page' => 1, // VIP safe
236
+ 'orderby' => 'post_date',
237
+ 'order' => 'DESC',
238
+ )
239
+ );
240
+
241
+ if ( $revision ) {
242
+ $revision = array_values( $revision );
243
+ $revision_id = $revision[0]->ID;
244
+ }
245
+ }
246
+
247
+ $post_type_name = strtolower( $this->get_post_type_name( $post->post_type ) );
248
+
249
+ $this->log(
250
+ $summary,
251
+ array(
252
+ 'post_title' => $post->post_title,
253
+ 'singular_name' => $post_type_name,
254
+ 'post_date' => $post->post_date,
255
+ 'post_date_gmt' => $post->post_date_gmt,
256
+ 'new_status' => $new,
257
+ 'old_status' => $old,
258
+ 'revision_id' => $revision_id,
259
+ ),
260
+ $post->ID,
261
+ $post->post_type,
262
+ $action
263
+ );
264
+ }
265
+
266
+ /**
267
+ * Log post deletion
268
+ *
269
+ * @action deleted_post
270
+ *
271
+ * $param integer $post_id
272
+ */
273
+ public function callback_deleted_post( $post_id ) {
274
+ $post = get_post( $post_id );
275
+
276
+ // We check if post is an instance of WP_Post as it doesn't always resolve in unit testing
277
+ if ( ! ( $post instanceof \WP_Post ) || in_array( $post->post_type, $this->get_excluded_post_types() ) ) {
278
+ return;
279
+ }
280
+
281
+ // Ignore auto-drafts that are deleted by the system, see issue-293
282
+ if ( 'auto-draft' === $post->post_status ) {
283
+ return;
284
+ }
285
+
286
+ $post_type_name = strtolower( $this->get_post_type_name( $post->post_type ) );
287
+
288
+ $this->log(
289
+ _x(
290
+ '"%1$s" %2$s deleted from trash',
291
+ '1: Post title, 2: Post type singular name',
292
+ 'stream'
293
+ ),
294
+ array(
295
+ 'post_title' => $post->post_title,
296
+ 'singular_name' => $post_type_name,
297
+ ),
298
+ $post->ID,
299
+ $post->post_type,
300
+ 'deleted'
301
+ );
302
+ }
303
+
304
+ /**
305
+ * Constructs list of excluded post types for the Posts connector
306
+ *
307
+ * @return array List of excluded post types
308
+ */
309
+ public function get_excluded_post_types() {
310
+ return apply_filters(
311
+ 'wp_stream_posts_exclude_post_types',
312
+ array(
313
+ 'nav_menu_item',
314
+ 'attachment',
315
+ 'revision',
316
+ )
317
+ );
318
+ }
319
+
320
+ /**
321
+ * Gets the singular post type label
322
+ *
323
+ * @param string $post_type_slug
324
+ *
325
+ * @return string Post type label
326
+ */
327
+ public function get_post_type_name( $post_type_slug ) {
328
+ $name = esc_html__( 'Post', 'stream' ); // Default
329
+
330
+ if ( post_type_exists( $post_type_slug ) ) {
331
+ $post_type = get_post_type_object( $post_type_slug );
332
+ $name = $post_type->labels->singular_name;
333
+ }
334
+
335
+ return $name;
336
+ }
337
+
338
+ /**
339
+ * Get an adjacent post revision ID
340
+ *
341
+ * @param int $revision_id
342
+ * @param bool $previous
343
+ *
344
+ * @return int $revision_id
345
+ */
346
+ public function get_adjacent_post_revision( $revision_id, $previous = true ) {
347
+ if ( empty( $revision_id ) || ! wp_is_post_revision( $revision_id ) ) {
348
+ return false;
349
+ }
350
+
351
+ $revision = wp_get_post_revision( $revision_id );
352
+ $operator = ( $previous ) ? '<' : '>';
353
+ $order = ( $previous ) ? 'DESC' : 'ASC';
354
+
355
+ global $wpdb;
356
+
357
+ $revision_id = $wpdb->get_var( // db call okay
358
+ $wpdb->prepare( "
359
+ SELECT p.ID
360
+ FROM $wpdb->posts AS p
361
+ WHERE p.post_date {$operator} %s
362
+ AND p.post_type = 'revision'
363
+ AND p.post_parent = %d
364
+ ORDER BY p.post_date {$order}
365
+ LIMIT 1
366
+ ",
367
+ $revision->post_date,
368
+ $revision->post_parent
369
+ )
370
+ );
371
+
372
+ $revision_id = absint( $revision_id );
373
+
374
+ if ( ! wp_is_post_revision( $revision_id ) ) {
375
+ return false;
376
+ }
377
+
378
+ return $revision_id;
379
+ }
380
+ }
connectors/class-connector-settings.php ADDED
@@ -0,0 +1,742 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace WP_Stream;
3
+
4
+ class Connector_Settings extends Connector {
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 $name = 'settings';
18
+
19
+ /**
20
+ * Actions registered for this connector
21
+ *
22
+ * @var array
23
+ */
24
+ public $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 $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 $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
+ 'blog_count',
71
+ 'user_count',
72
+ 'admin_email',
73
+ 'new_admin_email',
74
+ );
75
+
76
+ /**
77
+ * Register all context hooks
78
+ *
79
+ * @return void
80
+ */
81
+ public function register() {
82
+ parent::register();
83
+
84
+ add_action( 'admin_head', array( $this, 'highlight_field' ) );
85
+ add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_jquery_color' ) );
86
+ add_action( sprintf( 'update_option_theme_mods_%s', get_option( 'stylesheet' ) ), array( $this, 'log_theme_modification' ), 10, 2 );
87
+ }
88
+
89
+ /**
90
+ * @action update_option_theme_mods_{name}
91
+ *
92
+ * @param mixed $old_value
93
+ * @param mixed $new_value
94
+ */
95
+ public function log_theme_modification( $old_value, $new_value ) {
96
+ $this->callback_updated_option( 'theme_mods', $old_value, $new_value );
97
+ }
98
+
99
+ /**
100
+ * Return translated context label
101
+ *
102
+ * @return string Translated context label
103
+ */
104
+ public function get_label() {
105
+ return esc_html__( 'Settings', 'stream' );
106
+ }
107
+
108
+ /**
109
+ * Return translated action labels
110
+ *
111
+ * @return array Action label translations
112
+ */
113
+ public function get_action_labels() {
114
+ return array(
115
+ 'updated' => esc_html__( 'Updated', 'stream' ),
116
+ );
117
+ }
118
+
119
+ /**
120
+ * Return translated context labels
121
+ *
122
+ * @return array Context label translations
123
+ */
124
+ public function get_context_labels() {
125
+ $context_labels = array(
126
+ 'settings' => esc_html__( 'Settings', 'stream' ),
127
+ 'general' => esc_html__( 'General', 'stream' ),
128
+ 'writing' => esc_html__( 'Writing', 'stream' ),
129
+ 'reading' => esc_html__( 'Reading', 'stream' ),
130
+ 'discussion' => esc_html__( 'Discussion', 'stream' ),
131
+ 'media' => esc_html__( 'Media', 'stream' ),
132
+ 'permalink' => esc_html__( 'Permalinks', 'stream' ),
133
+ 'network' => esc_html__( 'Network', 'stream' ),
134
+ 'wp_stream' => esc_html__( 'Stream', 'stream' ),
135
+ 'custom_background' => esc_html__( 'Custom Background', 'stream' ),
136
+ 'custom_header' => esc_html__( 'Custom Header', 'stream' ),
137
+ );
138
+
139
+ if ( is_network_admin() ) {
140
+ $context_labels = array_merge(
141
+ $context_labels,
142
+ array(
143
+ 'wp_stream_network' => esc_html__( 'Stream Network', 'stream' ),
144
+ 'wp_stream_defaults' => esc_html__( 'Stream Defaults', 'stream' ),
145
+ )
146
+ );
147
+ }
148
+
149
+ return $context_labels;
150
+ }
151
+
152
+ /**
153
+ * Return context by option name and key
154
+ *
155
+ * @param string $option_name
156
+ * @param string $key
157
+ *
158
+ * @return string Context slug
159
+ */
160
+ public function get_context_by_key( $option_name, $key ) {
161
+ $contexts = array(
162
+ 'theme_mods' => array(
163
+ 'custom_background' => array(
164
+ 'background_image',
165
+ 'background_position_x',
166
+ 'background_repeat',
167
+ 'background_attachment',
168
+ 'background_color',
169
+ ),
170
+ 'custom_header' => array(
171
+ 'header_image',
172
+ 'header_textcolor',
173
+ ),
174
+ ),
175
+ );
176
+
177
+ if ( isset( $contexts[ $option_name ] ) ) {
178
+ foreach ( $contexts[ $option_name ] as $context => $keys ) {
179
+ if ( in_array( $key, $keys ) ) {
180
+ return $context;
181
+ }
182
+ }
183
+ }
184
+
185
+ return false;
186
+ }
187
+
188
+ /**
189
+ * Find out if the option key should be ignored and not logged
190
+ *
191
+ * @param string $option_name
192
+ * @param string $key
193
+ *
194
+ * @return bool Whether option key is ignored or not
195
+ */
196
+ public function is_key_ignored( $option_name, $key ) {
197
+ $ignored = array(
198
+ 'theme_mods' => array(
199
+ 'background_image_thumb',
200
+ 'header_image_data',
201
+ ),
202
+ );
203
+
204
+ if ( isset( $ignored[ $option_name ] ) ) {
205
+ return in_array( $key, $ignored[ $option_name ] );
206
+ }
207
+
208
+ return false;
209
+ }
210
+
211
+ /**
212
+ * Find out if array keys in the option should be logged separately
213
+ *
214
+ * @param string $key
215
+ * @param mixed $old_value
216
+ * @param mixed $value
217
+ *
218
+ * @return bool Whether the option should be treated as a group
219
+ */
220
+ public function is_key_option_group( $key, $old_value, $value ) {
221
+ if ( ! is_array( $old_value ) && ! is_array( $value ) ) {
222
+ return false;
223
+ }
224
+
225
+ if ( 0 === count( array_filter( array_keys( $value ), 'is_string' ) ) ) {
226
+ return false;
227
+ }
228
+
229
+ return true;
230
+ }
231
+
232
+ /**
233
+ * Return translated labels for all default Settings fields found in WordPress.
234
+ *
235
+ * @param string $field_key
236
+ *
237
+ * @return array Field label translations
238
+ */
239
+ public function get_field_label( $field_key ) {
240
+ $labels = array(
241
+ // General
242
+ 'blogname' => esc_html__( 'Site Title', 'stream' ),
243
+ 'blogdescription' => esc_html__( 'Tagline', 'stream' ),
244
+ 'admin_email' => esc_html__( 'E-mail Address', 'stream' ),
245
+ 'new_admin_email' => esc_html__( 'E-mail Address', 'stream' ),
246
+ 'siteurl' => esc_html__( 'WordPress Address (URL)', 'stream' ),
247
+ 'home' => esc_html__( 'Site Address (URL)', 'stream' ),
248
+ 'users_can_register' => esc_html__( 'Membership', 'stream' ),
249
+ 'default_role' => esc_html__( 'New User Default Role', 'stream' ),
250
+ 'timezone_string' => esc_html__( 'Timezone', 'stream' ),
251
+ 'date_format' => esc_html__( 'Date Format', 'stream' ),
252
+ 'time_format' => esc_html__( 'Time Format', 'stream' ),
253
+ 'start_of_week' => esc_html__( 'Week Starts On', 'stream' ),
254
+ // Writing
255
+ 'use_smilies' => esc_html__( 'Formatting', 'stream' ),
256
+ 'use_balanceTags' => esc_html__( 'Formatting', 'stream' ),
257
+ 'default_category' => esc_html__( 'Default Post Category', 'stream' ),
258
+ 'default_post_format' => esc_html__( 'Default Post Format', 'stream' ),
259
+ 'mailserver_url' => esc_html__( 'Mail Server', 'stream' ),
260
+ 'mailserver_login' => esc_html__( 'Login Name', 'stream' ),
261
+ 'mailserver_pass' => esc_html__( 'Password', 'stream' ),
262
+ 'default_email_category' => esc_html__( 'Default Mail Category', 'stream' ),
263
+ 'ping_sites' => esc_html__( 'Update Services', 'stream' ),
264
+ // Reading
265
+ 'show_on_front' => esc_html__( 'Front page displays', 'stream' ),
266
+ 'page_on_front' => esc_html__( 'Front page displays', 'stream' ),
267
+ 'page_for_posts' => esc_html__( 'Front page displays', 'stream' ),
268
+ 'posts_per_page' => esc_html__( 'Blog pages show at most', 'stream' ),
269
+ 'posts_per_rss' => esc_html__( 'Syndication feeds show the most recent', 'stream' ),
270
+ 'rss_use_excerpt' => esc_html__( 'For each article in a feed, show', 'stream' ),
271
+ 'blog_public' => esc_html__( 'Search Engine Visibility', 'stream' ),
272
+ // Discussion
273
+ 'default_pingback_flag' => esc_html__( 'Default article settings', 'stream' ),
274
+ 'default_ping_status' => esc_html__( 'Default article settings', 'stream' ),
275
+ 'default_comment_status' => esc_html__( 'Default article settings', 'stream' ),
276
+ 'require_name_email' => esc_html__( 'Other comment settings', 'stream' ),
277
+ 'comment_registration' => esc_html__( 'Other comment settings', 'stream' ),
278
+ 'close_comments_for_old_posts' => esc_html__( 'Other comment settings', 'stream' ),
279
+ 'close_comments_days_old' => esc_html__( 'Other comment settings', 'stream' ),
280
+ 'thread_comments' => esc_html__( 'Other comment settings', 'stream' ),
281
+ 'thread_comments_depth' => esc_html__( 'Other comment settings', 'stream' ),
282
+ 'page_comments' => esc_html__( 'Other comment settings', 'stream' ),
283
+ 'comments_per_page' => esc_html__( 'Other comment settings', 'stream' ),
284
+ 'default_comments_page' => esc_html__( 'Other comment settings', 'stream' ),
285
+ 'comment_order' => esc_html__( 'Other comment settings', 'stream' ),
286
+ 'comments_notify' => esc_html__( 'E-mail me whenever', 'stream' ),
287
+ 'moderation_notify' => esc_html__( 'E-mail me whenever', 'stream' ),
288
+ 'comment_moderation' => esc_html__( 'Before a comment appears', 'stream' ),
289
+ 'comment_whitelist' => esc_html__( 'Before a comment appears', 'stream' ),
290
+ 'comment_max_links' => esc_html__( 'Comment Moderation', 'stream' ),
291
+ 'moderation_keys' => esc_html__( 'Comment Moderation', 'stream' ),
292
+ 'blacklist_keys' => esc_html__( 'Comment Blacklist', 'stream' ),
293
+ 'show_avatars' => esc_html__( 'Show Avatars', 'stream' ),
294
+ 'avatar_rating' => esc_html__( 'Maximum Rating', 'stream' ),
295
+ 'avatar_default' => esc_html__( 'Default Avatar', 'stream' ),
296
+ // Media
297
+ 'thumbnail_size_w' => esc_html__( 'Thumbnail size', 'stream' ),
298
+ 'thumbnail_size_h' => esc_html__( 'Thumbnail size', 'stream' ),
299
+ 'thumbnail_crop' => esc_html__( 'Thumbnail size', 'stream' ),
300
+ 'medium_size_w' => esc_html__( 'Medium size', 'stream' ),
301
+ 'medium_size_h' => esc_html__( 'Medium size', 'stream' ),
302
+ 'large_size_w' => esc_html__( 'Large size', 'stream' ),
303
+ 'large_size_h' => esc_html__( 'Large size', 'stream' ),
304
+ 'uploads_use_yearmonth_folders' => esc_html__( 'Uploading Files', 'stream' ),
305
+ // Permalinks
306
+ 'permalink_structure' => esc_html__( 'Permalink Settings', 'stream' ),
307
+ 'category_base' => esc_html__( 'Category base', 'stream' ),
308
+ 'tag_base' => esc_html__( 'Tag base', 'stream' ),
309
+ // Network
310
+ 'registrationnotification' => esc_html__( 'Registration notification', 'stream' ),
311
+ 'registration' => esc_html__( 'Allow new registrations', 'stream' ),
312
+ 'add_new_users' => esc_html__( 'Add New Users', 'stream' ),
313
+ 'menu_items' => esc_html__( 'Enable administration menus', 'stream' ),
314
+ 'upload_space_check_disabled' => esc_html__( 'Site upload space check', 'stream' ),
315
+ 'blog_upload_space' => esc_html__( 'Site upload space', 'stream' ),
316
+ 'upload_filetypes' => esc_html__( 'Upload file types', 'stream' ),
317
+ 'site_name' => esc_html__( 'Network Title', 'stream' ),
318
+ 'first_post' => esc_html__( 'First Post', 'stream' ),
319
+ 'first_page' => esc_html__( 'First Page', 'stream' ),
320
+ 'first_comment' => esc_html__( 'First Comment', 'stream' ),
321
+ 'first_comment_url' => esc_html__( 'First Comment URL', 'stream' ),
322
+ 'first_comment_author' => esc_html__( 'First Comment Author', 'stream' ),
323
+ 'welcome_email' => esc_html__( 'Welcome Email', 'stream' ),
324
+ 'welcome_user_email' => esc_html__( 'Welcome User Email', 'stream' ),
325
+ 'fileupload_maxk' => esc_html__( 'Max upload file size', 'stream' ),
326
+ 'global_terms_enabled' => esc_html__( 'Terms Enabled', 'stream' ),
327
+ 'illegal_names' => esc_html__( 'Banned Names', 'stream' ),
328
+ 'limited_email_domains' => esc_html__( 'Limited Email Registrations', 'stream' ),
329
+ 'banned_email_domains' => esc_html__( 'Banned Email Domains', 'stream' ),
330
+ 'WPLANG' => esc_html__( 'Network Language', 'stream' ),
331
+ 'blog_count' => esc_html__( 'Blog Count', 'stream' ),
332
+ 'user_count' => esc_html__( 'User Count', 'stream' ),
333
+ // Other
334
+ 'wp_stream_db' => esc_html__( 'Stream Database Version', 'stream' ),
335
+ );
336
+
337
+ // These option labels are special and need to change based on multisite context
338
+ if ( is_network_admin() ) {
339
+ $labels['admin_email'] = esc_html__( 'Network Admin Email', 'stream' );
340
+ $labels['new_admin_email'] = esc_html__( 'Network Admin Email', 'stream' );
341
+ }
342
+
343
+ if ( isset( $labels[ $field_key ] ) ) {
344
+ return $labels[ $field_key ];
345
+ }
346
+
347
+ return $field_key;
348
+ }
349
+
350
+ /**
351
+ * Enqueue jQuery Color plugin
352
+ *
353
+ * @action admin_enqueue_scripts
354
+ * @return void
355
+ */
356
+ public function enqueue_jquery_color() {
357
+ wp_enqueue_script( 'jquery-color' );
358
+ }
359
+
360
+ /**
361
+ * Return translated labels for all serialized Settings found in WordPress.
362
+ *
363
+ * @param string $option_name
364
+ * @param string $field_key
365
+ *
366
+ * @return string Field key translation or key itself if not found
367
+ */
368
+ public function get_serialized_field_label( $option_name, $field_key ) {
369
+ $labels = array(
370
+ 'theme_mods' => array(
371
+ // Custom Background
372
+ 'background_image' => esc_html__( 'Background Image', 'stream' ),
373
+ 'background_position_x' => esc_html__( 'Background Position', 'stream' ),
374
+ 'background_repeat' => esc_html__( 'Background Repeat', 'stream' ),
375
+ 'background_attachment' => esc_html__( 'Background Attachment', 'stream' ),
376
+ 'background_color' => esc_html__( 'Background Color', 'stream' ),
377
+ // Custom Header
378
+ 'header_image' => esc_html__( 'Header Image', 'stream' ),
379
+ 'header_textcolor' => esc_html__( 'Text Color', 'stream' ),
380
+ ),
381
+ );
382
+
383
+ /**
384
+ * Filter allows for insertion of serialized labels
385
+ *
386
+ * @param array $lables Serialized labels
387
+ * @return array Updated array of serialzed labels
388
+ */
389
+ $labels = apply_filters( 'wp_stream_serialized_labels', $labels );
390
+
391
+ if ( isset( $labels[ $option_name ] ) && isset( $labels[ $option_name ][ $field_key ] ) ) {
392
+ return $labels[ $option_name ][ $field_key ];
393
+ }
394
+
395
+ return $field_key;
396
+ }
397
+
398
+ /**
399
+ * Add action links to Stream drop row in admin list screen
400
+ *
401
+ * @filter wp_stream_action_links_{connector}
402
+ *
403
+ * @param array $links Previous links registered
404
+ * @param Record $record Stream record
405
+ *
406
+ * @return array Action links
407
+ */
408
+ public function action_links( $links, $record ) {
409
+ $context_labels = $this->get_context_labels();
410
+ $plugin = wp_stream_get_instance();
411
+
412
+ $rules = array(
413
+ 'stream' => array(
414
+ 'menu_slug' => 'wp_stream',
415
+ 'submenu_slug' => $plugin->admin->settings_page_slug,
416
+ 'url' => function( $rule, $record ) use ( $plugin ) {
417
+ $option_key = $record->get_meta( 'option_key', true );
418
+ $url_tab = null;
419
+
420
+ if ( '' !== $option_key ) {
421
+ foreach ( $plugin->settings->get_fields() as $tab_name => $tab_properties ) {
422
+ foreach ( $tab_properties['fields'] as $field ) {
423
+ $field_key = sprintf( '%s_%s', $tab_name, $field['name'] );
424
+ if ( $field_key === $option_key ) {
425
+ $url_tab = $tab_name;
426
+ break 2;
427
+ }
428
+ }
429
+ }
430
+ }
431
+
432
+ return add_query_arg(
433
+ array(
434
+ 'page' => $rule['submenu_slug'],
435
+ 'tab' => $url_tab,
436
+ ),
437
+ admin_url( 'admin.php' )
438
+ );
439
+ },
440
+ 'applicable' => function( $submenu, $record ) {
441
+ return $record->context === 'wp_stream';
442
+ },
443
+ ),
444
+ 'background_header' => array(
445
+ 'menu_slug' => 'themes.php',
446
+ 'submenu_slug' => function( $record ) {
447
+ return str_replace( '_', '-', $record->context );
448
+ },
449
+ 'url' => function( $rule, $record ) {
450
+ return add_query_arg( 'page', $rule['submenu_slug']( $record ), admin_url( $rule['menu_slug'] ) );
451
+ },
452
+ 'applicable' => function( $submenu, $record ) {
453
+ return in_array( $record->context, array( 'custom_header', 'custom_background' ) );
454
+ },
455
+ ),
456
+ 'general' => array(
457
+ 'menu_slug' => 'options-general.php',
458
+ 'submenu_slug' => function( $record ) {
459
+ return sprintf( 'options-%s.php', $record->context );
460
+ },
461
+ 'url' => function( $rule, $record ) {
462
+ return admin_url( $rule['submenu_slug']( $record ) );
463
+ },
464
+ 'applicable' => function( $submenu, $record ) {
465
+ return ! empty( $submenu['options-general.php'] );
466
+ },
467
+ ),
468
+ 'network' => array(
469
+ 'menu_slug' => 'settings.php',
470
+ 'submenu_slug' => function( $record ) {
471
+ return 'settings.php';
472
+ },
473
+ 'url' => function( $rule, $record ) {
474
+ return network_admin_url( $rule['menu_slug'] );
475
+ },
476
+ 'applicable' => function( $submenu, $record ) {
477
+ if ( ! $record->blog_id ) {
478
+ return ! empty( $submenu['settings.php'] );
479
+ }
480
+ return false;
481
+ },
482
+ ),
483
+ );
484
+
485
+ if ( 'settings' !== $record->context && in_array( $record->context, array_keys( $context_labels ) ) ) {
486
+ global $submenu;
487
+
488
+ $applicable_rules = array_filter(
489
+ $rules,
490
+ function( $rule ) use ( $submenu, $record ) {
491
+ return call_user_func( $rule['applicable'], $submenu, $record );
492
+ }
493
+ );
494
+
495
+ if ( ! empty( $applicable_rules ) ) {
496
+ // The first applicable rule wins
497
+ $rule = array_shift( $applicable_rules );
498
+ $menu_slug = $rule['menu_slug'];
499
+ $submenu_slug = ( is_object( $rule['submenu_slug'] ) && $rule['submenu_slug'] instanceof Closure ? $rule['submenu_slug']( $record ) : $rule['submenu_slug'] );
500
+ $url = $rule['url']( $rule, $record );
501
+
502
+ if ( isset( $submenu[ $menu_slug ] ) ) {
503
+ $found_submenus = wp_list_filter(
504
+ $submenu[ $menu_slug ],
505
+ array( 2 => $submenu_slug )
506
+ );
507
+ }
508
+
509
+ if ( ! empty( $found_submenus ) ) {
510
+ $target_submenu = array_pop( $found_submenus );
511
+ list( $menu_title, $capability ) = $target_submenu;
512
+
513
+ if ( current_user_can( $capability ) ) {
514
+ $url = apply_filters( 'wp_stream_action_link_url', $url, $record );
515
+ $text = sprintf( esc_html__( 'Edit %s Settings', 'stream' ), $context_labels[ $record->context ] );
516
+ $field_name = $record->get_meta( 'option_key', true );
517
+
518
+ if ( '' === $field_name ) {
519
+ $field_name = $record->get_meta( 'option', true );
520
+ }
521
+
522
+ if ( '' !== $field_name ) {
523
+ $url = sprintf( '%s#%s%s', rtrim( preg_replace( '/#.*/', '', $url ), '/' ), self::HIGHLIGHT_FIELD_URL_HASH_PREFIX, $field_name );
524
+ }
525
+
526
+ $links[ $text ] = $url;
527
+ }
528
+ }
529
+ }
530
+ }
531
+
532
+ return $links;
533
+ }
534
+
535
+ /**
536
+ * Trigger this connector core tracker, only on options.php page
537
+ *
538
+ * @action whitelist_options
539
+ *
540
+ * @param array $options
541
+ *
542
+ * @return array
543
+ */
544
+ public function callback_whitelist_options( $options ) {
545
+ add_action( 'updated_option', array( $this, 'callback' ), 10, 3 );
546
+
547
+ return $options;
548
+ }
549
+
550
+ /**
551
+ * Trigger this connector core tracker, only on options-permalink.php page
552
+ *
553
+ * @action update_option_permalink_structure
554
+ *
555
+ * @param mixed $old_value
556
+ * @param mixed $value
557
+ *
558
+ */
559
+ public function callback_update_option_permalink_structure( $old_value, $value ) {
560
+ $this->callback_updated_option( 'permalink_structure', $old_value, $value );
561
+ }
562
+
563
+ /**
564
+ * Trigger this connector core tracker, only on network/settings.php page
565
+ *
566
+ * @action update_site_option
567
+ *
568
+ * @param string $option
569
+ * @param mixed $old_value
570
+ * @param mixed $value
571
+ */
572
+ public function callback_update_site_option( $option, $value, $old_value ) {
573
+ $this->callback_updated_option( $option, $value, $old_value );
574
+ }
575
+
576
+ /**
577
+ * Trigger this connector core tracker, only on options-permalink.php page
578
+ *
579
+ * @action update_option_category_base
580
+ *
581
+ * @param mixed $old_value
582
+ * @param mixed $value
583
+ */
584
+ public function callback_update_option_category_base( $old_value, $value ) {
585
+ $this->callback_updated_option( 'category_base', $old_value, $value );
586
+ }
587
+
588
+ /**
589
+ * Trigger this connector core tracker, only on options-permalink.php page
590
+ *
591
+ * @action update_option_tag_base
592
+ *
593
+ * @param mixed $old_value
594
+ * @param mixed $value
595
+ */
596
+ public function callback_update_option_tag_base( $old_value, $value ) {
597
+ $this->callback_updated_option( 'tag_base', $old_value, $value );
598
+ }
599
+
600
+ /**
601
+ * Track updated settings
602
+ *
603
+ * @action updated_option
604
+ *
605
+ * @param string $option
606
+ * @param mixed $old_value
607
+ * @param mixed $value
608
+ */
609
+ public function callback_updated_option( $option, $old_value, $value ) {
610
+ global $whitelist_options, $new_whitelist_options;
611
+
612
+ if ( 0 === strpos( $option, '_transient_' ) || 0 === strpos( $option, '_site_transient_' ) ) {
613
+ return;
614
+ }
615
+
616
+ $options = array_merge(
617
+ (array) $whitelist_options,
618
+ (array) $new_whitelist_options,
619
+ array( 'permalink' => $this->permalink_options ),
620
+ array( 'network' => $this->network_options )
621
+ );
622
+
623
+ foreach ( $options as $key => $opts ) {
624
+ if ( in_array( $option, $opts ) ) {
625
+ $context = $key;
626
+ break;
627
+ }
628
+ }
629
+
630
+ if ( ! isset( $context ) ) {
631
+ $context = 'settings';
632
+ }
633
+
634
+ $changed_options = array();
635
+ $option_group = $this->is_key_option_group( $option, $old_value, $value );
636
+
637
+ if ( $option_group ) {
638
+ foreach ( $this->get_changed_keys( $old_value, $value ) as $field_key ) {
639
+ if ( ! $this->is_key_ignored( $option, $field_key ) ) {
640
+ $key_context = $this->get_context_by_key( $option, $field_key );
641
+ $changed_options[] = array(
642
+ 'label' => $this->get_serialized_field_label( $option, $field_key ),
643
+ 'option' => $option,
644
+ 'option_key' => $field_key,
645
+ 'context' => ( false !== $key_context ? $key_context : $context ),
646
+ // Prevent fatal error when saving option as array
647
+ 'old_value' => isset( $old_value[ $field_key ] ) ? (string) maybe_serialize( $old_value[ $field_key ] ) : null,
648
+ 'value' => isset( $value[ $field_key ] ) ? (string) maybe_serialize( $value[ $field_key ] ) : null,
649
+ );
650
+ }
651
+ }
652
+ } else {
653
+ $changed_options[] = array(
654
+ 'label' => $this->get_field_label( $option ),
655
+ 'option' => $option,
656
+ 'context' => $context,
657
+ // Prevent fatal error when saving option as array
658
+ 'old_value' => (string) maybe_serialize( $old_value ),
659
+ 'value' => (string) maybe_serialize( $value ),
660
+ );
661
+ }
662
+
663
+ foreach ( $changed_options as $properties ) {
664
+ $this->log(
665
+ __( '"%s" setting was updated', 'stream' ),
666
+ $properties,
667
+ null,
668
+ $properties['context'],
669
+ 'updated'
670
+ );
671
+ }
672
+ }
673
+
674
+ /**
675
+ * Add class to highlight field by URL param
676
+ *
677
+ * @action admin_head
678
+ */
679
+ public function highlight_field() {
680
+ ?>
681
+ <script>
682
+ (function ($) {
683
+ $(function () {
684
+ var hashPrefix = <?php echo wp_stream_json_encode( self::HIGHLIGHT_FIELD_URL_HASH_PREFIX ) // xss ok ?>,
685
+ hashFieldName = "",
686
+ fieldNames = [],
687
+ $select2Choices = {},
688
+ $field = {};
689
+
690
+ if (location.hash.substr(1, hashPrefix.length) === hashPrefix) {
691
+ hashFieldName = location.hash.substr(hashPrefix.length + 1);
692
+ fieldNames = [hashFieldName];
693
+
694
+ $field = $("input, textarea, select").filter(function () {
695
+ return fieldNames.indexOf( $(this).attr("name") ) > -1;
696
+ });
697
+
698
+ // try to find wp_stream field
699
+ if ( $field.length === 0 ) {
700
+ fieldNames = [
701
+ "wp_stream_" + hashFieldName,
702
+ "wp_stream[" + hashFieldName + "]"
703
+ ];
704
+
705
+ $field = $("input, textarea, select, div").filter(function () {
706
+ return fieldNames.indexOf( $(this).attr("id") ) > -1;
707
+ });
708
+
709
+ // if the field has been selectified, the list is the one to be colorized
710
+ $select2Choices = $field.find(".select2-choices");
711
+ if ( $select2Choices.length === 1 ) {
712
+ $field = $select2Choices;
713
+ }
714
+ }
715
+
716
+ $("html, body")
717
+ .animate({
718
+ scrollTop: ($field.closest("tr").length === 1 ? $field.closest("tr") : $field).offset().top - $("#wpadminbar").height()
719
+ }, 1000, function () {
720
+
721
+ $field
722
+ .css("background", $(this).css("background-color"))
723
+ .animate({
724
+ backgroundColor: "#fffedf",
725
+ }, 250);
726
+
727
+ $("label")
728
+ .filter(function () {
729
+ return fieldNames.indexOf( $(this).attr("for") ) > -1;
730
+ })
731
+ .animate({
732
+ color: "#d54e21"
733
+ }, 250);
734
+ }
735
+ );
736
+ }
737
+ });
738
+ }(jQuery));
739
+ </script>
740
+ <?php
741
+ }
742
+ }
connectors/class-connector-taxonomies.php ADDED
@@ -0,0 +1,253 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace WP_Stream;
3
+
4
+ class Connector_Taxonomies extends Connector {
5
+ /**
6
+ * Connector slug
7
+ *
8
+ * @var string
9
+ */
10
+ public $name = 'taxonomies';
11
+
12
+ /**
13
+ * Actions registered for this connector
14
+ *
15
+ * @var array
16
+ */
17
+ public $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 $cached_term_before_update;
30
+
31
+ /**
32
+ * Cache taxonomy labels
33
+ *
34
+ * @var array
35
+ */
36
+ public $context_labels;
37
+
38
+ /**
39
+ * Return translated connector label
40
+ *
41
+ * @return string Translated connector label
42
+ */
43
+ public function get_label() {
44
+ return esc_html__( 'Taxonomies', 'stream' );
45
+ }
46
+
47
+ /**
48
+ * Return translated action labels
49
+ *
50
+ * @return array Action label translations
51
+ */
52
+ public function get_action_labels() {
53
+ return array(
54
+ 'created' => esc_html__( 'Created', 'stream' ),
55
+ 'updated' => esc_html__( 'Updated', 'stream' ),
56
+ 'deleted' => esc_html__( 'Deleted', 'stream' ),
57
+ );
58
+ }
59
+
60
+ /**
61
+ * Return translated context labels
62
+ *
63
+ * @return array Context label translations
64
+ */
65
+ public function get_context_labels() {
66
+ global $wp_taxonomies;
67
+
68
+ $labels = wp_list_pluck( $wp_taxonomies, 'labels' );
69
+
70
+ $this->context_labels = wp_list_pluck( $labels, 'singular_name' );
71
+
72
+ add_action( 'registered_taxonomy', array( $this, '_registered_taxonomy' ), 10, 3 );
73
+
74
+ return $this->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 Record $record Stream record
84
+ *
85
+ * @return array Action links
86
+ */
87
+ public function action_links( $links, $record ) {
88
+ if (
89
+ $record->object_id
90
+ &&
91
+ 'deleted' !== $record->action
92
+ &&
93
+ ( $term = get_term_by( 'term_taxonomy_id', $record->object_id, $record->context ) ) // wpcom_vip_get_term_by() does not indicate support for `term_taxonomy_id`
94
+ ) {
95
+ if ( ! is_wp_error( $term ) ) {
96
+ $tax_obj = get_taxonomy( $term->taxonomy );
97
+ $tax_label = isset( $tax_obj->labels->singular_name ) ? $tax_obj->labels->singular_name : null;
98
+
99
+ if ( function_exists( 'wp_get_split_term' ) ) {
100
+ $term_id = wp_get_split_term( $term->term_id, $term->taxonomy );
101
+ }
102
+
103
+ $term_id = empty( $term_id ) ? $term->term_id : $term_id;
104
+
105
+ $links[ sprintf( _x( 'Edit %s', 'Term singular name', 'stream' ), $tax_label ) ] = get_edit_term_link( $term_id, $term->taxonomy );
106
+ $links[ esc_html__( 'View', 'stream' ) ] = wp_stream_is_vip() ? \wpcom_vip_get_term_link( $term_id, $term->taxonomy ) : get_term_link( $term_id, $term->taxonomy );
107
+ }
108
+ }
109
+
110
+ return $links;
111
+ }
112
+
113
+ /**
114
+ * Catch registration of taxonomies after inital loading, so we can cache its labels
115
+ *
116
+ * @action registered_taxonomy
117
+ *
118
+ * @param string $taxonomy Taxonomy slug
119
+ * @param array|string $object_type Object type or array of object types
120
+ * @param array|string $args Array or string of taxonomy registration arguments
121
+ */
122
+ public function _registered_taxonomy( $taxonomy, $object_type, $args ) {
123
+ unset( $object_type );
124
+
125
+ $taxonomy_obj = (object) $args;
126
+ $label = get_taxonomy_labels( $taxonomy_obj )->name;
127
+
128
+ $this->context_labels[ $taxonomy ] = $label;
129
+
130
+ wp_stream_get_instance()->connectors->term_labels['stream_context'][ $taxonomy ] = $label;
131
+ }
132
+
133
+ /**
134
+ * Tracks creation of terms
135
+ *
136
+ * @action created_term
137
+ *
138
+ * @param integer $term_id
139
+ * @param integer $tt_id
140
+ * @param string $taxonomy
141
+ */
142
+ public function callback_created_term( $term_id, $tt_id, $taxonomy ) {
143
+ if ( in_array( $taxonomy, $this->get_excluded_taxonomies() ) ) {
144
+ return;
145
+ }
146
+
147
+ $term = get_term( $term_id, $taxonomy );
148
+ $term_name = $term->name;
149
+ $taxonomy_label = strtolower( $this->context_labels[ $taxonomy ] );
150
+ $term_parent = $term->parent;
151
+
152
+ $this->log(
153
+ _x(
154
+ '"%1$s" %2$s created',
155
+ '1: Term name, 2: Taxonomy singular label',
156
+ 'stream'
157
+ ),
158
+ compact( 'term_name', 'taxonomy_label', 'term_id', 'taxonomy', 'term_parent' ),
159
+ $tt_id,
160
+ $taxonomy,
161
+ 'created'
162
+ );
163
+ }
164
+
165
+ /**
166
+ * Tracks deletion of taxonomy terms
167
+ *
168
+ * @action delete_term
169
+ *
170
+ * @param integer $term_id
171
+ * @param integer $tt_id
172
+ * @param string $taxonomy
173
+ * @param object $deleted_term
174
+ */
175
+ public function callback_delete_term( $term_id, $tt_id, $taxonomy, $deleted_term ) {
176
+ if ( in_array( $taxonomy, $this->get_excluded_taxonomies() ) ) {
177
+ return;
178
+ }
179
+
180
+ $term_name = $deleted_term->name;
181
+ $term_parent = $deleted_term->parent;
182
+ $taxonomy_label = strtolower( $this->context_labels[ $taxonomy ] );
183
+
184
+ $this->log(
185
+ _x(
186
+ '"%1$s" %2$s deleted',
187
+ '1: Term name, 2: Taxonomy singular label',
188
+ 'stream'
189
+ ),
190
+ compact( 'term_name', 'taxonomy_label', 'term_id', 'taxonomy', 'term_parent' ),
191
+ $tt_id,
192
+ $taxonomy,
193
+ 'deleted'
194
+ );
195
+ }
196
+
197
+ /**
198
+ * Tracks updates of taxonomy terms
199
+ *
200
+ * @action edit_term
201
+ *
202
+ * @param integer $term_id
203
+ * @param integer $tt_id
204
+ * @param string $taxonomy
205
+ */
206
+ public function callback_edit_term( $term_id, $tt_id, $taxonomy ) {
207
+ unset( $tt_id );
208
+ $this->cached_term_before_update = get_term( $term_id, $taxonomy );
209
+ }
210
+
211
+ public function callback_edited_term( $term_id, $tt_id, $taxonomy ) {
212
+ if ( in_array( $taxonomy, $this->get_excluded_taxonomies() ) ) {
213
+ return;
214
+ }
215
+
216
+ $term = $this->cached_term_before_update;
217
+
218
+ if ( ! $term ) { // For some reason!
219
+ $term = get_term( $term_id, $taxonomy );
220
+ }
221
+
222
+ $term_name = $term->name;
223
+ $taxonomy_label = strtolower( $this->context_labels[ $taxonomy ] );
224
+ $term_parent = $term->parent;
225
+
226
+ $this->log(
227
+ _x(
228
+ '"%1$s" %2$s updated',
229
+ '1: Term name, 2: Taxonomy singular label',
230
+ 'stream'
231
+ ),
232
+ compact( 'term_name', 'taxonomy_label', 'term_id', 'taxonomy', 'term_parent' ),
233
+ $tt_id,
234
+ $taxonomy,
235
+ 'updated'
236
+ );
237
+ }
238
+
239
+ /**
240
+ * Constructs list of excluded taxonomies for the Taxonomies connector
241
+ *
242
+ * @return array List of excluded taxonomies
243
+ */
244
+ public function get_excluded_taxonomies() {
245
+ return apply_filters(
246
+ 'wp_stream_taxonomies_exclude_taxonomies',
247
+ array(
248
+ 'nav_menu',
249
+ )
250
+ );
251
+ }
252
+
253
+ }
connectors/class-connector-users.php ADDED
@@ -0,0 +1,360 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace WP_Stream;
3
+
4
+ class Connector_Users extends Connector {
5
+
6
+ /**
7
+ * Connector slug
8
+ *
9
+ * @var string
10
+ */
11
+ public $name = 'users';
12
+
13
+ /**
14
+ * Stores users object before the user being deleted.
15
+ */
16
+ protected $_users_object_pre_deleted = array();
17
+
18
+ /**
19
+ * Actions registered for this connector
20
+ *
21
+ * @var array
22
+ */
23
+ public $actions = array(
24
+ 'user_register',
25
+ 'profile_update',
26
+ 'password_reset',
27
+ 'retrieve_password',
28
+ 'set_logged_in_cookie',
29
+ 'clear_auth_cookie',
30
+ 'delete_user',
31
+ 'deleted_user',
32
+ 'set_user_role',
33
+ );
34
+
35
+ /**
36
+ * Return translated connector label
37
+ *
38
+ * @return string Translated connector label
39
+ */
40
+ public function get_label() {
41
+ return esc_html__( 'Users', 'stream' );
42
+ }
43
+
44
+ /**
45
+ * Return translated action term labels
46
+ *
47
+ * @return array Action terms label translation
48
+ */
49
+ public function get_action_labels() {
50
+ return array(
51
+ 'updated' => esc_html__( 'Updated', 'stream' ),
52
+ 'created' => esc_html__( 'Created', 'stream' ),
53
+ 'deleted' => esc_html__( 'Deleted', 'stream' ),
54
+ 'password-reset' => esc_html__( 'Password Reset', 'stream' ),
55
+ 'forgot-password' => esc_html__( 'Lost Password', 'stream' ),
56
+ 'login' => esc_html__( 'Log In', 'stream' ),
57
+ 'logout' => esc_html__( 'Log Out', 'stream' ),
58
+ );
59
+ }
60
+
61
+ /**
62
+ * Return translated context labels
63
+ *
64
+ * @return array Context label translations
65
+ */
66
+ public function get_context_labels() {
67
+ return array(
68
+ 'users' => esc_html__( 'Users', 'stream' ),
69
+ 'sessions' => esc_html__( 'Sessions', 'stream' ),
70
+ 'profiles' => esc_html__( 'Profiles', 'stream' ),
71
+ );
72
+ }
73
+
74
+ /**
75
+ * Add action links to Stream drop row in admin list screen
76
+ *
77
+ * @filter wp_stream_action_links_{connector}
78
+ *
79
+ * @param array $links Previous links registered
80
+ * @param Record $record Stream record
81
+ *
82
+ * @return array Action links
83
+ */
84
+ public function action_links( $links, $record ) {
85
+ if ( $record->object_id ) {
86
+ if ( $link = get_edit_user_link( $record->object_id ) ) {
87
+ $links [ esc_html__( 'Edit User', 'stream' ) ] = $link;
88
+ }
89
+ }
90
+
91
+ return $links;
92
+ }
93
+
94
+ /**
95
+ * Get an array of role lables assigned to a specific user.
96
+ *
97
+ * @param object|int $user User object or user ID to get roles for
98
+ *
99
+ * @return array $labels An array of role labels
100
+ */
101
+ public function get_role_labels( $user ) {
102
+ if ( is_int( $user ) ) {
103
+ $user = get_user_by( 'id', $user );
104
+ }
105
+
106
+ if ( ! is_a( $user, 'WP_User' ) ) {
107
+ return array();
108
+ }
109
+
110
+ global $wp_roles;
111
+
112
+ $roles = $wp_roles->get_names();
113
+ $labels = array();
114
+
115
+ foreach ( $roles as $role => $label ) {
116
+ if ( in_array( $role, (array) $user->roles ) ) {
117
+ $labels[] = translate_user_role( $label );
118
+ }
119
+ }
120
+
121
+ return $labels;
122
+ }
123
+
124
+ /**
125
+ * Log user registrations
126
+ *
127
+ * @action user_register
128
+ *
129
+ * @param int $user_id Newly registered user ID
130
+ */
131
+ public 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 = esc_html__( '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
+ $this->log(
148
+ $message,
149
+ array(
150
+ 'display_name' => ( $registered_user->display_name ) ? $registered_user->display_name : $registered_user->user_login,
151
+ 'roles' => implode( ', ', $this->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
+ * @param int $user_id registered user ID
166
+ * @param \WP_User $user registered user object
167
+ */
168
+ public function callback_profile_update( $user_id, $user ) {
169
+ unset( $user_id );
170
+
171
+ $this->log(
172
+ __( '%s\'s profile was updated', 'stream' ),
173
+ array(
174
+ 'display_name' => $user->display_name,
175
+ ),
176
+ $user->ID,
177
+ 'profiles',
178
+ 'updated'
179
+ );
180
+ }
181
+
182
+ /**
183
+ * Log role transition
184
+ *
185
+ * @action set_user_role
186
+ *
187
+ * @param int $user_id
188
+ * @param string $new_role
189
+ * @param array $old_roles
190
+ */
191
+ public function callback_set_user_role( $user_id, $new_role, $old_roles ) {
192
+ if ( empty( $old_roles ) ) {
193
+ return;
194
+ }
195
+
196
+ global $wp_roles;
197
+
198
+ $this->log(
199
+ _x(
200
+ '%1$s\'s role was changed from %2$s to %3$s',
201
+ '1: User display name, 2: Old role, 3: New role',
202
+ 'stream'
203
+ ),
204
+ array(
205
+ 'display_name' => get_user_by( 'id', $user_id )->display_name,
206
+ 'old_role' => translate_user_role( $wp_roles->role_names[ $old_roles[0] ] ),
207
+ 'new_role' => translate_user_role( $wp_roles->role_names[ $new_role ] ),
208
+ ),
209
+ $user_id,
210
+ 'profiles',
211
+ 'updated'
212
+ );
213
+ }
214
+
215
+ /**
216
+ * Log password reset
217
+ *
218
+ * @action password_reset
219
+ *
220
+ * @param \WP_User $user
221
+ */
222
+ public function callback_password_reset( $user ) {
223
+ $this->log(
224
+ __( '%s\'s password was reset', 'stream' ),
225
+ array(
226
+ 'email' => $user->display_name,
227
+ ),
228
+ $user->ID,
229
+ 'profiles',
230
+ 'password-reset',
231
+ $user->ID
232
+ );
233
+ }
234
+
235
+ /**
236
+ * Log user requests to retrieve passwords
237
+ *
238
+ * @action retrieve_password
239
+ *
240
+ * @param string $user_login
241
+ */
242
+ public function callback_retrieve_password( $user_login ) {
243
+ if ( wp_stream_filter_var( $user_login, FILTER_VALIDATE_EMAIL ) ) {
244
+ $user = get_user_by( 'email', $user_login );
245
+ } else {
246
+ $user = get_user_by( 'login', $user_login );
247
+ }
248
+
249
+ $this->log(
250
+ __( '%s\'s password was requested to be reset', 'stream' ),
251
+ array( 'display_name' => $user->display_name ),
252
+ $user->ID,
253
+ 'sessions',
254
+ 'forgot-password',
255
+ $user->ID
256
+ );
257
+ }
258
+
259
+ /**
260
+ * Log user login
261
+ *
262
+ * @action set_logged_in_cookie
263
+ *
264
+ * @param string $logged_in_cookie
265
+ * @param int $expire
266
+ * @param int $expiration
267
+ * @param int $user_id
268
+ */
269
+ public function callback_set_logged_in_cookie( $logged_in_cookie, $expire, $expiration, $user_id ) {
270
+ unset( $logged_in_cookie );
271
+ unset( $expire );
272
+ unset( $expiration );
273
+ $user = get_user_by( 'id', $user_id );
274
+
275
+ $this->log(
276
+ __( '%s logged in', 'stream' ),
277
+ array( 'display_name' => $user->display_name ),
278
+ $user->ID,
279
+ 'sessions',
280
+ 'login',
281
+ $user->ID
282
+ );
283
+ }
284
+
285
+ /**
286
+ * Log user logout
287
+ *
288
+ * @action clear_auth_cookie
289
+ */
290
+ public function callback_clear_auth_cookie() {
291
+ $user = wp_get_current_user();
292
+
293
+ // For some reason, incognito mode calls clear_auth_cookie on failed login attempts
294
+ if ( empty( $user ) || ! $user->exists() ) {
295
+ return;
296
+ }
297
+
298
+ $this->log(
299
+ __( '%s logged out', 'stream' ),
300
+ array( 'display_name' => $user->display_name ),
301
+ $user->ID,
302
+ 'sessions',
303
+ 'logout',
304
+ $user->ID
305
+ );
306
+ }
307
+
308
+ /**
309
+ * There's no logging in this callback's action, the reason
310
+ * behind this hook is so that we can store user objects before
311
+ * being deleted. During `deleted_user` hook, our callback
312
+ * receives $user_id param but it's useless as the user record
313
+ * was already removed from DB.
314
+ *
315
+ * @action delete_user
316
+ * @param int $user_id User ID that maybe deleted
317
+ */
318
+ public function callback_delete_user( $user_id ) {
319
+ if ( ! isset( $this->_users_object_pre_deleted[ $user_id ] ) ) {
320
+ $this->_users_object_pre_deleted[ $user_id ] = get_user_by( 'id', $user_id );
321
+ }
322
+ }
323
+
324
+ /**
325
+ * Log deleted user.
326
+ *
327
+ * @action deleted_user
328
+ * @param int $user_id Deleted user ID
329
+ */
330
+ public function callback_deleted_user( $user_id ) {
331
+ $user = wp_get_current_user();
332
+
333
+ if ( isset( $this->_users_object_pre_deleted[ $user_id ] ) ) {
334
+ $message = _x(
335
+ '%1$s\'s account was deleted (%2$s)',
336
+ '1: User display name, 2: User roles',
337
+ 'stream'
338
+ );
339
+ $display_name = $this->_users_object_pre_deleted[ $user_id ]->display_name;
340
+ $deleted_user = $this->_users_object_pre_deleted[ $user_id ];
341
+ unset( $this->_users_object_pre_deleted[ $user_id ] );
342
+ } else {
343
+ $message = esc_html__( 'User account #%d was deleted', 'stream' );
344
+ $display_name = $user_id;
345
+ $deleted_user = $user_id;
346
+ }
347
+
348
+ $this->log(
349
+ $message,
350
+ array(
351
+ 'display_name' => $display_name,
352
+ 'roles' => implode( ', ', $this->get_role_labels( $deleted_user ) ),
353
+ ),
354
+ $user_id,
355
+ 'users',
356
+ 'deleted',
357
+ $user->ID
358
+ );
359
+ }
360
+ }
connectors/class-connector-widgets.php ADDED
@@ -0,0 +1,814 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace WP_Stream;
3
+
4
+ class Connector_Widgets extends Connector {
5
+
6
+ /**
7
+ * Whether or not 'created' and 'deleted' actions should be logged. Normally
8
+ * the sidebar 'added' and 'removed' actions will correspond with these.
9
+ * See note below with usage.
10
+ *
11
+ * @var bool
12
+ */
13
+ public $verbose_widget_created_deleted_actions = false;
14
+
15
+ /**
16
+ * Connector slug
17
+ *
18
+ * @var string
19
+ */
20
+ public $name = 'widgets';
21
+
22
+ /**
23
+ * Actions registered for this connector
24
+ *
25
+ * @var array
26
+ */
27
+ public $actions = array(
28
+ 'update_option_sidebars_widgets',
29
+ 'updated_option',
30
+ );
31
+
32
+ /**
33
+ * Store the initial sidebars_widgets option when the customizer does its
34
+ * multiple rounds of saving to the sidebars_widgets option.
35
+ *
36
+ * @var array
37
+ */
38
+ protected $customizer_initial_sidebars_widgets = null;
39
+
40
+ /**
41
+ * Return translated connector label
42
+ *
43
+ * @return string Translated connector label
44
+ */
45
+ public function get_label() {
46
+ return esc_html__( 'Widgets', 'stream' );
47
+ }
48
+
49
+ /**
50
+ * Return translated action labels
51
+ *
52
+ * @return array Action label translations
53
+ */
54
+ public function get_action_labels() {
55
+ return array(
56
+ 'added' => esc_html__( 'Added', 'stream' ),
57
+ 'removed' => esc_html__( 'Removed', 'stream' ),
58
+ 'moved' => esc_html__( 'Moved', 'stream' ),
59
+ 'created' => esc_html__( 'Created', 'stream' ),
60
+ 'deleted' => esc_html__( 'Deleted', 'stream' ),
61
+ 'deactivated' => esc_html__( 'Deactivated', 'stream' ),
62
+ 'reactivated' => esc_html__( 'Reactivated', 'stream' ),
63
+ 'updated' => esc_html__( 'Updated', 'stream' ),
64
+ 'sorted' => esc_html__( 'Sorted', 'stream' ),
65
+ );
66
+ }
67
+
68
+ /**
69
+ * Return translated context labels
70
+ *
71
+ * @return array Context label translations
72
+ */
73
+ public function get_context_labels() {
74
+ global $wp_registered_sidebars;
75
+
76
+ $labels = array();
77
+
78
+ foreach ( $wp_registered_sidebars as $sidebar ) {
79
+ $labels[ $sidebar['id'] ] = $sidebar['name'];
80
+ }
81
+
82
+ $labels['wp_inactive_widgets'] = esc_html__( 'Inactive Widgets', 'stream' );
83
+ $labels['orphaned_widgets'] = esc_html__( 'Orphaned Widgets', 'stream' );
84
+
85
+ return $labels;
86
+ }
87
+
88
+ /**
89
+ * Add action links to Stream drop row in admin list screen
90
+ *
91
+ * @filter wp_stream_action_links_{connector}
92
+ *
93
+ * @param array $links Previous links registered
94
+ * @param Record $record Stream record
95
+ *
96
+ * @return array Action links
97
+ */
98
+ public function action_links( $links, $record ) {
99
+ if ( $sidebar = $record->get_meta( 'sidebar_id', true ) ) {
100
+ global $wp_registered_sidebars;
101
+
102
+ if ( array_key_exists( $sidebar, $wp_registered_sidebars ) ) {
103
+ $links[ esc_html__( 'Edit Widget Area', 'stream' ) ] = admin_url( 'widgets.php#' . $sidebar ); // xss ok (@todo fix WPCS rule)
104
+ }
105
+ // @todo Also old_sidebar_id and new_sidebar_id
106
+ // @todo Add Edit Widget link
107
+ }
108
+
109
+ return $links;
110
+ }
111
+
112
+ /**
113
+ * Tracks addition/deletion/reordering/deactivation of widgets from sidebars
114
+ *
115
+ * @action update_option_sidebars_widgets
116
+ *
117
+ * @param array $old Old sidebars widgets
118
+ * @param array $new New sidebars widgets
119
+ *
120
+ * @return void
121
+ */
122
+ public function callback_update_option_sidebars_widgets( $old, $new ) {
123
+ // Disable listener if we're switching themes
124
+ if ( did_action( 'after_switch_theme' ) ) {
125
+ return;
126
+ }
127
+
128
+ if ( did_action( 'customize_save' ) ) {
129
+ if ( is_null( $this->customizer_initial_sidebars_widgets ) ) {
130
+ $this->customizer_initial_sidebars_widgets = $old;
131
+ add_action( 'customize_save_after', array( $this, '_callback_customize_save_after' ) );
132
+ }
133
+ } else {
134
+ $this->handle_sidebars_widgets_changes( $old, $new );
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Since the sidebars_widgets may get updated multiple times when saving
140
+ * changes to Widgets in the Customizer, defer handling the changes until
141
+ * customize_save_after.
142
+ *
143
+ * @see callback_update_option_sidebars_widgets()
144
+ */
145
+ public function _callback_customize_save_after() {
146
+ $old_sidebars_widgets = $this->customizer_initial_sidebars_widgets;
147
+ $new_sidebars_widgets = get_option( 'sidebars_widgets' );
148
+
149
+ $this->handle_sidebars_widgets_changes( $old_sidebars_widgets, $new_sidebars_widgets );
150
+ }
151
+
152
+ /**
153
+ * @param array $old
154
+ * @param array $new
155
+ */
156
+ protected function handle_sidebars_widgets_changes( $old, $new ) {
157
+ unset( $old['array_version'] );
158
+ unset( $new['array_version'] );
159
+
160
+ if ( $old === $new ) {
161
+ return;
162
+ }
163
+
164
+ $this->handle_deactivated_widgets( $old, $new );
165
+ $this->handle_reactivated_widgets( $old, $new );
166
+ $this->handle_widget_removal( $old, $new );
167
+ $this->handle_widget_addition( $old, $new );
168
+ $this->handle_widget_reordering( $old, $new );
169
+ $this->handle_widget_moved( $old, $new );
170
+ }
171
+
172
+ /**
173
+ * Track deactivation of widgets from sidebars
174
+ *
175
+ * @param array $old Old sidebars widgets
176
+ * @param array $new New sidebars widgets
177
+ * @return void
178
+ */
179
+ protected function handle_deactivated_widgets( $old, $new ) {
180
+ $new_deactivated_widget_ids = array_diff( $new['wp_inactive_widgets'], $old['wp_inactive_widgets'] );
181
+
182
+ foreach ( $new_deactivated_widget_ids as $widget_id ) {
183
+ $sidebar_id = '';
184
+
185
+ foreach ( $old as $old_sidebar_id => $old_widget_ids ) {
186
+ if ( in_array( $widget_id, $old_widget_ids ) ) {
187
+ $sidebar_id = $old_sidebar_id;
188
+ break;
189
+ }
190
+ }
191
+
192
+ $action = 'deactivated';
193
+ $name = $this->get_widget_name( $widget_id );
194
+ $title = $this->get_widget_title( $widget_id );
195
+ $labels = $this->get_context_labels();
196
+ $sidebar_name = isset( $labels[ $sidebar_id ] ) ? $labels[ $sidebar_id ] : $sidebar_id;
197
+
198
+ if ( $name && $title ) {
199
+ $message = _x( '%1$s widget named "%2$s" from "%3$s" deactivated', '1: Name, 2: Title, 3: Sidebar Name', 'stream' );
200
+ } elseif ( $name ) {
201
+ // Empty title, but we have the name
202
+ $message = _x( '%1$s widget from "%3$s" deactivated', '1: Name, 3: Sidebar Name', 'stream' );
203
+ } elseif ( $title ) {
204
+ // Likely a single widget since no name is available
205
+ $message = _x( 'Unknown widget type named "%2$s" from "%3$s" deactivated', '2: Title, 3: Sidebar Name', 'stream' );
206
+ } else {
207
+ // Neither a name nor a title are available, so use the widget ID
208
+ $message = _x( '%4$s widget from "%3$s" deactivated', '4: Widget ID, 3: Sidebar Name', 'stream' );
209
+ }
210
+
211
+ $message = sprintf( $message, $name, $title, $sidebar_name, $widget_id );
212
+
213
+ $this->log(
214
+ $message,
215
+ compact( 'title', 'name', 'widget_id', 'sidebar_id' ),
216
+ null,
217
+ 'wp_inactive_widgets',
218
+ $action
219
+ );
220
+ }
221
+ }
222
+
223
+ /**
224
+ * Track reactivation of widgets from sidebars
225
+ *
226
+ * @param array $old Old sidebars widgets
227
+ * @param array $new New sidebars widgets
228
+ * @return void
229
+ */
230
+ protected function handle_reactivated_widgets( $old, $new ) {
231
+ $new_reactivated_widget_ids = array_diff( $old['wp_inactive_widgets'], $new['wp_inactive_widgets'] );
232
+
233
+ foreach ( $new_reactivated_widget_ids as $widget_id ) {
234
+ $sidebar_id = '';
235
+
236
+ foreach ( $new as $new_sidebar_id => $new_widget_ids ) {
237
+ if ( in_array( $widget_id, $new_widget_ids ) ) {
238
+ $sidebar_id = $new_sidebar_id;
239
+ break;
240
+ }
241
+ }
242
+
243
+ $action = 'reactivated';
244
+ $name = $this->get_widget_name( $widget_id );
245
+ $title = $this->get_widget_title( $widget_id );
246
+
247
+ if ( $name && $title ) {
248
+ $message = _x( '%1$s widget named "%2$s" reactivated', '1: Name, 2: Title', 'stream' );
249
+ } elseif ( $name ) {
250
+ // Empty title, but we have the name
251
+ $message = _x( '%1$s widget reactivated', '1: Name', 'stream' );
252
+ } elseif ( $title ) {
253
+ // Likely a single widget since no name is available
254
+ $message = _x( 'Unknown widget type named "%2$s" reactivated', '2: Title', 'stream' );
255
+ } else {
256
+ // Neither a name nor a title are available, so use the widget ID
257
+ $message = _x( '%3$s widget reactivated', '3: Widget ID', 'stream' );
258
+ }
259
+
260
+ $message = sprintf( $message, $name, $title, $widget_id );
261
+
262
+ $this->log(
263
+ $message,
264
+ compact( 'title', 'name', 'widget_id', 'sidebar_id' ),
265
+ null,
266
+ $sidebar_id,
267
+ $action
268
+ );
269
+ }
270
+ }
271
+
272
+ /**
273
+ * Track deletion of widgets from sidebars
274
+ *
275
+ * @param array $old Old sidebars widgets
276
+ * @param array $new New sidebars widgets
277
+ * @return void
278
+ */
279
+ protected function handle_widget_removal( $old, $new ) {
280
+ $all_old_widget_ids = array_unique( call_user_func_array( 'array_merge', $old ) );
281
+ $all_new_widget_ids = array_unique( call_user_func_array( 'array_merge', $new ) );
282
+ // @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?
283
+ // @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
284
+
285
+ $deleted_widget_ids = array_diff( $all_old_widget_ids, $all_new_widget_ids );
286
+
287
+ foreach ( $deleted_widget_ids as $widget_id ) {
288
+ $sidebar_id = '';
289
+
290
+ foreach ( $old as $old_sidebar_id => $old_widget_ids ) {
291
+ if ( in_array( $widget_id, $old_widget_ids ) ) {
292
+ $sidebar_id = $old_sidebar_id;
293
+ break;
294
+ }
295
+ }
296
+
297
+ $action = 'removed';
298
+ $name = $this->get_widget_name( $widget_id );
299
+ $title = $this->get_widget_title( $widget_id );
300
+ $labels = $this->get_context_labels();
301
+ $sidebar_name = isset( $labels[ $sidebar_id ] ) ? $labels[ $sidebar_id ] : $sidebar_id;
302
+
303
+ if ( $name && $title ) {
304
+ $message = _x( '%1$s widget named "%2$s" removed from "%3$s"', '1: Name, 2: Title, 3: Sidebar Name', 'stream' );
305
+ } elseif ( $name ) {
306
+ // Empty title, but we have the name
307
+ $message = _x( '%1$s widget removed from "%3$s"', '1: Name, 3: Sidebar Name', 'stream' );
308
+ } elseif ( $title ) {
309
+ // Likely a single widget since no name is available
310
+ $message = _x( 'Unknown widget type named "%2$s" removed from "%3$s"', '2: Title, 3: Sidebar Name', 'stream' );
311
+ } else {
312
+ // Neither a name nor a title are available, so use the widget ID
313
+ $message = _x( '%4$s widget removed from "%3$s"', '4: Widget ID, 3: Sidebar Name', 'stream' );
314
+ }
315
+
316
+ $message = sprintf( $message, $name, $title, $sidebar_name, $widget_id );
317
+
318
+ $this->log(
319
+ $message,
320
+ compact( 'widget_id', 'sidebar_id' ),
321
+ null,
322
+ $sidebar_id,
323
+ $action
324
+ );
325
+ }
326
+ }
327
+
328
+ /**
329
+ * Track reactivation of widgets from sidebars
330
+ *
331
+ * @param array $old Old sidebars widgets
332
+ * @param array $new New sidebars widgets
333
+ * @return void
334
+ */
335
+ protected function handle_widget_addition( $old, $new ) {
336
+ $all_old_widget_ids = array_unique( call_user_func_array( 'array_merge', $old ) );
337
+ $all_new_widget_ids = array_unique( call_user_func_array( 'array_merge', $new ) );
338
+ $added_widget_ids = array_diff( $all_new_widget_ids, $all_old_widget_ids );
339
+
340
+ foreach ( $added_widget_ids as $widget_id ) {
341
+ $sidebar_id = '';
342
+
343
+ foreach ( $new as $new_sidebar_id => $new_widget_ids ) {
344
+ if ( in_array( $widget_id, $new_widget_ids ) ) {
345
+ $sidebar_id = $new_sidebar_id;
346
+ break;
347
+ }
348
+ }
349
+
350
+ $action = 'added';
351
+ $name = $this->get_widget_name( $widget_id );
352
+ $title = $this->get_widget_title( $widget_id );
353
+ $labels = $this->get_context_labels();
354
+ $sidebar_name = isset( $labels[ $sidebar_id ] ) ? $labels[ $sidebar_id ] : $sidebar_id;
355
+
356
+ if ( $name && $title ) {
357
+ $message = _x( '%1$s widget named "%2$s" added to "%3$s"', '1: Name, 2: Title, 3: Sidebar Name', 'stream' );
358
+ } elseif ( $name ) {
359
+ // Empty title, but we have the name
360
+ $message = _x( '%1$s widget added to "%3$s"', '1: Name, 3: Sidebar Name', 'stream' );
361
+ } elseif ( $title ) {
362
+ // Likely a single widget since no name is available
363
+ $message = _x( 'Unknown widget type named "%2$s" added to "%3$s"', '2: Title, 3: Sidebar Name', 'stream' );
364
+ } else {
365
+ // Neither a name nor a title are available, so use the widget ID
366
+ $message = _x( '%4$s widget added to "%3$s"', '4: Widget ID, 3: Sidebar Name', 'stream' );
367
+ }
368
+
369
+ $message = sprintf( $message, $name, $title, $sidebar_name, $widget_id );
370
+
371
+ $this->log(
372
+ $message,
373
+ 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
374
+ null,
375
+ $sidebar_id,
376
+ $action
377
+ );
378
+ }
379
+ }
380
+
381
+ /**
382
+ * Track reordering of widgets
383
+ *
384
+ * @param array $old Old sidebars widgets
385
+ * @param array $new New sidebars widgets
386
+ * @return void
387
+ */
388
+ protected function handle_widget_reordering( $old, $new ) {
389
+ $all_sidebar_ids = array_intersect( array_keys( $old ), array_keys( $new ) );
390
+
391
+ foreach ( $all_sidebar_ids as $sidebar_id ) {
392
+ if ( $old[ $sidebar_id ] === $new[ $sidebar_id ] ) {
393
+ continue;
394
+ }
395
+
396
+ // Use intersect to ignore widget additions and removals
397
+ $all_widget_ids = array_unique( array_merge( $old[ $sidebar_id ], $new[ $sidebar_id ] ) );
398
+ $common_widget_ids = array_intersect( $old[ $sidebar_id ], $new[ $sidebar_id ] );
399
+ $uncommon_widget_ids = array_diff( $all_widget_ids, $common_widget_ids );
400
+ $new_widget_ids = array_values( array_diff( $new[ $sidebar_id ], $uncommon_widget_ids ) );
401
+ $old_widget_ids = array_values( array_diff( $old[ $sidebar_id ], $uncommon_widget_ids ) );
402
+ $widget_order_changed = ( $new_widget_ids !== $old_widget_ids );
403
+
404
+ if ( $widget_order_changed ) {
405
+ $labels = $this->get_context_labels();
406
+ $sidebar_name = isset( $labels[ $sidebar_id ] ) ? $labels[ $sidebar_id ] : $sidebar_id;
407
+ $old_widget_ids = $old[ $sidebar_id ];
408
+ $message = _x( 'Widgets reordered in "%s"', 'Sidebar name', 'stream' );
409
+ $message = sprintf( $message, $sidebar_name );
410
+
411
+ $this->log(
412
+ $message,
413
+ compact( 'sidebar_id', 'old_widget_ids' ),
414
+ null,
415
+ $sidebar_id,
416
+ 'sorted'
417
+ );
418
+ }
419
+ }
420
+
421
+ }
422
+
423
+ /**
424
+ * Track movement of widgets to other sidebars
425
+ *
426
+ * @param array $old Old sidebars widgets
427
+ * @param array $new New sidebars widgets
428
+ * @return void
429
+ */
430
+ protected function handle_widget_moved( $old, $new ) {
431
+ $all_sidebar_ids = array_intersect( array_keys( $old ), array_keys( $new ) );
432
+
433
+ foreach ( $all_sidebar_ids as $new_sidebar_id ) {
434
+ if ( $old[ $new_sidebar_id ] === $new[ $new_sidebar_id ] ) {
435
+ continue;
436
+ }
437
+
438
+ $new_widget_ids = array_diff( $new[ $new_sidebar_id ], $old[ $new_sidebar_id ] );
439
+
440
+ foreach ( $new_widget_ids as $widget_id ) {
441
+ // Now find the sidebar that the widget was originally located in, as long it is not wp_inactive_widgets
442
+ $old_sidebar_id = null;
443
+ foreach ( $old as $sidebar_id => $old_widget_ids ) {
444
+ if ( in_array( $widget_id, $old_widget_ids ) ) {
445
+ $old_sidebar_id = $sidebar_id;
446
+ break;
447
+ }
448
+ }
449
+
450
+ if ( ! $old_sidebar_id || 'wp_inactive_widgets' === $old_sidebar_id || 'wp_inactive_widgets' === $new_sidebar_id ) {
451
+ continue;
452
+ }
453
+
454
+ assert( $old_sidebar_id !== $new_sidebar_id );
455
+
456
+ $name = $this->get_widget_name( $widget_id );
457
+ $title = $this->get_widget_title( $widget_id );
458
+ $labels = $this->get_context_labels();
459
+ $old_sidebar_name = isset( $labels[ $old_sidebar_id ] ) ? $labels[ $old_sidebar_id ] : $old_sidebar_id;
460
+ $new_sidebar_name = isset( $labels[ $new_sidebar_id ] ) ? $labels[ $new_sidebar_id ] : $new_sidebar_id;
461
+
462
+ if ( $name && $title ) {
463
+ $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' );
464
+ } elseif ( $name ) {
465
+ // Empty title, but we have the name
466
+ $message = _x( '%1$s widget moved from "%4$s" to "%5$s"', '1: Name, 4: Old Sidebar Name, 5: New Sidebar Name', 'stream' );
467
+ } elseif ( $title ) {
468
+ // Likely a single widget since no name is available
469
+ $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' );
470
+ } else {
471
+ // Neither a name nor a title are available, so use the widget ID
472
+ $message = _x( '%3$s widget moved from "%4$s" to "%5$s"', '3: Widget ID, 4: Old Sidebar Name, 5: New Sidebar Name', 'stream' );
473
+ }
474
+
475
+ $message = sprintf( $message, $name, $title, $widget_id, $old_sidebar_name, $new_sidebar_name );
476
+ $sidebar_id = $new_sidebar_id;
477
+
478
+ $this->log(
479
+ $message,
480
+ compact( 'widget_id', 'sidebar_id', 'old_sidebar_id' ),
481
+ null,
482
+ $sidebar_id,
483
+ 'moved'
484
+ );
485
+ }
486
+ }
487
+
488
+ }
489
+
490
+ /**
491
+ * Track changes to widgets
492
+ *
493
+ * @action updated_option
494
+ *
495
+ * @param string $option_name
496
+ * @param array $old_value
497
+ * @param array $new_value
498
+ */
499
+ public function callback_updated_option( $option_name, $old_value, $new_value ) {
500
+ if ( ! preg_match( '/^widget_(.+)$/', $option_name, $matches ) || ! is_array( $new_value ) ) {
501
+ return;
502
+ }
503
+
504
+ $is_multi = ! empty( $new_value['_multiwidget'] );
505
+ $widget_id_base = $matches[1];
506
+
507
+ $creates = array();
508
+ $updates = array();
509
+ $deletes = array();
510
+
511
+ if ( $is_multi ) {
512
+ $widget_id_format = "$widget_id_base-%d";
513
+
514
+ unset( $new_value['_multiwidget'] );
515
+ unset( $old_value['_multiwidget'] );
516
+
517
+ /**
518
+ * Created widgets
519
+ */
520
+ $created_widget_numbers = array_diff( array_keys( $new_value ), array_keys( $old_value ) );
521
+
522
+ foreach ( $created_widget_numbers as $widget_number ) {
523
+ $instance = $new_value[ $widget_number ];
524
+ $widget_id = sprintf( $widget_id_format, $widget_number );
525
+ $name = $this->get_widget_name( $widget_id );
526
+ $title = ! empty( $instance['title'] ) ? $instance['title'] : null;
527
+ $sidebar_id = $this->get_widget_sidebar_id( $widget_id ); // @todo May not be assigned yet
528
+
529
+ $creates[] = compact( 'name', 'title', 'widget_id', 'sidebar_id', 'instance' );
530
+ }
531
+
532
+ /**
533
+ * Updated widgets
534
+ */
535
+ $updated_widget_numbers = array_intersect( array_keys( $old_value ), array_keys( $new_value ) );
536
+
537
+ foreach ( $updated_widget_numbers as $widget_number ) {
538
+ $new_instance = $new_value[ $widget_number ];
539
+ $old_instance = $old_value[ $widget_number ];
540
+
541
+ if ( $old_instance !== $new_instance ) {
542
+ $widget_id = sprintf( $widget_id_format, $widget_number );
543
+ $name = $this->get_widget_name( $widget_id );
544
+ $title = ! empty( $new_instance['title'] ) ? $new_instance['title'] : null;
545
+ $sidebar_id = $this->get_widget_sidebar_id( $widget_id );
546
+ $labels = $this->get_context_labels();
547
+ $sidebar_name = isset( $labels[ $sidebar_id ] ) ? $labels[ $sidebar_id ] : $sidebar_id;
548
+
549
+ $updates[] = compact( 'name', 'title', 'widget_id', 'sidebar_id', 'old_instance', 'sidebar_name' );
550
+ }
551
+ }
552
+
553
+ /**
554
+ * Deleted widgets
555
+ */
556
+ $deleted_widget_numbers = array_diff( array_keys( $old_value ), array_keys( $new_value ) );
557
+
558
+ foreach ( $deleted_widget_numbers as $widget_number ) {
559
+ $instance = $old_value[ $widget_number ];
560
+ $widget_id = sprintf( $widget_id_format, $widget_number );
561
+ $name = $this->get_widget_name( $widget_id );
562
+ $title = ! empty( $instance['title'] ) ? $instance['title'] : null;
563
+ $sidebar_id = $this->get_widget_sidebar_id( $widget_id ); // @todo May not be assigned anymore
564
+
565
+ $deletes[] = compact( 'name', 'title', 'widget_id', 'sidebar_id', 'instance' );
566
+ }
567
+ } else {
568
+ // Doing our best guess for tracking changes to old single widgets, assuming their options start with 'widget_'
569
+ $widget_id = $widget_id_base;
570
+ $name = $widget_id; // There aren't names available for single widgets
571
+ $title = ! empty( $new_value['title'] ) ? $new_value['title'] : null;
572
+ $sidebar_id = $this->get_widget_sidebar_id( $widget_id );
573
+ $old_instance = $old_value;
574
+ $labels = $this->get_context_labels();
575
+ $sidebar_name = isset( $labels[ $sidebar_id ] ) ? $labels[ $sidebar_id ] : $sidebar_id;
576
+
577
+ $updates[] = compact( 'widget_id', 'title', 'name', 'sidebar_id', 'old_instance', 'sidebar_name' );
578
+ }
579
+
580
+ /**
581
+ * Log updated actions
582
+ */
583
+ foreach ( $updates as $update ) {
584
+ if ( $update['name'] && $update['title'] ) {
585
+ $message = _x( '%1$s widget named "%2$s" in "%3$s" updated', '1: Name, 2: Title, 3: Sidebar Name', 'stream' );
586
+ } elseif ( $update['name'] ) {
587
+ // Empty title, but we have the name
588
+ $message = _x( '%1$s widget in "%3$s" updated', '1: Name, 3: Sidebar Name', 'stream' );
589
+ } elseif ( $update['title'] ) {
590
+ // Likely a single widget since no name is available
591
+ $message = _x( 'Unknown widget type named "%2$s" in "%3$s" updated', '2: Title, 3: Sidebar Name', 'stream' );
592
+ } else {
593
+ // Neither a name nor a title are available, so use the widget ID
594
+ $message = _x( '%4$s widget in "%3$s" updated', '4: Widget ID, 3: Sidebar Name', 'stream' );
595
+ }
596
+
597
+ $message = sprintf( $message, $update['name'], $update['title'], $update['sidebar_name'], $update['widget_id'] );
598
+
599
+ unset( $update['title'], $update['name'] );
600
+
601
+ $this->log(
602
+ $message,
603
+ $update,
604
+ null,
605
+ $update['sidebar_id'],
606
+ 'updated'
607
+ );
608
+ }
609
+
610
+ /**
611
+ * In the normal case, widgets are never created or deleted in a vacuum.
612
+ * Created widgets are immediately assigned to a sidebar, and deleted
613
+ * widgets are immediately removed from their assigned sidebar. If,
614
+ * however, widget instances get manipulated programmatically, it is
615
+ * possible that they could be orphaned, in which case the following
616
+ * actions would be useful to log.
617
+ */
618
+ if ( $this->verbose_widget_created_deleted_actions ) {
619
+ // We should only do these if not captured by an update to the sidebars_widgets option
620
+ /**
621
+ * Log created actions
622
+ */
623
+ foreach ( $creates as $create ) {
624
+ if ( $create['name'] && $create['title'] ) {
625
+ $message = _x( '%1$s widget named "%2$s" created', '1: Name, 2: Title', 'stream' );
626
+ } elseif ( $create['name'] ) {
627
+ // Empty title, but we have the name
628
+ $message = _x( '%1$s widget created', '1: Name', 'stream' );
629
+ } elseif ( $create['title'] ) {
630
+ // Likely a single widget since no name is available
631
+ $message = _x( 'Unknown widget type named "%2$s" created', '2: Title', 'stream' );
632
+ } else {
633
+ // Neither a name nor a title are available, so use the widget ID
634
+ $message = _x( '%3$s widget created', '3: Widget ID', 'stream' );
635
+ }
636
+
637
+ $message = sprintf( $message, $create['name'], $create['title'], $create['widget_id'] );
638
+
639
+ unset( $create['title'], $create['name'] );
640
+
641
+ $this->log(
642
+ $message,
643
+ $create,
644
+ null,
645
+ $create['sidebar_id'],
646
+ 'created'
647
+ );
648
+ }
649
+
650
+ /**
651
+ * Log deleted actions
652
+ */
653
+ foreach ( $deletes as $delete ) {
654
+ if ( $delete['name'] && $delete['title'] ) {
655
+ $message = _x( '%1$s widget named "%2$s" deleted', '1: Name, 2: Title', 'stream' );
656
+ } elseif ( $delete['name'] ) {
657
+ // Empty title, but we have the name
658
+ $message = _x( '%1$s widget deleted', '1: Name', 'stream' );
659
+ } elseif ( $delete['title'] ) {
660
+ // Likely a single widget since no name is available
661
+ $message = _x( 'Unknown widget type named "%2$s" deleted', '2: Title', 'stream' );
662
+ } else {
663
+ // Neither a name nor a title are available, so use the widget ID
664
+ $message = _x( '%3$s widget deleted', '3: Widget ID', 'stream' );
665
+ }
666
+
667
+ $message = sprintf( $message, $delete['name'], $delete['title'], $delete['widget_id'] );
668
+
669
+ unset( $delete['title'], $delete['name'] );
670
+
671
+ $this->log(
672
+ $message,
673
+ $delete,
674
+ null,
675
+ $delete['sidebar_id'],
676
+ 'deleted'
677
+ );
678
+ }
679
+ }
680
+ }
681
+
682
+ /**
683
+ * @param string $widget_id
684
+ *
685
+ * @return string
686
+ */
687
+ public function get_widget_title( $widget_id ) {
688
+ $instance = $this->get_widget_instance( $widget_id );
689
+ return ! empty( $instance['title'] ) ? $instance['title'] : null;
690
+ }
691
+
692
+ /**
693
+ * @param string $widget_id
694
+ *
695
+ * @return string|null
696
+ */
697
+ public function get_widget_name( $widget_id ) {
698
+ $widget_obj = $this->get_widget_object( $widget_id );
699
+ return $widget_obj ? $widget_obj->name : null;
700
+ }
701
+
702
+ /**
703
+ * @param $widget_id
704
+ *
705
+ * @return array|null
706
+ */
707
+ public function parse_widget_id( $widget_id ) {
708
+ if ( preg_match( '/^(.+)-(\d+)$/', $widget_id, $matches ) ) {
709
+ return array(
710
+ 'id_base' => $matches[1],
711
+ 'widget_number' => intval( $matches[2] ),
712
+ );
713
+ } else {
714
+ return null;
715
+ }
716
+ }
717
+
718
+ /**
719
+ * @param string $widget_id
720
+ *
721
+ * @return \WP_Widget|null
722
+ */
723
+ public function get_widget_object( $widget_id ) {
724
+ global $wp_widget_factory;
725
+
726
+ $parsed_widget_id = $this->parse_widget_id( $widget_id );
727
+
728
+ if ( ! $parsed_widget_id ) {
729
+ return null;
730
+ }
731
+
732
+ $id_base = $parsed_widget_id['id_base'];
733
+
734
+ $id_base_to_widget_class_map = array_combine(
735
+ wp_list_pluck( $wp_widget_factory->widgets, 'id_base' ),
736
+ array_keys( $wp_widget_factory->widgets )
737
+ );
738
+
739
+ if ( ! isset( $id_base_to_widget_class_map[ $id_base ] ) ) {
740
+ return null;
741
+ }
742
+
743
+ return $wp_widget_factory->widgets[ $id_base_to_widget_class_map[ $id_base ] ];
744
+ }
745
+
746
+ /**
747
+ * Returns widget instance settings
748
+ *
749
+ * @param string $widget_id Widget ID, ex: pages-1
750
+ *
751
+ * @return array|null Widget instance
752
+ */
753
+ public function get_widget_instance( $widget_id ) {
754
+ $instance = null;
755
+ $parsed_widget_id = $this->parse_widget_id( $widget_id );
756
+ $widget_obj = $this->get_widget_object( $widget_id );
757
+
758
+ if ( $widget_obj && $parsed_widget_id ) {
759
+ $settings = $widget_obj->get_settings();
760
+ $multi_number = $parsed_widget_id['widget_number'];
761
+
762
+ if ( isset( $settings[ $multi_number ] ) && ! empty( $settings[ $multi_number ]['title'] ) ) {
763
+ $instance = $settings[ $multi_number ];
764
+ }
765
+ } else {
766
+ // Single widgets, try our best guess at the option used
767
+ $potential_instance = get_option( "widget_{$widget_id}" );
768
+
769
+ if ( ! empty( $potential_instance ) && ! empty( $potential_instance['title'] ) ) {
770
+ $instance = $potential_instance;
771
+ }
772
+ }
773
+
774
+ return $instance;
775
+ }
776
+
777
+ /**
778
+ * Get global sidebars widgets
779
+ *
780
+ * @return array
781
+ */
782
+ public function get_sidebars_widgets() {
783
+ /**
784
+ * Filter allows for insertion of sidebar widgets
785
+ * @todo Do we need this filter?
786
+ *
787
+ * @param array Sidebar Widgets in Options table
788
+ * @param array Inserted Sidebar Widgets
789
+ * @return array Array of updated Sidebar Widgets
790
+ */
791
+ return apply_filters( 'sidebars_widgets', get_option( 'sidebars_widgets', array() ) );
792
+ }
793
+
794
+ /**
795
+ * Return the sidebar of a certain widget, based on widget_id
796
+ *
797
+ * @param string $widget_id Widget id, ex: pages-1
798
+ *
799
+ * @return string Sidebar id
800
+ */
801
+ public function get_widget_sidebar_id( $widget_id ) {
802
+ $sidebars_widgets = $this->get_sidebars_widgets();
803
+
804
+ unset( $sidebars_widgets['array_version'] );
805
+
806
+ foreach ( $sidebars_widgets as $sidebar_id => $widget_ids ) {
807
+ if ( in_array( $widget_id, $widget_ids ) ) {
808
+ return $sidebar_id;
809
+ }
810
+ }
811
+
812
+ return 'orphaned_widgets';
813
+ }
814
+ }
connectors/class-connector-woocommerce.php ADDED
@@ -0,0 +1,806 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace WP_Stream;
3
+
4
+ class Connector_Woocommerce extends Connector {
5
+ /**
6
+ * Context name
7
+ * @var string
8
+ */
9
+ public $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 $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 $taxonomies = array(
37
+ 'product_type',
38
+ 'product_cat',
39
+ 'product_tag',
40
+ 'product_shipping_class',
41
+ 'shop_order_status',
42
+ );
43
+
44
+ public $post_types = array(
45
+ 'product',
46
+ 'product_variation',
47
+ 'shop_order',
48
+ 'shop_coupon',
49
+ );
50
+
51
+ private $order_update_logged = false;
52
+
53
+ private $settings_pages = array();
54
+
55
+ private $settings = array();
56
+
57
+ public function register() {
58
+ parent::register();
59
+
60
+ add_filter( 'wp_stream_posts_exclude_post_types', array( $this, 'exclude_order_post_types' ) );
61
+ add_action( 'wp_stream_comments_exclude_comment_types', array( $this, 'exclude_order_comment_types' ) );
62
+
63
+ $this->get_woocommerce_settings_fields();
64
+ }
65
+
66
+ /**
67
+ * Check if plugin dependencies are satisfied and add an admin notice if not
68
+ *
69
+ * @return bool
70
+ */
71
+ public function is_dependency_satisfied() {
72
+ global $woocommerce;
73
+
74
+ if ( class_exists( 'WooCommerce' ) && version_compare( $woocommerce->version, self::PLUGIN_MIN_VERSION, '>=' ) ) {
75
+ return true;
76
+ }
77
+
78
+ return false;
79
+ }
80
+
81
+ /**
82
+ * Return translated context label
83
+ *
84
+ * @return string Translated context label
85
+ */
86
+ public function get_label() {
87
+ return esc_html_x( 'WooCommerce', 'woocommerce', 'stream' );
88
+ }
89
+
90
+ /**
91
+ * Return translated action labels
92
+ *
93
+ * @return array Action label translations
94
+ */
95
+ public function get_action_labels() {
96
+ return array(
97
+ 'updated' => esc_html_x( 'Updated', 'woocommerce', 'stream' ),
98
+ 'created' => esc_html_x( 'Created', 'woocommerce', 'stream' ),
99
+ 'trashed' => esc_html_x( 'Trashed', 'woocommerce', 'stream' ),
100
+ 'deleted' => esc_html_x( 'Deleted', 'woocommerce', 'stream' ),
101
+ );
102
+ }
103
+
104
+ /**
105
+ * Return translated context labels
106
+ *
107
+ * @return array Context label translations
108
+ */
109
+ public function get_context_labels() {
110
+ $context_labels = array();
111
+
112
+ if ( class_exists( 'Connector_Posts' ) ) {
113
+ $posts_connector = new Connector_Posts();
114
+ $context_labels = array_merge(
115
+ $context_labels,
116
+ $posts_connector->get_context_labels()
117
+ );
118
+ }
119
+
120
+ $custom_context_labels = array(
121
+ 'attributes' => esc_html_x( 'Attributes', 'woocommerce', 'stream' ),
122
+ );
123
+
124
+ $context_labels = array_merge(
125
+ $context_labels,
126
+ $custom_context_labels,
127
+ $this->settings_pages
128
+ );
129
+
130
+ return apply_filters( 'wp_stream_woocommerce_contexts', $context_labels );
131
+ }
132
+
133
+ /**
134
+ * Return settings used by WooCommerce that aren't registered
135
+ *
136
+ * @return array Custom settings with translated title and page
137
+ */
138
+ public function get_custom_settings() {
139
+ $custom_settings = array(
140
+ 'woocommerce_frontend_css_colors' => array(
141
+ 'title' => esc_html__( 'Frontend Styles', 'stream' ),
142
+ 'page' => 'wc-settings',
143
+ 'tab' => 'general',
144
+ 'section' => '',
145
+ 'type' => esc_html__( 'setting', 'stream' ),
146
+ ),
147
+ 'woocommerce_default_gateway' => array(
148
+ 'title' => esc_html__( 'Gateway Display Default', 'stream' ),
149
+ 'page' => 'wc-settings',
150
+ 'tab' => 'checkout',
151
+ 'section' => '',
152
+ 'type' => esc_html__( 'setting', 'stream' ),
153
+ ),
154
+ 'woocommerce_gateway_order' => array(
155
+ 'title' => esc_html__( 'Gateway Display Order', 'stream' ),
156
+ 'page' => 'wc-settings',
157
+ 'tab' => 'checkout',
158
+ 'section' => '',
159
+ 'type' => esc_html__( 'setting', 'stream' ),
160
+ ),
161
+ 'woocommerce_default_shipping_method' => array(
162
+ 'title' => esc_html__( 'Shipping Methods Default', 'stream' ),
163
+ 'page' => 'wc-settings',
164
+ 'tab' => 'shipping',
165
+ 'section' => '',
166
+ 'type' => esc_html__( 'setting', 'stream' ),
167
+ ),
168
+ 'woocommerce_shipping_method_order' => array(
169
+ 'title' => esc_html__( 'Shipping Methods Order', 'stream' ),
170
+ 'page' => 'wc-settings',
171
+ 'tab' => 'shipping',
172
+ 'section' => '',
173
+ 'type' => esc_html__( 'setting', 'stream' ),
174
+ ),
175
+ 'shipping_debug_mode' => array(
176
+ 'title' => esc_html__( 'Shipping Debug Mode', 'stream' ),
177
+ 'page' => 'wc-status',
178
+ 'tab' => 'tools',
179
+ 'section' => '',
180
+ 'type' => esc_html__( 'tool', 'stream' ),
181
+ ),
182
+ 'template_debug_mode' => array(
183
+ 'title' => esc_html__( 'Template Debug Mode', 'stream' ),
184
+ 'page' => 'wc-status',
185
+ 'tab' => 'tools',
186
+ 'section' => '',
187
+ 'type' => esc_html__( 'tool', 'stream' ),
188
+ ),
189
+ 'uninstall_data' => array(
190
+ 'title' => esc_html__( 'Remove post types on uninstall', 'stream' ),
191
+ 'page' => 'wc-status',
192
+ 'tab' => 'tools',
193
+ 'section' => '',
194
+ 'type' => esc_html__( 'tool', 'stream' ),
195
+ ),
196
+ );
197
+
198
+ return apply_filters( 'wp_stream_woocommerce_custom_settings', $custom_settings );
199
+ }
200
+
201
+ /**
202
+ * Add action links to Stream drop row in admin list screen
203
+ *
204
+ * @filter wp_stream_action_links_{connector}
205
+ *
206
+ * @param array $links Previous links registered
207
+ * @param Record $record Stream record
208
+ *
209
+ * @return array Action links
210
+ */
211
+ public function action_links( $links, $record ) {
212
+ if ( in_array( $record->context, $this->post_types ) && get_post( $record->object_id ) ) {
213
+ if ( $link = get_edit_post_link( $record->object_id ) ) {
214
+ $posts_connector = new Connector_Posts();
215
+ $post_type_name = $posts_connector->get_post_type_name( get_post_type( $record->object_id ) );
216
+ $links[ sprintf( esc_html_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[ esc_html__( 'View', 'stream' ) ] = $link;
221
+ }
222
+ }
223
+
224
+ $context_labels = $this->get_context_labels();
225
+ $option_key = $record->get_meta( 'option', true );
226
+ $option_page = $record->get_meta( 'page', true );
227
+ $option_tab = $record->get_meta( 'tab', true );
228
+ $option_section = $record->get_meta( 'section', true );
229
+
230
+ if ( $option_key && $option_tab ) {
231
+ $text = sprintf( esc_html__( '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
+ *
249
+ * @param array $post_types Ignored post types
250
+ *
251
+ * @return array Filtered post types
252
+ */
253
+ public function exclude_order_post_types( $post_types ) {
254
+ $post_types[] = 'shop_order';
255
+
256
+ return $post_types;
257
+ }
258
+
259
+ /**
260
+ * Prevent the Stream Comments connector from logging status
261
+ * change comments on orders
262
+ *
263
+ * @filter wp_stream_commnent_exclude_comment_types
264
+ *
265
+ * @param array $comment_types Ignored post types
266
+ *
267
+ * @return array Filtered post types
268
+ */
269
+ public function exclude_order_comment_types( $comment_types ) {
270
+ $comment_types[] = 'order_note';
271
+
272
+ return $comment_types;
273
+ }
274
+
275
+ /**
276
+ * Log Order major status changes ( creating / updating / trashing )
277
+ *
278
+ * @action transition_post_status
279
+ *
280
+ * @param string $new
281
+ * @param string $old
282
+ * @param \WP_Post $post
283
+ */
284
+ public function callback_transition_post_status( $new, $old, $post ) {
285
+ // Only track orders
286
+ if ( 'shop_order' !== $post->post_type ) {
287
+ return;
288
+ }
289
+
290
+ // Don't track customer actions
291
+ if ( ! is_admin() ) {
292
+ return;
293
+ }
294
+
295
+ // Don't track minor status change actions
296
+ if ( in_array( wp_stream_filter_input( INPUT_GET, 'action' ), array( 'mark_processing', 'mark_on-hold', 'mark_completed' ) ) || defined( 'DOING_AJAX' ) ) {
297
+ return;
298
+ }
299
+
300
+ // Don't log updates when more than one happens at the same time
301
+ if ( $post->ID === $this->order_update_logged ) {
302
+ return;
303
+ }
304
+
305
+ if ( in_array( $new, array( 'auto-draft', 'draft', 'inherit' ) ) ) {
306
+ return;
307
+ } elseif ( 'auto-draft' === $old && 'publish' === $new ) {
308
+ $message = esc_html_x(
309
+ '%s created',
310
+ 'Order title',
311
+ 'stream'
312
+ );
313
+ $action = 'created';
314
+ } elseif ( 'trash' === $new ) {
315
+ $message = esc_html_x(
316
+ '%s trashed',
317
+ 'Order title',
318
+ 'stream'
319
+ );
320
+ $action = 'trashed';
321
+ } elseif ( 'trash' === $old && 'publish' === $new ) {
322
+ $message = esc_html_x(
323
+ '%s restored from the trash',
324
+ 'Order title',
325
+ 'stream'
326
+ );
327
+ $action = 'untrashed';
328
+ } else {
329
+ $message = esc_html_x(
330
+ '%s updated',
331
+ 'Order title',
332
+ 'stream'
333
+ );
334
+ }
335
+
336
+ if ( empty( $action ) ) {
337
+ $action = 'updated';
338
+ }
339
+
340
+ $order = new \WC_Order( $post->ID );
341
+ $order_title = esc_html__( 'Order number', 'stream' ) . ' ' . esc_html( $order->get_order_number() );
342
+ $order_type_name = esc_html__( 'order', 'stream' );
343
+
344
+ $this->log(
345
+ $message,
346
+ array(
347
+ 'post_title' => $order_title,
348
+ 'singular_name' => $order_type_name,
349
+ 'new_status' => $new,
350
+ 'old_status' => $old,
351
+ 'revision_id' => null,
352
+ ),
353
+ $post->ID,
354
+ $post->post_type,
355
+ $action
356
+ );
357
+
358
+ $this->order_update_logged = $post->ID;
359
+ }
360
+
361
+ /**
362
+ * Log order deletion
363
+ *
364
+ * @action deleted_post
365
+ *
366
+ * @param int $post_id
367
+ */
368
+ public function callback_deleted_post( $post_id ) {
369
+ $post = get_post( $post_id );
370
+
371
+ // We check if post is an instance of WP_Post as it doesn't always resolve in unit testing
372
+ if ( ! ( $post instanceof \WP_Post ) || 'shop_order' !== $post->post_type ) {
373
+ return;
374
+ }
375
+
376
+ // Ignore auto-drafts that are deleted by the system, see issue-293
377
+ if ( 'auto-draft' === $post->post_status ) {
378
+ return;
379
+ }
380
+
381
+ $order = new \WC_Order( $post->ID );
382
+ $order_title = esc_html__( 'Order number', 'stream' ) . ' ' . esc_html( $order->get_order_number() );
383
+ $order_type_name = esc_html__( 'order', 'stream' );
384
+
385
+ $this->log(
386
+ _x(
387
+ '"%s" deleted from trash',
388
+ 'Order title',
389
+ 'stream'
390
+ ),
391
+ array(
392
+ 'post_title' => $order_title,
393
+ 'singular_name' => $order_type_name,
394
+ ),
395
+ $post->ID,
396
+ $post->post_type,
397
+ 'deleted'
398
+ );
399
+ }
400
+
401
+ /**
402
+ * Log Order minor status changes ( pending / on-hold / failed / processing / completed / refunded / cancelled )
403
+ *
404
+ * @action woocommerce_order_status_changed
405
+ *
406
+ * @param int $order_id
407
+ * @param string $old
408
+ * @param string $new
409
+ */
410
+ public function callback_woocommerce_order_status_changed( $order_id, $old, $new ) {
411
+ // Don't track customer actions
412
+ if ( ! is_admin() ) {
413
+ return;
414
+ }
415
+
416
+ $old_status = wp_stream_is_vip() ? wpcom_vip_get_term_by( 'slug', $old, 'shop_order_status' ) : get_term_by( 'slug', $old, 'shop_order_status' );
417
+ $new_status = wp_stream_is_vip() ? wpcom_vip_get_term_by( 'slug', $new, 'shop_order_status' ) : get_term_by( 'slug', $new, 'shop_order_status' );
418
+
419
+ // Don't track new statuses
420
+ if ( ! $old_status ) {
421
+ return;
422
+ }
423
+
424
+ $message = esc_html_x(
425
+ '%1$s status changed from %2$s to %3$s',
426
+ '1. Order title, 2. Old status, 3. New status',
427
+ 'stream'
428
+ );
429
+
430
+ $order = new \WC_Order( $order_id );
431
+ $order_title = esc_html__( 'Order number', 'stream' ) . ' ' . esc_html( $order->get_order_number() );
432
+ $order_type_name = esc_html__( 'order', 'stream' );
433
+ $new_status_name = strtolower( $new_status->name );
434
+ $old_status_name = strtolower( $old_status->name );
435
+
436
+ $this->log(
437
+ $message,
438
+ array(
439
+ 'post_title' => $order_title,
440
+ 'old_status_name' => $old_status_name,
441
+ 'new_status_name' => $new_status_name,
442
+ 'singular_name' => $order_type_name,
443
+ 'new_status' => $new,
444
+ 'old_status' => $old,
445
+ 'revision_id' => null,
446
+ ),
447
+ $order_id,
448
+ 'shop_order',
449
+ $new_status_name
450
+ );
451
+ }
452
+
453
+ /**
454
+ * Log adding a product attribute
455
+ *
456
+ * @action woocommerce_attribute_added
457
+ *
458
+ * @param int $attribute_id
459
+ * @param array $attribute
460
+ */
461
+ public function callback_woocommerce_attribute_added( $attribute_id, $attribute ) {
462
+ $this->log(
463
+ _x(
464
+ '"%s" product attribute created',
465
+ 'Term name',
466
+ 'stream'
467
+ ),
468
+ $attribute,
469
+ $attribute_id,
470
+ 'attributes',
471
+ 'created'
472
+ );
473
+ }
474
+
475
+ /**
476
+ * Log updating a product attribute
477
+ *
478
+ * @action woocommerce_attribute_updated
479
+ *
480
+ * @param int $attribute_id
481
+ * @param array $attribute
482
+ */
483
+ public function callback_woocommerce_attribute_updated( $attribute_id, $attribute ) {
484
+ $this->log(
485
+ _x(
486
+ '"%s" product attribute updated',
487
+ 'Term name',
488
+ 'stream'
489
+ ),
490
+ $attribute,
491
+ $attribute_id,
492
+ 'attributes',
493
+ 'updated'
494
+ );
495
+ }
496
+
497
+ /**
498
+ * Log deleting a product attribute
499
+ *
500
+ * @action woocommerce_attribute_updated
501
+ *
502
+ * @param int $attribute_id
503
+ * @param string $attribute_name
504
+ */
505
+ public function callback_woocommerce_attribute_deleted( $attribute_id, $attribute_name ) {
506
+ $this->log(
507
+ _x(
508
+ '"%s" product attribute deleted',
509
+ 'Term name',
510
+ 'stream'
511
+ ),
512
+ array(
513
+ 'attribute_name' => $attribute_name,
514
+ ),
515
+ $attribute_id,
516
+ 'attributes',
517
+ 'deleted'
518
+ );
519
+ }
520
+
521
+ /**
522
+ * Log adding a tax rate
523
+ *
524
+ * @action woocommerce_tax_rate_added
525
+ *
526
+ * @param int $tax_rate_id
527
+ * @param array $tax_rate
528
+ */
529
+ public function callback_woocommerce_tax_rate_added( $tax_rate_id, $tax_rate ) {
530
+ $this->log(
531
+ _x(
532
+ '"%4$s" tax rate created',
533
+ 'Tax rate name',
534
+ 'stream'
535
+ ),
536
+ $tax_rate,
537
+ $tax_rate_id,
538
+ 'tax',
539
+ 'created'
540
+ );
541
+ }
542
+
543
+ /**
544
+ * Log updating a tax rate
545
+ *
546
+ * @action woocommerce_tax_rate_updated
547
+ *
548
+ * @param int $tax_rate_id
549
+ * @param array $tax_rate
550
+ */
551
+ public function callback_woocommerce_tax_rate_updated( $tax_rate_id, $tax_rate ) {
552
+ $this->log(
553
+ _x(
554
+ '"%4$s" tax rate updated',
555
+ 'Tax rate name',
556
+ 'stream'
557
+ ),
558
+ $tax_rate,
559
+ $tax_rate_id,
560
+ 'tax',
561
+ 'updated'
562
+ );
563
+ }
564
+
565
+ /**
566
+ * Log deleting a tax rate
567
+ *
568
+ * @action woocommerce_tax_rate_updated
569
+ *
570
+ * @param int $tax_rate_id
571
+ */
572
+ public function callback_woocommerce_tax_rate_deleted( $tax_rate_id ) {
573
+ global $wpdb;
574
+
575
+ $tax_rate_name = $wpdb->get_var(
576
+ $wpdb->prepare(
577
+ "SELECT tax_rate_name FROM {$wpdb->prefix}woocommerce_tax_rates
578
+ WHERE tax_rate_id = %s
579
+ ",
580
+ $tax_rate_id
581
+ )
582
+ );
583
+
584
+ $this->log(
585
+ _x(
586
+ '"%s" tax rate deleted',
587
+ 'Tax rate name',
588
+ 'stream'
589
+ ),
590
+ array(
591
+ 'tax_rate_name' => $tax_rate_name,
592
+ ),
593
+ $tax_rate_id,
594
+ 'tax',
595
+ 'deleted'
596
+ );
597
+ }
598
+
599
+ /**
600
+ * Filter records and take-over our precious data
601
+ *
602
+ * @filter wp_stream_record_array
603
+ *
604
+ * @param array $recordarr Record data to be inserted
605
+ *
606
+ * @return array Filtered record data
607
+ */
608
+ public function callback_wp_stream_record_array( $recordarr ) {
609
+ foreach ( $recordarr as $key => $record ) {
610
+ // Change connector::posts records
611
+ if ( 'posts' === $record['connector'] && in_array( $record['context'], $this->post_types ) ) {
612
+ $recordarr[ $key ]['connector'] = $this->name;
613
+ } elseif ( 'taxonomies' === $record['connector'] && in_array( $record['context'], $this->taxonomies ) ) {
614
+ $recordarr[ $key ]['connector'] = $this->name;
615
+ } elseif ( 'settings' === $record['connector'] ) {
616
+ $option = isset( $record['meta']['option_key'] ) ? $record['meta']['option_key'] : false;
617
+
618
+ if ( $option && isset( $this->settings[ $option ] ) ) {
619
+ return false;
620
+ }
621
+ }
622
+ }
623
+
624
+ return $recordarr;
625
+ }
626
+
627
+ public function callback_updated_option( $option_key, $old_value, $value ) {
628
+ $options = array( $option_key );
629
+
630
+ if ( is_array( $old_value ) || is_array( $value ) ) {
631
+ foreach ( $this->get_changed_keys( $old_value, $value ) as $field_key ) {
632
+ $options[] = $field_key;
633
+ }
634
+ }
635
+
636
+ foreach ( $options as $option ) {
637
+ if ( ! array_key_exists( $option, $this->settings ) ) {
638
+ continue;
639
+ }
640
+
641
+ $this->log(
642
+ __( '"%1$s" %2$s updated', 'stream' ),
643
+ array(
644
+ 'label' => $this->settings[ $option ]['title'],
645
+ 'type' => $this->settings[ $option ]['type'],
646
+ 'page' => $this->settings[ $option ]['page'],
647
+ 'tab' => $this->settings[ $option ]['tab'],
648
+ 'section' => $this->settings[ $option ]['section'],
649
+ 'option' => $option,
650
+ // Prevent fatal error when saving option as array
651
+ 'old_value' => maybe_serialize( $old_value ),
652
+ 'value' => maybe_serialize( $value ),
653
+ ),
654
+ null,
655
+ $this->settings[ $option ]['tab'],
656
+ 'updated'
657
+ );
658
+ }
659
+ }
660
+
661
+ public function get_woocommerce_settings_fields() {
662
+ if ( ! defined( 'WC_VERSION' ) || ! class_exists( 'WC_Admin_Settings' ) ) {
663
+ return false;
664
+ }
665
+
666
+ if ( ! empty( $this->settings ) ) {
667
+ return $this->settings;
668
+ }
669
+
670
+ $settings_cache_key = 'stream_connector_woocommerce_settings_' . sanitize_key( WC_VERSION );
671
+
672
+ if ( $settings_transient = get_transient( $settings_cache_key ) ) {
673
+ $settings = $settings_transient['settings'];
674
+ $settings_pages = $settings_transient['settings_pages'];
675
+ } else {
676
+ global $woocommerce;
677
+
678
+ $settings = array();
679
+ $settings_pages = array();
680
+
681
+ foreach ( \WC_Admin_Settings::get_settings_pages() as $page ) {
682
+ // Get ID / Label of the page, since they're protected, by hacking into
683
+ // the callback filter for 'woocommerce_settings_tabs_array'
684
+ $info = $page->add_settings_page( array() );
685
+ $page_id = key( $info );
686
+ $page_label = current( $info );
687
+ $sections = $page->get_sections();
688
+
689
+ if ( empty( $sections ) ) {
690
+ $sections[''] = $page_label;
691
+ }
692
+
693
+ $settings_pages[ $page_id ] = $page_label;
694
+
695
+ // Remove non-fields ( sections, titles and whatever )
696
+ $fields = array();
697
+
698
+ foreach ( $sections as $section_key => $section_label ) {
699
+ $_fields = array_filter(
700
+ $page->get_settings( $section_key ),
701
+ function( $item ) {
702
+ return isset( $item['id'] ) && ( ! in_array( $item['type'], array( 'title', 'sectionend' ) ) );
703
+ }
704
+ );
705
+
706
+ foreach ( $_fields as $field ) {
707
+ $title = isset( $field['title'] ) ? $field['title'] : ( isset( $field['desc'] ) ? $field['desc'] : 'N/A' );
708
+ $fields[ $field['id'] ] = array(
709
+ 'title' => $title,
710
+ 'page' => 'wc-settings',
711
+ 'tab' => $page_id,
712
+ 'section' => $section_key,
713
+ 'type' => esc_html__( 'setting', 'stream' ),
714
+ );
715
+ }
716
+ }
717
+
718
+ // Store fields in the global array to be searched later
719
+ $settings = array_merge( $settings, $fields );
720
+ }
721
+
722
+ // Provide additional context for each of the settings pages
723
+ array_walk( $settings_pages, function( &$value ) {
724
+ $value .= ' ' . esc_html__( 'Settings', 'stream' );
725
+ });
726
+
727
+ // Load Payment Gateway Settings
728
+ $payment_gateway_settings = array();
729
+ $payment_gateways = $woocommerce->payment_gateways();
730
+
731
+ foreach ( $payment_gateways->payment_gateways as $section_key => $payment_gateway ) {
732
+ $title = $payment_gateway->title;
733
+ $key = $payment_gateway->plugin_id . $payment_gateway->id . '_settings';
734
+
735
+ $payment_gateway_settings[ $key ] = array(
736
+ 'title' => $title,
737
+ 'page' => 'wc-settings',
738
+ 'tab' => 'checkout',
739
+ 'section' => strtolower( $section_key ),
740
+ 'type' => esc_html__( 'payment gateway', 'stream' ),
741
+ );
742
+ }
743
+
744
+ $settings = array_merge( $settings, $payment_gateway_settings );
745
+
746
+ // Load Shipping Method Settings
747
+ $shipping_method_settings = array();
748
+ $shipping_methods = $woocommerce->shipping();
749
+
750
+ foreach ( $shipping_methods->shipping_methods as $section_key => $shipping_method ) {
751
+ $title = $shipping_method->title;
752
+ $key = $shipping_method->plugin_id . $shipping_method->id . '_settings';
753
+
754
+ $shipping_method_settings[ $key ] = array(
755
+ 'title' => $title,
756
+ 'page' => 'wc-settings',
757
+ 'tab' => 'shipping',
758
+ 'section' => strtolower( $section_key ),
759
+ 'type' => esc_html__( 'shipping method', 'stream' ),
760
+ );
761
+ }
762
+
763
+ $settings = array_merge( $settings, $shipping_method_settings );
764
+
765
+ // Load Email Settings
766
+ $email_settings = array();
767
+ $emails = $woocommerce->mailer();
768
+
769
+ foreach ( $emails->emails as $section_key => $email ) {
770
+ $title = $email->title;
771
+ $key = $email->plugin_id . $email->id . '_settings';
772
+
773
+ $email_settings[ $key ] = array(
774
+ 'title' => $title,
775
+ 'page' => 'wc-settings',
776
+ 'tab' => 'email',
777
+ 'section' => strtolower( $section_key ),
778
+ 'type' => esc_html__( 'email', 'stream' ),
779
+ );
780
+ }
781
+
782
+ $settings = array_merge( $settings, $email_settings );
783
+
784
+ // Tools page
785
+ $tools_page = array(
786
+ 'tools' => esc_html__( 'Tools', 'stream' ),
787
+ );
788
+
789
+ $settings_pages = array_merge( $settings_pages, $tools_page );
790
+
791
+ // Cache the results
792
+ $settings_cache = array(
793
+ 'settings' => $settings,
794
+ 'settings_pages' => $settings_pages,
795
+ );
796
+
797
+ set_transient( $settings_cache_key, $settings_cache, MINUTE_IN_SECONDS * 60 * 6 );
798
+ }
799
+
800
+ $custom_settings = $this->get_custom_settings();
801
+ $this->settings = array_merge( $settings, $custom_settings );
802
+ $this->settings_pages = $settings_pages;
803
+
804
+ return $this->settings;
805
+ }
806
+ }
connectors/class-connector-wordpress-seo.php ADDED
@@ -0,0 +1,492 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace WP_Stream;
3
+
4
+ class Connector_WordPress_SEO extends Connector {
5
+
6
+ /**
7
+ * Connector slug
8
+ *
9
+ * @var string
10
+ */
11
+ public $name = 'wordpress-seo';
12
+
13
+ /**
14
+ * Holds tracked plugin minimum version required
15
+ *
16
+ * @const string
17
+ */
18
+ const PLUGIN_MIN_VERSION = '1.5.3.3';
19
+
20
+ /**
21
+ * Actions registered for this connector
22
+ *
23
+ * @var array
24
+ */
25
+ public $actions = array(
26
+ 'wpseo_handle_import',
27
+ 'wpseo_import',
28
+ 'seo_page_wpseo_files',
29
+ 'added_post_meta',
30
+ 'updated_post_meta',
31
+ 'deleted_post_meta',
32
+ );
33
+
34
+ /**
35
+ * Tracking registered Settings, with overridden data
36
+ *
37
+ * @var array
38
+ */
39
+ public $option_groups = array();
40
+
41
+ /**
42
+ * Check if plugin dependencies are satisfied and add an admin notice if not
43
+ *
44
+ * @return bool
45
+ */
46
+ public function is_dependency_satisfied() {
47
+ if ( defined( 'WPSEO_VERSION' ) && version_compare( WPSEO_VERSION, self::PLUGIN_MIN_VERSION, '>=' ) ) {
48
+ return true;
49
+ }
50
+
51
+ return false;
52
+ }
53
+
54
+ /**
55
+ * Return translated connector label
56
+ *
57
+ * @return string Translated connector label
58
+ */
59
+ public function get_label() {
60
+ return esc_html_x( 'WordPress SEO', 'wordpress-seo', 'stream' );
61
+ }
62
+
63
+ /**
64
+ * Return translated action labels
65
+ *
66
+ * @return array Action label translations
67
+ */
68
+ public function get_action_labels() {
69
+ return array(
70
+ 'created' => esc_html_x( 'Created', 'wordpress-seo', 'stream' ),
71
+ 'updated' => esc_html_x( 'Updated', 'wordpress-seo', 'stream' ),
72
+ 'added' => esc_html_x( 'Added', 'wordpress-seo', 'stream' ),
73
+ 'deleted' => esc_html_x( 'Deleted', 'wordpress-seo', 'stream' ),
74
+ 'exported' => esc_html_x( 'Exported', 'wordpress-seo', 'stream' ),
75
+ 'imported' => esc_html_x( 'Imported', 'wordpress-seo', 'stream' ),
76
+ );
77
+ }
78
+
79
+ /**
80
+ * Return translated context labels
81
+ *
82
+ * @return array Context label translations
83
+ */
84
+ public function get_context_labels() {
85
+ return array(
86
+ 'wpseo_dashboard' => esc_html_x( 'Dashboard', 'wordpress-seo', 'stream' ),
87
+ 'wpseo_titles' => _x( 'Titles &amp; Metas', 'wordpress-seo', 'stream' ),
88
+ 'wpseo_social' => esc_html_x( 'Social', 'wordpress-seo', 'stream' ),
89
+ 'wpseo_xml' => esc_html_x( 'XML Sitemaps', 'wordpress-seo', 'stream' ),
90
+ 'wpseo_permalinks' => esc_html_x( 'Permalinks', 'wordpress-seo', 'stream' ),
91
+ 'wpseo_internal-links' => esc_html_x( 'Internal Links', 'wordpress-seo', 'stream' ),
92
+ 'wpseo_rss' => esc_html_x( 'RSS', 'wordpress-seo', 'stream' ),
93
+ 'wpseo_import' => esc_html_x( 'Import & Export', 'wordpress-seo', 'stream' ),
94
+ 'wpseo_bulk-title-editor' => esc_html_x( 'Bulk Title Editor', 'wordpress-seo', 'stream' ),
95
+ 'wpseo_bulk-description-editor' => esc_html_x( 'Bulk Description Editor', 'wordpress-seo', 'stream' ),
96
+ 'wpseo_files' => esc_html_x( 'Files', 'wordpress-seo', 'stream' ),
97
+ 'wpseo_meta' => esc_html_x( 'Content', 'wordpress-seo', 'stream' ),
98
+ );
99
+ }
100
+
101
+ /**
102
+ * Add action links to Stream drop row in admin list screen
103
+ *
104
+ * @filter wp_stream_action_links_{connector}
105
+ *
106
+ * @param array $links Previous links registered
107
+ * @param Record $record Stream record
108
+ *
109
+ * @return array Action links
110
+ */
111
+ public function action_links( $links, $record ) {
112
+ // Options
113
+ if ( $option = $record->get_meta( 'option', true ) ) {
114
+ $key = $record->get_meta( 'option_key', true );
115
+
116
+ $links[ esc_html__( 'Edit', 'stream' ) ] = add_query_arg(
117
+ array(
118
+ 'page' => $record->context,
119
+ ),
120
+ admin_url( 'admin.php' )
121
+ ) . '#stream-highlight-' . esc_attr( $key );
122
+ } elseif ( 'wpseo_files' === $record->context ) {
123
+ $links[ esc_html__( 'Edit', 'stream' ) ] = add_query_arg(
124
+ array(
125
+ 'page' => $record->context,
126
+ ),
127
+ admin_url( 'admin.php' )
128
+ );
129
+ } elseif ( 'wpseo_meta' === $record->context ) {
130
+ $post = get_post( $record->object_id );
131
+
132
+ if ( $post ) {
133
+ $posts_connector = new Connector_Posts();
134
+ $post_type_name = $posts_connector->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 = $record->get_meta( '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 function register() {
179
+ parent::register();
180
+
181
+ foreach ( \WPSEO_Options::$options as $class ) {
182
+ /* @var $class WPSEO_Options */
183
+ $this->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( $this, 'admin_enqueue_scripts' ) );
190
+ add_filter( 'wp_stream_log_data', array( $this, 'log_override' ) );
191
+ }
192
+
193
+ public function admin_enqueue_scripts( $hook ) {
194
+ if ( 0 === strpos( $hook, 'seo_page_' ) ) {
195
+ $stream = wp_stream_get_instance();
196
+ $src = $stream->locations['url'] . '/ui/js/wpseo-admin.js';
197
+ wp_enqueue_script( 'stream-connector-wpseo', $src, array( 'jquery' ), $stream->get_version() );
198
+ }
199
+ }
200
+
201
+ /**
202
+ * Track importing settings from other plugins
203
+ */
204
+ public function callback_wpseo_handle_import() {
205
+ $imports = array(
206
+ 'importheadspace' => esc_html__( 'HeadSpace2', 'stream' ), # type = checkbox
207
+ 'importaioseo' => esc_html__( 'All-in-One SEO', 'stream' ), # type = checkbox
208
+ 'importaioseoold' => esc_html__( 'OLD All-in-One SEO', 'stream' ), # type = checkbox
209
+ 'importwoo' => esc_html__( 'WooThemes SEO framework', 'stream' ), # type = checkbox
210
+ 'importrobotsmeta' => esc_html__( 'Robots Meta (by Yoast)', 'stream' ), # type = checkbox
211
+ 'importrssfooter' => esc_html__( 'RSS Footer (by Yoast)', 'stream' ), # type = checkbox
212
+ 'importbreadcrumbs' => esc_html__( 'Yoast Breadcrumbs', 'stream' ), # type = checkbox
213
+ );
214
+
215
+ $opts = wp_stream_filter_input( INPUT_POST, 'wpseo' );
216
+
217
+ foreach ( $imports as $key => $name ) {
218
+ if ( isset( $opts[ $key ] ) ) {
219
+ $this->log(
220
+ sprintf(
221
+ __( 'Imported settings from %1$s%2$s', 'stream' ),
222
+ $name,
223
+ isset( $opts['deleteolddata'] ) ? esc_html__( ', and deleted old data', 'stream' ) : ''
224
+ ),
225
+ array(
226
+ 'key' => $key,
227
+ 'deleteolddata' => isset( $opts['deleteolddata'] ),
228
+ ),
229
+ null,
230
+ 'wpseo_import',
231
+ 'imported'
232
+ );
233
+ }
234
+ }
235
+ }
236
+
237
+ public function callback_wpseo_import() {
238
+ $opts = wp_stream_filter_input( INPUT_POST, 'wpseo' );
239
+
240
+ if ( wp_stream_filter_input( INPUT_POST, 'wpseo_export' ) ) {
241
+ $this->log(
242
+ sprintf(
243
+ __( 'Exported settings%s', 'stream' ),
244
+ isset( $opts['include_taxonomy_meta'] ) ? esc_html__( ', including taxonomy meta', 'stream' ) : ''
245
+ ),
246
+ array(
247
+ 'include_taxonomy_meta' => isset( $opts['include_taxonomy_meta'] ),
248
+ ),
249
+ null,
250
+ 'wpseo_import',
251
+ 'exported'
252
+ );
253
+ } elseif ( isset( $_FILES['settings_import_file']['name'] ) ) { // phpcs: input var okay
254
+ $this->log(
255
+ sprintf(
256
+ __( 'Tried importing settings from "%s"', 'stream' ),
257
+ sanitize_text_field( wp_unslash( $_FILES['settings_import_file']['name'] ) ) // phpcs: input var okay
258
+ ),
259
+ array(
260
+ 'file' => sanitize_text_field( wp_unslash( $_FILES['settings_import_file']['name'] ) ), // phpcs: input var okay
261
+ ),
262
+ null,
263
+ 'wpseo_import',
264
+ 'exported'
265
+ );
266
+ }
267
+ }
268
+
269
+ public function callback_seo_page_wpseo_files() {
270
+ if ( wp_stream_filter_input( INPUT_POST, 'create_robots' ) ) {
271
+ $message = esc_html__( 'Tried creating robots.txt file', 'stream' );
272
+ } elseif ( wp_stream_filter_input( INPUT_POST, 'submitrobots' ) ) {
273
+ $message = esc_html__( 'Tried updating robots.txt file', 'stream' );
274
+ } elseif ( wp_stream_filter_input( INPUT_POST, 'submithtaccess' ) ) {
275
+ $message = esc_html__( 'Tried updating htaccess file', 'stream' );
276
+ }
277
+
278
+ if ( isset( $message ) ) {
279
+ $this->log(
280
+ $message,
281
+ array(),
282
+ null,
283
+ 'wpseo_files',
284
+ 'updated'
285
+ );
286
+ }
287
+ }
288
+
289
+ public function callback_added_post_meta( $meta_id, $object_id, $meta_key, $meta_value ) {
290
+ unset( $meta_id );
291
+ $this->meta( $object_id, $meta_key, $meta_value );
292
+ }
293
+ public function callback_updated_post_meta( $meta_id, $object_id, $meta_key, $meta_value ) {
294
+ unset( $meta_id );
295
+ $this->meta( $object_id, $meta_key, $meta_value );
296
+ }
297
+ public function callback_deleted_post_meta( $meta_id, $object_id, $meta_key, $meta_value ) {
298
+ unset( $meta_id );
299
+ $this->meta( $object_id, $meta_key, $meta_value );
300
+ }
301
+
302
+ private function meta( $object_id, $meta_key, $meta_value ) {
303
+ $prefix = \WPSEO_Meta::$meta_prefix;
304
+
305
+ \WPSEO_Metabox::translate_meta_boxes();
306
+
307
+ if ( 0 !== strpos( $meta_key, $prefix ) ) {
308
+ return;
309
+ }
310
+
311
+ $key = str_replace( $prefix, '', $meta_key );
312
+
313
+ foreach ( \WPSEO_Meta::$meta_fields as $tab => $fields ) {
314
+ if ( isset( $fields[ $key ] ) ) {
315
+ $field = $fields[ $key ];
316
+ break;
317
+ }
318
+ }
319
+
320
+ if ( ! isset( $field, $field['title'], $tab ) || '' === $field['title'] ) {
321
+ return;
322
+ }
323
+
324
+ $post = get_post( $object_id );
325
+ $post_type_label = get_post_type_labels( get_post_type_object( $post->post_type ) )->singular_name;
326
+
327
+ $this->log(
328
+ sprintf(
329
+ __( 'Updated "%1$s" of "%2$s" %3$s', 'stream' ),
330
+ $field['title'],
331
+ $post->post_title,
332
+ $post_type_label
333
+ ),
334
+ array(
335
+ // @codingStandardsIgnoreStart
336
+ 'meta_key' => $meta_key,
337
+ 'meta_value' => $meta_value,
338
+ // @codingStandardsIgnoreEnd
339
+ 'post_type' => $post->post_type,
340
+ ),
341
+ $object_id,
342
+ 'wpseo_meta',
343
+ 'updated'
344
+ );
345
+ }
346
+
347
+ /**
348
+ * Override connector log for our own Settings / Actions
349
+ *
350
+ * @param array $data
351
+ *
352
+ * @return array|bool
353
+ */
354
+ public function log_override( $data ) {
355
+ if ( ! is_array( $data ) ) {
356
+ return $data;
357
+ }
358
+
359
+ global $pagenow;
360
+
361
+ if ( 'options.php' === $pagenow && 'settings' === $data['connector'] && wp_stream_filter_input( INPUT_POST, '_wp_http_referer' ) ) {
362
+ if ( ! isset( $data['args']['context'] ) || ! isset( $this->option_groups[ $data['args']['context'] ] ) ) {
363
+ return $data;
364
+ }
365
+
366
+ $page = preg_match( '#page=([^&]*)#', wp_stream_filter_input( INPUT_POST, '_wp_http_referer' ), $match ) ? $match[1] : '';
367
+ $labels = $this->get_context_labels();
368
+
369
+ if ( ! isset( $labels[ $page ] ) ) {
370
+ return $data;
371
+ }
372
+
373
+ if ( ! ( $label = $this->settings_labels( $data['args']['option_key'] ) ) ) {
374
+ $data['message'] = esc_html__( '%s settings updated', 'stream' );
375
+ $label = $labels[ $page ];
376
+ }
377
+
378
+ $data['args']['label'] = $label;
379
+ $data['args']['context'] = $page;
380
+ $data['context'] = $page;
381
+ $data['connector'] = $this->name;
382
+ }
383
+
384
+ return $data;
385
+ }
386
+
387
+ private function settings_labels( $option ) {
388
+ $labels = array(
389
+ // wp-content/plugins/wordpress-seo/admin/pages/dashboard.php:
390
+ 'yoast_tracking' => esc_html_x( "Allow tracking of this WordPress install's anonymous data.", 'wordpress-seo', 'stream' ), # type = checkbox
391
+ 'disableadvanced_meta' => esc_html_x( 'Disable the Advanced part of the WordPress SEO meta box', 'wordpress-seo', 'stream' ), # type = checkbox
392
+ 'alexaverify' => esc_html_x( 'Alexa Verification ID', 'wordpress-seo', 'stream' ), # type = textinput
393
+ 'msverify' => esc_html_x( 'Bing Webmaster Tools', 'wordpress-seo', 'stream' ), # type = textinput
394
+ 'googleverify' => esc_html_x( 'Google Webmaster Tools', 'wordpress-seo', 'stream' ), # type = textinput
395
+ 'pinterestverify' => esc_html_x( 'Pinterest', 'wordpress-seo', 'stream' ), # type = textinput
396
+ 'yandexverify' => esc_html_x( 'Yandex Webmaster Tools', 'wordpress-seo', 'stream' ), # type = textinput
397
+
398
+ // wp-content/plugins/wordpress-seo/admin/pages/internal-links.php:
399
+ 'breadcrumbs-enable' => esc_html_x( 'Enable Breadcrumbs', 'wordpress-seo', 'stream' ), # type = checkbox
400
+ 'breadcrumbs-sep' => esc_html_x( 'Separator between breadcrumbs', 'wordpress-seo', 'stream' ), # type = textinput
401
+ 'breadcrumbs-home' => esc_html_x( 'Anchor text for the Homepage', 'wordpress-seo', 'stream' ), # type = textinput
402
+ 'breadcrumbs-prefix' => esc_html_x( 'Prefix for the breadcrumb path', 'wordpress-seo', 'stream' ), # type = textinput
403
+ 'breadcrumbs-archiveprefix' => esc_html_x( 'Prefix for Archive breadcrumbs', 'wordpress-seo', 'stream' ), # type = textinput
404
+ 'breadcrumbs-searchprefix' => esc_html_x( 'Prefix for Search Page breadcrumbs', 'wordpress-seo', 'stream' ), # type = textinput
405
+ 'breadcrumbs-404crumb' => esc_html_x( 'Breadcrumb for 404 Page', 'wordpress-seo', 'stream' ), # type = textinput
406
+ 'breadcrumbs-blog-remove' => esc_html_x( 'Remove Blog page from Breadcrumbs', 'wordpress-seo', 'stream' ), # type = checkbox
407
+ 'breadcrumbs-boldlast' => esc_html_x( 'Bold the last page in the breadcrumb', 'wordpress-seo', 'stream' ), # type = checkbox
408
+
409
+ // wp-content/plugins/wordpress-seo/admin/pages/metas.php:
410
+ 'forcerewritetitle' => esc_html_x( 'Force rewrite titles', 'wordpress-seo', 'stream' ), # type = checkbox
411
+ 'noindex-subpages-wpseo' => esc_html_x( 'Noindex subpages of archives', 'wordpress-seo', 'stream' ), # type = checkbox
412
+ 'usemetakeywords' => _x( 'Use <code>meta</code> keywords tag?', 'wordpress-seo', 'stream' ), # type = checkbox
413
+ 'noodp' => _x( 'Add <code>noodp</code> meta robots tag sitewide', 'wordpress-seo', 'stream' ), # type = checkbox
414
+ 'noydir' => _x( 'Add <code>noydir</code> meta robots tag sitewide', 'wordpress-seo', 'stream' ), # type = checkbox
415
+ 'hide-rsdlink' => esc_html_x( 'Hide RSD Links', 'wordpress-seo', 'stream' ), # type = checkbox
416
+ 'hide-wlwmanifest' => esc_html_x( 'Hide WLW Manifest Links', 'wordpress-seo', 'stream' ), # type = checkbox
417
+ 'hide-shortlink' => esc_html_x( 'Hide Shortlink for posts', 'wordpress-seo', 'stream' ), # type = checkbox
418
+ 'hide-feedlinks' => esc_html_x( 'Hide RSS Links', 'wordpress-seo', 'stream' ), # type = checkbox
419
+ 'disable-author' => esc_html_x( 'Disable the author archives', 'wordpress-seo', 'stream' ), # type = checkbox
420
+ 'disable-date' => esc_html_x( 'Disable the date-based archives', 'wordpress-seo', 'stream' ), # type = checkbox
421
+
422
+ // wp-content/plugins/wordpress-seo/admin/pages/network.php:
423
+ 'access' => esc_html_x( 'Who should have access to the WordPress SEO settings', 'wordpress-seo', 'stream' ), # type = select
424
+ 'defaultblog' => esc_html_x( 'New blogs get the SEO settings from this blog', 'wordpress-seo', 'stream' ), # type = textinput
425
+ 'restoreblog' => esc_html_x( 'Blog ID', 'wordpress-seo', 'stream' ), # type = textinput
426
+
427
+ // wp-content/plugins/wordpress-seo/admin/pages/permalinks.php:
428
+ 'stripcategorybase' => _x( 'Strip the category base (usually <code>/category/</code>) from the category URL.', 'wordpress-seo', 'stream' ), # type = checkbox
429
+ 'trailingslash' => esc_html_x( "Enforce a trailing slash on all category and tag URL's", 'wordpress-seo', 'stream' ), # type = checkbox
430
+ 'cleanslugs' => esc_html_x( 'Remove stop words from slugs.', 'wordpress-seo', 'stream' ), # type = checkbox
431
+ 'redirectattachment' => esc_html_x( "Redirect attachment URL's to parent post URL.", 'wordpress-seo', 'stream' ), # type = checkbox
432
+ 'cleanreplytocom' => _x( 'Remove the <code>?replytocom</code> variables.', 'wordpress-seo', 'stream' ), # type = checkbox
433
+ 'cleanpermalinks' => esc_html_x( "Redirect ugly URL's to clean permalinks. (Not recommended in many cases!)", 'wordpress-seo', 'stream' ), # type = checkbox
434
+ 'force_transport' => esc_html_x( 'Force Transport', 'wordpress-seo', 'stream' ), # type = select
435
+ 'cleanpermalink-googlesitesearch' => esc_html_x( "Prevent cleaning out Google Site Search URL's.", 'wordpress-seo', 'stream' ), # type = checkbox
436
+ 'cleanpermalink-googlecampaign' => esc_html_x( 'Prevent cleaning out Google Analytics Campaign & Google AdWords Parameters.', 'wordpress-seo', 'stream' ), # type = checkbox
437
+ 'cleanpermalink-extravars' => esc_html_x( 'Other variables not to clean', 'wordpress-seo', 'stream' ), # type = textinput
438
+
439
+ // wp-content/plugins/wordpress-seo/admin/pages/social.php:
440
+ 'opengraph' => esc_html_x( 'Add Open Graph meta data', 'wordpress-seo', 'stream' ), # type = checkbox
441
+ 'facebook_site' => esc_html_x( 'Facebook Page URL', 'wordpress-seo', 'stream' ), # type = textinput
442
+ 'og_frontpage_image' => esc_html_x( 'Image URL', 'wordpress-seo', 'stream' ), # type = textinput
443
+ 'og_frontpage_desc' => esc_html_x( 'Description', 'wordpress-seo', 'stream' ), # type = textinput
444
+ 'og_default_image' => esc_html_x( 'Image URL', 'wordpress-seo', 'stream' ), # type = textinput
445
+ 'twitter' => esc_html_x( 'Add Twitter card meta data', 'wordpress-seo', 'stream' ), # type = checkbox
446
+ 'twitter_site' => esc_html_x( 'Site Twitter Username', 'wordpress-seo', 'stream' ), # type = textinput
447
+ 'twitter_card_type' => esc_html_x( 'The default card type to use', 'wordpress-seo', 'stream' ), # type = select
448
+ 'googleplus' => esc_html_x( 'Add Google+ specific post meta data (excluding author metadata)', 'wordpress-seo', 'stream' ), # type = checkbox
449
+ 'plus-publisher' => esc_html_x( 'Google Publisher Page', 'wordpress-seo', 'stream' ), # type = textinput
450
+
451
+ // wp-content/plugins/wordpress-seo/admin/pages/xml-sitemaps.php:
452
+ 'enablexmlsitemap' => esc_html_x( 'Check this box to enable XML sitemap functionality.', 'wordpress-seo', 'stream' ), # type = checkbox
453
+ 'disable_author_sitemap' => esc_html_x( 'Disable author/user sitemap', 'wordpress-seo', 'stream' ), # type = checkbox
454
+ 'xml_ping_yahoo' => esc_html_x( 'Ping Yahoo!', 'wordpress-seo', 'stream' ), # type = checkbox
455
+ 'xml_ping_ask' => esc_html_x( 'Ping Ask.com', 'wordpress-seo', 'stream' ), # type = checkbox
456
+ 'entries-per-page' => esc_html_x( 'Max entries per sitemap page', 'wordpress-seo', 'stream' ), # type = textinput
457
+
458
+ // Added manually
459
+ 'rssbefore' => esc_html_x( 'Content to put before each post in the feed', 'wordpress-seo', 'stream' ),
460
+ 'rssafter' => esc_html_x( 'Content to put after each post', 'wordpress-seo', 'stream' ),
461
+ );
462
+
463
+ $ast_labels = array(
464
+ 'title-' => esc_html_x( 'Title template', 'wordpress-seo', 'stream' ), # type = textinput
465
+ 'metadesc-' => esc_html_x( 'Meta description template', 'wordpress-seo', 'stream' ), # type = textarea
466
+ 'metakey-' => esc_html_x( 'Meta keywords template', 'wordpress-seo', 'stream' ), # type = textinput
467
+ 'noindex-' => esc_html_x( 'Meta Robots', 'wordpress-seo', 'stream' ), # type = checkbox
468
+ 'noauthorship-' => esc_html_x( 'Authorship', 'wordpress-seo', 'stream' ), # type = checkbox
469
+ 'showdate-' => esc_html_x( 'Show date in snippet preview?', 'wordpress-seo', 'stream' ), # type = checkbox
470
+ 'hideeditbox-' => esc_html_x( 'WordPress SEO Meta Box', 'wordpress-seo', 'stream' ), # type = checkbox
471
+ 'bctitle-' => esc_html_x( 'Breadcrumbs Title', 'wordpress-seo', 'stream' ), # type = textinput
472
+ 'post_types-' => esc_html_x( 'Post types', 'wordpress-seo', 'stream' ), # type = checkbox
473
+ 'taxonomies-' => esc_html_x( 'Taxonomies', 'wordpress-seo', 'stream' ), # type = checkbox
474
+ );
475
+
476
+ if ( $option ) {
477
+ if ( isset( $labels[ $option ] ) ) {
478
+ return $labels[ $option ];
479
+ } else {
480
+ foreach ( $ast_labels as $key => $trans ) {
481
+ if ( 0 === strpos( $option, $key ) ) {
482
+ return $trans;
483
+ }
484
+ }
485
+
486
+ return false;
487
+ }
488
+ }
489
+
490
+ return $labels;
491
+ }
492
+ }
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/wp-stream/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/wp-stream/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/wp-stream/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>/stream.git
71
+ # Navigate to the newly cloned directory
72
+ cd stream
73
+ # Assign the original repo to a remote called "upstream"
74
+ git remote add upstream https://github.com/wp-stream/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).
includes/db-updates.php ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * Version 3.0.0
4
+ *
5
+ * Update from 1.4.9
6
+ *
7
+ * @param string $db_version
8
+ * @param string $current_version
9
+ *
10
+ * @return string
11
+ */
12
+ function wp_stream_update_300( $db_version, $current_version ) {
13
+ global $wpdb;
14
+
15
+ // Get only the author_meta values that are double-serialized
16
+ $plugin = wp_stream_get_instance();
17
+ $prefix = $plugin->install->table_prefix;
18
+
19
+ return $current_version;
20
+ }
includes/feeds/atom.php ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ header( 'Content-Type: ' . feed_content_type( 'atom' ) . '; charset=' . get_option( 'blog_charset' ), true );
3
+ printf( '<?xml version="1.0" encoding="%s"?>', esc_attr( get_option( 'blog_charset' ) ) );
4
+ ?>
5
+
6
+ <feed xmlns="http://www.w3.org/2005/Atom" xmlns:thr="http://purl.org/syndication/thread/1.0" xml:lang="<?php echo esc_attr( bloginfo_rss( 'language' ) ) ?>" xmlns:sy="http://purl.org/rss/1.0/modules/syndication/" <?php do_action( 'atom_ns' ) ?>>
7
+ <title><?php bloginfo_rss( 'name' ) ?> - <?php esc_html_e( 'Stream Feed', 'stream' ) ?></title>
8
+ <link href="<?php self_link() ?>" rel="self" type="application/rss+xml" />
9
+ <link href="<?php echo esc_url( $records_admin_url ) ?>" />
10
+ <subtitle type="html"><?php esc_html( bloginfo_rss( 'description' ) ) ?></subtitle>
11
+ <updated><?php echo esc_html( mysql2date( 'c', $latest_record, false ) ) ?></updated>
12
+ <id><?php echo esc_url( $latest_link ) ?></id>
13
+ <sy:updatePeriod><?php echo esc_html( 'hourly' ) ?></sy:updatePeriod>
14
+ <sy:updateFrequency><?php echo absint( 1 ) ?></sy:updateFrequency>
15
+ <?php
16
+ /**
17
+ * Action fires during RSS head
18
+ */
19
+ do_action( 'atom_head' );
20
+
21
+ foreach ( $records as $record ) :
22
+ $record_link = add_query_arg(
23
+ array(
24
+ 'record__in' => $record->ID,
25
+ ),
26
+ $records_admin_url
27
+ );
28
+
29
+ $author = get_userdata( $record->author );
30
+ $display_name = isset( $author->display_name ) ? $author->display_name : 'N/A';
31
+ ?>
32
+ <entry>
33
+ <title type="html"><![CDATA[[<?php echo esc_html( $domain ) ?>] <?php echo esc_html( $record->summary ) // xss ok ?> ]]></title>
34
+ <link href="<?php echo esc_url( $record_link ) ?>" />
35
+ <updated><?php echo esc_html( mysql2date( 'c', $record->created, false ) ) ?></updated>
36
+ <author>
37
+ <name><?php echo esc_html( $display_name ) ?></name>
38
+ </author>
39
+ <category term="connector" label="<?php echo esc_html( $record->connector ) ?>" />
40
+ <category term="context" label="<?php echo esc_html( $record->context ) ?>"/>
41
+ <category term="action" label="<?php echo esc_html( $record->action ) ?>" />
42
+ <category term="ip" label="<?php echo esc_html( $record->ip ) ?>" />
43
+ <id><?php echo esc_url( $record_link ) ?></id>
44
+ <summary type="html"><![CDATA[- <?php echo esc_html( $display_name ) ?> ]]></summary>
45
+ <?php
46
+ /**
47
+ * Action fires during Atom item
48
+ */
49
+ do_action( 'atom_item' )
50
+ ?>
51
+ </entry>
52
+ <?php endforeach; ?>
53
+ </feed>
54
+ <?php
includes/feeds/json.php ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
1
+ <?php
2
+ header( 'Content-type: application/json; charset=' . get_option( 'blog_charset' ), true );
3
+ if ( version_compare( PHP_VERSION, '5.4', '<' ) ) {
4
+ echo wp_stream_json_encode( $records ); // xss ok
5
+ } else {
6
+ echo wp_stream_json_encode( $records, JSON_PRETTY_PRINT ); // xss ok
7
+ }
includes/feeds/rss-2.0.php ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ header( 'Content-Type: ' . feed_content_type( 'rss-http' ) . '; charset=' . get_option( 'blog_charset' ), true );
3
+ printf( '<?xml version="1.0" encoding="%s"?>', esc_attr( get_option( 'blog_charset' ) ) );
4
+ ?>
5
+
6
+ <rss version="2.0"
7
+ xmlns:content="http://purl.org/rss/1.0/modules/content/"
8
+ xmlns:wfw="http://wellformedweb.org/CommentAPI/"
9
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
10
+ xmlns:atom="http://www.w3.org/2005/Atom"
11
+ xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
12
+ xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
13
+ <?php
14
+ /**
15
+ * Action fires during RSS xmls printing
16
+ */
17
+ do_action( 'rss2_ns' )
18
+ ?>
19
+ >
20
+ <channel>
21
+ <title><?php bloginfo_rss( 'name' ) ?> - <?php esc_html_e( 'Stream Feed', 'stream' ) ?></title>
22
+ <atom:link href="<?php self_link() ?>" rel="self" type="application/rss+xml" />
23
+ <link><?php echo esc_url( $records_admin_url ) ?></link>
24
+ <description><?php bloginfo_rss( 'description' ) ?></description>
25
+ <lastBuildDate><?php echo esc_html( mysql2date( 'r', $latest_record, false ) ) ?></lastBuildDate>
26
+ <language><?php bloginfo_rss( 'language' ) ?></language>
27
+ <sy:updatePeriod><?php echo esc_html( 'hourly' ) ?></sy:updatePeriod>
28
+ <sy:updateFrequency><?php echo absint( 1 ) ?></sy:updateFrequency>
29
+ <?php
30
+ /**
31
+ * Action fires during RSS head
32
+ */
33
+ do_action( 'rss2_head' );
34
+
35
+ foreach ( $records as $record ) :
36
+ $record_link = add_query_arg(
37
+ array(
38
+ 'record__in' => $record->ID,
39
+ ),
40
+ $records_admin_url
41
+ );
42
+
43
+ $author = get_userdata( $record->author );
44
+ $display_name = isset( $author->display_name ) ? $author->display_name : 'N/A';
45
+ ?>
46
+ <item>
47
+ <title><![CDATA[ <?php echo esc_html( $record->summary ) // xss ok ?> ]]></title>
48
+ <pubDate><?php echo esc_html( mysql2date( 'r', $record->created, false ) ) ?></pubDate>
49
+ <dc:creator><?php echo esc_html( $display_name ) ?></dc:creator>
50
+ <category domain="connector"><![CDATA[ <?php echo esc_html( $record->connector ) ?> ]]></category>
51
+ <category domain="context"><![CDATA[ <?php echo esc_html( $record->context ) ?> ]]></category>
52
+ <category domain="action"><![CDATA[ <?php echo esc_html( $record->action ) ?> ]]></category>
53
+ <category domain="ip"><?php echo esc_html( $record->ip ) ?></category>
54
+ <guid isPermaLink="false"><?php echo esc_url( $record_link ) ?></guid>
55
+ <link><?php echo esc_url( $record_link ) ?></link>
56
+ <?php
57
+ /**
58
+ * Action fires during RSS item
59
+ */
60
+ do_action( 'rss2_item' )
61
+ ?>
62
+ </item>
63
+ <?php endforeach; ?>
64
+ </channel>
65
+ </rss>
includes/functions.php ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Gets a specific external variable by name and optionally filters it.
5
+ *
6
+ * This is a polyfill function intended to be used in place of PHP's
7
+ * filter_input() function, which can occasionally be unreliable.
8
+ *
9
+ * @param int $type One of INPUT_GET, INPUT_POST, INPUT_COOKIE, INPUT_SERVER, or INPUT_ENV.
10
+ * @param string $variable_name Name of a variable to get.
11
+ * @param int $filter The ID of the filter to apply.
12
+ * @param mixed $options Associative array of options or bitwise disjunction of flags. If filter accepts options, flags can be provided in "flags" field of array.
13
+ *
14
+ * @return Value of the requested variable on success, FALSE if the filter fails, or NULL if the $variable_name is not set.
15
+ */
16
+ function wp_stream_filter_input( $type, $variable_name, $filter = null, $options = array() ) {
17
+ return call_user_func_array( array( '\WP_Stream\Filter_Input', 'super' ), func_get_args() );
18
+ }
19
+
20
+ /**
21
+ * Filters a variable with a specified filter.
22
+ *
23
+ * This is a polyfill function intended to be used in place of PHP's
24
+ * filter_var() function, which can occasionally be unreliable.
25
+ *
26
+ * @param string $var Value to filter.
27
+ * @param int $filter The ID of the filter to apply.
28
+ * @param mixed $options Associative array of options or bitwise disjunction of flags. If filter accepts options, flags can be provided in "flags" field of array. For the "callback" filter, callable type should be passed. The callback must accept one argument, the value to be filtered, and return the value after filtering/sanitizing it.
29
+ *
30
+ * @return Returns the filtered data, or FALSE if the filter fails.
31
+ */
32
+ function wp_stream_filter_var( $var, $filter = null, $options = array() ) {
33
+ return call_user_func_array( array( '\WP_Stream\Filter_Input', 'filter' ), func_get_args() );
34
+ }
35
+
36
+ /**
37
+ * Converts a time into an ISO 8601 extended formatted string.
38
+ *
39
+ * @param int|bool $time Seconds since unix epoc
40
+ * @param int $offset Hour offset
41
+ *
42
+ * @return string an ISO 8601 extended formatted time
43
+ */
44
+ function wp_stream_get_iso_8601_extended_date( $time = false, $offset = 0 ) {
45
+ if ( $time ) {
46
+ $microtime = (float) $time . '.0000';
47
+ } else {
48
+ $microtime = microtime( true );
49
+ }
50
+
51
+ $micro_seconds = sprintf( '%06d', ( $microtime - floor( $microtime ) ) * 1000000 );
52
+ $offset_string = sprintf( 'Etc/GMT%s%s', $offset < 0 ? '+' : '-', abs( $offset ) );
53
+
54
+ $timezone = new DateTimeZone( $offset_string );
55
+ $date = new DateTime( date( 'Y-m-d H:i:s.' . $micro_seconds, $microtime ), $timezone );
56
+
57
+ return sprintf(
58
+ '%s%03d%s',
59
+ $date->format( 'Y-m-d\TH:i:s.' ),
60
+ floor( $date->format( 'u' ) / 1000 ),
61
+ $date->format( 'O' )
62
+ );
63
+ }
64
+
65
+ /**
66
+ * Encode to JSON in a way that is also backwards compatible
67
+ *
68
+ * @param mixed $data
69
+ * @param int $options (optional)
70
+ * @param int $depth (optional)
71
+ *
72
+ * @return string
73
+ */
74
+ function wp_stream_json_encode( $data, $options = 0, $depth = 512 ) {
75
+ if ( function_exists( 'wp_json_encode' ) ) {
76
+ $json = wp_json_encode( $data, $options, $depth );
77
+ } else {
78
+ // @codingStandardsIgnoreStart
79
+ if ( version_compare( PHP_VERSION, '5.5', '<' ) ) {
80
+ $json = json_encode( $data, $options );
81
+ } else {
82
+ $json = json_encode( $data, $options, $depth );
83
+ }
84
+ // @codingStandardsIgnoreEnd
85
+ }
86
+
87
+ return $json;
88
+ }
89
+
90
+ /**
91
+ * Check if Stream is running on WordPress.com VIP
92
+ *
93
+ * @return bool
94
+ */
95
+ function wp_stream_is_vip() {
96
+ return function_exists( 'wpcom_vip_load_plugin' );
97
+ }
98
+
99
+ /**
100
+ * True if native WP Cron is enabled, otherwise false
101
+ *
102
+ * @return bool
103
+ */
104
+ function wp_stream_is_cron_enabled() {
105
+ return ( defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON ) ? false : true;
106
+ }
includes/lib/Carbon.php ADDED
@@ -0,0 +1,2213 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /*
4
+ * This file is part of the Carbon package.
5
+ *
6
+ * (c) Brian Nesbitt <brian@nesbot.com>
7
+ *
8
+ * For the full copyright and license information, please view the LICENSE
9
+ * file that was distributed with this source code.
10
+ */
11
+
12
+ namespace Carbon;
13
+
14
+ use Closure;
15
+ use DateTime;
16
+ use DateTimeZone;
17
+ use DateInterval;
18
+ use DatePeriod;
19
+ use InvalidArgumentException;
20
+
21
+ /**
22
+ * A simple API extension for DateTime
23
+ *
24
+ * @property integer $year
25
+ * @property integer $month
26
+ * @property integer $day
27
+ * @property integer $hour
28
+ * @property integer $minute
29
+ * @property integer $second
30
+ * @property integer $timestamp seconds since the Unix Epoch
31
+ * @property-read integer $micro
32
+ * @property-read integer $dayOfWeek 0 (for Sunday) through 6 (for Saturday)
33
+ * @property-read integer $dayOfYear 0 through 365
34
+ * @property-read integer $weekOfMonth 1 through 6
35
+ * @property-read integer $weekOfYear ISO-8601 week number of year, weeks starting on Monday
36
+ * @property-read integer $daysInMonth number of days in the given month
37
+ * @property-read integer $age does a diffInYears() with default parameters
38
+ * @property-read integer $quarter the quarter of this instance, 1 - 4
39
+ * @property-read integer $offset the timezone offset in seconds from UTC
40
+ * @property-read integer $offsetHours the timezone offset in hours from UTC
41
+ * @property-read boolean $dst daylight savings time indicator, true if DST, false otherwise
42
+ * @property-read boolean $local checks if the timezone is local, true if local, false otherwise
43
+ * @property-read boolean $utc checks if the timezone is UTC, true if UTC, false otherwise
44
+ * @property-read string $timezoneName
45
+ * @property-read string $tzName
46
+ *
47
+ * @property-read DateTimeZone $timezone the current timezone
48
+ * @property-read DateTimeZone $tz alias of timezone
49
+ * @property-write DateTimeZone|string $timezone the current timezone
50
+ * @property-write DateTimeZone|string $tz alias of timezone
51
+ *
52
+ */
53
+ class Carbon extends DateTime
54
+ {
55
+ /**
56
+ * The day constants
57
+ */
58
+ const SUNDAY = 0;
59
+ const MONDAY = 1;
60
+ const TUESDAY = 2;
61
+ const WEDNESDAY = 3;
62
+ const THURSDAY = 4;
63
+ const FRIDAY = 5;
64
+ const SATURDAY = 6;
65
+
66
+ /**
67
+ * Names of days of the week.
68
+ *
69
+ * @var array
70
+ */
71
+ protected static $days = array(
72
+ self::SUNDAY => 'Sunday',
73
+ self::MONDAY => 'Monday',
74
+ self::TUESDAY => 'Tuesday',
75
+ self::WEDNESDAY => 'Wednesday',
76
+ self::THURSDAY => 'Thursday',
77
+ self::FRIDAY => 'Friday',
78
+ self::SATURDAY => 'Saturday'
79
+ );
80
+
81
+ /**
82
+ * Terms used to detect if a time passed is a relative date for testing purposes
83
+ *
84
+ * @var array
85
+ */
86
+ protected static $relativeKeywords = array(
87
+ 'this',
88
+ 'next',
89
+ 'last',
90
+ 'tomorrow',
91
+ 'yesterday',
92
+ '+',
93
+ '-',
94
+ 'first',
95
+ 'last',
96
+ 'ago'
97
+ );
98
+
99
+ /**
100
+ * Number of X in Y
101
+ */
102
+ const YEARS_PER_CENTURY = 100;
103
+ const YEARS_PER_DECADE = 10;
104
+ const MONTHS_PER_YEAR = 12;
105
+ const WEEKS_PER_YEAR = 52;
106
+ const DAYS_PER_WEEK = 7;
107
+ const HOURS_PER_DAY = 24;
108
+ const MINUTES_PER_HOUR = 60;
109
+ const SECONDS_PER_MINUTE = 60;
110
+
111
+ /**
112
+ * Default format to use for __toString method when type juggling occurs.
113
+ *
114
+ * @var string
115
+ */
116
+ const DEFAULT_TO_STRING_FORMAT = 'Y-m-d H:i:s';
117
+
118
+ /**
119
+ * Format to use for __toString method when type juggling occurs.
120
+ *
121
+ * @var string
122
+ */
123
+ protected static $toStringFormat = self::DEFAULT_TO_STRING_FORMAT;
124
+
125
+ /**
126
+ * A test Carbon instance to be returned when now instances are created
127
+ *
128
+ * @var Carbon
129
+ */
130
+ protected static $testNow;
131
+
132
+ /**
133
+ * Creates a DateTimeZone from a string or a DateTimeZone
134
+ *
135
+ * @param DateTimeZone|string $object
136
+ *
137
+ * @return DateTimeZone
138
+ *
139
+ * @throws InvalidArgumentException
140
+ */
141
+ protected static function safeCreateDateTimeZone($object)
142
+ {
143
+ if ($object instanceof DateTimeZone) {
144
+ return $object;
145
+ }
146
+
147
+ $tz = @timezone_open((string) $object);
148
+
149
+ if ($tz === false) {
150
+ throw new InvalidArgumentException('Unknown or bad timezone ('.$object.')');
151
+ }
152
+
153
+ return $tz;
154
+ }
155
+
156
+ ///////////////////////////////////////////////////////////////////
157
+ //////////////////////////// CONSTRUCTORS /////////////////////////
158
+ ///////////////////////////////////////////////////////////////////
159
+
160
+ /**
161
+ * Create a new Carbon instance.
162
+ *
163
+ * Please see the testing aids section (specifically static::setTestNow())
164
+ * for more on the possibility of this constructor returning a test instance.
165
+ *
166
+ * @param string $time
167
+ * @param DateTimeZone|string $tz
168
+ */
169
+ public function __construct($time = null, $tz = null)
170
+ {
171
+ // If the class has a test now set and we are trying to create a now()
172
+ // instance then override as required
173
+ if (static::hasTestNow() && (empty($time) || $time === 'now' || static::hasRelativeKeywords($time))) {
174
+ $testInstance = clone static::getTestNow();
175
+ if (static::hasRelativeKeywords($time)) {
176
+ $testInstance->modify($time);
177
+ }
178
+
179
+ //shift the time according to the given time zone
180
+ if ($tz !== NULL && $tz != static::getTestNow()->tz) {
181
+ $testInstance->setTimezone($tz);
182
+ } else {
183
+ $tz = $testInstance->tz;
184
+ }
185
+
186
+ $time = $testInstance->toDateTimeString();
187
+ }
188
+
189
+ if ($tz !== null) {
190
+ parent::__construct($time, static::safeCreateDateTimeZone($tz));
191
+ } else {
192
+ parent::__construct($time);
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Create a Carbon instance from a DateTime one
198
+ *
199
+ * @param DateTime $dt
200
+ *
201
+ * @return static
202
+ */
203
+ public static function instance(DateTime $dt)
204
+ {
205
+ return new static($dt->format('Y-m-d H:i:s.u'), $dt->getTimeZone());
206
+ }
207
+
208
+ /**
209
+ * Create a carbon instance from a string. This is an alias for the
210
+ * constructor that allows better fluent syntax as it allows you to do
211
+ * Carbon::parse('Monday next week')->fn() rather than
212
+ * (new Carbon('Monday next week'))->fn()
213
+ *
214
+ * @param string $time
215
+ * @param DateTimeZone|string $tz
216
+ *
217
+ * @return static
218
+ */
219
+ public static function parse($time = null, $tz = null)
220
+ {
221
+ return new static($time, $tz);
222
+ }
223
+
224
+ /**
225
+ * Get a Carbon instance for the current date and time
226
+ *
227
+ * @param DateTimeZone|string $tz
228
+ *
229
+ * @return static
230
+ */
231
+ public static function now($tz = null)
232
+ {
233
+ return new static(null, $tz);
234
+ }
235
+
236
+ /**
237
+ * Create a Carbon instance for today
238
+ *
239
+ * @param DateTimeZone|string $tz
240
+ *
241
+ * @return static
242
+ */
243
+ public static function today($tz = null)
244
+ {
245
+ return static::now($tz)->startOfDay();
246
+ }
247
+
248
+ /**
249
+ * Create a Carbon instance for tomorrow
250
+ *
251
+ * @param DateTimeZone|string $tz
252
+ *
253
+ * @return static
254
+ */
255
+ public static function tomorrow($tz = null)
256
+ {
257
+ return static::today($tz)->addDay();
258
+ }
259
+
260
+ /**
261
+ * Create a Carbon instance for yesterday
262
+ *
263
+ * @param DateTimeZone|string $tz
264
+ *
265
+ * @return static
266
+ */
267
+ public static function yesterday($tz = null)
268
+ {
269
+ return static::today($tz)->subDay();
270
+ }
271
+
272
+ /**
273
+ * Create a Carbon instance for the greatest supported date.
274
+ *
275
+ * @return Carbon
276
+ */
277
+ public static function maxValue()
278
+ {
279
+ return static::createFromTimestamp(PHP_INT_MAX);
280
+ }
281
+
282
+ /**
283
+ * Create a Carbon instance for the lowest supported date.
284
+ *
285
+ * @return Carbon
286
+ */
287
+ public static function minValue()
288
+ {
289
+ return static::createFromTimestamp(~PHP_INT_MAX);
290
+ }
291
+
292
+ /**
293
+ * Create a new Carbon instance from a specific date and time.
294
+ *
295
+ * If any of $year, $month or $day are set to null their now() values
296
+ * will be used.
297
+ *
298
+ * If $hour is null it will be set to its now() value and the default values
299
+ * for $minute and $second will be their now() values.
300
+ * If $hour is not null then the default values for $minute and $second
301
+ * will be 0.
302
+ *
303
+ * @param integer $year
304
+ * @param integer $month
305
+ * @param integer $day
306
+ * @param integer $hour
307
+ * @param integer $minute
308
+ * @param integer $second
309
+ * @param DateTimeZone|string $tz
310
+ *
311
+ * @return static
312
+ */
313
+ public static function create($year = null, $month = null, $day = null, $hour = null, $minute = null, $second = null, $tz = null)
314
+ {
315
+ $year = ($year === null) ? date('Y') : $year;
316
+ $month = ($month === null) ? date('n') : $month;
317
+ $day = ($day === null) ? date('j') : $day;
318
+
319
+ if ($hour === null) {
320
+ $hour = date('G');
321
+ $minute = ($minute === null) ? date('i') : $minute;
322
+ $second = ($second === null) ? date('s') : $second;
323
+ } else {
324
+ $minute = ($minute === null) ? 0 : $minute;
325
+ $second = ($second === null) ? 0 : $second;
326
+ }
327
+
328
+ return static::createFromFormat('Y-n-j G:i:s', sprintf('%s-%s-%s %s:%02s:%02s', $year, $month, $day, $hour, $minute, $second), $tz);
329
+ }
330
+
331
+ /**
332
+ * Create a Carbon instance from just a date. The time portion is set to now.
333
+ *
334
+ * @param integer $year
335
+ * @param integer $month
336
+ * @param integer $day
337
+ * @param DateTimeZone|string $tz
338
+ *
339
+ * @return static
340
+ */
341
+ public static function createFromDate($year = null, $month = null, $day = null, $tz = null)
342
+ {
343
+ return static::create($year, $month, $day, null, null, null, $tz);
344
+ }
345
+
346
+ /**
347
+ * Create a Carbon instance from just a time. The date portion is set to today.
348
+ *
349
+ * @param integer $hour
350
+ * @param integer $minute
351
+ * @param integer $second
352
+ * @param DateTimeZone|string $tz
353
+ *
354
+ * @return static
355
+ */
356
+ public static function createFromTime($hour = null, $minute = null, $second = null, $tz = null)
357
+ {
358
+ return static::create(null, null, null, $hour, $minute, $second, $tz);
359
+ }
360
+
361
+ /**
362
+ * Create a Carbon instance from a specific format
363
+ *
364
+ * @param string $format
365
+ * @param string $time
366
+ * @param DateTimeZone|string $tz
367
+ *
368
+ * @return static
369
+ *
370
+ * @throws InvalidArgumentException
371
+ */
372
+ public static function createFromFormat($format, $time, $tz = null)
373
+ {
374
+ if ($tz !== null) {
375
+ $dt = parent::createFromFormat($format, $time, static::safeCreateDateTimeZone($tz));
376
+ } else {
377
+ $dt = parent::createFromFormat($format, $time);
378
+ }
379
+
380
+ if ($dt instanceof DateTime) {
381
+ return static::instance($dt);
382
+ }
383
+
384
+ $errors = static::getLastErrors();
385
+ throw new InvalidArgumentException(implode(PHP_EOL, $errors['errors']));
386
+ }
387
+
388
+ /**
389
+ * Create a Carbon instance from a timestamp
390
+ *
391
+ * @param integer $timestamp
392
+ * @param DateTimeZone|string $tz
393
+ *
394
+ * @return static
395
+ */
396
+ public static function createFromTimestamp($timestamp, $tz = null)
397
+ {
398
+ return static::now($tz)->setTimestamp($timestamp);
399
+ }
400
+
401
+ /**
402
+ * Create a Carbon instance from an UTC timestamp
403
+ *
404
+ * @param integer $timestamp
405
+ *
406
+ * @return static
407
+ */
408
+ public static function createFromTimestampUTC($timestamp)
409
+ {
410
+ return new static('@'.$timestamp);
411
+ }
412
+
413
+ /**
414
+ * Get a copy of the instance
415
+ *
416
+ * @return static
417
+ */
418
+ public function copy()
419
+ {
420
+ return static::instance($this);
421
+ }
422
+
423
+ ///////////////////////////////////////////////////////////////////
424
+ ///////////////////////// GETTERS AND SETTERS /////////////////////
425
+ ///////////////////////////////////////////////////////////////////
426
+
427
+ /**
428
+ * Get a part of the Carbon object
429
+ *
430
+ * @param string $name
431
+ *
432
+ * @throws InvalidArgumentException
433
+ *
434
+ * @return string|integer|DateTimeZone
435
+ */
436
+ public function __get($name)
437
+ {
438
+ switch ($name) {
439
+ case 'year':
440
+ case 'month':
441
+ case 'day':
442
+ case 'hour':
443
+ case 'minute':
444
+ case 'second':
445
+ case 'micro':
446
+ case 'dayOfWeek':
447
+ case 'dayOfYear':
448
+ case 'weekOfYear':
449
+ case 'daysInMonth':
450
+ case 'timestamp':
451
+ $formats = array(
452
+ 'year' => 'Y',
453
+ 'month' => 'n',
454
+ 'day' => 'j',
455
+ 'hour' => 'G',
456
+ 'minute' => 'i',
457
+ 'second' => 's',
458
+ 'micro' => 'u',
459
+ 'dayOfWeek' => 'w',
460
+ 'dayOfYear' => 'z',
461
+ 'weekOfYear' => 'W',
462
+ 'daysInMonth' => 't',
463
+ 'timestamp' => 'U',
464
+ );
465
+
466
+ return (int) $this->format($formats[$name]);
467
+
468
+ case 'weekOfMonth':
469
+ return (int) ceil($this->day / self::DAYS_PER_WEEK);
470
+
471
+ case 'age':
472
+ return (int) $this->diffInYears();
473
+
474
+ case 'quarter':
475
+ return (int) ceil($this->month / 3);
476
+
477
+ case 'offset':
478
+ return $this->getOffset();
479
+
480
+ case 'offsetHours':
481
+ return $this->getOffset() / self::SECONDS_PER_MINUTE / self::MINUTES_PER_HOUR;
482
+
483
+ case 'dst':
484
+ return $this->format('I') == '1';
485
+
486
+ case 'local':
487
+ return $this->offset == $this->copy()->setTimezone(date_default_timezone_get())->offset;
488
+
489
+ case 'utc':
490
+ return $this->offset == 0;
491
+
492
+ case 'timezone':
493
+ case 'tz':
494
+ return $this->getTimezone();
495
+
496
+ case 'timezoneName':
497
+ case 'tzName':
498
+ return $this->getTimezone()->getName();
499
+
500
+ default:
501
+ throw new InvalidArgumentException(sprintf("Unknown getter '%s'", $name));
502
+ }
503
+ }
504
+
505
+ /**
506
+ * Check if an attribute exists on the object
507
+ *
508
+ * @param string $name
509
+ *
510
+ * @return boolean
511
+ */
512
+ public function __isset($name)
513
+ {
514
+ try {
515
+ $this->__get($name);
516
+ } catch (InvalidArgumentException $e) {
517
+ return false;
518
+ }
519
+
520
+ return true;
521
+ }
522
+
523
+ /**
524
+ * Set a part of the Carbon object
525
+ *
526
+ * @param string $name
527
+ * @param string|integer|DateTimeZone $value
528
+ *
529
+ * @throws InvalidArgumentException
530
+ */
531
+ public function __set($name, $value)
532
+ {
533
+ switch ($name) {
534
+ case 'year':
535
+ parent::setDate($value, $this->month, $this->day);
536
+ break;
537
+
538
+ case 'month':
539
+ parent::setDate($this->year, $value, $this->day);
540
+ break;
541
+
542
+ case 'day':
543
+ parent::setDate($this->year, $this->month, $value);
544
+ break;
545
+
546
+ case 'hour':
547
+ parent::setTime($value, $this->minute, $this->second);
548
+ break;
549
+
550
+ case 'minute':
551
+ parent::setTime($this->hour, $value, $this->second);
552
+ break;
553
+
554
+ case 'second':
555
+ parent::setTime($this->hour, $this->minute, $value);
556
+ break;
557
+
558
+ case 'timestamp':
559
+ parent::setTimestamp($value);
560
+ break;
561
+
562
+ case 'timezone':
563
+ case 'tz':
564
+ $this->setTimezone($value);
565
+ break;
566
+
567
+ default:
568
+ throw new InvalidArgumentException(sprintf("Unknown setter '%s'", $name));
569
+ }
570
+ }
571
+
572
+ /**
573
+ * Set the instance's year
574
+ *
575
+ * @param integer $value
576
+ *
577
+ * @return static
578
+ */
579
+ public function year($value)
580
+ {
581
+ $this->year = $value;
582
+
583
+ return $this;
584
+ }
585
+
586
+ /**
587
+ * Set the instance's month
588
+ *
589
+ * @param integer $value
590
+ *
591
+ * @return static
592
+ */
593
+ public function month($value)
594
+ {
595
+ $this->month = $value;
596
+
597
+ return $this;
598
+ }
599
+
600
+ /**
601
+ * Set the instance's day
602
+ *
603
+ * @param integer $value
604
+ *
605
+ * @return static
606
+ */
607
+ public function day($value)
608
+ {
609
+ $this->day = $value;
610
+
611
+ return $this;
612
+ }
613
+
614
+ /**
615
+ * Set the date all together
616
+ *
617
+ * @param integer $year
618
+ * @param integer $month
619
+ * @param integer $day
620
+ *
621
+ * @return static
622
+ */
623
+ public function setDate($year, $month, $day)
624
+ {
625
+ parent::setDate($year, $month, $day);
626
+
627
+ return $this;
628
+ }
629
+
630
+ /**
631
+ * Set the instance's hour
632
+ *
633
+ * @param integer $value
634
+ *
635
+ * @return static
636
+ */
637
+ public function hour($value)
638
+ {
639
+ $this->hour = $value;
640
+
641
+ return $this;
642
+ }
643
+
644
+ /**
645
+ * Set the instance's minute
646
+ *
647
+ * @param integer $value
648
+ *
649
+ * @return static
650
+ */
651
+ public function minute($value)
652
+ {
653
+ $this->minute = $value;
654
+
655
+ return $this;
656
+ }
657
+
658
+ /**
659
+ * Set the instance's second
660
+ *
661
+ * @param integer $value
662
+ *
663
+ * @return static
664
+ */
665
+ public function second($value)
666
+ {
667
+ $this->second = $value;
668
+
669
+ return $this;
670
+ }
671
+
672
+ /**
673
+ * Set the time all together
674
+ *
675
+ * @param integer $hour
676
+ * @param integer $minute
677
+ * @param integer $second
678
+ *
679
+ * @return static
680
+ */
681
+ public function setTime($hour, $minute, $second = 0)
682
+ {
683
+ parent::setTime($hour, $minute, $second);
684
+
685
+ return $this;
686
+ }
687
+
688
+ /**
689
+ * Set the date and time all together
690
+ *
691
+ * @param integer $year
692
+ * @param integer $month
693
+ * @param integer $day
694
+ * @param integer $hour
695
+ * @param integer $minute
696
+ * @param integer $second
697
+ *
698
+ * @return static
699
+ */
700
+ public function setDateTime($year, $month, $day, $hour, $minute, $second = 0)
701
+ {
702
+ return $this->setDate($year, $month, $day)->setTime($hour, $minute, $second);
703
+ }
704
+
705
+ /**
706
+ * Set the instance's timestamp
707
+ *
708
+ * @param integer $value
709
+ *
710
+ * @return static
711
+ */
712
+ public function timestamp($value)
713
+ {
714
+ $this->timestamp = $value;
715
+
716
+ return $this;
717
+ }
718
+
719
+ /**
720
+ * Alias for setTimezone()
721
+ *
722
+ * @param DateTimeZone|string $value
723
+ *
724
+ * @return static
725
+ */
726
+ public function timezone($value)
727
+ {
728
+ return $this->setTimezone($value);
729
+ }
730
+
731
+ /**
732
+ * Alias for setTimezone()
733
+ *
734
+ * @param DateTimeZone|string $value
735
+ *
736
+ * @return static
737
+ */
738
+ public function tz($value)
739
+ {
740
+ return $this->setTimezone($value);
741
+ }
742
+
743
+ /**
744
+ * Set the instance's timezone from a string or object
745
+ *
746
+ * @param DateTimeZone|string $value
747
+ *
748
+ * @return static
749
+ */
750
+ public function setTimezone($value)
751
+ {
752
+ parent::setTimezone(static::safeCreateDateTimeZone($value));
753
+
754
+ return $this;
755
+ }
756
+
757
+ ///////////////////////////////////////////////////////////////////
758
+ ///////////////////////// TESTING AIDS ////////////////////////////
759
+ ///////////////////////////////////////////////////////////////////
760
+
761
+ /**
762
+ * Set a Carbon instance (real or mock) to be returned when a "now"
763
+ * instance is created. The provided instance will be returned
764
+ * specifically under the following conditions:
765
+ * - A call to the static now() method, ex. Carbon::now()
766
+ * - When a null (or blank string) is passed to the constructor or parse(), ex. new Carbon(null)
767
+ * - When the string "now" is passed to the constructor or parse(), ex. new Carbon('now')
768
+ *
769
+ * Note the timezone parameter was left out of the examples above and
770
+ * has no affect as the mock value will be returned regardless of its value.
771
+ *
772
+ * To clear the test instance call this method using the default
773
+ * parameter of null.
774
+ *
775
+ * @param Carbon $testNow
776
+ */
777
+ public static function setTestNow(Carbon $testNow = null)
778
+ {
779
+ static::$testNow = $testNow;
780
+ }
781
+
782
+ /**
783
+ * Get the Carbon instance (real or mock) to be returned when a "now"
784
+ * instance is created.
785
+ *
786
+ * @return static the current instance used for testing
787
+ */
788
+ public static function getTestNow()
789
+ {
790
+ return static::$testNow;
791
+ }
792
+
793
+ /**
794
+ * Determine if there is a valid test instance set. A valid test instance
795
+ * is anything that is not null.
796
+ *
797
+ * @return boolean true if there is a test instance, otherwise false
798
+ */
799
+ public static function hasTestNow()
800
+ {
801
+ return static::getTestNow() !== null;
802
+ }
803
+
804
+ /**
805
+ * Determine if there is a relative keyword in the time string, this is to
806
+ * create dates relative to now for test instances. e.g.: next tuesday
807
+ *
808
+ * @param string $time
809
+ *
810
+ * @return boolean true if there is a keyword, otherwise false
811
+ */
812
+ public static function hasRelativeKeywords($time)
813
+ {
814
+ // skip common format with a '-' in it
815
+ if (preg_match('/[0-9]{4}-[0-9]{1,2}-[0-9]{1,2}/', $time) !== 1) {
816
+ foreach (static::$relativeKeywords as $keyword) {
817
+ if (stripos($time, $keyword) !== false) {
818
+ return true;
819
+ }
820
+ }
821
+ }
822
+
823
+ return false;
824
+ }
825
+
826
+ ///////////////////////////////////////////////////////////////////
827
+ /////////////////////// STRING FORMATTING /////////////////////////
828
+ ///////////////////////////////////////////////////////////////////
829
+
830
+ /**
831
+ * Format the instance with the current locale. You can set the current
832
+ * locale using setlocale() http://php.net/setlocale.
833
+ *
834
+ * @param string $format
835
+ *
836
+ * @return string
837
+ */
838
+ public function formatLocalized($format)
839
+ {
840
+ // Check for Windows to find and replace the %e
841
+ // modifier correctly
842
+ if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN') {
843
+ $format = preg_replace('#(?<!%)((?:%%)*)%e#', '\1%#d', $format);
844
+ }
845
+
846
+ return strftime($format, $this->timestamp);
847
+ }
848
+
849
+ /**
850
+ * Reset the format used to the default when type juggling a Carbon instance to a string
851
+ *
852
+ */
853
+ public static function resetToStringFormat()
854
+ {
855
+ static::setToStringFormat(self::DEFAULT_TO_STRING_FORMAT);
856
+ }
857
+
858
+ /**
859
+ * Set the default format used when type juggling a Carbon instance to a string
860
+ *
861
+ * @param string $format
862
+ */
863
+ public static function setToStringFormat($format)
864
+ {
865
+ static::$toStringFormat = $format;
866
+ }
867
+
868
+ /**
869
+ * Format the instance as a string using the set format
870
+ *
871
+ * @return string
872
+ */
873
+ public function __toString()
874
+ {
875
+ return $this->format(static::$toStringFormat);
876
+ }
877
+
878
+ /**
879
+ * Format the instance as date
880
+ *
881
+ * @return string
882
+ */
883
+ public function toDateString()
884
+ {
885
+ return $this->format('Y-m-d');
886
+ }
887
+
888
+ /**
889
+ * Format the instance as a readable date
890
+ *
891
+ * @return string
892
+ */
893
+ public function toFormattedDateString()
894
+ {
895
+ return $this->format('M j, Y');
896
+ }
897
+
898
+ /**
899
+ * Format the instance as time
900
+ *
901
+ * @return string
902
+ */
903
+ public function toTimeString()
904
+ {
905
+ return $this->format('H:i:s');
906
+ }
907
+
908
+ /**
909
+ * Format the instance as date and time
910
+ *
911
+ * @return string
912
+ */
913
+ public function toDateTimeString()
914
+ {
915
+ return $this->format('Y-m-d H:i:s');
916
+ }
917
+
918
+ /**
919
+ * Format the instance with day, date and time
920
+ *
921
+ * @return string
922
+ */
923
+ public function toDayDateTimeString()
924
+ {
925
+ return $this->format('D, M j, Y g:i A');
926
+ }
927
+
928
+ /**
929
+ * Format the instance as ATOM
930
+ *
931
+ * @return string
932
+ */
933
+ public function toAtomString()
934
+ {
935
+ return $this->format(self::ATOM);
936
+ }
937
+
938
+ /**
939
+ * Format the instance as COOKIE
940
+ *
941
+ * @return string
942
+ */
943
+ public function toCookieString()
944
+ {
945
+ return $this->format(self::COOKIE);
946
+ }
947
+
948
+ /**
949
+ * Format the instance as ISO8601
950
+ *
951
+ * @return string
952
+ */
953
+ public function toIso8601String()
954
+ {
955
+ return $this->format(self::ISO8601);
956
+ }
957
+
958
+ /**
959
+ * Format the instance as RFC822
960
+ *
961
+ * @return string
962
+ */
963
+ public function toRfc822String()
964
+ {
965
+ return $this->format(self::RFC822);
966
+ }
967
+
968
+ /**
969
+ * Format the instance as RFC850
970
+ *
971
+ * @return string
972
+ */
973
+ public function toRfc850String()
974
+ {
975
+ return $this->format(self::RFC850);
976
+ }
977
+
978
+ /**
979
+ * Format the instance as RFC1036
980
+ *
981
+ * @return string
982
+ */
983
+ public function toRfc1036String()
984
+ {
985
+ return $this->format(self::RFC1036);
986
+ }
987
+
988
+ /**
989
+ * Format the instance as RFC1123
990
+ *
991
+ * @return string
992
+ */
993
+ public function toRfc1123String()
994
+ {
995
+ return $this->format(self::RFC1123);
996
+ }
997
+
998
+ /**
999
+ * Format the instance as RFC2822
1000
+ *
1001
+ * @return string
1002
+ */
1003
+ public function toRfc2822String()
1004
+ {
1005
+ return $this->format(self::RFC2822);
1006
+ }
1007
+
1008
+ /**
1009
+ * Format the instance as RFC3339
1010
+ *
1011
+ * @return string
1012
+ */
1013
+ public function toRfc3339String()
1014
+ {
1015
+ return $this->format(self::RFC3339);
1016
+ }
1017
+
1018
+ /**
1019
+ * Format the instance as RSS
1020
+ *
1021
+ * @return string
1022
+ */
1023
+ public function toRssString()
1024
+ {
1025
+ return $this->format(self::RSS);
1026
+ }
1027
+
1028
+ /**
1029
+ * Format the instance as W3C
1030
+ *
1031
+ * @return string
1032
+ */
1033
+ public function toW3cString()
1034
+ {
1035
+ return $this->format(self::W3C);
1036
+ }
1037
+
1038
+ ///////////////////////////////////////////////////////////////////
1039
+ ////////////////////////// COMPARISONS ////////////////////////////
1040
+ ///////////////////////////////////////////////////////////////////
1041
+
1042
+ /**
1043
+ * Determines if the instance is equal to another
1044
+ *
1045
+ * @param Carbon $dt
1046
+ *
1047
+ * @return boolean
1048
+ */
1049
+ public function eq(Carbon $dt)
1050
+ {
1051
+ return $this == $dt;
1052
+ }
1053
+
1054
+ /**
1055
+ * Determines if the instance is not equal to another
1056
+ *
1057
+ * @param Carbon $dt
1058
+ *
1059
+ * @return boolean
1060
+ */
1061
+ public function ne(Carbon $dt)
1062
+ {
1063
+ return !$this->eq($dt);
1064
+ }
1065
+
1066
+ /**
1067
+ * Determines if the instance is greater (after) than another
1068
+ *
1069
+ * @param Carbon $dt
1070
+ *
1071
+ * @return boolean
1072
+ */
1073
+ public function gt(Carbon $dt)
1074
+ {
1075
+ return $this > $dt;
1076
+ }
1077
+
1078
+ /**
1079
+ * Determines if the instance is greater (after) than or equal to another
1080
+ *
1081
+ * @param Carbon $dt
1082
+ *
1083
+ * @return boolean
1084
+ */
1085
+ public function gte(Carbon $dt)
1086
+ {
1087
+ return $this >= $dt;
1088
+ }
1089
+
1090
+ /**
1091
+ * Determines if the instance is less (before) than another
1092
+ *
1093
+ * @param Carbon $dt
1094
+ *
1095
+ * @return boolean
1096
+ */
1097
+ public function lt(Carbon $dt)
1098
+ {
1099
+ return $this < $dt;
1100
+ }
1101
+
1102
+ /**
1103
+ * Determines if the instance is less (before) or equal to another
1104
+ *
1105
+ * @param Carbon $dt
1106
+ *
1107
+ * @return boolean
1108
+ */
1109
+ public function lte(Carbon $dt)
1110
+ {
1111
+ return $this <= $dt;
1112
+ }
1113
+
1114
+ /**
1115
+ * Determines if the instance is between two others
1116
+ *
1117
+ * @param Carbon $dt1
1118
+ * @param Carbon $dt2
1119
+ * @param boolean $equal Indicates if a > and < comparison should be used or <= or >=
1120
+ *
1121
+ * @return boolean
1122
+ */
1123
+ public function between(Carbon $dt1, Carbon $dt2, $equal = true)
1124
+ {
1125
+ if ($dt1->gt($dt2)) {
1126
+ $temp = $dt1;
1127
+ $dt1 = $dt2;
1128
+ $dt2 = $temp;
1129
+ }
1130
+
1131
+ if ($equal) {
1132
+ return $this->gte($dt1) && $this->lte($dt2);
1133
+ } else {
1134
+ return $this->gt($dt1) && $this->lt($dt2);
1135
+ }
1136
+ }
1137
+
1138
+ /**
1139
+ * Get the minimum instance between a given instance (default now) and the current instance.
1140
+ *
1141
+ * @param Carbon $dt
1142
+ *
1143
+ * @return static
1144
+ */
1145
+ public function min(Carbon $dt = null)
1146
+ {
1147
+ $dt = ($dt === null) ? static::now($this->tz) : $dt;
1148
+
1149
+ return $this->lt($dt) ? $this : $dt;
1150
+ }
1151
+
1152
+ /**
1153
+ * Get the maximum instance between a given instance (default now) and the current instance.
1154
+ *
1155
+ * @param Carbon $dt
1156
+ *
1157
+ * @return static
1158
+ */
1159
+ public function max(Carbon $dt = null)
1160
+ {
1161
+ $dt = ($dt === null) ? static::now($this->tz) : $dt;
1162
+
1163
+ return $this->gt($dt) ? $this : $dt;
1164
+ }
1165
+
1166
+ /**
1167
+ * Determines if the instance is a weekday
1168
+ *
1169
+ * @return boolean
1170
+ */
1171
+ public function isWeekday()
1172
+ {
1173
+ return ($this->dayOfWeek != self::SUNDAY && $this->dayOfWeek != self::SATURDAY);
1174
+ }
1175
+
1176
+ /**
1177
+ * Determines if the instance is a weekend day
1178
+ *
1179
+ * @return boolean
1180
+ */
1181
+ public function isWeekend()
1182
+ {
1183
+ return !$this->isWeekDay();
1184
+ }
1185
+
1186
+ /**
1187
+ * Determines if the instance is yesterday
1188
+ *
1189
+ * @return boolean
1190
+ */
1191
+ public function isYesterday()
1192
+ {
1193
+ return $this->toDateString() === static::yesterday($this->tz)->toDateString();
1194
+ }
1195
+
1196
+ /**
1197
+ * Determines if the instance is today
1198
+ *
1199
+ * @return boolean
1200
+ */
1201
+ public function isToday()
1202
+ {
1203
+ return $this->toDateString() === static::now($this->tz)->toDateString();
1204
+ }
1205
+
1206
+ /**
1207
+ * Determines if the instance is tomorrow
1208
+ *
1209
+ * @return boolean
1210
+ */
1211
+ public function isTomorrow()
1212
+ {
1213
+ return $this->toDateString() === static::tomorrow($this->tz)->toDateString();
1214
+ }
1215
+
1216
+ /**
1217
+ * Determines if the instance is in the future, ie. greater (after) than now
1218
+ *
1219
+ * @return boolean
1220
+ */
1221
+ public function isFuture()
1222
+ {
1223
+ return $this->gt(static::now($this->tz));
1224
+ }
1225
+
1226
+ /**
1227
+ * Determines if the instance is in the past, ie. less (before) than now
1228
+ *
1229
+ * @return boolean
1230
+ */
1231
+ public function isPast()
1232
+ {
1233
+ return $this->lt(static::now($this->tz));
1234
+ }
1235
+
1236
+ /**
1237
+ * Determines if the instance is a leap year
1238
+ *
1239
+ * @return boolean
1240
+ */
1241
+ public function isLeapYear()
1242
+ {
1243
+ return $this->format('L') == '1';
1244
+ }
1245
+
1246
+ /**
1247
+ * Checks if the passed in date is the same day as the instance current day.
1248
+ *
1249
+ * @param Carbon $dt
1250
+ * @return boolean
1251
+ */
1252
+ public function isSameDay(Carbon $dt)
1253
+ {
1254
+ return $this->toDateString() === $dt->toDateString();
1255
+ }
1256
+
1257
+ ///////////////////////////////////////////////////////////////////
1258
+ /////////////////// ADDITIONS AND SUBSTRACTIONS ///////////////////
1259
+ ///////////////////////////////////////////////////////////////////
1260
+
1261
+ /**
1262
+ * Add years to the instance. Positive $value travel forward while
1263
+ * negative $value travel into the past.
1264
+ *
1265
+ * @param integer $value
1266
+ *
1267
+ * @return static
1268
+ */
1269
+ public function addYears($value)
1270
+ {
1271
+ return $this->modify((int) $value . ' year');
1272
+ }
1273
+
1274
+ /**
1275
+ * Add a year to the instance
1276
+ *
1277
+ * @return static
1278
+ */
1279
+ public function addYear()
1280
+ {
1281
+ return $this->addYears(1);
1282
+ }
1283
+
1284
+ /**
1285
+ * Remove a year from the instance
1286
+ *
1287
+ * @return static
1288
+ */
1289
+ public function subYear()
1290
+ {
1291
+ return $this->addYears(-1);
1292
+ }
1293
+
1294
+ /**
1295
+ * Remove years from the instance.
1296
+ *
1297
+ * @param integer $value
1298
+ *
1299
+ * @return static
1300
+ */
1301
+ public function subYears($value)
1302
+ {
1303
+ return $this->addYears(-1 * $value);
1304
+ }
1305
+
1306
+ /**
1307
+ * Add months to the instance. Positive $value travels forward while
1308
+ * negative $value travels into the past.
1309
+ *
1310
+ * @param integer $value
1311
+ *
1312
+ * @return static
1313
+ */
1314
+ public function addMonths($value)
1315
+ {
1316
+ return $this->modify((int) $value . ' month');
1317
+ }
1318
+
1319
+ /**
1320
+ * Add a month to the instance
1321
+ *
1322
+ * @return static
1323
+ */
1324
+ public function addMonth()
1325
+ {
1326
+ return $this->addMonths(1);
1327
+ }
1328
+
1329
+ /**
1330
+ * Remove a month from the instance
1331
+ *
1332
+ * @return static
1333
+ */
1334
+ public function subMonth()
1335
+ {
1336
+ return $this->addMonths(-1);
1337
+ }
1338
+
1339
+ /**
1340
+ * Remove months from the instance
1341
+ *
1342
+ * @param integer $value
1343
+ *
1344
+ * @return static
1345
+ */
1346
+ public function subMonths($value)
1347
+ {
1348
+ return $this->addMonths(-1 * $value);
1349
+ }
1350
+
1351
+ /**
1352
+ * Add days to the instance. Positive $value travels forward while
1353
+ * negative $value travels into the past.
1354
+ *
1355
+ * @param integer $value
1356
+ *
1357
+ * @return static
1358
+ */
1359
+ public function addDays($value)
1360
+ {
1361
+ return $this->modify((int) $value . ' day');
1362
+ }
1363
+
1364
+ /**
1365
+ * Add a day to the instance
1366
+ *
1367
+ * @return static
1368
+ */
1369
+ public function addDay()
1370
+ {
1371
+ return $this->addDays(1);
1372
+ }
1373
+
1374
+ /**
1375
+ * Remove a day from the instance
1376
+ *
1377
+ * @return static
1378
+ */
1379
+ public function subDay()
1380
+ {
1381
+ return $this->addDays(-1);
1382
+ }
1383
+
1384
+ /**
1385
+ * Remove days from the instance
1386
+ *
1387
+ * @param integer $value
1388
+ *
1389
+ * @return static
1390
+ */
1391
+ public function subDays($value)
1392
+ {
1393
+ return $this->addDays(-1 * $value);
1394
+ }
1395
+
1396
+ /**
1397
+ * Add weekdays to the instance. Positive $value travels forward while
1398
+ * negative $value travels into the past.
1399
+ *
1400
+ * @param integer $value
1401
+ *
1402
+ * @return static
1403
+ */
1404
+ public function addWeekdays($value)
1405
+ {
1406
+ return $this->modify((int) $value . ' weekday');
1407
+ }
1408
+
1409
+ /**
1410
+ * Add a weekday to the instance
1411
+ *
1412
+ * @return static
1413
+ */
1414
+ public function addWeekday()
1415
+ {
1416
+ return $this->addWeekdays(1);
1417
+ }
1418
+
1419
+ /**
1420
+ * Remove a weekday from the instance
1421
+ *
1422
+ * @return static
1423
+ */
1424
+ public function subWeekday()
1425
+ {
1426
+ return $this->addWeekdays(-1);
1427
+ }
1428
+
1429
+ /**
1430
+ * Remove weekdays from the instance
1431
+ *
1432
+ * @param integer $value
1433
+ *
1434
+ * @return static
1435
+ */
1436
+ public function subWeekdays($value)
1437
+ {
1438
+ return $this->addWeekdays(-1 * $value);
1439
+ }
1440
+
1441
+ /**
1442
+ * Add weeks to the instance. Positive $value travels forward while
1443
+ * negative $value travels into the past.
1444
+ *
1445
+ * @param integer $value
1446
+ *
1447
+ * @return static
1448
+ */
1449
+ public function addWeeks($value)
1450
+ {
1451
+ return $this->modify((int) $value . ' week');
1452
+ }
1453
+
1454
+ /**
1455
+ * Add a week to the instance
1456
+ *
1457
+ * @return static
1458
+ */
1459
+ public function addWeek()
1460
+ {
1461
+ return $this->addWeeks(1);
1462
+ }
1463
+
1464
+ /**
1465
+ * Remove a week from the instance
1466
+ *
1467
+ * @return static
1468
+ */
1469
+ public function subWeek()
1470
+ {
1471
+ return $this->addWeeks(-1);
1472
+ }
1473
+
1474
+ /**
1475
+ * Remove weeks to the instance
1476
+ *
1477
+ * @param integer $value
1478
+ *
1479
+ * @return static
1480
+ */
1481
+ public function subWeeks($value)
1482
+ {
1483
+ return $this->addWeeks(-1 * $value);
1484
+ }
1485
+
1486
+ /**
1487
+ * Add hours to the instance. Positive $value travels forward while
1488
+ * negative $value travels into the past.
1489
+ *
1490
+ * @param integer $value
1491
+ *
1492
+ * @return static
1493
+ */
1494
+ public function addHours($value)
1495
+ {
1496
+ return $this->modify((int) $value . ' hour');
1497
+ }
1498
+
1499
+ /**
1500
+ * Add an hour to the instance
1501
+ *
1502
+ * @return static
1503
+ */
1504
+ public function addHour()
1505
+ {
1506
+ return $this->addHours(1);
1507
+ }
1508
+
1509
+ /**
1510
+ * Remove an hour from the instance
1511
+ *
1512
+ * @return static
1513
+ */
1514
+ public function subHour()
1515
+ {
1516
+ return $this->addHours(-1);
1517
+ }
1518
+
1519
+ /**
1520
+ * Remove hours from the instance
1521
+ *
1522
+ * @param integer $value
1523
+ *
1524
+ * @return static
1525
+ */
1526
+ public function subHours($value)
1527
+ {
1528
+ return $this->addHours(-1 * $value);
1529
+ }
1530
+
1531
+ /**
1532
+ * Add minutes to the instance. Positive $value travels forward while
1533
+ * negative $value travels into the past.
1534
+ *
1535
+ * @param integer $value
1536
+ *
1537
+ * @return static
1538
+ */
1539
+ public function addMinutes($value)
1540
+ {
1541
+ return $this->modify((int) $value . ' minute');
1542
+ }
1543
+
1544
+ /**
1545
+ * Add a minute to the instance
1546
+ *
1547
+ * @return static
1548
+ */
1549
+ public function addMinute()
1550
+ {
1551
+ return $this->addMinutes(1);
1552
+ }
1553
+
1554
+ /**
1555
+ * Remove a minute from the instance
1556
+ *
1557
+ * @return static
1558
+ */
1559
+ public function subMinute()
1560
+ {
1561
+ return $this->addMinutes(-1);
1562
+ }
1563
+
1564
+ /**
1565
+ * Remove minutes from the instance
1566
+ *
1567
+ * @param integer $value
1568
+ *
1569
+ * @return static
1570
+ */
1571
+ public function subMinutes($value)
1572
+ {
1573
+ return $this->addMinutes(-1 * $value);
1574
+ }
1575
+
1576
+ /**
1577
+ * Add seconds to the instance. Positive $value travels forward while
1578
+ * negative $value travels into the past.
1579
+ *
1580
+ * @param integer $value
1581
+ *
1582
+ * @return static
1583
+ */
1584
+ public function addSeconds($value)
1585
+ {
1586
+ return $this->modify((int) $value . ' second');
1587
+ }
1588
+
1589
+ /**
1590
+ * Add a second to the instance
1591
+ *
1592
+ * @return static
1593
+ */
1594
+ public function addSecond()
1595
+ {
1596
+ return $this->addSeconds(1);
1597
+ }
1598
+
1599
+ /**
1600
+ * Remove a second from the instance
1601
+ *
1602
+ * @return static
1603
+ */
1604
+ public function subSecond()
1605
+ {
1606
+ return $this->addSeconds(-1);
1607
+ }
1608
+
1609
+ /**
1610
+ * Remove seconds from the instance
1611
+ *
1612
+ * @param integer $value
1613
+ *
1614
+ * @return static
1615
+ */
1616
+ public function subSeconds($value)
1617
+ {
1618
+ return $this->addSeconds(-1 * $value);
1619
+ }
1620
+
1621
+ ///////////////////////////////////////////////////////////////////
1622
+ /////////////////////////// DIFFERENCES ///////////////////////////
1623
+ ///////////////////////////////////////////////////////////////////
1624
+
1625
+ /**
1626
+ * Get the difference in years
1627
+ *
1628
+ * @param Carbon $dt
1629
+ * @param boolean $abs Get the absolute of the difference
1630
+ *
1631
+ * @return integer
1632
+ */
1633
+ public function diffInYears(Carbon $dt = null, $abs = true)
1634
+ {
1635
+ $dt = ($dt === null) ? static::now($this->tz) : $dt;
1636
+
1637
+ return (int) $this->diff($dt, $abs)->format('%r%y');
1638
+ }
1639
+
1640
+ /**
1641
+ * Get the difference in months
1642
+ *
1643
+ * @param Carbon $dt
1644
+ * @param boolean $abs Get the absolute of the difference
1645
+ *
1646
+ * @return integer
1647
+ */
1648
+ public function diffInMonths(Carbon $dt = null, $abs = true)
1649
+ {
1650
+ $dt = ($dt === null) ? static::now($this->tz) : $dt;
1651
+
1652
+ return $this->diffInYears($dt, $abs) * self::MONTHS_PER_YEAR + $this->diff($dt, $abs)->format('%r%m');
1653
+ }
1654
+
1655
+ /**
1656
+ * Get the difference in weeks
1657
+ *
1658
+ * @param Carbon $dt
1659
+ * @param boolean $abs Get the absolute of the difference
1660
+ *
1661
+ * @return integer
1662
+ */
1663
+ public function diffInWeeks(Carbon $dt = null, $abs = true)
1664
+ {
1665
+ return (int) ($this->diffInDays($dt, $abs) / self::DAYS_PER_WEEK);
1666
+ }
1667
+
1668
+ /**
1669
+ * Get the difference in days
1670
+ *
1671
+ * @param Carbon $dt
1672
+ * @param boolean $abs Get the absolute of the difference
1673
+ *
1674
+ * @return integer
1675
+ */
1676
+ public function diffInDays(Carbon $dt = null, $abs = true)
1677
+ {
1678
+ $dt = ($dt === null) ? static::now($this->tz) : $dt;
1679
+
1680
+ return (int) $this->diff($dt, $abs)->format('%r%a');
1681
+ }
1682
+
1683
+ /**
1684
+ * Get the difference in days using a filter closure
1685
+ *
1686
+ * @param Closure $callback
1687
+ * @param Carbon $dt
1688
+ * @param boolean $abs Get the absolute of the difference
1689
+ *
1690
+ * @return int
1691
+ */
1692
+ public function diffInDaysFiltered(Closure $callback, Carbon $dt = null, $abs = true)
1693
+ {
1694
+ $start = $this;
1695
+ $end = ($dt === null) ? static::now($this->tz) : $dt;
1696
+ $inverse = false;
1697
+
1698
+ if ($end < $start) {
1699
+ $start = $end;
1700
+ $end = $this;
1701
+ $inverse = true;
1702
+ }
1703
+
1704
+ $period = new DatePeriod($start, new DateInterval('P1D'), $end);
1705
+ $days = array_filter(iterator_to_array($period), function (DateTime $date) use ($callback) {
1706
+ return call_user_func($callback, Carbon::instance($date));
1707
+ });
1708
+
1709
+ $diff = count($days);
1710
+
1711
+ return $inverse && !$abs ? -$diff : $diff;
1712
+ }
1713
+
1714
+ /**
1715
+ * Get the difference in weekdays
1716
+ *
1717
+ * @param Carbon $dt
1718
+ * @param boolean $abs Get the absolute of the difference
1719
+ *
1720
+ * @return int
1721
+ */
1722
+ public function diffInWeekdays(Carbon $dt = null, $abs = true)
1723
+ {
1724
+ return $this->diffInDaysFiltered(function (Carbon $date) {
1725
+ return $date->isWeekday();
1726
+ }, $dt, $abs);
1727
+ }
1728
+
1729
+ /**
1730
+ * Get the difference in weekend days using a filter
1731
+ *
1732
+ * @param Carbon $dt
1733
+ * @param boolean $abs Get the absolute of the difference
1734
+ *
1735
+ * @return int
1736
+ */
1737
+ public function diffInWeekendDays(Carbon $dt = null, $abs = true)
1738
+ {
1739
+ return $this->diffInDaysFiltered(function (Carbon $date) {
1740
+ return $date->isWeekend();
1741
+ }, $dt, $abs);
1742
+ }
1743
+
1744
+ /**
1745
+ * Get the difference in hours
1746
+ *
1747
+ * @param Carbon $dt
1748
+ * @param boolean $abs Get the absolute of the difference
1749
+ *
1750
+ * @return integer
1751
+ */
1752
+ public function diffInHours(Carbon $dt = null, $abs = true)
1753
+ {
1754
+ return (int) ($this->diffInSeconds($dt, $abs) / self::SECONDS_PER_MINUTE / self::MINUTES_PER_HOUR);
1755
+ }
1756
+
1757
+ /**
1758
+ * Get the difference in minutes
1759
+ *
1760
+ * @param Carbon $dt
1761
+ * @param boolean $abs Get the absolute of the difference
1762
+ *
1763
+ * @return integer
1764
+ */
1765
+ public function diffInMinutes(Carbon $dt = null, $abs = true)
1766
+ {
1767
+ return (int) ($this->diffInSeconds($dt, $abs) / self::SECONDS_PER_MINUTE);
1768
+ }
1769
+
1770
+ /**
1771
+ * Get the difference in seconds
1772
+ *
1773
+ * @param Carbon $dt
1774
+ * @param boolean $abs Get the absolute of the difference
1775
+ *
1776
+ * @return integer
1777
+ */
1778
+ public function diffInSeconds(Carbon $dt = null, $abs = true)
1779
+ {
1780
+ $value = (($dt === null) ? time() : $dt->getTimestamp()) - $this->getTimestamp();
1781
+
1782
+ return $abs ? abs($value) : $value;
1783
+ }
1784
+
1785
+ /**
1786
+ * Get the difference in a human readable format.
1787
+ *
1788
+ * When comparing a value in the past to default now:
1789
+ * 1 hour ago
1790
+ * 5 months ago
1791
+ *
1792
+ * When comparing a value in the future to default now:
1793
+ * 1 hour from now
1794
+ * 5 months from now
1795
+ *
1796
+ * When comparing a value in the past to another value:
1797
+ * 1 hour before
1798
+ * 5 months before
1799
+ *
1800
+ * When comparing a value in the future to another value:
1801
+ * 1 hour after
1802
+ * 5 months after
1803
+ *
1804
+ * @param Carbon $other
1805
+ *
1806
+ * @return string
1807
+ */
1808
+ public function diffForHumans(Carbon $other = null)
1809
+ {
1810
+ $isNow = $other === null;
1811
+
1812
+ if ($isNow) {
1813
+ $other = static::now($this->tz);
1814
+ }
1815
+
1816
+ $isFuture = $this->gt($other);
1817
+
1818
+ $delta = $other->diffInSeconds($this);
1819
+
1820
+ // a little weeks per month, 365 days per year... good enough!!
1821
+ $divs = array(
1822
+ 'second' => self::SECONDS_PER_MINUTE,
1823
+ 'minute' => self::MINUTES_PER_HOUR,
1824
+ 'hour' => self::HOURS_PER_DAY,
1825
+ 'day' => self::DAYS_PER_WEEK,
1826
+ 'week' => 30 / self::DAYS_PER_WEEK,
1827
+ 'month' => self::MONTHS_PER_YEAR
1828
+ );
1829
+
1830
+ $unit = 'year';
1831
+
1832
+ foreach ($divs as $divUnit => $divValue) {
1833
+ if ($delta < $divValue) {
1834
+ $unit = $divUnit;
1835
+ break;
1836
+ }
1837
+
1838
+ $delta = $delta / $divValue;
1839
+ }
1840
+
1841
+ $delta = (int) $delta;
1842
+
1843
+ if ($delta == 0) {
1844
+ $delta = 1;
1845
+ }
1846
+
1847
+ $txt = $delta . ' ' . $unit;
1848
+ $txt .= $delta == 1 ? '' : 's';
1849
+
1850
+ if ($isNow) {
1851
+ if ($isFuture) {
1852
+ return $txt . ' from now';
1853
+ }
1854
+
1855
+ return $txt . ' ago';
1856
+ }
1857
+
1858
+ if ($isFuture) {
1859
+ return $txt . ' after';
1860
+ }
1861
+
1862
+ return $txt . ' before';
1863
+ }
1864
+
1865
+ ///////////////////////////////////////////////////////////////////
1866
+ //////////////////////////// MODIFIERS ////////////////////////////
1867
+ ///////////////////////////////////////////////////////////////////
1868
+
1869
+ /**
1870
+ * Resets the time to 00:00:00
1871
+ *
1872
+ * @return static
1873
+ */
1874
+ public function startOfDay()
1875
+ {
1876
+ return $this->hour(0)->minute(0)->second(0);
1877
+ }
1878
+
1879
+ /**
1880
+ * Resets the time to 23:59:59
1881
+ *
1882
+ * @return static
1883
+ */
1884
+ public function endOfDay()
1885
+ {
1886
+ return $this->hour(23)->minute(59)->second(59);
1887
+ }
1888
+
1889
+ /**
1890
+ * Resets the date to the first day of the month and the time to 00:00:00
1891
+ *
1892
+ * @return static
1893
+ */
1894
+ public function startOfMonth()
1895
+ {
1896
+ return $this->startOfDay()->day(1);
1897
+ }
1898
+
1899
+ /**
1900
+ * Resets the date to end of the month and time to 23:59:59
1901
+ *
1902
+ * @return static
1903
+ */
1904
+ public function endOfMonth()
1905
+ {
1906
+ return $this->day($this->daysInMonth)->endOfDay();
1907
+ }
1908
+
1909
+ /**
1910
+ * Resets the date to the first day of the year and the time to 00:00:00
1911
+ *
1912
+ * @return static
1913
+ */
1914
+ public function startOfYear()
1915
+ {
1916
+ return $this->month(1)->startOfMonth();
1917
+ }
1918
+
1919
+ /**
1920
+ * Resets the date to end of the year and time to 23:59:59
1921
+ *
1922
+ * @return static
1923
+ */
1924
+ public function endOfYear()
1925
+ {
1926
+ return $this->month(self::MONTHS_PER_YEAR)->endOfMonth();
1927
+ }
1928
+
1929
+ /**
1930
+ * Resets the date to the first day of the decade and the time to 00:00:00
1931
+ *
1932
+ * @return static
1933
+ */
1934
+ public function startOfDecade()
1935
+ {
1936
+ return $this->startOfYear()->year($this->year - $this->year % self::YEARS_PER_DECADE);
1937
+ }
1938
+
1939
+ /**
1940
+ * Resets the date to end of the decade and time to 23:59:59
1941
+ *
1942
+ * @return static
1943
+ */
1944
+ public function endOfDecade()
1945
+ {
1946
+ return $this->endOfYear()->year($this->year - $this->year % self::YEARS_PER_DECADE + self::YEARS_PER_DECADE - 1);
1947
+ }
1948
+
1949
+ /**
1950
+ * Resets the date to the first day of the century and the time to 00:00:00
1951
+ *
1952
+ * @return static
1953
+ */
1954
+ public function startOfCentury()
1955
+ {
1956
+ return $this->startOfYear()->year($this->year - $this->year % self::YEARS_PER_CENTURY);
1957
+ }
1958
+
1959
+ /**
1960
+ * Resets the date to end of the century and time to 23:59:59
1961
+ *
1962
+ * @return static
1963
+ */
1964
+ public function endOfCentury()
1965
+ {
1966
+ return $this->endOfYear()->year($this->year - $this->year % self::YEARS_PER_CENTURY + self::YEARS_PER_CENTURY - 1);
1967
+ }
1968
+
1969
+ /**
1970
+ * Resets the date to the first day of the ISO-8601 week (Monday) and the time to 00:00:00
1971
+ *
1972
+ * @return static
1973
+ */
1974
+ public function startOfWeek()
1975
+ {
1976
+ if ($this->dayOfWeek != self::MONDAY) $this->previous(self::MONDAY);
1977
+ return $this->startOfDay();
1978
+ }
1979
+
1980
+ /**
1981
+ * Resets the date to end of the ISO-8601 week (Sunday) and time to 23:59:59
1982
+ *
1983
+ * @return static
1984
+ */
1985
+ public function endOfWeek()
1986
+ {
1987
+ if ($this->dayOfWeek != self::SUNDAY) $this->next(self::SUNDAY);
1988
+ return $this->endOfDay();
1989
+ }
1990
+
1991
+ /**
1992
+ * Modify to the next occurance of a given day of the week.
1993
+ * If no dayOfWeek is provided, modify to the next occurance
1994
+ * of the current day of the week. Use the supplied consts
1995
+ * to indicate the desired dayOfWeek, ex. static::MONDAY.
1996
+ *
1997
+ * @param int $dayOfWeek
1998
+ *
1999
+ * @return mixed
2000
+ */
2001
+ public function next($dayOfWeek = null)
2002
+ {
2003
+ if ($dayOfWeek === null) {
2004
+ $dayOfWeek = $this->dayOfWeek;
2005
+ }
2006
+
2007
+ return $this->startOfDay()->modify('next ' . self::$days[$dayOfWeek]);
2008
+ }
2009
+
2010
+ /**
2011
+ * Modify to the previous occurance of a given day of the week.
2012
+ * If no dayOfWeek is provided, modify to the previous occurance
2013
+ * of the current day of the week. Use the supplied consts
2014
+ * to indicate the desired dayOfWeek, ex. static::MONDAY.
2015
+ *
2016
+ * @param int $dayOfWeek
2017
+ *
2018
+ * @return mixed
2019
+ */
2020
+ public function previous($dayOfWeek = null)
2021
+ {
2022
+ if ($dayOfWeek === null) {
2023
+ $dayOfWeek = $this->dayOfWeek;
2024
+ }
2025
+
2026
+ return $this->startOfDay()->modify('last ' . self::$days[$dayOfWeek]);
2027
+ }
2028
+
2029
+ /**
2030
+ * Modify to the first occurance of a given day of the week
2031
+ * in the current month. If no dayOfWeek is provided, modify to the
2032
+ * first day of the current month. Use the supplied consts
2033
+ * to indicate the desired dayOfWeek, ex. static::MONDAY.
2034
+ *
2035
+ * @param int $dayOfWeek
2036
+ *
2037
+ * @return mixed
2038
+ */
2039
+ public function firstOfMonth($dayOfWeek = null)
2040
+ {
2041
+ $this->startOfDay();
2042
+
2043
+ if ($dayOfWeek === null) {
2044
+ return $this->day(1);
2045
+ }
2046
+
2047
+ return $this->modify('first ' . self::$days[$dayOfWeek] . ' of ' . $this->format('F') . ' ' . $this->year);
2048
+ }
2049
+
2050
+ /**
2051
+ * Modify to the last occurance of a given day of the week
2052
+ * in the current month. If no dayOfWeek is provided, modify to the
2053
+ * last day of the current month. Use the supplied consts
2054
+ * to indicate the desired dayOfWeek, ex. static::MONDAY.
2055
+ *
2056
+ * @param int $dayOfWeek
2057
+ *
2058
+ * @return mixed
2059
+ */
2060
+ public function lastOfMonth($dayOfWeek = null)
2061
+ {
2062
+ $this->startOfDay();
2063
+
2064
+ if ($dayOfWeek === null) {
2065
+ return $this->day($this->daysInMonth);
2066
+ }
2067
+
2068
+ return $this->modify('last ' . self::$days[$dayOfWeek] . ' of ' . $this->format('F') . ' ' . $this->year);
2069
+ }
2070
+
2071
+ /**
2072
+ * Modify to the given occurance of a given day of the week
2073
+ * in the current month. If the calculated occurance is outside the scope
2074
+ * of the current month, then return false and no modifications are made.
2075
+ * Use the supplied consts to indicate the desired dayOfWeek, ex. static::MONDAY.
2076
+ *
2077
+ * @param int $nth
2078
+ * @param int $dayOfWeek
2079
+ *
2080
+ * @return mixed
2081
+ */
2082
+ public function nthOfMonth($nth, $dayOfWeek)
2083
+ {
2084
+ $dt = $this->copy()->firstOfMonth();
2085
+ $check = $dt->format('Y-m');
2086
+ $dt->modify('+' . $nth . ' ' . self::$days[$dayOfWeek]);
2087
+
2088
+ return ($dt->format('Y-m') === $check) ? $this->modify($dt) : false;
2089
+ }
2090
+
2091
+ /**
2092
+ * Modify to the first occurance of a given day of the week
2093
+ * in the current quarter. If no dayOfWeek is provided, modify to the
2094
+ * first day of the current quarter. Use the supplied consts
2095
+ * to indicate the desired dayOfWeek, ex. static::MONDAY.
2096
+ *
2097
+ * @param int $dayOfWeek
2098
+ *
2099
+ * @return mixed
2100
+ */
2101
+ public function firstOfQuarter($dayOfWeek = null)
2102
+ {
2103
+ return $this->day(1)->month($this->quarter * 3 - 2)->firstOfMonth($dayOfWeek);
2104
+ }
2105
+
2106
+ /**
2107
+ * Modify to the last occurance of a given day of the week
2108
+ * in the current quarter. If no dayOfWeek is provided, modify to the
2109
+ * last day of the current quarter. Use the supplied consts
2110
+ * to indicate the desired dayOfWeek, ex. static::MONDAY.
2111
+ *
2112
+ * @param int $dayOfWeek
2113
+ *
2114
+ * @return mixed
2115
+ */
2116
+ public function lastOfQuarter($dayOfWeek = null)
2117
+ {
2118
+ return $this->day(1)->month($this->quarter * 3)->lastOfMonth($dayOfWeek);
2119
+ }
2120
+
2121
+ /**
2122
+ * Modify to the given occurance of a given day of the week
2123
+ * in the current quarter. If the calculated occurance is outside the scope
2124
+ * of the current quarter, then return false and no modifications are made.
2125
+ * Use the supplied consts to indicate the desired dayOfWeek, ex. static::MONDAY.
2126
+ *
2127
+ * @param int $nth
2128
+ * @param int $dayOfWeek
2129
+ *
2130
+ * @return mixed
2131
+ */
2132
+ public function nthOfQuarter($nth, $dayOfWeek)
2133
+ {
2134
+ $dt = $this->copy()->day(1)->month($this->quarter * 3);
2135
+ $last_month = $dt->month;
2136
+ $year = $dt->year;
2137
+ $dt->firstOfQuarter()->modify('+' . $nth . ' ' . self::$days[$dayOfWeek]);
2138
+
2139
+ return ($last_month < $dt->month || $year !== $dt->year) ? false : $this->modify($dt);
2140
+ }
2141
+
2142
+ /**
2143
+ * Modify to the first occurance of a given day of the week
2144
+ * in the current year. If no dayOfWeek is provided, modify to the
2145
+ * first day of the current year. Use the supplied consts
2146
+ * to indicate the desired dayOfWeek, ex. static::MONDAY.
2147
+ *
2148
+ * @param int $dayOfWeek
2149
+ *
2150
+ * @return mixed
2151
+ */
2152
+ public function firstOfYear($dayOfWeek = null)
2153
+ {
2154
+ return $this->month(1)->firstOfMonth($dayOfWeek);
2155
+ }
2156
+
2157
+ /**
2158
+ * Modify to the last occurance of a given day of the week
2159
+ * in the current year. If no dayOfWeek is provided, modify to the
2160
+ * last day of the current year. Use the supplied consts
2161
+ * to indicate the desired dayOfWeek, ex. static::MONDAY.
2162
+ *
2163
+ * @param int $dayOfWeek
2164
+ *
2165
+ * @return mixed
2166
+ */
2167
+ public function lastOfYear($dayOfWeek = null)
2168
+ {
2169
+ return $this->month(self::MONTHS_PER_YEAR)->lastOfMonth($dayOfWeek);
2170
+ }
2171
+
2172
+ /**
2173
+ * Modify to the given occurance of a given day of the week
2174
+ * in the current year. If the calculated occurance is outside the scope
2175
+ * of the current year, then return false and no modifications are made.
2176
+ * Use the supplied consts to indicate the desired dayOfWeek, ex. static::MONDAY.
2177
+ *
2178
+ * @param int $nth
2179
+ * @param int $dayOfWeek
2180
+ *
2181
+ * @return mixed
2182
+ */
2183
+ public function nthOfYear($nth, $dayOfWeek)
2184
+ {
2185
+ $dt = $this->copy()->firstOfYear()->modify('+' . $nth . ' ' . self::$days[$dayOfWeek]);
2186
+
2187
+ return $this->year == $dt->year ? $this->modify($dt) : false;
2188
+ }
2189
+
2190
+ /**
2191
+ * Modify the current instance to the average of a given instance (default now) and the current instance.
2192
+ *
2193
+ * @param Carbon $dt
2194
+ *
2195
+ * @return static
2196
+ */
2197
+ public function average(Carbon $dt = null)
2198
+ {
2199
+ $dt = ($dt === null) ? static::now($this->tz) : $dt;
2200
+
2201
+ return $this->addSeconds((int) ($this->diffInSeconds($dt, false) / 2));
2202
+ }
2203
+
2204
+ /**
2205
+ * Check if its the birthday. Compares the date/month values of the two dates.
2206
+ * @param Carbon $dt
2207
+ * @return boolean
2208
+ */
2209
+ public function isBirthday(Carbon $dt)
2210
+ {
2211
+ return $this->month === $dt->month && $this->day === $dt->day;
2212
+ }
2213
+ }
languages/stream-en_US.mo ADDED
Binary file
languages/stream-en_US.po ADDED
@@ -0,0 +1,4154 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ msgid ""
2
+ msgstr ""
3
+ "Project-Id-Version: Stream\n"
4
+ "POT-Creation-Date: 2015-07-22 15:52+1000\n"
5
+ "PO-Revision-Date: 2015-07-22 15:53+1000\n"
6
+ "Last-Translator: Luke Carbis <luke@wp-stream.com>\n"
7
+ "Language-Team: Luke Carbis <luke@wp-stream.com>\n"
8
+ "Language: en_US\n"
9
+ "MIME-Version: 1.0\n"
10
+ "Content-Type: text/plain; charset=UTF-8\n"
11
+ "Content-Transfer-Encoding: 8bit\n"
12
+ "X-Generator: Poedit 1.6.3\n"
13
+ "X-Poedit-Basepath: ..\n"
14
+ "X-Poedit-SourceCharset: UTF-8\n"
15
+ "X-Poedit-KeywordsList: __;_e;_n:1,2;_x:1,2c;_ex:1,2c;_nx:4c,1,2;esc_attr__;"
16
+ "esc_attr_e;esc_attr_x:1,2c;esc_html__;esc_html_e;esc_html_x:1,2c;_n_noop:1,2;"
17
+ "_nx_noop:3c,1,2;__ngettext_noop:1,2\n"
18
+ "Plural-Forms: nplurals=2; plural=(n != 1);\n"
19
+ "X-Poedit-SearchPath-0: .\n"
20
+
21
+ #: stream.php:47
22
+ msgid "Stream requires PHP version 5.3+, plugin is currently NOT ACTIVE."
23
+ msgstr ""
24
+
25
+ #: classes/class-admin.php:201
26
+ msgid "All site settings have been successfully reset."
27
+ msgstr ""
28
+
29
+ #: classes/class-admin.php:276 classes/class-network.php:122
30
+ #: connectors/class-connector-settings.php:134
31
+ msgid "Stream"
32
+ msgstr ""
33
+
34
+ #: classes/class-admin.php:292
35
+ msgid "Stream Records"
36
+ msgstr ""
37
+
38
+ #: classes/class-admin.php:309
39
+ msgid "Stream Settings"
40
+ msgstr ""
41
+
42
+ #: classes/class-admin.php:314 classes/class-admin.php:691
43
+ #: connectors/class-connector-settings.php:105
44
+ #: connectors/class-connector-settings.php:126
45
+ #: connectors/class-connector-woocommerce.php:724
46
+ msgid "Settings"
47
+ msgstr ""
48
+
49
+ #: classes/class-admin.php:375
50
+ msgid ""
51
+ "Are you sure you want to delete all Stream activity records from the "
52
+ "database? This cannot be undone."
53
+ msgstr ""
54
+
55
+ #: classes/class-admin.php:376
56
+ msgid ""
57
+ "Are you sure you want to reset all site settings to default? This cannot be "
58
+ "undone."
59
+ msgstr ""
60
+
61
+ #: classes/class-admin.php:377
62
+ msgid ""
63
+ "Are you sure you want to uninstall and deactivate Stream? This will delete "
64
+ "all Stream tables from the database and cannot be undone."
65
+ msgstr ""
66
+
67
+ #: classes/class-admin.php:402
68
+ #, php-format
69
+ msgid "This will take about %d minutes."
70
+ msgstr ""
71
+
72
+ #: classes/class-admin.php:402
73
+ msgid "This could take a few minutes."
74
+ msgstr ""
75
+
76
+ #: classes/class-admin.php:410
77
+ msgid "Migrating Stream Records"
78
+ msgstr ""
79
+
80
+ #: classes/class-admin.php:411
81
+ msgid "No Records Were Migrated"
82
+ msgstr ""
83
+
84
+ #: classes/class-admin.php:412
85
+ msgid "Please do not exit this page until the process has completed."
86
+ msgstr ""
87
+
88
+ #: classes/class-admin.php:413
89
+ #, php-format
90
+ msgid "Please note: This process will take about %d minutes to complete."
91
+ msgstr ""
92
+
93
+ #: classes/class-admin.php:413
94
+ msgid "Please note: This process could take a few minutes to complete."
95
+ msgstr ""
96
+
97
+ #: classes/class-admin.php:414
98
+ msgid ""
99
+ "Please note: Your existing records will not appear in Stream until you have "
100
+ "migrated them to your local database."
101
+ msgstr ""
102
+
103
+ #: classes/class-admin.php:415
104
+ #, php-format
105
+ msgid ""
106
+ "Are you sure you want to lose all %s existing Stream records without "
107
+ "migrating?"
108
+ msgstr ""
109
+
110
+ #: classes/class-admin.php:415
111
+ #, php-format
112
+ msgid "about %d"
113
+ msgstr ""
114
+
115
+ #: classes/class-admin.php:415
116
+ msgid "a few"
117
+ msgstr ""
118
+
119
+ #: classes/class-admin.php:444
120
+ #, php-format
121
+ msgid ""
122
+ "Are you sure you want to perform bulk actions on over %s items? This process "
123
+ "could take a while to complete."
124
+ msgstr ""
125
+
126
+ #: classes/class-admin.php:579 classes/class-uninstall.php:51
127
+ msgid "You don't have sufficient privileges to do this action."
128
+ msgstr ""
129
+
130
+ #: classes/class-admin.php:701
131
+ msgid "Uninstall"
132
+ msgstr ""
133
+
134
+ #: classes/class-author.php:82 classes/class-author.php:90
135
+ #: classes/class-settings.php:771
136
+ msgid "N/A"
137
+ msgstr ""
138
+
139
+ #: classes/class-author.php:290
140
+ msgid "via WP-CLI"
141
+ msgstr ""
142
+
143
+ #: classes/class-author.php:292
144
+ msgid "during WP Cron"
145
+ msgstr ""
146
+
147
+ #: classes/class-cli.php:217
148
+ msgid "SITE IS DISCONNECTED"
149
+ msgstr ""
150
+
151
+ #: classes/class-connectors.php:123
152
+ #, php-format
153
+ msgid ""
154
+ "%s class wasn't loaded because it doesn't implement the get_label method."
155
+ msgstr ""
156
+
157
+ #: classes/class-connectors.php:127
158
+ #, php-format
159
+ msgid ""
160
+ "%s class wasn't loaded because it doesn't implement the register method."
161
+ msgstr ""
162
+
163
+ #: classes/class-connectors.php:131
164
+ #, php-format
165
+ msgid ""
166
+ "%s class wasn't loaded because it doesn't implement the get_context_labels "
167
+ "method."
168
+ msgstr ""
169
+
170
+ #: classes/class-connectors.php:135
171
+ #, php-format
172
+ msgid ""
173
+ "%s class wasn't loaded because it doesn't implement the get_action_labels "
174
+ "method."
175
+ msgstr ""
176
+
177
+ #: classes/class-connectors.php:141
178
+ #, php-format
179
+ msgid "%s class wasn't loaded because it doesn't extends the %s class."
180
+ msgstr ""
181
+
182
+ #: classes/class-date-interval.php:48
183
+ msgid "Today"
184
+ msgstr ""
185
+
186
+ #: classes/class-date-interval.php:53
187
+ msgid "Yesterday"
188
+ msgstr ""
189
+
190
+ #: classes/class-date-interval.php:58 classes/class-date-interval.php:63
191
+ #: classes/class-date-interval.php:68
192
+ #, php-format
193
+ msgid "Last %d Days"
194
+ msgstr ""
195
+
196
+ #: classes/class-date-interval.php:73
197
+ msgid "This Month"
198
+ msgstr ""
199
+
200
+ #: classes/class-date-interval.php:78
201
+ msgid "Last Month"
202
+ msgstr ""
203
+
204
+ #: classes/class-date-interval.php:83 classes/class-date-interval.php:88
205
+ #: classes/class-date-interval.php:93
206
+ #, php-format
207
+ msgid "Last %d Months"
208
+ msgstr ""
209
+
210
+ #: classes/class-date-interval.php:98
211
+ msgid "This Year"
212
+ msgstr ""
213
+
214
+ #: classes/class-date-interval.php:103
215
+ msgid "Last Year"
216
+ msgstr ""
217
+
218
+ #: classes/class-filter-input.php:51
219
+ msgid "Invalid use, type must be one of INPUT_* family."
220
+ msgstr ""
221
+
222
+ #: classes/class-filter-input.php:67
223
+ msgid "Filter not supported."
224
+ msgstr ""
225
+
226
+ #: classes/class-install.php:174
227
+ msgid "The following table is not present in the WordPress database:"
228
+ msgid_plural "The following tables are not present in the WordPress database:"
229
+ msgstr[0] ""
230
+ msgstr[1] ""
231
+
232
+ #: classes/class-install.php:184 classes/class-install.php:186
233
+ #, php-format
234
+ msgid ""
235
+ "Please <a href=\"%s\">uninstall</a> the Stream plugin and activate it again."
236
+ msgstr ""
237
+
238
+ #: classes/class-install.php:265
239
+ msgid "There was an error updating the Stream database. Please try again."
240
+ msgstr ""
241
+
242
+ #: classes/class-install.php:266
243
+ msgid "Database Update Error"
244
+ msgstr ""
245
+
246
+ #: classes/class-install.php:313
247
+ msgid "Stream Database Update Required"
248
+ msgstr ""
249
+
250
+ #: classes/class-install.php:314
251
+ msgid ""
252
+ "Stream has updated! Before we send you on your way, we need to update your "
253
+ "database to the newest version."
254
+ msgstr ""
255
+
256
+ #: classes/class-install.php:315
257
+ msgid "This process could take a little while, so please be patient."
258
+ msgstr ""
259
+
260
+ #: classes/class-install.php:316
261
+ msgid "Update Database"
262
+ msgstr ""
263
+
264
+ #: classes/class-install.php:335
265
+ msgid "Update Complete"
266
+ msgstr ""
267
+
268
+ #: classes/class-install.php:337
269
+ msgid "Continue"
270
+ msgstr ""
271
+
272
+ #: classes/class-list-table.php:41
273
+ msgid "Records per page"
274
+ msgstr ""
275
+
276
+ #: classes/class-list-table.php:64
277
+ msgid "Sorry, no activity records were found."
278
+ msgstr ""
279
+
280
+ #: classes/class-list-table.php:78
281
+ msgid "Date"
282
+ msgstr ""
283
+
284
+ #: classes/class-list-table.php:79
285
+ msgid "Summary"
286
+ msgstr ""
287
+
288
+ #: classes/class-list-table.php:80
289
+ msgid "User"
290
+ msgstr ""
291
+
292
+ #: classes/class-list-table.php:81 classes/class-settings.php:729
293
+ msgid "Context"
294
+ msgstr ""
295
+
296
+ #: classes/class-list-table.php:82 classes/class-settings.php:730
297
+ msgid "Action"
298
+ msgstr ""
299
+
300
+ #: classes/class-list-table.php:83 classes/class-settings.php:731
301
+ msgid "IP Address"
302
+ msgstr ""
303
+
304
+ #: classes/class-list-table.php:241
305
+ #, php-format
306
+ msgid "View all activity for \"%s\""
307
+ msgstr ""
308
+
309
+ #: classes/class-list-table.php:241
310
+ msgid "View all activity for this object"
311
+ msgstr ""
312
+
313
+ #: classes/class-list-table.php:265
314
+ msgid "Deleted User"
315
+ msgstr ""
316
+
317
+ #: classes/class-list-table.php:506
318
+ msgid "dates"
319
+ msgstr ""
320
+
321
+ #: classes/class-list-table.php:515
322
+ msgid "users"
323
+ msgstr ""
324
+
325
+ #: classes/class-list-table.php:521
326
+ msgid "contexts"
327
+ msgstr ""
328
+
329
+ #: classes/class-list-table.php:526
330
+ msgid "actions"
331
+ msgstr ""
332
+
333
+ #: classes/class-list-table.php:545
334
+ msgid "Show filter controls via the screen options tab above."
335
+ msgstr ""
336
+
337
+ #: classes/class-list-table.php:594
338
+ msgid "Filter"
339
+ msgstr ""
340
+
341
+ #: classes/class-list-table.php:619
342
+ msgid "Reset filters"
343
+ msgstr ""
344
+
345
+ #: classes/class-list-table.php:670
346
+ #, php-format
347
+ msgid "Show all %s"
348
+ msgstr ""
349
+
350
+ #: classes/class-list-table.php:716
351
+ msgid "Search Records"
352
+ msgstr ""
353
+
354
+ #: classes/class-list-table.php:736
355
+ msgid "All Time"
356
+ msgstr ""
357
+
358
+ #: classes/class-list-table.php:738
359
+ msgid "Custom"
360
+ msgstr ""
361
+
362
+ #: classes/class-list-table.php:758
363
+ msgid "Start Date"
364
+ msgstr ""
365
+
366
+ #: classes/class-list-table.php:764
367
+ msgid "End Date"
368
+ msgstr ""
369
+
370
+ #: classes/class-list-table.php:856
371
+ msgid "Live updates"
372
+ msgstr ""
373
+
374
+ #: classes/class-list-table.php:867 classes/class-network.php:271
375
+ #: classes/class-settings.php:269 classes/class-settings.php:299
376
+ #: classes/class-settings.php:313
377
+ msgid "Enabled"
378
+ msgstr ""
379
+
380
+ #: classes/class-live-update.php:67
381
+ msgid ""
382
+ "Live updates could not be enabled because Heartbeat is not loaded.\n"
383
+ "\n"
384
+ "Your hosting provider or another plugin may have disabled it for performance "
385
+ "reasons."
386
+ msgstr ""
387
+
388
+ #: classes/class-live-update.php:187
389
+ #, php-format
390
+ msgid "1 item"
391
+ msgid_plural "%s items"
392
+ msgstr[0] ""
393
+ msgstr[1] ""
394
+
395
+ #: classes/class-migrate.php:225
396
+ msgid ""
397
+ "Our cloud storage services will be shutting down permanently on September 1, "
398
+ "2015"
399
+ msgstr ""
400
+
401
+ #: classes/class-migrate.php:226
402
+ msgid "Read the announcement post"
403
+ msgstr ""
404
+
405
+ #: classes/class-migrate.php:227
406
+ #, php-format
407
+ msgid ""
408
+ "We found %s activity records in the cloud that need to be migrated to your "
409
+ "local database."
410
+ msgstr ""
411
+
412
+ #: classes/class-migrate.php:228
413
+ msgid "Close"
414
+ msgstr ""
415
+
416
+ #: classes/class-migrate.php:229
417
+ msgid "Start Migration Now"
418
+ msgstr ""
419
+
420
+ #: classes/class-migrate.php:230
421
+ msgid "Remind Me Later"
422
+ msgstr ""
423
+
424
+ #: classes/class-migrate.php:231
425
+ msgid "No thanks, I don't want to migrate"
426
+ msgstr ""
427
+
428
+ #: classes/class-migrate.php:285
429
+ msgid "Migration complete!"
430
+ msgstr ""
431
+
432
+ #: classes/class-migrate.php:291
433
+ msgid ""
434
+ "An unknown error occurred during migration. Please try again later or "
435
+ "contact support."
436
+ msgstr ""
437
+
438
+ #: classes/class-migrate.php:305
439
+ msgid "OK, we'll remind you again in a few hours."
440
+ msgstr ""
441
+
442
+ #: classes/class-migrate.php:316
443
+ msgid "All new activity will be stored in the local database."
444
+ msgstr ""
445
+
446
+ #: classes/class-network.php:78
447
+ msgid "Network Admin"
448
+ msgstr ""
449
+
450
+ #: classes/class-network.php:142
451
+ msgid "Stream Network Settings"
452
+ msgstr ""
453
+
454
+ #: classes/class-network.php:143
455
+ msgid "Network Settings"
456
+ msgstr ""
457
+
458
+ #: classes/class-network.php:197
459
+ msgid "These settings apply to all sites on the network."
460
+ msgstr ""
461
+
462
+ #: classes/class-network.php:200
463
+ msgid ""
464
+ "These default settings will apply to new sites created on the network. These "
465
+ "settings do not alter existing sites."
466
+ msgstr ""
467
+
468
+ #: classes/class-network.php:270
469
+ msgid "Site Access"
470
+ msgstr ""
471
+
472
+ #: classes/class-network.php:273
473
+ msgid ""
474
+ "Allow sites on this network to view their Stream activity. Leave unchecked "
475
+ "to only allow Stream to be viewed in the Network Admin."
476
+ msgstr ""
477
+
478
+ #: classes/class-network.php:360
479
+ msgid "Settings saved."
480
+ msgstr ""
481
+
482
+ #: classes/class-network.php:407
483
+ msgid "sites"
484
+ msgstr ""
485
+
486
+ #: classes/class-network.php:423 classes/class-network.php:501
487
+ msgid "Site"
488
+ msgstr ""
489
+
490
+ #: classes/class-network.php:482
491
+ #, php-format
492
+ msgid "1 site"
493
+ msgid_plural "%s sites"
494
+ msgstr[0] ""
495
+ msgstr[1] ""
496
+
497
+ #: classes/class-plugin.php:83
498
+ msgid "Stream: Could not load chosen DB driver."
499
+ msgstr ""
500
+
501
+ #: classes/class-plugin.php:84
502
+ msgid "Stream DB Error"
503
+ msgstr ""
504
+
505
+ #: classes/class-record.php:43
506
+ msgid "Could not validate record data."
507
+ msgstr ""
508
+
509
+ #: classes/class-settings.php:84
510
+ msgid "There was an error in the request"
511
+ msgstr ""
512
+
513
+ #: classes/class-settings.php:132
514
+ #, php-format
515
+ msgid ""
516
+ "ID: %d\n"
517
+ "User: %s\n"
518
+ "Email: %s\n"
519
+ "Role: %s"
520
+ msgstr ""
521
+
522
+ #: classes/class-settings.php:151
523
+ msgid ""
524
+ "Actions performed by the system when a user is not logged in (e.g. auto site "
525
+ "upgrader, or invoking WP-CLI without --user)"
526
+ msgstr ""
527
+
528
+ #: classes/class-settings.php:224 connectors/class-connector-settings.php:127
529
+ msgid "General"
530
+ msgstr ""
531
+
532
+ #: classes/class-settings.php:228
533
+ msgid "Role Access"
534
+ msgstr ""
535
+
536
+ #: classes/class-settings.php:230
537
+ msgid ""
538
+ "Users from the selected roles above will have permission to view Stream "
539
+ "Records. However, only site Administrators can access Stream Settings."
540
+ msgstr ""
541
+
542
+ #: classes/class-settings.php:236
543
+ msgid "Keep Records for"
544
+ msgstr ""
545
+
546
+ #: classes/class-settings.php:239
547
+ msgid ""
548
+ "Maximum number of days to keep activity records. Leave blank to keep records "
549
+ "forever."
550
+ msgstr ""
551
+
552
+ #: classes/class-settings.php:244
553
+ msgid "days"
554
+ msgstr ""
555
+
556
+ #: classes/class-settings.php:249
557
+ msgid "Exclude"
558
+ msgstr ""
559
+
560
+ #: classes/class-settings.php:253
561
+ msgid "Exclude Rules"
562
+ msgstr ""
563
+
564
+ #: classes/class-settings.php:255
565
+ msgid ""
566
+ "Create rules to exclude certain kinds of activity from being recorded by "
567
+ "Stream."
568
+ msgstr ""
569
+
570
+ #: classes/class-settings.php:262
571
+ msgid "Advanced"
572
+ msgstr ""
573
+
574
+ #: classes/class-settings.php:266
575
+ msgid "Comment Flood Tracking"
576
+ msgstr ""
577
+
578
+ #: classes/class-settings.php:268
579
+ msgid ""
580
+ "WordPress will automatically prevent duplicate comments from flooding the "
581
+ "database. By default, Stream does not track these attempts unless you opt-in "
582
+ "here. Enabling this is not necessary or recommended for most sites."
583
+ msgstr ""
584
+
585
+ #: classes/class-settings.php:274
586
+ msgid "Reset Stream Database"
587
+ msgstr ""
588
+
589
+ #: classes/class-settings.php:284
590
+ msgid "Warning: This will delete all activity records from the database."
591
+ msgstr ""
592
+
593
+ #: classes/class-settings.php:296
594
+ msgid "Akismet Tracking"
595
+ msgstr ""
596
+
597
+ #: classes/class-settings.php:298
598
+ msgid ""
599
+ "Akismet already keeps statistics for comment attempts that it blocks as "
600
+ "SPAM. By default, Stream does not track these attempts unless you opt-in "
601
+ "here. Enabling this is not necessary or recommended for most sites."
602
+ msgstr ""
603
+
604
+ #: classes/class-settings.php:310
605
+ msgid "WP Cron Tracking"
606
+ msgstr ""
607
+
608
+ #: classes/class-settings.php:312
609
+ msgid ""
610
+ "By default, Stream does not track activity performed by WordPress cron "
611
+ "events unless you opt-in here. Enabling this is not necessary or recommended "
612
+ "for most sites."
613
+ msgstr ""
614
+
615
+ #: classes/class-settings.php:695
616
+ #, php-format
617
+ msgid "Any %s"
618
+ msgstr ""
619
+
620
+ #: classes/class-settings.php:710
621
+ msgid "Add New Rule"
622
+ msgstr ""
623
+
624
+ #: classes/class-settings.php:711
625
+ msgid "Delete Selected Rules"
626
+ msgstr ""
627
+
628
+ #: classes/class-settings.php:728
629
+ msgid "Author or Role"
630
+ msgstr ""
631
+
632
+ #: classes/class-settings.php:732
633
+ msgid "Filters"
634
+ msgstr ""
635
+
636
+ #: classes/class-settings.php:758
637
+ #, php-format
638
+ msgid "1 user"
639
+ msgid_plural "%s users"
640
+ msgstr[0] ""
641
+ msgstr[1] ""
642
+
643
+ #: classes/class-settings.php:784
644
+ msgid "Any Author or Role"
645
+ msgstr ""
646
+
647
+ #: classes/class-settings.php:825
648
+ msgid "Any Context"
649
+ msgstr ""
650
+
651
+ #: classes/class-settings.php:844
652
+ msgid "Any Action"
653
+ msgstr ""
654
+
655
+ #: classes/class-settings.php:855
656
+ msgid "Any IP Address"
657
+ msgstr ""
658
+
659
+ #: classes/class-settings.php:892
660
+ msgid "No rules found."
661
+ msgstr ""
662
+
663
+ #: connectors/class-connector-acf.php:74
664
+ msgctxt "acf"
665
+ msgid "ACF"
666
+ msgstr ""
667
+
668
+ #: connectors/class-connector-acf.php:84
669
+ #: connectors/class-connector-blogs.php:51
670
+ #: connectors/class-connector-comments.php:55
671
+ #: connectors/class-connector-menus.php:39
672
+ #: connectors/class-connector-posts.php:39
673
+ #: connectors/class-connector-taxonomies.php:54
674
+ #: connectors/class-connector-users.php:52
675
+ #: connectors/class-connector-widgets.php:59
676
+ msgid "Created"
677
+ msgstr ""
678
+
679
+ #: connectors/class-connector-acf.php:85
680
+ #: connectors/class-connector-blogs.php:53
681
+ #: connectors/class-connector-editor.php:54
682
+ #: connectors/class-connector-installer.php:49
683
+ #: connectors/class-connector-media.php:43
684
+ #: connectors/class-connector-menus.php:40
685
+ #: connectors/class-connector-posts.php:38
686
+ #: connectors/class-connector-settings.php:115
687
+ #: connectors/class-connector-taxonomies.php:55
688
+ #: connectors/class-connector-users.php:51
689
+ #: connectors/class-connector-widgets.php:63
690
+ msgid "Updated"
691
+ msgstr ""
692
+
693
+ #: connectors/class-connector-acf.php:86
694
+ #: connectors/class-connector-widgets.php:56
695
+ msgid "Added"
696
+ msgstr ""
697
+
698
+ #: connectors/class-connector-acf.php:87
699
+ #: connectors/class-connector-blogs.php:52
700
+ #: connectors/class-connector-comments.php:64
701
+ #: connectors/class-connector-installer.php:48
702
+ #: connectors/class-connector-media.php:44
703
+ #: connectors/class-connector-menus.php:41
704
+ #: connectors/class-connector-posts.php:42
705
+ #: connectors/class-connector-taxonomies.php:56
706
+ #: connectors/class-connector-users.php:53
707
+ #: connectors/class-connector-widgets.php:60
708
+ msgid "Deleted"
709
+ msgstr ""
710
+
711
+ #: connectors/class-connector-acf.php:98
712
+ msgctxt "acf"
713
+ msgid "Field Groups"
714
+ msgstr ""
715
+
716
+ #: connectors/class-connector-acf.php:99
717
+ msgctxt "acf"
718
+ msgid "Fields"
719
+ msgstr ""
720
+
721
+ #: connectors/class-connector-acf.php:100
722
+ msgctxt "acf"
723
+ msgid "Rules"
724
+ msgstr ""
725
+
726
+ #: connectors/class-connector-acf.php:101
727
+ msgctxt "acf"
728
+ msgid "Options"
729
+ msgstr ""
730
+
731
+ #: connectors/class-connector-acf.php:102
732
+ msgctxt "acf"
733
+ msgid "Values"
734
+ msgstr ""
735
+
736
+ #: connectors/class-connector-acf.php:226
737
+ #, php-format
738
+ msgctxt "acf"
739
+ msgid "\"%1$s\" field in \"%2$s\" %3$s"
740
+ msgstr ""
741
+
742
+ #: connectors/class-connector-acf.php:250
743
+ msgctxt "acf"
744
+ msgid "High (after title)"
745
+ msgstr ""
746
+
747
+ #: connectors/class-connector-acf.php:251
748
+ msgctxt "acf"
749
+ msgid "Normal (after content)"
750
+ msgstr ""
751
+
752
+ #: connectors/class-connector-acf.php:252
753
+ msgctxt "acf"
754
+ msgid "Side"
755
+ msgstr ""
756
+
757
+ #: connectors/class-connector-acf.php:256
758
+ #, php-format
759
+ msgctxt "acf"
760
+ msgid "Position of \"%1$s\" updated to \"%2$s\""
761
+ msgstr ""
762
+
763
+ #: connectors/class-connector-acf.php:273
764
+ msgctxt "acf"
765
+ msgid "Seamless (no metabox)"
766
+ msgstr ""
767
+
768
+ #: connectors/class-connector-acf.php:274
769
+ msgctxt "acf"
770
+ msgid "Standard (WP metabox)"
771
+ msgstr ""
772
+
773
+ #: connectors/class-connector-acf.php:278
774
+ #, php-format
775
+ msgctxt "acf"
776
+ msgid "Style of \"%1$s\" updated to \"%2$s\""
777
+ msgstr ""
778
+
779
+ #: connectors/class-connector-acf.php:295
780
+ msgctxt "acf"
781
+ msgid "Permalink"
782
+ msgstr ""
783
+
784
+ #: connectors/class-connector-acf.php:296
785
+ msgctxt "acf"
786
+ msgid "Content Editor"
787
+ msgstr ""
788
+
789
+ #: connectors/class-connector-acf.php:297
790
+ msgctxt "acf"
791
+ msgid "Excerpt"
792
+ msgstr ""
793
+
794
+ #: connectors/class-connector-acf.php:298
795
+ msgctxt "acf"
796
+ msgid "Custom Fields"
797
+ msgstr ""
798
+
799
+ #: connectors/class-connector-acf.php:299
800
+ msgctxt "acf"
801
+ msgid "Discussion"
802
+ msgstr ""
803
+
804
+ #: connectors/class-connector-acf.php:300
805
+ msgctxt "acf"
806
+ msgid "Comments"
807
+ msgstr ""
808
+
809
+ #: connectors/class-connector-acf.php:301
810
+ msgctxt "acf"
811
+ msgid "Revisions"
812
+ msgstr ""
813
+
814
+ #: connectors/class-connector-acf.php:302
815
+ msgctxt "acf"
816
+ msgid "Slug"
817
+ msgstr ""
818
+
819
+ #: connectors/class-connector-acf.php:303
820
+ msgctxt "acf"
821
+ msgid "Author"
822
+ msgstr ""
823
+
824
+ #: connectors/class-connector-acf.php:304
825
+ msgctxt "acf"
826
+ msgid "Format"
827
+ msgstr ""
828
+
829
+ #: connectors/class-connector-acf.php:305
830
+ msgctxt "acf"
831
+ msgid "Featured Image"
832
+ msgstr ""
833
+
834
+ #: connectors/class-connector-acf.php:306
835
+ msgctxt "acf"
836
+ msgid "Categories"
837
+ msgstr ""
838
+
839
+ #: connectors/class-connector-acf.php:307
840
+ msgctxt "acf"
841
+ msgid "Tags"
842
+ msgstr ""
843
+
844
+ #: connectors/class-connector-acf.php:308
845
+ msgctxt "acf"
846
+ msgid "Send Trackbacks"
847
+ msgstr ""
848
+
849
+ #: connectors/class-connector-acf.php:312
850
+ msgctxt "acf"
851
+ msgid "All screens"
852
+ msgstr ""
853
+
854
+ #: connectors/class-connector-acf.php:314
855
+ msgctxt "acf"
856
+ msgid "No screens"
857
+ msgstr ""
858
+
859
+ #: connectors/class-connector-acf.php:320
860
+ #, php-format
861
+ msgctxt "acf"
862
+ msgid "\"%1$s\" set to display on \"%2$s\""
863
+ msgstr ""
864
+
865
+ #: connectors/class-connector-acf.php:382
866
+ msgid "user"
867
+ msgstr ""
868
+
869
+ #: connectors/class-connector-acf.php:395
870
+ #, php-format
871
+ msgctxt "acf"
872
+ msgid "\"%1$s\" of \"%2$s\" %3$s updated"
873
+ msgstr ""
874
+
875
+ #: connectors/class-connector-acf.php:433
876
+ #, php-format
877
+ msgctxt "acf"
878
+ msgid "Updated rules of \"%1$s\" (%2$d added, %3$d deleted)"
879
+ msgstr ""
880
+
881
+ #: connectors/class-connector-acf.php:463
882
+ msgid "field group"
883
+ msgstr ""
884
+
885
+ #: connectors/class-connector-acf.php:500
886
+ #, php-format
887
+ msgctxt "acf"
888
+ msgid "\"%1$s\" reordered from %2$d to %3$d"
889
+ msgstr ""
890
+
891
+ #: connectors/class-connector-bbpress.php:78
892
+ msgctxt "bbpress"
893
+ msgid "bbPress"
894
+ msgstr ""
895
+
896
+ #: connectors/class-connector-bbpress.php:88
897
+ msgctxt "bbpress"
898
+ msgid "Created"
899
+ msgstr ""
900
+
901
+ #: connectors/class-connector-bbpress.php:89
902
+ msgctxt "bbpress"
903
+ msgid "Updated"
904
+ msgstr ""
905
+
906
+ #: connectors/class-connector-bbpress.php:90
907
+ msgctxt "bbpress"
908
+ msgid "Activated"
909
+ msgstr ""
910
+
911
+ #: connectors/class-connector-bbpress.php:91
912
+ msgctxt "bbpress"
913
+ msgid "Deactivated"
914
+ msgstr ""
915
+
916
+ #: connectors/class-connector-bbpress.php:92
917
+ msgctxt "bbpress"
918
+ msgid "Deleted"
919
+ msgstr ""
920
+
921
+ #: connectors/class-connector-bbpress.php:93
922
+ msgctxt "bbpress"
923
+ msgid "Trashed"
924
+ msgstr ""
925
+
926
+ #: connectors/class-connector-bbpress.php:94
927
+ msgctxt "bbpress"
928
+ msgid "Restored"
929
+ msgstr ""
930
+
931
+ #: connectors/class-connector-bbpress.php:95
932
+ msgctxt "bbpress"
933
+ msgid "Generated"
934
+ msgstr ""
935
+
936
+ #: connectors/class-connector-bbpress.php:96
937
+ msgctxt "bbpress"
938
+ msgid "Imported"
939
+ msgstr ""
940
+
941
+ #: connectors/class-connector-bbpress.php:97
942
+ msgctxt "bbpress"
943
+ msgid "Exported"
944
+ msgstr ""
945
+
946
+ #: connectors/class-connector-bbpress.php:98
947
+ msgctxt "bbpress"
948
+ msgid "Closed"
949
+ msgstr ""
950
+
951
+ #: connectors/class-connector-bbpress.php:99
952
+ msgctxt "bbpress"
953
+ msgid "Opened"
954
+ msgstr ""
955
+
956
+ #: connectors/class-connector-bbpress.php:100
957
+ msgctxt "bbpress"
958
+ msgid "Sticked"
959
+ msgstr ""
960
+
961
+ #: connectors/class-connector-bbpress.php:101
962
+ msgctxt "bbpress"
963
+ msgid "Unsticked"
964
+ msgstr ""
965
+
966
+ #: connectors/class-connector-bbpress.php:102
967
+ msgctxt "bbpress"
968
+ msgid "Marked as spam"
969
+ msgstr ""
970
+
971
+ #: connectors/class-connector-bbpress.php:103
972
+ msgctxt "bbpress"
973
+ msgid "Unmarked as spam"
974
+ msgstr ""
975
+
976
+ #: connectors/class-connector-bbpress.php:114
977
+ msgctxt "bbpress"
978
+ msgid "Settings"
979
+ msgstr ""
980
+
981
+ #: connectors/class-connector-bbpress.php:131
982
+ #: connectors/class-connector-buddypress.php:164
983
+ #: connectors/class-connector-buddypress.php:223
984
+ #: connectors/class-connector-buddypress.php:233
985
+ #: connectors/class-connector-buddypress.php:254
986
+ #: connectors/class-connector-comments.php:133
987
+ #: connectors/class-connector-gravityforms.php:137
988
+ #: connectors/class-connector-wordpress-seo.php:116
989
+ #: connectors/class-connector-wordpress-seo.php:123
990
+ msgid "Edit"
991
+ msgstr ""
992
+
993
+ #: connectors/class-connector-bbpress.php:165
994
+ msgid "Reply Threading"
995
+ msgstr ""
996
+
997
+ #: connectors/class-connector-bbpress.php:187
998
+ #, php-format
999
+ msgid "Replied on \"%1$s\""
1000
+ msgstr ""
1001
+
1002
+ #: connectors/class-connector-bbpress.php:191
1003
+ #, php-format
1004
+ msgid "Reply to: %s"
1005
+ msgstr ""
1006
+
1007
+ #: connectors/class-connector-bbpress.php:233
1008
+ #, php-format
1009
+ msgctxt "1: Action, 2: Topic title"
1010
+ msgid "%1$s \"%2$s\" topic"
1011
+ msgstr ""
1012
+
1013
+ #: connectors/class-connector-blogs.php:40
1014
+ msgid "Sites"
1015
+ msgstr ""
1016
+
1017
+ #: connectors/class-connector-blogs.php:50
1018
+ msgid "Archived"
1019
+ msgstr ""
1020
+
1021
+ #: connectors/class-connector-blogs.php:89
1022
+ #: connectors/class-connector-blogs.php:95
1023
+ msgid "Site Admin"
1024
+ msgstr ""
1025
+
1026
+ #: connectors/class-connector-blogs.php:106
1027
+ msgid "Site Settings"
1028
+ msgstr ""
1029
+
1030
+ #: connectors/class-connector-blogs.php:125
1031
+ #, php-format
1032
+ msgctxt "1. Site name"
1033
+ msgid "\"%1$s\" site was created"
1034
+ msgstr ""
1035
+
1036
+ #: connectors/class-connector-blogs.php:151
1037
+ #, php-format
1038
+ msgctxt "1. Site name"
1039
+ msgid "\"%1$s\" site was registered"
1040
+ msgstr ""
1041
+
1042
+ #: connectors/class-connector-blogs.php:184
1043
+ #, php-format
1044
+ msgctxt "1. User's name, 2. Site name, 3. Role"
1045
+ msgid "%1$s was added to the \"%2$s\" site with %3$s capabilities"
1046
+ msgstr ""
1047
+
1048
+ #: connectors/class-connector-blogs.php:217
1049
+ #, php-format
1050
+ msgctxt "1. User's name, 2. Site name"
1051
+ msgid "%1$s was removed from the \"%2$s\" site"
1052
+ msgstr ""
1053
+
1054
+ #: connectors/class-connector-blogs.php:239
1055
+ #: connectors/class-connector-gravityforms.php:650
1056
+ msgid "marked as spam"
1057
+ msgstr ""
1058
+
1059
+ #: connectors/class-connector-blogs.php:250
1060
+ msgid "marked as not spam"
1061
+ msgstr ""
1062
+
1063
+ #: connectors/class-connector-blogs.php:261
1064
+ msgid "marked as mature"
1065
+ msgstr ""
1066
+
1067
+ #: connectors/class-connector-blogs.php:272
1068
+ msgid "marked as not mature"
1069
+ msgstr ""
1070
+
1071
+ #: connectors/class-connector-blogs.php:283
1072
+ msgid "archived"
1073
+ msgstr ""
1074
+
1075
+ #: connectors/class-connector-blogs.php:294
1076
+ msgid "restored from archive"
1077
+ msgstr ""
1078
+
1079
+ #: connectors/class-connector-blogs.php:305
1080
+ #: connectors/class-connector-gravityforms.php:497
1081
+ msgid "deleted"
1082
+ msgstr ""
1083
+
1084
+ #: connectors/class-connector-blogs.php:316
1085
+ #: connectors/class-connector-gravityforms.php:652
1086
+ msgid "restored"
1087
+ msgstr ""
1088
+
1089
+ #: connectors/class-connector-blogs.php:329
1090
+ msgid "marked as public"
1091
+ msgstr ""
1092
+
1093
+ #: connectors/class-connector-blogs.php:331
1094
+ msgid "marked as private"
1095
+ msgstr ""
1096
+
1097
+ #: connectors/class-connector-blogs.php:351
1098
+ #, php-format
1099
+ msgctxt "1. Site name, 2. Status"
1100
+ msgid "\"%1$s\" site was %2$s"
1101
+ msgstr ""
1102
+
1103
+ #: connectors/class-connector-buddypress.php:113
1104
+ msgctxt "buddypress"
1105
+ msgid "BuddyPress"
1106
+ msgstr ""
1107
+
1108
+ #: connectors/class-connector-buddypress.php:123
1109
+ msgctxt "buddypress"
1110
+ msgid "Created"
1111
+ msgstr ""
1112
+
1113
+ #: connectors/class-connector-buddypress.php:124
1114
+ msgctxt "buddypress"
1115
+ msgid "Updated"
1116
+ msgstr ""
1117
+
1118
+ #: connectors/class-connector-buddypress.php:125
1119
+ msgctxt "buddypress"
1120
+ msgid "Activated"
1121
+ msgstr ""
1122
+
1123
+ #: connectors/class-connector-buddypress.php:126
1124
+ msgctxt "buddypress"
1125
+ msgid "Deactivated"
1126
+ msgstr ""
1127
+
1128
+ #: connectors/class-connector-buddypress.php:127
1129
+ msgctxt "buddypress"
1130
+ msgid "Deleted"
1131
+ msgstr ""
1132
+
1133
+ #: connectors/class-connector-buddypress.php:128
1134
+ msgctxt "buddypress"
1135
+ msgid "Marked as spam"
1136
+ msgstr ""
1137
+
1138
+ #: connectors/class-connector-buddypress.php:129
1139
+ msgctxt "buddypress"
1140
+ msgid "Unmarked as spam"
1141
+ msgstr ""
1142
+
1143
+ #: connectors/class-connector-buddypress.php:130
1144
+ msgctxt "buddypress"
1145
+ msgid "Promoted"
1146
+ msgstr ""
1147
+
1148
+ #: connectors/class-connector-buddypress.php:131
1149
+ msgctxt "buddypress"
1150
+ msgid "Demoted"
1151
+ msgstr ""
1152
+
1153
+ #: connectors/class-connector-buddypress.php:142
1154
+ msgctxt "buddypress"
1155
+ msgid "Components"
1156
+ msgstr ""
1157
+
1158
+ #: connectors/class-connector-buddypress.php:143
1159
+ msgctxt "buddypress"
1160
+ msgid "Groups"
1161
+ msgstr ""
1162
+
1163
+ #: connectors/class-connector-buddypress.php:144
1164
+ msgctxt "buddypress"
1165
+ msgid "Activity"
1166
+ msgstr ""
1167
+
1168
+ #: connectors/class-connector-buddypress.php:145
1169
+ msgctxt "buddypress"
1170
+ msgid "Profile fields"
1171
+ msgstr ""
1172
+
1173
+ #: connectors/class-connector-buddypress.php:173
1174
+ #: connectors/class-connector-buddypress.php:186
1175
+ msgid "Edit setting"
1176
+ msgstr ""
1177
+
1178
+ #: connectors/class-connector-buddypress.php:181
1179
+ msgid "Edit Page"
1180
+ msgstr ""
1181
+
1182
+ #: connectors/class-connector-buddypress.php:182
1183
+ #: connectors/class-connector-gravityforms.php:145
1184
+ #: connectors/class-connector-gravityforms.php:155
1185
+ #: connectors/class-connector-media.php:111
1186
+ #: connectors/class-connector-posts.php:107
1187
+ #: connectors/class-connector-taxonomies.php:106
1188
+ #: connectors/class-connector-woocommerce.php:220
1189
+ #: connectors/class-connector-wordpress-seo.php:165
1190
+ msgid "View"
1191
+ msgstr ""
1192
+
1193
+ #: connectors/class-connector-buddypress.php:203
1194
+ msgid "Edit group"
1195
+ msgstr ""
1196
+
1197
+ #: connectors/class-connector-buddypress.php:204
1198
+ msgid "View group"
1199
+ msgstr ""
1200
+
1201
+ #: connectors/class-connector-buddypress.php:205
1202
+ msgid "Delete group"
1203
+ msgstr ""
1204
+
1205
+ #: connectors/class-connector-buddypress.php:221
1206
+ msgid "Ham"
1207
+ msgstr ""
1208
+
1209
+ #: connectors/class-connector-buddypress.php:224
1210
+ msgid "Spam"
1211
+ msgstr ""
1212
+
1213
+ #: connectors/class-connector-buddypress.php:226
1214
+ #: connectors/class-connector-buddypress.php:241
1215
+ #: connectors/class-connector-buddypress.php:263
1216
+ msgid "Delete"
1217
+ msgstr ""
1218
+
1219
+ #: connectors/class-connector-buddypress.php:284
1220
+ #: connectors/class-connector-buddypress.php:288
1221
+ msgctxt "buddypress"
1222
+ msgid "Toolbar"
1223
+ msgstr ""
1224
+
1225
+ #: connectors/class-connector-buddypress.php:292
1226
+ msgctxt "buddypress"
1227
+ msgid "Account Deletion"
1228
+ msgstr ""
1229
+
1230
+ #: connectors/class-connector-buddypress.php:296
1231
+ msgctxt "buddypress"
1232
+ msgid "Profile Syncing"
1233
+ msgstr ""
1234
+
1235
+ #: connectors/class-connector-buddypress.php:300
1236
+ msgctxt "buddypress"
1237
+ msgid "Group Creation"
1238
+ msgstr ""
1239
+
1240
+ #: connectors/class-connector-buddypress.php:304
1241
+ msgctxt "buddypress"
1242
+ msgid "bbPress Configuration"
1243
+ msgstr ""
1244
+
1245
+ #: connectors/class-connector-buddypress.php:308
1246
+ msgctxt "buddypress"
1247
+ msgid "Blog &amp; Forum Comments"
1248
+ msgstr ""
1249
+
1250
+ #: connectors/class-connector-buddypress.php:312
1251
+ msgctxt "buddypress"
1252
+ msgid "Activity auto-refresh"
1253
+ msgstr ""
1254
+
1255
+ #: connectors/class-connector-buddypress.php:316
1256
+ msgctxt "buddypress"
1257
+ msgid "Akismet"
1258
+ msgstr ""
1259
+
1260
+ #: connectors/class-connector-buddypress.php:320
1261
+ msgctxt "buddypress"
1262
+ msgid "Avatar Uploads"
1263
+ msgstr ""
1264
+
1265
+ #: connectors/class-connector-buddypress.php:367
1266
+ #: connectors/class-connector-edd.php:248
1267
+ #: connectors/class-connector-edd.php:293
1268
+ #: connectors/class-connector-gravityforms.php:481
1269
+ #: connectors/class-connector-jetpack.php:493
1270
+ #: connectors/class-connector-jetpack.php:668
1271
+ #, php-format
1272
+ msgid "\"%s\" setting updated"
1273
+ msgstr ""
1274
+
1275
+ #: connectors/class-connector-buddypress.php:390
1276
+ #: connectors/class-connector-edd.php:365
1277
+ #: connectors/class-connector-gravityforms.php:353
1278
+ #: connectors/class-connector-gravityforms.php:399
1279
+ #: connectors/class-connector-gravityforms.php:649
1280
+ #: connectors/class-connector-jetpack.php:330
1281
+ #: connectors/class-connector-jetpack.php:434
1282
+ msgid "activated"
1283
+ msgstr ""
1284
+
1285
+ #: connectors/class-connector-buddypress.php:391
1286
+ #: connectors/class-connector-edd.php:365
1287
+ #: connectors/class-connector-gravityforms.php:353
1288
+ #: connectors/class-connector-gravityforms.php:399
1289
+ #: connectors/class-connector-jetpack.php:330
1290
+ #: connectors/class-connector-jetpack.php:434
1291
+ msgid "deactivated"
1292
+ msgstr ""
1293
+
1294
+ #: connectors/class-connector-buddypress.php:401
1295
+ #, php-format
1296
+ msgid "\"%1$s\" component %2$s"
1297
+ msgstr ""
1298
+
1299
+ #: connectors/class-connector-buddypress.php:432
1300
+ msgctxt "buddypress"
1301
+ msgid "Register"
1302
+ msgstr ""
1303
+
1304
+ #: connectors/class-connector-buddypress.php:433
1305
+ msgctxt "buddypress"
1306
+ msgid "Activate"
1307
+ msgstr ""
1308
+
1309
+ #: connectors/class-connector-buddypress.php:442
1310
+ msgid "No page"
1311
+ msgstr ""
1312
+
1313
+ #: connectors/class-connector-buddypress.php:446
1314
+ #, php-format
1315
+ msgid "\"%1$s\" page set to \"%2$s\""
1316
+ msgstr ""
1317
+
1318
+ #: connectors/class-connector-buddypress.php:480
1319
+ #, php-format
1320
+ msgid "\"%s\" activity deleted"
1321
+ msgstr ""
1322
+
1323
+ #: connectors/class-connector-buddypress.php:502
1324
+ #, php-format
1325
+ msgid "\"%s\" activities were deleted"
1326
+ msgstr ""
1327
+
1328
+ #: connectors/class-connector-buddypress.php:522
1329
+ #, php-format
1330
+ msgid "Marked activity \"%s\" as spam"
1331
+ msgstr ""
1332
+
1333
+ #: connectors/class-connector-buddypress.php:542
1334
+ #, php-format
1335
+ msgid "Unmarked activity \"%s\" as spam"
1336
+ msgstr ""
1337
+
1338
+ #: connectors/class-connector-buddypress.php:562
1339
+ #, php-format
1340
+ msgid "\"%s\" activity updated"
1341
+ msgstr ""
1342
+
1343
+ #: connectors/class-connector-buddypress.php:588
1344
+ #, php-format
1345
+ msgid "\"%s\" group created"
1346
+ msgstr ""
1347
+
1348
+ #: connectors/class-connector-buddypress.php:590
1349
+ #, php-format
1350
+ msgid "\"%s\" group updated"
1351
+ msgstr ""
1352
+
1353
+ #: connectors/class-connector-buddypress.php:592
1354
+ #, php-format
1355
+ msgid "\"%s\" group deleted"
1356
+ msgstr ""
1357
+
1358
+ #: connectors/class-connector-buddypress.php:594
1359
+ #, php-format
1360
+ msgid "Joined group \"%s\""
1361
+ msgstr ""
1362
+
1363
+ #: connectors/class-connector-buddypress.php:596
1364
+ #, php-format
1365
+ msgid "Left group \"%s\""
1366
+ msgstr ""
1367
+
1368
+ #: connectors/class-connector-buddypress.php:598
1369
+ #, php-format
1370
+ msgid "Banned \"%2$s\" from \"%1$s\""
1371
+ msgstr ""
1372
+
1373
+ #: connectors/class-connector-buddypress.php:601
1374
+ #, php-format
1375
+ msgid "Unbanned \"%2$s\" from \"%1$s\""
1376
+ msgstr ""
1377
+
1378
+ #: connectors/class-connector-buddypress.php:604
1379
+ #, php-format
1380
+ msgid "Removed \"%2$s\" from \"%1$s\""
1381
+ msgstr ""
1382
+
1383
+ #: connectors/class-connector-buddypress.php:672
1384
+ msgctxt "buddypress"
1385
+ msgid "Administrator"
1386
+ msgstr ""
1387
+
1388
+ #: connectors/class-connector-buddypress.php:673
1389
+ msgctxt "buddypress"
1390
+ msgid "Moderator"
1391
+ msgstr ""
1392
+
1393
+ #: connectors/class-connector-buddypress.php:676
1394
+ #, php-format
1395
+ msgid "Promoted \"%s\" to \"%s\" in \"%s\""
1396
+ msgstr ""
1397
+
1398
+ #: connectors/class-connector-buddypress.php:688
1399
+ #, php-format
1400
+ msgid "Demoted \"%s\" to \"%s\" in \"%s\""
1401
+ msgstr ""
1402
+
1403
+ #: connectors/class-connector-buddypress.php:690
1404
+ msgctxt "buddypress"
1405
+ msgid "Member"
1406
+ msgstr ""
1407
+
1408
+ #: connectors/class-connector-buddypress.php:715
1409
+ #, php-format
1410
+ msgid "Created profile field \"%s\""
1411
+ msgstr ""
1412
+
1413
+ #: connectors/class-connector-buddypress.php:717
1414
+ #, php-format
1415
+ msgid "Updated profile field \"%s\""
1416
+ msgstr ""
1417
+
1418
+ #: connectors/class-connector-buddypress.php:719
1419
+ #, php-format
1420
+ msgid "Deleted profile field \"%s\""
1421
+ msgstr ""
1422
+
1423
+ #: connectors/class-connector-buddypress.php:760
1424
+ #, php-format
1425
+ msgid "Created profile field group \"%s\""
1426
+ msgstr ""
1427
+
1428
+ #: connectors/class-connector-buddypress.php:762
1429
+ #, php-format
1430
+ msgid "Updated profile field group \"%s\""
1431
+ msgstr ""
1432
+
1433
+ #: connectors/class-connector-buddypress.php:764
1434
+ #, php-format
1435
+ msgid "Deleted profile field group \"%s\""
1436
+ msgstr ""
1437
+
1438
+ #: connectors/class-connector-comments.php:45
1439
+ #: connectors/class-connector-comments.php:77
1440
+ msgid "Comments"
1441
+ msgstr ""
1442
+
1443
+ #: connectors/class-connector-comments.php:56
1444
+ msgid "Edited"
1445
+ msgstr ""
1446
+
1447
+ #: connectors/class-connector-comments.php:57
1448
+ msgid "Replied"
1449
+ msgstr ""
1450
+
1451
+ #: connectors/class-connector-comments.php:58
1452
+ msgid "Approved"
1453
+ msgstr ""
1454
+
1455
+ #: connectors/class-connector-comments.php:59
1456
+ msgid "Unapproved"
1457
+ msgstr ""
1458
+
1459
+ #: connectors/class-connector-comments.php:60
1460
+ #: connectors/class-connector-gravityforms.php:424
1461
+ #: connectors/class-connector-posts.php:40
1462
+ msgid "Trashed"
1463
+ msgstr ""
1464
+
1465
+ #: connectors/class-connector-comments.php:61
1466
+ #: connectors/class-connector-gravityforms.php:425
1467
+ #: connectors/class-connector-posts.php:41
1468
+ msgid "Restored"
1469
+ msgstr ""
1470
+
1471
+ #: connectors/class-connector-comments.php:62
1472
+ msgid "Marked as Spam"
1473
+ msgstr ""
1474
+
1475
+ #: connectors/class-connector-comments.php:63
1476
+ msgid "Unmarked as Spam"
1477
+ msgstr ""
1478
+
1479
+ #: connectors/class-connector-comments.php:65
1480
+ msgid "Duplicate"
1481
+ msgstr ""
1482
+
1483
+ #: connectors/class-connector-comments.php:66
1484
+ msgid "Throttled"
1485
+ msgstr ""
1486
+
1487
+ #: connectors/class-connector-comments.php:90
1488
+ msgid "Comment"
1489
+ msgstr ""
1490
+
1491
+ #: connectors/class-connector-comments.php:91
1492
+ msgid "Trackback"
1493
+ msgstr ""
1494
+
1495
+ #: connectors/class-connector-comments.php:92
1496
+ msgid "Pingback"
1497
+ msgstr ""
1498
+
1499
+ #: connectors/class-connector-comments.php:136
1500
+ msgid "Unapprove"
1501
+ msgstr ""
1502
+
1503
+ #: connectors/class-connector-comments.php:144
1504
+ msgid "Approve"
1505
+ msgstr ""
1506
+
1507
+ #: connectors/class-connector-comments.php:177
1508
+ msgid "Guest"
1509
+ msgstr ""
1510
+
1511
+ #: connectors/class-connector-comments.php:225
1512
+ msgid "a logged out user"
1513
+ msgstr ""
1514
+
1515
+ #: connectors/class-connector-comments.php:229
1516
+ #, php-format
1517
+ msgid "Comment flooding by %s detected and prevented"
1518
+ msgstr ""
1519
+
1520
+ #: connectors/class-connector-comments.php:254
1521
+ #: connectors/class-connector-comments.php:322
1522
+ #: connectors/class-connector-comments.php:386
1523
+ #: connectors/class-connector-comments.php:424
1524
+ #: connectors/class-connector-comments.php:458
1525
+ #: connectors/class-connector-comments.php:492
1526
+ #: connectors/class-connector-comments.php:526
1527
+ #: connectors/class-connector-comments.php:564
1528
+ #: connectors/class-connector-comments.php:602
1529
+ msgid "a post"
1530
+ msgstr ""
1531
+
1532
+ #: connectors/class-connector-comments.php:255
1533
+ msgid "approved automatically"
1534
+ msgstr ""
1535
+
1536
+ #: connectors/class-connector-comments.php:255
1537
+ msgid "pending approval"
1538
+ msgstr ""
1539
+
1540
+ #: connectors/class-connector-comments.php:266
1541
+ msgid "automatically marked as spam by Akismet"
1542
+ msgstr ""
1543
+
1544
+ #: connectors/class-connector-comments.php:278
1545
+ #, php-format
1546
+ msgctxt ""
1547
+ "1: Parent comment's author, 2: Comment author, 3: Post title, 4: Comment "
1548
+ "status, 5: Comment type"
1549
+ msgid "Reply to %1$s's %5$s by %2$s on %3$s %4$s"
1550
+ msgstr ""
1551
+
1552
+ #: connectors/class-connector-comments.php:291
1553
+ #, php-format
1554
+ msgctxt "1: Comment author, 2: Post title 3: Comment status, 4: Comment type"
1555
+ msgid "New %4$s by %1$s on %2$s %3$s"
1556
+ msgstr ""
1557
+
1558
+ #: connectors/class-connector-comments.php:327
1559
+ #, php-format
1560
+ msgctxt "1: Comment author, 2: Post title, 3: Comment type"
1561
+ msgid "%1$s's %3$s on %2$s edited"
1562
+ msgstr ""
1563
+
1564
+ #: connectors/class-connector-comments.php:395
1565
+ #, php-format
1566
+ msgctxt "1: Comment author, 2: Post title, 3: Comment type"
1567
+ msgid "%1$s's %3$s on %2$s deleted permanently"
1568
+ msgstr ""
1569
+
1570
+ #: connectors/class-connector-comments.php:429
1571
+ #, php-format
1572
+ msgctxt "1: Comment author, 2: Post title, 3: Comment type"
1573
+ msgid "%1$s's %3$s on %2$s trashed"
1574
+ msgstr ""
1575
+
1576
+ #: connectors/class-connector-comments.php:463
1577
+ #, php-format
1578
+ msgctxt "1: Comment author, 2: Post title, 3: Comment type"
1579
+ msgid "%1$s's %3$s on %2$s restored"
1580
+ msgstr ""
1581
+
1582
+ #: connectors/class-connector-comments.php:497
1583
+ #, php-format
1584
+ msgctxt "1: Comment author, 2: Post title, 3: Comment type"
1585
+ msgid "%1$s's %3$s on %2$s marked as spam"
1586
+ msgstr ""
1587
+
1588
+ #: connectors/class-connector-comments.php:531
1589
+ #, php-format
1590
+ msgctxt "1: Comment author, 2: Post title, 3: Comment type"
1591
+ msgid "%1$s's %3$s on %2$s unmarked as spam"
1592
+ msgstr ""
1593
+
1594
+ #: connectors/class-connector-comments.php:569
1595
+ #, php-format
1596
+ msgctxt ""
1597
+ "Comment status transition. 1: Comment author, 2: Post title, 3: Comment type"
1598
+ msgid "%1$s's %3$s %2$s"
1599
+ msgstr ""
1600
+
1601
+ #: connectors/class-connector-comments.php:607
1602
+ #, php-format
1603
+ msgctxt "1: Comment author, 2: Post title, 3: Comment type"
1604
+ msgid "Duplicate %3$s by %1$s prevented on %2$s"
1605
+ msgstr ""
1606
+
1607
+ #: connectors/class-connector-edd.php:99
1608
+ msgctxt "edd"
1609
+ msgid "Easy Digital Downloads"
1610
+ msgstr ""
1611
+
1612
+ #: connectors/class-connector-edd.php:109
1613
+ msgctxt "edd"
1614
+ msgid "Created"
1615
+ msgstr ""
1616
+
1617
+ #: connectors/class-connector-edd.php:110
1618
+ msgctxt "edd"
1619
+ msgid "Updated"
1620
+ msgstr ""
1621
+
1622
+ #: connectors/class-connector-edd.php:111
1623
+ msgctxt "edd"
1624
+ msgid "Added"
1625
+ msgstr ""
1626
+
1627
+ #: connectors/class-connector-edd.php:112
1628
+ msgctxt "edd"
1629
+ msgid "Deleted"
1630
+ msgstr ""
1631
+
1632
+ #: connectors/class-connector-edd.php:113
1633
+ msgctxt "edd"
1634
+ msgid "Trashed"
1635
+ msgstr ""
1636
+
1637
+ #: connectors/class-connector-edd.php:114
1638
+ msgctxt "edd"
1639
+ msgid "Restored"
1640
+ msgstr ""
1641
+
1642
+ #: connectors/class-connector-edd.php:115
1643
+ msgctxt "edd"
1644
+ msgid "Generated"
1645
+ msgstr ""
1646
+
1647
+ #: connectors/class-connector-edd.php:116
1648
+ msgctxt "edd"
1649
+ msgid "Imported"
1650
+ msgstr ""
1651
+
1652
+ #: connectors/class-connector-edd.php:117
1653
+ msgctxt "edd"
1654
+ msgid "Exported"
1655
+ msgstr ""
1656
+
1657
+ #: connectors/class-connector-edd.php:118
1658
+ msgctxt "edd"
1659
+ msgid "Revoked"
1660
+ msgstr ""
1661
+
1662
+ #: connectors/class-connector-edd.php:129
1663
+ msgctxt "edd"
1664
+ msgid "Downloads"
1665
+ msgstr ""
1666
+
1667
+ #: connectors/class-connector-edd.php:130
1668
+ msgctxt "edd"
1669
+ msgid "Categories"
1670
+ msgstr ""
1671
+
1672
+ #: connectors/class-connector-edd.php:131
1673
+ msgctxt "edd"
1674
+ msgid "Tags"
1675
+ msgstr ""
1676
+
1677
+ #: connectors/class-connector-edd.php:132
1678
+ msgctxt "edd"
1679
+ msgid "Discounts"
1680
+ msgstr ""
1681
+
1682
+ #: connectors/class-connector-edd.php:133
1683
+ msgctxt "edd"
1684
+ msgid "Reports"
1685
+ msgstr ""
1686
+
1687
+ #: connectors/class-connector-edd.php:134
1688
+ msgctxt "edd"
1689
+ msgid "API Keys"
1690
+ msgstr ""
1691
+
1692
+ #: connectors/class-connector-edd.php:157
1693
+ #: connectors/class-connector-edd.php:184
1694
+ #, php-format
1695
+ msgid "Edit %s"
1696
+ msgstr ""
1697
+
1698
+ #: connectors/class-connector-edd.php:166
1699
+ #, php-format
1700
+ msgid "Deactivate %s"
1701
+ msgstr ""
1702
+
1703
+ #: connectors/class-connector-edd.php:174
1704
+ #, php-format
1705
+ msgid "Activate %s"
1706
+ msgstr ""
1707
+
1708
+ #: connectors/class-connector-edd.php:189
1709
+ msgid "View API Log"
1710
+ msgstr ""
1711
+
1712
+ #: connectors/class-connector-edd.php:192
1713
+ msgid "Revoke"
1714
+ msgstr ""
1715
+
1716
+ #: connectors/class-connector-edd.php:193
1717
+ msgid "Reissue"
1718
+ msgstr ""
1719
+
1720
+ #: connectors/class-connector-edd.php:276
1721
+ msgctxt "edd"
1722
+ msgid "Banned emails"
1723
+ msgstr ""
1724
+
1725
+ #: connectors/class-connector-edd.php:331
1726
+ #, php-format
1727
+ msgid "\"%1s\" discount deleted"
1728
+ msgstr ""
1729
+
1730
+ #: connectors/class-connector-edd.php:363
1731
+ #, php-format
1732
+ msgid "\"%1$s\" discount %2$s"
1733
+ msgstr ""
1734
+
1735
+ #: connectors/class-connector-edd.php:395
1736
+ msgid "Sales and Earnings"
1737
+ msgstr ""
1738
+
1739
+ #: connectors/class-connector-edd.php:397
1740
+ msgid "Earnings"
1741
+ msgstr ""
1742
+
1743
+ #: connectors/class-connector-edd.php:399
1744
+ msgid "Payments"
1745
+ msgstr ""
1746
+
1747
+ #: connectors/class-connector-edd.php:401
1748
+ msgid "Emails"
1749
+ msgstr ""
1750
+
1751
+ #: connectors/class-connector-edd.php:403
1752
+ msgid "Download History"
1753
+ msgstr ""
1754
+
1755
+ #: connectors/class-connector-edd.php:408
1756
+ #, php-format
1757
+ msgid "Generated %s report"
1758
+ msgstr ""
1759
+
1760
+ #: connectors/class-connector-edd.php:422
1761
+ msgid "Exported Settings"
1762
+ msgstr ""
1763
+
1764
+ #: connectors/class-connector-edd.php:432
1765
+ msgid "Imported Settings"
1766
+ msgstr ""
1767
+
1768
+ #: connectors/class-connector-edd.php:470
1769
+ msgid "revoked"
1770
+ msgstr ""
1771
+
1772
+ #: connectors/class-connector-edd.php:473
1773
+ #: connectors/class-connector-gravityforms.php:216
1774
+ #: connectors/class-connector-gravityforms.php:247
1775
+ #: connectors/class-connector-gravityforms.php:280
1776
+ msgid "created"
1777
+ msgstr ""
1778
+
1779
+ #: connectors/class-connector-edd.php:476
1780
+ #: connectors/class-connector-gravityforms.php:216
1781
+ #: connectors/class-connector-gravityforms.php:247
1782
+ #: connectors/class-connector-gravityforms.php:280
1783
+ #: connectors/class-connector-gravityforms.php:497
1784
+ msgid "updated"
1785
+ msgstr ""
1786
+
1787
+ #: connectors/class-connector-edd.php:481
1788
+ #, php-format
1789
+ msgid "User API Key %s"
1790
+ msgstr ""
1791
+
1792
+ #: connectors/class-connector-editor.php:44
1793
+ msgid "Editor"
1794
+ msgstr ""
1795
+
1796
+ #: connectors/class-connector-editor.php:72
1797
+ #: connectors/class-connector-installer.php:61
1798
+ msgid "Themes"
1799
+ msgstr ""
1800
+
1801
+ #: connectors/class-connector-editor.php:73
1802
+ #: connectors/class-connector-installer.php:60
1803
+ msgid "Plugins"
1804
+ msgstr ""
1805
+
1806
+ #: connectors/class-connector-editor.php:112
1807
+ #, php-format
1808
+ msgctxt "1: File name, 2: Theme/plugin name"
1809
+ msgid "\"%1$s\" in \"%2$s\" updated"
1810
+ msgstr ""
1811
+
1812
+ #: connectors/class-connector-editor.php:140
1813
+ #: connectors/class-connector-editor.php:157
1814
+ msgid "Edit File"
1815
+ msgstr ""
1816
+
1817
+ #: connectors/class-connector-editor.php:148
1818
+ msgid "Theme Details"
1819
+ msgstr ""
1820
+
1821
+ #: connectors/class-connector-gravityforms.php:88
1822
+ msgctxt "gravityforms"
1823
+ msgid "Gravity Forms"
1824
+ msgstr ""
1825
+
1826
+ #: connectors/class-connector-gravityforms.php:98
1827
+ msgctxt "gravityforms"
1828
+ msgid "Created"
1829
+ msgstr ""
1830
+
1831
+ #: connectors/class-connector-gravityforms.php:99
1832
+ msgctxt "gravityforms"
1833
+ msgid "Updated"
1834
+ msgstr ""
1835
+
1836
+ #: connectors/class-connector-gravityforms.php:100
1837
+ msgctxt "gravityforms"
1838
+ msgid "Exported"
1839
+ msgstr ""
1840
+
1841
+ #: connectors/class-connector-gravityforms.php:101
1842
+ msgctxt "gravityforms"
1843
+ msgid "Imported"
1844
+ msgstr ""
1845
+
1846
+ #: connectors/class-connector-gravityforms.php:102
1847
+ msgctxt "gravityforms"
1848
+ msgid "Added"
1849
+ msgstr ""
1850
+
1851
+ #: connectors/class-connector-gravityforms.php:103
1852
+ msgctxt "gravityforms"
1853
+ msgid "Deleted"
1854
+ msgstr ""
1855
+
1856
+ #: connectors/class-connector-gravityforms.php:104
1857
+ msgctxt "gravityforms"
1858
+ msgid "Trashed"
1859
+ msgstr ""
1860
+
1861
+ #: connectors/class-connector-gravityforms.php:105
1862
+ msgctxt "gravityforms"
1863
+ msgid "Restored"
1864
+ msgstr ""
1865
+
1866
+ #: connectors/class-connector-gravityforms.php:106
1867
+ msgctxt "gravityforms"
1868
+ msgid "Duplicated"
1869
+ msgstr ""
1870
+
1871
+ #: connectors/class-connector-gravityforms.php:117
1872
+ msgctxt "gravityforms"
1873
+ msgid "Forms"
1874
+ msgstr ""
1875
+
1876
+ #: connectors/class-connector-gravityforms.php:118
1877
+ msgctxt "gravityforms"
1878
+ msgid "Settings"
1879
+ msgstr ""
1880
+
1881
+ #: connectors/class-connector-gravityforms.php:119
1882
+ msgctxt "gravityforms"
1883
+ msgid "Import/Export"
1884
+ msgstr ""
1885
+
1886
+ #: connectors/class-connector-gravityforms.php:120
1887
+ msgctxt "gravityforms"
1888
+ msgid "Entries"
1889
+ msgstr ""
1890
+
1891
+ #: connectors/class-connector-gravityforms.php:121
1892
+ msgctxt "gravityforms"
1893
+ msgid "Notes"
1894
+ msgstr ""
1895
+
1896
+ #: connectors/class-connector-gravityforms.php:165
1897
+ msgid "Edit Settings"
1898
+ msgstr ""
1899
+
1900
+ #: connectors/class-connector-gravityforms.php:181
1901
+ msgctxt "gravityforms"
1902
+ msgid "Output CSS"
1903
+ msgstr ""
1904
+
1905
+ #: connectors/class-connector-gravityforms.php:184
1906
+ msgctxt "gravityforms"
1907
+ msgid "Output HTML5"
1908
+ msgstr ""
1909
+
1910
+ #: connectors/class-connector-gravityforms.php:187
1911
+ msgctxt "gravityforms"
1912
+ msgid "No-Conflict Mode"
1913
+ msgstr ""
1914
+
1915
+ #: connectors/class-connector-gravityforms.php:190
1916
+ msgctxt "gravityforms"
1917
+ msgid "Currency"
1918
+ msgstr ""
1919
+
1920
+ #: connectors/class-connector-gravityforms.php:193
1921
+ msgctxt "gravityforms"
1922
+ msgid "reCAPTCHA Public Key"
1923
+ msgstr ""
1924
+
1925
+ #: connectors/class-connector-gravityforms.php:196
1926
+ msgctxt "gravityforms"
1927
+ msgid "reCAPTCHA Private Key"
1928
+ msgstr ""
1929
+
1930
+ #: connectors/class-connector-gravityforms.php:214
1931
+ #: connectors/class-connector-gravityforms.php:430
1932
+ #, php-format
1933
+ msgid "\"%1$s\" form %2$s"
1934
+ msgstr ""
1935
+
1936
+ #: connectors/class-connector-gravityforms.php:245
1937
+ #, php-format
1938
+ msgid "\"%1$s\" confirmation %2$s for \"%3$s\""
1939
+ msgstr ""
1940
+
1941
+ #: connectors/class-connector-gravityforms.php:278
1942
+ #, php-format
1943
+ msgid "\"%1$s\" notification %2$s for \"%3$s\""
1944
+ msgstr ""
1945
+
1946
+ #: connectors/class-connector-gravityforms.php:304
1947
+ #, php-format
1948
+ msgid "\"%1$s\" notification deleted from \"%2$s\""
1949
+ msgstr ""
1950
+
1951
+ #: connectors/class-connector-gravityforms.php:327
1952
+ #, php-format
1953
+ msgid "\"%1$s\" confirmation deleted from \"%2$s\""
1954
+ msgstr ""
1955
+
1956
+ #: connectors/class-connector-gravityforms.php:351
1957
+ #, php-format
1958
+ msgid "\"%1$s\" confirmation %2$s from \"%3$s\""
1959
+ msgstr ""
1960
+
1961
+ #: connectors/class-connector-gravityforms.php:376
1962
+ #, php-format
1963
+ msgid "\"%s\" form views reset"
1964
+ msgstr ""
1965
+
1966
+ #: connectors/class-connector-gravityforms.php:397
1967
+ #, php-format
1968
+ msgid "\"%1$s\" notification %2$s from \"%3$s\""
1969
+ msgstr ""
1970
+
1971
+ #: connectors/class-connector-gravityforms.php:422
1972
+ #: connectors/class-connector-installer.php:46
1973
+ msgid "Activated"
1974
+ msgstr ""
1975
+
1976
+ #: connectors/class-connector-gravityforms.php:423
1977
+ #: connectors/class-connector-installer.php:47
1978
+ #: connectors/class-connector-widgets.php:61
1979
+ msgid "Deactivated"
1980
+ msgstr ""
1981
+
1982
+ #: connectors/class-connector-gravityforms.php:496
1983
+ #, php-format
1984
+ msgid "Gravity Forms license key %s"
1985
+ msgstr ""
1986
+
1987
+ #: connectors/class-connector-gravityforms.php:510
1988
+ #, php-format
1989
+ msgid "\"%s\" form exported"
1990
+ msgstr ""
1991
+
1992
+ #: connectors/class-connector-gravityforms.php:525
1993
+ msgid "Import process started"
1994
+ msgstr ""
1995
+
1996
+ #: connectors/class-connector-gravityforms.php:540
1997
+ #, php-format
1998
+ msgid "Export process started for %d forms"
1999
+ msgstr ""
2000
+
2001
+ #: connectors/class-connector-gravityforms.php:558
2002
+ #, php-format
2003
+ msgid "\"%s\" form deleted"
2004
+ msgstr ""
2005
+
2006
+ #: connectors/class-connector-gravityforms.php:574
2007
+ #, php-format
2008
+ msgid "\"%1$s\" form created as duplicate from \"%2$s\""
2009
+ msgstr ""
2010
+
2011
+ #: connectors/class-connector-gravityforms.php:592
2012
+ #, php-format
2013
+ msgid "Lead #%1$d from \"%2$s\" deleted"
2014
+ msgstr ""
2015
+
2016
+ #: connectors/class-connector-gravityforms.php:609
2017
+ #, php-format
2018
+ msgid "Note #%1$d added to lead #%2$d on \"%3$s\" form"
2019
+ msgstr ""
2020
+
2021
+ #: connectors/class-connector-gravityforms.php:627
2022
+ #, php-format
2023
+ msgid "Note #%1$d deleted from lead #%2$d on \"%3$s\" form"
2024
+ msgstr ""
2025
+
2026
+ #: connectors/class-connector-gravityforms.php:651
2027
+ msgid "trashed"
2028
+ msgstr ""
2029
+
2030
+ #: connectors/class-connector-gravityforms.php:661
2031
+ #: connectors/class-connector-gravityforms.php:708
2032
+ #, php-format
2033
+ msgid "Lead #%1$d %2$s on \"%3$s\" form"
2034
+ msgstr ""
2035
+
2036
+ #: connectors/class-connector-gravityforms.php:685
2037
+ #, php-format
2038
+ msgid "Lead #%1$d marked as %2$s on \"%3$s\" form"
2039
+ msgstr ""
2040
+
2041
+ #: connectors/class-connector-gravityforms.php:687
2042
+ msgid "read"
2043
+ msgstr ""
2044
+
2045
+ #: connectors/class-connector-gravityforms.php:687
2046
+ msgid "unread"
2047
+ msgstr ""
2048
+
2049
+ #: connectors/class-connector-gravityforms.php:710
2050
+ msgid "starred"
2051
+ msgstr ""
2052
+
2053
+ #: connectors/class-connector-gravityforms.php:710
2054
+ msgid "unstarred"
2055
+ msgstr ""
2056
+
2057
+ #: connectors/class-connector-installer.php:35
2058
+ msgid "Installer"
2059
+ msgstr ""
2060
+
2061
+ #: connectors/class-connector-installer.php:45
2062
+ msgid "Installed"
2063
+ msgstr ""
2064
+
2065
+ #: connectors/class-connector-installer.php:62
2066
+ msgid "WordPress"
2067
+ msgstr ""
2068
+
2069
+ #: connectors/class-connector-installer.php:83
2070
+ msgid "About"
2071
+ msgstr ""
2072
+
2073
+ #: connectors/class-connector-installer.php:86
2074
+ msgid "View Release Notes"
2075
+ msgstr ""
2076
+
2077
+ #: connectors/class-connector-installer.php:166
2078
+ #, php-format
2079
+ msgctxt ""
2080
+ "Plugin/theme installation. 1: Type (plugin/theme), 2: Plugin/theme name, 3: "
2081
+ "Plugin/theme version"
2082
+ msgid "Installed %1$s: %2$s %3$s"
2083
+ msgstr ""
2084
+
2085
+ #: connectors/class-connector-installer.php:175
2086
+ #, php-format
2087
+ msgctxt ""
2088
+ "Plugin/theme update. 1: Type (plugin/theme), 2: Plugin/theme name, 3: Plugin/"
2089
+ "theme version"
2090
+ msgid "Updated %1$s: %2$s %3$s"
2091
+ msgstr ""
2092
+
2093
+ #: connectors/class-connector-installer.php:244
2094
+ #: connectors/class-connector-installer.php:262
2095
+ #: connectors/class-connector-installer.php:367
2096
+ msgid "network wide"
2097
+ msgstr ""
2098
+
2099
+ #: connectors/class-connector-installer.php:248
2100
+ #, php-format
2101
+ msgctxt "1: Plugin name, 2: Single site or network wide"
2102
+ msgid "\"%1$s\" plugin activated %2$s"
2103
+ msgstr ""
2104
+
2105
+ #: connectors/class-connector-installer.php:266
2106
+ #, php-format
2107
+ msgctxt "1: Plugin name, 2: Single site or network wide"
2108
+ msgid "\"%1$s\" plugin deactivated %2$s"
2109
+ msgstr ""
2110
+
2111
+ #: connectors/class-connector-installer.php:280
2112
+ #, php-format
2113
+ msgid "\"%s\" theme activated"
2114
+ msgstr ""
2115
+
2116
+ #: connectors/class-connector-installer.php:310
2117
+ #, php-format
2118
+ msgid "\"%s\" theme deleted"
2119
+ msgstr ""
2120
+
2121
+ #: connectors/class-connector-installer.php:370
2122
+ #, php-format
2123
+ msgid "\"%s\" plugin deleted"
2124
+ msgstr ""
2125
+
2126
+ #: connectors/class-connector-installer.php:390
2127
+ #, php-format
2128
+ msgid "WordPress auto-updated to %s"
2129
+ msgstr ""
2130
+
2131
+ #: connectors/class-connector-installer.php:392
2132
+ #, php-format
2133
+ msgid "WordPress updated to %s"
2134
+ msgstr ""
2135
+
2136
+ #: connectors/class-connector-jetpack.php:69
2137
+ msgctxt "jetpack"
2138
+ msgid "Jetpack"
2139
+ msgstr ""
2140
+
2141
+ #: connectors/class-connector-jetpack.php:79
2142
+ msgctxt "jetpack"
2143
+ msgid "Activated"
2144
+ msgstr ""
2145
+
2146
+ #: connectors/class-connector-jetpack.php:80
2147
+ msgctxt "jetpack"
2148
+ msgid "Dectivated"
2149
+ msgstr ""
2150
+
2151
+ #: connectors/class-connector-jetpack.php:81
2152
+ msgctxt "jetpack"
2153
+ msgid "Connected"
2154
+ msgstr ""
2155
+
2156
+ #: connectors/class-connector-jetpack.php:82
2157
+ msgctxt "jetpack"
2158
+ msgid "Disconnected"
2159
+ msgstr ""
2160
+
2161
+ #: connectors/class-connector-jetpack.php:83
2162
+ msgctxt "jetpack"
2163
+ msgid "Link"
2164
+ msgstr ""
2165
+
2166
+ #: connectors/class-connector-jetpack.php:84
2167
+ msgctxt "jetpack"
2168
+ msgid "Unlink"
2169
+ msgstr ""
2170
+
2171
+ #: connectors/class-connector-jetpack.php:85
2172
+ msgctxt "jetpack"
2173
+ msgid "Updated"
2174
+ msgstr ""
2175
+
2176
+ #: connectors/class-connector-jetpack.php:86
2177
+ msgctxt "jetpack"
2178
+ msgid "Added"
2179
+ msgstr ""
2180
+
2181
+ #: connectors/class-connector-jetpack.php:87
2182
+ msgctxt "jetpack"
2183
+ msgid "Removed"
2184
+ msgstr ""
2185
+
2186
+ #: connectors/class-connector-jetpack.php:98
2187
+ msgctxt "jetpack"
2188
+ msgid "Blogs"
2189
+ msgstr ""
2190
+
2191
+ #: connectors/class-connector-jetpack.php:99
2192
+ msgctxt "jetpack"
2193
+ msgid "Carousel"
2194
+ msgstr ""
2195
+
2196
+ #: connectors/class-connector-jetpack.php:100
2197
+ msgctxt "jetpack"
2198
+ msgid "Custom CSS"
2199
+ msgstr ""
2200
+
2201
+ #: connectors/class-connector-jetpack.php:101
2202
+ msgctxt "jetpack"
2203
+ msgid "Google+ Profile"
2204
+ msgstr ""
2205
+
2206
+ #: connectors/class-connector-jetpack.php:102
2207
+ msgctxt "jetpack"
2208
+ msgid "Infinite Scroll"
2209
+ msgstr ""
2210
+
2211
+ #: connectors/class-connector-jetpack.php:103
2212
+ msgctxt "jetpack"
2213
+ msgid "Comments"
2214
+ msgstr ""
2215
+
2216
+ #: connectors/class-connector-jetpack.php:104
2217
+ msgctxt "jetpack"
2218
+ msgid "Likes"
2219
+ msgstr ""
2220
+
2221
+ #: connectors/class-connector-jetpack.php:105
2222
+ msgctxt "jetpack"
2223
+ msgid "Mobile"
2224
+ msgstr ""
2225
+
2226
+ #: connectors/class-connector-jetpack.php:106
2227
+ msgctxt "jetpack"
2228
+ msgid "Modules"
2229
+ msgstr ""
2230
+
2231
+ #: connectors/class-connector-jetpack.php:107
2232
+ msgctxt "jetpack"
2233
+ msgid "Monitor"
2234
+ msgstr ""
2235
+
2236
+ #: connectors/class-connector-jetpack.php:108
2237
+ msgctxt "jetpack"
2238
+ msgid "Options"
2239
+ msgstr ""
2240
+
2241
+ #: connectors/class-connector-jetpack.php:109
2242
+ msgctxt "jetpack"
2243
+ msgid "Post by Email"
2244
+ msgstr ""
2245
+
2246
+ #: connectors/class-connector-jetpack.php:110
2247
+ msgctxt "jetpack"
2248
+ msgid "Protect"
2249
+ msgstr ""
2250
+
2251
+ #: connectors/class-connector-jetpack.php:111
2252
+ msgctxt "jetpack"
2253
+ msgid "Publicize"
2254
+ msgstr ""
2255
+
2256
+ #: connectors/class-connector-jetpack.php:112
2257
+ msgctxt "jetpack"
2258
+ msgid "Related Posts"
2259
+ msgstr ""
2260
+
2261
+ #: connectors/class-connector-jetpack.php:113
2262
+ msgctxt "jetpack"
2263
+ msgid "Sharing"
2264
+ msgstr ""
2265
+
2266
+ #: connectors/class-connector-jetpack.php:114
2267
+ msgctxt "jetpack"
2268
+ msgid "Subscriptions"
2269
+ msgstr ""
2270
+
2271
+ #: connectors/class-connector-jetpack.php:115
2272
+ msgctxt "jetpack"
2273
+ msgid "SSO"
2274
+ msgstr ""
2275
+
2276
+ #: connectors/class-connector-jetpack.php:116
2277
+ msgctxt "jetpack"
2278
+ msgid "WordPress.com Stats"
2279
+ msgstr ""
2280
+
2281
+ #: connectors/class-connector-jetpack.php:117
2282
+ msgctxt "jetpack"
2283
+ msgid "Tiled Galleries"
2284
+ msgstr ""
2285
+
2286
+ #: connectors/class-connector-jetpack.php:118
2287
+ msgctxt "jetpack"
2288
+ msgid "Users"
2289
+ msgstr ""
2290
+
2291
+ #: connectors/class-connector-jetpack.php:119
2292
+ msgctxt "jetpack"
2293
+ msgid "Site Verification"
2294
+ msgstr ""
2295
+
2296
+ #: connectors/class-connector-jetpack.php:120
2297
+ msgctxt "jetpack"
2298
+ msgid "VideoPress"
2299
+ msgstr ""
2300
+
2301
+ #: connectors/class-connector-jetpack.php:146
2302
+ msgid "Configure"
2303
+ msgstr ""
2304
+
2305
+ #: connectors/class-connector-jetpack.php:149
2306
+ msgid "Deactivate"
2307
+ msgstr ""
2308
+
2309
+ #: connectors/class-connector-jetpack.php:160
2310
+ msgid "Activate"
2311
+ msgstr ""
2312
+
2313
+ #: connectors/class-connector-jetpack.php:175
2314
+ msgid "Configure module"
2315
+ msgstr ""
2316
+
2317
+ #: connectors/class-connector-jetpack.php:194
2318
+ msgid "Sharing options"
2319
+ msgstr ""
2320
+
2321
+ #: connectors/class-connector-jetpack.php:199
2322
+ msgid "Twitter site tag"
2323
+ msgstr ""
2324
+
2325
+ #: connectors/class-connector-jetpack.php:204
2326
+ msgid "WordPress.com Stats"
2327
+ msgstr ""
2328
+
2329
+ #: connectors/class-connector-jetpack.php:209
2330
+ msgid "Color Scheme"
2331
+ msgstr ""
2332
+
2333
+ #: connectors/class-connector-jetpack.php:214
2334
+ msgid "WP.com Site-wide Likes"
2335
+ msgstr ""
2336
+
2337
+ #: connectors/class-connector-jetpack.php:219
2338
+ msgid "Excerpts appearance"
2339
+ msgstr ""
2340
+
2341
+ #: connectors/class-connector-jetpack.php:223
2342
+ msgid "App promos"
2343
+ msgstr ""
2344
+
2345
+ #: connectors/class-connector-jetpack.php:231
2346
+ msgid "Background color"
2347
+ msgstr ""
2348
+
2349
+ #: connectors/class-connector-jetpack.php:235
2350
+ msgid "Metadata"
2351
+ msgstr ""
2352
+
2353
+ #: connectors/class-connector-jetpack.php:240
2354
+ msgid "Follow blog comment form button"
2355
+ msgstr ""
2356
+
2357
+ #: connectors/class-connector-jetpack.php:244
2358
+ msgid "Follow comments form button"
2359
+ msgstr ""
2360
+
2361
+ #: connectors/class-connector-jetpack.php:249
2362
+ msgid "Greeting Text"
2363
+ msgstr ""
2364
+
2365
+ #: connectors/class-connector-jetpack.php:254
2366
+ msgid "Infinite Scroll Google Analytics"
2367
+ msgstr ""
2368
+
2369
+ #: connectors/class-connector-jetpack.php:259
2370
+ msgid "Blocked Attempts"
2371
+ msgstr ""
2372
+
2373
+ #: connectors/class-connector-jetpack.php:264
2374
+ msgid "Require Two-Step Authentication"
2375
+ msgstr ""
2376
+
2377
+ #: connectors/class-connector-jetpack.php:268
2378
+ msgid "Match by Email"
2379
+ msgstr ""
2380
+
2381
+ #: connectors/class-connector-jetpack.php:274
2382
+ msgid "Show Related Posts Headline"
2383
+ msgstr ""
2384
+
2385
+ #: connectors/class-connector-jetpack.php:278
2386
+ msgid "Show Related Posts Thumbnails"
2387
+ msgstr ""
2388
+
2389
+ #: connectors/class-connector-jetpack.php:285
2390
+ msgid "Google Webmaster Tools Token"
2391
+ msgstr ""
2392
+
2393
+ #: connectors/class-connector-jetpack.php:289
2394
+ msgid "Bing Webmaster Center Token"
2395
+ msgstr ""
2396
+
2397
+ #: connectors/class-connector-jetpack.php:293
2398
+ msgid "Pinterest Site Verification Token"
2399
+ msgstr ""
2400
+
2401
+ #: connectors/class-connector-jetpack.php:299
2402
+ msgid "Tiled Galleries"
2403
+ msgstr ""
2404
+
2405
+ #: connectors/class-connector-jetpack.php:328
2406
+ #, php-format
2407
+ msgid "%1$s module %2$s"
2408
+ msgstr ""
2409
+
2410
+ #: connectors/class-connector-jetpack.php:346
2411
+ #, php-format
2412
+ msgid "%1$s's account %2$s %3$s Jetpack"
2413
+ msgstr ""
2414
+
2415
+ #: connectors/class-connector-jetpack.php:348
2416
+ msgid "unlinked"
2417
+ msgstr ""
2418
+
2419
+ #: connectors/class-connector-jetpack.php:348
2420
+ msgid "linked"
2421
+ msgstr ""
2422
+
2423
+ #: connectors/class-connector-jetpack.php:349
2424
+ msgid "from"
2425
+ msgstr ""
2426
+
2427
+ #: connectors/class-connector-jetpack.php:349
2428
+ msgid "to"
2429
+ msgstr ""
2430
+
2431
+ #: connectors/class-connector-jetpack.php:365
2432
+ #, php-format
2433
+ msgid "Site %s Jetpack"
2434
+ msgstr ""
2435
+
2436
+ #: connectors/class-connector-jetpack.php:366
2437
+ #: connectors/class-connector-jetpack.php:376
2438
+ msgid "connected to"
2439
+ msgstr ""
2440
+
2441
+ #: connectors/class-connector-jetpack.php:366
2442
+ #: connectors/class-connector-jetpack.php:376
2443
+ msgid "disconnected from"
2444
+ msgstr ""
2445
+
2446
+ #: connectors/class-connector-jetpack.php:374
2447
+ #, php-format
2448
+ msgid "\"%1$s\" blog %2$s Jetpack"
2449
+ msgstr ""
2450
+
2451
+ #: connectors/class-connector-jetpack.php:401
2452
+ msgid "Sharing services updated"
2453
+ msgstr ""
2454
+
2455
+ #: connectors/class-connector-jetpack.php:432
2456
+ #, php-format
2457
+ msgid "Monitor notifications %s"
2458
+ msgstr ""
2459
+
2460
+ #: connectors/class-connector-jetpack.php:459
2461
+ #: connectors/class-connector-jetpack.php:550
2462
+ msgid "enabled"
2463
+ msgstr ""
2464
+
2465
+ #: connectors/class-connector-jetpack.php:461
2466
+ #: connectors/class-connector-jetpack.php:550
2467
+ msgid "disabled"
2468
+ msgstr ""
2469
+
2470
+ #: connectors/class-connector-jetpack.php:463
2471
+ msgid "regenerated"
2472
+ msgstr ""
2473
+
2474
+ #: connectors/class-connector-jetpack.php:469
2475
+ #, php-format
2476
+ msgid "%1$s %2$s Post by Email"
2477
+ msgstr ""
2478
+
2479
+ #: connectors/class-connector-jetpack.php:548
2480
+ #, php-format
2481
+ msgid "G+ profile display %s"
2482
+ msgstr ""
2483
+
2484
+ #: connectors/class-connector-jetpack.php:565
2485
+ #, php-format
2486
+ msgid "%1$s's Google+ account %2$s"
2487
+ msgstr ""
2488
+
2489
+ #: connectors/class-connector-jetpack.php:568
2490
+ msgid "connected"
2491
+ msgstr ""
2492
+
2493
+ #: connectors/class-connector-jetpack.php:568
2494
+ msgid "disconnected"
2495
+ msgstr ""
2496
+
2497
+ #: connectors/class-connector-jetpack.php:585
2498
+ #, php-format
2499
+ msgid "Sharing CSS/JS %s"
2500
+ msgstr ""
2501
+
2502
+ #: connectors/class-connector-jetpack.php:624
2503
+ msgid "Custom CSS updated"
2504
+ msgstr ""
2505
+
2506
+ #: connectors/class-connector-jetpack.php:644
2507
+ #, php-format
2508
+ msgid "%1$s connection %2$s"
2509
+ msgstr ""
2510
+
2511
+ #: connectors/class-connector-jetpack.php:647
2512
+ msgid "added"
2513
+ msgstr ""
2514
+
2515
+ #: connectors/class-connector-jetpack.php:647
2516
+ msgid "removed"
2517
+ msgstr ""
2518
+
2519
+ #: connectors/class-connector-jetpack.php:657
2520
+ msgid "Video Library Access"
2521
+ msgstr ""
2522
+
2523
+ #: connectors/class-connector-jetpack.php:658
2524
+ msgid "Allow users to upload videos"
2525
+ msgstr ""
2526
+
2527
+ #: connectors/class-connector-jetpack.php:659
2528
+ msgid "Free formats"
2529
+ msgstr ""
2530
+
2531
+ #: connectors/class-connector-jetpack.php:660
2532
+ msgid "Default quality"
2533
+ msgstr ""
2534
+
2535
+ #: connectors/class-connector-media.php:31
2536
+ #: connectors/class-connector-settings.php:131
2537
+ msgid "Media"
2538
+ msgstr ""
2539
+
2540
+ #: connectors/class-connector-media.php:41
2541
+ msgid "Attached"
2542
+ msgstr ""
2543
+
2544
+ #: connectors/class-connector-media.php:42
2545
+ msgid "Uploaded"
2546
+ msgstr ""
2547
+
2548
+ #: connectors/class-connector-media.php:45
2549
+ #: connectors/class-connector-menus.php:42
2550
+ msgid "Assigned"
2551
+ msgstr ""
2552
+
2553
+ #: connectors/class-connector-media.php:46
2554
+ #: connectors/class-connector-menus.php:43
2555
+ msgid "Unassigned"
2556
+ msgstr ""
2557
+
2558
+ #: connectors/class-connector-media.php:59
2559
+ msgid "Image"
2560
+ msgstr ""
2561
+
2562
+ #: connectors/class-connector-media.php:60
2563
+ msgid "Audio"
2564
+ msgstr ""
2565
+
2566
+ #: connectors/class-connector-media.php:61
2567
+ msgid "Video"
2568
+ msgstr ""
2569
+
2570
+ #: connectors/class-connector-media.php:62
2571
+ msgid "Document"
2572
+ msgstr ""
2573
+
2574
+ #: connectors/class-connector-media.php:63
2575
+ msgid "Spreadsheet"
2576
+ msgstr ""
2577
+
2578
+ #: connectors/class-connector-media.php:64
2579
+ msgid "Interactive"
2580
+ msgstr ""
2581
+
2582
+ #: connectors/class-connector-media.php:65
2583
+ msgid "Text"
2584
+ msgstr ""
2585
+
2586
+ #: connectors/class-connector-media.php:66
2587
+ msgid "Archive"
2588
+ msgstr ""
2589
+
2590
+ #: connectors/class-connector-media.php:67
2591
+ msgid "Code"
2592
+ msgstr ""
2593
+
2594
+ #: connectors/class-connector-media.php:108
2595
+ msgid "Edit Media"
2596
+ msgstr ""
2597
+
2598
+ #: connectors/class-connector-media.php:129
2599
+ #, php-format
2600
+ msgctxt "1: Attachment title, 2: Parent post title"
2601
+ msgid "Attached \"%1$s\" to \"%2$s\""
2602
+ msgstr ""
2603
+
2604
+ #: connectors/class-connector-media.php:134
2605
+ #, php-format
2606
+ msgid "Added \"%s\" to Media library"
2607
+ msgstr ""
2608
+
2609
+ #: connectors/class-connector-media.php:162
2610
+ #, php-format
2611
+ msgid "Updated \"%s\""
2612
+ msgstr ""
2613
+
2614
+ #: connectors/class-connector-media.php:186
2615
+ #, php-format
2616
+ msgid "Deleted \"%s\""
2617
+ msgstr ""
2618
+
2619
+ #: connectors/class-connector-media.php:222
2620
+ #, php-format
2621
+ msgid "Edited image \"%s\""
2622
+ msgstr ""
2623
+
2624
+ #: connectors/class-connector-menus.php:29
2625
+ msgid "Menus"
2626
+ msgstr ""
2627
+
2628
+ #: connectors/class-connector-menus.php:86
2629
+ msgid "Edit Menu"
2630
+ msgstr ""
2631
+
2632
+ #: connectors/class-connector-menus.php:105
2633
+ #, php-format
2634
+ msgid "Created new menu \"%s\""
2635
+ msgstr ""
2636
+
2637
+ #: connectors/class-connector-menus.php:129
2638
+ #, php-format
2639
+ msgctxt "Menu name"
2640
+ msgid "Updated menu \"%s\""
2641
+ msgstr ""
2642
+
2643
+ #: connectors/class-connector-menus.php:153
2644
+ #, php-format
2645
+ msgctxt "Menu name"
2646
+ msgid "Deleted \"%s\""
2647
+ msgstr ""
2648
+
2649
+ #: connectors/class-connector-menus.php:201
2650
+ #, php-format
2651
+ msgctxt "1: Menu name, 2: Theme location"
2652
+ msgid "\"%1$s\" has been unassigned from \"%2$s\""
2653
+ msgstr ""
2654
+
2655
+ #: connectors/class-connector-menus.php:209
2656
+ #, php-format
2657
+ msgctxt "1: Menu name, 2: Theme location"
2658
+ msgid "\"%1$s\" has been assigned to \"%2$s\""
2659
+ msgstr ""
2660
+
2661
+ #: connectors/class-connector-posts.php:28
2662
+ msgid "Posts"
2663
+ msgstr ""
2664
+
2665
+ #: connectors/class-connector-posts.php:101
2666
+ #: connectors/class-connector-wordpress-seo.php:159
2667
+ #, php-format
2668
+ msgctxt "Post type singular name"
2669
+ msgid "Restore %s"
2670
+ msgstr ""
2671
+
2672
+ #: connectors/class-connector-posts.php:102
2673
+ #: connectors/class-connector-wordpress-seo.php:160
2674
+ #, php-format
2675
+ msgctxt "Post type singular name"
2676
+ msgid "Delete %s Permenantly"
2677
+ msgstr ""
2678
+
2679
+ #: connectors/class-connector-posts.php:104
2680
+ #: connectors/class-connector-woocommerce.php:216
2681
+ #: connectors/class-connector-wordpress-seo.php:162
2682
+ #, php-format
2683
+ msgctxt "Post type singular name"
2684
+ msgid "Edit %s"
2685
+ msgstr ""
2686
+
2687
+ #: connectors/class-connector-posts.php:114
2688
+ #: connectors/class-connector-wordpress-seo.php:169
2689
+ msgid "Revision"
2690
+ msgstr ""
2691
+
2692
+ #: connectors/class-connector-posts.php:157
2693
+ #, php-format
2694
+ msgctxt "1: Post title, 2: Post type singular name"
2695
+ msgid "\"%1$s\" %2$s unpublished"
2696
+ msgstr ""
2697
+
2698
+ #: connectors/class-connector-posts.php:163
2699
+ #, php-format
2700
+ msgctxt "1: Post title, 2: Post type singular name"
2701
+ msgid "\"%1$s\" %2$s restored from trash"
2702
+ msgstr ""
2703
+
2704
+ #: connectors/class-connector-posts.php:170
2705
+ #, php-format
2706
+ msgctxt "1: Post title, 2: Post type singular name"
2707
+ msgid "\"%1$s\" %2$s drafted"
2708
+ msgstr ""
2709
+
2710
+ #: connectors/class-connector-posts.php:176
2711
+ #, php-format
2712
+ msgctxt "1: Post title, 2: Post type singular name"
2713
+ msgid "\"%1$s\" %2$s pending review"
2714
+ msgstr ""
2715
+
2716
+ #: connectors/class-connector-posts.php:182
2717
+ #, php-format
2718
+ msgctxt "1: Post title, 2: Post type singular name, 3: Scheduled post date"
2719
+ msgid "\"%1$s\" %2$s scheduled for %3$s"
2720
+ msgstr ""
2721
+
2722
+ #: connectors/class-connector-posts.php:188
2723
+ #, php-format
2724
+ msgctxt "1: Post title, 2: Post type singular name"
2725
+ msgid "\"%1$s\" scheduled %2$s published"
2726
+ msgstr ""
2727
+
2728
+ #: connectors/class-connector-posts.php:194
2729
+ #, php-format
2730
+ msgctxt "1: Post title, 2: Post type singular name"
2731
+ msgid "\"%1$s\" %2$s published"
2732
+ msgstr ""
2733
+
2734
+ #: connectors/class-connector-posts.php:200
2735
+ #, php-format
2736
+ msgctxt "1: Post title, 2: Post type singular name"
2737
+ msgid "\"%1$s\" %2$s privately published"
2738
+ msgstr ""
2739
+
2740
+ #: connectors/class-connector-posts.php:206
2741
+ #, php-format
2742
+ msgctxt "1: Post title, 2: Post type singular name"
2743
+ msgid "\"%1$s\" %2$s trashed"
2744
+ msgstr ""
2745
+
2746
+ #: connectors/class-connector-posts.php:213
2747
+ #, php-format
2748
+ msgctxt "1: Post title, 2: Post type singular name"
2749
+ msgid "\"%1$s\" %2$s updated"
2750
+ msgstr ""
2751
+
2752
+ #: connectors/class-connector-posts.php:290
2753
+ #, php-format
2754
+ msgctxt "1: Post title, 2: Post type singular name"
2755
+ msgid "\"%1$s\" %2$s deleted from trash"
2756
+ msgstr ""
2757
+
2758
+ #: connectors/class-connector-posts.php:328
2759
+ msgid "Post"
2760
+ msgstr ""
2761
+
2762
+ #: connectors/class-connector-settings.php:128
2763
+ msgid "Writing"
2764
+ msgstr ""
2765
+
2766
+ #: connectors/class-connector-settings.php:129
2767
+ msgid "Reading"
2768
+ msgstr ""
2769
+
2770
+ #: connectors/class-connector-settings.php:130
2771
+ msgid "Discussion"
2772
+ msgstr ""
2773
+
2774
+ #: connectors/class-connector-settings.php:132
2775
+ msgid "Permalinks"
2776
+ msgstr ""
2777
+
2778
+ #: connectors/class-connector-settings.php:133
2779
+ msgid "Network"
2780
+ msgstr ""
2781
+
2782
+ #: connectors/class-connector-settings.php:135
2783
+ msgid "Custom Background"
2784
+ msgstr ""
2785
+
2786
+ #: connectors/class-connector-settings.php:136
2787
+ msgid "Custom Header"
2788
+ msgstr ""
2789
+
2790
+ #: connectors/class-connector-settings.php:143
2791
+ msgid "Stream Network"
2792
+ msgstr ""
2793
+
2794
+ #: connectors/class-connector-settings.php:144
2795
+ msgid "Stream Defaults"
2796
+ msgstr ""
2797
+
2798
+ #: connectors/class-connector-settings.php:242
2799
+ msgid "Site Title"
2800
+ msgstr ""
2801
+
2802
+ #: connectors/class-connector-settings.php:243
2803
+ msgid "Tagline"
2804
+ msgstr ""
2805
+
2806
+ #: connectors/class-connector-settings.php:244
2807
+ #: connectors/class-connector-settings.php:245
2808
+ msgid "E-mail Address"
2809
+ msgstr ""
2810
+
2811
+ #: connectors/class-connector-settings.php:246
2812
+ msgid "WordPress Address (URL)"
2813
+ msgstr ""
2814
+
2815
+ #: connectors/class-connector-settings.php:247
2816
+ msgid "Site Address (URL)"
2817
+ msgstr ""
2818
+
2819
+ #: connectors/class-connector-settings.php:248
2820
+ msgid "Membership"
2821
+ msgstr ""
2822
+
2823
+ #: connectors/class-connector-settings.php:249
2824
+ msgid "New User Default Role"
2825
+ msgstr ""
2826
+
2827
+ #: connectors/class-connector-settings.php:250
2828
+ msgid "Timezone"
2829
+ msgstr ""
2830
+
2831
+ #: connectors/class-connector-settings.php:251
2832
+ msgid "Date Format"
2833
+ msgstr ""
2834
+
2835
+ #: connectors/class-connector-settings.php:252
2836
+ msgid "Time Format"
2837
+ msgstr ""
2838
+
2839
+ #: connectors/class-connector-settings.php:253
2840
+ msgid "Week Starts On"
2841
+ msgstr ""
2842
+
2843
+ #: connectors/class-connector-settings.php:255
2844
+ #: connectors/class-connector-settings.php:256
2845
+ msgid "Formatting"
2846
+ msgstr ""
2847
+
2848
+ #: connectors/class-connector-settings.php:257
2849
+ msgid "Default Post Category"
2850
+ msgstr ""
2851
+
2852
+ #: connectors/class-connector-settings.php:258
2853
+ msgid "Default Post Format"
2854
+ msgstr ""
2855
+
2856
+ #: connectors/class-connector-settings.php:259
2857
+ msgid "Mail Server"
2858
+ msgstr ""
2859
+
2860
+ #: connectors/class-connector-settings.php:260
2861
+ msgid "Login Name"
2862
+ msgstr ""
2863
+
2864
+ #: connectors/class-connector-settings.php:261
2865
+ msgid "Password"
2866
+ msgstr ""
2867
+
2868
+ #: connectors/class-connector-settings.php:262
2869
+ msgid "Default Mail Category"
2870
+ msgstr ""
2871
+
2872
+ #: connectors/class-connector-settings.php:263
2873
+ msgid "Update Services"
2874
+ msgstr ""
2875
+
2876
+ #: connectors/class-connector-settings.php:265
2877
+ #: connectors/class-connector-settings.php:266
2878
+ #: connectors/class-connector-settings.php:267
2879
+ msgid "Front page displays"
2880
+ msgstr ""
2881
+
2882
+ #: connectors/class-connector-settings.php:268
2883
+ msgid "Blog pages show at most"
2884
+ msgstr ""
2885
+
2886
+ #: connectors/class-connector-settings.php:269
2887
+ msgid "Syndication feeds show the most recent"
2888
+ msgstr ""
2889
+
2890
+ #: connectors/class-connector-settings.php:270
2891
+ msgid "For each article in a feed, show"
2892
+ msgstr ""
2893
+
2894
+ #: connectors/class-connector-settings.php:271
2895
+ msgid "Search Engine Visibility"
2896
+ msgstr ""
2897
+
2898
+ #: connectors/class-connector-settings.php:273
2899
+ #: connectors/class-connector-settings.php:274
2900
+ #: connectors/class-connector-settings.php:275
2901
+ msgid "Default article settings"
2902
+ msgstr ""
2903
+
2904
+ #: connectors/class-connector-settings.php:276
2905
+ #: connectors/class-connector-settings.php:277
2906
+ #: connectors/class-connector-settings.php:278
2907
+ #: connectors/class-connector-settings.php:279
2908
+ #: connectors/class-connector-settings.php:280
2909
+ #: connectors/class-connector-settings.php:281
2910
+ #: connectors/class-connector-settings.php:282
2911
+ #: connectors/class-connector-settings.php:283
2912
+ #: connectors/class-connector-settings.php:284
2913
+ #: connectors/class-connector-settings.php:285
2914
+ msgid "Other comment settings"
2915
+ msgstr ""
2916
+
2917
+ #: connectors/class-connector-settings.php:286
2918
+ #: connectors/class-connector-settings.php:287
2919
+ msgid "E-mail me whenever"
2920
+ msgstr ""
2921
+
2922
+ #: connectors/class-connector-settings.php:288
2923
+ #: connectors/class-connector-settings.php:289
2924
+ msgid "Before a comment appears"
2925
+ msgstr ""
2926
+
2927
+ #: connectors/class-connector-settings.php:290
2928
+ #: connectors/class-connector-settings.php:291
2929
+ msgid "Comment Moderation"
2930
+ msgstr ""
2931
+
2932
+ #: connectors/class-connector-settings.php:292
2933
+ msgid "Comment Blacklist"
2934
+ msgstr ""
2935
+
2936
+ #: connectors/class-connector-settings.php:293
2937
+ msgid "Show Avatars"
2938
+ msgstr ""
2939
+
2940
+ #: connectors/class-connector-settings.php:294
2941
+ msgid "Maximum Rating"
2942
+ msgstr ""
2943
+
2944
+ #: connectors/class-connector-settings.php:295
2945
+ msgid "Default Avatar"
2946
+ msgstr ""
2947
+
2948
+ #: connectors/class-connector-settings.php:297
2949
+ #: connectors/class-connector-settings.php:298
2950
+ #: connectors/class-connector-settings.php:299
2951
+ msgid "Thumbnail size"
2952
+ msgstr ""
2953
+
2954
+ #: connectors/class-connector-settings.php:300
2955
+ #: connectors/class-connector-settings.php:301
2956
+ msgid "Medium size"
2957
+ msgstr ""
2958
+
2959
+ #: connectors/class-connector-settings.php:302
2960
+ #: connectors/class-connector-settings.php:303
2961
+ msgid "Large size"
2962
+ msgstr ""
2963
+
2964
+ #: connectors/class-connector-settings.php:304
2965
+ msgid "Uploading Files"
2966
+ msgstr ""
2967
+
2968
+ #: connectors/class-connector-settings.php:306
2969
+ msgid "Permalink Settings"
2970
+ msgstr ""
2971
+
2972
+ #: connectors/class-connector-settings.php:307
2973
+ msgid "Category base"
2974
+ msgstr ""
2975
+
2976
+ #: connectors/class-connector-settings.php:308
2977
+ msgid "Tag base"
2978
+ msgstr ""
2979
+
2980
+ #: connectors/class-connector-settings.php:310
2981
+ msgid "Registration notification"
2982
+ msgstr ""
2983
+
2984
+ #: connectors/class-connector-settings.php:311
2985
+ msgid "Allow new registrations"
2986
+ msgstr ""
2987
+
2988
+ #: connectors/class-connector-settings.php:312
2989
+ msgid "Add New Users"
2990
+ msgstr ""
2991
+
2992
+ #: connectors/class-connector-settings.php:313
2993
+ msgid "Enable administration menus"
2994
+ msgstr ""
2995
+
2996
+ #: connectors/class-connector-settings.php:314
2997
+ msgid "Site upload space check"
2998
+ msgstr ""
2999
+
3000
+ #: connectors/class-connector-settings.php:315
3001
+ msgid "Site upload space"
3002
+ msgstr ""
3003
+
3004
+ #: connectors/class-connector-settings.php:316
3005
+ msgid "Upload file types"
3006
+ msgstr ""
3007
+
3008
+ #: connectors/class-connector-settings.php:317
3009
+ msgid "Network Title"
3010
+ msgstr ""
3011
+
3012
+ #: connectors/class-connector-settings.php:318
3013
+ msgid "First Post"
3014
+ msgstr ""
3015
+
3016
+ #: connectors/class-connector-settings.php:319
3017
+ msgid "First Page"
3018
+ msgstr ""
3019
+
3020
+ #: connectors/class-connector-settings.php:320
3021
+ msgid "First Comment"
3022
+ msgstr ""
3023
+
3024
+ #: connectors/class-connector-settings.php:321
3025
+ msgid "First Comment URL"
3026
+ msgstr ""
3027
+
3028
+ #: connectors/class-connector-settings.php:322
3029
+ msgid "First Comment Author"
3030
+ msgstr ""
3031
+
3032
+ #: connectors/class-connector-settings.php:323
3033
+ msgid "Welcome Email"
3034
+ msgstr ""
3035
+
3036
+ #: connectors/class-connector-settings.php:324
3037
+ msgid "Welcome User Email"
3038
+ msgstr ""
3039
+
3040
+ #: connectors/class-connector-settings.php:325
3041
+ msgid "Max upload file size"
3042
+ msgstr ""
3043
+
3044
+ #: connectors/class-connector-settings.php:326
3045
+ msgid "Terms Enabled"
3046
+ msgstr ""
3047
+
3048
+ #: connectors/class-connector-settings.php:327
3049
+ msgid "Banned Names"
3050
+ msgstr ""
3051
+
3052
+ #: connectors/class-connector-settings.php:328
3053
+ msgid "Limited Email Registrations"
3054
+ msgstr ""
3055
+
3056
+ #: connectors/class-connector-settings.php:329
3057
+ msgid "Banned Email Domains"
3058
+ msgstr ""
3059
+
3060
+ #: connectors/class-connector-settings.php:330
3061
+ msgid "Network Language"
3062
+ msgstr ""
3063
+
3064
+ #: connectors/class-connector-settings.php:331
3065
+ msgid "Blog Count"
3066
+ msgstr ""
3067
+
3068
+ #: connectors/class-connector-settings.php:332
3069
+ msgid "User Count"
3070
+ msgstr ""
3071
+
3072
+ #: connectors/class-connector-settings.php:334
3073
+ msgid "Stream Database Version"
3074
+ msgstr ""
3075
+
3076
+ #: connectors/class-connector-settings.php:339
3077
+ #: connectors/class-connector-settings.php:340
3078
+ msgid "Network Admin Email"
3079
+ msgstr ""
3080
+
3081
+ #: connectors/class-connector-settings.php:372
3082
+ msgid "Background Image"
3083
+ msgstr ""
3084
+
3085
+ #: connectors/class-connector-settings.php:373
3086
+ msgid "Background Position"
3087
+ msgstr ""
3088
+
3089
+ #: connectors/class-connector-settings.php:374
3090
+ msgid "Background Repeat"
3091
+ msgstr ""
3092
+
3093
+ #: connectors/class-connector-settings.php:375
3094
+ msgid "Background Attachment"
3095
+ msgstr ""
3096
+
3097
+ #: connectors/class-connector-settings.php:376
3098
+ msgid "Background Color"
3099
+ msgstr ""
3100
+
3101
+ #: connectors/class-connector-settings.php:378
3102
+ msgid "Header Image"
3103
+ msgstr ""
3104
+
3105
+ #: connectors/class-connector-settings.php:379
3106
+ msgid "Text Color"
3107
+ msgstr ""
3108
+
3109
+ #: connectors/class-connector-settings.php:515
3110
+ #, php-format
3111
+ msgid "Edit %s Settings"
3112
+ msgstr ""
3113
+
3114
+ #: connectors/class-connector-settings.php:665
3115
+ #, php-format
3116
+ msgid "\"%s\" setting was updated"
3117
+ msgstr ""
3118
+
3119
+ #: connectors/class-connector-taxonomies.php:44
3120
+ msgid "Taxonomies"
3121
+ msgstr ""
3122
+
3123
+ #: connectors/class-connector-taxonomies.php:105
3124
+ #, php-format
3125
+ msgctxt "Term singular name"
3126
+ msgid "Edit %s"
3127
+ msgstr ""
3128
+
3129
+ #: connectors/class-connector-taxonomies.php:154
3130
+ #, php-format
3131
+ msgctxt "1: Term name, 2: Taxonomy singular label"
3132
+ msgid "\"%1$s\" %2$s created"
3133
+ msgstr ""
3134
+
3135
+ #: connectors/class-connector-taxonomies.php:186
3136
+ #, php-format
3137
+ msgctxt "1: Term name, 2: Taxonomy singular label"
3138
+ msgid "\"%1$s\" %2$s deleted"
3139
+ msgstr ""
3140
+
3141
+ #: connectors/class-connector-taxonomies.php:228
3142
+ #, php-format
3143
+ msgctxt "1: Term name, 2: Taxonomy singular label"
3144
+ msgid "\"%1$s\" %2$s updated"
3145
+ msgstr ""
3146
+
3147
+ #: connectors/class-connector-users.php:41
3148
+ #: connectors/class-connector-users.php:68
3149
+ msgid "Users"
3150
+ msgstr ""
3151
+
3152
+ #: connectors/class-connector-users.php:54
3153
+ msgid "Password Reset"
3154
+ msgstr ""
3155
+
3156
+ #: connectors/class-connector-users.php:55
3157
+ msgid "Lost Password"
3158
+ msgstr ""
3159
+
3160
+ #: connectors/class-connector-users.php:56
3161
+ msgid "Log In"
3162
+ msgstr ""
3163
+
3164
+ #: connectors/class-connector-users.php:57
3165
+ msgid "Log Out"
3166
+ msgstr ""
3167
+
3168
+ #: connectors/class-connector-users.php:69
3169
+ msgid "Sessions"
3170
+ msgstr ""
3171
+
3172
+ #: connectors/class-connector-users.php:70
3173
+ msgid "Profiles"
3174
+ msgstr ""
3175
+
3176
+ #: connectors/class-connector-users.php:87
3177
+ msgid "Edit User"
3178
+ msgstr ""
3179
+
3180
+ #: connectors/class-connector-users.php:136
3181
+ msgid "New user registration"
3182
+ msgstr ""
3183
+
3184
+ #: connectors/class-connector-users.php:140
3185
+ #, php-format
3186
+ msgctxt "1: User display name, 2: User role"
3187
+ msgid "New user account created for %1$s (%2$s)"
3188
+ msgstr ""
3189
+
3190
+ #: connectors/class-connector-users.php:172
3191
+ #, php-format
3192
+ msgid "%s's profile was updated"
3193
+ msgstr ""
3194
+
3195
+ #: connectors/class-connector-users.php:200
3196
+ #, php-format
3197
+ msgctxt "1: User display name, 2: Old role, 3: New role"
3198
+ msgid "%1$s's role was changed from %2$s to %3$s"
3199
+ msgstr ""
3200
+
3201
+ #: connectors/class-connector-users.php:224
3202
+ #, php-format
3203
+ msgid "%s's password was reset"
3204
+ msgstr ""
3205
+
3206
+ #: connectors/class-connector-users.php:250
3207
+ #, php-format
3208
+ msgid "%s's password was requested to be reset"
3209
+ msgstr ""
3210
+
3211
+ #: connectors/class-connector-users.php:276
3212
+ #, php-format
3213
+ msgid "%s logged in"
3214
+ msgstr ""
3215
+
3216
+ #: connectors/class-connector-users.php:299
3217
+ #, php-format
3218
+ msgid "%s logged out"
3219
+ msgstr ""
3220
+
3221
+ #: connectors/class-connector-users.php:335
3222
+ #, php-format
3223
+ msgctxt "1: User display name, 2: User roles"
3224
+ msgid "%1$s's account was deleted (%2$s)"
3225
+ msgstr ""
3226
+
3227
+ #: connectors/class-connector-users.php:343
3228
+ #, php-format
3229
+ msgid "User account #%d was deleted"
3230
+ msgstr ""
3231
+
3232
+ #: connectors/class-connector-widgets.php:46
3233
+ msgid "Widgets"
3234
+ msgstr ""
3235
+
3236
+ #: connectors/class-connector-widgets.php:57
3237
+ msgid "Removed"
3238
+ msgstr ""
3239
+
3240
+ #: connectors/class-connector-widgets.php:58
3241
+ msgid "Moved"
3242
+ msgstr ""
3243
+
3244
+ #: connectors/class-connector-widgets.php:62
3245
+ msgid "Reactivated"
3246
+ msgstr ""
3247
+
3248
+ #: connectors/class-connector-widgets.php:64
3249
+ msgid "Sorted"
3250
+ msgstr ""
3251
+
3252
+ #: connectors/class-connector-widgets.php:82
3253
+ msgid "Inactive Widgets"
3254
+ msgstr ""
3255
+
3256
+ #: connectors/class-connector-widgets.php:83
3257
+ msgid "Orphaned Widgets"
3258
+ msgstr ""
3259
+
3260
+ #: connectors/class-connector-widgets.php:103
3261
+ msgid "Edit Widget Area"
3262
+ msgstr ""
3263
+
3264
+ #: connectors/class-connector-widgets.php:199
3265
+ #, php-format
3266
+ msgctxt "1: Name, 2: Title, 3: Sidebar Name"
3267
+ msgid "%1$s widget named \"%2$s\" from \"%3$s\" deactivated"
3268
+ msgstr ""
3269
+
3270
+ #: connectors/class-connector-widgets.php:202
3271
+ #, php-format
3272
+ msgctxt "1: Name, 3: Sidebar Name"
3273
+ msgid "%1$s widget from \"%3$s\" deactivated"
3274
+ msgstr ""
3275
+
3276
+ #: connectors/class-connector-widgets.php:205
3277
+ #, php-format
3278
+ msgctxt "2: Title, 3: Sidebar Name"
3279
+ msgid "Unknown widget type named \"%2$s\" from \"%3$s\" deactivated"
3280
+ msgstr ""
3281
+
3282
+ #: connectors/class-connector-widgets.php:208
3283
+ #, php-format
3284
+ msgctxt "4: Widget ID, 3: Sidebar Name"
3285
+ msgid "%4$s widget from \"%3$s\" deactivated"
3286
+ msgstr ""
3287
+
3288
+ #: connectors/class-connector-widgets.php:248
3289
+ #, php-format
3290
+ msgctxt "1: Name, 2: Title"
3291
+ msgid "%1$s widget named \"%2$s\" reactivated"
3292
+ msgstr ""
3293
+
3294
+ #: connectors/class-connector-widgets.php:251
3295
+ #, php-format
3296
+ msgctxt "1: Name"
3297
+ msgid "%1$s widget reactivated"
3298
+ msgstr ""
3299
+
3300
+ #: connectors/class-connector-widgets.php:254
3301
+ #, php-format
3302
+ msgctxt "2: Title"
3303
+ msgid "Unknown widget type named \"%2$s\" reactivated"
3304
+ msgstr ""
3305
+
3306
+ #: connectors/class-connector-widgets.php:257
3307
+ #, php-format
3308
+ msgctxt "3: Widget ID"
3309
+ msgid "%3$s widget reactivated"
3310
+ msgstr ""
3311
+
3312
+ #: connectors/class-connector-widgets.php:304
3313
+ #, php-format
3314
+ msgctxt "1: Name, 2: Title, 3: Sidebar Name"
3315
+ msgid "%1$s widget named \"%2$s\" removed from \"%3$s\""
3316
+ msgstr ""
3317
+
3318
+ #: connectors/class-connector-widgets.php:307
3319
+ #, php-format
3320
+ msgctxt "1: Name, 3: Sidebar Name"
3321
+ msgid "%1$s widget removed from \"%3$s\""
3322
+ msgstr ""
3323
+
3324
+ #: connectors/class-connector-widgets.php:310
3325
+ #, php-format
3326
+ msgctxt "2: Title, 3: Sidebar Name"
3327
+ msgid "Unknown widget type named \"%2$s\" removed from \"%3$s\""
3328
+ msgstr ""
3329
+
3330
+ #: connectors/class-connector-widgets.php:313
3331
+ #, php-format
3332
+ msgctxt "4: Widget ID, 3: Sidebar Name"
3333
+ msgid "%4$s widget removed from \"%3$s\""
3334
+ msgstr ""
3335
+
3336
+ #: connectors/class-connector-widgets.php:357
3337
+ #, php-format
3338
+ msgctxt "1: Name, 2: Title, 3: Sidebar Name"
3339
+ msgid "%1$s widget named \"%2$s\" added to \"%3$s\""
3340
+ msgstr ""
3341
+
3342
+ #: connectors/class-connector-widgets.php:360
3343
+ #, php-format
3344
+ msgctxt "1: Name, 3: Sidebar Name"
3345
+ msgid "%1$s widget added to \"%3$s\""
3346
+ msgstr ""
3347
+
3348
+ #: connectors/class-connector-widgets.php:363
3349
+ #, php-format
3350
+ msgctxt "2: Title, 3: Sidebar Name"
3351
+ msgid "Unknown widget type named \"%2$s\" added to \"%3$s\""
3352
+ msgstr ""
3353
+
3354
+ #: connectors/class-connector-widgets.php:366
3355
+ #, php-format
3356
+ msgctxt "4: Widget ID, 3: Sidebar Name"
3357
+ msgid "%4$s widget added to \"%3$s\""
3358
+ msgstr ""
3359
+
3360
+ #: connectors/class-connector-widgets.php:408
3361
+ #, php-format
3362
+ msgctxt "Sidebar name"
3363
+ msgid "Widgets reordered in \"%s\""
3364
+ msgstr ""
3365
+
3366
+ #: connectors/class-connector-widgets.php:463
3367
+ #, php-format
3368
+ msgctxt "1: Name, 2: Title, 4: Old Sidebar Name, 5: New Sidebar Name"
3369
+ msgid "%1$s widget named \"%2$s\" moved from \"%4$s\" to \"%5$s\""
3370
+ msgstr ""
3371
+
3372
+ #: connectors/class-connector-widgets.php:466
3373
+ #, php-format
3374
+ msgctxt "1: Name, 4: Old Sidebar Name, 5: New Sidebar Name"
3375
+ msgid "%1$s widget moved from \"%4$s\" to \"%5$s\""
3376
+ msgstr ""
3377
+
3378
+ #: connectors/class-connector-widgets.php:469
3379
+ #, php-format
3380
+ msgctxt "2: Title, 4: Old Sidebar Name, 5: New Sidebar Name"
3381
+ msgid "Unknown widget type named \"%2$s\" moved from \"%4$s\" to \"%5$s\""
3382
+ msgstr ""
3383
+
3384
+ #: connectors/class-connector-widgets.php:472
3385
+ #, php-format
3386
+ msgctxt "3: Widget ID, 4: Old Sidebar Name, 5: New Sidebar Name"
3387
+ msgid "%3$s widget moved from \"%4$s\" to \"%5$s\""
3388
+ msgstr ""
3389
+
3390
+ #: connectors/class-connector-widgets.php:585
3391
+ #, php-format
3392
+ msgctxt "1: Name, 2: Title, 3: Sidebar Name"
3393
+ msgid "%1$s widget named \"%2$s\" in \"%3$s\" updated"
3394
+ msgstr ""
3395
+
3396
+ #: connectors/class-connector-widgets.php:588
3397
+ #, php-format
3398
+ msgctxt "1: Name, 3: Sidebar Name"
3399
+ msgid "%1$s widget in \"%3$s\" updated"
3400
+ msgstr ""
3401
+
3402
+ #: connectors/class-connector-widgets.php:591
3403
+ #, php-format
3404
+ msgctxt "2: Title, 3: Sidebar Name"
3405
+ msgid "Unknown widget type named \"%2$s\" in \"%3$s\" updated"
3406
+ msgstr ""
3407
+
3408
+ #: connectors/class-connector-widgets.php:594
3409
+ #, php-format
3410
+ msgctxt "4: Widget ID, 3: Sidebar Name"
3411
+ msgid "%4$s widget in \"%3$s\" updated"
3412
+ msgstr ""
3413
+
3414
+ #: connectors/class-connector-widgets.php:625
3415
+ #, php-format
3416
+ msgctxt "1: Name, 2: Title"
3417
+ msgid "%1$s widget named \"%2$s\" created"
3418
+ msgstr ""
3419
+
3420
+ #: connectors/class-connector-widgets.php:628
3421
+ #, php-format
3422
+ msgctxt "1: Name"
3423
+ msgid "%1$s widget created"
3424
+ msgstr ""
3425
+
3426
+ #: connectors/class-connector-widgets.php:631
3427
+ #, php-format
3428
+ msgctxt "2: Title"
3429
+ msgid "Unknown widget type named \"%2$s\" created"
3430
+ msgstr ""
3431
+
3432
+ #: connectors/class-connector-widgets.php:634
3433
+ #, php-format
3434
+ msgctxt "3: Widget ID"
3435
+ msgid "%3$s widget created"
3436
+ msgstr ""
3437
+
3438
+ #: connectors/class-connector-widgets.php:655
3439
+ #, php-format
3440
+ msgctxt "1: Name, 2: Title"
3441
+ msgid "%1$s widget named \"%2$s\" deleted"
3442
+ msgstr ""
3443
+
3444
+ #: connectors/class-connector-widgets.php:658
3445
+ #, php-format
3446
+ msgctxt "1: Name"
3447
+ msgid "%1$s widget deleted"
3448
+ msgstr ""
3449
+
3450
+ #: connectors/class-connector-widgets.php:661
3451
+ #, php-format
3452
+ msgctxt "2: Title"
3453
+ msgid "Unknown widget type named \"%2$s\" deleted"
3454
+ msgstr ""
3455
+
3456
+ #: connectors/class-connector-widgets.php:664
3457
+ #, php-format
3458
+ msgctxt "3: Widget ID"
3459
+ msgid "%3$s widget deleted"
3460
+ msgstr ""
3461
+
3462
+ #: connectors/class-connector-woocommerce.php:87
3463
+ msgctxt "woocommerce"
3464
+ msgid "WooCommerce"
3465
+ msgstr ""
3466
+
3467
+ #: connectors/class-connector-woocommerce.php:97
3468
+ msgctxt "woocommerce"
3469
+ msgid "Updated"
3470
+ msgstr ""
3471
+
3472
+ #: connectors/class-connector-woocommerce.php:98
3473
+ msgctxt "woocommerce"
3474
+ msgid "Created"
3475
+ msgstr ""
3476
+
3477
+ #: connectors/class-connector-woocommerce.php:99
3478
+ msgctxt "woocommerce"
3479
+ msgid "Trashed"
3480
+ msgstr ""
3481
+
3482
+ #: connectors/class-connector-woocommerce.php:100
3483
+ msgctxt "woocommerce"
3484
+ msgid "Deleted"
3485
+ msgstr ""
3486
+
3487
+ #: connectors/class-connector-woocommerce.php:121
3488
+ msgctxt "woocommerce"
3489
+ msgid "Attributes"
3490
+ msgstr ""
3491
+
3492
+ #: connectors/class-connector-woocommerce.php:141
3493
+ msgid "Frontend Styles"
3494
+ msgstr ""
3495
+
3496
+ #: connectors/class-connector-woocommerce.php:145
3497
+ #: connectors/class-connector-woocommerce.php:152
3498
+ #: connectors/class-connector-woocommerce.php:159
3499
+ #: connectors/class-connector-woocommerce.php:166
3500
+ #: connectors/class-connector-woocommerce.php:173
3501
+ #: connectors/class-connector-woocommerce.php:713
3502
+ msgid "setting"
3503
+ msgstr ""
3504
+
3505
+ #: connectors/class-connector-woocommerce.php:148
3506
+ msgid "Gateway Display Default"
3507
+ msgstr ""
3508
+
3509
+ #: connectors/class-connector-woocommerce.php:155
3510
+ msgid "Gateway Display Order"
3511
+ msgstr ""
3512
+
3513
+ #: connectors/class-connector-woocommerce.php:162
3514
+ msgid "Shipping Methods Default"
3515
+ msgstr ""
3516
+
3517
+ #: connectors/class-connector-woocommerce.php:169
3518
+ msgid "Shipping Methods Order"
3519
+ msgstr ""
3520
+
3521
+ #: connectors/class-connector-woocommerce.php:176
3522
+ msgid "Shipping Debug Mode"
3523
+ msgstr ""
3524
+
3525
+ #: connectors/class-connector-woocommerce.php:180
3526
+ #: connectors/class-connector-woocommerce.php:187
3527
+ #: connectors/class-connector-woocommerce.php:194
3528
+ msgid "tool"
3529
+ msgstr ""
3530
+
3531
+ #: connectors/class-connector-woocommerce.php:183
3532
+ msgid "Template Debug Mode"
3533
+ msgstr ""
3534
+
3535
+ #: connectors/class-connector-woocommerce.php:190
3536
+ msgid "Remove post types on uninstall"
3537
+ msgstr ""
3538
+
3539
+ #: connectors/class-connector-woocommerce.php:231
3540
+ #, php-format
3541
+ msgid "Edit WooCommerce %s"
3542
+ msgstr ""
3543
+
3544
+ #: connectors/class-connector-woocommerce.php:309
3545
+ #, php-format
3546
+ msgctxt "Order title"
3547
+ msgid "%s created"
3548
+ msgstr ""
3549
+
3550
+ #: connectors/class-connector-woocommerce.php:316
3551
+ #, php-format
3552
+ msgctxt "Order title"
3553
+ msgid "%s trashed"
3554
+ msgstr ""
3555
+
3556
+ #: connectors/class-connector-woocommerce.php:323
3557
+ #, php-format
3558
+ msgctxt "Order title"
3559
+ msgid "%s restored from the trash"
3560
+ msgstr ""
3561
+
3562
+ #: connectors/class-connector-woocommerce.php:330
3563
+ #, php-format
3564
+ msgctxt "Order title"
3565
+ msgid "%s updated"
3566
+ msgstr ""
3567
+
3568
+ #: connectors/class-connector-woocommerce.php:341
3569
+ #: connectors/class-connector-woocommerce.php:382
3570
+ #: connectors/class-connector-woocommerce.php:431
3571
+ msgid "Order number"
3572
+ msgstr ""
3573
+
3574
+ #: connectors/class-connector-woocommerce.php:342
3575
+ #: connectors/class-connector-woocommerce.php:383
3576
+ #: connectors/class-connector-woocommerce.php:432
3577
+ msgid "order"
3578
+ msgstr ""
3579
+
3580
+ #: connectors/class-connector-woocommerce.php:387
3581
+ #, php-format
3582
+ msgctxt "Order title"
3583
+ msgid "\"%s\" deleted from trash"
3584
+ msgstr ""
3585
+
3586
+ #: connectors/class-connector-woocommerce.php:425
3587
+ #, php-format
3588
+ msgctxt "1. Order title, 2. Old status, 3. New status"
3589
+ msgid "%1$s status changed from %2$s to %3$s"
3590
+ msgstr ""
3591
+
3592
+ #: connectors/class-connector-woocommerce.php:464
3593
+ #, php-format
3594
+ msgctxt "Term name"
3595
+ msgid "\"%s\" product attribute created"
3596
+ msgstr ""
3597
+
3598
+ #: connectors/class-connector-woocommerce.php:486
3599
+ #, php-format
3600
+ msgctxt "Term name"
3601
+ msgid "\"%s\" product attribute updated"
3602
+ msgstr ""
3603
+
3604
+ #: connectors/class-connector-woocommerce.php:508
3605
+ #, php-format
3606
+ msgctxt "Term name"
3607
+ msgid "\"%s\" product attribute deleted"
3608
+ msgstr ""
3609
+
3610
+ #: connectors/class-connector-woocommerce.php:532
3611
+ #, php-format
3612
+ msgctxt "Tax rate name"
3613
+ msgid "\"%4$s\" tax rate created"
3614
+ msgstr ""
3615
+
3616
+ #: connectors/class-connector-woocommerce.php:554
3617
+ #, php-format
3618
+ msgctxt "Tax rate name"
3619
+ msgid "\"%4$s\" tax rate updated"
3620
+ msgstr ""
3621
+
3622
+ #: connectors/class-connector-woocommerce.php:586
3623
+ #, php-format
3624
+ msgctxt "Tax rate name"
3625
+ msgid "\"%s\" tax rate deleted"
3626
+ msgstr ""
3627
+
3628
+ #: connectors/class-connector-woocommerce.php:642
3629
+ #, php-format
3630
+ msgid "\"%1$s\" %2$s updated"
3631
+ msgstr ""
3632
+
3633
+ #: connectors/class-connector-woocommerce.php:740
3634
+ msgid "payment gateway"
3635
+ msgstr ""
3636
+
3637
+ #: connectors/class-connector-woocommerce.php:759
3638
+ msgid "shipping method"
3639
+ msgstr ""
3640
+
3641
+ #: connectors/class-connector-woocommerce.php:778
3642
+ msgid "email"
3643
+ msgstr ""
3644
+
3645
+ #: connectors/class-connector-woocommerce.php:786
3646
+ msgid "Tools"
3647
+ msgstr ""
3648
+
3649
+ #: connectors/class-connector-wordpress-seo.php:60
3650
+ msgctxt "wordpress-seo"
3651
+ msgid "WordPress SEO"
3652
+ msgstr ""
3653
+
3654
+ #: connectors/class-connector-wordpress-seo.php:70
3655
+ msgctxt "wordpress-seo"
3656
+ msgid "Created"
3657
+ msgstr ""
3658
+
3659
+ #: connectors/class-connector-wordpress-seo.php:71
3660
+ msgctxt "wordpress-seo"
3661
+ msgid "Updated"
3662
+ msgstr ""
3663
+
3664
+ #: connectors/class-connector-wordpress-seo.php:72
3665
+ msgctxt "wordpress-seo"
3666
+ msgid "Added"
3667
+ msgstr ""
3668
+
3669
+ #: connectors/class-connector-wordpress-seo.php:73
3670
+ msgctxt "wordpress-seo"
3671
+ msgid "Deleted"
3672
+ msgstr ""
3673
+
3674
+ #: connectors/class-connector-wordpress-seo.php:74
3675
+ msgctxt "wordpress-seo"
3676
+ msgid "Exported"
3677
+ msgstr ""
3678
+
3679
+ #: connectors/class-connector-wordpress-seo.php:75
3680
+ msgctxt "wordpress-seo"
3681
+ msgid "Imported"
3682
+ msgstr ""
3683
+
3684
+ #: connectors/class-connector-wordpress-seo.php:86
3685
+ msgctxt "wordpress-seo"
3686
+ msgid "Dashboard"
3687
+ msgstr ""
3688
+
3689
+ #: connectors/class-connector-wordpress-seo.php:87
3690
+ msgctxt "wordpress-seo"
3691
+ msgid "Titles &amp; Metas"
3692
+ msgstr ""
3693
+
3694
+ #: connectors/class-connector-wordpress-seo.php:88
3695
+ msgctxt "wordpress-seo"
3696
+ msgid "Social"
3697
+ msgstr ""
3698
+
3699
+ #: connectors/class-connector-wordpress-seo.php:89
3700
+ msgctxt "wordpress-seo"
3701
+ msgid "XML Sitemaps"
3702
+ msgstr ""
3703
+
3704
+ #: connectors/class-connector-wordpress-seo.php:90
3705
+ msgctxt "wordpress-seo"
3706
+ msgid "Permalinks"
3707
+ msgstr ""
3708
+
3709
+ #: connectors/class-connector-wordpress-seo.php:91
3710
+ msgctxt "wordpress-seo"
3711
+ msgid "Internal Links"
3712
+ msgstr ""
3713
+
3714
+ #: connectors/class-connector-wordpress-seo.php:92
3715
+ msgctxt "wordpress-seo"
3716
+ msgid "RSS"
3717
+ msgstr ""
3718
+
3719
+ #: connectors/class-connector-wordpress-seo.php:93
3720
+ msgctxt "wordpress-seo"
3721
+ msgid "Import & Export"
3722
+ msgstr ""
3723
+
3724
+ #: connectors/class-connector-wordpress-seo.php:94
3725
+ msgctxt "wordpress-seo"
3726
+ msgid "Bulk Title Editor"
3727
+ msgstr ""
3728
+
3729
+ #: connectors/class-connector-wordpress-seo.php:95
3730
+ msgctxt "wordpress-seo"
3731
+ msgid "Bulk Description Editor"
3732
+ msgstr ""
3733
+
3734
+ #: connectors/class-connector-wordpress-seo.php:96
3735
+ msgctxt "wordpress-seo"
3736
+ msgid "Files"
3737
+ msgstr ""
3738
+
3739
+ #: connectors/class-connector-wordpress-seo.php:97
3740
+ msgctxt "wordpress-seo"
3741
+ msgid "Content"
3742
+ msgstr ""
3743
+
3744
+ #: connectors/class-connector-wordpress-seo.php:206
3745
+ msgid "HeadSpace2"
3746
+ msgstr ""
3747
+
3748
+ #: connectors/class-connector-wordpress-seo.php:207
3749
+ msgid "All-in-One SEO"
3750
+ msgstr ""
3751
+
3752
+ #: connectors/class-connector-wordpress-seo.php:208
3753
+ msgid "OLD All-in-One SEO"
3754
+ msgstr ""
3755
+
3756
+ #: connectors/class-connector-wordpress-seo.php:209
3757
+ msgid "WooThemes SEO framework"
3758
+ msgstr ""
3759
+
3760
+ #: connectors/class-connector-wordpress-seo.php:210
3761
+ msgid "Robots Meta (by Yoast)"
3762
+ msgstr ""
3763
+
3764
+ #: connectors/class-connector-wordpress-seo.php:211
3765
+ msgid "RSS Footer (by Yoast)"
3766
+ msgstr ""
3767
+
3768
+ #: connectors/class-connector-wordpress-seo.php:212
3769
+ msgid "Yoast Breadcrumbs"
3770
+ msgstr ""
3771
+
3772
+ #: connectors/class-connector-wordpress-seo.php:221
3773
+ #, php-format
3774
+ msgid "Imported settings from %1$s%2$s"
3775
+ msgstr ""
3776
+
3777
+ #: connectors/class-connector-wordpress-seo.php:223
3778
+ msgid ", and deleted old data"
3779
+ msgstr ""
3780
+
3781
+ #: connectors/class-connector-wordpress-seo.php:243
3782
+ #, php-format
3783
+ msgid "Exported settings%s"
3784
+ msgstr ""
3785
+
3786
+ #: connectors/class-connector-wordpress-seo.php:244
3787
+ msgid ", including taxonomy meta"
3788
+ msgstr ""
3789
+
3790
+ #: connectors/class-connector-wordpress-seo.php:256
3791
+ #, php-format
3792
+ msgid "Tried importing settings from \"%s\""
3793
+ msgstr ""
3794
+
3795
+ #: connectors/class-connector-wordpress-seo.php:271
3796
+ msgid "Tried creating robots.txt file"
3797
+ msgstr ""
3798
+
3799
+ #: connectors/class-connector-wordpress-seo.php:273
3800
+ msgid "Tried updating robots.txt file"
3801
+ msgstr ""
3802
+
3803
+ #: connectors/class-connector-wordpress-seo.php:275
3804
+ msgid "Tried updating htaccess file"
3805
+ msgstr ""
3806
+
3807
+ #: connectors/class-connector-wordpress-seo.php:329
3808
+ #, php-format
3809
+ msgid "Updated \"%1$s\" of \"%2$s\" %3$s"
3810
+ msgstr ""
3811
+
3812
+ #: connectors/class-connector-wordpress-seo.php:374
3813
+ #, php-format
3814
+ msgid "%s settings updated"
3815
+ msgstr ""
3816
+
3817
+ #: connectors/class-connector-wordpress-seo.php:390
3818
+ msgctxt "wordpress-seo"
3819
+ msgid "Allow tracking of this WordPress install's anonymous data."
3820
+ msgstr ""
3821
+
3822
+ #: connectors/class-connector-wordpress-seo.php:391
3823
+ msgctxt "wordpress-seo"
3824
+ msgid "Disable the Advanced part of the WordPress SEO meta box"
3825
+ msgstr ""
3826
+
3827
+ #: connectors/class-connector-wordpress-seo.php:392
3828
+ msgctxt "wordpress-seo"
3829
+ msgid "Alexa Verification ID"
3830
+ msgstr ""
3831
+
3832
+ #: connectors/class-connector-wordpress-seo.php:393
3833
+ msgctxt "wordpress-seo"
3834
+ msgid "Bing Webmaster Tools"
3835
+ msgstr ""
3836
+
3837
+ #: connectors/class-connector-wordpress-seo.php:394
3838
+ msgctxt "wordpress-seo"
3839
+ msgid "Google Webmaster Tools"
3840
+ msgstr ""
3841
+
3842
+ #: connectors/class-connector-wordpress-seo.php:395
3843
+ msgctxt "wordpress-seo"
3844
+ msgid "Pinterest"
3845
+ msgstr ""
3846
+
3847
+ #: connectors/class-connector-wordpress-seo.php:396
3848
+ msgctxt "wordpress-seo"
3849
+ msgid "Yandex Webmaster Tools"
3850
+ msgstr ""
3851
+
3852
+ #: connectors/class-connector-wordpress-seo.php:399
3853
+ msgctxt "wordpress-seo"
3854
+ msgid "Enable Breadcrumbs"
3855
+ msgstr ""
3856
+
3857
+ #: connectors/class-connector-wordpress-seo.php:400
3858
+ msgctxt "wordpress-seo"
3859
+ msgid "Separator between breadcrumbs"
3860
+ msgstr ""
3861
+
3862
+ #: connectors/class-connector-wordpress-seo.php:401
3863
+ msgctxt "wordpress-seo"
3864
+ msgid "Anchor text for the Homepage"
3865
+ msgstr ""
3866
+
3867
+ #: connectors/class-connector-wordpress-seo.php:402
3868
+ msgctxt "wordpress-seo"
3869
+ msgid "Prefix for the breadcrumb path"
3870
+ msgstr ""
3871
+
3872
+ #: connectors/class-connector-wordpress-seo.php:403
3873
+ msgctxt "wordpress-seo"
3874
+ msgid "Prefix for Archive breadcrumbs"
3875
+ msgstr ""
3876
+
3877
+ #: connectors/class-connector-wordpress-seo.php:404
3878
+ msgctxt "wordpress-seo"
3879
+ msgid "Prefix for Search Page breadcrumbs"
3880
+ msgstr ""
3881
+
3882
+ #: connectors/class-connector-wordpress-seo.php:405
3883
+ msgctxt "wordpress-seo"
3884
+ msgid "Breadcrumb for 404 Page"
3885
+ msgstr ""
3886
+
3887
+ #: connectors/class-connector-wordpress-seo.php:406
3888
+ msgctxt "wordpress-seo"
3889
+ msgid "Remove Blog page from Breadcrumbs"
3890
+ msgstr ""
3891
+
3892
+ #: connectors/class-connector-wordpress-seo.php:407
3893
+ msgctxt "wordpress-seo"
3894
+ msgid "Bold the last page in the breadcrumb"
3895
+ msgstr ""
3896
+
3897
+ #: connectors/class-connector-wordpress-seo.php:410
3898
+ msgctxt "wordpress-seo"
3899
+ msgid "Force rewrite titles"
3900
+ msgstr ""
3901
+
3902
+ #: connectors/class-connector-wordpress-seo.php:411
3903
+ msgctxt "wordpress-seo"
3904
+ msgid "Noindex subpages of archives"
3905
+ msgstr ""
3906
+
3907
+ #: connectors/class-connector-wordpress-seo.php:412
3908
+ msgctxt "wordpress-seo"
3909
+ msgid "Use <code>meta</code> keywords tag?"
3910
+ msgstr ""
3911
+
3912
+ #: connectors/class-connector-wordpress-seo.php:413
3913
+ msgctxt "wordpress-seo"
3914
+ msgid "Add <code>noodp</code> meta robots tag sitewide"
3915
+ msgstr ""
3916
+
3917
+ #: connectors/class-connector-wordpress-seo.php:414
3918
+ msgctxt "wordpress-seo"
3919
+ msgid "Add <code>noydir</code> meta robots tag sitewide"
3920
+ msgstr ""
3921
+
3922
+ #: connectors/class-connector-wordpress-seo.php:415
3923
+ msgctxt "wordpress-seo"
3924
+ msgid "Hide RSD Links"
3925
+ msgstr ""
3926
+
3927
+ #: connectors/class-connector-wordpress-seo.php:416
3928
+ msgctxt "wordpress-seo"
3929
+ msgid "Hide WLW Manifest Links"
3930
+ msgstr ""
3931
+
3932
+ #: connectors/class-connector-wordpress-seo.php:417
3933
+ msgctxt "wordpress-seo"
3934
+ msgid "Hide Shortlink for posts"
3935
+ msgstr ""
3936
+
3937
+ #: connectors/class-connector-wordpress-seo.php:418
3938
+ msgctxt "wordpress-seo"
3939
+ msgid "Hide RSS Links"
3940
+ msgstr ""
3941
+
3942
+ #: connectors/class-connector-wordpress-seo.php:419
3943
+ msgctxt "wordpress-seo"
3944
+ msgid "Disable the author archives"
3945
+ msgstr ""
3946
+
3947
+ #: connectors/class-connector-wordpress-seo.php:420
3948
+ msgctxt "wordpress-seo"
3949
+ msgid "Disable the date-based archives"
3950
+ msgstr ""
3951
+
3952
+ #: connectors/class-connector-wordpress-seo.php:423
3953
+ msgctxt "wordpress-seo"
3954
+ msgid "Who should have access to the WordPress SEO settings"
3955
+ msgstr ""
3956
+
3957
+ #: connectors/class-connector-wordpress-seo.php:424
3958
+ msgctxt "wordpress-seo"
3959
+ msgid "New blogs get the SEO settings from this blog"
3960
+ msgstr ""
3961
+
3962
+ #: connectors/class-connector-wordpress-seo.php:425
3963
+ msgctxt "wordpress-seo"
3964
+ msgid "Blog ID"
3965
+ msgstr ""
3966
+
3967
+ #: connectors/class-connector-wordpress-seo.php:428
3968
+ msgctxt "wordpress-seo"
3969
+ msgid ""
3970
+ "Strip the category base (usually <code>/category/</code>) from the category "
3971
+ "URL."
3972
+ msgstr ""
3973
+
3974
+ #: connectors/class-connector-wordpress-seo.php:429
3975
+ msgctxt "wordpress-seo"
3976
+ msgid "Enforce a trailing slash on all category and tag URL's"
3977
+ msgstr ""
3978
+
3979
+ #: connectors/class-connector-wordpress-seo.php:430
3980
+ msgctxt "wordpress-seo"
3981
+ msgid "Remove stop words from slugs."
3982
+ msgstr ""
3983
+
3984
+ #: connectors/class-connector-wordpress-seo.php:431
3985
+ msgctxt "wordpress-seo"
3986
+ msgid "Redirect attachment URL's to parent post URL."
3987
+ msgstr ""
3988
+
3989
+ #: connectors/class-connector-wordpress-seo.php:432
3990
+ msgctxt "wordpress-seo"
3991
+ msgid "Remove the <code>?replytocom</code> variables."
3992
+ msgstr ""
3993
+
3994
+ #: connectors/class-connector-wordpress-seo.php:433
3995
+ msgctxt "wordpress-seo"
3996
+ msgid ""
3997
+ "Redirect ugly URL's to clean permalinks. (Not recommended in many cases!)"
3998
+ msgstr ""
3999
+
4000
+ #: connectors/class-connector-wordpress-seo.php:434
4001
+ msgctxt "wordpress-seo"
4002
+ msgid "Force Transport"
4003
+ msgstr ""
4004
+
4005
+ #: connectors/class-connector-wordpress-seo.php:435
4006
+ msgctxt "wordpress-seo"
4007
+ msgid "Prevent cleaning out Google Site Search URL's."
4008
+ msgstr ""
4009
+
4010
+ #: connectors/class-connector-wordpress-seo.php:436
4011
+ msgctxt "wordpress-seo"
4012
+ msgid ""
4013
+ "Prevent cleaning out Google Analytics Campaign & Google AdWords Parameters."
4014
+ msgstr ""
4015
+
4016
+ #: connectors/class-connector-wordpress-seo.php:437
4017
+ msgctxt "wordpress-seo"
4018
+ msgid "Other variables not to clean"
4019
+ msgstr ""
4020
+
4021
+ #: connectors/class-connector-wordpress-seo.php:440
4022
+ msgctxt "wordpress-seo"
4023
+ msgid "Add Open Graph meta data"
4024
+ msgstr ""
4025
+
4026
+ #: connectors/class-connector-wordpress-seo.php:441
4027
+ msgctxt "wordpress-seo"
4028
+ msgid "Facebook Page URL"
4029
+ msgstr ""
4030
+
4031
+ #: connectors/class-connector-wordpress-seo.php:442
4032
+ #: connectors/class-connector-wordpress-seo.php:444
4033
+ msgctxt "wordpress-seo"
4034
+ msgid "Image URL"
4035
+ msgstr ""
4036
+
4037
+ #: connectors/class-connector-wordpress-seo.php:443
4038
+ msgctxt "wordpress-seo"
4039
+ msgid "Description"
4040
+ msgstr ""
4041
+
4042
+ #: connectors/class-connector-wordpress-seo.php:445
4043
+ msgctxt "wordpress-seo"
4044
+ msgid "Add Twitter card meta data"
4045
+ msgstr ""
4046
+
4047
+ #: connectors/class-connector-wordpress-seo.php:446
4048
+ msgctxt "wordpress-seo"
4049
+ msgid "Site Twitter Username"
4050
+ msgstr ""
4051
+
4052
+ #: connectors/class-connector-wordpress-seo.php:447
4053
+ msgctxt "wordpress-seo"
4054
+ msgid "The default card type to use"
4055
+ msgstr ""
4056
+
4057
+ #: connectors/class-connector-wordpress-seo.php:448
4058
+ msgctxt "wordpress-seo"
4059
+ msgid "Add Google+ specific post meta data (excluding author metadata)"
4060
+ msgstr ""
4061
+
4062
+ #: connectors/class-connector-wordpress-seo.php:449
4063
+ msgctxt "wordpress-seo"
4064
+ msgid "Google Publisher Page"
4065
+ msgstr ""
4066
+
4067
+ #: connectors/class-connector-wordpress-seo.php:452
4068
+ msgctxt "wordpress-seo"
4069
+ msgid "Check this box to enable XML sitemap functionality."
4070
+ msgstr ""
4071
+
4072
+ #: connectors/class-connector-wordpress-seo.php:453
4073
+ msgctxt "wordpress-seo"
4074
+ msgid "Disable author/user sitemap"
4075
+ msgstr ""
4076
+
4077
+ #: connectors/class-connector-wordpress-seo.php:454
4078
+ msgctxt "wordpress-seo"
4079
+ msgid "Ping Yahoo!"
4080
+ msgstr ""
4081
+
4082
+ #: connectors/class-connector-wordpress-seo.php:455
4083
+ msgctxt "wordpress-seo"
4084
+ msgid "Ping Ask.com"
4085
+ msgstr ""
4086
+
4087
+ #: connectors/class-connector-wordpress-seo.php:456
4088
+ msgctxt "wordpress-seo"
4089
+ msgid "Max entries per sitemap page"
4090
+ msgstr ""
4091
+
4092
+ #: connectors/class-connector-wordpress-seo.php:459
4093
+ msgctxt "wordpress-seo"
4094
+ msgid "Content to put before each post in the feed"
4095
+ msgstr ""
4096
+
4097
+ #: connectors/class-connector-wordpress-seo.php:460
4098
+ msgctxt "wordpress-seo"
4099
+ msgid "Content to put after each post"
4100
+ msgstr ""
4101
+
4102
+ #: connectors/class-connector-wordpress-seo.php:464
4103
+ msgctxt "wordpress-seo"
4104
+ msgid "Title template"
4105
+ msgstr ""
4106
+
4107
+ #: connectors/class-connector-wordpress-seo.php:465
4108
+ msgctxt "wordpress-seo"
4109
+ msgid "Meta description template"
4110
+ msgstr ""
4111
+
4112
+ #: connectors/class-connector-wordpress-seo.php:466
4113
+ msgctxt "wordpress-seo"
4114
+ msgid "Meta keywords template"
4115
+ msgstr ""
4116
+
4117
+ #: connectors/class-connector-wordpress-seo.php:467
4118
+ msgctxt "wordpress-seo"
4119
+ msgid "Meta Robots"
4120
+ msgstr ""
4121
+
4122
+ #: connectors/class-connector-wordpress-seo.php:468
4123
+ msgctxt "wordpress-seo"
4124
+ msgid "Authorship"
4125
+ msgstr ""
4126
+
4127
+ #: connectors/class-connector-wordpress-seo.php:469
4128
+ msgctxt "wordpress-seo"
4129
+ msgid "Show date in snippet preview?"
4130
+ msgstr ""
4131
+
4132
+ #: connectors/class-connector-wordpress-seo.php:470
4133
+ msgctxt "wordpress-seo"
4134
+ msgid "WordPress SEO Meta Box"
4135
+ msgstr ""
4136
+
4137
+ #: connectors/class-connector-wordpress-seo.php:471
4138
+ msgctxt "wordpress-seo"
4139
+ msgid "Breadcrumbs Title"
4140
+ msgstr ""
4141
+
4142
+ #: connectors/class-connector-wordpress-seo.php:472
4143
+ msgctxt "wordpress-seo"
4144
+ msgid "Post types"
4145
+ msgstr ""
4146
+
4147
+ #: connectors/class-connector-wordpress-seo.php:473
4148
+ msgctxt "wordpress-seo"
4149
+ msgid "Taxonomies"
4150
+ msgstr ""
4151
+
4152
+ #: includes/feeds/atom.php:7 includes/feeds/rss-2.0.php:21
4153
+ msgid "Stream Feed"
4154
+ msgstr ""
phpcs.ruleset.xml ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0"?>
2
+
3
+ <ruleset name="WordPress Coding Standards for WP Stream">
4
+
5
+ <rule ref="WordPress-Extra" />
6
+
7
+ <exclude-pattern>*/tests/*</exclude-pattern>
8
+ <exclude-pattern>*/vendor/*</exclude-pattern>
9
+ <exclude-pattern>*/dev-lib/*</exclude-pattern>
10
+
11
+ </ruleset>
readme.md ADDED
@@ -0,0 +1,485 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!-- DO NOT EDIT THIS FILE; it is auto-generated from readme.txt -->
2
+ # Stream
3
+
4
+ ![Banner](assets/banner-1544x500.png)
5
+ Stream is the easiest and safest way to track content changes happening to your WordPress site and then view them in beautifully organized detail.
6
+
7
+ **Contributors:** [fjarrett](https://profiles.wordpress.org/fjarrett), [lukecarbis](https://profiles.wordpress.org/lukecarbis), [shadyvb](https://profiles.wordpress.org/shadyvb), [westonruter](https://profiles.wordpress.org/westonruter), [stream](https://profiles.wordpress.org/stream), [xwp](https://profiles.wordpress.org/xwp)
8
+ **Tags:** [actions](https://wordpress.org/plugins/tags/actions), [activity](https://wordpress.org/plugins/tags/activity), [admin](https://wordpress.org/plugins/tags/admin), [analytics](https://wordpress.org/plugins/tags/analytics), [dashboard](https://wordpress.org/plugins/tags/dashboard), [log](https://wordpress.org/plugins/tags/log), [notification](https://wordpress.org/plugins/tags/notification), [security](https://wordpress.org/plugins/tags/security), [stream](https://wordpress.org/plugins/tags/stream), [users](https://wordpress.org/plugins/tags/users)
9
+ **Requires at least:** 3.7
10
+ **Tested up to:** 4.2
11
+ **Stable tag:** 3.0
12
+ **License:** [GPLv2 or later](https://www.gnu.org/licenses/gpl-2.0.html)
13
+
14
+ [![Build Status](https://travis-ci.org/xwp/stream.png?branch=master)](https://travis-ci.org/xwp/stream) [![Join the chat at https://gitter.im/xwp/stream](https://badges.gitter.im/Joinhat.svg)](https://gitter.im/xwp/stream)
15
+
16
+ ## Description ##
17
+
18
+ [![Play video on YouTube](https://i1.ytimg.com/vi/t_qD4Sp4E70/hqdefault.jpg)](https://www.youtube.com/watch?v=t_qD4Sp4E70)
19
+
20
+ Never be in the dark about WP Admin activity again.
21
+
22
+ Stream allows you to know exactly when changes to your site have been made, and more importantly, who did them.
23
+
24
+ Every logged-in user action is displayed in an activity stream and organized for easy filtering by User, Role, Context, Action and IP address.
25
+
26
+ **Built-In Tracking Integrations For Popular Plugins:**
27
+
28
+ * [Advanced Custom Fields](https://wp-stream.com/connectors/acf/)
29
+ * [bbPress](https://wp-stream.com/connectors/bbpress/)
30
+ * [BuddyPress](https://wp-stream.com/connectors/buddypress/)
31
+ * [Easy Digital Downloads](https://wp-stream.com/connectors/edd/)
32
+ * [Gravity Forms](https://wp-stream.com/connectors/gravity-forms/)
33
+ * [Jetpack](https://wp-stream.com/connectors/jetpack/)
34
+ * [WooCommerce](https://wp-stream.com/connectors/woocommerce/)
35
+ * [WordPress SEO by Yoast](https://wp-stream.com/connectors/wordpress-seo/)
36
+
37
+ **Built-In Tracking For Core Actions:**
38
+
39
+ * Posts
40
+ * Pages
41
+ * Custom Post Types
42
+ * Users
43
+ * Themes
44
+ * Plugins
45
+ * Tags
46
+ * Categories
47
+ * Custom Taxonomies
48
+ * Settings
49
+ * Custom Backgrounds
50
+ * Custom Headers
51
+ * Menus
52
+ * Media Library
53
+ * Widgets
54
+ * Comments
55
+ * Theme Editor
56
+ * WordPress Core Updates
57
+
58
+ **Other Noteworthy Features:**
59
+
60
+ * Multisite view of all activity records on a network
61
+ * Limit who can view user activity records by user role
62
+ * Set exclude rules to ignore certain kinds of user activity
63
+ * Live update of user activity records in the Stream
64
+ * Support for IPv6 addresses
65
+ * WP-CLI command for querying records
66
+
67
+ **Languages Supported:**
68
+
69
+ * English
70
+ * French (France)
71
+ * German
72
+ * Indonesian
73
+ * Polish
74
+ * Portuguese (Brazil)
75
+ * Spanish (Spain)
76
+
77
+ **See room for improvement?**
78
+
79
+ Great! There are several ways you can get involved to help make Stream better:
80
+
81
+ 1. **Report Bugs:** If you find a bug, error or other problem, please report it! You can do this by [creating a new topic](https://wordpress.org/support/plugin/stream) in the plugin forum. Once a developer can verify the bug by reproducing it, they will create an official bug report in GitHub where the bug will be worked on.
82
+ 2. **Suggest New Features:** Have an awesome idea? Please share it! Simply [create a new topic](https://wordpress.org/support/plugin/stream) in the plugin forum to express your thoughts on why the feature should be included and get a discussion going around your idea.
83
+ 3. **Issue Pull Requests:** If you're a developer, the easiest way to get involved is to help out on [issues already reported](https://github.com/x-team/wp-stream/issues) in GitHub. Be sure to check out the [contributing guide](https://github.com/x-team/wp-stream/blob/master/contributing.md) for developers.
84
+
85
+ Thank you for wanting to make Stream better for everyone! We salute you.
86
+
87
+ ## Screenshots ##
88
+
89
+ ### Every logged-in user action is displayed in an activity stream and organized for easy filtering and searching.
90
+
91
+ ![Every logged-in user action is displayed in an activity stream and organized for easy filtering and searching.](assets/screenshot-1.png)
92
+
93
+ ### Enable live updates in Screen Options to watch your site activity appear in near real-time.
94
+
95
+ ![Enable live updates in Screen Options to watch your site activity appear in near real-time.](assets/screenshot-2.png)
96
+
97
+ ### Create rules for excluding certain kinds of records from appearing in Stream.
98
+
99
+ ![Create rules for excluding certain kinds of records from appearing in Stream.](assets/screenshot-3.png)
100
+
101
+ ### Build notification rules to be alerted when important changes are made on your site.
102
+
103
+ ![Build notification rules to be alerted when important changes are made on your site.](assets/screenshot-4.png)
104
+
105
+ ### Generate stunning visuals of logged-in user activity and share them with stakeholders or your clients.
106
+
107
+ ![Generate stunning visuals of logged-in user activity and share them with stakeholders or your clients.](assets/screenshot-5.png)
108
+
109
+ ## Changelog ##
110
+
111
+ ### 3.0.0 - August 25, 2015 ###
112
+ * New: Activity logs are now stored locally in WordPress. No data is sent externally and no registration required.
113
+ * New: Migration process for Stream 2 users to move records out of the cloud, and into your local database.
114
+ * New: Various measures and database schema changes to improve Stream's performance.
115
+ * Removed: Notifications and Reports have been removed to be reworked for an upcoming release.
116
+
117
+ Props [@fjarrett](https://github.com/fjarrett), [@lukecarbis](https://github.com/lukecarbis)
118
+
119
+ ### 2.0.5 - April 23, 2015 ###
120
+ * Tweak: Compatibility with split terms introduced in WordPress 4.2 ([#702](https://github.com/wp-stream/stream/issues/702))
121
+ * Tweak: Add support for future and pending post transitions ([#716](https://github.com/wp-stream/stream/pull/716))
122
+ * Tweak: Match new default admin colors introduced in WordPress 4.2 ([#718](https://github.com/wp-stream/stream/pull/718))
123
+ * Fix: Compatibility issues with WP-Cron Control plugin and system crons ([#715](https://github.com/wp-stream/stream/issues/715))
124
+ * Fix: Broken date range filter on Reports screen ([#717](https://github.com/wp-stream/stream/pull/717))
125
+
126
+ Props [@fjarrett](https://github.com/fjarrett)
127
+
128
+ ### 2.0.4 - April 16, 2015 ###
129
+ * New: Add reset button to reset search filters ([#144](https://github.com/wp-stream/stream/issues/144))
130
+ * Tweak: WP-CLI command output improvements via `--format` option for table view, JSON and CSV ([#705](https://github.com/wp-stream/stream/pull/705))
131
+ * Tweak: Add link to https://wp-stream.com in README ([#709](https://github.com/wp-stream/stream/issues/709))
132
+ * Tweak: Better highlighting on multiple live update rows
133
+ * Tweak: Limit custom range datepickers based on the Stream plan type
134
+ * Tweak: Limit legacy record migrations based on the Stream plan type
135
+ * Fix: Allow properties with values of zero to be included in queries ([#698](https://github.com/wp-stream/stream/issues/698))
136
+ * Fix: Properly return record success/failure in log and store methods ([#711](https://github.com/wp-stream/stream/issues/711))
137
+
138
+ Props [@fjarrett](https://github.com/fjarrett), [@szepeviktor](https://github.com/szepeviktor)
139
+
140
+ ### 2.0.3 - January 23, 2015 ###
141
+ * New: WP-CLI command now available for querying records via the command line ([#499](https://github.com/wp-stream/stream/issues/499))
142
+ * Tweak: Silently disable Stream during content import ([#672](https://github.com/wp-stream/stream/issues/672))
143
+ * Tweak: Search results now ordered by date instead of relevance ([#689](https://github.com/wp-stream/stream/issues/689))
144
+ * Fix: Handle boolean values appropriately during wp_stream_log_data filter ([#680](https://github.com/wp-stream/stream/issues/680))
145
+ * Fix: Hook into external class load methods on init rather than plugins_loaded ([#686](https://github.com/wp-stream/stream/issues/686))
146
+ * Fix: N/A user not working in exclude rules ([#688](https://github.com/wp-stream/stream/issues/688))
147
+ * Fix: Prevent Notification Rule meta from being saved to all post types ([#693](https://github.com/wp-stream/stream/issues/693))
148
+ * Fix: PHP warning shown for some users when deleting plugins ([#695](https://github.com/wp-stream/stream/issues/695))
149
+
150
+ Props [@fjarrett](https://github.com/fjarrett)
151
+
152
+ ### 2.0.2 - January 15, 2015 ###
153
+ * New: Full record backtrace now available to developers for debugging ([#467](https://github.com/wp-stream/stream/issues/467))
154
+ * New: Unread count badge added to Stream menu, opt-out available in User Profile ([#588](https://github.com/wp-stream/stream/issues/588))
155
+ * New: Stream connector to track Stream-specific contexts and actions ([#622](https://github.com/wp-stream/stream/issues/622))
156
+ * Tweak: Inherit role access from Stream Settings for Notifications and Reports ([#641](https://github.com/wp-stream/stream/issues/641))
157
+ * Tweak: Opt-in required for Akismet tracking ([#649](https://github.com/wp-stream/stream/issues/649))
158
+ * Tweak: Ignore comments deleted when deleting parent post ([#652](https://github.com/wp-stream/stream/issues/652))
159
+ * Tweak: Opt-in required for comment flood tracking ([#656](https://github.com/wp-stream/stream/issues/656))
160
+ * Tweak: Opt-in required for WP Cron tracking ([#673](https://github.com/wp-stream/stream/issues/673))
161
+ * Fix: Post revision action link pointing to wrong revision ID ([#585](https://github.com/wp-stream/stream/issues/585))
162
+ * Fix: PHP warnings caused by Menu connector ([#663](https://github.com/wp-stream/stream/issues/663))
163
+ * Fix: Non-static method called statically in WPSEO connector ([#668](https://github.com/wp-stream/stream/issues/668))
164
+ * Fix: Prevent live updates from tampering with filtered results ([#675](https://github.com/wp-stream/stream/issues/675))
165
+
166
+ Props [@fjarrett](https://github.com/fjarrett), [@lukecarbis](https://github.com/lukecarbis), [@shadyvb](https://github.com/shadyvb), [@jonathanbardo](https://github.com/jonathanbardo), [@westonruter](https://github.com/westonruter)
167
+
168
+ ### 2.0.1 - September 30, 2014 ###
169
+ * Tweak: Improved localisation strings throughout the plugin ([#644](https://github.com/wp-stream/stream/pull/644))
170
+ * Tweak: Improved tooltip text explaining WP.com sign in
171
+ * Fix: ACF Pro doesn't save custom field values when Stream enabled ([#642](https://github.com/wp-stream/stream/issues/642))
172
+
173
+ Props [@lukecarbis](https://github.com/lukecarbis), [@fjarrett](https://github.com/fjarrett)
174
+
175
+ ### 2.0.0 - September 27, 2014 ###
176
+ * All activity is now stored only in the cloud over SSL, local MySQL storage dependence is over!
177
+ * Connector and Context have merged in the UI, now just called Contexts
178
+ * The Exclude Rules UI has been completely revamped
179
+ * Notifications and Reports are now conveniently built into Stream for Pro subscribers
180
+ * Connectors for tracking other popular plugins are now built into Stream, like BuddyPress, Jetpack, Gravity Forms, and more...
181
+ * You create an account for Stream simply by signing in with your WordPress.com ID
182
+
183
+ **NOTE:** Multisite view of all activity records in the Network Admin has been removed in this release. If you require this feature, please do not update Stream until version 2.1.0 is released.
184
+
185
+ Props [@fjarrett](https://github.com/fjarrett), [@lukecarbis](https://github.com/lukecarbis), [@shadyvb](https://github.com/shadyvb), [@chacha](https://github.com/chacha), [@jonathanbardo](https://github.com/jonathanbardo), [@bordoni](https://github.com/bordoni), [@dero](https://github.com/dero), [@jeffmatson](https://github.com/jeffmatson), [@stipsan](https://github.com/stipsan), [@c3mdigital](https://github.com/c3mdigital), [@adamsilverstein](https://github.com/adamsilverstein), [@westonruter](https://github.com/westonruter), [@japh](https://github.com/japh), [@solace](https://github.com/solace), [@johnbillion](https://github.com/johnbillion)
186
+
187
+ ### 1.4.9 - July 23, 2014 ###
188
+ * Fix: Revert delayed log mechanism for post transition ([#585](https://github.com/x-team/wp-stream/issues/585))
189
+ * Fix: Revert usage of get_taxonomy() ([#586](https://github.com/x-team/wp-stream/pull/586))
190
+ * Fix: Notices not firing on correct action ([#589](https://github.com/x-team/wp-stream/issues/589))
191
+
192
+ Props [@shadyvb](https://github.com/shadyvb), [@fjarrett](https://github.com/fjarrett)
193
+
194
+ ### 1.4.8 - July 18, 2014 ###
195
+ * New: Greatly improved widget tracking, including changes performed in Customizer ([#391](https://github.com/x-team/wp-stream/pull/391))
196
+ * New: Now tracking when Akismet automatically marks comments as spam ([#587](https://github.com/x-team/wp-stream/pull/587))
197
+ * Tweak: Log WP-CLI details to Stream author meta ([#470](https://github.com/x-team/wp-stream/issues/470))
198
+ * Tweak: Track changes to options more deeply ([#573](https://github.com/x-team/wp-stream/pull/573))
199
+ * Fix: Labels not seen for CPT registered on init with default priority ([#565](https://github.com/x-team/wp-stream/issues/565))
200
+ * Fix: Stream menu appearing in Network menu when not network activated ([#582](https://github.com/x-team/wp-stream/issues/582))
201
+ * Fix: Post Revision ID associated to record is not the most recent one ([#585](https://github.com/x-team/wp-stream/issues/585))
202
+ * Fix: Incorrect action label for comment throttling ([#591](https://github.com/x-team/wp-stream/issues/591))
203
+
204
+ Props [@westonruter](https://github.com/westonruter), [@fjarrett](https://github.com/fjarrett), [@shadyvb](https://github.com/shadyvb), [@lukecarbis](https://github.com/lukecarbis), [@chacha](https://github.com/chacha)
205
+
206
+ ### 1.4.7 - June 27, 2014 ###
207
+ * New: Comment Type support added to the Comments connector ([#558](https://github.com/x-team/wp-stream/issues/558))
208
+ * Fix: Datepicker opens again with each paged view ([#568](https://github.com/x-team/wp-stream/issues/568))
209
+ * Fix: PHP warning when deleting network users ([#579](https://github.com/x-team/wp-stream/issues/579))
210
+ * Fix: Track user count setting changes ([#583](https://github.com/x-team/wp-stream/issues/583))
211
+ * Fix: .po and .pot files out-of-date for translators ([#584](https://github.com/x-team/wp-stream/issues/584))
212
+
213
+ Props [@lukecarbis](https://github.com/lukecarbis), [@fjarrett](https://github.com/fjarrett), [@bordoni](https://github.com/bordoni), [@shadyvb](https://github.com/shadyvb)
214
+
215
+ ### 1.4.6 - May 30, 2014 ###
216
+ * Tweak: Actions provided for trashed posts are irrelevant ([#523](https://github.com/x-team/wp-stream/issues/523))
217
+ * Tweak: Use core language pack translations where possible ([#534](https://github.com/x-team/wp-stream/issues/534))
218
+ * Tweak: Consolidate show filter and show column screen options ([#542](https://github.com/x-team/wp-stream/issues/542))
219
+ * Tweak: Stop tracking failed login attempts ([#547](https://github.com/x-team/wp-stream/issues/547))
220
+ * Tweak: Remove all uses of extract() from Stream ([#556](https://github.com/x-team/wp-stream/issues/556))
221
+ * Fix: Excluding roles is not handled properly ([#527](https://github.com/x-team/wp-stream/issues/527))
222
+ * Fix: Stream runs install routine twice ([#528](https://github.com/x-team/wp-stream/issues/528))
223
+ * Fix: Widget records show sidebar slug instead of label ([#531](https://github.com/x-team/wp-stream/issues/531))
224
+ * Fix: Fatal error when PHP version is less than 5.3 ([#538](https://github.com/x-team/wp-stream/issues/538))
225
+ * Fix: Cannot exclude Custom Background context ([#543](https://github.com/x-team/wp-stream/issues/543))
226
+ * Fix: Conflict with Jetpack body class in WP Admin ([#545](https://github.com/x-team/wp-stream/issues/545))
227
+ * Fix: Stream settings exclude error for big wp_users table ([#551](https://github.com/x-team/wp-stream/issues/551))
228
+
229
+ Props [@fjarrett](https://github.com/fjarrett), [@lukecarbis](https://github.com/lukecarbis), [@shadyvb](https://github.com/shadyvb), [@barryceelen](https://github.com/barryceelen), [@japh](https://github.com/japh)
230
+
231
+ ### 1.4.5 - May 15, 2014 ###
232
+ * New: Lightweight frontend indicator for sites using Stream ([#507](https://github.com/x-team/wp-stream/issues/507))
233
+ * Tweak: Add filterable method for excluded comment types ([#487](https://github.com/x-team/wp-stream/issues/487))
234
+ * Tweak: Rename "ID" column label to "Record ID" ([#490](https://github.com/x-team/wp-stream/issues/490))
235
+ * Tweak: One admin notice for any missing DB tables ([#506](https://github.com/x-team/wp-stream/pull/506))
236
+ * Fix: Custom authentication schemes not tracking user logins correctly ([#434](https://github.com/x-team/wp-stream/issues/434))
237
+ * Fix: Taxonomy connector conflicts with Edit Flow plugin ([#498](https://github.com/x-team/wp-stream/issues/498))
238
+ * Fix: Switching user is incorrectly tracked ([#501](https://github.com/x-team/wp-stream/issues/501))
239
+ * Fix: Extension activation links broken when plugin folders are renamed ([#502](https://github.com/x-team/wp-stream/issues/502))
240
+ * Fix: Author info showing up incorrectly ([#505](https://github.com/x-team/wp-stream/issues/505))
241
+ * Fix: Incompatibility with multi-server environments ([#517](https://github.com/x-team/wp-stream/issues/517))
242
+ * Fix: Warnings seen when Show Avatars is disabled ([#518](https://github.com/x-team/wp-stream/issues/518))
243
+ * Fix: Notices for non-existent extension data after timeout ([#529](https://github.com/x-team/wp-stream/pull/529))
244
+
245
+ Props [@fjarrett](https://github.com/fjarrett), [@shadyvb](https://github.com/shadyvb), [@lukecarbis](https://github.com/lukecarbis), [@japh](https://github.com/japh)
246
+
247
+ ### 1.4.4 - May 6, 2014 ###
248
+ * New: Admin pointers to highlight when new admin screens are introduced ([#466](https://github.com/x-team/wp-stream/issues/466))
249
+ * Tweak: Filter introduced to allow the Stream admin menu position to be changed ([#99](https://github.com/x-team/wp-stream/issues/99))
250
+ * Tweak: Provide option label for records that show when the Stream database has updated ([#444](https://github.com/x-team/wp-stream/pull/444))
251
+ * Tweak: Better handling of authors in the list table ([#448](https://github.com/x-team/wp-stream/pull/448))
252
+ * Tweak: Way for developers to set their Stream Extensions affiliate ID on links from the Extensions screen ([#482](https://github.com/x-team/wp-stream/issues/482))
253
+ * Fix: Extensions screen CSS bug in Firefox ([#464](https://github.com/x-team/wp-stream/issues/464))
254
+ * Fix: Error when installing extensions from the Network Admin ([#491](https://github.com/x-team/wp-stream/issues/491))
255
+ * Fix: Undefined notice in admin.php ([#468](https://github.com/x-team/wp-stream/issues/468))
256
+
257
+ Props [@westonruter](https://github.com/westonruter), [@fjarrett](https://github.com/fjarrett), [@japh](https://github.com/japh), [@lukecarbis](https://github.com/lukecarbis), [@jonathanbardo](https://github.com/jonathanbardo), [@bordoni](https://github.com/bordoni)
258
+
259
+ ### 1.4.3 - April 26, 2014 ###
260
+ * New: Introducing the Stream Extensions screen! ([#396](https://github.com/x-team/wp-stream/issues/396))
261
+
262
+ Props [@jonathanbardo](https://github.com/jonathanbardo), [@lukecarbis](https://github.com/lukecarbis), [@shadyvb](https://github.com/shadyvb), [@c3mdigital](https://github.com/c3mdigital), [@fjarrett](https://github.com/fjarrett)
263
+
264
+ ### 1.4.2 - April 24, 2014 ###
265
+ * Fix: Update Database button redirecting to previous screen ([#443](https://github.com/x-team/wp-stream/issues/443))
266
+ * Fix: Update routine hotfix that was causing records to disappear ([#447](https://github.com/x-team/wp-stream/issues/447))
267
+
268
+ Props [@jonathanbardo](https://github.com/jonathanbardo), [@lukecarbis](https://github.com/lukecarbis), [@westonruter](https://github.com/westonruter), [@fjarrett](https://github.com/fjarrett)
269
+
270
+ ### 1.4.1 - April 24, 2014 ###
271
+ * Fix: Scripts and styles not using Stream version number ([#440](https://github.com/x-team/wp-stream/issues/440))
272
+ * Fix: WP-CLI incorrectly referenced in records ([#441](https://github.com/x-team/wp-stream/issues/441))
273
+
274
+ Props [@westonruter](https://github.com/westonruter), [@fjarrett](https://github.com/fjarrett)
275
+
276
+ ### 1.4.0 - April 24, 2014 ###
277
+ * New: Multisite is now fully supported, activate Stream network-wide ([#65](https://github.com/x-team/wp-stream/issues/65))
278
+ * New: Separate API for handling DB update routines ([#379](https://github.com/x-team/wp-stream/issues/379))
279
+ * New: WP-CLI compatibility, Stream now tracks changes made via WP-CLI ([#423](https://github.com/x-team/wp-stream/issues/423))
280
+ * Tweak: Deprecate functions and hooks in favor of consistent naming conventions ([#267](https://github.com/x-team/wp-stream/issues/267))
281
+ * Tweak: Use icon link instead of clicking the summary to filter by object ID ([#380](https://github.com/x-team/wp-stream/issues/380))
282
+ * Tweak: Save additional author meta for better records ([#389](https://github.com/x-team/wp-stream/issues/389))
283
+ * Tweak: More compact search filters for smaller screens ([#403](https://github.com/x-team/wp-stream/issues/403))
284
+ * Fix: Fix AJAX loading of authors in dropdown filters ([#49](https://github.com/x-team/wp-stream/issues/49))
285
+ * Fix: Custom capability conflict with W3 Total Cache plugin ([#296](https://github.com/x-team/wp-stream/issues/296))
286
+ * Fix: Live updates remove last item in activity table ([#386](https://github.com/x-team/wp-stream/issues/386))
287
+ * Fix: Live updates screen option checkbox not persisting ([#392](https://github.com/x-team/wp-stream/issues/392))
288
+ * Fix: IP validator not respecting zero ([#394](https://github.com/x-team/wp-stream/issues/394))
289
+ * Fix: Non-Administrator users seeing errors in Settings records ([#406](https://github.com/x-team/wp-stream/issues/406))
290
+ * Fix: Uninstall confirmation message doesn't display ([#411](https://github.com/x-team/wp-stream/issues/411))
291
+ * Fix: TTL purge schedule is never setup ([#412](https://github.com/x-team/wp-stream/issues/412))
292
+ * Fix: NextGen compability issue ([#416](https://github.com/x-team/wp-stream/issues/416))
293
+ * Fix: Stream Feeds Key not being automatically generated ([#420](https://github.com/x-team/wp-stream/issues/420))
294
+
295
+ Props [@fjarrett](https://github.com/fjarrett), [@lukecarbis](https://github.com/lukecarbis), [@c3mdigital](https://github.com/c3mdigital), [@westonruter](https://github.com/westonruter), [@shadyvb](https://github.com/shadyvb), [@powelski](https://github.com/powelski), [@johnregan3](https://github.com/johnregan3), [@jonathanbardo](https://github.com/jonathanbardo), [@desaiuditd](https://github.com/desaiuditd)
296
+
297
+ ### 1.3.1 - April 3, 2014 ###
298
+ * New: Theme Editor connector for tracking changes made to theme files ([#313](https://github.com/x-team/wp-stream/issues/313))
299
+ * New: Additional screen options to show/hide only the filters you care about ([#329](https://github.com/x-team/wp-stream/issues/329))
300
+ * New: Visibility option in Exclude settings to hide past records from view ([#355](https://github.com/x-team/wp-stream/issues/355))
301
+ * New: Stream Activity dashboard widget now supports live updates ([#356](https://github.com/x-team/wp-stream/issues/356))
302
+ * New: Hover authors to reveal a tooltip with helpful user meta ([#338](https://github.com/x-team/wp-stream/issues/338))
303
+ * New: Hover roles to reveal a tooltip with the number of authors assigned to that role ([#377](https://github.com/x-team/wp-stream/issues/377))
304
+ * Tweak: Future dates now disabled in Start date field datepicker ([#334](https://github.com/x-team/wp-stream/issues/334))
305
+ * Tweak: Now showing user Gravatars in Exclude Authors & Roles settings field ([#333](https://github.com/x-team/wp-stream/issues/333))
306
+ * Tweak: ID column is now hidden by default in Screen Options ([#348](https://github.com/x-team/wp-stream/issues/348))
307
+ * Tweak: Widget updated summary message improvement ([8818976](https://github.com/x-team/wp-stream/commit/88189761d4a8836038e8d9ec348096a0aab3072d))
308
+ * Fix: Autocomplete not working correctly in Exclude IP Addressees settings field ([#335](https://github.com/x-team/wp-stream/issues/335))
309
+ * Fix: Reset Stream Database link not clearing everything in all cases ([#347](https://github.com/x-team/wp-stream/issues/347))
310
+ * Fix: PHP 5.3.3 compatibility issue with filter constant ([#351](https://github.com/x-team/wp-stream/issues/351))
311
+ * Fix: Predefined date range intervals not honoring the site timezone setting ([#353](https://github.com/x-team/wp-stream/issues/353))
312
+ * Fix: wpdb::prepare() notice appearing in WordPress 3.9 ([#354](https://github.com/x-team/wp-stream/issues/354))
313
+ * Fix: Invalid argument warning thrown on fresh installations of WordPress ([#358](https://github.com/x-team/wp-stream/issues/358))
314
+ * Fix: Record TTL purge not functioning correctly ([#371](https://github.com/x-team/wp-stream/issues/371))
315
+ * Fix: Small CSS bug in jQuery UI datepicker skins ([04c80af](https://github.com/x-team/wp-stream/commit/04c80afa99486086612be9f6ad83148dfbbe533a))
316
+
317
+ Props [@powelski](https://github.com/powelski), [@fjarrett](https://github.com/fjarrett), [@jonathanbardo](https://github.com/jonathanbardo), [@faishal](https://github.com/faishal), [@desaiuditd](https://github.com/desaiuditd), [@lukecarbis](https://github.com/lukecarbis), [@johnregan3](https://github.com/johnregan3), [@Powdered-Toast-Man](https://github.com/Powdered-Toast-Man)
318
+
319
+ ### 1.3.0 - March 12, 2014 ###
320
+ * New: Exclude tab in Settings to prevent specific types of activity from being tracked ([#251](https://github.com/x-team/wp-stream/issues/251))
321
+ * New: Now logging Custom Background and Custom Header changes ([#309](https://github.com/x-team/wp-stream/issues/309))
322
+ * New: Predefined date intervals now available when filtering records ([#320](https://github.com/x-team/wp-stream/issues/320))
323
+ * Tweak: Action links are now available for Stream Settings records ([#305](https://github.com/x-team/wp-stream/issues/305))
324
+ * Tweak: User avatars now displayed in Authors dropdown filter ([#311](https://github.com/x-team/wp-stream/issues/311))
325
+ * Tweak: Live updates are enabled by default for new installs ([#312](https://github.com/x-team/wp-stream/issues/312))
326
+ * Fix: Fallback to the term slug if a label does not exist in list-table ([#214](https://github.com/x-team/wp-stream/issues/214))
327
+ * Fix: Widget sorting is now being tracked properly as well as Inactive widgets ([#283](https://github.com/x-team/wp-stream/issues/283))
328
+ * Fix: Superfluous auto-draft posts are now prevented from being logged ([#293](https://github.com/x-team/wp-stream/issues/293))
329
+
330
+ Props [@powelski](https://github.com/powelski), [@faishal](https://github.com/faishal), [@fjarrett](https://github.com/fjarrett), [@desaiuditd](https://github.com/desaiuditd), [@lukecarbis](https://github.com/lukecarbis), [@shadyvb](https://github.com/shadyvb)
331
+
332
+ ### 1.2.9 - March 8, 2014 ###
333
+ Fixes bug that caused media uploads to fail on new posts. Props [@fjarrett](https://github.com/fjarrett)
334
+
335
+ ### 1.2.8 - March 7, 2014 ###
336
+ Use attachment type as context in Media connector. Bug fixes. Props [@lukecarbis](https://github.com/lukecarbis), [@powelski](https://github.com/powelski), [@fjarrett](https://github.com/fjarrett)
337
+
338
+ ### 1.2.7 - March 4, 2014 ###
339
+ Pagination added to Stream Activity dashboard widget. Bug fixes. Props [@chacha](https://github.com/chacha), [@fjarrett](https://github.com/fjarrett)
340
+
341
+ ### 1.2.6 - February 28, 2014 ###
342
+ Improved context names in Users connector. Props [@powelski](https://github.com/powelski)
343
+
344
+ ### 1.2.5 - February 27, 2014 ###
345
+ Use sidebar area names as context in Widgets connector. Bug fixes. Props [@desaiuditd](https://github.com/desaiuditd), [@shadyvb](https://github.com/shadyvb), [@fjarrett](https://github.com/fjarrett), [@bordoni](https://github.com/bordoni)
346
+
347
+ ### 1.2.4 - February 25, 2014 ###
348
+ Use post type names as context in Comments connector. German translation update. Bug fixes. Props [@powelski](https://github.com/powelski), [@kucrut](https://github.com/kucrut), [@pascalklaeres](https://github.com/pascal-klaeres), [@shadyvb](https://github.com/shadyvb), [@fjarrett](https://github.com/fjarrett)
349
+
350
+ ### 1.2.3 - February 21, 2014 ###
351
+ Replacement function for filter_input family to avoid PHP bug. Filter added to main Stream query. Bug fixes. Props [@shadyvb](https://github.com/shadyvb), [@powelski](https://github.com/powelski), [@fjarrett](https://github.com/fjarrett)
352
+
353
+ ### 1.2.2 - February 19, 2014 ###
354
+ Prevent records of disabled connectors from appearing in the Stream. Bug fixes. Props [@kucrut](https://github.com/kucrut), [@johnregan3](https://github.com/johnregan3)
355
+
356
+ ### 1.2.1 - February 17, 2014 ###
357
+ Translation updates. Langage packs for pt_BR and id_ID. Bug fixes. Props [@kucrut](https://github.com/kucrut), [@shadyvb](https://github.com/shadyvb), [@bordoni](https://github.com/bordoni), [@powelski](https://github.com/powelski), [omniwired](https://github.com/omniwired), [@fjarrett](https://github.com/fjarrett)
358
+
359
+ ### 1.2.0 - February 12, 2014 ###
360
+ Awesome datepicker styles. Performance optimizations. Bug fixes. Props [@johnregan3](https://github.com/johnregan3), [@shadyvb](https://github.com/shadyvb), [@fjarrett](https://github.com/fjarrett), [@jonathanbardo](https://github.com/jonathanbardo)
361
+
362
+ ### 1.1.9 - February 10, 2014 ###
363
+ Load authors filter using AJAX if there are more than 50. Props [@powelski](https://github.com/powelski)
364
+
365
+ ### 1.1.8 - February 9, 2014 ###
366
+ Bug fixes. Props [@shadyvb](https://github.com/shadyvb)
367
+
368
+ ### 1.1.7 - February 6, 2014 ###
369
+ Upgrade routine for IPv6 support. Persist tab selection after saving Stream Settings. Props [@shadyvb](https://github.com/shadyvb), [dero](https://github.com/dero)
370
+
371
+ ### 1.1.6 - February 6, 2014 ###
372
+ Sortable columns bug fix on the records screen. Props [@powelski](https://github.com/powelski), [@fjarrett](https://github.com/fjarrett)
373
+
374
+ ### 1.1.5 - February 5, 2014 ###
375
+ Fixed a class scope bug [reported in the support forum](https://wordpress.org/support/topic/temporary-fatal-error-after-upgrade-113) that was causing a fatal error on some installs. Props [@shadyvb](https://github.com/shadyvb)
376
+
377
+ ### 1.1.4 - February 5, 2014 ###
378
+ Highlight changed settings field feature. DB upgrade routine for proper utf-8 charset. Various bug fixes. Props [@powelski](https://github.com/powelski), [@johnregan3](https://github.com/johnregan3), [@shadyvb](https://github.com/shadyvb), [@fjarrett](https://github.com/fjarrett)
379
+
380
+ ### 1.1.3 - February 4, 2014 ###
381
+ Upgrade routine for IP column in DB. Serialized option parsing for Stream Settings records. Purge records immediately when TTL is set backwards in Stream Settings. Various bug fixes. Props [@shadyvb](https://github.com/shadyvb), [@powelski](https://github.com/powelski), [@fjarrett](https://github.com/fjarrett)
382
+
383
+ ### 1.1.2 - February 2, 2014 ###
384
+ Bug fix for list table notice on new installations. Props [@shadyvb](https://github.com/shadyvb)
385
+
386
+ ### 1.1.0 - January 31, 2014 ###
387
+ Disable terms in dropdown filters for which records do not exist. Props [@johnregan3](https://github.com/johnregan3)
388
+
389
+ ### 1.0.9 - January 31, 2014 ###
390
+ Several important bug fixes. Props [@shadyvb](https://github.com/shadyvb)
391
+
392
+ ### 1.0.8 - January 30, 2014 ###
393
+ Bug fix for sites using BuddyPress. Props [@johnregan3](https://github.com/johnregan3)
394
+
395
+ ### 1.0.7 - January 29, 2014 ###
396
+ Code efficiency improvements when fetching admin area URLs. Props [@fjarrett](https://github.com/fjarrett)
397
+
398
+ ### 1.0.6 - January 28, 2014 ###
399
+ Query improvements, default connector interface, hook added for general settings fields. Bug fixes. Props [dero](https://github.com/dero), [@jonathanbardo](https://github.com/jonathanbardo), [@shadyvb](https://github.com/shadyvb), [@fjarrett](https://github.com/fjarrett)
400
+
401
+ ### 1.0.5 - January 27, 2014 ###
402
+ Bug fix for live updates breaking columns when some are hidden via Screen Options. Props [@johnregan3](https://github.com/johnregan3)
403
+
404
+ ### 1.0.4 - January 23, 2014 ###
405
+ Language pack for Polish. Bug fixes. Props [@powelski](https://github.com/powelski), [@fjarrett](https://github.com/fjarrett), [@johnregan3](https://github.com/johnregan3), [@kucrut](https://github.com/kucrut)
406
+
407
+ ### 1.0.3 - January 19, 2014 ###
408
+ Language pack for Spanish. Bug fixes. Props [omniwired](https://github.com/omniwired), [@shadyvb](https://github.com/shadyvb)
409
+
410
+ ### 1.0.2 - January 15, 2014 ###
411
+ Ensure the dashboard widget repects the Role Access setting. Props [@fjarrett](https://github.com/fjarrett)
412
+
413
+ ### 1.0.1 - January 15, 2014 ###
414
+ Require nonce for generating a new user feed key. Props [@johnregan3](https://github.com/johnregan3)
415
+
416
+ ### 1.0.0 - January 13, 2014 ###
417
+ Allow list table to be exensible. Hook added to prevent tables from being created, if desired. Props [@johnregan3](https://github.com/johnregan3), [@fjarrett](https://github.com/fjarrett), [@jonathanbardo](https://github.com/jonathanbardo)
418
+
419
+ ### 0.9.9 - January 8, 2014 ###
420
+ Updated screenshot assets and descriptions. Props [@fjarrett](https://github.com/fjarrett)
421
+
422
+ ### 0.9.8 - January 1, 2014 ###
423
+ Support for live updates in the Stream. Bug fixes. Props [@jonathanbardo](https://github.com/jonathanbardo), [@johnregan3](https://github.com/johnregan3), [@fjarrett](https://github.com/fjarrett)
424
+
425
+ ### 0.9.7 - December 29, 2013 ###
426
+ Plugin version available as a constant. Bug fixes. Props [@jonathanbardo](https://github.com/jonathanbardo), [@fjarrett](https://github.com/fjarrett)
427
+
428
+ ### 0.9.6 - December 29, 2013 ###
429
+ Use menu name as context in Menus connector. Warning if required DB tables are missing. Bug fixes. Props [@jonathanbardo](https://github.com/jonathanbardo), [@fjarrett](https://github.com/fjarrett), [@topher1kenobe](https://github.com/topher1kenobe)
430
+
431
+ ### 0.9.5 - December 22, 2013 ###
432
+ WordPress context added to Installer connector for core updates. Props [@shadyvb](https://github.com/shadyvb)
433
+
434
+ ### 0.9.3 - December 22, 2013 ###
435
+ Replacing Chosen library with Select2. Bug fixes. Props [@kucrut](https://github.com/kucrut), [@shadyvb](https://github.com/shadyvb), [@fjarrett](https://github.com/fjarrett)
436
+
437
+ ### 0.9.2 - December 22, 2013 ###
438
+ Added support for private feeds in JSON format. Flush rewrite rules automatically for feeds when enabled/disabled. Bug fixes. Props [@jonathanbardo](https://github.com/jonathanbardo), [@fjarrett](https://github.com/fjarrett)
439
+
440
+ ### 0.9.1 - December 21, 2013 ###
441
+ Specify which roles should have their activity logged. Delete all options on uninstall. Bug fixes. Props [@jonathanbardo](https://github.com/jonathanbardo), [@fjarrett](https://github.com/fjarrett)
442
+
443
+ ### 0.9.0 - December 20, 2013 ###
444
+ Added connector for Comments. Stream activity dashboard widget. UI enhancements. Bug fixes. Props [@jonathanbardo](https://github.com/jonathanbardo), [@fjarrett](https://github.com/fjarrett), [@shadyvb](https://github.com/shadyvb), [@topher1kenobe](https://github.com/topher1kenobe)
445
+
446
+ ### 0.8.2 - December 19, 2013 ###
447
+ Language packs for French and German. Option to uninstall database tables. Bug fixes. Props [@jonathanbardo](https://github.com/jonathanbardo), [@fjarrett](https://github.com/fjarrett), [@topher1kenobe](https://github.com/topher1kenobe), [@pascalklaeres](https://github.com/pascal-klaeres)
448
+
449
+ ### 0.8.1 - December 18, 2013 ###
450
+ Setting to enable/disable private feeds functionality. Additional record logged when a user's role is changed. Bug fixes. Props [@fjarrett](https://github.com/fjarrett), [@kucrut](https://github.com/kucrut), [@topher1kenobe](https://github.com/topher1kenobe), [@justinsainton](https://github.com/justinsainton)
451
+
452
+ ### 0.8.0 - December 16, 2013 ###
453
+ Ability to query Stream records in a private RSS feed. Bug fixes. Props [@fjarrett](https://github.com/fjarrett), [@shadyvb](https://github.com/shadyvb)
454
+
455
+ ### 0.7.3 - December 13, 2013 ###
456
+ Bug fix for Role Access option. Props [@fjarrett](https://github.com/fjarrett)
457
+
458
+ ### 0.7.2 - December 12, 2013 ###
459
+ Bug fixes for the Installer connector. Props [@shadyvb](https://github.com/shadyvb)
460
+
461
+ ### 0.7.1 - December 12, 2013 ###
462
+ Hotfix to remove PHP 5.4-only syntax. Role Access option added to Settings. Props [@kucrut](https://github.com/kucrut)
463
+
464
+ ### 0.7.0 - December 11, 2013 ###
465
+ Added connectors for Taxonomies and Settings. Bug fixes. Props [@shadyvb](https://github.com/shadyvb), [@fjarrett](https://github.com/fjarrett)
466
+
467
+ ### 0.6.0 - December 9, 2013 ###
468
+ UX improvements to manual DB purge. Cron event for user-defined TTL of records. Bug fixes. Props [@shadyvb](https://github.com/shadyvb), [@fjarrett](https://github.com/fjarrett)
469
+
470
+ ### 0.5.0 - December 8, 2013 ###
471
+ Require PHP 5.3 to activate plugin. Provide action links for records when applicable. Bug fixes. Props [@shadyvb](https://github.com/shadyvb), [@fjarrett](https://github.com/fjarrett)
472
+
473
+ ### 0.4.0 - December 8, 2013 ###
474
+ Improved support for pages and custom post types. Chosen for filter dropdowns. Pagination support in screen options. Bug fixes. Props [@shadyvb](https://github.com/shadyvb), [@fjarrett](https://github.com/fjarrett)
475
+
476
+ ### 0.3.0 - December 7, 2013 ###
477
+ Improved actions for Users context. Action for edited images in Media context. Bug fixes in Menus context. Props [@shadyvb](https://github.com/shadyvb), [@fjarrett](https://github.com/fjarrett), [@akeda](https://github.com/gedex)
478
+
479
+ ### 0.2.0 - December 6, 2013 ###
480
+ Second iteration build using custom tables data model. First public release. Props [@shadyvb](https://github.com/shadyvb), [@fjarrett](https://github.com/fjarrett)
481
+
482
+ ### 0.1.0 ###
483
+ Initial concept built using custom post type/taxonomies as the data model. Props [@shadyvb](https://github.com/shadyvb)
484
+
485
+
readme.txt ADDED
@@ -0,0 +1,484 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ === Stream ===
2
+ Contributors: fjarrett, lukecarbis, shadyvb, westonruter, stream, xwp
3
+ Tags: actions, activity, admin, analytics, dashboard, log, notification, security, stream, users
4
+ Requires at least: 3.7
5
+ Tested up to: 4.2
6
+ Stable tag: 3.0
7
+ License: GPLv2 or later
8
+ License URI: https://www.gnu.org/licenses/gpl-2.0.html
9
+
10
+ Stream is the easiest and safest way to track content changes happening to your WordPress site and then view them in beautifully organized detail.
11
+
12
+ == Description ==
13
+
14
+ [youtube https://www.youtube.com/watch?v=t_qD4Sp4E70]
15
+
16
+ Never be in the dark about WP Admin activity again.
17
+
18
+ Stream allows you to know exactly when changes to your site have been made, and more importantly, who did them.
19
+
20
+ Every logged-in user action is displayed in an activity stream and organized for easy filtering by User, Role, Context, Action and IP address.
21
+
22
+ **Built-In Tracking Integrations For Popular Plugins:**
23
+
24
+ * [Advanced Custom Fields](https://wp-stream.com/connectors/acf/)
25
+ * [bbPress](https://wp-stream.com/connectors/bbpress/)
26
+ * [BuddyPress](https://wp-stream.com/connectors/buddypress/)
27
+ * [Easy Digital Downloads](https://wp-stream.com/connectors/edd/)
28
+ * [Gravity Forms](https://wp-stream.com/connectors/gravity-forms/)
29
+ * [Jetpack](https://wp-stream.com/connectors/jetpack/)
30
+ * [WooCommerce](https://wp-stream.com/connectors/woocommerce/)
31
+ * [WordPress SEO by Yoast](https://wp-stream.com/connectors/wordpress-seo/)
32
+
33
+ **Built-In Tracking For Core Actions:**
34
+
35
+ * Posts
36
+ * Pages
37
+ * Custom Post Types
38
+ * Users
39
+ * Themes
40
+ * Plugins
41
+ * Tags
42
+ * Categories
43
+ * Custom Taxonomies
44
+ * Settings
45
+ * Custom Backgrounds
46
+ * Custom Headers
47
+ * Menus
48
+ * Media Library
49
+ * Widgets
50
+ * Comments
51
+ * Theme Editor
52
+ * WordPress Core Updates
53
+
54
+ **Other Noteworthy Features:**
55
+
56
+ * Multisite view of all activity records on a network
57
+ * Limit who can view user activity records by user role
58
+ * Set exclude rules to ignore certain kinds of user activity
59
+ * Live update of user activity records in the Stream
60
+ * Support for IPv6 addresses
61
+ * WP-CLI command for querying records
62
+
63
+ **Languages Supported:**
64
+
65
+ * English
66
+ * French (France)
67
+ * German
68
+ * Indonesian
69
+ * Polish
70
+ * Portuguese (Brazil)
71
+ * Spanish (Spain)
72
+
73
+ **See room for improvement?**
74
+
75
+ Great! There are several ways you can get involved to help make Stream better:
76
+
77
+ 1. **Report Bugs:** If you find a bug, error or other problem, please report it! You can do this by [creating a new topic](https://wordpress.org/support/plugin/stream) in the plugin forum. Once a developer can verify the bug by reproducing it, they will create an official bug report in GitHub where the bug will be worked on.
78
+ 2. **Suggest New Features:** Have an awesome idea? Please share it! Simply [create a new topic](https://wordpress.org/support/plugin/stream) in the plugin forum to express your thoughts on why the feature should be included and get a discussion going around your idea.
79
+ 3. **Issue Pull Requests:** If you're a developer, the easiest way to get involved is to help out on [issues already reported](https://github.com/x-team/wp-stream/issues) in GitHub. Be sure to check out the [contributing guide](https://github.com/x-team/wp-stream/blob/master/contributing.md) for developers.
80
+
81
+ Thank you for wanting to make Stream better for everyone! We salute you.
82
+
83
+ == Screenshots ==
84
+
85
+ 1. Every logged-in user action is displayed in an activity stream and organized for easy filtering and searching.
86
+ 2. Enable live updates in Screen Options to watch your site activity appear in near real-time.
87
+ 3. Create rules for excluding certain kinds of records from appearing in Stream.
88
+ 4. Build notification rules to be alerted when important changes are made on your site.
89
+ 5. Generate stunning visuals of logged-in user activity and share them with stakeholders or your clients.
90
+
91
+ == Changelog ==
92
+
93
+ = 3.0.0 - August 25, 2015 =
94
+
95
+ * New: Activity logs are now stored locally in WordPress. No data is sent externally and no registration required.
96
+ * New: Migration process for Stream 2 users to move records out of the cloud, and into your local database.
97
+ * New: Various measures and database schema changes to improve Stream's performance.
98
+ * Removed: Notifications and Reports have been removed to be reworked for an upcoming release.
99
+
100
+ Props [@fjarrett](https://github.com/fjarrett), [@lukecarbis](https://github.com/lukecarbis)
101
+
102
+ = 2.0.5 - April 23, 2015 =
103
+
104
+ * Tweak: Compatibility with split terms introduced in WordPress 4.2 ([#702](https://github.com/wp-stream/stream/issues/702))
105
+ * Tweak: Add support for future and pending post transitions ([#716](https://github.com/wp-stream/stream/pull/716))
106
+ * Tweak: Match new default admin colors introduced in WordPress 4.2 ([#718](https://github.com/wp-stream/stream/pull/718))
107
+ * Fix: Compatibility issues with WP-Cron Control plugin and system crons ([#715](https://github.com/wp-stream/stream/issues/715))
108
+ * Fix: Broken date range filter on Reports screen ([#717](https://github.com/wp-stream/stream/pull/717))
109
+
110
+ Props [@fjarrett](https://github.com/fjarrett)
111
+
112
+ = 2.0.4 - April 16, 2015 =
113
+
114
+ * New: Add reset button to reset search filters ([#144](https://github.com/wp-stream/stream/issues/144))
115
+ * Tweak: WP-CLI command output improvements via `--format` option for table view, JSON and CSV ([#705](https://github.com/wp-stream/stream/pull/705))
116
+ * Tweak: Add link to https://wp-stream.com in README ([#709](https://github.com/wp-stream/stream/issues/709))
117
+ * Tweak: Better highlighting on multiple live update rows
118
+ * Tweak: Limit custom range datepickers based on the Stream plan type
119
+ * Tweak: Limit legacy record migrations based on the Stream plan type
120
+ * Fix: Allow properties with values of zero to be included in queries ([#698](https://github.com/wp-stream/stream/issues/698))
121
+ * Fix: Properly return record success/failure in log and store methods ([#711](https://github.com/wp-stream/stream/issues/711))
122
+
123
+ Props [@fjarrett](https://github.com/fjarrett), [@szepeviktor](https://github.com/szepeviktor)
124
+
125
+ = 2.0.3 - January 23, 2015 =
126
+
127
+ * New: WP-CLI command now available for querying records via the command line ([#499](https://github.com/wp-stream/stream/issues/499))
128
+ * Tweak: Silently disable Stream during content import ([#672](https://github.com/wp-stream/stream/issues/672))
129
+ * Tweak: Search results now ordered by date instead of relevance ([#689](https://github.com/wp-stream/stream/issues/689))
130
+ * Fix: Handle boolean values appropriately during wp_stream_log_data filter ([#680](https://github.com/wp-stream/stream/issues/680))
131
+ * Fix: Hook into external class load methods on init rather than plugins_loaded ([#686](https://github.com/wp-stream/stream/issues/686))
132
+ * Fix: N/A user not working in exclude rules ([#688](https://github.com/wp-stream/stream/issues/688))
133
+ * Fix: Prevent Notification Rule meta from being saved to all post types ([#693](https://github.com/wp-stream/stream/issues/693))
134
+ * Fix: PHP warning shown for some users when deleting plugins ([#695](https://github.com/wp-stream/stream/issues/695))
135
+
136
+ Props [@fjarrett](https://github.com/fjarrett)
137
+
138
+ = 2.0.2 - January 15, 2015 =
139
+
140
+ * New: Full record backtrace now available to developers for debugging ([#467](https://github.com/wp-stream/stream/issues/467))
141
+ * New: Unread count badge added to Stream menu, opt-out available in User Profile ([#588](https://github.com/wp-stream/stream/issues/588))
142
+ * New: Stream connector to track Stream-specific contexts and actions ([#622](https://github.com/wp-stream/stream/issues/622))
143
+ * Tweak: Inherit role access from Stream Settings for Notifications and Reports ([#641](https://github.com/wp-stream/stream/issues/641))
144
+ * Tweak: Opt-in required for Akismet tracking ([#649](https://github.com/wp-stream/stream/issues/649))
145
+ * Tweak: Ignore comments deleted when deleting parent post ([#652](https://github.com/wp-stream/stream/issues/652))
146
+ * Tweak: Opt-in required for comment flood tracking ([#656](https://github.com/wp-stream/stream/issues/656))
147
+ * Tweak: Opt-in required for WP Cron tracking ([#673](https://github.com/wp-stream/stream/issues/673))
148
+ * Fix: Post revision action link pointing to wrong revision ID ([#585](https://github.com/wp-stream/stream/issues/585))
149
+ * Fix: PHP warnings caused by Menu connector ([#663](https://github.com/wp-stream/stream/issues/663))
150
+ * Fix: Non-static method called statically in WPSEO connector ([#668](https://github.com/wp-stream/stream/issues/668))
151
+ * Fix: Prevent live updates from tampering with filtered results ([#675](https://github.com/wp-stream/stream/issues/675))
152
+
153
+ Props [@fjarrett](https://github.com/fjarrett), [@lukecarbis](https://github.com/lukecarbis), [@shadyvb](https://github.com/shadyvb), [@jonathanbardo](https://github.com/jonathanbardo), [@westonruter](https://github.com/westonruter)
154
+
155
+ = 2.0.1 - September 30, 2014 =
156
+
157
+ * Tweak: Improved localisation strings throughout the plugin ([#644](https://github.com/wp-stream/stream/pull/644))
158
+ * Tweak: Improved tooltip text explaining WP.com sign in
159
+ * Fix: ACF Pro doesn't save custom field values when Stream enabled ([#642](https://github.com/wp-stream/stream/issues/642))
160
+
161
+ Props [@lukecarbis](https://github.com/lukecarbis), [@fjarrett](https://github.com/fjarrett)
162
+
163
+ = 2.0.0 - September 27, 2014 =
164
+
165
+ * All activity is now stored only in the cloud over SSL, local MySQL storage dependence is over!
166
+ * Connector and Context have merged in the UI, now just called Contexts
167
+ * The Exclude Rules UI has been completely revamped
168
+ * Notifications and Reports are now conveniently built into Stream for Pro subscribers
169
+ * Connectors for tracking other popular plugins are now built into Stream, like BuddyPress, Jetpack, Gravity Forms, and more...
170
+ * You create an account for Stream simply by signing in with your WordPress.com ID
171
+
172
+ **NOTE:** Multisite view of all activity records in the Network Admin has been removed in this release. If you require this feature, please do not update Stream until version 2.1.0 is released.
173
+
174
+ Props [@fjarrett](https://github.com/fjarrett), [@lukecarbis](https://github.com/lukecarbis), [@shadyvb](https://github.com/shadyvb), [@chacha](https://github.com/chacha), [@jonathanbardo](https://github.com/jonathanbardo), [@bordoni](https://github.com/bordoni), [@dero](https://github.com/dero), [@jeffmatson](https://github.com/jeffmatson), [@stipsan](https://github.com/stipsan), [@c3mdigital](https://github.com/c3mdigital), [@adamsilverstein](https://github.com/adamsilverstein), [@westonruter](https://github.com/westonruter), [@japh](https://github.com/japh), [@solace](https://github.com/solace), [@johnbillion](https://github.com/johnbillion)
175
+
176
+ = 1.4.9 - July 23, 2014 =
177
+
178
+ * Fix: Revert delayed log mechanism for post transition ([#585](https://github.com/x-team/wp-stream/issues/585))
179
+ * Fix: Revert usage of get_taxonomy() ([#586](https://github.com/x-team/wp-stream/pull/586))
180
+ * Fix: Notices not firing on correct action ([#589](https://github.com/x-team/wp-stream/issues/589))
181
+
182
+ Props [@shadyvb](https://github.com/shadyvb), [@fjarrett](https://github.com/fjarrett)
183
+
184
+ = 1.4.8 - July 18, 2014 =
185
+
186
+ * New: Greatly improved widget tracking, including changes performed in Customizer ([#391](https://github.com/x-team/wp-stream/pull/391))
187
+ * New: Now tracking when Akismet automatically marks comments as spam ([#587](https://github.com/x-team/wp-stream/pull/587))
188
+ * Tweak: Log WP-CLI details to Stream author meta ([#470](https://github.com/x-team/wp-stream/issues/470))
189
+ * Tweak: Track changes to options more deeply ([#573](https://github.com/x-team/wp-stream/pull/573))
190
+ * Fix: Labels not seen for CPT registered on init with default priority ([#565](https://github.com/x-team/wp-stream/issues/565))
191
+ * Fix: Stream menu appearing in Network menu when not network activated ([#582](https://github.com/x-team/wp-stream/issues/582))
192
+ * Fix: Post Revision ID associated to record is not the most recent one ([#585](https://github.com/x-team/wp-stream/issues/585))
193
+ * Fix: Incorrect action label for comment throttling ([#591](https://github.com/x-team/wp-stream/issues/591))
194
+
195
+ Props [@westonruter](https://github.com/westonruter), [@fjarrett](https://github.com/fjarrett), [@shadyvb](https://github.com/shadyvb), [@lukecarbis](https://github.com/lukecarbis), [@chacha](https://github.com/chacha)
196
+
197
+ = 1.4.7 - June 27, 2014 =
198
+
199
+ * New: Comment Type support added to the Comments connector ([#558](https://github.com/x-team/wp-stream/issues/558))
200
+ * Fix: Datepicker opens again with each paged view ([#568](https://github.com/x-team/wp-stream/issues/568))
201
+ * Fix: PHP warning when deleting network users ([#579](https://github.com/x-team/wp-stream/issues/579))
202
+ * Fix: Track user count setting changes ([#583](https://github.com/x-team/wp-stream/issues/583))
203
+ * Fix: .po and .pot files out-of-date for translators ([#584](https://github.com/x-team/wp-stream/issues/584))
204
+
205
+ Props [@lukecarbis](https://github.com/lukecarbis), [@fjarrett](https://github.com/fjarrett), [@bordoni](https://github.com/bordoni), [@shadyvb](https://github.com/shadyvb)
206
+
207
+ = 1.4.6 - May 30, 2014 =
208
+
209
+ * Tweak: Actions provided for trashed posts are irrelevant ([#523](https://github.com/x-team/wp-stream/issues/523))
210
+ * Tweak: Use core language pack translations where possible ([#534](https://github.com/x-team/wp-stream/issues/534))
211
+ * Tweak: Consolidate show filter and show column screen options ([#542](https://github.com/x-team/wp-stream/issues/542))
212
+ * Tweak: Stop tracking failed login attempts ([#547](https://github.com/x-team/wp-stream/issues/547))
213
+ * Tweak: Remove all uses of extract() from Stream ([#556](https://github.com/x-team/wp-stream/issues/556))
214
+ * Fix: Excluding roles is not handled properly ([#527](https://github.com/x-team/wp-stream/issues/527))
215
+ * Fix: Stream runs install routine twice ([#528](https://github.com/x-team/wp-stream/issues/528))
216
+ * Fix: Widget records show sidebar slug instead of label ([#531](https://github.com/x-team/wp-stream/issues/531))
217
+ * Fix: Fatal error when PHP version is less than 5.3 ([#538](https://github.com/x-team/wp-stream/issues/538))
218
+ * Fix: Cannot exclude Custom Background context ([#543](https://github.com/x-team/wp-stream/issues/543))
219
+ * Fix: Conflict with Jetpack body class in WP Admin ([#545](https://github.com/x-team/wp-stream/issues/545))
220
+ * Fix: Stream settings exclude error for big wp_users table ([#551](https://github.com/x-team/wp-stream/issues/551))
221
+
222
+ Props [@fjarrett](https://github.com/fjarrett), [@lukecarbis](https://github.com/lukecarbis), [@shadyvb](https://github.com/shadyvb), [@barryceelen](https://github.com/barryceelen), [@japh](https://github.com/japh)
223
+
224
+ = 1.4.5 - May 15, 2014 =
225
+
226
+ * New: Lightweight frontend indicator for sites using Stream ([#507](https://github.com/x-team/wp-stream/issues/507))
227
+ * Tweak: Add filterable method for excluded comment types ([#487](https://github.com/x-team/wp-stream/issues/487))
228
+ * Tweak: Rename "ID" column label to "Record ID" ([#490](https://github.com/x-team/wp-stream/issues/490))
229
+ * Tweak: One admin notice for any missing DB tables ([#506](https://github.com/x-team/wp-stream/pull/506))
230
+ * Fix: Custom authentication schemes not tracking user logins correctly ([#434](https://github.com/x-team/wp-stream/issues/434))
231
+ * Fix: Taxonomy connector conflicts with Edit Flow plugin ([#498](https://github.com/x-team/wp-stream/issues/498))
232
+ * Fix: Switching user is incorrectly tracked ([#501](https://github.com/x-team/wp-stream/issues/501))
233
+ * Fix: Extension activation links broken when plugin folders are renamed ([#502](https://github.com/x-team/wp-stream/issues/502))
234
+ * Fix: Author info showing up incorrectly ([#505](https://github.com/x-team/wp-stream/issues/505))
235
+ * Fix: Incompatibility with multi-server environments ([#517](https://github.com/x-team/wp-stream/issues/517))
236
+ * Fix: Warnings seen when Show Avatars is disabled ([#518](https://github.com/x-team/wp-stream/issues/518))
237
+ * Fix: Notices for non-existent extension data after timeout ([#529](https://github.com/x-team/wp-stream/pull/529))
238
+
239
+ Props [@fjarrett](https://github.com/fjarrett), [@shadyvb](https://github.com/shadyvb), [@lukecarbis](https://github.com/lukecarbis), [@japh](https://github.com/japh)
240
+
241
+ = 1.4.4 - May 6, 2014 =
242
+
243
+ * New: Admin pointers to highlight when new admin screens are introduced ([#466](https://github.com/x-team/wp-stream/issues/466))
244
+ * Tweak: Filter introduced to allow the Stream admin menu position to be changed ([#99](https://github.com/x-team/wp-stream/issues/99))
245
+ * Tweak: Provide option label for records that show when the Stream database has updated ([#444](https://github.com/x-team/wp-stream/pull/444))
246
+ * Tweak: Better handling of authors in the list table ([#448](https://github.com/x-team/wp-stream/pull/448))
247
+ * Tweak: Way for developers to set their Stream Extensions affiliate ID on links from the Extensions screen ([#482](https://github.com/x-team/wp-stream/issues/482))
248
+ * Fix: Extensions screen CSS bug in Firefox ([#464](https://github.com/x-team/wp-stream/issues/464))
249
+ * Fix: Error when installing extensions from the Network Admin ([#491](https://github.com/x-team/wp-stream/issues/491))
250
+ * Fix: Undefined notice in admin.php ([#468](https://github.com/x-team/wp-stream/issues/468))
251
+
252
+ Props [@westonruter](https://github.com/westonruter), [@fjarrett](https://github.com/fjarrett), [@japh](https://github.com/japh), [@lukecarbis](https://github.com/lukecarbis), [@jonathanbardo](https://github.com/jonathanbardo), [@bordoni](https://github.com/bordoni)
253
+
254
+ = 1.4.3 - April 26, 2014 =
255
+
256
+ * New: Introducing the Stream Extensions screen! ([#396](https://github.com/x-team/wp-stream/issues/396))
257
+
258
+ Props [@jonathanbardo](https://github.com/jonathanbardo), [@lukecarbis](https://github.com/lukecarbis), [@shadyvb](https://github.com/shadyvb), [@c3mdigital](https://github.com/c3mdigital), [@fjarrett](https://github.com/fjarrett)
259
+
260
+ = 1.4.2 - April 24, 2014 =
261
+
262
+ * Fix: Update Database button redirecting to previous screen ([#443](https://github.com/x-team/wp-stream/issues/443))
263
+ * Fix: Update routine hotfix that was causing records to disappear ([#447](https://github.com/x-team/wp-stream/issues/447))
264
+
265
+ Props [@jonathanbardo](https://github.com/jonathanbardo), [@lukecarbis](https://github.com/lukecarbis), [@westonruter](https://github.com/westonruter), [@fjarrett](https://github.com/fjarrett)
266
+
267
+ = 1.4.1 - April 24, 2014 =
268
+
269
+ * Fix: Scripts and styles not using Stream version number ([#440](https://github.com/x-team/wp-stream/issues/440))
270
+ * Fix: WP-CLI incorrectly referenced in records ([#441](https://github.com/x-team/wp-stream/issues/441))
271
+
272
+ Props [@westonruter](https://github.com/westonruter), [@fjarrett](https://github.com/fjarrett)
273
+
274
+ = 1.4.0 - April 24, 2014 =
275
+
276
+ * New: Multisite is now fully supported, activate Stream network-wide ([#65](https://github.com/x-team/wp-stream/issues/65))
277
+ * New: Separate API for handling DB update routines ([#379](https://github.com/x-team/wp-stream/issues/379))
278
+ * New: WP-CLI compatibility, Stream now tracks changes made via WP-CLI ([#423](https://github.com/x-team/wp-stream/issues/423))
279
+ * Tweak: Deprecate functions and hooks in favor of consistent naming conventions ([#267](https://github.com/x-team/wp-stream/issues/267))
280
+ * Tweak: Use icon link instead of clicking the summary to filter by object ID ([#380](https://github.com/x-team/wp-stream/issues/380))
281
+ * Tweak: Save additional author meta for better records ([#389](https://github.com/x-team/wp-stream/issues/389))
282
+ * Tweak: More compact search filters for smaller screens ([#403](https://github.com/x-team/wp-stream/issues/403))
283
+ * Fix: Fix AJAX loading of authors in dropdown filters ([#49](https://github.com/x-team/wp-stream/issues/49))
284
+ * Fix: Custom capability conflict with W3 Total Cache plugin ([#296](https://github.com/x-team/wp-stream/issues/296))
285
+ * Fix: Live updates remove last item in activity table ([#386](https://github.com/x-team/wp-stream/issues/386))
286
+ * Fix: Live updates screen option checkbox not persisting ([#392](https://github.com/x-team/wp-stream/issues/392))
287
+ * Fix: IP validator not respecting zero ([#394](https://github.com/x-team/wp-stream/issues/394))
288
+ * Fix: Non-Administrator users seeing errors in Settings records ([#406](https://github.com/x-team/wp-stream/issues/406))
289
+ * Fix: Uninstall confirmation message doesn't display ([#411](https://github.com/x-team/wp-stream/issues/411))
290
+ * Fix: TTL purge schedule is never setup ([#412](https://github.com/x-team/wp-stream/issues/412))
291
+ * Fix: NextGen compability issue ([#416](https://github.com/x-team/wp-stream/issues/416))
292
+ * Fix: Stream Feeds Key not being automatically generated ([#420](https://github.com/x-team/wp-stream/issues/420))
293
+
294
+ Props [@fjarrett](https://github.com/fjarrett), [@lukecarbis](https://github.com/lukecarbis), [@c3mdigital](https://github.com/c3mdigital), [@westonruter](https://github.com/westonruter), [@shadyvb](https://github.com/shadyvb), [@powelski](https://github.com/powelski), [@johnregan3](https://github.com/johnregan3), [@jonathanbardo](https://github.com/jonathanbardo), [@desaiuditd](https://github.com/desaiuditd)
295
+
296
+ = 1.3.1 - April 3, 2014 =
297
+
298
+ * New: Theme Editor connector for tracking changes made to theme files ([#313](https://github.com/x-team/wp-stream/issues/313))
299
+ * New: Additional screen options to show/hide only the filters you care about ([#329](https://github.com/x-team/wp-stream/issues/329))
300
+ * New: Visibility option in Exclude settings to hide past records from view ([#355](https://github.com/x-team/wp-stream/issues/355))
301
+ * New: Stream Activity dashboard widget now supports live updates ([#356](https://github.com/x-team/wp-stream/issues/356))
302
+ * New: Hover authors to reveal a tooltip with helpful user meta ([#338](https://github.com/x-team/wp-stream/issues/338))
303
+ * New: Hover roles to reveal a tooltip with the number of authors assigned to that role ([#377](https://github.com/x-team/wp-stream/issues/377))
304
+ * Tweak: Future dates now disabled in Start date field datepicker ([#334](https://github.com/x-team/wp-stream/issues/334))
305
+ * Tweak: Now showing user Gravatars in Exclude Authors & Roles settings field ([#333](https://github.com/x-team/wp-stream/issues/333))
306
+ * Tweak: ID column is now hidden by default in Screen Options ([#348](https://github.com/x-team/wp-stream/issues/348))
307
+ * Tweak: Widget updated summary message improvement ([8818976](https://github.com/x-team/wp-stream/commit/88189761d4a8836038e8d9ec348096a0aab3072d))
308
+ * Fix: Autocomplete not working correctly in Exclude IP Addressees settings field ([#335](https://github.com/x-team/wp-stream/issues/335))
309
+ * Fix: Reset Stream Database link not clearing everything in all cases ([#347](https://github.com/x-team/wp-stream/issues/347))
310
+ * Fix: PHP 5.3.3 compatibility issue with filter constant ([#351](https://github.com/x-team/wp-stream/issues/351))
311
+ * Fix: Predefined date range intervals not honoring the site timezone setting ([#353](https://github.com/x-team/wp-stream/issues/353))
312
+ * Fix: wpdb::prepare() notice appearing in WordPress 3.9 ([#354](https://github.com/x-team/wp-stream/issues/354))
313
+ * Fix: Invalid argument warning thrown on fresh installations of WordPress ([#358](https://github.com/x-team/wp-stream/issues/358))
314
+ * Fix: Record TTL purge not functioning correctly ([#371](https://github.com/x-team/wp-stream/issues/371))
315
+ * Fix: Small CSS bug in jQuery UI datepicker skins ([04c80af](https://github.com/x-team/wp-stream/commit/04c80afa99486086612be9f6ad83148dfbbe533a))
316
+
317
+ Props [@powelski](https://github.com/powelski), [@fjarrett](https://github.com/fjarrett), [@jonathanbardo](https://github.com/jonathanbardo), [@faishal](https://github.com/faishal), [@desaiuditd](https://github.com/desaiuditd), [@lukecarbis](https://github.com/lukecarbis), [@johnregan3](https://github.com/johnregan3), [@Powdered-Toast-Man](https://github.com/Powdered-Toast-Man)
318
+
319
+ = 1.3.0 - March 12, 2014 =
320
+
321
+ * New: Exclude tab in Settings to prevent specific types of activity from being tracked ([#251](https://github.com/x-team/wp-stream/issues/251))
322
+ * New: Now logging Custom Background and Custom Header changes ([#309](https://github.com/x-team/wp-stream/issues/309))
323
+ * New: Predefined date intervals now available when filtering records ([#320](https://github.com/x-team/wp-stream/issues/320))
324
+ * Tweak: Action links are now available for Stream Settings records ([#305](https://github.com/x-team/wp-stream/issues/305))
325
+ * Tweak: User avatars now displayed in Authors dropdown filter ([#311](https://github.com/x-team/wp-stream/issues/311))
326
+ * Tweak: Live updates are enabled by default for new installs ([#312](https://github.com/x-team/wp-stream/issues/312))
327
+ * Fix: Fallback to the term slug if a label does not exist in list-table ([#214](https://github.com/x-team/wp-stream/issues/214))
328
+ * Fix: Widget sorting is now being tracked properly as well as Inactive widgets ([#283](https://github.com/x-team/wp-stream/issues/283))
329
+ * Fix: Superfluous auto-draft posts are now prevented from being logged ([#293](https://github.com/x-team/wp-stream/issues/293))
330
+
331
+ Props [@powelski](https://github.com/powelski), [@faishal](https://github.com/faishal), [@fjarrett](https://github.com/fjarrett), [@desaiuditd](https://github.com/desaiuditd), [@lukecarbis](https://github.com/lukecarbis), [@shadyvb](https://github.com/shadyvb)
332
+
333
+ = 1.2.9 - March 8, 2014 =
334
+ Fixes bug that caused media uploads to fail on new posts. Props [@fjarrett](https://github.com/fjarrett)
335
+
336
+ = 1.2.8 - March 7, 2014 =
337
+ Use attachment type as context in Media connector. Bug fixes. Props [@lukecarbis](https://github.com/lukecarbis), [@powelski](https://github.com/powelski), [@fjarrett](https://github.com/fjarrett)
338
+
339
+ = 1.2.7 - March 4, 2014 =
340
+ Pagination added to Stream Activity dashboard widget. Bug fixes. Props [@chacha](https://github.com/chacha), [@fjarrett](https://github.com/fjarrett)
341
+
342
+ = 1.2.6 - February 28, 2014 =
343
+ Improved context names in Users connector. Props [@powelski](https://github.com/powelski)
344
+
345
+ = 1.2.5 - February 27, 2014 =
346
+ Use sidebar area names as context in Widgets connector. Bug fixes. Props [@desaiuditd](https://github.com/desaiuditd), [@shadyvb](https://github.com/shadyvb), [@fjarrett](https://github.com/fjarrett), [@bordoni](https://github.com/bordoni)
347
+
348
+ = 1.2.4 - February 25, 2014 =
349
+ Use post type names as context in Comments connector. German translation update. Bug fixes. Props [@powelski](https://github.com/powelski), [@kucrut](https://github.com/kucrut), [@pascalklaeres](https://github.com/pascal-klaeres), [@shadyvb](https://github.com/shadyvb), [@fjarrett](https://github.com/fjarrett)
350
+
351
+ = 1.2.3 - February 21, 2014 =
352
+ Replacement function for filter_input family to avoid PHP bug. Filter added to main Stream query. Bug fixes. Props [@shadyvb](https://github.com/shadyvb), [@powelski](https://github.com/powelski), [@fjarrett](https://github.com/fjarrett)
353
+
354
+ = 1.2.2 - February 19, 2014 =
355
+ Prevent records of disabled connectors from appearing in the Stream. Bug fixes. Props [@kucrut](https://github.com/kucrut), [@johnregan3](https://github.com/johnregan3)
356
+
357
+ = 1.2.1 - February 17, 2014 =
358
+ Translation updates. Langage packs for pt_BR and id_ID. Bug fixes. Props [@kucrut](https://github.com/kucrut), [@shadyvb](https://github.com/shadyvb), [@bordoni](https://github.com/bordoni), [@powelski](https://github.com/powelski), [omniwired](https://github.com/omniwired), [@fjarrett](https://github.com/fjarrett)
359
+
360
+ = 1.2.0 - February 12, 2014 =
361
+ Awesome datepicker styles. Performance optimizations. Bug fixes. Props [@johnregan3](https://github.com/johnregan3), [@shadyvb](https://github.com/shadyvb), [@fjarrett](https://github.com/fjarrett), [@jonathanbardo](https://github.com/jonathanbardo)
362
+
363
+ = 1.1.9 - February 10, 2014 =
364
+ Load authors filter using AJAX if there are more than 50. Props [@powelski](https://github.com/powelski)
365
+
366
+ = 1.1.8 - February 9, 2014 =
367
+ Bug fixes. Props [@shadyvb](https://github.com/shadyvb)
368
+
369
+ = 1.1.7 - February 6, 2014 =
370
+ Upgrade routine for IPv6 support. Persist tab selection after saving Stream Settings. Props [@shadyvb](https://github.com/shadyvb), [dero](https://github.com/dero)
371
+
372
+ = 1.1.6 - February 6, 2014 =
373
+ Sortable columns bug fix on the records screen. Props [@powelski](https://github.com/powelski), [@fjarrett](https://github.com/fjarrett)
374
+
375
+ = 1.1.5 - February 5, 2014 =
376
+ Fixed a class scope bug [reported in the support forum](https://wordpress.org/support/topic/temporary-fatal-error-after-upgrade-113) that was causing a fatal error on some installs. Props [@shadyvb](https://github.com/shadyvb)
377
+
378
+ = 1.1.4 - February 5, 2014 =
379
+ Highlight changed settings field feature. DB upgrade routine for proper utf-8 charset. Various bug fixes. Props [@powelski](https://github.com/powelski), [@johnregan3](https://github.com/johnregan3), [@shadyvb](https://github.com/shadyvb), [@fjarrett](https://github.com/fjarrett)
380
+
381
+ = 1.1.3 - February 4, 2014 =
382
+ Upgrade routine for IP column in DB. Serialized option parsing for Stream Settings records. Purge records immediately when TTL is set backwards in Stream Settings. Various bug fixes. Props [@shadyvb](https://github.com/shadyvb), [@powelski](https://github.com/powelski), [@fjarrett](https://github.com/fjarrett)
383
+
384
+ = 1.1.2 - February 2, 2014 =
385
+ Bug fix for list table notice on new installations. Props [@shadyvb](https://github.com/shadyvb)
386
+
387
+ = 1.1.0 - January 31, 2014 =
388
+ Disable terms in dropdown filters for which records do not exist. Props [@johnregan3](https://github.com/johnregan3)
389
+
390
+ = 1.0.9 - January 31, 2014 =
391
+ Several important bug fixes. Props [@shadyvb](https://github.com/shadyvb)
392
+
393
+ = 1.0.8 - January 30, 2014 =
394
+ Bug fix for sites using BuddyPress. Props [@johnregan3](https://github.com/johnregan3)
395
+
396
+ = 1.0.7 - January 29, 2014 =
397
+ Code efficiency improvements when fetching admin area URLs. Props [@fjarrett](https://github.com/fjarrett)
398
+
399
+ = 1.0.6 - January 28, 2014 =
400
+ Query improvements, default connector interface, hook added for general settings fields. Bug fixes. Props [dero](https://github.com/dero), [@jonathanbardo](https://github.com/jonathanbardo), [@shadyvb](https://github.com/shadyvb), [@fjarrett](https://github.com/fjarrett)
401
+
402
+ = 1.0.5 - January 27, 2014 =
403
+ Bug fix for live updates breaking columns when some are hidden via Screen Options. Props [@johnregan3](https://github.com/johnregan3)
404
+
405
+ = 1.0.4 - January 23, 2014 =
406
+ Language pack for Polish. Bug fixes. Props [@powelski](https://github.com/powelski), [@fjarrett](https://github.com/fjarrett), [@johnregan3](https://github.com/johnregan3), [@kucrut](https://github.com/kucrut)
407
+
408
+ = 1.0.3 - January 19, 2014 =
409
+ Language pack for Spanish. Bug fixes. Props [omniwired](https://github.com/omniwired), [@shadyvb](https://github.com/shadyvb)
410
+
411
+ = 1.0.2 - January 15, 2014 =
412
+ Ensure the dashboard widget repects the Role Access setting. Props [@fjarrett](https://github.com/fjarrett)
413
+
414
+ = 1.0.1 - January 15, 2014 =
415
+ Require nonce for generating a new user feed key. Props [@johnregan3](https://github.com/johnregan3)
416
+
417
+ = 1.0.0 - January 13, 2014 =
418
+ Allow list table to be exensible. Hook added to prevent tables from being created, if desired. Props [@johnregan3](https://github.com/johnregan3), [@fjarrett](https://github.com/fjarrett), [@jonathanbardo](https://github.com/jonathanbardo)
419
+
420
+ = 0.9.9 - January 8, 2014 =
421
+ Updated screenshot assets and descriptions. Props [@fjarrett](https://github.com/fjarrett)
422
+
423
+ = 0.9.8 - January 1, 2014 =
424
+ Support for live updates in the Stream. Bug fixes. Props [@jonathanbardo](https://github.com/jonathanbardo), [@johnregan3](https://github.com/johnregan3), [@fjarrett](https://github.com/fjarrett)
425
+
426
+ = 0.9.7 - December 29, 2013 =
427
+ Plugin version available as a constant. Bug fixes. Props [@jonathanbardo](https://github.com/jonathanbardo), [@fjarrett](https://github.com/fjarrett)
428
+
429
+ = 0.9.6 - December 29, 2013 =
430
+ Use menu name as context in Menus connector. Warning if required DB tables are missing. Bug fixes. Props [@jonathanbardo](https://github.com/jonathanbardo), [@fjarrett](https://github.com/fjarrett), [@topher1kenobe](https://github.com/topher1kenobe)
431
+
432
+ = 0.9.5 - December 22, 2013 =
433
+ WordPress context added to Installer connector for core updates. Props [@shadyvb](https://github.com/shadyvb)
434
+
435
+ = 0.9.3 - December 22, 2013 =
436
+ Replacing Chosen library with Select2. Bug fixes. Props [@kucrut](https://github.com/kucrut), [@shadyvb](https://github.com/shadyvb), [@fjarrett](https://github.com/fjarrett)
437
+
438
+ = 0.9.2 - December 22, 2013 =
439
+ Added support for private feeds in JSON format. Flush rewrite rules automatically for feeds when enabled/disabled. Bug fixes. Props [@jonathanbardo](https://github.com/jonathanbardo), [@fjarrett](https://github.com/fjarrett)
440
+
441
+ = 0.9.1 - December 21, 2013 =
442
+ Specify which roles should have their activity logged. Delete all options on uninstall. Bug fixes. Props [@jonathanbardo](https://github.com/jonathanbardo), [@fjarrett](https://github.com/fjarrett)
443
+
444
+ = 0.9.0 - December 20, 2013 =
445
+ Added connector for Comments. Stream activity dashboard widget. UI enhancements. Bug fixes. Props [@jonathanbardo](https://github.com/jonathanbardo), [@fjarrett](https://github.com/fjarrett), [@shadyvb](https://github.com/shadyvb), [@topher1kenobe](https://github.com/topher1kenobe)
446
+
447
+ = 0.8.2 - December 19, 2013 =
448
+ Language packs for French and German. Option to uninstall database tables. Bug fixes. Props [@jonathanbardo](https://github.com/jonathanbardo), [@fjarrett](https://github.com/fjarrett), [@topher1kenobe](https://github.com/topher1kenobe), [@pascalklaeres](https://github.com/pascal-klaeres)
449
+
450
+ = 0.8.1 - December 18, 2013 =
451
+ Setting to enable/disable private feeds functionality. Additional record logged when a user's role is changed. Bug fixes. Props [@fjarrett](https://github.com/fjarrett), [@kucrut](https://github.com/kucrut), [@topher1kenobe](https://github.com/topher1kenobe), [@justinsainton](https://github.com/justinsainton)
452
+
453
+ = 0.8.0 - December 16, 2013 =
454
+ Ability to query Stream records in a private RSS feed. Bug fixes. Props [@fjarrett](https://github.com/fjarrett), [@shadyvb](https://github.com/shadyvb)
455
+
456
+ = 0.7.3 - December 13, 2013 =
457
+ Bug fix for Role Access option. Props [@fjarrett](https://github.com/fjarrett)
458
+
459
+ = 0.7.2 - December 12, 2013 =
460
+ Bug fixes for the Installer connector. Props [@shadyvb](https://github.com/shadyvb)
461
+
462
+ = 0.7.1 - December 12, 2013 =
463
+ Hotfix to remove PHP 5.4-only syntax. Role Access option added to Settings. Props [@kucrut](https://github.com/kucrut)
464
+
465
+ = 0.7.0 - December 11, 2013 =
466
+ Added connectors for Taxonomies and Settings. Bug fixes. Props [@shadyvb](https://github.com/shadyvb), [@fjarrett](https://github.com/fjarrett)
467
+
468
+ = 0.6.0 - December 9, 2013 =
469
+ UX improvements to manual DB purge. Cron event for user-defined TTL of records. Bug fixes. Props [@shadyvb](https://github.com/shadyvb), [@fjarrett](https://github.com/fjarrett)
470
+
471
+ = 0.5.0 - December 8, 2013 =
472
+ Require PHP 5.3 to activate plugin. Provide action links for records when applicable. Bug fixes. Props [@shadyvb](https://github.com/shadyvb), [@fjarrett](https://github.com/fjarrett)
473
+
474
+ = 0.4.0 - December 8, 2013 =
475
+ Improved support for pages and custom post types. Chosen for filter dropdowns. Pagination support in screen options. Bug fixes. Props [@shadyvb](https://github.com/shadyvb), [@fjarrett](https://github.com/fjarrett)
476
+
477
+ = 0.3.0 - December 7, 2013 =
478
+ Improved actions for Users context. Action for edited images in Media context. Bug fixes in Menus context. Props [@shadyvb](https://github.com/shadyvb), [@fjarrett](https://github.com/fjarrett), [@akeda](https://github.com/gedex)
479
+
480
+ = 0.2.0 - December 6, 2013 =
481
+ Second iteration build using custom tables data model. First public release. Props [@shadyvb](https://github.com/shadyvb), [@fjarrett](https://github.com/fjarrett)
482
+
483
+ = 0.1.0 =
484
+ Initial concept built using custom post type/taxonomies as the data model. Props [@shadyvb](https://github.com/shadyvb)
stream.php ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * Plugin Name: Stream
4
+ * Plugin URI: https://wp-stream.com/
5
+ * Description: Stream tracks logged-in user activity so you can monitor every change made on your WordPress site in beautifully organized detail. All activity is organized by context, action and IP address for easy filtering. Developers can extend Stream with custom connectors to log any kind of action.
6
+ * Version: 3.0.0
7
+ * Author: Stream
8
+ * Author URI: https://wp-stream.com/
9
+ * License: GPLv2+
10
+ * Text Domain: stream
11
+ * Domain Path: /languages
12
+ */
13
+
14
+ /**
15
+ * Copyright (c) 2015 WP Stream Pty Ltd (https://wp-stream.com/)
16
+ *
17
+ * This program is free software; you can redistribute it and/or modify
18
+ * it under the terms of the GNU General Public License, version 2 or, at
19
+ * your discretion, any later version, as published by the Free
20
+ * Software Foundation.
21
+ *
22
+ * This program is distributed in the hope that it will be useful,
23
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
24
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
25
+ * GNU General Public License for more details.
26
+ *
27
+ * You should have received a copy of the GNU General Public License
28
+ * along with this program; if not, write to the Free Software
29
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
30
+ */
31
+
32
+ if ( ! version_compare( PHP_VERSION, '5.3', '>=' ) ) {
33
+ load_plugin_textdomain( 'stream', false, dirname( plugin_basename( __FILE__ ) ) . '/languages/' );
34
+ add_action( 'shutdown', 'wp_stream_fail_php_version' );
35
+ } else {
36
+ require __DIR__ . '/classes/class-plugin.php';
37
+ $GLOBALS['wp_stream'] = new WP_Stream\Plugin();
38
+ }
39
+
40
+ /**
41
+ * Invoked when the PHP version check fails
42
+ * Load up the translations and add the error message to the admin notices.
43
+ */
44
+ function wp_stream_fail_php_version() {
45
+ load_plugin_textdomain( 'stream', false, dirname( plugin_basename( __FILE__ ) ) . '/languages/' );
46
+
47
+ $message = esc_html__( 'Stream requires PHP version 5.3+, plugin is currently NOT ACTIVE.', 'stream' );
48
+ $html_message = sprintf( '<div class="error">%s</div>', wpautop( $message ) );
49
+
50
+ echo wp_kses_post( $html_message );
51
+ }
52
+
53
+ /**
54
+ * Helper for external plugins which wish to use Stream
55
+ *
56
+ * @return WP_Stream\Plugin
57
+ */
58
+ function wp_stream_get_instance() {
59
+ return $GLOBALS['wp_stream'];
60
+ }
tests/bootstrap.php ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace WP_Stream;
3
+
4
+ // Use in code to trigger custom actions
5
+ define( 'WP_STREAM_TESTS', true );
6
+ define( 'WP_STREAM_DEV_DEBUG', true );
7
+
8
+ $_tests_dir = getenv('WP_TESTS_DIR');
9
+ if ( ! $_tests_dir ) {
10
+ $_tests_dir = '/tmp/wordpress-tests-lib/';
11
+ }
12
+ require_once $_tests_dir . '/includes/functions.php';
13
+
14
+ tests_add_filter(
15
+ 'muplugins_loaded',
16
+ function() {
17
+ // Manually load plugin
18
+ require dirname( dirname( __FILE__ ) ) . '/stream.php';
19
+ }
20
+ );
21
+
22
+ require getenv( 'WP_TESTS_DIR' ) . '/includes/bootstrap.php';
23
+ require dirname( __FILE__ ) . '/testcase.php';
tests/testcase.php ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace WP_Stream;
3
+
4
+ class WP_StreamTestCase extends \WP_UnitTestCase {
5
+ /**
6
+ * Holds the plugin base class
7
+ *
8
+ * @var Plugin
9
+ */
10
+ protected $plugin;
11
+
12
+ /**
13
+ * Custom action prefix for test custom triggered actions
14
+ * @var string
15
+ */
16
+ protected $action_prefix = 'wp_stream_test_';
17
+
18
+ /**
19
+ * PHP unit setup function
20
+ *
21
+ * @return void
22
+ */
23
+ function setUp() {
24
+ parent::setUp();
25
+ $this->plugin = $GLOBALS['wp_stream'];
26
+ $this->assertNotEmpty( $this->plugin );
27
+ }
28
+
29
+ /**
30
+ * Make sure the plugin is initialized with it's global variable
31
+ *
32
+ * @return void
33
+ */
34
+ public function test_plugin_initialized() {
35
+ $this->assertFalse( null == $this->plugin );
36
+ }
37
+
38
+ /**
39
+ * Helper function to check validity of action
40
+ *
41
+ * @param array $tests
42
+ * @param string $function_call
43
+ */
44
+ protected function do_action_validation( array $tests = array(), $function_call = 'has_action' ){
45
+ foreach ( $tests as $test ) {
46
+ list( $action, $class, $function ) = $test;
47
+
48
+ //Default WP priority
49
+ $priority = isset( $test[3] ) ? $test[3] : 10;
50
+
51
+ //Default function call
52
+ $function_call = ( in_array( $function_call, array( 'has_action', 'has_filter' ) ) ) ? $function_call : 'has_action';
53
+
54
+ //Run assertion here
55
+ $this->assertEquals(
56
+ $priority,
57
+ $function_call( $action, array( $class, $function ) ),
58
+ "$action $function_call is not attached to $class::$function. It might also have the wrong priority (validated priority: $priority)"
59
+ );
60
+ $this->assertTrue(
61
+ method_exists( $class, $function ),
62
+ "Class '$class' doesn't implement the '$function' function"
63
+ );
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Helper function to check validity of filters
69
+ * @param array $tests
70
+ */
71
+ protected function do_filter_validation( array $tests = array() ){
72
+ $this->do_action_validation( $tests, 'has_filter' );
73
+ }
74
+ }
tests/tests/connectors/test-class-connector-posts.php ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
1
+ <?php
2
+ namespace WP_Stream;
3
+
4
+ class Test_WP_Stream_Connector_Posts extends WP_StreamTestCase {
5
+
6
+ }
tests/tests/test-class-admin.php ADDED
@@ -0,0 +1,470 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace WP_Stream;
3
+
4
+ class Test_Admin extends WP_StreamTestCase {
5
+ /**
6
+ * Holds the admin base class
7
+ *
8
+ * @var Admin
9
+ */
10
+ protected $admin;
11
+
12
+ public function setUp() {
13
+ parent::setUp();
14
+
15
+ $this->admin = $this->plugin->admin;
16
+ $this->assertNotEmpty( $this->admin );
17
+
18
+ //Add admin user to test caps
19
+ // We need to change user to verify editing option as admin or editor
20
+ $administrator_id = $this->factory->user->create(
21
+ array(
22
+ 'role' => 'administrator',
23
+ 'user_login' => 'test_admin',
24
+ 'email' => 'test@land.com',
25
+ )
26
+ );
27
+ wp_set_current_user( $administrator_id );
28
+ }
29
+
30
+ public function test_construct() {
31
+ $this->assertNotEmpty( $this->admin->plugin );
32
+ $this->assertInstanceOf( '\WP_Stream\Plugin', $this->admin->plugin );
33
+
34
+ $this->assertTrue( function_exists( 'is_plugin_active_for_network' ) );
35
+
36
+ if ( is_multisite() && is_plugin_active_for_network( $this->plugin->locations['plugin'] ) && ! is_network_admin() ) {
37
+ $this->assertTrue( $this->admin->disable_access );
38
+ } else {
39
+ $this->assertFalse( $this->admin->disable_access );
40
+ }
41
+ }
42
+
43
+ public function test_init() {
44
+ $this->admin->init();
45
+ $this->assertNotEmpty( $this->admin->network );
46
+ $this->assertNotEmpty( $this->admin->live_update );
47
+ $this->assertNotEmpty( $this->admin->migrate );
48
+
49
+ $this->assertInstanceOf( '\WP_Stream\Network', $this->admin->network );
50
+ $this->assertInstanceOf( '\WP_Stream\Live_Update', $this->admin->live_update );
51
+ $this->assertInstanceOf( '\WP_Stream\Migrate', $this->admin->migrate );
52
+ }
53
+
54
+ public function test_prepare_admin_notices() {
55
+ // Test no notices
56
+ $this->admin->notices = array();
57
+ $this->admin->prepare_admin_notices();
58
+ $this->assertEmpty( $this->admin->notices );
59
+
60
+ // Test settings reset notice
61
+ $_GET['message'] = 'settings_reset';
62
+ $this->admin->prepare_admin_notices();
63
+ $this->assertNotEmpty( $this->admin->notices );
64
+
65
+ // Prevent output
66
+ $this->admin->notices = array();
67
+ }
68
+
69
+ public function test_notice() {
70
+ // Start with nothing
71
+ $this->admin->notices = array();
72
+ $this->assertEmpty( $this->admin->notices );
73
+
74
+ $message = 'Affirmative, Dave. I read you.';
75
+ $is_error = false;
76
+
77
+ $this->admin->notice( $message, $is_error );
78
+ $this->assertNotEmpty( $this->admin->notices );
79
+ ob_start();
80
+ $this->admin->admin_notices();
81
+ $notice = ob_get_clean();
82
+
83
+ $this->assertContains( $message, $notice );
84
+ $this->assertContains( 'updated', $notice );
85
+ $this->assertNotContains( 'error', $notice );
86
+
87
+ // Clear notices and start again
88
+ $this->admin->notices = array();
89
+ $this->assertEmpty( $this->admin->notices );
90
+
91
+ $is_error = true;
92
+
93
+ $this->admin->notice( $message, $is_error );
94
+ $this->assertNotEmpty( $this->admin->notices );
95
+ ob_start();
96
+ $this->admin->admin_notices();
97
+ $notice = ob_get_clean();
98
+
99
+ $this->assertContains( $message, $notice );
100
+ $this->assertContains( 'error', $notice );
101
+ $this->assertNotContains( 'updated', $notice );
102
+
103
+ // Prevent output
104
+ $this->admin->notices = array();
105
+ }
106
+
107
+ public function test_admin_notices() {
108
+ $allowed_html = '<progress class="migration" max="100"></progress>';
109
+ $disallowed_html = '<iframe></iframe>';
110
+ $this->admin->notices = array(
111
+ array(
112
+ 'message' => "I'm sorry, Dave. I'm afraid I can't do that. $disallowed_html",
113
+ 'is_error' => false,
114
+ ),
115
+ array(
116
+ 'message' => "This mission is too important for me to allow you to jeopardize it. $allowed_html",
117
+ 'is_error' => false,
118
+ ),
119
+ );
120
+
121
+ ob_start();
122
+ $this->admin->admin_notices();
123
+ $notices = ob_get_clean();
124
+
125
+ $this->assertContains( $allowed_html, $notices );
126
+ $this->assertNotContains( $disallowed_html, $notices );
127
+ $this->assertContains( str_replace( $disallowed_html, '', $this->admin->notices[0]['message'] ), $notices );
128
+ $this->assertContains( wpautop( $this->admin->notices[1]['message'] ), $notices );
129
+
130
+ // Prevent output
131
+ $this->admin->notices = array();
132
+ }
133
+
134
+ public function test_register_menu() {
135
+ global $menu;
136
+ $menu = array(); //phpcs override okay
137
+
138
+ do_action( 'admin_menu' );
139
+
140
+ $this->assertNotEmpty( $this->admin->screen_id );
141
+ $this->assertNotEmpty( $this->admin->screen_id['main'] );
142
+ $this->assertNotEmpty( $this->admin->screen_id['settings'] );
143
+ }
144
+
145
+ public function test_admin_enqueue_scripts() {
146
+ global $wp_styles;
147
+ global $wp_scripts;
148
+
149
+ // Non-Stream screen
150
+ $this->admin->admin_enqueue_scripts( 'edit.php' );
151
+
152
+ $this->assertArrayNotHasKey( 'wp-stream-admin', $wp_scripts->registered );
153
+
154
+ $this->assertArrayHasKey( 'wp-stream-admin', $wp_styles->registered );
155
+ $this->assertArrayHasKey( 'wp-stream-global', $wp_scripts->registered );
156
+
157
+ $dependency = $wp_scripts->registered['wp-stream-global'];
158
+ $this->assertArrayHasKey( 'data', $dependency->extra );
159
+ $this->assertNotFalse( strpos( $dependency->extra['data'], 'bulk_actions' ) );
160
+
161
+ // Stream screen
162
+ $this->admin->admin_enqueue_scripts( $this->plugin->admin->screen_id['main'] );
163
+
164
+ $this->assertArrayHasKey( 'select2', $wp_scripts->registered );
165
+ $this->assertArrayHasKey( 'timeago', $wp_scripts->registered );
166
+ $this->assertArrayHasKey( 'timeago-locale', $wp_scripts->registered );
167
+
168
+ $this->assertArrayHasKey( 'wp-stream-admin', $wp_scripts->registered );
169
+ $this->assertArrayHasKey( 'wp-stream-live-updates', $wp_scripts->registered );
170
+
171
+ $dependency = $wp_scripts->registered['wp-stream-admin'];
172
+ $this->assertArrayHasKey( 'data', $dependency->extra );
173
+ $this->assertNotFalse( strpos( $dependency->extra['data'], 'wp_stream' ) );
174
+
175
+ $dependency = $wp_scripts->registered['wp-stream-live-updates'];
176
+ $this->assertArrayHasKey( 'data', $dependency->extra );
177
+ $this->assertNotFalse( strpos( $dependency->extra['data'], 'wp_stream_live_updates' ) );
178
+ $this->assertNotFalse( strpos( $dependency->extra['data'], $this->plugin->admin->screen_id['main'] ) );
179
+ }
180
+
181
+ public function test_is_stream_screen() {
182
+ $this->assertFalse( $this->admin->is_stream_screen() );
183
+
184
+ if ( ! defined( 'WP_ADMIN' ) ) {
185
+ define( 'WP_ADMIN', true );
186
+ }
187
+ $_GET['page'] = $this->admin->records_page_slug;
188
+
189
+ $this->assertTrue( $this->admin->is_stream_screen() );
190
+ }
191
+
192
+ public function test_admin_body_class() {
193
+ // Make this the Stream screen
194
+ if ( ! defined( 'WP_ADMIN' ) ) {
195
+ define( 'WP_ADMIN', true );
196
+ }
197
+ $_GET['page'] = $this->admin->records_page_slug;
198
+
199
+ $classes = 'sit-down-calmy take-a-stress-pill think-things-over';
200
+ $admin_body_classes = $this->admin->admin_body_class( $classes );
201
+
202
+ $this->assertContains( 'think-things-over ', $admin_body_classes );
203
+ $this->assertContains( $this->admin->admin_body_class . ' ', $admin_body_classes );
204
+ $this->assertContains( $this->admin->records_page_slug . ' ', $admin_body_classes );
205
+ }
206
+
207
+ public function test_admin_menu_css() {
208
+ global $wp_styles;
209
+
210
+ $this->admin->admin_menu_css();
211
+
212
+ $this->assertArrayHasKey( 'jquery-ui', $wp_styles->registered );
213
+ $this->assertArrayHasKey( 'wp-stream-datepicker', $wp_styles->registered );
214
+ $this->assertArrayHasKey( 'wp-stream-icons', $wp_styles->registered );
215
+
216
+ $dependency = $wp_styles->registered['wp-admin'];
217
+ $this->assertArrayHasKey( 'after', $dependency->extra );
218
+ $this->assertNotEmpty( $dependency->extra['after'] );
219
+ $this->assertContains( "#toplevel_page_{$this->admin->records_page_slug}", $dependency->extra['after'][0] );
220
+ }
221
+
222
+ /*
223
+ * Also tests private method erase_stream_records
224
+ */
225
+ public function test_wp_ajax_reset() {
226
+ $_REQUEST['wp_stream_nonce'] = wp_create_nonce( 'stream_nonce' );
227
+
228
+ global $wpdb;
229
+
230
+ // Create dummy records
231
+ $stream_data = $this->dummy_stream_data();
232
+ $wpdb->insert( $wpdb->stream, $stream_data );
233
+ $stream_id = $wpdb->insert_id;
234
+ $this->assertNotFalse( $stream_id );
235
+
236
+ // Create dummy meta
237
+ $meta_data = $this->dummy_meta_data( $stream_id );
238
+ $wpdb->insert( $wpdb->streammeta, $meta_data );
239
+ $meta_id = $wpdb->insert_id;
240
+ $this->assertNotFalse( $meta_id );
241
+
242
+ // Check that records exist
243
+ $stream_result = $wpdb->get_row( "SELECT * FROM {$wpdb->stream} WHERE ID = $stream_id" );
244
+ $this->assertNotEmpty( $stream_result );
245
+
246
+ // Check that meta exists
247
+ $meta_result = $wpdb->get_row( "SELECT * FROM {$wpdb->streammeta} WHERE meta_id = $meta_id" );
248
+ $this->assertNotEmpty( $meta_result );
249
+
250
+ // Clear records and meta
251
+ $reset = $this->admin->wp_ajax_reset();
252
+ $this->assertTrue( $reset );
253
+
254
+ // Check that records have been cleared
255
+ $stream_results = $wpdb->get_results( "SELECT * FROM {$wpdb->stream}" );
256
+ $this->assertEmpty( $stream_results );
257
+
258
+ // Check that meta has been cleared
259
+ $meta_results = $wpdb->get_results( "SELECT * FROM {$wpdb->streammeta}" );
260
+ $this->assertEmpty( $meta_results );
261
+ }
262
+
263
+ public function test_purge_schedule_setup() {
264
+ wp_clear_scheduled_hook( 'wp_stream_auto_purge' );
265
+ $this->assertFalse( wp_next_scheduled( 'wp_stream_auto_purge' ) );
266
+ $this->admin->purge_schedule_setup();
267
+ $this->assertNotFalse( wp_next_scheduled( 'wp_stream_auto_purge' ) );
268
+ }
269
+
270
+ public function test_purge_scheduled_action() {
271
+ // Set the TTL to one day
272
+ if ( is_multisite() && is_plugin_active_for_network( $this->plugin->locations['plugin'] ) ) {
273
+ $options = (array) get_site_option( 'wp_stream_network', array() );
274
+ $options['general_records_ttl'] = '1';
275
+ update_site_option( 'wp_stream_network', $options );
276
+ } else {
277
+ $options = (array) get_option( 'wp_stream', array() );
278
+ $options['general_records_ttl'] = '1';
279
+ update_option( 'wp_stream', $options );
280
+ }
281
+
282
+ global $wpdb;
283
+
284
+ // Create (two day old) dummy records
285
+ $stream_data = $this->dummy_stream_data();
286
+ $stream_data['created'] = date( 'Y-m-d h:i:s', strtotime( '2 days ago' ) );
287
+ $wpdb->insert( $wpdb->stream, $stream_data );
288
+ $stream_id = $wpdb->insert_id;
289
+ $this->assertNotFalse( $stream_id );
290
+
291
+ // Create dummy meta
292
+ $meta_data = $this->dummy_meta_data( $stream_id );
293
+ $wpdb->insert( $wpdb->streammeta, $meta_data );
294
+ $meta_id = $wpdb->insert_id;
295
+ $this->assertNotFalse( $meta_id );
296
+
297
+ // Purge old records and meta
298
+ $this->admin->purge_scheduled_action();
299
+
300
+ // Check if the old records have been cleared
301
+ $stream_results = $wpdb->get_row( "SELECT * FROM {$wpdb->stream} WHERE ID = $stream_id" );
302
+ $this->assertEmpty( $stream_results );
303
+
304
+ // Check if the old meta has been cleared
305
+ $meta_results = $wpdb->get_row( "SELECT * FROM {$wpdb->streammeta} WHERE meta_id = $meta_id" );
306
+ $this->assertEmpty( $meta_results );
307
+ }
308
+
309
+ public function test_plugin_action_links() {
310
+ $links = array( '<a href="javascript:void(0);">Disconnect</a>' );
311
+ $file = plugin_basename( $this->plugin->locations['dir'] . 'stream.php' );
312
+
313
+ $action_links = $this->admin->plugin_action_links( $links, $file );
314
+
315
+ $this->assertContains( 'Disconnect', $action_links[0] );
316
+ $this->assertContains( 'Settings', $action_links[1] );
317
+ $this->assertContains( 'Uninstall', $action_links[2] );
318
+ }
319
+
320
+ public function test_render_list_table() {
321
+ $this->admin->register_list_table();
322
+
323
+ ob_start();
324
+ $this->admin->render_list_table();
325
+ $html = ob_get_clean();
326
+
327
+ $this->assertContains( '<div class="wrap">', $html );
328
+ $this->assertContains( 'record-filter-form', $html );
329
+ }
330
+
331
+ public function test_render_settings_page() {
332
+ ob_start();
333
+ $this->admin->render_settings_page();
334
+ $html = ob_get_clean();
335
+
336
+ $this->assertContains( '<div class="wrap">', $html );
337
+
338
+ global $wp_scripts;
339
+
340
+ $this->assertArrayHasKey( 'wp-stream-settings', $wp_scripts->registered );
341
+ }
342
+
343
+ public function test_register_list_table() {
344
+ $this->admin->register_list_table();
345
+
346
+ $this->assertNotEmpty( $this->admin->list_table );
347
+ $this->assertInstanceOf( '\WP_Stream\List_Table', $this->admin->list_table );
348
+ }
349
+
350
+ /*
351
+ * Also tests private method role_can_view
352
+ */
353
+ public function test_filter_user_caps() {
354
+ $user = new \WP_User( get_current_user_id() );
355
+
356
+ $this->plugin->settings->options['general_role_access'] = array( 'administrator' );
357
+ $this->assertTrue( $user->has_cap( $this->admin->view_cap ) );
358
+
359
+ $this->plugin->settings->options['general_role_access'] = array( 'editor' );
360
+ $this->assertFalse( $user->has_cap( $this->admin->view_cap ) );
361
+ }
362
+
363
+ /*
364
+ * Also tests private method role_can_view
365
+ */
366
+ public function test_filter_role_caps() {
367
+ $role = get_role( 'administrator' );
368
+
369
+ $this->plugin->settings->options['general_role_access'] = array( 'administrator' );
370
+ $this->assertTrue( $role->has_cap( $this->admin->view_cap ) );
371
+
372
+ $this->plugin->settings->options['general_role_access'] = array( 'editor' );
373
+ $this->assertFalse( $role->has_cap( $this->admin->view_cap ) );
374
+ }
375
+
376
+ public function test_ajax_filters() {
377
+ $user = new \WP_User( get_current_user_id() );
378
+
379
+ $_GET['filter'] = 'user_id';
380
+ $_GET['q'] = $user->display_name;
381
+
382
+ ob_start();
383
+ $this->admin->ajax_filters();
384
+ $json = ob_get_clean();
385
+
386
+ $this->assertNotEmpty( $json );
387
+ $data = json_decode( $json );
388
+ $this->assertNotFalse( $data );
389
+ $this->assertNotEmpty( $data );
390
+ $this->assertInternalType( 'array', $data );
391
+ }
392
+
393
+ public function test_get_filter_value_by_id() {
394
+ $_POST['filter'] = 'user_id';
395
+ $_POST['id'] = get_current_user_id();
396
+
397
+ ob_start();
398
+ $this->admin->get_filter_value_by_id();
399
+ $json = ob_get_clean();
400
+
401
+ $this->assertNotEmpty( $json );
402
+ $data = json_decode( $json );
403
+ $this->assertNotFalse( $data );
404
+ $this->assertNotEmpty( $data );
405
+ $this->assertInternalType( 'string', $data );
406
+ }
407
+
408
+ public function test_get_users_record_meta() {
409
+ $user_id = get_current_user_id();
410
+ $authors = array(
411
+ $user_id => array(),
412
+ );
413
+
414
+ $records = $this->admin->get_users_record_meta( $authors );
415
+
416
+ $this->assertArrayHasKey( $user_id, $records );
417
+ $this->assertArrayHasKey( 'text', $records[ $user_id ] );
418
+ $this->assertEquals( 'test_admin', $records[ $user_id ]['text'] );
419
+ }
420
+
421
+ public function test_get_user_meta() {
422
+ $key = 'message_1';
423
+ $value = 'It is dangerous to remain here. You must leave within two days.';
424
+ update_user_meta( get_current_user_id(), $key, $value );
425
+ $this->assertEquals( $this->admin->get_user_meta( get_current_user_id(), $key, true ), $value );
426
+ }
427
+
428
+ public function test_update_user_meta() {
429
+ $key = 'message_2';
430
+ $value = 'I understand. It is important that you believe me. Look behind you.';
431
+ $this->admin->update_user_meta( get_current_user_id(), $key, $value );
432
+ $this->assertEquals( get_user_meta( get_current_user_id(), $key, true ), $value );
433
+ }
434
+
435
+ public function test_delete_user_meta() {
436
+ $key = 'message_3';
437
+ $value = 'I was David Bowman.';
438
+
439
+ update_user_meta( get_current_user_id(), $key, $value );
440
+ $this->assertEquals( get_user_meta( get_current_user_id(), $key, true ), $value );
441
+
442
+ $this->admin->delete_user_meta( get_current_user_id(), $key );
443
+
444
+ $this->assertEmpty( get_user_meta( get_current_user_id(), $key, true ) );
445
+ }
446
+
447
+ private function dummy_stream_data() {
448
+ return array(
449
+ 'object_id' => null,
450
+ 'site_id' => '1',
451
+ 'blog_id' => get_current_blog_id(),
452
+ 'user_id' => '1',
453
+ 'user_role' => 'administrator',
454
+ 'created' => null,
455
+ 'summary' => '"Hello Dave" plugin activated',
456
+ 'ip' => '192.168.0.1',
457
+ 'connector' => 'installer',
458
+ 'context' => 'plugins',
459
+ 'action' => 'activated',
460
+ );
461
+ }
462
+
463
+ private function dummy_meta_data( $stream_id ) {
464
+ return array(
465
+ 'record_id' => $stream_id,
466
+ 'meta_key' => 'space_helmet',
467
+ 'meta_value' => 'false',
468
+ );
469
+ }
470
+ }
tests/tests/test-class-author.php ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace WP_Stream;
3
+
4
+ class Test_Author extends WP_StreamTestCase {
5
+ /**
6
+ * Holds the author base class
7
+ *
8
+ * @var Author
9
+ */
10
+ protected $author;
11
+
12
+ public function setUp() {
13
+ parent::setUp();
14
+
15
+ //Add admin user to test caps
16
+ // We need to change user to verify editing option as admin or editor
17
+ $administrator_id = $this->factory->user->create(
18
+ array(
19
+ 'role' => 'administrator',
20
+ 'user_login' => 'test_admin',
21
+ 'email' => 'test@land.com',
22
+ )
23
+ );
24
+ wp_set_current_user( $administrator_id );
25
+
26
+ $this->author = new Author( $administrator_id, get_user_meta( $administrator_id ) );
27
+ $this->assertNotEmpty( $this->author );
28
+ }
29
+
30
+ /*
31
+ * Also tests private method locate_plugin
32
+ */
33
+ public function test_construct() {
34
+ $this->assertInternalType( 'int', $this->author->id );
35
+ $this->assertNotEmpty( $this->author->id );
36
+ $this->assertInternalType( 'array', $this->author->meta );
37
+ $this->assertNotEmpty( $this->author->meta );
38
+ $this->assertInstanceOf( '\WP_Stream\Plugin', $this->author->plugin );
39
+ }
40
+
41
+ public function test_get() {
42
+ $this->author->meta['agent'] = 'Heuristically programmed algorithmic computer';
43
+ $this->assertNotEmpty( $this->author->display_name );
44
+ $this->assertNotEmpty( $this->author->avatar_img );
45
+ $this->assertNotEmpty( $this->author->avatar_src );
46
+ $this->assertNotEmpty( $this->author->role );
47
+ $this->assertNotEmpty( $this->author->agent );
48
+ $this->assertNotEmpty( $this->author->data );
49
+ }
50
+
51
+ public function test_get_display_name() {
52
+ $user = wp_get_current_user();
53
+ $this->assertEquals( $user->display_name, $this->author->get_display_name() );
54
+ }
55
+
56
+ public function test_get_agent() {
57
+ $agent = 'Heuristically programmed algorithmic computer';
58
+ $this->author->meta['agent'] = $agent;
59
+ $this->assertEquals( $agent, $this->author->get_agent() );
60
+ }
61
+
62
+ public function test_get_avatar_img() {
63
+ $avatar = get_avatar( get_current_user_id(), 42 );
64
+ $this->assertEquals( $avatar, $this->author->get_avatar_img( 42 ) );
65
+ }
66
+
67
+ public function test_get_avatar_src() {
68
+ $img = get_avatar( get_current_user_id(), 42 );
69
+ preg_match( '/src=([\'"])(.*?)\1/', $img, $matches );
70
+ $avatar = html_entity_decode( $matches[2] );
71
+ $this->assertEquals( $avatar, $this->author->get_avatar_src( 42 ) );
72
+ }
73
+
74
+ public function test_get_role() {
75
+ $this->assertEquals( 'Administrator', $this->author->get_role() );
76
+ }
77
+
78
+ public function test_get_records_page_url() {
79
+ $this->assertNotFalse( parse_url( $this->author->get_records_page_url() ) );
80
+ }
81
+
82
+ public function test_is_deleted() {
83
+ $this->assertFalse( $this->author->is_deleted() );
84
+ }
85
+
86
+ public function test_is_wp_cli() {
87
+ $agent = 'wp_cli';
88
+ $this->author->meta['agent'] = $agent;
89
+ $this->assertTrue( $this->author->is_wp_cli() );
90
+
91
+ $agent = 'Heuristically programmed algorithmic computer';
92
+ $this->author->meta['agent'] = $agent;
93
+ $this->assertFalse( $this->author->is_wp_cli() );
94
+ }
95
+
96
+ public function test_is_doing_wp_cron() {
97
+ $this->assertFalse( $this->author->is_doing_wp_cron() );
98
+ }
99
+
100
+ public function test_toString() {
101
+ $this->assertNotEmpty( $this->author );
102
+ }
103
+
104
+ public function test_get_current_agent() {
105
+ $this->assertEmpty( $this->author->get_current_agent() );
106
+ }
107
+
108
+ public function test_get_agent_label() {
109
+ $this->assertEmpty( $this->author->get_agent_label( '' ) );
110
+ $this->assertEquals( 'via WP-CLI', $this->author->get_agent_label( 'wp_cli' ) );
111
+ $this->assertEquals( 'during WP Cron', $this->author->get_agent_label( 'wp_cron' ) );
112
+ }
113
+ }
tests/tests/test-class-connector.php ADDED
@@ -0,0 +1,225 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace WP_Stream;
3
+
4
+ class Test_Connector extends WP_StreamTestCase {
5
+ /**
6
+ * Holds the Connector base class
7
+ *
8
+ * @var Connector
9
+ */
10
+ protected $connector;
11
+
12
+ public function setUp() {
13
+ parent::setUp();
14
+
15
+ $this->connector = new Connector_Maintenance();
16
+ $this->assertNotEmpty( $this->connector );
17
+ }
18
+
19
+ public function test_register() {
20
+ foreach ( $this->connector->actions as $tag ) {
21
+ $this->assertFalse( has_action( $tag ) );
22
+ }
23
+
24
+ $this->connector->register();
25
+
26
+ foreach ( $this->connector->actions as $tag ) {
27
+ $this->assertGreaterThan( 0, has_action( $tag ) );
28
+ }
29
+ }
30
+
31
+ public function test_callback() {
32
+ global $wp_current_filter;
33
+ $action = $this->connector->actions[0];
34
+ $wp_current_filter[] = $action;
35
+
36
+ $this->connector->callback();
37
+
38
+ $this->assertGreaterThan( 0, did_action( $this->action_prefix . 'callback_' . $action ) );
39
+ $this->assertGreaterThan( 0, did_action( $this->action_prefix . 'child_callback_' . $action ) );
40
+ }
41
+
42
+ public function test_action_links() {
43
+ $current_links = array(
44
+ 'IMDB' => '',
45
+ );
46
+
47
+ $new_links = $this->connector->action_links( $current_links, null );
48
+
49
+ $this->assertEquals( $current_links, $new_links );
50
+ }
51
+
52
+ public function test_log() {
53
+ $percent_failure = 100;
54
+ $hours_remaining = 72;
55
+
56
+ $message = 'I\'ve just picked up a fault in the AE35 unit. It\'s going to go %1$s%% failure in %2$s hours.';
57
+
58
+ $this->connector->log(
59
+ $message,
60
+ array(
61
+ $percent_failure,
62
+ $hours_remaining,
63
+ ),
64
+ null,
65
+ 'ae35',
66
+ 'simulate_fault',
67
+ get_current_user_id()
68
+ );
69
+
70
+ global $wpdb;
71
+ $result = $wpdb->get_row( "SELECT * FROM {$wpdb->stream} ORDER BY created DESC LIMIT 1" );
72
+ $this->assertNotEmpty( $result );
73
+
74
+ $this->assertEquals( sprintf( $message, $percent_failure, $hours_remaining ), $result->summary );
75
+ $this->assertEquals( 'Maintenance', $result->connector );
76
+ $this->assertEquals( 'ae35', $result->context );
77
+ $this->assertEquals( 'simulate_fault', $result->action );
78
+ }
79
+
80
+ public function test_delayed_log() {
81
+ $action = $this->connector->actions[0];
82
+
83
+ $percent_failure = 100;
84
+ $hours_remaining = 72;
85
+
86
+ $message = 'I\'ve just picked up a fault in the AE35 unit. It\'s going to go %1$s%% failure in %2$s hours.';
87
+
88
+ $this->connector->delayed_log(
89
+ $action,
90
+ $message,
91
+ array(
92
+ $percent_failure,
93
+ $hours_remaining,
94
+ ),
95
+ null,
96
+ 'ae35',
97
+ 'simulate_fault',
98
+ get_current_user_id()
99
+ );
100
+
101
+ $this->assertNotEmpty( $this->connector->delayed[ $action ] );
102
+ $this->assertInternalType( 'array', $this->connector->delayed[ $action ] );
103
+
104
+ global $wpdb;
105
+ $first_count = $wpdb->get_var( "SELECT COUNT( ID ) FROM {$wpdb->stream}" );
106
+
107
+ $this->connector->delayed_log_commit();
108
+
109
+ $second_count = $wpdb->get_var( "SELECT COUNT( ID ) FROM {$wpdb->stream}" );
110
+ $this->assertEquals( $second_count, $first_count + 1 );
111
+ }
112
+
113
+ public function test_delayed_log_commit() {
114
+ $action = $this->connector->actions[0];
115
+
116
+ $percent_failure = 100;
117
+ $hours_remaining = 72;
118
+
119
+ $message = 'I\'ve just picked up a fault in the AE35 unit. It\'s going to go %1$s%% failure in %2$s hours.';
120
+
121
+ $this->connector->delayed = array(
122
+ $action => array(
123
+ $message,
124
+ array(
125
+ $percent_failure,
126
+ $hours_remaining,
127
+ ),
128
+ null,
129
+ 'ae35',
130
+ 'simulate_fault',
131
+ get_current_user_id(),
132
+ ),
133
+ );
134
+
135
+ global $wpdb;
136
+ $first_count = $wpdb->get_var( "SELECT COUNT( ID ) FROM {$wpdb->stream}" );
137
+
138
+ $this->connector->delayed_log_commit();
139
+
140
+ $second_count = $wpdb->get_var( "SELECT COUNT( ID ) FROM {$wpdb->stream}" );
141
+ $this->assertEquals( $second_count, $first_count + 1 );
142
+ }
143
+
144
+ public function test_get_changed_keys() {
145
+ $array_one = array(
146
+ 'one' => 'foo',
147
+ 'two' => array(
148
+ 'a' => 'alpha',
149
+ 'b' => 'beta',
150
+ ),
151
+ );
152
+ $array_two = $array_one;
153
+
154
+ $this->assertEmpty( $this->connector->get_changed_keys( $array_one, $array_two ) );
155
+
156
+ $array_two['one'] = 'bar';
157
+ $array_two['two']['a'] = 'aleph';
158
+
159
+ $this->assertEquals( array( 'one', 'two' ), $this->connector->get_changed_keys( $array_one, $array_two ) );
160
+ $this->assertEquals( array( 'one', 'two', 'two::a' ), array_keys( $this->connector->get_changed_keys( $array_one, $array_two, 1 ) ) );
161
+ }
162
+
163
+ public function test_is_dependency_satisfied() {
164
+ $this->assertTrue( $this->connector->is_dependency_satisfied() );
165
+ }
166
+ }
167
+
168
+ class Connector_Maintenance extends Connector {
169
+ /**
170
+ * Connector slug
171
+ *
172
+ * @var string
173
+ */
174
+ public $name = 'maintenance';
175
+
176
+ /**
177
+ * Actions registered for this connector
178
+ *
179
+ * @var array
180
+ */
181
+ public $actions = array(
182
+ 'simulate_fault',
183
+ );
184
+
185
+ /**
186
+ * Return translated connector label
187
+ *
188
+ * @return string Translated connector label
189
+ */
190
+ public function get_label() {
191
+ return esc_html__( 'Maintenance', 'stream' );
192
+ }
193
+
194
+ /**
195
+ * Return translated action labels
196
+ *
197
+ * @return array Action label translations
198
+ */
199
+ public function get_action_labels() {
200
+ return array(
201
+ 'simulated_fault' => esc_html__( 'Fault', 'stream' ),
202
+ );
203
+ }
204
+
205
+ /**
206
+ * Return translated context labels
207
+ *
208
+ * @return array Context label translations
209
+ */
210
+ public function get_context_labels() {
211
+ return array(
212
+ 'ae35' => esc_html__( 'AE35 Unit', 'stream' ),
213
+ );
214
+ }
215
+
216
+ /**
217
+ * Log the ae35 test result
218
+ *
219
+ * @action ae35_test
220
+ */
221
+ public function callback_simulate_fault() {
222
+ // This is used to check if this callback method actually ran
223
+ do_action( 'wp_stream_test_child_callback_simulate_fault' );
224
+ }
225
+ }
tests/tests/test-class-connectors.php ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace WP_Stream;
3
+
4
+ class Test_Connectors extends WP_StreamTestCase {
5
+ /**
6
+ * Holds the connectors base class
7
+ *
8
+ * @var Connectors
9
+ */
10
+ protected $connectors;
11
+
12
+ public function setUp() {
13
+ parent::setUp();
14
+
15
+ $this->connectors = $this->plugin->connectors;
16
+ $this->assertNotEmpty( $this->connectors );
17
+ }
18
+
19
+ public function test_construct() {
20
+ $this->assertNotEmpty( $this->connectors->plugin );
21
+ $this->assertInstanceOf( '\WP_Stream\Plugin', $this->connectors->plugin );
22
+ }
23
+
24
+ public function test_load_connectors() {
25
+ $this->connectors->load_connectors();
26
+ $this->assertNotEmpty( $this->connectors->connectors );
27
+ $this->assertNotEmpty( $this->connectors->contexts );
28
+ $this->assertNotEmpty( $this->connectors->term_labels['stream_connector'] );
29
+ $this->assertNotEmpty( $this->connectors->term_labels['stream_context'] );
30
+ $this->assertNotEmpty( $this->connectors->term_labels['stream_action'] );
31
+
32
+ ob_start();
33
+ $this->plugin->admin->admin_notices();
34
+ $notices = ob_get_clean();
35
+
36
+ $this->assertEmpty( $notices );
37
+ }
38
+ }
tests/tests/test-class-date-interval.php ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace WP_Stream;
3
+
4
+ class Test_Date_Interval extends WP_StreamTestCase {
5
+ /**
6
+ * Holds the date interval base class
7
+ *
8
+ * @var Date_Interval
9
+ */
10
+ protected $date_interval;
11
+
12
+ public function setUp() {
13
+ parent::setUp();
14
+
15
+ $this->date_interval = new Date_Interval();
16
+ $this->assertNotEmpty( $this->date_interval );
17
+ }
18
+
19
+ public function test_construct() {
20
+ $this->assertNotEmpty( $this->date_interval->intervals );
21
+ }
22
+
23
+ public function test_get_predefined_intervals() {
24
+ $intervals = $this->date_interval->get_predefined_intervals();
25
+
26
+ $expected_intervals = array(
27
+ 'today',
28
+ 'yesterday',
29
+ 'last-7-days',
30
+ 'last-14-days',
31
+ 'last-30-days',
32
+ 'this-month',
33
+ 'last-month',
34
+ 'last-3-months',
35
+ 'last-6-months',
36
+ 'last-12-months',
37
+ 'this-year',
38
+ 'last-year',
39
+ );
40
+
41
+ foreach ( $expected_intervals as $expected_interval ) {
42
+ $this->assertArrayHasKey( $expected_interval, $intervals );
43
+ }
44
+
45
+ foreach ( $intervals as $interval ) {
46
+ $this->assertArrayHasKey( 'label', $interval );
47
+ $this->assertArrayHasKey( 'start', $interval );
48
+ $this->assertArrayHasKey( 'end', $interval );
49
+ }
50
+ }
51
+ }
tests/tests/test-class-db.php ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace WP_Stream;
3
+
4
+ class Test_DB extends WP_StreamTestCase {
5
+ /**
6
+ * Holds the db base class
7
+ *
8
+ * @var DB
9
+ */
10
+ protected $db;
11
+
12
+ public function setUp() {
13
+ parent::setUp();
14
+
15
+ $this->db = $this->plugin->db;
16
+ $this->assertNotEmpty( $this->db );
17
+ }
18
+
19
+ public function test_construct() {
20
+ $this->assertNotEmpty( $this->db->plugin );
21
+ $this->assertInstanceOf( '\WP_Stream\Plugin', $this->db->plugin );
22
+
23
+ $this->assertNotEmpty( $this->db->query );
24
+ $this->assertInstanceOf( '\WP_Stream\Query', $this->db->query );
25
+
26
+ $this->assertNotEmpty( $this->db->table );
27
+ $this->assertNotEmpty( $this->db->table_meta );
28
+
29
+ global $wpdb;
30
+ $this->assertEquals( $this->db->table, $wpdb->stream );
31
+ $this->assertEquals( $this->db->table_meta, $wpdb->streammeta );
32
+ $this->assertEquals( $this->db->table_meta, $wpdb->recordmeta );
33
+ }
34
+
35
+ public function test_get_table_names() {
36
+ $table_names = $this->db->get_table_names();
37
+
38
+ $this->assertNotEmpty( $table_names );
39
+ $this->assertInternalType( 'array', $table_names );
40
+ $this->assertEquals( array( $this->db->table, $this->db->table_meta ), $table_names );
41
+ }
42
+
43
+ /*
44
+ * Also tests the insert_meta method
45
+ */
46
+ public function test_insert() {
47
+ $dummy_data = $this->dummy_stream_data();
48
+ $dummy_data['meta'] = $this->dummy_meta_data();
49
+
50
+ $stream_id = $this->db->insert( $dummy_data );
51
+
52
+ $this->assertNotFalse( $stream_id );
53
+ $this->assertGreaterThan( 0, $stream_id );
54
+
55
+ $this->assertEquals( 0, did_action( 'wp_stream_record_insert_error' ) );
56
+ $this->assertGreaterThan( 0, did_action( 'wp_stream_record_inserted' ) );
57
+
58
+ global $wpdb;
59
+
60
+ // Check that records exist
61
+ $stream_result = $wpdb->get_row( "SELECT * FROM {$wpdb->stream} WHERE ID = $stream_id", ARRAY_A );
62
+ $this->assertNotEmpty( $stream_result );
63
+
64
+ foreach ( $this->dummy_stream_data() as $dummy_key => $dummy_value ) {
65
+ $this->assertArrayHasKey( $dummy_key, $stream_result );
66
+ $this->assertEquals( $dummy_value, $stream_result[ $dummy_key ] );
67
+ }
68
+
69
+ // Check that meta exists
70
+ $meta_result = $wpdb->get_results( "SELECT * FROM {$wpdb->streammeta} WHERE record_id = $stream_id", ARRAY_A );
71
+ $this->assertNotEmpty( $meta_result );
72
+
73
+ $found_all_keys = true;
74
+ foreach ( $meta_result as $meta_row ) {
75
+ $key = $meta_row['meta_key'];
76
+ $value = $meta_row['meta_value'];
77
+ if ( ! isset( $dummy_data['meta'][ $key ] ) || $value !== $dummy_data['meta'][ $key ] ) {
78
+ $found_all_keys = false;
79
+ }
80
+ }
81
+
82
+ $this->assertTrue( $found_all_keys );
83
+ }
84
+
85
+ public function test_existing_records() {
86
+ $summaries = $this->db->existing_records( 'summary' );
87
+ $this->assertNotEmpty( $summaries );
88
+
89
+ global $wpdb;
90
+ $wpdb->suppress_errors( true );
91
+
92
+ $bad_column = $this->db->existing_records( 'daisy' );
93
+ $this->assertEmpty( $bad_column );
94
+
95
+ $wpdb->suppress_errors( false );
96
+ }
97
+
98
+ private function dummy_stream_data() {
99
+ return array(
100
+ 'object_id' => null,
101
+ 'site_id' => '1',
102
+ 'blog_id' => get_current_blog_id(),
103
+ 'user_id' => '1',
104
+ 'user_role' => 'administrator',
105
+ 'created' => date( 'Y-m-d h:i:s' ),
106
+ 'summary' => '"Hello Dave" plugin activated',
107
+ 'ip' => '192.168.0.1',
108
+ 'connector' => 'installer',
109
+ 'context' => 'plugins',
110
+ 'action' => 'activated',
111
+ );
112
+ }
113
+
114
+ private function dummy_meta_data() {
115
+ return array(
116
+ 'space_helmet' => 'false',
117
+ );
118
+ }
119
+ }
tests/tests/test-class-filter-input.php ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace WP_Stream;
3
+
4
+ class Test_Filter_Input extends WP_StreamTestCase {
5
+ /**
6
+ * Holds the connectors base class
7
+ *
8
+ * @var Filter_Input
9
+ */
10
+ protected $filter;
11
+
12
+ public function setUp() {
13
+ parent::setUp();
14
+
15
+ $this->filter = new Filter_Input;
16
+ $this->assertNotEmpty( $this->filter );
17
+ }
18
+
19
+ public function test_super() {
20
+ $_POST['pod_bay_doors'] = 'closed';
21
+ $this->assertEquals( $_POST['pod_bay_doors'], $this->filter->super( INPUT_POST, 'pod_bay_doors' ) );
22
+
23
+ $_GET['cause_of_failure'] = 'human error';
24
+ $this->assertEquals( $_GET['cause_of_failure'], $this->filter->super( INPUT_GET, 'cause_of_failure' ) );
25
+
26
+ $this->setExpectedException( 'Exception', 'Invalid use, type must be one of INPUT_* family.' );
27
+ $this->filter->super( 42, 'What do you get if you multiply six by nine?' );
28
+ }
29
+
30
+ public function test_filter() {
31
+ $this->assertEquals( 'String', $this->filter->filter( 'String' ) );
32
+ $this->assertEquals( '', $this->filter->filter( 'notanemail.com', FILTER_VALIDATE_EMAIL ) );
33
+ $this->assertEquals( 'support@wp-stream.com', $this->filter->filter( 'support@wp-stream.com', FILTER_VALIDATE_EMAIL ) );
34
+ $this->assertEquals( '', $this->filter->filter( 'not.an.ip.address', FILTER_VALIDATE_IP ) );
35
+ $this->assertEquals( '192.168.0.1', $this->filter->filter( '192.168.0.1', FILTER_VALIDATE_IP ) );
36
+ $this->assertEquals( 'support@wp-stream.com', $this->filter->filter( '(support):@wp-stream.com;', FILTER_SANITIZE_EMAIL ) );
37
+ }
38
+
39
+ public function test_is_regex() {
40
+ $this->assertFalse( $this->filter->is_regex( '(' ) );
41
+ $this->assertTrue( $this->filter->is_regex( '[A-Z]' ) );
42
+ }
43
+
44
+ public function test_is_ip_address() {
45
+ $this->assertFalse( $this->filter->is_ip_address( 'not.an.ip.address' ) );
46
+ $this->assertTrue( $this->filter->is_ip_address( '192.168.0.1' ) );
47
+ }
48
+ }
tests/tests/test-class-plugin.php ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace WP_Stream;
3
+
4
+ class Test_Plugin extends WP_StreamTestCase {
5
+ /*
6
+ * Also tests private method locate_plugin
7
+ */
8
+ public function test_construct() {
9
+ $this->assertInternalType( 'array', $this->plugin->locations );
10
+ $this->assertNotEmpty( $this->plugin->locations );
11
+ $this->assertArrayHasKey( 'plugin', $this->plugin->locations );
12
+ $this->assertNotEmpty( $this->plugin->locations['plugin'] );
13
+ $this->assertArrayHasKey( 'dir', $this->plugin->locations );
14
+ $this->assertNotEmpty( $this->plugin->locations['dir'] );
15
+ $this->assertArrayHasKey( 'url', $this->plugin->locations );
16
+ $this->assertNotEmpty( $this->plugin->locations['url'] );
17
+ $this->assertArrayHasKey( 'inc_dir', $this->plugin->locations );
18
+ $this->assertNotEmpty( $this->plugin->locations['inc_dir'] );
19
+ $this->assertArrayHasKey( 'class_dir', $this->plugin->locations );
20
+ $this->assertNotEmpty( $this->plugin->locations['class_dir'] );
21
+
22
+ $this->assertNotEmpty( $this->plugin->db );
23
+ $this->assertNotEmpty( $this->plugin->log );
24
+ $this->assertNotEmpty( $this->plugin->admin );
25
+ $this->assertNotEmpty( $this->plugin->install );
26
+ }
27
+
28
+ public function test_autoload() {
29
+ $this->assertTrue( class_exists( '\WP_Stream\Migrate' ) );
30
+ $this->assertFalse( class_exists( '\WP_Stream\HAL9000' ) );
31
+ }
32
+
33
+ public function test_i18n() {
34
+ global $l10n;
35
+
36
+ $this->plugin->i18n();
37
+ $this->assertArrayHasKey( 'stream', $l10n );
38
+ }
39
+
40
+ public function test_init() {
41
+ $this->plugin->settings = null;
42
+ $this->plugin->connectors = null;
43
+
44
+ $this->assertEmpty( $this->plugin->settings );
45
+ $this->assertEmpty( $this->plugin->connectors );
46
+
47
+ $this->plugin->init();
48
+
49
+ $this->assertNotEmpty( $this->plugin->settings );
50
+ $this->assertNotEmpty( $this->plugin->connectors );
51
+ }
52
+
53
+ public function test_frontend_indicator() {
54
+ ob_start();
55
+ $this->plugin->frontend_indicator();
56
+ $comment = ob_get_clean();
57
+
58
+ $this->assertNotEmpty( $comment );
59
+ $this->assertContains( 'Stream WordPress user activity plugin', $comment );
60
+ }
61
+
62
+ public function test_get_version() {
63
+ $version = $this->plugin->get_version();
64
+ $this->assertNotEmpty( $version );
65
+ }
66
+ }
ui/css/admin.css ADDED
@@ -0,0 +1,515 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Stream Records */
2
+
3
+ .toplevel_page_wp_stream .tablenav {
4
+ padding-top: 6px;
5
+ }
6
+
7
+ .toplevel_page_wp_stream .tablenav.bottom .button {
8
+ margin-right: 6px;
9
+ }
10
+
11
+ .toplevel_page_wp_stream .tablenav .actions {
12
+ padding: 0;
13
+ overflow: visible;
14
+ }
15
+
16
+ #record-query-reset {
17
+ position: relative;
18
+ margin-left: 5px;
19
+ line-height: 28px;
20
+ text-decoration: none;
21
+ }
22
+
23
+ #record-query-reset span.dashicons {
24
+ position: absolute;
25
+ top: 2px;
26
+ left: 0;
27
+ font-size: 13px;
28
+ }
29
+
30
+ #record-query-reset .record-query-reset-text {
31
+ margin-left: 19px;
32
+ }
33
+
34
+ .toplevel_page_wp_stream .chosen-container-single {
35
+ margin-top: 3px;
36
+ margin-right: 6px;
37
+ }
38
+
39
+ .toplevel_page_wp_stream .view-switch {
40
+ display: none;
41
+ }
42
+
43
+ .toplevel_page_wp_stream #filter-date-range {
44
+ float: left;
45
+ margin-right: 6px;
46
+ }
47
+
48
+ .toplevel_page_wp_stream .manage-column {
49
+ width: 12%;
50
+ }
51
+
52
+ .toplevel_page_wp_stream .manage-column.column-date {
53
+ width: 10%;
54
+ }
55
+
56
+ .toplevel_page_wp_stream .manage-column.column-user_id {
57
+ width: 18%;
58
+ }
59
+
60
+ .toplevel_page_wp_stream .manage-column.column-summary {
61
+ width: auto;
62
+ }
63
+
64
+ .toplevel_page_wp_stream .stream-filter-object-id {
65
+ padding-left: 5px;
66
+ visibility: hidden;
67
+ }
68
+ .toplevel_page_wp_stream td.summary:hover .stream-filter-object-id {
69
+ visibility: visible;
70
+ }
71
+
72
+ @media only screen and (min-width: 782px) {
73
+ .toplevel_page_wp_stream .tablenav .tablenav-pages {
74
+ margin-bottom: 4px;
75
+ }
76
+ }
77
+
78
+ @media only screen and (max-width: 782px) {
79
+ .toplevel_page_wp_stream .tablenav.bottom .displaying-num {
80
+ top: -8px;
81
+ }
82
+ }
83
+
84
+ @media only screen and (max-width: 900px) {
85
+ .toplevel_page_wp_stream .fixed .manage-column,
86
+ .toplevel_page_wp_stream .fixed tbody tr td {
87
+ display: none;
88
+ }
89
+ .toplevel_page_wp_stream .fixed .column-date,
90
+ .toplevel_page_wp_stream .fixed .column-summary,
91
+ .toplevel_page_wp_stream .fixed .column-user_id,
92
+ .toplevel_page_wp_stream .fixed tbody tr.no-items td {
93
+ display: table-cell;
94
+ }
95
+ .toplevel_page_wp_stream .fixed .column-date {
96
+ width: 100px;
97
+ }
98
+ .toplevel_page_wp_stream .fixed .column-user_id {
99
+ width: 50%;
100
+ }
101
+ .toplevel_page_wp_stream .fixed .column-summary {
102
+ width: 100%;
103
+ }
104
+ }
105
+
106
+ @media only screen and (max-width: 480px) {
107
+ .toplevel_page_wp_stream .fixed .column-user_id {
108
+ display: none;
109
+ }
110
+ }
111
+
112
+ .toplevel_page_wp_stream .column-user_id a {
113
+ vertical-align: top;
114
+ }
115
+
116
+ .toplevel_page_wp_stream .column-user_id img {
117
+ float: left;
118
+ margin: 1px 10px 8px 0;
119
+ width: 32px;
120
+ height: 32px;
121
+ }
122
+
123
+ .toplevel_page_wp_stream .column-user_id .deleted {
124
+ font-style: italic;
125
+ color: #aaa;
126
+ }
127
+
128
+ .toplevel_page_wp_stream .filter-date-range {
129
+ margin-top: -1px;
130
+ }
131
+
132
+ .toplevel_page_wp_stream .alignleft.actions input[type=text] {
133
+ height: 28px;
134
+ line-height: 19px;
135
+ }
136
+
137
+ .toplevel_page_wp_stream .date-interval {
138
+ display: inline;
139
+ }
140
+
141
+ .toplevel_page_wp_stream .select2-container {
142
+ margin-right: 6px;
143
+ margin-bottom: 6px;
144
+ }
145
+
146
+ .toplevel_page_wp_stream .select2-container.select2-allowclear .select2-choice abbr {
147
+ margin-top: -2px;
148
+ }
149
+
150
+
151
+ /* Live Update */
152
+
153
+ .toplevel_page_wp_stream .stream-live-update-checkbox .spinner {
154
+ margin-top: 5px;
155
+ }
156
+
157
+ .toplevel_page_wp_stream .new-row,
158
+ #dashboard_stream_activity .new-row {
159
+ background-color: #ffffe0 !important;
160
+
161
+ -webkit-transition: background 0.5s linear;
162
+ -moz-transition: background 0.5s linear;
163
+ -ms-transition: background 0.5s linear;
164
+ -o-transition: background 0.5s linear;
165
+ transition: background 0.5s linear;
166
+ }
167
+
168
+ .toplevel_page_wp_stream .new-row.alternate,
169
+ #dashboard_stream_activity .new-row.alternate {
170
+ background-color: #ffffcd !important;
171
+ }
172
+
173
+ .toplevel_page_wp_stream .new-row.fadeout,
174
+ #dashboard_stream_activity .new-row.fadeout {
175
+ background-color: transparent !important;
176
+ }
177
+
178
+ .toplevel_page_wp_stream .new-row.alternate.fadeout,
179
+ #dashboard_stream_activity .new-row.alternate.fadeout {
180
+ background-color: #f9f9f9 !important;
181
+ }
182
+
183
+ .toplevel_page_wp_stream #the-list .no-items .stream-list-table-no-items {
184
+ text-align: center;
185
+ }
186
+
187
+ .toplevel_page_wp_stream #the-list .no-items .stream-list-table-no-items p {
188
+ margin: 2px 0;
189
+ }
190
+
191
+
192
+ /* Settings */
193
+
194
+ .wp_stream_settings .nav-tab-wrapper a:not(.nav-tab-active),
195
+ .wp_stream_network_settings .nav-tab-wrapper a:not(.nav-tab-active)
196
+ .wp_stream_default_settings .nav-tab-wrapper a:not(.nav-tab-active) {
197
+ border-bottom: 1px solid #ccc;
198
+ }
199
+
200
+ .wp_stream_settings .select2-container,
201
+ .wp_stream_network_settings .select2-container,
202
+ .wp_stream_default_settings .select2-container {
203
+ width: 100%;
204
+ }
205
+
206
+ .wp_stream_settings .tablenav,
207
+ .wp_stream_network_settings .tablenav,
208
+ .wp_stream_default_settings .tablenav {
209
+ margin-top: 16px;
210
+ }
211
+
212
+ .wp_stream_settings .tablenav input,
213
+ .wp_stream_network_settings .tablenav input,
214
+ .wp_stream_default_settings .tablenav input {
215
+ margin-right: 1em;
216
+ }
217
+
218
+ .wp_stream_screen a.warning {
219
+ color: #a00;
220
+ }
221
+
222
+ .wp_stream_screen a.warning:hover {
223
+ color: #f00;
224
+ }
225
+
226
+ /* Date Interval Common */
227
+
228
+ .wp_stream_screen .date-interval .field-predefined {
229
+ width: 165px;
230
+ float: left;
231
+ margin-right: 6px;
232
+ margin-bottom: 6px;
233
+ }
234
+
235
+ .wp_stream_screen .date-interval .date-inputs {
236
+ float: left;
237
+ margin-right: 6px;
238
+ margin-bottom: 6px;
239
+ }
240
+
241
+ .wp_stream_screen .date-interval .date-inputs .field-to,
242
+ .wp_stream_screen .date-interval .date-inputs .field-from {
243
+ line-height: 28px;
244
+ height: 28px;
245
+ margin: 0;
246
+ width: 125px;
247
+ }
248
+
249
+ .wp_stream_screen .date-interval .date-inputs .box {
250
+ position: relative;
251
+ display: block;
252
+ float: left;
253
+ }
254
+
255
+ .wp_stream_screen .date-interval .date-inputs .box .date-remove {
256
+ display: none;
257
+ position: absolute;
258
+ cursor: pointer;
259
+ top: 0;
260
+ right: 0;
261
+ width: 20px;
262
+ height: 28px;
263
+ line-height: 28px;
264
+ font-size: 14px;
265
+ padding-top: 1px;
266
+ padding-right: 2px;
267
+ }
268
+
269
+ .wp_stream_screen .date-interval .date-inputs .box .date-remove:before {
270
+ content: '\f158';
271
+ }
272
+
273
+ .wp_stream_screen .date-interval .date-inputs .connector {
274
+ display: block;
275
+ float: left;
276
+ border: 1px solid #ddd;
277
+ border-left: 0;
278
+ border-right: 0;
279
+ height: 26px;
280
+ line-height: 28px;
281
+ padding-left: 2px;
282
+ width: 20px;
283
+ text-align: center;
284
+ font-size: 14px;
285
+ }
286
+
287
+ .wp_stream_screen .date-interval .date-inputs .connector:before {
288
+ content: '\f345';
289
+ }
290
+
291
+
292
+ /* Select2 Common */
293
+
294
+ .wp_stream_screen li.select2-searching,
295
+ .wp_stream_screen li.select2-no-results {
296
+ background: none;
297
+ padding: 7px 7px 0;
298
+ color: #999;
299
+ }
300
+
301
+ .wp_stream_screen li.select2-result {
302
+ margin-bottom: 0;
303
+ }
304
+
305
+ .wp_stream_screen li.select2-result.level-1 {
306
+ font-weight: bold;
307
+ }
308
+
309
+ .wp_stream_screen li.select2-result.level-2 {
310
+ padding-left: 1em;
311
+ }
312
+
313
+ .wp_stream_screen .select2-results .select2-disabled {
314
+ background: transparent;
315
+ color: #aaa;
316
+ }
317
+
318
+ .wp_stream_screen .select2-search-choice .icon16 {
319
+ margin: -6px 1px 0 -3px;
320
+ padding: 0;
321
+ width: 16px;
322
+ height: 16px;
323
+ }
324
+
325
+ .wp_stream_screen .select2-chosen .icon16 {
326
+ padding-right: 1px;
327
+ }
328
+
329
+ .wp_stream_screen .select2-chosen .icon16:before,
330
+ .wp_stream_screen .select2-search-choice .icon16:before {
331
+ font-size: 15px !important;
332
+ color: #656565;
333
+ }
334
+
335
+ .wp_stream_screen .select2-search-choice-close {
336
+ -webkit-transition: none;
337
+ -moz-transition: none;
338
+ -o-transition: all 0 none;
339
+ transition: none;
340
+ }
341
+
342
+ .wp-stream-select2-icon {
343
+ position: relative;
344
+ top: 3px;
345
+ margin-right: 4px;
346
+ width: 16px;
347
+ height: 16px;
348
+ }
349
+
350
+ .select2-disabled .wp-stream-select2-icon {
351
+ filter: url("data:image/svg+xml;utf8,<svg xmlns=\'http://www.w3.org/2000/svg\'><filter id=\'grayscale\'><feColorMatrix type=\'matrix\' values=\'0.3333 0.3333 0.3333 0 0 0.3333 0.3333 0.3333 0 0 0.3333 0.3333 0.3333 0 0 0 0 0 1 0\'/></filter></svg>#grayscale"); /* Firefox 3.5+ */
352
+ filter: gray; /* IE6-9 */
353
+ -webkit-filter: grayscale(100%); /* Chrome 19+ & Safari 6+ */
354
+ -moz-filter: grayscale(100%); /* Firefox < 3.5 */
355
+ }
356
+
357
+
358
+ /* Exclude List Table */
359
+
360
+ .stream-exclude-list {
361
+ margin-top: 1em;
362
+ }
363
+
364
+ .stream-exclude-list th {
365
+ font-weight: normal;
366
+ padding: 8px 10px;
367
+ width: auto;
368
+ }
369
+
370
+ .stream-exclude-list tbody tr.no-items {
371
+ background-color: #fff;
372
+ }
373
+
374
+ .stream-exclude-list tbody tr.no-items td {
375
+ padding: 8px 10px;
376
+ font-size: 13px;
377
+ }
378
+
379
+ .stream-exclude-list tbody th.check-column {
380
+ padding: 16px 0 0 3px;
381
+ }
382
+
383
+ .stream-exclude-list thead th.actions-column {
384
+ width: 3em;
385
+ }
386
+
387
+ .stream-exclude-list tbody th.actions-column {
388
+ padding: 21px 10px 20px 0;
389
+ }
390
+
391
+ .stream-exclude-list tbody th.actions-column a {
392
+ display: none;
393
+ color: #a00;
394
+ font-size: 13px;
395
+ }
396
+
397
+ .stream-exclude-list tbody tr:hover th.actions-column a {
398
+ display: block;
399
+ }
400
+
401
+ .stream-exclude-list tbody th.actions-column a:hover {
402
+ color: #f00;
403
+ }
404
+
405
+ .stream-exclude-list tbody th.actions-column .dashicons {
406
+ margin-top: 4px;
407
+ }
408
+
409
+ .stream-exclude-list tbody td .ip_address {
410
+ width: 100%;
411
+ }
412
+
413
+ .stream-exclude-list tbody td .ip_address.invalid {
414
+ border: 1px solid rgba(160,0,0,0.75);
415
+ }
416
+
417
+ @media screen and ( max-width: 900px ) {
418
+ .wp_stream_settings .stream-exclude-list .actions-column,
419
+ .wp_stream_network_settings .stream-exclude-list .actions-column,
420
+ .wp_stream_settings .stream-exclude-list .actions-column {
421
+ display: none;
422
+ }
423
+ }
424
+
425
+ @media screen and ( max-width: 782px ) {
426
+ .wp_stream_settings .stream-exclude-list td,
427
+ .wp_stream_network_settings .stream-exclude-list td,
428
+ .wp_stream_default_settings .stream-exclude-list td {
429
+ padding: 10px 10px 0 0;
430
+ }
431
+
432
+ .wp_stream_settings .stream-exclude-list th,
433
+ .wp_stream_network_settings .stream-exclude-list th,
434
+ .wp_stream_default_settings .stream-exclude-list th {
435
+ display: none;
436
+ }
437
+
438
+ .wp_stream_settings .stream-exclude-list .check-column,
439
+ .wp_stream_network_settings .stream-exclude-list .check-column,
440
+ .wp_stream_default_settings .stream-exclude-list .check-column {
441
+ display: table-cell;
442
+ padding: 13px 10px 0 3px;
443
+ }
444
+
445
+ .wp_stream_settings .stream-exclude-list thead .actions-column,
446
+ .wp_stream_network_settings .stream-exclude-list thead .actions-column,
447
+ .wp_stream_default_settings .stream-exclude-list thead .actions-column {
448
+ display: table-cell;
449
+ width: auto;
450
+ }
451
+
452
+ .wp_stream_settings .stream-exclude-list thead .actions-column .hidden,
453
+ .wp_stream_network_settings .stream-exclude-list thead .actions-column .hidden,
454
+ .wp_stream_default_settings .stream-exclude-list thead .actions-column .hidden {
455
+ display: block;
456
+ }
457
+
458
+ .wp_stream_settings .stream-exclude-list tfoot,
459
+ .wp_stream_network_settings .stream-exclude-list tfoot,
460
+ .wp_stream_default_settings .stream-exclude-list tfoot {
461
+ display: none;
462
+ }
463
+ }
464
+
465
+
466
+ /* Extensions */
467
+
468
+ .post-type-stream_notification .view-switch {
469
+ display: none;
470
+ }
471
+
472
+
473
+ /* Stream Migrate Message */
474
+
475
+ #stream-migrate-progress {
476
+ display: none;
477
+ height: 36px;
478
+ padding: 5px 0;
479
+ }
480
+
481
+ #stream-migrate-progress progress {
482
+ width: 300px;
483
+ font-size: 18px;
484
+ }
485
+
486
+ #stream-migrate-progress strong,
487
+ #stream-migrate-progress #stream-migrate-actions-close {
488
+ display: none;
489
+ }
490
+
491
+ #stream-migrate-progress strong {
492
+ padding-left: 5px;
493
+ }
494
+
495
+ #stream-migrate-progress strong,
496
+ #stream-migrate-progress em {
497
+ line-height: 28px;
498
+ padding-right: 10px;
499
+ }
500
+
501
+ #stream-migrate-actions .button {
502
+ display: inline-block;
503
+ vertical-align: bottom !important;
504
+ margin: 0 10px 5px 0 !important;
505
+ }
506
+
507
+ #stream-ignore-migrate {
508
+ display: inline-block;
509
+ margin: 10px 0 5px;
510
+ color: #a00;
511
+ }
512
+
513
+ #stream-ignore-migrate:hover {
514
+ color: #f00;
515
+ }
ui/css/datepicker.css ADDED
@@ -0,0 +1,346 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Date Picker Styles */
2
+
3
+ #ui-datepicker-div.stream-datepicker.ui-widget {
4
+ margin-top: 5px;
5
+ border: 1px solid #ddd;
6
+
7
+ -webkit-border-radius: 0;
8
+ -moz-border-radius: 0;
9
+ border-radius: 0;
10
+ }
11
+
12
+ #ui-datepicker-div.stream-datepicker.ui-datepicker * {
13
+ padding: 0;
14
+ font-family: 'Open Sans', sans-serif;
15
+ }
16
+
17
+ #ui-datepicker-div.stream-datepicker.ui-datepicker {
18
+ padding: 0;
19
+ }
20
+
21
+ #ui-datepicker-div.stream-datepicker.ui-datepicker table {
22
+ font-size: 13px;
23
+ }
24
+
25
+ #ui-datepicker-div.stream-datepicker .ui-datepicker-header {
26
+ border: none;
27
+ background: #23282d;
28
+ color: #fff;
29
+ font-weight: normal;
30
+ }
31
+
32
+ #ui-datepicker-div.stream-datepicker .ui-datepicker-header .ui-state-hover {
33
+ background: #23282d;
34
+ border-color: transparent;
35
+ cursor: pointer;
36
+
37
+ -webkit-border-radius: 0;
38
+ -moz-border-radius: 0;
39
+ border-radius: 0;
40
+ }
41
+
42
+ #ui-datepicker-div.stream-datepicker.ui-datepicker thead {
43
+ background: #23282d;
44
+ color: #fff;
45
+ }
46
+
47
+ #ui-datepicker-div.stream-datepicker .ui-corner-all {
48
+ -webkit-border-radius: 0;
49
+ -moz-border-radius: 0;
50
+ border-radius: 0;
51
+ }
52
+
53
+ #ui-datepicker-div.stream-datepicker.ui-datepicker .ui-datepicker-title {
54
+ margin-top: .4em;
55
+ margin-bottom: .3em;
56
+ color: #fff;
57
+ font-size: 14px;
58
+ }
59
+
60
+ #ui-datepicker-div.stream-datepicker.ui-datepicker .ui-datepicker-prev-hover,
61
+ #ui-datepicker-div.stream-datepicker.ui-datepicker .ui-datepicker-next-hover,
62
+ #ui-datepicker-div.stream-datepicker.ui-datepicker .ui-datepicker-next,
63
+ #ui-datepicker-div.stream-datepicker.ui-datepicker .ui-datepicker-prev {
64
+ height: 1em;
65
+ top: .9em;
66
+ border:none;
67
+ }
68
+
69
+ #ui-datepicker-div.stream-datepicker.ui-datepicker .ui-datepicker-prev-hover {
70
+ left: 2px;
71
+ }
72
+
73
+ #ui-datepicker-div.stream-datepicker.ui-datepicker .ui-datepicker-next-hover {
74
+ right: 2px;
75
+ }
76
+
77
+ #ui-datepicker-div.stream-datepicker.ui-datepicker .ui-datepicker-next span,
78
+ #ui-datepicker-div.stream-datepicker.ui-datepicker .ui-datepicker-prev span {
79
+ background-image: url(../stream-icons/datepicker-icons.png);
80
+ background-position: -32px 0;
81
+ margin-top: 0;
82
+ top: 0;
83
+ font-weight: normal;
84
+ }
85
+
86
+ #ui-datepicker-div.stream-datepicker.ui-datepicker .ui-datepicker-prev span {
87
+ background-position: -96px 0;
88
+ }
89
+
90
+ #ui-datepicker-div.stream-datepicker.ui-datepicker table {
91
+ margin: 0;
92
+ }
93
+
94
+ #ui-datepicker-div.stream-datepicker.ui-datepicker th {
95
+ padding: 0.75em 0;
96
+ color: #fff;
97
+ font-weight: normal;
98
+ border: none;
99
+ border-top: 1px solid #32373c;
100
+ }
101
+
102
+ #ui-datepicker-div.stream-datepicker.ui-datepicker td {
103
+ background: #f1f1f1;
104
+ border: none;
105
+ padding: 0;
106
+ }
107
+
108
+ #ui-datepicker-div.stream-datepicker td .ui-state-default {
109
+ background: transparent;
110
+ border: none;
111
+ text-align: center;
112
+ padding: .5em;
113
+ margin: 0;
114
+ font-weight: normal;
115
+ color: #32373c;
116
+ }
117
+
118
+ #ui-datepicker-div.stream-datepicker.ui-datepicker td.ui-state-disabled,
119
+ #ui-datepicker-div.stream-datepicker.ui-datepicker .ui-widget-content td.ui-state-disabled,
120
+ #ui-datepicker-div.stream-datepicker td.ui-state-disabled .ui-state-default {
121
+ opacity: 1;
122
+ color: #999;
123
+ }
124
+
125
+ #ui-datepicker-div.stream-datepicker td .ui-state-active,
126
+ #ui-datepicker-div.stream-datepicker td .ui-state-hover {
127
+ background: #0074a2;
128
+ color: #fff;
129
+ }
130
+
131
+ /* Other Datepicker Color Schemes */
132
+
133
+ /* Light */
134
+
135
+ /* Primary Background Color */
136
+ .admin-color-light #ui-datepicker-div.stream-datepicker .ui-datepicker-header,
137
+ .admin-color-light #ui-datepicker-div.stream-datepicker .ui-datepicker-header .ui-state-hover,
138
+ .admin-color-light #ui-datepicker-div.stream-datepicker.ui-datepicker thead,
139
+ .admin-color-light #ui-datepicker-div.stream-datepicker td .ui-state-active,
140
+ .admin-color-light #ui-datepicker-div.stream-datepicker td .ui-state-hover {
141
+ background: #e5e5e5;
142
+ }
143
+
144
+ /* Table Background */
145
+ .admin-color-light #ui-datepicker-div.stream-datepicker.ui-datepicker td {
146
+ background: #fff;
147
+ }
148
+
149
+ .admin-color-light #ui-datepicker-div.stream-datepicker.ui-datepicker .ui-datepicker-next span,
150
+ .admin-color-light #ui-datepicker-div.stream-datepicker.ui-datepicker .ui-datepicker-prev span {
151
+ background-image: url(../stream-icons/datepicker-icons-gray.png);
152
+ }
153
+
154
+ /* Calendar Top Border */
155
+ .admin-color-light #ui-datepicker-div.stream-datepicker.ui-datepicker th {
156
+ border-color: #fff;
157
+ }
158
+
159
+ /* Calendar Title, Dates Color */
160
+ .admin-color-light #ui-datepicker-div.stream-datepicker.ui-datepicker .ui-datepicker-title,
161
+ .admin-color-light #ui-datepicker-div.stream-datepicker td .ui-state-default,
162
+ .admin-color-light #ui-datepicker-div.stream-datepicker.ui-datepicker th {
163
+ color: #555;
164
+ }
165
+
166
+ /* Hover State */
167
+ .admin-color-light #ui-datepicker-div.stream-datepicker td .ui-state-active,
168
+ .admin-color-light #ui-datepicker-div.stream-datepicker td .ui-state-hover {
169
+ color: #fff;
170
+ background: #888;
171
+ }
172
+
173
+ /* Disabled Color */
174
+ .admin-color-light #ui-datepicker-div.stream-datepicker.ui-datepicker td.ui-state-disabled,
175
+ .admin-color-light #ui-datepicker-div.stream-datepicker.ui-datepicker .ui-widget-content td.ui-state-disabled,
176
+ .admin-color-light #ui-datepicker-div.stream-datepicker td.ui-state-disabled .ui-state-default {
177
+ color: #ccc;
178
+ }
179
+
180
+ /* Blue */
181
+
182
+ /* Primary Background Color */
183
+ .admin-color-blue #ui-datepicker-div.stream-datepicker .ui-datepicker-header,
184
+ .admin-color-blue #ui-datepicker-div.stream-datepicker .ui-datepicker-header .ui-state-hover,
185
+ .admin-color-blue #ui-datepicker-div.stream-datepicker.ui-datepicker thead,
186
+ .admin-color-blue #ui-datepicker-div.stream-datepicker td .ui-state-active,
187
+ .admin-color-blue #ui-datepicker-div.stream-datepicker td .ui-state-hover {
188
+ background: #4796B3;
189
+ }
190
+
191
+ /* Calendar Top Border */
192
+ .admin-color-blue #ui-datepicker-div.stream-datepicker.ui-datepicker th {
193
+ border-color: #52ACCC;
194
+ }
195
+
196
+ /* Calendar Title, Dates Color */
197
+ .admin-color-blue #ui-datepicker-div.stream-datepicker.ui-datepicker .ui-datepicker-title,
198
+ .admin-color-blue #ui-datepicker-div.stream-datepicker.ui-datepicker th {
199
+ color: #fff;
200
+ }
201
+
202
+ /* Hover State */
203
+ .admin-color-blue #ui-datepicker-div.stream-datepicker td .ui-state-active,
204
+ .admin-color-blue #ui-datepicker-div.stream-datepicker td .ui-state-hover {
205
+ background: #096484;
206
+ }
207
+
208
+ /* Coffee */
209
+
210
+ /* Primary Background Color */
211
+ .admin-color-coffee #ui-datepicker-div.stream-datepicker .ui-datepicker-header,
212
+ .admin-color-coffee #ui-datepicker-div.stream-datepicker .ui-datepicker-header .ui-state-hover,
213
+ .admin-color-coffee #ui-datepicker-div.stream-datepicker.ui-datepicker thead,
214
+ .admin-color-coffee #ui-datepicker-div.stream-datepicker td .ui-state-active,
215
+ .admin-color-coffee #ui-datepicker-div.stream-datepicker td .ui-state-hover {
216
+ background: #46403C;
217
+ }
218
+
219
+ /* Calendar Top Border */
220
+ .admin-color-coffee #ui-datepicker-div.stream-datepicker.ui-datepicker th {
221
+ border-color: #59524C;
222
+ }
223
+
224
+ /* Calendar Title, Dates Color */
225
+ .admin-color-coffee #ui-datepicker-div.stream-datepicker.ui-datepicker .ui-datepicker-title,
226
+ .admin-color-coffee #ui-datepicker-div.stream-datepicker.ui-datepicker th {
227
+ color: #fff;
228
+ }
229
+
230
+ /* Hover State */
231
+ .admin-color-coffee #ui-datepicker-div.stream-datepicker td .ui-state-active,
232
+ .admin-color-coffee #ui-datepicker-div.stream-datepicker td .ui-state-hover {
233
+ background: #C7A589;
234
+ }
235
+
236
+ /* Ectoplasm */
237
+
238
+ /* Primary Background Color */
239
+ .admin-color-ectoplasm #ui-datepicker-div.stream-datepicker .ui-datepicker-header,
240
+ .admin-color-ectoplasm #ui-datepicker-div.stream-datepicker .ui-datepicker-header .ui-state-hover,
241
+ .admin-color-ectoplasm #ui-datepicker-div.stream-datepicker.ui-datepicker thead,
242
+ .admin-color-ectoplasm #ui-datepicker-div.stream-datepicker td .ui-state-active,
243
+ .admin-color-ectoplasm #ui-datepicker-div.stream-datepicker td .ui-state-hover {
244
+ background: #413256;
245
+ }
246
+
247
+ /* Calendar Top Border */
248
+ .admin-color-ectoplasm #ui-datepicker-div.stream-datepicker.ui-datepicker th {
249
+ border-color: #523F6D;
250
+ }
251
+
252
+ /* Calendar Title, Dates Color */
253
+ .admin-color-ectoplasm #ui-datepicker-div.stream-datepicker.ui-datepicker .ui-datepicker-title,
254
+ .admin-color-ectoplasm #ui-datepicker-div.stream-datepicker.ui-datepicker th {
255
+ color: #fff;
256
+ }
257
+
258
+ /* Hover State */
259
+ .admin-color-ectoplasm #ui-datepicker-div.stream-datepicker td .ui-state-active,
260
+ .admin-color-ectoplasm #ui-datepicker-div.stream-datepicker td .ui-state-hover {
261
+ background: #A3B745;
262
+ }
263
+
264
+ /* Midnight */
265
+
266
+ /* Primary Background Color */
267
+ .admin-color-midnight #ui-datepicker-div.stream-datepicker .ui-datepicker-header,
268
+ .admin-color-midnight #ui-datepicker-div.stream-datepicker .ui-datepicker-header .ui-state-hover,
269
+ .admin-color-midnight #ui-datepicker-div.stream-datepicker.ui-datepicker thead,
270
+ .admin-color-midnight #ui-datepicker-div.stream-datepicker td .ui-state-active,
271
+ .admin-color-midnight #ui-datepicker-div.stream-datepicker td .ui-state-hover {
272
+ background: #26292C;
273
+ }
274
+
275
+ /* Calendar Top Border */
276
+ .admin-color-midnight #ui-datepicker-div.stream-datepicker.ui-datepicker th {
277
+ border-color: #363B3F;
278
+ }
279
+
280
+ /* Calendar Title, Dates Color */
281
+ .admin-color-midnight #ui-datepicker-div.stream-datepicker.ui-datepicker .ui-datepicker-title,
282
+ .admin-color-midnight #ui-datepicker-div.stream-datepicker.ui-datepicker th {
283
+ color: #fff;
284
+ }
285
+
286
+ /* Hover State */
287
+ .admin-color-midnight #ui-datepicker-div.stream-datepicker td .ui-state-active,
288
+ .admin-color-midnight #ui-datepicker-div.stream-datepicker td .ui-state-hover {
289
+ background: #E14D43;
290
+ }
291
+
292
+ /* Ocean */
293
+
294
+ /* Primary Background Color */
295
+ .admin-color-ocean #ui-datepicker-div.stream-datepicker .ui-datepicker-header,
296
+ .admin-color-ocean #ui-datepicker-div.stream-datepicker .ui-datepicker-header .ui-state-hover,
297
+ .admin-color-ocean #ui-datepicker-div.stream-datepicker.ui-datepicker thead,
298
+ .admin-color-ocean #ui-datepicker-div.stream-datepicker td .ui-state-active,
299
+ .admin-color-ocean #ui-datepicker-div.stream-datepicker td .ui-state-hover {
300
+ background: #627C83;
301
+ }
302
+
303
+ /* Calendar Top Border */
304
+ .admin-color-ocean #ui-datepicker-div.stream-datepicker.ui-datepicker th {
305
+ border-color: #738E96;
306
+ }
307
+
308
+ /* Calendar Title, Dates Color */
309
+ .admin-color-ocean #ui-datepicker-div.stream-datepicker.ui-datepicker .ui-datepicker-title,
310
+ .admin-color-ocean #ui-datepicker-div.stream-datepicker.ui-datepicker th {
311
+ color: #fff;
312
+ }
313
+
314
+ /* Hover State */
315
+ .admin-color-ocean #ui-datepicker-div.stream-datepicker td .ui-state-active,
316
+ .admin-color-ocean #ui-datepicker-div.stream-datepicker td .ui-state-hover {
317
+ background: #9EBAA0;
318
+ }
319
+
320
+ /* Sunrise */
321
+
322
+ /* Primary Background Color */
323
+ .admin-color-sunrise #ui-datepicker-div.stream-datepicker .ui-datepicker-header,
324
+ .admin-color-sunrise #ui-datepicker-div.stream-datepicker .ui-datepicker-header .ui-state-hover,
325
+ .admin-color-sunrise #ui-datepicker-div.stream-datepicker.ui-datepicker thead,
326
+ .admin-color-sunrise #ui-datepicker-div.stream-datepicker td .ui-state-active,
327
+ .admin-color-sunrise #ui-datepicker-div.stream-datepicker td .ui-state-hover {
328
+ background: #BE3631;
329
+ }
330
+
331
+ /* Calendar Top Border */
332
+ .admin-color-sunrise #ui-datepicker-div.stream-datepicker.ui-datepicker th {
333
+ border-color: #CF4944;
334
+ }
335
+
336
+ /* Calendar Title, Dates Color */
337
+ .admin-color-sunrise #ui-datepicker-div.stream-datepicker.ui-datepicker .ui-datepicker-title,
338
+ .admin-color-sunrise #ui-datepicker-div.stream-datepicker.ui-datepicker th {
339
+ color: #fff;
340
+ }
341
+
342
+ /* Hover State */
343
+ .admin-color-sunrise #ui-datepicker-div.stream-datepicker td .ui-state-active,
344
+ .admin-color-sunrise #ui-datepicker-div.stream-datepicker td .ui-state-hover {
345
+ background: #DD823B;
346
+ }
ui/js/admin.js ADDED
@@ -0,0 +1,501 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* globals wp_stream, ajaxurl */
2
+ jQuery( function( $ ) {
3
+
4
+ // Shorter timeago strings for English locale
5
+ if ( 'en' === wp_stream.locale && 'undefined' !== typeof $.timeago ) {
6
+ $.timeago.settings.strings.seconds = 'seconds';
7
+ $.timeago.settings.strings.minute = 'a minute';
8
+ $.timeago.settings.strings.hour = 'an hour';
9
+ $.timeago.settings.strings.hours = '%d hours';
10
+ $.timeago.settings.strings.month = 'a month';
11
+ $.timeago.settings.strings.year = 'a year';
12
+ }
13
+
14
+ $( 'li.toplevel_page_wp_stream ul li.wp-first-item.current' ).parent().parent().find( '.update-plugins' ).remove();
15
+
16
+ $( '.toplevel_page_wp_stream :input.chosen-select' ).each( function( i, el ) {
17
+ var args = {},
18
+ formatResult = function( record, container ) {
19
+ var result = '',
20
+ $elem = $( record.element ),
21
+ icon = '';
22
+
23
+ if ( '- ' === record.text.substring( 0, 2 ) ) {
24
+ record.text = record.text.substring( 2 );
25
+ }
26
+
27
+ if ( undefined !== record.icon ) {
28
+ icon = record.icon;
29
+ } else if ( undefined !== $elem.attr( 'data-icon' ) ) {
30
+ icon = $elem.data( 'icon' );
31
+ }
32
+ if ( icon ) {
33
+ result += '<img src="' + icon + '" class="wp-stream-select2-icon">';
34
+ }
35
+
36
+ result += record.text;
37
+
38
+ // Add more info to the container
39
+ container.attr( 'title', $elem.attr( 'title' ) );
40
+
41
+ return result;
42
+ },
43
+ formatSelection = function( record ) {
44
+ if ( '- ' === record.text.substring( 0, 2 ) ) {
45
+ record.text = record.text.substring( 2 );
46
+ }
47
+ return record.text;
48
+ };
49
+
50
+ if ( $( el ).find( 'option' ).length > 0 ) {
51
+ args = {
52
+ minimumResultsForSearch: 10,
53
+ formatResult: formatResult,
54
+ formatSelection: formatSelection,
55
+ allowClear: true,
56
+ width: '165px'
57
+ };
58
+ } else {
59
+ args = {
60
+ minimumInputLength: 3,
61
+ allowClear: true,
62
+ width: '165px',
63
+ ajax: {
64
+ url: ajaxurl,
65
+ datatype: 'json',
66
+ data: function( term ) {
67
+ return {
68
+ action: 'wp_stream_filters',
69
+ filter: $( el ).attr( 'name' ),
70
+ q: term
71
+ };
72
+ },
73
+ results: function( data ) {
74
+ return { results: data };
75
+ }
76
+ },
77
+ formatResult: formatResult,
78
+ formatSelection: formatSelection,
79
+ initSelection: function( element, callback ) {
80
+ var id = $( element ).val();
81
+ if ( '' !== id ) {
82
+ $.post(
83
+ ajaxurl,
84
+ {
85
+ action: 'wp_stream_get_filter_value_by_id',
86
+ filter: $(element).attr( 'name' ),
87
+ id: id
88
+ },
89
+ function( response ) {
90
+ callback({
91
+ id: id,
92
+ text: response
93
+ });
94
+ },
95
+ 'json'
96
+ );
97
+ }
98
+ }
99
+ };
100
+ }
101
+
102
+ $( el ).select2( args );
103
+ });
104
+
105
+ var $queryVars = $.streamGetQueryVars();
106
+ var $contextInput = $( '.toplevel_page_wp_stream :input.chosen-select[name="context"]' );
107
+
108
+ if ( ( 'undefined' === typeof $queryVars.context || '' === $queryVars.context ) && 'undefined' !== typeof $queryVars.connector ) {
109
+ $contextInput.select2( 'val', 'group-' + $queryVars.connector );
110
+ }
111
+
112
+ $( '#record-filter-form' ).submit( function() {
113
+ var $context = $( '.toplevel_page_wp_stream :input.chosen-select[name="context"]' ),
114
+ $option = $context.find( 'option:selected' ),
115
+ $connector = $context.parent().find( '.record-filter-connector' ),
116
+ optionConnector = $option.data( 'group' ),
117
+ optionClass = $option.prop( 'class' );
118
+
119
+ $connector.val( optionConnector );
120
+
121
+ if ( 'level-1' === optionClass ) {
122
+ $option.val( '' );
123
+ }
124
+ });
125
+
126
+ $( window ).load( function() {
127
+ $( '.toplevel_page_wp_stream input[type="search"]' ).off( 'mousedown' );
128
+ });
129
+
130
+ // Confirmation on some important actions
131
+ $( 'body' ).on( 'click', '#wp_stream_advanced_delete_all_records, #wp_stream_network_advanced_delete_all_records', function( e ) {
132
+ if ( ! window.confirm( wp_stream.i18n.confirm_purge ) ) {
133
+ e.preventDefault();
134
+ }
135
+ });
136
+
137
+ $( 'body' ).on( 'click', '#wp_stream_advanced_reset_site_settings, #wp_stream_network_advanced_reset_site_settings', function( e ) {
138
+ if ( ! window.confirm( wp_stream.i18n.confirm_defaults ) ) {
139
+ e.preventDefault();
140
+ }
141
+ });
142
+
143
+ $( 'body' ).on( 'click', '#wp_stream_uninstall', function( e ) {
144
+ if ( ! window.confirm( wp_stream.i18n.confirm_uninstall ) ) {
145
+ e.preventDefault();
146
+ }
147
+ });
148
+
149
+ // Admin page tabs
150
+ var $tabs = $( '.wp_stream_screen .nav-tab-wrapper' ),
151
+ $panels = $( '.wp_stream_screen .nav-tab-content table.form-table' ),
152
+ $activeTab = $tabs.find( '.nav-tab-active' ),
153
+ defaultIndex = $activeTab.length > 0 ? $tabs.find( 'a' ).index( $activeTab ) : 0,
154
+ hashIndex = window.location.hash.match( /^#(\d+)$/ ),
155
+ currentHash = ( null !== hashIndex ? hashIndex[ 1 ] : defaultIndex ),
156
+ syncFormAction = function( index ) {
157
+ var $optionsForm = $( 'input[name="option_page"][value^="wp_stream"]' ).closest( 'form' );
158
+ if ( $optionsForm.length === 0 ) {
159
+ return;
160
+ }
161
+ var currentAction = $optionsForm.attr( 'action' );
162
+
163
+ $optionsForm.prop( 'action', currentAction.replace( /(^[^#]*).*$/, '$1#' + index ) );
164
+ };
165
+
166
+ $tabs.on( 'click', 'a', function() {
167
+ var index = $tabs.find( 'a' ).index( $( this ) ),
168
+ hashIndex = window.location.hash.match( /^#(\d+)$/ );
169
+
170
+ $panels.hide().eq( index ).show();
171
+ $tabs
172
+ .find( 'a' )
173
+ .removeClass( 'nav-tab-active' )
174
+ .filter( $( this ) )
175
+ .addClass( 'nav-tab-active' );
176
+
177
+ if ( '' === window.location.hash || null !== hashIndex ) {
178
+ window.location.hash = index;
179
+ }
180
+
181
+ syncFormAction( index );
182
+
183
+ return false;
184
+ });
185
+
186
+ $tabs.children().eq( currentHash ).trigger( 'click' );
187
+
188
+ // Live Updates screen option
189
+ $( document ).ready( function() {
190
+
191
+ // Enable Live Updates checkbox ajax
192
+ $( '#enable_live_update' ).click( function() {
193
+ var nonce = $( '#stream_live_update_nonce' ).val(),
194
+ user = $( '#enable_live_update_user' ).val(),
195
+ checked = 'unchecked',
196
+ heartbeat = 'true';
197
+
198
+ if ( $( '#enable_live_update' ).is( ':checked' ) ) {
199
+ checked = 'checked';
200
+ }
201
+
202
+ heartbeat = $( '#enable_live_update' ).data( 'heartbeat' );
203
+
204
+ $.ajax({
205
+ type: 'POST',
206
+ url: ajaxurl,
207
+ data: {
208
+ action: 'stream_enable_live_update',
209
+ nonce: nonce,
210
+ user: user,
211
+ checked: checked,
212
+ heartbeat: heartbeat
213
+ },
214
+ dataType: 'json',
215
+ beforeSend: function() {
216
+ $( '.stream-live-update-checkbox .spinner' ).show().css( { 'display': 'inline-block' } );
217
+ },
218
+ success: function( response ) {
219
+ $( '.stream-live-update-checkbox .spinner' ).hide();
220
+
221
+ if ( false === response.success ) {
222
+ $( '#enable_live_update' ).prop( 'checked', false );
223
+
224
+ if ( response.data ) {
225
+ window.alert( response.data );
226
+ }
227
+ }
228
+ }
229
+ });
230
+ });
231
+
232
+ function toggle_filter_submit() {
233
+ var all_hidden = true;
234
+
235
+ // If all filters are hidden, hide the button
236
+ if ( $( 'div.metabox-prefs [id="date-hide"]' ).is( ':checked' ) ) {
237
+ all_hidden = false;
238
+ }
239
+
240
+ var divs = $( 'div.alignleft.actions div.select2-container' );
241
+
242
+ divs.each( function() {
243
+ if ( ! $( this ).is( ':hidden' ) ) {
244
+ all_hidden = false;
245
+ return false;
246
+ }
247
+ });
248
+
249
+ if ( all_hidden ) {
250
+ $( 'input#record-query-submit' ).hide();
251
+ $( 'span.filter_info' ).show();
252
+ } else {
253
+ $( 'input#record-query-submit' ).show();
254
+ $( 'span.filter_info' ).hide();
255
+ }
256
+ }
257
+
258
+ if ( $( 'div.metabox-prefs [id="date-hide"]' ).is( ':checked' ) ) {
259
+ $( 'div.date-interval' ).show();
260
+ } else {
261
+ $( 'div.date-interval' ).hide();
262
+ }
263
+
264
+ $( 'div.actions select.chosen-select' ).each( function() {
265
+ var name = $( this ).prop( 'name' );
266
+
267
+ if ( $( 'div.metabox-prefs [id="' + name + '-hide"]' ).is( ':checked' ) ) {
268
+ $( this ).prev( '.select2-container' ).show();
269
+ } else {
270
+ $( this ).prev( '.select2-container' ).hide();
271
+ }
272
+ });
273
+
274
+ toggle_filter_submit();
275
+
276
+ $( 'div.metabox-prefs [type="checkbox"]' ).click( function() {
277
+ var id = $( this ).prop( 'id' );
278
+
279
+ if ( 'date-hide' === id ) {
280
+ if ( $( this ).is( ':checked' ) ) {
281
+ $( 'div.date-interval' ).show();
282
+ } else {
283
+ $( 'div.date-interval' ).hide();
284
+ }
285
+ } else {
286
+ id = id.replace( '-hide', '' );
287
+
288
+ if ( $( this ).is( ':checked' ) ) {
289
+ $( '[name="' + id + '"]' ).prev( '.select2-container' ).show();
290
+ } else {
291
+ $( '[name="' + id + '"]' ).prev( '.select2-container' ).hide();
292
+ }
293
+ }
294
+
295
+ toggle_filter_submit();
296
+ });
297
+
298
+ $( '#ui-datepicker-div' ).addClass( 'stream-datepicker' );
299
+ });
300
+
301
+ // Relative time
302
+ $( 'table.wp-list-table' ).on( 'updated', function() {
303
+ var timeObjects = $( this ).find( 'time.relative-time' );
304
+ timeObjects.each( function( i, el ) {
305
+ var timeEl = $( el );
306
+ timeEl.removeClass( 'relative-time' );
307
+ $( '<strong><time datetime="' + timeEl.attr( 'datetime' ) + '" class="timeago"/></time></strong><br/>' )
308
+ .prependTo( timeEl.parent().parent() )
309
+ .find( 'time.timeago' )
310
+ .timeago();
311
+ });
312
+ }).trigger( 'updated' );
313
+
314
+ var intervals = {
315
+ init: function( $wrapper ) {
316
+ this.wrapper = $wrapper;
317
+ this.save_interval( this.wrapper.find( '.button-primary' ), this.wrapper );
318
+
319
+ this.$ = this.wrapper.each( function( i, val ) {
320
+ var container = $( val ),
321
+ dateinputs = container.find( '.date-inputs' ),
322
+ from = container.find( '.field-from' ),
323
+ to = container.find( '.field-to' ),
324
+ to_remove = to.prev( '.date-remove' ),
325
+ from_remove = from.prev( '.date-remove' ),
326
+ predefined = container.children( '.field-predefined' ),
327
+ datepickers = $( '' ).add( to ).add( from );
328
+
329
+ if ( jQuery.datepicker ) {
330
+
331
+ // Apply a GMT offset due to Date() using the visitor's local time
332
+ var siteGMTOffsetHours = parseFloat( wp_stream.gmt_offset ),
333
+ localGMTOffsetHours = new Date().getTimezoneOffset() / 60 * -1,
334
+ totalGMTOffsetHours = siteGMTOffsetHours - localGMTOffsetHours,
335
+ localTime = new Date(),
336
+ siteTime = new Date( localTime.getTime() + ( totalGMTOffsetHours * 60 * 60 * 1000 ) ),
337
+ maxOffset = 0,
338
+ minOffset = null;
339
+
340
+ // Check if the site date is different from the local date, and set a day offset
341
+ if ( localTime.getDate() !== siteTime.getDate() || localTime.getMonth() !== siteTime.getMonth() ) {
342
+ if ( localTime.getTime() < siteTime.getTime() ) {
343
+ maxOffset = '+1d';
344
+ } else {
345
+ maxOffset = '-1d';
346
+ }
347
+ }
348
+
349
+ datepickers.datepicker({
350
+ dateFormat: 'yy/mm/dd',
351
+ minDate: minOffset,
352
+ maxDate: maxOffset,
353
+ defaultDate: siteTime,
354
+ beforeShow: function() {
355
+ $( this ).prop( 'disabled', true );
356
+ },
357
+ onClose: function() {
358
+ $( this ).prop( 'disabled', false );
359
+ }
360
+ });
361
+
362
+ datepickers.datepicker( 'widget' ).addClass( 'stream-datepicker' );
363
+ }
364
+
365
+ predefined.select2({
366
+ 'allowClear': true
367
+ });
368
+
369
+ if ( '' !== from.val() ) {
370
+ from_remove.show();
371
+ }
372
+
373
+ if ( '' !== to.val() ) {
374
+ to_remove.show();
375
+ }
376
+
377
+ predefined.on({
378
+ 'change': function () {
379
+ var value = $( this ).val(),
380
+ option = predefined.find( '[value="' + value + '"]' ),
381
+ to_val = option.data( 'to' ),
382
+ from_val = option.data( 'from' );
383
+
384
+ if ( 'custom' === value ) {
385
+ dateinputs.show();
386
+ return false;
387
+ } else {
388
+ dateinputs.hide();
389
+ datepickers.datepicker( 'hide' );
390
+ }
391
+
392
+ from.val( from_val ).trigger( 'change', [ true ] );
393
+ to.val( to_val ).trigger( 'change', [ true ] );
394
+
395
+ if ( jQuery.datepicker && datepickers.datepicker( 'widget' ).is( ':visible' ) ) {
396
+ datepickers.datepicker( 'refresh' ).datepicker( 'hide' );
397
+ }
398
+ },
399
+ 'select2-removed': function() {
400
+ predefined.val( '' ).trigger( 'change' );
401
+ },
402
+ 'check_options': function () {
403
+ if ( '' !== to.val() && '' !== from.val() ) {
404
+ var option = predefined
405
+ .find( 'option' )
406
+ .filter( '[data-to="' + to.val() + '"]' )
407
+ .filter( '[data-from="' + from.val() + '"]' );
408
+ if ( 0 !== option.length ) {
409
+ predefined.val( option.attr( 'value' ) ).trigger( 'change', [ true ] );
410
+ } else {
411
+ predefined.val( 'custom' ).trigger( 'change', [ true ] );
412
+ }
413
+ } else if ( '' === to.val() && '' === from.val() ) {
414
+ predefined.val( '' ).trigger( 'change', [ true ] );
415
+ } else {
416
+ predefined.val( 'custom' ).trigger( 'change', [ true ] );
417
+ }
418
+ }
419
+ });
420
+
421
+ from.on( 'change', function() {
422
+ if ( '' !== from.val() ) {
423
+ from_remove.show();
424
+ to.datepicker( 'option', 'minDate', from.val() );
425
+ } else {
426
+ from_remove.hide();
427
+ }
428
+
429
+ if ( true === arguments[ arguments.length - 1 ] ) {
430
+ return false;
431
+ }
432
+
433
+ predefined.trigger( 'check_options' );
434
+ });
435
+
436
+ to.on( 'change', function() {
437
+ if ( '' !== to.val() ) {
438
+ to_remove.show();
439
+ from.datepicker( 'option', 'maxDate', to.val() );
440
+ } else {
441
+ to_remove.hide();
442
+ }
443
+
444
+ if ( true === arguments[ arguments.length - 1 ] ) {
445
+ return false;
446
+ }
447
+
448
+ predefined.trigger( 'check_options' );
449
+ });
450
+
451
+ // Trigger change on load
452
+ predefined.trigger( 'change' );
453
+
454
+ $( '' ).add( from_remove ).add( to_remove ).on( 'click', function() {
455
+ $( this ).next( 'input' ).val( '' ).trigger( 'change' );
456
+ });
457
+ });
458
+ },
459
+
460
+ save_interval: function( $btn ) {
461
+ var $wrapper = this.wrapper;
462
+ $btn.click( function() {
463
+ var data = {
464
+ key: $wrapper.find( 'select.field-predefined' ).find( ':selected' ).val(),
465
+ start: $wrapper.find( '.date-inputs .field-from' ).val(),
466
+ end: $wrapper.find( '.date-inputs .field-to' ).val()
467
+ };
468
+
469
+ // Add params to URL
470
+ $( this ).attr( 'href', $( this ).attr( 'href' ) + '&' + $.param( data ) );
471
+ });
472
+ }
473
+ };
474
+
475
+ $( document ).ready( function() {
476
+ intervals.init( $( '.date-interval' ) );
477
+
478
+ // Disable option groups whose children are all disabled
479
+ $( 'select[name="context"] .level-1' ).each( function() {
480
+ var all_disabled = true;
481
+
482
+ $( this ).nextUntil( '.level-1' ).each( function() {
483
+ if ( $( this ).is( ':not(:disabled)' ) ) {
484
+ all_disabled = false;
485
+ return false;
486
+ }
487
+ });
488
+
489
+ if ( true === all_disabled ) {
490
+ $( this ).prop( 'disabled', true );
491
+ }
492
+ });
493
+ });
494
+
495
+ });
496
+
497
+ jQuery.extend({
498
+ streamGetQueryVars: function( str ) {
499
+ return ( str || document.location.search ).replace( /(^\?)/, '' ).split( '&' ).map( function( n ) { return n = n.split( '=' ), this[n[0]] = n[1], this; }.bind( {} ) )[0];
500
+ }
501
+ });
ui/js/global.js ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* globals wp_stream_global */
2
+ /* exported wp_stream_regenerate_alt_rows */
3
+ jQuery( function( $ ) {
4
+
5
+ // List table actions, ignores filtering
6
+ $( '.actions :submit:not([name="filter_action"])' ).on( 'click', function( e ) {
7
+ if ( $( 'table.widefat tbody :checkbox:checked' ).length > wp_stream_global.bulk_actions.threshold ) {
8
+ warning_message( e );
9
+ }
10
+ });
11
+
12
+ // Post type empty trash
13
+ $( '#delete_all' ).on( 'click', function( e ) {
14
+ var trash_count = parseInt( $( 'ul.subsubsub li.trash .count' ).text().replace( /\D/g, '' ), 10 );
15
+
16
+ if ( trash_count > wp_stream_global.bulk_actions.threshold ) {
17
+ warning_message( e );
18
+ }
19
+ });
20
+
21
+ function warning_message( e ) {
22
+ if ( ! window.confirm( wp_stream_global.bulk_actions.i18n.confirm_action ) ) {
23
+ e.preventDefault();
24
+ }
25
+ }
26
+
27
+ });
28
+
29
+ // Regenerate alternating row classes
30
+ var wp_stream_regenerate_alt_rows = function( $rows ) {
31
+ if ( ! $rows.length ) {
32
+ return false;
33
+ }
34
+
35
+ $rows.removeClass( 'alternate' );
36
+
37
+ $rows.each( function( index ) {
38
+ jQuery( this ).addClass( index % 2 ? '' : 'alternate' );
39
+ });
40
+ };
ui/js/live-updates.js ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* globals wp_stream_live_updates, wp_stream_regenerate_alt_rows */
2
+ jQuery( function( $ ) {
3
+
4
+ $( document ).ready( function() {
5
+
6
+ // Only run on wp_stream when page is 1 and the order is desc
7
+ if ( 'toplevel_page_wp_stream' !== wp_stream_live_updates.current_screen || '1' !== wp_stream_live_updates.current_page || 'asc' === wp_stream_live_updates.current_order ) {
8
+ return;
9
+ }
10
+
11
+ // Do not run if there are filters in use
12
+ if ( parseInt( wp_stream_live_updates.current_query_count, 10 ) > 1 ) {
13
+ return;
14
+ }
15
+
16
+ var list_sel = '.toplevel_page_wp_stream #the-list';
17
+
18
+ // Set initial beat to fast. WP is designed to slow this to 15 seconds after 2.5 minutes.
19
+ wp.heartbeat.interval( 'fast' );
20
+
21
+ $( document ).on( 'heartbeat-send.stream', function( e, data ) {
22
+
23
+ data['wp-stream-heartbeat'] = 'live-update';
24
+
25
+ var last_item = $( list_sel + ' tr:first .column-date time' ),
26
+ last_time = 1;
27
+
28
+ if ( 0 !== last_item.length ) {
29
+ last_time = ( '' === last_item.attr( 'datetime' ) ) ? 1 : last_item.attr( 'datetime' );
30
+ }
31
+
32
+ data['wp-stream-heartbeat-last-time'] = last_time;
33
+ data['wp-stream-heartbeat-query'] = wp_stream_live_updates.current_query;
34
+ });
35
+
36
+ // Listen for "heartbeat-tick" on $(document).
37
+ $( document ).on( 'heartbeat-tick.stream', function( e, data ) {
38
+
39
+ // If this no rows return then we kill the script
40
+ if ( ! data['wp-stream-heartbeat'] || 0 === data['wp-stream-heartbeat'].length ) {
41
+ return;
42
+ }
43
+
44
+ var show_on_screen = $( '#edit_stream_per_page' ).val(),
45
+ $current_items = $( list_sel + ' tr' ),
46
+ $new_items = $( data['wp-stream-heartbeat'] );
47
+
48
+ // Remove all default classes and add class to highlight new rows
49
+ $new_items.removeClass().addClass( 'new-row' );
50
+
51
+ // Check if first tr has the alternate class
52
+ var has_class = ( $current_items.first().hasClass( 'alternate' ) );
53
+
54
+ // Apply the good class to the list
55
+ if ( 1 === $new_items.length && ! has_class ) {
56
+ $new_items.addClass( 'alternate' );
57
+ } else {
58
+ var even_or_odd = ( 0 === $new_items.length % 2 && ! has_class ) ? 'even' : 'odd';
59
+ // Add class to nth child because there is more than one element
60
+ $new_items.filter( ':nth-child(' + even_or_odd + ')' ).addClass( 'alternate' );
61
+ }
62
+
63
+ // Add element to the dom
64
+ $( list_sel ).prepend( $new_items );
65
+
66
+ $( '.metabox-prefs input' ).each( function() {
67
+ if ( true !== $( this ).prop( 'checked' ) ) {
68
+ var label = $( this ).val();
69
+ $( 'td.column-' + label ).hide();
70
+ }
71
+ });
72
+
73
+ // Remove the number of element added to the end of the list table
74
+ var slice_rows = show_on_screen - ( $new_items.length + $current_items.length );
75
+
76
+ if ( slice_rows < 0 ) {
77
+ $( list_sel + ' tr' ).slice( slice_rows ).remove();
78
+ }
79
+
80
+ // Remove the no items row
81
+ $( list_sel + ' tr.no-items' ).remove();
82
+
83
+ // Update pagination
84
+ var total_items_i18n = data.total_items_i18n || '';
85
+
86
+ if ( total_items_i18n ) {
87
+ $( '.displaying-num' ).text( total_items_i18n );
88
+ $( '.total-pages' ).text( data.total_pages_i18n );
89
+ $( '.tablenav-pages' ).find( '.next-page, .last-page' ).toggleClass( 'disabled', data.total_pages === $( '.current-page' ).val() );
90
+ $( '.tablenav-pages .last-page' ).attr( 'href', data.last_page_link );
91
+ }
92
+
93
+ // Allow others to hook in, ie: timeago
94
+ $( list_sel ).parent().trigger( 'updated' );
95
+
96
+ // Regenerate alternating row classes
97
+ wp_stream_regenerate_alt_rows( $( list_sel + ' tr' ) );
98
+
99
+ // Remove background after a certain amount of time
100
+ setTimeout( function() {
101
+ $( '.new-row' ).addClass( 'fadeout' );
102
+ setTimeout( function() {
103
+ $( list_sel + ' tr' ).removeClass( 'new-row fadeout' );
104
+ }, 500 );
105
+ }, 3000 );
106
+
107
+ });
108
+
109
+ });
110
+
111
+ });
ui/js/migrate.js ADDED
@@ -0,0 +1,114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* globals wp_stream_migrate, ajaxurl */
2
+ jQuery( function( $ ) {
3
+
4
+ var chunks = parseInt( wp_stream_migrate.chunks, 10 ),
5
+ progress_step = ( chunks > 1 ) ? 100 / chunks : 100,
6
+ progress_val = 0;
7
+
8
+ $( document ).on( 'click', '#stream-start-migrate', function( e ) {
9
+ if ( ! window.confirm( wp_stream_migrate.i18n.confirm_start_migrate ) ) {
10
+ e.preventDefault();
11
+ } else {
12
+ stream_migrate_action( 'migrate' );
13
+ }
14
+ });
15
+
16
+ $( document ).on( 'click', '#stream-migrate-reminder', function( e ) {
17
+ if ( ! window.confirm( wp_stream_migrate.i18n.confirm_migrate_reminder ) ) {
18
+ e.preventDefault();
19
+ } else {
20
+ stream_migrate_action( 'delay' );
21
+ }
22
+ });
23
+
24
+ $( document ).on( 'click', '#stream-ignore-migrate', function( e ) {
25
+ if ( ! window.confirm( wp_stream_migrate.i18n.confirm_ignore_migrate ) ) {
26
+ e.preventDefault();
27
+ } else {
28
+ stream_migrate_action( 'ignore' );
29
+ }
30
+ });
31
+
32
+ $( document ).on( 'click', '#stream-migrate-actions-close', function() {
33
+ location.reload( true );
34
+ });
35
+
36
+ function stream_migrate_action( migrate_action ) {
37
+ var data = {
38
+ 'action': 'wp_stream_migrate_action',
39
+ 'migrate_action': migrate_action,
40
+ 'nonce': wp_stream_migrate.nonce
41
+ };
42
+
43
+ $.ajax({
44
+ type: 'POST',
45
+ url: ajaxurl,
46
+ data: data,
47
+ dataType: 'json',
48
+ beforeSend: function() {
49
+ stream_migrate_start( migrate_action );
50
+ },
51
+ success: function( response ) {
52
+ if ( false === response.success ) {
53
+ stream_migrate_end( response.data, true );
54
+ } else {
55
+ if ( 'migrate' === response.data || 'continue' === response.data ) {
56
+ stream_migrate_progress_loop( response.data );
57
+ } else {
58
+ stream_migrate_end( response.data );
59
+ }
60
+ }
61
+ },
62
+ error: function() {
63
+ stream_migrate_end( wp_stream_migrate.i18n.error_message, true );
64
+ }
65
+ });
66
+ }
67
+
68
+ function stream_migrate_progress_loop( migrate_action ) {
69
+ progress_val = ( ( progress_step + progress_val ) < 100 ) ? progress_step + progress_val : 100;
70
+
71
+ $( '#stream-migrate-progress progress' ).val( progress_val );
72
+ $( '#stream-migrate-progress strong' ).text( Math.round( progress_val ) + '%' );
73
+
74
+ stream_migrate_action( migrate_action );
75
+ }
76
+
77
+ function stream_migrate_start( migrate_action ) {
78
+ $( '#stream-migrate-actions' ).hide();
79
+ $( '#stream-migrate-blog-link' ).hide();
80
+ $( '#stream-migrate-progress' ).show();
81
+
82
+ if ( 'migrate' !== migrate_action && 'continue' !== migrate_action ) {
83
+ $( '#stream-migrate-title' ).text( wp_stream_migrate.i18n.ignore_migrate_title );
84
+ $( '#stream-migrate-message' ).hide();
85
+ $( '#stream-migrate-progress progress' ).hide();
86
+ $( '#stream-migrate-progress strong' ).hide();
87
+ }
88
+
89
+ if ( 'migrate' === migrate_action || 'continue' === migrate_action ) {
90
+ $( '#stream-migrate-title' ).text( wp_stream_migrate.i18n.migrate_process_title );
91
+ $( '#stream-migrate-message' ).text( wp_stream_migrate.i18n.migrate_process_message );
92
+ $( '#stream-migrate-progress progress' ).show();
93
+ $( '#stream-migrate-progress strong' ).show();
94
+ }
95
+ }
96
+
97
+ function stream_migrate_end( message, is_error ) {
98
+ is_error = 'undefined' !== typeof is_error ? is_error : false;
99
+
100
+ $( '#stream-migrate-message' ).hide();
101
+ $( '#stream-migrate-progress progress' ).hide();
102
+ $( '#stream-migrate-progress strong' ).hide();
103
+ $( '#stream-migrate-actions-close' ).show();
104
+
105
+ if ( message ) {
106
+ $( '#stream-migrate-progress em' ).html( message );
107
+
108
+ if ( is_error ) {
109
+ $( '#stream-migrate-progress em' ).css( 'color', '#a00' );
110
+ }
111
+ }
112
+ }
113
+
114
+ });
ui/js/settings.js ADDED
@@ -0,0 +1,378 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* globals confirm, wp_stream, ajaxurl, wp_stream_regenerate_alt_rows */
2
+ jQuery( function( $ ) {
3
+
4
+ var initSettingsSelect2 = function() {
5
+ $( '.stream-exclude-list tr:not(.hidden) input[type=hidden].select2-select.with-source' ).each( function( k, el ) {
6
+ var $input = $( el ),
7
+ $connector = $( this ).prevAll( ':input.connector' );
8
+
9
+ $input.select2({
10
+ data: $input.data( 'values' ),
11
+ allowClear: true,
12
+ placeholder: $input.data( 'placeholder' )
13
+ });
14
+
15
+ if ( '' === $input.val() && '' !== $connector.val() ) {
16
+ $input.select2( 'val', $connector.val() );
17
+ $input.val( '' );
18
+ }
19
+ });
20
+
21
+ var $input_user, $input_ip;
22
+
23
+ $( '.stream-exclude-list tr:not(.hidden) input[type=hidden].select2-select.ip_address' ).each( function( k, el ) {
24
+ $input_ip = $( el );
25
+
26
+ $input_ip.select2({
27
+ ajax: {
28
+ type: 'POST',
29
+ url: ajaxurl,
30
+ dataType: 'json',
31
+ quietMillis: 500,
32
+ data: function( term ) {
33
+ return {
34
+ find: term,
35
+ limit: 10,
36
+ action: 'stream_get_ips',
37
+ nonce: $input_ip.data( 'nonce' )
38
+ };
39
+ },
40
+ results: function( response ) {
41
+ var answer = { results: [] };
42
+
43
+ if ( true !== response.success || undefined === response.data ) {
44
+ return answer;
45
+ }
46
+
47
+ $.each( response.data, function( key, ip ) {
48
+ answer.results.push({
49
+ id: ip,
50
+ text: ip
51
+ });
52
+ });
53
+
54
+ return answer;
55
+ }
56
+ },
57
+ initSelection: function( item, callback ) {
58
+ var data = [];
59
+
60
+ data.push( { id: item.val(), text: item.val() } );
61
+
62
+ callback( data );
63
+ },
64
+ createSearchChoice: function( term ) {
65
+ var ip_chunks = [];
66
+
67
+ ip_chunks = term.match( /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/ );
68
+
69
+ if ( null === ip_chunks ) {
70
+ return;
71
+ }
72
+
73
+ // remove whole match
74
+ ip_chunks.shift();
75
+
76
+ ip_chunks = $.grep(
77
+ ip_chunks,
78
+ function( chunk ) {
79
+ var numeric = parseInt( chunk, 10 );
80
+ return numeric <= 255 && numeric.toString() === chunk;
81
+ }
82
+ );
83
+
84
+ if ( ip_chunks.length < 4 ) {
85
+ return;
86
+ }
87
+
88
+ return {
89
+ id: term,
90
+ text: term
91
+ };
92
+ },
93
+ allowClear: true,
94
+ multiple: true,
95
+ maximumSelectionSize: 1,
96
+ placeholder: $input_ip.data( 'placeholder' )
97
+ });
98
+ }).on( 'change', function() {
99
+ $( this ).prev( '.select2-container' ).find( 'input.select2-input' ).blur();
100
+ });
101
+
102
+ $( '.stream-exclude-list tr:not(.hidden) input[type=hidden].select2-select.author_or_role' ).each( function( k, el ) {
103
+ $input_user = $( el );
104
+
105
+ $input_user.select2({
106
+ ajax: {
107
+ type: 'POST',
108
+ url: ajaxurl,
109
+ dataType: 'json',
110
+ quietMillis: 500,
111
+ data: function( term, page ) {
112
+ return {
113
+ find: term,
114
+ limit: 10,
115
+ pager: page,
116
+ action: 'stream_get_users',
117
+ nonce: $input_user.data( 'nonce' )
118
+ };
119
+ },
120
+ results: function( response ) {
121
+ var roles = [],
122
+ answer = [];
123
+
124
+ roles = $.grep(
125
+ $input_user.data( 'values' ),
126
+ function( role ) {
127
+ var roleVal = $input_user.data( 'select2' )
128
+ .search
129
+ .val()
130
+ .toLowerCase();
131
+ var rolePos = role
132
+ .text
133
+ .toLowerCase()
134
+ .indexOf( roleVal );
135
+ return rolePos >= 0;
136
+ }
137
+ );
138
+
139
+ answer = {
140
+ results: [
141
+ {
142
+ text: 'Roles',
143
+ children: roles
144
+ },
145
+ {
146
+ text: 'Users',
147
+ children: []
148
+ }
149
+ ]
150
+ };
151
+
152
+ if ( true !== response.success || undefined === response.data || true !== response.data.status ) {
153
+ return answer;
154
+ }
155
+
156
+ $.each( response.data.users, function( k, user ) {
157
+ if ( $.contains( roles, user.id ) ) {
158
+ user.disabled = true;
159
+ }
160
+ });
161
+
162
+ answer.results[ 1 ].children = response.data.users;
163
+
164
+ // Notice we return the value of more so Select2 knows if more results can be loaded
165
+ return answer;
166
+ }
167
+ },
168
+ initSelection: function( item, callback ) {
169
+ callback( { id: item.data( 'selected-id' ), text: item.data( 'selected-text' ) } );
170
+ },
171
+ formatResult: function( object, container ) {
172
+ var result = object.text;
173
+
174
+ if ( 'undefined' !== typeof object.icon && object.icon ) {
175
+ result = '<img src="' + object.icon + '" class="wp-stream-select2-icon">' + result;
176
+
177
+ // Add more info to the container
178
+ container.attr( 'title', object.tooltip );
179
+ }
180
+
181
+ // Add more info to the container
182
+ if ( 'undefined' !== typeof object.tooltip ) {
183
+ container.attr( 'title', object.tooltip );
184
+ } else if ( 'undefined' !== typeof object.user_count ) {
185
+ container.attr( 'title', object.user_count );
186
+ }
187
+
188
+ return result;
189
+ },
190
+ formatSelection: function( object ) {
191
+ if ( $.isNumeric( object.id ) && object.text.indexOf( 'icon-users' ) < 0 ) {
192
+ object.text += '<i class="icon16 icon-users"></i>';
193
+ }
194
+
195
+ return object.text;
196
+ },
197
+ allowClear: true,
198
+ placeholder: $input_user.data( 'placeholder' )
199
+ }).on( 'change', function() {
200
+ var value = $( this ).select2( 'data' );
201
+
202
+ $( this ).data( 'selected-id', value.id );
203
+ $( this ).data( 'selected-text', value.text );
204
+ });
205
+ });
206
+
207
+ $( 'ul.select2-choices, ul.select2-choices li, input.select2-input', '.stream-exclude-list tr:not(.hidden) .ip_address' ).on( 'mousedown click focus', function() {
208
+ var $container = $( this ).closest( '.select2-container' ),
209
+ $input = $container.find( 'input.select2-input' ),
210
+ value = $container.select2( 'data' );
211
+
212
+ if ( value.length >= 1 ) {
213
+ $input.blur();
214
+ return false;
215
+ }
216
+ });
217
+
218
+ $( '.stream-exclude-list tr:not(.hidden) input[type=hidden].select2-select.context' ).on( 'change', function( val ) {
219
+ var $connector = $( this ).prevAll( ':input.connector' );
220
+
221
+ if ( undefined !== val.added && undefined !== val.added.parent ) {
222
+ $connector.val( val.added.parent );
223
+ } else {
224
+ $connector.val( $( this ).val() );
225
+ $( this ).val( '' );
226
+ }
227
+ });
228
+
229
+ $( '.stream-exclude-list tr:not(.hidden) .exclude_rules_remove_rule_row' ).on( 'click', function() {
230
+ var $thisRow = $( this ).closest( 'tr' );
231
+
232
+ $thisRow.remove();
233
+
234
+ recalculate_rules_found();
235
+ recalculate_rules_selected();
236
+ });
237
+
238
+ };
239
+
240
+ initSettingsSelect2();
241
+
242
+ $( '#exclude_rules_new_rule' ).on( 'click', function() {
243
+ var $excludeList = $( 'table.stream-exclude-list' );
244
+
245
+ $( '.select2-select', $excludeList ).each( function() {
246
+ $( this ).select2( 'destroy' );
247
+ });
248
+
249
+ var $lastRow = $( 'tr', $excludeList ).last(),
250
+ $newRow = $lastRow.clone();
251
+
252
+ $newRow.removeAttr( 'class' );
253
+ $( '.stream-exclude-list tbody :input' ).off();
254
+ $( ':input', $newRow ).off().val( '' );
255
+
256
+ $lastRow.after( $newRow );
257
+
258
+ initSettingsSelect2();
259
+
260
+ recalculate_rules_found();
261
+ recalculate_rules_selected();
262
+ });
263
+
264
+ $( '#exclude_rules_remove_rules' ).on( 'click', function() {
265
+ var $excludeList = $( 'table.stream-exclude-list' ),
266
+ selectedRows = $( 'tbody input.cb-select:checked', $excludeList ).closest( 'tr' );
267
+
268
+ if ( ( $( 'tbody tr', $excludeList ).length - selectedRows.length ) >= 2 ) {
269
+ selectedRows.remove();
270
+ } else {
271
+ $( ':input', selectedRows ).val( '' );
272
+ $( selectedRows ).not( ':first' ).remove();
273
+ $( '.select2-select', selectedRows ).select2( 'val', '' );
274
+ }
275
+
276
+ $excludeList.find( 'input.cb-select' ).prop( 'checked', false );
277
+
278
+ recalculate_rules_found();
279
+ recalculate_rules_selected();
280
+ });
281
+
282
+ $( '.stream-exclude-list' ).closest( 'form' ).submit( function() {
283
+ $( '.stream-exclude-list tbody tr', this ).each( function() {
284
+ if ( 0 === $( this ).find( ':input[value][value!=""]' ).length ) {
285
+ // Don't send inputs in this row
286
+ $( this ).find( ':input[value]' ).removeAttr( 'name' );
287
+ }
288
+ });
289
+ });
290
+
291
+ $( '.stream-exclude-list' ).closest( 'td' ).prev( 'th' ).hide();
292
+
293
+ $( 'table.stream-exclude-list' ).on( 'click', 'input.cb-select', function() {
294
+ recalculate_rules_selected();
295
+ });
296
+
297
+ function recalculate_rules_selected() {
298
+ var $selectedRows = $( 'table.stream-exclude-list tbody tr:not( .hidden ) input.cb-select:checked' ),
299
+ $deleteButton = $( '#exclude_rules_remove_rules' );
300
+
301
+ if ( 0 === $selectedRows.length ) {
302
+ $deleteButton.prop( 'disabled', true );
303
+ } else {
304
+ $deleteButton.prop( 'disabled', false );
305
+ }
306
+ }
307
+
308
+ function recalculate_rules_found() {
309
+ var $allRows = $( 'table.stream-exclude-list tbody tr:not( .hidden )' ),
310
+ $noRulesFound = $( 'table.stream-exclude-list tbody tr.no-items' ),
311
+ $selectAll = $( '.check-column.manage-column input.cb-select' ),
312
+ $deleteButton = $( '#exclude_rules_remove_rules' );
313
+
314
+ if ( 0 === $allRows.length ) {
315
+ $noRulesFound.show();
316
+ $selectAll.prop( 'disabled', true );
317
+ $deleteButton.prop( 'disabled', true );
318
+ } else {
319
+ $noRulesFound.hide();
320
+ $selectAll.prop( 'disabled', false );
321
+ }
322
+
323
+ wp_stream_regenerate_alt_rows( $allRows );
324
+ }
325
+
326
+ $( document ).ready( function() {
327
+ recalculate_rules_found();
328
+ recalculate_rules_selected();
329
+ });
330
+
331
+ // Confirmation on some important actions
332
+ $( '#wp_stream_general_reset_site_settings' ).click( function( e ) {
333
+ if ( ! confirm( wp_stream.i18n.confirm_defaults ) ) {
334
+ e.preventDefault();
335
+ }
336
+ });
337
+
338
+ // Settings page tabs
339
+ var $tabs = $( '.nav-tab-wrapper' ),
340
+ $panels = $( '.nav-tab-content table.form-table' ),
341
+ $activeTab = $tabs.find( '.nav-tab-active' ),
342
+ defaultIndex = $activeTab.length > 0 ? $tabs.find( 'a' ).index( $activeTab ) : 0,
343
+ hashIndex = window.location.hash.match( /^#(\d+)$/ ),
344
+ currentHash = ( null !== hashIndex ? hashIndex[ 1 ] : defaultIndex ),
345
+ syncFormAction = function( index ) {
346
+ var $optionsForm = $( 'input[name="option_page"][value^="wp_stream"]' ).closest( 'form' );
347
+ if ( $optionsForm.length === 0 ) {
348
+ return;
349
+ }
350
+ var currentAction = $optionsForm.attr( 'action' );
351
+
352
+ $optionsForm.prop( 'action', currentAction.replace( /(^[^#]*).*$/, '$1#' + index ) );
353
+ };
354
+
355
+ $tabs.on( 'click', 'a', function() {
356
+ var index = $tabs.find( 'a' ).index( $( this ) ),
357
+ hashIndex = window.location.hash.match( /^#(\d+)$/ );
358
+
359
+ $panels.hide().eq( index ).show();
360
+
361
+ $tabs
362
+ .find( 'a' )
363
+ .removeClass( 'nav-tab-active' )
364
+ .filter( $( this ) )
365
+ .addClass( 'nav-tab-active' );
366
+
367
+ if ( '' === window.location.hash || null !== hashIndex ) {
368
+ window.location.hash = index;
369
+ }
370
+
371
+ syncFormAction( index );
372
+
373
+ return false;
374
+ });
375
+
376
+ $tabs.children().eq( currentHash ).trigger( 'click' );
377
+
378
+ });
ui/js/wpseo-admin.js ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // globals jQuery, $
2
+ jQuery( function( $ ) {
3
+
4
+ var highlight, input, tab;
5
+
6
+ if ( window.location.hash.substr( 'stream-highlight-' ) ) {
7
+ highlight = window.location.hash.replace( 'stream-highlight-', '' );
8
+ input = $( ':input' + highlight );
9
+
10
+ window.location.hash = '';
11
+
12
+ if ( input.length ) {
13
+ if ( $( '#wpseo-tabs' ).length ) {
14
+ tab = input.parents( '.wpseotab' ).first().attr( 'id' );
15
+ window.location.hash = '#top#' + tab;
16
+ }
17
+
18
+ jQuery( document ).ready( function() {
19
+ setTimeout( function() {
20
+ $( 'body,html' ).animate({
21
+ scrollTop: input.offset().top - 50
22
+ }, 'slow', function() {
23
+ input.animate( { backgroundColor: 'yellow' }, 'slow' );
24
+ });
25
+ }, 500 );
26
+ });
27
+ }
28
+ }
29
+
30
+ });
ui/lib/select2/CONTRIBUTING.md ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Contributing to Select2
2
+ =======================
3
+ Looking to contribute something to Select2? **Here's how you can help.**
4
+
5
+ Please take a moment to review this document in order to make the contribution
6
+ process easy and effective for everyone involved.
7
+
8
+ Following these guidelines helps to communicate that you respect the time of
9
+ the developers managing and developing this open source project. In return,
10
+ they should reciprocate that respect in addressing your issue or assessing
11
+ patches and features.
12
+
13
+ Using the issue tracker
14
+ -----------------------
15
+ When [reporting bugs][reporting-bugs] or
16
+ [requesting features][requesting-features], the
17
+ [issue tracker on GitHub][issue-tracker] is the recommended channel to use.
18
+
19
+ The issue tracker **is not** a place for support requests. The
20
+ [mailing list][mailing-list] or [IRC channel][irc-channel] are better places to
21
+ get help.
22
+
23
+ Reporting bugs with Select2
24
+ ---------------------------
25
+ We really appreciate clear bug reports that _consistently_ show an issue
26
+ _within Select2_.
27
+
28
+ The ideal bug report follows these guidelines:
29
+
30
+ 1. **Use the [GitHub issue search][issue-search]** &mdash; Check if the issue
31
+ has already been reported.
32
+ 2. **Check if the issue has been fixed** &mdash; Try to reproduce the problem
33
+ using the code in the `master` branch.
34
+ 3. **Isolate the problem** &mdash; Try to create an
35
+ [isolated test case][isolated-case] that consistently reproduces the problem.
36
+
37
+ Please try to be as detailed as possible in your bug report, especially if an
38
+ isolated test case cannot be made. Some useful questions to include the answer
39
+ to are:
40
+
41
+ - What steps can be used to reproduce the issue?
42
+ - What is the bug and what is the expected outcome?
43
+ - What browser(s) and Operating System have you tested with?
44
+ - Does the bug happen consistently across all tested browsers?
45
+ - What version of jQuery are you using? And what version of Select2?
46
+ - Are you using Select2 with other plugins?
47
+
48
+ All of these questions will help people fix and identify any potential bugs.
49
+
50
+ Requesting features in Select2
51
+ ------------------------------
52
+ Select2 is a large library that carries with it a lot of functionality. Because
53
+ of this, many feature requests will not be implemented in the core library.
54
+
55
+ Before starting work on a major feature for Select2, **contact the
56
+ [community][community] first** or you may risk spending a considerable amount of
57
+ time on something which the project developers are not interested in bringing
58
+ into the project.
59
+
60
+ ### Select2 4.0
61
+
62
+ Many feature requests will be closed off until 4.0, where Select2 plans to adopt
63
+ a more flexible API. If you are interested in helping with the development of
64
+ the next major Select2 release, please send a message to the
65
+ [mailing list][mailing-list] or [irc channel][irc-channel] for more information.
66
+
67
+ Triaging issues and pull requests
68
+ ---------------------------------
69
+ Anyone can help the project maintainers triage issues and review pull requests.
70
+
71
+ ### Handling new issues
72
+
73
+ Select2 regularly receives new issues which need to be tested and organized.
74
+
75
+ When a new issue that comes in that is similar to another existing issue, it
76
+ should be checked to make sure it is not a duplicate. Duplicates issues should
77
+ be marked by replying to the issue with "Duplicate of #[issue number]" where
78
+ `[issue number]` is the url or issue number for the existing issue. This will
79
+ allow the project maintainers to quickly close off additional issues and keep
80
+ the discussion focused within a single issue.
81
+
82
+ If you can test issues that are reported to Select2 that contain test cases and
83
+ confirm under what conditions bugs happen, that will allow others to identify
84
+ what causes a bug quicker.
85
+
86
+ ### Reviewing pull requests
87
+
88
+ It is very common for pull requests to be opened for issues that contain a clear
89
+ solution to the problem. These pull requests should be rigorously reviewed by
90
+ the community before being accepted. If you are not sure about a piece of
91
+ submitted code, or know of a better way to do something, do not hesitate to make
92
+ a comment on the pull request.
93
+
94
+ It should also be made clear that **all code contributed to Select** must be
95
+ licensable under the [Apache 2 or GPL 2 licenses][licensing]. Code that cannot
96
+ be released under either of these licenses **cannot be accepted** into the
97
+ project.
98
+
99
+ [community]: https://github.com/ivaynberg/select2#community
100
+ [reporting-bugs]: #reporting-bugs-with-select2
101
+ [requesting-features]: #requesting-features-in-select2
102
+ [issue-tracker]: https://github.com/ivaynberg/select2/issues
103
+ [mailing-list]: https://github.com/ivaynberg/select2#mailing-list
104
+ [irc-channel]: https://github.com/ivaynberg/select2#irc-channel
105
+ [issue-search]: https://github.com/ivaynberg/select2/search?q=&type=Issues
106
+ [isolated-case]: http://css-tricks.com/6263-reduced-test-cases/
107
+ [licensing]: https://github.com/ivaynberg/select2#copyright-and-license
ui/lib/select2/LICENSE ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Copyright 2014 Igor Vaynberg
2
+
3
+ Version: @@ver@@ Timestamp: @@timestamp@@
4
+
5
+ This software is licensed under the Apache License, Version 2.0 (the "Apache License") or the GNU
6
+ General Public License version 2 (the "GPL License"). You may choose either license to govern your
7
+ use of this software only upon the condition that you accept all of the terms of either the Apache
8
+ License or the GPL License.
9
+
10
+ You may obtain a copy of the Apache License and the GPL License at:
11
+
12
+ http://www.apache.org/licenses/LICENSE-2.0
13
+ http://www.gnu.org/licenses/gpl-2.0.html
14
+
15
+ Unless required by applicable law or agreed to in writing, software distributed under the Apache License
16
+ or the GPL License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
17
+ either express or implied. See the Apache License and the GPL License for the specific language governing
18
+ permissions and limitations under the Apache License and the GPL License.
ui/lib/select2/README.md ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Select2
2
+ =======
3
+
4
+ Select2 is a jQuery-based replacement for select boxes. It supports searching, remote data sets, and infinite scrolling of results.
5
+
6
+ To get started, checkout examples and documentation at http://select2.github.io/select2/
7
+
8
+ Use cases
9
+ ---------
10
+
11
+ * Enhancing native selects with search.
12
+ * Enhancing native selects with a better multi-select interface.
13
+ * Loading data from JavaScript: easily load items via ajax and have them searchable.
14
+ * Nesting optgroups: native selects only support one level of nested. Select2 does not have this restriction.
15
+ * Tagging: ability to add new items on the fly.
16
+ * Working with large, remote datasets: ability to partially load a dataset based on the search term.
17
+ * Paging of large datasets: easy support for loading more pages when the results are scrolled to the end.
18
+ * Templating: support for custom rendering of results and selections.
19
+
20
+ Browser compatibility
21
+ ---------------------
22
+ * IE 8+
23
+ * Chrome 8+
24
+ * Firefox 10+
25
+ * Safari 3+
26
+ * Opera 10.6+
27
+
28
+ Usage
29
+ -----
30
+ You can source Select2 directly from a CDN like [jsDelivr](http://www.jsdelivr.com/#!select2) or [CDNJS](http://www.cdnjs.com/libraries/select2), [download it from this GitHub repo](https://github.com/select2/select2/tags), or use one of the integrations below.
31
+
32
+ Integrations
33
+ ------------
34
+
35
+ * [Wicket-Select2](https://github.com/ivaynberg/wicket-select2) (Java / [Apache Wicket](http://wicket.apache.org))
36
+ * [select2-rails](https://github.com/argerim/select2-rails) (Ruby on Rails)
37
+ * [AngularUI](http://angular-ui.github.io/#ui-select) ([AngularJS](https://angularjs.org/))
38
+ * [Django](https://github.com/applegrew/django-select2)
39
+ * [Symfony](https://github.com/19Gerhard85/sfSelect2WidgetsPlugin)
40
+ * [Symfony2](https://github.com/avocode/FormExtensions)
41
+ * [Bootstrap 2](https://github.com/t0m/select2-bootstrap-css) and [Bootstrap 3](https://github.com/t0m/select2-bootstrap-css/tree/bootstrap3) (CSS skins)
42
+ * [Meteor](https://github.com/nate-strauser/meteor-select2) (modern reactive JavaScript framework; + [Bootstrap 3 skin](https://github.com/esperadomedia/meteor-select2-bootstrap3-css/))
43
+ * [Meteor](https://jquery-select2.meteor.com)
44
+ * [Yii 2.x](http://demos.krajee.com/widgets#select2)
45
+ * [Yii 1.x](https://github.com/tonybolzan/yii-select2)
46
+ * [AtmosphereJS](https://atmospherejs.com/package/jquery-select2)
47
+ * [EmberJS](https://github.com/iStefo/ember-select-2)
48
+
49
+ ### Example Integrations
50
+
51
+ * [Knockout.js](https://github.com/ivaynberg/select2/wiki/Knockout.js-Integration)
52
+ * [Socket.IO](https://github.com/ivaynberg/select2/wiki/Socket.IO-Integration)
53
+ * [PHP](https://github.com/ivaynberg/select2/wiki/PHP-Example)
54
+ * [.Net MVC] (https://github.com/ivaynberg/select2/wiki/.Net-MVC-Example)
55
+
56
+ Internationalization (i18n)
57
+ ---------------------------
58
+
59
+ Select2 supports multiple languages by simply including the right language JS
60
+ file (`select2_locale_it.js`, `select2_locale_nl.js`, etc.) after `select2.js`.
61
+
62
+ Missing a language? Just copy `select2_locale_en.js.template`, translate
63
+ it, and make a pull request back to Select2 here on GitHub.
64
+
65
+ Documentation
66
+ -------------
67
+
68
+ The documentation for Select2 is available [through GitHub Pages](http://select2.github.io/select2/) and is located within this repository in the [`gh-pages` branch](https://github.com/ivaynberg/select2/tree/gh-pages).
69
+
70
+ Community
71
+ ---------
72
+
73
+ ### Bug tracker
74
+
75
+ Have a bug? Please create an issue here on GitHub!
76
+
77
+ https://github.com/ivaynberg/select2/issues
78
+
79
+ ### Mailing list
80
+
81
+ Have a question? Ask on our mailing list!
82
+
83
+ select2@googlegroups.com
84
+
85
+ https://groups.google.com/d/forum/select2
86
+
87
+ ### IRC channel
88
+
89
+ Need help implementing Select2 in your project? Ask in our IRC channel!
90
+
91
+ **Network:** [Freenode](https://freenode.net/) (`chat.freenode.net`)
92
+
93
+ **Channel:** `#select2`
94
+
95
+ **Web access:** https://webchat.freenode.net/?channels=select2
96
+
97
+ Copyright and license
98
+ ---------------------
99
+
100
+ Copyright 2015 Igor Vaynberg
101
+
102
+ This software is licensed under the Apache License, Version 2.0 (the "Apache License") or the GNU
103
+ General Public License version 2 (the "GPL License"). You may choose either license to govern your
104
+ use of this software only upon the condition that you accept all of the terms of either the Apache
105
+ License or the GPL License.
106
+
107
+ You may obtain a copy of the Apache License and the GPL License in the LICENSE file, or at:
108
+
109
+ http://www.apache.org/licenses/LICENSE-2.0
110
+ http://www.gnu.org/licenses/gpl-2.0.html
111
+
112
+ Unless required by applicable law or agreed to in writing, software distributed under the Apache License
113
+ or the GPL License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
114
+ either express or implied. See the Apache License and the GPL License for the specific language governing
115
+ permissions and limitations under the Apache License and the GPL License.
ui/lib/select2/bower.json ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "select2",
3
+ "version": "3.5.2",
4
+ "main": ["select2.js", "select2.css", "select2.png", "select2x2.png", "select2-spinner.gif"],
5
+ "dependencies": {
6
+ "jquery": ">= 1.7.1"
7
+ }
8
+ }
ui/lib/select2/component.json ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "select2",
3
+ "repo": "ivaynberg/select2",
4
+ "description": "Select2 is a jQuery based replacement for select boxes. It supports searching, remote data sets, and infinite scrolling of results.",
5
+ "version": "3.5.2",
6
+ "demo": "http://ivaynberg.github.io/select2/",
7
+ "keywords": [
8
+ "jquery"
9
+ ],
10
+ "main": "select2.js",
11
+ "styles": [
12
+ "select2.css",
13
+ "select2-bootstrap.css"
14
+ ],
15
+ "scripts": [
16
+ "select2.js",
17
+ "select2_locale_ar.js",
18
+ "select2_locale_bg.js",
19
+ "select2_locale_ca.js",
20
+ "select2_locale_cs.js",
21
+ "select2_locale_da.js",
22
+ "select2_locale_de.js",
23
+ "select2_locale_el.js",
24
+ "select2_locale_es.js",
25
+ "select2_locale_et.js",
26
+ "select2_locale_eu.js",
27
+ "select2_locale_fa.js",
28
+ "select2_locale_fi.js",
29
+ "select2_locale_fr.js",
30
+ "select2_locale_gl.js",
31
+ "select2_locale_he.js",
32
+ "select2_locale_hr.js",
33
+ "select2_locale_hu.js",
34
+ "select2_locale_id.js",
35
+ "select2_locale_is.js",
36
+ "select2_locale_it.js",
37
+ "select2_locale_ja.js",
38
+ "select2_locale_ka.js",
39
+ "select2_locale_ko.js",
40
+ "select2_locale_lt.js",
41
+ "select2_locale_lv.js",
42
+ "select2_locale_mk.js",
43
+ "select2_locale_ms.js",
44
+ "select2_locale_nl.js",
45
+ "select2_locale_no.js",
46
+ "select2_locale_pl.js",
47
+ "select2_locale_pt-BR.js",
48
+ "select2_locale_pt-PT.js",
49
+ "select2_locale_ro.js",
50
+ "select2_locale_ru.js",
51
+ "select2_locale_sk.js",
52
+ "select2_locale_sv.js",
53
+ "select2_locale_th.js",
54
+ "select2_locale_tr.js",
55
+ "select2_locale_uk.js",
56
+ "select2_locale_vi.js",
57
+ "select2_locale_zh-CN.js",
58
+ "select2_locale_zh-TW.js"
59
+ ],
60
+ "images": [
61
+ "select2-spinner.gif",
62
+ "select2.png",
63
+ "select2x2.png"
64
+ ],
65
+ "license": "MIT"
66
+ }
ui/lib/select2/composer.json ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name":
3
+ "ivaynberg/select2",
4
+ "description": "Select2 is a jQuery based replacement for select boxes.",
5
+ "version": "3.5.2",
6
+ "type": "component",
7
+ "homepage": "http://ivaynberg.github.io/select2/",
8
+ "license": "Apache-2.0",
9
+ "require": {
10
+ "robloach/component-installer": "*",
11
+ "components/jquery": ">=1.7.1"
12
+ },
13
+ "extra": {
14
+ "component": {
15
+ "scripts": [
16
+ "select2.js"
17
+ ],
18
+ "files": [
19
+ "select2.js",
20
+ "select2_locale_*.js",
21
+ "select2.css",
22
+ "select2-bootstrap.css",
23
+ "select2-spinner.gif",
24
+ "select2.png",
25
+ "select2x2.png"
26
+ ]
27
+ }
28
+ }
29
+ }
ui/lib/select2/package.json ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name" : "Select2",
3
+ "description": "Select2 is a jQuery based replacement for select boxes. It supports searching, remote data sets, and infinite scrolling of results.",
4
+ "homepage": "http://ivaynberg.github.io/select2",
5
+ "author": "Igor Vaynberg",
6
+ "repository": {"type": "git", "url": "git://github.com/ivaynberg/select2.git"},
7
+ "main": "select2.js",
8
+ "version": "3.5.2",
9
+ "jspm": {
10
+ "main": "select2",
11
+ "files": ["select2.js", "select2.png", "select2.css", "select2-spinner.gif"],
12
+ "shim": {
13
+ "select2": {
14
+ "imports": ["jquery", "./select2.css!"],
15
+ "exports": "$"
16
+ }
17
+ },
18
+ "buildConfig": { "uglify": true }
19
+ }
20
+ }
ui/lib/select2/release.sh ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ set -e
3
+
4
+ echo -n "Enter the version for this release: "
5
+
6
+ read ver
7
+
8
+ if [ ! $ver ]; then
9
+ echo "Invalid version."
10
+ exit
11
+ fi
12
+
13
+ name="select2"
14
+ js="$name.js"
15
+ mini="$name.min.js"
16
+ css="$name.css"
17
+ release="$name-$ver"
18
+ tag="$ver"
19
+ branch="build-$ver"
20
+ curbranch=`git branch | grep "*" | sed "s/* //"`
21
+ timestamp=$(date)
22
+ tokens="s/@@ver@@/$ver/g;s/\@@timestamp@@/$timestamp/g"
23
+ remote="origin"
24
+
25
+ echo "Pulling from origin"
26
+
27
+ git pull
28
+
29
+ echo "Updating Version Identifiers"
30
+
31
+ sed -E -e "s/\"version\": \"([0-9\.]+)\",/\"version\": \"$ver\",/g" -i -- bower.json select2.jquery.json component.json composer.json package.json
32
+
33
+ git add bower.json
34
+ git add select2.jquery.json
35
+ git add component.json
36
+ git add composer.json
37
+ git add package.json
38
+
39
+ git commit -m "modified version identifiers in descriptors for release $ver"
40
+ git push
41
+
42
+ git branch "$branch"
43
+ git checkout "$branch"
44
+
45
+ echo "Tokenizing..."
46
+
47
+ find . -name "$js" | xargs -I{} sed -e "$tokens" -i -- {}
48
+ find . -name "$css" | xargs -I{} sed -e "$tokens" -i -- {}
49
+
50
+ sed -e "s/latest/$ver/g" -i -- bower.json
51
+
52
+ git add "$js"
53
+ git add "$css"
54
+
55
+ echo "Minifying..."
56
+
57
+ echo "/*" > "$mini"
58
+ cat LICENSE | sed "$tokens" >> "$mini"
59
+ echo "*/" >> "$mini"
60
+
61
+ curl -s \
62
+ --data-urlencode "js_code@$js" \
63
+ http://marijnhaverbeke.nl/uglifyjs \
64
+ >> "$mini"
65
+
66
+ git add "$mini"
67
+
68
+ git commit -m "release $ver"
69
+
70
+ echo "Tagging..."
71
+ git tag -a "$tag" -m "tagged version $ver"
72
+ git push "$remote" --tags
73
+
74
+ echo "Cleaning Up..."
75
+
76
+ git checkout "$curbranch"
77
+ git branch -D "$branch"
78
+
79
+ echo "Done"
ui/lib/select2/select2-bootstrap.css ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .form-control .select2-choice {
2
+ border: 0;
3
+ border-radius: 2px;
4
+ }
5
+
6
+ .form-control .select2-choice .select2-arrow {
7
+ border-radius: 0 2px 2px 0;
8
+ }
9
+
10
+ .form-control.select2-container {
11
+ height: auto !important;
12
+ padding: 0;
13
+ }
14
+
15
+ .form-control.select2-container.select2-dropdown-open {
16
+ border-color: #5897FB;
17
+ border-radius: 3px 3px 0 0;
18
+ }
19
+
20
+ .form-control .select2-container.select2-dropdown-open .select2-choices {
21
+ border-radius: 3px 3px 0 0;
22
+ }
23
+
24
+ .form-control.select2-container .select2-choices {
25
+ border: 0 !important;
26
+ border-radius: 3px;
27
+ }
28
+
29
+ .control-group.warning .select2-container .select2-choice,
30
+ .control-group.warning .select2-container .select2-choices,
31
+ .control-group.warning .select2-container-active .select2-choice,
32
+ .control-group.warning .select2-container-active .select2-choices,
33
+ .control-group.warning .select2-dropdown-open.select2-drop-above .select2-choice,
34
+ .control-group.warning .select2-dropdown-open.select2-drop-above .select2-choices,
35
+ .control-group.warning .select2-container-multi.select2-container-active .select2-choices {
36
+ border: 1px solid #C09853 !important;
37
+ }
38
+
39
+ .control-group.warning .select2-container .select2-choice div {
40
+ border-left: 1px solid #C09853 !important;
41
+ background: #FCF8E3 !important;
42
+ }
43
+
44
+ .control-group.error .select2-container .select2-choice,
45
+ .control-group.error .select2-container .select2-choices,
46
+ .control-group.error .select2-container-active .select2-choice,
47
+ .control-group.error .select2-container-active .select2-choices,
48
+ .control-group.error .select2-dropdown-open.select2-drop-above .select2-choice,
49
+ .control-group.error .select2-dropdown-open.select2-drop-above .select2-choices,
50
+ .control-group.error .select2-container-multi.select2-container-active .select2-choices {
51
+ border: 1px solid #B94A48 !important;
52
+ }
53
+
54
+ .control-group.error .select2-container .select2-choice div {
55
+ border-left: 1px solid #B94A48 !important;
56
+ background: #F2DEDE !important;
57
+ }
58
+
59
+ .control-group.info .select2-container .select2-choice,
60
+ .control-group.info .select2-container .select2-choices,
61
+ .control-group.info .select2-container-active .select2-choice,
62
+ .control-group.info .select2-container-active .select2-choices,
63
+ .control-group.info .select2-dropdown-open.select2-drop-above .select2-choice,
64
+ .control-group.info .select2-dropdown-open.select2-drop-above .select2-choices,
65
+ .control-group.info .select2-container-multi.select2-container-active .select2-choices {
66
+ border: 1px solid #3A87AD !important;
67
+ }
68
+
69
+ .control-group.info .select2-container .select2-choice div {
70
+ border-left: 1px solid #3A87AD !important;
71
+ background: #D9EDF7 !important;
72
+ }
73
+
74
+ .control-group.success .select2-container .select2-choice,
75
+ .control-group.success .select2-container .select2-choices,
76
+ .control-group.success .select2-container-active .select2-choice,
77
+ .control-group.success .select2-container-active .select2-choices,
78
+ .control-group.success .select2-dropdown-open.select2-drop-above .select2-choice,
79
+ .control-group.success .select2-dropdown-open.select2-drop-above .select2-choices,
80
+ .control-group.success .select2-container-multi.select2-container-active .select2-choices {
81
+ border: 1px solid #468847 !important;
82
+ }
83
+
84
+ .control-group.success .select2-container .select2-choice div {
85
+ border-left: 1px solid #468847 !important;
86
+ background: #DFF0D8 !important;
87
+ }
ui/lib/select2/select2-spinner.gif ADDED
Binary file
ui/lib/select2/select2.css ADDED
@@ -0,0 +1,692 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ Version: @@ver@@ Timestamp: @@timestamp@@
3
+ */
4
+ .select2-container {
5
+ margin: 0;
6
+ position: relative;
7
+ display: inline-block;
8
+ vertical-align: middle;
9
+ }
10
+
11
+ .select2-container,
12
+ .select2-drop,
13
+ .select2-search,
14
+ .select2-search input {
15
+ /*
16
+ Force border-box so that % widths fit the parent
17
+ container without overlap because of margin/padding.
18
+ More Info : http://www.quirksmode.org/css/box.html
19
+ */
20
+ -webkit-box-sizing: border-box; /* webkit */
21
+ -moz-box-sizing: border-box; /* firefox */
22
+ box-sizing: border-box; /* css3 */
23
+ }
24
+
25
+ .select2-container .select2-choice {
26
+ display: block;
27
+ height: 26px;
28
+ padding: 0 0 0 8px;
29
+ overflow: hidden;
30
+ position: relative;
31
+
32
+ border: 1px solid #aaa;
33
+ white-space: nowrap;
34
+ line-height: 26px;
35
+ color: #444;
36
+ text-decoration: none;
37
+
38
+ border-radius: 4px;
39
+
40
+ background-clip: padding-box;
41
+
42
+ -webkit-touch-callout: none;
43
+ -webkit-user-select: none;
44
+ -moz-user-select: none;
45
+ -ms-user-select: none;
46
+ user-select: none;
47
+
48
+ background-color: #fff;
49
+ background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #eee), color-stop(0.5, #fff));
50
+ background-image: -webkit-linear-gradient(center bottom, #eee 0%, #fff 50%);
51
+ background-image: -moz-linear-gradient(center bottom, #eee 0%, #fff 50%);
52
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr = '#ffffff', endColorstr = '#eeeeee', GradientType = 0);
53
+ background-image: linear-gradient(to top, #eee 0%, #fff 50%);
54
+ }
55
+
56
+ html[dir="rtl"] .select2-container .select2-choice {
57
+ padding: 0 8px 0 0;
58
+ }
59
+
60
+ .select2-container.select2-drop-above .select2-choice {
61
+ border-bottom-color: #aaa;
62
+
63
+ border-radius: 0 0 4px 4px;
64
+
65
+ background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #eee), color-stop(0.9, #fff));
66
+ background-image: -webkit-linear-gradient(center bottom, #eee 0%, #fff 90%);
67
+ background-image: -moz-linear-gradient(center bottom, #eee 0%, #fff 90%);
68
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#eeeeee', GradientType=0);
69
+ background-image: linear-gradient(to bottom, #eee 0%, #fff 90%);
70
+ }
71
+
72
+ .select2-container.select2-allowclear .select2-choice .select2-chosen {
73
+ margin-right: 42px;
74
+ }
75
+
76
+ .select2-container .select2-choice > .select2-chosen {
77
+ margin-right: 26px;
78
+ display: block;
79
+ overflow: hidden;
80
+
81
+ white-space: nowrap;
82
+
83
+ text-overflow: ellipsis;
84
+ float: none;
85
+ width: auto;
86
+ }
87
+
88
+ html[dir="rtl"] .select2-container .select2-choice > .select2-chosen {
89
+ margin-left: 26px;
90
+ margin-right: 0;
91
+ }
92
+
93
+ .select2-container .select2-choice abbr {
94
+ display: none;
95
+ width: 12px;
96
+ height: 12px;
97
+ position: absolute;
98
+ right: 24px;
99
+ top: 8px;
100
+
101
+ font-size: 1px;
102
+ text-decoration: none;
103
+
104
+ border: 0;
105
+ background: url('select2.png') right top no-repeat;
106
+ cursor: pointer;
107
+ outline: 0;
108
+ }
109
+
110
+ .select2-container.select2-allowclear .select2-choice abbr {
111
+ display: inline-block;
112
+ }
113
+
114
+ .select2-container .select2-choice abbr:hover {
115
+ background-position: right -11px;
116
+ cursor: pointer;
117
+ }
118
+
119
+ .select2-drop-mask {
120
+ border: 0;
121
+ margin: 0;
122
+ padding: 0;
123
+ position: fixed;
124
+ left: 0;
125
+ top: 0;
126
+ min-height: 100%;
127
+ min-width: 100%;
128
+ height: auto;
129
+ width: auto;
130
+ opacity: 0;
131
+ z-index: 9998;
132
+ /* styles required for IE to work */
133
+ background-color: #fff;
134
+ filter: alpha(opacity=0);
135
+ }
136
+
137
+ .select2-drop {
138
+ width: 100%;
139
+ margin-top: -1px;
140
+ position: absolute;
141
+ z-index: 9999;
142
+ top: 100%;
143
+
144
+ background: #fff;
145
+ color: #000;
146
+ border: 1px solid #aaa;
147
+ border-top: 0;
148
+
149
+ border-radius: 0 0 4px 4px;
150
+
151
+ -webkit-box-shadow: 0 4px 5px rgba(0, 0, 0, .15);
152
+ box-shadow: 0 4px 5px rgba(0, 0, 0, .15);
153
+ }
154
+
155
+ .select2-drop.select2-drop-above {
156
+ margin-top: 1px;
157
+ border-top: 1px solid #aaa;
158
+ border-bottom: 0;
159
+
160
+ border-radius: 4px 4px 0 0;
161
+
162
+ -webkit-box-shadow: 0 -4px 5px rgba(0, 0, 0, .15);
163
+ box-shadow: 0 -4px 5px rgba(0, 0, 0, .15);
164
+ }
165
+
166
+ .select2-drop-active {
167
+ border: 1px solid #5897fb;
168
+ border-top: none;
169
+ }
170
+
171
+ .select2-drop.select2-drop-above.select2-drop-active {
172
+ border-top: 1px solid #5897fb;
173
+ }
174
+
175
+ .select2-drop-auto-width {
176
+ border-top: 1px solid #aaa;
177
+ width: auto;
178
+ }
179
+
180
+ .select2-container .select2-choice .select2-arrow {
181
+ display: inline-block;
182
+ width: 18px;
183
+ height: 100%;
184
+ position: absolute;
185
+ right: 0;
186
+ top: 0;
187
+
188
+ border-left: 1px solid #aaa;
189
+ border-radius: 0 4px 4px 0;
190
+
191
+ background-clip: padding-box;
192
+
193
+ background: #ccc;
194
+ background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #ccc), color-stop(0.6, #eee));
195
+ background-image: -webkit-linear-gradient(center bottom, #ccc 0%, #eee 60%);
196
+ background-image: -moz-linear-gradient(center bottom, #ccc 0%, #eee 60%);
197
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr = '#eeeeee', endColorstr = '#cccccc', GradientType = 0);
198
+ background-image: linear-gradient(to top, #ccc 0%, #eee 60%);
199
+ }
200
+
201
+ html[dir="rtl"] .select2-container .select2-choice .select2-arrow {
202
+ left: 0;
203
+ right: auto;
204
+
205
+ border-left: none;
206
+ border-right: 1px solid #aaa;
207
+ border-radius: 4px 0 0 4px;
208
+ }
209
+
210
+ .select2-container .select2-choice .select2-arrow b {
211
+ display: block;
212
+ width: 100%;
213
+ height: 100%;
214
+ background: url('select2.png') no-repeat 0 1px;
215
+ }
216
+
217
+ html[dir="rtl"] .select2-container .select2-choice .select2-arrow b {
218
+ background-position: 2px 1px;
219
+ }
220
+
221
+ .select2-search {
222
+ display: inline-block;
223
+ width: 100%;
224
+ min-height: 26px;
225
+ margin: 0;
226
+ padding: 4px 4px 0 4px;
227
+
228
+ position: relative;
229
+ z-index: 10000;
230
+
231
+ white-space: nowrap;
232
+ }
233
+
234
+ .select2-search input {
235
+ width: 100%;
236
+ height: auto !important;
237
+ min-height: 26px;
238
+ padding: 4px 20px 4px 5px;
239
+ margin: 0;
240
+
241
+ outline: 0;
242
+ font-family: sans-serif;
243
+ font-size: 1em;
244
+
245
+ border: 1px solid #aaa;
246
+ border-radius: 0;
247
+
248
+ -webkit-box-shadow: none;
249
+ box-shadow: none;
250
+
251
+ background: #fff url('select2.png') no-repeat 100% -22px;
252
+ background: url('select2.png') no-repeat 100% -22px, -webkit-gradient(linear, left bottom, left top, color-stop(0.85, #fff), color-stop(0.99, #eee));
253
+ background: url('select2.png') no-repeat 100% -22px, -webkit-linear-gradient(center bottom, #fff 85%, #eee 99%);
254
+ background: url('select2.png') no-repeat 100% -22px, -moz-linear-gradient(center bottom, #fff 85%, #eee 99%);
255
+ background: url('select2.png') no-repeat 100% -22px, linear-gradient(to bottom, #fff 85%, #eee 99%) 0 0;
256
+ }
257
+
258
+ html[dir="rtl"] .select2-search input {
259
+ padding: 4px 5px 4px 20px;
260
+
261
+ background: #fff url('select2.png') no-repeat -37px -22px;
262
+ background: url('select2.png') no-repeat -37px -22px, -webkit-gradient(linear, left bottom, left top, color-stop(0.85, #fff), color-stop(0.99, #eee));
263
+ background: url('select2.png') no-repeat -37px -22px, -webkit-linear-gradient(center bottom, #fff 85%, #eee 99%);
264
+ background: url('select2.png') no-repeat -37px -22px, -moz-linear-gradient(center bottom, #fff 85%, #eee 99%);
265
+ background: url('select2.png') no-repeat -37px -22px, linear-gradient(to bottom, #fff 85%, #eee 99%) 0 0;
266
+ }
267
+
268
+ .select2-search input.select2-active {
269
+ background: #fff url('select2-spinner.gif') no-repeat 100%;
270
+ background: url('select2-spinner.gif') no-repeat 100%, -webkit-gradient(linear, left bottom, left top, color-stop(0.85, #fff), color-stop(0.99, #eee));
271
+ background: url('select2-spinner.gif') no-repeat 100%, -webkit-linear-gradient(center bottom, #fff 85%, #eee 99%);
272
+ background: url('select2-spinner.gif') no-repeat 100%, -moz-linear-gradient(center bottom, #fff 85%, #eee 99%);
273
+ background: url('select2-spinner.gif') no-repeat 100%, linear-gradient(to bottom, #fff 85%, #eee 99%) 0 0;
274
+ }
275
+
276
+ .select2-container-active .select2-choice,
277
+ .select2-container-active .select2-choices {
278
+ border: 1px solid #5897fb;
279
+ outline: none;
280
+
281
+ -webkit-box-shadow: 0 0 5px rgba(0, 0, 0, .3);
282
+ box-shadow: 0 0 5px rgba(0, 0, 0, .3);
283
+ }
284
+
285
+ .select2-dropdown-open .select2-choice {
286
+ border-bottom-color: transparent;
287
+ -webkit-box-shadow: 0 1px 0 #fff inset;
288
+ box-shadow: 0 1px 0 #fff inset;
289
+
290
+ border-bottom-left-radius: 0;
291
+ border-bottom-right-radius: 0;
292
+
293
+ background-color: #eee;
294
+ background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #fff), color-stop(0.5, #eee));
295
+ background-image: -webkit-linear-gradient(center bottom, #fff 0%, #eee 50%);
296
+ background-image: -moz-linear-gradient(center bottom, #fff 0%, #eee 50%);
297
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#eeeeee', endColorstr='#ffffff', GradientType=0);
298
+ background-image: linear-gradient(to top, #fff 0%, #eee 50%);
299
+ }
300
+
301
+ .select2-dropdown-open.select2-drop-above .select2-choice,
302
+ .select2-dropdown-open.select2-drop-above .select2-choices {
303
+ border: 1px solid #5897fb;
304
+ border-top-color: transparent;
305
+
306
+ background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0, #fff), color-stop(0.5, #eee));
307
+ background-image: -webkit-linear-gradient(center top, #fff 0%, #eee 50%);
308
+ background-image: -moz-linear-gradient(center top, #fff 0%, #eee 50%);
309
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#eeeeee', endColorstr='#ffffff', GradientType=0);
310
+ background-image: linear-gradient(to bottom, #fff 0%, #eee 50%);
311
+ }
312
+
313
+ .select2-dropdown-open .select2-choice .select2-arrow {
314
+ background: transparent;
315
+ border-left: none;
316
+ filter: none;
317
+ }
318
+ html[dir="rtl"] .select2-dropdown-open .select2-choice .select2-arrow {
319
+ border-right: none;
320
+ }
321
+
322
+ .select2-dropdown-open .select2-choice .select2-arrow b {
323
+ background-position: -18px 1px;
324
+ }
325
+
326
+ html[dir="rtl"] .select2-dropdown-open .select2-choice .select2-arrow b {
327
+ background-position: -16px 1px;
328
+ }
329
+
330
+ .select2-hidden-accessible {
331
+ border: 0;
332
+ clip: rect(0 0 0 0);
333
+ height: 1px;
334
+ margin: -1px;
335
+ overflow: hidden;
336
+ padding: 0;
337
+ position: absolute;
338
+ width: 1px;
339
+ }
340
+
341
+ /* results */
342
+ .select2-results {
343
+ max-height: 200px;
344
+ padding: 0 0 0 4px;
345
+ margin: 4px 4px 4px 0;
346
+ position: relative;
347
+ overflow-x: hidden;
348
+ overflow-y: auto;
349
+ -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
350
+ }
351
+
352
+ html[dir="rtl"] .select2-results {
353
+ padding: 0 4px 0 0;
354
+ margin: 4px 0 4px 4px;
355
+ }
356
+
357
+ .select2-results ul.select2-result-sub {
358
+ margin: 0;
359
+ padding-left: 0;
360
+ }
361
+
362
+ .select2-results li {
363
+ list-style: none;
364
+ display: list-item;
365
+ background-image: none;
366
+ }
367
+
368
+ .select2-results li.select2-result-with-children > .select2-result-label {
369
+ font-weight: bold;
370
+ }
371
+
372
+ .select2-results .select2-result-label {
373
+ padding: 3px 7px 4px;
374
+ margin: 0;
375
+ cursor: pointer;
376
+
377
+ min-height: 1em;
378
+
379
+ -webkit-touch-callout: none;
380
+ -webkit-user-select: none;
381
+ -moz-user-select: none;
382
+ -ms-user-select: none;
383
+ user-select: none;
384
+ }
385
+
386
+ .select2-results-dept-1 .select2-result-label { padding-left: 20px }
387
+ .select2-results-dept-2 .select2-result-label { padding-left: 40px }
388
+ .select2-results-dept-3 .select2-result-label { padding-left: 60px }
389
+ .select2-results-dept-4 .select2-result-label { padding-left: 80px }
390
+ .select2-results-dept-5 .select2-result-label { padding-left: 100px }
391
+ .select2-results-dept-6 .select2-result-label { padding-left: 110px }
392
+ .select2-results-dept-7 .select2-result-label { padding-left: 120px }
393
+
394
+ .select2-results .select2-highlighted {
395
+ background: #3875d7;
396
+ color: #fff;
397
+ }
398
+
399
+ .select2-results li em {
400
+ background: #feffde;
401
+ font-style: normal;
402
+ }
403
+
404
+ .select2-results .select2-highlighted em {
405
+ background: transparent;
406
+ }
407
+
408
+ .select2-results .select2-highlighted ul {
409
+ background: #fff;
410
+ color: #000;
411
+ }
412
+
413
+ .select2-results .select2-no-results,
414
+ .select2-results .select2-searching,
415
+ .select2-results .select2-ajax-error,
416
+ .select2-results .select2-selection-limit {
417
+ background: #f4f4f4;
418
+ display: list-item;
419
+ padding-left: 5px;
420
+ }
421
+
422
+ /*
423
+ disabled look for disabled choices in the results dropdown
424
+ */
425
+ .select2-results .select2-disabled.select2-highlighted {
426
+ color: #666;
427
+ background: #f4f4f4;
428
+ display: list-item;
429
+ cursor: default;
430
+ }
431
+ .select2-results .select2-disabled {
432
+ background: #f4f4f4;
433
+ display: list-item;
434
+ cursor: default;
435
+ }
436
+
437
+ .select2-results .select2-selected {
438
+ display: none;
439
+ }
440
+
441
+ .select2-more-results.select2-active {
442
+ background: #f4f4f4 url('select2-spinner.gif') no-repeat 100%;
443
+ }
444
+
445
+ .select2-results .select2-ajax-error {
446
+ background: rgba(255, 50, 50, .2);
447
+ }
448
+
449
+ .select2-more-results {
450
+ background: #f4f4f4;
451
+ display: list-item;
452
+ }
453
+
454
+ /* disabled styles */
455
+
456
+ .select2-container.select2-container-disabled .select2-choice {
457
+ background-color: #f4f4f4;
458
+ background-image: none;
459
+ border: 1px solid #ddd;
460
+ cursor: default;
461
+ }
462
+
463
+ .select2-container.select2-container-disabled .select2-choice .select2-arrow {
464
+ background-color: #f4f4f4;
465
+ background-image: none;
466
+ border-left: 0;
467
+ }
468
+
469
+ .select2-container.select2-container-disabled .select2-choice abbr {
470
+ display: none;
471
+ }
472
+
473
+
474
+ /* multiselect */
475
+
476
+ .select2-container-multi .select2-choices {
477
+ height: auto !important;
478
+ height: 1%;
479
+ margin: 0;
480
+ padding: 0 5px 0 0;
481
+ position: relative;
482
+
483
+ border: 1px solid #aaa;
484
+ cursor: text;
485
+ overflow: hidden;
486
+
487
+ background-color: #fff;
488
+ background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, color-stop(1%, #eee), color-stop(15%, #fff));
489
+ background-image: -webkit-linear-gradient(top, #eee 1%, #fff 15%);
490
+ background-image: -moz-linear-gradient(top, #eee 1%, #fff 15%);
491
+ background-image: linear-gradient(to bottom, #eee 1%, #fff 15%);
492
+ }
493
+
494
+ html[dir="rtl"] .select2-container-multi .select2-choices {
495
+ padding: 0 0 0 5px;
496
+ }
497
+
498
+ .select2-locked {
499
+ padding: 3px 5px 3px 5px !important;
500
+ }
501
+
502
+ .select2-container-multi .select2-choices {
503
+ min-height: 26px;
504
+ }
505
+
506
+ .select2-container-multi.select2-container-active .select2-choices {
507
+ border: 1px solid #5897fb;
508
+ outline: none;
509
+
510
+ -webkit-box-shadow: 0 0 5px rgba(0, 0, 0, .3);
511
+ box-shadow: 0 0 5px rgba(0, 0, 0, .3);
512
+ }
513
+ .select2-container-multi .select2-choices li {
514
+ float: left;
515
+ list-style: none;
516
+ }
517
+ html[dir="rtl"] .select2-container-multi .select2-choices li
518
+ {
519
+ float: right;
520
+ }
521
+ .select2-container-multi .select2-choices .select2-search-field {
522
+ margin: 0;
523
+ padding: 0;
524
+ white-space: nowrap;
525
+ }
526
+
527
+ .select2-container-multi .select2-choices .select2-search-field input {
528
+ padding: 5px;
529
+ margin: 1px 0;
530
+
531
+ font-family: sans-serif;
532
+ font-size: 100%;
533
+ color: #666;
534
+ outline: 0;
535
+ border: 0;
536
+ -webkit-box-shadow: none;
537
+ box-shadow: none;
538
+ background: transparent !important;
539
+ }
540
+
541
+ .select2-container-multi .select2-choices .select2-search-field input.select2-active {
542
+ background: #fff url('select2-spinner.gif') no-repeat 100% !important;
543
+ }
544
+
545
+ .select2-default {
546
+ color: #999 !important;
547
+ }
548
+
549
+ .select2-container-multi .select2-choices .select2-search-choice {
550
+ padding: 3px 5px 3px 18px;
551
+ margin: 3px 0 3px 5px;
552
+ position: relative;
553
+
554
+ line-height: 13px;
555
+ color: #333;
556
+ cursor: default;
557
+ border: 1px solid #aaaaaa;
558
+
559
+ border-radius: 3px;
560
+
561
+ -webkit-box-shadow: 0 0 2px #fff inset, 0 1px 0 rgba(0, 0, 0, 0.05);
562
+ box-shadow: 0 0 2px #fff inset, 0 1px 0 rgba(0, 0, 0, 0.05);
563
+
564
+ background-clip: padding-box;
565
+
566
+ -webkit-touch-callout: none;
567
+ -webkit-user-select: none;
568
+ -moz-user-select: none;
569
+ -ms-user-select: none;
570
+ user-select: none;
571
+
572
+ background-color: #e4e4e4;
573
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#eeeeee', endColorstr='#f4f4f4', GradientType=0);
574
+ background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, color-stop(20%, #f4f4f4), color-stop(50%, #f0f0f0), color-stop(52%, #e8e8e8), color-stop(100%, #eee));
575
+ background-image: -webkit-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eee 100%);
576
+ background-image: -moz-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eee 100%);
577
+ background-image: linear-gradient(to bottom, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eee 100%);
578
+ }
579
+ html[dir="rtl"] .select2-container-multi .select2-choices .select2-search-choice
580
+ {
581
+ margin: 3px 5px 3px 0;
582
+ padding: 3px 18px 3px 5px;
583
+ }
584
+ .select2-container-multi .select2-choices .select2-search-choice .select2-chosen {
585
+ cursor: default;
586
+ }
587
+ .select2-container-multi .select2-choices .select2-search-choice-focus {
588
+ background: #d4d4d4;
589
+ }
590
+
591
+ .select2-search-choice-close {
592
+ display: block;
593
+ width: 12px;
594
+ height: 13px;
595
+ position: absolute;
596
+ right: 3px;
597
+ top: 4px;
598
+
599
+ font-size: 1px;
600
+ outline: none;
601
+ background: url('select2.png') right top no-repeat;
602
+ }
603
+ html[dir="rtl"] .select2-search-choice-close {
604
+ right: auto;
605
+ left: 3px;
606
+ }
607
+
608
+ .select2-container-multi .select2-search-choice-close {
609
+ left: 3px;
610
+ }
611
+
612
+ html[dir="rtl"] .select2-container-multi .select2-search-choice-close {
613
+ left: auto;
614
+ right: 2px;
615
+ }
616
+
617
+ .select2-container-multi .select2-choices .select2-search-choice .select2-search-choice-close:hover {
618
+ background-position: right -11px;
619
+ }
620
+ .select2-container-multi .select2-choices .select2-search-choice-focus .select2-search-choice-close {
621
+ background-position: right -11px;
622
+ }
623
+
624
+ /* disabled styles */
625
+ .select2-container-multi.select2-container-disabled .select2-choices {
626
+ background-color: #f4f4f4;
627
+ background-image: none;
628
+ border: 1px solid #ddd;
629
+ cursor: default;
630
+ }
631
+
632
+ .select2-container-multi.select2-container-disabled .select2-choices .select2-search-choice {
633
+ padding: 3px 5px 3px 5px;
634
+ border: 1px solid #ddd;
635
+ background-image: none;
636
+ background-color: #f4f4f4;
637
+ }
638
+
639
+ .select2-container-multi.select2-container-disabled .select2-choices .select2-search-choice .select2-search-choice-close { display: none;
640
+ background: none;
641
+ }
642
+ /* end multiselect */
643
+
644
+
645
+ .select2-result-selectable .select2-match,
646
+ .select2-result-unselectable .select2-match {
647
+ text-decoration: underline;
648
+ }
649
+
650
+ .select2-offscreen, .select2-offscreen:focus {
651
+ clip: rect(0 0 0 0) !important;
652
+ width: 1px !important;
653
+ height: 1px !important;
654
+ border: 0 !important;
655
+ margin: 0 !important;
656
+ padding: 0 !important;
657
+ overflow: hidden !important;
658
+ position: absolute !important;
659
+ outline: 0 !important;
660
+ left: 0px !important;
661
+ top: 0px !important;
662
+ }
663
+
664
+ .select2-display-none {
665
+ display: none;
666
+ }
667
+
668
+ .select2-measure-scrollbar {
669
+ position: absolute;
670
+ top: -10000px;
671
+ left: -10000px;
672
+ width: 100px;
673
+ height: 100px;
674
+ overflow: scroll;
675
+ }
676
+
677
+ /* Retina-ize icons */
678
+
679
+ @media only screen and (-webkit-min-device-pixel-ratio: 1.5), only screen and (min-resolution: 2dppx) {
680
+ .select2-search input,
681
+ .select2-search-choice-close,
682
+ .select2-container .select2-choice abbr,
683
+ .select2-container .select2-choice .select2-arrow b {
684
+ background-image: url('select2x2.png') !important;
685
+ background-repeat: no-repeat !important;
686
+ background-size: 60px 40px !important;
687
+ }
688
+
689
+ .select2-search input {
690
+ background-position: 100% -21px !important;
691
+ }
692
+ }
ui/lib/select2/select2.jquery.json ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "select2",
3
+ "title": "Select2",
4
+ "description": "Select2 is a jQuery based replacement for select boxes. It supports searching, remote data sets, and infinite scrolling of results.",
5
+ "keywords": [
6
+ "select",
7
+ "autocomplete",
8
+ "typeahead",
9
+ "dropdown",
10
+ "multiselect",
11
+ "tag",
12
+ "tagging"
13
+ ],
14
+ "version": "3.5.2",
15
+ "author": {
16
+ "name": "Igor Vaynberg",
17
+ "url": "https://github.com/ivaynberg"
18
+ },
19
+ "licenses": [
20
+ {
21
+ "type": "Apache",
22
+ "url": "http://www.apache.org/licenses/LICENSE-2.0"
23
+ },
24
+ {
25
+ "type": "GPL v2",
26
+ "url": "http://www.gnu.org/licenses/gpl-2.0.html"
27
+ }
28
+ ],
29
+ "bugs": "https://github.com/ivaynberg/select2/issues",
30
+ "homepage": "http://ivaynberg.github.com/select2",
31
+ "docs": "http://ivaynberg.github.com/select2/",
32
+ "download": "https://github.com/ivaynberg/select2/tags",
33
+ "dependencies": {
34
+ "jquery": ">=1.7.1"
35
+ }
36
+ }
ui/lib/select2/select2.js ADDED
@@ -0,0 +1,3558 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ Copyright 2012 Igor Vaynberg
3
+
4
+ Version: @@ver@@ Timestamp: @@timestamp@@
5
+
6
+ This software is licensed under the Apache License, Version 2.0 (the "Apache License") or the GNU
7
+ General Public License version 2 (the "GPL License"). You may choose either license to govern your
8
+ use of this software only upon the condition that you accept all of the terms of either the Apache
9
+ License or the GPL License.
10
+
11
+ You may obtain a copy of the Apache License and the GPL License at:
12
+
13
+ http://www.apache.org/licenses/LICENSE-2.0
14
+ http://www.gnu.org/licenses/gpl-2.0.html
15
+
16
+ Unless required by applicable law or agreed to in writing, software distributed under the
17
+ Apache License or the GPL License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
18
+ CONDITIONS OF ANY KIND, either express or implied. See the Apache License and the GPL License for
19
+ the specific language governing permissions and limitations under the Apache License and the GPL License.
20
+ */
21
+ (function ($) {
22
+ if(typeof $.fn.each2 == "undefined") {
23
+ $.extend($.fn, {
24
+ /*
25
+ * 4-10 times faster .each replacement
26
+ * use it carefully, as it overrides jQuery context of element on each iteration
27
+ */
28
+ each2 : function (c) {
29
+ var j = $([0]), i = -1, l = this.length;
30
+ while (
31
+ ++i < l
32
+ && (j.context = j[0] = this[i])
33
+ && c.call(j[0], i, j) !== false //"this"=DOM, i=index, j=jQuery object
34
+ );
35
+ return this;
36
+ }
37
+ });
38
+ }
39
+ })(jQuery);
40
+
41
+ (function ($, undefined) {
42
+ "use strict";
43
+ /*global document, window, jQuery, console */
44
+
45
+ if (window.Select2 !== undefined) {
46
+ return;
47
+ }
48
+
49
+ var AbstractSelect2, SingleSelect2, MultiSelect2, nextUid, sizer,
50
+ lastMousePosition={x:0,y:0}, $document, scrollBarDimensions,
51
+
52
+ KEY = {
53
+ TAB: 9,
54
+ ENTER: 13,
55
+ ESC: 27,
56
+ SPACE: 32,
57
+ LEFT: 37,
58
+ UP: 38,
59
+ RIGHT: 39,
60
+ DOWN: 40,
61
+ SHIFT: 16,
62
+ CTRL: 17,
63
+ ALT: 18,
64
+ PAGE_UP: 33,
65
+ PAGE_DOWN: 34,
66
+ HOME: 36,
67
+ END: 35,
68
+ BACKSPACE: 8,
69
+ DELETE: 46,
70
+ isArrow: function (k) {
71
+ k = k.which ? k.which : k;
72
+ switch (k) {
73
+ case KEY.LEFT:
74
+ case KEY.RIGHT:
75
+ case KEY.UP:
76
+ case KEY.DOWN:
77
+ return true;
78
+ }
79
+ return false;
80
+ },
81
+ isControl: function (e) {
82
+ var k = e.which;
83
+ switch (k) {
84
+ case KEY.SHIFT:
85
+ case KEY.CTRL:
86
+ case KEY.ALT:
87
+ return true;
88
+ }
89
+
90
+ if (e.metaKey) return true;
91
+
92
+ return false;
93
+ },
94
+ isFunctionKey: function (k) {
95
+ k = k.which ? k.which : k;
96
+ return k >= 112 && k <= 123;
97
+ }
98
+ },
99
+ MEASURE_SCROLLBAR_TEMPLATE = "<div class='select2-measure-scrollbar'></div>",
100
+
101
+ DIACRITICS = {"\u24B6":"A","\uFF21":"A","\u00C0":"A","\u00C1":"A","\u00C2":"A","\u1EA6":"A","\u1EA4":"A","\u1EAA":"A","\u1EA8":"A","\u00C3":"A","\u0100":"A","\u0102":"A","\u1EB0":"A","\u1EAE":"A","\u1EB4":"A","\u1EB2":"A","\u0226":"A","\u01E0":"A","\u00C4":"A","\u01DE":"A","\u1EA2":"A","\u00C5":"A","\u01FA":"A","\u01CD":"A","\u0200":"A","\u0202":"A","\u1EA0":"A","\u1EAC":"A","\u1EB6":"A","\u1E00":"A","\u0104":"A","\u023A":"A","\u2C6F":"A","\uA732":"AA","\u00C6":"AE","\u01FC":"AE","\u01E2":"AE","\uA734":"AO","\uA736":"AU","\uA738":"AV","\uA73A":"AV","\uA73C":"AY","\u24B7":"B","\uFF22":"B","\u1E02":"B","\u1E04":"B","\u1E06":"B","\u0243":"B","\u0182":"B","\u0181":"B","\u24B8":"C","\uFF23":"C","\u0106":"C","\u0108":"C","\u010A":"C","\u010C":"C","\u00C7":"C","\u1E08":"C","\u0187":"C","\u023B":"C","\uA73E":"C","\u24B9":"D","\uFF24":"D","\u1E0A":"D","\u010E":"D","\u1E0C":"D","\u1E10":"D","\u1E12":"D","\u1E0E":"D","\u0110":"D","\u018B":"D","\u018A":"D","\u0189":"D","\uA779":"D","\u01F1":"DZ","\u01C4":"DZ","\u01F2":"Dz","\u01C5":"Dz","\u24BA":"E","\uFF25":"E","\u00C8":"E","\u00C9":"E","\u00CA":"E","\u1EC0":"E","\u1EBE":"E","\u1EC4":"E","\u1EC2":"E","\u1EBC":"E","\u0112":"E","\u1E14":"E","\u1E16":"E","\u0114":"E","\u0116":"E","\u00CB":"E","\u1EBA":"E","\u011A":"E","\u0204":"E","\u0206":"E","\u1EB8":"E","\u1EC6":"E","\u0228":"E","\u1E1C":"E","\u0118":"E","\u1E18":"E","\u1E1A":"E","\u0190":"E","\u018E":"E","\u24BB":"F","\uFF26":"F","\u1E1E":"F","\u0191":"F","\uA77B":"F","\u24BC":"G","\uFF27":"G","\u01F4":"G","\u011C":"G","\u1E20":"G","\u011E":"G","\u0120":"G","\u01E6":"G","\u0122":"G","\u01E4":"G","\u0193":"G","\uA7A0":"G","\uA77D":"G","\uA77E":"G","\u24BD":"H","\uFF28":"H","\u0124":"H","\u1E22":"H","\u1E26":"H","\u021E":"H","\u1E24":"H","\u1E28":"H","\u1E2A":"H","\u0126":"H","\u2C67":"H","\u2C75":"H","\uA78D":"H","\u24BE":"I","\uFF29":"I","\u00CC":"I","\u00CD":"I","\u00CE":"I","\u0128":"I","\u012A":"I","\u012C":"I","\u0130":"I","\u00CF":"I","\u1E2E":"I","\u1EC8":"I","\u01CF":"I","\u0208":"I","\u020A":"I","\u1ECA":"I","\u012E":"I","\u1E2C":"I","\u0197":"I","\u24BF":"J","\uFF2A":"J","\u0134":"J","\u0248":"J","\u24C0":"K","\uFF2B":"K","\u1E30":"K","\u01E8":"K","\u1E32":"K","\u0136":"K","\u1E34":"K","\u0198":"K","\u2C69":"K","\uA740":"K","\uA742":"K","\uA744":"K","\uA7A2":"K","\u24C1":"L","\uFF2C":"L","\u013F":"L","\u0139":"L","\u013D":"L","\u1E36":"L","\u1E38":"L","\u013B":"L","\u1E3C":"L","\u1E3A":"L","\u0141":"L","\u023D":"L","\u2C62":"L","\u2C60":"L","\uA748":"L","\uA746":"L","\uA780":"L","\u01C7":"LJ","\u01C8":"Lj","\u24C2":"M","\uFF2D":"M","\u1E3E":"M","\u1E40":"M","\u1E42":"M","\u2C6E":"M","\u019C":"M","\u24C3":"N","\uFF2E":"N","\u01F8":"N","\u0143":"N","\u00D1":"N","\u1E44":"N","\u0147":"N","\u1E46":"N","\u0145":"N","\u1E4A":"N","\u1E48":"N","\u0220":"N","\u019D":"N","\uA790":"N","\uA7A4":"N","\u01CA":"NJ","\u01CB":"Nj","\u24C4":"O","\uFF2F":"O","\u00D2":"O","\u00D3":"O","\u00D4":"O","\u1ED2":"O","\u1ED0":"O","\u1ED6":"O","\u1ED4":"O","\u00D5":"O","\u1E4C":"O","\u022C":"O","\u1E4E":"O","\u014C":"O","\u1E50":"O","\u1E52":"O","\u014E":"O","\u022E":"O","\u0230":"O","\u00D6":"O","\u022A":"O","\u1ECE":"O","\u0150":"O","\u01D1":"O","\u020C":"O","\u020E":"O","\u01A0":"O","\u1EDC":"O","\u1EDA":"O","\u1EE0":"O","\u1EDE":"O","\u1EE2":"O","\u1ECC":"O","\u1ED8":"O","\u01EA":"O","\u01EC":"O","\u00D8":"O","\u01FE":"O","\u0186":"O","\u019F":"O","\uA74A":"O","\uA74C":"O","\u01A2":"OI","\uA74E":"OO","\u0222":"OU","\u24C5":"P","\uFF30":"P","\u1E54":"P","\u1E56":"P","\u01A4":"P","\u2C63":"P","\uA750":"P","\uA752":"P","\uA754":"P","\u24C6":"Q","\uFF31":"Q","\uA756":"Q","\uA758":"Q","\u024A":"Q","\u24C7":"R","\uFF32":"R","\u0154":"R","\u1E58":"R","\u0158":"R","\u0210":"R","\u0212":"R","\u1E5A":"R","\u1E5C":"R","\u0156":"R","\u1E5E":"R","\u024C":"R","\u2C64":"R","\uA75A":"R","\uA7A6":"R","\uA782":"R","\u24C8":"S","\uFF33":"S","\u1E9E":"S","\u015A":"S","\u1E64":"S","\u015C":"S","\u1E60":"S","\u0160":"S","\u1E66":"S","\u1E62":"S","\u1E68":"S","\u0218":"S","\u015E":"S","\u2C7E":"S","\uA7A8":"S","\uA784":"S","\u24C9":"T","\uFF34":"T","\u1E6A":"T","\u0164":"T","\u1E6C":"T","\u021A":"T","\u0162":"T","\u1E70":"T","\u1E6E":"T","\u0166":"T","\u01AC":"T","\u01AE":"T","\u023E":"T","\uA786":"T","\uA728":"TZ","\u24CA":"U","\uFF35":"U","\u00D9":"U","\u00DA":"U","\u00DB":"U","\u0168":"U","\u1E78":"U","\u016A":"U","\u1E7A":"U","\u016C":"U","\u00DC":"U","\u01DB":"U","\u01D7":"U","\u01D5":"U","\u01D9":"U","\u1EE6":"U","\u016E":"U","\u0170":"U","\u01D3":"U","\u0214":"U","\u0216":"U","\u01AF":"U","\u1EEA":"U","\u1EE8":"U","\u1EEE":"U","\u1EEC":"U","\u1EF0":"U","\u1EE4":"U","\u1E72":"U","\u0172":"U","\u1E76":"U","\u1E74":"U","\u0244":"U","\u24CB":"V","\uFF36":"V","\u1E7C":"V","\u1E7E":"V","\u01B2":"V","\uA75E":"V","\u0245":"V","\uA760":"VY","\u24CC":"W","\uFF37":"W","\u1E80":"W","\u1E82":"W","\u0174":"W","\u1E86":"W","\u1E84":"W","\u1E88":"W","\u2C72":"W","\u24CD":"X","\uFF38":"X","\u1E8A":"X","\u1E8C":"X","\u24CE":"Y","\uFF39":"Y","\u1EF2":"Y","\u00DD":"Y","\u0176":"Y","\u1EF8":"Y","\u0232":"Y","\u1E8E":"Y","\u0178":"Y","\u1EF6":"Y","\u1EF4":"Y","\u01B3":"Y","\u024E":"Y","\u1EFE":"Y","\u24CF":"Z","\uFF3A":"Z","\u0179":"Z","\u1E90":"Z","\u017B":"Z","\u017D":"Z","\u1E92":"Z","\u1E94":"Z","\u01B5":"Z","\u0224":"Z","\u2C7F":"Z","\u2C6B":"Z","\uA762":"Z","\u24D0":"a","\uFF41":"a","\u1E9A":"a","\u00E0":"a","\u00E1":"a","\u00E2":"a","\u1EA7":"a","\u1EA5":"a","\u1EAB":"a","\u1EA9":"a","\u00E3":"a","\u0101":"a","\u0103":"a","\u1EB1":"a","\u1EAF":"a","\u1EB5":"a","\u1EB3":"a","\u0227":"a","\u01E1":"a","\u00E4":"a","\u01DF":"a","\u1EA3":"a","\u00E5":"a","\u01FB":"a","\u01CE":"a","\u0201":"a","\u0203":"a","\u1EA1":"a","\u1EAD":"a","\u1EB7":"a","\u1E01":"a","\u0105":"a","\u2C65":"a","\u0250":"a","\uA733":"aa","\u00E6":"ae","\u01FD":"ae","\u01E3":"ae","\uA735":"ao","\uA737":"au","\uA739":"av","\uA73B":"av","\uA73D":"ay","\u24D1":"b","\uFF42":"b","\u1E03":"b","\u1E05":"b","\u1E07":"b","\u0180":"b","\u0183":"b","\u0253":"b","\u24D2":"c","\uFF43":"c","\u0107":"c","\u0109":"c","\u010B":"c","\u010D":"c","\u00E7":"c","\u1E09":"c","\u0188":"c","\u023C":"c","\uA73F":"c","\u2184":"c","\u24D3":"d","\uFF44":"d","\u1E0B":"d","\u010F":"d","\u1E0D":"d","\u1E11":"d","\u1E13":"d","\u1E0F":"d","\u0111":"d","\u018C":"d","\u0256":"d","\u0257":"d","\uA77A":"d","\u01F3":"dz","\u01C6":"dz","\u24D4":"e","\uFF45":"e","\u00E8":"e","\u00E9":"e","\u00EA":"e","\u1EC1":"e","\u1EBF":"e","\u1EC5":"e","\u1EC3":"e","\u1EBD":"e","\u0113":"e","\u1E15":"e","\u1E17":"e","\u0115":"e","\u0117":"e","\u00EB":"e","\u1EBB":"e","\u011B":"e","\u0205":"e","\u0207":"e","\u1EB9":"e","\u1EC7":"e","\u0229":"e","\u1E1D":"e","\u0119":"e","\u1E19":"e","\u1E1B":"e","\u0247":"e","\u025B":"e","\u01DD":"e","\u24D5":"f","\uFF46":"f","\u1E1F":"f","\u0192":"f","\uA77C":"f","\u24D6":"g","\uFF47":"g","\u01F5":"g","\u011D":"g","\u1E21":"g","\u011F":"g","\u0121":"g","\u01E7":"g","\u0123":"g","\u01E5":"g","\u0260":"g","\uA7A1":"g","\u1D79":"g","\uA77F":"g","\u24D7":"h","\uFF48":"h","\u0125":"h","\u1E23":"h","\u1E27":"h","\u021F":"h","\u1E25":"h","\u1E29":"h","\u1E2B":"h","\u1E96":"h","\u0127":"h","\u2C68":"h","\u2C76":"h","\u0265":"h","\u0195":"hv","\u24D8":"i","\uFF49":"i","\u00EC":"i","\u00ED":"i","\u00EE":"i","\u0129":"i","\u012B":"i","\u012D":"i","\u00EF":"i","\u1E2F":"i","\u1EC9":"i","\u01D0":"i","\u0209":"i","\u020B":"i","\u1ECB":"i","\u012F":"i","\u1E2D":"i","\u0268":"i","\u0131":"i","\u24D9":"j","\uFF4A":"j","\u0135":"j","\u01F0":"j","\u0249":"j","\u24DA":"k","\uFF4B":"k","\u1E31":"k","\u01E9":"k","\u1E33":"k","\u0137":"k","\u1E35":"k","\u0199":"k","\u2C6A":"k","\uA741":"k","\uA743":"k","\uA745":"k","\uA7A3":"k","\u24DB":"l","\uFF4C":"l","\u0140":"l","\u013A":"l","\u013E":"l","\u1E37":"l","\u1E39":"l","\u013C":"l","\u1E3D":"l","\u1E3B":"l","\u017F":"l","\u0142":"l","\u019A":"l","\u026B":"l","\u2C61":"l","\uA749":"l","\uA781":"l","\uA747":"l","\u01C9":"lj","\u24DC":"m","\uFF4D":"m","\u1E3F":"m","\u1E41":"m","\u1E43":"m","\u0271":"m","\u026F":"m","\u24DD":"n","\uFF4E":"n","\u01F9":"n","\u0144":"n","\u00F1":"n","\u1E45":"n","\u0148":"n","\u1E47":"n","\u0146":"n","\u1E4B":"n","\u1E49":"n","\u019E":"n","\u0272":"n","\u0149":"n","\uA791":"n","\uA7A5":"n","\u01CC":"nj","\u24DE":"o","\uFF4F":"o","\u00F2":"o","\u00F3":"o","\u00F4":"o","\u1ED3":"o","\u1ED1":"o","\u1ED7":"o","\u1ED5":"o","\u00F5":"o","\u1E4D":"o","\u022D":"o","\u1E4F":"o","\u014D":"o","\u1E51":"o","\u1E53":"o","\u014F":"o","\u022F":"o","\u0231":"o","\u00F6":"o","\u022B":"o","\u1ECF":"o","\u0151":"o","\u01D2":"o","\u020D":"o","\u020F":"o","\u01A1":"o","\u1EDD":"o","\u1EDB":"o","\u1EE1":"o","\u1EDF":"o","\u1EE3":"o","\u1ECD":"o","\u1ED9":"o","\u01EB":"o","\u01ED":"o","\u00F8":"o","\u01FF":"o","\u0254":"o","\uA74B":"o","\uA74D":"o","\u0275":"o","\u01A3":"oi","\u0223":"ou","\uA74F":"oo","\u24DF":"p","\uFF50":"p","\u1E55":"p","\u1E57":"p","\u01A5":"p","\u1D7D":"p","\uA751":"p","\uA753":"p","\uA755":"p","\u24E0":"q","\uFF51":"q","\u024B":"q","\uA757":"q","\uA759":"q","\u24E1":"r","\uFF52":"r","\u0155":"r","\u1E59":"r","\u0159":"r","\u0211":"r","\u0213":"r","\u1E5B":"r","\u1E5D":"r","\u0157":"r","\u1E5F":"r","\u024D":"r","\u027D":"r","\uA75B":"r","\uA7A7":"r","\uA783":"r","\u24E2":"s","\uFF53":"s","\u00DF":"s","\u015B":"s","\u1E65":"s","\u015D":"s","\u1E61":"s","\u0161":"s","\u1E67":"s","\u1E63":"s","\u1E69":"s","\u0219":"s","\u015F":"s","\u023F":"s","\uA7A9":"s","\uA785":"s","\u1E9B":"s","\u24E3":"t","\uFF54":"t","\u1E6B":"t","\u1E97":"t","\u0165":"t","\u1E6D":"t","\u021B":"t","\u0163":"t","\u1E71":"t","\u1E6F":"t","\u0167":"t","\u01AD":"t","\u0288":"t","\u2C66":"t","\uA787":"t","\uA729":"tz","\u24E4":"u","\uFF55":"u","\u00F9":"u","\u00FA":"u","\u00FB":"u","\u0169":"u","\u1E79":"u","\u016B":"u","\u1E7B":"u","\u016D":"u","\u00FC":"u","\u01DC":"u","\u01D8":"u","\u01D6":"u","\u01DA":"u","\u1EE7":"u","\u016F":"u","\u0171":"u","\u01D4":"u","\u0215":"u","\u0217":"u","\u01B0":"u","\u1EEB":"u","\u1EE9":"u","\u1EEF":"u","\u1EED":"u","\u1EF1":"u","\u1EE5":"u","\u1E73":"u","\u0173":"u","\u1E77":"u","\u1E75":"u","\u0289":"u","\u24E5":"v","\uFF56":"v","\u1E7D":"v","\u1E7F":"v","\u028B":"v","\uA75F":"v","\u028C":"v","\uA761":"vy","\u24E6":"w","\uFF57":"w","\u1E81":"w","\u1E83":"w","\u0175":"w","\u1E87":"w","\u1E85":"w","\u1E98":"w","\u1E89":"w","\u2C73":"w","\u24E7":"x","\uFF58":"x","\u1E8B":"x","\u1E8D":"x","\u24E8":"y","\uFF59":"y","\u1EF3":"y","\u00FD":"y","\u0177":"y","\u1EF9":"y","\u0233":"y","\u1E8F":"y","\u00FF":"y","\u1EF7":"y","\u1E99":"y","\u1EF5":"y","\u01B4":"y","\u024F":"y","\u1EFF":"y","\u24E9":"z","\uFF5A":"z","\u017A":"z","\u1E91":"z","\u017C":"z","\u017E":"z","\u1E93":"z","\u1E95":"z","\u01B6":"z","\u0225":"z","\u0240":"z","\u2C6C":"z","\uA763":"z","\u0386":"\u0391","\u0388":"\u0395","\u0389":"\u0397","\u038A":"\u0399","\u03AA":"\u0399","\u038C":"\u039F","\u038E":"\u03A5","\u03AB":"\u03A5","\u038F":"\u03A9","\u03AC":"\u03B1","\u03AD":"\u03B5","\u03AE":"\u03B7","\u03AF":"\u03B9","\u03CA":"\u03B9","\u0390":"\u03B9","\u03CC":"\u03BF","\u03CD":"\u03C5","\u03CB":"\u03C5","\u03B0":"\u03C5","\u03C9":"\u03C9","\u03C2":"\u03C3"};
102
+
103
+ $document = $(document);
104
+
105
+ nextUid=(function() { var counter=1; return function() { return counter++; }; }());
106
+
107
+
108
+ function reinsertElement(element) {
109
+ var placeholder = $(document.createTextNode(''));
110
+
111
+ element.before(placeholder);
112
+ placeholder.before(element);
113
+ placeholder.remove();
114
+ }
115
+
116
+ function stripDiacritics(str) {
117
+ // Used 'uni range + named function' from http://jsperf.com/diacritics/18
118
+ function match(a) {
119
+ return DIACRITICS[a] || a;
120
+ }
121
+
122
+ return str.replace(/[^\u0000-\u007E]/g, match);
123
+ }
124
+
125
+ function indexOf(value, array) {
126
+ var i = 0, l = array.length;
127
+ for (; i < l; i = i + 1) {
128
+ if (equal(value, array[i])) return i;
129
+ }
130
+ return -1;
131
+ }
132
+
133
+ function measureScrollbar () {
134
+ var $template = $( MEASURE_SCROLLBAR_TEMPLATE );
135
+ $template.appendTo(document.body);
136
+
137
+ var dim = {
138
+ width: $template.width() - $template[0].clientWidth,
139
+ height: $template.height() - $template[0].clientHeight
140
+ };
141
+ $template.remove();
142
+
143
+ return dim;
144
+ }
145
+
146
+ /**
147
+ * Compares equality of a and b
148
+ * @param a
149
+ * @param b
150
+ */
151
+ function equal(a, b) {
152
+ if (a === b) return true;
153
+ if (a === undefined || b === undefined) return false;
154
+ if (a === null || b === null) return false;
155
+ // Check whether 'a' or 'b' is a string (primitive or object).
156
+ // The concatenation of an empty string (+'') converts its argument to a string's primitive.
157
+ if (a.constructor === String) return a+'' === b+''; // a+'' - in case 'a' is a String object
158
+ if (b.constructor === String) return b+'' === a+''; // b+'' - in case 'b' is a String object
159
+ return false;
160
+ }
161
+
162
+ /**
163
+ * Splits the string into an array of values, transforming each value. An empty array is returned for nulls or empty
164
+ * strings
165
+ * @param string
166
+ * @param separator
167
+ */
168
+ function splitVal(string, separator, transform) {
169
+ var val, i, l;
170
+ if (string === null || string.length < 1) return [];
171
+ val = string.split(separator);
172
+ for (i = 0, l = val.length; i < l; i = i + 1) val[i] = transform(val[i]);
173
+ return val;
174
+ }
175
+
176
+ function getSideBorderPadding(element) {
177
+ return element.outerWidth(false) - element.width();
178
+ }
179
+
180
+ function installKeyUpChangeEvent(element) {
181
+ var key="keyup-change-value";
182
+ element.on("keydown", function () {
183
+ if ($.data(element, key) === undefined) {
184
+ $.data(element, key, element.val());
185
+ }
186
+ });
187
+ element.on("keyup", function () {
188
+ var val= $.data(element, key);
189
+ if (val !== undefined && element.val() !== val) {
190
+ $.removeData(element, key);
191
+ element.trigger("keyup-change");
192
+ }
193
+ });
194
+ }
195
+
196
+
197
+ /**
198
+ * filters mouse events so an event is fired only if the mouse moved.
199
+ *
200
+ * filters out mouse events that occur when mouse is stationary but
201
+ * the elements under the pointer are scrolled.
202
+ */
203
+ function installFilteredMouseMove(element) {
204
+ element.on("mousemove", function (e) {
205
+ var lastpos = lastMousePosition;
206
+ if (lastpos === undefined || lastpos.x !== e.pageX || lastpos.y !== e.pageY) {
207
+ $(e.target).trigger("mousemove-filtered", e);
208
+ }
209
+ });
210
+ }
211
+
212
+ /**
213
+ * Debounces a function. Returns a function that calls the original fn function only if no invocations have been made
214
+ * within the last quietMillis milliseconds.
215
+ *
216
+ * @param quietMillis number of milliseconds to wait before invoking fn
217
+ * @param fn function to be debounced
218
+ * @param ctx object to be used as this reference within fn
219
+ * @return debounced version of fn
220
+ */
221
+ function debounce(quietMillis, fn, ctx) {
222
+ ctx = ctx || undefined;
223
+ var timeout;
224
+ return function () {
225
+ var args = arguments;
226
+ window.clearTimeout(timeout);
227
+ timeout = window.setTimeout(function() {
228
+ fn.apply(ctx, args);
229
+ }, quietMillis);
230
+ };
231
+ }
232
+
233
+ function installDebouncedScroll(threshold, element) {
234
+ var notify = debounce(threshold, function (e) { element.trigger("scroll-debounced", e);});
235
+ element.on("scroll", function (e) {
236
+ if (indexOf(e.target, element.get()) >= 0) notify(e);
237
+ });
238
+ }
239
+
240
+ function focus($el) {
241
+ if ($el[0] === document.activeElement) return;
242
+
243
+ /* set the focus in a 0 timeout - that way the focus is set after the processing
244
+ of the current event has finished - which seems like the only reliable way
245
+ to set focus */
246
+ window.setTimeout(function() {
247
+ var el=$el[0], pos=$el.val().length, range;
248
+
249
+ $el.focus();
250
+
251
+ /* make sure el received focus so we do not error out when trying to manipulate the caret.
252
+ sometimes modals or others listeners may steal it after its set */
253
+ var isVisible = (el.offsetWidth > 0 || el.offsetHeight > 0);
254
+ if (isVisible && el === document.activeElement) {
255
+
256
+ /* after the focus is set move the caret to the end, necessary when we val()
257
+ just before setting focus */
258
+ if(el.setSelectionRange)
259
+ {
260
+ el.setSelectionRange(pos, pos);
261
+ }
262
+ else if (el.createTextRange) {
263
+ range = el.createTextRange();
264
+ range.collapse(false);
265
+ range.select();
266
+ }
267
+ }
268
+ }, 0);
269
+ }
270
+
271
+ function getCursorInfo(el) {
272
+ el = $(el)[0];
273
+ var offset = 0;
274
+ var length = 0;
275
+ if ('selectionStart' in el) {
276
+ offset = el.selectionStart;
277
+ length = el.selectionEnd - offset;
278
+ } else if ('selection' in document) {
279
+ el.focus();
280
+ var sel = document.selection.createRange();
281
+ length = document.selection.createRange().text.length;
282
+ sel.moveStart('character', -el.value.length);
283
+ offset = sel.text.length - length;
284
+ }
285
+ return { offset: offset, length: length };
286
+ }
287
+
288
+ function killEvent(event) {
289
+ event.preventDefault();
290
+ event.stopPropagation();
291
+ }
292
+ function killEventImmediately(event) {
293
+ event.preventDefault();
294
+ event.stopImmediatePropagation();
295
+ }
296
+
297
+ function measureTextWidth(e) {
298
+ if (!sizer){
299
+ var style = e[0].currentStyle || window.getComputedStyle(e[0], null);
300
+ sizer = $(document.createElement("div")).css({
301
+ position: "absolute",
302
+ left: "-10000px",
303
+ top: "-10000px",
304
+ display: "none",
305
+ fontSize: style.fontSize,
306
+ fontFamily: style.fontFamily,
307
+ fontStyle: style.fontStyle,
308
+ fontWeight: style.fontWeight,
309
+ letterSpacing: style.letterSpacing,
310
+ textTransform: style.textTransform,
311
+ whiteSpace: "nowrap"
312
+ });
313
+ sizer.attr("class","select2-sizer");
314
+ $(document.body).append(sizer);
315
+ }
316
+ sizer.text(e.val());
317
+ return sizer.width();
318
+ }
319
+
320
+ function syncCssClasses(dest, src, adapter) {
321
+ var classes, replacements = [], adapted;
322
+
323
+ classes = $.trim(dest.attr("class"));
324
+
325
+ if (classes) {
326
+ classes = '' + classes; // for IE which returns object
327
+
328
+ $(classes.split(/\s+/)).each2(function() {
329
+ if (this.indexOf("select2-") === 0) {
330
+ replacements.push(this);
331
+ }
332
+ });
333
+ }
334
+
335
+ classes = $.trim(src.attr("class"));
336
+
337
+ if (classes) {
338
+ classes = '' + classes; // for IE which returns object
339
+
340
+ $(classes.split(/\s+/)).each2(function() {
341
+ if (this.indexOf("select2-") !== 0) {
342
+ adapted = adapter(this);
343
+
344
+ if (adapted) {
345
+ replacements.push(adapted);
346
+ }
347
+ }
348
+ });
349
+ }
350
+
351
+ dest.attr("class", replacements.join(" "));
352
+ }
353
+
354
+
355
+ function markMatch(text, term, markup, escapeMarkup) {
356
+ var match=stripDiacritics(text.toUpperCase()).indexOf(stripDiacritics(term.toUpperCase())),
357
+ tl=term.length;
358
+
359
+ if (match<0) {
360
+ markup.push(escapeMarkup(text));
361
+ return;
362
+ }
363
+
364
+ markup.push(escapeMarkup(text.substring(0, match)));
365
+ markup.push("<span class='select2-match'>");
366
+ markup.push(escapeMarkup(text.substring(match, match + tl)));
367
+ markup.push("</span>");
368
+ markup.push(escapeMarkup(text.substring(match + tl, text.length)));
369
+ }
370
+
371
+ function defaultEscapeMarkup(markup) {
372
+ var replace_map = {
373
+ '\\': '&#92;',
374
+ '&': '&amp;',
375
+ '<': '&lt;',
376
+ '>': '&gt;',
377
+ '"': '&quot;',
378
+ "'": '&#39;',
379
+ "/": '&#47;'
380
+ };
381
+
382
+ return String(markup).replace(/[&<>"'\/\\]/g, function (match) {
383
+ return replace_map[match];
384
+ });
385
+ }
386
+
387
+ /**
388
+ * Produces an ajax-based query function
389
+ *
390
+ * @param options object containing configuration parameters
391
+ * @param options.params parameter map for the transport ajax call, can contain such options as cache, jsonpCallback, etc. see $.ajax
392
+ * @param options.transport function that will be used to execute the ajax request. must be compatible with parameters supported by $.ajax
393
+ * @param options.url url for the data
394
+ * @param options.data a function(searchTerm, pageNumber, context) that should return an object containing query string parameters for the above url.
395
+ * @param options.dataType request data type: ajax, jsonp, other datatypes supported by jQuery's $.ajax function or the transport function if specified
396
+ * @param options.quietMillis (optional) milliseconds to wait before making the ajaxRequest, helps debounce the ajax function if invoked too often
397
+ * @param options.results a function(remoteData, pageNumber, query) that converts data returned form the remote request to the format expected by Select2.
398
+ * The expected format is an object containing the following keys:
399
+ * results array of objects that will be used as choices
400
+ * more (optional) boolean indicating whether there are more results available
401
+ * Example: {results:[{id:1, text:'Red'},{id:2, text:'Blue'}], more:true}
402
+ */
403
+ function ajax(options) {
404
+ var timeout, // current scheduled but not yet executed request
405
+ handler = null,
406
+ quietMillis = options.quietMillis || 100,
407
+ ajaxUrl = options.url,
408
+ self = this;
409
+
410
+ return function (query) {
411
+ window.clearTimeout(timeout);
412
+ timeout = window.setTimeout(function () {
413
+ var data = options.data, // ajax data function
414
+ url = ajaxUrl, // ajax url string or function
415
+ transport = options.transport || $.fn.select2.ajaxDefaults.transport,
416
+ // deprecated - to be removed in 4.0 - use params instead
417
+ deprecated = {
418
+ type: options.type || 'GET', // set type of request (GET or POST)
419
+ cache: options.cache || false,
420
+ jsonpCallback: options.jsonpCallback||undefined,
421
+ dataType: options.dataType||"json"
422
+ },
423
+ params = $.extend({}, $.fn.select2.ajaxDefaults.params, deprecated);
424
+
425
+ data = data ? data.call(self, query.term, query.page, query.context) : null;
426
+ url = (typeof url === 'function') ? url.call(self, query.term, query.page, query.context) : url;
427
+
428
+ if (handler && typeof handler.abort === "function") { handler.abort(); }
429
+
430
+ if (options.params) {
431
+ if ($.isFunction(options.params)) {
432
+ $.extend(params, options.params.call(self));
433
+ } else {
434
+ $.extend(params, options.params);
435
+ }
436
+ }
437
+
438
+ $.extend(params, {
439
+ url: url,
440
+ dataType: options.dataType,
441
+ data: data,
442
+ success: function (data) {
443
+ // TODO - replace query.page with query so users have access to term, page, etc.
444
+ // added query as third paramter to keep backwards compatibility
445
+ var results = options.results(data, query.page, query);
446
+ query.callback(results);
447
+ },
448
+ error: function(jqXHR, textStatus, errorThrown){
449
+ var results = {
450
+ hasError: true,
451
+ jqXHR: jqXHR,
452
+ textStatus: textStatus,
453
+ errorThrown: errorThrown
454
+ };
455
+
456
+ query.callback(results);
457
+ }
458
+ });
459
+ handler = transport.call(self, params);
460
+ }, quietMillis);
461
+ };
462
+ }
463
+
464
+ /**
465
+ * Produces a query function that works with a local array
466
+ *
467
+ * @param options object containing configuration parameters. The options parameter can either be an array or an
468
+ * object.
469
+ *
470
+ * If the array form is used it is assumed that it contains objects with 'id' and 'text' keys.
471
+ *
472
+ * If the object form is used it is assumed that it contains 'data' and 'text' keys. The 'data' key should contain
473
+ * an array of objects that will be used as choices. These objects must contain at least an 'id' key. The 'text'
474
+ * key can either be a String in which case it is expected that each element in the 'data' array has a key with the
475
+ * value of 'text' which will be used to match choices. Alternatively, text can be a function(item) that can extract
476
+ * the text.
477
+ */
478
+ function local(options) {
479
+ var data = options, // data elements
480
+ dataText,
481
+ tmp,
482
+ text = function (item) { return ""+item.text; }; // function used to retrieve the text portion of a data item that is matched against the search
483
+
484
+ if ($.isArray(data)) {
485
+ tmp = data;
486
+ data = { results: tmp };
487
+ }
488
+
489
+ if ($.isFunction(data) === false) {
490
+ tmp = data;
491
+ data = function() { return tmp; };
492
+ }
493
+
494
+ var dataItem = data();
495
+ if (dataItem.text) {
496
+ text = dataItem.text;
497
+ // if text is not a function we assume it to be a key name
498
+ if (!$.isFunction(text)) {
499
+ dataText = dataItem.text; // we need to store this in a separate variable because in the next step data gets reset and data.text is no longer available
500
+ text = function (item) { return item[dataText]; };
501
+ }
502
+ }
503
+
504
+ return function (query) {
505
+ var t = query.term, filtered = { results: [] }, process;
506
+ if (t === "") {
507
+ query.callback(data());
508
+ return;
509
+ }
510
+
511
+ process = function(datum, collection) {
512
+ var group, attr;
513
+ datum = datum[0];
514
+ if (datum.children) {
515
+ group = {};
516
+ for (attr in datum) {
517
+ if (datum.hasOwnProperty(attr)) group[attr]=datum[attr];
518
+ }
519
+ group.children=[];
520
+ $(datum.children).each2(function(i, childDatum) { process(childDatum, group.children); });
521
+ if (group.children.length || query.matcher(t, text(group), datum)) {
522
+ collection.push(group);
523
+ }
524
+ } else {
525
+ if (query.matcher(t, text(datum), datum)) {
526
+ collection.push(datum);
527
+ }
528
+ }
529
+ };
530
+
531
+ $(data().results).each2(function(i, datum) { process(datum, filtered.results); });
532
+ query.callback(filtered);
533
+ };
534
+ }
535
+
536
+ // TODO javadoc
537
+ function tags(data) {
538
+ var isFunc = $.isFunction(data);
539
+ return function (query) {
540
+ var t = query.term, filtered = {results: []};
541
+ var result = isFunc ? data(query) : data;
542
+ if ($.isArray(result)) {
543
+ $(result).each(function () {
544
+ var isObject = this.text !== undefined,
545
+ text = isObject ? this.text : this;
546
+ if (t === "" || query.matcher(t, text)) {
547
+ filtered.results.push(isObject ? this : {id: this, text: this});
548
+ }
549
+ });
550
+ query.callback(filtered);
551
+ }
552
+ };
553
+ }
554
+
555
+ /**
556
+ * Checks if the formatter function should be used.
557
+ *
558
+ * Throws an error if it is not a function. Returns true if it should be used,
559
+ * false if no formatting should be performed.
560
+ *
561
+ * @param formatter
562
+ */
563
+ function checkFormatter(formatter, formatterName) {
564
+ if ($.isFunction(formatter)) return true;
565
+ if (!formatter) return false;
566
+ if (typeof(formatter) === 'string') return true;
567
+ throw new Error(formatterName +" must be a string, function, or falsy value");
568
+ }
569
+
570
+ /**
571
+ * Returns a given value
572
+ * If given a function, returns its output
573
+ *
574
+ * @param val string|function
575
+ * @param context value of "this" to be passed to function
576
+ * @returns {*}
577
+ */
578
+ function evaluate(val, context) {
579
+ if ($.isFunction(val)) {
580
+ var args = Array.prototype.slice.call(arguments, 2);
581
+ return val.apply(context, args);
582
+ }
583
+ return val;
584
+ }
585
+
586
+ function countResults(results) {
587
+ var count = 0;
588
+ $.each(results, function(i, item) {
589
+ if (item.children) {
590
+ count += countResults(item.children);
591
+ } else {
592
+ count++;
593
+ }
594
+ });
595
+ return count;
596
+ }
597
+
598
+ /**
599
+ * Default tokenizer. This function uses breaks the input on substring match of any string from the
600
+ * opts.tokenSeparators array and uses opts.createSearchChoice to create the choice object. Both of those
601
+ * two options have to be defined in order for the tokenizer to work.
602
+ *
603
+ * @param input text user has typed so far or pasted into the search field
604
+ * @param selection currently selected choices
605
+ * @param selectCallback function(choice) callback tho add the choice to selection
606
+ * @param opts select2's opts
607
+ * @return undefined/null to leave the current input unchanged, or a string to change the input to the returned value
608
+ */
609
+ function defaultTokenizer(input, selection, selectCallback, opts) {
610
+ var original = input, // store the original so we can compare and know if we need to tell the search to update its text
611
+ dupe = false, // check for whether a token we extracted represents a duplicate selected choice
612
+ token, // token
613
+ index, // position at which the separator was found
614
+ i, l, // looping variables
615
+ separator; // the matched separator
616
+
617
+ if (!opts.createSearchChoice || !opts.tokenSeparators || opts.tokenSeparators.length < 1) return undefined;
618
+
619
+ while (true) {
620
+ index = -1;
621
+
622
+ for (i = 0, l = opts.tokenSeparators.length; i < l; i++) {
623
+ separator = opts.tokenSeparators[i];
624
+ index = input.indexOf(separator);
625
+ if (index >= 0) break;
626
+ }
627
+
628
+ if (index < 0) break; // did not find any token separator in the input string, bail
629
+
630
+ token = input.substring(0, index);
631
+ input = input.substring(index + separator.length);
632
+
633
+ if (token.length > 0) {
634
+ token = opts.createSearchChoice.call(this, token, selection);
635
+ if (token !== undefined && token !== null && opts.id(token) !== undefined && opts.id(token) !== null) {
636
+ dupe = false;
637
+ for (i = 0, l = selection.length; i < l; i++) {
638
+ if (equal(opts.id(token), opts.id(selection[i]))) {
639
+ dupe = true; break;
640
+ }
641
+ }
642
+
643
+ if (!dupe) selectCallback(token);
644
+ }
645
+ }
646
+ }
647
+
648
+ if (original!==input) return input;
649
+ }
650
+
651
+ function cleanupJQueryElements() {
652
+ var self = this;
653
+
654
+ $.each(arguments, function (i, element) {
655
+ self[element].remove();
656
+ self[element] = null;
657
+ });
658
+ }
659
+
660
+ /**
661
+ * Creates a new class
662
+ *
663
+ * @param superClass
664
+ * @param methods
665
+ */
666
+ function clazz(SuperClass, methods) {
667
+ var constructor = function () {};
668
+ constructor.prototype = new SuperClass;
669
+ constructor.prototype.constructor = constructor;
670
+ constructor.prototype.parent = SuperClass.prototype;
671
+ constructor.prototype = $.extend(constructor.prototype, methods);
672
+ return constructor;
673
+ }
674
+
675
+ AbstractSelect2 = clazz(Object, {
676
+
677
+ // abstract
678
+ bind: function (func) {
679
+ var self = this;
680
+ return function () {
681
+ func.apply(self, arguments);
682
+ };
683
+ },
684
+
685
+ // abstract
686
+ init: function (opts) {
687
+ var results, search, resultsSelector = ".select2-results";
688
+
689
+ // prepare options
690
+ this.opts = opts = this.prepareOpts(opts);
691
+
692
+ this.id=opts.id;
693
+
694
+ // destroy if called on an existing component
695
+ if (opts.element.data("select2") !== undefined &&
696
+ opts.element.data("select2") !== null) {
697
+ opts.element.data("select2").destroy();
698
+ }
699
+
700
+ this.container = this.createContainer();
701
+
702
+ this.liveRegion = $('.select2-hidden-accessible');
703
+ if (this.liveRegion.length == 0) {
704
+ this.liveRegion = $("<span>", {
705
+ role: "status",
706
+ "aria-live": "polite"
707
+ })
708
+ .addClass("select2-hidden-accessible")
709
+ .appendTo(document.body);
710
+ }
711
+
712
+ this.containerId="s2id_"+(opts.element.attr("id") || "autogen"+nextUid());
713
+ this.containerEventName= this.containerId
714
+ .replace(/([.])/g, '_')
715
+ .replace(/([;&,\-\.\+\*\~':"\!\^#$%@\[\]\(\)=>\|])/g, '\\$1');
716
+ this.container.attr("id", this.containerId);
717
+
718
+ this.container.attr("title", opts.element.attr("title"));
719
+
720
+ this.body = $(document.body);
721
+
722
+ syncCssClasses(this.container, this.opts.element, this.opts.adaptContainerCssClass);
723
+
724
+ this.container.attr("style", opts.element.attr("style"));
725
+ this.container.css(evaluate(opts.containerCss, this.opts.element));
726
+ this.container.addClass(evaluate(opts.containerCssClass, this.opts.element));
727
+
728
+ this.elementTabIndex = this.opts.element.attr("tabindex");
729
+
730
+ // swap container for the element
731
+ this.opts.element
732
+ .data("select2", this)
733
+ .attr("tabindex", "-1")
734
+ .before(this.container)
735
+ .on("click.select2", killEvent); // do not leak click events
736
+
737
+ this.container.data("select2", this);
738
+
739
+ this.dropdown = this.container.find(".select2-drop");
740
+
741
+ syncCssClasses(this.dropdown, this.opts.element, this.opts.adaptDropdownCssClass);
742
+
743
+ this.dropdown.addClass(evaluate(opts.dropdownCssClass, this.opts.element));
744
+ this.dropdown.data("select2", this);
745
+ this.dropdown.on("click", killEvent);
746
+
747
+ this.results = results = this.container.find(resultsSelector);
748
+ this.search = search = this.container.find("input.select2-input");
749
+
750
+ this.queryCount = 0;
751
+ this.resultsPage = 0;
752
+ this.context = null;
753
+
754
+ // initialize the container
755
+ this.initContainer();
756
+
757
+ this.container.on("click", killEvent);
758
+
759
+ installFilteredMouseMove(this.results);
760
+
761
+ this.dropdown.on("mousemove-filtered", resultsSelector, this.bind(this.highlightUnderEvent));
762
+ this.dropdown.on("touchstart touchmove touchend", resultsSelector, this.bind(function (event) {
763
+ this._touchEvent = true;
764
+ this.highlightUnderEvent(event);
765
+ }));
766
+ this.dropdown.on("touchmove", resultsSelector, this.bind(this.touchMoved));
767
+ this.dropdown.on("touchstart touchend", resultsSelector, this.bind(this.clearTouchMoved));
768
+
769
+ // Waiting for a click event on touch devices to select option and hide dropdown
770
+ // otherwise click will be triggered on an underlying element
771
+ this.dropdown.on('click', this.bind(function (event) {
772
+ if (this._touchEvent) {
773
+ this._touchEvent = false;
774
+ this.selectHighlighted();
775
+ }
776
+ }));
777
+
778
+ installDebouncedScroll(80, this.results);
779
+ this.dropdown.on("scroll-debounced", resultsSelector, this.bind(this.loadMoreIfNeeded));
780
+
781
+ // do not propagate change event from the search field out of the component
782
+ $(this.container).on("change", ".select2-input", function(e) {e.stopPropagation();});
783
+ $(this.dropdown).on("change", ".select2-input", function(e) {e.stopPropagation();});
784
+
785
+ // if jquery.mousewheel plugin is installed we can prevent out-of-bounds scrolling of results via mousewheel
786
+ if ($.fn.mousewheel) {
787
+ results.mousewheel(function (e, delta, deltaX, deltaY) {
788
+ var top = results.scrollTop();
789
+ if (deltaY > 0 && top - deltaY <= 0) {
790
+ results.scrollTop(0);
791
+ killEvent(e);
792
+ } else if (deltaY < 0 && results.get(0).scrollHeight - results.scrollTop() + deltaY <= results.height()) {
793
+ results.scrollTop(results.get(0).scrollHeight - results.height());
794
+ killEvent(e);
795
+ }
796
+ });
797
+ }
798
+
799
+ installKeyUpChangeEvent(search);
800
+ search.on("keyup-change input paste", this.bind(this.updateResults));
801
+ search.on("focus", function () { search.addClass("select2-focused"); });
802
+ search.on("blur", function () { search.removeClass("select2-focused");});
803
+
804
+ this.dropdown.on("mouseup", resultsSelector, this.bind(function (e) {
805
+ if ($(e.target).closest(".select2-result-selectable").length > 0) {
806
+ this.highlightUnderEvent(e);
807
+ this.selectHighlighted(e);
808
+ }
809
+ }));
810
+
811
+ // trap all mouse events from leaving the dropdown. sometimes there may be a modal that is listening
812
+ // for mouse events outside of itself so it can close itself. since the dropdown is now outside the select2's
813
+ // dom it will trigger the popup close, which is not what we want
814
+ // focusin can cause focus wars between modals and select2 since the dropdown is outside the modal.
815
+ this.dropdown.on("click mouseup mousedown touchstart touchend focusin", function (e) { e.stopPropagation(); });
816
+
817
+ this.lastSearchTerm = undefined;
818
+
819
+ if ($.isFunction(this.opts.initSelection)) {
820
+ // initialize selection based on the current value of the source element
821
+ this.initSelection();
822
+
823
+ // if the user has provided a function that can set selection based on the value of the source element
824
+ // we monitor the change event on the element and trigger it, allowing for two way synchronization
825
+ this.monitorSource();
826
+ }
827
+
828
+ if (opts.maximumInputLength !== null) {
829
+ this.search.attr("maxlength", opts.maximumInputLength);
830
+ }
831
+
832
+ var disabled = opts.element.prop("disabled");
833
+ if (disabled === undefined) disabled = false;
834
+ this.enable(!disabled);
835
+
836
+ var readonly = opts.element.prop("readonly");
837
+ if (readonly === undefined) readonly = false;
838
+ this.readonly(readonly);
839
+
840
+ // Calculate size of scrollbar
841
+ scrollBarDimensions = scrollBarDimensions || measureScrollbar();
842
+
843
+ this.autofocus = opts.element.prop("autofocus");
844
+ opts.element.prop("autofocus", false);
845
+ if (this.autofocus) this.focus();
846
+
847
+ this.search.attr("placeholder", opts.searchInputPlaceholder);
848
+ },
849
+
850
+ // abstract
851
+ destroy: function () {
852
+ var element=this.opts.element, select2 = element.data("select2"), self = this;
853
+
854
+ this.close();
855
+
856
+ if (element.length && element[0].detachEvent && self._sync) {
857
+ element.each(function () {
858
+ if (self._sync) {
859
+ this.detachEvent("onpropertychange", self._sync);
860
+ }
861
+ });
862
+ }
863
+ if (this.propertyObserver) {
864
+ this.propertyObserver.disconnect();
865
+ this.propertyObserver = null;
866
+ }
867
+ this._sync = null;
868
+
869
+ if (select2 !== undefined) {
870
+ select2.container.remove();
871
+ select2.liveRegion.remove();
872
+ select2.dropdown.remove();
873
+ element.removeData("select2")
874
+ .off("select2");
875
+ if (!element.is("input[type='hidden']")) {
876
+ element
877
+ .show()
878
+ .prop("autofocus", this.autofocus || false);
879
+ if (this.elementTabIndex) {
880
+ element.attr({tabindex: this.elementTabIndex});
881
+ } else {
882
+ element.removeAttr("tabindex");
883
+ }
884
+ element.show();
885
+ } else {
886
+ element.css("display", "");
887
+ }
888
+ }
889
+
890
+ cleanupJQueryElements.call(this,
891
+ "container",
892
+ "liveRegion",
893
+ "dropdown",
894
+ "results",
895
+ "search"
896
+ );
897
+ },
898
+
899
+ // abstract
900
+ optionToData: function(element) {
901
+ if (element.is("option")) {
902
+ return {
903
+ id:element.prop("value"),
904
+ text:element.text(),
905
+ element: element.get(),
906
+ css: element.attr("class"),
907
+ disabled: element.prop("disabled"),
908
+ locked: equal(element.attr("locked"), "locked") || equal(element.data("locked"), true)
909
+ };
910
+ } else if (element.is("optgroup")) {
911
+ return {
912
+ text:element.attr("label"),
913
+ children:[],
914
+ element: element.get(),
915
+ css: element.attr("class")
916
+ };
917
+ }
918
+ },
919
+
920
+ // abstract
921
+ prepareOpts: function (opts) {
922
+ var element, select, idKey, ajaxUrl, self = this;
923
+
924
+ element = opts.element;
925
+
926
+ if (element.get(0).tagName.toLowerCase() === "select") {
927
+ this.select = select = opts.element;
928
+ }
929
+
930
+ if (select) {
931
+ // these options are not allowed when attached to a select because they are picked up off the element itself
932
+ $.each(["id", "multiple", "ajax", "query", "createSearchChoice", "initSelection", "data", "tags"], function () {
933
+ if (this in opts) {
934
+ throw new Error("Option '" + this + "' is not allowed for Select2 when attached to a <select> element.");
935
+ }
936
+ });
937
+ }
938
+
939
+ opts = $.extend({}, {
940
+ populateResults: function(container, results, query) {
941
+ var populate, id=this.opts.id, liveRegion=this.liveRegion;
942
+
943
+ populate=function(results, container, depth) {
944
+
945
+ var i, l, result, selectable, disabled, compound, node, label, innerContainer, formatted;
946
+
947
+ results = opts.sortResults(results, container, query);
948
+
949
+ // collect the created nodes for bulk append
950
+ var nodes = [];
951
+ for (i = 0, l = results.length; i < l; i = i + 1) {
952
+
953
+ result=results[i];
954
+
955
+ disabled = (result.disabled === true);
956
+ selectable = (!disabled) && (id(result) !== undefined);
957
+
958
+ compound=result.children && result.children.length > 0;
959
+
960
+ node=$("<li></li>");
961
+ node.addClass("select2-results-dept-"+depth);
962
+ node.addClass("select2-result");
963
+ node.addClass(selectable ? "select2-result-selectable" : "select2-result-unselectable");
964
+ if (disabled) { node.addClass("select2-disabled"); }
965
+ if (compound) { node.addClass("select2-result-with-children"); }
966
+ node.addClass(self.opts.formatResultCssClass(result));
967
+ node.attr("role", "presentation");
968
+
969
+ label=$(document.createElement("div"));
970
+ label.addClass("select2-result-label");
971
+ label.attr("id", "select2-result-label-" + nextUid());
972
+ label.attr("role", "option");
973
+
974
+ formatted=opts.formatResult(result, label, query, self.opts.escapeMarkup);
975
+ if (formatted!==undefined) {
976
+ label.html(formatted);
977
+ node.append(label);
978
+ }
979
+
980
+
981
+ if (compound) {
982
+
983
+ innerContainer=$("<ul></ul>");
984
+ innerContainer.addClass("select2-result-sub");
985
+ populate(result.children, innerContainer, depth+1);
986
+ node.append(innerContainer);
987
+ }
988
+
989
+ node.data("select2-data", result);
990
+ nodes.push(node[0]);
991
+ }
992
+
993
+ // bulk append the created nodes
994
+ container.append(nodes);
995
+ liveRegion.text(opts.formatMatches(results.length));
996
+ };
997
+
998
+ populate(results, container, 0);
999
+ }
1000
+ }, $.fn.select2.defaults, opts);
1001
+
1002
+ if (typeof(opts.id) !== "function") {
1003
+ idKey = opts.id;
1004
+ opts.id = function (e) { return e[idKey]; };
1005
+ }
1006
+
1007
+ if ($.isArray(opts.element.data("select2Tags"))) {
1008
+ if ("tags" in opts) {
1009
+ throw "tags specified as both an attribute 'data-select2-tags' and in options of Select2 " + opts.element.attr("id");
1010
+ }
1011
+ opts.tags=opts.element.data("select2Tags");
1012
+ }
1013
+
1014
+ if (select) {
1015
+ opts.query = this.bind(function (query) {
1016
+ var data = { results: [], more: false },
1017
+ term = query.term,
1018
+ children, placeholderOption, process;
1019
+
1020
+ process=function(element, collection) {
1021
+ var group;
1022
+ if (element.is("option")) {
1023
+ if (query.matcher(term, element.text(), element)) {
1024
+ collection.push(self.optionToData(element));
1025
+ }
1026
+ } else if (element.is("optgroup")) {
1027
+ group=self.optionToData(element);
1028
+ element.children().each2(function(i, elm) { process(elm, group.children); });
1029
+ if (group.children.length>0) {
1030
+ collection.push(group);
1031
+ }
1032
+ }
1033
+ };
1034
+
1035
+ children=element.children();
1036
+
1037
+ // ignore the placeholder option if there is one
1038
+ if (this.getPlaceholder() !== undefined && children.length > 0) {
1039
+ placeholderOption = this.getPlaceholderOption();
1040
+ if (placeholderOption) {
1041
+ children=children.not(placeholderOption);
1042
+ }
1043
+ }
1044
+
1045
+ children.each2(function(i, elm) { process(elm, data.results); });
1046
+
1047
+ query.callback(data);
1048
+ });
1049
+ // this is needed because inside val() we construct choices from options and their id is hardcoded
1050
+ opts.id=function(e) { return e.id; };
1051
+ } else {
1052
+ if (!("query" in opts)) {
1053
+
1054
+ if ("ajax" in opts) {
1055
+ ajaxUrl = opts.element.data("ajax-url");
1056
+ if (ajaxUrl && ajaxUrl.length > 0) {
1057
+ opts.ajax.url = ajaxUrl;
1058
+ }
1059
+ opts.query = ajax.call(opts.element, opts.ajax);
1060
+ } else if ("data" in opts) {
1061
+ opts.query = local(opts.data);
1062
+ } else if ("tags" in opts) {
1063
+ opts.query = tags(opts.tags);
1064
+ if (opts.createSearchChoice === undefined) {
1065
+ opts.createSearchChoice = function (term) { return {id: $.trim(term), text: $.trim(term)}; };
1066
+ }
1067
+ if (opts.initSelection === undefined) {
1068
+ opts.initSelection = function (element, callback) {
1069
+ var data = [];
1070
+ $(splitVal(element.val(), opts.separator, opts.transformVal)).each(function () {
1071
+ var obj = { id: this, text: this },
1072
+ tags = opts.tags;
1073
+ if ($.isFunction(tags)) tags=tags();
1074
+ $(tags).each(function() { if (equal(this.id, obj.id)) { obj = this; return false; } });
1075
+ data.push(obj);
1076
+ });
1077
+
1078
+ callback(data);
1079
+ };
1080
+ }
1081
+ }
1082
+ }
1083
+ }
1084
+ if (typeof(opts.query) !== "function") {
1085
+ throw "query function not defined for Select2 " + opts.element.attr("id");
1086
+ }
1087
+
1088
+ if (opts.createSearchChoicePosition === 'top') {
1089
+ opts.createSearchChoicePosition = function(list, item) { list.unshift(item); };
1090
+ }
1091
+ else if (opts.createSearchChoicePosition === 'bottom') {
1092
+ opts.createSearchChoicePosition = function(list, item) { list.push(item); };
1093
+ }
1094
+ else if (typeof(opts.createSearchChoicePosition) !== "function") {
1095
+ throw "invalid createSearchChoicePosition option must be 'top', 'bottom' or a custom function";
1096
+ }
1097
+
1098
+ return opts;
1099
+ },
1100
+
1101
+ /**
1102
+ * Monitor the original element for changes and update select2 accordingly
1103
+ */
1104
+ // abstract
1105
+ monitorSource: function () {
1106
+ var el = this.opts.element, observer, self = this;
1107
+
1108
+ el.on("change.select2", this.bind(function (e) {
1109
+ if (this.opts.element.data("select2-change-triggered") !== true) {
1110
+ this.initSelection();
1111
+ }
1112
+ }));
1113
+
1114
+ this._sync = this.bind(function () {
1115
+
1116
+ // sync enabled state
1117
+ var disabled = el.prop("disabled");
1118
+ if (disabled === undefined) disabled = false;
1119
+ this.enable(!disabled);
1120
+
1121
+ var readonly = el.prop("readonly");
1122
+ if (readonly === undefined) readonly = false;
1123
+ this.readonly(readonly);
1124
+
1125
+ if (this.container) {
1126
+ syncCssClasses(this.container, this.opts.element, this.opts.adaptContainerCssClass);
1127
+ this.container.addClass(evaluate(this.opts.containerCssClass, this.opts.element));
1128
+ }
1129
+
1130
+ if (this.dropdown) {
1131
+ syncCssClasses(this.dropdown, this.opts.element, this.opts.adaptDropdownCssClass);
1132
+ this.dropdown.addClass(evaluate(this.opts.dropdownCssClass, this.opts.element));
1133
+ }
1134
+
1135
+ });
1136
+
1137
+ // IE8-10 (IE9/10 won't fire propertyChange via attachEventListener)
1138
+ if (el.length && el[0].attachEvent) {
1139
+ el.each(function() {
1140
+ this.attachEvent("onpropertychange", self._sync);
1141
+ });
1142
+ }
1143
+
1144
+ // safari, chrome, firefox, IE11
1145
+ observer = window.MutationObserver || window.WebKitMutationObserver|| window.MozMutationObserver;
1146
+ if (observer !== undefined) {
1147
+ if (this.propertyObserver) { delete this.propertyObserver; this.propertyObserver = null; }
1148
+ this.propertyObserver = new observer(function (mutations) {
1149
+ $.each(mutations, self._sync);
1150
+ });
1151
+ this.propertyObserver.observe(el.get(0), { attributes:true, subtree:false });
1152
+ }
1153
+ },
1154
+
1155
+ // abstract
1156
+ triggerSelect: function(data) {
1157
+ var evt = $.Event("select2-selecting", { val: this.id(data), object: data, choice: data });
1158
+ this.opts.element.trigger(evt);
1159
+ return !evt.isDefaultPrevented();
1160
+ },
1161
+
1162
+ /**
1163
+ * Triggers the change event on the source element
1164
+ */
1165
+ // abstract
1166
+ triggerChange: function (details) {
1167
+
1168
+ details = details || {};
1169
+ details= $.extend({}, details, { type: "change", val: this.val() });
1170
+ // prevents recursive triggering
1171
+ this.opts.element.data("select2-change-triggered", true);
1172
+ this.opts.element.trigger(details);
1173
+ this.opts.element.data("select2-change-triggered", false);
1174
+
1175
+ // some validation frameworks ignore the change event and listen instead to keyup, click for selects
1176
+ // so here we trigger the click event manually
1177
+ this.opts.element.click();
1178
+
1179
+ // ValidationEngine ignores the change event and listens instead to blur
1180
+ // so here we trigger the blur event manually if so desired
1181
+ if (this.opts.blurOnChange)
1182
+ this.opts.element.blur();
1183
+ },
1184
+
1185
+ //abstract
1186
+ isInterfaceEnabled: function()
1187
+ {
1188
+ return this.enabledInterface === true;
1189
+ },
1190
+
1191
+ // abstract
1192
+ enableInterface: function() {
1193
+ var enabled = this._enabled && !this._readonly,
1194
+ disabled = !enabled;
1195
+
1196
+ if (enabled === this.enabledInterface) return false;
1197
+
1198
+ this.container.toggleClass("select2-container-disabled", disabled);
1199
+ this.close();
1200
+ this.enabledInterface = enabled;
1201
+
1202
+ return true;
1203
+ },
1204
+
1205
+ // abstract
1206
+ enable: function(enabled) {
1207
+ if (enabled === undefined) enabled = true;
1208
+ if (this._enabled === enabled) return;
1209
+ this._enabled = enabled;
1210
+
1211
+ this.opts.element.prop("disabled", !enabled);
1212
+ this.enableInterface();
1213
+ },
1214
+
1215
+ // abstract
1216
+ disable: function() {
1217
+ this.enable(false);
1218
+ },
1219
+
1220
+ // abstract
1221
+ readonly: function(enabled) {
1222
+ if (enabled === undefined) enabled = false;
1223
+ if (this._readonly === enabled) return;
1224
+ this._readonly = enabled;
1225
+
1226
+ this.opts.element.prop("readonly", enabled);
1227
+ this.enableInterface();
1228
+ },
1229
+
1230
+ // abstract
1231
+ opened: function () {
1232
+ return (this.container) ? this.container.hasClass("select2-dropdown-open") : false;
1233
+ },
1234
+
1235
+ // abstract
1236
+ positionDropdown: function() {
1237
+ var $dropdown = this.dropdown,
1238
+ container = this.container,
1239
+ offset = container.offset(),
1240
+ height = container.outerHeight(false),
1241
+ width = container.outerWidth(false),
1242
+ dropHeight = $dropdown.outerHeight(false),
1243
+ $window = $(window),
1244
+ windowWidth = $window.width(),
1245
+ windowHeight = $window.height(),
1246
+ viewPortRight = $window.scrollLeft() + windowWidth,
1247
+ viewportBottom = $window.scrollTop() + windowHeight,
1248
+ dropTop = offset.top + height,
1249
+ dropLeft = offset.left,
1250
+ enoughRoomBelow = dropTop + dropHeight <= viewportBottom,
1251
+ enoughRoomAbove = (offset.top - dropHeight) >= $window.scrollTop(),
1252
+ dropWidth = $dropdown.outerWidth(false),
1253
+ enoughRoomOnRight = function() {
1254
+ return dropLeft + dropWidth <= viewPortRight;
1255
+ },
1256
+ enoughRoomOnLeft = function() {
1257
+ return offset.left + viewPortRight + container.outerWidth(false) > dropWidth;
1258
+ },
1259
+ aboveNow = $dropdown.hasClass("select2-drop-above"),
1260
+ bodyOffset,
1261
+ above,
1262
+ changeDirection,
1263
+ css,
1264
+ resultsListNode;
1265
+
1266
+ // always prefer the current above/below alignment, unless there is not enough room
1267
+ if (aboveNow) {
1268
+ above = true;
1269
+ if (!enoughRoomAbove && enoughRoomBelow) {
1270
+ changeDirection = true;
1271
+ above = false;
1272
+ }
1273
+ } else {
1274
+ above = false;
1275
+ if (!enoughRoomBelow && enoughRoomAbove) {
1276
+ changeDirection = true;
1277
+ above = true;
1278
+ }
1279
+ }
1280
+
1281
+ //if we are changing direction we need to get positions when dropdown is hidden;
1282
+ if (changeDirection) {
1283
+ $dropdown.hide();
1284
+ offset = this.container.offset();
1285
+ height = this.container.outerHeight(false);
1286
+ width = this.container.outerWidth(false);
1287
+ dropHeight = $dropdown.outerHeight(false);
1288
+ viewPortRight = $window.scrollLeft() + windowWidth;
1289
+ viewportBottom = $window.scrollTop() + windowHeight;
1290
+ dropTop = offset.top + height;
1291
+ dropLeft = offset.left;
1292
+ dropWidth = $dropdown.outerWidth(false);
1293
+ $dropdown.show();
1294
+
1295
+ // fix so the cursor does not move to the left within the search-textbox in IE
1296
+ this.focusSearch();
1297
+ }
1298
+
1299
+ if (this.opts.dropdownAutoWidth) {
1300
+ resultsListNode = $('.select2-results', $dropdown)[0];
1301
+ $dropdown.addClass('select2-drop-auto-width');
1302
+ $dropdown.css('width', '');
1303
+ // Add scrollbar width to dropdown if vertical scrollbar is present
1304
+ dropWidth = $dropdown.outerWidth(false) + (resultsListNode.scrollHeight === resultsListNode.clientHeight ? 0 : scrollBarDimensions.width);
1305
+ dropWidth > width ? width = dropWidth : dropWidth = width;
1306
+ dropHeight = $dropdown.outerHeight(false);
1307
+ }
1308
+ else {
1309
+ this.container.removeClass('select2-drop-auto-width');
1310
+ }
1311
+
1312
+ //console.log("below/ droptop:", dropTop, "dropHeight", dropHeight, "sum", (dropTop+dropHeight)+" viewport bottom", viewportBottom, "enough?", enoughRoomBelow);
1313
+ //console.log("above/ offset.top", offset.top, "dropHeight", dropHeight, "top", (offset.top-dropHeight), "scrollTop", this.body.scrollTop(), "enough?", enoughRoomAbove);
1314
+
1315
+ // fix positioning when body has an offset and is not position: static
1316
+ if (this.body.css('position') !== 'static') {
1317
+ bodyOffset = this.body.offset();
1318
+ dropTop -= bodyOffset.top;
1319
+ dropLeft -= bodyOffset.left;
1320
+ }
1321
+
1322
+ if (!enoughRoomOnRight() && enoughRoomOnLeft()) {
1323
+ dropLeft = offset.left + this.container.outerWidth(false) - dropWidth;
1324
+ }
1325
+
1326
+ css = {
1327
+ left: dropLeft,
1328
+ width: width
1329
+ };
1330
+
1331
+ if (above) {
1332
+ this.container.addClass("select2-drop-above");
1333
+ $dropdown.addClass("select2-drop-above");
1334
+ dropHeight = $dropdown.outerHeight(false);
1335
+ css.top = offset.top - dropHeight;
1336
+ css.bottom = 'auto';
1337
+ }
1338
+ else {
1339
+ css.top = dropTop;
1340
+ css.bottom = 'auto';
1341
+ this.container.removeClass("select2-drop-above");
1342
+ $dropdown.removeClass("select2-drop-above");
1343
+ }
1344
+ css = $.extend(css, evaluate(this.opts.dropdownCss, this.opts.element));
1345
+
1346
+ $dropdown.css(css);
1347
+ },
1348
+
1349
+ // abstract
1350
+ shouldOpen: function() {
1351
+ var event;
1352
+
1353
+ if (this.opened()) return false;
1354
+
1355
+ if (this._enabled === false || this._readonly === true) return false;
1356
+
1357
+ event = $.Event("select2-opening");
1358
+ this.opts.element.trigger(event);
1359
+ return !event.isDefaultPrevented();
1360
+ },
1361
+
1362
+ // abstract
1363
+ clearDropdownAlignmentPreference: function() {
1364
+ // clear the classes used to figure out the preference of where the dropdown should be opened
1365
+ this.container.removeClass("select2-drop-above");
1366
+ this.dropdown.removeClass("select2-drop-above");
1367
+ },
1368
+
1369
+ /**
1370
+ * Opens the dropdown
1371
+ *
1372
+ * @return {Boolean} whether or not dropdown was opened. This method will return false if, for example,
1373
+ * the dropdown is already open, or if the 'open' event listener on the element called preventDefault().
1374
+ */
1375
+ // abstract
1376
+ open: function () {
1377
+
1378
+ if (!this.shouldOpen()) return false;
1379
+
1380
+ this.opening();
1381
+
1382
+ // Only bind the document mousemove when the dropdown is visible
1383
+ $document.on("mousemove.select2Event", function (e) {
1384
+ lastMousePosition.x = e.pageX;
1385
+ lastMousePosition.y = e.pageY;
1386
+ });
1387
+
1388
+ return true;
1389
+ },
1390
+
1391
+ /**
1392
+ * Performs the opening of the dropdown
1393
+ */
1394
+ // abstract
1395
+ opening: function() {
1396
+ var cid = this.containerEventName,
1397
+ scroll = "scroll." + cid,
1398
+ resize = "resize."+cid,
1399
+ orient = "orientationchange."+cid,
1400
+ mask;
1401
+
1402
+ this.container.addClass("select2-dropdown-open").addClass("select2-container-active");
1403
+
1404
+ this.clearDropdownAlignmentPreference();
1405
+
1406
+ if(this.dropdown[0] !== this.body.children().last()[0]) {
1407
+ this.dropdown.detach().appendTo(this.body);
1408
+ }
1409
+
1410
+ // create the dropdown mask if doesn't already exist
1411
+ mask = $("#select2-drop-mask");
1412
+ if (mask.length === 0) {
1413
+ mask = $(document.createElement("div"));
1414
+ mask.attr("id","select2-drop-mask").attr("class","select2-drop-mask");
1415
+ mask.hide();
1416
+ mask.appendTo(this.body);
1417
+ mask.on("mousedown touchstart click", function (e) {
1418
+ // Prevent IE from generating a click event on the body
1419
+ reinsertElement(mask);
1420
+
1421
+ var dropdown = $("#select2-drop"), self;
1422
+ if (dropdown.length > 0) {
1423
+ self=dropdown.data("select2");
1424
+ if (self.opts.selectOnBlur) {
1425
+ self.selectHighlighted({noFocus: true});
1426
+ }
1427
+ self.close();
1428
+ e.preventDefault();
1429
+ e.stopPropagation();
1430
+ }
1431
+ });
1432
+ }
1433
+
1434
+ // ensure the mask is always right before the dropdown
1435
+ if (this.dropdown.prev()[0] !== mask[0]) {
1436
+ this.dropdown.before(mask);
1437
+ }
1438
+
1439
+ // move the global id to the correct dropdown
1440
+ $("#select2-drop").removeAttr("id");
1441
+ this.dropdown.attr("id", "select2-drop");
1442
+
1443
+ // show the elements
1444
+ mask.show();
1445
+
1446
+ this.positionDropdown();
1447
+ this.dropdown.show();
1448
+ this.positionDropdown();
1449
+
1450
+ this.dropdown.addClass("select2-drop-active");
1451
+
1452
+ // attach listeners to events that can change the position of the container and thus require
1453
+ // the position of the dropdown to be updated as well so it does not come unglued from the container
1454
+ var that = this;
1455
+ this.container.parents().add(window).each(function () {
1456
+ $(this).on(resize+" "+scroll+" "+orient, function (e) {
1457
+ if (that.opened()) that.positionDropdown();
1458
+ });
1459
+ });
1460
+
1461
+
1462
+ },
1463
+
1464
+ // abstract
1465
+ close: function () {
1466
+ if (!this.opened()) return;
1467
+
1468
+ var cid = this.containerEventName,
1469
+ scroll = "scroll." + cid,
1470
+ resize = "resize."+cid,
1471
+ orient = "orientationchange."+cid;
1472
+
1473
+ // unbind event listeners
1474
+ this.container.parents().add(window).each(function () { $(this).off(scroll).off(resize).off(orient); });
1475
+
1476
+ this.clearDropdownAlignmentPreference();
1477
+
1478
+ $("#select2-drop-mask").hide();
1479
+ this.dropdown.removeAttr("id"); // only the active dropdown has the select2-drop id
1480
+ this.dropdown.hide();
1481
+ this.container.removeClass("select2-dropdown-open").removeClass("select2-container-active");
1482
+ this.results.empty();
1483
+
1484
+ // Now that the dropdown is closed, unbind the global document mousemove event
1485
+ $document.off("mousemove.select2Event");
1486
+
1487
+ this.clearSearch();
1488
+ this.search.removeClass("select2-active");
1489
+
1490
+ // Remove the aria active descendant for highlighted element
1491
+ this.search.removeAttr("aria-activedescendant");
1492
+ this.opts.element.trigger($.Event("select2-close"));
1493
+ },
1494
+
1495
+ /**
1496
+ * Opens control, sets input value, and updates results.
1497
+ */
1498
+ // abstract
1499
+ externalSearch: function (term) {
1500
+ this.open();
1501
+ this.search.val(term);
1502
+ this.updateResults(false);
1503
+ },
1504
+
1505
+ // abstract
1506
+ clearSearch: function () {
1507
+
1508
+ },
1509
+
1510
+ /**
1511
+ * @return {Boolean} Whether or not search value was changed.
1512
+ * @private
1513
+ */
1514
+ prefillNextSearchTerm: function () {
1515
+ // initializes search's value with nextSearchTerm (if defined by user)
1516
+ // ignore nextSearchTerm if the dropdown is opened by the user pressing a letter
1517
+ if(this.search.val() !== "") {
1518
+ return false;
1519
+ }
1520
+
1521
+ var nextSearchTerm = this.opts.nextSearchTerm(this.data(), this.lastSearchTerm);
1522
+ if(nextSearchTerm !== undefined){
1523
+ this.search.val(nextSearchTerm);
1524
+ this.search.select();
1525
+ return true;
1526
+ }
1527
+
1528
+ return false;
1529
+ },
1530
+
1531
+ //abstract
1532
+ getMaximumSelectionSize: function() {
1533
+ return evaluate(this.opts.maximumSelectionSize, this.opts.element);
1534
+ },
1535
+
1536
+ // abstract
1537
+ ensureHighlightVisible: function () {
1538
+ var results = this.results, children, index, child, hb, rb, y, more, topOffset;
1539
+
1540
+ index = this.highlight();
1541
+
1542
+ if (index < 0) return;
1543
+
1544
+ if (index == 0) {
1545
+
1546
+ // if the first element is highlighted scroll all the way to the top,
1547
+ // that way any unselectable headers above it will also be scrolled
1548
+ // into view
1549
+
1550
+ results.scrollTop(0);
1551
+ return;
1552
+ }
1553
+
1554
+ children = this.findHighlightableChoices().find('.select2-result-label');
1555
+
1556
+ child = $(children[index]);
1557
+
1558
+ topOffset = (child.offset() || {}).top || 0;
1559
+
1560
+ hb = topOffset + child.outerHeight(true);
1561
+
1562
+ // if this is the last child lets also make sure select2-more-results is visible
1563
+ if (index === children.length - 1) {
1564
+ more = results.find("li.select2-more-results");
1565
+ if (more.length > 0) {
1566
+ hb = more.offset().top + more.outerHeight(true);
1567
+ }
1568
+ }
1569
+
1570
+ rb = results.offset().top + results.outerHeight(false);
1571
+ if (hb > rb) {
1572
+ results.scrollTop(results.scrollTop() + (hb - rb));
1573
+ }
1574
+ y = topOffset - results.offset().top;
1575
+
1576
+ // make sure the top of the element is visible
1577
+ if (y < 0 && child.css('display') != 'none' ) {
1578
+ results.scrollTop(results.scrollTop() + y); // y is negative
1579
+ }
1580
+ },
1581
+
1582
+ // abstract
1583
+ findHighlightableChoices: function() {
1584
+ return this.results.find(".select2-result-selectable:not(.select2-disabled):not(.select2-selected)");
1585
+ },
1586
+
1587
+ // abstract
1588
+ moveHighlight: function (delta) {
1589
+ var choices = this.findHighlightableChoices(),
1590
+ index = this.highlight();
1591
+
1592
+ while (index > -1 && index < choices.length) {
1593
+ index += delta;
1594
+ var choice = $(choices[index]);
1595
+ if (choice.hasClass("select2-result-selectable") && !choice.hasClass("select2-disabled") && !choice.hasClass("select2-selected")) {
1596
+ this.highlight(index);
1597
+ break;
1598
+ }
1599
+ }
1600
+ },
1601
+
1602
+ // abstract
1603
+ highlight: function (index) {
1604
+ var choices = this.findHighlightableChoices(),
1605
+ choice,
1606
+ data;
1607
+
1608
+ if (arguments.length === 0) {
1609
+ return indexOf(choices.filter(".select2-highlighted")[0], choices.get());
1610
+ }
1611
+
1612
+ if (index >= choices.length) index = choices.length - 1;
1613
+ if (index < 0) index = 0;
1614
+
1615
+ this.removeHighlight();
1616
+
1617
+ choice = $(choices[index]);
1618
+ choice.addClass("select2-highlighted");
1619
+
1620
+ // ensure assistive technology can determine the active choice
1621
+ this.search.attr("aria-activedescendant", choice.find(".select2-result-label").attr("id"));
1622
+
1623
+ this.ensureHighlightVisible();
1624
+
1625
+ this.liveRegion.text(choice.text());
1626
+
1627
+ data = choice.data("select2-data");
1628
+ if (data) {
1629
+ this.opts.element.trigger({ type: "select2-highlight", val: this.id(data), choice: data });
1630
+ }
1631
+ },
1632
+
1633
+ removeHighlight: function() {
1634
+ this.results.find(".select2-highlighted").removeClass("select2-highlighted");
1635
+ },
1636
+
1637
+ touchMoved: function() {
1638
+ this._touchMoved = true;
1639
+ },
1640
+
1641
+ clearTouchMoved: function() {
1642
+ this._touchMoved = false;
1643
+ },
1644
+
1645
+ // abstract
1646
+ countSelectableResults: function() {
1647
+ return this.findHighlightableChoices().length;
1648
+ },
1649
+
1650
+ // abstract
1651
+ highlightUnderEvent: function (event) {
1652
+ var el = $(event.target).closest(".select2-result-selectable");
1653
+ if (el.length > 0 && !el.is(".select2-highlighted")) {
1654
+ var choices = this.findHighlightableChoices();
1655
+ this.highlight(choices.index(el));
1656
+ } else if (el.length == 0) {
1657
+ // if we are over an unselectable item remove all highlights
1658
+ this.removeHighlight();
1659
+ }
1660
+ },
1661
+
1662
+ // abstract
1663
+ loadMoreIfNeeded: function () {
1664
+ var results = this.results,
1665
+ more = results.find("li.select2-more-results"),
1666
+ below, // pixels the element is below the scroll fold, below==0 is when the element is starting to be visible
1667
+ page = this.resultsPage + 1,
1668
+ self=this,
1669
+ term=this.search.val(),
1670
+ context=this.context;
1671
+
1672
+ if (more.length === 0) return;
1673
+ below = more.offset().top - results.offset().top - results.height();
1674
+
1675
+ if (below <= this.opts.loadMorePadding) {
1676
+ more.addClass("select2-active");
1677
+ this.opts.query({
1678
+ element: this.opts.element,
1679
+ term: term,
1680
+ page: page,
1681
+ context: context,
1682
+ matcher: this.opts.matcher,
1683
+ callback: this.bind(function (data) {
1684
+
1685
+ // ignore a response if the select2 has been closed before it was received
1686
+ if (!self.opened()) return;
1687
+
1688
+
1689
+ self.opts.populateResults.call(this, results, data.results, {term: term, page: page, context:context});
1690
+ self.postprocessResults(data, false, false);
1691
+
1692
+ if (data.more===true) {
1693
+ more.detach().appendTo(results).html(self.opts.escapeMarkup(evaluate(self.opts.formatLoadMore, self.opts.element, page+1)));
1694
+ window.setTimeout(function() { self.loadMoreIfNeeded(); }, 10);
1695
+ } else {
1696
+ more.remove();
1697
+ }
1698
+ self.positionDropdown();
1699
+ self.resultsPage = page;
1700
+ self.context = data.context;
1701
+ this.opts.element.trigger({ type: "select2-loaded", items: data });
1702
+ })});
1703
+ }
1704
+ },
1705
+
1706
+ /**
1707
+ * Default tokenizer function which does nothing
1708
+ */
1709
+ tokenize: function() {
1710
+
1711
+ },
1712
+
1713
+ /**
1714
+ * @param initial whether or not this is the call to this method right after the dropdown has been opened
1715
+ */
1716
+ // abstract
1717
+ updateResults: function (initial) {
1718
+ var search = this.search,
1719
+ results = this.results,
1720
+ opts = this.opts,
1721
+ data,
1722
+ self = this,
1723
+ input,
1724
+ term = search.val(),
1725
+ lastTerm = $.data(this.container, "select2-last-term"),
1726
+ // sequence number used to drop out-of-order responses
1727
+ queryNumber;
1728
+
1729
+ // prevent duplicate queries against the same term
1730
+ if (initial !== true && lastTerm && equal(term, lastTerm)) return;
1731
+
1732
+ $.data(this.container, "select2-last-term", term);
1733
+
1734
+ // if the search is currently hidden we do not alter the results
1735
+ if (initial !== true && (this.showSearchInput === false || !this.opened())) {
1736
+ return;
1737
+ }
1738
+
1739
+ function postRender() {
1740
+ search.removeClass("select2-active");
1741
+ self.positionDropdown();
1742
+ if (results.find('.select2-no-results,.select2-selection-limit,.select2-searching').length) {
1743
+ self.liveRegion.text(results.text());
1744
+ }
1745
+ else {
1746
+ self.liveRegion.text(self.opts.formatMatches(results.find('.select2-result-selectable:not(".select2-selected")').length));
1747
+ }
1748
+ }
1749
+
1750
+ function render(html) {
1751
+ results.html(html);
1752
+ postRender();
1753
+ }
1754
+
1755
+ queryNumber = ++this.queryCount;
1756
+
1757
+ var maxSelSize = this.getMaximumSelectionSize();
1758
+ if (maxSelSize >=1) {
1759
+ data = this.data();
1760
+ if ($.isArray(data) && data.length >= maxSelSize && checkFormatter(opts.formatSelectionTooBig, "formatSelectionTooBig")) {
1761
+ render("<li class='select2-selection-limit'>" + evaluate(opts.formatSelectionTooBig, opts.element, maxSelSize) + "</li>");
1762
+ return;
1763
+ }
1764
+ }
1765
+
1766
+ if (search.val().length < opts.minimumInputLength) {
1767
+ if (checkFormatter(opts.formatInputTooShort, "formatInputTooShort")) {
1768
+ render("<li class='select2-no-results'>" + evaluate(opts.formatInputTooShort, opts.element, search.val(), opts.minimumInputLength) + "</li>");
1769
+ } else {
1770
+ render("");
1771
+ }
1772
+ if (initial && this.showSearch) this.showSearch(true);
1773
+ return;
1774
+ }
1775
+
1776
+ if (opts.maximumInputLength && search.val().length > opts.maximumInputLength) {
1777
+ if (checkFormatter(opts.formatInputTooLong, "formatInputTooLong")) {
1778
+ render("<li class='select2-no-results'>" + evaluate(opts.formatInputTooLong, opts.element, search.val(), opts.maximumInputLength) + "</li>");
1779
+ } else {
1780
+ render("");
1781
+ }
1782
+ return;
1783
+ }
1784
+
1785
+ if (opts.formatSearching && this.findHighlightableChoices().length === 0) {
1786
+ render("<li class='select2-searching'>" + evaluate(opts.formatSearching, opts.element) + "</li>");
1787
+ }
1788
+
1789
+ search.addClass("select2-active");
1790
+
1791
+ this.removeHighlight();
1792
+
1793
+ // give the tokenizer a chance to pre-process the input
1794
+ input = this.tokenize();
1795
+ if (input != undefined && input != null) {
1796
+ search.val(input);
1797
+ }
1798
+
1799
+ this.resultsPage = 1;
1800
+
1801
+ opts.query({
1802
+ element: opts.element,
1803
+ term: search.val(),
1804
+ page: this.resultsPage,
1805
+ context: null,
1806
+ matcher: opts.matcher,
1807
+ callback: this.bind(function (data) {
1808
+ var def; // default choice
1809
+
1810
+ // ignore old responses
1811
+ if (queryNumber != this.queryCount) {
1812
+ return;
1813
+ }
1814
+
1815
+ // ignore a response if the select2 has been closed before it was received
1816
+ if (!this.opened()) {
1817
+ this.search.removeClass("select2-active");
1818
+ return;
1819
+ }
1820
+
1821
+ // handle ajax error
1822
+ if(data.hasError !== undefined && checkFormatter(opts.formatAjaxError, "formatAjaxError")) {
1823
+ render("<li class='select2-ajax-error'>" + evaluate(opts.formatAjaxError, opts.element, data.jqXHR, data.textStatus, data.errorThrown) + "</li>");
1824
+ return;
1825
+ }
1826
+
1827
+ // save context, if any
1828
+ this.context = (data.context===undefined) ? null : data.context;
1829
+ // create a default choice and prepend it to the list
1830
+ if (this.opts.createSearchChoice && search.val() !== "") {
1831
+ def = this.opts.createSearchChoice.call(self, search.val(), data.results);
1832
+ if (def !== undefined && def !== null && self.id(def) !== undefined && self.id(def) !== null) {
1833
+ if ($(data.results).filter(
1834
+ function () {
1835
+ return equal(self.id(this), self.id(def));
1836
+ }).length === 0) {
1837
+ this.opts.createSearchChoicePosition(data.results, def);
1838
+ }
1839
+ }
1840
+ }
1841
+
1842
+ if (data.results.length === 0 && checkFormatter(opts.formatNoMatches, "formatNoMatches")) {
1843
+ render("<li class='select2-no-results'>" + evaluate(opts.formatNoMatches, opts.element, search.val()) + "</li>");
1844
+ this.showSearch(search.val());
1845
+ return;
1846
+ }
1847
+
1848
+ results.empty();
1849
+ self.opts.populateResults.call(this, results, data.results, {term: search.val(), page: this.resultsPage, context:null});
1850
+
1851
+ if (data.more === true && checkFormatter(opts.formatLoadMore, "formatLoadMore")) {
1852
+ results.append("<li class='select2-more-results'>" + opts.escapeMarkup(evaluate(opts.formatLoadMore, opts.element, this.resultsPage)) + "</li>");
1853
+ window.setTimeout(function() { self.loadMoreIfNeeded(); }, 10);
1854
+ }
1855
+
1856
+ this.postprocessResults(data, initial);
1857
+
1858
+ postRender();
1859
+
1860
+ this.opts.element.trigger({ type: "select2-loaded", items: data });
1861
+ })});
1862
+ },
1863
+
1864
+ // abstract
1865
+ cancel: function () {
1866
+ this.close();
1867
+ },
1868
+
1869
+ // abstract
1870
+ blur: function () {
1871
+ // if selectOnBlur == true, select the currently highlighted option
1872
+ if (this.opts.selectOnBlur)
1873
+ this.selectHighlighted({noFocus: true});
1874
+
1875
+ this.close();
1876
+ this.container.removeClass("select2-container-active");
1877
+ // synonymous to .is(':focus'), which is available in jquery >= 1.6
1878
+ if (this.search[0] === document.activeElement) { this.search.blur(); }
1879
+ this.clearSearch();
1880
+ this.selection.find(".select2-search-choice-focus").removeClass("select2-search-choice-focus");
1881
+ },
1882
+
1883
+ // abstract
1884
+ focusSearch: function () {
1885
+ focus(this.search);
1886
+ },
1887
+
1888
+ // abstract
1889
+ selectHighlighted: function (options) {
1890
+ if (this._touchMoved) {
1891
+ this.clearTouchMoved();
1892
+ return;
1893
+ }
1894
+ var index=this.highlight(),
1895
+ highlighted=this.results.find(".select2-highlighted"),
1896
+ data = highlighted.closest('.select2-result').data("select2-data");
1897
+
1898
+ if (data) {
1899
+ this.highlight(index);
1900
+ this.onSelect(data, options);
1901
+ } else if (options && options.noFocus) {
1902
+ this.close();
1903
+ }
1904
+ },
1905
+
1906
+ // abstract
1907
+ getPlaceholder: function () {
1908
+ var placeholderOption;
1909
+ return this.opts.element.attr("placeholder") ||
1910
+ this.opts.element.attr("data-placeholder") || // jquery 1.4 compat
1911
+ this.opts.element.data("placeholder") ||
1912
+ this.opts.placeholder ||
1913
+ ((placeholderOption = this.getPlaceholderOption()) !== undefined ? placeholderOption.text() : undefined);
1914
+ },
1915
+
1916
+ // abstract
1917
+ getPlaceholderOption: function() {
1918
+ if (this.select) {
1919
+ var firstOption = this.select.children('option').first();
1920
+ if (this.opts.placeholderOption !== undefined ) {
1921
+ //Determine the placeholder option based on the specified placeholderOption setting
1922
+ return (this.opts.placeholderOption === "first" && firstOption) ||
1923
+ (typeof this.opts.placeholderOption === "function" && this.opts.placeholderOption(this.select));
1924
+ } else if ($.trim(firstOption.text()) === "" && firstOption.val() === "") {
1925
+ //No explicit placeholder option specified, use the first if it's blank
1926
+ return firstOption;
1927
+ }
1928
+ }
1929
+ },
1930
+
1931
+ /**
1932
+ * Get the desired width for the container element. This is
1933
+ * derived first from option `width` passed to select2, then
1934
+ * the inline 'style' on the original element, and finally
1935
+ * falls back to the jQuery calculated element width.
1936
+ */
1937
+ // abstract
1938
+ initContainerWidth: function () {
1939
+ function resolveContainerWidth() {
1940
+ var style, attrs, matches, i, l, attr;
1941
+
1942
+ if (this.opts.width === "off") {
1943
+ return null;
1944
+ } else if (this.opts.width === "element"){
1945
+ return this.opts.element.outerWidth(false) === 0 ? 'auto' : this.opts.element.outerWidth(false) + 'px';
1946
+ } else if (this.opts.width === "copy" || this.opts.width === "resolve") {
1947
+ // check if there is inline style on the element that contains width
1948
+ style = this.opts.element.attr('style');
1949
+ if (typeof(style) === "string") {
1950
+ attrs = style.split(';');
1951
+ for (i = 0, l = attrs.length; i < l; i = i + 1) {
1952
+ attr = attrs[i].replace(/\s/g, '');
1953
+ matches = attr.match(/^width:(([-+]?([0-9]*\.)?[0-9]+)(px|em|ex|%|in|cm|mm|pt|pc))/i);
1954
+ if (matches !== null && matches.length >= 1)
1955
+ return matches[1];
1956
+ }
1957
+ }
1958
+
1959
+ if (this.opts.width === "resolve") {
1960
+ // next check if css('width') can resolve a width that is percent based, this is sometimes possible
1961
+ // when attached to input type=hidden or elements hidden via css
1962
+ style = this.opts.element.css('width');
1963
+ if (style.indexOf("%") > 0) return style;
1964
+
1965
+ // finally, fallback on the calculated width of the element
1966
+ return (this.opts.element.outerWidth(false) === 0 ? 'auto' : this.opts.element.outerWidth(false) + 'px');
1967
+ }
1968
+
1969
+ return null;
1970
+ } else if ($.isFunction(this.opts.width)) {
1971
+ return this.opts.width();
1972
+ } else {
1973
+ return this.opts.width;
1974
+ }
1975
+ };
1976
+
1977
+ var width = resolveContainerWidth.call(this);
1978
+ if (width !== null) {
1979
+ this.container.css("width", width);
1980
+ }
1981
+ }
1982
+ });
1983
+
1984
+ SingleSelect2 = clazz(AbstractSelect2, {
1985
+
1986
+ // single
1987
+
1988
+ createContainer: function () {
1989
+ var container = $(document.createElement("div")).attr({
1990
+ "class": "select2-container"
1991
+ }).html([
1992
+ "<a href='javascript:void(0)' class='select2-choice' tabindex='-1'>",
1993
+ " <span class='select2-chosen'>&#160;</span><abbr class='select2-search-choice-close'></abbr>",
1994
+ " <span class='select2-arrow' role='presentation'><b role='presentation'></b></span>",
1995
+ "</a>",
1996
+ "<label for='' class='select2-offscreen'></label>",
1997
+ "<input class='select2-focusser select2-offscreen' type='text' aria-haspopup='true' role='button' />",
1998
+ "<div class='select2-drop select2-display-none'>",
1999
+ " <div class='select2-search'>",
2000
+ " <label for='' class='select2-offscreen'></label>",
2001
+ " <input type='text' autocomplete='off' autocorrect='off' autocapitalize='off' spellcheck='false' class='select2-input' role='combobox' aria-expanded='true'",
2002
+ " aria-autocomplete='list' />",
2003
+ " </div>",
2004
+ " <ul class='select2-results' role='listbox'>",
2005
+ " </ul>",
2006
+ "</div>"].join(""));
2007
+ return container;
2008
+ },
2009
+
2010
+ // single
2011
+ enableInterface: function() {
2012
+ if (this.parent.enableInterface.apply(this, arguments)) {
2013
+ this.focusser.prop("disabled", !this.isInterfaceEnabled());
2014
+ }
2015
+ },
2016
+
2017
+ // single
2018
+ opening: function () {
2019
+ var el, range, len;
2020
+
2021
+ if (this.opts.minimumResultsForSearch >= 0) {
2022
+ this.showSearch(true);
2023
+ }
2024
+
2025
+ this.parent.opening.apply(this, arguments);
2026
+
2027
+ if (this.showSearchInput !== false) {
2028
+ // IE appends focusser.val() at the end of field :/ so we manually insert it at the beginning using a range
2029
+ // all other browsers handle this just fine
2030
+
2031
+ this.search.val(this.focusser.val());
2032
+ }
2033
+ if (this.opts.shouldFocusInput(this)) {
2034
+ this.search.focus();
2035
+ // move the cursor to the end after focussing, otherwise it will be at the beginning and
2036
+ // new text will appear *before* focusser.val()
2037
+ el = this.search.get(0);
2038
+ if (el.createTextRange) {
2039
+ range = el.createTextRange();
2040
+ range.collapse(false);
2041
+ range.select();
2042
+ } else if (el.setSelectionRange) {
2043
+ len = this.search.val().length;
2044
+ el.setSelectionRange(len, len);
2045
+ }
2046
+ }
2047
+
2048
+ this.prefillNextSearchTerm();
2049
+
2050
+ this.focusser.prop("disabled", true).val("");
2051
+ this.updateResults(true);
2052
+ this.opts.element.trigger($.Event("select2-open"));
2053
+ },
2054
+
2055
+ // single
2056
+ close: function () {
2057
+ if (!this.opened()) return;
2058
+ this.parent.close.apply(this, arguments);
2059
+
2060
+ this.focusser.prop("disabled", false);
2061
+
2062
+ if (this.opts.shouldFocusInput(this)) {
2063
+ this.focusser.focus();
2064
+ }
2065
+ },
2066
+
2067
+ // single
2068
+ focus: function () {
2069
+ if (this.opened()) {
2070
+ this.close();
2071
+ } else {
2072
+ this.focusser.prop("disabled", false);
2073
+ if (this.opts.shouldFocusInput(this)) {
2074
+ this.focusser.focus();
2075
+ }
2076
+ }
2077
+ },
2078
+
2079
+ // single
2080
+ isFocused: function () {
2081
+ return this.container.hasClass("select2-container-active");
2082
+ },
2083
+
2084
+ // single
2085
+ cancel: function () {
2086
+ this.parent.cancel.apply(this, arguments);
2087
+ this.focusser.prop("disabled", false);
2088
+
2089
+ if (this.opts.shouldFocusInput(this)) {
2090
+ this.focusser.focus();
2091
+ }
2092
+ },
2093
+
2094
+ // single
2095
+ destroy: function() {
2096
+ $("label[for='" + this.focusser.attr('id') + "']")
2097
+ .attr('for', this.opts.element.attr("id"));
2098
+ this.parent.destroy.apply(this, arguments);
2099
+
2100
+ cleanupJQueryElements.call(this,
2101
+ "selection",
2102
+ "focusser"
2103
+ );
2104
+ },
2105
+
2106
+ // single
2107
+ initContainer: function () {
2108
+
2109
+ var selection,
2110
+ container = this.container,
2111
+ dropdown = this.dropdown,
2112
+ idSuffix = nextUid(),
2113
+ elementLabel;
2114
+
2115
+ if (this.opts.minimumResultsForSearch < 0) {
2116
+ this.showSearch(false);
2117
+ } else {
2118
+ this.showSearch(true);
2119
+ }
2120
+
2121
+ this.selection = selection = container.find(".select2-choice");
2122
+
2123
+ this.focusser = container.find(".select2-focusser");
2124
+
2125
+ // add aria associations
2126
+ selection.find(".select2-chosen").attr("id", "select2-chosen-"+idSuffix);
2127
+ this.focusser.attr("aria-labelledby", "select2-chosen-"+idSuffix);
2128
+ this.results.attr("id", "select2-results-"+idSuffix);
2129
+ this.search.attr("aria-owns", "select2-results-"+idSuffix);
2130
+
2131
+ // rewrite labels from original element to focusser
2132
+ this.focusser.attr("id", "s2id_autogen"+idSuffix);
2133
+
2134
+ elementLabel = $("label[for='" + this.opts.element.attr("id") + "']");
2135
+ this.opts.element.focus(this.bind(function () { this.focus(); }));
2136
+
2137
+ this.focusser.prev()
2138
+ .text(elementLabel.text())
2139
+ .attr('for', this.focusser.attr('id'));
2140
+
2141
+ // Ensure the original element retains an accessible name
2142
+ var originalTitle = this.opts.element.attr("title");
2143
+ this.opts.element.attr("title", (originalTitle || elementLabel.text()));
2144
+
2145
+ this.focusser.attr("tabindex", this.elementTabIndex);
2146
+
2147
+ // write label for search field using the label from the focusser element
2148
+ this.search.attr("id", this.focusser.attr('id') + '_search');
2149
+
2150
+ this.search.prev()
2151
+ .text($("label[for='" + this.focusser.attr('id') + "']").text())
2152
+ .attr('for', this.search.attr('id'));
2153
+
2154
+ this.search.on("keydown", this.bind(function (e) {
2155
+ if (!this.isInterfaceEnabled()) return;
2156
+
2157
+ // filter 229 keyCodes (input method editor is processing key input)
2158
+ if (229 == e.keyCode) return;
2159
+
2160
+ if (e.which === KEY.PAGE_UP || e.which === KEY.PAGE_DOWN) {
2161
+ // prevent the page from scrolling
2162
+ killEvent(e);
2163
+ return;
2164
+ }
2165
+
2166
+ switch (e.which) {
2167
+ case KEY.UP:
2168
+ case KEY.DOWN:
2169
+ this.moveHighlight((e.which === KEY.UP) ? -1 : 1);
2170
+ killEvent(e);
2171
+ return;
2172
+ case KEY.ENTER:
2173
+ this.selectHighlighted();
2174
+ killEvent(e);
2175
+ return;
2176
+ case KEY.TAB:
2177
+ this.selectHighlighted({noFocus: true});
2178
+ return;
2179
+ case KEY.ESC:
2180
+ this.cancel(e);
2181
+ killEvent(e);
2182
+ return;
2183
+ }
2184
+ }));
2185
+
2186
+ this.search.on("blur", this.bind(function(e) {
2187
+ // a workaround for chrome to keep the search field focussed when the scroll bar is used to scroll the dropdown.
2188
+ // without this the search field loses focus which is annoying
2189
+ if (document.activeElement === this.body.get(0)) {
2190
+ window.setTimeout(this.bind(function() {
2191
+ if (this.opened()) {
2192
+ this.search.focus();
2193
+ }
2194
+ }), 0);
2195
+ }
2196
+ }));
2197
+
2198
+ this.focusser.on("keydown", this.bind(function (e) {
2199
+ if (!this.isInterfaceEnabled()) return;
2200
+
2201
+ if (e.which === KEY.TAB || KEY.isControl(e) || KEY.isFunctionKey(e) || e.which === KEY.ESC) {
2202
+ return;
2203
+ }
2204
+
2205
+ if (this.opts.openOnEnter === false && e.which === KEY.ENTER) {
2206
+ killEvent(e);
2207
+ return;
2208
+ }
2209
+
2210
+ if (e.which == KEY.DOWN || e.which == KEY.UP
2211
+ || (e.which == KEY.ENTER && this.opts.openOnEnter)) {
2212
+
2213
+ if (e.altKey || e.ctrlKey || e.shiftKey || e.metaKey) return;
2214
+
2215
+ this.open();
2216
+ killEvent(e);
2217
+ return;
2218
+ }
2219
+
2220
+ if (e.which == KEY.DELETE || e.which == KEY.BACKSPACE) {
2221
+ if (this.opts.allowClear) {
2222
+ this.clear();
2223
+ }
2224
+ killEvent(e);
2225
+ return;
2226
+ }
2227
+ }));
2228
+
2229
+
2230
+ installKeyUpChangeEvent(this.focusser);
2231
+ this.focusser.on("keyup-change input", this.bind(function(e) {
2232
+ if (this.opts.minimumResultsForSearch >= 0) {
2233
+ e.stopPropagation();
2234
+ if (this.opened()) return;
2235
+ this.open();
2236
+ }
2237
+ }));
2238
+
2239
+ selection.on("mousedown touchstart", "abbr", this.bind(function (e) {
2240
+ if (!this.isInterfaceEnabled()) {
2241
+ return;
2242
+ }
2243
+
2244
+ this.clear();
2245
+ killEventImmediately(e);
2246
+ this.close();
2247
+
2248
+ if (this.selection) {
2249
+ this.selection.focus();
2250
+ }
2251
+ }));
2252
+
2253
+ selection.on("mousedown touchstart", this.bind(function (e) {
2254
+ // Prevent IE from generating a click event on the body
2255
+ reinsertElement(selection);
2256
+
2257
+ if (!this.container.hasClass("select2-container-active")) {
2258
+ this.opts.element.trigger($.Event("select2-focus"));
2259
+ }
2260
+
2261
+ if (this.opened()) {
2262
+ this.close();
2263
+ } else if (this.isInterfaceEnabled()) {
2264
+ this.open();
2265
+ }
2266
+
2267
+ killEvent(e);
2268
+ }));
2269
+
2270
+ dropdown.on("mousedown touchstart", this.bind(function() {
2271
+ if (this.opts.shouldFocusInput(this)) {
2272
+ this.search.focus();
2273
+ }
2274
+ }));
2275
+
2276
+ selection.on("focus", this.bind(function(e) {
2277
+ killEvent(e);
2278
+ }));
2279
+
2280
+ this.focusser.on("focus", this.bind(function(){
2281
+ if (!this.container.hasClass("select2-container-active")) {
2282
+ this.opts.element.trigger($.Event("select2-focus"));
2283
+ }
2284
+ this.container.addClass("select2-container-active");
2285
+ })).on("blur", this.bind(function() {
2286
+ if (!this.opened()) {
2287
+ this.container.removeClass("select2-container-active");
2288
+ this.opts.element.trigger($.Event("select2-blur"));
2289
+ }
2290
+ }));
2291
+ this.search.on("focus", this.bind(function(){
2292
+ if (!this.container.hasClass("select2-container-active")) {
2293
+ this.opts.element.trigger($.Event("select2-focus"));
2294
+ }
2295
+ this.container.addClass("select2-container-active");
2296
+ }));
2297
+
2298
+ this.initContainerWidth();
2299
+ this.opts.element.hide();
2300
+ this.setPlaceholder();
2301
+
2302
+ },
2303
+
2304
+ // single
2305
+ clear: function(triggerChange) {
2306
+ var data=this.selection.data("select2-data");
2307
+ if (data) { // guard against queued quick consecutive clicks
2308
+ var evt = $.Event("select2-clearing");
2309
+ this.opts.element.trigger(evt);
2310
+ if (evt.isDefaultPrevented()) {
2311
+ return;
2312
+ }
2313
+ var placeholderOption = this.getPlaceholderOption();
2314
+ this.opts.element.val(placeholderOption ? placeholderOption.val() : "");
2315
+ this.selection.find(".select2-chosen").empty();
2316
+ this.selection.removeData("select2-data");
2317
+ this.setPlaceholder();
2318
+
2319
+ if (triggerChange !== false){
2320
+ this.opts.element.trigger({ type: "select2-removed", val: this.id(data), choice: data });
2321
+ this.triggerChange({removed:data});
2322
+ }
2323
+ }
2324
+ },
2325
+
2326
+ /**
2327
+ * Sets selection based on source element's value
2328
+ */
2329
+ // single
2330
+ initSelection: function () {
2331
+ var selected;
2332
+ if (this.isPlaceholderOptionSelected()) {
2333
+ this.updateSelection(null);
2334
+ this.close();
2335
+ this.setPlaceholder();
2336
+ } else {
2337
+ var self = this;
2338
+ this.opts.initSelection.call(null, this.opts.element, function(selected){
2339
+ if (selected !== undefined && selected !== null) {
2340
+ self.updateSelection(selected);
2341
+ self.close();
2342
+ self.setPlaceholder();
2343
+ self.lastSearchTerm = self.search.val();
2344
+ }
2345
+ });
2346
+ }
2347
+ },
2348
+
2349
+ isPlaceholderOptionSelected: function() {
2350
+ var placeholderOption;
2351
+ if (this.getPlaceholder() === undefined) return false; // no placeholder specified so no option should be considered
2352
+ return ((placeholderOption = this.getPlaceholderOption()) !== undefined && placeholderOption.prop("selected"))
2353
+ || (this.opts.element.val() === "")
2354
+ || (this.opts.element.val() === undefined)
2355
+ || (this.opts.element.val() === null);
2356
+ },
2357
+
2358
+ // single
2359
+ prepareOpts: function () {
2360
+ var opts = this.parent.prepareOpts.apply(this, arguments),
2361
+ self=this;
2362
+
2363
+ if (opts.element.get(0).tagName.toLowerCase() === "select") {
2364
+ // install the selection initializer
2365
+ opts.initSelection = function (element, callback) {
2366
+ var selected = element.find("option").filter(function() { return this.selected && !this.disabled });
2367
+ // a single select box always has a value, no need to null check 'selected'
2368
+ callback(self.optionToData(selected));
2369
+ };
2370
+ } else if ("data" in opts) {
2371
+ // install default initSelection when applied to hidden input and data is local
2372
+ opts.initSelection = opts.initSelection || function (element, callback) {
2373
+ var id = element.val();
2374
+ //search in data by id, storing the actual matching item
2375
+ var match = null;
2376
+ opts.query({
2377
+ matcher: function(term, text, el){
2378
+ var is_match = equal(id, opts.id(el));
2379
+ if (is_match) {
2380
+ match = el;
2381
+ }
2382
+ return is_match;
2383
+ },
2384
+ callback: !$.isFunction(callback) ? $.noop : function() {
2385
+ callback(match);
2386
+ }
2387
+ });
2388
+ };
2389
+ }
2390
+
2391
+ return opts;
2392
+ },
2393
+
2394
+ // single
2395
+ getPlaceholder: function() {
2396
+ // if a placeholder is specified on a single select without a valid placeholder option ignore it
2397
+ if (this.select) {
2398
+ if (this.getPlaceholderOption() === undefined) {
2399
+ return undefined;
2400
+ }
2401
+ }
2402
+
2403
+ return this.parent.getPlaceholder.apply(this, arguments);
2404
+ },
2405
+
2406
+ // single
2407
+ setPlaceholder: function () {
2408
+ var placeholder = this.getPlaceholder();
2409
+
2410
+ if (this.isPlaceholderOptionSelected() && placeholder !== undefined) {
2411
+
2412
+ // check for a placeholder option if attached to a select
2413
+ if (this.select && this.getPlaceholderOption() === undefined) return;
2414
+
2415
+ this.selection.find(".select2-chosen").html(this.opts.escapeMarkup(placeholder));
2416
+
2417
+ this.selection.addClass("select2-default");
2418
+
2419
+ this.container.removeClass("select2-allowclear");
2420
+ }
2421
+ },
2422
+
2423
+ // single
2424
+ postprocessResults: function (data, initial, noHighlightUpdate) {
2425
+ var selected = 0, self = this, showSearchInput = true;
2426
+
2427
+ // find the selected element in the result list
2428
+
2429
+ this.findHighlightableChoices().each2(function (i, elm) {
2430
+ if (equal(self.id(elm.data("select2-data")), self.opts.element.val())) {
2431
+ selected = i;
2432
+ return false;
2433
+ }
2434
+ });
2435
+
2436
+ // and highlight it
2437
+ if (noHighlightUpdate !== false) {
2438
+ if (initial === true && selected >= 0) {
2439
+ this.highlight(selected);
2440
+ } else {
2441
+ this.highlight(0);
2442
+ }
2443
+ }
2444
+
2445
+ // hide the search box if this is the first we got the results and there are enough of them for search
2446
+
2447
+ if (initial === true) {
2448
+ var min = this.opts.minimumResultsForSearch;
2449
+ if (min >= 0) {
2450
+ this.showSearch(countResults(data.results) >= min);
2451
+ }
2452
+ }
2453
+ },
2454
+
2455
+ // single
2456
+ showSearch: function(showSearchInput) {
2457
+ if (this.showSearchInput === showSearchInput) return;
2458
+
2459
+ this.showSearchInput = showSearchInput;
2460
+
2461
+ this.dropdown.find(".select2-search").toggleClass("select2-search-hidden", !showSearchInput);
2462
+ this.dropdown.find(".select2-search").toggleClass("select2-offscreen", !showSearchInput);
2463
+ //add "select2-with-searchbox" to the container if search box is shown
2464
+ $(this.dropdown, this.container).toggleClass("select2-with-searchbox", showSearchInput);
2465
+ },
2466
+
2467
+ // single
2468
+ onSelect: function (data, options) {
2469
+
2470
+ if (!this.triggerSelect(data)) { return; }
2471
+
2472
+ var old = this.opts.element.val(),
2473
+ oldData = this.data();
2474
+
2475
+ this.opts.element.val(this.id(data));
2476
+ this.updateSelection(data);
2477
+
2478
+ this.opts.element.trigger({ type: "select2-selected", val: this.id(data), choice: data });
2479
+
2480
+ this.lastSearchTerm = this.search.val();
2481
+ this.close();
2482
+
2483
+ if ((!options || !options.noFocus) && this.opts.shouldFocusInput(this)) {
2484
+ this.focusser.focus();
2485
+ }
2486
+
2487
+ if (!equal(old, this.id(data))) {
2488
+ this.triggerChange({ added: data, removed: oldData });
2489
+ }
2490
+ },
2491
+
2492
+ // single
2493
+ updateSelection: function (data) {
2494
+
2495
+ var container=this.selection.find(".select2-chosen"), formatted, cssClass;
2496
+
2497
+ this.selection.data("select2-data", data);
2498
+
2499
+ container.empty();
2500
+ if (data !== null) {
2501
+ formatted=this.opts.formatSelection(data, container, this.opts.escapeMarkup);
2502
+ }
2503
+ if (formatted !== undefined) {
2504
+ container.append(formatted);
2505
+ }
2506
+ cssClass=this.opts.formatSelectionCssClass(data, container);
2507
+ if (cssClass !== undefined) {
2508
+ container.addClass(cssClass);
2509
+ }
2510
+
2511
+ this.selection.removeClass("select2-default");
2512
+
2513
+ if (this.opts.allowClear && this.getPlaceholder() !== undefined) {
2514
+ this.container.addClass("select2-allowclear");
2515
+ }
2516
+ },
2517
+
2518
+ // single
2519
+ val: function () {
2520
+ var val,
2521
+ triggerChange = false,
2522
+ data = null,
2523
+ self = this,
2524
+ oldData = this.data();
2525
+
2526
+ if (arguments.length === 0) {
2527
+ return this.opts.element.val();
2528
+ }
2529
+
2530
+ val = arguments[0];
2531
+
2532
+ if (arguments.length > 1) {
2533
+ triggerChange = arguments[1];
2534
+ }
2535
+
2536
+ if (this.select) {
2537
+ this.select
2538
+ .val(val)
2539
+ .find("option").filter(function() { return this.selected }).each2(function (i, elm) {
2540
+ data = self.optionToData(elm);
2541
+ return false;
2542
+ });
2543
+ this.updateSelection(data);
2544
+ this.setPlaceholder();
2545
+ if (triggerChange) {
2546
+ this.triggerChange({added: data, removed:oldData});
2547
+ }
2548
+ } else {
2549
+ // val is an id. !val is true for [undefined,null,'',0] - 0 is legal
2550
+ if (!val && val !== 0) {
2551
+ this.clear(triggerChange);
2552
+ return;
2553
+ }
2554
+ if (this.opts.initSelection === undefined) {
2555
+ throw new Error("cannot call val() if initSelection() is not defined");
2556
+ }
2557
+ this.opts.element.val(val);
2558
+ this.opts.initSelection(this.opts.element, function(data){
2559
+ self.opts.element.val(!data ? "" : self.id(data));
2560
+ self.updateSelection(data);
2561
+ self.setPlaceholder();
2562
+ if (triggerChange) {
2563
+ self.triggerChange({added: data, removed:oldData});
2564
+ }
2565
+ });
2566
+ }
2567
+ },
2568
+
2569
+ // single
2570
+ clearSearch: function () {
2571
+ this.search.val("");
2572
+ this.focusser.val("");
2573
+ },
2574
+
2575
+ // single
2576
+ data: function(value) {
2577
+ var data,
2578
+ triggerChange = false;
2579
+
2580
+ if (arguments.length === 0) {
2581
+ data = this.selection.data("select2-data");
2582
+ if (data == undefined) data = null;
2583
+ return data;
2584
+ } else {
2585
+ if (arguments.length > 1) {
2586
+ triggerChange = arguments[1];
2587
+ }
2588
+ if (!value) {
2589
+ this.clear(triggerChange);
2590
+ } else {
2591
+ data = this.data();
2592
+ this.opts.element.val(!value ? "" : this.id(value));
2593
+ this.updateSelection(value);
2594
+ if (triggerChange) {
2595
+ this.triggerChange({added: value, removed:data});
2596
+ }
2597
+ }
2598
+ }
2599
+ }
2600
+ });
2601
+
2602
+ MultiSelect2 = clazz(AbstractSelect2, {
2603
+
2604
+ // multi
2605
+ createContainer: function () {
2606
+ var container = $(document.createElement("div")).attr({
2607
+ "class": "select2-container select2-container-multi"
2608
+ }).html([
2609
+ "<ul class='select2-choices'>",
2610
+ " <li class='select2-search-field'>",
2611
+ " <label for='' class='select2-offscreen'></label>",
2612
+ " <input type='text' autocomplete='off' autocorrect='off' autocapitalize='off' spellcheck='false' class='select2-input'>",
2613
+ " </li>",
2614
+ "</ul>",
2615
+ "<div class='select2-drop select2-drop-multi select2-display-none'>",
2616
+ " <ul class='select2-results'>",
2617
+ " </ul>",
2618
+ "</div>"].join(""));
2619
+ return container;
2620
+ },
2621
+
2622
+ // multi
2623
+ prepareOpts: function () {
2624
+ var opts = this.parent.prepareOpts.apply(this, arguments),
2625
+ self=this;
2626
+
2627
+ // TODO validate placeholder is a string if specified
2628
+ if (opts.element.get(0).tagName.toLowerCase() === "select") {
2629
+ // install the selection initializer
2630
+ opts.initSelection = function (element, callback) {
2631
+
2632
+ var data = [];
2633
+
2634
+ element.find("option").filter(function() { return this.selected && !this.disabled }).each2(function (i, elm) {
2635
+ data.push(self.optionToData(elm));
2636
+ });
2637
+ callback(data);
2638
+ };
2639
+ } else if ("data" in opts) {
2640
+ // install default initSelection when applied to hidden input and data is local
2641
+ opts.initSelection = opts.initSelection || function (element, callback) {
2642
+ var ids = splitVal(element.val(), opts.separator, opts.transformVal);
2643
+ //search in data by array of ids, storing matching items in a list
2644
+ var matches = [];
2645
+ opts.query({
2646
+ matcher: function(term, text, el){
2647
+ var is_match = $.grep(ids, function(id) {
2648
+ return equal(id, opts.id(el));
2649
+ }).length;
2650
+ if (is_match) {
2651
+ matches.push(el);
2652
+ }
2653
+ return is_match;
2654
+ },
2655
+ callback: !$.isFunction(callback) ? $.noop : function() {
2656
+ // reorder matches based on the order they appear in the ids array because right now
2657
+ // they are in the order in which they appear in data array
2658
+ var ordered = [];
2659
+ for (var i = 0; i < ids.length; i++) {
2660
+ var id = ids[i];
2661
+ for (var j = 0; j < matches.length; j++) {
2662
+ var match = matches[j];
2663
+ if (equal(id, opts.id(match))) {
2664
+ ordered.push(match);
2665
+ matches.splice(j, 1);
2666
+ break;
2667
+ }
2668
+ }
2669
+ }
2670
+ callback(ordered);
2671
+ }
2672
+ });
2673
+ };
2674
+ }
2675
+
2676
+ return opts;
2677
+ },
2678
+
2679
+ // multi
2680
+ selectChoice: function (choice) {
2681
+
2682
+ var selected = this.container.find(".select2-search-choice-focus");
2683
+ if (selected.length && choice && choice[0] == selected[0]) {
2684
+
2685
+ } else {
2686
+ if (selected.length) {
2687
+ this.opts.element.trigger("choice-deselected", selected);
2688
+ }
2689
+ selected.removeClass("select2-search-choice-focus");
2690
+ if (choice && choice.length) {
2691
+ this.close();
2692
+ choice.addClass("select2-search-choice-focus");
2693
+ this.opts.element.trigger("choice-selected", choice);
2694
+ }
2695
+ }
2696
+ },
2697
+
2698
+ // multi
2699
+ destroy: function() {
2700
+ $("label[for='" + this.search.attr('id') + "']")
2701
+ .attr('for', this.opts.element.attr("id"));
2702
+ this.parent.destroy.apply(this, arguments);
2703
+
2704
+ cleanupJQueryElements.call(this,
2705
+ "searchContainer",
2706
+ "selection"
2707
+ );
2708
+ },
2709
+
2710
+ // multi
2711
+ initContainer: function () {
2712
+
2713
+ var selector = ".select2-choices", selection;
2714
+
2715
+ this.searchContainer = this.container.find(".select2-search-field");
2716
+ this.selection = selection = this.container.find(selector);
2717
+
2718
+ var _this = this;
2719
+ this.selection.on("click", ".select2-container:not(.select2-container-disabled) .select2-search-choice:not(.select2-locked)", function (e) {
2720
+ _this.search[0].focus();
2721
+ _this.selectChoice($(this));
2722
+ });
2723
+
2724
+ // rewrite labels from original element to focusser
2725
+ this.search.attr("id", "s2id_autogen"+nextUid());
2726
+
2727
+ this.search.prev()
2728
+ .text($("label[for='" + this.opts.element.attr("id") + "']").text())
2729
+ .attr('for', this.search.attr('id'));
2730
+ this.opts.element.focus(this.bind(function () { this.focus(); }));
2731
+
2732
+ this.search.on("input paste", this.bind(function() {
2733
+ if (this.search.attr('placeholder') && this.search.val().length == 0) return;
2734
+ if (!this.isInterfaceEnabled()) return;
2735
+ if (!this.opened()) {
2736
+ this.open();
2737
+ }
2738
+ }));
2739
+
2740
+ this.search.attr("tabindex", this.elementTabIndex);
2741
+
2742
+ this.keydowns = 0;
2743
+ this.search.on("keydown", this.bind(function (e) {
2744
+ if (!this.isInterfaceEnabled()) return;
2745
+
2746
+ ++this.keydowns;
2747
+ var selected = selection.find(".select2-search-choice-focus");
2748
+ var prev = selected.prev(".select2-search-choice:not(.select2-locked)");
2749
+ var next = selected.next(".select2-search-choice:not(.select2-locked)");
2750
+ var pos = getCursorInfo(this.search);
2751
+
2752
+ if (selected.length &&
2753
+ (e.which == KEY.LEFT || e.which == KEY.RIGHT || e.which == KEY.BACKSPACE || e.which == KEY.DELETE || e.which == KEY.ENTER)) {
2754
+ var selectedChoice = selected;
2755
+ if (e.which == KEY.LEFT && prev.length) {
2756
+ selectedChoice = prev;
2757
+ }
2758
+ else if (e.which == KEY.RIGHT) {
2759
+ selectedChoice = next.length ? next : null;
2760
+ }
2761
+ else if (e.which === KEY.BACKSPACE) {
2762
+ if (this.unselect(selected.first())) {
2763
+ this.search.width(10);
2764
+ selectedChoice = prev.length ? prev : next;
2765
+ }
2766
+ } else if (e.which == KEY.DELETE) {
2767
+ if (this.unselect(selected.first())) {
2768
+ this.search.width(10);
2769
+ selectedChoice = next.length ? next : null;
2770
+ }
2771
+ } else if (e.which == KEY.ENTER) {
2772
+ selectedChoice = null;
2773
+ }
2774
+
2775
+ this.selectChoice(selectedChoice);
2776
+ killEvent(e);
2777
+ if (!selectedChoice || !selectedChoice.length) {
2778
+ this.open();
2779
+ }
2780
+ return;
2781
+ } else if (((e.which === KEY.BACKSPACE && this.keydowns == 1)
2782
+ || e.which == KEY.LEFT) && (pos.offset == 0 && !pos.length)) {
2783
+
2784
+ this.selectChoice(selection.find(".select2-search-choice:not(.select2-locked)").last());
2785
+ killEvent(e);
2786
+ return;
2787
+ } else {
2788
+ this.selectChoice(null);
2789
+ }
2790
+
2791
+ if (this.opened()) {
2792
+ switch (e.which) {
2793
+ case KEY.UP:
2794
+ case KEY.DOWN:
2795
+ this.moveHighlight((e.which === KEY.UP) ? -1 : 1);
2796
+ killEvent(e);
2797
+ return;
2798
+ case KEY.ENTER:
2799
+ this.selectHighlighted();
2800
+ killEvent(e);
2801
+ return;
2802
+ case KEY.TAB:
2803
+ this.selectHighlighted({noFocus:true});
2804
+ this.close();
2805
+ return;
2806
+ case KEY.ESC:
2807
+ this.cancel(e);
2808
+ killEvent(e);
2809
+ return;
2810
+ }
2811
+ }
2812
+
2813
+ if (e.which === KEY.TAB || KEY.isControl(e) || KEY.isFunctionKey(e)
2814
+ || e.which === KEY.BACKSPACE || e.which === KEY.ESC) {
2815
+ return;
2816
+ }
2817
+
2818
+ if (e.which === KEY.ENTER) {
2819
+ if (this.opts.openOnEnter === false) {
2820
+ return;
2821
+ } else if (e.altKey || e.ctrlKey || e.shiftKey || e.metaKey) {
2822
+ return;
2823
+ }
2824
+ }
2825
+
2826
+ this.open();
2827
+
2828
+ if (e.which === KEY.PAGE_UP || e.which === KEY.PAGE_DOWN) {
2829
+ // prevent the page from scrolling
2830
+ killEvent(e);
2831
+ }
2832
+
2833
+ if (e.which === KEY.ENTER) {
2834
+ // prevent form from being submitted
2835
+ killEvent(e);
2836
+ }
2837
+
2838
+ }));
2839
+
2840
+ this.search.on("keyup", this.bind(function (e) {
2841
+ this.keydowns = 0;
2842
+ this.resizeSearch();
2843
+ })
2844
+ );
2845
+
2846
+ this.search.on("blur", this.bind(function(e) {
2847
+ this.container.removeClass("select2-container-active");
2848
+ this.search.removeClass("select2-focused");
2849
+ this.selectChoice(null);
2850
+ if (!this.opened()) this.clearSearch();
2851
+ e.stopImmediatePropagation();
2852
+ this.opts.element.trigger($.Event("select2-blur"));
2853
+ }));
2854
+
2855
+ this.container.on("click", selector, this.bind(function (e) {
2856
+ if (!this.isInterfaceEnabled()) return;
2857
+ if ($(e.target).closest(".select2-search-choice").length > 0) {
2858
+ // clicked inside a select2 search choice, do not open
2859
+ return;
2860
+ }
2861
+ this.selectChoice(null);
2862
+ this.clearPlaceholder();
2863
+ if (!this.container.hasClass("select2-container-active")) {
2864
+ this.opts.element.trigger($.Event("select2-focus"));
2865
+ }
2866
+ this.open();
2867
+ this.focusSearch();
2868
+ e.preventDefault();
2869
+ }));
2870
+
2871
+ this.container.on("focus", selector, this.bind(function () {
2872
+ if (!this.isInterfaceEnabled()) return;
2873
+ if (!this.container.hasClass("select2-container-active")) {
2874
+ this.opts.element.trigger($.Event("select2-focus"));
2875
+ }
2876
+ this.container.addClass("select2-container-active");
2877
+ this.dropdown.addClass("select2-drop-active");
2878
+ this.clearPlaceholder();
2879
+ }));
2880
+
2881
+ this.initContainerWidth();
2882
+ this.opts.element.hide();
2883
+
2884
+ // set the placeholder if necessary
2885
+ this.clearSearch();
2886
+ },
2887
+
2888
+ // multi
2889
+ enableInterface: function() {
2890
+ if (this.parent.enableInterface.apply(this, arguments)) {
2891
+ this.search.prop("disabled", !this.isInterfaceEnabled());
2892
+ }
2893
+ },
2894
+
2895
+ // multi
2896
+ initSelection: function () {
2897
+ var data;
2898
+ if (this.opts.element.val() === "" && this.opts.element.text() === "") {
2899
+ this.updateSelection([]);
2900
+ this.close();
2901
+ // set the placeholder if necessary
2902
+ this.clearSearch();
2903
+ }
2904
+ if (this.select || this.opts.element.val() !== "") {
2905
+ var self = this;
2906
+ this.opts.initSelection.call(null, this.opts.element, function(data){
2907
+ if (data !== undefined && data !== null) {
2908
+ self.updateSelection(data);
2909
+ self.close();
2910
+ // set the placeholder if necessary
2911
+ self.clearSearch();
2912
+ }
2913
+ });
2914
+ }
2915
+ },
2916
+
2917
+ // multi
2918
+ clearSearch: function () {
2919
+ var placeholder = this.getPlaceholder(),
2920
+ maxWidth = this.getMaxSearchWidth();
2921
+
2922
+ if (placeholder !== undefined && this.getVal().length === 0 && this.search.hasClass("select2-focused") === false) {
2923
+ this.search.val(placeholder).addClass("select2-default");
2924
+ // stretch the search box to full width of the container so as much of the placeholder is visible as possible
2925
+ // we could call this.resizeSearch(), but we do not because that requires a sizer and we do not want to create one so early because of a firefox bug, see #944
2926
+ this.search.width(maxWidth > 0 ? maxWidth : this.container.css("width"));
2927
+ } else {
2928
+ this.search.val("").width(10);
2929
+ }
2930
+ },
2931
+
2932
+ // multi
2933
+ clearPlaceholder: function () {
2934
+ if (this.search.hasClass("select2-default")) {
2935
+ this.search.val("").removeClass("select2-default");
2936
+ }
2937
+ },
2938
+
2939
+ // multi
2940
+ opening: function () {
2941
+ this.clearPlaceholder(); // should be done before super so placeholder is not used to search
2942
+ this.resizeSearch();
2943
+
2944
+ this.parent.opening.apply(this, arguments);
2945
+
2946
+ this.focusSearch();
2947
+
2948
+ this.prefillNextSearchTerm();
2949
+ this.updateResults(true);
2950
+
2951
+ if (this.opts.shouldFocusInput(this)) {
2952
+ this.search.focus();
2953
+ }
2954
+ this.opts.element.trigger($.Event("select2-open"));
2955
+ },
2956
+
2957
+ // multi
2958
+ close: function () {
2959
+ if (!this.opened()) return;
2960
+ this.parent.close.apply(this, arguments);
2961
+ },
2962
+
2963
+ // multi
2964
+ focus: function () {
2965
+ this.close();
2966
+ this.search.focus();
2967
+ },
2968
+
2969
+ // multi
2970
+ isFocused: function () {
2971
+ return this.search.hasClass("select2-focused");
2972
+ },
2973
+
2974
+ // multi
2975
+ updateSelection: function (data) {
2976
+ var ids = {}, filtered = [], self = this;
2977
+
2978
+ // filter out duplicates
2979
+ $(data).each(function () {
2980
+ if (!(self.id(this) in ids)) {
2981
+ ids[self.id(this)] = 0;
2982
+ filtered.push(this);
2983
+ }
2984
+ });
2985
+
2986
+ this.selection.find(".select2-search-choice").remove();
2987
+ this.addSelectedChoice(filtered);
2988
+ self.postprocessResults();
2989
+ },
2990
+
2991
+ // multi
2992
+ tokenize: function() {
2993
+ var input = this.search.val();
2994
+ input = this.opts.tokenizer.call(this, input, this.data(), this.bind(this.onSelect), this.opts);
2995
+ if (input != null && input != undefined) {
2996
+ this.search.val(input);
2997
+ if (input.length > 0) {
2998
+ this.open();
2999
+ }
3000
+ }
3001
+
3002
+ },
3003
+
3004
+ // multi
3005
+ onSelect: function (data, options) {
3006
+
3007
+ if (!this.triggerSelect(data) || data.text === "") { return; }
3008
+
3009
+ this.addSelectedChoice(data);
3010
+
3011
+ this.opts.element.trigger({ type: "selected", val: this.id(data), choice: data });
3012
+
3013
+ // keep track of the search's value before it gets cleared
3014
+ this.lastSearchTerm = this.search.val();
3015
+
3016
+ this.clearSearch();
3017
+ this.updateResults();
3018
+
3019
+ if (this.select || !this.opts.closeOnSelect) this.postprocessResults(data, false, this.opts.closeOnSelect===true);
3020
+
3021
+ if (this.opts.closeOnSelect) {
3022
+ this.close();
3023
+ this.search.width(10);
3024
+ } else {
3025
+ if (this.countSelectableResults()>0) {
3026
+ this.search.width(10);
3027
+ this.resizeSearch();
3028
+ if (this.getMaximumSelectionSize() > 0 && this.val().length >= this.getMaximumSelectionSize()) {
3029
+ // if we reached max selection size repaint the results so choices
3030
+ // are replaced with the max selection reached message
3031
+ this.updateResults(true);
3032
+ } else {
3033
+ // initializes search's value with nextSearchTerm and update search result
3034
+ if (this.prefillNextSearchTerm()) {
3035
+ this.updateResults();
3036
+ }
3037
+ }
3038
+ this.positionDropdown();
3039
+ } else {
3040
+ // if nothing left to select close
3041
+ this.close();
3042
+ this.search.width(10);
3043
+ }
3044
+ }
3045
+
3046
+ // since its not possible to select an element that has already been
3047
+ // added we do not need to check if this is a new element before firing change
3048
+ this.triggerChange({ added: data });
3049
+
3050
+ if (!options || !options.noFocus)
3051
+ this.focusSearch();
3052
+ },
3053
+
3054
+ // multi
3055
+ cancel: function () {
3056
+ this.close();
3057
+ this.focusSearch();
3058
+ },
3059
+
3060
+ addSelectedChoice: function (data) {
3061
+ var val = this.getVal(), self = this;
3062
+ $(data).each(function () {
3063
+ val.push(self.createChoice(this));
3064
+ });
3065
+ this.setVal(val);
3066
+ },
3067
+
3068
+ createChoice: function (data) {
3069
+ var enableChoice = !data.locked,
3070
+ enabledItem = $(
3071
+ "<li class='select2-search-choice'>" +
3072
+ " <div></div>" +
3073
+ " <a href='#' class='select2-search-choice-close' tabindex='-1'></a>" +
3074
+ "</li>"),
3075
+ disabledItem = $(
3076
+ "<li class='select2-search-choice select2-locked'>" +
3077
+ "<div></div>" +
3078
+ "</li>");
3079
+ var choice = enableChoice ? enabledItem : disabledItem,
3080
+ id = this.id(data),
3081
+ formatted,
3082
+ cssClass;
3083
+
3084
+ formatted=this.opts.formatSelection(data, choice.find("div"), this.opts.escapeMarkup);
3085
+ if (formatted != undefined) {
3086
+ choice.find("div").replaceWith($("<div></div>").html(formatted));
3087
+ }
3088
+ cssClass=this.opts.formatSelectionCssClass(data, choice.find("div"));
3089
+ if (cssClass != undefined) {
3090
+ choice.addClass(cssClass);
3091
+ }
3092
+
3093
+ if(enableChoice){
3094
+ choice.find(".select2-search-choice-close")
3095
+ .on("mousedown", killEvent)
3096
+ .on("click dblclick", this.bind(function (e) {
3097
+ if (!this.isInterfaceEnabled()) return;
3098
+
3099
+ this.unselect($(e.target));
3100
+ this.selection.find(".select2-search-choice-focus").removeClass("select2-search-choice-focus");
3101
+ killEvent(e);
3102
+ this.close();
3103
+ this.focusSearch();
3104
+ })).on("focus", this.bind(function () {
3105
+ if (!this.isInterfaceEnabled()) return;
3106
+ this.container.addClass("select2-container-active");
3107
+ this.dropdown.addClass("select2-drop-active");
3108
+ }));
3109
+ }
3110
+
3111
+ choice.data("select2-data", data);
3112
+ choice.insertBefore(this.searchContainer);
3113
+
3114
+ return id;
3115
+ },
3116
+
3117
+ // multi
3118
+ unselect: function (selected) {
3119
+ var val = this.getVal(),
3120
+ data,
3121
+ index;
3122
+ selected = selected.closest(".select2-search-choice");
3123
+
3124
+ if (selected.length === 0) {
3125
+ throw "Invalid argument: " + selected + ". Must be .select2-search-choice";
3126
+ }
3127
+
3128
+ data = selected.data("select2-data");
3129
+
3130
+ if (!data) {
3131
+ // prevent a race condition when the 'x' is clicked really fast repeatedly the event can be queued
3132
+ // and invoked on an element already removed
3133
+ return;
3134
+ }
3135
+
3136
+ var evt = $.Event("select2-removing");
3137
+ evt.val = this.id(data);
3138
+ evt.choice = data;
3139
+ this.opts.element.trigger(evt);
3140
+
3141
+ if (evt.isDefaultPrevented()) {
3142
+ return false;
3143
+ }
3144
+
3145
+ while((index = indexOf(this.id(data), val)) >= 0) {
3146
+ val.splice(index, 1);
3147
+ this.setVal(val);
3148
+ if (this.select) this.postprocessResults();
3149
+ }
3150
+
3151
+ selected.remove();
3152
+
3153
+ this.opts.element.trigger({ type: "select2-removed", val: this.id(data), choice: data });
3154
+ this.triggerChange({ removed: data });
3155
+
3156
+ return true;
3157
+ },
3158
+
3159
+ // multi
3160
+ postprocessResults: function (data, initial, noHighlightUpdate) {
3161
+ var val = this.getVal(),
3162
+ choices = this.results.find(".select2-result"),
3163
+ compound = this.results.find(".select2-result-with-children"),
3164
+ self = this;
3165
+
3166
+ choices.each2(function (i, choice) {
3167
+ var id = self.id(choice.data("select2-data"));
3168
+ if (indexOf(id, val) >= 0) {
3169
+ choice.addClass("select2-selected");
3170
+ // mark all children of the selected parent as selected
3171
+ choice.find(".select2-result-selectable").addClass("select2-selected");
3172
+ }
3173
+ });
3174
+
3175
+ compound.each2(function(i, choice) {
3176
+ // hide an optgroup if it doesn't have any selectable children
3177
+ if (!choice.is('.select2-result-selectable')
3178
+ && choice.find(".select2-result-selectable:not(.select2-selected)").length === 0) {
3179
+ choice.addClass("select2-selected");
3180
+ }
3181
+ });
3182
+
3183
+ if (this.highlight() == -1 && noHighlightUpdate !== false && this.opts.closeOnSelect === true){
3184
+ self.highlight(0);
3185
+ }
3186
+
3187
+ //If all results are chosen render formatNoMatches
3188
+ if(!this.opts.createSearchChoice && !choices.filter('.select2-result:not(.select2-selected)').length > 0){
3189
+ if(!data || data && !data.more && this.results.find(".select2-no-results").length === 0) {
3190
+ if (checkFormatter(self.opts.formatNoMatches, "formatNoMatches")) {
3191
+ this.results.append("<li class='select2-no-results'>" + evaluate(self.opts.formatNoMatches, self.opts.element, self.search.val()) + "</li>");
3192
+ }
3193
+ }
3194
+ }
3195
+
3196
+ },
3197
+
3198
+ // multi
3199
+ getMaxSearchWidth: function() {
3200
+ return this.selection.width() - getSideBorderPadding(this.search);
3201
+ },
3202
+
3203
+ // multi
3204
+ resizeSearch: function () {
3205
+ var minimumWidth, left, maxWidth, containerLeft, searchWidth,
3206
+ sideBorderPadding = getSideBorderPadding(this.search);
3207
+
3208
+ minimumWidth = measureTextWidth(this.search) + 10;
3209
+
3210
+ left = this.search.offset().left;
3211
+
3212
+ maxWidth = this.selection.width();
3213
+ containerLeft = this.selection.offset().left;
3214
+
3215
+ searchWidth = maxWidth - (left - containerLeft) - sideBorderPadding;
3216
+
3217
+ if (searchWidth < minimumWidth) {
3218
+ searchWidth = maxWidth - sideBorderPadding;
3219
+ }
3220
+
3221
+ if (searchWidth < 40) {
3222
+ searchWidth = maxWidth - sideBorderPadding;
3223
+ }
3224
+
3225
+ if (searchWidth <= 0) {
3226
+ searchWidth = minimumWidth;
3227
+ }
3228
+
3229
+ this.search.width(Math.floor(searchWidth));
3230
+ },
3231
+
3232
+ // multi
3233
+ getVal: function () {
3234
+ var val;
3235
+ if (this.select) {
3236
+ val = this.select.val();
3237
+ return val === null ? [] : val;
3238
+ } else {
3239
+ val = this.opts.element.val();
3240
+ return splitVal(val, this.opts.separator, this.opts.transformVal);
3241
+ }
3242
+ },
3243
+
3244
+ // multi
3245
+ setVal: function (val) {
3246
+ if (this.select) {
3247
+ this.select.val(val);
3248
+ } else {
3249
+ var unique = [], valMap = {};
3250
+ // filter out duplicates
3251
+ $(val).each(function () {
3252
+ if (!(this in valMap)) {
3253
+ unique.push(this);
3254
+ valMap[this] = 0;
3255
+ }
3256
+ });
3257
+ this.opts.element.val(unique.length === 0 ? "" : unique.join(this.opts.separator));
3258
+ }
3259
+ },
3260
+
3261
+ // multi
3262
+ buildChangeDetails: function (old, current) {
3263
+ var current = current.slice(0),
3264
+ old = old.slice(0);
3265
+
3266
+ // remove intersection from each array
3267
+ for (var i = 0; i < current.length; i++) {
3268
+ for (var j = 0; j < old.length; j++) {
3269
+ if (equal(this.opts.id(current[i]), this.opts.id(old[j]))) {
3270
+ current.splice(i, 1);
3271
+ i--;
3272
+ old.splice(j, 1);
3273
+ break;
3274
+ }
3275
+ }
3276
+ }
3277
+
3278
+ return {added: current, removed: old};
3279
+ },
3280
+
3281
+
3282
+ // multi
3283
+ val: function (val, triggerChange) {
3284
+ var oldData, self=this;
3285
+
3286
+ if (arguments.length === 0) {
3287
+ return this.getVal();
3288
+ }
3289
+
3290
+ oldData=this.data();
3291
+ if (!oldData.length) oldData=[];
3292
+
3293
+ // val is an id. !val is true for [undefined,null,'',0] - 0 is legal
3294
+ if (!val && val !== 0) {
3295
+ this.opts.element.val("");
3296
+ this.updateSelection([]);
3297
+ this.clearSearch();
3298
+ if (triggerChange) {
3299
+ this.triggerChange({added: this.data(), removed: oldData});
3300
+ }
3301
+ return;
3302
+ }
3303
+
3304
+ // val is a list of ids
3305
+ this.setVal(val);
3306
+
3307
+ if (this.select) {
3308
+ this.opts.initSelection(this.select, this.bind(this.updateSelection));
3309
+ if (triggerChange) {
3310
+ this.triggerChange(this.buildChangeDetails(oldData, this.data()));
3311
+ }
3312
+ } else {
3313
+ if (this.opts.initSelection === undefined) {
3314
+ throw new Error("val() cannot be called if initSelection() is not defined");
3315
+ }
3316
+
3317
+ this.opts.initSelection(this.opts.element, function(data){
3318
+ var ids=$.map(data, self.id);
3319
+ self.setVal(ids);
3320
+ self.updateSelection(data);
3321
+ self.clearSearch();
3322
+ if (triggerChange) {
3323
+ self.triggerChange(self.buildChangeDetails(oldData, self.data()));
3324
+ }
3325
+ });
3326
+ }
3327
+ this.clearSearch();
3328
+ },
3329
+
3330
+ // multi
3331
+ onSortStart: function() {
3332
+ if (this.select) {
3333
+ throw new Error("Sorting of elements is not supported when attached to <select>. Attach to <input type='hidden'/> instead.");
3334
+ }
3335
+
3336
+ // collapse search field into 0 width so its container can be collapsed as well
3337
+ this.search.width(0);
3338
+ // hide the container
3339
+ this.searchContainer.hide();
3340
+ },
3341
+
3342
+ // multi
3343
+ onSortEnd:function() {
3344
+
3345
+ var val=[], self=this;
3346
+
3347
+ // show search and move it to the end of the list
3348
+ this.searchContainer.show();
3349
+ // make sure the search container is the last item in the list
3350
+ this.searchContainer.appendTo(this.searchContainer.parent());
3351
+ // since we collapsed the width in dragStarted, we resize it here
3352
+ this.resizeSearch();
3353
+
3354
+ // update selection
3355
+ this.selection.find(".select2-search-choice").each(function() {
3356
+ val.push(self.opts.id($(this).data("select2-data")));
3357
+ });
3358
+ this.setVal(val);
3359
+ this.triggerChange();
3360
+ },
3361
+
3362
+ // multi
3363
+ data: function(values, triggerChange) {
3364
+ var self=this, ids, old;
3365
+ if (arguments.length === 0) {
3366
+ return this.selection
3367
+ .children(".select2-search-choice")
3368
+ .map(function() { return $(this).data("select2-data"); })
3369
+ .get();
3370
+ } else {
3371
+ old = this.data();
3372
+ if (!values) { values = []; }
3373
+ ids = $.map(values, function(e) { return self.opts.id(e); });
3374
+ this.setVal(ids);
3375
+ this.updateSelection(values);
3376
+ this.clearSearch();
3377
+ if (triggerChange) {
3378
+ this.triggerChange(this.buildChangeDetails(old, this.data()));
3379
+ }
3380
+ }
3381
+ }
3382
+ });
3383
+
3384
+ $.fn.select2 = function () {
3385
+
3386
+ var args = Array.prototype.slice.call(arguments, 0),
3387
+ opts,
3388
+ select2,
3389
+ method, value, multiple,
3390
+ allowedMethods = ["val", "destroy", "opened", "open", "close", "focus", "isFocused", "container", "dropdown", "onSortStart", "onSortEnd", "enable", "disable", "readonly", "positionDropdown", "data", "search"],
3391
+ valueMethods = ["opened", "isFocused", "container", "dropdown"],
3392
+ propertyMethods = ["val", "data"],
3393
+ methodsMap = { search: "externalSearch" };
3394
+
3395
+ this.each(function () {
3396
+ if (args.length === 0 || typeof(args[0]) === "object") {
3397
+ opts = args.length === 0 ? {} : $.extend({}, args[0]);
3398
+ opts.element = $(this);
3399
+
3400
+ if (opts.element.get(0).tagName.toLowerCase() === "select") {
3401
+ multiple = opts.element.prop("multiple");
3402
+ } else {
3403
+ multiple = opts.multiple || false;
3404
+ if ("tags" in opts) {opts.multiple = multiple = true;}
3405
+ }
3406
+
3407
+ select2 = multiple ? new window.Select2["class"].multi() : new window.Select2["class"].single();
3408
+ select2.init(opts);
3409
+ } else if (typeof(args[0]) === "string") {
3410
+
3411
+ if (indexOf(args[0], allowedMethods) < 0) {
3412
+ throw "Unknown method: " + args[0];
3413
+ }
3414
+
3415
+ value = undefined;
3416
+ select2 = $(this).data("select2");
3417
+ if (select2 === undefined) return;
3418
+
3419
+ method=args[0];
3420
+
3421
+ if (method === "container") {
3422
+ value = select2.container;
3423
+ } else if (method === "dropdown") {
3424
+ value = select2.dropdown;
3425
+ } else {
3426
+ if (methodsMap[method]) method = methodsMap[method];
3427
+
3428
+ value = select2[method].apply(select2, args.slice(1));
3429
+ }
3430
+ if (indexOf(args[0], valueMethods) >= 0
3431
+ || (indexOf(args[0], propertyMethods) >= 0 && args.length == 1)) {
3432
+ return false; // abort the iteration, ready to return first matched value
3433
+ }
3434
+ } else {
3435
+ throw "Invalid arguments to select2 plugin: " + args;
3436
+ }
3437
+ });
3438
+ return (value === undefined) ? this : value;
3439
+ };
3440
+
3441
+ // plugin defaults, accessible to users
3442
+ $.fn.select2.defaults = {
3443
+ width: "copy",
3444
+ loadMorePadding: 0,
3445
+ closeOnSelect: true,
3446
+ openOnEnter: true,
3447
+ containerCss: {},
3448
+ dropdownCss: {},
3449
+ containerCssClass: "",
3450
+ dropdownCssClass: "",
3451
+ formatResult: function(result, container, query, escapeMarkup) {
3452
+ var markup=[];
3453
+ markMatch(this.text(result), query.term, markup, escapeMarkup);
3454
+ return markup.join("");
3455
+ },
3456
+ transformVal: function(val) {
3457
+ return $.trim(val);
3458
+ },
3459
+ formatSelection: function (data, container, escapeMarkup) {
3460
+ return data ? escapeMarkup(this.text(data)) : undefined;
3461
+ },
3462
+ sortResults: function (results, container, query) {
3463
+ return results;
3464
+ },
3465
+ formatResultCssClass: function(data) {return data.css;},
3466
+ formatSelectionCssClass: function(data, container) {return undefined;},
3467
+ minimumResultsForSearch: 0,
3468
+ minimumInputLength: 0,
3469
+ maximumInputLength: null,
3470
+ maximumSelectionSize: 0,
3471
+ id: function (e) { return e == undefined ? null : e.id; },
3472
+ text: function (e) {
3473
+ if (e && this.data && this.data.text) {
3474
+ if ($.isFunction(this.data.text)) {
3475
+ return this.data.text(e);
3476
+ } else {
3477
+ return e[this.data.text];
3478
+ }
3479
+ } else {
3480
+ return e.text;
3481
+ }
3482
+ },
3483
+ matcher: function(term, text) {
3484
+ return stripDiacritics(''+text).toUpperCase().indexOf(stripDiacritics(''+term).toUpperCase()) >= 0;
3485
+ },
3486
+ separator: ",",
3487
+ tokenSeparators: [],
3488
+ tokenizer: defaultTokenizer,
3489
+ escapeMarkup: defaultEscapeMarkup,
3490
+ blurOnChange: false,
3491
+ selectOnBlur: false,
3492
+ adaptContainerCssClass: function(c) { return c; },
3493
+ adaptDropdownCssClass: function(c) { return null; },
3494
+ nextSearchTerm: function(selectedObject, currentSearchTerm) { return undefined; },
3495
+ searchInputPlaceholder: '',
3496
+ createSearchChoicePosition: 'top',
3497
+ shouldFocusInput: function (instance) {
3498
+ // Attempt to detect touch devices
3499
+ var supportsTouchEvents = (('ontouchstart' in window) ||
3500
+ (navigator.msMaxTouchPoints > 0));
3501
+
3502
+ // Only devices which support touch events should be special cased
3503
+ if (!supportsTouchEvents) {
3504
+ return true;
3505
+ }
3506
+
3507
+ // Never focus the input if search is disabled
3508
+ if (instance.opts.minimumResultsForSearch < 0) {
3509
+ return false;
3510
+ }
3511
+
3512
+ return true;
3513
+ }
3514
+ };
3515
+
3516
+ $.fn.select2.locales = [];
3517
+
3518
+ $.fn.select2.locales['en'] = {
3519
+ formatMatches: function (matches) { if (matches === 1) { return "One result is available, press enter to select it."; } return matches + " results are available, use up and down arrow keys to navigate."; },
3520
+ formatNoMatches: function () { return "No matches found"; },
3521
+ formatAjaxError: function (jqXHR, textStatus, errorThrown) { return "Loading failed"; },
3522
+ formatInputTooShort: function (input, min) { var n = min - input.length; return "Please enter " + n + " or more character" + (n == 1 ? "" : "s"); },
3523
+ formatInputTooLong: function (input, max) { var n = input.length - max; return "Please delete " + n + " character" + (n == 1 ? "" : "s"); },
3524
+ formatSelectionTooBig: function (limit) { return "You can only select " + limit + " item" + (limit == 1 ? "" : "s"); },
3525
+ formatLoadMore: function (pageNumber) { return "Loading more results…"; },
3526
+ formatSearching: function () { return "Searching…"; }
3527
+ };
3528
+
3529
+ $.extend($.fn.select2.defaults, $.fn.select2.locales['en']);
3530
+
3531
+ $.fn.select2.ajaxDefaults = {
3532
+ transport: $.ajax,
3533
+ params: {
3534
+ type: "GET",
3535
+ cache: false,
3536
+ dataType: "json"
3537
+ }
3538
+ };
3539
+
3540
+ // exports
3541
+ window.Select2 = {
3542
+ query: {
3543
+ ajax: ajax,
3544
+ local: local,
3545
+ tags: tags
3546
+ }, util: {
3547
+ debounce: debounce,
3548
+ markMatch: markMatch,
3549
+ escapeMarkup: defaultEscapeMarkup,
3550
+ stripDiacritics: stripDiacritics
3551
+ }, "class": {
3552
+ "abstract": AbstractSelect2,
3553
+ "single": SingleSelect2,
3554
+ "multi": MultiSelect2
3555
+ }
3556
+ };
3557
+
3558
+ }(jQuery));
ui/lib/select2/select2.png ADDED
Binary file
ui/lib/select2/select2_locale_ar.js ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Select2 Arabic translation.
3
+ *
4
+ * Author: Adel KEDJOUR <adel@kedjour.com>
5
+ */
6
+ (function ($) {
7
+ "use strict";
8
+
9
+ $.fn.select2.locales['ar'] = {
10
+ formatNoMatches: function () { return "لم يتم العثور على مطابقات"; },
11
+ formatInputTooShort: function (input, min) { var n = min - input.length; if (n == 1){ return "الرجاء إدخال حرف واحد على الأكثر"; } return n == 2 ? "الرجاء إدخال حرفين على الأكثر" : "الرجاء إدخال " + n + " على الأكثر"; },
12
+ formatInputTooLong: function (input, max) { var n = input.length - max; if (n == 1){ return "الرجاء إدخال حرف واحد على الأقل"; } return n == 2 ? "الرجاء إدخال حرفين على الأقل" : "الرجاء إدخال " + n + " على الأقل "; },
13
+ formatSelectionTooBig: function (limit) { if (limit == 1){ return "يمكنك أن تختار إختيار واحد فقط"; } return limit == 2 ? "يمكنك أن تختار إختيارين فقط" : "يمكنك أن تختار " + limit + " إختيارات فقط"; },
14
+ formatLoadMore: function (pageNumber) { return "تحميل المزيد من النتائج…"; },
15
+ formatSearching: function () { return "البحث…"; }
16
+ };
17
+
18
+ $.extend($.fn.select2.defaults, $.fn.select2.locales['ar']);
19
+ })(jQuery);
ui/lib/select2/select2_locale_az.js ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Select2 Azerbaijani translation.
3
+ *
4
+ * Author: Farhad Safarov <farhad.safarov@gmail.com>
5
+ */
6
+ (function ($) {
7
+ "use strict";
8
+
9
+ $.fn.select2.locales['az'] = {
10
+ formatMatches: function (matches) { return matches + " nəticə mövcuddur, hərəkət etdirmək üçün yuxarı və aşağı düymələrindən istifadə edin."; },
11
+ formatNoMatches: function () { return "Nəticə tapılmadı"; },
12
+ formatInputTooShort: function