Changeset 3273810
- Timestamp:
- 04/15/2025 05:37:56 PM (12 months ago)
- Location:
- apocalypse-meow
- Files:
-
- 12 edited
-
assets/banner-1544x500.png (modified) (previous)
-
assets/banner-772x250.png (modified) (previous)
-
assets/icon-128x128.png (modified) (previous)
-
assets/icon-256x256.png (modified) (previous)
-
assets/screenshot-1.png (modified) (previous)
-
assets/screenshot-2.png (modified) (previous)
-
assets/screenshot-4.png (modified) (previous)
-
assets/screenshot-5.png (modified) (previous)
-
trunk/index.php (modified) (2 diffs)
-
trunk/lib/vendor/blobfolio/blob-common/lib/blobfolio/common/dom.php (modified) (1 diff)
-
trunk/lib/vendor/erusev/parsedown/Parsedown.php (modified) (2 diffs)
-
trunk/readme.txt (modified) (4 diffs)
Legend:
- Unmodified
- Added
- Removed
-
apocalypse-meow/trunk/index.php
r3239036 r3273810 4 4 * 5 5 * @package Apocalypse Meow 6 * @version 22.0. 06 * @version 22.0.1 7 7 * 8 8 * @wordpress-plugin 9 9 * Plugin Name: Apocalypse Meow 10 * Version: 22.0. 010 * Version: 22.0.1 11 11 * Plugin URI: https://wordpress.org/plugins/apocalypse-meow/ 12 12 * Description: A simple, light-weight collection of tools to harden WordPress security and help mitigate common types of attacks. … … 33 33 34 34 // Constants. 35 define('MEOW_VERSION', '22.0. 0');35 define('MEOW_VERSION', '22.0.1'); 36 36 define('MEOW_MIN_PHP', '7.2.0'); 37 37 define('MEOW_PLUGIN_DIR', __DIR__ . '/'); -
apocalypse-meow/trunk/lib/vendor/blobfolio/blob-common/lib/blobfolio/common/dom.php
r2517177 r3273810 1 1 <?php 2 namespace blobfolio\wp\meow\vendor\common; class dom { public static function load_svg($svg='') { ref\cast::string($svg, true); $svg = \preg_replace( array( '/<svg/ui', '/<\/svg>/ui', ), array( '<svg', '</svg>', ), $svg ); if ( (false === ($start = mb::strpos($svg, '<svg'))) || (false === ($end = mb::strrpos($svg, '</svg>'))) ) { return false; } $svg = mb::substr($svg, $start, ($end - $start + 6)); $svg = \str_replace( \array_keys(constants::SVG_ATTR_CORRECTIONS), \array_values(constants::SVG_ATTR_CORRECTIONS), $svg ); $svg = \preg_replace( array( '/<\?(.*)\?>/Us', '/<\%(.*)\%>/Us', '/<!--(.*)-->/Us', '/\/\*(.*)\*\//Us', ), '', $svg ); if ( (false !== \strpos($svg, '<?')) || (false !== \strpos($svg, '<%')) || (false !== \strpos($svg, '<!--')) || (false !== \strpos($svg, '/*')) ) { return null; } \libxml_use_internal_errors(true); if (\PHP_VERSION_ID < 80000) { \libxml_disable_entity_loader(true); } $dom = new \DOMDocument('1.0', 'UTF-8'); $dom->formatOutput = false; $dom->preserveWhiteSpace = false; $dom->loadXML(constants::SVG_HEADER . "\n{$svg}"); $svgs = $dom->getElementsByTagName('svg'); if (! $svgs->length) { return false; } return $dom; } public static function save_svg(\DOMDocument $dom) { $svgs = $dom->getElementsByTagName('svg'); if (! $svgs->length) { return ''; } $svg = $svgs->item(0)->ownerDocument->saveXML( $svgs->item(0), \LIBXML_NOBLANKS ); $svg = \preg_replace( '/xmlns\s*=\s*"[^"]*"/', 'xmlns="' . constants::SVG_NAMESPACE . '"', $svg ); $svg = \preg_replace( array( '/<\?(.*)\?>/Us', '/<\%(.*)\%>/Us', '/<!--(.*)-->/Us', '/\/\*(.*)\*\//Us', ), '', $svg ); if ( (false !== \strpos($svg, '<?')) || (false !== \strpos($svg, '<%')) || (false !== \strpos($svg, '<!--')) || (false !== \strpos($svg, '/*')) ) { return null; } if ( (false === ($start = mb::strpos($svg, '<svg'))) || (false === ($end = mb::strrpos($svg, '</svg>'))) ) { return false; } $svg = mb::substr($svg, $start, ($end - $start + 6)); return $svg; } public static function get_nodes_by_class($parent, $class=null, bool $all=false) { $nodes = array(); if (! \method_exists($parent, 'getElementsByTagName')) { return $nodes; } ref\cast::array($class); foreach ($class as $k=>$v) { $class[$k] = \ltrim(\trim($class[$k]), '.'); if (! $class[$k]) { unset($class[$k]); } } if (empty($class)) { return $nodes; } $class = \array_unique($class); \sort($class); $class_length = \count($class); $possible = $parent->getElementsByTagName('*'); if ($possible->length) { foreach ($possible as $child) { if ($child->hasAttribute('class')) { $classes = $child->getAttribute('class'); ref\sanitize::whitespace($classes); $classes = \explode(' ', $classes); $overlap = \array_intersect($classes, $class); $count = \count($overlap); if ($count && (! $all || $count === $class_length)) { $nodes[] = $child; } } } } return $nodes; } public static function innerhtml($node, bool $xml=false, int $flags=null) { if ( ! \is_a($node, 'DOMElement') && ! \is_a($node, 'DOMNode') ) { return ''; } $content = ''; $children = $node->childNodes; if ($children->length) { if ($xml) { foreach ($children as $child) { if ($flags) { $content .= $node->ownerDocument->saveXML($child, $flags); } else { $content .= $node->ownerDocument->saveXML($child); } } } else { foreach ($children as $child) { $content .= $node->ownerDocument->saveHTML($child); } } } return $content; } public static function parse_css($styles='') { ref\cast::string($styles, true); while (false !== ($start = mb::strpos($styles, '/*'))) { if (false !== ($end = mb::strpos($styles, '*/'))) { $styles = \str_replace( mb::substr($styles, $start, ($end - $start + 2)), '', $styles ); } else { $styles = mb::substr($styles, 0, $start); } } $styles = \str_replace( array('<!--', '//-->', '-->', '//<![CDATA[', '//]]>', '<![CDATA[', ']]>'), '', $styles ); ref\sanitize::quotes($styles); $styles = \str_replace("'", '"', $styles); ref\sanitize::whitespace($styles, 0); if (! $styles) { return array(); } $styles = \preg_replace( array( '/\{(?![^"]*"(?:(?:[^"]*"){2})*[^"]*$)/u', '/\}(?![^"]*"(?:(?:[^"]*"){2})*[^"]*$)/u', '/\s*(\()\s*(?![^"]*"(?:(?:[^"]*"){2})*[^"]*$)/u', '/\s*(\))\s*(?![^"]*"(?:(?:[^"]*"){2})*[^"]*$)/u', '/\s*(⠁|⠈|@)\s*/u', ), array( '⠁', '⠈', ' (', ') ', '$1', ), $styles); $styles = \str_replace('@', "\n@", $styles); $styles = \explode("\n", $styles); $styles = \array_map('trim', $styles); foreach ($styles as $k=>$v) { if (0 === \strpos($styles[$k], '@')) { if (false !== \strpos($styles[$k], '⠈⠈')) { $styles[$k] = \preg_replace('/(⠈{2,})/u', "$1\n", $styles[$k]); } elseif (false !== \strpos($styles[$k], '⠈')) { $styles[$k] = \str_replace('⠈', "⠈\n", $styles[$k]); } elseif (\preg_match('/;(?![^"]*"(?:(?:[^"]*"){2})*[^"]*$)/', $styles[$k])) { $styles[$k] = \preg_replace( '/;(?![^"]*"(?:(?:[^"]*"){2})*[^"]*$)/u', ";\n", $styles[$k], 1 ); } $tmp = \explode("\n", $styles[$k]); $length = \count($tmp); for ($x = 1; $x < $length; ++$x) { $tmp[$x] = \str_replace('⠈', "⠈\n", $tmp[$x]); } $styles[$k] = \implode("\n", $tmp); } else { $styles[$k] = \str_replace('⠈', "⠈\n", $styles[$k]); } } $styles = \implode("\n", $styles); $styles = \preg_replace( array( '/\)\s(,|;)(?![^"]*"(?:(?:[^"]*"){2})*[^"]*$)/u', '/(url|rgba?)\s+\(/', ), array( ')$1', '$1(', ), $styles ); $styles = \explode("\n", $styles); $styles = \array_filter($styles, 'strlen'); $out = array(); foreach ($styles as $k=>$v) { $styles[$k] = \trim($styles[$k]); if ( (0 === \strpos($styles[$k], '@')) && (false !== \strpos($styles[$k], '⠈⠈')) ) { $tmp = constants::CSS_NESTED; \preg_match_all('/^@([a-z\-]+)/ui', $styles[$k], $matches); $tmp['@'] = mb::strtolower($matches[1][0]); if (false === ($start = mb::strpos($styles[$k], '⠁'))) { continue; } $tmp['selector'] = mb::strtolower( \trim( mb::substr($styles[$k], 0, $start) ), false, true ); $chunk = mb::substr($styles[$k], $start + 1, -1); $chunk = \str_replace(array('⠁', '⠈'), array('{', '}'), $chunk); $tmp['nest'] = static::parse_css($chunk); $tmp['raw'] = $tmp['selector'] . '{'; foreach ($tmp['nest'] as $n) { $tmp['raw'] .= $n['raw']; } $tmp['raw'] .= '}'; } else { $tmp = constants::CSS_FLAT; if (0 === \strpos($styles[$k], '@')) { \preg_match_all('/^@([a-z\-]+)/ui', $styles[$k], $matches); $tmp['@'] = mb::strtolower($matches[1][0], false); } \preg_match_all('/^([^⠁]+)⠁([^⠈]*)⠈/u', $styles[$k], $matches); if (\count($matches[0])) { $tmp['selectors'] = \explode(',', $matches[1][0]); $tmp['selectors'] = \array_map('trim', $tmp['selectors']); $rules = \explode(';', $matches[2][0]); $rules = \array_map('trim', $rules); $rules = \array_filter($rules, 'strlen'); if (! \count($rules)) { continue; } foreach ($rules as $k2=>$v2) { $rules[$k2] = \rtrim($rules[$k2], ';') . ';'; if (\preg_match( '/:(?![^"]*"(?:(?:[^"]*"){2})*[^"]*$)/', $rules[$k2] )) { $rules[$k2] = \preg_replace( '/:(?![^"]*"(?:(?:[^"]*"){2})*[^"]*$)/u', "\n", $rules[$k2], 1 ); list($key, $value) = \explode("\n", $rules[$k2]); $key = mb::strtolower(\trim($key), false); $value = \trim($value); $tmp['rules'][$key] = $value; } else { $tmp['rules']['__NONE__'] = $value; } } $tmp['raw'] = \implode(',', $tmp['selectors']) . '{'; foreach ($tmp['rules'] as $k=>$v) { if ('__NONE__' === $k) { $tmp['raw'] .= $v; } else { $tmp['raw'] .= "$k:$v"; } } $tmp['raw'] .= '}'; } else { $styles[$k] = \str_replace(array('⠁', '⠈'), array('{', '}'), $styles[$k]); $styles[$k] = \trim(\rtrim(\trim($styles[$k]), ';')); if (\substr($styles[$k], -1) !== '}') { $styles[$k] .= ';'; } $tmp['rules'][] = $styles[$k]; $tmp['raw'] = $styles[$k]; } } $out[] = $tmp; } return $out; } public static function remove_namespace(\DOMDocument $dom, string $namespace) { if (! $namespace) { return false; } $xpath = new \DOMXPath($dom); $nodes = $xpath->query("//*[namespace::{$namespace} and not(../namespace::{$namespace})]"); for ($x = 0; $x < $nodes->length; ++$x) { $node = $nodes->item($x); $node->removeAttributeNS( $node->lookupNamespaceURI($namespace), $namespace ); } return true; } public static function remove_nodes(\DOMNodeList $nodes) { while ($nodes->length) { static::remove_node($nodes->item(0)); } return true; } public static function remove_node($node) { if ( ! \is_a($node, 'DOMElement') && ! \is_a($node, 'DOMNode') ) { return false; } $node->parentNode->removeChild($node); return true; } }2 namespace blobfolio\wp\meow\vendor\common; class dom { public static function load_svg($svg='') { ref\cast::string($svg, true); $svg = \preg_replace( array( '/<svg/ui', '/<\/svg>/ui', ), array( '<svg', '</svg>', ), $svg ); if ( (false === ($start = mb::strpos($svg, '<svg'))) || (false === ($end = mb::strrpos($svg, '</svg>'))) ) { return false; } $svg = mb::substr($svg, $start, ($end - $start + 6)); $svg = \str_replace( \array_keys(constants::SVG_ATTR_CORRECTIONS), \array_values(constants::SVG_ATTR_CORRECTIONS), $svg ); $svg = \preg_replace( array( '/<\?(.*)\?>/Us', '/<\%(.*)\%>/Us', '/<!--(.*)-->/Us', '/\/\*(.*)\*\//Us', ), '', $svg ); if ( (false !== \strpos($svg, '<?')) || (false !== \strpos($svg, '<%')) || (false !== \strpos($svg, '<!--')) || (false !== \strpos($svg, '/*')) ) { return null; } \libxml_use_internal_errors(true); if (\PHP_VERSION_ID < 80000) { \libxml_disable_entity_loader(true); } $dom = new \DOMDocument('1.0', 'UTF-8'); $dom->formatOutput = false; $dom->preserveWhiteSpace = false; $dom->loadXML(constants::SVG_HEADER . "\n{$svg}"); $svgs = $dom->getElementsByTagName('svg'); if (! $svgs->length) { return false; } return $dom; } public static function save_svg(\DOMDocument $dom) { $svgs = $dom->getElementsByTagName('svg'); if (! $svgs->length) { return ''; } $svg = $svgs->item(0)->ownerDocument->saveXML( $svgs->item(0), \LIBXML_NOBLANKS ); $svg = \preg_replace( '/xmlns\s*=\s*"[^"]*"/', 'xmlns="' . constants::SVG_NAMESPACE . '"', $svg ); $svg = \preg_replace( array( '/<\?(.*)\?>/Us', '/<\%(.*)\%>/Us', '/<!--(.*)-->/Us', '/\/\*(.*)\*\//Us', ), '', $svg ); if ( (false !== \strpos($svg, '<?')) || (false !== \strpos($svg, '<%')) || (false !== \strpos($svg, '<!--')) || (false !== \strpos($svg, '/*')) ) { return null; } if ( (false === ($start = mb::strpos($svg, '<svg'))) || (false === ($end = mb::strrpos($svg, '</svg>'))) ) { return false; } $svg = mb::substr($svg, $start, ($end - $start + 6)); return $svg; } public static function get_nodes_by_class($parent, $class=null, bool $all=false) { $nodes = array(); if (! \method_exists($parent, 'getElementsByTagName')) { return $nodes; } ref\cast::array($class); foreach ($class as $k=>$v) { $class[$k] = \ltrim(\trim($class[$k]), '.'); if (! $class[$k]) { unset($class[$k]); } } if (empty($class)) { return $nodes; } $class = \array_unique($class); \sort($class); $class_length = \count($class); $possible = $parent->getElementsByTagName('*'); if ($possible->length) { foreach ($possible as $child) { if ($child->hasAttribute('class')) { $classes = $child->getAttribute('class'); ref\sanitize::whitespace($classes); $classes = \explode(' ', $classes); $overlap = \array_intersect($classes, $class); $count = \count($overlap); if ($count && (! $all || $count === $class_length)) { $nodes[] = $child; } } } } return $nodes; } public static function innerhtml($node, bool $xml=false, ?int $flags=null) { if ( ! \is_a($node, 'DOMElement') && ! \is_a($node, 'DOMNode') ) { return ''; } $content = ''; $children = $node->childNodes; if ($children->length) { if ($xml) { foreach ($children as $child) { if ($flags) { $content .= $node->ownerDocument->saveXML($child, $flags); } else { $content .= $node->ownerDocument->saveXML($child); } } } else { foreach ($children as $child) { $content .= $node->ownerDocument->saveHTML($child); } } } return $content; } public static function parse_css($styles='') { ref\cast::string($styles, true); while (false !== ($start = mb::strpos($styles, '/*'))) { if (false !== ($end = mb::strpos($styles, '*/'))) { $styles = \str_replace( mb::substr($styles, $start, ($end - $start + 2)), '', $styles ); } else { $styles = mb::substr($styles, 0, $start); } } $styles = \str_replace( array('<!--', '//-->', '-->', '//<![CDATA[', '//]]>', '<![CDATA[', ']]>'), '', $styles ); ref\sanitize::quotes($styles); $styles = \str_replace("'", '"', $styles); ref\sanitize::whitespace($styles, 0); if (! $styles) { return array(); } $styles = \preg_replace( array( '/\{(?![^"]*"(?:(?:[^"]*"){2})*[^"]*$)/u', '/\}(?![^"]*"(?:(?:[^"]*"){2})*[^"]*$)/u', '/\s*(\()\s*(?![^"]*"(?:(?:[^"]*"){2})*[^"]*$)/u', '/\s*(\))\s*(?![^"]*"(?:(?:[^"]*"){2})*[^"]*$)/u', '/\s*(⠁|⠈|@)\s*/u', ), array( '⠁', '⠈', ' (', ') ', '$1', ), $styles); $styles = \str_replace('@', "\n@", $styles); $styles = \explode("\n", $styles); $styles = \array_map('trim', $styles); foreach ($styles as $k=>$v) { if (0 === \strpos($styles[$k], '@')) { if (false !== \strpos($styles[$k], '⠈⠈')) { $styles[$k] = \preg_replace('/(⠈{2,})/u', "$1\n", $styles[$k]); } elseif (false !== \strpos($styles[$k], '⠈')) { $styles[$k] = \str_replace('⠈', "⠈\n", $styles[$k]); } elseif (\preg_match('/;(?![^"]*"(?:(?:[^"]*"){2})*[^"]*$)/', $styles[$k])) { $styles[$k] = \preg_replace( '/;(?![^"]*"(?:(?:[^"]*"){2})*[^"]*$)/u', ";\n", $styles[$k], 1 ); } $tmp = \explode("\n", $styles[$k]); $length = \count($tmp); for ($x = 1; $x < $length; ++$x) { $tmp[$x] = \str_replace('⠈', "⠈\n", $tmp[$x]); } $styles[$k] = \implode("\n", $tmp); } else { $styles[$k] = \str_replace('⠈', "⠈\n", $styles[$k]); } } $styles = \implode("\n", $styles); $styles = \preg_replace( array( '/\)\s(,|;)(?![^"]*"(?:(?:[^"]*"){2})*[^"]*$)/u', '/(url|rgba?)\s+\(/', ), array( ')$1', '$1(', ), $styles ); $styles = \explode("\n", $styles); $styles = \array_filter($styles, 'strlen'); $out = array(); foreach ($styles as $k=>$v) { $styles[$k] = \trim($styles[$k]); if ( (0 === \strpos($styles[$k], '@')) && (false !== \strpos($styles[$k], '⠈⠈')) ) { $tmp = constants::CSS_NESTED; \preg_match_all('/^@([a-z\-]+)/ui', $styles[$k], $matches); $tmp['@'] = mb::strtolower($matches[1][0]); if (false === ($start = mb::strpos($styles[$k], '⠁'))) { continue; } $tmp['selector'] = mb::strtolower( \trim( mb::substr($styles[$k], 0, $start) ), false, true ); $chunk = mb::substr($styles[$k], $start + 1, -1); $chunk = \str_replace(array('⠁', '⠈'), array('{', '}'), $chunk); $tmp['nest'] = static::parse_css($chunk); $tmp['raw'] = $tmp['selector'] . '{'; foreach ($tmp['nest'] as $n) { $tmp['raw'] .= $n['raw']; } $tmp['raw'] .= '}'; } else { $tmp = constants::CSS_FLAT; if (0 === \strpos($styles[$k], '@')) { \preg_match_all('/^@([a-z\-]+)/ui', $styles[$k], $matches); $tmp['@'] = mb::strtolower($matches[1][0], false); } \preg_match_all('/^([^⠁]+)⠁([^⠈]*)⠈/u', $styles[$k], $matches); if (\count($matches[0])) { $tmp['selectors'] = \explode(',', $matches[1][0]); $tmp['selectors'] = \array_map('trim', $tmp['selectors']); $rules = \explode(';', $matches[2][0]); $rules = \array_map('trim', $rules); $rules = \array_filter($rules, 'strlen'); if (! \count($rules)) { continue; } foreach ($rules as $k2=>$v2) { $rules[$k2] = \rtrim($rules[$k2], ';') . ';'; if (\preg_match( '/:(?![^"]*"(?:(?:[^"]*"){2})*[^"]*$)/', $rules[$k2] )) { $rules[$k2] = \preg_replace( '/:(?![^"]*"(?:(?:[^"]*"){2})*[^"]*$)/u', "\n", $rules[$k2], 1 ); list($key, $value) = \explode("\n", $rules[$k2]); $key = mb::strtolower(\trim($key), false); $value = \trim($value); $tmp['rules'][$key] = $value; } else { $tmp['rules']['__NONE__'] = $value; } } $tmp['raw'] = \implode(',', $tmp['selectors']) . '{'; foreach ($tmp['rules'] as $k=>$v) { if ('__NONE__' === $k) { $tmp['raw'] .= $v; } else { $tmp['raw'] .= "$k:$v"; } } $tmp['raw'] .= '}'; } else { $styles[$k] = \str_replace(array('⠁', '⠈'), array('{', '}'), $styles[$k]); $styles[$k] = \trim(\rtrim(\trim($styles[$k]), ';')); if (\substr($styles[$k], -1) !== '}') { $styles[$k] .= ';'; } $tmp['rules'][] = $styles[$k]; $tmp['raw'] = $styles[$k]; } } $out[] = $tmp; } return $out; } public static function remove_namespace(\DOMDocument $dom, string $namespace) { if (! $namespace) { return false; } $xpath = new \DOMXPath($dom); $nodes = $xpath->query("//*[namespace::{$namespace} and not(../namespace::{$namespace})]"); for ($x = 0; $x < $nodes->length; ++$x) { $node = $nodes->item($x); $node->removeAttributeNS( $node->lookupNamespaceURI($namespace), $namespace ); } return true; } public static function remove_nodes(\DOMNodeList $nodes) { while ($nodes->length) { static::remove_node($nodes->item(0)); } return true; } public static function remove_node($node) { if ( ! \is_a($node, 'DOMElement') && ! \is_a($node, 'DOMNode') ) { return false; } $node->parentNode->removeChild($node); return true; } } -
apocalypse-meow/trunk/lib/vendor/erusev/parsedown/Parsedown.php
r2383133 r3273810 717 717 # Setext 718 718 719 protected function blockSetextHeader($Line, array $Block = null)719 protected function blockSetextHeader($Line, ?array $Block = null) 720 720 { 721 721 if ( ! isset($Block) or isset($Block['type']) or isset($Block['interrupted'])) … … 855 855 # Table 856 856 857 protected function blockTable($Line, array $Block = null)857 protected function blockTable($Line, ?array $Block = null) 858 858 { 859 859 if ( ! isset($Block) or isset($Block['type']) or isset($Block['interrupted'])) -
apocalypse-meow/trunk/readme.txt
r3239036 r3273810 4 4 Tags: login security, wordpress security, security plugin, brute-force, security, opsec, passwords, sessions, secure, malware, antivirus, block hackers, exploit, infection, protection, spam 5 5 Requires at least: 4.4 6 Tested up to: 6. 76 Tested up to: 6.8 7 7 Requires PHP: 7.3 8 8 Stable tag: trunk … … 115 115 == Changelog == 116 116 117 = 22.0.1 = 118 * [Fix] Improve PHP 8.4 compatibility 119 117 120 = 22.0.0 = 118 121 * [Remove] This release removes the Community Pool feature. End of an era. ;) … … 128 131 * [New] Login Lockdown option to help mitigate distributed brute-force attacks. 129 132 130 = 21.7.5 = 131 * [Fix] Add workaround to fix compatibility with (unaffiliated) `activitypub` plugin. 132 * [Fix] Remove obsolete documentation. 133 == Upgrade Notice == 133 134 134 == Upgrade Notice == 135 = 22.0.1 = 136 This release improves PHP 8.4 compatibility. 135 137 136 138 = 22.0.0 = … … 145 147 = 21.8.0 = 146 148 This release adds a new Login Lockdown option to help mitigate distributed brute-force attacks. 147 148 = 21.7.5 =149 This release adds a workaround to fix compatibility issues with the (unaffiliated) `activitypub` plugin, and removes some obsolete documentation.
Note: See TracChangeset
for help on using the changeset viewer.