*/ // must be run within Dokuwiki if(!defined('DOKU_INC')) die(); class helper_plugin_ghissues_apiCacheInterface extends DokuWiki_Plugin { var $_GH_API_limit; /** * Return info about supported methods in this Helper Plugin * * @return array of public methods */ public function getMethods() { return array( array( 'name' => 'checkIssuesCache', 'desc' => 'Takes an api URL, checks local cache for up-to-date, then returns cache path for use in depends["files"]', 'params' => array( 'apiURL' => 'string', ), 'return' => array('cachePath' => 'string') ), array( 'name' => 'checkCacheFreshness', 'desc' => 'returns true if the cache is up-to-date. Includes API call if required', 'params' => array( 'apiURL' => 'string', 'cache (optional)' => 'class' ), 'return' => array('useCache' => 'boolean') ), array( 'name' => 'callGithubAPI', 'desc' => 'makes the API call given by apiUrl. Saves result in a cache after formatting', 'params' => array( 'apiURL' => 'string', 'cache (optional)' => 'class' ), 'return' => array('useCache' => 'boolean') ), array( 'name' => 'formatApiResponse', 'desc' => 'takes JSON response from API and returns formatted output in xhtml', 'params' => array( 'rawJSON' => 'string', ), 'return' => array('outputXML' => 'string') ), array( 'name' => 'getRenderedRequest', 'desc' => 'returns rendered output from an API request, using cache if valid', 'params' => array( 'apiURL' => 'string', ), 'return' => array('outputXML' => 'string') ) ); } // Master cache checker. First checks if the cache expired, then checks if the page is older than the (possibly updated) cache public function checkIssuesCache( $apiURL ) { $cache = new cache_ghissues_api($apiURL); $this->checkCacheFreshness($apiURL, $cache); return ($cache->cache); } // return true if still fresh. Otherwise you'll confuse the bears public function checkCacheFreshness($apiURL, &$cache=NULL) { //dbglog('ghissues: In checkCacheFreshness'); if ( !isset($cache) ) { $cache = new cache_ghissues_api($apiURL); } // Check if we've done a cached API call recently. If you have, the cache is plenty fresh if ( $cache->sniffETag($this->getConf('ghissuerefresh')) ) return true; // Old cache, time to check in with GH. return $this->callGithubAPI($apiURL, $cache); } // return true if no update since the last time we asked. public function callGithubAPI($apiURL, &$cache=NULL) { //dbglog('ghissues: In callGithubAPI'); if ( !isset($cache) ) { $cache = new cache_ghissues_api($apiURL); } //dbglog('ghissues: Make HTTP Client'); $http = new DokuHTTPClient(); //dbglog('ghissues: Made HTTP Client'); $oauth_token = $this->getConf('ghissueoauth'); if ( !empty($oauth_token) ) { $http->user = $oauth_token; $http->pass = 'x-oauth-basic'; } $http->agent = substr($http->agent,0,-1).' via ghissue plugin from user '.$this->getConf('ghissueuser').')'; $http->headers['Accept'] = 'application/vnd.github.v3.text+json'; $http->keep_alive = FALSE; //dbglog('ghissues: Set Base Headers'); $lastETag = $cache->retrieveETag(); //dbglog('ghissues: Cache etag retrieval: '.$lastETag); if ( !empty($lastETag) ) { $http->headers['If-None-Match'] = $lastETag; } //dbglog('ghissues: Start request'); $apiResp = $http->get($apiURL); $apiHead = array(); //dbglog('ghissues: madeRequest'); $apiHead = $http->resp_headers; //dbglog('ghissues: '.$apiURL); //dbglog('ghissues: '.var_export($http->resp_headers, TRUE)); $this->_GH_API_limit = intval($apiHead['x-ratelimit-remaining']); //dbglog('ghissues: rateLimit='.$this->_GH_API_limit); $apiStatus = substr($apiHead['status'],0,3); //dbglog('ghissues: status='.$apiHead['status']); if ( $apiStatus == '304' ) { // No modification $cache->storeETag($apiHead['etag']); // Update the last time we checked return true; } else if ( $apiStatus == '200' ) { // Updated content! But will the table change? // Collate results if GitHub paginated them. (Walk the URL ladder) if ( !empty($apiHead['link']) ) { $nextLink = $apiHead['link']; $matches = array(); if(preg_match('/<(.*?)>; rel="next"/', $nextLink, $matches)) { $apiResp = substr($apiResp,0,-1); $apiResp .= $this->_chaseGithubNextLinks($http, $matches[1]); } }; // Build the actual table using the response, then make sure it has changed. // Because we don't use all information from the resopnse, it is possible only // things we don't check updated. If that is the case, no need to change the cache $newTable = $this->formatApiResponse($apiResp); if ( $newTable != $cache->retrieveCache() ) { if (!$cache->storeCache($newTable)) { //dbglog('ghissues: Unable to save cache file. Hint: disk full; file permissions; safe_mode setting.',-1); return true; // Couldn't save the update, can't reread from new cache } $cache->storeETag($apiHead['etag']); // Update the last time we checked return false; } $cache->storeETag($apiHead['etag']); // Update the last time we checked return true; // All that for a table that looks the same... } else { // Some other HTTP status, we're not handling. Save old table plus error message // Don't update when we checked in case it was a temporary thing (it probably wasn't though) // $cache->storeETag($apiHead['etag']); // Update the last time we checked $errorTable = '
'; $errorTable .= htmlentities(strftime($conf['dformat'])); $errorTable .= sprintf($this->getLang('badhttpstatus'), htmlentities($apiHead['status'])); $errorTable .= '
'."\n".$cache->retrieveCache(); if (!$cache->storeCache($errorTable)) { //dbglog('ghissues: Unable to save cache file. Hint: disk full; file permissions; safe_mode setting.',-1); return true; // Couldn't save the update, can't reread from new cache } return false; } // Fallback on true because we don't know what is going on. return true; } public function formatApiResponse($rawJSON) { //dbglog('ghissues: In formatApiResponse'); global $conf; if ($rawJSON == '[]') { $outputXML = '
'; $outputXML .= $this->getLang('noIssues'); $outputXML .= '
'."\n"; return $outputXML; } $json = new JSON(); $response = $json->decode($rawJSON); // Assume the top is already there, as that can be built without the API request. $outputXML = ''."\n"; return $outputXML; } public function getRenderedRequest($apiURL) { //dbglog('ghissues: In getRenderedRequest'); $outputCache = new cache_ghissues_api($apiURL); // Make sure we've got a good copy $this->checkCacheFreshness($apiURL, $outputCache); return $outputCache->retrieveCache(); } private function _getSpanClassFromBG($htmlbg) { $colorval = hexdec($htmlbg); $red = 0xFF & ($colorval >> 0x10); $green = 0xFF & ($colorval >> 0x08); $blue = 0xFF & $colorval; $lum = 1.0 - ( 0.299 * $red + 0.587 * $green + 0.114 * $blue)/255.0; if( $lum < 0.5 ) { return '"ghissues_light"'; } else { return '"ghissues_dark"'; } } private function _chaseGithubNextLinks(&$http, $apiURL) { //dbglog('ghissues: In _chaseGithubNextLinks'); $http->agent = substr($http->agent,0,-1).' via ghissue plugin from user '.$this->getConf('ghissueuser').')'; $http->headers['Accept'] = 'application/vnd.github.v3.text+json'; unset($http->headers['If-None-Match']); $apiNext = $http->get($apiURL); $apiHead = array(); $apiHead = $http->resp_headers; $this->_GH_API_limit = intval($apiHead['x-ratelimit-remaining']); $apiStatus = substr($apiHead['status'],0,3); // If request somehow failed, do it quietly since the first one didn't if ( $apiStatus != '200' ) return ']'; // If we're on the last page, there will be no "next" if ( !empty($apiHead['link']) ) { $nextLink = $apiHead['link']; $matches = array(); if(preg_match('/<(.*?)>; rel="next"/', $nextLink, $matches)) { return ','.substr($apiNext,1,-1).$this->_chaseGithubNextLinks($http, $matches[1]); } }; return ','.substr($apiNext,1); } } class cache_ghissues_api extends cache { public $etag = ''; var $_etag_time; public function cache_ghissues_api($requestURL) { parent::cache($requestURL,'.ghissues'); $this->etag = substr($this->cache, 0, -9).'.etag'; } public function retrieveETag($clean=true) { return io_readFile($this->etag, $clean); } public function storeETag($etagValue) { if ( $this->_nocache ) return false; return io_saveFile($this->etag, $etagValue); } // Sniff to see if it is rotten (expired). <0 means always OK, 0 is never ok. public function sniffETag($expireInterval) { if ( $expireInterval < 0 ) return true; if ( $expireInterval == 0 ) return false; if (!($this->_etag_time = @filemtime($this->etag))) return false; // Check if cache is there if ( (time() - $this->_etag_time) > $expireInterval ) return false; // Past Sell-By return true; } } // vim:ts=4:sw=4:et: