PluginProbe
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) / 1.8.13.1
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) v1.8.13.1
1.8.13.1 1.8.13 1.8.12.3 1.8.12.2 1.8.12.1 1.8.12 1.8.11.3 1.8.11.2 1.8.11.1 1.8.11 1.6.6 1.6.60 1.6.7 1.6.8 1.6.9 1.7.0 1.7.0.1 1.7.0.11 1.7.0.12 1.7.0.14 1.7.0.2 1.7.0.3 1.7.0.5 1.7.0.6 1.7.0.7 All 212 releases
charitable / includes / admin / addons-directory / class-charitable-addons-catalogue.php

class-charitable-addons-catalogue.php in Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) 1.8.13.1, at includes/admin/addons-directory/class-charitable-addons-catalogue.php

209 lines 5.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Static addon catalogue for sites with no licence key.
4 *
5 * A licence-free site cannot act on the /edd-api/versions-v3/ response - `install` is a
6 * licence-keyed download URL - so it no longer asks for one. This serves the same record
7 * shape from a static file instead: no licence key and no home_url() on the wire, a weekly
8 * cache, and a refresh on a schedule rather than during a page render.
9 *
10 * Falls back to the copy bundled in the plugin, so the catalogue is present before the
11 * remote file exists, offline, and when the host is unreachable.
12 *
13 * @package Charitable/Classes/Charitable_Addons_Catalogue
14 * @since 1.8.13.1
15 */
16
17 if ( ! defined( 'ABSPATH' ) ) {
18 exit;
19 }
20
21 if ( ! class_exists( 'Charitable_Addons_Catalogue' ) ) :
22
23 /**
24 * Charitable_Addons_Catalogue
25 *
26 * @since 1.8.13.1
27 */
28 class Charitable_Addons_Catalogue {
29
30 /**
31 * Single instance.
32 *
33 * @var Charitable_Addons_Catalogue|null
34 */
35 private static $instance = null;
36
37 /**
38 * Remote catalogue, alongside the other static feeds already hosted there.
39 */
40 const SOURCE_URL = 'https://plugin.wpcharitable.com/wp-content/charitable-addons.json';
41
42 /**
43 * Option holding the fetched catalogue.
44 */
45 const OPTION = 'charitable_addons_catalogue';
46
47 /**
48 * Weekly refresh event.
49 */
50 const CRON_HOOK = 'charitable_addons_catalogue_refresh';
51
52 /**
53 * Returns and/or creates the single instance.
54 *
55 * @since 1.8.13.1
56 *
57 * @return Charitable_Addons_Catalogue
58 */
59 public static function get_instance() {
60 if ( is_null( self::$instance ) ) {
61 self::$instance = new self();
62 }
63 return self::$instance;
64 }
65
66 /**
67 * Book the weekly refresh if it is not already scheduled.
68 *
69 * @since 1.8.13.1
70 *
71 * @return void
72 */
73 public function maybe_schedule() {
74 if ( wp_next_scheduled( self::CRON_HOOK ) ) {
75 return;
76 }
77
78 // First run is jittered so every site does not fetch at once.
79 wp_schedule_event( time() + wp_rand( HOUR_IN_SECONDS, DAY_IN_SECONDS ), 'weekly', self::CRON_HOOK );
80 }
81
82 /**
83 * The catalogue: stored copy if fresh, otherwise the bundled copy.
84 *
85 * Never fetches. A page render must not block on the network, which is the whole
86 * reason the licensed path was a problem; the cron handler does the fetching.
87 *
88 * @since 1.8.13.1
89 *
90 * @return array
91 */
92 public function get() {
93 $stored = get_option( self::OPTION );
94
95 if ( is_array( $stored ) && ! empty( $stored['addons'] ) ) {
96 return $stored['addons'];
97 }
98
99 return $this->get_bundled();
100 }
101
102 /**
103 * The copy shipped inside the plugin.
104 *
105 * @since 1.8.13.1
106 *
107 * @return array
108 */
109 public function get_bundled() {
110 $path = charitable()->get_path( 'includes', true ) . 'admin/addons-directory/data/charitable-addons.json';
111
112 if ( ! file_exists( $path ) ) {
113 return array();
114 }
115
116 $json = file_get_contents( $path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- local, bundled file.
117
118 return $this->validate( json_decode( $json, true ) );
119 }
120
121 /**
122 * Fetch the remote catalogue and store it. Called by cron.
123 *
124 * @since 1.8.13.1
125 *
126 * @return bool Whether a usable catalogue was stored.
127 */
128 public function fetch_and_store() {
129 $response = wp_remote_get(
130 apply_filters( 'charitable_addons_catalogue_source_url', self::SOURCE_URL ),
131 array(
132 'timeout' => 10,
133 // Admin-only helper, and this runs in cron. Core's default already carries the
134 // same WordPress version and site URL, so falling back loses only the edition tag.
135 'user-agent' => function_exists( 'charitable_get_default_user_agent' ) ? charitable_get_default_user_agent() : null,
136 )
137 );
138
139 if ( is_wp_error( $response ) || 200 !== (int) wp_remote_retrieve_response_code( $response ) ) {
140 return false;
141 }
142
143 $addons = $this->validate( json_decode( wp_remote_retrieve_body( $response ), true ) );
144
145 // Keep the previous copy rather than replacing it with nothing.
146 if ( empty( $addons ) ) {
147 return false;
148 }
149
150 update_option(
151 self::OPTION,
152 array(
153 'addons' => $addons,
154 'fetched' => time(),
155 ),
156 false
157 );
158
159 return true;
160 }
161
162 /**
163 * Keep only well-formed records, and never trust a download URL from this source.
164 *
165 * The static catalogue is published with install/package/download_link blanked. If a
166 * value ever appears in one, something upstream regenerated the file wrongly, so they
167 * are blanked again here rather than handed to the installer.
168 *
169 * @since 1.8.13.1
170 *
171 * @param mixed $addons Decoded catalogue.
172 * @return array
173 */
174 private function validate( $addons ) {
175 if ( ! is_array( $addons ) ) {
176 return array();
177 }
178
179 $clean = array();
180
181 foreach ( $addons as $addon ) {
182 if ( ! is_array( $addon ) || empty( $addon['slug'] ) || ! is_string( $addon['slug'] ) ) {
183 continue;
184 }
185
186 $addon['install'] = '';
187 $addon['package'] = '';
188 $addon['download_link'] = '';
189
190 // Consumers read these without guarding, so guarantee they exist.
191 foreach ( array( 'name', 'path', 'icon', 'homepage', 'new_version' ) as $key ) {
192 $addon[ $key ] = isset( $addon[ $key ] ) && is_scalar( $addon[ $key ] ) ? (string) $addon[ $key ] : '';
193 }
194
195 foreach ( array( 'license', 'featured' ) as $key ) {
196 $addon[ $key ] = isset( $addon[ $key ] ) && is_array( $addon[ $key ] ) ? array_values( $addon[ $key ] ) : array();
197 }
198
199 $addon['sections'] = isset( $addon['sections'] ) && is_array( $addon['sections'] ) ? $addon['sections'] : array();
200
201 $clean[ $addon['slug'] ] = $addon;
202 }
203
204 return $clean;
205 }
206 }
207
208 endif;
209