PluginProbe ʕ •ᴥ•ʔ
EmbedPress – PDF Embedder, 3D PDF FlipBook, Google Reviews, YouTube Videos, Upload & Embed PDF documents / 4.6.0
EmbedPress – PDF Embedder, 3D PDF FlipBook, Google Reviews, YouTube Videos, Upload & Embed PDF documents v4.6.0
4.6.0 4.5.6 4.5.5 4.5.4 4.5.3 4.5.2 trunk 1.0.0 1.1.0 1.1.1 1.1.2 1.1.3 1.2.0 1.3.0 1.3.1 1.4.0 1.4.1 1.4.2 1.4.3 1.4.4 1.5.0 1.6.0 1.6.1 1.6.2 1.6.3 1.7.0 1.7.1 1.7.2 1.7.3 1.7.4 1.7.5 2.0.0 2.0.1 2.0.2 2.0.3 2.1.0 2.1.1 2.1.2 2.1.3 2.1.4 2.1.5 2.1.6 2.2.0 2.2.1 2.2.2 2.3.0 2.3.1 2.3.2 2.3.3 2.4.0 2.4.1 2.5.0 2.5.1 2.5.2 2.5.3 2.5.4 2.5.5 2.6.0 2.6.1 2.6.2 2.7.0 2.7.1 2.7.2 2.7.3 2.7.4 2.7.5 2.7.6 2.7.7 3.0.0 3.0.1 3.0.2 3.0.3 3.0.4 3.1.0 3.1.1 3.1.2 3.1.3 3.2.0 3.2.1 3.3.0 3.3.1 3.3.2 3.3.3 3.3.4 3.3.5 3.3.6 3.3.7 3.4.0 3.4.1 3.4.2 3.4.3 3.5.0 3.5.1 3.5.2 3.5.3 3.6.0 3.6.1 3.6.2 3.6.3 3.6.4 3.6.5 3.6.6 3.6.7 3.6.8 3.7.0 3.7.1 3.7.2 3.7.3 3.8.0 3.8.1 3.8.2 3.8.3 3.8.4 3.8.5 3.9.0 3.9.1 3.9.10 3.9.11 3.9.12 3.9.13 3.9.14 3.9.15 3.9.16 3.9.17 3.9.2 3.9.3 3.9.4 3.9.5 3.9.6 3.9.7 3.9.8 3.9.9 4.0.0 4.0.1 4.0.10 4.0.11 4.0.12 4.0.13 4.0.14 4.0.2 4.0.3 4.0.4 4.0.5 4.0.6 4.0.7 4.0.8 4.0.9 4.1.0 4.1.1 4.1.10 4.1.2 4.1.3 4.1.4 4.1.5 4.1.6 4.1.7 4.1.8 4.1.9 4.2.0 4.2.1 4.2.2 4.2.3 4.2.4 4.2.5 4.2.6 4.2.7 4.2.8 4.2.9 4.3.0 4.3.1 4.4.0 4.4.1 4.4.10 4.4.11 4.4.2 4.4.3 4.4.4 4.4.5 4.4.6 4.4.7 4.4.8 4.4.9 4.5.0 4.5.1
embedpress / EmbedPress / ThirdParty / Googlecalendar / Embedpress_Google_Helper.php
embedpress / EmbedPress / ThirdParty / Googlecalendar Last commit date
assets 1 month ago Embedpress_Google_Helper.php 3 days ago GoogleClient.php 3 days ago
Embedpress_Google_Helper.php
1020 lines
1 <?php
2
3 if ( !class_exists( 'EmbedPress_GoogleClient') ) {
4 require_once 'GoogleClient.php';
5 }
6 if ( !defined( 'EPGC_NOTICES_VERIFY_SUCCESS') ) {
7 define('EPGC_NOTICES_VERIFY_SUCCESS', 'Verify OK!');
8 define('EPGC_NOTICES_REVOKE_SUCCESS', 'Access revoked. This plugin does not have access to your calendars anymore.');
9 define('EPGC_NOTICES_REMOVE_SUCCESS', sprintf('Plugin data removed. Make sure to also manually revoke access to your calendars in the Google <a target="__blank" href="%s">Permissions</a> page!', 'https://myaccount.google.com/permissions'));
10 define('EPGC_NOTICES_CALENDARLIST_UPDATE_SUCCESS', 'Calendars updated.');
11 define('EPGC_NOTICES_COLORLIST_UPDATE_SUCCESS', 'Colors updated.');
12 define('EPGC_NOTICES_CACHE_DELETED', 'Cache deleted.');
13
14 define('EPGC_ERRORS_CLIENT_SECRET_MISSING', 'No client secret.');
15 define('EPGC_ERRORS_CLIENT_SECRET_INVALID', 'Invalid client secret.');
16 define('EPGC_ERRORS_ACCESS_TOKEN_MISSING', 'No access token.');
17 define('EPGC_ERRORS_REFRESH_TOKEN_MISSING', sprintf('Your refresh token is missing!<br><br>This can only be solved by manually revoking this plugin&#39;s access in the Google <a target="__blank" href="%s">Permissions</a> page and remove all plugin data.', 'https://myaccount.google.com/permissions'));
18 define('EPGC_ERRORS_ACCESS_REFRESH_TOKEN_MISSING', 'No access and refresh tokens.');
19 define('EPGC_ERRORS_REDIRECT_URI_MISSING', 'URI <code>%s</code> missing in the client secret file. Adjust your Google project and upload the new client secret file.');
20 define('EPGC_ERRORS_INVALID_FORMAT', 'Invalid format');
21 define('EPGC_ERRORS_NO_CALENDARS', 'No calendars');
22 define('EPGC_ERRORS_NO_SELECTED_CALENDARS', 'No selected calendars');
23 define('EPGC_ERRORS_TOKEN_AND_API_KEY_MISSING', 'Access token and API key are missing.');
24 define('EPGC_TRANSIENT_PREFIX', 'pgc_ev_');
25 define('EPGC_ENQUEUE_ACTION_PRIORITY', 11);
26 define( 'EPGC_REDIRECT_URL', admin_url('admin.php?page=embedpress&page_type=google-calendar'));
27 }
28 if (!defined('EPGC_EVENTS_MAX_RESULTS')) {
29 define('EPGC_EVENTS_MAX_RESULTS', 250);
30 }
31
32 if (!defined('EPGC_EVENTS_DEFAULT_TITLE')) {
33 define('EPGC_EVENTS_DEFAULT_TITLE', '');
34 }
35
36
37 if (!defined('EPGC_ASSET_URL')) {
38 define('EPGC_ASSET_URL', plugin_dir_url(__FILE__) .'assets/');
39 }
40
41 class Embedpress_Google_Helper {
42
43 public static function print_calendar_list($calendarList = []) {
44 if ( empty( $calendarList) ) {
45 $calendarList = static::getDecoded( 'epgc_calendarlist' ); //settings_selected_calendar_ids_json_cb
46 }
47 if ( ! empty( $calendarList ) ) {
48 $selectedCalendarIds = get_option( 'epgc_selected_calendar_ids' ); // array
49 if ( empty( $selectedCalendarIds ) ) {
50 $selectedCalendarIds = [];
51 }
52 ?>
53 <ul>
54 <?php foreach ( $calendarList as $calendar ) { ?>
55 <?php
56 $calendarId = $calendar['id'];
57 $htmlId = md5( $calendarId );
58 ?>
59 <p class="epgc-calendar-filter">
60 <input id="<?php echo $htmlId; ?>" type="checkbox" name="epgc_selected_calendar_ids[]"
61 <?php if ( in_array( $calendarId, $selectedCalendarIds ) ) {
62 echo ' checked ';
63 } ?>
64 value="<?php echo esc_attr( $calendarId ); ?>"/>
65 <label for="<?php echo $htmlId; ?>">
66 <span class="epgc-calendar-color" style="background-color:<?php echo esc_attr( $calendar['backgroundColor'] ); ?>"></span>
67 <?php echo esc_html( $calendar['summary'] ); ?><?php if ( ! empty( $calendar['primary'] ) ) {
68 echo ' (primary)';
69 } ?>
70 </label>
71 <br>ID: <?php echo esc_html( $calendarId ); ?>
72 </p>
73 <?php } ?>
74 </ul>
75 <?php
76 $refreshToken = get_option( "epgc_refresh_token" );
77 if ( empty( $refreshToken ) ) {
78 static::show_notice( EPGC_ERRORS_REFRESH_TOKEN_MISSING, 'error', false );
79 }
80 } else {
81 ?>
82 <p><?php _e( 'No calendar was found.', 'embedpress' ); ?></p>
83 <?php
84 }
85 }
86
87 /**
88 * Helper function to return array from option (that should be a JSON string).
89 * @return array or $default = null
90 */
91 public static function getDecoded($optionName, $default = null) {
92 $item = get_option($optionName);
93 // $item should be a JSON string.
94 if (!empty($item)) {
95 return json_decode($item, true);
96 }
97 return $default;
98 }
99
100
101 public static function show_notice($notice, $type, $dismissable) {
102 ?>
103 <div class="notice notice-<?php echo esc_attr($type); echo $dismissable ? ' is-dismissible' : ''; ?>">
104 <p><?php echo $notice; ?></p>
105 </div>
106 <?php
107 }
108
109 public static function ajax_get_calendar() {
110
111 // Nonces are user-session-bound, so a nonce minted for a logged-in author
112 // won't verify for an anonymous visitor served the same cached page HTML —
113 // the request would 403 even though the public calendar data is harmless
114 // to expose. Enforce the nonce only for authenticated requests; anon hits
115 // fall back to API-key/public-mode handling below.
116 if ( is_user_logged_in() ) {
117 check_ajax_referer('epgc_nonce');
118 }
119
120 // Capability gate only applies to OAuth mode — public API-key mode must
121 // stay reachable for anonymous visitors of the public calendar widget.
122 $hasAccessToken = get_option('epgc_access_token');
123
124 if ( ! empty( $hasAccessToken ) && is_user_logged_in() && ! current_user_can( 'read' ) ) {
125 wp_send_json_error( [ 'error' => 'Unauthorized' ], 403 );
126 }
127
128 try {
129
130 if (empty($_POST['start']) || empty($_POST['end'])) {
131 throw new Exception(EPGC_ERRORS_INVALID_FORMAT);
132 }
133
134 // Start and end are in ISO8601 string format with timezone offset (e.g. 2018-09-01T12:30:00-05:00)
135 $start = $_POST['start'];
136 $end = $_POST['end'];
137
138 $thisCalendarids = [];
139 $postedCalendarIds = [];
140 if (array_key_exists('thisCalendarids', $_POST) && !empty($_POST['thisCalendarids'])) {
141 $postedCalendarIds = array_map('trim', explode(',', $_POST['thisCalendarids']));
142 }
143 $privateSettingsCalendarListIds = array_map(function($item) {
144 return $item['id'];
145 }, static::getDecoded('epgc_calendarlist', []));
146 if (!empty($hasAccessToken)) {
147 // OAuth path: only IDs that are both known AND selected may pass.
148 // If the synced calendar list is missing (e.g. token saved but sync
149 // failed), refuse rather than fall through — otherwise a logged-in
150 // subscriber could submit `primary` and reach the admin's calendar
151 // via the stored bearer token.
152 $privateSettingsSelectedCalendarListIds = get_option('epgc_selected_calendar_ids');
153 if (empty($privateSettingsCalendarListIds) || empty($privateSettingsSelectedCalendarListIds)) {
154 throw new Exception(EPGC_ERRORS_NO_SELECTED_CALENDARS);
155 }
156 foreach ($postedCalendarIds as $calId) {
157 if (in_array($calId, $privateSettingsCalendarListIds, true) && in_array($calId, $privateSettingsSelectedCalendarListIds, true)) {
158 $thisCalendarids[] = $calId;
159 }
160 }
161 if (empty($thisCalendarids)) {
162 throw new Exception(EPGC_ERRORS_NO_SELECTED_CALENDARS);
163 }
164 } else {
165 // API-key (public) path: IDs come from widget/shortcode attributes
166 // and can only resolve to public Google calendars.
167 $thisCalendarids = $postedCalendarIds;
168 }
169
170 $cacheTime = get_option('epgc_cache_time'); // empty == no cache!
171
172 // We can have mutiple calendars with different calendar selections,
173 // so key should be including calendar selection.
174 $transientKey = EPGC_TRANSIENT_PREFIX . $start . $end . md5(implode('-', $thisCalendarids));
175
176 $transientItems = !empty($cacheTime) ? get_transient($transientKey) : false;
177
178 $calendarListByKey = static::get_calendars_by_key($thisCalendarids);
179
180 if ($transientItems !== false) {
181 wp_send_json(['items' => $transientItems, 'calendars' => $calendarListByKey]);
182 wp_die();
183 }
184
185 $colorList = false; // false means not queried yet / otherwise [] or filled []
186
187 $results = [];
188
189 $optParams = array(
190 'maxResults' => EPGC_EVENTS_MAX_RESULTS,
191 'orderBy' => 'startTime',
192 'singleEvents' => 'true',
193 'timeMin' => $start,
194 'timeMax' => $end,
195 );
196 if (!empty($_POST['timeZone'])) {
197 $optParams['timeZone'] = $_POST['timeZone'];
198 }
199
200 $hasAccessToken = get_option('epgc_access_token');
201
202 if (!empty($hasAccessToken)) {
203
204 $client = static::getGoogleClient(true);
205 if ($client->isAccessTokenExpired()) {
206 if (!$client->getRefreshTOken()) {
207 throw new Exception(EPGC_ERRORS_REFRESH_TOKEN_MISSING);
208 }
209 $client->refreshAccessToken();
210 }
211 $service = new EmbedPress_GoogleCalendarClient($client);
212
213 foreach ($thisCalendarids as $calendarId) {
214 $results[$calendarId] = $service->getEvents($calendarId, $optParams);
215 }
216
217 } elseif (!empty(get_option('epgc_api_key'))) {
218
219 $referer = !empty($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : null;
220 $apiKey = get_option('epgc_api_key');
221 $service = new EmbedPress_GoogleCalendarClient(null);
222 foreach ($thisCalendarids as $calendarId) {
223 $results[$calendarId] = $service->getEventsPublic($calendarId, $optParams, $apiKey, $referer);
224 }
225
226 } else {
227 // No API key and no OAuth2 token
228 throw new Exception(EPGC_ERRORS_TOKEN_AND_API_KEY_MISSING);
229 }
230
231 $items = [];
232 foreach ($results as $calendarId => $events) {
233 foreach ($events as $item) {
234 $newItem = [
235 'title' => empty($item['summary']) ? EPGC_EVENTS_DEFAULT_TITLE : $item['summary'],
236 'htmlLink' => $item['htmlLink'],
237 'description' => !empty($item['description']) ? $item['description'] : '',
238 'calId' => $calendarId,
239 'creator' => !empty($item['creator']) ? $item['creator'] : [],
240 'attendees' => !empty($item['attendees']) ? $item['attendees'] : [],
241 'attachments' => !empty($item['attachments']) ? $item['attachments'] : [],
242 'location' => !empty($item['location']) ? $item['location'] : ''
243 ];
244 if (!empty($item['start']['date'])) {
245 $newItem['allDay'] = true;
246 $newItem['start'] = $item['start']['date'];
247 $newItem['end'] = $item['end']['date'];
248 // $newItem['timeZone'] = $item['start']['timeZone']; // TODO? end timezone also exists...
249 } else {
250 $newItem['start'] = $item['start']['dateTime'];
251 $newItem['end'] = $item['end']['dateTime'];
252 // $newItem['timeZone'] = $item['start']['timeZone']; // TODO? end timezone also exists...
253 }
254 if (!empty($item['colorId'])) {
255 if ($colorList === false) {
256 $colorList = static::getDecoded('pgc_colorlist', []);
257 }
258 if (array_key_exists('event', $colorList) && array_key_exists($item['colorId'], $colorList['event'])) {
259 $newItem['bColor'] = $colorList['event'][$item['colorId']]['background'];
260 $newItem['fColor'] = $colorList['event'][$item['colorId']]['foreground'];
261 }
262 }
263
264 $items[] = $newItem;
265 }
266 }
267
268 if (!empty($cacheTime)) {
269 set_transient($transientKey, $items, $cacheTime * MINUTE_IN_SECONDS);
270 }
271
272 wp_send_json(['items' => $items, 'calendars' => $calendarListByKey]);
273 wp_die();
274 } catch (EmbedPress_GoogleClient_RequestException $ex) {
275 wp_send_json([
276 'error' => $ex->getMessage(),
277 'errorCode' => $ex->getCode(),
278 'errorDescription' => $ex->getDescription()]);
279 wp_die();
280 } catch (Exception $ex) {
281 wp_send_json([
282 'error' => $ex->getMessage(),
283 'errorCode' => $ex->getCode()]);
284 wp_die();
285 }
286 }
287
288 public static function get_calendars_by_key($calendarIds) {
289
290 $publicCalendarList = get_option('pgc_public_calendarlist');
291 if (empty($publicCalendarList)) {
292 $publicCalendarList = [];
293 }
294 $privateCalendarList = static::getDecoded('epgc_calendarlist', []);
295 if (empty($privateCalendarList)) {
296 $privateCalendarList = [];
297 }
298 $calendarList = $publicCalendarList + $privateCalendarList;
299 $keyedCalendarList = [];
300 foreach ($calendarList as $cal) {
301 $keyedCalendarList[$cal['id']] = $cal;
302 }
303
304 $calendarListByKey = [];
305 foreach ($calendarIds as $calId) {
306 $cal = array_key_exists($calId, $keyedCalendarList) ? $keyedCalendarList[$calId] : [
307 'summary' => $calId,
308 'backgroundColor' => 'rgb(121, 134, 203)'
309 ];
310 $calendarListByKey[$calId] = [
311 'summary' => $cal['summary'],
312 'backgroundColor' => $cal['backgroundColor']
313 ];
314 }
315
316 return $calendarListByKey;
317 }
318 /**
319 * Helper function that returns a valid Google Client.
320 * @return Embedpress_GoogleClient instance
321 * @param bool $withTokens If true, also get tokens.
322 * @throws Exception.
323 */
324 public static function getGoogleClient($withTokens = false) {
325
326 $authConfig = get_option('epgc_client_secret');
327 if (empty($authConfig)) {
328 throw new Exception(EPGC_ERRORS_CLIENT_SECRET_MISSING);
329 }
330 $authConfig = static::getDecoded('epgc_client_secret');
331 if (empty($authConfig)) {
332 throw new Exception(EPGC_ERRORS_CLIENT_SECRET_INVALID);
333 }
334
335 $c = new Embedpress_GoogleClient($authConfig);
336 $c->setScope('https://www.googleapis.com/auth/calendar.readonly');
337 if (!self::check_redirect_uri($authConfig)) {
338 throw new Exception(sprintf(EPGC_ERRORS_REDIRECT_URI_MISSING, EPGC_REDIRECT_URL));
339 }
340 $c->setRedirectUri(EPGC_REDIRECT_URL);
341 $c->setTokenCallback(function($accessTokenInfo, $refreshToken) {
342 update_option('epgc_access_token', json_encode($accessTokenInfo, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES), false);
343 if (!empty($refreshToken)) {
344 update_option('epgc_refresh_token', $refreshToken, false);
345 }
346 });
347
348 if ($withTokens) {
349 $accessToken = static::getDecoded('epgc_access_token');
350 if (empty($accessToken)) {
351 throw new Exception(EPGC_ERRORS_ACCESS_TOKEN_MISSING);
352 }
353 $c->setAccessTokenInfo($accessToken);
354 $refreshToken = get_option("epgc_refresh_token");
355 if (empty($refreshToken)) {
356 throw new Exception(EPGC_ERRORS_REFRESH_TOKEN_MISSING);
357 }
358 $c->setRefreshToken($refreshToken);
359 }
360
361 return $c;
362
363 }
364
365 /**
366 * Helper function to check if we have a valid redirect uri in the client secret.
367 * @return bool
368 */
369 public static function check_redirect_uri($decodedClientSecret) {
370 return !empty($decodedClientSecret)
371 && !empty($decodedClientSecret['web'])
372 && !empty($decodedClientSecret['web']['redirect_uris'])
373 && in_array(EPGC_REDIRECT_URL, $decodedClientSecret['web']['redirect_uris']);
374 }
375
376
377 /**
378 * Get a valid formatted client secret.
379 * @return array|false Secret Array, false if no exists, Exception for invalid one
380 **/
381 public static function get_valid_client_secret(&$error = '') {
382 $clientSecret = get_option('epgc_client_secret');
383 if (empty($clientSecret)) {
384 return false;
385 }
386 $clientSecret = static::getDecoded('epgc_client_secret');
387 if (empty($clientSecret)
388 || empty($clientSecret['web'])
389 || empty($clientSecret['web']['client_secret'])
390 || empty($clientSecret['web']['client_id']))
391 {
392 $error = EPGC_ERRORS_CLIENT_SECRET_INVALID;
393 } elseif (!self::check_redirect_uri($clientSecret))
394 {
395 $error = sprintf(EPGC_ERRORS_REDIRECT_URI_MISSING, admin_url('options-general.php?page=pgc'));
396 }
397 return $clientSecret;
398 }
399
400 public static function delete_calendar_cache() {
401 global $wpdb;
402 $wpdb->query("DELETE FROM " . $wpdb->options
403 . " WHERE option_name LIKE '_transient_timeout_" . EPGC_TRANSIENT_PREFIX . "%' OR option_name LIKE '_transient_" . EPGC_TRANSIENT_PREFIX . "%'");
404 }
405
406 /**
407 * Helper function to delete all plugin options.
408 */
409 public static function delete_options($which) { // which = all, public, private
410 if ($which === 'all' || $which === 'private') {
411 delete_option('epgc_access_token');
412 delete_option('epgc_refresh_token');
413 delete_option('epgc_selected_calendar_ids');
414 delete_option('epgc_calendarlist');
415 delete_option('epgc_client_secret');
416 }
417 if ($which === 'all' || $which === 'public') {
418 delete_option('epgc_api_key');
419 }
420 if ($which === 'all') {
421 delete_option('epgc_cache_time');
422 }
423 }
424
425 public static function uninstall() {
426 try {
427 $client = static::getGoogleClient();
428 $accessToken = static::getDecoded('epgc_access_token');
429 if (!empty($accessToken)) {
430 $client->setAccessTokenInfo($accessToken);
431 }
432 $refreshToken = get_option("epgc_refresh_token");
433 if (!empty($refreshToken)) {
434 $client->setRefreshToken($refreshToken);
435 }
436 if (empty($accessToken) && empty($refreshToken)) {
437 throw new Exception(EPGC_ERRORS_ACCESS_REFRESH_TOKEN_MISSING);
438 }
439 $client->revoke();
440 } catch (Exception $ex) {
441 // Too bad...
442 } finally {
443 // Clear all plugin data
444 static::delete_plugin_data();
445 }
446 }
447
448 /**
449 * Helper function to delete all plugin data.
450 */
451 public static function delete_plugin_data($which = 'all') {
452 self::delete_calendar_cache();
453 self::delete_options($which);
454 }
455 public static function removable_query_args($removable_query_args) {
456 $removable_query_args[] = 'epgcnotice';
457 return $removable_query_args;
458 }
459
460 public static function notices_init() {
461 if (!empty($_GET['epgcnotice'])) {
462 $epgcnotices = get_option('epgc_notices_' . get_current_user_id());
463 if (empty($epgcnotices)) {
464 return;
465 }
466 delete_option('epgc_notices_' . get_current_user_id());
467 add_action('admin_notices', function() use ($epgcnotices) {
468 foreach ($epgcnotices as $notice) {
469 ?>
470 <div class="notice notice-<?php echo esc_attr($notice['type']); ?> is-dismissible">
471 <p><?php echo esc_html($notice['content']); ?></p>
472 </div>
473 <?php
474 }
475 });
476 }
477 }
478
479 /**
480 * Helper function to add notice messages.
481 * @param bool $redirect Redirect if true.
482 */
483 public static function add_notice($content, $type = 'success', $redirect = false) {
484 $epgcnotices = get_option('epgc_notices_' . get_current_user_id());
485 if (empty($epgcnotices)) {
486 $epgcnotices = [];
487 }
488 $epgcnotices[] = [
489 'content' => $content,
490 'type' => $type
491 ];
492 update_option('epgc_notices_' . get_current_user_id(), $epgcnotices, false);
493 if ($redirect) {
494 wp_redirect(EPGC_REDIRECT_URL ."&epgcnotice=true");
495 }
496 }
497
498 /**
499 * Helper function die with different kind of errors.
500 */
501 public static function embedpress_die($error = null) {
502 $backLink = '<br><br><a href="' . admin_url('admin.php?page=embedpress&page_type=google-calendar') . '">' . __('Back', 'embedpress') . '</a>';
503 if (empty($error)) {
504 wp_die(__('Unknown error', 'embedpress') . $backLink);
505 }
506 if ($error instanceof Exception) {
507 $s = [];
508 if ($error->getCode()) {
509 $x[] = $error->getCode();
510 }
511 $s[] = $error->getMessage();
512 if ($error instanceof Embedpress_GoogleClient_RequestException) {
513 if ($error->getDescription()) {
514 $s[] = $error->getDescription();
515 }
516 }
517 wp_die(implode("<br>", array_map('esc_html', $s)) . $backLink);
518 } elseif (is_array($error)) {
519 wp_die(implode("<br>", array_map('esc_html', $error)) . $backLink);
520 } elseif (is_string($error)) {
521 wp_die(esc_html($error) . $backLink);
522 } else {
523 wp_die(__('Unknown error format', 'embedpress') . $backLink);
524 }
525 }
526
527 /**
528 * Helper function to return pretty printed JSON string.
529 * @return string
530 */
531 public static function getPrettyJSONString($jsonObject) {
532 return str_replace(" ", " ", json_encode($jsonObject, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
533 }
534
535 public static function sort_calendars(&$items) {
536 // Set locale to UTF-8 variant if this is not the case.
537 if (strpos(setlocale(LC_COLLATE, 0), '.UTF-8') === false) {
538 // If we set this to a non existing locale it will be the default locale after this call.
539 setlocale(LC_COLLATE, get_locale() . '.UTF-8');
540 }
541 usort($items, function($a, $b) {
542 return strcoll($a['summary'], $b['summary']);
543 });
544 }
545 public static function shortcode($atts = [], $content = null) {
546
547 // When we have no attributes, $atts is an empty string
548 if (!is_array($atts)) {
549 $atts = [];
550 }
551
552 foreach ($atts as &$value) {
553 $value = esc_attr($value);
554 }
555 unset($value); // Unset reference
556
557 // Assets are now handled by AssetManager
558 // Calendar-specific assets are loaded when calendar shortcode is used
559 $defaultConfig = [
560 'header' => [
561 'left' => 'prev,next today',
562 'center' => 'title',
563 'right' => 'dayGridMonth,timeGridWeek,listWeek'
564 ]
565 ];
566 $userConfig = $defaultConfig; // copy
567 $userFilter = 'top';
568 $userEventPopup = 'true';
569 $userEventLink = 'true';
570 $userHidePassed = 'false';
571 $userHideFuture = 'false';
572 $userEventDescription = 'true';
573 $userEventLocation = 'true';
574 $userEventAttendees = 'false';
575 $userEventAttachments = 'false';
576 $userEventCreator = 'false';
577 $userEventCalendarname = 'false';
578 $calendarIds = '';
579 $uncheckedCalendarIds = ''; // in filter
580 // Get all non-fullcalendar known properties
581 foreach ($atts as $key => $value) {
582 if ($key === 'public') {
583 // This existsed in old versions, but we don't want it in our shortcode output, so skip it.
584 continue;
585 }
586 if ($key === 'filter') {
587 $userFilter = $value === 'true' ? 'top' : $value;
588 continue;
589 }
590 if ($key === 'eventpopup') {
591 $userEventPopup = $value;
592 continue;
593 }
594 if ($key === 'eventlink') {
595 $userEventLink = $value;
596 continue;
597 }
598 if ($key === 'hidepassed') {
599 $userHidePassed = $value;
600 continue;
601 }
602 if ($key === 'hidefuture') {
603 $userHideFuture = $value;
604 continue;
605 }
606 if ($key === 'eventdescription') {
607 $userEventDescription = $value;
608 continue;
609 }
610 if ($key === 'eventattachments') {
611 $userEventAttachments = $value;
612 continue;
613 }
614 if ($key === 'eventattendees') {
615 $userEventAttendees = $value;
616 continue;
617 }
618 if ($key === 'eventlocation') {
619 $userEventLocation = $value;
620 continue;
621 }
622 if ($key === 'eventcreator') {
623 $userEventCreator = $value;
624 continue;
625 }
626 if ($key === 'eventcalendarname') {
627 $userEventCalendarname = $value;
628 continue;
629 }
630 if ($key === 'uncheckedcalendarids' && !empty($value)) {
631 $uncheckedCalendarIds = $value; // comma separated string
632 continue;
633 }
634
635 if ($key === 'calendarids') {
636 if (!empty($value)) {
637 $calendarIds = $value; // comma separated string
638 }
639 continue;
640 }
641 if ($key === 'fullcalendarconfig') {
642 // A JSON string that we can directly send to FullCalendar
643 $userConfig = json_decode($value, true);
644 } else {
645 // Fullcalendar properties that get passed to fullCalendar instance.
646 $parts = explode('-', $key);
647 $partsCount = count($parts);
648 if ($partsCount > 1) {
649 $currentUserConfigLayer = &$userConfig;
650 for ($i = 0; $i < $partsCount; $i++) {
651 $part = $parts[$i];
652 if ($i + 1 === $partsCount) {
653 if ($value === 'true') {
654 $value = true;
655 } elseif ($value === 'false') {
656 $value = $value;
657 }
658 $currentUserConfigLayer[$part] = $value;
659 } else {
660 if (!array_key_exists($part, $currentUserConfigLayer)) {
661 $currentUserConfigLayer[$part] = [];
662 }
663 $currentUserConfigLayer = &$currentUserConfigLayer[$part];
664 }
665 }
666 } else {
667 $userConfig[$key] = $value;
668 }
669 }
670 }
671
672 $dataCalendarIds = '';
673 if (!empty($calendarIds)) {
674 $dataCalendarIds = 'data-calendarids=\'' . json_encode(array_map('trim', explode(',', $calendarIds))) . '\'';
675 } else {
676 $privateSettingsSelectedCalendarListIds = get_option('epgc_selected_calendar_ids', []);
677 if (!empty($privateSettingsSelectedCalendarListIds)) {
678 $dataCalendarIds = 'data-calendarids=\'' . json_encode($privateSettingsSelectedCalendarListIds) . '\'';
679 }
680 }
681
682 $dataUnchekedCalendarIds = '';
683 if (!empty($uncheckedCalendarIds)) {
684 $dataUnchekedCalendarIds = 'data-uncheckedcalendarids=\'' . json_encode(array_map('trim', explode(',', $uncheckedCalendarIds))) . '\'';
685 }
686
687 $filterHTML = '<div class="epgc-calendar-filter" ' . $dataUnchekedCalendarIds . '></div>';
688
689 // Embed nonce + ajax URL directly on the wrapper so the front-end JS keeps
690 // working even when an optimizer/cache plugin strips the inline
691 // wp_localize_script() block that normally exposes `embedpressCalendarData`.
692 $wrapperData = ' data-nonce="' . esc_attr(wp_create_nonce('epgc_nonce'))
693 . '" data-ajaxurl="' . esc_url(admin_url('admin-ajax.php')) . '"';
694
695 return '<div class="epgc-calendar-wrapper epgc-calendar-page"' . $wrapperData . '>' . ($userFilter === 'top' ? wp_kses_post($filterHTML) : '') . '<div '
696
697 . $dataCalendarIds . ' data-filter="' . esc_attr($userFilter) . '" data-eventpopup="' . esc_attr($userEventPopup) . '" data-eventlink="'
698 . esc_attr($userEventLink) . '" data-eventdescription="' . esc_attr($userEventDescription) . '" data-eventlocation="'
699 . esc_attr($userEventLocation) . '" data-eventattachments="' . esc_attr($userEventAttachments) . '" data-eventattendees="'
700 . esc_attr($userEventAttendees) . '" data-eventcreator="' . esc_attr($userEventCreator) . '" data-eventcalendarname="'
701 . esc_attr($userEventCalendarname) . '" data-hidefuture="' . esc_attr($userHideFuture) . '" data-hidepassed="'
702 . esc_attr($userHidePassed) . '" data-config="' . esc_attr(json_encode($userConfig)) . '" data-locale="'
703 . esc_attr(get_locale()) . '" class="epgc-calendar"></div>' . ($userFilter === 'bottom' ? $filterHTML : '') . '</div>';
704 }
705
706 /**
707 * Reject an admin-post request that fails the capability or nonce check
708 * and stop execution. Used by the Google Calendar admin_post_* handlers so
709 * an unauthorized role can't drive the site's stored Google OAuth
710 * credentials and a forged (CSRF) request without a valid nonce is denied.
711 */
712 private static function admin_post_access_denied() {
713 wp_die(
714 esc_html__( 'Sorry, you are not allowed to perform this action.', 'embedpress' ),
715 esc_html__( 'Forbidden', 'embedpress' ),
716 [ 'response' => 403 ]
717 );
718 }
719
720 public static function admin_post_calendarlist() {
721 if ( ! current_user_can( 'manage_options' )
722 || ! isset( $_POST['epgc_calendarlist_data'] )
723 || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['epgc_calendarlist_data'] ) ), 'epgc_calendarlist' ) ) {
724 self::admin_post_access_denied();
725 }
726 try {
727 $client = static::getGoogleClient(true);
728 if ($client->isAccessTokenExpired()) {
729 if (!$client->getRefreshToken()) {
730 throw new Exception(EPGC_ERRORS_REFRESH_TOKEN_MISSING);
731 }
732 $client->refreshAccessToken();
733 }
734 $service = new Embedpress_GoogleCalendarClient($client);
735 $items = $service->getCalendarList();
736
737 self::sort_calendars($items);
738
739 update_option('epgc_calendarlist', self::getPrettyJSONString($items), false);
740 self::add_notice(PGC_NOTICES_CALENDARLIST_UPDATE_SUCCESS, 'success', true);
741 exit;
742 } catch (Exception $ex) {
743 self::embedpress_die($ex);
744 }
745 }
746 public static function admin_post_colorlist() {
747 if ( ! current_user_can( 'manage_options' )
748 || ! isset( $_POST['epgc_colorlist_data'] )
749 || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['epgc_colorlist_data'] ) ), 'epgc_colorlist' ) ) {
750 self::admin_post_access_denied();
751 }
752 try {
753 $client = static::getGoogleClient(true);
754 if ($client->isAccessTokenExpired()) {
755 if (!$client->getRefreshToken()) {
756 throw new Exception(PGC_ERRORS_REFRESH_TOKEN_MISSING);
757 }
758 $client->refreshAccessToken();
759 }
760 $service = new Embedpress_GoogleCalendarClient($client);
761 $items = $service->getColorList();
762 update_option('epgc_colorlist', self::getPrettyJSONString($items), false);
763 self::add_notice(EPGC_NOTICES_COLORLIST_UPDATE_SUCCESS, 'success', true);
764 exit;
765 } catch (Exception $ex) {
766 self::embedpress_die($ex);
767 }
768 }
769 public static function admin_post_deletecache() {
770 if ( ! isset( $_POST['epgc_deletecache_data'] ) || ! wp_verify_nonce( $_POST['epgc_deletecache_data'], 'epgc_deletecache' ) || !current_user_can('manage_options')) {
771 print 'Sorry, your nonce did not verify.';
772 exit;
773 } else {
774 self::delete_calendar_cache();
775 self::add_notice(PGC_NOTICES_CACHE_DELETED, 'success', true);
776 exit;
777 }
778 }
779 public static function admin_post_verify() {
780 if ( ! current_user_can( 'manage_options' )
781 || ! isset( $_POST['epgc_verify_data'] )
782 || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['epgc_verify_data'] ) ), 'epgc_verify' ) ) {
783 self::admin_post_access_denied();
784 }
785 try {
786 $client = static::getGoogleClient(true);
787 $client->refreshAccessToken();
788 self::add_notice(PGC_NOTICES_VERIFY_SUCCESS, 'success', true);
789 exit;
790 } catch (Exception $ex) {
791 self::embedpress_die($ex);
792 }
793 }
794 /**
795 * Register + enqueue the FullCalendar assets in the Gutenberg block editor
796 * so the Calendar block's ServerSideRender preview can hydrate. Without
797 * this, the editor pulls the server-rendered .epgc-calendar-wrapper HTML
798 * but main.js never loads to attach FullCalendar to it.
799 */
800 public static function enqueue_block_editor_assets() {
801 self::enqueue_scripts(); // registers everything
802 wp_enqueue_style('epgc');
803 wp_enqueue_script('epgc');
804 wp_enqueue_script('fullcalendar_moment_timezone');
805 wp_enqueue_script('fullcalendar_daygrid');
806 wp_enqueue_script('fullcalendar_timegrid');
807 wp_enqueue_script('fullcalendar_list');
808 wp_enqueue_script('fullcalendar_locales');
809 wp_enqueue_script('tippy');
810 }
811
812 public static function enqueue_scripts() {
813 // Dashicons are now handled by AssetManager
814 wp_register_style('fullcalendar', EPGC_ASSET_URL . 'lib/fullcalendar4/core/main.min.css', null, EMBEDPRESS_VERSION);
815 wp_register_style('fullcalendar_daygrid', EPGC_ASSET_URL . 'lib/fullcalendar4/daygrid/main.min.css', ['fullcalendar'], EMBEDPRESS_VERSION);
816 wp_register_style('fullcalendar_timegrid', EPGC_ASSET_URL . 'lib/fullcalendar4/timegrid/main.min.css', ['fullcalendar_daygrid'], EMBEDPRESS_VERSION);
817 wp_register_style('fullcalendar_list', EPGC_ASSET_URL . 'lib/fullcalendar4/list/main.min.css', ['fullcalendar'], EMBEDPRESS_VERSION);
818 wp_register_style('epgc', EPGC_ASSET_URL . 'css/epgc.css', ['fullcalendar_timegrid'], EMBEDPRESS_VERSION);
819 wp_register_style('tippy_light', EPGC_ASSET_URL . 'lib/tippy/light-border.css', null, EMBEDPRESS_VERSION);
820
821 //wp_enqueue_style( 'fullcalendar');
822 //wp_enqueue_style( 'fullcalendar_daygrid');
823 //wp_enqueue_style( 'fullcalendar_timegrid');
824 //wp_enqueue_style( 'fullcalendar_list');
825 //wp_enqueue_style( 'epgc');
826 //wp_enqueue_style( 'tippy_light');
827
828
829 wp_register_script('popper',EPGC_ASSET_URL . 'lib/popper.min.js', null, EMBEDPRESS_VERSION, true);
830 wp_register_script('tippy',EPGC_ASSET_URL . 'lib/tippy/tippy-bundle.umd.min.js', ['popper'], EMBEDPRESS_VERSION, true);
831 wp_register_script('my_moment',EPGC_ASSET_URL . 'lib/moment/moment-with-locales.min.js', null, EMBEDPRESS_VERSION, true);
832 wp_register_script('my_moment_timezone',EPGC_ASSET_URL . 'lib/moment/moment-timezone-with-data.min.js', ['my_moment'], EMBEDPRESS_VERSION, true);
833 wp_register_script('fullcalendar',EPGC_ASSET_URL . 'lib/fullcalendar4/core/main.min.js', ['my_moment_timezone'], EMBEDPRESS_VERSION, true);
834 wp_register_script('fullcalendar_moment',EPGC_ASSET_URL . 'lib/fullcalendar4/moment/main.min.js', ['fullcalendar'], EMBEDPRESS_VERSION, true);
835 wp_register_script('fullcalendar_moment_timezone',EPGC_ASSET_URL . 'lib/fullcalendar4/moment-timezone/main.min.js', ['fullcalendar_moment'], EMBEDPRESS_VERSION, true);
836 wp_register_script('fullcalendar_daygrid',EPGC_ASSET_URL . 'lib/fullcalendar4/daygrid/main.min.js', ['fullcalendar'], EMBEDPRESS_VERSION, true);
837 wp_register_script('fullcalendar_timegrid',EPGC_ASSET_URL . 'lib/fullcalendar4/timegrid/main.min.js', ['fullcalendar_daygrid'], EMBEDPRESS_VERSION, true);
838 wp_register_script('fullcalendar_list',EPGC_ASSET_URL . 'lib/fullcalendar4/list/main.min.js', ['fullcalendar'], EMBEDPRESS_VERSION, true);
839 wp_register_script('fullcalendar_locales',EPGC_ASSET_URL . 'lib/fullcalendar4/core/locales-all.min.js',['fullcalendar'], EMBEDPRESS_VERSION, true);
840 wp_register_script('epgc', EPGC_ASSET_URL . 'js/main.js',['fullcalendar'], EMBEDPRESS_VERSION, true);
841
842 //wp_enqueue_script('popper');
843 //wp_enqueue_script('my_moment');
844 //wp_enqueue_script('my_moment_timezone');
845 //wp_enqueue_script('fullcalendar');
846 //wp_enqueue_script('fullcalendar_moment');
847 //wp_enqueue_script('fullcalendar_moment_timezone');
848 //wp_enqueue_script('fullcalendar_daygrid');
849 //wp_enqueue_script('fullcalendar_timegrid');
850 //wp_enqueue_script('fullcalendar_list');
851 //wp_enqueue_script('fullcalendar_locales');
852 //wp_enqueue_script('epgc');
853
854 // Localization is now handled by LocalizationManager
855 // This section is kept for backward compatibility but functionality has been moved
856
857 }
858
859 public static function remove_private_data() {
860 if ( ! isset( $_POST['epgc_remove_private_data'] ) || ! wp_verify_nonce( $_POST['epgc_remove_private_data'], 'epgc_remove_private' ) || !current_user_can('manage_options')) {
861 print 'Sorry, your nonce did not verify.';
862 exit;
863 } else {
864 self::delete_plugin_data('private');
865 self::add_notice(EPGC_NOTICES_REMOVE_SUCCESS, 'success', true);
866 exit;
867 }
868 }
869
870 public static function admin_post_remove() {
871
872 if ( ! isset( $_POST['epgc_remove_private_data'] ) || ! wp_verify_nonce( $_POST['epgc_remove_private_data'], 'epgc_remove_private' ) || !current_user_can('manage_options')) {
873 print 'Sorry, your nonce did not verify.';
874 exit;
875 } else {
876
877 self::delete_plugin_data();
878 self::add_notice(EPGC_NOTICES_REMOVE_SUCCESS, 'success', true);
879 exit;
880 }
881
882 }
883 public static function admin_post_revoke() {
884 try {
885 $client = self::getGoogleClient();
886 $accessToken = self::getDecoded('epgc_access_token');
887 if (!empty($accessToken)) {
888 $client->setAccessTokenInfo($accessToken);
889 }
890 $refreshToken = get_option("epgc_refresh_token");
891 if (!empty($refreshToken)) {
892 $client->setRefreshToken($refreshToken);
893 }
894 if (empty($accessToken) && empty($refreshToken)) {
895 throw new Exception(EPGC_ERRORS_ACCESS_REFRESH_TOKEN_MISSING);
896 }
897 $client->revoke();
898 // Clear access and refresh tokens
899 self::delete_plugin_data('private');
900 self::add_notice(EPGC_NOTICES_REVOKE_SUCCESS, 'success', true);
901 exit;
902 } catch (Exception $ex) {
903 self::embedpress_die($ex);
904 }
905 }
906 public static function admin_post_authorize() {
907 if ( ! isset( $_POST['epgc_authorize_data'] ) || ! wp_verify_nonce( $_POST['epgc_authorize_data'], 'epgc_authorize' ) || !current_user_can('manage_options')) {
908 print 'Sorry, your nonce did not verify.';
909 exit;
910 } else {
911 try {
912 $client = self::getGoogleClient();
913 $client->authorize();
914 exit;
915 } catch (Exception $ex) {
916 self::embedpress_die($ex);
917 }
918 }
919
920 }
921
922 public static function fetch_calendar() {
923 if ( empty( $_GET['page']) || 'embedpress' !== $_GET['page'] ) {
924 return;
925 }
926
927 if ( !current_user_can( 'manage_options') ) {
928 return;
929 }
930 if (!empty($_GET['code'])) {
931 // Redirect from Google authorize with code that we can use to get access and refresh tokens.
932 try {
933 $client = self::getGoogleClient();
934 // This will also set the access and refresh tokens on the client
935 // and call the token callback we have set to save them in the options table.
936 $client->handleCodeRedirect();
937 $service = new Embedpress_GoogleCalendarClient($client);
938 $items = $service->getCalendarList();
939 self::sort_calendars($items);
940
941 update_option('epgc_calendarlist', self::getPrettyJSONString($items), false);
942 wp_redirect(EPGC_REDIRECT_URL);
943 exit;
944 } catch (Exception $ex) {
945 self::embedpress_die($ex);
946 }
947
948 }
949
950 $clientSecretError = '';
951 $clientSecret = self::get_valid_client_secret($clientSecretError);
952
953 $accessToken = self::getDecoded('epgc_access_token');
954
955 if (empty($clientSecret) || !empty($clientSecretError)) {
956 update_option('epgc_selected_calendar_ids', [], false);
957 }
958 if (!empty($accessToken)) {
959 // validate_selected_calendar_ids
960 }
961 if (empty($clientSecret) || !empty($clientSecretError)) {
962 // save new data from user input, show them input
963
964 } elseif (self::getDecoded('epgc_calendarlist')) {
965 // show calendar list
966 }
967
968 }
969 }
970
971
972
973
974
975
976 /**
977 * Add 'eepgcnotice' to the removable_query_args filter, so we can set this and
978 * WP will remove it for us. We use this for our custom admin notices. This way
979 * you can add parameters to the URL and check for them, but we won't see them
980 * in the URL.
981 */
982 add_filter('removable_query_args', [Embedpress_Google_Helper::class, 'removable_query_args']);
983
984 /**
985 * Check for 'epgcnotice' parameter and show admin notice if we have a option.
986 */
987 add_action('admin_init', [Embedpress_Google_Helper::class,'notices_init']);
988
989 /**
990 * Handle AJAX request from frontend.
991 */
992 add_action('wp_ajax_epgc_ajax_get_calendar', [Embedpress_Google_Helper::class, 'ajax_get_calendar']);
993 add_action('wp_ajax_nopriv_epgc_ajax_get_calendar', [Embedpress_Google_Helper::class, 'ajax_get_calendar']);
994
995 add_action('admin_post_epgc_calendarlist', [Embedpress_Google_Helper::class,'admin_post_calendarlist']);
996
997
998 add_action('admin_post_epgc_colorlist', [Embedpress_Google_Helper::class, 'admin_post_colorlist']);
999 add_action('admin_post_epgc_deletecache', [Embedpress_Google_Helper::class, 'admin_post_deletecache']);
1000
1001
1002 /**
1003 * Admin post action to verify if we have valid access and refresh token.
1004 */
1005 add_action('admin_post_epgc_verify', [Embedpress_Google_Helper::class, 'admin_post_verify']);
1006
1007 add_shortcode( 'embedpress_calendar', [Embedpress_Google_Helper::class, 'shortcode']);
1008 add_action('wp_enqueue_scripts', [Embedpress_Google_Helper::class, 'enqueue_scripts'], EPGC_ENQUEUE_ACTION_PRIORITY);
1009 add_action('enqueue_block_editor_assets', [Embedpress_Google_Helper::class, 'enqueue_block_editor_assets'], EPGC_ENQUEUE_ACTION_PRIORITY);
1010
1011 add_action('admin_post_epgc_remove_private', [Embedpress_Google_Helper::class, 'remove_private_data']);
1012
1013 /**
1014 * Admin post action to authorize access.
1015 */
1016 add_action('admin_post_epgc_authorize', [Embedpress_Google_Helper::class, 'admin_post_authorize']);
1017
1018 add_action('admin_init', [Embedpress_Google_Helper::class, 'fetch_calendar']);
1019
1020