Changeset 2750673
- Timestamp:
- 07/01/2022 08:51:22 AM (3 years ago)
- Location:
- restful-syndication/trunk
- Files:
-
- 2 edited
-
index.php (modified) (16 diffs)
-
readme.txt (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
restful-syndication/trunk/index.php
r2215405 r2750673 4 4 Plugin URI: https://mediarealm.com.au/ 5 5 Description: Import content from the Wordpress REST API on another Wordpress site 6 Version: 1. 0.66 Version: 1.1.0 7 7 Author: Media Realm 8 8 Author URI: https://www.mediarealm.com.au/ … … 74 74 "options" => array() 75 75 ), 76 "remote_push_key" => array( 77 "title" => "Remote Content Push - Secure Key", 78 "type" => "readonly", 79 ), 80 "remote_push_domains" => array( 81 "title" => "Remote Content Push - Allowed Domains (one per line)", 82 "type" => "textarea", 83 ), 84 76 85 ); 77 86 … … 81 90 add_action('restful-syndication_cron', array($this, 'syndicate')); 82 91 add_filter('cron_schedules', array($this, 'cron_schedules')); 92 93 add_action('rest_api_init', function () { 94 register_rest_route('restful-syndication/v1', '/push/', array( 95 'methods' => 'POST', 96 'callback' => array($this, 'push_receive'), 97 )); 98 }); 83 99 } 84 100 … … 155 171 } 156 172 173 if($args['field_key'] == 'remote_push_key' && empty($value)) { 174 $value = $this->random_str(32); 175 } 176 157 177 if($field['type'] == "text") { 158 178 // Text fields 159 179 echo '<input type="text" name="restful-syndication_settings['.$args['field_key'].']" value="'.htmlspecialchars($value, ENT_QUOTES).'" />'; 180 } elseif($field['type'] == "textarea") { 181 // Textarea fields 182 echo '<textarea name="restful-syndication_settings['.$args['field_key'].']">'.htmlspecialchars($value, ENT_QUOTES).'</textarea>'; 160 183 } elseif($field['type'] == "password") { 161 184 // Password fields … … 171 194 // Checkbox fields 172 195 echo '<input type="checkbox" name="restful-syndication_settings['.$args['field_key'].']" value="true" '.("true" == $value ? "checked" : "").' />'; 196 } elseif($field['type'] == "readonly") { 197 // Readonly field 198 echo '<input type="text" name="restful-syndication_settings['.$args['field_key'].']" value="'.htmlspecialchars($value, ENT_QUOTES).'" readonly />'; 173 199 } 174 200 } … … 223 249 $options = get_option($this->settings_prefix . 'settings'); 224 250 225 if($full === false && (!isset($options['site_url']) || filter_var($options['site_url'], FILTER_VALIDATE_URL) === FALSE)) {251 if($full === false && (!isset($options['site_url']) || empty($options['site_url']) || filter_var($options['site_url'], FILTER_VALIDATE_URL) === FALSE)) { 226 252 $this->log("Master Site URL not specified, or URL format invalid."); 227 253 return; … … 263 289 } 264 290 291 $options = get_option($this->settings_prefix . 'settings'); 292 293 if($options['create_categories'] == "true") { 294 require_once(ABSPATH . '/wp-admin/includes/taxonomy.php'); 295 } 296 265 297 $count = 0; 266 298 267 299 foreach($posts as $post) { 268 300 // Loop over every post and create a post entry 269 270 // Have we already ingested this post? 271 if($this->post_guid_exists($post['guid']['rendered']) !== null) { 272 // Already exists on this site - skip over this post 273 continue; 274 } 275 276 $options = get_option($this->settings_prefix . 'settings'); 277 278 // Do not import posts earlier than a certain date 301 $this->syndicate_one($post); 302 $count++; 303 304 } 305 306 if($count > 0) { 307 $runs = get_option($this->settings_prefix . 'history', array()); 308 $runs[time()] = $count; 309 update_option($this->settings_prefix . 'history', $runs); 310 } 311 312 update_option($this->settings_prefix . 'last_attempt', time()); 313 } 314 315 private function syndicate_one($post, $allow_old = false, $force_publish = false, $match_author = false) { 316 // Process one post 317 318 $options = get_option($this->settings_prefix . 'settings'); 319 320 // Have we already ingested this post? 321 if($this->post_guid_exists($post['guid']['rendered']) !== null) { 322 // Already exists on this site - skip over this post 323 return; 324 } 325 326 // Do not import posts earlier than a certain date 327 if(!$allow_old) { 279 328 if(isset($options['earliest_post_date']) && strtotime($options['earliest_post_date']) !== false && strtotime($post['date']) <= strtotime($options['earliest_post_date'])) { 280 329 // This post is earlier than the specified start date 330 return; 331 } 332 } 333 334 // Download any embedded images found in the HTML 335 $dom = new domDocument; 336 $dom->loadHTML('<?xml encoding="utf-8" ?>' . $post['content']['rendered'], LIBXML_HTML_NOIMPLIED|LIBXML_HTML_NODEFDTD); 337 338 $images = $dom->getElementsByTagName('img'); 339 $images_to_attach = array(); 340 341 foreach($images as $imgKey => $img) { 342 // Download the image and attach it 343 $url = $img->getAttribute('src'); 344 $attachment_id = $this->ingest_image($url); 345 346 // Update the SRC in the HTML 347 $url_new = wp_get_attachment_image_src($attachment_id, $options['image_embed_size']); 348 $img->setAttribute('src', $url_new[0]); 349 $img->setAttribute('width', $url_new[1]); 350 $img->setAttribute('height', $url_new[2]); 351 $img->removeAttribute('srcset'); 352 $img->removeAttribute('sizes'); 353 354 // Later on, we'll link these attachments to this specific post 355 $images_to_attach[] = $attachment_id; 356 357 // Fix up the classes 358 $classes = explode(" ", $img->getAttribute('class')); 359 foreach($classes as $classKey => $class) { 360 if(substr($class, 0, 9) == "wp-image-") { 361 $classes[$classKey] = "wp-image-" . $attachment_id; 362 } elseif(substr($class, 0, 5) == "size-") { 363 $classes[$classKey] = "size-" . $options['image_embed_size']; 364 } 365 } 366 $img->setAttribute('class', implode(" ", $classes)); 367 } 368 369 // Turn <audio> tags into [audio] shortcodes 370 $audios = $dom->getElementsByTagName('audio'); 371 372 foreach($audios as $audioKey => $audio) { 373 // Get the original audio URL 374 $audio_source = $audio->getElementsByTagName('source'); 375 $url = $audio_source->item(0)->getAttribute('src'); 376 377 // There is a bug in Wordpress causing audio URLs with URL Parameters to fail to load the player 378 // See https://core.trac.wordpress.org/ticket/30377 379 // As a workaround, we strip URL parameters 380 if(strpos($url, "?") !== false) { 381 $url = substr($url, 0, strpos($url, "?")); 382 } 383 384 // Create a new paragraph, and insert the audio shortcode 385 $audio_shortcode = $dom->createElement('p'); 386 $audio_shortcode->nodeValue = '[audio src="'.$url.'"]'; 387 388 // Replace the original <audio> tag with this new <p>[audio]</p> arrangement 389 $audio->parentNode->replaceChild($audio_shortcode, $audio); 390 } 391 392 // Find YouTube embeds, and turn them into [embed] shortcodes 393 $youtubes = $dom->getElementsByTagName('div'); 394 395 foreach($youtubes as $youtubeKey => $youtube) { 396 397 // Skip non-youtube divs 398 if(!$youtube->hasAttribute('class') || strpos($youtube->getAttribute('class'), 'embed_youtube') === false) 281 399 continue; 282 } 283 284 // Download any embedded images found in the HTML 285 $dom = new domDocument; 286 $dom->loadHTML('<?xml encoding="utf-8" ?>' . $post['content']['rendered'], LIBXML_HTML_NOIMPLIED|LIBXML_HTML_NODEFDTD); 287 288 $images = $dom->getElementsByTagName('img'); 289 $images_to_attach = array(); 290 291 foreach($images as $imgKey => $img) { 292 // Download the image and attach it 293 $url = $img->getAttribute('src'); 294 $attachment_id = $this->ingest_image($url); 295 296 // Update the SRC in the HTML 297 $url_new = wp_get_attachment_image_src($attachment_id, $options['image_embed_size']); 298 $img->setAttribute('src', $url_new[0]); 299 $img->setAttribute('width', $url_new[1]); 300 $img->setAttribute('height', $url_new[2]); 301 $img->removeAttribute('srcset'); 302 $img->removeAttribute('sizes'); 303 304 // Later on, we'll link these attachments to this specific post 305 $images_to_attach[] = $attachment_id; 306 307 // Fix up the classes 308 $classes = explode(" ", $img->getAttribute('class')); 309 foreach($classes as $classKey => $class) { 310 if(substr($class, 0, 9) == "wp-image-") { 311 $classes[$classKey] = "wp-image-" . $attachment_id; 312 } elseif(substr($class, 0, 5) == "size-") { 313 $classes[$classKey] = "size-" . $options['image_embed_size']; 314 } 315 } 316 $img->setAttribute('class', implode(" ", $classes)); 317 } 318 319 // Turn <audio> tags into [audio] shortcodes 320 $audios = $dom->getElementsByTagName('audio'); 321 322 foreach($audios as $audioKey => $audio) { 323 // Get the original audio URL 324 $audio_source = $audio->getElementsByTagName('source'); 325 $url = $audio_source->item(0)->getAttribute('src'); 326 327 // There is a bug in Wordpress causing audio URLs with URL Parameters to fail to load the player 328 // See https://core.trac.wordpress.org/ticket/30377 329 // As a workaround, we strip URL parameters 330 if(strpos($url, "?") !== false) { 331 $url = substr($url, 0, strpos($url, "?")); 332 } 400 401 // Get the original YouTube embed URL 402 $video_source = $youtube->getElementsByTagName('iframe'); 403 $url = $video_source->item(0)->getAttribute('src'); 404 405 // Parse the Video ID from the URL 406 if(preg_match("/^((?:https?:)?\\/\\/)?((?:www|m)\\.)?((?:youtube\\.com|youtu.be))(\\/(?:[\\w\\-]+\\?v=|embed\\/|v\\/)?)([\\w\\-]+)(\\S+)?$/", $url, $matches_youtube) === 1) { 407 // Create the new URL 408 $url_new = "https://youtube.com/watch?v=" . $matches_youtube[5]; 333 409 334 410 // Create a new paragraph, and insert the audio shortcode 335 $audio_shortcode = $dom->createElement('p'); 336 $audio_shortcode->nodeValue = '[audio src="'.$url.'"]'; 337 338 // Replace the original <audio> tag with this new <p>[audio]</p> arrangement 339 $audio->parentNode->replaceChild($audio_shortcode, $audio); 340 } 341 342 // Find YouTube embeds, and turn them into [embed] shortcodes 343 $youtubes = $dom->getElementsByTagName('div'); 344 345 foreach($youtubes as $youtubeKey => $youtube) { 346 347 // Skip non-youtube divs 348 if(!$youtube->hasAttribute('class') || strpos($youtube->getAttribute('class'), 'embed_youtube') === false) 349 continue; 350 351 // Get the original YouTube embed URL 352 $video_source = $youtube->getElementsByTagName('iframe'); 353 $url = $video_source->item(0)->getAttribute('src'); 354 355 // Parse the Video ID from the URL 356 if(preg_match("/^((?:https?:)?\\/\\/)?((?:www|m)\\.)?((?:youtube\\.com|youtu.be))(\\/(?:[\\w\\-]+\\?v=|embed\\/|v\\/)?)([\\w\\-]+)(\\S+)?$/", $url, $matches_youtube) === 1) { 357 // Create the new URL 358 $url_new = "https://youtube.com/watch?v=" . $matches_youtube[5]; 359 360 // Create a new paragraph, and insert the audio shortcode 361 $embed_shortcode = $dom->createElement('p'); 362 $embed_shortcode->nodeValue = '[embed]'.$url_new.'[/embed]'; 363 364 // Replace the original <div class="embed_youtube"> tag with this new <p>[embed]url[/embed]</p> arrangement 365 $youtube->parentNode->replaceChild($embed_shortcode, $youtube); 366 } 367 } 368 369 $html = $dom->saveHTML(); 370 $html = str_replace('<?xml encoding="utf-8" ?>', '', $html); 371 372 // Find local matching categories, or create missing ones 373 $categories = array(); 374 375 foreach($post['categories'] as $category) { 376 $category_data = $this->rest_fetch('/wp-json/wp/v2/categories/'.$category); 411 $embed_shortcode = $dom->createElement('p'); 412 $embed_shortcode->nodeValue = '[embed]'.$url_new.'[/embed]'; 413 414 // Replace the original <div class="embed_youtube"> tag with this new <p>[embed]url[/embed]</p> arrangement 415 $youtube->parentNode->replaceChild($embed_shortcode, $youtube); 416 } 417 } 418 419 $html = $dom->saveHTML(); 420 $html = str_replace('<?xml encoding="utf-8" ?>', '', $html); 421 422 // Find local matching categories, or create missing ones 423 $categories = array(); 424 425 foreach($post['categories'] as $category) { 426 $category_data = $this->rest_fetch('/wp-json/wp/v2/categories/'.$category); 427 428 if(isset($category_data['name']) && !empty($category_data['name'])) { 377 429 $term = get_term_by('name', $category_data['name'], 'category'); 378 430 … … 380 432 // Category already exists 381 433 $categories[] = $term->term_id; 382 } elseif( $options['create_categories'] == "true") {434 } elseif(isset($options['create_categories']) && $options['create_categories'] == "true") { 383 435 // Create the category 384 436 $categories[] = wp_insert_category(array('cat_name' => $category_data['name'])); … … 386 438 } 387 439 388 // Find local matching tags 389 $tags = array(); 390 391 foreach($post['tags'] as $tag) { 392 $tag_data = $this->rest_fetch('/wp-json/wp/v2/tags/'.$tag); 440 } 441 442 // Find local matching tags 443 $tags = array(); 444 445 foreach($post['tags'] as $tag) { 446 $tag_data = $this->rest_fetch('/wp-json/wp/v2/tags/'.$tag); 447 448 if(isset($tag_data['name']) && !empty($tag_data['name'])) { 393 449 $term = get_term_by('name', $tag_data['name'], 'post_tag'); 394 450 … … 398 454 } elseif($options['create_tags'] == "true") { 399 455 // Create the category 400 $tag = wp_insert_term($tag_data['name'], 'post_tag' ); 401 $tags[] = $tag['term_id']; 402 456 $tag = wp_insert_term($tag_data['name'], 'post_tag'); 457 458 if(is_array($tag)) { 459 $tags[] = $tag['term_id']; 460 } 403 461 } 404 462 } 405 463 406 if(!isset($post['yoast_meta']['yoast_wpseo_metadesc'])) { 407 $post['yoast_meta']['yoast_wpseo_metadesc'] = ""; 408 } 409 410 if(!isset($post['yoast_meta']['yoast_wpseo_canonical'])) { 411 $post['yoast_meta']['yoast_wpseo_canonical'] = $post['link']; 412 } 413 414 if($options['yoast_noindex'] == "true") { 415 $post['yoast_meta']['yoast_wpseo_noindex'] = "1"; 464 } 465 466 // Try and match to a local author 467 $author = $options['default_author']; 468 if($match_author === true && is_numeric($post['author'])) { 469 $author_data = $this->rest_fetch('/wp-json/wp/v2/users/'.$post['author']); 470 471 if(isset($author_data['name']) && !empty($author_data['name'])) { 472 $user_lookup = $this->find_user($author_data['name']); 473 474 if($user_lookup != false) { 475 $author = $user_lookup; 476 } 477 } 478 } 479 480 if(!isset($post['yoast_meta']['yoast_wpseo_metadesc'])) { 481 $post['yoast_meta']['yoast_wpseo_metadesc'] = ""; 482 } 483 484 if(!isset($post['yoast_meta']['yoast_wpseo_canonical'])) { 485 $post['yoast_meta']['yoast_wpseo_canonical'] = $post['link']; 486 } 487 488 if($options['yoast_noindex'] == "true") { 489 $post['yoast_meta']['yoast_wpseo_noindex'] = "1"; 490 } else { 491 $post['yoast_meta']['yoast_wpseo_noindex'] = "0"; 492 } 493 494 if($force_publish == true) { 495 $post_status = 'publish'; 496 } else { 497 $post_status = $options['default_status']; 498 } 499 500 // Insert a new post 501 $post_id = wp_insert_post(array( 502 'ID' => 0, 503 'post_author' => $author, 504 'post_date' => $post['date'], 505 'post_date_gmt' => $post['date_gmt'], 506 'post_content' => $html, 507 'post_title' => $post['title']['rendered'], 508 'post_excerpt' => strip_tags($post['excerpt']['rendered']), 509 'post_status' => $post_status, 510 'post_type' => 'post', 511 'guid' => $post['guid']['rendered'], 512 'post_category' => $categories, 513 'tags_input' => $tags, 514 'meta_input' => array( 515 '_'.$this->settings_prefix.'source_guid' => $post['guid']['rendered'], 516 '_yoast_wpseo_metadesc' => $post['yoast_meta']['yoast_wpseo_metadesc'], 517 '_yoast_wpseo_canonical' => $post['yoast_meta']['yoast_wpseo_canonical'], 518 '_yoast_wpseo_meta-robots-noindex' => $post['yoast_meta']['yoast_wpseo_noindex'], 519 ) 520 ), false); 521 522 if($post_id > 0 && isset($post['_links']['wp:featuredmedia'][0]['href'])) { 523 // Download and attach featured image 524 525 $api_url = $post['_links']['wp:featuredmedia'][0]['href']; 526 $attachment = $this->rest_fetch($api_url, true); 527 528 if(isset($attachment['media_details']['sizes']['full']['source_url'])) { 529 $featured_attachment_id = $this->ingest_image($attachment['media_details']['sizes']['full']['source_url'], $post_id); 530 set_post_thumbnail($post_id, $featured_attachment_id); 416 531 } else { 417 $post['yoast_meta']['yoast_wpseo_noindex'] = "0"; 418 } 419 420 // Insert a new post 421 $post_id = wp_insert_post(array( 422 'ID' => 0, 423 'post_author' => $options['default_author'], 424 'post_date' => $post['date'], 425 'post_date_gmt' => $post['date_gmt'], 426 'post_content' => $html, 427 'post_title' => $post['title']['rendered'], 428 'post_excerpt' => strip_tags($post['excerpt']['rendered']), 429 'post_status' => $options['default_status'], 430 'post_type' => 'post', 431 'guid' => $post['guid']['rendered'], 432 'post_category' => $categories, 433 'tags_input' => $tags, 434 'meta_input' => array( 435 '_'.$this->settings_prefix.'source_guid' => $post['guid']['rendered'], 436 '_yoast_wpseo_metadesc' => $post['yoast_meta']['yoast_wpseo_metadesc'], 437 '_yoast_wpseo_canonical' => $post['yoast_meta']['yoast_wpseo_canonical'], 438 '_yoast_wpseo_meta-robots-noindex' => $post['yoast_meta']['yoast_wpseo_noindex'], 439 ) 440 ), false); 441 442 if($post_id > 0 && isset($post['_links']['wp:featuredmedia'][0]['href'])) { 443 // Download and attach featured image 444 445 $api_url = $post['_links']['wp:featuredmedia'][0]['href']; 446 $attachment = $this->rest_fetch($api_url, true); 447 448 if(isset($attachment['media_details']['sizes']['full']['source_url'])) { 449 $featured_attachment_id = $this->ingest_image($attachment['media_details']['sizes']['full']['source_url'], $post_id); 450 set_post_thumbnail($post_id, $featured_attachment_id); 451 } else { 452 $this->log("Attachment full image not found: " . $api_url); 453 } 454 } 455 456 if($post_id > 0) { 457 foreach($images_to_attach as $attachment_id) { 458 // Attach images to this new post 459 wp_update_post( 460 array( 461 'ID' => $attachment_id, 462 'post_parent' => $post_id 463 ) 464 ); 465 } 466 } 467 468 $count++; 469 470 } 471 472 if($count > 0) { 473 $runs = get_option($this->settings_prefix . 'history', array()); 474 $runs[time()] = $count; 475 update_option($this->settings_prefix . 'history', $runs); 476 } 477 478 update_option($this->settings_prefix . 'last_attempt', time()); 532 $this->log("Attachment full image not found: " . $api_url); 533 } 534 } 535 536 if($post_id > 0) { 537 foreach($images_to_attach as $attachment_id) { 538 // Attach images to this new post 539 wp_update_post( 540 array( 541 'ID' => $attachment_id, 542 'post_parent' => $post_id 543 ) 544 ); 545 } 546 } 547 548 return $post_id; 549 } 550 551 public function push_receive() { 552 // Receive a push request from another server 553 global $wp; 554 555 // Check credentials 556 if(!isset($_POST['restful_push_key'])) { 557 return array('error' => 'Authentication failure'); 558 } 559 560 $options = get_option($this->settings_prefix . 'settings'); 561 562 if($_POST['restful_push_key'] !== $options['remote_push_key'] || strlen($options['remote_push_key']) <= 31) { 563 return array('error' => 'Authentication failure'); 564 } 565 566 // Check list of allowed domains 567 if(!isset($_POST['restful_push_url'])) { 568 return array('error' => 'URL not provided'); 569 } 570 571 if(empty($options['remote_push_domains'])) { 572 return array('error' => 'Allowed domains not configured'); 573 } 574 575 $domains = explode("\n", $options['remote_push_domains']); 576 $supplied_url_info = parse_url($_POST['restful_push_url']); 577 $supplied_domain = $supplied_url_info['host']; 578 579 if(!in_array($supplied_domain, $domains)) { 580 return array('error' => 'Could not validate domain'); 581 } 582 583 // Request data 584 $payload = $this->rest_fetch($_POST['restful_push_url'], true); 585 586 if(empty($payload) || $payload == null) { 587 return array("error_msg" => 'Failed to fetch post data from API'); 588 } 589 590 // Should this be auto-published? 591 if(isset($_POST['restful_publish']) && $_POST['restful_publish'] == 'true') { 592 $force_publish = true; 593 } else { 594 $force_publish = false; 595 } 596 597 // Process data 598 $post_id = $this->syndicate_one($payload, true, $force_publish, true); 599 600 return array("post_id" => $post_id); 479 601 } 480 602 … … 493 615 AND meta_value = %s 494 616 AND $wpdb->postmeta.post_id = $wpdb->posts.ID 495 AND $wpdb->posts.post_status != 'trash'496 617 ", 497 618 '_'.$this->settings_prefix.'source_guid', … … 510 631 $filename = basename($url); 511 632 633 if(empty($image_data)) { 634 $this->log("Failed to download image " . $url); 635 return null; 636 } 637 512 638 $upload_dir = wp_upload_dir(); 513 639 … … 527 653 // File doesn't already exist - save it 528 654 file_put_contents($file, $image_data); 655 656 if(!file_exists($file)) { 657 $this->log("Failed to save image " . $file); 658 return null; 659 } 529 660 530 661 $wp_filetype = wp_check_filetype($filename, null); … … 541 672 $attach_data = wp_generate_attachment_metadata($attach_id, $file); 542 673 wp_update_attachment_metadata($attach_id, $attach_data); 674 675 if(!file_exists($file)) { 676 $this->log("Image doesn't exist after processing " . $file); 677 return null; 678 } 679 543 680 } 544 681 … … 546 683 547 684 return $attach_id; 685 } 686 687 private function find_user($name) { 688 // Lookup a user by their display name 689 global $wpdb; 690 691 if(!$user = $wpdb->get_row($wpdb->prepare("SELECT `ID` FROM $wpdb->users WHERE `display_name` = %s", $name))) 692 return false; 693 694 return $user->ID; 548 695 } 549 696 … … 557 704 } 558 705 706 private function random_str($length, $keyspace = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*()-=+`~') { 707 // From https://stackoverflow.com/a/31284266/2888733 708 $str = ''; 709 $max = mb_strlen($keyspace, '8bit') - 1; 710 for ($i = 0; $i < $length; ++$i) { 711 $str .= $keyspace[random_int(0, $max)]; 712 } 713 return $str; 714 } 715 559 716 } 560 717 -
restful-syndication/trunk/readme.txt
r2215406 r2750673 3 3 Tags: syndication, rest, wp-rest 4 4 Requires at least: 5.0 5 Tested up to: 5.3.26 Requires PHP: 5.6.05 Tested up to: 6.0.0 6 Requires PHP: 7.4.0 7 7 License: GPLv2 or later 8 8 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 59 59 == Changelog == 60 60 61 = 1.1.0 = 62 63 * PHP 8 compatibility 64 * Bugfixes when adding tags and categories 65 * Add logging for failed image downloads 66 * Push data receive: Check if no payload is received from remote server 67 61 68 = 1.0.6 = 62 69 * Handle YouTube embeds, and convert them into the [embed] shortcode
Note: See TracChangeset
for help on using the changeset viewer.