Plugin Directory

Changeset 3490608


Ignore:
Timestamp:
03/25/2026 07:25:29 AM (8 days ago)
Author:
eugenezolo
Message:

Release version 1.0.8

Location:
outrank
Files:
19 added
2 deleted
3 edited

Legend:

Unmodified
Added
Removed
  • outrank/trunk/libs/api.php

    r3476368 r3490608  
    1010        'methods' => 'POST',
    1111        'callback' => 'outrank_receive_article',
     12        'permission_callback' => function ($request) {
     13            $secretKey = $request->get_header('X-Secret-Key');
     14            if (!$secretKey) {
     15                $secretKey = $request->get_header('x-secret-key');
     16            }
     17            return $secretKey && hash_equals(OUTRANK_API_SECRET, $secretKey);
     18        }
     19    ]);
     20
     21    register_rest_route('outrank/v1', '/edit', [
     22        'methods' => 'PUT',
     23        'callback' => 'outrank_edit_article',
    1224        'permission_callback' => function ($request) {
    1325            $secretKey = $request->get_header('X-Secret-Key');
     
    5062        'permission_callback' => '__return_true'
    5163    ]);
     64
     65    register_rest_route('outrank/v1', '/capabilities', [
     66        'methods' => 'GET',
     67        'callback' => 'outrank_get_capabilities',
     68        'permission_callback' => '__return_true'
     69    ]);
    5270});
    5371
     
    7088
    7189    return new WP_REST_Response(['success' => true], 200);
     90}
     91
     92function outrank_get_capabilities() {
     93    $capabilities = ['publish', 'edit', 'list'];
     94
     95    $plugin_file = OUTRANK_PLUGIN_PATH . 'outrank.php';
     96    $plugin_data = get_file_data($plugin_file, ['Version' => 'Version']);
     97    $version = !empty($plugin_data['Version']) ? $plugin_data['Version'] : '0.0.0';
     98
     99    return new WP_REST_Response([
     100        'version' => $version,
     101        'capabilities' => $capabilities,
     102    ], 200);
    72103}
    73104
     
    182213}
    183214
     215function outrank_edit_article($request) {
     216    global $wpdb;
     217
     218    outrank_ensure_table_exists();
     219
     220    $params = $request->get_json_params();
     221    if (!is_array($params)) {
     222        $params = [];
     223    }
     224
     225    $secret = sanitize_text_field($params['secret'] ?? '');
     226    $storedSecret = get_option('outrank_api_key');
     227
     228    if (!$secret || !$storedSecret || !hash_equals($storedSecret, $secret)) {
     229        return new WP_REST_Response(['error' => 'Invalid or missing secret'], 403);
     230    }
     231
     232    $post_id = isset($params['id']) ? absint($params['id']) : 0;
     233    $current_slug = isset($params['current_slug']) ? sanitize_title($params['current_slug']) : '';
     234
     235    if (!$post_id && $current_slug === '') {
     236        return new WP_REST_Response(['error' => 'Either id or current_slug is required'], 400);
     237    }
     238
     239    $post = $post_id ? get_post($post_id) : get_page_by_path($current_slug, OBJECT, 'post');
     240    if (!$post || $post->post_type !== 'post') {
     241        return new WP_REST_Response(['error' => 'Post not found'], 404);
     242    }
     243
     244    $original_slug = $post->post_name;
     245    $original_thumbnail_id = (int) get_post_thumbnail_id($post->ID);
     246    $table_name = $wpdb->prefix . 'outrank_manage';
     247
     248    $has_title = array_key_exists('title', $params);
     249    $has_content = array_key_exists('content', $params);
     250    $has_slug = array_key_exists('slug', $params);
     251    $has_status = array_key_exists('status', $params);
     252    $has_author = array_key_exists('author', $params);
     253    $has_category = array_key_exists('category', $params);
     254    $has_tags = array_key_exists('tags', $params);
     255    $has_image_url = array_key_exists('image_url', $params) && !empty($params['image_url']);
     256    $has_meta_description = array_key_exists('meta_description', $params) && $params['meta_description'] !== '';
     257    $has_focus_keyword = array_key_exists('focus_keyword', $params) && $params['focus_keyword'] !== '';
     258    $has_focus_keyphrase = array_key_exists('focus_keyphrase', $params) && $params['focus_keyphrase'] !== '';
     259
     260    if (
     261        !$has_title &&
     262        !$has_content &&
     263        !$has_slug &&
     264        !$has_status &&
     265        !$has_author &&
     266        !$has_category &&
     267        !$has_tags &&
     268        !$has_image_url &&
     269        !$has_meta_description &&
     270        !$has_focus_keyword &&
     271        !$has_focus_keyphrase
     272    ) {
     273        return new WP_REST_Response(['error' => 'No update fields provided'], 400);
     274    }
     275
     276    $post_update = [
     277        'ID' => $post->ID,
     278    ];
     279
     280    if ($has_title) {
     281        $title = sanitize_text_field($params['title']);
     282        if ($title === '') {
     283            return new WP_REST_Response(['error' => 'Invalid title'], 400);
     284        }
     285        $post_update['post_title'] = $title;
     286    }
     287
     288    if ($has_content) {
     289        $post_update['post_content'] = outrank_sanitize_content((string) $params['content']);
     290    }
     291
     292    if ($has_status) {
     293        $status = sanitize_key($params['status']);
     294        $allowed_statuses = ['publish', 'draft', 'pending', 'private', 'future', 'trash'];
     295        if (!in_array($status, $allowed_statuses, true)) {
     296            return new WP_REST_Response(['error' => 'Invalid status'], 400);
     297        }
     298        $post_update['post_status'] = $status;
     299    }
     300
     301    if ($has_author) {
     302        $author = $params['author'];
     303        $author_id = 0;
     304
     305        if (is_numeric($author)) {
     306            $author_id = absint($author);
     307            if ($author_id && !get_userdata($author_id)) {
     308                return new WP_REST_Response(['error' => 'Invalid author'], 400);
     309            }
     310        } else {
     311            $user = get_user_by('login', sanitize_user((string) $author, true));
     312            if (!$user) {
     313                return new WP_REST_Response(['error' => 'Invalid author'], 400);
     314            }
     315            $author_id = (int) $user->ID;
     316        }
     317
     318        if (!$author_id) {
     319            return new WP_REST_Response(['error' => 'Invalid author'], 400);
     320        }
     321
     322        $post_update['post_author'] = $author_id;
     323    }
     324
     325    if ($has_category) {
     326        $post_update['post_category'] = outrank_resolve_category_ids($params['category']);
     327    }
     328
     329    if ($has_tags) {
     330        $tags = is_array($params['tags']) ? $params['tags'] : [$params['tags']];
     331        $post_update['tags_input'] = array_map('sanitize_text_field', $tags);
     332    }
     333
     334    if ($has_slug) {
     335        $new_slug = sanitize_title($params['slug']);
     336        if ($new_slug === '') {
     337            return new WP_REST_Response(['error' => 'Invalid slug'], 400);
     338        }
     339
     340        if ($new_slug !== $original_slug && outrank_slug_exists_for_other_post($new_slug, $post->ID, $original_slug)) {
     341            return new WP_REST_Response(['error' => 'Slug already exists'], 409);
     342        }
     343
     344        $post_update['post_name'] = $new_slug;
     345    }
     346
     347    $new_thumbnail_id = 0;
     348    $existing_attachment_id = 0;
     349    if ($has_image_url) {
     350        $image_url = esc_url_raw((string) $params['image_url']);
     351        if (!$image_url) {
     352            return new WP_REST_Response(['error' => 'Invalid image_url'], 400);
     353        }
     354
     355        $existing_attachment_id = outrank_find_attachment_by_source_url($image_url);
     356        if ($existing_attachment_id) {
     357            $new_thumbnail_id = $existing_attachment_id;
     358        } else {
     359            $new_thumbnail_id = (int) outrank_upload_image_from_url($image_url, $post->ID);
     360            if (!$new_thumbnail_id) {
     361                return new WP_REST_Response(['error' => 'Failed to upload image'], 500);
     362            }
     363        }
     364    }
     365
     366    $warnings = [];
     367
     368    kses_remove_filters();
     369    try {
     370        $updated_post_id = wp_update_post($post_update, true);
     371        if (is_wp_error($updated_post_id)) {
     372            throw new RuntimeException($updated_post_id->get_error_message());
     373        }
     374    } catch (RuntimeException $e) {
     375        kses_init_filters();
     376        if ($new_thumbnail_id && !$existing_attachment_id) {
     377            wp_delete_attachment($new_thumbnail_id, true);
     378        }
     379        return new WP_REST_Response(['error' => 'Failed to update post: ' . $e->getMessage()], 500);
     380    }
     381
     382    if ($has_content) {
     383        $saved_content = isset($post_update['post_content']) ? $post_update['post_content'] : get_post_field('post_content', $post->ID);
     384        $localized_content = outrank_download_content_images($saved_content, $post->ID);
     385
     386        if ($localized_content !== $saved_content) {
     387            $content_update_result = wp_update_post([
     388                'ID' => $post->ID,
     389                'post_content' => $localized_content,
     390            ], true);
     391
     392            if (is_wp_error($content_update_result)) {
     393                $warnings[] = 'Content images could not be localized.';
     394            }
     395        }
     396    }
     397
     398    kses_init_filters();
     399
     400    if ($new_thumbnail_id) {
     401        if (!set_post_thumbnail($post->ID, $new_thumbnail_id)) {
     402            $warnings[] = 'Featured image could not be assigned.';
     403        }
     404
     405        $final_post = get_post($post->ID);
     406        if ($final_post && !$existing_attachment_id) {
     407            $attachment_update_result = wp_update_post([
     408                'ID' => $new_thumbnail_id,
     409                'post_author' => (int) $final_post->post_author,
     410                'post_parent' => $post->ID,
     411            ], true);
     412
     413            if (is_wp_error($attachment_update_result)) {
     414                $warnings[] = 'Featured image metadata could not be updated.';
     415            }
     416        }
     417
     418        if (
     419            $original_thumbnail_id &&
     420            $original_thumbnail_id !== $new_thumbnail_id &&
     421            get_post_meta($original_thumbnail_id, '_outrank_source_url', true) &&
     422            !outrank_attachment_is_featured_elsewhere($original_thumbnail_id, $post->ID)
     423        ) {
     424            wp_delete_attachment($original_thumbnail_id, true);
     425        }
     426    }
     427
     428    $final_post = get_post($post->ID);
     429    if (!$final_post) {
     430        return new WP_REST_Response(['error' => 'Post not found after update'], 500);
     431    }
     432
     433    $final_title = $final_post->post_title;
     434    $final_status = $final_post->post_status;
     435    $final_slug = $final_post->post_name;
     436    $tracking_slug = outrank_normalize_tracking_slug($final_slug);
     437    $final_thumbnail_id = (int) get_post_thumbnail_id($post->ID);
     438
     439    if ($has_title || $has_meta_description || $has_focus_keyword || $has_focus_keyphrase) {
     440        $meta_description = $has_meta_description ? sanitize_text_field($params['meta_description']) : null;
     441        $focus_keyword = null;
     442
     443        if ($has_focus_keyword) {
     444            $focus_keyword = sanitize_text_field($params['focus_keyword']);
     445        } elseif ($has_focus_keyphrase) {
     446            $focus_keyword = sanitize_text_field($params['focus_keyphrase']);
     447        }
     448
     449        outrank_set_seo_meta($post->ID, $has_title ? $final_title : null, $meta_description, $focus_keyword);
     450        outrank_set_squirrly_seo($post->ID, $has_title ? $final_title : null, $meta_description, $focus_keyword);
     451    }
     452
     453    $original_tracking_slug = outrank_normalize_tracking_slug($original_slug);
     454
     455    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
     456    $tracking_row = $wpdb->get_row($wpdb->prepare(
     457        "SELECT id FROM {$table_name} WHERE slug = %s OR slug = %s LIMIT 1",
     458        $original_slug,
     459        $original_tracking_slug
     460    ));
     461
     462    if ($tracking_row) {
     463        $tracking_update = [
     464            'slug' => $tracking_slug,
     465            'title' => $final_title,
     466            'status' => $final_status,
     467            'image' => $final_thumbnail_id ? (string) $final_thumbnail_id : '',
     468        ];
     469
     470        if ($has_meta_description) {
     471            $tracking_update['meta_description'] = sanitize_text_field($params['meta_description']);
     472        }
     473
     474        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
     475        $wpdb->update(
     476            $table_name,
     477            $tracking_update,
     478            ['id' => (int) $tracking_row->id]
     479        );
     480    }
     481
     482    outrank_clear_articles_cache();
     483
     484    $response = [
     485        'success' => true,
     486        'post_id' => $post->ID,
     487    ];
     488
     489    if (!empty($warnings)) {
     490        $response['warnings'] = array_values(array_unique($warnings));
     491    }
     492
     493    return new WP_REST_Response($response, 200);
     494}
     495
    184496function outrank_test_integration($request) {
    185497    // 1. Get integration key from request
     
    238550        'message' => 'Integration test successful'
    239551    ], 200);
     552}
     553
     554function outrank_find_attachment_by_source_url($image_url) {
     555    $existing = get_posts([
     556        'post_type'   => 'attachment',
     557        'meta_key'    => '_outrank_source_url', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
     558        'meta_value'  => $image_url, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
     559        'numberposts' => 1,
     560        'fields'      => 'ids',
     561    ]);
     562
     563    if (empty($existing)) {
     564        return 0;
     565    }
     566
     567    return (int) $existing[0];
     568}
     569
     570function outrank_attachment_is_featured_elsewhere($attachment_id, $exclude_post_id = 0) {
     571    global $wpdb;
     572
     573    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
     574    $count = $wpdb->get_var($wpdb->prepare(
     575        "SELECT COUNT(*) FROM {$wpdb->postmeta} pm
     576        INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id
     577        WHERE pm.meta_key = '_thumbnail_id'
     578        AND pm.meta_value = %d
     579        AND p.post_type = 'post'
     580        AND p.ID != %d",
     581        $attachment_id,
     582        $exclude_post_id
     583    ));
     584
     585    return (int) $count > 0;
     586}
     587
     588function outrank_normalize_tracking_slug($slug) {
     589    return preg_replace('/__trashed$/', '', (string) $slug);
     590}
     591
     592function outrank_slug_exists_for_other_post($slug, $post_id, $original_slug) {
     593    global $wpdb;
     594
     595    $table_name = $wpdb->prefix . 'outrank_manage';
     596    $original_tracking_slug = outrank_normalize_tracking_slug($original_slug);
     597
     598    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
     599    $wp_conflict = $wpdb->get_var($wpdb->prepare(
     600        "SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_name = %s AND post_type = 'post' AND ID != %d",
     601        $slug,
     602        $post_id
     603    ));
     604
     605    if ((int) $wp_conflict > 0) {
     606        return true;
     607    }
     608
     609    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
     610    $table_conflict = $wpdb->get_var($wpdb->prepare(
     611        "SELECT COUNT(*) FROM {$table_name} WHERE slug = %s AND slug NOT IN (%s, %s)",
     612        $slug,
     613        $original_slug,
     614        $original_tracking_slug
     615    ));
     616
     617    return (int) $table_conflict > 0;
    240618}
    241619
     
    401779}
    402780
    403 function outrank_set_seo_meta($post_id, $title, $meta_description = '', $focus_keyword = '') {
    404     if (!empty($meta_description)) {
     781function outrank_set_seo_meta($post_id, $title = null, $meta_description = null, $focus_keyword = null) {
     782    if ($meta_description !== null && $meta_description !== '') {
    405783        update_post_meta($post_id, '_yoast_wpseo_metadesc', $meta_description);
    406784        update_post_meta($post_id, 'rank_math_description', $meta_description);
     
    408786        update_post_meta($post_id, '_seopress_titles_desc', $meta_description);
    409787    }
    410     if (!empty($focus_keyword)) {
     788    if ($focus_keyword !== null && $focus_keyword !== '') {
    411789        update_post_meta($post_id, '_yoast_wpseo_focuskw', $focus_keyword);
    412790        update_post_meta($post_id, 'rank_math_focus_keyword', $focus_keyword);
     
    416794        update_post_meta($post_id, '_seopress_analysis_target_kw', $focus_keyword);
    417795    }
    418     if (!empty($title)) {
     796    if ($title !== null && $title !== '') {
    419797        update_post_meta($post_id, '_yoast_wpseo_title', $title);
    420798        update_post_meta($post_id, 'rank_math_title', $title);
     
    424802}
    425803
    426 function outrank_set_squirrly_seo($post_id, $title, $meta_description = '', $focus_keyword = '') {
     804function outrank_set_squirrly_seo($post_id, $title = null, $meta_description = null, $focus_keyword = null) {
    427805    global $wpdb;
    428806    $sq_table = $wpdb->prefix . 'qss';
     
    460838    }
    461839
    462     $seo_data['title'] = $title ?? '';
    463     $seo_data['description'] = $meta_description;
    464     $seo_data['keywords'] = $focus_keyword;
     840    if ($title !== null) {
     841        $seo_data['title'] = $title;
     842    }
     843    if ($meta_description !== null) {
     844        $seo_data['description'] = $meta_description;
     845    }
     846    if ($focus_keyword !== null) {
     847        $seo_data['keywords'] = $focus_keyword;
     848    }
    465849    $seo_data['doseo'] = 1;
    466850
  • outrank/trunk/outrank.php

    r3476368 r3490608  
    66 * Plugin URI: https://outrank.so
    77 * Description: Get traffic and outrank competitors with automatic SEO-optimized content generation published to your WordPress site.
    8  * Version: 1.0.7
     8 * Version: 1.0.8
    99 * Author: Outrank
    1010 * License: GPLv2 or later
     
    258258    if (strpos($hook_suffix, 'outrank') === false) return; // Only enqueue on outrank pages
    259259
    260     wp_enqueue_style('outrank-style', OUTRANK_PLUGIN_URL . 'css/manage.css', [], '1.0.7');
    261     wp_enqueue_style('outrank-home-style', OUTRANK_PLUGIN_URL . 'css/home.css', [], '1.0.7');
    262 
    263     wp_enqueue_script('outrank-script', OUTRANK_PLUGIN_URL . 'script/manage.js', ['jquery'], '1.0.7', true);
     260    wp_enqueue_style('outrank-style', OUTRANK_PLUGIN_URL . 'css/manage.css', [], '1.0.8');
     261    wp_enqueue_style('outrank-home-style', OUTRANK_PLUGIN_URL . 'css/home.css', [], '1.0.8');
     262
     263    wp_enqueue_script('outrank-script', OUTRANK_PLUGIN_URL . 'script/manage.js', ['jquery'], '1.0.8', true);
    264264}
    265265
     
    339339    outrank_clear_articles_cache();
    340340}
    341 
  • outrank/trunk/readme.txt

    r3476368 r3490608  
    55Tested up to: 6.9
    66Requires PHP: 8.0 
    7 Stable tag: 1.0.7
     7Stable tag: 1.0.8
    88License: GPLv2 or later 
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html 
     
    7171== Changelog ==
    7272
     73= 1.0.8 =
     74* Added support for updating existing synced articles from Outrank
     75* Improved integration compatibility by exposing available plugin capabilities
     76* Internal maintenance and packaging improvements
     77
    7378= 1.0.7 =
    7479* External images in articles are now downloaded to the WordPress media library for better performance and SEO
Note: See TracChangeset for help on using the changeset viewer.