TemplateLayouts
3 weeks ago
AirTable.php
1 year ago
Boomplay.php
1 year ago
Calendly.php
2 years ago
Canva.php
1 year ago
FITE.php
1 year ago
GettyImages.php
9 months ago
Giphy.php
2 years ago
GitHub.php
2 years ago
GoogleCalendar.php
7 months ago
GoogleDocs.php
2 years ago
GoogleDrive.php
2 years ago
GoogleMaps.php
2 years ago
GooglePhotos.php
3 months ago
Gumroad.php
2 years ago
InstagramFeed.php
3 weeks ago
LinkedIn.php
2 years ago
Meetup.php
5 months ago
NRKRadio.php
2 years ago
OneDrive.php
10 months ago
OpenSea.php
9 months ago
SelfHosted.php
1 month ago
Spreaker.php
1 year ago
TikTok.php
2 years ago
Twitch.php
2 years ago
Wistia.php
3 months ago
Wrapper.php
2 years ago
X.php
2 years ago
Youtube.php
3 weeks ago
index.html
7 years ago
Meetup.php
949 lines
| 1 | <?php |
| 2 | |
| 3 | /** |
| 4 | * Meetup.php |
| 5 | * |
| 6 | * @package Embera |
| 7 | * @author Michael Pratt <yo@michael-pratt.com> |
| 8 | * @link http://www.michael-pratt.com/ |
| 9 | * |
| 10 | * For the full copyright and license information, please view the LICENSE |
| 11 | * file that was distributed with this source code. |
| 12 | */ |
| 13 | |
| 14 | namespace EmbedPress\Providers; |
| 15 | |
| 16 | use EmbedPress\Includes\Classes\Helper; |
| 17 | use Embera\Provider\ProviderAdapter; |
| 18 | use Embera\Provider\ProviderInterface; |
| 19 | use Embera\Url; |
| 20 | |
| 21 | (defined('ABSPATH') && defined('EMBEDPRESS_IS_LOADED')) or die("No direct script access allowed."); |
| 22 | |
| 23 | /** |
| 24 | * Meetup Provider |
| 25 | * @link https://meetup.com |
| 26 | */ |
| 27 | class Meetup extends ProviderAdapter implements ProviderInterface |
| 28 | { |
| 29 | /** inline {@inheritdoc} */ |
| 30 | protected $shouldSendRequest = false; |
| 31 | /** inline {@inheritdoc} */ |
| 32 | protected $endpoint = 'https://api.meetup.com/oembed?format=json'; |
| 33 | |
| 34 | /** inline {@inheritdoc} */ |
| 35 | protected static $hosts = [ |
| 36 | 'meetup.com' |
| 37 | ]; |
| 38 | |
| 39 | /** inline {@inheritdoc} */ |
| 40 | protected $httpsSupport = true; |
| 41 | |
| 42 | /** inline {@inheritdoc} */ |
| 43 | protected $responsiveSupport = true; |
| 44 | |
| 45 | /** @var array Array with allowed params for the Meetup Provider */ |
| 46 | protected $allowedParams = [ |
| 47 | 'maxwidth', |
| 48 | 'maxheight', |
| 49 | 'timezone', |
| 50 | 'date_format', |
| 51 | 'time_format', |
| 52 | 'orderby', |
| 53 | 'order', |
| 54 | 'per_page', |
| 55 | 'enable_pagination' |
| 56 | ]; |
| 57 | |
| 58 | /** inline {@inheritdoc} */ |
| 59 | public function validateUrl(Url $url) |
| 60 | { |
| 61 | return (bool) ( |
| 62 | preg_match('~meetup\.com/(?:.+)~i', (string) $url) || |
| 63 | preg_match('~meetu\.ps/(?:\w+)/?$~i', (string) $url) |
| 64 | ); |
| 65 | } |
| 66 | |
| 67 | /** |
| 68 | * Check if the URL is an RSS feed URL or a group URL that should be treated as RSS |
| 69 | */ |
| 70 | private function isRssUrl($url) |
| 71 | { |
| 72 | // Check if it's already an RSS URL |
| 73 | if (preg_match('~meetup\.com/.+/events/rss~i', $url)) { |
| 74 | return true; |
| 75 | } |
| 76 | |
| 77 | // Check if it's a group URL that should be converted to RSS |
| 78 | if (preg_match('~meetup\.com/[^/]+/?$~i', $url)) { |
| 79 | return true; |
| 80 | } |
| 81 | |
| 82 | // Check if it's an events URL that should be converted to RSS |
| 83 | if (preg_match('~meetup\.com/[^/]+/events/?$~i', $url)) { |
| 84 | return true; |
| 85 | } |
| 86 | |
| 87 | return false; |
| 88 | } |
| 89 | |
| 90 | /** |
| 91 | * Check if pro features are enabled |
| 92 | */ |
| 93 | protected function isProFeaturesEnabled() |
| 94 | { |
| 95 | // Use the Helper class to check pro features |
| 96 | if (class_exists('\EmbedPress\Includes\Classes\Helper')) { |
| 97 | return Helper::is_pro_features_enabled(); |
| 98 | } |
| 99 | |
| 100 | // Fallback: check if pro plugin is active |
| 101 | return function_exists('is_embedpress_pro_active') && is_embedpress_pro_active(); |
| 102 | } |
| 103 | |
| 104 | /** |
| 105 | * Get pro upgrade message for RSS feeds using EmbedPress styling |
| 106 | */ |
| 107 | private function getProUpgradeMessage($response) |
| 108 | { |
| 109 | // Get the alert icon URL |
| 110 | $alert_icon_url = defined('EMBEDPRESS_URL_ASSETS') ? EMBEDPRESS_URL_ASSETS . 'images/alert.svg' : ''; |
| 111 | |
| 112 | $response['html'] = '<div class="embedpress-meetup-upgrade-notice" style=" |
| 113 | width: calc(100% - 30px); |
| 114 | max-width: 500px; |
| 115 | margin: 20px auto; |
| 116 | background: #fff; |
| 117 | border-radius: 20px; |
| 118 | padding: 30px; |
| 119 | display: flex; |
| 120 | flex-direction: column; |
| 121 | align-items: center; |
| 122 | text-align: center; |
| 123 | "> |
| 124 | ' . ($alert_icon_url ? '<img src="' . esc_url($alert_icon_url) . '" alt="" style="height: 100px; margin-bottom: 20px;">' : '') . ' |
| 125 | <h2 style=" |
| 126 | font-size: 32px; |
| 127 | font-weight: 450; |
| 128 | color: #131f4d; |
| 129 | margin-bottom: 15px; |
| 130 | margin-top: 0; |
| 131 | "> |
| 132 | ' . __('Meetup Events Feed', 'embedpress') . ' |
| 133 | </h2> |
| 134 | <p style=" |
| 135 | font-size: 14px; |
| 136 | font-weight: 400; |
| 137 | color: #7c8db5; |
| 138 | margin-top: 10px; |
| 139 | margin-bottom: 15px; |
| 140 | line-height: 1.5; |
| 141 | "> |
| 142 | ' . sprintf( |
| 143 | __('Display multiple Meetup events from RSS feeds is a premium feature. You need to upgrade to the <a href="%s" target="_blank">Premium</a> Version to use this feature.', 'embedpress'), |
| 144 | 'https://wpdeveloper.com/in/upgrade-embedpress' |
| 145 | ) . ' |
| 146 | </p> |
| 147 | <p style=" |
| 148 | font-size: 12px; |
| 149 | font-weight: 400; |
| 150 | color: #7c8db5; |
| 151 | margin: 0; |
| 152 | "> |
| 153 | ' . __('For single events, use the individual event URL instead of the RSS feed.', 'embedpress') . ' |
| 154 | </p> |
| 155 | </div> |
| 156 | |
| 157 | <style> |
| 158 | .embedpress-meetup-upgrade-notice p a { |
| 159 | text-decoration: underline; |
| 160 | font-weight: 700; |
| 161 | color: #131f4d; |
| 162 | } |
| 163 | .embedpress-meetup-upgrade-notice p a:hover { |
| 164 | color: #0f1a3a; |
| 165 | } |
| 166 | </style>'; |
| 167 | |
| 168 | return $response; |
| 169 | } |
| 170 | |
| 171 | /** inline {@inheritdoc} */ |
| 172 | public function normalizeUrl(Url $url) |
| 173 | { |
| 174 | $url->convertToHttps(); |
| 175 | $url->removeQueryString(); |
| 176 | |
| 177 | $url_string = (string) $url; |
| 178 | |
| 179 | // If it's a group URL without /events/rss, append it |
| 180 | if (preg_match('~meetup\.com/[^/]+/?$~i', $url_string)) { |
| 181 | // Don't append if it already has /events/rss |
| 182 | if (!preg_match('~meetup\.com/.+/events/rss~i', $url_string)) { |
| 183 | $url_string = rtrim($url_string, '/') . '/events/rss'; |
| 184 | $url = new Url($url_string); |
| 185 | } |
| 186 | } |
| 187 | |
| 188 | // If it's an events URL without /rss, append it |
| 189 | if (preg_match('~meetup\.com/[^/]+/events/?$~i', $url_string)) { |
| 190 | // Don't append if it already has /rss |
| 191 | if (!preg_match('~meetup\.com/.+/events/rss~i', $url_string)) { |
| 192 | $url_string = rtrim($url_string, '/') . '/rss'; |
| 193 | $url = new Url($url_string); |
| 194 | } |
| 195 | } |
| 196 | |
| 197 | return $url; |
| 198 | } |
| 199 | |
| 200 | public function getStaticResponse() |
| 201 | { |
| 202 | $meetup_website = 'https://meetup.com'; |
| 203 | $response = []; |
| 204 | $response['type'] = 'rich'; |
| 205 | $response['provider_name'] = 'Meetup'; |
| 206 | $response['provider_url'] = $meetup_website; |
| 207 | $response['url'] = $this->getUrl(); |
| 208 | |
| 209 | add_filter('safe_style_css', [$this, 'safe_style_css']); |
| 210 | $allowed_protocols = wp_allowed_protocols(); |
| 211 | $allowed_protocols[] = 'data'; |
| 212 | |
| 213 | // Get parameters using getParams() instead of direct config access |
| 214 | $params = $this->getParams(); |
| 215 | |
| 216 | // Check if this is an RSS feed URL |
| 217 | if ($this->isRssUrl($this->getUrl())) { |
| 218 | // Check if pro features are enabled for RSS feeds |
| 219 | if (!$this->isProFeaturesEnabled()) { |
| 220 | return $this->getProUpgradeMessage($response); |
| 221 | } |
| 222 | |
| 223 | // Delegate RSS feed handling to the pro plugin |
| 224 | if (class_exists('\Embedpress\Pro\Providers\Meetup')) { |
| 225 | $hash = 'mu_' . md5($this->getUrl() . serialize($params)); |
| 226 | $cache_duration = apply_filters('embedpress_meetup_rss_cache_duration', 3600 * 6); // 6 hour default |
| 227 | $filename = wp_get_upload_dir()['basedir'] . "/embedpress/{$hash}.txt"; |
| 228 | |
| 229 | return \Embedpress\Pro\Providers\Meetup::handleRssFeed($response, $filename, $this->getUrl(), $cache_duration, $params); |
| 230 | } else { |
| 231 | // Pro plugin not available, show upgrade message |
| 232 | return $this->getProUpgradeMessage($response); |
| 233 | } |
| 234 | } |
| 235 | |
| 236 | // Get cached data or fetch new data |
| 237 | $event_data = $this->getCachedEventData(); |
| 238 | |
| 239 | if (!$event_data) { |
| 240 | $t = wp_remote_get($this->getUrl(), ['timeout' => 10]); |
| 241 | if (!is_wp_error($t)) { |
| 242 | if ($meetup_page_content = wp_remote_retrieve_body($t)) { |
| 243 | $dom = str_get_html($meetup_page_content); |
| 244 | $event_data = $this->extractEventDataFromDom($dom); |
| 245 | if ($event_data) { |
| 246 | $this->cacheEventData($event_data); |
| 247 | } |
| 248 | } |
| 249 | } |
| 250 | } |
| 251 | |
| 252 | |
| 253 | if (!$event_data) { |
| 254 | $response['html'] = $this->getUrl(); |
| 255 | return $response; |
| 256 | } |
| 257 | |
| 258 | // Generate HTML from cached data |
| 259 | $response['html'] = $this->generateEventHtml($event_data, $allowed_protocols); |
| 260 | remove_filter('safe_style_css', [$this, 'safe_style_css']); |
| 261 | return $response; |
| 262 | } |
| 263 | |
| 264 | /** |
| 265 | * Get cached event data using transients |
| 266 | */ |
| 267 | private function getCachedEventData() |
| 268 | { |
| 269 | // Include params in cache key so different timezone/format settings create different cache entries |
| 270 | $params = $this->getParams(); |
| 271 | $cache_key_data = $this->getUrl() . serialize($params); |
| 272 | $url_hash = md5($cache_key_data); |
| 273 | $data_transient_key = 'meetup_event_data_' . $url_hash; |
| 274 | |
| 275 | return get_transient($data_transient_key); |
| 276 | } |
| 277 | |
| 278 | /** |
| 279 | * Cache event data using transients |
| 280 | */ |
| 281 | private function cacheEventData($event_data, $expiration = 3600) |
| 282 | { |
| 283 | // Include params in cache key so different timezone/format settings create different cache entries |
| 284 | $params = $this->getParams(); |
| 285 | $cache_key_data = $this->getUrl() . serialize($params); |
| 286 | $url_hash = md5($cache_key_data); |
| 287 | $data_transient_key = 'meetup_event_data_' . $url_hash; |
| 288 | |
| 289 | set_transient($data_transient_key, $event_data, $expiration); |
| 290 | } |
| 291 | |
| 292 | /** |
| 293 | * Extract event data from Next.js JSON or fallback to DOM parsing |
| 294 | */ |
| 295 | private function extractEventDataFromDom($dom) |
| 296 | { |
| 297 | if (empty($dom) || !is_object($dom)) { |
| 298 | return false; |
| 299 | } |
| 300 | |
| 301 | // First, try to extract from __NEXT_DATA__ JSON (new Meetup structure) |
| 302 | $next_data_script = $dom->find('script#__NEXT_DATA__', 0); |
| 303 | if ($next_data_script) { |
| 304 | $json_data = $next_data_script->innertext(); |
| 305 | $data = json_decode($json_data, true); |
| 306 | |
| 307 | if ($data && isset($data['props']['pageProps']['event'])) { |
| 308 | $event = $data['props']['pageProps']['event']; |
| 309 | return $this->extractFromNextData($event); |
| 310 | } |
| 311 | } |
| 312 | |
| 313 | // Fallback to old DOM parsing method |
| 314 | $header_dom = $dom->find('div[data-event-label="top"]', 0); |
| 315 | $body_dom = $dom->find('div[data-event-label="body"]', 0); |
| 316 | $event_location_info = $dom->find('div[data-event-label="info"] .sticky', 0); |
| 317 | |
| 318 | if (empty($header_dom) || empty($body_dom) || empty($event_location_info)) { |
| 319 | return false; |
| 320 | } |
| 321 | |
| 322 | return $this->extractFromDomElements($header_dom, $body_dom, $event_location_info); |
| 323 | } |
| 324 | |
| 325 | /** |
| 326 | * Format event date with timezone conversion and custom formatting |
| 327 | * |
| 328 | * @param string $date_string The date string to format |
| 329 | * @param array $params Parameters containing timezone and format settings |
| 330 | * @return string Formatted date string or HTML with data attributes for JS conversion |
| 331 | */ |
| 332 | private function formatEventDate($date_string, $params = []) |
| 333 | { |
| 334 | // Get timezone setting |
| 335 | $timezone_setting = isset($params['timezone']) ? $params['timezone'] : 'visitor_timezone'; |
| 336 | |
| 337 | // Get date and time format settings |
| 338 | $date_format = isset($params['date_format']) ? $params['date_format'] : 'wp_date_format'; |
| 339 | $time_format = isset($params['time_format']) ? $params['time_format'] : 'wp_time_format'; |
| 340 | |
| 341 | // Resolve WordPress format placeholders |
| 342 | if ($date_format === 'wp_date_format') { |
| 343 | $date_format = get_option('date_format', 'F j, Y'); |
| 344 | } |
| 345 | if ($time_format === 'wp_time_format') { |
| 346 | $time_format = get_option('time_format', 'g:i A'); |
| 347 | } |
| 348 | |
| 349 | // Parse the date string to timestamp |
| 350 | // Ensure we're parsing as UTC by using DateTime |
| 351 | try { |
| 352 | $date_obj = new \DateTime($date_string, new \DateTimeZone('UTC')); |
| 353 | $date_timestamp = $date_obj->getTimestamp(); |
| 354 | } catch (\Exception $e) { |
| 355 | // Fallback to strtotime if DateTime fails |
| 356 | $date_timestamp = strtotime($date_string); |
| 357 | } |
| 358 | |
| 359 | // If visitor timezone is selected, return HTML with data attributes for JS conversion |
| 360 | if ($timezone_setting === 'visitor_timezone') { |
| 361 | return sprintf( |
| 362 | '<span class="ep-event-date" data-visitor-timezone="true" data-utc-timestamp="%d" data-date-format="%s" data-time-format="%s">%s</span>', |
| 363 | $date_timestamp, |
| 364 | esc_attr($date_format), |
| 365 | esc_attr($time_format), |
| 366 | esc_html(gmdate('M j, Y, g:i A', $date_timestamp) . ' UTC') |
| 367 | ); |
| 368 | } |
| 369 | |
| 370 | // Get timezone object |
| 371 | if ($timezone_setting === 'wp_timezone') { |
| 372 | $timezone = wp_timezone(); |
| 373 | } else { |
| 374 | try { |
| 375 | $timezone = new \DateTimeZone($timezone_setting); |
| 376 | } catch (\Exception $e) { |
| 377 | // Fallback to WordPress timezone if invalid timezone provided |
| 378 | $timezone = wp_timezone(); |
| 379 | } |
| 380 | } |
| 381 | |
| 382 | // Create DateTime object in UTC (Meetup dates are in UTC) |
| 383 | $date = new \DateTime('@' . $date_timestamp); |
| 384 | |
| 385 | // Convert to target timezone |
| 386 | $date->setTimezone($timezone); |
| 387 | |
| 388 | // Format the date and time |
| 389 | $formatted_date = wp_date($date_format, $date->getTimestamp(), $timezone); |
| 390 | $formatted_time = wp_date($time_format, $date->getTimestamp(), $timezone); |
| 391 | $timezone_abbr = $date->format('T'); |
| 392 | |
| 393 | // Combine date, time, and timezone |
| 394 | $full_date = $formatted_date . ', ' . $formatted_time . ' ' . $timezone_abbr; |
| 395 | |
| 396 | return $full_date; |
| 397 | } |
| 398 | |
| 399 | /** |
| 400 | * Format event time only (for end time) |
| 401 | * |
| 402 | * @param string $date_string The date string to format |
| 403 | * @param array $params Parameters containing timezone and format settings |
| 404 | * @return string Formatted time string or HTML with data attributes for JS conversion |
| 405 | */ |
| 406 | private function formatEventTime($date_string, $params = []) |
| 407 | { |
| 408 | // Get timezone setting |
| 409 | $timezone_setting = isset($params['timezone']) ? $params['timezone'] : 'visitor_timezone'; |
| 410 | |
| 411 | // Get time format setting |
| 412 | $time_format = isset($params['time_format']) ? $params['time_format'] : 'wp_time_format'; |
| 413 | |
| 414 | // Resolve WordPress format placeholder |
| 415 | if ($time_format === 'wp_time_format') { |
| 416 | $time_format = get_option('time_format', 'g:i A'); |
| 417 | } |
| 418 | |
| 419 | // Parse the date string to timestamp |
| 420 | // Ensure we're parsing as UTC by using DateTime |
| 421 | try { |
| 422 | $date_obj = new \DateTime($date_string, new \DateTimeZone('UTC')); |
| 423 | $date_timestamp = $date_obj->getTimestamp(); |
| 424 | } catch (\Exception $e) { |
| 425 | // Fallback to strtotime if DateTime fails |
| 426 | $date_timestamp = strtotime($date_string); |
| 427 | } |
| 428 | |
| 429 | // If visitor timezone is selected, return HTML with data attributes for JS conversion |
| 430 | if ($timezone_setting === 'visitor_timezone') { |
| 431 | return sprintf( |
| 432 | '<span class="ep-event-end-time" data-visitor-timezone="true" data-utc-timestamp="%d" data-time-format="%s">%s</span>', |
| 433 | $date_timestamp, |
| 434 | esc_attr($time_format), |
| 435 | esc_html(gmdate('g:i A', $date_timestamp) . ' UTC') |
| 436 | ); |
| 437 | } |
| 438 | |
| 439 | // Get timezone object |
| 440 | if ($timezone_setting === 'wp_timezone') { |
| 441 | $timezone = wp_timezone(); |
| 442 | } else { |
| 443 | try { |
| 444 | $timezone = new \DateTimeZone($timezone_setting); |
| 445 | } catch (\Exception $e) { |
| 446 | // Fallback to WordPress timezone if invalid timezone provided |
| 447 | $timezone = wp_timezone(); |
| 448 | } |
| 449 | } |
| 450 | |
| 451 | // Create DateTime object in UTC (Meetup dates are in UTC) |
| 452 | $date = new \DateTime('@' . $date_timestamp); |
| 453 | |
| 454 | // Convert to target timezone |
| 455 | $date->setTimezone($timezone); |
| 456 | |
| 457 | // Format the time and timezone |
| 458 | $formatted_time = wp_date($time_format, $date->getTimestamp(), $timezone); |
| 459 | $timezone_abbr = $date->format('T'); |
| 460 | |
| 461 | return $formatted_time . ' ' . $timezone_abbr; |
| 462 | } |
| 463 | |
| 464 | /** |
| 465 | * Extract event data from Next.js __NEXT_DATA__ JSON |
| 466 | */ |
| 467 | private function extractFromNextData($event) |
| 468 | { |
| 469 | // Get parameters for timezone and format settings |
| 470 | $params = $this->getParams(); |
| 471 | |
| 472 | // Extract basic event info |
| 473 | $title = isset($event['title']) ? $event['title'] : ''; |
| 474 | $description = isset($event['description']) ? $event['description'] : ''; |
| 475 | $event_url = isset($event['eventUrl']) ? $event['eventUrl'] : $this->getUrl(); |
| 476 | |
| 477 | // Extract date/time with timezone and format settings |
| 478 | $date_time = ''; |
| 479 | if (isset($event['dateTime'])) { |
| 480 | $date_time = $this->formatEventDate($event['dateTime'], $params); |
| 481 | if (isset($event['endTime'])) { |
| 482 | $end_time = $this->formatEventTime($event['endTime'], $params); |
| 483 | $date_time .= ' to ' . $end_time; |
| 484 | } |
| 485 | } |
| 486 | |
| 487 | // Extract hosts |
| 488 | $hosts_html = ''; |
| 489 | if (isset($event['eventHosts']) && is_array($event['eventHosts'])) { |
| 490 | $host_names = array(); |
| 491 | foreach ($event['eventHosts'] as $host) { |
| 492 | if (isset($host['name'])) { |
| 493 | $host_names[] = esc_html($host['name']); |
| 494 | } |
| 495 | } |
| 496 | if (!empty($host_names)) { |
| 497 | $hosts_html = '<div class="ep-event-hosts">Hosted by ' . implode(', ', $host_names) . '</div>'; |
| 498 | } |
| 499 | } |
| 500 | |
| 501 | // Extract venue/location |
| 502 | $location_html = ''; |
| 503 | if (isset($event['eventType']) && $event['eventType'] === 'ONLINE') { |
| 504 | $location_html = '<div class="ep-event-location"><strong>📍 Online event</strong></div>'; |
| 505 | } elseif (isset($event['venue'])) { |
| 506 | $venue = $event['venue']; |
| 507 | $location_parts = array(); |
| 508 | if (!empty($venue['name'])) $location_parts[] = $venue['name']; |
| 509 | if (!empty($venue['address'])) $location_parts[] = $venue['address']; |
| 510 | if (!empty($venue['city'])) $location_parts[] = $venue['city']; |
| 511 | if (!empty($venue['state'])) $location_parts[] = $venue['state']; |
| 512 | |
| 513 | if (!empty($location_parts)) { |
| 514 | $location_html = '<div class="ep-event-location"><strong>📍 ' . esc_html(implode(', ', $location_parts)) . '</strong></div>'; |
| 515 | } |
| 516 | } |
| 517 | |
| 518 | // Extract featured image |
| 519 | $image_html = ''; |
| 520 | if (isset($event['featuredEventPhoto']['source'])) { |
| 521 | $image_url = esc_url($event['featuredEventPhoto']['source']); |
| 522 | $image_html = '<div class="ep-event-single-image"><img src="' . $image_url . '" alt="' . esc_attr($title) . '" /></div>'; |
| 523 | } |
| 524 | |
| 525 | // Format description (convert markdown-style links to HTML) |
| 526 | $description_html = $this->formatDescription($description); |
| 527 | |
| 528 | return array( |
| 529 | 'date' => $date_time, |
| 530 | 'title' => $title, |
| 531 | 'content' => $image_html . '<div class="ep-event-description">' . $description_html . '</div>', |
| 532 | 'host_info' => $hosts_html, |
| 533 | 'event_location_info' => $location_html, |
| 534 | 'url' => $event_url |
| 535 | ); |
| 536 | } |
| 537 | |
| 538 | /** |
| 539 | * Format description text (convert markdown-style links to HTML) |
| 540 | */ |
| 541 | private function formatDescription($description) |
| 542 | { |
| 543 | // Convert markdown links [text](url) to HTML |
| 544 | $description = preg_replace('/\[([^\]]+)\]\(([^\)]+)\)/', '<a href="$2" target="_blank" rel="noopener">$1</a>', $description); |
| 545 | |
| 546 | // Convert **bold** to <strong> |
| 547 | $description = preg_replace('/\*\*([^\*]+)\*\*/', '<strong>$1</strong>', $description); |
| 548 | |
| 549 | // Convert line breaks to <br> |
| 550 | $description = nl2br($description); |
| 551 | |
| 552 | return $description; |
| 553 | } |
| 554 | |
| 555 | /** |
| 556 | * Extract event data from DOM elements (legacy method) |
| 557 | */ |
| 558 | private function extractFromDomElements($header_dom, $body_dom, $event_location_info) |
| 559 | { |
| 560 | |
| 561 | // Process location info images |
| 562 | $dewqijm = $event_location_info->find('.dewqijm', 0)->find('span', 0); |
| 563 | if (!empty($dewqijm)) { |
| 564 | $img = $dewqijm->find('noscript', 0)->innertext(); |
| 565 | $dewqijm->removeChild($dewqijm->find('img', 1)); |
| 566 | $dewqijm->find('noscript', 0)->remove(); |
| 567 | $dewqijm->outertext = $dewqijm->makeup() . $dewqijm->innertext . $img . '</span>'; |
| 568 | } |
| 569 | |
| 570 | $date = $this->embedpress_get_markup_from_node($header_dom->find('time', 0)); |
| 571 | $title = $this->embedpress_get_markup_from_node($header_dom->find('h1', 0)); |
| 572 | $emrv9za = $body_dom->find('div.emrv9za', 0); |
| 573 | $picture = $emrv9za->find('picture[data-testid="event-description-image"]', 0); |
| 574 | if (!empty($picture) && $picture->find('img', 0)) { |
| 575 | if ($picture->find('noscript', 0)) { |
| 576 | $picture->find('img', 0)->remove(); |
| 577 | $img = $picture->find('noscript', 0)->innertext(); |
| 578 | $img = str_replace('/_next/image/', 'https://www.meetup.com/_next/image/', $img); |
| 579 | $picture->find('noscript', 0)->remove(); |
| 580 | $span = $picture->find('div', 0)->find('span', 0); |
| 581 | $span->outertext = $span->makeup() . $span->innertext . $img . '</span>'; |
| 582 | } else { |
| 583 | $img = $picture->find('img', 0); |
| 584 | $src = $img->src; |
| 585 | if ($src && strpos($src, '/_next/image/') === 0) { |
| 586 | $img->src = 'https://www.meetup.com' . $img->src; |
| 587 | } else if (strpos($src, '//') === false && $srcset = $img->srcset) { |
| 588 | $img->src = $this->getLargestImage($srcset); |
| 589 | if (strpos($img->src, '//') === false) { |
| 590 | $img->src = 'https://www.meetup.com' . $img->src; |
| 591 | } |
| 592 | } |
| 593 | } |
| 594 | } |
| 595 | |
| 596 | $content = $this->embedpress_get_markup_from_node($emrv9za); |
| 597 | |
| 598 | $host_info = $header_dom->find('a[data-event-label="hosted-by"]', 0); |
| 599 | ob_start(); |
| 600 | echo $host_info; |
| 601 | $host_info = ob_get_clean(); |
| 602 | |
| 603 | ob_start(); |
| 604 | echo $event_location_info; |
| 605 | $event_location_info = ob_get_clean(); |
| 606 | |
| 607 | // Return structured data instead of generating HTML |
| 608 | return [ |
| 609 | 'date' => $date, |
| 610 | 'title' => $title, |
| 611 | 'content' => $content, |
| 612 | 'host_info' => $host_info, |
| 613 | 'event_location_info' => $event_location_info, |
| 614 | 'url' => $this->getUrl() |
| 615 | ]; |
| 616 | } |
| 617 | |
| 618 | /** |
| 619 | * Generate HTML from cached event data |
| 620 | */ |
| 621 | private function generateEventHtml($event_data, $allowed_protocols) |
| 622 | { |
| 623 | ob_start(); |
| 624 | ?> |
| 625 | <article class="embedpress-event embedpress-event--modern"> |
| 626 | <div class="ep-event-card"> |
| 627 | <header class="ep-event-header"> |
| 628 | <div class="ep-event-meta"> |
| 629 | <span class="ep-event--date"> |
| 630 | <svg class="ep-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| 631 | <rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect> |
| 632 | <line x1="16" y1="2" x2="16" y2="6"></line> |
| 633 | <line x1="8" y1="2" x2="8" y2="6"></line> |
| 634 | <line x1="3" y1="10" x2="21" y2="10"></line> |
| 635 | </svg> |
| 636 | <?php echo wp_kses_post($event_data['date']); ?> |
| 637 | </span> |
| 638 | <?php if (!empty($event_data['event_location_info'])): ?> |
| 639 | <div class="ep-event--location"> |
| 640 | <?php echo wp_kses($event_data['event_location_info'], 'post', $allowed_protocols); ?> |
| 641 | </div> |
| 642 | <?php endif; ?> |
| 643 | </div> |
| 644 | |
| 645 | <a class="ep-event-link" href="<?php echo esc_url($event_data['url']); ?>" target="_blank" rel="noopener noreferrer"> |
| 646 | <h2 class="ep-event--title"><?php echo esc_html($event_data['title']); ?></h2> |
| 647 | </a> |
| 648 | |
| 649 | <?php if (!empty($event_data['host_info'])): ?> |
| 650 | <div class="ep-event--host"> |
| 651 | <?php echo wp_kses_post($event_data['host_info']); ?> |
| 652 | </div> |
| 653 | <?php endif; ?> |
| 654 | </header> |
| 655 | |
| 656 | <section class="ep-event-content"> |
| 657 | <div class="ep-event--description"> |
| 658 | <?php echo wp_kses_post($event_data['content']); ?> |
| 659 | </div> |
| 660 | </section> |
| 661 | |
| 662 | <footer class="ep-event-footer"> |
| 663 | <a href="<?php echo esc_url($event_data['url']); ?>" target="_blank" rel="noopener noreferrer" class="ep-event-cta"> |
| 664 | View Event Details |
| 665 | <svg class="ep-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| 666 | <line x1="5" y1="12" x2="19" y2="12"></line> |
| 667 | <polyline points="12 5 19 12 12 19"></polyline> |
| 668 | </svg> |
| 669 | </a> |
| 670 | </footer> |
| 671 | </div> |
| 672 | </article> |
| 673 | |
| 674 | <style> |
| 675 | /* Modern Meetup Event Card Styles */ |
| 676 | .embedpress-event--modern { |
| 677 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; |
| 678 | margin: 24px 0; |
| 679 | max-width: 100%; |
| 680 | } |
| 681 | |
| 682 | .embedpress-event--modern .ep-event-card { |
| 683 | background: #ffffff; |
| 684 | border: 1px solid #e5e7eb; |
| 685 | border-radius: 12px; |
| 686 | overflow: hidden; |
| 687 | box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1); |
| 688 | transition: all 0.3s ease; |
| 689 | } |
| 690 | |
| 691 | .embedpress-event--modern .ep-event-card:hover { |
| 692 | box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1); |
| 693 | transform: translateY(-2px); |
| 694 | } |
| 695 | |
| 696 | /* Header Section */ |
| 697 | .embedpress-event--modern .ep-event-header { |
| 698 | padding: 24px; |
| 699 | border-bottom: 1px solid #f3f4f6; |
| 700 | } |
| 701 | |
| 702 | .embedpress-event--modern .ep-event-meta { |
| 703 | display: flex; |
| 704 | flex-wrap: wrap; |
| 705 | gap: 16px; |
| 706 | margin-bottom: 16px; |
| 707 | } |
| 708 | |
| 709 | .embedpress-event--modern .ep-event--date { |
| 710 | display: inline-flex; |
| 711 | align-items: center; |
| 712 | gap: 6px; |
| 713 | color: #6b7280; |
| 714 | font-size: 14px; |
| 715 | font-weight: 500; |
| 716 | padding: 6px 12px; |
| 717 | background: #f9fafb; |
| 718 | border-radius: 6px; |
| 719 | } |
| 720 | |
| 721 | .embedpress-event--modern .ep-event--location { |
| 722 | display: inline-flex; |
| 723 | align-items: center; |
| 724 | gap: 6px; |
| 725 | color: #059669; |
| 726 | font-size: 14px; |
| 727 | font-weight: 500; |
| 728 | padding: 6px 12px; |
| 729 | background: #ecfdf5; |
| 730 | border-radius: 6px; |
| 731 | } |
| 732 | |
| 733 | .embedpress-event--modern .ep-icon { |
| 734 | flex-shrink: 0; |
| 735 | width: 16px; |
| 736 | height: 16px; |
| 737 | } |
| 738 | |
| 739 | .embedpress-event--modern .ep-event-link { |
| 740 | text-decoration: none; |
| 741 | color: inherit; |
| 742 | display: block; |
| 743 | transition: color 0.2s ease; |
| 744 | } |
| 745 | |
| 746 | .embedpress-event--modern .ep-event-link:hover { |
| 747 | color: #007cba; |
| 748 | } |
| 749 | |
| 750 | .embedpress-event--modern .ep-event--title { |
| 751 | font-size: 24px; |
| 752 | font-weight: 700; |
| 753 | line-height: 1.3; |
| 754 | color: #111827; |
| 755 | margin: 0 0 16px 0; |
| 756 | transition: color 0.2s ease; |
| 757 | } |
| 758 | |
| 759 | .embedpress-event--modern .ep-event-link:hover .ep-event--title { |
| 760 | color: #007cba; |
| 761 | } |
| 762 | |
| 763 | .embedpress-event--modern .ep-event--host { |
| 764 | display: flex; |
| 765 | align-items: center; |
| 766 | gap: 8px; |
| 767 | color: #6b7280; |
| 768 | font-size: 14px; |
| 769 | } |
| 770 | |
| 771 | .embedpress-event--modern .ep-event--host::before { |
| 772 | content: "👤"; |
| 773 | font-size: 16px; |
| 774 | } |
| 775 | |
| 776 | /* Content Section */ |
| 777 | .embedpress-event--modern .ep-event-content { |
| 778 | padding: 24px; |
| 779 | } |
| 780 | |
| 781 | .embedpress-event--modern .ep-event--description { |
| 782 | color: #374151; |
| 783 | font-size: 15px; |
| 784 | line-height: 1.6; |
| 785 | } |
| 786 | |
| 787 | .embedpress-event--modern .ep-event-image { |
| 788 | margin-bottom: 16px; |
| 789 | border-radius: 8px; |
| 790 | overflow: hidden; |
| 791 | } |
| 792 | |
| 793 | .embedpress-event--modern .ep-event-image img { |
| 794 | width: 100%; |
| 795 | height: auto; |
| 796 | display: block; |
| 797 | transition: transform 0.3s ease; |
| 798 | } |
| 799 | |
| 800 | .embedpress-event--modern .ep-event-card:hover .ep-event-image img { |
| 801 | transform: scale(1.02); |
| 802 | } |
| 803 | |
| 804 | .embedpress-event--modern .ep-event-description p { |
| 805 | margin: 0 0 12px 0; |
| 806 | } |
| 807 | |
| 808 | .embedpress-event--modern .ep-event-description p:last-child { |
| 809 | margin-bottom: 0; |
| 810 | } |
| 811 | |
| 812 | .embedpress-event--modern .ep-event-description a { |
| 813 | color: #007cba; |
| 814 | text-decoration: underline; |
| 815 | transition: color 0.2s ease; |
| 816 | } |
| 817 | |
| 818 | .embedpress-event--modern .ep-event-description a:hover { |
| 819 | color: #005a87; |
| 820 | } |
| 821 | |
| 822 | .embedpress-event--modern .ep-event-description strong { |
| 823 | color: #111827; |
| 824 | font-weight: 600; |
| 825 | } |
| 826 | |
| 827 | /* Footer Section */ |
| 828 | .embedpress-event--modern .ep-event-footer { |
| 829 | padding: 20px 24px; |
| 830 | background: #f9fafb; |
| 831 | border-top: 1px solid #e5e7eb; |
| 832 | } |
| 833 | |
| 834 | .embedpress-event--modern .ep-event-cta { |
| 835 | display: inline-flex; |
| 836 | align-items: center; |
| 837 | gap: 8px; |
| 838 | padding: 10px 20px; |
| 839 | background: #007cba; |
| 840 | color: #ffffff; |
| 841 | font-size: 14px; |
| 842 | font-weight: 600; |
| 843 | border-radius: 6px; |
| 844 | text-decoration: none; |
| 845 | transition: all 0.2s ease; |
| 846 | } |
| 847 | |
| 848 | .embedpress-event--modern .ep-event-cta:hover { |
| 849 | background: #005a87; |
| 850 | transform: translateX(2px); |
| 851 | color: #ffffff; |
| 852 | } |
| 853 | |
| 854 | .embedpress-event--modern .ep-event-cta .ep-icon { |
| 855 | transition: transform 0.2s ease; |
| 856 | } |
| 857 | |
| 858 | .embedpress-event--modern .ep-event-cta:hover .ep-icon { |
| 859 | transform: translateX(3px); |
| 860 | } |
| 861 | |
| 862 | /* Responsive Design */ |
| 863 | @media (max-width: 640px) { |
| 864 | .embedpress-event--modern .ep-event-header, |
| 865 | .embedpress-event--modern .ep-event-content, |
| 866 | .embedpress-event--modern .ep-event-footer { |
| 867 | padding: 16px; |
| 868 | } |
| 869 | |
| 870 | .embedpress-event--modern .ep-event--title { |
| 871 | font-size: 20px; |
| 872 | } |
| 873 | |
| 874 | .embedpress-event--modern .ep-event-meta { |
| 875 | flex-direction: column; |
| 876 | gap: 8px; |
| 877 | } |
| 878 | |
| 879 | .embedpress-event--modern .ep-event-cta { |
| 880 | width: 100%; |
| 881 | justify-content: center; |
| 882 | } |
| 883 | } |
| 884 | </style> |
| 885 | |
| 886 | <?php |
| 887 | return ob_get_clean(); |
| 888 | } |
| 889 | |
| 890 | public function safe_style_css($styles) |
| 891 | { |
| 892 | $styles[] = 'position'; |
| 893 | $styles[] = 'display'; |
| 894 | $styles[] = 'opacity'; |
| 895 | $styles[] = 'box-sizing'; |
| 896 | $styles[] = 'left'; |
| 897 | $styles[] = 'bottom'; |
| 898 | $styles[] = 'right'; |
| 899 | $styles[] = 'top'; |
| 900 | return $styles; |
| 901 | } |
| 902 | |
| 903 | /** |
| 904 | * It checks for data in the node before returning. |
| 905 | * |
| 906 | * @param \simple_html_dom_node $node |
| 907 | * @param string $method |
| 908 | * @param string $attr_name |
| 909 | * |
| 910 | * @return string it returns data from the node if found or empty strings otherwise. |
| 911 | */ |
| 912 | public function embedpress_get_markup_from_node($node, $method = 'innertext', $attr_name = '') |
| 913 | { |
| 914 | if (!empty($node) && is_object($node)) { |
| 915 | if (!empty($attr_name)) { |
| 916 | return $node->getAttribute($attr_name); |
| 917 | } |
| 918 | if (!empty($method) && method_exists($node, $method)) { |
| 919 | return $node->{$method}(); |
| 920 | } |
| 921 | return ''; |
| 922 | } |
| 923 | return ''; |
| 924 | } |
| 925 | |
| 926 | function getLargestImage($srcsetString) |
| 927 | { |
| 928 | $images = array(); |
| 929 | // split on comma |
| 930 | $srcsetArray = explode(",", $srcsetString); |
| 931 | foreach ($srcsetArray as $srcString) { |
| 932 | // split on whitespace - optional descriptor |
| 933 | $imgArray = explode(" ", trim($srcString)); |
| 934 | // cast w or x descriptor as an Integer |
| 935 | $images[(int)$imgArray[1]] = $imgArray[0]; |
| 936 | } |
| 937 | // find the max |
| 938 | $maxIndex = max(array_keys($images)); |
| 939 | return $images[$maxIndex]; |
| 940 | } |
| 941 | |
| 942 | |
| 943 | |
| 944 | |
| 945 | |
| 946 | |
| 947 | |
| 948 | } |
| 949 |