--- /dev/null
+<?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));
+}