├── .github └── workflows │ └── tests.yml ├── .gitignore ├── LICENSE ├── composer.json └── src ├── Bundle.php ├── Bundles ├── Fatdown.php ├── Fatdown │ └── Renderer.php ├── Forum.php ├── Forum │ └── Renderer.php ├── MediaPack.php └── MediaPack │ └── Renderer.php ├── Configurator.php ├── Configurator ├── Bundle.php ├── BundleGenerator.php ├── Bundles │ ├── Fatdown.php │ ├── Forum.php │ └── MediaPack.php ├── Collections │ ├── AttributeCollection.php │ ├── AttributeFilterChain.php │ ├── AttributeFilterCollection.php │ ├── AttributeList.php │ ├── AttributePreprocessorCollection.php │ ├── Collection.php │ ├── FilterChain.php │ ├── HostnameList.php │ ├── MinifierList.php │ ├── NormalizedCollection.php │ ├── NormalizedList.php │ ├── PluginCollection.php │ ├── RulesGeneratorList.php │ ├── Ruleset.php │ ├── SchemeList.php │ ├── TagCollection.php │ ├── TagFilterChain.php │ ├── TagList.php │ ├── TemplateCheckList.php │ ├── TemplateNormalizationList.php │ └── TemplateParameterCollection.php ├── ConfigProvider.php ├── Exceptions │ └── UnsafeTemplateException.php ├── FilterableConfigValue.php ├── Helpers │ ├── AVTHelper.php │ ├── ConfigHelper.php │ ├── ContextSafeness.php │ ├── ElementInspector.php │ ├── FilterHelper.php │ ├── FilterSyntaxMatcher.php │ ├── NodeLocator.php │ ├── RegexpBuilder.php │ ├── RegexpParser.php │ ├── RulesHelper.php │ ├── TemplateHelper.php │ ├── TemplateInspector.php │ ├── TemplateLoader.php │ ├── TemplateModifier.php │ ├── TemplateParser.php │ ├── TemplateParser │ │ ├── IRProcessor.php │ │ ├── Normalizer.php │ │ ├── Optimizer.php │ │ └── Parser.php │ └── XPathHelper.php ├── Items │ ├── Attribute.php │ ├── AttributeFilter.php │ ├── AttributeFilters │ │ ├── AbstractMapFilter.php │ │ ├── AlnumFilter.php │ │ ├── ChoiceFilter.php │ │ ├── ColorFilter.php │ │ ├── EmailFilter.php │ │ ├── FalseFilter.php │ │ ├── FloatFilter.php │ │ ├── FontfamilyFilter.php │ │ ├── HashmapFilter.php │ │ ├── IdentifierFilter.php │ │ ├── IntFilter.php │ │ ├── IpFilter.php │ │ ├── IpportFilter.php │ │ ├── Ipv4Filter.php │ │ ├── Ipv6Filter.php │ │ ├── MapFilter.php │ │ ├── NumberFilter.php │ │ ├── RangeFilter.php │ │ ├── RegexpFilter.php │ │ ├── SimpletextFilter.php │ │ ├── TimestampFilter.php │ │ ├── UintFilter.php │ │ └── UrlFilter.php │ ├── AttributePreprocessor.php │ ├── Filter.php │ ├── ProgrammableCallback.php │ ├── Regexp.php │ ├── Tag.php │ ├── TagFilter.php │ ├── Template.php │ ├── TemplateDocument.php │ └── UnsafeTemplate.php ├── JavaScript.php ├── JavaScript │ ├── CallbackGenerator.php │ ├── Code.php │ ├── ConfigOptimizer.php │ ├── ConfigValue.php │ ├── Dictionary.php │ ├── Encoder.php │ ├── FunctionCache.php │ ├── FunctionProvider.php │ ├── Hasher.php │ ├── HintGenerator.php │ ├── Minifier.php │ ├── Minifiers │ │ ├── ClosureCompilerApplication.php │ │ ├── ClosureCompilerService.php │ │ ├── FirstAvailable.php │ │ ├── MatthiasMullieMinify.php │ │ └── Noop.php │ ├── OnlineMinifier.php │ ├── RegexpConvertor.php │ ├── StylesheetCompressor.php │ ├── externs.application.js │ └── externs.service.js ├── RecursiveParser.php ├── RecursiveParser │ ├── AbstractRecursiveMatcher.php │ ├── CachingRecursiveParser.php │ └── MatcherInterface.php ├── RendererGenerator.php ├── RendererGenerators │ ├── PHP.php │ ├── PHP │ │ ├── AbstractOptimizer.php │ │ ├── BranchOutputOptimizer.php │ │ ├── ControlStructuresOptimizer.php │ │ ├── Optimizer.php │ │ ├── Quick.php │ │ ├── Serializer.php │ │ ├── SwitchStatement.php │ │ ├── XPathConvertor.php │ │ └── XPathConvertor │ │ │ └── Convertors │ │ │ ├── AbstractConvertor.php │ │ │ ├── BooleanFunctions.php │ │ │ ├── BooleanOperators.php │ │ │ ├── Comparisons.php │ │ │ ├── Core.php │ │ │ ├── Math.php │ │ │ ├── MultiByteStringManipulation.php │ │ │ ├── PHP80Functions.php │ │ │ ├── SingleByteStringFunctions.php │ │ │ └── SingleByteStringManipulation.php │ ├── Unformatted.php │ └── XSLT.php ├── Rendering.php ├── RulesGenerator.php ├── RulesGenerators │ ├── AllowAll.php │ ├── AutoCloseIfVoid.php │ ├── AutoReopenFormattingElements.php │ ├── BlockElementsCloseFormattingElements.php │ ├── BlockElementsFosterFormattingElements.php │ ├── DisableAutoLineBreaksIfNewLinesArePreserved.php │ ├── EnforceContentModels.php │ ├── EnforceOptionalEndTags.php │ ├── IgnoreTagsInCode.php │ ├── IgnoreTextIfDisallowed.php │ ├── IgnoreWhitespaceAroundBlockElements.php │ ├── Interfaces │ │ ├── BooleanRulesGenerator.php │ │ └── TargetedRulesGenerator.php │ ├── ManageParagraphs.php │ └── TrimFirstLineInCodeBlocks.php ├── TemplateCheck.php ├── TemplateChecker.php ├── TemplateChecks │ ├── AbstractDynamicContentCheck.php │ ├── AbstractFlashRestriction.php │ ├── AbstractXSLSupportCheck.php │ ├── DisallowAttributeSets.php │ ├── DisallowCopy.php │ ├── DisallowDisableOutputEscaping.php │ ├── DisallowDynamicAttributeNames.php │ ├── DisallowDynamicElementNames.php │ ├── DisallowElement.php │ ├── DisallowElementNS.php │ ├── DisallowFlashFullScreen.php │ ├── DisallowNodeByXPath.php │ ├── DisallowObjectParamsWithGeneratedName.php │ ├── DisallowPHPTags.php │ ├── DisallowUncompilableXSL.php │ ├── DisallowUnsafeCopyOf.php │ ├── DisallowUnsafeDynamicCSS.php │ ├── DisallowUnsafeDynamicJS.php │ ├── DisallowUnsafeDynamicURL.php │ ├── DisallowUnsupportedXSL.php │ ├── DisallowXPathFunction.php │ ├── RestrictFlashNetworking.php │ └── RestrictFlashScriptAccess.php ├── TemplateNormalizations │ ├── AbstractChooseOptimization.php │ ├── AbstractConstantFolding.php │ ├── AbstractNormalization.php │ ├── AddAttributeValueToElements.php │ ├── ConvertCurlyExpressionsInText.php │ ├── Custom.php │ ├── DeoptimizeIf.php │ ├── EnforceHTMLOmittedEndTags.php │ ├── FixUnescapedCurlyBracesInHtmlAttributes.php │ ├── FoldArithmeticConstants.php │ ├── FoldConstantXPathExpressions.php │ ├── InlineAttributes.php │ ├── InlineCDATA.php │ ├── InlineElements.php │ ├── InlineInferredValues.php │ ├── InlineTextElements.php │ ├── InlineXPathLiterals.php │ ├── MergeConsecutiveCopyOf.php │ ├── MergeIdenticalConditionalBranches.php │ ├── MinifyInlineCSS.php │ ├── MinifyXPathExpressions.php │ ├── NormalizeAttributeNames.php │ ├── NormalizeElementNames.php │ ├── NormalizeUrls.php │ ├── OptimizeChoose.php │ ├── OptimizeChooseAttributes.php │ ├── OptimizeChooseDeadBranches.php │ ├── OptimizeChooseText.php │ ├── OptimizeConditionalAttributes.php │ ├── OptimizeConditionalValueOf.php │ ├── OptimizeNestedConditionals.php │ ├── PreserveSingleSpaces.php │ ├── RemoveComments.php │ ├── RemoveInterElementWhitespace.php │ ├── RemoveLivePreviewAttributes.php │ ├── RenameLivePreviewEvent.php │ ├── SetAttributeOnElements.php │ ├── SetRelNoreferrerOnTargetedLinks.php │ ├── SortAttributesByName.php │ ├── TransposeComments.php │ └── UninlineAttributes.php ├── TemplateNormalizer.php ├── Traits │ ├── CollectionProxy.php │ ├── Configurable.php │ └── TemplateSafeness.php ├── UrlConfig.php └── Validators │ ├── AttributeName.php │ ├── TagName.php │ └── TemplateParameterName.php ├── Parser.js ├── Parser.php ├── Parser ├── AttributeFilters │ ├── EmailFilter.js │ ├── EmailFilter.php │ ├── FalseFilter.js │ ├── FalseFilter.php │ ├── HashmapFilter.js │ ├── HashmapFilter.php │ ├── MapFilter.js │ ├── MapFilter.php │ ├── NetworkFilter.js │ ├── NetworkFilter.php │ ├── NumericFilter.js │ ├── NumericFilter.php │ ├── RegexpFilter.js │ ├── RegexpFilter.php │ ├── TimestampFilter.js │ ├── TimestampFilter.php │ ├── UrlFilter.js │ └── UrlFilter.php ├── FilterProcessing.js ├── FilterProcessing.php ├── Logger.js ├── Logger.php ├── NullLogger.js ├── Tag.js ├── Tag.php └── utils.js ├── Plugins ├── AbstractStaticUrlReplacer │ ├── AbstractConfigurator.php │ ├── AbstractParser.php │ └── Parser.js ├── Autoemail │ ├── Configurator.php │ ├── Parser.js │ └── Parser.php ├── Autoimage │ ├── Configurator.php │ ├── Parser.js │ └── Parser.php ├── Autolink │ ├── Configurator.php │ ├── Parser.js │ └── Parser.php ├── Autovideo │ ├── Configurator.php │ ├── Parser.js │ └── Parser.php ├── BBCodes │ ├── Configurator.php │ ├── Configurator │ │ ├── BBCode.php │ │ ├── BBCodeCollection.php │ │ ├── BBCodeMonkey.php │ │ ├── Repository.php │ │ ├── RepositoryCollection.php │ │ └── repository.xml │ ├── Parser.js │ └── Parser.php ├── Censor │ ├── Configurator.php │ ├── Helper.php │ ├── Parser.js │ └── Parser.php ├── ConfiguratorBase.php ├── Emoji │ ├── Configurator.php │ ├── Parser.js │ └── Parser.php ├── Emoticons │ ├── Configurator.php │ ├── Configurator │ │ └── EmoticonCollection.php │ ├── Parser.js │ └── Parser.php ├── Escaper │ ├── Configurator.php │ ├── Parser.js │ └── Parser.php ├── FancyPants │ ├── Configurator.php │ ├── Parser.js │ └── Parser.php ├── HTMLComments │ ├── Configurator.php │ ├── Parser.js │ └── Parser.php ├── HTMLElements │ ├── Configurator.php │ ├── Parser.js │ └── Parser.php ├── HTMLEntities │ ├── Configurator.php │ ├── Parser.js │ └── Parser.php ├── Keywords │ ├── Configurator.php │ ├── Parser.js │ └── Parser.php ├── Litedown │ ├── Configurator.php │ ├── Parser.php │ └── Parser │ │ ├── LinkAttributesSetter.js │ │ ├── LinkAttributesSetter.php │ │ ├── ParsedText.js │ │ ├── ParsedText.php │ │ ├── Passes │ │ ├── AbstractInlineMarkup.js │ │ ├── AbstractInlineMarkup.php │ │ ├── AbstractPass.php │ │ ├── AbstractScript.js │ │ ├── AbstractScript.php │ │ ├── Blocks.js │ │ ├── Blocks.php │ │ ├── Emphasis.js │ │ ├── Emphasis.php │ │ ├── ForcedLineBreaks.js │ │ ├── ForcedLineBreaks.php │ │ ├── Images.js │ │ ├── Images.php │ │ ├── InlineCode.js │ │ ├── InlineCode.php │ │ ├── InlineSpoiler.js │ │ ├── InlineSpoiler.php │ │ ├── LinkReferences.js │ │ ├── LinkReferences.php │ │ ├── Links.js │ │ ├── Links.php │ │ ├── Strikethrough.js │ │ ├── Strikethrough.php │ │ ├── Subscript.js │ │ ├── Subscript.php │ │ ├── Superscript.js │ │ └── Superscript.php │ │ ├── Slugger.js │ │ └── Slugger.php ├── MediaEmbed │ ├── Configurator.php │ ├── Configurator │ │ ├── AbstractConfigurableHostHelper.php │ │ ├── Collections │ │ │ ├── CachedDefinitionCollection.php │ │ │ ├── SiteDefinitionCollection.php │ │ │ └── XmlFileDefinitionCollection.php │ │ ├── MastodonHelper.php │ │ ├── SiteHelpers │ │ │ ├── AbstractConfigurableHostHelper.php │ │ │ ├── AbstractSiteHelper.php │ │ │ ├── BlueskyHelper.php │ │ │ ├── MastodonHelper.php │ │ │ └── XenForoHelper.php │ │ ├── TemplateBuilder.php │ │ ├── TemplateGenerator.php │ │ ├── TemplateGenerators │ │ │ ├── Choose.php │ │ │ ├── Flash.php │ │ │ └── Iframe.php │ │ └── XenForoHelper.php │ ├── Parser.js │ ├── Parser.php │ └── Parser │ │ └── tagFilter.js ├── ParserBase.php ├── PipeTables │ ├── Configurator.php │ ├── Parser.js │ └── Parser.php ├── Preg │ ├── Configurator.php │ ├── Parser.js │ └── Parser.php └── TaskLists │ ├── Configurator.php │ ├── Helper.php │ └── filterListItem.js ├── Renderer.php ├── Renderers ├── PHP.php ├── Unformatted.php └── XSLT.php ├── Unparser.php ├── Utils.php ├── Utils ├── Http.php ├── Http │ ├── Client.php │ └── Clients │ │ ├── Cached.php │ │ ├── Curl.php │ │ └── Native.php ├── ParsedDOM.php ├── ParsedDOM │ ├── Document.php │ └── Element.php └── XPath.php └── render.js /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | outdated: 11 | runs-on: ubuntu-latest 12 | container: setupphp/node@sha256:9271c0a914deb70c1717ec113410c9d43e48123d0ed398bb696f00f4f0ef15ba 13 | 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | php-version: 18 | - "8.1" 19 | 20 | steps: 21 | - uses: "actions/checkout@v4" 22 | - uses: "shivammathur/setup-php@v2" 23 | with: 24 | php-version: ${{ matrix.php-version }} 25 | 26 | - name: Cache Composer packages 27 | id: composer-cache 28 | uses: actions/cache@v4 29 | with: 30 | path: vendor 31 | key: php-outdated 32 | 33 | - name: Install Composer dependencies 34 | run: composer install --prefer-dist --no-progress 35 | 36 | - name: Run test suite 37 | run: composer run-script test 38 | 39 | current: 40 | runs-on: ubuntu-latest 41 | strategy: 42 | matrix: 43 | php-version: 44 | - "8.1" 45 | - "8.2" 46 | - "8.3" 47 | - "8.4" 48 | 49 | steps: 50 | - uses: "actions/checkout@v4" 51 | - uses: "shivammathur/setup-php@v2" 52 | with: 53 | php-version: ${{ matrix.php-version }} 54 | 55 | - name: Validate composer.json and composer.lock 56 | run: composer validate --strict 57 | 58 | - name: Cache Composer packages 59 | id: composer-cache 60 | uses: actions/cache@v4 61 | with: 62 | path: vendor 63 | key: php-${{ matrix.php-version }} 64 | 65 | - name: Install Composer dependencies 66 | run: composer install --prefer-dist --no-progress 67 | 68 | - name: Run test suite 69 | run: composer run-script test 70 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.* 2 | /composer.lock 3 | /scripts/composer.lock 4 | /scripts/vendor 5 | /tests/.cache 6 | /vendor 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) The s9e authors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "s9e/text-formatter", 3 | "type": "library", 4 | "description": "Multi-purpose text formatting and markup library. Plugins offer support for BBCodes, Markdown, emoticons, HTML, embedding third-party media (YouTube, etc...), enhanced typography and more.", 5 | "homepage": "https://github.com/s9e/TextFormatter/", 6 | "keywords": ["bbcode","bbcodes","blog","censor","embed","emoji","emoticons","engine","forum","html","markdown","markup","media","parser","shortcodes"], 7 | "license": "MIT", 8 | "require": { 9 | "php": "^8.1", 10 | "ext-dom": "*", 11 | "ext-filter": "*", 12 | "lib-pcre": ">=8.13", 13 | 14 | "s9e/regexp-builder": "^1.4", 15 | "s9e/sweetdom": "^3.4" 16 | }, 17 | "require-dev": { 18 | "code-lts/doctum": "*", 19 | "matthiasmullie/minify": "*", 20 | "phpunit/phpunit": "^9.5", 21 | "friendsofphp/php-cs-fixer": "^3.52" 22 | }, 23 | "suggest": { 24 | "ext-curl": "Improves the performance of the MediaEmbed plugin and some JavaScript minifiers", 25 | "ext-intl": "Allows international URLs to be accepted by the URL filter", 26 | "ext-json": "Enables the generation of a JavaScript parser", 27 | "ext-mbstring": "Improves the performance of the PHP renderer", 28 | "ext-tokenizer": "Improves the performance of the PHP renderer", 29 | "ext-xsl": "Enables the XSLT renderer", 30 | "ext-zlib": "Enables gzip compression when scraping content via the MediaEmbed plugin" 31 | }, 32 | "autoload": { 33 | "psr-4": { 34 | "s9e\\TextFormatter\\": "src" 35 | } 36 | }, 37 | "autoload-dev": { 38 | "psr-4": { 39 | "s9e\\TextFormatter\\Tests\\": "tests" 40 | } 41 | }, 42 | "scripts": { 43 | "post-update-cmd": "php scripts/patchReadme.php", 44 | "test": "phpunit --exclude-group ''" 45 | }, 46 | "extra": { 47 | "version": "2.19.1-dev" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Configurator/Bundle.php: -------------------------------------------------------------------------------- 1 | configure($configurator); 33 | 34 | return $configurator; 35 | } 36 | 37 | /** 38 | * Return extra options to be passed to the bundle generator 39 | * 40 | * Used by scripts/generateBundles.php 41 | * 42 | * @return array 43 | */ 44 | public static function getOptions() 45 | { 46 | return []; 47 | } 48 | } -------------------------------------------------------------------------------- /src/Configurator/Bundles/MediaPack.php: -------------------------------------------------------------------------------- 1 | MediaEmbed)) 22 | { 23 | // Only create BBCodes if the BBCodes plugin is already loaded 24 | $pluginOptions = ['createMediaBBCode' => isset($configurator->BBCodes)]; 25 | 26 | $configurator->plugins->load('MediaEmbed', $pluginOptions); 27 | } 28 | 29 | foreach ($configurator->MediaEmbed->defaultSites as $siteId => $siteConfig) 30 | { 31 | $configurator->MediaEmbed->add($siteId); 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /src/Configurator/Collections/AttributeCollection.php: -------------------------------------------------------------------------------- 1 | items); 35 | sort($list); 36 | 37 | return $list; 38 | } 39 | } -------------------------------------------------------------------------------- /src/Configurator/Collections/MinifierList.php: -------------------------------------------------------------------------------- 1 | getMinifierInstance($minifier); 27 | } 28 | elseif (is_array($minifier) && !empty($minifier[0])) 29 | { 30 | $minifier = $this->getMinifierInstance($minifier[0], array_slice($minifier, 1)); 31 | } 32 | 33 | if (!($minifier instanceof Minifier)) 34 | { 35 | throw new InvalidArgumentException('Invalid minifier ' . var_export($minifier, true)); 36 | } 37 | 38 | return $minifier; 39 | } 40 | 41 | /** 42 | * Create and return a Minifier instance 43 | * 44 | * @param string $name Minifier's name 45 | * @param array $args Constructor's arguments 46 | * @return Minifier 47 | */ 48 | protected function getMinifierInstance($name, array $args = []) 49 | { 50 | $className = 's9e\\TextFormatter\\Configurator\\JavaScript\\Minifiers\\' . $name; 51 | if (!class_exists($className)) 52 | { 53 | throw new InvalidArgumentException('Invalid minifier ' . var_export($name, true)); 54 | } 55 | 56 | $reflection = new ReflectionClass($className); 57 | $minifier = (empty($args)) ? $reflection->newInstance() : $reflection->newInstanceArgs($args); 58 | 59 | return $minifier; 60 | } 61 | } -------------------------------------------------------------------------------- /src/Configurator/Collections/RulesGeneratorList.php: -------------------------------------------------------------------------------- 1 | normalizeValue($value)); 52 | 53 | $cnt = 0; 54 | foreach ($this->items as $i => $rulesGenerator) 55 | { 56 | if ($rulesGenerator instanceof $className) 57 | { 58 | ++$cnt; 59 | unset($this->items[$i]); 60 | } 61 | } 62 | $this->items = array_values($this->items); 63 | 64 | return $cnt; 65 | } 66 | 67 | return parent::remove($value); 68 | } 69 | } -------------------------------------------------------------------------------- /src/Configurator/Collections/SchemeList.php: -------------------------------------------------------------------------------- 1 | items) . '$/Di'); 24 | } 25 | 26 | /** 27 | * Validate and normalize a scheme name to lowercase, or throw an exception if invalid 28 | * 29 | * @link http://tools.ietf.org/html/rfc3986#section-3.1 30 | * 31 | * @param string $scheme URL scheme, e.g. "file" or "ed2k" 32 | * @return string 33 | */ 34 | public function normalizeValue($scheme) 35 | { 36 | if (!preg_match('#^[a-z][a-z0-9+\\-.]*$#Di', $scheme)) 37 | { 38 | throw new InvalidArgumentException("Invalid scheme name '" . $scheme . "'"); 39 | } 40 | 41 | return strtolower($scheme); 42 | } 43 | } -------------------------------------------------------------------------------- /src/Configurator/Collections/TagCollection.php: -------------------------------------------------------------------------------- 1 | items); 34 | sort($list); 35 | 36 | return $list; 37 | } 38 | } -------------------------------------------------------------------------------- /src/Configurator/Collections/TemplateCheckList.php: -------------------------------------------------------------------------------- 1 | node = $node; 29 | } 30 | 31 | /** 32 | * Return the node that has caused this exception 33 | * 34 | * @return DOMNode 35 | */ 36 | public function getNode() 37 | { 38 | return $this->node; 39 | } 40 | 41 | /** 42 | * Highlight the source of the template that has caused this exception, with the node highlighted 43 | * 44 | * @param string $prepend HTML to prepend 45 | * @param string $append HTML to append 46 | * @return string Template's source, as HTML 47 | */ 48 | public function highlightNode($prepend = '', $append = '') 49 | { 50 | return TemplateHelper::highlightNode($this->node, $prepend, $append); 51 | } 52 | 53 | /** 54 | * Change the node associated with this exception 55 | * 56 | * @param DOMNode $node 57 | * @return void 58 | */ 59 | public function setNode(DOMNode $node) 60 | { 61 | $this->node = $node; 62 | } 63 | } -------------------------------------------------------------------------------- /src/Configurator/FilterableConfigValue.php: -------------------------------------------------------------------------------- 1 | '/', 25 | 'caseInsensitive' => false, 26 | 'specialChars' => [], 27 | 'unicode' => true 28 | ]; 29 | 30 | // Normalize ASCII if the regexp is meant to be case-insensitive 31 | if ($options['caseInsensitive']) 32 | { 33 | foreach ($words as &$word) 34 | { 35 | $word = strtr($word, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); 36 | } 37 | unset($word); 38 | } 39 | 40 | $builder = new Builder([ 41 | 'delimiter' => $options['delimiter'], 42 | 'meta' => $options['specialChars'], 43 | 'input' => $options['unicode'] ? 'Utf8' : 'Bytes', 44 | 'output' => $options['unicode'] ? 'Utf8' : 'Bytes' 45 | ]); 46 | 47 | return $builder->build($words); 48 | } 49 | } -------------------------------------------------------------------------------- /src/Configurator/Helpers/TemplateParser.php: -------------------------------------------------------------------------------- 1 | parse($template); 38 | } 39 | } -------------------------------------------------------------------------------- /src/Configurator/Items/AttributeFilter.php: -------------------------------------------------------------------------------- 1 | resetParameters(); 27 | $this->addParameterByName('attrValue'); 28 | } 29 | 30 | /** 31 | * Return whether this filter makes a value safe to be used in JavaScript 32 | * 33 | * @return bool 34 | */ 35 | public function isSafeInJS() 36 | { 37 | // List of callbacks that make a value safe to be used in a script, hardcoded for 38 | // convenience. Technically, there are numerous built-in PHP functions that would make an 39 | // arbitrary value safe in JS, but only a handful have the potential to be used as an 40 | // attribute filter 41 | $safeCallbacks = [ 42 | 'urlencode', 43 | 'strtotime', 44 | 'rawurlencode' 45 | ]; 46 | 47 | if (in_array($this->callback, $safeCallbacks, true)) 48 | { 49 | return true; 50 | } 51 | 52 | return $this->isSafe('InJS'); 53 | } 54 | } -------------------------------------------------------------------------------- /src/Configurator/Items/AttributeFilters/AbstractMapFilter.php: -------------------------------------------------------------------------------- 1 | vars['map'])) 22 | { 23 | $name = preg_replace('(.*\\\\|Filter$)', '', get_class($this)); 24 | 25 | throw new RuntimeException($name . " filter is missing a 'map' value"); 26 | } 27 | 28 | return parent::asConfig(); 29 | } 30 | 31 | /** 32 | * Assess the safeness of this attribute filter based on given list of strings 33 | * 34 | * @param string[] $strings 35 | * @return void 36 | */ 37 | protected function assessSafeness(array $strings): void 38 | { 39 | $str = implode('', $strings); 40 | foreach (['AsURL', 'InCSS', 'InJS'] as $context) 41 | { 42 | $callback = ContextSafeness::class . '::getDisallowedCharacters' . $context; 43 | foreach ($callback() as $char) 44 | { 45 | if (strpos($str, $char) !== false) 46 | { 47 | continue 2; 48 | } 49 | } 50 | 51 | $methodName = 'markAsSafe' . $context; 52 | $this->$methodName(); 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /src/Configurator/Items/AttributeFilters/AlnumFilter.php: -------------------------------------------------------------------------------- 1 | markAsSafeAsURL(); 19 | $this->markAsSafeInCSS(); 20 | } 21 | } -------------------------------------------------------------------------------- /src/Configurator/Items/AttributeFilters/ChoiceFilter.php: -------------------------------------------------------------------------------- 1 | setValues($values, $caseSensitive); 28 | } 29 | } 30 | 31 | /** 32 | * Set the list of allowed values 33 | * 34 | * @param array $values List of allowed values 35 | * @param bool $caseSensitive Whether the choice is case-sensitive 36 | * @return void 37 | */ 38 | public function setValues(array $values, $caseSensitive = false) 39 | { 40 | if (!is_bool($caseSensitive)) 41 | { 42 | throw new InvalidArgumentException('Argument 2 passed to ' . __METHOD__ . ' must be a boolean'); 43 | } 44 | 45 | // Create a regexp based on the list of allowed values 46 | $regexp = RegexpBuilder::fromList($values, ['delimiter' => '/']); 47 | $regexp = '/^' . $regexp . '$/D'; 48 | 49 | // Add the case-insensitive flag if applicable 50 | if (!$caseSensitive) 51 | { 52 | $regexp .= 'i'; 53 | } 54 | 55 | // Add the Unicode flag if the regexp isn't purely ASCII 56 | if (!preg_match('#^[[:ascii:]]*$#D', $regexp)) 57 | { 58 | $regexp .= 'u'; 59 | } 60 | 61 | // Set the regexp associated with this list of values 62 | $this->setRegexp($regexp); 63 | } 64 | } -------------------------------------------------------------------------------- /src/Configurator/Items/AttributeFilters/ColorFilter.php: -------------------------------------------------------------------------------- 1 | markAsSafeInCSS(); 19 | } 20 | } -------------------------------------------------------------------------------- /src/Configurator/Items/AttributeFilters/EmailFilter.php: -------------------------------------------------------------------------------- 1 | setJS('EmailFilter.filter'); 21 | } 22 | } -------------------------------------------------------------------------------- /src/Configurator/Items/AttributeFilters/FalseFilter.php: -------------------------------------------------------------------------------- 1 | setJS('FalseFilter.filter'); 21 | } 22 | } -------------------------------------------------------------------------------- /src/Configurator/Items/AttributeFilters/FloatFilter.php: -------------------------------------------------------------------------------- 1 | setJS('NumericFilter.filterFloat'); 21 | $this->markAsSafeAsURL(); 22 | $this->markAsSafeInCSS(); 23 | $this->markAsSafeInJS(); 24 | } 25 | } -------------------------------------------------------------------------------- /src/Configurator/Items/AttributeFilters/FontfamilyFilter.php: -------------------------------------------------------------------------------- 1 | markAsSafeInCSS(); 23 | } 24 | } -------------------------------------------------------------------------------- /src/Configurator/Items/AttributeFilters/IdentifierFilter.php: -------------------------------------------------------------------------------- 1 | markAsSafeAsURL(); 19 | $this->markAsSafeInCSS(); 20 | } 21 | } -------------------------------------------------------------------------------- /src/Configurator/Items/AttributeFilters/IntFilter.php: -------------------------------------------------------------------------------- 1 | setJS('NumericFilter.filterInt'); 21 | $this->markAsSafeAsURL(); 22 | $this->markAsSafeInCSS(); 23 | $this->markAsSafeInJS(); 24 | } 25 | } -------------------------------------------------------------------------------- /src/Configurator/Items/AttributeFilters/IpFilter.php: -------------------------------------------------------------------------------- 1 | setJS('NetworkFilter.filterIp'); 21 | } 22 | } -------------------------------------------------------------------------------- /src/Configurator/Items/AttributeFilters/IpportFilter.php: -------------------------------------------------------------------------------- 1 | setJS('NetworkFilter.filterIpport'); 21 | } 22 | } -------------------------------------------------------------------------------- /src/Configurator/Items/AttributeFilters/Ipv4Filter.php: -------------------------------------------------------------------------------- 1 | setJS('NetworkFilter.filterIpv4'); 21 | } 22 | } -------------------------------------------------------------------------------- /src/Configurator/Items/AttributeFilters/Ipv6Filter.php: -------------------------------------------------------------------------------- 1 | setJS('NetworkFilter.filterIpv6'); 21 | } 22 | } -------------------------------------------------------------------------------- /src/Configurator/Items/AttributeFilters/NumberFilter.php: -------------------------------------------------------------------------------- 1 | markAsSafeAsURL(); 19 | $this->markAsSafeInCSS(); 20 | $this->markAsSafeInJS(); 21 | } 22 | } -------------------------------------------------------------------------------- /src/Configurator/Items/AttributeFilters/SimpletextFilter.php: -------------------------------------------------------------------------------- 1 | markAsSafeInCSS(); 19 | } 20 | } -------------------------------------------------------------------------------- /src/Configurator/Items/AttributeFilters/TimestampFilter.php: -------------------------------------------------------------------------------- 1 | setJS('TimestampFilter.filter'); 21 | $this->markAsSafeAsURL(); 22 | $this->markAsSafeInCSS(); 23 | $this->markAsSafeInJS(); 24 | } 25 | } -------------------------------------------------------------------------------- /src/Configurator/Items/AttributeFilters/UintFilter.php: -------------------------------------------------------------------------------- 1 | setJS('NumericFilter.filterUint'); 21 | } 22 | 23 | /** 24 | * {@inheritdoc} 25 | */ 26 | public function isSafeInCSS() 27 | { 28 | return true; 29 | } 30 | 31 | /** 32 | * {@inheritdoc} 33 | */ 34 | public function isSafeInJS() 35 | { 36 | return true; 37 | } 38 | 39 | /** 40 | * {@inheritdoc} 41 | */ 42 | public function isSafeAsURL() 43 | { 44 | return true; 45 | } 46 | } -------------------------------------------------------------------------------- /src/Configurator/Items/AttributeFilters/UrlFilter.php: -------------------------------------------------------------------------------- 1 | resetParameters(); 22 | $this->addParameterByName('attrValue'); 23 | $this->addParameterByName('urlConfig'); 24 | $this->addParameterByName('logger'); 25 | $this->setJS('UrlFilter.filter'); 26 | } 27 | 28 | /** 29 | * {@inheritdoc} 30 | */ 31 | public function isSafeInCSS() 32 | { 33 | return true; 34 | } 35 | 36 | /** 37 | * {@inheritdoc} 38 | */ 39 | public function isSafeInJS() 40 | { 41 | return true; 42 | } 43 | 44 | /** 45 | * {@inheritdoc} 46 | */ 47 | public function isSafeAsURL() 48 | { 49 | return true; 50 | } 51 | } -------------------------------------------------------------------------------- /src/Configurator/Items/AttributePreprocessor.php: -------------------------------------------------------------------------------- 1 | regexp] 19 | */ 20 | public function getAttributes() 21 | { 22 | return $this->getNamedCaptures(); 23 | } 24 | 25 | /** 26 | * Return the regexp this preprocessor is based on 27 | * 28 | * @return string 29 | */ 30 | public function getRegexp() 31 | { 32 | return $this->regexp; 33 | } 34 | } -------------------------------------------------------------------------------- /src/Configurator/Items/Filter.php: -------------------------------------------------------------------------------- 1 | resetParameters(); 23 | $this->addParameterByName('tag'); 24 | } 25 | } -------------------------------------------------------------------------------- /src/Configurator/Items/TemplateDocument.php: -------------------------------------------------------------------------------- 1 | template = $template; 30 | } 31 | 32 | /** 33 | * Update the original template with this document's content 34 | * 35 | * @return void 36 | */ 37 | public function saveChanges() 38 | { 39 | $this->template->setContent(TemplateLoader::save($this)); 40 | } 41 | } -------------------------------------------------------------------------------- /src/Configurator/Items/UnsafeTemplate.php: -------------------------------------------------------------------------------- 1 | code = $code; 30 | } 31 | 32 | /** 33 | * Return this source code 34 | * 35 | * @return string 36 | */ 37 | public function __toString() 38 | { 39 | return (string) $this->code; 40 | } 41 | 42 | /** 43 | * {@inheritdoc} 44 | */ 45 | public function filterConfig($target) 46 | { 47 | return ($target === 'JS') ? $this : null; 48 | } 49 | } -------------------------------------------------------------------------------- /src/Configurator/JavaScript/Dictionary.php: -------------------------------------------------------------------------------- 1 | getArrayCopy(); 25 | if ($target === 'JS') 26 | { 27 | $value = new Dictionary(ConfigHelper::filterConfig($value, $target)); 28 | } 29 | 30 | return $value; 31 | } 32 | } -------------------------------------------------------------------------------- /src/Configurator/JavaScript/Minifiers/MatthiasMullieMinify.php: -------------------------------------------------------------------------------- 1 | minify(); 29 | } 30 | } -------------------------------------------------------------------------------- /src/Configurator/JavaScript/Minifiers/Noop.php: -------------------------------------------------------------------------------- 1 | httpClient = Http::getClient(); 27 | } 28 | } -------------------------------------------------------------------------------- /src/Configurator/RecursiveParser/AbstractRecursiveMatcher.php: -------------------------------------------------------------------------------- 1 | parser = $parser; 26 | } 27 | 28 | /** 29 | * Parse given string and return its value 30 | * 31 | * @param string $str 32 | * @param string $restrict Pipe-separated list of allowed matches (ignored if empty) 33 | * @return mixed 34 | */ 35 | protected function recurse(string $str, string $restrict = '') 36 | { 37 | return $this->parser->parse($str, $restrict)['value']; 38 | } 39 | } -------------------------------------------------------------------------------- /src/Configurator/RecursiveParser/CachingRecursiveParser.php: -------------------------------------------------------------------------------- 1 | cache[$restrict][$str])) 25 | { 26 | $this->cache[$restrict][$str] = parent::parse($str, $restrict); 27 | } 28 | 29 | return $this->cache[$restrict][$str]; 30 | } 31 | 32 | /** 33 | * {@inheritdoc} 34 | */ 35 | public function setMatchers(array $matchers): void 36 | { 37 | $this->cache = []; 38 | parent::setMatchers($matchers); 39 | } 40 | } -------------------------------------------------------------------------------- /src/Configurator/RecursiveParser/MatcherInterface.php: -------------------------------------------------------------------------------- 1 | '((?&Boolean)) and ((?&BooleanExpression)|(?&Boolean))', 19 | 'Boolean:BooleanSubExpr' => '\\( ((?&BooleanExpression)|(?&Boolean)) \\)', 20 | 'BooleanExpression:Or' => '((?&Boolean)) or ((?&BooleanExpression)|(?&Boolean))' 21 | ]; 22 | } 23 | 24 | /** 25 | * Convert a "and" operation 26 | * 27 | * @param string $expr1 28 | * @param string $expr2 29 | * @return string 30 | */ 31 | public function parseAnd($expr1, $expr2) 32 | { 33 | return $this->recurse($expr1) . '&&' . $this->recurse($expr2); 34 | } 35 | 36 | /** 37 | * Convert a boolean subexpression 38 | * 39 | * @param string $expr 40 | * @return string 41 | */ 42 | public function parseBooleanSubExpr($expr) 43 | { 44 | return '(' . $this->recurse($expr) . ')'; 45 | } 46 | 47 | /** 48 | * Convert a "or" operation 49 | * 50 | * @param string $expr1 51 | * @param string $expr2 52 | * @return string 53 | */ 54 | public function parseOr($expr1, $expr2) 55 | { 56 | return $this->recurse($expr1) . '||' . $this->recurse($expr2); 57 | } 58 | } -------------------------------------------------------------------------------- /src/Configurator/RendererGenerators/Unformatted.php: -------------------------------------------------------------------------------- 1 | isVoid()) ? ['autoClose' => true] : []; 21 | } 22 | } -------------------------------------------------------------------------------- /src/Configurator/RulesGenerators/AutoReopenFormattingElements.php: -------------------------------------------------------------------------------- 1 | isFormattingElement()) ? ['autoReopen' => true] : []; 21 | } 22 | } -------------------------------------------------------------------------------- /src/Configurator/RulesGenerators/BlockElementsCloseFormattingElements.php: -------------------------------------------------------------------------------- 1 | isBlock() && $trg->isFormattingElement()) ? ['closeParent'] : []; 21 | } 22 | } -------------------------------------------------------------------------------- /src/Configurator/RulesGenerators/BlockElementsFosterFormattingElements.php: -------------------------------------------------------------------------------- 1 | isBlock() && $src->isPassthrough() && $trg->isFormattingElement()) ? ['fosterParent'] : []; 21 | } 22 | } -------------------------------------------------------------------------------- /src/Configurator/RulesGenerators/DisableAutoLineBreaksIfNewLinesArePreserved.php: -------------------------------------------------------------------------------- 1 | preservesNewLines()) ? ['disableAutoLineBreaks' => true] : []; 21 | } 22 | } -------------------------------------------------------------------------------- /src/Configurator/RulesGenerators/EnforceOptionalEndTags.php: -------------------------------------------------------------------------------- 1 | closesParent($trg)) ? ['closeParent'] : []; 21 | } 22 | } -------------------------------------------------------------------------------- /src/Configurator/RulesGenerators/IgnoreTagsInCode.php: -------------------------------------------------------------------------------- 1 | evaluate('count(//code//xsl:apply-templates)')) ? ['ignoreTags' => true] : []; 21 | } 22 | } -------------------------------------------------------------------------------- /src/Configurator/RulesGenerators/IgnoreTextIfDisallowed.php: -------------------------------------------------------------------------------- 1 | allowsText()) ? [] : ['ignoreText' => true]; 21 | } 22 | } -------------------------------------------------------------------------------- /src/Configurator/RulesGenerators/IgnoreWhitespaceAroundBlockElements.php: -------------------------------------------------------------------------------- 1 | isBlock()) ? ['ignoreSurroundingWhitespace' => true] : []; 21 | } 22 | } -------------------------------------------------------------------------------- /src/Configurator/RulesGenerators/Interfaces/BooleanRulesGenerator.php: -------------------------------------------------------------------------------- 1 | bool] 19 | */ 20 | public function generateBooleanRules(TemplateInspector $src); 21 | } -------------------------------------------------------------------------------- /src/Configurator/RulesGenerators/Interfaces/TargetedRulesGenerator.php: -------------------------------------------------------------------------------- 1 | 24 | */ 25 | public function __construct() 26 | { 27 | $this->p = new TemplateInspector('

'); 28 | } 29 | 30 | /** 31 | * {@inheritdoc} 32 | */ 33 | public function generateBooleanRules(TemplateInspector $src) 34 | { 35 | $rules = []; 36 | 37 | if ($src->allowsChild($this->p) && $src->isBlock() && !$this->p->closesParent($src)) 38 | { 39 | $rules['createParagraphs'] = true; 40 | } 41 | 42 | if ($src->closesParent($this->p)) 43 | { 44 | $rules['breakParagraph'] = true; 45 | } 46 | 47 | return $rules; 48 | } 49 | } -------------------------------------------------------------------------------- /src/Configurator/RulesGenerators/TrimFirstLineInCodeBlocks.php: -------------------------------------------------------------------------------- 1 | evaluate('count(//pre//code//xsl:apply-templates)')) ? ['trimFirstLine' => true] : []; 21 | } 22 | } -------------------------------------------------------------------------------- /src/Configurator/TemplateCheck.php: -------------------------------------------------------------------------------- 1 | node 27 | * @param Tag $tag Tag this template belongs to 28 | * @return void 29 | */ 30 | abstract public function check(DOMElement $template, Tag $tag); 31 | } -------------------------------------------------------------------------------- /src/Configurator/TemplateChecks/DisallowAttributeSets.php: -------------------------------------------------------------------------------- 1 | 20 | * 21 | * Templates are checked outside of their stylesheet, which means we don't have access to the 22 | * declarations and we can't easily test them. Attribute sets are fairly 23 | * uncommon and there's little incentive to use them in small stylesheets 24 | * 25 | * @param DOMElement $template node 26 | * @param Tag $tag Tag this template belongs to 27 | * @return void 28 | */ 29 | public function check(DOMElement $template, Tag $tag) 30 | { 31 | $xpath = new DOMXPath($template->ownerDocument); 32 | $nodes = $xpath->query('//@use-attribute-sets'); 33 | 34 | if ($nodes->length) 35 | { 36 | throw new UnsafeTemplateException('Cannot assess the safety of attribute sets', $nodes->item(0)); 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /src/Configurator/TemplateChecks/DisallowCopy.php: -------------------------------------------------------------------------------- 1 | elements 19 | * 20 | * @param DOMElement $template node 21 | * @param Tag $tag Tag this template belongs to 22 | * @return void 23 | */ 24 | public function check(DOMElement $template, Tag $tag) 25 | { 26 | $nodes = $template->getElementsByTagNameNS(self::XMLNS_XSL, 'copy'); 27 | $node = $nodes->item(0); 28 | 29 | if ($node) 30 | { 31 | throw new UnsafeTemplateException("Cannot assess the safety of an '" . $node->nodeName . "' element", $node); 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /src/Configurator/TemplateChecks/DisallowDisableOutputEscaping.php: -------------------------------------------------------------------------------- 1 | node 22 | * @param Tag $tag Tag this template belongs to 23 | * @return void 24 | */ 25 | public function check(DOMElement $template, Tag $tag) 26 | { 27 | $xpath = new DOMXPath($template->ownerDocument); 28 | $node = $xpath->query('//@disable-output-escaping')->item(0); 29 | 30 | if ($node) 31 | { 32 | throw new UnsafeTemplateException("The template contains a 'disable-output-escaping' attribute", $node); 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /src/Configurator/TemplateChecks/DisallowDynamicAttributeNames.php: -------------------------------------------------------------------------------- 1 | node using a dynamic name 19 | * 20 | * @param DOMElement $template node 21 | * @param Tag $tag Tag this template belongs to 22 | * @return void 23 | */ 24 | public function check(DOMElement $template, Tag $tag) 25 | { 26 | $nodes = $template->getElementsByTagNameNS(self::XMLNS_XSL, 'attribute'); 27 | foreach ($nodes as $node) 28 | { 29 | if (strpos($node->getAttribute('name'), '{') !== false) 30 | { 31 | throw new UnsafeTemplateException('Dynamic names are disallowed', $node); 32 | } 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /src/Configurator/TemplateChecks/DisallowDynamicElementNames.php: -------------------------------------------------------------------------------- 1 | node using a dynamic name 19 | * 20 | * @param DOMElement $template node 21 | * @param Tag $tag Tag this template belongs to 22 | * @return void 23 | */ 24 | public function check(DOMElement $template, Tag $tag) 25 | { 26 | $nodes = $template->getElementsByTagNameNS(self::XMLNS_XSL, 'element'); 27 | foreach ($nodes as $node) 28 | { 29 | if (strpos($node->getAttribute('name'), '{') !== false) 30 | { 31 | throw new UnsafeTemplateException('Dynamic names are disallowed', $node); 32 | } 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /src/Configurator/TemplateChecks/DisallowElement.php: -------------------------------------------------------------------------------- 1 | elName = strtolower($elName); 32 | } 33 | 34 | /** 35 | * Test for the presence of an element of given name 36 | * 37 | * @param DOMElement $template node 38 | * @param Tag $tag Tag this template belongs to 39 | * @return void 40 | */ 41 | public function check(DOMElement $template, Tag $tag) 42 | { 43 | $xpath = new DOMXPath($template->ownerDocument); 44 | $query 45 | = '//*[translate(local-name(), "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz") = "' . $this->elName . '"]' 46 | . '|' 47 | . '//xsl:element[translate(@name,"ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz") = "' . $this->elName . '"]'; 48 | 49 | $node = $xpath->query($query)->item(0); 50 | if ($node) 51 | { 52 | throw new UnsafeTemplateException("Element '" . $this->elName . "' is disallowed", $node); 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /src/Configurator/TemplateChecks/DisallowElementNS.php: -------------------------------------------------------------------------------- 1 | namespaceURI = $namespaceURI; 36 | $this->elName = $elName; 37 | } 38 | 39 | /** 40 | * Test for the presence of an element of given name in given namespace 41 | * 42 | * @param DOMElement $template node 43 | * @param Tag $tag Tag this template belongs to 44 | * @return void 45 | */ 46 | public function check(DOMElement $template, Tag $tag) 47 | { 48 | $node = $template->getElementsByTagNameNS($this->namespaceURI, $this->elName)->item(0); 49 | 50 | if ($node) 51 | { 52 | throw new UnsafeTemplateException("Element '" . $node->nodeName . "' is disallowed", $node); 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /src/Configurator/TemplateChecks/DisallowFlashFullScreen.php: -------------------------------------------------------------------------------- 1 | 1, 28 | 'false' => 0 29 | ]; 30 | 31 | /** 32 | * Constructor 33 | * 34 | * @param bool $onlyIfDynamic Whether this restriction applies only to elements using any kind 35 | * of dynamic markup: XSL elements or attribute value templates 36 | */ 37 | public function __construct($onlyIfDynamic = false) 38 | { 39 | parent::__construct('false', $onlyIfDynamic); 40 | } 41 | } -------------------------------------------------------------------------------- /src/Configurator/TemplateChecks/DisallowNodeByXPath.php: -------------------------------------------------------------------------------- 1 | query = $query; 31 | } 32 | 33 | /** 34 | * Test for the presence of an element of given name 35 | * 36 | * @param DOMElement $template node 37 | * @param Tag $tag Tag this template belongs to 38 | * @return void 39 | */ 40 | public function check(DOMElement $template, Tag $tag) 41 | { 42 | $xpath = new DOMXPath($template->ownerDocument); 43 | 44 | foreach ($xpath->query($this->query) as $node) 45 | { 46 | throw new UnsafeTemplateException("Node '" . $node->nodeName . "' is disallowed because it matches '" . $this->query . "'", $node); 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /src/Configurator/TemplateChecks/DisallowObjectParamsWithGeneratedName.php: -------------------------------------------------------------------------------- 1 | elements with a generated "name" attribute 20 | * 21 | * This check will reject elements whose "name" attribute is generated by an 22 | * element. This is a setup that has no practical use and should be eliminated 23 | * because it makes it much harder to check the param's name, and therefore infer the type of 24 | * content it expects 25 | * 26 | * @param DOMElement $template node 27 | * @param Tag $tag Tag this template belongs to 28 | * @return void 29 | */ 30 | public function check(DOMElement $template, Tag $tag) 31 | { 32 | $xpath = new DOMXPath($template->ownerDocument); 33 | $query = '//object//param[contains(@name, "{") or .//xsl:attribute[translate(@name, "NAME", "name") = "name"]]'; 34 | $nodes = $xpath->query($query); 35 | 36 | foreach ($nodes as $node) 37 | { 38 | throw new UnsafeTemplateException("A 'param' element with a suspect name has been found", $node); 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /src/Configurator/TemplateChecks/DisallowUncompilableXSL.php: -------------------------------------------------------------------------------- 1 | getAttribute('select'); 22 | if (!preg_match('#^@[-\\w]+(?:\\s*\\|\\s*@[-\\w]+)*$#', $expr) && $expr !== '@*') 23 | { 24 | throw new RuntimeException("Unsupported xsl:copy-of expression '" . $expr . "'"); 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /src/Configurator/TemplateChecks/DisallowUnsafeCopyOf.php: -------------------------------------------------------------------------------- 1 | elements 19 | * 20 | * Any select expression that is not a set of named attributes is considered unsafe 21 | * 22 | * @param DOMElement $template node 23 | * @param Tag $tag Tag this template belongs to 24 | * @return void 25 | */ 26 | public function check(DOMElement $template, Tag $tag) 27 | { 28 | $nodes = $template->getElementsByTagNameNS(self::XMLNS_XSL, 'copy-of'); 29 | foreach ($nodes as $node) 30 | { 31 | $expr = $node->getAttribute('select'); 32 | 33 | if (!preg_match('#^@[-\\w]*(?:\\s*\\|\\s*@[-\\w]*)*$#D', $expr)) 34 | { 35 | throw new UnsafeTemplateException("Cannot assess the safety of '" . $node->nodeName . "' select expression '" . $expr . "'", $node); 36 | } 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /src/Configurator/TemplateChecks/DisallowUnsafeDynamicCSS.php: -------------------------------------------------------------------------------- 1 | ownerDocument); 23 | } 24 | 25 | /** 26 | * {@inheritdoc} 27 | */ 28 | protected function isExpressionSafe($expr) 29 | { 30 | return XPathHelper::isExpressionNumeric($expr); 31 | } 32 | 33 | /** 34 | * {@inheritdoc} 35 | */ 36 | protected function isSafe(Attribute $attribute) 37 | { 38 | return $attribute->isSafeInCSS(); 39 | } 40 | } -------------------------------------------------------------------------------- /src/Configurator/TemplateChecks/DisallowUnsafeDynamicJS.php: -------------------------------------------------------------------------------- 1 | ownerDocument); 23 | } 24 | 25 | /** 26 | * {@inheritdoc} 27 | */ 28 | protected function isExpressionSafe($expr) 29 | { 30 | return XPathHelper::isExpressionNumeric($expr); 31 | } 32 | 33 | /** 34 | * {@inheritdoc} 35 | */ 36 | protected function isSafe(Attribute $attribute) 37 | { 38 | return $attribute->isSafeInJS(); 39 | } 40 | } -------------------------------------------------------------------------------- /src/Configurator/TemplateChecks/RestrictFlashNetworking.php: -------------------------------------------------------------------------------- 1 | 3, 28 | 'internal' => 2, 29 | 'none' => 1 30 | ]; 31 | } -------------------------------------------------------------------------------- /src/Configurator/TemplateChecks/RestrictFlashScriptAccess.php: -------------------------------------------------------------------------------- 1 | 3, 28 | 'samedomain' => 2, 29 | 'never' => 1 30 | ]; 31 | } -------------------------------------------------------------------------------- /src/Configurator/TemplateNormalizations/AddAttributeValueToElements.php: -------------------------------------------------------------------------------- 1 | attrName = $attrName; 35 | $this->queries = [$query]; 36 | $this->value = $value; 37 | } 38 | 39 | /** 40 | * Explode a string of space-separated values into an array 41 | * 42 | * @param string $attrValue Attribute's value 43 | * @return string[] 44 | */ 45 | protected function getValues(string $attrValue): array 46 | { 47 | return preg_match_all('(\\S++)', $attrValue, $m) ? $m[0] : []; 48 | } 49 | 50 | /** 51 | * {@inheritdoc} 52 | */ 53 | protected function normalizeElement(Element $element): void 54 | { 55 | $currentValues = $this->getValues($element->getAttribute($this->attrName)); 56 | if (!in_array($this->value, $currentValues, true)) 57 | { 58 | $currentValues[] = $this->value; 59 | 60 | $element->setAttribute($this->attrName, implode(' ', $currentValues)); 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /src/Configurator/TemplateNormalizations/ConvertCurlyExpressionsInText.php: -------------------------------------------------------------------------------- 1 | {$FOO}{@bar} 17 | * with 18 | * 19 | */ 20 | class ConvertCurlyExpressionsInText extends AbstractNormalization 21 | { 22 | /** 23 | * {@inheritdoc} 24 | */ 25 | protected array $queries = ['//*[namespace-uri() != "' . self::XMLNS_XSL . '"]/text()[contains(., "{@") or contains(., "{$")]']; 26 | 27 | /** 28 | * Insert a text node before given node 29 | */ 30 | protected function insertTextBefore(string $text, Text $node): void 31 | { 32 | if ($text > '') 33 | { 34 | $node->before($this->createPolymorphicText($text)); 35 | } 36 | } 37 | 38 | /** 39 | * {@inheritdoc} 40 | */ 41 | protected function normalizeText(Text $node): void 42 | { 43 | preg_match_all( 44 | '#\\{([$@][-\\w]+)\\}#', 45 | $node->textContent, 46 | $matches, 47 | PREG_SET_ORDER | PREG_OFFSET_CAPTURE 48 | ); 49 | 50 | $lastPos = 0; 51 | foreach ($matches as $m) 52 | { 53 | $pos = $m[0][1]; 54 | 55 | // Catch up to current position 56 | if ($pos > $lastPos) 57 | { 58 | $text = substr($node->textContent, $lastPos, $pos - $lastPos); 59 | $this->insertTextBefore($text, $node); 60 | } 61 | $lastPos = $pos + strlen($m[0][0]); 62 | 63 | // Add the xsl:value-of element 64 | $node->beforeXslValueOf($m[1][0]); 65 | } 66 | 67 | // Append the rest of the text 68 | $text = substr($node->textContent, $lastPos); 69 | $this->insertTextBefore($text, $node); 70 | 71 | // Now remove the old text node 72 | $node->remove(); 73 | } 74 | } -------------------------------------------------------------------------------- /src/Configurator/TemplateNormalizations/Custom.php: -------------------------------------------------------------------------------- 1 | callback = $callback; 27 | } 28 | 29 | /** 30 | * Call the user-supplied callback 31 | * 32 | * @param Element $template node 33 | * @return void 34 | */ 35 | public function normalize(Element $template): void 36 | { 37 | call_user_func($this->callback, $template); 38 | $this->reset(); 39 | } 40 | } -------------------------------------------------------------------------------- /src/Configurator/TemplateNormalizations/DeoptimizeIf.php: -------------------------------------------------------------------------------- 1 | replaceWithXslChoose(); 28 | $when = $choose->appendXslWhen($element->getAttribute('test')); 29 | $when->append(...$element->childNodes); 30 | } 31 | } -------------------------------------------------------------------------------- /src/Configurator/TemplateNormalizations/EnforceHTMLOmittedEndTags.php: -------------------------------------------------------------------------------- 1 | .

.

18 | * with 19 | *

.

.

20 | */ 21 | class EnforceHTMLOmittedEndTags extends AbstractNormalization 22 | { 23 | /** 24 | * {@inheritdoc} 25 | */ 26 | protected array $queries = ['//*[namespace-uri() = ""]/*[namespace-uri() = ""]']; 27 | 28 | /** 29 | * {@inheritdoc} 30 | */ 31 | protected function normalizeElement(Element $element): void 32 | { 33 | $parentNode = $element->parentNode; 34 | if (ElementInspector::isVoid($parentNode) || ElementInspector::closesParent($element, $parentNode)) 35 | { 36 | $this->reparentElement($element); 37 | } 38 | } 39 | 40 | /** 41 | * Move given element and its following siblings after its parent element 42 | * 43 | * @param Element $element First element to move 44 | * @return void 45 | */ 46 | protected function reparentElement(Element $element) 47 | { 48 | $parentNode = $element->parentNode; 49 | do 50 | { 51 | $lastChild = $parentNode->lastChild; 52 | $parentNode->parentNode->insertBefore($lastChild, $parentNode->nextSibling); 53 | } 54 | while (!$lastChild->isSameNode($element)); 55 | } 56 | } -------------------------------------------------------------------------------- /src/Configurator/TemplateNormalizations/FixUnescapedCurlyBracesInHtmlAttributes.php: -------------------------------------------------------------------------------- 1 | 17 | *
18 | * with 19 | *
20 | *
21 | */ 22 | class FixUnescapedCurlyBracesInHtmlAttributes extends AbstractNormalization 23 | { 24 | /** 25 | * {@inheritdoc} 26 | */ 27 | protected array $queries = ['//*[namespace-uri() != "' . self::XMLNS_XSL . '"]/@*[contains(., "{")]']; 28 | 29 | /** 30 | * {@inheritdoc} 31 | */ 32 | protected function normalizeAttribute(Attr $attribute): void 33 | { 34 | $match = [ 35 | '(\\b(?:do|else|(?:if|while)\\s*\\(.*?\\))\\s*\\{(?![{@]))', 36 | '(\\bfunction\\s*\\w*\\s*\\([^\\)]*\\)\\s*\\{(?!\\{))', 37 | '(=(?:>|>)\\s*\\{(?!\\{))', 38 | '((?value); 49 | $attribute->value = htmlspecialchars($attrValue, ENT_NOQUOTES, 'UTF-8'); 50 | } 51 | } -------------------------------------------------------------------------------- /src/Configurator/TemplateNormalizations/InlineAttributes.php: -------------------------------------------------------------------------------- 1 | ... 18 | * with 19 | * ... 20 | */ 21 | class InlineAttributes extends AbstractNormalization 22 | { 23 | /** 24 | * {@inheritdoc} 25 | */ 26 | protected array $queries = ['//*[namespace-uri() != "' . self::XMLNS_XSL . '"]/xsl:attribute']; 27 | 28 | /** 29 | * {@inheritdoc} 30 | */ 31 | protected function normalizeElement(Element $element): void 32 | { 33 | $value = ''; 34 | foreach ($element->childNodes as $node) 35 | { 36 | if ($node instanceof Text || $this->isXsl($node, 'text')) 37 | { 38 | $value .= preg_replace('([{}])', '$0$0', $node->textContent); 39 | } 40 | elseif ($this->isXsl($node, 'value-of')) 41 | { 42 | $value .= '{' . $node->getAttribute('select') . '}'; 43 | } 44 | else 45 | { 46 | // Can't inline this attribute 47 | return; 48 | } 49 | } 50 | $element->parentNode->setAttribute($element->getAttribute('name'), $value); 51 | $element->remove(); 52 | } 53 | } -------------------------------------------------------------------------------- /src/Configurator/TemplateNormalizations/InlineCDATA.php: -------------------------------------------------------------------------------- 1 | replaceWith($this->createPolymorphicText($cdata->textContent)); 25 | } 26 | } -------------------------------------------------------------------------------- /src/Configurator/TemplateNormalizations/InlineElements.php: -------------------------------------------------------------------------------- 1 |
18 | * with 19 | *
20 | */ 21 | class InlineElements extends AbstractNormalization 22 | { 23 | /** 24 | * {@inheritdoc} 25 | */ 26 | protected array $queries = ['//xsl:element']; 27 | 28 | /** 29 | * {@inheritdoc} 30 | */ 31 | protected function normalizeElement(Element $element): void 32 | { 33 | $elName = $element->getAttribute('name'); 34 | $dom = $element->ownerDocument; 35 | 36 | try 37 | { 38 | // Create the new static element 39 | $newElement = ($element->hasAttribute('namespace')) 40 | ? $dom->createElementNS($element->getAttribute('namespace'), $elName) 41 | : $dom->createElement($elName); 42 | } 43 | catch (DOMException $e) 44 | { 45 | // Ignore this element if an exception got thrown 46 | return; 47 | } 48 | 49 | // Replace the old with it. We do it now so that libxml doesn't have to 50 | // redeclare the XSL namespace 51 | $element->replaceWith($newElement); 52 | 53 | // Move all the nodes from the old element to the new one 54 | $newElement->append(...$element->childNodes); 55 | } 56 | } -------------------------------------------------------------------------------- /src/Configurator/TemplateNormalizations/InlineTextElements.php: -------------------------------------------------------------------------------- 1 | nextSibling && $element->nextSibling->nodeType === XML_TEXT_NODE); 28 | } 29 | 30 | /** 31 | * Test whether an element is preceded by a text node 32 | * 33 | * @param Element $element 34 | * @return bool 35 | */ 36 | protected function isPrecededByText(Element $element) 37 | { 38 | return ($element->previousSibling && $element->previousSibling->nodeType === XML_TEXT_NODE); 39 | } 40 | 41 | /** 42 | * {@inheritdoc} 43 | */ 44 | protected function normalizeElement(Element $element): void 45 | { 46 | // If this node's content is whitespace, ensure it's preceded or followed by a text node 47 | if (trim($element->textContent) === '') 48 | { 49 | if (!$this->isFollowedByText($element) && !$this->isPrecededByText($element)) 50 | { 51 | // This would become inter-element whitespace, therefore we can't inline 52 | return; 53 | } 54 | } 55 | $element->replaceWith($element->textContent); 56 | } 57 | } -------------------------------------------------------------------------------- /src/Configurator/TemplateNormalizations/InlineXPathLiterals.php: -------------------------------------------------------------------------------- 1 | getTextContent($token[1]); 53 | if ($textContent !== false) 54 | { 55 | // Turn this token into a literal 56 | $token = ['literal', $textContent]; 57 | } 58 | } 59 | 60 | return $token; 61 | } 62 | ); 63 | } 64 | 65 | /** 66 | * {@inheritdoc} 67 | */ 68 | protected function normalizeElement(Element $element): void 69 | { 70 | $textContent = $this->getTextContent($element->getAttribute('select')); 71 | if ($textContent !== false) 72 | { 73 | $element->replaceWith($this->createPolymorphicText($textContent)); 74 | } 75 | } 76 | } -------------------------------------------------------------------------------- /src/Configurator/TemplateNormalizations/MergeConsecutiveCopyOf.php: -------------------------------------------------------------------------------- 1 | nextSiblingIsCopyOf($element)) 25 | { 26 | $element->setAttribute('select', $element->getAttribute('select') . '|' . $element->nextSibling->getAttribute('select')); 27 | $element->nextSibling->remove(); 28 | } 29 | } 30 | 31 | /** 32 | * Test whether the next sibling to given element is an xsl:copy-of element 33 | * 34 | * @param Element $element Context node 35 | * @return bool 36 | */ 37 | protected function nextSiblingIsCopyOf(Element $element) 38 | { 39 | return ($element->nextSibling && $this->isXsl($element->nextSibling, 'copy-of')); 40 | } 41 | } -------------------------------------------------------------------------------- /src/Configurator/TemplateNormalizations/MinifyInlineCSS.php: -------------------------------------------------------------------------------- 1 | nodeValue; 26 | 27 | // Only minify if the value does not contain any XPath expression that's not an attribute 28 | if (!preg_match('(\\{(?!@\\w+\\}))', $css)) 29 | { 30 | $attribute->nodeValue = $this->minify($css); 31 | } 32 | } 33 | 34 | /** 35 | * Minify a CSS string 36 | * 37 | * @param string $css Original CSS 38 | * @return string Minified CSS 39 | */ 40 | protected function minify($css) 41 | { 42 | $css = trim($css, " \n\t;"); 43 | $css = preg_replace('(\\s*([,:;])\\s*)', '$1', $css); 44 | $css = preg_replace_callback( 45 | '((?<=[\\s:])#[0-9a-f]{3,6})i', 46 | function ($m) 47 | { 48 | return strtolower($m[0]); 49 | }, 50 | $css 51 | ); 52 | $css = preg_replace('((?<=[\\s:])#([0-9a-f])\\1([0-9a-f])\\2([0-9a-f])\\3)', '#$1$2$3', $css); 53 | $css = preg_replace('((?<=[\\s:])#f00\\b)', 'red', $css); 54 | $css = preg_replace('((?<=[\\s:])0px\\b)', '0', $css); 55 | 56 | return $css; 57 | } 58 | } -------------------------------------------------------------------------------- /src/Configurator/TemplateNormalizations/MinifyXPathExpressions.php: -------------------------------------------------------------------------------- 1 | parentNode; 27 | if (!$this->isXsl($element)) 28 | { 29 | // Replace XPath expressions in non-XSL elements 30 | $this->replaceAVT($attribute); 31 | } 32 | elseif (in_array($attribute->nodeName, ['match', 'select', 'test'], true)) 33 | { 34 | // Replace the content of match, select and test attributes of an XSL element 35 | $expr = XPathHelper::minify($attribute->nodeValue); 36 | $element->setAttribute($attribute->nodeName, $expr); 37 | } 38 | } 39 | 40 | /** 41 | * Minify XPath expressions in given attribute 42 | */ 43 | protected function replaceAVT(Attr $attribute) 44 | { 45 | AVTHelper::replace( 46 | $attribute, 47 | function ($token) 48 | { 49 | if ($token[0] === 'expression') 50 | { 51 | $token[1] = XPathHelper::minify($token[1]); 52 | } 53 | 54 | return $token; 55 | } 56 | ); 57 | } 58 | } -------------------------------------------------------------------------------- /src/Configurator/TemplateNormalizations/NormalizeAttributeNames.php: -------------------------------------------------------------------------------- 1 | parentNode->setAttribute($this->lowercase($attribute->localName), $attribute->value); 29 | $attribute->parentNode->removeAttributeNode($attribute); 30 | } 31 | 32 | /** 33 | * {@inheritdoc} 34 | */ 35 | protected function normalizeElement(Element $element): void 36 | { 37 | $element->setAttribute('name', $this->lowercase($element->getAttribute('name'))); 38 | } 39 | } -------------------------------------------------------------------------------- /src/Configurator/TemplateNormalizations/OptimizeConditionalAttributes.php: -------------------------------------------------------------------------------- 1 | , e.g. 16 | * 17 | * 18 | * 19 | * 20 | * 21 | * into 22 | * 23 | */ 24 | class OptimizeConditionalAttributes extends AbstractNormalization 25 | { 26 | /** 27 | * {@inheritdoc} 28 | */ 29 | protected array $queries = ['//xsl:if[starts-with(@test, "@")][count(descendant::node()) = 2][xsl:attribute[@name = substring(../@test, 2)][xsl:value-of[@select = ../../@test]]]']; 30 | 31 | /** 32 | * {@inheritdoc} 33 | */ 34 | protected function normalizeElement(Element $element): void 35 | { 36 | $element->replaceWithXslCopyOf($element->getAttribute('test')); 37 | } 38 | } -------------------------------------------------------------------------------- /src/Configurator/TemplateNormalizations/OptimizeConditionalValueOf.php: -------------------------------------------------------------------------------- 1 | tests around 14 | * 15 | * NOTE: should be performed before attributes are inlined for maximum effect 16 | */ 17 | class OptimizeConditionalValueOf extends AbstractNormalization 18 | { 19 | /** 20 | * {@inheritdoc} 21 | */ 22 | protected array $queries = ['//xsl:if[count(descendant::node()) = 1]/xsl:value-of']; 23 | 24 | /** 25 | * {@inheritdoc} 26 | */ 27 | protected function normalizeElement(Element $element): void 28 | { 29 | $if = $element->parentNode; 30 | $test = $if->getAttribute('test'); 31 | $select = $element->getAttribute('select'); 32 | 33 | // Ensure that the expressions match, and that they select one single attribute 34 | if ($select !== $test || !preg_match('#^@[-\\w]+$#D', $select)) 35 | { 36 | return; 37 | } 38 | 39 | // Replace the xsl:if element with the xsl:value-of element 40 | $if->replaceWith($element); 41 | } 42 | } -------------------------------------------------------------------------------- /src/Configurator/TemplateNormalizations/OptimizeNestedConditionals.php: -------------------------------------------------------------------------------- 1 | parentNode; 34 | $outerChoose = $otherwise->parentNode; 35 | $outerChoose->append(...$element->childNodes); 36 | 37 | $otherwise->remove(); 38 | } 39 | } -------------------------------------------------------------------------------- /src/Configurator/TemplateNormalizations/PreserveSingleSpaces.php: -------------------------------------------------------------------------------- 1 | replaceWithXslText(' '); 28 | } 29 | } -------------------------------------------------------------------------------- /src/Configurator/TemplateNormalizations/RemoveComments.php: -------------------------------------------------------------------------------- 1 | remove(); 28 | } 29 | } -------------------------------------------------------------------------------- /src/Configurator/TemplateNormalizations/RemoveInterElementWhitespace.php: -------------------------------------------------------------------------------- 1 | remove(); 28 | } 29 | } -------------------------------------------------------------------------------- /src/Configurator/TemplateNormalizations/RemoveLivePreviewAttributes.php: -------------------------------------------------------------------------------- 1 | parentNode->removeAttributeNode($attribute); 32 | } 33 | 34 | /** 35 | * {@inheritdoc} 36 | */ 37 | protected function normalizeElement(Element $element): void 38 | { 39 | $element->remove(); 40 | } 41 | } -------------------------------------------------------------------------------- /src/Configurator/TemplateNormalizations/RenameLivePreviewEvent.php: -------------------------------------------------------------------------------- 1 | value = 'data-s9e-livepreview-onrender'; 32 | } 33 | 34 | /** 35 | * {@inheritdoc} 36 | */ 37 | protected function normalizeElement(Element $element): void 38 | { 39 | $value = $element->getAttribute('data-s9e-livepreview-postprocess'); 40 | $element->setAttribute('data-s9e-livepreview-onrender', $value); 41 | $element->removeAttribute('data-s9e-livepreview-postprocess'); 42 | } 43 | } -------------------------------------------------------------------------------- /src/Configurator/TemplateNormalizations/SetAttributeOnElements.php: -------------------------------------------------------------------------------- 1 | getAttribute('rel'))) 35 | { 36 | parent::normalizeElement($element); 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /src/Configurator/TemplateNormalizations/SortAttributesByName.php: -------------------------------------------------------------------------------- 1 | attributes as $name => $attribute) 31 | { 32 | $attributes[$name] = $element->removeAttributeNode($attribute); 33 | } 34 | 35 | ksort($attributes); 36 | foreach ($attributes as $attribute) 37 | { 38 | $element->setAttributeNode($attribute); 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /src/Configurator/TemplateNormalizations/TransposeComments.php: -------------------------------------------------------------------------------- 1 | replaceWithXslComment($comment->textContent); 28 | } 29 | } -------------------------------------------------------------------------------- /src/Configurator/Validators/AttributeName.php: -------------------------------------------------------------------------------- 1 | } map 6 | * @return {*} 7 | */ 8 | filter: function(attrValue, map) 9 | { 10 | let i = -1, cnt = map.length; 11 | while (++i < cnt) 12 | { 13 | if (map[i][0].test(attrValue)) 14 | { 15 | return map[i][1]; 16 | } 17 | } 18 | 19 | return attrValue; 20 | } 21 | }; -------------------------------------------------------------------------------- /src/Parser/AttributeFilters/MapFilter.php: -------------------------------------------------------------------------------- 1 | , ]] 19 | * @return mixed Filtered value, or FALSE if invalid 20 | */ 21 | public static function filter($attrValue, array $map) 22 | { 23 | foreach ($map as $pair) 24 | { 25 | if (preg_match($pair[0], $attrValue)) 26 | { 27 | return $pair[1]; 28 | } 29 | } 30 | 31 | return $attrValue; 32 | } 33 | } -------------------------------------------------------------------------------- /src/Parser/AttributeFilters/NetworkFilter.js: -------------------------------------------------------------------------------- 1 | const NetworkFilter = 2 | { 3 | /** 4 | * @param {*} attrValue 5 | * @return {*} 6 | */ 7 | filterIp: function(attrValue) 8 | { 9 | if (/^[\d.]+$/.test(attrValue)) 10 | { 11 | return NetworkFilter.filterIpv4(attrValue); 12 | } 13 | 14 | if (/^[\da-f:]+$/i.test(attrValue)) 15 | { 16 | return NetworkFilter.filterIpv6(attrValue); 17 | } 18 | 19 | return false; 20 | }, 21 | 22 | /** 23 | * @param {*} attrValue 24 | * @return {*} 25 | */ 26 | filterIpport: function(attrValue) 27 | { 28 | let m, ip; 29 | 30 | if (m = /^\[([\da-f:]+)(\]:[1-9]\d*)$/i.exec(attrValue)) 31 | { 32 | ip = NetworkFilter.filterIpv6(m[1]); 33 | 34 | if (ip === false) 35 | { 36 | return false; 37 | } 38 | 39 | return '[' + ip + m[2]; 40 | } 41 | 42 | if (m = /^([\d.]+)(:[1-9]\d*)$/.exec(attrValue)) 43 | { 44 | ip = NetworkFilter.filterIpv4(m[1]); 45 | 46 | if (ip === false) 47 | { 48 | return false; 49 | } 50 | 51 | return ip + m[2]; 52 | } 53 | 54 | return false; 55 | }, 56 | 57 | /** 58 | * @param {*} attrValue 59 | * @return {*} 60 | */ 61 | filterIpv4: function(attrValue) 62 | { 63 | if (!/^\d+\.\d+\.\d+\.\d+$/.test(attrValue)) 64 | { 65 | return false; 66 | } 67 | 68 | let i = 4, p = attrValue.split('.'); 69 | while (--i >= 0) 70 | { 71 | // NOTE: ext/filter doesn't support octal notation 72 | if (p[i][0] === '0' || p[i] > 255) 73 | { 74 | return false; 75 | } 76 | } 77 | 78 | return attrValue; 79 | }, 80 | 81 | /** 82 | * @param {*} attrValue 83 | * @return {*} 84 | */ 85 | filterIpv6: function(attrValue) 86 | { 87 | return /^([\da-f]{0,4}:){2,7}(?:[\da-f]{0,4}|\d+\.\d+\.\d+\.\d+)$/.test(attrValue) ? attrValue : false; 88 | } 89 | }; -------------------------------------------------------------------------------- /src/Parser/AttributeFilters/NetworkFilter.php: -------------------------------------------------------------------------------- 1 | max) 55 | { 56 | if (logger) 57 | { 58 | logger.warn( 59 | 'Value outside of range, adjusted down to max value', 60 | { 61 | 'attrValue' : attrValue, 62 | 'min' : min, 63 | 'max' : max 64 | } 65 | ); 66 | } 67 | 68 | return max; 69 | } 70 | 71 | return attrValue; 72 | }, 73 | 74 | /** 75 | * @param {*} attrValue 76 | * @return {*} 77 | */ 78 | filterUint: function(attrValue) 79 | { 80 | return /^(?:0|[1-9]\d*)$/.test(attrValue) ? attrValue : false; 81 | } 82 | }; -------------------------------------------------------------------------------- /src/Parser/AttributeFilters/RegexpFilter.js: -------------------------------------------------------------------------------- 1 | const RegexpFilter = 2 | { 3 | /** 4 | * @param {*} attrValue 5 | * @param {!RegExp} regexp 6 | * @return {*} 7 | */ 8 | filter: function(attrValue, regexp) 9 | { 10 | return regexp.test(attrValue) ? attrValue : false; 11 | } 12 | }; -------------------------------------------------------------------------------- /src/Parser/AttributeFilters/RegexpFilter.php: -------------------------------------------------------------------------------- 1 | ['regexp' => $regexp] 23 | ]); 24 | } 25 | } -------------------------------------------------------------------------------- /src/Parser/AttributeFilters/TimestampFilter.js: -------------------------------------------------------------------------------- 1 | const TimestampFilter = 2 | { 3 | /** 4 | * @param {*} attrValue 5 | * @return {*} 6 | */ 7 | filter: function(attrValue) 8 | { 9 | let m = /^(?=\d)(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?$/.exec(attrValue); 10 | if (m) 11 | { 12 | return 3600 * (m[1] || 0) + 60 * (m[2] || 0) + (+m[3] || 0); 13 | } 14 | 15 | return NumericFilter.filterUint(attrValue); 16 | } 17 | }; -------------------------------------------------------------------------------- /src/Parser/AttributeFilters/TimestampFilter.php: -------------------------------------------------------------------------------- 1 | 12 | b.innerHTML = str.replace(/&"]/g, 28 | /** 29 | * @param {string} c 30 | * @return {string} 31 | */ 32 | (c) => 33 | { 34 | const t = { 35 | '<' : '<', 36 | '>' : '>', 37 | '&' : '&', 38 | '"' : '"' 39 | }; 40 | return t[c]; 41 | } 42 | ); 43 | } 44 | 45 | /** 46 | * @param {string} str 47 | * @return {string} 48 | */ 49 | function htmlspecialchars_noquotes(str) 50 | { 51 | return str.replace( 52 | /[<>&]/g, 53 | /** 54 | * @param {string} c 55 | * @return {string} 56 | */ 57 | (c) => 58 | { 59 | const t = { 60 | '<' : '<', 61 | '>' : '>', 62 | '&' : '&' 63 | }; 64 | return t[c]; 65 | } 66 | ); 67 | } 68 | 69 | /** 70 | * @param {string} str 71 | * @return {string} 72 | */ 73 | function rawurlencode(str) 74 | { 75 | return encodeURIComponent(str).replace( 76 | /[!'()*]/g, 77 | /** 78 | * @param {string} c 79 | * @return {string} 80 | */ 81 | (c) => 82 | { 83 | return '%' + c.charCodeAt(0).toString(16).toUpperCase(); 84 | } 85 | ); 86 | } 87 | 88 | /** 89 | * @return {boolean} 90 | */ 91 | function returnFalse() 92 | { 93 | return false; 94 | } 95 | 96 | /** 97 | * @return {boolean} 98 | */ 99 | function returnTrue() 100 | { 101 | return true; 102 | } -------------------------------------------------------------------------------- /src/Plugins/AbstractStaticUrlReplacer/AbstractParser.php: -------------------------------------------------------------------------------- 1 | config['tagName']; 22 | $attrName = $this->config['attrName']; 23 | $prio = $this->tagPriority; 24 | foreach ($matches as $m) 25 | { 26 | $this->parser->addTagPair($tagName, $m[0][1], 0, $m[0][1] + strlen($m[0][0]), 0, $prio) 27 | ->setAttribute($attrName, $m[0][0]); 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /src/Plugins/AbstractStaticUrlReplacer/Parser.js: -------------------------------------------------------------------------------- 1 | const tagName = config.tagName, 2 | attrName = config.attrName, 3 | prio = tagPriority || 0; 4 | 5 | matches.forEach((m) => 6 | { 7 | addTagPair(tagName, m[0][1], 0, m[0][1] + m[0][0].length, 0, prio).setAttribute(attrName, m[0][0]); 8 | }); -------------------------------------------------------------------------------- /src/Plugins/Autoemail/Configurator.php: -------------------------------------------------------------------------------- 1 | configurator->tags[$this->tagName])) 42 | { 43 | return; 44 | } 45 | 46 | // Create a tag 47 | $tag = $this->configurator->tags->add($this->tagName); 48 | 49 | // Add an attribute using the default email filter 50 | $filter = $this->configurator->attributeFilters->get('#email'); 51 | $tag->attributes->add($this->attrName)->filterChain->append($filter); 52 | 53 | // Set the default template 54 | $tag->template = ''; 55 | } 56 | } -------------------------------------------------------------------------------- /src/Plugins/Autoemail/Parser.js: -------------------------------------------------------------------------------- 1 | let tagName = config.tagName, 2 | attrName = config.attrName; 3 | 4 | matches.forEach((m) => 5 | { 6 | // Create a zero-width start tag right before the address 7 | let startTag = addStartTag(tagName, m[0][1], 0); 8 | startTag.setAttribute(attrName, m[0][0]); 9 | 10 | // Create a zero-width end tag right after the address 11 | let endTag = addEndTag(tagName, m[0][1] + m[0][0].length, 0); 12 | 13 | // Pair the tags together 14 | startTag.pairWith(endTag); 15 | }); -------------------------------------------------------------------------------- /src/Plugins/Autoemail/Parser.php: -------------------------------------------------------------------------------- 1 | config['tagName']; 20 | $attrName = $this->config['attrName']; 21 | 22 | foreach ($matches as $m) 23 | { 24 | // Create a zero-width start tag right before the address 25 | $startTag = $this->parser->addStartTag($tagName, $m[0][1], 0); 26 | $startTag->setAttribute($attrName, $m[0][0]); 27 | 28 | // Create a zero-width end tag right after the address 29 | $endTag = $this->parser->addEndTag($tagName, $m[0][1] + strlen($m[0][0]), 0); 30 | 31 | // Pair the tags together 32 | $startTag->pairWith($endTag); 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /src/Plugins/Autoimage/Configurator.php: -------------------------------------------------------------------------------- 1 | attrName . '}"/>'; 21 | } 22 | } -------------------------------------------------------------------------------- /src/Plugins/Autoimage/Parser.js: -------------------------------------------------------------------------------- 1 | const tagPriority = 2; -------------------------------------------------------------------------------- /src/Plugins/Autoimage/Parser.php: -------------------------------------------------------------------------------- 1 | 2 | { 3 | // Linkify the trimmed URL 4 | linkifyUrl(m[0][1], trimUrl(m[0][0])); 5 | }); 6 | 7 | /** 8 | * Linkify given URL at given position 9 | * 10 | * @param {number} tagPos URL's position in the text 11 | * @param {string} url URL 12 | */ 13 | function linkifyUrl(tagPos, url) 14 | { 15 | // Create a zero-width end tag right after the URL 16 | let endPos = tagPos + url.length, 17 | endTag = addEndTag(config.tagName, endPos, 0); 18 | 19 | // If the URL starts with "www." we prepend "http://" 20 | if (url[3] === '.') 21 | { 22 | url = 'http://' + url; 23 | } 24 | 25 | // Create a zero-width start tag right before the URL, with a slightly worse priority to 26 | // allow specialized plugins to use the URL instead 27 | let startTag = addStartTag(config.tagName, tagPos, 0, 1); 28 | startTag.setAttribute(config.attrName, url); 29 | 30 | // Pair the tags together 31 | startTag.pairWith(endTag); 32 | 33 | // Protect the tag's content from partial replacements with a low priority tag 34 | let contentTag = addVerbatim(tagPos, endPos - tagPos, 1000); 35 | startTag.cascadeInvalidationTo(contentTag); 36 | } 37 | 38 | /** 39 | * Remove trailing punctuation from given URL 40 | * 41 | * We remove most ASCII non-letters from the end of the string. 42 | * Exceptions: 43 | * - dashes and underscores, (base64 IDs could end with one) 44 | * - equal signs, (because of "foo?bar=") 45 | * - plus signs, (used by some file share services to force download) 46 | * - trailing slashes, 47 | * - closing parentheses. (they are balanced separately) 48 | * 49 | * @param {string} url Original URL 50 | * @return {string} Trimmed URL 51 | */ 52 | function trimUrl(url) 53 | { 54 | return url.replace(/(?:(?![-=+)\/_])[\s!-.:-@[-`{-~])+$/, ''); 55 | } -------------------------------------------------------------------------------- /src/Plugins/Autovideo/Configurator.php: -------------------------------------------------------------------------------- 1 | attrName . '}"/>'; 21 | } 22 | } -------------------------------------------------------------------------------- /src/Plugins/Autovideo/Parser.js: -------------------------------------------------------------------------------- 1 | const tagPriority = -1; -------------------------------------------------------------------------------- /src/Plugins/Autovideo/Parser.php: -------------------------------------------------------------------------------- 1 | bbcodeMonkey = $bbcodeMonkey; 28 | } 29 | 30 | /** 31 | * Normalize a value for storage 32 | * 33 | * @param mixed $value Original value 34 | * @return Repository Normalized value 35 | */ 36 | public function normalizeValue($value) 37 | { 38 | return ($value instanceof Repository) 39 | ? $value 40 | : new Repository($value, $this->bbcodeMonkey); 41 | } 42 | } -------------------------------------------------------------------------------- /src/Plugins/Censor/Parser.js: -------------------------------------------------------------------------------- 1 | let tagName = config.tagName, 2 | attrName = config.attrName; 3 | 4 | matches.forEach((m) => 5 | { 6 | if (isAllowed(m[0][0])) 7 | { 8 | return; 9 | } 10 | 11 | // NOTE: unlike the PCRE regexp, the JavaScript regexp can consume an extra character at the 12 | // start of the match, so we have to adjust the position and length accordingly 13 | let offset = /^\W/.test(m[0][0]) ? 1 : 0, 14 | word = m[0][0].substring(offset), 15 | tag = addSelfClosingTag(tagName, m[0][1] + offset, word.length); 16 | 17 | if (HINT.CENSOR_HAS_REPLACEMENTS && config.replacements) 18 | { 19 | for (let i = 0; i < config.replacements.length; ++i) 20 | { 21 | let regexp = config.replacements[i][0], 22 | replacement = config.replacements[i][1]; 23 | 24 | if (regexp.test(word)) 25 | { 26 | tag.setAttribute(attrName, replacement); 27 | break; 28 | } 29 | } 30 | } 31 | }); 32 | 33 | /** 34 | * Test whether given word is allowed 35 | * 36 | * @param {string} word 37 | * @return {boolean} 38 | */ 39 | function isAllowed(word) 40 | { 41 | return (HINT.CENSOR_HAS_ALLOWED && config.allowed && config.allowed.test(word)); 42 | } -------------------------------------------------------------------------------- /src/Plugins/Censor/Parser.php: -------------------------------------------------------------------------------- 1 | config['tagName']; 20 | $attrName = $this->config['attrName']; 21 | $replacements = $this->config['replacements'] ?? []; 22 | foreach ($matches as $m) 23 | { 24 | if ($this->isAllowed($m[0][0])) 25 | { 26 | continue; 27 | } 28 | 29 | $tag = $this->parser->addSelfClosingTag($tagName, $m[0][1], strlen($m[0][0])); 30 | foreach ($replacements as list($regexp, $replacement)) 31 | { 32 | if (preg_match($regexp, $m[0][0])) 33 | { 34 | $tag->setAttribute($attrName, $replacement); 35 | break; 36 | } 37 | } 38 | } 39 | } 40 | 41 | /** 42 | * Test whether given word is allowed 43 | * 44 | * @param string $word 45 | * @return bool 46 | */ 47 | protected function isAllowed($word) 48 | { 49 | return (isset($this->config['allowed']) && preg_match($this->config['allowed'], $word)); 50 | } 51 | } -------------------------------------------------------------------------------- /src/Plugins/Emoticons/Configurator/EmoticonCollection.php: -------------------------------------------------------------------------------- 1 | 2 | { 3 | if (HINT.EMOTICONS_NOT_AFTER && config.notAfter && m[0][1] && config.notAfter.test(text[m[0][1] - 1])) 4 | { 5 | return; 6 | } 7 | 8 | addSelfClosingTag(config.tagName, m[0][1], m[0][0].length); 9 | }); -------------------------------------------------------------------------------- /src/Plugins/Emoticons/Parser.php: -------------------------------------------------------------------------------- 1 | parser->addSelfClosingTag($this->config['tagName'], $m[0][1], strlen($m[0][0])); 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /src/Plugins/Escaper/Configurator.php: -------------------------------------------------------------------------------- 1 | regexp = ($bool) ? '/\\\\./su' : '/\\\\[-!#()*+.:<>@[\\\\\\]^_`{|}~]/'; 38 | } 39 | 40 | /** 41 | * {@inheritdoc} 42 | */ 43 | protected function setUp() 44 | { 45 | // Set the default regexp 46 | $this->escapeAll(false); 47 | 48 | // Create the tag 49 | $tag = $this->configurator->tags->add($this->tagName); 50 | $tag->rules->disableAutoLineBreaks(); 51 | $tag->rules->ignoreTags(); 52 | $tag->rules->preventLineBreaks(); 53 | $tag->template = ''; 54 | } 55 | } -------------------------------------------------------------------------------- /src/Plugins/Escaper/Parser.js: -------------------------------------------------------------------------------- 1 | matches.forEach((m) => 2 | { 3 | addTagPair( 4 | config.tagName, 5 | m[0][1], 6 | 1, 7 | m[0][1] + m[0][0].length, 8 | 0 9 | ); 10 | }); -------------------------------------------------------------------------------- /src/Plugins/Escaper/Parser.php: -------------------------------------------------------------------------------- 1 | parser->addTagPair( 22 | $this->config['tagName'], 23 | $m[0][1], 24 | 1, 25 | $m[0][1] + strlen($m[0][0]), 26 | 0 27 | ); 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /src/Plugins/HTMLComments/Configurator.php: -------------------------------------------------------------------------------- 1 | /is'; 28 | 29 | /** 30 | * @var string Name of the tag used by this plugin 31 | */ 32 | protected $tagName = 'HC'; 33 | 34 | /** 35 | * {@inheritdoc} 36 | */ 37 | protected function setUp() 38 | { 39 | $tag = $this->configurator->tags->add($this->tagName); 40 | $tag->attributes->add($this->attrName); 41 | $tag->rules->ignoreTags(); 42 | $tag->template = ''; 43 | } 44 | } -------------------------------------------------------------------------------- /src/Plugins/HTMLComments/Parser.js: -------------------------------------------------------------------------------- 1 | let tagName = config.tagName, 2 | attrName = config.attrName; 3 | 4 | matches.forEach((m) => 5 | { 6 | // Decode HTML entities 7 | let content = html_entity_decode(m[0][0].substring(4, m[0][0].length - 3)); 8 | 9 | // Remove angle brackets from the content 10 | content = content.replace(/[<>]/g, ''); 11 | 12 | // Remove trailing dashes 13 | content = content.replace(/-+$/, ''); 14 | 15 | // Remove the illegal sequence "--" from the content 16 | content = content.replace(/--/g, ''); 17 | 18 | addSelfClosingTag(tagName, m[0][1], m[0][0].length).setAttribute(attrName, content); 19 | }); -------------------------------------------------------------------------------- /src/Plugins/HTMLComments/Parser.php: -------------------------------------------------------------------------------- 1 | config['tagName']; 20 | $attrName = $this->config['attrName']; 21 | 22 | foreach ($matches as $m) 23 | { 24 | // Decode HTML entities 25 | $content = html_entity_decode(substr($m[0][0], 4, -3), ENT_QUOTES, 'UTF-8'); 26 | 27 | // Remove angle brackets from the content 28 | $content = str_replace(['<', '>'], '', $content); 29 | 30 | // Remove trailing dashes 31 | $content = rtrim($content, '-'); 32 | 33 | // Remove the illegal sequence "--" from the content 34 | $content = str_replace('--', '', $content); 35 | 36 | $this->parser->addSelfClosingTag($tagName, $m[0][1], strlen($m[0][0]))->setAttribute($attrName, $content); 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /src/Plugins/HTMLEntities/Configurator.php: -------------------------------------------------------------------------------- 1 | [a-z]+|#(?>[0-9]+|x[0-9a-f]+));/i'; 28 | 29 | /** 30 | * @var string Name of the tag used by this plugin 31 | */ 32 | protected $tagName = 'HE'; 33 | 34 | /** 35 | * {@inheritdoc} 36 | */ 37 | protected function setUp() 38 | { 39 | $tag = $this->configurator->tags->add($this->tagName); 40 | $tag->attributes->add($this->attrName); 41 | $tag->template 42 | = ''; 43 | } 44 | } -------------------------------------------------------------------------------- /src/Plugins/HTMLEntities/Parser.js: -------------------------------------------------------------------------------- 1 | let tagName = config.tagName, 2 | attrName = config.attrName; 3 | 4 | matches.forEach((m) => 5 | { 6 | let entity = m[0][0], 7 | chr = html_entity_decode(entity); 8 | 9 | if (chr === entity || chr.charCodeAt(0) < 32) 10 | { 11 | // If the entity was not decoded, we assume it's not valid and we ignore it. 12 | // Same thing if it's a control character 13 | return; 14 | } 15 | 16 | addSelfClosingTag(tagName, m[0][1], entity.length).setAttribute(attrName, chr); 17 | }); -------------------------------------------------------------------------------- /src/Plugins/HTMLEntities/Parser.php: -------------------------------------------------------------------------------- 1 | config['tagName']; 20 | $attrName = $this->config['attrName']; 21 | 22 | foreach ($matches as $m) 23 | { 24 | $entity = $m[0][0]; 25 | $chr = html_entity_decode($entity, ENT_HTML5 | ENT_QUOTES, 'UTF-8'); 26 | 27 | if ($chr === $entity || ord($chr) < 32) 28 | { 29 | // If the entity was not decoded, we assume it's not valid and we ignore it. 30 | // Same thing if it's a control character 31 | continue; 32 | } 33 | 34 | $this->parser->addSelfClosingTag($tagName, $m[0][1], strlen($entity))->setAttribute($attrName, $chr); 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /src/Plugins/Keywords/Parser.js: -------------------------------------------------------------------------------- 1 | const regexps = config.regexps, 2 | tagName = config.tagName, 3 | attrName = config.attrName; 4 | 5 | let onlyFirst = typeof config.onlyFirst !== 'undefined', 6 | keywords = {}; 7 | 8 | regexps.forEach((regexp) => 9 | { 10 | let m; 11 | 12 | regexp.lastIndex = 0; 13 | while (m = regexp.exec(text)) 14 | { 15 | let value = m[0], 16 | pos = m.index; 17 | 18 | if (onlyFirst) 19 | { 20 | if (value in keywords) 21 | { 22 | continue; 23 | } 24 | 25 | keywords[value] = 1; 26 | } 27 | 28 | addSelfClosingTag(tagName, pos, value.length).setAttribute(attrName, value); 29 | } 30 | }); -------------------------------------------------------------------------------- /src/Plugins/Keywords/Parser.php: -------------------------------------------------------------------------------- 1 | config['regexps']; 20 | $tagName = $this->config['tagName']; 21 | $attrName = $this->config['attrName']; 22 | 23 | $onlyFirst = !empty($this->config['onlyFirst']); 24 | $keywords = []; 25 | 26 | foreach ($regexps as $regexp) 27 | { 28 | preg_match_all($regexp, $text, $matches, PREG_OFFSET_CAPTURE); 29 | 30 | foreach ($matches[0] as list($value, $pos)) 31 | { 32 | if ($onlyFirst) 33 | { 34 | if (isset($keywords[$value])) 35 | { 36 | continue; 37 | } 38 | 39 | $keywords[$value] = 1; 40 | } 41 | 42 | $this->parser->addSelfClosingTag($tagName, $pos, strlen($value)) 43 | ->setAttribute($attrName, $value); 44 | } 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /src/Plugins/Litedown/Parser/LinkAttributesSetter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Set a URL or IMG tag's attributes 3 | * 4 | * @param {!Tag} tag URL or IMG tag 5 | * @param {string} linkInfo Link's info: an URL optionally followed by spaces and a title 6 | * @param {string} attrName Name of the URL attribute 7 | */ 8 | function setLinkAttributes(tag, linkInfo, attrName) 9 | { 10 | let url = linkInfo.replace(/^\s*/, '').replace(/\s*$/, ''), 11 | title = '', 12 | pos = url.indexOf(' '); 13 | if (pos !== -1) 14 | { 15 | title = url.substring(pos).replace(/^\s*\S/, '').replace(/\S\s*$/, ''); 16 | url = url.substring(0, pos); 17 | } 18 | if (/^<.+>$/.test(url)) 19 | { 20 | url = url.replace(/^<(.+)>$/, '$1').replace(/\\>/g, '>'); 21 | } 22 | 23 | tag.setAttribute(attrName, decode(url)); 24 | if (title > '') 25 | { 26 | tag.setAttribute('title', decode(title)); 27 | } 28 | } -------------------------------------------------------------------------------- /src/Plugins/Litedown/Parser/LinkAttributesSetter.php: -------------------------------------------------------------------------------- 1 | $/', $url)) 33 | { 34 | $url = str_replace('\\>', '>', substr($url, 1, -1)); 35 | } 36 | 37 | $tag->setAttribute($attrName, $this->text->decode($url)); 38 | if ($title > '') 39 | { 40 | $tag->setAttribute('title', $this->text->decode($title)); 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /src/Plugins/Litedown/Parser/Passes/AbstractInlineMarkup.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Parse given inline markup in text 3 | * 4 | * The markup must start and end with exactly 2 characters 5 | * 6 | * @param {string} str First markup string 7 | * @param {!RegExp} regexp Regexp used to match the markup's span 8 | * @param {string} tagName Name of the tag produced by this markup 9 | */ 10 | function parseInlineMarkup(str, regexp, tagName) 11 | { 12 | if (text.indexOf(str) === -1) 13 | { 14 | return; 15 | } 16 | 17 | let m; 18 | while (m = regexp.exec(text)) 19 | { 20 | let match = m[0], 21 | matchPos = m.index, 22 | matchLen = match.length, 23 | endPos = matchPos + matchLen - 2; 24 | 25 | addTagPair(tagName, matchPos, 2, endPos, 2); 26 | overwrite(matchPos, 2); 27 | overwrite(endPos, 2); 28 | } 29 | } -------------------------------------------------------------------------------- /src/Plugins/Litedown/Parser/Passes/AbstractInlineMarkup.php: -------------------------------------------------------------------------------- 1 | text->indexOf($str); 25 | if ($pos === false) 26 | { 27 | return; 28 | } 29 | 30 | preg_match_all($regexp, $this->text, $matches, PREG_OFFSET_CAPTURE, $pos); 31 | foreach ($matches[0] as [$match, $matchPos]) 32 | { 33 | $matchLen = strlen($match); 34 | $endPos = $matchPos + $matchLen - 2; 35 | 36 | $this->parser->addTagPair($tagName, $matchPos, 2, $endPos, 2); 37 | $this->text->overwrite($matchPos, 2); 38 | $this->text->overwrite($endPos, 2); 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /src/Plugins/Litedown/Parser/Passes/AbstractPass.php: -------------------------------------------------------------------------------- 1 | parser = $parser; 32 | $this->text = $text; 33 | } 34 | 35 | /** 36 | * Parse the prepared text from stored parser 37 | * 38 | * @return void 39 | */ 40 | abstract public function parse(); 41 | } -------------------------------------------------------------------------------- /src/Plugins/Litedown/Parser/Passes/ForcedLineBreaks.js: -------------------------------------------------------------------------------- 1 | function parse() 2 | { 3 | let pos = text.indexOf(" \n"); 4 | while (pos > 0) 5 | { 6 | addBrTag(pos + 2).cascadeInvalidationTo( 7 | addVerbatim(pos + 2, 1) 8 | ); 9 | pos = text.indexOf(" \n", pos + 3); 10 | } 11 | } -------------------------------------------------------------------------------- /src/Plugins/Litedown/Parser/Passes/ForcedLineBreaks.php: -------------------------------------------------------------------------------- 1 | text->indexOf(" \n"); 18 | while ($pos !== false) 19 | { 20 | $this->parser->addBrTag($pos + 2)->cascadeInvalidationTo( 21 | $this->parser->addVerbatim($pos + 2, 1) 22 | ); 23 | $pos = $this->text->indexOf(" \n", $pos + 3); 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /src/Plugins/Litedown/Parser/Passes/InlineCode.js: -------------------------------------------------------------------------------- 1 | function parse() 2 | { 3 | let markers = getInlineCodeMarkers(), 4 | i = -1, 5 | cnt = markers.length; 6 | while (++i < (cnt - 1)) 7 | { 8 | let pos = markers[i].next, 9 | j = i; 10 | if (text[markers[i].pos] !== '`') 11 | { 12 | // Adjust the left marker if its first backtick was escaped 13 | ++markers[i].pos; 14 | --markers[i].len; 15 | } 16 | while (++j < cnt && markers[j].pos === pos) 17 | { 18 | if (markers[j].len === markers[i].len) 19 | { 20 | addInlineCodeTags(markers[i], markers[j]); 21 | i = j; 22 | break; 23 | } 24 | pos = markers[j].next; 25 | } 26 | } 27 | } 28 | 29 | /** 30 | * Add the tag pair for an inline code span 31 | * 32 | * @param {!Object} left Left marker 33 | * @param {!Object} right Right marker 34 | */ 35 | function addInlineCodeTags(left, right) 36 | { 37 | let startPos = left.pos, 38 | startLen = left.len + left.trimAfter, 39 | endPos = right.pos - right.trimBefore, 40 | endLen = right.len + right.trimBefore; 41 | addTagPair('C', startPos, startLen, endPos, endLen); 42 | overwrite(startPos, endPos + endLen - startPos); 43 | } 44 | 45 | 46 | /** 47 | * Capture and return inline code markers 48 | * 49 | * @return {!Array} 50 | */ 51 | function getInlineCodeMarkers() 52 | { 53 | let pos = text.indexOf('`'); 54 | if (pos < 0) 55 | { 56 | return []; 57 | } 58 | 59 | let regexp = /(`+)(\s*)[^\x17`]*/g, 60 | trimNext = 0, 61 | markers = [], 62 | _text = text.replace(/\x1BD/g, '\\`'), 63 | m; 64 | regexp.lastIndex = pos; 65 | while (m = regexp.exec(_text)) 66 | { 67 | markers.push({ 68 | pos : m.index, 69 | len : m[1].length, 70 | trimBefore : trimNext, 71 | trimAfter : m[2].length, 72 | next : m.index + m[0].length 73 | }); 74 | trimNext = m[0].length - m[0].replace(/\s+$/, '').length; 75 | } 76 | 77 | return markers; 78 | } -------------------------------------------------------------------------------- /src/Plugins/Litedown/Parser/Passes/InlineSpoiler.js: -------------------------------------------------------------------------------- 1 | function parse() 2 | { 3 | parseInlineMarkup('>!', />![^\x17]+?!parseInlineMarkup('>!', '/>![^\\x17]+?!parseInlineMarkup('||', '/\\|\\|[^\\x17]+?\\|\\|/', 'ISPOILER'); 19 | } 20 | } -------------------------------------------------------------------------------- /src/Plugins/Litedown/Parser/Passes/LinkReferences.js: -------------------------------------------------------------------------------- 1 | function parse() 2 | { 3 | if (text.indexOf(']:') < 0) 4 | { 5 | return; 6 | } 7 | 8 | let m, regexp = /^\x1A* {0,3}\[([^\x17\]]+)\]: *([^[\s\x17]+ *(?:"[^\x17]*?"|'[^\x17]*?'|\([^\x17)]*\))?) *(?=$|\x17)\n?/gm; 9 | while (m = regexp.exec(text)) 10 | { 11 | addIgnoreTag(m.index, m[0].length); 12 | 13 | // Only add the reference if it does not already exist 14 | let id = m[1].toLowerCase(); 15 | if (!linkReferences[id]) 16 | { 17 | hasReferences = true; 18 | linkReferences[id] = m[2]; 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /src/Plugins/Litedown/Parser/Passes/LinkReferences.php: -------------------------------------------------------------------------------- 1 | text->indexOf(']:') === false) 18 | { 19 | return; 20 | } 21 | 22 | $regexp = '/^\\x1A* {0,3}\\[([^\\x17\\]]+)\\]: *([^[\\s\\x17]+ *(?:"[^\\x17]*?"|\'[^\\x17]*?\'|\\([^\\x17)]*\\))?) *(?=$|\\x17)\\n?/m'; 23 | preg_match_all($regexp, $this->text, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER); 24 | foreach ($matches as $m) 25 | { 26 | $this->parser->addIgnoreTag($m[0][1], strlen($m[0][0])); 27 | 28 | // Only add the reference if it does not already exist 29 | $id = strtolower($m[1][0]); 30 | if (!isset($this->text->linkReferences[$id])) 31 | { 32 | $this->text->hasReferences = true; 33 | $this->text->linkReferences[$id] = $m[2][0]; 34 | } 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /src/Plugins/Litedown/Parser/Passes/Strikethrough.js: -------------------------------------------------------------------------------- 1 | function parse() 2 | { 3 | parseInlineMarkup('~~', /~~[^\x17]+?~~(?!~)/g, 'DEL'); 4 | } -------------------------------------------------------------------------------- /src/Plugins/Litedown/Parser/Passes/Strikethrough.php: -------------------------------------------------------------------------------- 1 | parseInlineMarkup('~~', '/~~[^\\x17]+?~~(?!~)/', 'DEL'); 18 | } 19 | } -------------------------------------------------------------------------------- /src/Plugins/Litedown/Parser/Passes/Subscript.js: -------------------------------------------------------------------------------- 1 | function parse() 2 | { 3 | parseAbstractScript('SUB', '~', /~[^\x17\s!"#$%&\'()*+,\-.\/:;<=>?@[\]^_`{}|~]+~?/g, /~\([^\x17()]+\)/g); 4 | } -------------------------------------------------------------------------------- /src/Plugins/Litedown/Parser/Passes/Subscript.php: -------------------------------------------------------------------------------- 1 | parseAbstractScript('SUB', '~', '/~[^\\x17\\s!"#$%&\'()*+,\\-.\\/:;<=>?@[\\]^_`{}|~]++~?/', '/~\\([^\\x17()]++\\)/'); 18 | } 19 | } -------------------------------------------------------------------------------- /src/Plugins/Litedown/Parser/Passes/Superscript.js: -------------------------------------------------------------------------------- 1 | function parse() 2 | { 3 | parseAbstractScript('SUP', '^', /\^[^\x17\s!"#$%&\'()*+,\-.\/:;<=>?@[\]^_`{}|~]+\^?/g, /\^\([^\x17()]+\)/g); 4 | } -------------------------------------------------------------------------------- /src/Plugins/Litedown/Parser/Passes/Superscript.php: -------------------------------------------------------------------------------- 1 | parseAbstractScript('SUP', '^', '/\\^[^\\x17\\s!"#$%&\'()*+,\\-.\\/:;<=>?@[\\]^_`{}|~]++\\^?/', '/\\^\\([^\\x17()]++\\)/'); 18 | } 19 | } -------------------------------------------------------------------------------- /src/Plugins/Litedown/Parser/Slugger.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {!Tag} tag 3 | * @param {string} innerText 4 | */ 5 | function filterTag(tag, innerText) 6 | { 7 | let slug = innerText.toLowerCase(); 8 | slug = slug.replace(/[^a-z0-9]+/g, '-'); 9 | slug = slug.replace(/^-/, '').replace(/-$/, ''); 10 | if (slug !== '') 11 | { 12 | tag.setAttribute('slug', slug); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Plugins/Litedown/Parser/Slugger.php: -------------------------------------------------------------------------------- 1 | setAttribute('slug', $slug); 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /src/Plugins/MediaEmbed/Configurator/AbstractConfigurableHostHelper.php: -------------------------------------------------------------------------------- 1 | addHosts([$host]); 17 | } 18 | 19 | public function addHosts(array $hosts): void 20 | { 21 | $siteId = $this->getSiteId(); 22 | if (!isset($this->configurator->registeredVars['MediaEmbed.sites'][$siteId])) 23 | { 24 | $this->configurator->MediaEmbed->add($siteId); 25 | } 26 | 27 | foreach ($hosts as $host) 28 | { 29 | $host = strtolower($host); 30 | $this->configurator->registeredVars['MediaEmbed.hosts'][$host] = $siteId; 31 | } 32 | } 33 | 34 | public function getHosts(): array 35 | { 36 | $hosts = array_keys( 37 | (array) ($this->configurator->registeredVars['MediaEmbed.hosts'] ?? []), 38 | $this->getSiteId(), 39 | true 40 | ); 41 | sort($hosts, SORT_STRING); 42 | 43 | return $hosts; 44 | } 45 | 46 | abstract protected function getSiteId(): string; 47 | 48 | public function setHosts(array $hosts): void 49 | { 50 | $siteId = $this->getSiteId(); 51 | if (!isset($this->configurator->registeredVars['MediaEmbed.sites'][$siteId])) 52 | { 53 | $this->configurator->MediaEmbed->add($siteId); 54 | } 55 | 56 | // Remove previously set hosts for this site 57 | foreach ($this->getHosts() as $host) 58 | { 59 | unset($this->configurator->registeredVars['MediaEmbed.hosts'][$host]); 60 | } 61 | 62 | $this->addHosts($hosts); 63 | } 64 | } -------------------------------------------------------------------------------- /src/Plugins/MediaEmbed/Configurator/SiteHelpers/AbstractSiteHelper.php: -------------------------------------------------------------------------------- 1 | builder)) 21 | { 22 | $this->builder = new Builder; 23 | } 24 | 25 | $siteId = $this->getSiteId(); 26 | $hosts = $this->getHosts(); 27 | 28 | $this->configurator->tags[$siteId]->attributes['embedder']->filterChain[0]->setRegexp( 29 | '/^(?:[-\w]*\.)*' . $this->builder->build($hosts) . '$/' 30 | ); 31 | } 32 | 33 | protected function getSiteId(): string 34 | { 35 | return 'bluesky'; 36 | } 37 | } -------------------------------------------------------------------------------- /src/Plugins/MediaEmbed/Configurator/SiteHelpers/MastodonHelper.php: -------------------------------------------------------------------------------- 1 | templateBuilder = $templateBuilder; 28 | } 29 | 30 | /** 31 | * {@inheritdoc} 32 | */ 33 | protected function needsWrapper() 34 | { 35 | return false; 36 | } 37 | 38 | /** 39 | * {@inheritdoc} 40 | */ 41 | protected function getContentTemplate() 42 | { 43 | $branches = (isset($this->attributes['when'][0])) ? $this->attributes['when'] : [$this->attributes['when']]; 44 | $template = ''; 45 | foreach ($branches as $when) 46 | { 47 | $template .= '' . $this->templateBuilder->getTemplate($when) . ''; 48 | } 49 | $template .= '' . $this->templateBuilder->getTemplate($this->attributes['otherwise']) . ''; 50 | 51 | return $template; 52 | } 53 | } -------------------------------------------------------------------------------- /src/Plugins/MediaEmbed/Configurator/TemplateGenerators/Flash.php: -------------------------------------------------------------------------------- 1 | $this->attributes['src'], 24 | 'style' => $this->attributes['style'], 25 | 'type' => 'application/x-shockwave-flash', 26 | 'typemustmatch' => '' 27 | ]; 28 | 29 | $flashVarsParam = ''; 30 | if (isset($this->attributes['flashvars'])) 31 | { 32 | $flashVarsParam = $this->generateParamElement('flashvars', $this->attributes['flashvars']); 33 | } 34 | 35 | $template = '' 36 | . $this->generateAttributes($attributes) 37 | . $this->generateParamElement('allowfullscreen', 'true') 38 | . $flashVarsParam 39 | . ''; 40 | 41 | return $template; 42 | } 43 | 44 | /** 45 | * Generate a param element to be used inside of an object element 46 | * 47 | * @param string $paramName 48 | * @param string $paramValue 49 | * @return string 50 | */ 51 | protected function generateParamElement($paramName, $paramValue) 52 | { 53 | return '' . $this->generateAttributes(['value' => $paramValue]) . ''; 54 | } 55 | } -------------------------------------------------------------------------------- /src/Plugins/MediaEmbed/Configurator/TemplateGenerators/Iframe.php: -------------------------------------------------------------------------------- 1 | '', 19 | 'loading' => 'lazy', 20 | 'scrolling' => 'no', 21 | 'style' => ['border' => '0'] 22 | ]; 23 | 24 | /** 25 | * @var string[] List of attributes to be passed to the iframe 26 | */ 27 | protected $iframeAttributes = ['allow', 'data-s9e-livepreview-ignore-attrs', 'data-s9e-livepreview-onrender', 'onload', 'scrolling', 'src', 'style']; 28 | 29 | /** 30 | * {@inheritdoc} 31 | */ 32 | protected function getContentTemplate() 33 | { 34 | $attributes = $this->mergeAttributes($this->defaultIframeAttributes, $this->getFilteredAttributes()); 35 | 36 | return ''; 37 | } 38 | 39 | /** 40 | * Filter the attributes to keep only those that can be used in an iframe 41 | * 42 | * @return array 43 | */ 44 | protected function getFilteredAttributes() 45 | { 46 | return array_intersect_key($this->attributes, array_flip($this->iframeAttributes)); 47 | } 48 | } -------------------------------------------------------------------------------- /src/Plugins/MediaEmbed/Configurator/XenForoHelper.php: -------------------------------------------------------------------------------- 1 | 2 | { 3 | let tagName = config.tagName, 4 | url = m[0][0], 5 | pos = m[0][1], 6 | len = url.length; 7 | 8 | // Give that tag priority over other tags such as Autolink's 9 | addSelfClosingTag(tagName, pos, len, -10).setAttribute('url', url); 10 | }); -------------------------------------------------------------------------------- /src/Plugins/ParserBase.php: -------------------------------------------------------------------------------- 1 | parser = $parser; 33 | $this->config = $config; 34 | 35 | $this->setUp(); 36 | } 37 | 38 | /** 39 | * Plugin's setup 40 | * 41 | * @return void 42 | */ 43 | protected function setUp() 44 | { 45 | } 46 | 47 | /** 48 | * @param string $text 49 | * @param array $matches If the config array has a "regexp" key, the corresponding matches are 50 | * passed as second parameter. Otherwise, an empty array is passed 51 | * @return void 52 | */ 53 | abstract public function parse($text, array $matches); 54 | } -------------------------------------------------------------------------------- /src/Plugins/Preg/Parser.js: -------------------------------------------------------------------------------- 1 | config.generics.forEach((entry) => 2 | { 3 | let tagName = entry[0], 4 | regexp = entry[1], 5 | passthroughIdx = entry[2], 6 | map = entry[3], 7 | m; 8 | 9 | // Reset the regexp 10 | regexp.lastIndex = 0; 11 | 12 | while (m = regexp.exec(text)) 13 | { 14 | let startTagPos = m.index, 15 | matchLen = m[0].length, 16 | tag; 17 | 18 | if (HINT.PREG_HAS_PASSTHROUGH && passthroughIdx && m[passthroughIdx] !== '') 19 | { 20 | // Compute the position and length of the start tag, end tag, and the content in 21 | // between. m.index gives us the position of the start tag but we don't know its length. 22 | // We use indexOf() to locate the content part so that we know how long the start tag 23 | // is. It is an imperfect solution but it should work well enough in most cases. 24 | let contentPos = text.indexOf(m[passthroughIdx], startTagPos), 25 | contentLen = m[passthroughIdx].length, 26 | startTagLen = contentPos - startTagPos, 27 | endTagPos = contentPos + contentLen, 28 | endTagLen = matchLen - (startTagLen + contentLen); 29 | 30 | tag = addTagPair(tagName, startTagPos, startTagLen, endTagPos, endTagLen, -100); 31 | } 32 | else 33 | { 34 | tag = addSelfClosingTag(tagName, startTagPos, matchLen, -100); 35 | } 36 | 37 | map.forEach((attrName, i) => 38 | { 39 | // NOTE: subpatterns with no name have an empty entry to preserve the array indices 40 | if (attrName && typeof m[i] !== 'undefined') 41 | { 42 | tag.setAttribute(attrName, m[i]); 43 | } 44 | }); 45 | } 46 | }); -------------------------------------------------------------------------------- /src/Plugins/Preg/Parser.php: -------------------------------------------------------------------------------- 1 | config['generics'] as list($tagName, $regexp, $passthroughIdx, $map)) 20 | { 21 | preg_match_all($regexp, $text, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE); 22 | 23 | foreach ($matches as $m) 24 | { 25 | $startTagPos = $m[0][1]; 26 | $matchLen = strlen($m[0][0]); 27 | 28 | if ($passthroughIdx && isset($m[$passthroughIdx]) && $m[$passthroughIdx][0] !== '') 29 | { 30 | // Compute the position and length of the start tag, end tag, and the content in 31 | // between. PREG_OFFSET_CAPTURE gives us the position of the content, and we 32 | // know its length. Everything before is considered part of the start tag, and 33 | // everything after is considered part of the end tag 34 | $contentPos = $m[$passthroughIdx][1]; 35 | $contentLen = strlen($m[$passthroughIdx][0]); 36 | $startTagLen = $contentPos - $startTagPos; 37 | $endTagPos = $contentPos + $contentLen; 38 | $endTagLen = $matchLen - ($startTagLen + $contentLen); 39 | 40 | $tag = $this->parser->addTagPair($tagName, $startTagPos, $startTagLen, $endTagPos, $endTagLen, -100); 41 | } 42 | else 43 | { 44 | $tag = $this->parser->addSelfClosingTag($tagName, $startTagPos, $matchLen, -100); 45 | } 46 | 47 | foreach ($map as $i => $attrName) 48 | { 49 | if ($attrName && isset($m[$i]) && $m[$i][0] !== '') 50 | { 51 | $tag->setAttribute($attrName, $m[$i][0]); 52 | } 53 | } 54 | } 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /src/Plugins/TaskLists/filterListItem.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {!Tag} listItem 3 | * @param {string} text 4 | */ 5 | function (listItem, text) 6 | { 7 | // Test whether the list item is followed by a task checkbox 8 | let pos = listItem.getPos() + listItem.getLen(); 9 | while (text.charAt(pos) === ' ') 10 | { 11 | ++pos; 12 | } 13 | let str = text.substring(pos, pos + 3); 14 | if (!/\[[ Xx]\]/.test(str)) 15 | { 16 | return; 17 | } 18 | 19 | // Create a tag for the task and assign it a random ID 20 | let taskId = Math.random().toString(16).substring(2), 21 | taskState = (str === '[ ]') ? 'unchecked' : 'checked', 22 | task = addSelfClosingTag('TASK', pos, 3); 23 | 24 | task.setAttribute('id', taskId); 25 | task.setAttribute('state', taskState); 26 | 27 | listItem.cascadeInvalidationTo(task); 28 | } -------------------------------------------------------------------------------- /src/Renderers/Unformatted.php: -------------------------------------------------------------------------------- 1 | \n", htmlspecialchars(strip_tags($xml), ENT_COMPAT, 'UTF-8', false)); 24 | } 25 | } -------------------------------------------------------------------------------- /src/Unparser.php: -------------------------------------------------------------------------------- 1 | cacheDir = $cacheDir ?? sys_get_temp_dir(); 35 | 36 | return $client; 37 | } 38 | } -------------------------------------------------------------------------------- /src/Utils/Http/Client.php: -------------------------------------------------------------------------------- 1 | loadXML($xml, LIBXML_NONET); 19 | 20 | return $dom; 21 | } 22 | } --------------------------------------------------------------------------------