|
|
|
|
|
|
|
|
|
|
|
<?php if (!defined('PmWiki')) exit(); |
|
|
|
|
|
|
|
|
|
|
|
/* pm-pinboard-api.php |
|
|
|
|
|
* Pm Pinboard API (a PmWiki recipe for Pinboard (https://pinboard.in) integration) |
|
|
|
|
|
* |
|
|
|
|
|
* See http://www.pmwiki.org/wiki/Cookbook/PmPinboardAPI |
|
|
|
|
|
* for documentation and latest version. |
|
|
|
|
|
* |
|
|
|
|
|
* This program is free software; you can redistribute it |
|
|
|
|
|
* and/or modify it under the terms of the GNU General |
|
|
|
|
|
* Public License as published by the Free Software |
|
|
|
|
|
* Foundation; either version 3 of the License, or (at your |
|
|
|
|
|
* option) any later version. Available at |
|
|
|
|
|
* https://www.gnu.org/licenses/gpl.txt |
|
|
|
|
|
* |
|
|
|
|
|
* Copyright 2018 Said Achmiz. |
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
|
|
|
|
$RecipeInfo['PmPinboardAPI']['Version'] = '2018-01-09'; |
|
|
|
|
|
|
|
|
|
|
|
/*************/ |
|
|
|
|
|
/* VARIABLES */ |
|
|
|
|
|
/*************/ |
|
|
|
|
|
|
|
|
|
|
|
SDV($PinboardAPIEndpoint, 'https://api.pinboard.in/v1/'); |
|
|
|
|
|
SDV($PinboardAPIToken, 'YOUR_API_TOKEN_GOES_HERE'); |
|
|
|
|
|
SDVA($PinboardAPIAllowedMethods, [ |
|
|
|
|
|
'posts/update', |
|
|
|
|
|
// 'posts/add', |
|
|
|
|
|
// 'posts/delete', |
|
|
|
|
|
'posts/get', |
|
|
|
|
|
'posts/recent', |
|
|
|
|
|
'posts/dates', |
|
|
|
|
|
'posts/all', |
|
|
|
|
|
'posts/suggest', |
|
|
|
|
|
|
|
|
|
|
|
'tags/get', |
|
|
|
|
|
// 'tags/delete', |
|
|
|
|
|
// 'tags/rename', |
|
|
|
|
|
|
|
|
|
|
|
'user/secret', |
|
|
|
|
|
|
|
|
|
|
|
'notes/list', |
|
|
|
|
|
'notes/ID' |
|
|
|
|
|
]); |
|
|
|
|
|
SDVA($PinboardAPIMethodCooldowns, [ |
|
|
|
|
|
'global' => 3, |
|
|
|
|
|
'posts/recent' => 60, |
|
|
|
|
|
'posts/all' => 300 |
|
|
|
|
|
]); |
|
|
|
|
|
SDV($PinboardAPICacheFolder, 'pub/cache/pinboard/'); |
|
|
|
|
|
SDV($PinboardAPIResponseCacheDuration, 0); |
|
|
|
|
|
$PinboardAPIResponseCacheDuration = ($PinboardAPIResponseCacheDuration > 0) ? |
|
|
|
|
|
max($PinboardAPIResponseCacheDuration, max($PinboardAPIMethodCooldowns)) : |
|
|
|
|
|
0; |
|
|
|
|
|
|
|
|
|
|
|
/*********/ |
|
|
|
|
|
/* SETUP */ |
|
|
|
|
|
/*********/ |
|
|
|
|
|
|
|
|
|
|
|
if (!file_exists($PinboardAPICacheFolder)) |
|
|
|
|
|
mkdir($PinboardAPICacheFolder); |
|
|
|
|
|
|
|
|
|
|
|
$response_cache_file = $PinboardAPICacheFolder."response_cache.json"; |
|
|
|
|
|
$response_cache = PinboardAPIGetResponseCache(); |
|
|
|
|
|
|
|
|
|
|
|
$request_log_file = $PinboardAPICacheFolder."request_log.json"; |
|
|
|
|
|
$request_log = PinboardAPIGetRequestLog(); |
|
|
|
|
|
|
|
|
|
|
|
/*************/ |
|
|
|
|
|
/* FUNCTIONS */ |
|
|
|
|
|
/*************/ |
|
|
|
|
|
|
|
|
|
|
|
function PinboardAPIRequest($method, $params) { |
|
|
|
|
|
global $PinboardAPIToken, $PinboardAPIEndpoint, $PinboardAPIAllowedMethods, |
|
|
|
|
|
$PinboardAPIResponseCacheDuration, $PinboardAPIMethodCooldowns; |
|
|
|
|
|
global $request_log, $response_cache; |
|
|
|
|
|
|
|
|
|
|
|
## Check if specified method is allowed. If not, return an error. |
|
|
|
|
|
if (!(in_array($method, $PinboardAPIAllowedMethods) || |
|
|
|
|
|
(in_array('notes/ID', $PinboardAPIAllowedMethods) && preg_match("^notes\/", $method)))) |
|
|
|
|
|
return [ |
|
|
|
|
|
'error-text' => "The method “$method” is not permitted.", |
|
|
|
|
|
'error-html' => "<p style='color: red; font-weight: bold;'>The method “<code>$method</code>” is not permitted.</p>\n" |
|
|
|
|
|
]; |
|
|
|
|
|
|
|
|
|
|
|
## Build the request. |
|
|
|
|
|
$request_URL = $PinboardAPIEndpoint . $method; |
|
|
|
|
|
$request_params = [ |
|
|
|
|
|
'format' => 'json', |
|
|
|
|
|
'auth_token' => $PinboardAPIToken |
|
|
|
|
|
]; |
|
|
|
|
|
$request_params = array_merge($request_params, $params); |
|
|
|
|
|
$request_URL .= "?" . http_build_query($request_params); |
|
|
|
|
|
|
|
|
|
|
|
## This is for logging/caching. |
|
|
|
|
|
$request_URL_hash = md5($request_URL); |
|
|
|
|
|
$cur_time = time(); |
|
|
|
|
|
|
|
|
|
|
|
## If... |
|
|
|
|
|
## a) a response cache duration is specified, and... |
|
|
|
|
|
## b) there is a cached response for this request, and... |
|
|
|
|
|
## c) the cached response isn't too old... |
|
|
|
|
|
## ... then return the cached response. |
|
|
|
|
|
if ($PinboardAPIResponseCacheDuration > 0 && |
|
|
|
|
|
isset($response_cache[$request_URL_hash]) && |
|
|
|
|
|
($cur_time - $response_cache[$request_URL_hash]['request_time']) <= $PinboardAPIResponseCacheDuration) |
|
|
|
|
|
{ |
|
|
|
|
|
return $response_cache[$request_URL_hash]; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
## Check elapsed time since last request (or last request of this type, in the case of |
|
|
|
|
|
## methods that have their own rate limits (i.e. posts/recent and posts/all)). |
|
|
|
|
|
## If the new request comes too soon after the last one, return an error message. |
|
|
|
|
|
$cooldown_category = $PinboardAPIMethodCooldowns[$method] ? $method : 'global'; |
|
|
|
|
|
$cooldown = $PinboardAPIMethodCooldowns[$cooldown_category]; |
|
|
|
|
|
$elapsed = $cur_time - $request_log["last-{$cooldown_category}"]; |
|
|
|
|
|
## Alternatively, if the last request got an HTTP status code 429 (Too Many Requests), |
|
|
|
|
|
## then make sure a good long while has elapsed since the last request of any kind; |
|
|
|
|
|
## if it hasn't, then return an error. |
|
|
|
|
|
## (What's a "good long while"? Well, "twice as long as the longest cooldown" seems |
|
|
|
|
|
## like a reasonable value. (The longest cooldown should be the 5-minute cooldown for |
|
|
|
|
|
## posts/all, so the cooldown after a 429 ought to be 10 minutes (600 seconds).)) |
|
|
|
|
|
if (isset($request_log['last_request_hash']) && |
|
|
|
|
|
$response_cache[$request_log['last_request_hash']]['http_code'] == 429) { |
|
|
|
|
|
$cooldown = 2 * max($PinboardAPIMethodCooldowns); |
|
|
|
|
|
$elapsed = $cur_time - $request_log["last-global"]; |
|
|
|
|
|
} |
|
|
|
|
|
## In either case, if we're still within the relevant cooldown, return an error. |
|
|
|
|
|
if ($elapsed < $cooldown) |
|
|
|
|
|
return [ |
|
|
|
|
|
'error-text' => "Too many requests. Wait a bit, then try again.", |
|
|
|
|
|
'error-html' => "<p style='color: red; font-weight: bold;'>Too many requests. Wait a bit, then try again.</p>\n" |
|
|
|
|
|
]; |
|
|
|
|
|
|
|
|
|
|
|
## Send the request. |
|
|
|
|
|
$curl = curl_init($request_URL); |
|
|
|
|
|
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); |
|
|
|
|
|
$curl_response = curl_exec($curl); |
|
|
|
|
|
|
|
|
|
|
|
## Handle the response. |
|
|
|
|
|
$info = curl_getinfo($curl); |
|
|
|
|
|
$response = json_decode($curl_response, true); |
|
|
|
|
|
$response['http_code'] = $info['http_code']; |
|
|
|
|
|
$response['request_time'] = $cur_time; |
|
|
|
|
|
curl_close($curl); |
|
|
|
|
|
|
|
|
|
|
|
## Cache the response. |
|
|
|
|
|
PinboardAPIUpdateResponseCache($request_URL_hash, $response); |
|
|
|
|
|
|
|
|
|
|
|
## Update the last-request info. |
|
|
|
|
|
$request_log['last-global'] = $cur_time; |
|
|
|
|
|
if ($method == 'posts/recent') $request_log['last-posts/recent'] = $cur_time; |
|
|
|
|
|
else if ($method == 'posts/all') $request_log['last-posts/all'] = $cur_time; |
|
|
|
|
|
$request_log['last_request_hash'] = $request_URL_hash; |
|
|
|
|
|
PinboardAPISaveRequestLog($request_log); |
|
|
|
|
|
|
|
|
|
|
|
return $response; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/***************/ |
|
|
|
|
|
/* REQUEST LOG */ |
|
|
|
|
|
/***************/ |
|
|
|
|
|
|
|
|
|
|
|
function PinboardAPIResetRequestLog() { |
|
|
|
|
|
global $request_log_file; |
|
|
|
|
|
file_put_contents($request_log_file, json_encode([ |
|
|
|
|
|
'last-global' => 0, |
|
|
|
|
|
'last-posts/recent' => 0, |
|
|
|
|
|
'last-posts/all' => 0 |
|
|
|
|
|
])); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function PinboardAPIGetRequestLog() { |
|
|
|
|
|
global $request_log_file; |
|
|
|
|
|
if (!file_exists($request_log_file)) |
|
|
|
|
|
PinboardAPIResetRequestLog(); |
|
|
|
|
|
|
|
|
|
|
|
$request_log = json_decode(file_get_contents($request_log_file), true); |
|
|
|
|
|
return $request_log; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function PinboardAPISaveRequestLog($request_log) { |
|
|
|
|
|
global $request_log_file; |
|
|
|
|
|
file_put_contents($request_log_file, json_encode($request_log)); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/******************/ |
|
|
|
|
|
/* RESPONSE CACHE */ |
|
|
|
|
|
/******************/ |
|
|
|
|
|
|
|
|
|
|
|
function PinboardAPIClearResponseCache() { |
|
|
|
|
|
global $response_cache_file; |
|
|
|
|
|
file_put_contents($response_cache_file, json_encode([ ])); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function PinboardAPIGetResponseCache() { |
|
|
|
|
|
global $response_cache_file; |
|
|
|
|
|
if (!file_exists($response_cache_file)) { |
|
|
|
|
|
PinboardAPIClearResponseCache(); |
|
|
|
|
|
return [ ]; |
|
|
|
|
|
} else { |
|
|
|
|
|
return json_decode(file_get_contents($response_cache_file), true); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function PinboardAPIUpdateResponseCache($request_URL_hash, $response) { |
|
|
|
|
|
global $response_cache_file, $PinboardAPIResponseCacheDuration; |
|
|
|
|
|
|
|
|
|
|
|
$response_cache = ($PinboardAPIResponseCacheDuration > 0) ? PinboardAPIGetResponseCache() : [ ]; |
|
|
|
|
|
$response_cache[$request_URL_hash] = $response; |
|
|
|
|
|
file_put_contents($response_cache_file, json_encode($response_cache)); |
|
|
|
|
|
} |