diff options
Diffstat (limited to 'plugins/jetpack/jetpack_vendor/automattic/jetpack-publicize')
10 files changed, 4622 insertions, 0 deletions
diff --git a/plugins/jetpack/jetpack_vendor/automattic/jetpack-publicize/CHANGELOG.md b/plugins/jetpack/jetpack_vendor/automattic/jetpack-publicize/CHANGELOG.md new file mode 100644 index 00000000..fa6c6bf4 --- /dev/null +++ b/plugins/jetpack/jetpack_vendor/automattic/jetpack-publicize/CHANGELOG.md @@ -0,0 +1,71 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [0.5.0] - 2022-05-31 +### Added +- Changed logic to initialize publicize classes only if the publicize module is active. [#24451] + +### Changed +- Classic Editor: Replaced the initial settings form with the Jetpack redirect link [#24526] + +## [0.4.0] - 2022-05-24 +### Added +- Added the post field to the Publicize package [#24324] + +## [0.3.0] - 2022-05-18 +### Added +- Added new jetpack v4 end-point to list publicize connections. [#24293] + +### Changed +- Updated package dependencies. [#24153] [#24360] + +### Fixed +- Added check for wp_ajax_elementor_ajax to allow publicizing via elementor. [#24387] +- gitignore wordpress directory within the publicize package [#24339] + +## [0.2.1] - 2022-05-10 +### Fixed +- Publicize: Correct bad namespaces + +## [0.2.0] - 2022-05-04 +### Added +- Added redirect links for Jetpack cloud. [#24205] + +### Changed +- Updated package dependencies. [#24095] + +### Deprecated +- Moved the options class into Connection. [#24095] + +## [0.1.1] - 2022-05-19 +### Fixed +- Added check for wp_ajax_elementor_ajax to allow publicizing via elementor. +- Publicize: Correct bad namespaces + +## 0.1.0 - 2022-04-26 +### Added +- Added an empty shell package +- Added Publicize module files to Composer package +- Set composer package type to "jetpack-library" so i18n will work. +- Use the publicize package in the Jetpack plugin. + +### Changed +- Applied legacy Publicize filters to flag setting for Publicize +- Fix Composer dependencies +- Microperformance: Use === null instead of is_null +- PHPCS: Fix `WordPress.Security.ValidatedSanitizedInput` +- Publicize: Do not display legacy UI for block editor pages +- Sync'd changes with the equivalent files in the Publicize module +- Updated package dependencies. +- Update package.json metadata. + +[0.5.0]: https://github.com/Automattic/jetpack-publicize/compare/v0.4.0...v0.5.0 +[0.4.0]: https://github.com/Automattic/jetpack-publicize/compare/v0.3.0...v0.4.0 +[0.3.0]: https://github.com/Automattic/jetpack-publicize/compare/v0.2.1...v0.3.0 +[0.2.1]: https://github.com/Automattic/jetpack-publicize/compare/v0.2.0...v0.2.1 +[0.2.0]: https://github.com/Automattic/jetpack-publicize/compare/v0.1.0...v0.2.0 +[0.1.1]: https://github.com/Automattic/jetpack-publicize/compare/v0.1.0...v0.1.1 diff --git a/plugins/jetpack/jetpack_vendor/automattic/jetpack-publicize/LICENSE.txt b/plugins/jetpack/jetpack_vendor/automattic/jetpack-publicize/LICENSE.txt new file mode 100644 index 00000000..e82774c1 --- /dev/null +++ b/plugins/jetpack/jetpack_vendor/automattic/jetpack-publicize/LICENSE.txt @@ -0,0 +1,357 @@ +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + + +=================================== + + +GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + +Copyright (C) 1989, 1991 Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +Everyone is permitted to copy and distribute verbatim copies +of this license document, but changing it is not allowed. + + Preamble + +The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + +When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + +To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + +For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + +We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + +Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + +Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + +The precise terms and conditions for copying, distribution and +modification follow. + +GNU GENERAL PUBLIC LICENSE +TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + +0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + +1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + +2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + +a) You must cause the modified files to carry prominent notices +stating that you changed the files and the date of any change. + +b) You must cause any work that you distribute or publish, that in +whole or in part contains or is derived from the Program or any +part thereof, to be licensed as a whole at no charge to all third +parties under the terms of this License. + +c) If the modified program normally reads commands interactively +when run, you must cause it, when started running for such +interactive use in the most ordinary way, to print or display an +announcement including an appropriate copyright notice and a +notice that there is no warranty (or else, saying that you provide +a warranty) and that users may redistribute the program under +these conditions, and telling the user how to view a copy of this +License. (Exception: if the Program itself is interactive but +does not normally print such an announcement, your work based on +the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + +3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + +a) Accompany it with the complete corresponding machine-readable +source code, which must be distributed under the terms of Sections +1 and 2 above on a medium customarily used for software interchange; or, + +b) Accompany it with a written offer, valid for at least three +years, to give any third party, for a charge no more than your +cost of physically performing source distribution, a complete +machine-readable copy of the corresponding source code, to be +distributed under the terms of Sections 1 and 2 above on a medium +customarily used for software interchange; or, + +c) Accompany it with the information you received as to the offer +to distribute corresponding source code. (This alternative is +allowed only for noncommercial distribution and only if you +received the program in object code or executable form with such +an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + +4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + +5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + +6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + +7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + +8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + +9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + +10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + +11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + +12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + +How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + +<one line to give the program's name and a brief idea of what it does.> +Copyright (C) <year> <name of author> + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along +with this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + +Gnomovision version 69, Copyright (C) year name of author +Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. +This is free software, and you are welcome to redistribute it +under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + +Yoyodyne, Inc., hereby disclaims all copyright interest in the program +`Gnomovision' (which makes passes at compilers) written by James Hacker. + +<signature of Ty Coon>, 1 April 1989 +Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/plugins/jetpack/jetpack_vendor/automattic/jetpack-publicize/SECURITY.md b/plugins/jetpack/jetpack_vendor/automattic/jetpack-publicize/SECURITY.md new file mode 100644 index 00000000..b4b46c0e --- /dev/null +++ b/plugins/jetpack/jetpack_vendor/automattic/jetpack-publicize/SECURITY.md @@ -0,0 +1,38 @@ +# Security Policy + +Full details of the Automattic Security Policy can be found on [automattic.com](https://automattic.com/security/). + +## Supported Versions + +Generally, only the latest version of Jetpack has continued support. If a critical vulnerability is found in the current version of Jetpack, we may opt to backport any patches to previous versions. + +## Reporting a Vulnerability + +[Jetpack](https://jetpack.com/) is an open-source plugin for WordPress. Our HackerOne program covers the plugin software, as well as a variety of related projects and infrastructure. + +**For responsible disclosure of security issues and to be eligible for our bug bounty program, please submit your report via the [HackerOne](https://hackerone.com/automattic) portal.** + +Our most critical targets are: + +* Jetpack and the Jetpack composer packages (all within this repo) +* Jetpack.com -- the primary marketing site. +* cloud.jetpack.com -- a management site. +* wordpress.com -- the shared management site for both Jetpack and WordPress.com sites. + +For more targets, see the `In Scope` section on [HackerOne](https://hackerone.com/automattic). + +_Please note that the **WordPress software is a separate entity** from Automattic. Please report vulnerabilities for WordPress through [the WordPress Foundation's HackerOne page](https://hackerone.com/wordpress)._ + +## Guidelines + +We're committed to working with security researchers to resolve the vulnerabilities they discover. You can help us by following these guidelines: + +* Follow [HackerOne's disclosure guidelines](https://www.hackerone.com/disclosure-guidelines). +* Pen-testing Production: + * Please **setup a local environment** instead whenever possible. Most of our code is open source (see above). + * If that's not possible, **limit any data access/modification** to the bare minimum necessary to reproduce a PoC. + * **_Don't_ automate form submissions!** That's very annoying for us, because it adds extra work for the volunteers who manage those systems, and reduces the signal/noise ratio in our communication channels. + * To be eligible for a bounty, all of these guidelines must be followed. +* Be Patient - Give us a reasonable time to correct the issue before you disclose the vulnerability. + +We also expect you to comply with all applicable laws. You're responsible to pay any taxes associated with your bounties. diff --git a/plugins/jetpack/jetpack_vendor/automattic/jetpack-publicize/src/class-connections-post-field.php b/plugins/jetpack/jetpack_vendor/automattic/jetpack-publicize/src/class-connections-post-field.php new file mode 100644 index 00000000..0f44436e --- /dev/null +++ b/plugins/jetpack/jetpack_vendor/automattic/jetpack-publicize/src/class-connections-post-field.php @@ -0,0 +1,464 @@ +<?php +/** + * Registers the API field for Publicize connections. + * + * @package automattic/jetpack-publicize + */ + +namespace Automattic\Jetpack\Publicize; + +/** + * The class to register the field and augment requests + * to Publicize supported post types. + */ +class Connections_Post_Field { + + const FIELD_NAME = 'jetpack_publicize_connections'; + + /** + * Array of post IDs that have been updated. + * + * @var array + */ + private $meta_saved = array(); + + /** + * Used to memoize the updates for a given post. + * + * @var array + */ + public $memoized_updates = array(); + + /** + * Registers the jetpack_publicize_connections field. Called + * automatically on `rest_api_init()`. + */ + public function register_fields() { + $post_types = get_post_types_by_support( 'publicize' ); + foreach ( $post_types as $post_type ) { + // Adds meta support for those post types that don't already have it. + // Only runs during REST API requests, so it doesn't impact UI. + if ( ! post_type_supports( $post_type, 'custom-fields' ) ) { + add_post_type_support( $post_type, 'custom-fields' ); + } + + // We use these hooks and not the update_callback because we must updateth meta + // before we set the post as published, otherwise the wrong connections could be used. + add_filter( 'rest_pre_insert_' . $post_type, array( $this, 'rest_pre_insert' ), 10, 2 ); + add_action( 'rest_insert_' . $post_type, array( $this, 'rest_insert' ), 10, 3 ); + + register_rest_field( + $post_type, + self::FIELD_NAME, + array( + 'get_callback' => array( $this, 'get' ), + 'schema' => $this->get_schema(), + ) + ); + } + } + + /** + * Defines data structure and what elements are visible in which contexts + */ + public function get_schema() { + return array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'jetpack-publicize-post-connections', + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => $this->post_connection_schema(), + 'default' => array(), + ); + } + + /** + * Schema for the endpoint. + */ + private function post_connection_schema() { + return array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'jetpack-publicize-post-connection', + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the Publicize Connection', 'jetpack-publicize-pkg' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'service_name' => array( + 'description' => __( 'Alphanumeric identifier for the Publicize Service', 'jetpack-publicize-pkg' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'display_name' => array( + 'description' => __( 'Username of the connected account', 'jetpack-publicize-pkg' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'profile_picture' => array( + 'description' => __( 'Profile picture of the connected account', 'jetpack-publicize-pkg' ), + 'type' => 'string', + 'context' => array( 'edit' ), + 'readonly' => true, + ), + 'enabled' => array( + 'description' => __( 'Whether to share to this connection', 'jetpack-publicize-pkg' ), + 'type' => 'boolean', + 'context' => array( 'edit' ), + ), + 'done' => array( + 'description' => __( 'Whether Publicize has already finished sharing for this post', 'jetpack-publicize-pkg' ), + 'type' => 'boolean', + 'context' => array( 'edit' ), + 'readonly' => true, + ), + 'toggleable' => array( + 'description' => __( 'Whether `enable` can be changed for this post/connection', 'jetpack-publicize-pkg' ), + 'type' => 'boolean', + 'context' => array( 'edit' ), + 'readonly' => true, + ), + ), + ); + } + + /** + * Permission check, based on module availability and user capabilities. + * + * @param int $post_id Post ID. + * + * @return true|WP_Error + */ + public function permission_check( $post_id ) { + global $publicize; + + if ( ! $publicize ) { + return new \WP_Error( + 'publicize_not_available', + __( 'Sorry, Publicize is not available on your site right now.', 'jetpack-publicize-pkg' ), + array( 'status' => rest_authorization_required_code() ) + ); + } + + if ( $publicize->current_user_can_access_publicize_data( $post_id ) ) { + return true; + } + + return new \WP_Error( + 'invalid_user_permission_publicize', + __( 'Sorry, you are not allowed to access Publicize data for this post.', 'jetpack-publicize-pkg' ), + array( 'status' => rest_authorization_required_code() ) + ); + } + + /** + * The field's wrapped getter. Does permission checks and output preparation. + * + * This cannot be extended: implement `->get()` instead. + * + * @param mixed $post_array Probably an array. Whatever the endpoint returns. + * @param string $field_name Should always match `->field_name`. + * @param WP_REST_Request $request WP API request. + * @param string $object_type Should always match `->object_type`. + * + * @return mixed + */ + public function get( $post_array, $field_name, $request, $object_type ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable + global $publicize; + + $full_schema = $this->get_schema(); + $permission_check = $this->permission_check( empty( $post_array['id'] ) ? 0 : $post_array['id'] ); + if ( is_wp_error( $permission_check ) ) { + return $full_schema['default']; + } + + $schema = $full_schema['items']; + $properties = array_keys( $schema['properties'] ); + $connections = $publicize->get_filtered_connection_data( $post_array['id'] ); + + $output_connections = array(); + foreach ( $connections as $connection ) { + $output_connection = array(); + foreach ( $properties as $property ) { + if ( isset( $connection[ $property ] ) ) { + $output_connection[ $property ] = $connection[ $property ]; + } + } + + $output_connection['id'] = (string) $connection['unique_id']; + + $output_connections[] = $output_connection; + } + + // TODO: Work out if this is necessary. We shouldn't be creating an invalid value here. + $is_valid = rest_validate_value_from_schema( $output_connections, $full_schema, self::FIELD_NAME ); + if ( is_wp_error( $is_valid ) ) { + return $is_valid; + } + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + return $this->filter_response_by_context( $output_connections, $full_schema, $context ); + } + + /** + * Prior to updating the post, first calculate which Services to + * Publicize to and which to skip. + * + * @param object $post Post data to insert/update. + * @param WP_REST_Request $request API request. + * + * @return Filtered $post + */ + public function rest_pre_insert( $post, $request ) { + if ( ! isset( $request['jetpack_publicize_connections'] ) ) { + return $post; + } + + $permission_check = $this->permission_check( empty( $post->ID ) ? 0 : $post->ID ); + if ( is_wp_error( $permission_check ) ) { + return $permission_check; + } + // memoize. + $this->get_meta_to_update( $request['jetpack_publicize_connections'], isset( $post->ID ) ? $post->ID : 0 ); + + if ( isset( $post->ID ) ) { + // Set the meta before we mark the post as published so that publicize works as expected. + // If this is not the case post end up on social media when they are marked as skipped. + $this->update( $request['jetpack_publicize_connections'], $post ); + } + + return $post; + } + + /** + * After creating a new post, update our cached data to reflect + * the new post ID. + * + * @param WP_Post $post Post data to update. + * @param WP_REST_Request $request API request. + * @param bool $is_new Is this a new post. + */ + public function rest_insert( $post, $request, $is_new ) { + if ( ! $is_new ) { + // An existing post was edited - no need to update + // our cache - we started out knowing the correct + // post ID. + return; + } + + if ( ! isset( $request['jetpack_publicize_connections'] ) ) { + return; + } + + if ( ! isset( $this->memoized_updates[0] ) ) { + return; + } + + $this->memoized_updates[ $post->ID ] = $this->memoized_updates[0]; + unset( $this->memoized_updates[0] ); + } + + /** + * Get list of meta data to update per post ID. + * + * @param array $requested_connections Publicize connections to update. + * Items are either `{ id: (string) }` or `{ service_name: (string) }`. + * @param int $post_id Post ID. + */ + protected function get_meta_to_update( $requested_connections, $post_id = 0 ) { + global $publicize; + + if ( ! $publicize ) { + return array(); + } + + if ( isset( $this->memoized_updates[ $post_id ] ) ) { + return $this->memoized_updates[ $post_id ]; + } + + $available_connections = $publicize->get_filtered_connection_data( $post_id ); + + $changed_connections = array(); + + // Build lookup mappings. + $available_connections_by_unique_id = array(); + $available_connections_by_service_name = array(); + foreach ( $available_connections as $available_connection ) { + $available_connections_by_unique_id[ $available_connection['unique_id'] ] = $available_connection; + + if ( ! isset( $available_connections_by_service_name[ $available_connection['service_name'] ] ) ) { + $available_connections_by_service_name[ $available_connection['service_name'] ] = array(); + } + $available_connections_by_service_name[ $available_connection['service_name'] ][] = $available_connection; + } + + // Handle { service_name: $service_name, enabled: (bool) }. + // If the service is not available, it will be skipped. + foreach ( $requested_connections as $requested_connection ) { + if ( ! isset( $requested_connection['service_name'] ) ) { + continue; + } + + if ( ! isset( $available_connections_by_service_name[ $requested_connection['service_name'] ] ) ) { + continue; + } + + foreach ( $available_connections_by_service_name[ $requested_connection['service_name'] ] as $available_connection ) { + $changed_connections[ $available_connection['unique_id'] ] = $requested_connection['enabled']; + } + } + + // Handle { id: $id, enabled: (bool) } + // These override the service_name settings. + foreach ( $requested_connections as $requested_connection ) { + if ( ! isset( $requested_connection['id'] ) ) { + continue; + } + + if ( ! isset( $available_connections_by_unique_id[ $requested_connection['id'] ] ) ) { + continue; + } + + $changed_connections[ $requested_connection['id'] ] = $requested_connection['enabled']; + } + + // Set all changed connections to their new value. + foreach ( $changed_connections as $unique_id => $enabled ) { + $connection = $available_connections_by_unique_id[ $unique_id ]; + + if ( $connection['done'] || ! $connection['toggleable'] ) { + continue; + } + + $available_connections_by_unique_id[ $unique_id ]['enabled'] = $enabled; + } + + $meta_to_update = array(); + // For all connections, ensure correct post_meta. + foreach ( $available_connections_by_unique_id as $unique_id => $available_connection ) { + if ( $available_connection['enabled'] ) { + $meta_to_update[ $publicize->POST_SKIP . $unique_id ] = null; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase + } else { + $meta_to_update[ $publicize->POST_SKIP . $unique_id ] = 1; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase + } + } + + $this->memoized_updates[ $post_id ] = $meta_to_update; + + return $meta_to_update; + } + + /** + * Update the connections slated to be shared to. + * + * @param array $requested_connections Publicize connections to update. + * Items are either `{ id: (string) }` or `{ service_name: (string) }`. + * @param WP_Post $post Post data. + */ + public function update( $requested_connections, $post ) { + if ( isset( $this->meta_saved[ $post->ID ] ) ) { // Make sure we only save it once - per request. + return; + } + foreach ( $this->get_meta_to_update( $requested_connections, $post->ID ) as $meta_key => $meta_value ) { + if ( $meta_value === null ) { + delete_post_meta( $post->ID, $meta_key ); + } else { + update_post_meta( $post->ID, $meta_key, $meta_value ); + } + } + $this->meta_saved[ $post->ID ] = true; + } + + /** + * Removes properties that should not appear in the current + * request's context + * + * $context is a Core REST API Framework request attribute that is + * always one of: + * * view (what you see on the blog) + * * edit (what you see in an editor) + * * embed (what you see in, e.g., an oembed) + * + * Fields (and sub-fields, and sub-sub-...) can be flagged for a + * set of specific contexts via the field's schema. + * + * The Core API will filter out top-level fields with the wrong + * context, but will not recurse deeply enough into arrays/objects + * to remove all levels of sub-fields with the wrong context. + * + * This function handles that recursion. + * + * @param mixed $value Value passed to API request. + * @param array $schema Schema to validate against. + * @param string $context REST API Request context. + * + * @return mixed Filtered $value + */ + public function filter_response_by_context( $value, $schema, $context ) { + if ( ! $this->is_valid_for_context( $schema, $context ) ) { + // We use this intentionally odd looking WP_Error object + // internally only in this recursive function (see below + // in the `object` case). It will never be output by the REST API. + // If we return this for the top level object, Core + // correctly remove the top level object from the response + // for us. + return new \WP_Error( '__wrong-context__' ); + } + + switch ( $schema['type'] ) { + case 'array': + if ( ! isset( $schema['items'] ) ) { + return $value; + } + + // Shortcircuit if we know none of the items are valid for this context. + // This would only happen in a strangely written schema. + if ( ! $this->is_valid_for_context( $schema['items'], $context ) ) { + return array(); + } + + // Recurse to prune sub-properties of each item. + foreach ( $value as $key => $item ) { + $value[ $key ] = $this->filter_response_by_context( $item, $schema['items'], $context ); + } + + return $value; + case 'object': + if ( ! isset( $schema['properties'] ) ) { + return $value; + } + + foreach ( $value as $field_name => $field_value ) { + if ( isset( $schema['properties'][ $field_name ] ) ) { + $field_value = $this->filter_response_by_context( $field_value, $schema['properties'][ $field_name ], $context ); + if ( is_wp_error( $field_value ) && '__wrong-context__' === $field_value->get_error_code() ) { + unset( $value[ $field_name ] ); + } else { + // Respect recursion that pruned sub-properties of each property. + $value[ $field_name ] = $field_value; + } + } + } + + return (object) $value; + } + + return $value; + } + + /** + * Ensure that our request matches its expected context. + * + * @param array $schema Schema to validate against. + * @param string $context REST API Request context. + * @return bool + */ + private function is_valid_for_context( $schema, $context ) { + return empty( $schema['context'] ) || in_array( $context, $schema['context'], true ); + } + +} diff --git a/plugins/jetpack/jetpack_vendor/automattic/jetpack-publicize/src/class-keyring-helper.php b/plugins/jetpack/jetpack_vendor/automattic/jetpack-publicize/src/class-keyring-helper.php new file mode 100644 index 00000000..a92b6f51 --- /dev/null +++ b/plugins/jetpack/jetpack_vendor/automattic/jetpack-publicize/src/class-keyring-helper.php @@ -0,0 +1,297 @@ +<?php +/** + * Keyring helper. + * + * @package automattic/jetpack-publicize + */ + +namespace Automattic\Jetpack\Publicize; + +use Automattic\Jetpack\Connection\Secrets; +use Automattic\Jetpack\Paths; +use Jetpack_IXR_Client; +use Jetpack_Options; + +/** + * A series of utilities to interact with a Keyring instance. + */ +class Keyring_Helper { + /** + * Class instance + * + * @var \Automattic\Jetpack\Publicize\Keyring_Helper + */ + private static $instance = null; + + /** + * Whether the `sharing` page is registered. + * + * @var bool + */ + private static $is_sharing_page_registered = false; + + /** + * Initialize instance. + */ + public static function init() { + if ( self::$instance === null ) { + self::$instance = new Keyring_Helper(); + } + + return self::$instance; + } + + const SERVICES = array( + 'facebook' => array( + 'for' => 'publicize', + ), + 'twitter' => array( + 'for' => 'publicize', + ), + 'linkedin' => array( + 'for' => 'publicize', + ), + 'tumblr' => array( + 'for' => 'publicize', + ), + 'path' => array( + 'for' => 'publicize', + ), + 'google_plus' => array( + 'for' => 'publicize', + ), + 'google_site_verification' => array( + 'for' => 'other', + ), + ); + + /** + * Constructor + */ + private function __construct() { + add_action( 'admin_menu', array( __CLASS__, 'register_sharing_page' ) ); + + add_action( 'load-settings_page_sharing', array( __CLASS__, 'admin_page_load' ), 9 ); + } + + /** + * We need a `sharing` page to be able to connect and disconnect services. + */ + public static function register_sharing_page() { + if ( self::$is_sharing_page_registered ) { + return; + } + + self::$is_sharing_page_registered = true; + + if ( ! current_user_can( 'manage_options' ) ) { + return; + } + + global $_registered_pages; + + require_once ABSPATH . 'wp-admin/includes/plugin.php'; + + $hookname = get_plugin_page_hookname( 'sharing', 'options-general.php' ); + add_action( $hookname, array( __CLASS__, 'admin_page_load' ) ); + $_registered_pages[ $hookname ] = true; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited + } + + /** + * Gets a URL to the public-api actions. Works like WP's admin_url. + * On WordPress.com this is/calls Keyring::admin_url. + * + * @param string $service Shortname of a specific service. + * @param array $params Parameters to append to an API connection URL. + * + * @return URL to specific public-api process + */ + private static function api_url( $service = false, $params = array() ) { + /** + * Filters the API URL used to interact with WordPress.com. + * + * @since 2.0.0 + * + * @param string https://public-api.wordpress.com/connect/?jetpack=publicize Default Publicize API URL. + */ + $url = apply_filters( 'publicize_api_url', 'https://public-api.wordpress.com/connect/?jetpack=publicize' ); + + if ( $service ) { + $url = add_query_arg( array( 'service' => $service ), $url ); + } + + if ( count( $params ) ) { + $url = add_query_arg( $params, $url ); + } + + return $url; + } + + /** + * Build a connection URL (sharing settings page with unique query args to create a connection). + * + * @param string $service_name Service name. + * @param string $for Feature name. + */ + public static function connect_url( $service_name, $for ) { + return add_query_arg( + array( + 'action' => 'request', + 'service' => $service_name, + 'kr_nonce' => wp_create_nonce( 'keyring-request' ), + 'nonce' => wp_create_nonce( "keyring-request-$service_name" ), + 'for' => $for, + ), + admin_url( 'options-general.php?page=sharing' ) + ); + } + + /** + * Build a URL to refresh a connection (sharing settings page with unique query args to refresh a connection). + * Similar to connect_url, but with a refresh parameter. + * + * @param string $service_name Service name. + * @param string $for Feature name. + */ + public static function refresh_url( $service_name, $for ) { + return add_query_arg( + array( + 'action' => 'request', + 'service' => $service_name, + 'kr_nonce' => wp_create_nonce( 'keyring-request' ), + 'refresh' => 1, + 'for' => $for, + 'nonce' => wp_create_nonce( "keyring-request-$service_name" ), + ), + admin_url( 'options-general.php?page=sharing' ) + ); + } + + /** + * Build a URL to delete a connection (sharing settings page with unique query args to delete a connection). + * + * @param string $service_name Service name. + * @param string $id Connection ID. + */ + public static function disconnect_url( $service_name, $id ) { + return add_query_arg( + array( + 'action' => 'delete', + 'service' => $service_name, + 'id' => $id, + 'kr_nonce' => wp_create_nonce( 'keyring-request' ), + 'nonce' => wp_create_nonce( "keyring-request-$service_name" ), + ), + admin_url( 'options-general.php?page=sharing' ) + ); + } + + /** + * Build contents handling Keyring connection management into Sharing settings screen. + */ + public static function admin_page_load() { + if ( isset( $_GET['action'] ) ) { + if ( isset( $_GET['service'] ) ) { + $service_name = filter_var( wp_unslash( $_GET['service'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- We verify below. + } + + switch ( $_GET['action'] ) { + + case 'request': + check_admin_referer( 'keyring-request', 'kr_nonce' ); + check_admin_referer( "keyring-request-$service_name", 'nonce' ); + + $verification = ( new Secrets() )->generate( 'publicize' ); + if ( ! $verification ) { + $url = ( new Paths() )->admin_url( 'page=jetpack#/settings' ); + wp_die( + sprintf( + wp_kses( + /* Translators: placeholder is a URL to a Settings page. */ + __( "Jetpack is not connected. Please connect Jetpack by visiting <a href='%s'>Settings</a>.", 'jetpack-publicize-pkg' ), + array( + 'a' => array( + 'href' => array(), + ), + ) + ), + esc_url( $url ) + ) + ); + + } + $stats_options = get_option( 'stats_options' ); + $wpcom_blog_id = Jetpack_Options::get_option( 'id' ); + $wpcom_blog_id = ! empty( $wpcom_blog_id ) ? $wpcom_blog_id : $stats_options['blog_id']; + + $user = wp_get_current_user(); + $redirect = self::api_url( + $service_name, + urlencode_deep( + array( + 'action' => 'request', + 'redirect_uri' => add_query_arg( array( 'action' => 'done' ), menu_page_url( 'sharing', false ) ), + 'for' => 'publicize', + // required flag that says this connection is intended for publicize. + 'siteurl' => site_url(), + 'state' => $user->ID, + 'blog_id' => $wpcom_blog_id, + 'secret_1' => $verification['secret_1'], + 'secret_2' => $verification['secret_2'], + 'eol' => $verification['exp'], + ) + ) + ); + wp_redirect( $redirect ); // phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect -- The API URL is an external URL and is filterable. + exit; + + case 'completed': + /* + * We do not use a nonce here, + * since we're populating a local cache of + * the Publicize connections that were created and stored on WordPress.com. + */ + $xml = new Jetpack_IXR_Client(); + $xml->query( 'jetpack.fetchPublicizeConnections' ); + + if ( ! $xml->isError() ) { + $response = $xml->getResponse(); + Jetpack_Options::update_option( 'publicize_connections', $response ); + } + + break; + + case 'delete': + $id = isset( $_GET['id'] ) ? filter_var( wp_unslash( $_GET['id'] ) ) : null; + + check_admin_referer( 'keyring-request', 'kr_nonce' ); + check_admin_referer( "keyring-request-$service_name", 'nonce' ); + + self::disconnect( $service_name, $id ); + + do_action( 'connection_disconnected', $service_name ); + break; + } + } + } + + /** + * Remove a Publicize connection + * + * @param string $service_name Service name. + * @param string $connection_id Connection ID. + * @param int|bool $_blog_id Blog ID. + * @param int|bool $_user_id User ID. + * @param bool $force_delete Force delete the connection. + */ + public static function disconnect( $service_name, $connection_id, $_blog_id = false, $_user_id = false, $force_delete = false ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable + $xml = new Jetpack_IXR_Client(); + $xml->query( 'jetpack.deletePublicizeConnection', $connection_id ); + + if ( ! $xml->isError() ) { + Jetpack_Options::update_option( 'publicize_connections', $xml->getResponse() ); + } else { + return false; + } + } +} diff --git a/plugins/jetpack/jetpack_vendor/automattic/jetpack-publicize/src/class-publicize-base.php b/plugins/jetpack/jetpack_vendor/automattic/jetpack-publicize/src/class-publicize-base.php new file mode 100644 index 00000000..83ed9c42 --- /dev/null +++ b/plugins/jetpack/jetpack_vendor/automattic/jetpack-publicize/src/class-publicize-base.php @@ -0,0 +1,1452 @@ +<?php +/** + * Publicize_Base class. + * + * @package automattic/jetpack-publicize + */ + +// phpcs:disable WordPress.NamingConventions.ValidVariableName + +namespace Automattic\Jetpack\Publicize; + +use Automattic\Jetpack\Redirect; +use Automattic\Jetpack\Status; + +/** + * Base class for Publicize. + */ +abstract class Publicize_Base { + + /** + * Services that are currently connected to the given user + * through Publicize. + * + * @var array + */ + public $connected_services = array(); + + /** + * Services that are supported by publicize. They don't + * necessarily need to be connected to the current user. + * + * @var array + */ + public $services; + + /** + * Post meta key for admin page. + * + * @var string + */ + public $ADMIN_PAGE = 'wpas'; + + /** + * Post meta key for post message. + * + * @var string + */ + public $POST_MESS = '_wpas_mess'; + + /** + * Post meta key for flagging when the post is a tweetstorm. + * + * @var string + */ + public $POST_TWEETSTORM = '_wpas_is_tweetstorm'; + + /** + * Post meta key for the flagging when the post share feature is disabled. + * + * @var string + */ + const POST_PUBLICIZE_FEATURE_ENABLED = '_wpas_feature_enabled'; + + /** + * Connection ID appended to indicate that a connection should NOT be publicized to. + * + * @var string + */ + public $POST_SKIP = '_wpas_skip_'; + + /** + * Connection ID appended to indicate a connection has already been publicized to. + * + * @var string + */ + public $POST_DONE = '_wpas_done_'; + + /** + * Prefix for user authorization (used in publicize-wpcom.php) + * + * @var string + */ + public $USER_AUTH = 'wpas_authorize'; + + /** + * Prefix for user opt. + * + * @var string + */ + public $USER_OPT = 'wpas_'; + + /** + * Ready for Publicize to do its thing. + * + * @var string + */ + public $PENDING = '_publicize_pending'; + + /** + * Array of external IDs where we've Publicized. + * + * @var string + */ + public $POST_SERVICE_DONE = '_publicize_done_external'; + + /** + * Default pieces of the message used in constructing the + * content pushed out to other social networks. + */ + + /** + * Default prefix. + * + * @var string + */ + public $default_prefix = ''; + + /** + * Default message. + * + * @var string + */ + public $default_message = '%title%'; + + /** + * Default suffix. + * + * @var string + */ + public $default_suffix = ' '; + + /** + * What WP capability is require to create/delete global connections? + * All users with this cap can un-globalize all other global connections, and globalize any of their own + * Globalized connections cannot be unselected by users without this capability when publishing + * + * @var string + */ + public $GLOBAL_CAP = 'publish_posts'; + + /** + * Sets up the basics of Publicize. + */ + public function __construct() { + $this->default_message = self::build_sprintf( + array( + /** + * Filter the default Publicize message. + * + * @module publicize + * + * @since 2.0.0 + * + * @param string $this->default_message Publicize's default message. Default is the post title. + */ + apply_filters( 'wpas_default_message', $this->default_message ), + 'title', + 'url', + ) + ); + + $this->default_prefix = self::build_sprintf( + array( + /** + * Filter the message prepended to the Publicize custom message. + * + * @module publicize + * + * @since 2.0.0 + * + * @param string $this->default_prefix String prepended to the Publicize custom message. + */ + apply_filters( 'wpas_default_prefix', $this->default_prefix ), + 'url', + ) + ); + + $this->default_suffix = self::build_sprintf( + array( + /** + * Filter the message appended to the Publicize custom message. + * + * @module publicize + * + * @since 2.0.0 + * + * @param string $this->default_suffix String appended to the Publicize custom message. + */ + apply_filters( 'wpas_default_suffix', $this->default_suffix ), + 'url', + ) + ); + + /** + * Filter the capability to change global Publicize connection options. + * + * All users with this cap can un-globalize all other global connections, and globalize any of their own + * Globalized connections cannot be unselected by users without this capability when publishing. + * + * @module publicize + * + * @since 2.2.1 + * + * @param string $this->GLOBAL_CAP default capability in control of global Publicize connection options. Default to edit_others_posts. + */ + $this->GLOBAL_CAP = apply_filters( 'jetpack_publicize_global_connections_cap', $this->GLOBAL_CAP ); + + // stage 1 and 2 of 3-stage Publicize. Flag for Publicize on creation, save meta, + // then check meta and publicize based on that. stage 3 implemented on wpcom. + add_action( 'transition_post_status', array( $this, 'flag_post_for_publicize' ), 10, 3 ); + add_action( 'save_post', array( $this, 'save_meta' ), 20, 2 ); + + // Default checkbox state for each Connection. + add_filter( 'publicize_checkbox_default', array( $this, 'publicize_checkbox_default' ), 10, 2 ); + + // Alter the "Post Publish" admin notice to mention the Connections we Publicized to. + add_filter( 'post_updated_messages', array( $this, 'update_published_message' ), 20, 1 ); + + // Connection test callback. + add_action( 'wp_ajax_test_publicize_conns', array( $this, 'test_publicize_conns' ) ); + + add_action( 'init', array( $this, 'add_post_type_support' ) ); + add_action( 'init', array( $this, 'register_post_meta' ), 20 ); + } + + /** + * Services: Facebook, Twitter, etc. + */ + + /** + * Get services for the given blog and user. + * + * Can return all available services or just the ones with an active connection. + * + * @param string $filter Type of filter. + * 'all' (default) - Get all services available for connecting. + * 'connected' - Get all services currently connected. + * @param false|int $_blog_id The blog ID. Use false (default) for the current blog. + * @param false|int $_user_id The user ID. Use false (default) for the current user. + * @return array + */ + abstract public function get_services( $filter = 'all', $_blog_id = false, $_user_id = false ); + + /** + * Does the given user have a connection to the service on the given blog? + * + * @param string $service_name 'facebook', 'twitter', etc. + * @param false|int $_blog_id The blog ID. Use false (default) for the current blog. + * @param false|int $_user_id The user ID. Use false (default) for the current user. + * @return bool + */ + public function is_enabled( $service_name, $_blog_id = false, $_user_id = false ) { + if ( ! $_blog_id ) { + $_blog_id = $this->blog_id(); + } + + if ( ! $_user_id ) { + $_user_id = $this->user_id(); + } + + $connections = $this->get_connections( $service_name, $_blog_id, $_user_id ); + return ( is_array( $connections ) && count( $connections ) > 0 ? true : false ); + } + + /** + * Generates a connection URL. + * + * This is the URL, which, when visited by the user, starts the authentication + * process required to forge a connection. + * + * @param string $service_name 'facebook', 'twitter', etc. + * @return string + */ + abstract public function connect_url( $service_name ); + + /** + * Generates a Connection refresh URL. + * + * This is the URL, which, when visited by the user, re-authenticates their + * connection to the service. + * + * @param string $service_name 'facebook', 'twitter', etc. + * @return string + */ + abstract public function refresh_url( $service_name ); + + /** + * Generates a disconnection URL. + * + * This is the URL, which, when visited by the user, breaks their connection + * with the service. + * + * @param string $service_name 'facebook', 'twitter', etc. + * @param string $connection_id Connection ID. + * @return string + */ + abstract public function disconnect_url( $service_name, $connection_id ); + + /** + * Returns a display name for the Service + * + * @param string $service_name 'facebook', 'twitter', etc. + * @return string + */ + public static function get_service_label( $service_name ) { + switch ( $service_name ) { + case 'linkedin': + return 'LinkedIn'; + case 'google_drive': // google-drive used to be called google_drive. + case 'google-drive': + return 'Google Drive'; + case 'twitter': + case 'facebook': + case 'tumblr': + default: + return ucfirst( $service_name ); + } + } + + /** + * Connections: For each Service, there can be multiple connections + * for a given user. For example, one user could be connected to Twitter + * as both @jetpack and as @wordpressdotcom + * + * For historical reasons, Connections are represented as an object + * on WordPress.com and as an array in Jetpack. + */ + + /** + * Get the active Connections of a Service + * + * @param string $service_name 'facebook', 'twitter', etc. + * @param false|int $_blog_id The blog ID. Use false (default) for the current blog. + * @param false|int $_user_id The user ID. Use false (default) for the current user. + * @return false|object[]|array[] false if no connections exist + */ + abstract public function get_connections( $service_name, $_blog_id = false, $_user_id = false ); + + /** + * Get a single Connection of a Service + * + * @param string $service_name 'facebook', 'twitter', etc. + * @param string $connection_id Connection ID. + * @param false|int $_blog_id The blog ID. Use false (default) for the current blog. + * @param false|int $_user_id The user ID. Use false (default) for the current user. + * @return false|object[]|array[] false if no connections exist + */ + abstract public function get_connection( $service_name, $connection_id, $_blog_id = false, $_user_id = false ); + + /** + * Get the Connection ID. + * + * Note that this is different than the Connection's uniqueid. + * + * Via a quirk of history, ID is globally unique and unique_id + * is only unique per site. + * + * @param object|array $connection The Connection object (WordPress.com) or array (Jetpack). + * @return string + */ + abstract public function get_connection_id( $connection ); + + /** + * Get the Connection unique_id + * + * Note that this is different than the Connections ID. + * + * Via a quirk of history, ID is globally unique and unique_id + * is only unique per site. + * + * @param object|array $connection The Connection object (WordPress.com) or array (Jetpack). + * @return string + */ + abstract public function get_connection_unique_id( $connection ); + + /** + * Get the Connection's Meta data + * + * @param object|array $connection Connection. + * @return array Connection Meta + */ + abstract public function get_connection_meta( $connection ); + + /** + * Disconnect a Connection + * + * @param string $service_name 'facebook', 'twitter', etc. + * @param string $connection_id Connection ID. + * @param false|int $_blog_id The blog ID. Use false (default) for the current blog. + * @param false|int $_user_id The user ID. Use false (default) for the current user. + * @param bool $force_delete Whether to skip permissions checks. + * @return false|void False on failure. Void on success. + */ + abstract public function disconnect( $service_name, $connection_id, $_blog_id = false, $_user_id = false, $force_delete = false ); + + /** + * Globalizes a Connection + * + * @param string $connection_id Connection ID. + * @return bool Falsey on failure. Truthy on success. + */ + abstract public function globalize_connection( $connection_id ); + + /** + * Unglobalizes a Connection + * + * @param string $connection_id Connection ID. + * @return bool Falsey on failure. Truthy on success. + */ + abstract public function unglobalize_connection( $connection_id ); + + /** + * Returns an external URL to the Connection's profile + * + * @param string $service_name 'facebook', 'twitter', etc. + * @param object|array $connection The Connection object (WordPress.com) or array (Jetpack). + * @return false|string False on failure. URL on success. + */ + public function get_profile_link( $service_name, $connection ) { + $cmeta = $this->get_connection_meta( $connection ); + + if ( isset( $cmeta['connection_data']['meta']['link'] ) ) { + if ( 'facebook' === $service_name && 0 === strpos( wp_parse_url( $cmeta['connection_data']['meta']['link'], PHP_URL_PATH ), '/app_scoped_user_id/' ) ) { + // App-scoped Facebook user IDs are not usable profile links. + return false; + } + + return $cmeta['connection_data']['meta']['link']; + } + + if ( 'facebook' === $service_name && isset( $cmeta['connection_data']['meta']['facebook_page'] ) ) { + return 'https://facebook.com/' . $cmeta['connection_data']['meta']['facebook_page']; + } + + if ( 'tumblr' === $service_name && isset( $cmeta['connection_data']['meta']['tumblr_base_hostname'] ) ) { + return 'https://' . $cmeta['connection_data']['meta']['tumblr_base_hostname']; + } + + if ( 'twitter' === $service_name ) { + return 'https://twitter.com/' . substr( $cmeta['external_display'], 1 ); // Has a leading '@'. + } + + if ( 'linkedin' === $service_name ) { + if ( ! isset( $cmeta['connection_data']['meta']['profile_url'] ) ) { + return false; + } + + $profile_url_query = wp_parse_url( $cmeta['connection_data']['meta']['profile_url'], PHP_URL_QUERY ); + wp_parse_str( $profile_url_query, $profile_url_query_args ); + + $id = null; + + if ( isset( $profile_url_query_args['key'] ) ) { + $id = $profile_url_query_args['key']; + } elseif ( isset( $profile_url_query_args['id'] ) ) { + $id = $profile_url_query_args['id']; + } else { + return false; + } + + return esc_url_raw( add_query_arg( 'id', rawurlencode( $id ), 'https://www.linkedin.com/profile/view' ) ); + } + + return false; // no fallback. we just won't link it. + } + + /** + * Returns a display name for the Connection + * + * @param string $service_name 'facebook', 'twitter', etc. + * @param object|array $connection The Connection object (WordPress.com) or array (Jetpack). + * @return string + */ + public function get_display_name( $service_name, $connection ) { + $cmeta = $this->get_connection_meta( $connection ); + + if ( isset( $cmeta['connection_data']['meta']['display_name'] ) ) { + return $cmeta['connection_data']['meta']['display_name']; + } + + if ( 'tumblr' === $service_name && isset( $cmeta['connection_data']['meta']['tumblr_base_hostname'] ) ) { + return $cmeta['connection_data']['meta']['tumblr_base_hostname']; + } + + if ( 'twitter' === $service_name ) { + return $cmeta['external_display']; + } + + $connection_display = $cmeta['external_display']; + + if ( empty( $connection_display ) ) { + $connection_display = $cmeta['external_name']; + } + + return $connection_display; + } + + /** + * Returns a profile picture for the Connection + * + * @param object|array $connection The Connection object (WordPress.com) or array (Jetpack). + * @return string + */ + private function get_profile_picture( $connection ) { + $cmeta = $this->get_connection_meta( $connection ); + + if ( isset( $cmeta['profile_picture'] ) ) { + return $cmeta['profile_picture']; + } + + return ''; + } + + /** + * Whether the user needs to select additional options after connecting + * + * @param string $service_name 'facebook', 'twitter', etc. + * @param object|array $connection The Connection object (WordPress.com) or array (Jetpack). + * @return bool + */ + public function show_options_popup( $service_name, $connection ) { + $cmeta = $this->get_connection_meta( $connection ); + + // Always show if no selection has been made for Facebook. + if ( 'facebook' === $service_name && empty( $cmeta['connection_data']['meta']['facebook_profile'] ) && empty( $cmeta['connection_data']['meta']['facebook_page'] ) ) { + return true; + } + + // Always show if no selection has been made for Tumblr. + if ( 'tumblr' === $service_name && empty( $cmeta['connection_data']['meta']['tumblr_base_hostname'] ) ) { + return true; + } + + // if we have the specific connection info.. + $id = ! empty( $_GET['id'] ) ? sanitize_text_field( wp_unslash( $_GET['id'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended + + if ( $id ) { + if ( $cmeta['connection_data']['id'] === $id ) { + return true; + } + } else { + // Otherwise, just show if this is the completed step / first load. + // phpcs:disable WordPress.Security.NonceVerification.Recommended + $is_completed = ! empty( $_GET['action'] ) && 'completed' === $_GET['action']; + $service = ! empty( $_GET['service'] ) ? sanitize_text_field( wp_unslash( $_GET['service'] ) ) : false; + // phpcs:enable WordPress.Security.NonceVerification.Recommended + + if ( $is_completed && $service_name === $service && ! in_array( $service, array( 'facebook', 'tumblr' ), true ) ) { + return true; + } + } + + return false; + } + + /** + * Check if a connection is global + * + * @param array $connection Connection data. + * @return bool Whether the connection is global. + */ + public function is_global_connection( $connection ) { + return empty( $connection['connection_data']['user_id'] ); + } + + /** + * Whether the Connection is "valid" wrt Facebook's requirements. + * + * Must be connected to a Page (not a Profile). + * (Also returns true if we're in the middle of the connection process) + * + * @param object|array $connection The Connection object (WordPress.com) or array (Jetpack). + * @return bool + */ + public function is_valid_facebook_connection( $connection ) { + if ( $this->is_connecting_connection( $connection ) ) { + return true; + } + $connection_meta = $this->get_connection_meta( $connection ); + $connection_data = $connection_meta['connection_data']; + return isset( $connection_data['meta']['facebook_page'] ); + } + + /** + * LinkedIn needs to be reauthenticated to use v2 of their API. + * If it's using LinkedIn old API, it's an 'invalid' connection + * + * @param object|array $connection The Connection object (WordPress.com) or array (Jetpack). + * @return bool + */ + public function is_invalid_linkedin_connection( $connection ) { + // LinkedIn API v1 included the profile link in the connection data. + $connection_meta = $this->get_connection_meta( $connection ); + return isset( $connection_meta['connection_data']['meta']['profile_url'] ); + } + + /** + * Whether the Connection currently being connected + * + * @param object|array $connection The Connection object (WordPress.com) or array (Jetpack). + * @return bool + */ + public function is_connecting_connection( $connection ) { + $connection_meta = $this->get_connection_meta( $connection ); + $connection_data = $connection_meta['connection_data']; + return isset( $connection_data['meta']['options_responses'] ); + } + + /** + * AJAX Handler to run connection tests on all Connections + * + * @return void + */ + public function test_publicize_conns() { + wp_send_json_success( $this->get_publicize_conns_test_results() ); + } + + /** + * Run connection tests on all Connections + * + * @return array { + * Array of connection test results. + * + * @type string 'connectionID' Connection identifier string that is unique for each connection + * @type string 'serviceName' Slug of the connection's service (facebook, twitter, ...) + * @type bool 'connectionTestPassed' Whether the connection test was successful + * @type string 'connectionTestMessage' Test success or error message + * @type bool 'userCanRefresh' Whether the user can re-authenticate their connection to the service + * @type string 'refreshText' Message instructing user to re-authenticate their connection to the service + * @type string 'refreshURL' URL, which, when visited by the user, re-authenticates their connection to the service. + * @type string 'unique_id' ID string representing connection + * } + */ + public function get_publicize_conns_test_results() { + $test_results = array(); + + foreach ( (array) $this->get_services( 'connected' ) as $service_name => $connections ) { + foreach ( $connections as $connection ) { + + $id = $this->get_connection_id( $connection ); + + $connection_test_passed = true; + $connection_test_message = __( 'This connection is working correctly.', 'jetpack-publicize-pkg' ); + $user_can_refresh = false; + $refresh_text = ''; + $refresh_url = ''; + + $connection_test_result = true; + if ( method_exists( $this, 'test_connection' ) ) { + $connection_test_result = $this->test_connection( $service_name, $connection ); + } + + if ( is_wp_error( $connection_test_result ) ) { + $connection_test_passed = false; + $connection_test_message = $connection_test_result->get_error_message(); + $error_data = $connection_test_result->get_error_data(); + + $user_can_refresh = $error_data['user_can_refresh']; + $refresh_text = $error_data['refresh_text']; + $refresh_url = $error_data['refresh_url']; + } + // Mark Facebook profiles as deprecated. + if ( 'facebook' === $service_name ) { + if ( ! $this->is_valid_facebook_connection( $connection ) ) { + $connection_test_passed = false; + $user_can_refresh = false; + $connection_test_message = __( 'Please select a Facebook Page to publish updates.', 'jetpack-publicize-pkg' ); + } + } + + // LinkedIn needs reauthentication to be compatible with v2 of their API. + if ( 'linkedin' === $service_name && $this->is_invalid_linkedin_connection( $connection ) ) { + $connection_test_passed = 'must_reauth'; + $user_can_refresh = false; + $connection_test_message = esc_html__( 'Your LinkedIn connection needs to be reauthenticated to continue working – head to Sharing to take care of it.', 'jetpack-publicize-pkg' ); + } + + $unique_id = null; + + if ( ! empty( $connection->unique_id ) ) { + $unique_id = $connection->unique_id; + } elseif ( ! empty( $connection['connection_data']['token_id'] ) ) { + $unique_id = $connection['connection_data']['token_id']; + } + + $test_results[] = array( + 'connectionID' => $id, + 'serviceName' => $service_name, + 'connectionTestPassed' => $connection_test_passed, + 'connectionTestMessage' => esc_attr( $connection_test_message ), + 'userCanRefresh' => $user_can_refresh, + 'refreshText' => esc_attr( $refresh_text ), + 'refreshURL' => $refresh_url, + 'unique_id' => $unique_id, + ); + } + } + + return $test_results; + } + + /** + * Run the connection test for the Connection + * + * @param string $service_name $service_name 'facebook', 'twitter', etc. + * @param object|array $connection The Connection object (WordPress.com) or array (Jetpack). + * @return WP_Error|true WP_Error on failure. True on success + */ + abstract public function test_connection( $service_name, $connection ); + + /** + * Retrieves current list of connections and applies filters. + * + * Retrieves current available connections and checks if the connections + * have already been used to share current post. Finally, the checkbox + * form UI fields are calculated. This function exposes connection form + * data directly as array so it can be retrieved for static HTML generation + * or JSON consumption. + * + * @since 6.7.0 + * + * @param integer $selected_post_id Optional. Post ID to query connection status for. + * + * @return array { + * Array of UI setup data for connection list form. + * + * @type string 'unique_id' ID string representing connection + * @type string 'service_name' Slug of the connection's service (facebook, twitter, ...) + * @type string 'service_label' Service Label (Facebook, Twitter, ...) + * @type string 'display_name' Connection's human-readable Username: "@jetpack" + * @type string 'profile_picture' Connection profile picture. + * @type bool 'enabled' Default value for the connection (e.g., for a checkbox). + * @type bool 'done' Has this connection already been publicized to? + * @type bool 'toggleable' Is the user allowed to change the value for the connection? + * @type bool 'global' Is this connection a global one? + * } + */ + public function get_filtered_connection_data( $selected_post_id = null ) { + $connection_list = array(); + + $post = get_post( $selected_post_id ); // Defaults to current post if $post_id is null. + // Handle case where there is no current post. + if ( ! empty( $post ) ) { + $post_id = $post->ID; + } else { + $post_id = null; + } + + $services = $this->get_services( 'connected' ); + $all_done = $this->post_is_done_sharing( $post_id ); + + // We don't allow Publicizing to the same external id twice, to prevent spam. + $service_id_done = (array) get_post_meta( $post_id, $this->POST_SERVICE_DONE, true ); + + foreach ( $services as $service_name => $connections ) { + foreach ( $connections as $connection ) { + $connection_meta = $this->get_connection_meta( $connection ); + $connection_data = $connection_meta['connection_data']; + + $unique_id = $this->get_connection_unique_id( $connection ); + + // Was this connection (OR, old-format service) already Publicized to? + $done = ! empty( $post ) && ( + // New flags. + 1 === (int) get_post_meta( $post->ID, $this->POST_DONE . $unique_id, true ) + || + // Old flags. + 1 === (int) get_post_meta( $post->ID, $this->POST_DONE . $service_name, true ) + ); + + /** + * Filter whether a post should be publicized to a given service. + * + * @module publicize + * + * @since 2.0.0 + * + * @param bool true Should the post be publicized to a given service? Default to true. + * @param int $post_id Post ID. + * @param string $service_name Service name. + * @param array $connection_data Array of information about all Publicize details for the site. + */ + /* phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores */ + if ( ! apply_filters( 'wpas_submit_post?', true, $post_id, $service_name, $connection_data ) ) { + continue; + } + + // Should we be skipping this one? + $skip = ( + ( + ! empty( $post ) + && + in_array( $post->post_status, array( 'publish', 'draft', 'future' ), true ) + && + ( + // New flags. + get_post_meta( $post->ID, $this->POST_SKIP . $unique_id, true ) + || + // Old flags. + get_post_meta( $post->ID, $this->POST_SKIP . $service_name ) + ) + ) + || + ( + is_array( $connection ) + && + isset( $connection_meta['external_id'] ) && ! empty( $service_id_done[ $service_name ][ $connection_meta['external_id'] ] ) + ) + ); + + // If this one has already been publicized to, don't let it happen again. + $toggleable = ! $done && ! $all_done; + + // Determine the state of the checkbox (on/off) and allow filtering. + $enabled = $done || ! $skip; + /** + * Filter the checkbox state of each Publicize connection appearing in the post editor. + * + * @module publicize + * + * @since 2.0.1 + * + * @param bool $enabled Should the Publicize checkbox be enabled for a given service. + * @param int $post_id Post ID. + * @param string $service_name Service name. + * @param array $connection Array of connection details. + */ + $enabled = apply_filters( 'publicize_checkbox_default', $enabled, $post_id, $service_name, $connection ); + + /** + * If this is a global connection and this user doesn't have enough permissions to modify + * those connections, don't let them change it. + */ + if ( ! $done && $this->is_global_connection( $connection_meta ) && ! current_user_can( $this->GLOBAL_CAP ) ) { + $toggleable = false; + + /** + * Filters the checkboxes for global connections with non-prilvedged users. + * + * @module publicize + * + * @since 3.7.0 + * + * @param bool $enabled Indicates if this connection should be enabled. Default true. + * @param int $post_id ID of the current post + * @param string $service_name Name of the connection (Facebook, Twitter, etc) + * @param array $connection Array of data about the connection. + */ + $enabled = apply_filters( 'publicize_checkbox_global_default', $enabled, $post_id, $service_name, $connection ); + } + + // Force the checkbox to be checked if the post was DONE, regardless of what the filter does. + if ( $done ) { + $enabled = true; + } + + $connection_list[] = array( + 'unique_id' => $unique_id, + 'service_name' => $service_name, + 'service_label' => $this->get_service_label( $service_name ), + 'display_name' => $this->get_display_name( $service_name, $connection ), + 'profile_picture' => $this->get_profile_picture( $connection ), + + 'enabled' => $enabled, + 'done' => $done, + 'toggleable' => $toggleable, + 'global' => 0 == $connection_data['user_id'], // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual,WordPress.PHP.StrictComparisons.LooseComparison -- Other types can be used at times. + ); + } + } + + return $connection_list; + } + + /** + * Checks if post has already been shared by Publicize in the past. + * + * @since 6.7.0 + * + * @param integer $post_id Optional. Post ID to query connection status for: will use current post if missing. + * + * @return bool True if post has already been shared by Publicize, false otherwise. + */ + abstract public function post_is_done_sharing( $post_id = null ); + + /** + * Retrieves full list of available Publicize connection services. + * + * Retrieves current available publicize service connections + * with associated labels and URLs. + * + * @since 6.7.0 + * + * @return array { + * Array of UI service connection data for all services + * + * @type string 'name' Name of service. + * @type string 'label' Display label for service. + * @type string 'url' URL for adding connection to service. + * } + */ + public function get_available_service_data() { + $available_services = $this->get_services( 'all' ); + $available_service_data = array(); + + foreach ( $available_services as $service_name => $service ) { + $available_service_data[] = array( + 'name' => $service_name, + 'label' => $this->get_service_label( $service_name ), + 'url' => $this->connect_url( $service_name ), + ); + } + + return $available_service_data; + } + + /** + * Site Data + */ + + /** + * Get user ID. + * + * @return int The current user's ID, or 0 if no user is logged in. + */ + public function user_id() { + return get_current_user_id(); + } + + /** + * Get site ID. + * + * @return int Site ID. + */ + public function blog_id() { + return get_current_blog_id(); + } + + /** + * Posts + */ + + /** + * Checks old and new status to see if the post should be flagged as + * ready to Publicize. + * + * Attached to the `transition_post_status` filter. + * + * @param string $new_status New status. + * @param string $old_status Old status. + * @param WP_Post $post Post object. + * @return void + */ + abstract public function flag_post_for_publicize( $new_status, $old_status, $post ); + + /** + * Ensures the Post internal post-type supports `publicize` + * + * This feature support flag is used by the REST API. + */ + public function add_post_type_support() { + add_post_type_support( 'post', 'publicize' ); + } + + /** + * Can the current user access Publicize Data. + * + * @param int $post_id 0 for general access. Post_ID for specific access. + * @return bool + */ + public function current_user_can_access_publicize_data( $post_id = 0 ) { + /** + * Filter what user capability is required to use the publicize form on the edit post page. Useful if publish post capability has been removed from role. + * + * @module publicize + * + * @since 4.1.0 + * + * @param string $capability User capability needed to use publicize + */ + $capability = apply_filters( 'jetpack_publicize_capability', 'publish_posts' ); + + if ( 'publish_posts' === $capability && $post_id ) { + return current_user_can( 'publish_post', $post_id ); + } + + return current_user_can( $capability ); + } + + /** + * Auth callback for the protected ->POST_MESS post_meta + * + * @param int $object_id Post ID. + * @return bool + */ + public function message_meta_auth_callback( $object_id ) { + return $this->current_user_can_access_publicize_data( $object_id ); + } + + /** + * Registers the post_meta for use in the REST API. + * + * Registers for each post type that with `publicize` feature support. + */ + public function register_post_meta() { + $message_args = array( + 'type' => 'string', + 'description' => __( 'The message to use instead of the title when sharing to Publicize Services', 'jetpack-publicize-pkg' ), + 'single' => true, + 'default' => '', + 'show_in_rest' => array( + 'name' => 'jetpack_publicize_message', + ), + 'auth_callback' => array( $this, 'message_meta_auth_callback' ), + ); + + $tweetstorm_args = array( + 'type' => 'boolean', + 'description' => __( 'Whether or not the post should be treated as a Twitter thread.', 'jetpack-publicize-pkg' ), + 'single' => true, + 'default' => false, + 'show_in_rest' => array( + 'name' => 'jetpack_is_tweetstorm', + ), + 'auth_callback' => array( $this, 'message_meta_auth_callback' ), + ); + + $publicize_feature_enable_args = array( + 'type' => 'boolean', + 'description' => __( 'Whether or not the Share Post feature is enabled.', 'jetpack-publicize-pkg' ), + 'single' => true, + 'default' => true, + 'show_in_rest' => array( + 'name' => 'jetpack_publicize_feature_enabled', + ), + 'auth_callback' => array( $this, 'message_meta_auth_callback' ), + ); + + foreach ( get_post_types() as $post_type ) { + if ( ! $this->post_type_is_publicizeable( $post_type ) ) { + continue; + } + + $message_args['object_subtype'] = $post_type; + $tweetstorm_args['object_subtype'] = $post_type; + $publicize_feature_enable_args['object_subtype'] = $post_type; + + register_meta( 'post', $this->POST_MESS, $message_args ); + register_meta( 'post', $this->POST_TWEETSTORM, $tweetstorm_args ); + register_meta( 'post', self::POST_PUBLICIZE_FEATURE_ENABLED, $publicize_feature_enable_args ); + } + } + + /** + * Helper function to allow us to not publicize posts in certain contexts. + * + * @param WP_Post $post Post object. + */ + public function should_submit_post_pre_checks( $post ) { + $submit_post = true; + + if ( defined( 'WP_IMPORTING' ) && WP_IMPORTING ) { + $submit_post = false; + } + + if ( + defined( 'DOING_AUTOSAVE' ) + && + DOING_AUTOSAVE + ) { + $submit_post = false; + } + + // To stop quick edits from getting publicized. + if ( did_action( 'wp_ajax_inline-save' ) ) { + $submit_post = false; + } + + // phpcs:disable WordPress.Security.NonceVerification.Recommended + if ( ! empty( $_GET['bulk_edit'] ) ) { + $submit_post = false; + } + // phpcs:enable WordPress.Security.NonceVerification.Recommended + + // - API/XML-RPC Test Posts + if ( + ( + defined( 'XMLRPC_REQUEST' ) + && + XMLRPC_REQUEST + || + defined( 'APP_REQUEST' ) + && + APP_REQUEST + ) + && + 0 === strpos( $post->post_title, 'Temporary Post Used For Theme Detection' ) + ) { + $submit_post = false; + } + + // Only work with certain statuses (avoids inherits, auto drafts etc). + if ( ! in_array( $post->post_status, array( 'publish', 'draft', 'future' ), true ) ) { + $submit_post = false; + } + + // Don't publish password protected posts. + if ( '' !== $post->post_password ) { + $submit_post = false; + } + + return $submit_post; + } + + /** + * Fires when a post is saved, checks conditions and saves state in postmeta so that it + * can be picked up later by @see ::publicize_post() on WordPress.com codebase. + * + * Attached to the `save_post` action. + * + * @param int $post_id Post ID. + * @param WP_Post $post Post object. + */ + public function save_meta( $post_id, $post ) { + $cron_user = null; + $submit_post = true; + + if ( ! $this->post_type_is_publicizeable( $post->post_type ) ) { + return; + } + + $submit_post = $this->should_submit_post_pre_checks( $post ); + + // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- We're only checking if a value is set + $admin_page = isset( $_POST[ $this->ADMIN_PAGE ] ) ? $_POST[ $this->ADMIN_PAGE ] : null; + + // Did this request happen via wp-admin? + $from_web = isset( $_SERVER['REQUEST_METHOD'] ) + && + 'post' === strtolower( sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) ) + && + ! empty( $admin_page ); + + // phpcs:ignore WordPress.Security.NonceVerification.Missing + $title = isset( $_POST['wpas_title'] ) ? sanitize_textarea_field( wp_unslash( $_POST['wpas_title'] ) ) : null; + + if ( ( $from_web || defined( 'POST_BY_EMAIL' ) ) && $title ) { + if ( empty( $title ) ) { + delete_post_meta( $post_id, $this->POST_MESS ); + } else { + update_post_meta( $post_id, $this->POST_MESS, trim( stripslashes( $title ) ) ); + } + } + + // Change current user to provide context for get_services() if we're running during cron. + if ( defined( 'DOING_CRON' ) && DOING_CRON ) { + $cron_user = (int) $GLOBALS['user_ID']; + wp_set_current_user( $post->post_author ); + } + + /** + * In this phase, we mark connections that we want to SKIP. When Publicize is actually triggered, + * it will Publicize to everything *except* those marked for skipping. + */ + foreach ( (array) $this->get_services( 'connected' ) as $service_name => $connections ) { + foreach ( $connections as $connection ) { + $connection_data = ''; + if ( is_object( $connection ) && method_exists( $connection, 'get_meta' ) ) { + $connection_data = $connection->get_meta( 'connection_data' ); + } elseif ( ! empty( $connection['connection_data'] ) ) { + $connection_data = $connection['connection_data']; + } + + /** This action is documented in modules/publicize/ui.php */ + /* phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores */ + if ( false === apply_filters( 'wpas_submit_post?', $submit_post, $post_id, $service_name, $connection_data ) ) { + delete_post_meta( $post_id, $this->PENDING ); + continue; + } + + if ( ! empty( $connection->unique_id ) ) { + $unique_id = $connection->unique_id; + } elseif ( ! empty( $connection['connection_data']['token_id'] ) ) { + $unique_id = $connection['connection_data']['token_id']; + } + + // This was a wp-admin request, so we need to check the state of checkboxes. + if ( $from_web ) { + // Delete stray service-based post meta. + delete_post_meta( $post_id, $this->POST_SKIP . $service_name ); + + // We *unchecked* this stream from the admin page, or it's set to readonly, or it's a new addition. + if ( empty( $admin_page['submit'][ $unique_id ] ) ) { + // Also make sure that the service-specific input isn't there. + // If the user connected to a new service 'in-page' then a hidden field with the service + // name is added, so we just assume they wanted to Publicize to that service. + if ( empty( $admin_page['submit'][ $service_name ] ) ) { + // Nothing seems to be checked, so we're going to mark this one to be skipped. + update_post_meta( $post_id, $this->POST_SKIP . $unique_id, 1 ); + continue; + } else { + // Clean up any stray post meta. + delete_post_meta( $post_id, $this->POST_SKIP . $unique_id ); + } + } else { + // The checkbox for this connection is explicitly checked -- make sure we DON'T skip it. + delete_post_meta( $post_id, $this->POST_SKIP . $unique_id ); + } + } + + /** + * Fires right before the post is processed for Publicize. + * Users may hook in here and do anything else they need to after meta is written, + * and before the post is processed for Publicize. + * + * @since 2.1.2 + * + * @param bool $submit_post Should the post be publicized. + * @param int $post->ID Post ID. + * @param string $service_name Service name. + * @param array $connection Array of connection details. + */ + do_action( 'publicize_save_meta', $submit_post, $post_id, $service_name, $connection ); + } + } + + if ( defined( 'DOING_CRON' ) && DOING_CRON ) { + wp_set_current_user( $cron_user ); + } + + // Next up will be ::publicize_post(). + } + + /** + * Alters the "Post Published" message to include information about where the post + * was Publicized to. + * + * Attached to the `post_updated_messages` filter + * + * @param string[] $messages Array of messages. + * @return string[] + */ + public function update_published_message( $messages ) { + global $post_type, $post_type_object, $post; + if ( ! $this->post_type_is_publicizeable( $post_type ) ) { + return $messages; + } + + // Bail early if the post is private. + if ( 'publish' !== $post->post_status ) { + return $messages; + } + + $view_post_link_html = ''; + $viewable = is_post_type_viewable( $post_type_object ); + if ( $viewable ) { + /* phpcs:ignore WordPress.WP.I18n.MissingArgDomain, WordPress.Utils.I18nTextDomainFixer.MissingArgDomain */ + $view_text = esc_html__( 'View post' ); // Intentionally omitted domain. + + if ( 'jetpack-portfolio' === $post_type ) { + $view_text = esc_html__( 'View project', 'jetpack-publicize-pkg' ); + } + + $view_post_link_html = sprintf( + ' <a href="%1$s">%2$s</a>', + esc_url( get_permalink( $post ) ), + $view_text + ); + } + + $services = $this->get_publicizing_services( $post->ID ); + if ( empty( $services ) ) { + return $messages; + } + + $labels = array(); + foreach ( $services as $service_name => $display_names ) { + $labels[] = sprintf( + /* translators: Service name is %1$s, and account name is %2$s. */ + esc_html__( '%1$s (%2$s)', 'jetpack-publicize-pkg' ), + esc_html( $service_name ), + esc_html( is_array( $display_names ) ? implode( ', ', $display_names ) : $display_names ) + ); + } + + $messages['post'][6] = sprintf( + /* translators: %1$s is a comma-separated list of services and accounts. Ex. Facebook (@jetpack), Twitter (@jetpack) */ + esc_html__( 'Post published and sharing on %1$s.', 'jetpack-publicize-pkg' ), + implode( ', ', $labels ) + ) . $view_post_link_html; + + if ( 'post' === $post_type && class_exists( 'Jetpack_Subscriptions' ) ) { + $subscription = \Jetpack_Subscriptions::init(); + if ( $subscription->should_email_post_to_subscribers( $post ) ) { + $messages['post'][6] = sprintf( + /* translators: %1$s is a comma-separated list of services and accounts. Ex. Facebook (@jetpack), Twitter (@jetpack) */ + esc_html__( 'Post published, sending emails to subscribers and sharing post on %1$s.', 'jetpack-publicize-pkg' ), + implode( ', ', $labels ) + ) . $view_post_link_html; + } + } + + $messages['jetpack-portfolio'][6] = sprintf( + /* translators: %1$s is a comma-separated list of services and accounts. Ex. Facebook (@jetpack), Twitter (@jetpack) */ + esc_html__( 'Project published and sharing project on %1$s.', 'jetpack-publicize-pkg' ), + implode( ', ', $labels ) + ) . $view_post_link_html; + + return $messages; + } + + /** + * Get the Connections the Post was just Publicized to. + * + * Only reliable just after the Post was published. + * + * @param int $post_id Post ID. + * @return string[] Array of Service display name => Connection display name + */ + public function get_publicizing_services( $post_id ) { + $services = array(); + + foreach ( (array) $this->get_services( 'connected' ) as $service_name => $connections ) { + // services have multiple connections. + foreach ( $connections as $connection ) { + $unique_id = ''; + if ( ! empty( $connection->unique_id ) ) { + $unique_id = $connection->unique_id; + } elseif ( ! empty( $connection['connection_data']['token_id'] ) ) { + $unique_id = $connection['connection_data']['token_id']; + } + + // Did we skip this connection? + if ( get_post_meta( $post_id, $this->POST_SKIP . $unique_id, true ) ) { + continue; + } + $services[ $this->get_service_label( $service_name ) ][] = $this->get_display_name( $service_name, $connection ); + } + } + + return $services; + } + + /** + * Is the post Publicize-able? + * + * Only valid prior to Publicizing a Post. + * + * @param WP_Post $post Post to check. + * @return bool + */ + public function post_is_publicizeable( $post ) { + if ( ! $this->post_type_is_publicizeable( $post->post_type ) ) { + return false; + } + + // This is more a precaution. To only publicize posts that are published. (Mostly relevant for Jetpack sites). + if ( 'publish' !== $post->post_status ) { + return false; + } + + // If it's not flagged as ready, then abort. @see ::flag_post_for_publicize(). + if ( ! get_post_meta( $post->ID, $this->PENDING, true ) ) { + return false; + } + + return true; + } + + /** + * Is a given post type Publicize-able? + * + * Not every CPT lends itself to Publicize-ation. Allow CPTs to register by adding their CPT via + * the publicize_post_types array filter. + * + * @param string $post_type The post type to check. + * @return bool True if the post type can be Publicized. + */ + public function post_type_is_publicizeable( $post_type ) { + if ( 'post' === $post_type ) { + return true; + } + + return post_type_supports( $post_type, 'publicize' ); + } + + /** + * Already-published posts should not be Publicized by default. This filter sets checked to + * false if a post has already been published. + * + * Attached to the `publicize_checkbox_default` filter + * + * @param bool $checked True if checkbox is checked, false otherwise. + * @param int $post_id Post ID to set checkbox for. + * @return bool + */ + public function publicize_checkbox_default( $checked, $post_id ) { + if ( 'publish' === get_post_status( $post_id ) ) { + return false; + } + + return $checked; + } + + /** + * Util + */ + + /** + * Converts a Publicize message template string into a sprintf format string + * + * @param string[] $args Array of arguments. + * 0 - The Publicize message template: 'Check out my post: %title% @ %url' + * ... - The template tags 'title', 'url', etc. + * @return string + */ + protected static function build_sprintf( $args ) { + $search = array(); + $replace = array(); + foreach ( $args as $k => $arg ) { + if ( 0 === $k ) { + $string = $arg; + continue; + } + $search[] = "%$arg%"; + $replace[] = "%$k\$s"; + } + return str_replace( $search, $replace, $string ); + } + + /** + * Get Calypso URL for Publicize connections. + * + * @param string $source The idenfitier of the place the function is called from. + * @return string + */ + public function publicize_connections_url( $source = 'calypso-marketing-connections' ) { + $allowed_sources = array( 'jetpack-social-connections-admin-page', 'jetpack-social-connections-classic-editor', 'calypso-marketing-connections' ); + $source = in_array( $source, $allowed_sources, true ) ? $source : 'calypso-marketing-connections'; + return Redirect::get_url( $source, array( 'site' => ( new Status() )->get_site_suffix() ) ); + } +} + +/** + * Get Calypso URL for Publicize connections. + * + * @return string + */ +function publicize_calypso_url() { + _deprecated_function( __METHOD__, '0.2.0', 'Publicize::publicize_connections_url' ); + return Redirect::get_url( 'calypso-marketing-connections', array( 'site' => ( new Status() )->get_site_suffix() ) ); +} diff --git a/plugins/jetpack/jetpack_vendor/automattic/jetpack-publicize/src/class-publicize-setup.php b/plugins/jetpack/jetpack_vendor/automattic/jetpack-publicize/src/class-publicize-setup.php new file mode 100644 index 00000000..f3c4d316 --- /dev/null +++ b/plugins/jetpack/jetpack_vendor/automattic/jetpack-publicize/src/class-publicize-setup.php @@ -0,0 +1,39 @@ +<?php +/** + * Main Publicize class. + * + * @package automattic/jetpack + */ + +namespace Automattic\Jetpack\Publicize; + +/** + * The class to configure and initialize the publicize package. + */ +class Publicize_Setup { + /** + * To configure the publicize package, when called via the Config package. + */ + public static function configure() { + add_action( 'jetpack_feature_publicize_enabled', array( __CLASS__, 'on_jetpack_feature_publicize_enabled' ) ); + } + + /** + * To configure the publicize package, when called via the Config package. + */ + public static function on_jetpack_feature_publicize_enabled() { + + global $publicize_ui; + if ( ! isset( $publicize_ui ) ) { + $publicize_ui = new Publicize_UI(); + + } + // Adding on a higher priority to make sure we're the first field registered. + // The priority parameter can be removed once we deprecate WPCOM_REST_API_V2_Post_Publicize_Connections_Field + add_action( 'rest_api_init', array( new Connections_Post_Field(), 'register_fields' ), 5 ); + + add_action( 'rest_api_init', array( new REST_Controller(), 'register_rest_routes' ) ); + } +} + + diff --git a/plugins/jetpack/jetpack_vendor/automattic/jetpack-publicize/src/class-publicize-ui.php b/plugins/jetpack/jetpack_vendor/automattic/jetpack-publicize/src/class-publicize-ui.php new file mode 100644 index 00000000..d5b42f09 --- /dev/null +++ b/plugins/jetpack/jetpack_vendor/automattic/jetpack-publicize/src/class-publicize-ui.php @@ -0,0 +1,704 @@ +<?php +/** + * Publicize_UI class. + * + * @package automattic/jetpack-publicize + */ + +namespace Automattic\Jetpack\Publicize; + +/** + * Only user facing pieces of Publicize are found here. + */ +class Publicize_UI { + /** + * Contains an instance of class 'Publicize' which loads Keyring, sets up services, etc. + * + * @var Publicize Instance of Publicize + */ + public $publicize; + + /** + * URL to Sharing settings page in wordpress.com + * + * @var string + */ + protected $publicize_settings_url = ''; + + /** + * Hooks into WordPress to display the various pieces of UI and load our assets + */ + public function __construct() { + global $publicize; + + $publicize = new Publicize(); + $this->publicize = $publicize; + + add_action( 'init', array( $this, 'init' ) ); + } + + /** + * Initialize UI-related functionality. + */ + public function init() { + $this->publicize_settings_url = $this->publicize->publicize_connections_url(); + + // Show only to users with the capability required to manage their Publicize connections. + if ( ! $this->publicize->current_user_can_access_publicize_data() ) { + return; + } + + // Assets (css, js). + add_action( 'load-settings_page_sharing', array( $this, 'load_assets' ) ); + add_action( 'admin_head-post.php', array( $this, 'post_page_metabox_assets' ) ); + add_action( 'admin_head-post-new.php', array( $this, 'post_page_metabox_assets' ) ); + + // Management of publicize (sharing screen, ajax/lightbox popup, and metabox on post screen). + add_action( 'pre_admin_screen_sharing', array( $this, 'admin_page' ) ); + add_action( 'post_submitbox_misc_actions', array( $this, 'post_page_metabox' ) ); + } + + /** + * If the ShareDaddy plugin is not active we need to add the sharing settings page to the menu still + */ + public function sharing_menu() { + add_submenu_page( + 'options-general.php', + esc_html__( 'Sharing Settings', 'jetpack-publicize-pkg' ), + esc_html__( 'Sharing', 'jetpack-publicize-pkg' ), + 'publish_posts', + 'sharing', + array( $this, 'wrapper_admin_page' ) + ); + } + + /** + * Add admin page with wrapper. + */ + public function wrapper_admin_page() { + if ( class_exists( 'Jetpack_Admin_Page' ) ) { + \Jetpack_Admin_Page::wrap_ui( array( $this, 'management_page' ) ); + } + } + + /** + * Management page to load if Sharedaddy is not active so the 'pre_admin_screen_sharing' action exists. + */ + public function management_page() { + ?> + <div class="wrap"> + <div class="icon32" id="icon-options-general"><br /></div> + <h1><?php esc_html_e( 'Sharing Settings', 'jetpack-publicize-pkg' ); ?></h1> + + <?php + /** This action is documented in modules/sharedaddy/sharing.php */ + do_action( 'pre_admin_screen_sharing' ); + ?> + </div> + <?php + } + + /** + * Styling for the sharing screen and popups + * JS for the options and switching + */ + public function load_assets() { + if ( class_exists( 'Jetpack_Admin_Page' ) ) { + \Jetpack_Admin_Page::load_wrapper_styles(); + } + } + + /** + * Lists the current user's publicized accounts for the blog + * looks exactly like Publicize v1 for now, UI and functionality updates will come after the move to keyring + */ + public function admin_page() { + ?> + <h2 id="publicize"><?php esc_html_e( 'Publicize', 'jetpack-publicize-pkg' ); ?></h2> + <p><?php esc_html_e( 'Connect social media services to automatically share new posts.', 'jetpack-publicize-pkg' ); ?></p> + <h4> + <?php + printf( + wp_kses( + /* translators: %s is the link to the Publicize page in Calypso */ + __( "We've made some updates to Publicize. Please visit the <a href='%s' class='jptracks' data-jptracks-name='legacy_publicize_settings'>WordPress.com sharing page</a> to manage your publicize connections or use the button below.", 'jetpack-publicize-pkg' ), + array( + 'a' => array( + 'href' => array(), + 'class' => array(), + 'data-jptracks-name' => array(), + ), + ) + ), + esc_url( $this->publicize->publicize_connections_url() ) + ); + ?> + </h4> + + <a href="<?php echo esc_url( $this->publicize->publicize_connections_url() ); ?>" class="button button-primary jptracks" data-jptracks-name='legacy_publicize_settings'><?php esc_html_e( 'Publicize Settings', 'jetpack-publicize-pkg' ); ?></a> + <?php + } + + /** + * CSS for styling the publicize message box and counter that displays on the post page. + * There is also some JavaScript for length counting and some basic display effects. + */ + public function post_page_metabox_assets() { + // We don't need those assets for the block editor pages. + $current_screen = get_current_screen(); + if ( $current_screen && $current_screen->is_block_editor ) { + return; + } + + $default_prefix = $this->publicize->default_prefix; + $default_prefix = preg_replace( '/%([0-9])\$s/', "' + %\\1\$s + '", esc_js( $default_prefix ) ); + + $default_message = $this->publicize->default_message; + $default_message = preg_replace( '/%([0-9])\$s/', "' + %\\1\$s + '", esc_js( $default_message ) ); + + $default_suffix = $this->publicize->default_suffix; + $default_suffix = preg_replace( '/%([0-9])\$s/', "' + %\\1\$s + '", esc_js( $default_suffix ) ); + + $max_length = defined( 'JETPACK_PUBLICIZE_TWITTER_LENGTH' ) ? JETPACK_PUBLICIZE_TWITTER_LENGTH : 280; + $max_length = $max_length - 24; // t.co link, space. + + ?> + +<script type="text/javascript"> +jQuery( function($) { + var wpasTitleCounter = $( '#wpas-title-counter' ), + wpasTwitterCheckbox = $( '.wpas-submit-twitter' ).length, + postTitle = $( '#title' ), + wpasTitle = $( '#wpas-title' ).keyup( function() { + var postTitleVal, + length = wpasTitle.val().length; + + if ( ! length ) { + length = wpasTitle.attr( 'placeholder' ).length; + } + + wpasTitleCounter.text( length ).trigger( 'change' ); + } ), + authClick = false; + + wpasTitleCounter.on( 'change', function( e ) { + if ( wpasTwitterCheckbox && parseInt( $( e.currentTarget ).text(), 10 ) > <?php echo (int) $max_length; ?> ) { + wpasTitleCounter.addClass( 'wpas-twitter-length-limit' ); + } else { + wpasTitleCounter.removeClass( 'wpas-twitter-length-limit' ); + } + } ); + + // Keep the postTitle and the placeholder in sync + postTitle.on( 'keyup', function( e ) { + var url = $( '#sample-permalink' ).text(); + <?php // phpcs:ignore ?> + var defaultMessage = $.trim( '<?php printf( $default_prefix, 'url' ); printf( $default_message, 'e.currentTarget.value', 'url' ); printf( $default_suffix, 'url' ); ?>' ) + .replace( /<[^>]+>/g,''); + + wpasTitle.attr( 'placeholder', defaultMessage ); + wpasTitle.trigger( 'keyup' ); + } ); + + // set the initial placeholder + postTitle.trigger( 'keyup' ); + + // If a custom message has been provided, open the UI so the author remembers + if ( wpasTitle.val() && ! wpasTitle.prop( 'disabled' ) && wpasTitle.attr( 'placeholder' ) !== wpasTitle.val() ) { + $( '#publicize-form' ).show(); + $( '#publicize-defaults' ).hide(); + $( '#publicize-form-edit' ).hide(); + } + + $('#publicize-disconnected-form-show').click( function() { + $('#publicize-form').slideDown( 'fast' ); + $(this).hide(); + } ); + + $('#publicize-disconnected-form-hide').click( function() { + $('#publicize-form').slideUp( 'fast' ); + $('#publicize-disconnected-form-show').show(); + } ); + + $('#publicize-form-edit').click( function() { + $('#publicize-form').slideDown( 'fast', function() { + var selBeg = 0, selEnd = 0; + wpasTitle.focus(); + + if ( ! wpasTitle.text() ) { + wpasTitle.text( wpasTitle.attr( 'placeholder' ) ); + + selBeg = wpasTitle.text().indexOf( postTitle.val() ); + if ( selBeg < 0 ) { + selBeg = 0; + } else { + selEnd = selBeg + postTitle.val().length; + } + + var domObj = wpasTitle.get(0); + if ( domObj.setSelectionRange ) { + domObj.setSelectionRange( selBeg, selEnd ); + } else if ( domObj.createTextRange ) { + var r = domObj.createTextRange(); + r.moveStart( 'character', selBeg ); + r.moveEnd( 'character', selEnd ); + r.select(); + } + } + } ); + + $('#publicize-defaults').hide(); + $(this).hide(); + return false; + } ); + + $('#publicize-form-hide').click( function() { + var newList = $.map( $('#publicize-form').slideUp( 'fast' ).find( ':checked' ), function( el ) { + return $.trim( $(el).parent( 'label' ).text() ); + } ); + $('#publicize-defaults').html( '<strong>' + newList.join( '</strong>, <strong>' ) + '</strong>' ).show(); + $('#publicize-form-edit').show(); + return false; + } ); + + $('.authorize-link').click( function() { + if ( authClick ) { + return false; + } + authClick = true; + $(this).after( '<img src="images/loading.gif" class="alignleft" style="margin: 0 .5em" />' ); + $.ajaxSetup( { async: false } ); + + if ( window.wp && window.wp.autosave ) { + window.wp.autosave.server.triggerSave(); + } else { + autosave(); + } + + return true; + } ); + + $( '.pub-service' ).click( function() { + var service = $(this).data( 'service' ), + fakebox = '<input id="wpas-submit-' + service + '" type="hidden" value="1" name="wpas[submit][' + service + ']" />'; + $( '#add-publicize-check' ).append( fakebox ); + } ); + + publicizeConnTestStart = function() { + $( '#pub-connection-tests' ) + .removeClass( 'below-h2' ) + .removeClass( 'error' ) + .removeClass( 'publicize-token-refresh-message' ) + .addClass( 'test-in-progress' ) + .html( '' ); + $.post( ajaxurl, { action: 'test_publicize_conns' }, publicizeConnTestComplete ); + } + + publicizeConnRefreshClick = function( event ) { + event.preventDefault(); + var popupURL = event.currentTarget.href; + var popupTitle = event.currentTarget.title; + // open a popup window + // when it is closed, kick off the tests again + var popupWin = window.open( popupURL, popupTitle, '' ); + var popupWinTimer= window.setInterval( function() { + if ( popupWin.closed !== false ) { + window.clearInterval( popupWinTimer ); + publicizeConnTestStart(); + } + }, 500 ); + } + + publicizeConnTestComplete = function( response ) { + var testsSelector = $( '#pub-connection-tests' ); + testsSelector + .removeClass( 'test-in-progress' ) + .removeClass( 'below-h2' ) + .removeClass( 'error' ) + .removeClass( 'publicize-token-refresh-message' ) + .html( '' ); + + // If any of the tests failed, show some stuff + var somethingShownAlready = false; + var facebookNotice = false; + $.each( response.data, function( index, testResult ) { + // find the li for this connection + if ( ! testResult.connectionTestPassed && testResult.userCanRefresh ) { + if ( ! somethingShownAlready ) { + testsSelector + .addClass( 'below-h2' ) + .addClass( 'error' ) + .addClass( 'publicize-token-refresh-message' ) + .append( "<p><?php echo esc_html( __( 'Before you hit Publish, please refresh the following connection(s) to make sure we can Publicize your post:', 'jetpack-publicize-pkg' ) ); ?></p>" ); + somethingShownAlready = true; + } + + if ( testResult.userCanRefresh ) { + testsSelector.append( '<p/>' ); + $( '<a/>', { + 'class' : 'pub-refresh-button button', + 'title' : testResult.refreshText, + 'href' : testResult.refreshURL, + 'text' : testResult.refreshText, + 'target' : '_refresh_' + testResult.serviceName + } ) + .appendTo( testsSelector.children().last() ) + .click( publicizeConnRefreshClick ); + } + } + + if( ! testResult.connectionTestPassed && ! testResult.userCanRefresh ) { + $( '#wpas-submit-' + testResult.unique_id ).prop( "checked", false ).prop( "disabled", true ); + if ( ! facebookNotice ) { + var message = '<p>' + + testResult.connectionTestMessage + + '</p><p>' + + ' <a class="button" href="<?php echo esc_url( $this->publicize_settings_url ); ?>" rel="noopener noreferrer" target="_blank">' + + '<?php echo esc_html( __( 'Update Your Sharing Settings', 'jetpack-publicize-pkg' ) ); ?>' + + '</a>' + + '<p>'; + + testsSelector + .addClass( 'below-h2' ) + .addClass( 'error' ) + .addClass( 'publicize-token-refresh-message' ) + .append( message ); + facebookNotice = true; + } + } + } ); + } + + $( document ).ready( function() { + // If we have the #pub-connection-tests div present, kick off the connection test + if ( $( '#pub-connection-tests' ).length ) { + publicizeConnTestStart(); + } + } ); + +} ); +</script> + +<style type="text/css"> +#publicize { + line-height: 1.5; +} +#publicize ul { + margin: 4px 0 4px 6px; +} +#publicize li { + margin: 0; +} +#publicize textarea { + margin: 4px 0 0; + width: 100% +} +#publicize ul.not-connected { + list-style: square; + padding-left: 1em; +} +.publicize__notice-warning { + display: block; + padding: 7px 10px; + margin: 5px 0; + border-left-width: 4px; + border-left-style: solid; + font-size: 12px; + box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1); +} +.publicize-external-link { + display: block; + text-decoration: none; + margin-top: 8px; +} +.publicize-external-link__text { + text-decoration: underline; +} +#publicize-title:before { + content: "\f237"; + font: normal 20px/1 dashicons; + speak: none; + margin-left: -1px; + padding-right: 3px; + vertical-align: top; + -webkit-font-smoothing: antialiased; + color: #8c8f94; +} +.post-new-php .authorize-link, .post-php .authorize-link { + line-height: 1.5em; +} +.post-new-php .authorize-message, .post-php .authorize-message { + margin-bottom: 0; +} +#poststuff #publicize .updated p { + margin: .5em 0; +} +.wpas-twitter-length-limit { + color: red; +} +.publicize__notice-warning .dashicons { + font-size: 16px; + text-decoration: none; +} +</style> + <?php + } + + /** + * Get the connection label. + * + * @param string $service_label Service's human-readable Label ("Facebook", "Twitter", ...). + * @param string $display_name Connection's human-readable Username ("@jetpack", ...). + * @return string + */ + private function connection_label( $service_label, $display_name ) { + return sprintf( + /* translators: %1$s: Service Name (Facebook, Twitter, ...), %2$s: Username on Service (@jetpack, ...) */ + __( '%1$s: %2$s', 'jetpack-publicize-pkg' ), + $service_label, + $display_name + ); + } + + /** + * Extracts the connections that require reauthentication, for example, LinkedIn, when it switched v1 to v2 of its API. + * + * @return array Connections that must be reauthenticated + */ + public function get_must_reauth_connections() { + $must_reauth = array(); + $connections = $this->publicize->get_connections( 'linkedin' ); + if ( is_array( $connections ) ) { + foreach ( $connections as $index => $connection ) { + if ( $this->publicize->is_invalid_linkedin_connection( $connection ) ) { + $must_reauth[ $index ] = 'LinkedIn'; + } + } + } + return $must_reauth; + } + + /** + * Controls the metabox that is displayed on the post page + * Allows the user to customize the message that will be sent out to the social network, as well as pick which + * networks to publish to. Also displays the character counter and some other information. + */ + public function post_page_metabox() { + global $post; + + if ( ! $this->publicize->post_type_is_publicizeable( $post->post_type ) ) { + return; + } + + $connections_data = $this->publicize->get_filtered_connection_data(); + + $available_services = $this->publicize->get_services( 'all' ); + + if ( ! is_array( $available_services ) ) { + $available_services = array(); + } + + if ( ! is_array( $connections_data ) ) { + $connections_data = array(); + } + ?> + <div id="publicize" class="misc-pub-section misc-pub-section-last"> + <span id="publicize-title"> + <?php + esc_html_e( 'Publicize:', 'jetpack-publicize-pkg' ); + + if ( ! empty( $connections_data ) ) : + $publicize_form = $this->get_metabox_form_connected( $connections_data ); + + $must_reauth = $this->get_must_reauth_connections(); + if ( ! empty( $must_reauth ) ) { + foreach ( $must_reauth as $connection_name ) { + ?> + <span class="notice-warning publicize__notice-warning"> + <?php + printf( + /* translators: %s is the name of a Publicize service like "LinkedIn" */ + esc_html__( + 'Your %s connection needs to be reauthenticated to continue working – head to Sharing to take care of it.', + 'jetpack-publicize-pkg' + ), + $connection_name // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + ); + ?> + <a + class="publicize-external-link" + href="<?php echo esc_url( $this->publicize->publicize_connections_url() ); ?>" + target="_blank" + > + <span class="publicize-external-link__text"><?php esc_html_e( 'Go to Sharing settings', 'jetpack-publicize-pkg' ); ?></span> + <span class="dashicons dashicons-external"></span> + </a> + </span> + <?php + } + } + + $labels = array(); + + foreach ( $connections_data as $connection_data ) { + if ( ! $connection_data['enabled'] ) { + continue; + } + + $labels[] = sprintf( + '<strong>%s</strong>', + esc_html( $this->connection_label( $connection_data['service_label'], $connection_data['display_name'] ) ) + ); + } + + ?> + <?php // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- labels are already escaped above ?> + <span id="publicize-defaults"><?php echo join( ', ', $labels ); ?></span> + <a href="#" id="publicize-form-edit"><?php esc_html_e( 'Edit', 'jetpack-publicize-pkg' ); ?></a> <a href="<?php echo esc_url( $this->publicize->publicize_connections_url( 'jetpack-social-connections-classic-editor' ) ); ?>" rel="noopener noreferrer" target="_blank"><?php esc_html_e( 'Settings', 'jetpack-publicize-pkg' ); ?></a><br /> + <?php + else : + $publicize_form = $this->get_metabox_form_disconnected( $available_services ); + ?> + <strong><?php esc_html_e( 'Not Connected', 'jetpack-publicize-pkg' ); ?></strong> + <a href="<?php echo esc_url( $this->publicize->publicize_connections_url( 'jetpack-social-connections-classic-editor' ) ); ?>" rel="noopener noreferrer" target="_blank"><?php esc_html_e( 'Settings', 'jetpack-publicize-pkg' ); ?></a><br /> + <?php + + endif; + ?> + </span> + <?php + /** + * Filter the Publicize details form. + * + * @module publicize + * + * @since 2.0.0 + * + * @param string $publicize_form Publicize Details form appearing above Publish button in the editor. + */ + echo apply_filters( 'publicize_form', $publicize_form ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Parts of the form are escaped individually in the code above. + ?> + </div> + <?php + } + + /** + * Generates HTML content for connections form. + * + * @since 6.7 + * + * @global WP_Post $post The current post instance being published. + * + * @param array $connections_data Array of connections. + * @return array { + * Array of content for generating connection form. + * + * @type string HTML content of form + * @type array { + * Array of connection labels for active connections only. + * + * @type string Connection label string. + * } + * } + */ + private function get_metabox_form_connected( $connections_data ) { + global $post; + + $all_done = $this->publicize->post_is_done_sharing(); + $all_connections_done = true; + + ob_start(); + + ?> + <div id="publicize-form" class="hide-if-js"> + <ul> + <?php + + foreach ( $connections_data as $connection_data ) { + $all_connections_done = $all_connections_done && $connection_data['done']; + ?> + + <li> + <label for="wpas-submit-<?php echo esc_attr( $connection_data['unique_id'] ); ?>"> + <input + type="checkbox" + name="wpas[submit][<?php echo esc_attr( $connection_data['unique_id'] ); ?>]" + id="wpas-submit-<?php echo esc_attr( $connection_data['unique_id'] ); ?>" + class="wpas-submit-<?php echo esc_attr( $connection_data['service_name'] ); ?>" + value="1" + <?php + checked( true, $connection_data['enabled'] ); + disabled( false, $connection_data['toggleable'] ); + ?> + /> + <?php if ( $connection_data['enabled'] && ! $connection_data['toggleable'] ) : // Need to submit a value to force a global connection to POST. ?> + <input + type="hidden" + name="wpas[submit][<?php echo esc_attr( $connection_data['unique_id'] ); ?>]" + value="1" + /> + <?php endif; ?> + + <?php echo esc_html( $this->connection_label( $connection_data['service_label'], $connection_data['display_name'] ) ); ?> + + </label> + </li> + <?php + } + + $title = get_post_meta( $post->ID, $this->publicize->POST_MESS, true ); + if ( ! $title ) { + $title = ''; + } + + $all_done = $all_done || $all_connections_done; + + ?> + + </ul> + + <label for="wpas-title"><?php esc_html_e( 'Custom Message:', 'jetpack-publicize-pkg' ); ?></label> + <span id="wpas-title-counter" class="alignright hide-if-no-js">0</span> + <textarea name="wpas_title" id="wpas-title"<?php disabled( $all_done ); ?>><?php echo esc_textarea( $title ); ?></textarea> + <a href="#" class="hide-if-no-js button" id="publicize-form-hide"><?php esc_html_e( 'OK', 'jetpack-publicize-pkg' ); ?></a> + <input type="hidden" name="wpas[0]" value="1" /> + </div> + + <?php if ( ! $all_done ) : ?> + <div id="pub-connection-tests"></div> + <?php endif; ?> + <?php + + return ob_get_clean(); + } + + /** + * Metabox that is shown when no services are connected. + * + * @param array $available_services Array of available services for connecting. + */ + private function get_metabox_form_disconnected( $available_services ) { + ob_start(); + ?> + <div id="publicize-form" class="hide-if-js"> + <div id="add-publicize-check" style="display: none;"></div> + + <?php esc_html_e( 'Connect to', 'jetpack-publicize-pkg' ); ?>: + + <ul class="not-connected"> + <?php foreach ( $available_services as $service_name => $service ) : ?> + <li> + <?php /* translators: %s is the name of a Publicize service such as "LinkedIn" */ ?> + <a class="pub-service" data-service="<?php echo esc_attr( $service_name ); ?>" title="<?php echo esc_attr( sprintf( __( 'Connect and share your posts on %s', 'jetpack-publicize-pkg' ), $this->publicize->get_service_label( $service_name ) ) ); ?>" rel="noopener noreferrer" target="_blank" href="<?php echo esc_url( $this->publicize->connect_url( $service_name ) ); ?>"> + <?php echo esc_html( $this->publicize->get_service_label( $service_name ) ); ?> + </a> + </li> + <?php endforeach; ?> + </ul> + <a href="#" class="hide-if-no-js button" id="publicize-disconnected-form-hide"><?php esc_html_e( 'OK', 'jetpack-publicize-pkg' ); ?></a> + </div> + <?php + + return ob_get_clean(); + } +} diff --git a/plugins/jetpack/jetpack_vendor/automattic/jetpack-publicize/src/class-publicize.php b/plugins/jetpack/jetpack_vendor/automattic/jetpack-publicize/src/class-publicize.php new file mode 100644 index 00000000..5a6e1c91 --- /dev/null +++ b/plugins/jetpack/jetpack_vendor/automattic/jetpack-publicize/src/class-publicize.php @@ -0,0 +1,1085 @@ +<?php +/** + * Publicize class. + * + * @package automattic/jetpack-publicize + */ + +namespace Automattic\Jetpack\Publicize; + +use Automattic\Jetpack\Connection\Tokens; +use Automattic\Jetpack\Redirect; +use Jetpack_IXR_Client; +use Jetpack_Options; + +/** + * Extend the base class with Jetpack-specific functionality. + */ +class Publicize extends Publicize_Base { + + const CONNECTION_REFRESH_WAIT_TRANSIENT = 'jetpack_publicize_connection_refresh_wait'; + + /** + * Add hooks. + */ + public function __construct() { + parent::__construct(); + + add_filter( 'jetpack_xmlrpc_unauthenticated_methods', array( $this, 'register_update_publicize_connections_xmlrpc_method' ) ); + + add_action( 'load-settings_page_sharing', array( $this, 'admin_page_load' ), 9 ); + + add_action( 'wp_ajax_publicize_tumblr_options_page', array( $this, 'options_page_tumblr' ) ); + add_action( 'wp_ajax_publicize_facebook_options_page', array( $this, 'options_page_facebook' ) ); + add_action( 'wp_ajax_publicize_twitter_options_page', array( $this, 'options_page_twitter' ) ); + add_action( 'wp_ajax_publicize_linkedin_options_page', array( $this, 'options_page_linkedin' ) ); + + add_action( 'wp_ajax_publicize_tumblr_options_save', array( $this, 'options_save_tumblr' ) ); + add_action( 'wp_ajax_publicize_facebook_options_save', array( $this, 'options_save_facebook' ) ); + add_action( 'wp_ajax_publicize_twitter_options_save', array( $this, 'options_save_twitter' ) ); + add_action( 'wp_ajax_publicize_linkedin_options_save', array( $this, 'options_save_linkedin' ) ); + + add_action( 'load-settings_page_sharing', array( $this, 'force_user_connection' ) ); + + add_filter( 'jetpack_published_post_flags', array( $this, 'set_post_flags' ), 10, 2 ); + + add_action( 'wp_insert_post', array( $this, 'save_publicized' ), 11, 2 ); + + add_filter( 'jetpack_twitter_cards_site_tag', array( $this, 'enhaced_twitter_cards_site_tag' ) ); + + add_action( 'publicize_save_meta', array( $this, 'save_publicized_twitter_account' ), 10, 4 ); + add_action( 'publicize_save_meta', array( $this, 'save_publicized_facebook_account' ), 10, 4 ); + + add_action( 'connection_disconnected', array( $this, 'add_disconnect_notice' ) ); + + add_filter( 'jetpack_sharing_twitter_via', array( $this, 'get_publicized_twitter_account' ), 10, 2 ); + + add_action( 'updating_jetpack_version', array( $this, 'init_refresh_transient' ) ); + } + + /** + * Add a notice when a connection has been disconnected. + */ + public function add_disconnect_notice() { + add_action( 'admin_notices', array( $this, 'display_disconnected' ) ); + } + + /** + * Force user connection before showing the Publicize UI. + */ + public function force_user_connection() { + global $current_user; + + $user_token = ( new Tokens() )->get_access_token( $current_user->ID ); + $is_user_connected = $user_token && ! is_wp_error( $user_token ); + + // If the user is already connected via Jetpack, then we're good. + if ( $is_user_connected ) { + return; + } + + // If they're not connected, then remove the Publicize UI and tell them they need to connect first. + global $publicize_ui; + remove_action( 'pre_admin_screen_sharing', array( $publicize_ui, 'admin_page' ) ); + + // Do we really need `admin_styles`? With the new admin UI, it's breaking some bits. + // Jetpack::init()->admin_styles();. + add_action( 'pre_admin_screen_sharing', array( $this, 'admin_page_warning' ), 1 ); + } + + /** + * Show a warning when Publicize does not have a connection. + */ + public function admin_page_warning() { + $jetpack = \Jetpack::init(); + $blog_name = get_bloginfo( 'blogname' ); + if ( empty( $blog_name ) ) { + $blog_name = home_url( '/' ); + } + + ?> + <div id="message" class="updated jetpack-message jp-connect"> + <div class="jetpack-wrap-container"> + <div class="jetpack-text-container"> + <p> + <?php + printf( + /* translators: %s is the name of the blog */ + esc_html( wptexturize( __( "To use Publicize, you'll need to link your %s account to your WordPress.com account using the link below.", 'jetpack-publicize-pkg' ) ) ), + '<strong>' . esc_html( $blog_name ) . '</strong>' + ); + ?> + </p> + <p><?php echo esc_html( wptexturize( __( "If you don't have a WordPress.com account yet, you can sign up for free in just a few seconds.", 'jetpack-publicize-pkg' ) ) ); ?></p> + </div> + <div class="jetpack-install-container"> + <p class="submit"><a + href="<?php echo esc_url( $jetpack->build_connect_url( false, menu_page_url( 'sharing', false ) ) ); ?>" + class="button-connector" + id="wpcom-connect"><?php esc_html_e( 'Link account with WordPress.com', 'jetpack-publicize-pkg' ); ?></a> + </p> + <p class="jetpack-install-blurb"> + <?php jetpack_render_tos_blurb(); ?> + </p> + </div> + </div> + </div> + <?php + } + + /** + * Remove a Publicize Connection. + * + * @param string $service_name 'facebook', 'twitter', etc. + * @param string $connection_id Connection ID. + * @param false|int $_blog_id The blog ID. Use false (default) for the current blog. + * @param false|int $_user_id The user ID. Use false (default) for the current user. + * @param bool $force_delete Whether to skip permissions checks. + * @return false|void False on failure. Void on success. + */ + public function disconnect( $service_name, $connection_id, $_blog_id = false, $_user_id = false, $force_delete = false ) { + return Keyring_Helper::disconnect( $service_name, $connection_id, $_blog_id, $_user_id, $force_delete ); + } + + /** + * Set updated Publicize conntections. + * + * @param mixed $publicize_connections Updated connections. + * @return true + */ + public function receive_updated_publicize_connections( $publicize_connections ) { + Jetpack_Options::update_option( 'publicize_connections', $publicize_connections ); + + return true; + } + + /** + * Add method to update Publicize connections. + * + * @param array $methods Array of registered methods. + * @return array + */ + public function register_update_publicize_connections_xmlrpc_method( $methods ) { + return array_merge( + $methods, + array( + 'jetpack.updatePublicizeConnections' => array( $this, 'receive_updated_publicize_connections' ), + ) + ); + } + + /** + * Get a list of all connections. + * + * @return array + */ + public function get_all_connections() { + $this->refresh_connections(); + $connections = Jetpack_Options::get_option( 'publicize_connections' ); + if ( isset( $connections['google_plus'] ) ) { + unset( $connections['google_plus'] ); + } + return $connections; + } + + /** + * Get connections for a specific service. + * + * @param string $service_name 'facebook', 'twitter', etc. + * @param false|int $_blog_id The blog ID. Use false (default) for the current blog. + * @param false|int $_user_id The user ID. Use false (default) for the current user. + * @return false|object[]|array[] + */ + public function get_connections( $service_name, $_blog_id = false, $_user_id = false ) { + if ( false === $_user_id ) { + $_user_id = $this->user_id(); + } + + $connections = $this->get_all_connections(); + $connections_to_return = array(); + + if ( ! empty( $connections ) && is_array( $connections ) ) { + if ( ! empty( $connections[ $service_name ] ) ) { + foreach ( $connections[ $service_name ] as $id => $connection ) { + if ( $this->is_global_connection( $connection ) || $_user_id === (int) $connection['connection_data']['user_id'] ) { + $connections_to_return[ $id ] = $connection; + } + } + } + + return $connections_to_return; + } + + return false; + } + + /** + * Get all connections for a specific user. + * + * @return array|false + */ + public function get_all_connections_for_user() { + $connections = $this->get_all_connections(); + + $connections_to_return = array(); + if ( ! empty( $connections ) ) { + foreach ( (array) $connections as $service_name => $connections_for_service ) { + foreach ( $connections_for_service as $id => $connection ) { + $user_id = (int) $connection['connection_data']['user_id']; + // phpcs:ignore WordPress.PHP.YodaConditions.NotYoda + if ( $user_id === 0 || $this->user_id() === $user_id ) { + $connections_to_return[ $service_name ][ $id ] = $connection; + } + } + } + + return $connections_to_return; + } + + return false; + } + + /** + * Get the ID of a connection. + * + * @param array $connection The connection. + * @return string + */ + public function get_connection_id( $connection ) { + return $connection['connection_data']['id']; + } + + /** + * Get the unique ID of a connection. + * + * @param array $connection The connection. + * @return string + */ + public function get_connection_unique_id( $connection ) { + return $connection['connection_data']['token_id']; + } + + /** + * Get the meta of a connection. + * + * @param array $connection The connection. + * @return array + */ + public function get_connection_meta( $connection ) { + $connection['user_id'] = $connection['connection_data']['user_id']; // Allows for shared connections. + return $connection; + } + + /** + * Show error on settings page if applicable. + */ + public function admin_page_load() { + $action = isset( $_GET['action'] ) ? sanitize_text_field( wp_unslash( $_GET['action'] ) ) : null; // phpcs:ignore WordPress.Security.NonceVerification.Recommended + + if ( 'error' === $action ) { + add_action( 'pre_admin_screen_sharing', array( $this, 'display_connection_error' ), 9 ); + } + } + + /** + * Display an error message. + */ + public function display_connection_error() { + $code = false; + // phpcs:disable WordPress.Security.NonceVerification.Recommended + $service = isset( $_GET['service'] ) ? sanitize_text_field( wp_unslash( $_GET['service'] ) ) : null; + $publicize_error = isset( $_GET['publicize_error'] ) ? sanitize_text_field( wp_unslash( $_GET['publicize_error'] ) ) : null; + // phpcs:enable WordPress.Security.NonceVerification.Recommended + + if ( $service ) { + /* translators: %s is the name of the Publicize service (e.g. Facebook, Twitter) */ + $error = sprintf( __( 'There was a problem connecting to %s to create an authorized connection. Please try again in a moment.', 'jetpack-publicize-pkg' ), self::get_service_label( $service ) ); + } else { + if ( $publicize_error ) { + $code = strtolower( $publicize_error ); + switch ( $code ) { + case '400': + $error = __( 'An invalid request was made. This normally means that something intercepted or corrupted the request from your server to the Jetpack Server. Try again and see if it works this time.', 'jetpack-publicize-pkg' ); + break; + case 'secret_mismatch': + $error = __( 'We could not verify that your server is making an authorized request. Please try again, and make sure there is nothing interfering with requests from your server to the Jetpack Server.', 'jetpack-publicize-pkg' ); + break; + case 'empty_blog_id': + $error = __( 'No blog_id was included in your request. Please try disconnecting Jetpack from WordPress.com and then reconnecting it. Once you have done that, try connecting Publicize again.', 'jetpack-publicize-pkg' ); + break; + case 'empty_state': + /* translators: %s is the URL of the Jetpack admin page */ + $error = sprintf( __( 'No user information was included in your request. Please make sure that your user account has connected to Jetpack. Connect your user account by going to the <a href="%s">Jetpack page</a> within wp-admin.', 'jetpack-publicize-pkg' ), \Jetpack::admin_url() ); + break; + default: + $error = __( 'Something which should never happen, happened. Sorry about that. If you try again, maybe it will work.', 'jetpack-publicize-pkg' ); + break; + } + } else { + $error = __( 'There was a problem connecting with Publicize. Please try again in a moment.', 'jetpack-publicize-pkg' ); + } + } + // Using the same formatting/style as Jetpack::admin_notices() error. + ?> + <div id="message" class="jetpack-message jetpack-err"> + <div class="squeezer"> + <h2> + <?php + echo wp_kses( + $error, + array( + 'a' => array( + 'href' => true, + ), + 'code' => true, + 'strong' => true, + 'br' => true, + 'b' => true, + ) + ); + ?> + </h2> + <?php if ( $code ) : ?> + <p> + <?php + printf( + /* translators: %s is the name of the error */ + esc_html__( 'Error code: %s', 'jetpack-publicize-pkg' ), + esc_html( stripslashes( $code ) ) + ); + ?> + </p> + <?php endif; ?> + </div> + </div> + <?php + } + + /** + * Show a message that the connection has been removed. + */ + public function display_disconnected() { + echo "<div class='updated'>\n"; + echo '<p>' . esc_html( __( 'That connection has been removed.', 'jetpack-publicize-pkg' ) ) . "</p>\n"; + echo "</div>\n\n"; + } + + /** + * If applicable, globalize a connection. + * + * @param string $connection_id Connection ID. + */ + public function globalization( $connection_id ) { + if ( isset( $_REQUEST['global'] ) && 'on' === $_REQUEST['global'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- nonce check happens earlier in the process before we get here + if ( ! current_user_can( $this->GLOBAL_CAP ) ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase + return; + } + + $this->globalize_connection( $connection_id ); + } + } + + /** + * Globalize a connection. + * + * @param string $connection_id Connection ID. + */ + public function globalize_connection( $connection_id ) { + $xml = new Jetpack_IXR_Client(); + $xml->query( 'jetpack.globalizePublicizeConnection', $connection_id, 'globalize' ); + + if ( ! $xml->isError() ) { + $response = $xml->getResponse(); + $this->receive_updated_publicize_connections( $response ); + } + } + + /** + * Unglobalize a connection. + * + * @param string $connection_id Connection ID. + */ + public function unglobalize_connection( $connection_id ) { + $xml = new Jetpack_IXR_Client(); + $xml->query( 'jetpack.globalizePublicizeConnection', $connection_id, 'unglobalize' ); + + if ( ! $xml->isError() ) { + $response = $xml->getResponse(); + $this->receive_updated_publicize_connections( $response ); + } + } + + /** + * As Jetpack updates set the refresh transient to a random amount + * in order to spread out updates to the connection data. + * + * @param string $version The Jetpack version being updated to. + */ + public function init_refresh_transient( $version ) { + if ( version_compare( $version, '10.2.1', '>=' ) && ! get_transient( self::CONNECTION_REFRESH_WAIT_TRANSIENT ) ) { + $this->set_refresh_wait_transient( wp_rand( 10, HOUR_IN_SECONDS * 24 ) ); + } + } + + /** + * Grabs a fresh copy of the publicize connections data. + * Only refreshes once every 12 hours or retries after an hour with an error. + */ + public function refresh_connections() { + if ( get_transient( self::CONNECTION_REFRESH_WAIT_TRANSIENT ) ) { + return; + } + $xml = new Jetpack_IXR_Client(); + $xml->query( 'jetpack.fetchPublicizeConnections' ); + $wait_time = HOUR_IN_SECONDS * 24; + + if ( ! $xml->isError() ) { + $response = $xml->getResponse(); + $this->receive_updated_publicize_connections( $response ); + } else { + // Retry a bit quicker, but still wait. + $wait_time = HOUR_IN_SECONDS; + } + + $this->set_refresh_wait_transient( $wait_time ); + } + + /** + * Sets the transient to expire at the specified time in seconds. + * This prevents us from attempting to refresh the data too often. + * + * @param int $wait_time The number of seconds before the transient should expire. + */ + public function set_refresh_wait_transient( $wait_time ) { + set_transient( self::CONNECTION_REFRESH_WAIT_TRANSIENT, microtime( true ), $wait_time ); + } + + /** + * Get the Publicize connect URL from Keyring. + * + * @param string $service_name Name of the service to get connect URL for. + * @param string $for What the URL is for. Default 'publicize'. + * @return string + */ + public function connect_url( $service_name, $for = 'publicize' ) { + return Keyring_Helper::connect_url( $service_name, $for ); + } + + /** + * Get the Publicize refresh URL from Keyring. + * + * @param string $service_name Name of the service to get refresh URL for. + * @param string $for What the URL is for. Default 'publicize'. + * @return string + */ + public function refresh_url( $service_name, $for = 'publicize' ) { + return Keyring_Helper::refresh_url( $service_name, $for ); + } + + /** + * Get the Publicize disconnect URL from Keyring. + * + * @param string $service_name Name of the service to get disconnect URL for. + * @param mixed $id ID of the conenction to disconnect. + * @return string + */ + public function disconnect_url( $service_name, $id ) { + return Keyring_Helper::disconnect_url( $service_name, $id ); + } + + /** + * Get social networks, either all available or only those that the site is connected to. + * + * @since 2.0.0 + * @since 6.6.0 Removed Path. Service closed October 2018. + * + * @param string $filter Select the list of services that will be returned. Defaults to 'all', accepts 'connected'. + * @param false|int $_blog_id Get services for a specific blog by ID, or set to false for current blog. Default false. + * @param false|int $_user_id Get services for a specific user by ID, or set to false for current user. Default false. + * @return array List of social networks. + */ + public function get_services( $filter = 'all', $_blog_id = false, $_user_id = false ) { + $services = array( + 'facebook' => array(), + 'twitter' => array(), + 'linkedin' => array(), + 'tumblr' => array(), + ); + + if ( 'all' === $filter ) { + return $services; + } + + $connected_services = array(); + foreach ( $services as $service_name => $empty ) { + $connections = $this->get_connections( $service_name, $_blog_id, $_user_id ); + if ( $connections ) { + $connected_services[ $service_name ] = $connections; + } + } + return $connected_services; + } + + /** + * Get a specific connection. Stub. + * + * @param string $service_name 'facebook', 'twitter', etc. + * @param string $connection_id Connection ID. + * @param false|int $_blog_id The blog ID. Use false (default) for the current blog. + * @param false|int $_user_id The user ID. Use false (default) for the current user. + * @return void + */ + public function get_connection( $service_name, $connection_id, $_blog_id = false, $_user_id = false ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable + // Stub. + } + + /** + * Flag a post for Publicize after publishing. + * + * @param string $new_status New status of the post. + * @param string $old_status Old status of the post. + * @param WP_Post $post Post object. + */ + public function flag_post_for_publicize( $new_status, $old_status, $post ) { + if ( ! $this->post_type_is_publicizeable( $post->post_type ) ) { + return; + } + + $should_publicize = $this->should_submit_post_pre_checks( $post ); + + if ( 'publish' === $new_status && 'publish' !== $old_status ) { + /** + * Determines whether a post being published gets publicized. + * + * Side-note: Possibly our most alliterative filter name. + * + * @module publicize + * + * @since 0.1.0 No longer defaults to true. Adds checks to not publicize based on different contexts. + * @since 4.1.0 + * + * @param bool $should_publicize Should the post be publicized? Default to true. + * @param WP_POST $post Current Post object. + */ + $should_publicize = apply_filters( 'publicize_should_publicize_published_post', $should_publicize, $post ); + + if ( $should_publicize ) { + update_post_meta( $post->ID, $this->PENDING, true ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase + } + } + } + + /** + * Test a connection. + * + * @param string $service_name Name of the service. + * @param array $connection Connection to be tested. + */ + public function test_connection( $service_name, $connection ) { + $id = $this->get_connection_id( $connection ); + + $xml = new Jetpack_IXR_Client(); + $xml->query( 'jetpack.testPublicizeConnection', $id ); + + // Bail if all is well. + if ( ! $xml->isError() ) { + return true; + } + + $xml_response = $xml->getResponse(); + $connection_test_message = $xml_response['faultString']; + + // Set up refresh if the user can. + $user_can_refresh = current_user_can( $this->GLOBAL_CAP ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase + if ( $user_can_refresh ) { + /* translators: %s is the name of a social media service */ + $refresh_text = sprintf( _x( 'Refresh connection with %s', 'Refresh connection with {social media service}', 'jetpack-publicize-pkg' ), $this->get_service_label( $service_name ) ); + $refresh_url = $this->refresh_url( $service_name ); + } + + $error_data = array( + 'user_can_refresh' => $user_can_refresh, + 'refresh_text' => $refresh_text, + 'refresh_url' => $refresh_url, + ); + + return new \WP_Error( 'pub_conn_test_failed', $connection_test_message, $error_data ); + } + + /** + * Checks if post has already been shared by Publicize in the past. + * + * Jetpack uses two methods: + * 1. A POST_DONE . 'all' postmeta flag, or + * 2. if the post has already been published. + * + * @since 6.7.0 + * + * @param integer $post_id Optional. Post ID to query connection status for: will use current post if missing. + * + * @return bool True if post has already been shared by Publicize, false otherwise. + */ + public function post_is_done_sharing( $post_id = null ) { + // Defaults to current post if $post_id is null. + $post = get_post( $post_id ); + if ( $post === null ) { + return false; + } + + return 'publish' === $post->post_status || get_post_meta( $post->ID, $this->POST_DONE . 'all', true ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase + } + + /** + * Save a flag locally to indicate that this post has already been Publicized via the selected + * connections. + * + * @param int $post_ID Post ID. + * @param \WP_Post $post Post object. + */ + public function save_publicized( $post_ID, $post = null ) { + if ( $post === null ) { + return; + } + // Only do this when a post transitions to being published. + // phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase + if ( get_post_meta( $post->ID, $this->PENDING ) && $this->post_type_is_publicizeable( $post->post_type ) ) { + delete_post_meta( $post->ID, $this->PENDING ); + update_post_meta( $post->ID, $this->POST_DONE . 'all', true ); + } + // phpcs:enable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase + } + + /** + * Set post flags for Publicize. + * + * @param array $flags List of flags. + * @param \WP_Post $post Post object. + * @return array + */ + public function set_post_flags( $flags, $post ) { + $flags['publicize_post'] = false; + if ( ! $this->post_type_is_publicizeable( $post->post_type ) ) { + return $flags; + } + + $should_publicize = $this->should_submit_post_pre_checks( $post ); + + /** This filter is already documented in modules/publicize/publicize-jetpack.php */ + if ( ! apply_filters( 'publicize_should_publicize_published_post', $should_publicize, $post ) ) { + return $flags; + } + + $connected_services = $this->get_all_connections(); + + if ( empty( $connected_services ) ) { + return $flags; + } + + $flags['publicize_post'] = true; + + return $flags; + } + + /** + * Render Facebook options. + */ + public function options_page_facebook() { + $connection_name = isset( $_REQUEST['connection'] ) ? filter_var( wp_unslash( $_REQUEST['connection'] ) ) : null; + + // Nonce check. + check_admin_referer( 'options_page_facebook_' . $connection_name ); + + $connected_services = $this->get_all_connections(); + $connection = $connected_services['facebook'][ $connection_name ]; + $options_to_show = ( ! empty( $connection['connection_data']['meta']['options_responses'] ) ? $connection['connection_data']['meta']['options_responses'] : false ); + + $pages = ( ! empty( $options_to_show[1]['data'] ) ? $options_to_show[1]['data'] : false ); + + $page_selected = false; + if ( ! empty( $connection['connection_data']['meta']['facebook_page'] ) ) { + $found = false; + if ( $pages && isset( $pages->data ) && is_array( $pages->data ) ) { + foreach ( $pages->data as $page ) { + if ( $page->id === (int) $connection['connection_data']['meta']['facebook_page'] ) { + $found = true; + break; + } + } + } + + if ( $found ) { + $page_selected = $connection['connection_data']['meta']['facebook_page']; + } + } + + ?> + + <div id="thickbox-content"> + <?php + ob_start(); + Publicize_UI::connected_notice( 'Facebook' ); + $update_notice = ob_get_clean(); + + if ( ! empty( $update_notice ) ) { + echo wp_kses_post( $update_notice ); + } + $page_info_message = sprintf( + wp_kses( + /* translators: %s is the link to the support page about using Facebook with Publicize */ + __( 'Facebook supports Publicize connections to Facebook Pages, but not to Facebook Profiles. <a href="%s">Learn More about Publicize for Facebook</a>', 'jetpack-publicize-pkg' ), + array( 'a' => array( 'href' ) ) + ), + esc_url( Redirect::get_url( 'jetpack-support-publicize-facebook' ) ) + ); + + if ( $pages ) : + ?> + <p> + <?php + echo wp_kses( + __( 'Publicize to my <strong>Facebook Page</strong>:', 'jetpack-publicize-pkg' ), + array( 'strong' ) + ); + ?> + </p> + <table id="option-fb-fanpage"> + <tbody> + + <?php foreach ( $pages as $i => $page ) : ?> + <?php if ( ! ( $i % 2 ) ) : ?> + <tr> + <?php endif; ?> + <td class="radio"> + <input + type="radio" + name="option" + data-type="page" + id="<?php echo esc_attr( $page['id'] ); ?>" + value="<?php echo esc_attr( $page['id'] ); ?>" + <?php checked( $page_selected && (int) $page_selected === (int) $page['id'], true ); ?> /> + </td> + <td class="thumbnail"><label for="<?php echo esc_attr( $page['id'] ); ?>"><img + src="<?php echo esc_url( str_replace( '_s', '_q', $page['picture']['data']['url'] ) ); ?>" + width="50" height="50"/></label></td> + <td class="details"> + <label for="<?php echo esc_attr( $page['id'] ); ?>"> + <span class="name"><?php echo esc_html( $page['name'] ); ?></span><br/> + <span class="category"><?php echo esc_html( $page['category'] ); ?></span> + </label> + </td> + <?php if ( ( $i % 2 ) || ( count( $pages ) - 1 === $i ) ) : ?> + </tr> + <?php endif; ?> + <?php endforeach; ?> + + </tbody> + </table> + + <?php Publicize_UI::global_checkbox( 'facebook', $connection_name ); ?> + <p style="text-align: center;"> + <input type="submit" value="<?php esc_attr_e( 'OK', 'jetpack-publicize-pkg' ); ?>" + class="button fb-options save-options" name="save" + data-connection="<?php echo esc_attr( $connection_name ); ?>" + rel="<?php echo esc_attr( wp_create_nonce( 'save_fb_token_' . $connection_name ) ); ?>"/> + </p><br/> + <?php // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> + <p><?php echo $page_info_message; ?></p> + <?php else : ?> + <div> + <?php // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> + <p><?php echo $page_info_message; ?></p> + <p> + <?php + echo wp_kses( + sprintf( + /* translators: %1$s is the link to Facebook documentation to create a page, %2$s is the target of the link */ + __( '<a class="button" href="%1$s" target="%2$s">Create a Facebook page</a> to get started.', 'jetpack-publicize-pkg' ), + 'https://www.facebook.com/pages/creation/', + '_blank noopener noreferrer' + ), + array( 'a' => array( 'class', 'href', 'target' ) ) + ); + ?> + </p> + </div> + <?php endif; ?> + </div> + <?php + } + + /** + * Save Facebook options. + */ + public function options_save_facebook() { + $connection_name = isset( $_REQUEST['connection'] ) ? filter_var( wp_unslash( $_REQUEST['connection'] ) ) : null; + + // Nonce check. + check_admin_referer( 'save_fb_token_' . $connection_name ); + + if ( ! isset( $_POST['type'] ) || 'page' !== $_POST['type'] || ! isset( $_POST['selected_id'] ) ) { + return; + } + + // Check for a numeric page ID. + $page_id = $_POST['selected_id']; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- Manually validated just below + if ( ! ctype_digit( $page_id ) ) { + die( 'Security check' ); + } + + // Publish to Page. + $options = array( + 'facebook_page' => $page_id, + 'facebook_profile' => null, + ); + + $this->set_remote_publicize_options( $connection_name, $options ); + } + + /** + * Render Tumblr options. + */ + public function options_page_tumblr() { + $connection_name = isset( $_REQUEST['connection'] ) ? filter_var( wp_unslash( $_REQUEST['connection'] ) ) : null; + + // Nonce check. + check_admin_referer( 'options_page_tumblr_' . $connection_name ); + + $connected_services = $this->get_all_connections(); + $connection = $connected_services['tumblr'][ $connection_name ]; + $options_to_show = $connection['connection_data']['meta']['options_responses']; + $request = $options_to_show[0]; + + $blogs = $request['response']['user']['blogs']; + + $blog_selected = false; + + if ( ! empty( $connection['connection_data']['meta']['tumblr_base_hostname'] ) ) { + foreach ( $blogs as $blog ) { + if ( $connection['connection_data']['meta']['tumblr_base_hostname'] === $this->get_basehostname( $blog['url'] ) ) { + $blog_selected = $connection['connection_data']['meta']['tumblr_base_hostname']; + break; + } + } + } + + // Use their Primary blog if they haven't selected one yet. + if ( ! $blog_selected ) { + foreach ( $blogs as $blog ) { + if ( $blog['primary'] ) { + $blog_selected = $this->get_basehostname( $blog['url'] ); + } + } + } + ?> + + <div id="thickbox-content"> + + <?php + ob_start(); + Publicize_UI::connected_notice( 'Tumblr' ); + $update_notice = ob_get_clean(); + + if ( ! empty( $update_notice ) ) { + echo wp_kses_post( $update_notice ); + } + ?> + + <p><?php echo wp_kses( __( 'Publicize to my <strong>Tumblr blog</strong>:', 'jetpack-publicize-pkg' ), array( 'strong' ) ); ?></p> + + <ul id="option-tumblr-blog"> + + <?php + foreach ( $blogs as $blog ) { + $url = $this->get_basehostname( $blog['url'] ); + ?> + <li> + <input type="radio" name="option" data-type="blog" id="<?php echo esc_attr( $url ); ?>" + value="<?php echo esc_attr( $url ); ?>" <?php checked( $blog_selected === $url, true ); ?> /> + <label for="<?php echo esc_attr( $url ); ?>"><span + class="name"><?php echo esc_html( $blog['title'] ); ?></span></label> + </li> + <?php } ?> + + </ul> + + <?php Publicize_UI::global_checkbox( 'tumblr', $connection_name ); ?> + + <p style="text-align: center;"> + <input type="submit" value="<?php esc_attr_e( 'OK', 'jetpack-publicize-pkg' ); ?>" + class="button tumblr-options save-options" name="save" + data-connection="<?php echo esc_attr( $connection_name ); ?>" + rel="<?php echo esc_attr( wp_create_nonce( 'save_tumblr_blog_' . $connection_name ) ); ?>"/> + </p> <br/> + </div> + + <?php + } + + /** + * Get the hostname from a URL. + * + * @param string $url The URL to extract the hostname from. + * @return string|false|null + */ + public function get_basehostname( $url ) { + return wp_parse_url( $url, PHP_URL_HOST ); + } + + /** + * Save Tumblr options. + */ + public function options_save_tumblr() { + $connection_name = isset( $_POST['connection'] ) ? filter_var( wp_unslash( $_POST['connection'] ) ) : null; + + // Nonce check. + check_admin_referer( 'save_tumblr_blog_' . $connection_name ); + $options = array( 'tumblr_base_hostname' => isset( $_POST['selected_id'] ) ? sanitize_text_field( wp_unslash( $_POST['selected_id'] ) ) : null ); + + $this->set_remote_publicize_options( $connection_name, $options ); + + } + + /** + * Set remote Publicize options. + * + * @param int $id Connection ID. + * @param array $options Options to set. + */ + public function set_remote_publicize_options( $id, $options ) { + $xml = new Jetpack_IXR_Client(); + $xml->query( 'jetpack.setPublicizeOptions', $id, $options ); + + if ( ! $xml->isError() ) { + $response = $xml->getResponse(); + Jetpack_Options::update_option( 'publicize_connections', $response ); + $this->globalization( $id ); + } + } + + /** + * Render the options page for Twitter. + */ + public function options_page_twitter() { + Publicize_UI::options_page_other( 'twitter' ); + } + + /** + * Render the options page for LinkedIn. + */ + public function options_page_linkedin() { + Publicize_UI::options_page_other( 'linkedin' ); + } + + /** + * Save the options page for Twitter. + */ + public function options_save_twitter() { + $this->options_save_other( 'twitter' ); + } + + /** + * Save the options page for LinkedIn. + */ + public function options_save_linkedin() { + $this->options_save_other( 'linkedin' ); + } + + /** + * Save the options page for a service. + * + * @param string $service_name Name of the service to save options for. + */ + public function options_save_other( $service_name ) { + $connection_name = isset( $_REQUEST['connection'] ) ? filter_var( wp_unslash( $_REQUEST['connection'] ) ) : ''; + + // Nonce check. + check_admin_referer( 'save_' . $service_name . '_token_' . $connection_name ); + + $this->globalization( $connection_name ); + } + + /** + * If there's only one shared connection to Twitter set it as twitter:site tag. + * + * @param string $tag Tag. + */ + public function enhaced_twitter_cards_site_tag( $tag ) { + $custom_site_tag = get_option( 'jetpack-twitter-cards-site-tag' ); + if ( ! empty( $custom_site_tag ) ) { + return $tag; + } + if ( ! $this->is_enabled( 'twitter' ) ) { + return $tag; + } + $connections = $this->get_connections( 'twitter' ); + foreach ( $connections as $connection ) { + $connection_meta = $this->get_connection_meta( $connection ); + if ( $this->is_global_connection( $connection_meta ) ) { + // If the connection is shared. + return $this->get_display_name( 'twitter', $connection ); + } + } + + return $tag; + } + + /** + * Save the Publicized Twitter account when publishing a post. + * + * @param bool $submit_post Should the post be publicized. + * @param int $post_id Post ID. + * @param string $service_name Service name. + * @param array $connection Array of connection details. + */ + public function save_publicized_twitter_account( $submit_post, $post_id, $service_name, $connection ) { + if ( 'twitter' === $service_name && $submit_post ) { + $connection_meta = $this->get_connection_meta( $connection ); + $publicize_twitter_user = get_post_meta( $post_id, '_publicize_twitter_user' ); + if ( empty( $publicize_twitter_user ) || ! $this->is_global_connection( $connection_meta ) ) { + update_post_meta( $post_id, '_publicize_twitter_user', $this->get_display_name( 'twitter', $connection ) ); + } + } + } + + /** + * Get the Twitter username. + * + * @param string $account Twitter username. + * @param int $post_id ID of the post. + * @return string + */ + public function get_publicized_twitter_account( $account, $post_id ) { + if ( ! empty( $account ) ) { + return $account; + } + $account = get_post_meta( $post_id, '_publicize_twitter_user', true ); + if ( ! empty( $account ) ) { + return $account; + } + + return ''; + } + + /** + * Save the Publicized Facebook account when publishing a post + * Use only Personal accounts, not Facebook Pages + * + * @param bool $submit_post Should the post be publicized. + * @param int $post_id Post ID. + * @param string $service_name Service name. + * @param array $connection Array of connection details. + */ + public function save_publicized_facebook_account( $submit_post, $post_id, $service_name, $connection ) { + $connection_meta = $this->get_connection_meta( $connection ); + if ( 'facebook' === $service_name && isset( $connection_meta['connection_data']['meta']['facebook_profile'] ) && $submit_post ) { + $publicize_facebook_user = get_post_meta( $post_id, '_publicize_facebook_user' ); + if ( empty( $publicize_facebook_user ) || ! $this->is_global_connection( $connection_meta ) ) { + $profile_link = $this->get_profile_link( 'facebook', $connection ); + + if ( false !== $profile_link ) { + update_post_meta( $post_id, '_publicize_facebook_user', $profile_link ); + } + } + } + } +} diff --git a/plugins/jetpack/jetpack_vendor/automattic/jetpack-publicize/src/class-rest-controller.php b/plugins/jetpack/jetpack_vendor/automattic/jetpack-publicize/src/class-rest-controller.php new file mode 100644 index 00000000..68f19540 --- /dev/null +++ b/plugins/jetpack/jetpack_vendor/automattic/jetpack-publicize/src/class-rest-controller.php @@ -0,0 +1,115 @@ +<?php +/** + * The Publicize Rest Controller class. + * Registers the REST routes for Publicize. + * + * @package automattic/jetpack-publicize + */ + +namespace Automattic\Jetpack\Publicize; + +use Automattic\Jetpack\Connection\Client; +use Jetpack_Options; +use WP_Error; +use WP_REST_Server; + +/** + * Registers the REST routes for Search. + */ +class REST_Controller { + /** + * Whether it's run on WPCOM. + * + * @var bool + */ + protected $is_wpcom; + + /** + * Constructor + * + * @param bool $is_wpcom - Whether it's run on WPCOM. + */ + public function __construct( $is_wpcom = false ) { + $this->is_wpcom = $is_wpcom; + + } + + /** + * Registers the REST routes for Search. + * + * @access public + * @static + */ + public function register_rest_routes() { + register_rest_route( + 'jetpack/v4', + '/publicize/connections', + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_publicize_connections' ), + 'permission_callback' => array( $this, 'require_admin_privilege_callback' ), + ) + ); + } + + /** + * Only administrators can access the API. + * + * @return bool|WP_Error True if a blog token was used to sign the request, WP_Error otherwise. + */ + public function require_admin_privilege_callback() { + if ( current_user_can( 'manage_options' ) ) { + return true; + } + + $error_msg = esc_html__( + 'You are not allowed to perform this action.', + 'jetpack-publicize-pkg' + ); + + return new WP_Error( 'rest_forbidden', $error_msg, array( 'status' => rest_authorization_required_code() ) ); + } + + /** + * Gets the current Publicize connections for the site. + * + * GET `jetpack/v4/publicize/connections` + */ + public function get_publicize_connections() { + $blog_id = $this->get_blog_id(); + $path = sprintf( '/sites/%d/publicize/connections', absint( $blog_id ) ); + $response = Client::wpcom_json_api_request_as_user( $path, '2', array(), null, 'wpcom' ); + return rest_ensure_response( $this->make_proper_response( $response ) ); + } + + /** + * Forward remote response to client with error handling. + * + * @param array|WP_Error $response - Response from WPCOM. + */ + protected function make_proper_response( $response ) { + if ( is_wp_error( $response ) ) { + return $response; + } + + $body = json_decode( wp_remote_retrieve_body( $response ), true ); + $status_code = wp_remote_retrieve_response_code( $response ); + + if ( 200 === $status_code ) { + return $body; + } + + return new WP_Error( + isset( $body['error'] ) ? 'remote-error-' . $body['error'] : 'remote-error', + isset( $body['message'] ) ? $body['message'] : 'unknown remote error', + array( 'status' => $status_code ) + ); + } + + /** + * Get blog id + */ + protected function get_blog_id() { + return $this->is_wpcom ? get_current_blog_id() : Jetpack_Options::get_option( 'id' ); + } +} |