diff --git a/ActivityPubPlugin.php b/ActivityPubPlugin.php index 36a1596..7d2136c 100755 --- a/ActivityPubPlugin.php +++ b/ActivityPubPlugin.php @@ -35,6 +35,7 @@ date_default_timezone_set('UTC'); // Import required files by the plugin require_once __DIR__ . DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR . 'autoload.php'; require_once __DIR__ . DIRECTORY_SEPARATOR . 'utils' . DIRECTORY_SEPARATOR . 'discoveryhints.php'; +require_once __DIR__ . DIRECTORY_SEPARATOR . 'utils' . DIRECTORY_SEPARATOR . 'AcceptHeader.php'; require_once __DIR__ . DIRECTORY_SEPARATOR . 'utils' . DIRECTORY_SEPARATOR . 'explorer.php'; require_once __DIR__ . DIRECTORY_SEPARATOR . 'utils' . DIRECTORY_SEPARATOR . 'postman.php'; @@ -163,27 +164,29 @@ class ActivityPubPlugin extends Plugin */ public function onRouterInitialized(URLMapper $m) { - ActivityPubURLMapperOverwrite::variable( - $m, - 'user/:id', - ['id' => '[0-9]+'], - 'apActorProfile' - ); + if (ActivityPubURLMapperOverwrite::should()) { + ActivityPubURLMapperOverwrite::variable( + $m, + 'user/:id', + ['id' => '[0-9]+'], + 'apActorProfile' + ); - // Special route for webfinger purposes - ActivityPubURLMapperOverwrite::variable( - $m, - ':nickname', - ['nickname' => Nickname::DISPLAY_FMT], - 'apActorProfile' - ); + // Special route for webfinger purposes + ActivityPubURLMapperOverwrite::variable( + $m, + ':nickname', + ['nickname' => Nickname::DISPLAY_FMT], + 'apActorProfile' + ); - ActivityPubURLMapperOverwrite::variable( - $m, - 'notice/:id', - ['id' => '[0-9]+'], - 'apNotice' - ); + ActivityPubURLMapperOverwrite::variable( + $m, + 'notice/:id', + ['id' => '[0-9]+'], + 'apNotice' + ); + } $m->connect( 'user/:id/liked.json', @@ -944,59 +947,6 @@ class ActivityPubReturn echo json_encode($res, JSON_UNESCAPED_SLASHES); exit; } - - /** - * Select content type from HTTP Accept header - * - * @author Maciej Łebkowski - * @param array $mimeTypes Supported Types - * @return array|null of supported mime types sorted | null if none valid - */ - public static function getBestSupportedMimeType($mimeTypes) - { - // XXX: This function needs improvement! - if (!isset($_SERVER['HTTP_ACCEPT'])) { - return null; - } - - // This mime type was messing everything, thus the special case - if ($_SERVER['HTTP_ACCEPT'] == 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"') { - return true; - } - - // Values will be stored in this array - $AcceptTypes = []; - - // Accept header is case insensitive, and whitespace isn’t important - $accept = strtolower(str_replace(' ', '', $_SERVER['HTTP_ACCEPT'])); - // divide it into parts in the place of a "," - $accept = explode(',', $accept); - foreach ($accept as $a) { - // the default quality is 1. - $q = 1; - // check if there is a different quality - if (strpos($a, ';q=')) { - // divide "mime/type;q=X" into two parts: "mime/type" and "X" - list($a, $q) = explode(';q=', $a); - } - // mime-type $a is accepted with the quality $q - // WARNING: $q == 0 means, that mime-type isn’t supported! - $AcceptTypes[$a] = $q; - } - arsort($AcceptTypes); - - $mimeTypes = array_map('strtolower', $mimeTypes); - - // let’s check our supported types: - foreach ($AcceptTypes as $mime => $q) { - if ($q && in_array($mime, $mimeTypes)) { - return $mime; - } - } - - // no mime-type found - return null; - } } /** @@ -1004,19 +954,18 @@ class ActivityPubReturn */ class ActivityPubURLMapperOverwrite extends URLMapper { + /** + * Overwrites a route. + * + * @author Hannes Mannerheim + * @param URLMapper $m + * @param string $path + * @param string $paramPatterns + * @param string $newaction + * @return void + */ public static function variable($m, $path, $paramPatterns, $newaction) { - $mimes = [ - 'application/json', - 'application/activity+json', - 'application/ld+json', - 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' - ]; - - if (is_null(ActivityPubReturn::getBestSupportedMimeType($mimes))) { - return true; - } - $m->connect($path, array('action' => $newaction), $paramPatterns); $regex = self::makeRegex($path, $paramPatterns); foreach ($m->variables as $n => $v) { @@ -1025,4 +974,35 @@ class ActivityPubURLMapperOverwrite extends URLMapper } } } + + /** + * Determines whether the route should or not be overwrited. + * If ACCEPT header isn't set false will be returned. + * + * @author Diogo Cordeiro + * @return boolean true if it should, false otherwise + */ + public static function should() + { + // Do not operate without Accept Header + if (!isset($_SERVER['HTTP_ACCEPT'])) { + return false; + } + + $mimes = [ + 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' => 0, + 'application/activity+json' => 1, + 'application/json' => 2, + 'application/ld+json' => 3 + ]; + + $acceptheader = new AcceptHeader($_SERVER['HTTP_ACCEPT']); + foreach ($acceptheader as $ah) { + if (isset($mimes[$ah['raw']])) { + return true; + } + } + + return false; + } } diff --git a/actions/inbox/Create.php b/actions/inbox/Create.php index 4375489..b3af912 100755 --- a/actions/inbox/Create.php +++ b/actions/inbox/Create.php @@ -65,7 +65,7 @@ try { } catch (AlreadyFulfilledException $e) { // Notice URI already exists common_debug('ActivityPub Inbox Create Note: Note already exists: '.$e->getMessage()); - ActivityPubReturn::error('Note already exists.',202); + ActivityPubReturn::error('Note already exists.', 202); } catch (Exception $e) { common_debug('ActivityPub Inbox Create Note: Failed Create Note: '.$e->getMessage()); ActivityPubReturn::error($e->getMessage()); diff --git a/tests/Unit/AcceptHeaderTest.php b/tests/Unit/AcceptHeaderTest.php new file mode 100644 index 0000000..c2c4700 --- /dev/null +++ b/tests/Unit/AcceptHeaderTest.php @@ -0,0 +1,43 @@ +assertEquals('audio/basic', $this->_getMedia($acceptHeader[0])); + $this->assertEquals('audio/*; q=0.2', $this->_getMedia($acceptHeader[1])); + } + + public function testHeader2() + { + $acceptHeader = new AcceptHeader('text/*;q=0.3, text/html;q=0.7, text/html;level=1, text/html;level=2;q=0.4, */*;q=0.5'); + $this->assertEquals('text/html; level=1', $this->_getMedia($acceptHeader[0])); + $this->assertEquals('text/html; q=0.7', $this->_getMedia($acceptHeader[1])); + $this->assertEquals('*/*; q=0.5', $this->_getMedia($acceptHeader[2])); + $this->assertEquals('text/html; level=2; q=0.4', $this->_getMedia($acceptHeader[3])); + $this->assertEquals('text/*; q=0.3', $this->_getMedia($acceptHeader[4])); + } + + public function testHeader3() + { + $acceptHeader = new AcceptHeader('text/*, text/html, text/html;level=1, */*'); + $this->assertEquals('text/html; level=1', $this->_getMedia($acceptHeader[0])); + $this->assertEquals('text/html', $this->_getMedia($acceptHeader[1])); + $this->assertEquals('text/*', $this->_getMedia($acceptHeader[2])); + $this->assertEquals('*/*', $this->_getMedia($acceptHeader[3])); + } + + private function _getMedia(array $mediaType) + { + $str = $mediaType['type'] . '/' . $mediaType['subtype']; + if (!empty($mediaType['params'])) { + foreach ($mediaType['params'] as $k => $v) { + $str .= '; ' . $k . '=' . $v; + } + } + return $str; + } +} diff --git a/utils/AcceptHeader.php b/utils/AcceptHeader.php new file mode 100644 index 0000000..553b900 --- /dev/null +++ b/utils/AcceptHeader.php @@ -0,0 +1,116 @@ + + */ +class AcceptHeader extends \ArrayObject +{ + /** + * Constructor + * + * @param string $header Value of the Accept header + * @return void + */ + public function __construct($header) + { + $acceptedTypes = $this->_parse($header); + usort($acceptedTypes, [$this, '_compare']); + parent::__construct($acceptedTypes); + } + + /** + * Parse the accept header and return an array containing + * all the informations about the Accepted types + * + * @param string $header Value of the Accept header + * @return array + */ + private function _parse($data) + { + $array = []; + $items = explode(',', $data); + foreach ($items as $item) { + $elems = explode(';', $item); + + $acceptElement = []; + $mime = current($elems); + list($type, $subtype) = explode('/', $mime); + $acceptElement['type'] = trim($type); + $acceptElement['subtype'] = trim($subtype); + $acceptElement['raw'] = $mime; + + $acceptElement['params'] = []; + while (next($elems)) { + list($name, $value) = explode('=', current($elems)); + $acceptElement['params'][trim($name)] = trim($value); + } + + $array[] = $acceptElement; + } + return $array; + } + + /** + * Compare two Accepted types with their parameters to know + * if one media type should be used instead of an other + * + * @param array $a The first media type and its parameters + * @param array $b The second media type and its parameters + * @return int + */ + private function _compare($a, $b) + { + $a_q = isset($a['params']['q']) ? floatval($a['params']['q']) : 1.0; + $b_q = isset($b['params']['q']) ? floatval($b['params']['q']) : 1.0; + if ($a_q === $b_q) { + $a_count = count($a['params']); + $b_count = count($b['params']); + if ($a_count === $b_count) { + if ($r = $this->_compareSubType($a['subtype'], $b['subtype'])) { + return $r; + } else { + return $this->_compareSubType($a['type'], $b['type']); + } + } else { + return $a_count < $b_count; + } + } else { + return $a_q < $b_q; + } + } + + /** + * Compare two subtypes + * + * @param string $a First subtype to compare + * @param string $b Second subtype to compare + * @return int + */ + private function _compareSubType($a, $b) + { + if ($a === '*' && $b !== '*') { + return 1; + } elseif ($b === '*' && $a !== '*') { + return -1; + } else { + return 0; + } + } +}