diff options
Diffstat (limited to 'p/api/greader.php')
| -rw-r--r-- | p/api/greader.php | 197 |
1 files changed, 98 insertions, 99 deletions
diff --git a/p/api/greader.php b/p/api/greader.php index dd3b78235..9769f66cb 100644 --- a/p/api/greader.php +++ b/p/api/greader.php @@ -28,8 +28,6 @@ Server-side API compatible with Google Reader API layer 2 require(__DIR__ . '/../../constants.php'); require(LIB_PATH . '/lib_rss.php'); //Includes class autoloader -$ORIGINAL_INPUT = file_get_contents('php://input', false, null, 0, 1048576) ?: ''; - if (PHP_INT_SIZE < 8) { //32-bit /** @return numeric-string */ function hex2dec(string $hex): string { @@ -53,13 +51,13 @@ const JSON_OPTIONS = JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE; function headerVariable(string $headerName, string $varName): string { $header = ''; $upName = 'HTTP_' . strtoupper($headerName); - if (isset($_SERVER[$upName])) { + if (is_string($_SERVER[$upName] ?? null)) { $header = '' . $_SERVER[$upName]; - } elseif (isset($_SERVER['REDIRECT_' . $upName])) { + } elseif (is_string($_SERVER['REDIRECT_' . $upName] ?? null)) { $header = '' . $_SERVER['REDIRECT_' . $upName]; } elseif (function_exists('getallheaders')) { $ALL_HEADERS = getallheaders(); - if (isset($ALL_HEADERS[$headerName])) { + if (is_string($ALL_HEADERS[$headerName] ?? null)) { $header = '' . $ALL_HEADERS[$headerName]; } } @@ -70,47 +68,47 @@ function headerVariable(string $headerName, string $varName): string { return is_string($pairs[$varName]) ? $pairs[$varName] : ''; } -/** @return array<string> */ -function multiplePosts(string $name): array { - //https://bugs.php.net/bug.php?id=51633 - global $ORIGINAL_INPUT; - $inputs = explode('&', $ORIGINAL_INPUT); - $result = []; - $prefix = $name . '='; - $prefixLength = strlen($prefix); - foreach ($inputs as $input) { - if (str_starts_with($input, $prefix)) { - $result[] = urldecode(substr($input, $prefixLength)); +final class GReaderAPI { + + private static string $ORIGINAL_INPUT = ''; + + /** @return list<string> */ + private static function multiplePosts(string $name): array { + //https://bugs.php.net/bug.php?id=51633 + $inputs = explode('&', self::$ORIGINAL_INPUT); + $result = []; + $prefix = $name . '='; + $prefixLength = strlen($prefix); + foreach ($inputs as $input) { + if (str_starts_with($input, $prefix)) { + $result[] = urldecode(substr($input, $prefixLength)); + } } + return $result; } - return $result; -} -function debugInfo(): string { - if (function_exists('getallheaders')) { - $ALL_HEADERS = getallheaders(); - } else { //nginx http://php.net/getallheaders#84262 - $ALL_HEADERS = []; - foreach ($_SERVER as $name => $value) { - if (str_starts_with($name, 'HTTP_')) { - $ALL_HEADERS[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value; + private static function debugInfo(): string { + if (function_exists('getallheaders')) { + $ALL_HEADERS = getallheaders(); + } else { //nginx http://php.net/getallheaders#84262 + $ALL_HEADERS = []; + foreach ($_SERVER as $name => $value) { + if (is_string($name) && str_starts_with($name, 'HTTP_')) { + $ALL_HEADERS[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value; + } } } + $log = sensitive_log([ + 'date' => date('c'), + 'headers' => $ALL_HEADERS, + '_SERVER' => $_SERVER, + '_GET' => $_GET, + '_POST' => $_POST, + '_COOKIE' => $_COOKIE, + 'INPUT' => self::$ORIGINAL_INPUT, + ]); + return print_r($log, true); } - global $ORIGINAL_INPUT; - $log = sensitive_log([ - 'date' => date('c'), - 'headers' => $ALL_HEADERS, - '_SERVER' => $_SERVER, - '_GET' => $_GET, - '_POST' => $_POST, - '_COOKIE' => $_COOKIE, - 'INPUT' => $ORIGINAL_INPUT, - ]); - return print_r($log, true); -} - -final class GReaderAPI { private static function noContent(): never { header('HTTP/1.1 204 No Content'); @@ -119,7 +117,7 @@ final class GReaderAPI { private static function badRequest(): never { Minz_Log::warning(__METHOD__, API_LOG); - Minz_Log::debug(__METHOD__ . ' ' . debugInfo(), API_LOG); + Minz_Log::debug(__METHOD__ . ' ' . self::debugInfo(), API_LOG); header('HTTP/1.1 400 Bad Request'); header('Content-Type: text/plain; charset=UTF-8'); die('Bad Request!'); @@ -127,7 +125,7 @@ final class GReaderAPI { private static function unauthorized(): never { Minz_Log::warning(__METHOD__, API_LOG); - Minz_Log::debug(__METHOD__ . ' ' . debugInfo(), API_LOG); + Minz_Log::debug(__METHOD__ . ' ' . self::debugInfo(), API_LOG); header('HTTP/1.1 401 Unauthorized'); header('Content-Type: text/plain; charset=UTF-8'); header('Google-Bad-Token: true'); @@ -136,7 +134,7 @@ final class GReaderAPI { private static function internalServerError(): never { Minz_Log::warning(__METHOD__, API_LOG); - Minz_Log::debug(__METHOD__ . ' ' . debugInfo(), API_LOG); + Minz_Log::debug(__METHOD__ . ' ' . self::debugInfo(), API_LOG); header('HTTP/1.1 500 Internal Server Error'); header('Content-Type: text/plain; charset=UTF-8'); die('Internal Server Error!'); @@ -144,7 +142,7 @@ final class GReaderAPI { private static function notImplemented(): never { Minz_Log::warning(__METHOD__, API_LOG); - Minz_Log::debug(__METHOD__ . ' ' . debugInfo(), API_LOG); + Minz_Log::debug(__METHOD__ . ' ' . self::debugInfo(), API_LOG); header('HTTP/1.1 501 Not Implemented'); header('Content-Type: text/plain; charset=UTF-8'); die('Not Implemented!'); @@ -152,7 +150,7 @@ final class GReaderAPI { private static function serviceUnavailable(): never { Minz_Log::warning(__METHOD__, API_LOG); - Minz_Log::debug(__METHOD__ . ' ' . debugInfo(), API_LOG); + Minz_Log::debug(__METHOD__ . ' ' . self::debugInfo(), API_LOG); header('HTTP/1.1 503 Service Unavailable'); header('Content-Type: text/plain; charset=UTF-8'); die('Service Unavailable!'); @@ -160,7 +158,7 @@ final class GReaderAPI { private static function checkCompatibility(): never { Minz_Log::warning(__METHOD__, API_LOG); - Minz_Log::debug(__METHOD__ . ' ' . debugInfo(), API_LOG); + Minz_Log::debug(__METHOD__ . ' ' . self::debugInfo(), API_LOG); header('Content-Type: text/plain; charset=UTF-8'); if (PHP_INT_SIZE < 8 && !function_exists('gmp_init')) { die('FAIL 64-bit or GMP extension! Wrong PHP configuration.'); @@ -365,9 +363,9 @@ final class GReaderAPI { } /** - * @param array<string> $streamNames StreamId(s) to operate on. The parameter may be repeated to edit multiple subscriptions at once - * @param array<string> $titles Title(s) to use for the subscription(s). Each title is associated with the corresponding streamName - * @param 'subscribe'|'unsubscribe'|'edit' $action + * @param list<string> $streamNames StreamId(s) to operate on. The parameter may be repeated to edit multiple subscriptions at once + * @param list<string> $titles Title(s) to use for the subscription(s). Each title is associated with the corresponding streamName + * @param string $action 'subscribe'|'unsubscribe'|'edit' * @param string $add StreamId to add the subscription(s) to (generally a category) * @param string $remove StreamId to remove the subscription(s) from (generally a category) */ @@ -544,8 +542,8 @@ final class GReaderAPI { } /** - * @param array<FreshRSS_Entry> $entries - * @return array<array<string,mixed>> + * @param list<FreshRSS_Entry> $entries + * @return list<array<string,mixed>> */ private static function entriesToArray(array $entries): array { if (empty($entries)) { @@ -668,7 +666,7 @@ final class GReaderAPI { $entryDAO = FreshRSS_Factory::createEntryDao(); $entries = $entryDAO->listWhere($type, $include_target, $state, $order === 'o' ? 'ASC' : 'DESC', $count, 0, $continuation, $searches); - $entries = iterator_to_array($entries); //TODO: Improve + $entries = array_values(iterator_to_array($entries)); //TODO: Improve $items = self::entriesToArray($entries); @@ -754,7 +752,7 @@ final class GReaderAPI { } /** - * @param array<string> $e_ids + * @param list<string> $e_ids */ private static function streamContentsItems(array $e_ids, string $order): never { header('Content-Type: application/json; charset=UTF-8'); @@ -765,11 +763,11 @@ final class GReaderAPI { $e_ids[$i] = hex2dec(basename($e_id)); //Strip prefix 'tag:google.com,2005:reader/item/' } } - /** @var array<numeric-string> $e_ids */ + /** @var list<numeric-string> $e_ids */ $entryDAO = FreshRSS_Factory::createEntryDao(); $entries = $entryDAO->listByIds($e_ids, $order === 'o' ? 'ASC' : 'DESC'); - $entries = iterator_to_array($entries); //TODO: Improve + $entries = array_values(iterator_to_array($entries)); //TODO: Improve $items = self::entriesToArray($entries); @@ -785,9 +783,9 @@ final class GReaderAPI { } /** - * @param array<string> $e_ids IDs of the items to edit - * @param array<string> $as tags to add to all the listed items - * @param array<string> $rs tags to remove from all the listed items + * @param list<string> $e_ids IDs of the items to edit + * @param list<string> $as tags to add to all the listed items + * @param list<string> $rs tags to remove from all the listed items */ private static function editTag(array $e_ids, array $as, array $rs): never { foreach ($e_ids as $i => $e_id) { @@ -795,7 +793,7 @@ final class GReaderAPI { $e_ids[$i] = hex2dec(basename($e_id)); //Strip prefix 'tag:google.com,2005:reader/item/' } } - /** @var array<numeric-string> $e_ids */ + /** @var list<numeric-string> $e_ids */ $entryDAO = FreshRSS_Factory::createEntryDao(); $tagDAO = FreshRSS_Factory::createTagDao(); @@ -960,8 +958,6 @@ final class GReaderAPI { } public static function parse(): never { - global $ORIGINAL_INPUT; - header('Access-Control-Allow-Headers: Authorization'); header('Access-Control-Allow-Methods: GET, POST'); header('Access-Control-Allow-Origin: *'); @@ -971,8 +967,8 @@ final class GReaderAPI { } $pathInfo = ''; - if (empty($_SERVER['PATH_INFO'])) { - if (!empty($_SERVER['ORIG_PATH_INFO'])) { + if (empty($_SERVER['PATH_INFO']) || !is_string($_SERVER['PATH_INFO'])) { + if (!empty($_SERVER['ORIG_PATH_INFO']) && is_string($_SERVER['ORIG_PATH_INFO'])) { // Compatibility https://php.net/reserved.variables.server $pathInfo = $_SERVER['ORIG_PATH_INFO']; } @@ -992,7 +988,7 @@ final class GReaderAPI { FreshRSS_Context::initSystem(); //Minz_Log::debug('----------------------------------------------------------------', API_LOG); - //Minz_Log::debug(debugInfo(), API_LOG); + //Minz_Log::debug(self::debugInfo(), API_LOG); if (!FreshRSS_Context::hasSystemConf() || !FreshRSS_Context::systemConf()->api_enabled) { self::serviceUnavailable(); @@ -1013,15 +1009,18 @@ final class GReaderAPI { Minz_Translate::init(); } + self::$ORIGINAL_INPUT = file_get_contents('php://input', false, null, 0, 1048576) ?: ''; + if ($pathInfos[1] === 'accounts') { - if (($pathInfos[2] === 'ClientLogin') && isset($_REQUEST['Email']) && isset($_REQUEST['Passwd'])) { + if (($pathInfos[2] === 'ClientLogin') && is_string($_REQUEST['Email'] ?? null) && is_string($_REQUEST['Passwd'] ?? null)) { self::clientLogin($_REQUEST['Email'], $_REQUEST['Passwd']); } } elseif (isset($pathInfos[3], $pathInfos[4]) && $pathInfos[1] === 'reader' && $pathInfos[2] === 'api' && $pathInfos[3] === '0') { if (Minz_User::name() === null) { self::unauthorized(); } - $timestamp = isset($_GET['ck']) ? (int)$_GET['ck'] : 0; //ck=[unix timestamp] : Use the current Unix time here, helps Google with caching. + // ck=[unix timestamp]: Use the current Unix time here, helps Google with caching + $timestamp = is_numeric($_GET['ck'] ?? null) ? (int)$_GET['ck'] : 0; switch ($pathInfos[4]) { case 'stream': /** @@ -1031,41 +1030,41 @@ final class GReaderAPI { * exclude items from a particular feed (obviously not useful in this request, * but xt appears in other listing requests). */ - $exclude_target = $_GET['xt'] ?? ''; - $filter_target = $_GET['it'] ?? ''; + $exclude_target = is_string($_GET['xt'] ?? null) ? $_GET['xt'] : ''; + $filter_target = is_string($_GET['it'] ?? null) ? $_GET['it'] : ''; //n=[integer] : The maximum number of results to return. - $count = isset($_GET['n']) ? (int)$_GET['n'] : 20; + $count = is_numeric($_GET['n'] ?? null) ? (int)$_GET['n'] : 20; //r=[d|n|o] : Sort order of item results. d or n gives items in descending date order, o in ascending order. - $order = $_GET['r'] ?? 'd'; + $order = is_string($_GET['r'] ?? null) ? $_GET['r'] : 'd'; /** * ot=[unix timestamp] : The time from which you want to retrieve items. * Only items that have been crawled by Google Reader after this time will be returned. */ - $start_time = isset($_GET['ot']) ? (int)$_GET['ot'] : 0; - $stop_time = isset($_GET['nt']) ? (int)$_GET['nt'] : 0; + $start_time = is_numeric($_GET['ot'] ?? null) ? (int)$_GET['ot'] : 0; + $stop_time = is_numeric($_GET['nt'] ?? null) ? (int)$_GET['nt'] : 0; /** * Continuation token. If a StreamContents response does not represent * all items in a timestamp range, it will have a continuation attribute. * The same request can be re-issued with the value of that attribute put * in this parameter to get more items */ - $continuation = isset($_GET['c']) ? trim((string)$_GET['c']) : ''; + $continuation = is_string($_GET['c'] ?? null) ? trim($_GET['c']) : ''; if (!ctype_digit($continuation)) { $continuation = ''; } if (isset($pathInfos[5]) && $pathInfos[5] === 'contents') { - if (!isset($pathInfos[6]) && isset($_GET['s'])) { + if (!isset($pathInfos[6]) && is_string($_GET['s'] ?? null)) { // Compatibility BazQux API https://github.com/bazqux/bazqux-api#fetching-streams $streamIdInfos = explode('/', $_GET['s']); foreach ($streamIdInfos as $streamIdInfo) { $pathInfos[] = $streamIdInfo; } } - if (isset($pathInfos[6]) && isset($pathInfos[7])) { + if (isset($pathInfos[6], $pathInfos[7])) { if ($pathInfos[6] === 'feed') { $include_target = $pathInfos[7]; - if ($include_target != '' && !is_numeric($include_target)) { - $include_target = empty($_SERVER['REQUEST_URI']) ? '' : $_SERVER['REQUEST_URI']; + if ($include_target !== '' && !is_numeric($include_target)) { + $include_target = empty($_SERVER['REQUEST_URI']) || !is_string($_SERVER['REQUEST_URI']) ? '' : $_SERVER['REQUEST_URI']; if (preg_match('#/reader/api/0/stream/contents/feed/([A-Za-z0-9\'!*()%$_.~+-]+)#', $include_target, $matches) === 1) { $include_target = urldecode($matches[1]); } else { @@ -1095,13 +1094,13 @@ final class GReaderAPI { $count, $order, $filter_target, $exclude_target, $continuation); } } elseif ($pathInfos[5] === 'items') { - if ($pathInfos[6] === 'ids' && isset($_GET['s'])) { + if ($pathInfos[6] === 'ids' && is_string($_GET['s'] ?? null)) { // StreamId for which to fetch the item IDs. // TODO: support multiple streams $streamId = $_GET['s']; self::streamContentsItemsIds($streamId, $start_time, $stop_time, $count, $order, $filter_target, $exclude_target, $continuation); } elseif ($pathInfos[6] === 'contents' && isset($_POST['i'])) { //FeedMe - $e_ids = multiplePosts('i'); //item IDs + $e_ids = self::multiplePosts('i'); //item IDs self::streamContentsItems($e_ids, $order); } } @@ -1120,8 +1119,8 @@ final class GReaderAPI { self::subscriptionExport(); // Always exits case 'import': - if (isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'POST' && $ORIGINAL_INPUT != '') { - self::subscriptionImport($ORIGINAL_INPUT); + if (isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'POST' && self::$ORIGINAL_INPUT != '') { + self::subscriptionImport(self::$ORIGINAL_INPUT); } break; case 'list': @@ -1132,23 +1131,23 @@ final class GReaderAPI { case 'edit': if (isset($_REQUEST['s'], $_REQUEST['ac'])) { // StreamId to operate on. The parameter may be repeated to edit multiple subscriptions at once - $streamNames = empty($_POST['s']) && isset($_GET['s']) ? [$_GET['s']] : multiplePosts('s'); + $streamNames = empty($_POST['s']) && is_string($_GET['s'] ?? null) ? [$_GET['s']] : self::multiplePosts('s'); /* Title to use for the subscription. For the `subscribe` action, * if not specified then the feed’s current title will be used. Can * be used with the `edit` action to rename a subscription */ - $titles = empty($_POST['t']) && isset($_GET['t']) ? [$_GET['t']] : multiplePosts('t'); + $titles = empty($_POST['t']) && is_string($_GET['t'] ?? null) ? [$_GET['t']] : self::multiplePosts('t'); // Action to perform on the given StreamId. Possible values are `subscribe`, `unsubscribe` and `edit` - $action = $_REQUEST['ac']; + $action = is_string($_REQUEST['ac'] ?? null) ? $_REQUEST['ac'] : ''; // StreamId to add the subscription to (generally a user label) // (in FreshRSS, we do not support repeated values since a feed can only be in one category) - $add = $_REQUEST['a'] ?? ''; + $add = is_string($_REQUEST['a'] ?? null) ? $_REQUEST['a'] : ''; // StreamId to remove the subscription from (generally a user label) (in FreshRSS, we do not support repeated values) - $remove = $_REQUEST['r'] ?? ''; + $remove = is_string($_REQUEST['r'] ?? null) ? $_REQUEST['r'] : ''; self::subscriptionEdit($streamNames, $titles, $action, $add, $remove); } break; case 'quickadd': //https://github.com/theoldreader/api - if (isset($_REQUEST['quickadd'])) { + if (is_string($_REQUEST['quickadd'] ?? null)) { self::quickadd($_REQUEST['quickadd']); } break; @@ -1161,35 +1160,35 @@ final class GReaderAPI { self::unreadCount(); // Always exits case 'edit-tag': // https://web.archive.org/web/20200616071132/https://blog.martindoms.com/2010/01/20/using-the-google-reader-api-part-3 - $token = isset($_POST['T']) ? trim($_POST['T']) : ''; + $token = is_string($_POST['T'] ?? null) ? trim($_POST['T']) : ''; self::checkToken(FreshRSS_Context::userConf(), $token); // Add (Can be repeated to add multiple tags at once): user/-/state/com.google/read user/-/state/com.google/starred - $as = multiplePosts('a'); + $as = self::multiplePosts('a'); // Remove (Can be repeated to remove multiple tags at once): user/-/state/com.google/read user/-/state/com.google/starred - $rs = multiplePosts('r'); - $e_ids = multiplePosts('i'); //item IDs + $rs = self::multiplePosts('r'); + $e_ids = self::multiplePosts('i'); //item IDs self::editTag($e_ids, $as, $rs); // Always exits case 'rename-tag': //https://github.com/theoldreader/api - $token = isset($_POST['T']) ? trim($_POST['T']) : ''; + $token = is_string($_POST['T'] ?? null) ? trim($_POST['T']) : ''; self::checkToken(FreshRSS_Context::userConf(), $token); - $s = $_POST['s'] ?? ''; //user/-/label/Folder - $dest = $_POST['dest'] ?? ''; //user/-/label/NewFolder + $s = is_string($_POST['s'] ?? null) ? trim($_POST['s']) : ''; //user/-/label/Folder + $dest = is_string($_POST['dest'] ?? null) ? trim($_POST['dest']) : ''; //user/-/label/NewFolder self::renameTag($s, $dest); // Always exits case 'disable-tag': //https://github.com/theoldreader/api - $token = isset($_POST['T']) ? trim($_POST['T']) : ''; + $token = is_string($_POST['T'] ?? null) ? trim($_POST['T']) : ''; self::checkToken(FreshRSS_Context::userConf(), $token); - $s_s = multiplePosts('s'); + $s_s = self::multiplePosts('s'); foreach ($s_s as $s) { self::disableTag($s); //user/-/label/Folder } // Always exits case 'mark-all-as-read': - $token = isset($_POST['T']) ? trim($_POST['T']) : ''; + $token = is_string($_POST['T'] ?? null) ? trim($_POST['T']) : ''; self::checkToken(FreshRSS_Context::userConf(), $token); - $streamId = trim($_POST['s'] ?? ''); - $ts = trim($_POST['ts'] ?? '0'); //Older than timestamp in nanoseconds + $streamId = is_string($_POST['s'] ?? null) ? trim($_POST['s']) : ''; + $ts = is_string($_POST['ts'] ?? null) ? trim($_POST['ts']) : '0'; //Older than timestamp in nanoseconds if (!ctype_digit($ts)) { self::badRequest(); } |
