Initial commit
authorSaid Achmiz <[email protected]>
Sun, 12 Dec 2021 19:32:24 +0000 (12 14:32 -0500)
committerSaid Achmiz <[email protected]>
Sun, 12 Dec 2021 19:32:24 +0000 (12 14:32 -0500)
LICENSE [new file with mode: 0644]
README.md [new file with mode: 0644]
pm-pinboard-api.php [new file with mode: 0644]

diff --git a/LICENSE b/LICENSE
new file mode 100644 (file)
index 0000000..6abfad5
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,7 @@
+Copyright 2018-2021 Said Achmiz.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644 (file)
index 0000000..28a6304
--- /dev/null
+++ b/README.md
@@ -0,0 +1,9 @@
+VersionedAssets recipe for PmWiki.
+
+[https://www.pmwiki.org/wiki/Cookbook/VersionedAssets](https://www.pmwiki.org/wiki/Cookbook/VersionedAssets)
+
+This is a [PmWiki](https://www.pmwiki.org) recipe that adds versions (modification timestamps) to attachment URLs, so that browser caches invalidate properly when attachments are updated.
+
+Please see [the recipe page on pmwiki.org](https://www.pmwiki.org/wiki/Cookbook/VersionedAssets) for installation and usage instructions. (Refer to the Talk page of the recipe page for support.)
+
+Copyright 2017-2021 Said Achmiz.
diff --git a/pm-pinboard-api.php b/pm-pinboard-api.php
new file mode 100644 (file)
index 0000000..0d4e339
--- /dev/null
@@ -0,0 +1,213 @@
+<?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));
+}