Changeset 3799313
- Timestamp:
- 11/12/2026 09:48:00 PM (less than one hour ago)
- Location:
- devenia-autoposter-for-linkedin/trunk
- Files:
-
- 2 added
- 3 edited
-
devenia-autoposter-for-linkedin.php (modified) (34 diffs)
-
includes (added)
-
includes/devenia-updater-notice.php (added)
-
readme.txt (modified) (2 diffs)
-
uninstall.php (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
devenia-autoposter-for-linkedin/trunk/devenia-autoposter-for-linkedin.php
r3694248 r3799313 4 4 * Plugin URI: https://devenia.com/plugins/linkedin-autoposter/ 5 5 * Description: Automatically share posts to LinkedIn when published. Uses official LinkedIn API - no scraping, no bloat. 6 * Version: 1.5. 176 * Version: 1.5.21 7 7 * Requires at least: 5.4 8 8 * Requires PHP: 7.4 … … 19 19 } 20 20 21 define('DLAP_VERSION', '1.5.17'); 21 add_action( 'admin_init', static function () { 22 require_once __DIR__ . '/includes/devenia-updater-notice.php'; 23 dlap_Updater_Notice::register( __FILE__ ); 24 } ); 25 26 define('DLAP_VERSION', '1.5.21'); 22 27 define('DLAP_PLUGIN_DIR', plugin_dir_path(__FILE__)); 23 28 define('DLAP_PLUGIN_URL', plugin_dir_url(__FILE__)); … … 600 605 <?php if (!empty($readiness['surface_probe']['summary']['warnings'])): ?> 601 606 <div style="border-left: 4px solid #d63638; background: #fcf0f1; padding: 8px 12px; margin: 10px 0;"> 602 <strong>LinkedIn API surface warnings from last reconnect</strong> 607 <strong>Recorded API check results from last reconnect</strong> 608 <p>These are recorded check results, not a test of publishing. Administrative API access is separate from permission to create company posts.</p> 603 609 <ul style="margin: 6px 0 0 18px;"> 604 610 <?php foreach ($readiness['surface_probe']['summary']['warnings'] as $warning): ?> … … 868 874 */ 869 875 public function handle_oauth_callback() { 876 if (!current_user_can('manage_options')) { 877 return; 878 } 870 879 // Handle disconnect 871 880 $nonce = isset( $_GET['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ) : ''; 872 881 if ( isset( $_GET['dlap_disconnect'] ) && wp_verify_nonce( $nonce, 'dlap_disconnect' ) ) { 873 882 delete_option('dlap_access_token'); 883 delete_option('dlap_token_scopes'); 874 884 delete_option('dlap_token_expires'); 875 885 delete_option('dlap_member_id'); … … 925 935 update_option('dlap_access_token', $body['access_token']); 926 936 update_option('dlap_token_expires', time() + $body['expires_in']); 937 // Only the token response proves granted scopes; never infer them from the request. 938 $scope_text = isset($body['scope']) && is_string($body['scope']) ? $body['scope'] : ''; 939 $token_scopes = preg_split('/[\s,]+/', trim($scope_text), -1, PREG_SPLIT_NO_EMPTY); 940 update_option('dlap_token_scopes', $token_scopes ? array_values(array_unique($token_scopes)) : array(), false); 927 941 928 942 // Get member ID … … 1014 1028 } 1015 1029 1016 // Re turn success if at least one post succeeded1030 // Report complete success only when every selected destination succeeded. 1017 1031 if (!empty($results)) { 1018 1032 if (!empty($errors)) { 1019 1033 // Partial success - some posts failed 1020 set_transient('dlap_test_error', 'Partial success. Errors: ' . implode('; ', $errors), 60); 1034 set_transient('dlap_test_error', 'Partial success (' . implode(', ', array_keys($results)) . '). Errors: ' . implode('; ', $errors), 60); 1035 return false; 1021 1036 } 1022 1037 return true; … … 1032 1047 private function get_admin_organizations() { 1033 1048 $organizations = array(); 1049 1050 if (!$this->has_token_scope(array('r_organization_admin', 'rw_organization_admin'))) { 1051 return $organizations; 1052 } 1034 1053 1035 1054 // Get organization access control (admin roles) … … 1130 1149 } elseif (in_array('permission_error', $organization_probe_classifications, true)) { 1131 1150 $organization_permission_verification = 'unavailable'; 1132 } elseif (empty($organization_probe_classifications) ) {1151 } elseif (empty($organization_probe_classifications) || in_array('not_checked_scope', $organization_probe_classifications, true)) { 1133 1152 $organization_permission_verification = 'not_checked'; 1134 1153 } else { … … 1153 1172 $warnings[] = 'Company page target is enabled, but organization ID is missing.'; 1154 1173 } elseif ('unavailable' === $organization_permission_verification) { 1155 $warnings[] = ' LinkedIn company-page diagnostics are unavailable for this token. Publishing remains enabled because the Posts API is the authoritative permission check.';1174 $warnings[] = 'Optional company-page administrative checks were denied. This does not establish a posting failure. Company posting has not been verified by these checks; inspect the result of an authorized post.'; 1156 1175 } elseif (!$organization_permission_confirmed) { 1157 $warnings[] = 'Company page access was not confirmed by the optional diagnostics. Publishing remains enabled because the Posts API is the authoritative permission check.';1176 $warnings[] = 'Company-page administrative access has not been checked or confirmed. These optional checks do not verify posting; inspect the result of an authorized post.'; 1158 1177 } 1159 1178 } … … 1189 1208 1190 1209 /** 1210 * Check scopes actually returned for this token, without requesting broader access. 1211 * Older tokens with unknown scopes do not authorize optional administrative probes. 1212 */ 1213 private function has_token_scope($required_scopes) { 1214 $scopes = get_option('dlap_token_scopes', array()); 1215 return is_array($scopes) && (bool) array_intersect($required_scopes, $scopes); 1216 } 1217 1218 /** 1219 * Explain an optional probe that was not sent. This is not a successful check. 1220 */ 1221 private function skipped_admin_probe() { 1222 return array( 1223 'success' => null, 1224 'status_code' => 0, 1225 'message' => 'Not checked: the token has no confirmed administrative scope. Company posting uses w_organization_social, not administrative access.', 1226 'classification' => 'not_checked_scope', 1227 ); 1228 } 1229 1230 /** 1191 1231 * Return the last LinkedIn surface probe captured during reconnect. 1192 1232 */ … … 1230 1270 $probe['checks']['userinfo'] = $this->normalize_linkedin_response($userinfo_response, array(200), 'userinfo'); 1231 1271 1232 $acl_response = $this->linkedin_rest_request( 1233 'GET', 1234 'https://api.linkedin.com/rest/organizationAcls?q=roleAssignee&role=ADMINISTRATOR', 1235 $access_token 1236 ); 1237 $probe['checks']['organization_acls'] = $this->normalize_linkedin_response($acl_response, array(200), 'organization_acls'); 1238 1239 if ('' !== $member_id && '' !== $organization_id) { 1272 if ($this->has_token_scope(array('r_organization_admin', 'rw_organization_admin'))) { 1273 $acl_response = $this->linkedin_rest_request( 1274 'GET', 1275 'https://api.linkedin.com/rest/organizationAcls?q=roleAssignee&role=ADMINISTRATOR', 1276 $access_token 1277 ); 1278 $probe['checks']['organization_acls'] = $this->normalize_linkedin_response($acl_response, array(200), 'organization_acls'); 1279 1280 } else { 1281 $probe['checks']['organization_acls'] = $this->skipped_admin_probe(); 1282 } 1283 1284 if (!$this->has_token_scope(array('rw_organization_admin'))) { 1285 $probe['checks']['organization_authorization_organic_share_create'] = $this->skipped_admin_probe(); 1286 } elseif ('' !== $member_id && '' !== $organization_id) { 1240 1287 $person_urn = rawurlencode('urn:li:person:' . $member_id); 1241 1288 $organization_urn = rawurlencode('urn:li:organization:' . $organization_id); … … 1326 1373 * Create a share attempt record. 1327 1374 */ 1328 private function start_share_attempt($post_id, $attempt, $readiness ) {1375 private function start_share_attempt($post_id, $attempt, $readiness, $held_targets = array()) { 1329 1376 $record = array( 1330 1377 'id' => wp_generate_uuid4(), … … 1337 1384 'finished_at_iso'=> '', 1338 1385 'readiness' => $readiness, 1339 'targets' => array(),1386 'targets' => $held_targets, 1340 1387 'message' => '', 1341 1388 ); … … 1429 1476 'language_selection' => $this->get_post_language_rules($post, get_option('dlap_settings', array())), 1430 1477 'queue' => $this->get_queue_state($post_id), 1478 'delivery' => array( 1479 'personal' => get_option('dlap_delivery_' . $post_id . '_personal', array()), 1480 'organization' => get_option('dlap_delivery_' . $post_id . '_organization', array()), 1481 ), 1431 1482 'last_attempt' => get_post_meta($post_id, DLAP_LAST_ATTEMPT_META_KEY, true), 1432 1483 'attempts' => $history, … … 1584 1635 1585 1636 $last_response = false; 1637 $safe_to_retry = in_array(strtoupper($method), array('GET', 'HEAD'), true); 1586 1638 1587 1639 foreach ($this->get_linkedin_api_versions() as $version) { … … 1593 1645 1594 1646 if (is_wp_error($response)) { 1595 if ($ attempt < DLAP_HTTP_MAX_ATTEMPTS) {1647 if ($safe_to_retry && $attempt < DLAP_HTTP_MAX_ATTEMPTS) { 1596 1648 $delay = $this->get_http_retry_delay_seconds($attempt); 1597 1649 dlap_log('DLAP LinkedIn API - Request error, retrying in ' . $delay . 's: ' . $response->get_error_message()); … … 1611 1663 } 1612 1664 1613 if ($ this->is_retryable_linkedin_response_code($response_code) && $attempt < DLAP_HTTP_MAX_ATTEMPTS) {1665 if ($safe_to_retry && $this->is_retryable_linkedin_response_code($response_code) && $attempt < DLAP_HTTP_MAX_ATTEMPTS) { 1614 1666 $delay = $this->get_http_retry_delay_seconds($attempt, $response); 1615 1667 dlap_log('DLAP LinkedIn API - Retryable response (' . $response_code . '), retrying in ' . $delay . 's.'); … … 1859 1911 1860 1912 $readiness = $this->get_connection_readiness(); 1861 $attempt_record = $this->start_share_attempt($post_id, $attempt, $readiness); 1913 // Carry terminal outcomes only within this automatic retry chain. 1914 // A new explicit attempt starts a fresh chain after configuration is corrected. 1915 $previous = get_post_meta($post_id, DLAP_LAST_ATTEMPT_META_KEY, true); 1916 $held_targets = array(); 1917 if (is_array($previous) && ( 1918 ('retry_scheduled' === ($previous['status'] ?? '') && $attempt === (int) $previous['attempt'] + 1) || 1919 ('running' === ($previous['status'] ?? '') && $attempt === (int) $previous['attempt']) 1920 )) { 1921 foreach (($previous['targets'] ?? array()) as $target => $result) { 1922 if ('failed' === ($result['status'] ?? '') && empty($result['response']['retryable'])) { 1923 $held_targets[$target] = $result; 1924 } 1925 } 1926 } 1927 $attempt_record = $this->start_share_attempt($post_id, $attempt, $readiness, $held_targets); 1862 1928 1863 1929 try { … … 1875 1941 update_post_meta($post_id, '_dlap_share_queued', time() + $delay); 1876 1942 update_post_meta($post_id, '_dlap_share_attempt', $attempt); 1877 $this->finish_share_attempt($post_id, $attempt_record, 'retry_scheduled', $message, array());1943 $this->finish_share_attempt($post_id, $attempt_record, 'retry_scheduled', $message, $held_targets); 1878 1944 } else { 1879 $this->finish_share_attempt($post_id, $attempt_record, 'queue_failed', $message, array());1945 $this->finish_share_attempt($post_id, $attempt_record, 'queue_failed', $message, $held_targets); 1880 1946 } 1881 1947 } else { 1882 1948 $this->update_queue_state($post_id, 'exhausted', $attempt, 0, $message); 1883 $this->finish_share_attempt($post_id, $attempt_record, 'failed', $message, array());1949 $this->finish_share_attempt($post_id, $attempt_record, 'failed', $message, $held_targets); 1884 1950 } 1885 1951 return; 1886 1952 } 1887 1953 1888 $result = $this->share_post($post); 1954 $completed_targets = get_post_meta($post_id, '_dlap_post_id', true); 1955 $completed_targets = is_array($completed_targets) ? $completed_targets : array(); 1956 $result = $this->share_post($post, $attempt_record); 1957 if (is_array($result) && !empty($result['linkedin_ids'])) { 1958 update_post_meta($post_id, '_dlap_post_id', array_merge($completed_targets, $result['linkedin_ids'])); 1959 } 1889 1960 if (is_array($result) && !empty($result['skipped'])) { 1890 1961 delete_post_meta($post_id, '_dlap_error'); … … 1895 1966 return; 1896 1967 } 1897 if (is_array($result) && !empty($result['success']) ) {1968 if (is_array($result) && !empty($result['success']) && empty($result['incomplete'])) { 1898 1969 update_post_meta($post_id, '_dlap_shared', time()); 1899 update_post_meta($post_id, '_dlap_post_id', isset($result['linkedin_ids']) ? $result['linkedin_ids'] : array());1970 update_post_meta($post_id, '_dlap_post_id', array_merge($completed_targets, isset($result['linkedin_ids']) ? $result['linkedin_ids'] : array())); 1900 1971 delete_post_meta($post_id, '_dlap_error'); 1901 delete_post_meta($post_id, '_dlap_error_personal'); 1902 delete_post_meta($post_id, '_dlap_error_organization'); 1972 // Successful targets clear their own errors; preserve other target history. 1903 1973 delete_post_meta($post_id, '_dlap_share_attempt'); 1904 1974 delete_post_meta($post_id, '_dlap_share_queued'); … … 1915 1985 update_post_meta($post_id, '_dlap_error', $last_error); 1916 1986 update_post_meta($post_id, '_dlap_share_attempt', $attempt); 1987 1988 $retryable = false; 1989 $has_uncertain_delivery = false; 1990 foreach ($target_results as $target_result) { 1991 if ('delivery_uncertain' === ($target_result['response']['classification'] ?? '')) { 1992 $has_uncertain_delivery = true; 1993 } 1994 if ('failed' === $target_result['status']) { 1995 if (!empty($target_result['response']['retryable'])) { 1996 $retryable = true; 1997 } 1998 } 1999 } 2000 if (!$retryable) { 2001 $state = $has_uncertain_delivery ? 'needs_reconciliation' : (!empty($result['success']) ? 'partial_failed' : 'failed'); 2002 delete_post_meta($post_id, '_dlap_share_queued'); 2003 $this->update_queue_state($post_id, $state, $attempt, 0, $last_error); 2004 $this->finish_share_attempt($post_id, $attempt_record, $state, $last_error, $target_results); 2005 return; 2006 } 1917 2007 1918 2008 if ($attempt < DLAP_SHARE_MAX_ATTEMPTS) { … … 2112 2202 * Share post to LinkedIn 2113 2203 */ 2114 public function share_post($post) { 2204 public function share_post($post, $attempt_record = null) { 2205 $held_targets = is_array($attempt_record) ? $attempt_record['targets'] : array(); 2206 $completed_targets = get_post_meta($post->ID, '_dlap_post_id', true); 2207 $completed_targets = is_array($completed_targets) ? $completed_targets : array(); 2115 2208 $member_id = get_option('dlap_member_id', ''); 2116 2209 $access_token = get_option('dlap_access_token', ''); … … 2211 2304 $enabled_ready_target = false; 2212 2305 2213 if (($post_target === 'personal' || $post_target === 'both') && $language_rules['allowed']['personal'] && $this->is_target_ready('personal', $readiness)) {2306 if (($post_target === 'personal' || $post_target === 'both') && $language_rules['allowed']['personal'] && empty($completed_targets['personal']) && empty($held_targets['personal']) && $this->is_target_ready('personal', $readiness)) { 2214 2307 $enabled_ready_target = true; 2215 2308 } 2216 if (($post_target === 'organization' || $post_target === 'both') && $language_rules['allowed']['organization'] && $this->is_target_ready('organization', $readiness)) {2309 if (($post_target === 'organization' || $post_target === 'both') && $language_rules['allowed']['organization'] && empty($completed_targets['organization']) && empty($held_targets['organization']) && $this->is_target_ready('organization', $readiness)) { 2217 2310 $enabled_ready_target = true; 2218 2311 } … … 2224 2317 foreach (array('personal', 'organization') as $target) { 2225 2318 if ('both' === $post_target || $target === $post_target) { 2319 if (!empty($completed_targets[$target]) && $language_rules['allowed'][$target]) { 2320 $has_success = true; 2321 $linkedin_ids[$target] = $completed_targets[$target]; 2322 $targets[$target] = $this->build_target_result($target, 'success', 'Already shared; not sent again.', array('linkedin_id' => $completed_targets[$target])); 2323 continue; 2324 } 2226 2325 $targets[$target] = $language_rules['allowed'][$target] 2227 2326 ? $this->build_target_result($target, 'skipped', $message) … … 2229 2328 } 2230 2329 } 2231 return array('success' => false, 'message' => $message, 'targets' => $targets, 'linkedin_ids' => array());2330 return array('success' => $has_success, 'incomplete' => true, 'message' => $message, 'targets' => $targets, 'linkedin_ids' => $linkedin_ids); 2232 2331 } 2233 2332 … … 2236 2335 if (!$language_rules['allowed']['personal']) { 2237 2336 $targets['personal'] = $this->language_skip_result('personal', $language_rules); 2337 } elseif (!empty($completed_targets['personal'])) { 2338 $has_success = true; 2339 $linkedin_ids['personal'] = $completed_targets['personal']; 2340 $targets['personal'] = $this->build_target_result('personal', 'success', 'Already shared; not sent again.', array('linkedin_id' => $completed_targets['personal'])); 2341 } elseif (isset($held_targets['personal'])) { 2342 $targets['personal'] = $held_targets['personal']; 2238 2343 } elseif (!$this->is_target_ready('personal', $readiness)) { 2239 2344 $message = 'Personal profile target is not ready.'; … … 2256 2361 } 2257 2362 2258 $response = $this-> post_to_linkedin_result($access_token, $author_urn, $content, null, null, null, null, $image_urn);2363 $response = $this->deliver_post_target($post->ID, 'personal', $access_token, $author_urn, $content, $image_urn); 2259 2364 if (!empty($response['success'])) { 2260 2365 $has_success = true; … … 2277 2382 } 2278 2383 2384 // Preserve the first destination's outcome before the next external request. 2385 if (is_array($attempt_record)) { 2386 $attempt_record['targets'] = array_merge($held_targets, $targets); 2387 $this->save_share_attempt($post->ID, $attempt_record); 2388 } 2389 2279 2390 // Post to company page if target is organization or both 2280 2391 if ($post_target === 'organization' || $post_target === 'both') { 2281 2392 if (!$language_rules['allowed']['organization']) { 2282 2393 $targets['organization'] = $this->language_skip_result('organization', $language_rules); 2394 } elseif (!empty($completed_targets['organization'])) { 2395 $has_success = true; 2396 $linkedin_ids['organization'] = $completed_targets['organization']; 2397 $targets['organization'] = $this->build_target_result('organization', 'success', 'Already shared; not sent again.', array('linkedin_id' => $completed_targets['organization'])); 2398 } elseif (isset($held_targets['organization'])) { 2399 $targets['organization'] = $held_targets['organization']; 2283 2400 } elseif (!$this->is_target_ready('organization', $readiness)) { 2284 2401 $message = 'Company page target is not ready or was not confirmed for this token.'; … … 2301 2418 } 2302 2419 2303 $response = $this-> post_to_linkedin_result($access_token, $author_urn, $content, null, null, null, null, $image_urn);2420 $response = $this->deliver_post_target($post->ID, 'organization', $access_token, $author_urn, $content, $image_urn); 2304 2421 if (!empty($response['success'])) { 2305 2422 $has_success = true; … … 2324 2441 $failed_messages = array(); 2325 2442 foreach ($targets as $target_result) { 2326 if (empty($target_result['success']) && !empty($ target_result['message'])) {2443 if (empty($target_result['success']) && !empty($language_rules['allowed'][$target_result['target']]) && !empty($target_result['message'])) { 2327 2444 $failed_messages[] = $target_result['target'] . ': ' . $target_result['message']; 2328 2445 } … … 2331 2448 return array( 2332 2449 'success' => $has_success, 2333 'message' => $has_success ? 'At least one LinkedIn target succeeded.' : implode('; ', $failed_messages), 2450 'incomplete' => !empty($failed_messages), 2451 'message' => !empty($failed_messages) ? implode('; ', $failed_messages) : 'All eligible LinkedIn targets succeeded.', 2334 2452 'targets' => $targets, 2335 2453 'linkedin_ids' => $linkedin_ids, 2336 2454 ); 2455 } 2456 2457 /** 2458 * Own durable delivery at the post/destination seam, before the external POST. 2459 * A pending marker survives process failure and must not be sent again blindly. 2460 */ 2461 private function deliver_post_target($post_id, $target, $access_token, $author_urn, $content, $image_urn) { 2462 $marker = 'dlap_delivery_' . absint($post_id) . '_' . sanitize_key($target); 2463 $pending = array('state' => 'pending', 'author' => $author_urn, 'started_at' => time()); 2464 if (!add_option($marker, $pending, '', false)) { 2465 $existing = get_option($marker, array()); 2466 if (is_array($existing) && 'succeeded' === ($existing['state'] ?? '') && !empty($existing['response']['linkedin_id'])) { 2467 return $existing['response']; 2468 } 2469 return array('success' => false, 'status_code' => 0, 'classification' => 'delivery_uncertain', 'retryable' => false, 2470 'message' => 'An earlier delivery may have reached LinkedIn. Check its outcome before retrying this destination.', 'linkedin_id' => ''); 2471 } 2472 $response = $this->post_to_linkedin_result($access_token, $author_urn, $content, null, null, null, null, $image_urn); 2473 if (!empty($response['success'])) { 2474 // Save the provider receipt before attempting the next destination. 2475 $ids = get_post_meta($post_id, '_dlap_post_id', true); 2476 $ids = is_array($ids) ? $ids : array(); 2477 $ids[$target] = $response['linkedin_id']; 2478 update_post_meta($post_id, '_dlap_post_id', $ids); 2479 update_option($marker, array('state' => 'succeeded', 'author' => $author_urn, 'response' => $response), false); 2480 } elseif ('delivery_uncertain' === ($response['classification'] ?? '')) { 2481 update_option($marker, array('state' => 'uncertain', 'author' => $author_urn, 'response' => $response), false); 2482 } else { 2483 // An explicit rejection did not create a post; a later authorized retry is safe. 2484 delete_option($marker); 2485 } 2486 return $response; 2337 2487 } 2338 2488 … … 2576 2726 2577 2727 $result = $this->normalize_linkedin_response($response, array(201), 'posts_create'); 2728 // Transport failures, server errors, and missing creation receipts have uncertain effects. 2729 if (0 === $result['status_code'] || $result['status_code'] >= 500 || (!empty($result['success']) && empty($result['linkedin_id']))) { 2730 $result['success'] = false; 2731 $result['classification'] = 'delivery_uncertain'; 2732 $result['retryable'] = false; 2733 $result['message'] = 'LinkedIn delivery outcome is uncertain. Check for an existing post before retrying. ' . $result['message']; 2734 } 2735 2578 2736 if (empty($result['success'])) { 2579 2737 set_transient('dlap_last_error', isset($result['message']) ? $result['message'] : 'LinkedIn sharing failed.', 60); … … 2660 2818 <?php endif; ?> 2661 2819 2820 <?php if (current_user_can('manage_options')): ?> 2821 <?php foreach (array('personal', 'organization') as $target): 2822 $delivery = get_option('dlap_delivery_' . $post->ID . '_' . $target, array()); 2823 if (!is_array($delivery) || !in_array($delivery['state'] ?? '', array('pending', 'uncertain'), true)) { 2824 continue; 2825 } 2826 ?> 2827 <p><strong><?php echo esc_html(ucfirst($target)); ?>: <?php esc_html_e('Delivery needs reconciliation', 'devenia-autoposter-for-linkedin'); ?></strong><br> 2828 <?php esc_html_e('Check LinkedIn before choosing an outcome. Saving a confirmed non-publication queues a new attempt; an incorrect choice can create a duplicate.', 'devenia-autoposter-for-linkedin'); ?></p> 2829 <p><label><?php esc_html_e('Verified outcome', 'devenia-autoposter-for-linkedin'); ?> 2830 <select name="dlap_resolution[<?php echo esc_attr($target); ?>]"> 2831 <option value=""><?php esc_html_e('Leave unresolved', 'devenia-autoposter-for-linkedin'); ?></option> 2832 <option value="published"><?php esc_html_e('I confirmed this post is published', 'devenia-autoposter-for-linkedin'); ?></option> 2833 <option value="not_published"><?php esc_html_e('I confirmed it was not published; retry', 'devenia-autoposter-for-linkedin'); ?></option> 2834 </select> 2835 </label></p> 2836 <p><label><?php esc_html_e('Published LinkedIn post URN (required for published outcome)', 'devenia-autoposter-for-linkedin'); ?> 2837 <input type="text" name="dlap_resolution_id[<?php echo esc_attr($target); ?>]" placeholder="urn:li:share:123456789" class="widefat"> 2838 </label></p> 2839 <?php endforeach; ?> 2840 <?php endif; ?> 2841 2662 2842 <?php if (!$this->is_connected()): ?> 2663 2843 <p style="color: #d63638;"> … … 2688 2868 $disabled = isset( $_POST['dlap_disable'] ) ? '1' : ''; 2689 2869 update_post_meta( $post_id, '_dlap_disable', $disabled ); 2870 2871 // Reconciliation is an explicit administrator action through the native post form. 2872 $post = get_post($post_id); 2873 if (!current_user_can('manage_options') || $disabled || !$post instanceof WP_Post || 'publish' !== $post->post_status) { 2874 return; 2875 } 2876 if ((int) get_option('dlap_lock_post_share_' . $post_id, 0) > time()) { 2877 return; 2878 } 2879 $resolutions = isset($_POST['dlap_resolution']) && is_array($_POST['dlap_resolution']) ? array_map('sanitize_key', wp_unslash($_POST['dlap_resolution'])) : array(); 2880 $post_ids = isset($_POST['dlap_resolution_id']) && is_array($_POST['dlap_resolution_id']) ? array_map('sanitize_text_field', wp_unslash($_POST['dlap_resolution_id'])) : array(); 2881 $resolved = false; 2882 foreach (array('personal', 'organization') as $target) { 2883 $choice = isset($resolutions[$target]) && is_string($resolutions[$target]) ? $resolutions[$target] : ''; 2884 $marker = 'dlap_delivery_' . $post_id . '_' . $target; 2885 $delivery = get_option($marker, array()); 2886 if (!is_array($delivery) || !in_array($delivery['state'] ?? '', array('pending', 'uncertain'), true)) { 2887 continue; 2888 } 2889 $previous_delivery = $delivery; 2890 $id = isset($post_ids[$target]) && is_string($post_ids[$target]) ? $post_ids[$target] : ''; 2891 if ('published' === $choice && preg_match('/^urn:li:(share|ugcPost):[0-9]+$/', $id)) { 2892 $ids = get_post_meta($post_id, '_dlap_post_id', true); 2893 $ids = is_array($ids) ? $ids : array(); 2894 $ids[$target] = $id; 2895 update_post_meta($post_id, '_dlap_post_id', $ids); 2896 $delivery['state'] = 'succeeded'; 2897 $delivery['response'] = array('success' => true, 'linkedin_id' => $id, 'message' => 'Publication confirmed by administrator.', 'retryable' => false); 2898 update_option($marker, $delivery, false); 2899 } elseif ('not_published' === $choice) { 2900 delete_option($marker); 2901 } else { 2902 continue; 2903 } 2904 update_post_meta($post_id, '_dlap_resolution_' . $target, array('choice' => $choice, 'linkedin_id' => $id, 'resolved_at' => time(), 'previous_delivery' => $previous_delivery)); 2905 delete_post_meta($post_id, '_dlap_error_' . $target); 2906 $resolved = true; 2907 } 2908 if ($resolved) { 2909 $this->schedule_post_share($post_id, 1, 10); 2910 } 2690 2911 } 2691 2912 -
devenia-autoposter-for-linkedin/trunk/readme.txt
r3694264 r3799313 60 60 == Installation == 61 61 62 For update notifications in WordPress, install [Devenia MCP Updater](https://downloads.devenia.com/devenia-mcp-updater.zip). The updater is optional. You choose which plugins update automatically through WordPress. 63 62 64 1. Download the [plugin ZIP](https://downloads.devenia.com/devenia-autoposter-for-linkedin.zip). 63 65 2. In WordPress, open Plugins > Add New > Upload Plugin. Upload and activate it. … … 96 98 == Changelog == 97 99 100 = 1.5.21 = 101 * Preserve partial queue failures and successful destination receipts. A later retry does not resend to a destination that already succeeded. 102 * Report partial test-post results instead of claiming both destinations succeeded. 103 * Save delivery state before each post and preserve each receipt immediately. Interrupted or uncertain deliveries stop for reconciliation instead of being sent again blindly. 104 * Let administrators record a verified delivery outcome in the post editor before retrying. 105 106 = 1.5.20 = 107 * Run optional company administration checks only when the token confirms the required administrative scope. No broader OAuth permissions are requested. 108 * Distinguish unverified company posting from optional administrative check results, retaining recorded access denials. 109 * Keep personal and company publishing independent when either destination fails. 110 111 = 1.5.19 = 112 * Add one dismissible Plugins-screen reminder when Devenia MCP Updater is missing or inactive, with persistent install or activate links. Automatic updates remain your choice in WordPress. 113 98 114 = 1.5.17 = 99 115 * Added: Separate post-language selections for personal profiles and company pages. -
devenia-autoposter-for-linkedin/trunk/uninstall.php
r3561915 r3799313 17 17 delete_option( 'dlap_settings' ); 18 18 delete_option( 'dlap_access_token' ); 19 delete_option( 'dlap_token_scopes' ); 19 20 delete_option( 'dlap_token_expires' ); 20 21 delete_option( 'dlap_member_id' );
Note:
See TracChangeset
for help on using the changeset viewer.