├── .github
└── workflows
│ └── php-qa.yml
├── CHANGELOG.md
├── LICENSE
├── composer.json
├── phpunit.xml.dist
├── src
├── AdminNotices.php
├── Bootstrap.php
├── BoxAction.php
├── BoxInfo.php
├── BoxView.php
├── Boxes.php
├── Entity.php
├── Metabox.php
├── MetaboxAuth.php
├── NoopBoxAction.php
├── NoopBoxView.php
├── PostMetabox.php
├── PostMetaboxAuth.php
├── TermMetabox.php
└── TermMetaboxAuth.php
└── tests
├── boot.php
└── src
├── AdminNoticesTest.php
├── BootstrapTest.php
├── BoxInfoTest.php
├── NoopBoxActionTest.php
├── NoopBoxViewTest.php
├── PostMetaboxAuthTest.php
├── TermMetaboxAuthTest.php
└── TestCase.php
/.github/workflows/php-qa.yml:
--------------------------------------------------------------------------------
1 | name: PHP-QA
2 |
3 | on: [ push ]
4 |
5 | jobs:
6 | lint-php:
7 | strategy:
8 | matrix:
9 | php: ["7.2", "7.3", "7.4", "8.0", "8.1", "8.2"]
10 | uses: inpsyde/reusable-workflows/.github/workflows/lint-php.yml@main
11 | with:
12 | PHP_VERSION: ${{ matrix.php }}
13 | tests-unit-php:
14 | strategy:
15 | matrix:
16 | php: ["7.2", "7.3", "7.4", "8.0", "8.1", "8.2"]
17 | uses: inpsyde/reusable-workflows/.github/workflows/tests-unit-php.yml@main
18 | secrets:
19 | COMPOSER_AUTH_JSON: ${{ secrets.PACKAGIST_AUTH_JSON }}
20 | with:
21 | PHP_VERSION: ${{ matrix.php }}
22 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## 0.4.4
4 | - Fix PHP 8.1 warnings.
5 |
6 | ## 0.4.3
7 | - Fix: terms MetaBoxes is printing twice in case other plugins use same code structure.
8 |
9 | ## 0.4.2
10 | - Fix: metaBox not saved when other plugins switching between blogs
11 |
12 | ## 0.4.1
13 | - Fix bug of post boxes not saving on new posts.
14 |
15 | ## 0.4.0
16 | - Save post boxes even if post has empty content and WordPress will not proceed in save it (so `"wp_insert_post"` is not triggered).
17 | - Introduce `"metabox-orchestra.save-on-empty-post"` hook to add the possibility to disallow box saving if content is empty.
18 |
19 | ## 0.3.4
20 | - Added `Entity::id()` to obtain a type-safe id of wrapped entity and mark entity as not valid if its id is <= 0.
21 |
22 | ## 0.3.3
23 | - Added three new hooks: before showing boxes and before and after saving them.
24 | - Improved `Entity` object constructor, now accepts an instance of another `Entity`.
25 |
26 | ## 0.3.2
27 | - Prevent recursion when editing post/term object form inside a `BoxAction::save()` method.
28 |
29 | ## 0.3.1
30 | - Fix a bug in Boxes class.
31 | - Update README code sample.
32 |
33 | ## 0.3.0
34 | - Introduce `Entity` object to wrap supported objects for metaboxes (currently `\WP_Post` ad `\WP_Term`)
35 | - **[BREAKING]** `Metabox::create_info()` new requires two arguments, the second being current `Entity`.
36 |
37 | ## 0.2.0
38 | - Added `.travis.yml`
39 | - Added more links to README.
40 | - Added more tests.
41 |
42 | ## 0.1.1
43 | - Minor, no logic change to AdminNotices
44 | - Introduce unit tests
45 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | ### GNU GENERAL PUBLIC LICENSE
2 |
3 | Version 2, June 1991
4 |
5 | Copyright (C) 1989, 1991 Free Software Foundation, Inc.
6 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
7 |
8 | Everyone is permitted to copy and distribute verbatim copies
9 | of this license document, but changing it is not allowed.
10 |
11 | ### Preamble
12 |
13 | The licenses for most software are designed to take away your freedom
14 | to share and change it. By contrast, the GNU General Public License is
15 | intended to guarantee your freedom to share and change free
16 | software--to make sure the software is free for all its users. This
17 | General Public License applies to most of the Free Software
18 | Foundation's software and to any other program whose authors commit to
19 | using it. (Some other Free Software Foundation software is covered by
20 | the GNU Lesser General Public License instead.) You can apply it to
21 | your programs, too.
22 |
23 | When we speak of free software, we are referring to freedom, not
24 | price. Our General Public Licenses are designed to make sure that you
25 | have the freedom to distribute copies of free software (and charge for
26 | this service if you wish), that you receive source code or can get it
27 | if you want it, that you can change the software or use pieces of it
28 | in new free programs; and that you know you can do these things.
29 |
30 | To protect your rights, we need to make restrictions that forbid
31 | anyone to deny you these rights or to ask you to surrender the rights.
32 | These restrictions translate to certain responsibilities for you if
33 | you distribute copies of the software, or if you modify it.
34 |
35 | For example, if you distribute copies of such a program, whether
36 | gratis or for a fee, you must give the recipients all the rights that
37 | you have. You must make sure that they, too, receive or can get the
38 | source code. And you must show them these terms so they know their
39 | rights.
40 |
41 | We protect your rights with two steps: (1) copyright the software, and
42 | (2) offer you this license which gives you legal permission to copy,
43 | distribute and/or modify the software.
44 |
45 | Also, for each author's protection and ours, we want to make certain
46 | that everyone understands that there is no warranty for this free
47 | software. If the software is modified by someone else and passed on,
48 | we want its recipients to know that what they have is not the
49 | original, so that any problems introduced by others will not reflect
50 | on the original authors' reputations.
51 |
52 | Finally, any free program is threatened constantly by software
53 | patents. We wish to avoid the danger that redistributors of a free
54 | program will individually obtain patent licenses, in effect making the
55 | program proprietary. To prevent this, we have made it clear that any
56 | patent must be licensed for everyone's free use or not licensed at
57 | all.
58 |
59 | The precise terms and conditions for copying, distribution and
60 | modification follow.
61 |
62 | ### TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
63 |
64 | **0.** This License applies to any program or other work which
65 | contains a notice placed by the copyright holder saying it may be
66 | distributed under the terms of this General Public License. The
67 | "Program", below, refers to any such program or work, and a "work
68 | based on the Program" means either the Program or any derivative work
69 | under copyright law: that is to say, a work containing the Program or
70 | a portion of it, either verbatim or with modifications and/or
71 | translated into another language. (Hereinafter, translation is
72 | included without limitation in the term "modification".) Each licensee
73 | is addressed as "you".
74 |
75 | Activities other than copying, distribution and modification are not
76 | covered by this License; they are outside its scope. The act of
77 | running the Program is not restricted, and the output from the Program
78 | is covered only if its contents constitute a work based on the Program
79 | (independent of having been made by running the Program). Whether that
80 | is true depends on what the Program does.
81 |
82 | **1.** You may copy and distribute verbatim copies of the Program's
83 | source code as you receive it, in any medium, provided that you
84 | conspicuously and appropriately publish on each copy an appropriate
85 | copyright notice and disclaimer of warranty; keep intact all the
86 | notices that refer to this License and to the absence of any warranty;
87 | and give any other recipients of the Program a copy of this License
88 | along with the Program.
89 |
90 | You may charge a fee for the physical act of transferring a copy, and
91 | you may at your option offer warranty protection in exchange for a
92 | fee.
93 |
94 | **2.** You may modify your copy or copies of the Program or any
95 | portion of it, thus forming a work based on the Program, and copy and
96 | distribute such modifications or work under the terms of Section 1
97 | above, provided that you also meet all of these conditions:
98 |
99 |
100 | **a)** You must cause the modified files to carry prominent notices
101 | stating that you changed the files and the date of any change.
102 |
103 |
104 | **b)** You must cause any work that you distribute or publish, that in
105 | whole or in part contains or is derived from the Program or any part
106 | thereof, to be licensed as a whole at no charge to all third parties
107 | under the terms of this License.
108 |
109 |
110 | **c)** If the modified program normally reads commands interactively
111 | when run, you must cause it, when started running for such interactive
112 | use in the most ordinary way, to print or display an announcement
113 | including an appropriate copyright notice and a notice that there is
114 | no warranty (or else, saying that you provide a warranty) and that
115 | users may redistribute the program under these conditions, and telling
116 | the user how to view a copy of this License. (Exception: if the
117 | Program itself is interactive but does not normally print such an
118 | announcement, your work based on the Program is not required to print
119 | an announcement.)
120 |
121 | These requirements apply to the modified work as a whole. If
122 | identifiable sections of that work are not derived from the Program,
123 | and can be reasonably considered independent and separate works in
124 | themselves, then this License, and its terms, do not apply to those
125 | sections when you distribute them as separate works. But when you
126 | distribute the same sections as part of a whole which is a work based
127 | on the Program, the distribution of the whole must be on the terms of
128 | this License, whose permissions for other licensees extend to the
129 | entire whole, and thus to each and every part regardless of who wrote
130 | it.
131 |
132 | Thus, it is not the intent of this section to claim rights or contest
133 | your rights to work written entirely by you; rather, the intent is to
134 | exercise the right to control the distribution of derivative or
135 | collective works based on the Program.
136 |
137 | In addition, mere aggregation of another work not based on the Program
138 | with the Program (or with a work based on the Program) on a volume of
139 | a storage or distribution medium does not bring the other work under
140 | the scope of this License.
141 |
142 | **3.** You may copy and distribute the Program (or a work based on it,
143 | under Section 2) in object code or executable form under the terms of
144 | Sections 1 and 2 above provided that you also do one of the following:
145 |
146 |
147 | **a)** Accompany it with the complete corresponding machine-readable
148 | source code, which must be distributed under the terms of Sections 1
149 | and 2 above on a medium customarily used for software interchange; or,
150 |
151 |
152 | **b)** Accompany it with a written offer, valid for at least three
153 | years, to give any third party, for a charge no more than your cost of
154 | physically performing source distribution, a complete machine-readable
155 | copy of the corresponding source code, to be distributed under the
156 | terms of Sections 1 and 2 above on a medium customarily used for
157 | software interchange; or,
158 |
159 |
160 | **c)** Accompany it with the information you received as to the offer
161 | to distribute corresponding source code. (This alternative is allowed
162 | only for noncommercial distribution and only if you received the
163 | program in object code or executable form with such an offer, in
164 | accord with Subsection b above.)
165 |
166 | The source code for a work means the preferred form of the work for
167 | making modifications to it. For an executable work, complete source
168 | code means all the source code for all modules it contains, plus any
169 | associated interface definition files, plus the scripts used to
170 | control compilation and installation of the executable. However, as a
171 | special exception, the source code distributed need not include
172 | anything that is normally distributed (in either source or binary
173 | form) with the major components (compiler, kernel, and so on) of the
174 | operating system on which the executable runs, unless that component
175 | itself accompanies the executable.
176 |
177 | If distribution of executable or object code is made by offering
178 | access to copy from a designated place, then offering equivalent
179 | access to copy the source code from the same place counts as
180 | distribution of the source code, even though third parties are not
181 | compelled to copy the source along with the object code.
182 |
183 | **4.** You may not copy, modify, sublicense, or distribute the Program
184 | except as expressly provided under this License. Any attempt otherwise
185 | to copy, modify, sublicense or distribute the Program is void, and
186 | will automatically terminate your rights under this License. However,
187 | parties who have received copies, or rights, from you under this
188 | License will not have their licenses terminated so long as such
189 | parties remain in full compliance.
190 |
191 | **5.** You are not required to accept this License, since you have not
192 | signed it. However, nothing else grants you permission to modify or
193 | distribute the Program or its derivative works. These actions are
194 | prohibited by law if you do not accept this License. Therefore, by
195 | modifying or distributing the Program (or any work based on the
196 | Program), you indicate your acceptance of this License to do so, and
197 | all its terms and conditions for copying, distributing or modifying
198 | the Program or works based on it.
199 |
200 | **6.** Each time you redistribute the Program (or any work based on
201 | the Program), the recipient automatically receives a license from the
202 | original licensor to copy, distribute or modify the Program subject to
203 | these terms and conditions. You may not impose any further
204 | restrictions on the recipients' exercise of the rights granted herein.
205 | You are not responsible for enforcing compliance by third parties to
206 | this License.
207 |
208 | **7.** If, as a consequence of a court judgment or allegation of
209 | patent infringement or for any other reason (not limited to patent
210 | issues), conditions are imposed on you (whether by court order,
211 | agreement or otherwise) that contradict the conditions of this
212 | License, they do not excuse you from the conditions of this License.
213 | If you cannot distribute so as to satisfy simultaneously your
214 | obligations under this License and any other pertinent obligations,
215 | then as a consequence you may not distribute the Program at all. For
216 | example, if a patent license would not permit royalty-free
217 | redistribution of the Program by all those who receive copies directly
218 | or indirectly through you, then the only way you could satisfy both it
219 | and this License would be to refrain entirely from distribution of the
220 | Program.
221 |
222 | If any portion of this section is held invalid or unenforceable under
223 | any particular circumstance, the balance of the section is intended to
224 | apply and the section as a whole is intended to apply in other
225 | circumstances.
226 |
227 | It is not the purpose of this section to induce you to infringe any
228 | patents or other property right claims or to contest validity of any
229 | such claims; this section has the sole purpose of protecting the
230 | integrity of the free software distribution system, which is
231 | implemented by public license practices. Many people have made
232 | generous contributions to the wide range of software distributed
233 | through that system in reliance on consistent application of that
234 | system; it is up to the author/donor to decide if he or she is willing
235 | to distribute software through any other system and a licensee cannot
236 | impose that choice.
237 |
238 | This section is intended to make thoroughly clear what is believed to
239 | be a consequence of the rest of this License.
240 |
241 | **8.** If the distribution and/or use of the Program is restricted in
242 | certain countries either by patents or by copyrighted interfaces, the
243 | original copyright holder who places the Program under this License
244 | may add an explicit geographical distribution limitation excluding
245 | those countries, so that distribution is permitted only in or among
246 | countries not thus excluded. In such case, this License incorporates
247 | the limitation as if written in the body of this License.
248 |
249 | **9.** The Free Software Foundation may publish revised and/or new
250 | versions of the General Public License from time to time. Such new
251 | versions will be similar in spirit to the present version, but may
252 | differ in detail to address new problems or concerns.
253 |
254 | Each version is given a distinguishing version number. If the Program
255 | specifies a version number of this License which applies to it and
256 | "any later version", you have the option of following the terms and
257 | conditions either of that version or of any later version published by
258 | the Free Software Foundation. If the Program does not specify a
259 | version number of this License, you may choose any version ever
260 | published by the Free Software Foundation.
261 |
262 | **10.** If you wish to incorporate parts of the Program into other
263 | free programs whose distribution conditions are different, write to
264 | the author to ask for permission. For software which is copyrighted by
265 | the Free Software Foundation, write to the Free Software Foundation;
266 | we sometimes make exceptions for this. Our decision will be guided by
267 | the two goals of preserving the free status of all derivatives of our
268 | free software and of promoting the sharing and reuse of software
269 | generally.
270 |
271 | **NO WARRANTY**
272 |
273 | **11.** BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO
274 | WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
275 | EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
276 | OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY
277 | KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
278 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
279 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
280 | PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME
281 | THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
282 |
283 | **12.** IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
284 | WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
285 | AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU
286 | FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
287 | CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
288 | PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
289 | RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
290 | FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF
291 | SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
292 | DAMAGES.
293 |
294 | ### END OF TERMS AND CONDITIONS
295 |
296 | ### How to Apply These Terms to Your New Programs
297 |
298 | If you develop a new program, and you want it to be of the greatest
299 | possible use to the public, the best way to achieve this is to make it
300 | free software which everyone can redistribute and change under these
301 | terms.
302 |
303 | To do so, attach the following notices to the program. It is safest to
304 | attach them to the start of each source file to most effectively
305 | convey the exclusion of warranty; and each file should have at least
306 | the "copyright" line and a pointer to where the full notice is found.
307 |
308 | one line to give the program's name and an idea of what it does.
309 | Copyright (C) 2020 Inpsyde GmbH
310 |
311 | This program is free software; you can redistribute it and/or
312 | modify it under the terms of the GNU General Public License
313 | as published by the Free Software Foundation; either version 2
314 | of the License, or (at your option) any later version.
315 |
316 | This program is distributed in the hope that it will be useful,
317 | but WITHOUT ANY WARRANTY; without even the implied warranty of
318 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
319 | GNU General Public License for more details.
320 |
321 | You should have received a copy of the GNU General Public License
322 | along with this program; if not, write to the Free Software
323 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
324 |
325 | Also add information on how to contact you by electronic and paper
326 | mail.
327 |
328 | If the program is interactive, make it output a short notice like this
329 | when it starts in an interactive mode:
330 |
331 | Gnomovision version 69, Copyright (C) year name of author
332 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details
333 | type `show w'. This is free software, and you are welcome
334 | to redistribute it under certain conditions; type `show c'
335 | for details.
336 |
337 | The hypothetical commands \`show w' and \`show c' should show the
338 | appropriate parts of the General Public License. Of course, the
339 | commands you use may be called something other than \`show w' and
340 | \`show c'; they could even be mouse-clicks or menu items--whatever
341 | suits your program.
342 |
343 | You should also get your employer (if you work as a programmer) or
344 | your school, if any, to sign a "copyright disclaimer" for the program,
345 | if necessary. Here is a sample; alter the names:
346 |
347 | Yoyodyne, Inc., hereby disclaims all copyright
348 | interest in the program `Gnomovision'
349 | (which makes passes at compilers) written
350 | by James Hacker.
351 |
352 | signature of Ty Coon, 1 April 1989
353 | Ty Coon, President of Vice
354 |
355 | This General Public License does not permit incorporating your program
356 | into proprietary programs. If your program is a subroutine library,
357 | you may consider it more useful to permit linking proprietary
358 | applications with the library. If this is what you want to do, use the
359 | [GNU Lesser General Public
360 | License](https://www.gnu.org/licenses/lgpl.html) instead of this
361 | License.
362 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "inpsyde/metabox-orchestra",
3 | "description": "Package for OOP WordPress metabox orchestration.",
4 | "type": "library",
5 | "license": "GPL-2.0-or-later",
6 | "authors": [
7 | {
8 | "name": "Inpsyde GmbH",
9 | "email": "hello@inpsyde.com",
10 | "homepage": "http://inpsyde.com",
11 | "role": "Company"
12 | },
13 | {
14 | "name": "Giuseppe Mazzapica",
15 | "email": "g.mazzapica@inpsyde.com",
16 | "role": "Developer"
17 | },
18 | {
19 | "name": "Christian Leucht",
20 | "email": "c.leucht@inpsyde.com",
21 | "role": "Developer"
22 | }
23 | ],
24 | "minimum-stability": "stable",
25 | "require": {
26 | "php": ">=7.2",
27 | "brain/nonces": "^1.0"
28 | },
29 | "require-dev": {
30 | "phpunit/phpunit": "^8 | ^9",
31 | "doctrine/instantiator": "^1.0",
32 | "brain/monkey": "^2"
33 | },
34 | "autoload": {
35 | "psr-4": {
36 | "MetaboxOrchestra\\": "src/"
37 | }
38 | },
39 | "autoload-dev": {
40 | "psr-4": {
41 | "MetaboxOrchestra\\Tests\\": "tests/src/"
42 | }
43 | },
44 | "config": {
45 | "optimize-autoloader": true
46 | },
47 | "extra": {
48 | "branch-alias": {
49 | "dev-master": "0.x-dev"
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
14 |
15 |
16 | tests/src
17 |
18 |
19 |
20 |
21 | src
22 |
23 | tests
24 | vendor
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/src/AdminNotices.php:
--------------------------------------------------------------------------------
1 | use_default_screen( $default_screen );
67 | }
68 |
69 | /**
70 | * @param string $message
71 | * @param string $title
72 | * @param string $type
73 | * @param string|null $target_screen
74 | *
75 | * @return AdminNotices
76 | */
77 | public function add(
78 | string $message,
79 | string $title = '',
80 | string $type = self::ERROR,
81 | string $target_screen = null
82 | ): AdminNotices {
83 |
84 | $user_id = get_current_user_id();
85 | if ( ! $user_id ) {
86 | return $this;
87 | }
88 |
89 | $target_screen or $target_screen = $this->default_screen ? : get_current_screen()->id;
90 | $now = new \DateTimeImmutable( 'now', new \DateTimeZone( 'UTC' ) );
91 |
92 | isset( $this->messages[ $target_screen ] ) or $this->messages[ $target_screen ] = [];
93 | isset( $this->messages[ $target_screen ][ $type ] ) or $this->messages[ $target_screen ][ $type ] = [];
94 | isset( $this->messages[ $target_screen ][ $type ] ) or $this->messages[ $target_screen ][ $type ] = [];
95 | $this->messages[ $target_screen ][ $type ][] = [ $message, $title, $now->getTimestamp() ];
96 |
97 | return $this;
98 | }
99 |
100 | /**
101 | * @return bool
102 | */
103 | public function do_notices(): bool {
104 |
105 | if ( $this->printed || ! doing_action( 'admin_notices' ) ) {
106 | return FALSE;
107 | }
108 |
109 | $this->printed = TRUE;
110 |
111 | $user_id = get_current_user_id();
112 | $screen_id = get_current_screen()->id;
113 | $messages = (array) get_user_option( self::OPTION_NAME, $user_id );
114 |
115 | if ( ! empty( $messages[ $screen_id ] ) ) {
116 | $this->print_messages( (array) $messages[ $screen_id ] );
117 | unset( $messages[ $screen_id ] );
118 |
119 | return $messages
120 | ? (bool) update_user_option( $user_id, self::OPTION_NAME, $messages )
121 | : (bool) delete_user_option( $user_id, self::OPTION_NAME );
122 | }
123 |
124 | return (bool) delete_user_option( $user_id, self::OPTION_NAME );
125 | }
126 |
127 | /**
128 | * Store (or delete) messages on shutdown.
129 | *
130 | * @return bool
131 | */
132 | public function record(): bool {
133 |
134 | if ( $this->recorded || ! doing_action( 'shutdown' ) ) {
135 | return FALSE;
136 | }
137 |
138 | $this->recorded = TRUE;
139 |
140 | $user_id = get_current_user_id();
141 | if ( $user_id ) {
142 | return $this->messages
143 | ? (bool) update_user_option( $user_id, self::OPTION_NAME, $this->messages )
144 | : (bool) delete_user_option( $user_id, self::OPTION_NAME );
145 | }
146 |
147 | return FALSE;
148 | }
149 |
150 | /**
151 | * @param array $messages
152 | */
153 | private function print_messages( array $messages ) {
154 |
155 | foreach ( $messages as $type => $type_messages ) {
156 |
157 | if ( ! in_array( $type, [ self::ERROR, self::INFO, self::SUCCESS ], TRUE ) ) {
158 | continue;
159 | }
160 |
161 | foreach ( (array) $type_messages as list( $message, $title, $timestamp ) ) {
162 |
163 | $now_ts = ( new \DateTimeImmutable( 'now', new \DateTimeZone( 'UTC' ) ) )->getTimestamp();
164 | $target_ts = $now_ts - (int) apply_filters( 'metabox-orchestra.notice-ttl', self::DEFAULT_TTL );
165 |
166 | if ( $target_ts > $timestamp ) {
167 | continue;
168 | }
169 |
170 | ?>
171 |
172 |
173 |
174 | = esc_html( $title ) ?>
175 |
176 |
177 | = wp_kses_post( $message ) ?>
178 |
179 |
180 |
181 | default_screen = $screen;
194 |
195 | return $this;
196 | }
197 |
198 | }
--------------------------------------------------------------------------------
/src/Bootstrap.php:
--------------------------------------------------------------------------------
1 | storage = compact( 'title', 'id', 'context', 'priority' );
70 | }
71 |
72 | /**
73 | * @return string
74 | */
75 | public function id(): string {
76 |
77 | return $this->storage[ 'id' ];
78 | }
79 |
80 | /**
81 | * @return string
82 | */
83 | public function title(): string {
84 |
85 | return $this->storage[ 'title' ];
86 | }
87 |
88 | /**
89 | * @return string
90 | */
91 | public function context(): string {
92 |
93 | return $this->storage[ 'context' ];
94 | }
95 |
96 | /**
97 | * @return string
98 | */
99 | public function priority(): string {
100 |
101 | return $this->storage[ 'priority' ];
102 | }
103 |
104 | /**
105 | * @inheritdoc
106 | */
107 | public function offsetExists( $offset ): bool {
108 |
109 | return array_key_exists( $offset, $this->meta );
110 | }
111 |
112 | /**
113 | * @inheritdoc
114 | */
115 | #[\ReturnTypeWillChange]
116 | public function offsetGet( $offset ) {
117 |
118 | return $this->offsetExists( $offset ) ? $this->meta[ $offset ] : NULL;
119 | }
120 |
121 | /**
122 | * @inheritdoc
123 | */
124 | public function offsetSet( $offset, $value ): void {
125 |
126 | $this->meta[ $offset ] = $value;
127 | }
128 |
129 | /**
130 | * @inheritdoc
131 | */
132 | public function offsetUnset( $offset ): void {
133 |
134 | unset( $this->meta[ $offset ] );
135 | }
136 | }
137 |
--------------------------------------------------------------------------------
/src/BoxView.php:
--------------------------------------------------------------------------------
1 | taxonomy ) {
66 |
67 | $instance->init_for_term( $screen->taxonomy );
68 | add_action( "{$screen->taxonomy}_edit_form", [ $instance, 'print_term_boxes' ] );
69 |
70 | return;
71 | }
72 |
73 | $instance->init_for_post();
74 |
75 | }, PHP_INT_MAX );
76 | }
77 |
78 | /**
79 | * @param Metabox[] $boxes
80 | *
81 | * @return Boxes
82 | */
83 | public function add_box( Metabox ...$boxes ) {
84 |
85 | if ( $this->locked ) {
86 | throw new \BadMethodCallException( 'Cannot add boxes when controller is locked.' );
87 | }
88 |
89 | if (
90 | ! $this->target->valid()
91 | || ! in_array( $this->registering_for, [ Metabox::SAVE, Metabox::SHOW ], true )
92 | ) {
93 | return $this;
94 | }
95 |
96 | $is_post = $this->target->is( \WP_Post::class );
97 | $is_term = $this->target->is( \WP_Term::class );
98 |
99 | foreach ( $boxes as $box ) {
100 | if ( $is_post && $box instanceof PostMetabox ) {
101 | $this->boxes[ $box->create_info( $this->registering_for, $this->target )->id() ] = $box;
102 | } elseif ( $is_term && $box instanceof TermMetabox ) {
103 | $this->boxes[ $box->create_info( $this->registering_for, $this->target )->id() ] = $box;
104 | }
105 | }
106 |
107 | return $this;
108 | }
109 |
110 | /**
111 | * WordPress does not print metaboxes for terms, let's fix this.
112 | *
113 | * @param \WP_Term $term
114 | */
115 | public function print_term_boxes( \WP_Term $term ) {
116 |
117 | if ( current_filter() !== "{$term->taxonomy}_edit_form" ) {
118 | return;
119 | }
120 |
121 | global $wp_meta_boxes;
122 | if ( empty( $wp_meta_boxes[ "edit-{$term->taxonomy}" ] ) ) {
123 | return;
124 | }
125 |
126 | $script = '!function(J,D){J(function(){J(".meta-box-sortables").sortable();'
127 | . 'J(D).on("click",".termbox-container button.handlediv",function(){'
128 | . 'var D=J(this),t=D.siblings(".inside");t.toggle();var e=t.is(":visible")?"true":"false";'
129 | . 'D.attr("aria-expanded",e)})})}(jQuery,document);';
130 | wp_enqueue_script( 'jquery-ui-sortable' );
131 | wp_add_inline_script( 'jquery-ui-sortable', $script );
132 |
133 | echo '';
134 | // WordPress does not print metaboxes for terms, let's fix this
135 | do_meta_boxes( "edit-{$term->taxonomy}", 'side', $term );
136 | do_meta_boxes( "edit-{$term->taxonomy}", 'normal', $term );
137 | do_meta_boxes( "edit-{$term->taxonomy}", 'advanced', $term );
138 | echo '
';
139 | // remove the term hook after being executed
140 | unset($wp_meta_boxes["edit-{$term->taxonomy}"]);
141 | }
142 |
143 | /**
144 | * @return bool
145 | */
146 | private function init_for_post(): bool {
147 |
148 | // Show Boxes
149 | add_action( 'add_meta_boxes', function ( $post_type, $post ) {
150 | if ( $post instanceof \WP_Post ) {
151 |
152 | $entity = new Entity( $post );
153 |
154 | $this->prepare_target( new Entity( $post ), Metabox::SHOW );
155 |
156 | do_action( self::ACTION_SHOW, $entity );
157 |
158 | array_walk( $this->boxes, [ $this, 'add_meta_box' ] );
159 |
160 | $this->release_target();
161 | }
162 | }, 100, 2 );
163 |
164 | // Save Boxes even if WordPress says content is empty.
165 | add_filter(
166 | 'wp_insert_post_empty_content',
167 | function ( $empty, array $post_array ) {
168 |
169 | global $post;
170 | if ( ! $empty || ! $post instanceof \WP_Post || ! $post->ID ) {
171 | return $empty;
172 | }
173 |
174 | if ( apply_filters( 'metabox-orchestra.save-on-empty-post', TRUE, $post, $post_array ) ) {
175 | $this->on_post_save( $post );
176 | }
177 |
178 | return $empty;
179 | },
180 | PHP_INT_MAX,
181 | 2
182 | );
183 |
184 | // Save Boxes
185 | add_action( 'wp_insert_post', function ( $post_id, \WP_Post $post ) {
186 | $this->on_post_save( $post );
187 | }, 100, 2 );
188 |
189 | return true;
190 | }
191 |
192 | /**
193 | * @param string $taxonomy
194 | *
195 | * @return bool
196 | */
197 | private function init_for_term( string $taxonomy ): bool {
198 |
199 | // Show Boxes
200 | add_action( "{$taxonomy}_pre_edit_form", function ( \WP_Term $term ) {
201 |
202 | $entity = new Entity( $term );
203 |
204 | $this->prepare_target( $entity, Metabox::SHOW );
205 |
206 | do_action( self::ACTION_SHOW, $entity );
207 |
208 | array_walk( $this->boxes, [ $this, 'add_meta_box' ] );
209 |
210 | $this->release_target();
211 | }, 1 );
212 |
213 | // Save Boxes
214 | add_action( 'edit_term', function ( $term_id, $term_taxonomy_id, $term_taxonomy ) use ( $taxonomy ) {
215 |
216 | // This check allows to edit term object inside BoxAction::save() without recursion.
217 | if ( $this->saving === 'term' ) {
218 | return;
219 | }
220 |
221 | $term = get_term_by( 'term_taxonomy_id', $term_taxonomy_id );
222 |
223 | if (
224 | ! $term instanceof \WP_Term
225 | || (int) $term->term_id !== (int) $term_id
226 | || $term->taxonomy !== $term_taxonomy
227 | || $term->taxonomy !== $taxonomy
228 | ) {
229 | return;
230 | }
231 |
232 | $this->saving = 'term';
233 |
234 | $entity = new Entity( $term );
235 |
236 | $this->prepare_target( $entity, Metabox::SAVE );
237 |
238 | do_action( self::ACTION_SAVE, $entity );
239 |
240 | array_walk( $this->boxes, [ $this, 'save_meta_box' ] );
241 |
242 | do_action( self::ACTION_SAVED, $entity );
243 |
244 | $this->release_target();
245 |
246 | $this->saving = '';
247 |
248 | }, 100, 3 );
249 |
250 | return true;
251 | }
252 |
253 | /**
254 | * @param Entity $entity
255 | * @param string $show_or_save
256 | */
257 | private function prepare_target( Entity $entity, string $show_or_save ) {
258 |
259 | $this->target = $entity;
260 | $this->registering_for = $show_or_save;
261 | $this->boxes = [];
262 | $this->locked = false;
263 | $this->target->valid() and do_action( self::REGISTER_BOXES, $this, $this->target, $show_or_save );
264 | $this->locked = true;
265 | }
266 |
267 | /**
268 | * @param Metabox|PostMetabox|TermMetabox $box
269 | * @param string $type
270 | *
271 | * @return bool
272 | */
273 | private function box_enabled( Metabox $box, string $type ): bool {
274 |
275 | if ( ! $this->target->valid() ) {
276 | return false;
277 | }
278 |
279 | $accept = false;
280 | /** @var \WP_Post|\WP_Term $object */
281 | $object = $this->target->expose();
282 | switch ( true ) {
283 | case $this->target->is( \WP_Post::class ) && $box instanceof PostMetabox:
284 | $accept = $box->accept_post( $object, $type );
285 | break;
286 | case $this->target->is( \WP_Term::class ) && $box instanceof TermMetabox:
287 | $accept = $box->accept_term( $object, $type );
288 | break;
289 | }
290 |
291 | return (bool) apply_filters( 'metabox-orchestra.box-enabled', $accept, $box, $object );
292 | }
293 |
294 | /**
295 | * @param Metabox|PostMetabox|TermMetabox $box
296 | * @param string $box_id
297 | */
298 | private function add_meta_box( Metabox $box, string $box_id ) {
299 |
300 | if ( ! $this->box_enabled( $box, Metabox::SHOW ) ) {
301 | return;
302 | }
303 |
304 | $is_post = $this->target->is( \WP_Post::class );
305 | /** @var \WP_Post|\WP_Term $object */
306 | $object = $this->target->expose();
307 | $info = $box->create_info( Metabox::SHOW, $this->target );
308 | $view = $is_post ? $box->view_for_post( $object ) : $box->view_for_term( $object );
309 |
310 | $box_suffix = $is_post ? '-postbox' : '-termbox';
311 | $context = $info->context();
312 | $screen = $is_post ? null : "edit-{$object->taxonomy}";
313 | ( $context === BoxInfo::CONTEXT_SIDE && $is_post ) and $screen = $object->post_type;
314 |
315 | add_meta_box(
316 | $box_id . $box_suffix,
317 | $info->title(),
318 | static function ( $object ) use ( $box_id, $view, $box, $info ) {
319 |
320 | $object_id = $object instanceof \WP_Post ? $object->ID : $object->term_id;
321 |
322 | print \Brain\Nonces\formField( new WpNonce( $box_id . "-{$object_id}" ) );
323 |
324 | do_action( 'metabox-orchestra.inside-box-before', $box, $object, $info );
325 |
326 | print $view->render( $info );
327 |
328 | do_action( 'metabox-orchestra.inside-box-after', $box, $object, $info );
329 | },
330 | $screen,
331 | $context,
332 | $info->priority()
333 | );
334 | }
335 |
336 | /**
337 | * @param \WP_Post $post
338 | */
339 | private function on_post_save( \WP_Post $post ) {
340 |
341 | if (
342 | wp_is_post_autosave( $post )
343 | || wp_is_post_revision( $post )
344 | || ( is_multisite() && ms_is_switched() )
345 | ) {
346 | return;
347 | }
348 |
349 | // This check allows to edit post object inside BoxAction::save() without recursion.
350 | if ( $this->saving === 'post' ) {
351 | return;
352 | }
353 |
354 | static $saved;
355 | if ( $saved ) {
356 | return;
357 | }
358 |
359 | $saved = true;
360 |
361 | $entity = new Entity( $post );
362 |
363 | $this->saving = 'post';
364 |
365 | $this->prepare_target( $entity, Metabox::SAVE );
366 |
367 | do_action( self::ACTION_SAVE, $entity );
368 |
369 | array_walk( $this->boxes, [ $this, 'save_meta_box' ] );
370 |
371 | do_action( self::ACTION_SAVED, $entity );
372 |
373 | $this->release_target();
374 |
375 | $this->saving = '';
376 | }
377 |
378 | /**
379 | * @param Metabox|PostMetabox|TermMetabox $box
380 | * @param string $box_id
381 | */
382 | private function save_meta_box( Metabox $box, string $box_id ) {
383 |
384 | if ( ! $this->box_enabled( $box, Metabox::SAVE ) ) {
385 | return;
386 | }
387 |
388 | /** @var \WP_Post|\WP_Term $object */
389 | $object = $this->target->expose();
390 | $is_post = $object instanceof \WP_Post;
391 |
392 | $screen = $is_post ? $object->post_type : "edit-{$object->taxonomy}";
393 | $screen = apply_filters( 'metabox-orchestra.box-notices-screen', $screen, $box, $object );
394 | $object_id = $this->target->id();
395 | $auth_class = $is_post ? PostMetaboxAuth::class : TermMetaboxAuth::class;
396 |
397 | $notices = AdminNotices::init( (string) $screen );
398 |
399 | /** @var PostMetaboxAuth|TermMetaboxAuth $auth */
400 | $nonce = new WpNonce( "{$box_id}-{$object_id}" );
401 | $auth = new $auth_class( $object, $nonce );
402 |
403 | if ( ! $auth->authorized() ) {
404 | do_action( 'metabox-orchestra.unauthorized-box-save', $box, $object, $nonce );
405 |
406 | return;
407 | }
408 |
409 | $action = $is_post ? $box->action_for_post( $object ) : $box->action_for_term( $object );
410 | $action->save( $notices );
411 | }
412 |
413 | /**
414 | * Clean up state.
415 | */
416 | private function release_target() {
417 | $this->target = null;
418 | $this->registering_for = '';
419 | $this->boxes = [];
420 | $this->locked = false;
421 | }
422 |
423 | }
424 |
--------------------------------------------------------------------------------
/src/Entity.php:
--------------------------------------------------------------------------------
1 | entity = $object;
46 | $this->id = (int) $object->ID;
47 | break;
48 | case ( $object instanceof \WP_Term ):
49 | $this->entity = $object;
50 | $this->id = (int) $object->term_id;
51 | break;
52 | case ( $object instanceof Entity ) :
53 | $this->entity = $object->expose();
54 | $this->id = $object->id();
55 | break;
56 | }
57 | }
58 |
59 | /**
60 | * @param string $var
61 | *
62 | * @return mixed
63 | */
64 | public function __get( string $var ) {
65 |
66 | return $this->prop( $var );
67 | }
68 |
69 | /**
70 | * @return int
71 | */
72 | public function id(): int {
73 |
74 | return $this->id;
75 | }
76 |
77 | /**
78 | * @return bool
79 | */
80 | public function valid(): bool {
81 |
82 | return $this->entity && $this->id() > 0;
83 | }
84 |
85 | /**
86 | * @param string $type
87 | *
88 | * @return bool
89 | */
90 | public function is( string $type ): bool {
91 |
92 | return $this->valid() && is_a( $this->entity, $type );
93 | }
94 |
95 | /**
96 | * @param string $prop
97 | * @param null $default
98 | *
99 | * @return mixed
100 | */
101 | public function prop( string $prop, $default = null ) {
102 |
103 | if ( ! $this->valid() ) {
104 |
105 | return $default;
106 | }
107 |
108 | if ( is_array( $this->entity_array ) ) {
109 |
110 | return $this->entity_array[ $prop ] ?? $default;
111 | }
112 |
113 | if ( is_callable( [ $this->entity, 'to_array' ] ) ) {
114 | $this->entity_array = $this->entity->to_array();
115 |
116 | return $this->entity_array[ $prop ] ?? $default;
117 | }
118 |
119 | $this->entity_array = get_object_vars( $this->entity );
120 |
121 | return $this->entity_array[ $prop ] ?? $default;
122 | }
123 |
124 | /**
125 | * @return object|null
126 | */
127 | public function expose() {
128 |
129 | return $this->valid() ? clone $this->entity : null;
130 | }
131 |
132 | }
133 |
--------------------------------------------------------------------------------
/src/Metabox.php:
--------------------------------------------------------------------------------
1 | post = $post;
39 | $this->nonce = $nonce;
40 | }
41 |
42 | /**
43 | * @return bool
44 | */
45 | public function authorized(): bool {
46 |
47 | $type = get_post_type_object( $this->post->post_type );
48 |
49 | if ( ! current_user_can( $type->cap->edit_post, $this->post->ID ) ) {
50 | do_action(
51 | 'metabox-orchestra.error',
52 | new \Error( 'User not allowed to save post.', (int) $this->post->ID ),
53 | [
54 | 'post' => $this->post->ID,
55 | 'capability' => $type->cap->edit_post,
56 | 'blog' => get_current_blog_id(),
57 | ]
58 | );
59 |
60 | return FALSE;
61 | }
62 |
63 | if ( is_multisite() && ms_is_switched() ) {
64 | return FALSE;
65 | }
66 |
67 | $context = new ArrayContext( $_POST );
68 | $valid = $this->nonce->validate( $context );
69 |
70 | if ( ! $valid ) {
71 | $action = $this->nonce->action();
72 | do_action(
73 | 'metabox-orchestra.error',
74 | new \Error( 'Nonce did not validated.', (int) $this->post->ID ),
75 | [
76 | 'post' => $this->post->ID,
77 | 'nonce_action' => $action,
78 | 'nonce_value' => $context->offsetExists( $action ) ? $context[ $action ] : '*not present*',
79 | 'blog' => get_current_blog_id(),
80 | ]
81 | );
82 | }
83 |
84 | return $valid;
85 | }
86 | }
--------------------------------------------------------------------------------
/src/TermMetabox.php:
--------------------------------------------------------------------------------
1 | term = $term;
39 | $this->nonce = $nonce;
40 | }
41 |
42 | /**
43 | * @return bool
44 | */
45 | public function authorized(): bool {
46 |
47 | $taxonomy = get_taxonomy( $this->term->taxonomy );
48 |
49 | if ( ! $taxonomy || ! current_user_can( $taxonomy->cap->edit_terms ) ) {
50 | do_action(
51 | 'metabox-orchestra.error',
52 | new \Error( 'User not allowed to save term.', (int) $this->term->term_id ),
53 | [
54 | 'term' => $this->term->term_id,
55 | 'capability' => $taxonomy ? $taxonomy->capabilitites->edit_terms : '*taxonomy does not exists*',
56 | 'blog' => get_current_blog_id(),
57 | ]
58 | );
59 |
60 | return FALSE;
61 | }
62 |
63 | if ( is_multisite() && ms_is_switched() ) {
64 | return FALSE;
65 | }
66 |
67 | $context = new ArrayContext( $_POST );
68 | $valid = $this->nonce->validate( $context );
69 |
70 | if ( ! $valid ) {
71 | $action = $this->nonce->action();
72 | do_action(
73 | 'metabox-orchestra.error',
74 | new \Error( 'Nonce did not validated.', (int) $this->term->term_id ),
75 | [
76 | 'term' => $this->term->term_id,
77 | 'nonce_action' => $action,
78 | 'nonce_value' => $context->offsetExists( $action ) ? $context[ $action ] : '*not present*',
79 | 'blog' => get_current_blog_id(),
80 | ]
81 | );
82 | }
83 |
84 | return $valid;
85 | }
86 | }
--------------------------------------------------------------------------------
/tests/boot.php:
--------------------------------------------------------------------------------
1 |
18 | * @package MetaboxOrchestra
19 | * @license http://opensource.org/licenses/MIT MIT
20 | */
21 | class AdminNoticesTest extends TestCase {
22 |
23 | public function testAddedNoticesAreRecorded() {
24 |
25 | Functions\when( 'get_current_user_id' )->justReturn( 1 );
26 | Functions\when( 'get_current_screen' )->justReturn( (object) [ 'id' => 'screen_test' ] );
27 |
28 | static::assertSame( get_current_screen()->id, 'screen_test' );
29 |
30 | $notices = new AdminNotices();
31 |
32 | $notices->add( 'This is an error', 'Error!', AdminNotices::ERROR );
33 | $notices->add( 'This is a success', 'Success!', AdminNotices::SUCCESS );
34 |
35 | Functions\expect( 'doing_action' )
36 | ->with( 'shutdown' )
37 | ->andReturn( TRUE );
38 |
39 | Functions\expect( 'update_user_option' )
40 | ->once()
41 | ->with( 1, AdminNotices::OPTION_NAME, \Mockery::type( 'array' ) )
42 | ->andReturnUsing(
43 | function ( $id, $option, $messages ) {
44 |
45 | static::assertArrayHasKey( 'screen_test', $messages );
46 | static::assertArrayHasKey( AdminNotices::ERROR, $messages[ 'screen_test' ] );
47 | static::assertArrayHasKey( AdminNotices::SUCCESS, $messages[ 'screen_test' ] );
48 | static::assertIsArray( $messages[ 'screen_test' ][ AdminNotices::ERROR ] );
49 | static::assertIsArray( $messages[ 'screen_test' ][ AdminNotices::SUCCESS ] );
50 | static::assertCount( 1, $messages[ 'screen_test' ][ AdminNotices::ERROR ] );
51 | static::assertCount( 1, $messages[ 'screen_test' ][ AdminNotices::SUCCESS ] );
52 |
53 | $error = reset( $messages[ 'screen_test' ][ AdminNotices::ERROR ] );
54 | $success = reset( $messages[ 'screen_test' ][ AdminNotices::SUCCESS ] );
55 |
56 | static::assertIsArray( $error );
57 | static::assertIsArray( $success );
58 | static::assertContains( 'This is an error', $error );
59 | static::assertContains( 'Error!', $error );
60 | static::assertContains( 'This is a success', $success );
61 | static::assertContains( 'Success!', $success );
62 | }
63 | );
64 |
65 | $notices->record();
66 | }
67 |
68 | public function testAddedNoticesArePrinted() {
69 |
70 | Functions\when( 'get_current_user_id' )->justReturn( 123 );
71 | Functions\when( 'get_current_screen' )->justReturn( (object) [ 'id' => 'screen_test' ] );
72 |
73 | $notices = new AdminNotices();
74 |
75 | $notices->add( 'This is an error', 'Error!', AdminNotices::ERROR );
76 | $notices->add( 'This is a success', 'Success!', AdminNotices::SUCCESS );
77 |
78 | $to_print = NULL;
79 |
80 | Functions\expect( 'doing_action' )
81 | ->once()
82 | ->with( 'shutdown' )
83 | ->andReturn( TRUE );
84 | Functions\expect( 'update_user_option' )
85 | ->andReturnUsing(
86 | function ( $id, $option, $messages ) use ( &$to_print ) {
87 |
88 | $to_print = $messages;
89 |
90 | return TRUE;
91 | }
92 | );
93 |
94 | $notices->record();
95 |
96 | Functions\expect( 'doing_action' )
97 | ->once()
98 | ->with( 'admin_notices' )
99 | ->andReturn( TRUE );
100 | Functions\expect( 'get_user_option' )
101 | ->once()
102 | ->with( AdminNotices::OPTION_NAME, 123 )
103 | ->andReturn( $to_print );
104 | Functions\expect( 'delete_user_option' )
105 | ->once()
106 | ->with( 123, AdminNotices::OPTION_NAME );
107 | Functions\expect( 'esc_html' )->andReturnFirstArg();
108 | Functions\expect( 'wp_kses_post' )->andReturnFirstArg();
109 |
110 | ob_start();
111 | $notices->do_notices();
112 | $output = ob_get_clean();
113 |
114 | static::assertStringContainsString( 'notice-' . AdminNotices::ERROR, $output );
115 | static::assertStringContainsString( 'notice-' . AdminNotices::SUCCESS, $output );
116 | static::assertStringContainsString( 'Error!', $output );
117 | static::assertStringContainsString( 'Success!', $output );
118 | static::assertStringContainsString( 'This is an error', $output );
119 | static::assertStringContainsString( 'This is a success', $output );
120 | }
121 |
122 | public function testAddNoUserId() {
123 |
124 | Functions\expect( 'get_current_user_id' )
125 | ->once()
126 | ->andReturn( FALSE );
127 |
128 | $testee = new AdminNotices();
129 | static::assertInstanceOf( AdminNotices::class, $testee->add( '' ) );
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/tests/src/BootstrapTest.php:
--------------------------------------------------------------------------------
1 | justReturn( TRUE );
20 | Actions\expectAdded( 'admin_menu' );
21 |
22 | static::assertTrue( Bootstrap::bootstrap() );
23 | static::assertFalse( Bootstrap::bootstrap() );
24 | }
25 |
26 | public function testBootstrapInFrontend() {
27 |
28 | Functions\when( 'is_admin' )->justReturn( FALSE );
29 |
30 | static::assertFalse( Bootstrap::bootstrap() );
31 | }
32 | }
--------------------------------------------------------------------------------
/tests/src/BoxInfoTest.php:
--------------------------------------------------------------------------------
1 | title() );
18 | static::assertSame( 'foo', $testee->id() );
19 | static::assertSame( BoxInfo::PRIORITY_ADVANCED, $testee->priority() );
20 | static::assertSame( BoxInfo::CONTEXT_ADVANCED, $testee->context() );
21 | }
22 |
23 | public function testConstructorId() {
24 |
25 | Functions\stubs( [ 'sanitize_title_with_dashes' ] );
26 | $expected = 'unique-id';
27 | static::assertSame( $expected, ( new BoxInfo( 'foo', $expected ) )->id() );
28 | }
29 |
30 | /**
31 | * @param string $input
32 | * @param null|string $expected
33 | *
34 | * @dataProvider provideConstructorContext
35 | */
36 | public function testConstructorContext( $input, $expected = NULL ) {
37 |
38 | $expected = $expected ? : $input;
39 | Functions\stubs( [ 'sanitize_title_with_dashes' ] );
40 | static::assertSame(
41 | $expected,
42 | ( new BoxInfo( 'foo', '', $input ) )->context()
43 | );
44 | }
45 |
46 | public function provideConstructorContext() {
47 |
48 | return [
49 | 'side' => [ BoxInfo::CONTEXT_SIDE ],
50 | 'normal' => [ BoxInfo::CONTEXT_NORMAL ],
51 | 'advanced' => [ BoxInfo::CONTEXT_ADVANCED ],
52 | 'invalid context' => [ 'foo', BoxInfo::CONTEXT_ADVANCED ]
53 | ];
54 | }
55 |
56 | /**
57 | * @param string $input
58 | * @param null|string $expected
59 | *
60 | * @dataProvider provideConstructorPriority
61 | */
62 | public function testConstructorPriority( $input, $expected = NULL ) {
63 |
64 | $expected = $expected ? : $input;
65 | Functions\stubs( [ 'sanitize_title_with_dashes' ] );
66 | static::assertSame(
67 | $expected,
68 | ( new BoxInfo( 'foo', '', '', $input ) )->priority()
69 | );
70 | }
71 |
72 | public function provideConstructorPriority() {
73 |
74 | return [
75 | 'high' => [ BoxInfo::PRIORITY_HIGH ],
76 | 'sorted' => [ BoxInfo::PRIORITY_SORTED ],
77 | 'core' => [ BoxInfo::PRIORITY_CORE ],
78 | 'normal' => [ BoxInfo::PRIORITY_NORMAL ],
79 | 'advanced' => [ BoxInfo::PRIORITY_ADVANCED ],
80 | 'invalid priority' => [ 'foo', BoxInfo::PRIORITY_ADVANCED ]
81 | ];
82 | }
83 |
84 | public function testArrayAccess() {
85 |
86 | Functions\stubs( [ 'sanitize_title_with_dashes' ] );
87 | $testee = new BoxInfo( '' );
88 |
89 | $key = 'foo';
90 | $expected = 'bar';
91 |
92 | $testee[ $key ] = $expected;
93 |
94 | static::assertSame( $expected, $testee[ $key ] );
95 | static::assertTrue( isset( $testee[ $key ] ) );
96 |
97 | unset( $testee[ $key ] );
98 |
99 | static::assertFalse( isset( $testee[ $key ] ) );
100 | static::assertNull( $testee[ $key ] );
101 | }
102 | }
--------------------------------------------------------------------------------
/tests/src/NoopBoxActionTest.php:
--------------------------------------------------------------------------------
1 | save( $stub ) );
23 | }
24 | }
--------------------------------------------------------------------------------
/tests/src/NoopBoxViewTest.php:
--------------------------------------------------------------------------------
1 | render( new BoxInfo( '', '', '' ) )
26 | );
27 | }
28 | }
--------------------------------------------------------------------------------
/tests/src/PostMetaboxAuthTest.php:
--------------------------------------------------------------------------------
1 |
20 | * @package MetaboxOrchestra
21 | * @license http://opensource.org/licenses/MIT MIT
22 | */
23 | class PostMetaboxAuthTest extends TestCase {
24 |
25 | public function testAuthorizedFailsIfUserCantEditPost() {
26 |
27 | /** @var \stdClass|\WP_Post $post */
28 | $post = \Mockery::mock( '\WP_Post' );
29 | $post->ID = 123;
30 | $post->post_type = 'post';
31 | /** @var \Mockery\MockInterface|NonceInterface $nonce */
32 | $nonce = \Mockery::mock( NonceInterface::class );
33 |
34 | Functions\when( 'get_current_blog_id' )->justReturn( 1 );
35 |
36 | Functions\expect( 'get_post_type_object' )
37 | ->with( 'post' )
38 | ->andReturn( (object) [ 'cap' => (object) [ 'edit_post' => 'edit_post' ] ] );
39 |
40 | Functions\expect( 'current_user_can' )
41 | ->with( 'edit_post', 123 )
42 | ->andReturn( FALSE );
43 |
44 | Actions\expectDone( 'metabox-orchestra.error' )
45 | ->once()
46 | ->with( \Mockery::type( 'Error' ), \Mockery::type( 'array' ) );
47 |
48 | $auth = new PostMetaboxAuth( $post, $nonce );
49 |
50 | static::assertFalse( $auth->authorized() );
51 | }
52 |
53 | public function testAuthorizedFailsIfMultisiteSwitched() {
54 |
55 | /** @var \stdClass|\WP_Post $post */
56 | $post = \Mockery::mock( '\WP_Post' );
57 | $post->ID = 123;
58 | $post->post_type = 'post';
59 | /** @var \Mockery\MockInterface|NonceInterface $nonce */
60 | $nonce = \Mockery::mock( NonceInterface::class );
61 |
62 | Functions\when( 'get_post_type_object' )->justReturn( (object) [ 'cap' => (object) [ 'edit_post' => 'edit_post' ] ] );
63 | Functions\when( 'current_user_can' )->justReturn( TRUE );
64 | Functions\when( 'is_multisite' )->justReturn( TRUE );
65 | Functions\when( 'ms_is_switched' )->justReturn( TRUE );
66 |
67 | Actions\expectDone( 'metabox-orchestra.error' )->never();
68 |
69 | $auth = new PostMetaboxAuth( $post, $nonce );
70 |
71 | static::assertFalse( $auth->authorized() );
72 | }
73 |
74 | public function testAuthorizedFailsIfNonceFails() {
75 |
76 | /** @var \stdClass|\WP_Post $post */
77 | $post = \Mockery::mock( '\WP_Post' );
78 | $post->ID = 123;
79 | $post->post_type = 'post';
80 | /** @var \Mockery\MockInterface|NonceInterface $nonce */
81 | $nonce = \Mockery::mock( NonceInterface::class );
82 | $nonce->shouldReceive( 'validate' )->atLeast()->once()->andReturn( FALSE );
83 | $nonce->shouldReceive( 'action' )->atLeast()->once()->andReturn( 'test' );
84 |
85 | Functions\when( 'get_current_blog_id' )->justReturn( 1 );
86 | Functions\when( 'get_post_type_object' )->justReturn( (object) [ 'cap' => (object) [ 'edit_post' => 'edit_post' ] ] );
87 | Functions\when( 'current_user_can' )->justReturn( TRUE );
88 | Functions\when( 'is_multisite' )->justReturn( TRUE );
89 | Functions\when( 'ms_is_switched' )->justReturn( FALSE );
90 |
91 | $auth = new PostMetaboxAuth( $post, $nonce );
92 |
93 | Actions\expectDone( 'metabox-orchestra.error' )
94 | ->once()
95 | ->with( \Mockery::type( 'Error' ), \Mockery::type( 'array' ) )
96 | ->whenHappen( function ( \Error $error ) {
97 | static::assertStringContainsString( 'Nonce', $error->getMessage() );
98 | } );
99 |
100 | static::assertFalse( $auth->authorized() );
101 | }
102 |
103 | public function testAuthorizedSuccess() {
104 |
105 | /** @var \stdClass|\WP_Post $post */
106 | $post = \Mockery::mock( '\WP_Post' );
107 | $post->ID = 123;
108 | $post->post_type = 'post';
109 | /** @var \Mockery\MockInterface|NonceInterface $nonce */
110 | $nonce = \Mockery::mock( NonceInterface::class );
111 | $nonce->shouldReceive( 'validate' )->atLeast()->once()->andReturn( TRUE );
112 |
113 | Functions\when( 'get_current_blog_id' )->justReturn( 1 );
114 | Functions\when( 'get_post_type_object' )->justReturn( (object) [ 'cap' => (object) [ 'edit_post' => 'edit_post' ] ] );
115 | Functions\when( 'current_user_can' )->justReturn( TRUE );
116 | Functions\when( 'is_multisite' )->justReturn( TRUE );
117 | Functions\when( 'ms_is_switched' )->justReturn( FALSE );
118 |
119 | $auth = new PostMetaboxAuth( $post, $nonce );
120 |
121 | Actions\expectDone( 'metabox-orchestra.error' )->never();
122 |
123 | static::assertTrue( $auth->authorized() );
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/tests/src/TermMetaboxAuthTest.php:
--------------------------------------------------------------------------------
1 | taxonomy = NULL;
28 | $wp_term->term_id = NULL;
29 |
30 | /** @var NonceInterface $nonce */
31 | $nonce = \Mockery::mock( NonceInterface::class );
32 |
33 | Functions\expect( 'get_taxonomy' )
34 | ->once()
35 | ->andReturn( FALSE );
36 |
37 | Functions\expect( 'current_user_can' )->never();
38 |
39 | Functions\expect( 'get_current_blog_id' )
40 | ->once()
41 | ->andReturn( NULL );
42 |
43 | Actions\expectDone( 'metabox-orchestra.error' )
44 | ->once()
45 | ->with( \Mockery::type( \Error::class ), \Mockery::type( 'array' ) );
46 |
47 | static::assertFalse( ( new TermMetaboxAuth( $wp_term, $nonce ) )->authorized() );
48 | }
49 |
50 | public function testAuthorizedCurrentUserCanNot() {
51 |
52 | /** @var \WP_Term $wp_term */
53 | $wp_term = \Mockery::mock( '\WP_Term' );
54 | $wp_term->taxonomy = NULL;
55 | $wp_term->term_id = NULL;
56 |
57 | /** @var NonceInterface $nonce */
58 | $nonce = \Mockery::mock( NonceInterface::class );
59 |
60 | Functions\expect( 'get_taxonomy' )
61 | ->once()
62 | ->andReturn( $this->taxonomy() );
63 |
64 | Functions\expect( 'current_user_can' )
65 | ->once()
66 | ->andReturn( FALSE );
67 |
68 | Functions\expect( 'get_current_blog_id' )
69 | ->once()
70 | ->andReturn( NULL );
71 |
72 | Actions\expectDone( 'metabox-orchestra.error' )
73 | ->once()
74 | ->with( \Mockery::type( \Error::class ), \Mockery::type( 'array' ) );
75 |
76 | static::assertFalse( ( new TermMetaboxAuth( $wp_term, $nonce ) )->authorized() );
77 | }
78 |
79 | public function testAuthorizedMultisite() {
80 |
81 | /** @var \WP_Term $wp_term */
82 | $wp_term = \Mockery::mock( '\WP_Term' );
83 | $wp_term->taxonomy = NULL;
84 |
85 | /** @var NonceInterface $nonce */
86 | $nonce = \Mockery::mock( NonceInterface::class );
87 |
88 | Functions\expect( 'get_taxonomy' )
89 | ->once()
90 | ->andReturn( $this->taxonomy() );
91 |
92 | Functions\expect( 'current_user_can' )
93 | ->once()
94 | ->andReturn( TRUE );
95 |
96 | Functions\expect( 'is_multisite' )
97 | ->once()
98 | ->andReturn( TRUE );
99 |
100 | Functions\expect( 'ms_is_switched' )
101 | ->once()
102 | ->andReturn( TRUE );
103 |
104 | static::assertFalse( ( new TermMetaboxAuth( $wp_term, $nonce ) )->authorized() );
105 | }
106 |
107 | public function testAuthorizedInvalidNonce() {
108 |
109 | /** @var \WP_Term $wp_term */
110 | $wp_term = \Mockery::mock( '\WP_Term' );
111 | $wp_term->taxonomy = NULL;
112 | $wp_term->term_id = NULL;
113 |
114 | /** @var NonceInterface $nonce */
115 | $nonce = \Mockery::mock( NonceInterface::class );
116 | $nonce->shouldReceive( 'validate' )
117 | ->once()
118 | ->andReturn( FALSE );
119 | $nonce->shouldReceive( 'action' )
120 | ->once()
121 | ->andReturn( '' );
122 |
123 | Functions\expect( 'get_taxonomy' )
124 | ->once()
125 | ->andReturn( $this->taxonomy() );
126 |
127 | Functions\expect( 'current_user_can' )
128 | ->once()
129 | ->andReturn( TRUE );
130 |
131 | Functions\expect( 'is_multisite' )
132 | ->once()
133 | ->andReturn( FALSE );
134 |
135 | Functions\expect( 'ms_is_switched' )
136 | ->never();
137 |
138 | Actions\expectDone( 'metabox-orchestra.error' )
139 | ->once()
140 | ->with( \Mockery::type( \Error::class ), \Mockery::type( 'array' ) );
141 |
142 | Functions\expect( 'get_current_blog_id' )
143 | ->once()
144 | ->andReturn( NULL );
145 |
146 | static::assertFalse( ( new TermMetaboxAuth( $wp_term, $nonce ) )->authorized() );
147 | }
148 |
149 | public function testAuthorizedValidNonce() {
150 |
151 | /** @var \WP_Term $wp_term */
152 | $wp_term = \Mockery::mock( '\WP_Term' );
153 | $wp_term->taxonomy = NULL;
154 |
155 | Functions\expect( 'get_taxonomy' )
156 | ->once()
157 | ->andReturn( $this->taxonomy() );
158 |
159 | Functions\expect( 'current_user_can' )
160 | ->once()
161 | ->andReturn( TRUE );
162 |
163 | Functions\expect( 'is_multisite' )
164 | ->once()
165 | ->andReturn( FALSE );
166 |
167 | Functions\expect( 'ms_is_switched' )
168 | ->never();
169 |
170 | /** @var NonceInterface $nonce */
171 | $nonce = \Mockery::mock( NonceInterface::class );
172 | $nonce->shouldReceive( 'validate' )
173 | ->once()
174 | ->andReturn( TRUE );
175 |
176 | static::assertTrue( ( new TermMetaboxAuth( $wp_term, $nonce ) )->authorized() );
177 | }
178 |
179 | /**
180 | * Internal function for re-usage as response for get_taxonomy()-mock.
181 | *
182 | * @return \stdClass
183 | */
184 | private function taxonomy(): \stdClass {
185 |
186 | return (object) [
187 | 'cap' => (object) [ 'edit_terms' => '' ],
188 | 'capabilitites' => (object) [ 'edit_terms' => '' ]
189 | ];
190 | }
191 | }
--------------------------------------------------------------------------------
/tests/src/TestCase.php:
--------------------------------------------------------------------------------
1 |