@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Continue moving classes with no callers in libphutil or Arcanist to Phabricator

Summary: Ref T13395. Move cache classes, syntax highlighters, other markup classes, and sprite sheets to Phabricator.

Test Plan: Attempted to find any callers for any of this stuff in libphutil or Arcanist and couldn't.

Maniphest Tasks: T13395

Differential Revision: https://secure.phabricator.com/D20977

+4228 -5
+2 -2
.arclint
··· 64 64 "text": { 65 65 "type": "text", 66 66 "exclude": [ 67 - "(^src/(.*/)?__tests__/[^/]+/.*\\.(txt|json))" 67 + "(^src/(.*/)?__tests__/[^/]+/.*\\.(txt|json|expect))" 68 68 ] 69 69 }, 70 70 "text-without-length": { 71 71 "type": "text", 72 72 "include": [ 73 - "(^src/(.*/)?__tests__/[^/]+/.*\\.(txt|json))" 73 + "(^src/(.*/)?__tests__/[^/]+/.*\\.(txt|json|expect))" 74 74 ], 75 75 "severity": { 76 76 "3": "disabled"
+86
src/__phutil_library_map__.php
··· 5591 5591 'PhrictionTransactionComment' => 'applications/phriction/storage/PhrictionTransactionComment.php', 5592 5592 'PhrictionTransactionEditor' => 'applications/phriction/editor/PhrictionTransactionEditor.php', 5593 5593 'PhrictionTransactionQuery' => 'applications/phriction/query/PhrictionTransactionQuery.php', 5594 + 'PhutilAPCKeyValueCache' => 'infrastructure/cache/PhutilAPCKeyValueCache.php', 5594 5595 'PhutilAmazonAuthAdapter' => 'applications/auth/adapter/PhutilAmazonAuthAdapter.php', 5595 5596 'PhutilAsanaAuthAdapter' => 'applications/auth/adapter/PhutilAsanaAuthAdapter.php', 5596 5597 'PhutilAuthAdapter' => 'applications/auth/adapter/PhutilAuthAdapter.php', ··· 5620 5621 'PhutilCalendarRootNode' => 'applications/calendar/parser/data/PhutilCalendarRootNode.php', 5621 5622 'PhutilCalendarUserNode' => 'applications/calendar/parser/data/PhutilCalendarUserNode.php', 5622 5623 'PhutilCodeSnippetContextFreeGrammar' => 'infrastructure/lipsum/code/PhutilCodeSnippetContextFreeGrammar.php', 5624 + 'PhutilConsoleSyntaxHighlighter' => 'infrastructure/markup/syntax/highlighter/PhutilConsoleSyntaxHighlighter.php', 5625 + 'PhutilContextFreeGrammar' => 'infrastructure/lipsum/PhutilContextFreeGrammar.php', 5626 + 'PhutilDefaultSyntaxHighlighter' => 'infrastructure/markup/syntax/highlighter/PhutilDefaultSyntaxHighlighter.php', 5627 + 'PhutilDefaultSyntaxHighlighterEngine' => 'infrastructure/markup/syntax/engine/PhutilDefaultSyntaxHighlighterEngine.php', 5628 + 'PhutilDefaultSyntaxHighlighterEnginePygmentsFuture' => 'infrastructure/markup/syntax/highlighter/pygments/PhutilDefaultSyntaxHighlighterEnginePygmentsFuture.php', 5629 + 'PhutilDefaultSyntaxHighlighterEngineTestCase' => 'infrastructure/markup/syntax/engine/__tests__/PhutilDefaultSyntaxHighlighterEngineTestCase.php', 5630 + 'PhutilDirectoryKeyValueCache' => 'infrastructure/cache/PhutilDirectoryKeyValueCache.php', 5623 5631 'PhutilDisqusAuthAdapter' => 'applications/auth/adapter/PhutilDisqusAuthAdapter.php', 5632 + 'PhutilDivinerSyntaxHighlighter' => 'infrastructure/markup/syntax/highlighter/PhutilDivinerSyntaxHighlighter.php', 5624 5633 'PhutilEmptyAuthAdapter' => 'applications/auth/adapter/PhutilEmptyAuthAdapter.php', 5625 5634 'PhutilFacebookAuthAdapter' => 'applications/auth/adapter/PhutilFacebookAuthAdapter.php', 5626 5635 'PhutilGitHubAuthAdapter' => 'applications/auth/adapter/PhutilGitHubAuthAdapter.php', ··· 5630 5639 'PhutilICSParserTestCase' => 'applications/calendar/parser/ics/__tests__/PhutilICSParserTestCase.php', 5631 5640 'PhutilICSWriter' => 'applications/calendar/parser/ics/PhutilICSWriter.php', 5632 5641 'PhutilICSWriterTestCase' => 'applications/calendar/parser/ics/__tests__/PhutilICSWriterTestCase.php', 5642 + 'PhutilInRequestKeyValueCache' => 'infrastructure/cache/PhutilInRequestKeyValueCache.php', 5643 + 'PhutilInvisibleSyntaxHighlighter' => 'infrastructure/markup/syntax/highlighter/PhutilInvisibleSyntaxHighlighter.php', 5633 5644 'PhutilJIRAAuthAdapter' => 'applications/auth/adapter/PhutilJIRAAuthAdapter.php', 5645 + 'PhutilJSONFragmentLexerHighlighterTestCase' => 'infrastructure/markup/syntax/highlighter/__tests__/PhutilJSONFragmentLexerHighlighterTestCase.php', 5634 5646 'PhutilJavaCodeSnippetContextFreeGrammar' => 'infrastructure/lipsum/code/PhutilJavaCodeSnippetContextFreeGrammar.php', 5647 + 'PhutilKeyValueCache' => 'infrastructure/cache/PhutilKeyValueCache.php', 5648 + 'PhutilKeyValueCacheNamespace' => 'infrastructure/cache/PhutilKeyValueCacheNamespace.php', 5649 + 'PhutilKeyValueCacheProfiler' => 'infrastructure/cache/PhutilKeyValueCacheProfiler.php', 5650 + 'PhutilKeyValueCacheProxy' => 'infrastructure/cache/PhutilKeyValueCacheProxy.php', 5651 + 'PhutilKeyValueCacheStack' => 'infrastructure/cache/PhutilKeyValueCacheStack.php', 5652 + 'PhutilKeyValueCacheTestCase' => 'infrastructure/cache/__tests__/PhutilKeyValueCacheTestCase.php', 5635 5653 'PhutilLDAPAuthAdapter' => 'applications/auth/adapter/PhutilLDAPAuthAdapter.php', 5654 + 'PhutilLexerSyntaxHighlighter' => 'infrastructure/markup/syntax/highlighter/PhutilLexerSyntaxHighlighter.php', 5636 5655 'PhutilLipsumContextFreeGrammar' => 'infrastructure/lipsum/PhutilLipsumContextFreeGrammar.php', 5656 + 'PhutilMarkupEngine' => 'infrastructure/markup/PhutilMarkupEngine.php', 5657 + 'PhutilMarkupTestCase' => 'infrastructure/markup/__tests__/PhutilMarkupTestCase.php', 5658 + 'PhutilMemcacheKeyValueCache' => 'infrastructure/cache/PhutilMemcacheKeyValueCache.php', 5637 5659 'PhutilOAuth1AuthAdapter' => 'applications/auth/adapter/PhutilOAuth1AuthAdapter.php', 5638 5660 'PhutilOAuthAuthAdapter' => 'applications/auth/adapter/PhutilOAuthAuthAdapter.php', 5661 + 'PhutilOnDiskKeyValueCache' => 'infrastructure/cache/PhutilOnDiskKeyValueCache.php', 5639 5662 'PhutilPHPCodeSnippetContextFreeGrammar' => 'infrastructure/lipsum/code/PhutilPHPCodeSnippetContextFreeGrammar.php', 5663 + 'PhutilPHPFragmentLexerHighlighterTestCase' => 'infrastructure/markup/syntax/highlighter/__tests__/PhutilPHPFragmentLexerHighlighterTestCase.php', 5640 5664 'PhutilPhabricatorAuthAdapter' => 'applications/auth/adapter/PhutilPhabricatorAuthAdapter.php', 5641 5665 'PhutilProseDiff' => 'infrastructure/diff/prose/PhutilProseDiff.php', 5642 5666 'PhutilProseDiffTestCase' => 'infrastructure/diff/prose/__tests__/PhutilProseDiffTestCase.php', 5643 5667 'PhutilProseDifferenceEngine' => 'infrastructure/diff/prose/PhutilProseDifferenceEngine.php', 5668 + 'PhutilPygmentizeParser' => 'infrastructure/parser/PhutilPygmentizeParser.php', 5669 + 'PhutilPygmentizeParserTestCase' => 'infrastructure/parser/__tests__/PhutilPygmentizeParserTestCase.php', 5670 + 'PhutilPygmentsSyntaxHighlighter' => 'infrastructure/markup/syntax/highlighter/PhutilPygmentsSyntaxHighlighter.php', 5644 5671 'PhutilQsprintfInterface' => 'infrastructure/storage/xsprintf/PhutilQsprintfInterface.php', 5645 5672 'PhutilQueryString' => 'infrastructure/storage/xsprintf/PhutilQueryString.php', 5673 + 'PhutilRainbowSyntaxHighlighter' => 'infrastructure/markup/syntax/highlighter/PhutilRainbowSyntaxHighlighter.php', 5646 5674 'PhutilRealNameContextFreeGrammar' => 'infrastructure/lipsum/PhutilRealNameContextFreeGrammar.php', 5647 5675 'PhutilRemarkupAnchorRule' => 'infrastructure/markup/markuprule/PhutilRemarkupAnchorRule.php', 5648 5676 'PhutilRemarkupBlockInterpreter' => 'infrastructure/markup/blockrule/PhutilRemarkupBlockInterpreter.php', ··· 5678 5706 'PhutilRemarkupTableBlockRule' => 'infrastructure/markup/blockrule/PhutilRemarkupTableBlockRule.php', 5679 5707 'PhutilRemarkupTestInterpreterRule' => 'infrastructure/markup/blockrule/PhutilRemarkupTestInterpreterRule.php', 5680 5708 'PhutilRemarkupUnderlineRule' => 'infrastructure/markup/markuprule/PhutilRemarkupUnderlineRule.php', 5709 + 'PhutilSafeHTML' => 'infrastructure/markup/PhutilSafeHTML.php', 5710 + 'PhutilSafeHTMLProducerInterface' => 'infrastructure/markup/PhutilSafeHTMLProducerInterface.php', 5711 + 'PhutilSafeHTMLTestCase' => 'infrastructure/markup/__tests__/PhutilSafeHTMLTestCase.php', 5681 5712 'PhutilSearchQueryCompiler' => 'applications/search/compiler/PhutilSearchQueryCompiler.php', 5682 5713 'PhutilSearchQueryCompilerSyntaxException' => 'applications/search/compiler/PhutilSearchQueryCompilerSyntaxException.php', 5683 5714 'PhutilSearchQueryCompilerTestCase' => 'applications/search/compiler/__tests__/PhutilSearchQueryCompilerTestCase.php', ··· 5685 5716 'PhutilSearchStemmer' => 'applications/search/compiler/PhutilSearchStemmer.php', 5686 5717 'PhutilSearchStemmerTestCase' => 'applications/search/compiler/__tests__/PhutilSearchStemmerTestCase.php', 5687 5718 'PhutilSlackAuthAdapter' => 'applications/auth/adapter/PhutilSlackAuthAdapter.php', 5719 + 'PhutilSprite' => 'aphront/sprite/PhutilSprite.php', 5720 + 'PhutilSpriteSheet' => 'aphront/sprite/PhutilSpriteSheet.php', 5721 + 'PhutilSyntaxHighlighter' => 'infrastructure/markup/syntax/highlighter/PhutilSyntaxHighlighter.php', 5722 + 'PhutilSyntaxHighlighterEngine' => 'infrastructure/markup/syntax/engine/PhutilSyntaxHighlighterEngine.php', 5723 + 'PhutilSyntaxHighlighterException' => 'infrastructure/markup/syntax/highlighter/PhutilSyntaxHighlighterException.php', 5724 + 'PhutilTranslatedHTMLTestCase' => 'infrastructure/markup/__tests__/PhutilTranslatedHTMLTestCase.php', 5688 5725 'PhutilTwitchAuthAdapter' => 'applications/auth/adapter/PhutilTwitchAuthAdapter.php', 5689 5726 'PhutilTwitterAuthAdapter' => 'applications/auth/adapter/PhutilTwitterAuthAdapter.php', 5690 5727 'PhutilWordPressAuthAdapter' => 'applications/auth/adapter/PhutilWordPressAuthAdapter.php', 5728 + 'PhutilXHPASTSyntaxHighlighter' => 'infrastructure/markup/syntax/highlighter/PhutilXHPASTSyntaxHighlighter.php', 5729 + 'PhutilXHPASTSyntaxHighlighterFuture' => 'infrastructure/markup/syntax/highlighter/xhpast/PhutilXHPASTSyntaxHighlighterFuture.php', 5730 + 'PhutilXHPASTSyntaxHighlighterTestCase' => 'infrastructure/markup/syntax/highlighter/__tests__/PhutilXHPASTSyntaxHighlighterTestCase.php', 5691 5731 'PolicyLockOptionType' => 'applications/policy/config/PolicyLockOptionType.php', 5692 5732 'PonderAddAnswerView' => 'applications/ponder/view/PonderAddAnswerView.php', 5693 5733 'PonderAnswer' => 'applications/ponder/storage/PonderAnswer.php', ··· 5879 5919 'function' => array( 5880 5920 'celerity_generate_unique_node_id' => 'applications/celerity/api.php', 5881 5921 'celerity_get_resource_uri' => 'applications/celerity/api.php', 5922 + 'hsprintf' => 'infrastructure/markup/render.php', 5882 5923 'javelin_tag' => 'infrastructure/javelin/markup.php', 5883 5924 'phabricator_date' => 'view/viewutils.php', 5884 5925 'phabricator_datetime' => 'view/viewutils.php', ··· 5890 5931 'phid_get_subtype' => 'applications/phid/utils.php', 5891 5932 'phid_get_type' => 'applications/phid/utils.php', 5892 5933 'phid_group_by_type' => 'applications/phid/utils.php', 5934 + 'phutil_escape_html' => 'infrastructure/markup/render.php', 5935 + 'phutil_escape_html_newlines' => 'infrastructure/markup/render.php', 5936 + 'phutil_implode_html' => 'infrastructure/markup/render.php', 5937 + 'phutil_safe_html' => 'infrastructure/markup/render.php', 5938 + 'phutil_tag' => 'infrastructure/markup/render.php', 5939 + 'phutil_tag_div' => 'infrastructure/markup/render.php', 5893 5940 'qsprintf' => 'infrastructure/storage/xsprintf/qsprintf.php', 5894 5941 'qsprintf_check_scalar_type' => 'infrastructure/storage/xsprintf/qsprintf.php', 5895 5942 'qsprintf_check_type' => 'infrastructure/storage/xsprintf/qsprintf.php', ··· 12443 12490 'PhrictionTransactionComment' => 'PhabricatorApplicationTransactionComment', 12444 12491 'PhrictionTransactionEditor' => 'PhabricatorApplicationTransactionEditor', 12445 12492 'PhrictionTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 12493 + 'PhutilAPCKeyValueCache' => 'PhutilKeyValueCache', 12446 12494 'PhutilAmazonAuthAdapter' => 'PhutilOAuthAuthAdapter', 12447 12495 'PhutilAsanaAuthAdapter' => 'PhutilOAuthAuthAdapter', 12448 12496 'PhutilAuthAdapter' => 'Phobject', ··· 12472 12520 'PhutilCalendarRootNode' => 'PhutilCalendarContainerNode', 12473 12521 'PhutilCalendarUserNode' => 'PhutilCalendarNode', 12474 12522 'PhutilCodeSnippetContextFreeGrammar' => 'PhutilContextFreeGrammar', 12523 + 'PhutilConsoleSyntaxHighlighter' => 'Phobject', 12524 + 'PhutilContextFreeGrammar' => 'Phobject', 12525 + 'PhutilDefaultSyntaxHighlighter' => 'Phobject', 12526 + 'PhutilDefaultSyntaxHighlighterEngine' => 'PhutilSyntaxHighlighterEngine', 12527 + 'PhutilDefaultSyntaxHighlighterEnginePygmentsFuture' => 'FutureProxy', 12528 + 'PhutilDefaultSyntaxHighlighterEngineTestCase' => 'PhutilTestCase', 12529 + 'PhutilDirectoryKeyValueCache' => 'PhutilKeyValueCache', 12475 12530 'PhutilDisqusAuthAdapter' => 'PhutilOAuthAuthAdapter', 12531 + 'PhutilDivinerSyntaxHighlighter' => 'Phobject', 12476 12532 'PhutilEmptyAuthAdapter' => 'PhutilAuthAdapter', 12477 12533 'PhutilFacebookAuthAdapter' => 'PhutilOAuthAuthAdapter', 12478 12534 'PhutilGitHubAuthAdapter' => 'PhutilOAuthAuthAdapter', ··· 12482 12538 'PhutilICSParserTestCase' => 'PhutilTestCase', 12483 12539 'PhutilICSWriter' => 'Phobject', 12484 12540 'PhutilICSWriterTestCase' => 'PhutilTestCase', 12541 + 'PhutilInRequestKeyValueCache' => 'PhutilKeyValueCache', 12542 + 'PhutilInvisibleSyntaxHighlighter' => 'Phobject', 12485 12543 'PhutilJIRAAuthAdapter' => 'PhutilOAuth1AuthAdapter', 12544 + 'PhutilJSONFragmentLexerHighlighterTestCase' => 'PhutilTestCase', 12486 12545 'PhutilJavaCodeSnippetContextFreeGrammar' => 'PhutilCLikeCodeSnippetContextFreeGrammar', 12546 + 'PhutilKeyValueCache' => 'Phobject', 12547 + 'PhutilKeyValueCacheNamespace' => 'PhutilKeyValueCacheProxy', 12548 + 'PhutilKeyValueCacheProfiler' => 'PhutilKeyValueCacheProxy', 12549 + 'PhutilKeyValueCacheProxy' => 'PhutilKeyValueCache', 12550 + 'PhutilKeyValueCacheStack' => 'PhutilKeyValueCache', 12551 + 'PhutilKeyValueCacheTestCase' => 'PhutilTestCase', 12487 12552 'PhutilLDAPAuthAdapter' => 'PhutilAuthAdapter', 12553 + 'PhutilLexerSyntaxHighlighter' => 'PhutilSyntaxHighlighter', 12488 12554 'PhutilLipsumContextFreeGrammar' => 'PhutilContextFreeGrammar', 12555 + 'PhutilMarkupEngine' => 'Phobject', 12556 + 'PhutilMarkupTestCase' => 'PhutilTestCase', 12557 + 'PhutilMemcacheKeyValueCache' => 'PhutilKeyValueCache', 12489 12558 'PhutilOAuth1AuthAdapter' => 'PhutilAuthAdapter', 12490 12559 'PhutilOAuthAuthAdapter' => 'PhutilAuthAdapter', 12560 + 'PhutilOnDiskKeyValueCache' => 'PhutilKeyValueCache', 12491 12561 'PhutilPHPCodeSnippetContextFreeGrammar' => 'PhutilCLikeCodeSnippetContextFreeGrammar', 12562 + 'PhutilPHPFragmentLexerHighlighterTestCase' => 'PhutilTestCase', 12492 12563 'PhutilPhabricatorAuthAdapter' => 'PhutilOAuthAuthAdapter', 12493 12564 'PhutilProseDiff' => 'Phobject', 12494 12565 'PhutilProseDiffTestCase' => 'PhabricatorTestCase', 12495 12566 'PhutilProseDifferenceEngine' => 'Phobject', 12567 + 'PhutilPygmentizeParser' => 'Phobject', 12568 + 'PhutilPygmentizeParserTestCase' => 'PhutilTestCase', 12569 + 'PhutilPygmentsSyntaxHighlighter' => 'Phobject', 12496 12570 'PhutilQueryString' => 'Phobject', 12571 + 'PhutilRainbowSyntaxHighlighter' => 'Phobject', 12497 12572 'PhutilRealNameContextFreeGrammar' => 'PhutilContextFreeGrammar', 12498 12573 'PhutilRemarkupAnchorRule' => 'PhutilRemarkupRule', 12499 12574 'PhutilRemarkupBlockInterpreter' => 'Phobject', ··· 12529 12604 'PhutilRemarkupTableBlockRule' => 'PhutilRemarkupBlockRule', 12530 12605 'PhutilRemarkupTestInterpreterRule' => 'PhutilRemarkupBlockInterpreter', 12531 12606 'PhutilRemarkupUnderlineRule' => 'PhutilRemarkupRule', 12607 + 'PhutilSafeHTML' => 'Phobject', 12608 + 'PhutilSafeHTMLTestCase' => 'PhutilTestCase', 12532 12609 'PhutilSearchQueryCompiler' => 'Phobject', 12533 12610 'PhutilSearchQueryCompilerSyntaxException' => 'Exception', 12534 12611 'PhutilSearchQueryCompilerTestCase' => 'PhutilTestCase', ··· 12536 12613 'PhutilSearchStemmer' => 'Phobject', 12537 12614 'PhutilSearchStemmerTestCase' => 'PhutilTestCase', 12538 12615 'PhutilSlackAuthAdapter' => 'PhutilOAuthAuthAdapter', 12616 + 'PhutilSprite' => 'Phobject', 12617 + 'PhutilSpriteSheet' => 'Phobject', 12618 + 'PhutilSyntaxHighlighter' => 'Phobject', 12619 + 'PhutilSyntaxHighlighterEngine' => 'Phobject', 12620 + 'PhutilSyntaxHighlighterException' => 'Exception', 12621 + 'PhutilTranslatedHTMLTestCase' => 'PhutilTestCase', 12539 12622 'PhutilTwitchAuthAdapter' => 'PhutilOAuthAuthAdapter', 12540 12623 'PhutilTwitterAuthAdapter' => 'PhutilOAuth1AuthAdapter', 12541 12624 'PhutilWordPressAuthAdapter' => 'PhutilOAuthAuthAdapter', 12625 + 'PhutilXHPASTSyntaxHighlighter' => 'Phobject', 12626 + 'PhutilXHPASTSyntaxHighlighterFuture' => 'FutureProxy', 12627 + 'PhutilXHPASTSyntaxHighlighterTestCase' => 'PhutilTestCase', 12542 12628 'PolicyLockOptionType' => 'PhabricatorConfigJSONOptionType', 12543 12629 'PonderAddAnswerView' => 'AphrontView', 12544 12630 'PonderAnswer' => array(
+76
src/aphront/sprite/PhutilSprite.php
··· 1 + <?php 2 + 3 + /** 4 + * NOTE: This is very new and unstable. 5 + */ 6 + final class PhutilSprite extends Phobject { 7 + 8 + private $sourceFiles = array(); 9 + private $sourceX; 10 + private $sourceY; 11 + private $sourceW; 12 + private $sourceH; 13 + private $targetCSS; 14 + private $spriteSheet; 15 + private $name; 16 + 17 + public function setName($name) { 18 + $this->name = $name; 19 + return $this; 20 + } 21 + 22 + public function getName() { 23 + return $this->name; 24 + } 25 + 26 + public function setTargetCSS($target_css) { 27 + $this->targetCSS = $target_css; 28 + return $this; 29 + } 30 + 31 + public function getTargetCSS() { 32 + return $this->targetCSS; 33 + } 34 + 35 + public function setSourcePosition($x, $y) { 36 + $this->sourceX = $x; 37 + $this->sourceY = $y; 38 + return $this; 39 + } 40 + 41 + public function setSourceSize($w, $h) { 42 + $this->sourceW = $w; 43 + $this->sourceH = $h; 44 + return $this; 45 + } 46 + 47 + public function getSourceH() { 48 + return $this->sourceH; 49 + } 50 + 51 + public function getSourceW() { 52 + return $this->sourceW; 53 + } 54 + 55 + public function getSourceY() { 56 + return $this->sourceY; 57 + } 58 + 59 + public function getSourceX() { 60 + return $this->sourceX; 61 + } 62 + 63 + public function setSourceFile($source_file, $scale = 1) { 64 + $this->sourceFiles[$scale] = $source_file; 65 + return $this; 66 + } 67 + 68 + public function getSourceFile($scale) { 69 + if (empty($this->sourceFiles[$scale])) { 70 + throw new Exception(pht("No source file for scale '%s'!", $scale)); 71 + } 72 + 73 + return $this->sourceFiles[$scale]; 74 + } 75 + 76 + }
+385
src/aphront/sprite/PhutilSpriteSheet.php
··· 1 + <?php 2 + 3 + /** 4 + * NOTE: This is very new and unstable. 5 + */ 6 + final class PhutilSpriteSheet extends Phobject { 7 + 8 + const MANIFEST_VERSION = 1; 9 + 10 + const TYPE_STANDARD = 'standard'; 11 + const TYPE_REPEAT_X = 'repeat-x'; 12 + const TYPE_REPEAT_Y = 'repeat-y'; 13 + 14 + private $sprites = array(); 15 + private $sources = array(); 16 + private $hashes = array(); 17 + private $cssHeader; 18 + private $generated; 19 + private $scales = array(1); 20 + private $type = self::TYPE_STANDARD; 21 + private $basePath; 22 + 23 + private $css; 24 + private $images; 25 + 26 + public function addSprite(PhutilSprite $sprite) { 27 + $this->generated = false; 28 + $this->sprites[] = $sprite; 29 + return $this; 30 + } 31 + 32 + public function setCSSHeader($header) { 33 + $this->generated = false; 34 + $this->cssHeader = $header; 35 + return $this; 36 + } 37 + 38 + public function setScales(array $scales) { 39 + $this->scales = array_values($scales); 40 + return $this; 41 + } 42 + 43 + public function getScales() { 44 + return $this->scales; 45 + } 46 + 47 + public function setSheetType($type) { 48 + $this->type = $type; 49 + return $this; 50 + } 51 + 52 + public function setBasePath($base_path) { 53 + $this->basePath = $base_path; 54 + return $this; 55 + } 56 + 57 + private function generate() { 58 + if ($this->generated) { 59 + return; 60 + } 61 + 62 + $multi_row = true; 63 + $multi_col = true; 64 + $margin_w = 1; 65 + $margin_h = 1; 66 + 67 + $type = $this->type; 68 + switch ($type) { 69 + case self::TYPE_STANDARD: 70 + break; 71 + case self::TYPE_REPEAT_X: 72 + $multi_col = false; 73 + $margin_w = 0; 74 + 75 + $width = null; 76 + foreach ($this->sprites as $sprite) { 77 + if ($width === null) { 78 + $width = $sprite->getSourceW(); 79 + } else if ($width !== $sprite->getSourceW()) { 80 + throw new Exception( 81 + pht( 82 + "All sprites in a '%s' sheet must have the same width.", 83 + 'repeat-x')); 84 + } 85 + } 86 + break; 87 + case self::TYPE_REPEAT_Y: 88 + $multi_row = false; 89 + $margin_h = 0; 90 + 91 + $height = null; 92 + foreach ($this->sprites as $sprite) { 93 + if ($height === null) { 94 + $height = $sprite->getSourceH(); 95 + } else if ($height !== $sprite->getSourceH()) { 96 + throw new Exception( 97 + pht( 98 + "All sprites in a '%s' sheet must have the same height.", 99 + 'repeat-y')); 100 + } 101 + } 102 + break; 103 + default: 104 + throw new Exception(pht("Unknown sprite sheet type '%s'!", $type)); 105 + } 106 + 107 + 108 + $css = array(); 109 + if ($this->cssHeader) { 110 + $css[] = $this->cssHeader; 111 + } 112 + 113 + $out_w = 0; 114 + $out_h = 0; 115 + 116 + // Lay out the sprite sheet. We attempt to build a roughly square sheet 117 + // so it's easier to manage, since 2000x20 is more cumbersome for humans 118 + // to deal with than 200x200. 119 + // 120 + // To do this, we use a simple greedy algorithm, adding sprites one at a 121 + // time. For each sprite, if the sheet is at least as wide as it is tall 122 + // we create a new row. Otherwise, we try to add it to an existing row. 123 + // 124 + // This isn't optimal, but does a reasonable job in most cases and isn't 125 + // too messy. 126 + 127 + // Group the sprites by their sizes. We lay them out in the sheet as 128 + // boxes, but then put them into the boxes in the order they were added 129 + // so similar sprites end up nearby on the final sheet. 130 + $boxes = array(); 131 + foreach (array_reverse($this->sprites) as $sprite) { 132 + $s_w = $sprite->getSourceW() + $margin_w; 133 + $s_h = $sprite->getSourceH() + $margin_h; 134 + $boxes[$s_w][$s_h][] = $sprite; 135 + } 136 + 137 + $rows = array(); 138 + foreach ($this->sprites as $sprite) { 139 + $s_w = $sprite->getSourceW() + $margin_w; 140 + $s_h = $sprite->getSourceH() + $margin_h; 141 + 142 + // Choose a row for this sprite. 143 + $maybe = array(); 144 + foreach ($rows as $key => $row) { 145 + if ($row['h'] < $s_h) { 146 + // We can only add it to a row if the row is at least as tall as the 147 + // sprite. 148 + continue; 149 + } 150 + // We prefer rows which have the same height as the sprite, and then 151 + // rows which aren't yet very wide. 152 + $wasted_v = ($row['h'] - $s_h); 153 + $wasted_h = ($row['w'] / $out_w); 154 + $maybe[$key] = $wasted_v + $wasted_h; 155 + } 156 + 157 + $row_key = null; 158 + if ($maybe && $multi_col) { 159 + // If there were any candidate rows, pick the best one. 160 + asort($maybe); 161 + $row_key = head_key($maybe); 162 + } 163 + 164 + if ($row_key !== null && $multi_row) { 165 + // If there's a candidate row, but adding the sprite to it would make 166 + // the sprite wider than it is tall, create a new row instead. This 167 + // generally keeps the sprite square-ish. 168 + if ($rows[$row_key]['w'] + $s_w > $out_h) { 169 + $row_key = null; 170 + } 171 + } 172 + 173 + if ($row_key === null) { 174 + // Add a new row. 175 + $rows[] = array( 176 + 'w' => 0, 177 + 'h' => $s_h, 178 + 'boxes' => array(), 179 + ); 180 + $row_key = last_key($rows); 181 + $out_h += $s_h; 182 + } 183 + 184 + // Add the sprite box to the row. 185 + $row = $rows[$row_key]; 186 + $row['w'] += $s_w; 187 + $row['boxes'][] = array($s_w, $s_h); 188 + $rows[$row_key] = $row; 189 + 190 + $out_w = max($row['w'], $out_w); 191 + } 192 + 193 + $images = array(); 194 + foreach ($this->scales as $scale) { 195 + $img = imagecreatetruecolor($out_w * $scale, $out_h * $scale); 196 + imagesavealpha($img, true); 197 + imagefill($img, 0, 0, imagecolorallocatealpha($img, 0, 0, 0, 127)); 198 + 199 + $images[$scale] = $img; 200 + } 201 + 202 + 203 + // Put the shorter rows first. At the same height, put the wider rows first. 204 + // This makes the resulting sheet more human-readable. 205 + foreach ($rows as $key => $row) { 206 + $rows[$key]['sort'] = $row['h'] + (1 - ($row['w'] / $out_w)); 207 + } 208 + $rows = isort($rows, 'sort'); 209 + 210 + $pos_x = 0; 211 + $pos_y = 0; 212 + $rules = array(); 213 + foreach ($rows as $row) { 214 + $max_h = 0; 215 + foreach ($row['boxes'] as $box) { 216 + $sprite = array_pop($boxes[$box[0]][$box[1]]); 217 + 218 + foreach ($images as $scale => $img) { 219 + $src = $this->loadSource($sprite, $scale); 220 + imagecopy( 221 + $img, 222 + $src, 223 + $scale * $pos_x, $scale * $pos_y, 224 + $scale * $sprite->getSourceX(), $scale * $sprite->getSourceY(), 225 + $scale * $sprite->getSourceW(), $scale * $sprite->getSourceH()); 226 + } 227 + 228 + $rule = $sprite->getTargetCSS(); 229 + $cssx = (-$pos_x).'px'; 230 + $cssy = (-$pos_y).'px'; 231 + 232 + $rules[$sprite->getName()] = "{$rule} {\n". 233 + " background-position: {$cssx} {$cssy};\n}"; 234 + 235 + $pos_x += $sprite->getSourceW() + $margin_w; 236 + $max_h = max($max_h, $sprite->getSourceH()); 237 + } 238 + $pos_x = 0; 239 + $pos_y += $max_h + $margin_h; 240 + } 241 + 242 + // Generate CSS rules in input order. 243 + foreach ($this->sprites as $sprite) { 244 + $css[] = $rules[$sprite->getName()]; 245 + } 246 + 247 + $this->images = $images; 248 + $this->css = implode("\n\n", $css)."\n"; 249 + $this->generated = true; 250 + } 251 + 252 + public function generateImage($path, $scale = 1) { 253 + $this->generate(); 254 + $this->log(pht("Writing sprite '%s'...", $path)); 255 + imagepng($this->images[$scale], $path); 256 + return $this; 257 + } 258 + 259 + public function generateCSS($path) { 260 + $this->generate(); 261 + $this->log(pht("Writing CSS '%s'...", $path)); 262 + 263 + $out = $this->css; 264 + $out = str_replace('{X}', imagesx($this->images[1]), $out); 265 + $out = str_replace('{Y}', imagesy($this->images[1]), $out); 266 + 267 + Filesystem::writeFile($path, $out); 268 + return $this; 269 + } 270 + 271 + public function needsRegeneration(array $manifest) { 272 + return ($this->buildManifest() !== $manifest); 273 + } 274 + 275 + private function buildManifest() { 276 + $output = array(); 277 + foreach ($this->sprites as $sprite) { 278 + $output[$sprite->getName()] = array( 279 + 'name' => $sprite->getName(), 280 + 'rule' => $sprite->getTargetCSS(), 281 + 'hash' => $this->loadSourceHash($sprite), 282 + ); 283 + } 284 + 285 + ksort($output); 286 + 287 + $data = array( 288 + 'version' => self::MANIFEST_VERSION, 289 + 'sprites' => $output, 290 + 'scales' => $this->scales, 291 + 'header' => $this->cssHeader, 292 + 'type' => $this->type, 293 + ); 294 + 295 + return $data; 296 + } 297 + 298 + public function generateManifest($path) { 299 + $data = $this->buildManifest(); 300 + 301 + $json = new PhutilJSON(); 302 + $data = $json->encodeFormatted($data); 303 + Filesystem::writeFile($path, $data); 304 + return $this; 305 + } 306 + 307 + private function log($message) { 308 + echo $message."\n"; 309 + } 310 + 311 + private function loadSourceHash(PhutilSprite $sprite) { 312 + $inputs = array(); 313 + 314 + foreach ($this->scales as $scale) { 315 + $file = $sprite->getSourceFile($scale); 316 + 317 + // If two users have a project in different places, like: 318 + // 319 + // /home/alincoln/project 320 + // /home/htaft/project 321 + // 322 + // ...we want to ignore the `/home/alincoln` part when hashing the sheet, 323 + // since the sprites don't change when the project directory moves. If 324 + // the base path is set, build the hashes using paths relative to the 325 + // base path. 326 + 327 + $file_key = $file; 328 + if ($this->basePath) { 329 + $file_key = Filesystem::readablePath($file, $this->basePath); 330 + } 331 + 332 + if (empty($this->hashes[$file_key])) { 333 + $this->hashes[$file_key] = md5(Filesystem::readFile($file)); 334 + } 335 + 336 + $inputs[] = $file_key; 337 + $inputs[] = $this->hashes[$file_key]; 338 + } 339 + 340 + $inputs[] = $sprite->getSourceX(); 341 + $inputs[] = $sprite->getSourceY(); 342 + $inputs[] = $sprite->getSourceW(); 343 + $inputs[] = $sprite->getSourceH(); 344 + 345 + return md5(implode(':', $inputs)); 346 + } 347 + 348 + private function loadSource(PhutilSprite $sprite, $scale) { 349 + $file = $sprite->getSourceFile($scale); 350 + if (empty($this->sources[$file])) { 351 + $data = Filesystem::readFile($file); 352 + $image = imagecreatefromstring($data); 353 + $this->sources[$file] = array( 354 + 'image' => $image, 355 + 'x' => imagesx($image), 356 + 'y' => imagesy($image), 357 + ); 358 + } 359 + 360 + $s_w = $sprite->getSourceW() * $scale; 361 + $i_w = $this->sources[$file]['x']; 362 + if ($s_w > $i_w) { 363 + throw new Exception( 364 + pht( 365 + "Sprite source for '%s' is too small (expected width %d, found %d).", 366 + $file, 367 + $s_w, 368 + $i_w)); 369 + } 370 + 371 + $s_h = $sprite->getSourceH() * $scale; 372 + $i_h = $this->sources[$file]['y']; 373 + if ($s_h > $i_h) { 374 + throw new Exception( 375 + pht( 376 + "Sprite source for '%s' is too small (expected height %d, found %d).", 377 + $file, 378 + $s_h, 379 + $i_h)); 380 + } 381 + 382 + return $this->sources[$file]['image']; 383 + } 384 + 385 + }
+1 -1
src/applications/differential/parser/DifferentialChangesetParser.php
··· 592 592 $result = $text; 593 593 594 594 if (isset($intra[$key])) { 595 - $result = ArcanistDiffUtils::applyIntralineDiff( 595 + $result = PhabricatorDifferenceEngine::applyIntralineDiff( 596 596 $result, 597 597 $intra[$key]); 598 598 }
+2 -2
src/applications/files/document/PhabricatorJupyterDocumentEngine.php
··· 129 129 $v_segments[] = $v_segment; 130 130 } 131 131 132 - $usource = ArcanistDiffUtils::applyIntralineDiff( 132 + $usource = PhabricatorDifferenceEngine::applyIntralineDiff( 133 133 $udisplay, 134 134 $u_segments); 135 135 136 - $vsource = ArcanistDiffUtils::applyIntralineDiff( 136 + $vsource = PhabricatorDifferenceEngine::applyIntralineDiff( 137 137 $vdisplay, 138 138 $v_segments); 139 139
+97
src/infrastructure/cache/PhutilAPCKeyValueCache.php
··· 1 + <?php 2 + 3 + /** 4 + * Interface to the APC key-value cache. This is a very high-performance cache 5 + * which is local to the current machine. 6 + */ 7 + final class PhutilAPCKeyValueCache extends PhutilKeyValueCache { 8 + 9 + 10 + /* -( Key-Value Cache Implementation )------------------------------------- */ 11 + 12 + 13 + public function isAvailable() { 14 + return (function_exists('apc_fetch') || function_exists('apcu_fetch')) && 15 + ini_get('apc.enabled') && 16 + (ini_get('apc.enable_cli') || php_sapi_name() != 'cli'); 17 + } 18 + 19 + public function getKeys(array $keys, $ttl = null) { 20 + static $is_apcu; 21 + if ($is_apcu === null) { 22 + $is_apcu = self::isAPCu(); 23 + } 24 + 25 + $results = array(); 26 + $fetched = false; 27 + foreach ($keys as $key) { 28 + if ($is_apcu) { 29 + $result = apcu_fetch($key, $fetched); 30 + } else { 31 + $result = apc_fetch($key, $fetched); 32 + } 33 + 34 + if ($fetched) { 35 + $results[$key] = $result; 36 + } 37 + } 38 + return $results; 39 + } 40 + 41 + public function setKeys(array $keys, $ttl = null) { 42 + static $is_apcu; 43 + if ($is_apcu === null) { 44 + $is_apcu = self::isAPCu(); 45 + } 46 + 47 + // NOTE: Although modern APC supports passing an array to `apc_store()`, 48 + // it is not supported by older version of APC or by HPHP. 49 + 50 + foreach ($keys as $key => $value) { 51 + if ($is_apcu) { 52 + apcu_store($key, $value, $ttl); 53 + } else { 54 + apc_store($key, $value, $ttl); 55 + } 56 + } 57 + 58 + return $this; 59 + } 60 + 61 + public function deleteKeys(array $keys) { 62 + static $is_apcu; 63 + if ($is_apcu === null) { 64 + $is_apcu = self::isAPCu(); 65 + } 66 + 67 + foreach ($keys as $key) { 68 + if ($is_apcu) { 69 + apcu_delete($key); 70 + } else { 71 + apc_delete($key); 72 + } 73 + } 74 + 75 + return $this; 76 + } 77 + 78 + public function destroyCache() { 79 + static $is_apcu; 80 + if ($is_apcu === null) { 81 + $is_apcu = self::isAPCu(); 82 + } 83 + 84 + if ($is_apcu) { 85 + apcu_clear_cache(); 86 + } else { 87 + apc_clear_cache('user'); 88 + } 89 + 90 + return $this; 91 + } 92 + 93 + private static function isAPCu() { 94 + return function_exists('apcu_fetch'); 95 + } 96 + 97 + }
+244
src/infrastructure/cache/PhutilDirectoryKeyValueCache.php
··· 1 + <?php 2 + 3 + /** 4 + * Interface to a directory-based disk cache. Storage persists across requests. 5 + * 6 + * This cache is very very slow, and most suitable for command line scripts 7 + * which need to build large caches derived from sources like working copies 8 + * (for example, Diviner). This cache performs better for large amounts of 9 + * data than @{class:PhutilOnDiskKeyValueCache} because each key is serialized 10 + * individually, but this comes at the cost of having even slower reads and 11 + * writes. 12 + * 13 + * In addition to having slow reads and writes, this entire cache locks for 14 + * any read or write activity. 15 + * 16 + * Keys for this cache treat the character "/" specially, and encode it as 17 + * a new directory on disk. This can help keep the cache organized and keep the 18 + * number of items in any single directory under control, by using keys like 19 + * "ab/cd/efghijklmn". 20 + * 21 + * @task kvimpl Key-Value Cache Implementation 22 + * @task storage Cache Storage 23 + */ 24 + final class PhutilDirectoryKeyValueCache extends PhutilKeyValueCache { 25 + 26 + private $lock; 27 + private $cacheDirectory; 28 + 29 + 30 + /* -( Key-Value Cache Implementation )------------------------------------- */ 31 + 32 + 33 + public function isAvailable() { 34 + return true; 35 + } 36 + 37 + 38 + public function getKeys(array $keys) { 39 + $this->validateKeys($keys); 40 + 41 + try { 42 + $this->lockCache(); 43 + } catch (PhutilLockException $ex) { 44 + return array(); 45 + } 46 + 47 + $now = time(); 48 + 49 + $results = array(); 50 + foreach ($keys as $key) { 51 + $key_file = $this->getKeyFile($key); 52 + try { 53 + $data = Filesystem::readFile($key_file); 54 + } catch (FilesystemException $ex) { 55 + continue; 56 + } 57 + 58 + $data = unserialize($data); 59 + if (!$data) { 60 + continue; 61 + } 62 + 63 + if (isset($data['ttl']) && $data['ttl'] < $now) { 64 + continue; 65 + } 66 + 67 + $results[$key] = $data['value']; 68 + } 69 + 70 + $this->unlockCache(); 71 + 72 + return $results; 73 + } 74 + 75 + 76 + public function setKeys(array $keys, $ttl = null) { 77 + $this->validateKeys(array_keys($keys)); 78 + 79 + $this->lockCache(15); 80 + 81 + if ($ttl) { 82 + $ttl_epoch = time() + $ttl; 83 + } else { 84 + $ttl_epoch = null; 85 + } 86 + 87 + foreach ($keys as $key => $value) { 88 + $dict = array( 89 + 'value' => $value, 90 + ); 91 + if ($ttl_epoch) { 92 + $dict['ttl'] = $ttl_epoch; 93 + } 94 + 95 + try { 96 + $key_file = $this->getKeyFile($key); 97 + $key_dir = dirname($key_file); 98 + if (!Filesystem::pathExists($key_dir)) { 99 + Filesystem::createDirectory( 100 + $key_dir, 101 + $mask = 0755, 102 + $recursive = true); 103 + } 104 + 105 + $new_file = $key_file.'.new'; 106 + Filesystem::writeFile($new_file, serialize($dict)); 107 + Filesystem::rename($new_file, $key_file); 108 + } catch (FilesystemException $ex) { 109 + phlog($ex); 110 + } 111 + } 112 + 113 + $this->unlockCache(); 114 + 115 + return $this; 116 + } 117 + 118 + 119 + public function deleteKeys(array $keys) { 120 + $this->validateKeys($keys); 121 + 122 + $this->lockCache(15); 123 + 124 + foreach ($keys as $key) { 125 + $path = $this->getKeyFile($key); 126 + Filesystem::remove($path); 127 + 128 + // If removing this key leaves the directory empty, clean it up. Then 129 + // clean up any empty parent directories. 130 + $path = dirname($path); 131 + do { 132 + if (!Filesystem::isDescendant($path, $this->getCacheDirectory())) { 133 + break; 134 + } 135 + if (Filesystem::listDirectory($path, true)) { 136 + break; 137 + } 138 + Filesystem::remove($path); 139 + $path = dirname($path); 140 + } while (true); 141 + } 142 + 143 + $this->unlockCache(); 144 + 145 + return $this; 146 + } 147 + 148 + 149 + public function destroyCache() { 150 + Filesystem::remove($this->getCacheDirectory()); 151 + return $this; 152 + } 153 + 154 + 155 + /* -( Cache Storage )------------------------------------------------------ */ 156 + 157 + 158 + /** 159 + * @task storage 160 + */ 161 + public function setCacheDirectory($directory) { 162 + $this->cacheDirectory = rtrim($directory, '/').'/'; 163 + return $this; 164 + } 165 + 166 + 167 + /** 168 + * @task storage 169 + */ 170 + private function getCacheDirectory() { 171 + if (!$this->cacheDirectory) { 172 + throw new PhutilInvalidStateException('setCacheDirectory'); 173 + } 174 + return $this->cacheDirectory; 175 + } 176 + 177 + 178 + /** 179 + * @task storage 180 + */ 181 + private function getKeyFile($key) { 182 + // Colon is a drive separator on Windows. 183 + $key = str_replace(':', '_', $key); 184 + 185 + // NOTE: We add ".cache" to each file so we don't get a collision if you 186 + // set the keys "a" and "a/b". Without ".cache", the file "a" would need 187 + // to be both a file and a directory. 188 + return $this->getCacheDirectory().$key.'.cache'; 189 + } 190 + 191 + 192 + /** 193 + * @task storage 194 + */ 195 + private function validateKeys(array $keys) { 196 + foreach ($keys as $key) { 197 + // NOTE: Use of "." is reserved for ".lock", "key.new" and "key.cache". 198 + // Use of "_" is reserved for converting ":". 199 + if (!preg_match('@^[a-zA-Z0-9/:-]+$@', $key)) { 200 + throw new Exception( 201 + pht( 202 + "Invalid key '%s': directory caches may only contain letters, ". 203 + "numbers, hyphen, colon and slash.", 204 + $key)); 205 + } 206 + } 207 + } 208 + 209 + 210 + /** 211 + * @task storage 212 + */ 213 + private function lockCache($wait = 0) { 214 + if ($this->lock) { 215 + throw new Exception( 216 + pht( 217 + 'Trying to %s with a lock!', 218 + __FUNCTION__.'()')); 219 + } 220 + 221 + if (!Filesystem::pathExists($this->getCacheDirectory())) { 222 + Filesystem::createDirectory($this->getCacheDirectory(), 0755, true); 223 + } 224 + 225 + $lock = PhutilFileLock::newForPath($this->getCacheDirectory().'.lock'); 226 + $lock->lock($wait); 227 + 228 + $this->lock = $lock; 229 + } 230 + 231 + 232 + /** 233 + * @task storage 234 + */ 235 + private function unlockCache() { 236 + if (!$this->lock) { 237 + throw new PhutilInvalidStateException('lockCache'); 238 + } 239 + 240 + $this->lock->unlock(); 241 + $this->lock = null; 242 + } 243 + 244 + }
+118
src/infrastructure/cache/PhutilInRequestKeyValueCache.php
··· 1 + <?php 2 + 3 + /** 4 + * Key-value cache implemented in the current request. All storage is local 5 + * to this request (i.e., the current page) and destroyed after the request 6 + * exits. This means the first request to this cache for a given key on a page 7 + * will ALWAYS miss. 8 + * 9 + * This cache exists mostly to support unit tests. In a well-designed 10 + * applications, you generally should not be fetching the same data over and 11 + * over again in one request, so this cache should be of limited utility. 12 + * If using this cache improves application performance, especially if it 13 + * improves it significantly, it may indicate an architectural problem in your 14 + * application. 15 + */ 16 + final class PhutilInRequestKeyValueCache extends PhutilKeyValueCache { 17 + 18 + private $cache = array(); 19 + private $ttl = array(); 20 + private $limit = 0; 21 + 22 + 23 + /** 24 + * Set a limit on the number of keys this cache may contain. 25 + * 26 + * When too many keys are inserted, the oldest keys are removed from the 27 + * cache. Setting a limit of `0` disables the cache. 28 + * 29 + * @param int Maximum number of items to store in the cache. 30 + * @return this 31 + */ 32 + public function setLimit($limit) { 33 + $this->limit = $limit; 34 + return $this; 35 + } 36 + 37 + 38 + /* -( Key-Value Cache Implementation )------------------------------------- */ 39 + 40 + 41 + public function isAvailable() { 42 + return true; 43 + } 44 + 45 + public function getKeys(array $keys) { 46 + $results = array(); 47 + $now = time(); 48 + foreach ($keys as $key) { 49 + if (!isset($this->cache[$key]) && !array_key_exists($key, $this->cache)) { 50 + continue; 51 + } 52 + if (isset($this->ttl[$key]) && ($this->ttl[$key] < $now)) { 53 + continue; 54 + } 55 + $results[$key] = $this->cache[$key]; 56 + } 57 + 58 + return $results; 59 + } 60 + 61 + public function setKeys(array $keys, $ttl = null) { 62 + 63 + foreach ($keys as $key => $value) { 64 + $this->cache[$key] = $value; 65 + } 66 + 67 + if ($ttl) { 68 + $end = time() + $ttl; 69 + foreach ($keys as $key => $value) { 70 + $this->ttl[$key] = $end; 71 + } 72 + } else { 73 + foreach ($keys as $key => $value) { 74 + unset($this->ttl[$key]); 75 + } 76 + } 77 + 78 + if ($this->limit) { 79 + $count = count($this->cache); 80 + if ($count > $this->limit) { 81 + $remove = array(); 82 + foreach ($this->cache as $key => $value) { 83 + $remove[] = $key; 84 + 85 + $count--; 86 + if ($count <= $this->limit) { 87 + break; 88 + } 89 + } 90 + 91 + $this->deleteKeys($remove); 92 + } 93 + } 94 + 95 + return $this; 96 + } 97 + 98 + public function deleteKeys(array $keys) { 99 + foreach ($keys as $key) { 100 + unset($this->cache[$key]); 101 + unset($this->ttl[$key]); 102 + } 103 + 104 + return $this; 105 + } 106 + 107 + public function getAllKeys() { 108 + return $this->cache; 109 + } 110 + 111 + public function destroyCache() { 112 + $this->cache = array(); 113 + $this->ttl = array(); 114 + 115 + return $this; 116 + } 117 + 118 + }
+121
src/infrastructure/cache/PhutilKeyValueCache.php
··· 1 + <?php 2 + 3 + /** 4 + * Interface to a key-value cache like Memcache or APC. This class provides a 5 + * uniform interface to multiple different key-value caches and integration 6 + * with PhutilServiceProfiler. 7 + * 8 + * @task kvimpl Key-Value Cache Implementation 9 + */ 10 + abstract class PhutilKeyValueCache extends Phobject { 11 + 12 + 13 + /* -( Key-Value Cache Implementation )------------------------------------- */ 14 + 15 + 16 + /** 17 + * Determine if the cache is available. For example, the APC cache tests if 18 + * APC is installed. If this method returns false, the cache is not 19 + * operational and can not be used. 20 + * 21 + * @return bool True if the cache can be used. 22 + * @task kvimpl 23 + */ 24 + public function isAvailable() { 25 + return false; 26 + } 27 + 28 + 29 + /** 30 + * Get a single key from cache. See @{method:getKeys} to get multiple keys at 31 + * once. 32 + * 33 + * @param string Key to retrieve. 34 + * @param wild Optional value to return if the key is not found. By 35 + * default, returns null. 36 + * @return wild Cache value (on cache hit) or default value (on cache 37 + * miss). 38 + * @task kvimpl 39 + */ 40 + final public function getKey($key, $default = null) { 41 + $map = $this->getKeys(array($key)); 42 + return idx($map, $key, $default); 43 + } 44 + 45 + 46 + /** 47 + * Set a single key in cache. See @{method:setKeys} to set multiple keys at 48 + * once. 49 + * 50 + * See @{method:setKeys} for a description of TTLs. 51 + * 52 + * @param string Key to set. 53 + * @param wild Value to set. 54 + * @param int|null Optional TTL. 55 + * @return this 56 + * @task kvimpl 57 + */ 58 + final public function setKey($key, $value, $ttl = null) { 59 + return $this->setKeys(array($key => $value), $ttl); 60 + } 61 + 62 + 63 + /** 64 + * Delete a key from the cache. See @{method:deleteKeys} to delete multiple 65 + * keys at once. 66 + * 67 + * @param string Key to delete. 68 + * @return this 69 + * @task kvimpl 70 + */ 71 + final public function deleteKey($key) { 72 + return $this->deleteKeys(array($key)); 73 + } 74 + 75 + 76 + /** 77 + * Get data from the cache. 78 + * 79 + * @param list<string> List of cache keys to retrieve. 80 + * @return dict<string, wild> Dictionary of keys that were found in the 81 + * cache. Keys not present in the cache are 82 + * omitted, so you can detect a cache miss. 83 + * @task kvimpl 84 + */ 85 + abstract public function getKeys(array $keys); 86 + 87 + 88 + /** 89 + * Put data into the key-value cache. 90 + * 91 + * With a TTL ("time to live"), the cache will automatically delete the key 92 + * after a specified number of seconds. By default, there is no expiration 93 + * policy and data will persist in cache indefinitely. 94 + * 95 + * @param dict<string, wild> Map of cache keys to values. 96 + * @param int|null TTL for cache keys, in seconds. 97 + * @return this 98 + * @task kvimpl 99 + */ 100 + abstract public function setKeys(array $keys, $ttl = null); 101 + 102 + 103 + /** 104 + * Delete a list of keys from the cache. 105 + * 106 + * @param list<string> List of keys to delete. 107 + * @return this 108 + * @task kvimpl 109 + */ 110 + abstract public function deleteKeys(array $keys); 111 + 112 + 113 + /** 114 + * Completely destroy all data in the cache. 115 + * 116 + * @return this 117 + * @task kvimpl 118 + */ 119 + abstract public function destroyCache(); 120 + 121 + }
+65
src/infrastructure/cache/PhutilKeyValueCacheNamespace.php
··· 1 + <?php 2 + 3 + final class PhutilKeyValueCacheNamespace extends PhutilKeyValueCacheProxy { 4 + 5 + private $namespace; 6 + 7 + public function setNamespace($namespace) { 8 + if (strpos($namespace, ':') !== false) { 9 + throw new Exception(pht("Namespace can't contain colons.")); 10 + } 11 + 12 + $this->namespace = $namespace.':'; 13 + 14 + return $this; 15 + } 16 + 17 + public function setKeys(array $keys, $ttl = null) { 18 + return parent::setKeys(array_combine( 19 + $this->prefixKeys(array_keys($keys)), 20 + $keys), $ttl); 21 + } 22 + 23 + public function getKeys(array $keys) { 24 + $results = parent::getKeys($this->prefixKeys($keys)); 25 + 26 + if (!$results) { 27 + return array(); 28 + } 29 + 30 + return array_combine( 31 + $this->unprefixKeys(array_keys($results)), 32 + $results); 33 + } 34 + 35 + public function deleteKeys(array $keys) { 36 + return parent::deleteKeys($this->prefixKeys($keys)); 37 + } 38 + 39 + private function prefixKeys(array $keys) { 40 + if ($this->namespace == null) { 41 + throw new Exception(pht('Namespace not set.')); 42 + } 43 + 44 + $prefixed_keys = array(); 45 + foreach ($keys as $key) { 46 + $prefixed_keys[] = $this->namespace.$key; 47 + } 48 + 49 + return $prefixed_keys; 50 + } 51 + 52 + private function unprefixKeys(array $keys) { 53 + if ($this->namespace == null) { 54 + throw new Exception(pht('Namespace not set.')); 55 + } 56 + 57 + $unprefixed_keys = array(); 58 + foreach ($keys as $key) { 59 + $unprefixed_keys[] = substr($key, strlen($this->namespace)); 60 + } 61 + 62 + return $unprefixed_keys; 63 + } 64 + 65 + }
+108
src/infrastructure/cache/PhutilKeyValueCacheProfiler.php
··· 1 + <?php 2 + 3 + final class PhutilKeyValueCacheProfiler extends PhutilKeyValueCacheProxy { 4 + 5 + private $profiler; 6 + private $name; 7 + 8 + public function setName($name) { 9 + $this->name = $name; 10 + return $this; 11 + } 12 + 13 + public function getName() { 14 + return $this->name; 15 + } 16 + 17 + /** 18 + * Set a profiler for cache operations. 19 + * 20 + * @param PhutilServiceProfiler Service profiler. 21 + * @return this 22 + * @task kvimpl 23 + */ 24 + public function setProfiler(PhutilServiceProfiler $profiler) { 25 + $this->profiler = $profiler; 26 + return $this; 27 + } 28 + 29 + 30 + /** 31 + * Get the current profiler. 32 + * 33 + * @return PhutilServiceProfiler|null Profiler, or null if none is set. 34 + * @task kvimpl 35 + */ 36 + public function getProfiler() { 37 + return $this->profiler; 38 + } 39 + 40 + 41 + public function getKeys(array $keys) { 42 + $call_id = null; 43 + if ($this->getProfiler()) { 44 + $call_id = $this->getProfiler()->beginServiceCall( 45 + array( 46 + 'type' => 'kvcache-get', 47 + 'name' => $this->getName(), 48 + 'keys' => $keys, 49 + )); 50 + } 51 + 52 + $results = parent::getKeys($keys); 53 + 54 + if ($call_id !== null) { 55 + $this->getProfiler()->endServiceCall( 56 + $call_id, 57 + array( 58 + 'hits' => array_keys($results), 59 + )); 60 + } 61 + 62 + return $results; 63 + } 64 + 65 + 66 + public function setKeys(array $keys, $ttl = null) { 67 + $call_id = null; 68 + if ($this->getProfiler()) { 69 + $call_id = $this->getProfiler()->beginServiceCall( 70 + array( 71 + 'type' => 'kvcache-set', 72 + 'name' => $this->getName(), 73 + 'keys' => array_keys($keys), 74 + 'ttl' => $ttl, 75 + )); 76 + } 77 + 78 + $result = parent::setKeys($keys, $ttl); 79 + 80 + if ($call_id !== null) { 81 + $this->getProfiler()->endServiceCall($call_id, array()); 82 + } 83 + 84 + return $result; 85 + } 86 + 87 + 88 + public function deleteKeys(array $keys) { 89 + $call_id = null; 90 + if ($this->getProfiler()) { 91 + $call_id = $this->getProfiler()->beginServiceCall( 92 + array( 93 + 'type' => 'kvcache-del', 94 + 'name' => $this->getName(), 95 + 'keys' => $keys, 96 + )); 97 + } 98 + 99 + $result = parent::deleteKeys($keys); 100 + 101 + if ($call_id !== null) { 102 + $this->getProfiler()->endServiceCall($call_id, array()); 103 + } 104 + 105 + return $result; 106 + } 107 + 108 + }
+45
src/infrastructure/cache/PhutilKeyValueCacheProxy.php
··· 1 + <?php 2 + 3 + abstract class PhutilKeyValueCacheProxy extends PhutilKeyValueCache { 4 + 5 + private $proxy; 6 + 7 + final public function __construct(PhutilKeyValueCache $proxy) { 8 + $this->proxy = $proxy; 9 + } 10 + 11 + final protected function getProxy() { 12 + return $this->proxy; 13 + } 14 + 15 + public function isAvailable() { 16 + return $this->getProxy()->isAvailable(); 17 + } 18 + 19 + 20 + public function getKeys(array $keys) { 21 + return $this->getProxy()->getKeys($keys); 22 + } 23 + 24 + 25 + public function setKeys(array $keys, $ttl = null) { 26 + return $this->getProxy()->setKeys($keys, $ttl); 27 + } 28 + 29 + 30 + public function deleteKeys(array $keys) { 31 + return $this->getProxy()->deleteKeys($keys); 32 + } 33 + 34 + 35 + public function destroyCache() { 36 + return $this->getProxy()->destroyCache(); 37 + } 38 + 39 + public function __call($method, array $arguments) { 40 + return call_user_func_array( 41 + array($this->getProxy(), $method), 42 + $arguments); 43 + } 44 + 45 + }
+131
src/infrastructure/cache/PhutilKeyValueCacheStack.php
··· 1 + <?php 2 + 3 + /** 4 + * Stacks multiple caches on top of each other, with readthrough semantics: 5 + * 6 + * - For reads, we try each cache in order until we find all the keys. 7 + * - For writes, we set the keys in each cache. 8 + * 9 + * @task config Configuring the Stack 10 + */ 11 + final class PhutilKeyValueCacheStack extends PhutilKeyValueCache { 12 + 13 + 14 + /** 15 + * Forward list of caches in the stack (from the nearest cache to the farthest 16 + * cache). 17 + */ 18 + private $cachesForward; 19 + 20 + 21 + /** 22 + * Backward list of caches in the stack (from the farthest cache to the 23 + * nearest cache). 24 + */ 25 + private $cachesBackward; 26 + 27 + 28 + /** 29 + * TTL to use for any writes which are side effects of the next read 30 + * operation. 31 + */ 32 + private $nextTTL; 33 + 34 + 35 + /* -( Configuring the Stack )---------------------------------------------- */ 36 + 37 + 38 + /** 39 + * Set the caches which comprise this stack. 40 + * 41 + * @param list<PhutilKeyValueCache> Ordered list of key-value caches. 42 + * @return this 43 + * @task config 44 + */ 45 + public function setCaches(array $caches) { 46 + assert_instances_of($caches, 'PhutilKeyValueCache'); 47 + $this->cachesForward = $caches; 48 + $this->cachesBackward = array_reverse($caches); 49 + 50 + return $this; 51 + } 52 + 53 + 54 + /** 55 + * Set the readthrough TTL for the next cache operation. The TTL applies to 56 + * any keys set by the next call to @{method:getKey} or @{method:getKeys}, 57 + * and is reset after the call finishes. 58 + * 59 + * // If this causes any caches to fill, they'll fill with a 15-second TTL. 60 + * $stack->setNextTTL(15)->getKey('porcupine'); 61 + * 62 + * // TTL does not persist; this will use no TTL. 63 + * $stack->getKey('hedgehog'); 64 + * 65 + * @param int TTL in seconds. 66 + * @return this 67 + * 68 + * @task config 69 + */ 70 + public function setNextTTL($ttl) { 71 + $this->nextTTL = $ttl; 72 + return $this; 73 + } 74 + 75 + 76 + /* -( Key-Value Cache Implementation )------------------------------------- */ 77 + 78 + 79 + public function getKeys(array $keys) { 80 + 81 + $remaining = array_fuse($keys); 82 + $results = array(); 83 + $missed = array(); 84 + 85 + try { 86 + foreach ($this->cachesForward as $cache) { 87 + $result = $cache->getKeys($remaining); 88 + $remaining = array_diff_key($remaining, $result); 89 + $results += $result; 90 + if (!$remaining) { 91 + while ($cache = array_pop($missed)) { 92 + // TODO: This sets too many results in the closer caches, although 93 + // it probably isn't a big deal in most cases; normally we're just 94 + // filling the request cache. 95 + $cache->setKeys($result, $this->nextTTL); 96 + } 97 + break; 98 + } 99 + $missed[] = $cache; 100 + } 101 + $this->nextTTL = null; 102 + } catch (Exception $ex) { 103 + $this->nextTTL = null; 104 + throw $ex; 105 + } 106 + 107 + return $results; 108 + } 109 + 110 + 111 + public function setKeys(array $keys, $ttl = null) { 112 + foreach ($this->cachesBackward as $cache) { 113 + $cache->setKeys($keys, $ttl); 114 + } 115 + } 116 + 117 + 118 + public function deleteKeys(array $keys) { 119 + foreach ($this->cachesBackward as $cache) { 120 + $cache->deleteKeys($keys); 121 + } 122 + } 123 + 124 + 125 + public function destroyCache() { 126 + foreach ($this->cachesBackward as $cache) { 127 + $cache->destroyCache(); 128 + } 129 + } 130 + 131 + }
+153
src/infrastructure/cache/PhutilMemcacheKeyValueCache.php
··· 1 + <?php 2 + 3 + /** 4 + * @task memcache Managing Memcache 5 + */ 6 + final class PhutilMemcacheKeyValueCache extends PhutilKeyValueCache { 7 + 8 + private $servers = array(); 9 + private $connections = array(); 10 + 11 + 12 + /* -( Key-Value Cache Implementation )------------------------------------- */ 13 + 14 + 15 + public function isAvailable() { 16 + return function_exists('memcache_pconnect'); 17 + } 18 + 19 + public function getKeys(array $keys) { 20 + $buckets = $this->bucketKeys($keys); 21 + $results = array(); 22 + 23 + foreach ($buckets as $bucket => $bucket_keys) { 24 + $conn = $this->getConnection($bucket); 25 + $result = $conn->get($bucket_keys); 26 + if (!$result) { 27 + // If the call fails, treat it as a miss on all keys. 28 + $result = array(); 29 + } 30 + 31 + $results += $result; 32 + } 33 + 34 + return $results; 35 + } 36 + 37 + public function setKeys(array $keys, $ttl = null) { 38 + $buckets = $this->bucketKeys(array_keys($keys)); 39 + 40 + // Memcache interprets TTLs as: 41 + // 42 + // - Seconds from now, for values from 1 to 2592000 (30 days). 43 + // - Epoch timestamp, for values larger than 2592000. 44 + // 45 + // We support only relative TTLs, so convert excessively large relative 46 + // TTLs into epoch TTLs. 47 + if ($ttl > 2592000) { 48 + $effective_ttl = time() + $ttl; 49 + } else { 50 + $effective_ttl = $ttl; 51 + } 52 + 53 + foreach ($buckets as $bucket => $bucket_keys) { 54 + $conn = $this->getConnection($bucket); 55 + 56 + foreach ($bucket_keys as $key) { 57 + $conn->set($key, $keys[$key], 0, $effective_ttl); 58 + } 59 + } 60 + 61 + return $this; 62 + } 63 + 64 + public function deleteKeys(array $keys) { 65 + $buckets = $this->bucketKeys($keys); 66 + 67 + foreach ($buckets as $bucket => $bucket_keys) { 68 + $conn = $this->getConnection($bucket); 69 + foreach ($bucket_keys as $key) { 70 + $conn->delete($key); 71 + } 72 + } 73 + 74 + return $this; 75 + } 76 + 77 + public function destroyCache() { 78 + foreach ($this->servers as $key => $spec) { 79 + $this->getConnection($key)->flush(); 80 + } 81 + return $this; 82 + } 83 + 84 + 85 + /* -( Managing Memcache )-------------------------------------------------- */ 86 + 87 + 88 + /** 89 + * Set available memcache servers. For example: 90 + * 91 + * $cache->setServers( 92 + * array( 93 + * array( 94 + * 'host' => '10.0.0.20', 95 + * 'port' => 11211, 96 + * ), 97 + * array( 98 + * 'host' => '10.0.0.21', 99 + * 'port' => 11211, 100 + * ), 101 + * )); 102 + * 103 + * @param list<dict> List of server specifications. 104 + * @return this 105 + * @task memcache 106 + */ 107 + public function setServers(array $servers) { 108 + $this->servers = array_values($servers); 109 + return $this; 110 + } 111 + 112 + private function bucketKeys(array $keys) { 113 + $buckets = array(); 114 + $n = count($this->servers); 115 + 116 + if (!$n) { 117 + throw new PhutilInvalidStateException('setServers'); 118 + } 119 + 120 + foreach ($keys as $key) { 121 + $bucket = (int)((crc32($key) & 0x7FFFFFFF) % $n); 122 + $buckets[$bucket][] = $key; 123 + } 124 + 125 + return $buckets; 126 + } 127 + 128 + 129 + /** 130 + * @phutil-external-symbol function memcache_pconnect 131 + */ 132 + private function getConnection($server) { 133 + if (empty($this->connections[$server])) { 134 + $spec = $this->servers[$server]; 135 + $host = $spec['host']; 136 + $port = $spec['port']; 137 + 138 + $conn = memcache_pconnect($host, $spec['port']); 139 + 140 + if (!$conn) { 141 + throw new Exception( 142 + pht( 143 + 'Unable to connect to memcache server (%s:%d)!', 144 + $host, 145 + $port)); 146 + } 147 + 148 + $this->connections[$server] = $conn; 149 + } 150 + return $this->connections[$server]; 151 + } 152 + 153 + }
+205
src/infrastructure/cache/PhutilOnDiskKeyValueCache.php
··· 1 + <?php 2 + 3 + /** 4 + * Interface to a disk cache. Storage persists across requests. 5 + * 6 + * This cache is very slow compared to caches like APC. It is intended as a 7 + * specialized alternative to APC when APC is not available. 8 + * 9 + * This is a highly specialized cache and not appropriate for use as a 10 + * generalized key-value cache for arbitrary application data. 11 + * 12 + * Also note that reading and writing keys from the cache currently involves 13 + * loading and saving the entire cache, no matter how little data you touch. 14 + * 15 + * @task kvimpl Key-Value Cache Implementation 16 + * @task storage Cache Storage 17 + */ 18 + final class PhutilOnDiskKeyValueCache extends PhutilKeyValueCache { 19 + 20 + private $cache = array(); 21 + private $cacheFile; 22 + private $lock; 23 + private $wait = 0; 24 + 25 + 26 + /* -( Key-Value Cache Implementation )------------------------------------- */ 27 + 28 + 29 + public function isAvailable() { 30 + return true; 31 + } 32 + 33 + 34 + /** 35 + * Set duration (in seconds) to wait for the file lock. 36 + */ 37 + public function setWait($wait) { 38 + $this->wait = $wait; 39 + return $this; 40 + } 41 + 42 + public function getKeys(array $keys) { 43 + $now = time(); 44 + 45 + $results = array(); 46 + $reloaded = false; 47 + foreach ($keys as $key) { 48 + 49 + // Try to read the value from cache. If we miss, load (or reload) the 50 + // cache. 51 + 52 + while (true) { 53 + if (isset($this->cache[$key])) { 54 + $val = $this->cache[$key]; 55 + if (empty($val['ttl']) || $val['ttl'] >= $now) { 56 + $results[$key] = $val['val']; 57 + break; 58 + } 59 + } 60 + 61 + if ($reloaded) { 62 + break; 63 + } 64 + 65 + $this->loadCache($hold_lock = false); 66 + $reloaded = true; 67 + } 68 + } 69 + 70 + return $results; 71 + } 72 + 73 + 74 + public function setKeys(array $keys, $ttl = null) { 75 + if ($ttl) { 76 + $ttl_epoch = time() + $ttl; 77 + } else { 78 + $ttl_epoch = null; 79 + } 80 + 81 + $dicts = array(); 82 + foreach ($keys as $key => $value) { 83 + $dict = array( 84 + 'val' => $value, 85 + ); 86 + if ($ttl_epoch) { 87 + $dict['ttl'] = $ttl_epoch; 88 + } 89 + $dicts[$key] = $dict; 90 + } 91 + 92 + $this->loadCache($hold_lock = true); 93 + foreach ($dicts as $key => $dict) { 94 + $this->cache[$key] = $dict; 95 + } 96 + $this->saveCache(); 97 + 98 + return $this; 99 + } 100 + 101 + 102 + public function deleteKeys(array $keys) { 103 + $this->loadCache($hold_lock = true); 104 + foreach ($keys as $key) { 105 + unset($this->cache[$key]); 106 + } 107 + $this->saveCache(); 108 + 109 + return $this; 110 + } 111 + 112 + 113 + public function destroyCache() { 114 + Filesystem::remove($this->getCacheFile()); 115 + return $this; 116 + } 117 + 118 + 119 + /* -( Cache Storage )------------------------------------------------------ */ 120 + 121 + 122 + /** 123 + * @task storage 124 + */ 125 + public function setCacheFile($file) { 126 + $this->cacheFile = $file; 127 + return $this; 128 + } 129 + 130 + 131 + /** 132 + * @task storage 133 + */ 134 + private function loadCache($hold_lock) { 135 + if ($this->lock) { 136 + throw new Exception( 137 + pht( 138 + 'Trying to %s with a lock!', 139 + __FUNCTION__.'()')); 140 + } 141 + 142 + $lock = PhutilFileLock::newForPath($this->getCacheFile().'.lock'); 143 + try { 144 + $lock->lock($this->wait); 145 + } catch (PhutilLockException $ex) { 146 + if ($hold_lock) { 147 + throw $ex; 148 + } else { 149 + $this->cache = array(); 150 + return; 151 + } 152 + } 153 + 154 + try { 155 + $this->cache = array(); 156 + if (Filesystem::pathExists($this->getCacheFile())) { 157 + $cache = unserialize(Filesystem::readFile($this->getCacheFile())); 158 + if ($cache) { 159 + $this->cache = $cache; 160 + } 161 + } 162 + } catch (Exception $ex) { 163 + $lock->unlock(); 164 + throw $ex; 165 + } 166 + 167 + if ($hold_lock) { 168 + $this->lock = $lock; 169 + } else { 170 + $lock->unlock(); 171 + } 172 + } 173 + 174 + 175 + /** 176 + * @task storage 177 + */ 178 + private function saveCache() { 179 + if (!$this->lock) { 180 + throw new PhutilInvalidStateException('loadCache'); 181 + } 182 + 183 + // We're holding a lock so we're safe to do a write to a well-known file. 184 + // Write to the same directory as the cache so the rename won't imply a 185 + // copy across volumes. 186 + $new = $this->getCacheFile().'.new'; 187 + Filesystem::writeFile($new, serialize($this->cache)); 188 + Filesystem::rename($new, $this->getCacheFile()); 189 + 190 + $this->lock->unlock(); 191 + $this->lock = null; 192 + } 193 + 194 + 195 + /** 196 + * @task storage 197 + */ 198 + private function getCacheFile() { 199 + if (!$this->cacheFile) { 200 + throw new PhutilInvalidStateException('setCacheFile'); 201 + } 202 + return $this->cacheFile; 203 + } 204 + 205 + }
+267
src/infrastructure/cache/__tests__/PhutilKeyValueCacheTestCase.php
··· 1 + <?php 2 + 3 + final class PhutilKeyValueCacheTestCase extends PhutilTestCase { 4 + 5 + public function testInRequestCache() { 6 + $cache = new PhutilInRequestKeyValueCache(); 7 + $this->doCacheTest($cache); 8 + $cache->destroyCache(); 9 + } 10 + 11 + public function testInRequestCacheLimit() { 12 + $cache = new PhutilInRequestKeyValueCache(); 13 + $cache->setLimit(4); 14 + 15 + $cache->setKey(1, 1); 16 + $cache->setKey(2, 2); 17 + $cache->setKey(3, 3); 18 + $cache->setKey(4, 4); 19 + 20 + $this->assertEqual( 21 + array( 22 + 1 => 1, 23 + 2 => 2, 24 + 3 => 3, 25 + 4 => 4, 26 + ), 27 + $cache->getAllKeys()); 28 + 29 + 30 + $cache->setKey(5, 5); 31 + 32 + $this->assertEqual( 33 + array( 34 + 2 => 2, 35 + 3 => 3, 36 + 4 => 4, 37 + 5 => 5, 38 + ), 39 + $cache->getAllKeys()); 40 + } 41 + 42 + public function testOnDiskCache() { 43 + $cache = new PhutilOnDiskKeyValueCache(); 44 + $cache->setCacheFile(new TempFile()); 45 + $this->doCacheTest($cache); 46 + $cache->destroyCache(); 47 + } 48 + 49 + public function testAPCCache() { 50 + $cache = new PhutilAPCKeyValueCache(); 51 + if (!$cache->isAvailable()) { 52 + $this->assertSkipped(pht('Cache not available.')); 53 + } 54 + $this->doCacheTest($cache); 55 + } 56 + 57 + public function testDirectoryCache() { 58 + $cache = new PhutilDirectoryKeyValueCache(); 59 + 60 + $dir = Filesystem::createTemporaryDirectory(); 61 + $cache->setCacheDirectory($dir); 62 + $this->doCacheTest($cache); 63 + $cache->destroyCache(); 64 + } 65 + 66 + public function testDirectoryCacheSpecialDirectoryRules() { 67 + $cache = new PhutilDirectoryKeyValueCache(); 68 + 69 + $dir = Filesystem::createTemporaryDirectory(); 70 + $dir = $dir.'/dircache/'; 71 + $cache->setCacheDirectory($dir); 72 + 73 + $cache->setKey('a', 1); 74 + $this->assertEqual(true, Filesystem::pathExists($dir.'/a.cache')); 75 + 76 + $cache->setKey('a/b', 1); 77 + $this->assertEqual(true, Filesystem::pathExists($dir.'/a/')); 78 + $this->assertEqual(true, Filesystem::pathExists($dir.'/a/b.cache')); 79 + 80 + $cache->deleteKey('a/b'); 81 + $this->assertEqual(false, Filesystem::pathExists($dir.'/a/')); 82 + $this->assertEqual(false, Filesystem::pathExists($dir.'/a/b.cache')); 83 + 84 + $cache->destroyCache(); 85 + $this->assertEqual(false, Filesystem::pathExists($dir)); 86 + } 87 + 88 + public function testNamespaceCache() { 89 + $namespace = 'namespace'.mt_rand(); 90 + $in_request_cache = new PhutilInRequestKeyValueCache(); 91 + $cache = new PhutilKeyValueCacheNamespace($in_request_cache); 92 + $cache->setNamespace($namespace); 93 + 94 + $test_info = get_class($cache); 95 + $keys = array( 96 + 'key1' => mt_rand(), 97 + 'key2' => '', 98 + 'key3' => 'Phabricator', 99 + ); 100 + $cache->setKeys($keys); 101 + $cached_keys = $in_request_cache->getAllKeys(); 102 + 103 + foreach ($keys as $key => $value) { 104 + $cached_key = $namespace.':'.$key; 105 + 106 + $this->assertTrue( 107 + isset($cached_keys[$cached_key]), 108 + $test_info); 109 + 110 + $this->assertEqual( 111 + $value, 112 + $cached_keys[$cached_key], 113 + $test_info); 114 + } 115 + 116 + $cache->destroyCache(); 117 + 118 + $this->doCacheTest($cache); 119 + $cache->destroyCache(); 120 + } 121 + 122 + public function testCacheStack() { 123 + $req_cache = new PhutilInRequestKeyValueCache(); 124 + $disk_cache = new PhutilOnDiskKeyValueCache(); 125 + $disk_cache->setCacheFile(new TempFile()); 126 + $apc_cache = new PhutilAPCKeyValueCache(); 127 + 128 + $stack = array( 129 + $req_cache, 130 + $disk_cache, 131 + ); 132 + 133 + if ($apc_cache->isAvailable()) { 134 + $stack[] = $apc_cache; 135 + } 136 + 137 + $cache = new PhutilKeyValueCacheStack(); 138 + $cache->setCaches($stack); 139 + 140 + $this->doCacheTest($cache); 141 + 142 + $disk_cache->destroyCache(); 143 + $req_cache->destroyCache(); 144 + } 145 + 146 + private function doCacheTest(PhutilKeyValueCache $cache) { 147 + $key1 = 'test:'.mt_rand(); 148 + $key2 = 'test:'.mt_rand(); 149 + 150 + $default = 'cache-miss'; 151 + $value1 = 'cache-hit1'; 152 + $value2 = 'cache-hit2'; 153 + 154 + $test_info = get_class($cache); 155 + 156 + // Test that we miss correctly on missing values. 157 + 158 + $this->assertEqual( 159 + $default, 160 + $cache->getKey($key1, $default), 161 + $test_info); 162 + $this->assertEqual( 163 + array( 164 + ), 165 + $cache->getKeys(array($key1, $key2)), 166 + $test_info); 167 + 168 + 169 + // Test that we can set individual keys. 170 + 171 + $cache->setKey($key1, $value1); 172 + $this->assertEqual( 173 + $value1, 174 + $cache->getKey($key1, $default), 175 + $test_info); 176 + $this->assertEqual( 177 + array( 178 + $key1 => $value1, 179 + ), 180 + $cache->getKeys(array($key1, $key2)), 181 + $test_info); 182 + 183 + 184 + // Test that we can delete individual keys. 185 + 186 + $cache->deleteKey($key1); 187 + 188 + $this->assertEqual( 189 + $default, 190 + $cache->getKey($key1, $default), 191 + $test_info); 192 + $this->assertEqual( 193 + array( 194 + ), 195 + $cache->getKeys(array($key1, $key2)), 196 + $test_info); 197 + 198 + 199 + 200 + // Test that we can set multiple keys. 201 + 202 + $cache->setKeys( 203 + array( 204 + $key1 => $value1, 205 + $key2 => $value2, 206 + )); 207 + 208 + $this->assertEqual( 209 + $value1, 210 + $cache->getKey($key1, $default), 211 + $test_info); 212 + $this->assertEqual( 213 + array( 214 + $key1 => $value1, 215 + $key2 => $value2, 216 + ), 217 + $cache->getKeys(array($key1, $key2)), 218 + $test_info); 219 + 220 + 221 + // Test that we can delete multiple keys. 222 + 223 + $cache->deleteKeys(array($key1, $key2)); 224 + 225 + $this->assertEqual( 226 + $default, 227 + $cache->getKey($key1, $default), 228 + $test_info); 229 + $this->assertEqual( 230 + array( 231 + ), 232 + $cache->getKeys(array($key1, $key2)), 233 + $test_info); 234 + 235 + 236 + // NOTE: The TTL tests are necessarily slow (we must sleep() through the 237 + // TTLs) and do not work with APC (it does not TTL until the next request) 238 + // so they're disabled by default. If you're developing the cache stack, 239 + // it may be useful to run them. 240 + 241 + return; 242 + 243 + // Test that keys expire when they TTL. 244 + 245 + $cache->setKey($key1, $value1, 1); 246 + $cache->setKey($key2, $value2, 5); 247 + 248 + $this->assertEqual($value1, $cache->getKey($key1, $default)); 249 + $this->assertEqual($value2, $cache->getKey($key2, $default)); 250 + 251 + sleep(2); 252 + 253 + $this->assertEqual($default, $cache->getKey($key1, $default)); 254 + $this->assertEqual($value2, $cache->getKey($key2, $default)); 255 + 256 + 257 + // Test that setting a 0 TTL overwrites a nonzero TTL. 258 + 259 + $cache->setKey($key1, $value1, 1); 260 + $this->assertEqual($value1, $cache->getKey($key1, $default)); 261 + $cache->setKey($key1, $value1, 0); 262 + $this->assertEqual($value1, $cache->getKey($key1, $default)); 263 + sleep(2); 264 + $this->assertEqual($value1, $cache->getKey($key1, $default)); 265 + } 266 + 267 + }
+89
src/infrastructure/diff/PhabricatorDifferenceEngine.php
··· 169 169 return $corpus; 170 170 } 171 171 172 + public static function applyIntralineDiff($str, $intra_stack) { 173 + $buf = ''; 174 + $p = $s = $e = 0; // position, start, end 175 + $highlight = $tag = $ent = false; 176 + $highlight_o = '<span class="bright">'; 177 + $highlight_c = '</span>'; 178 + 179 + $depth_in = '<span class="depth-in">'; 180 + $depth_out = '<span class="depth-out">'; 181 + 182 + $is_html = false; 183 + if ($str instanceof PhutilSafeHTML) { 184 + $is_html = true; 185 + $str = $str->getHTMLContent(); 186 + } 187 + 188 + $n = strlen($str); 189 + for ($i = 0; $i < $n; $i++) { 190 + 191 + if ($p == $e) { 192 + do { 193 + if (empty($intra_stack)) { 194 + $buf .= substr($str, $i); 195 + break 2; 196 + } 197 + $stack = array_shift($intra_stack); 198 + $s = $e; 199 + $e += $stack[1]; 200 + } while ($stack[0] === 0); 201 + 202 + switch ($stack[0]) { 203 + case '>': 204 + $open_tag = $depth_in; 205 + break; 206 + case '<': 207 + $open_tag = $depth_out; 208 + break; 209 + default: 210 + $open_tag = $highlight_o; 211 + break; 212 + } 213 + } 214 + 215 + if (!$highlight && !$tag && !$ent && $p == $s) { 216 + $buf .= $open_tag; 217 + $highlight = true; 218 + } 219 + 220 + if ($str[$i] == '<') { 221 + $tag = true; 222 + if ($highlight) { 223 + $buf .= $highlight_c; 224 + } 225 + } 226 + 227 + if (!$tag) { 228 + if ($str[$i] == '&') { 229 + $ent = true; 230 + } 231 + if ($ent && $str[$i] == ';') { 232 + $ent = false; 233 + } 234 + if (!$ent) { 235 + $p++; 236 + } 237 + } 238 + 239 + $buf .= $str[$i]; 240 + 241 + if ($tag && $str[$i] == '>') { 242 + $tag = false; 243 + if ($highlight) { 244 + $buf .= $open_tag; 245 + } 246 + } 247 + 248 + if ($highlight && ($p == $e || $i == $n - 1)) { 249 + $buf .= $highlight_c; 250 + $highlight = false; 251 + } 252 + } 253 + 254 + if ($is_html) { 255 + return phutil_safe_html($buf); 256 + } 257 + 258 + return $buf; 259 + } 260 + 172 261 }
+93
src/infrastructure/lipsum/PhutilContextFreeGrammar.php
··· 1 + <?php 2 + 3 + /** 4 + * Generate nonsense test data according to a context-free grammar definition. 5 + */ 6 + abstract class PhutilContextFreeGrammar extends Phobject { 7 + 8 + private $limit = 65535; 9 + 10 + abstract protected function getRules(); 11 + 12 + public function generateSeveral($count, $implode = ' ') { 13 + $paragraph = array(); 14 + for ($ii = 0; $ii < $count; $ii++) { 15 + $paragraph[$ii] = $this->generate(); 16 + } 17 + return implode($implode, $paragraph); 18 + } 19 + 20 + public function generate() { 21 + $count = 0; 22 + $rules = $this->getRules(); 23 + return $this->applyRules('[start]', $count, $rules); 24 + } 25 + 26 + final protected function applyRules($input, &$count, array $rules) { 27 + if (++$count > $this->limit) { 28 + throw new Exception(pht('Token replacement count exceeded limit!')); 29 + } 30 + 31 + $matches = null; 32 + preg_match_all('/(\\[[^\\]]+\\])/', $input, $matches, PREG_OFFSET_CAPTURE); 33 + 34 + foreach (array_reverse($matches[1]) as $token_spec) { 35 + list($token, $offset) = $token_spec; 36 + $token_name = substr($token, 1, -1); 37 + $options = array(); 38 + 39 + if (($name_end = strpos($token_name, ','))) { 40 + $options_parser = new PhutilSimpleOptions(); 41 + $options = $options_parser->parse($token_name); 42 + $token_name = substr($token_name, 0, $name_end); 43 + } 44 + 45 + if (empty($rules[$token_name])) { 46 + throw new Exception(pht("Invalid token '%s' in grammar.", $token_name)); 47 + } 48 + 49 + $key = array_rand($rules[$token_name]); 50 + $replacement = $this->applyRules($rules[$token_name][$key], 51 + $count, $rules); 52 + 53 + if (isset($options['indent'])) { 54 + if (is_numeric($options['indent'])) { 55 + $replacement = self::strPadLines($replacement, $options['indent']); 56 + } else { 57 + $replacement = self::strPadLines($replacement); 58 + } 59 + } 60 + if (isset($options['trim'])) { 61 + switch ($options['trim']) { 62 + case 'left': 63 + $replacement = ltrim($replacement); 64 + break; 65 + case 'right': 66 + $replacement = rtrim($replacement); 67 + break; 68 + default: 69 + case 'both': 70 + $replacement = trim($replacement); 71 + break; 72 + } 73 + } 74 + if (isset($options['block'])) { 75 + $replacement = "\n".$replacement."\n"; 76 + } 77 + 78 + $input = substr_replace($input, $replacement, $offset, strlen($token)); 79 + } 80 + 81 + return $input; 82 + } 83 + 84 + private static function strPadLines($text, $num_spaces = 2) { 85 + $text_lines = phutil_split_lines($text); 86 + foreach ($text_lines as $linenr => $line) { 87 + $text_lines[$linenr] = str_repeat(' ', $num_spaces).$line; 88 + } 89 + 90 + return implode('', $text_lines); 91 + } 92 + 93 + }
+32
src/infrastructure/markup/PhutilMarkupEngine.php
··· 1 + <?php 2 + 3 + abstract class PhutilMarkupEngine extends Phobject { 4 + 5 + /** 6 + * Set a configuration parameter which the engine can read to customize how 7 + * the text is marked up. This is a generic interface; consult the 8 + * documentation for specific rules and blocks for what options are available 9 + * for configuration. 10 + * 11 + * @param string Key to set in the configuration dictionary. 12 + * @param string Value to set. 13 + * @return this 14 + */ 15 + abstract public function setConfig($key, $value); 16 + 17 + /** 18 + * After text has been marked up with @{method:markupText}, you can retrieve 19 + * any metadata the markup process generated by calling this method. This is 20 + * a generic interface that allows rules to export extra information about 21 + * text; consult the documentation for specific rules and blocks to see what 22 + * metadata may be available in your configuration. 23 + * 24 + * @param string Key to retrieve from metadata. 25 + * @param mixed Default value to return if the key is not available. 26 + * @return mixed Metadata property, or default value. 27 + */ 28 + abstract public function getTextMetadata($key, $default = null); 29 + 30 + abstract public function markupText($text); 31 + 32 + }
+44
src/infrastructure/markup/PhutilSafeHTML.php
··· 1 + <?php 2 + 3 + final class PhutilSafeHTML extends Phobject { 4 + 5 + private $content; 6 + 7 + public function __construct($content) { 8 + $this->content = (string)$content; 9 + } 10 + 11 + public function __toString() { 12 + return $this->content; 13 + } 14 + 15 + public function getHTMLContent() { 16 + return $this->content; 17 + } 18 + 19 + public function appendHTML($html /* , ... */) { 20 + foreach (func_get_args() as $html) { 21 + $this->content .= phutil_escape_html($html); 22 + } 23 + return $this; 24 + } 25 + 26 + public static function applyFunction($function, $string /* , ... */) { 27 + $args = func_get_args(); 28 + array_shift($args); 29 + $args = array_map('phutil_escape_html', $args); 30 + return new PhutilSafeHTML(call_user_func_array($function, $args)); 31 + } 32 + 33 + // Requires http://pecl.php.net/operator. 34 + 35 + public function __concat($html) { 36 + $clone = clone $this; 37 + return $clone->appendHTML($html); 38 + } 39 + 40 + public function __assign_concat($html) { 41 + return $this->appendHTML($html); 42 + } 43 + 44 + }
+12
src/infrastructure/markup/PhutilSafeHTMLProducerInterface.php
··· 1 + <?php 2 + 3 + /** 4 + * Implement this interface to mark an object as capable of producing a 5 + * PhutilSafeHTML representation. This is primarily useful for building 6 + * renderable HTML views. 7 + */ 8 + interface PhutilSafeHTMLProducerInterface { 9 + 10 + public function producePhutilSafeHTML(); 11 + 12 + }
+223
src/infrastructure/markup/__tests__/PhutilMarkupTestCase.php
··· 1 + <?php 2 + 3 + final class PhutilMarkupTestCase extends PhutilTestCase { 4 + 5 + public function testTagDefaults() { 6 + $this->assertEqual( 7 + (string)phutil_tag('br'), 8 + (string)phutil_tag('br', array())); 9 + 10 + $this->assertEqual( 11 + (string)phutil_tag('br', array()), 12 + (string)phutil_tag('br', array(), null)); 13 + } 14 + 15 + public function testTagEmpty() { 16 + $this->assertEqual( 17 + '<br />', 18 + (string)phutil_tag('br', array(), null)); 19 + 20 + $this->assertEqual( 21 + '<div></div>', 22 + (string)phutil_tag('div', array(), null)); 23 + 24 + $this->assertEqual( 25 + '<div></div>', 26 + (string)phutil_tag('div', array(), '')); 27 + } 28 + 29 + public function testTagBasics() { 30 + $this->assertEqual( 31 + '<br />', 32 + (string)phutil_tag('br')); 33 + 34 + $this->assertEqual( 35 + '<div>y</div>', 36 + (string)phutil_tag('div', array(), 'y')); 37 + } 38 + 39 + public function testTagAttributes() { 40 + $this->assertEqual( 41 + '<div u="v">y</div>', 42 + (string)phutil_tag('div', array('u' => 'v'), 'y')); 43 + 44 + $this->assertEqual( 45 + '<br u="v" />', 46 + (string)phutil_tag('br', array('u' => 'v'))); 47 + } 48 + 49 + public function testTagEscapes() { 50 + $this->assertEqual( 51 + '<br u="&lt;" />', 52 + (string)phutil_tag('br', array('u' => '<'))); 53 + 54 + $this->assertEqual( 55 + '<div><br /></div>', 56 + (string)phutil_tag('div', array(), phutil_tag('br'))); 57 + } 58 + 59 + public function testTagNullAttribute() { 60 + $this->assertEqual( 61 + '<br />', 62 + (string)phutil_tag('br', array('y' => null))); 63 + } 64 + 65 + public function testTagJavascriptProtocolRejection() { 66 + $hrefs = array( 67 + 'javascript:alert(1)' => true, 68 + 'JAVASCRIPT:alert(2)' => true, 69 + 70 + // NOTE: When interpreted as a URI, this is dropped because of leading 71 + // whitespace. 72 + ' javascript:alert(3)' => array(true, false), 73 + '/' => false, 74 + '/path/to/stuff/' => false, 75 + '' => false, 76 + 'http://example.com/' => false, 77 + '#' => false, 78 + 'javascript://anything' => true, 79 + 80 + // Chrome 33 and IE11, at a minimum, treat this as Javascript. 81 + "javascript\n:alert(4)" => true, 82 + 83 + // Opera currently accepts a variety of unicode spaces. This test case 84 + // has a smattering of them. 85 + "\xE2\x80\x89javascript:" => true, 86 + "javascript\xE2\x80\x89:" => true, 87 + "\xE2\x80\x84javascript:" => true, 88 + "javascript\xE2\x80\x84:" => true, 89 + 90 + // Because we're aggressive, all of unicode should trigger detection 91 + // by default. 92 + "\xE2\x98\x83javascript:" => true, 93 + "javascript\xE2\x98\x83:" => true, 94 + "\xE2\x98\x83javascript\xE2\x98\x83:" => true, 95 + 96 + // We're aggressive about this, so we'll intentionally raise false 97 + // positives in these cases. 98 + 'javascript~:alert(5)' => true, 99 + '!!!javascript!!!!:alert(6)' => true, 100 + 101 + // However, we should raise true negatives in these slightly more 102 + // reasonable cases. 103 + 'javascript/:docs.html' => false, 104 + 'javascripts:x.png' => false, 105 + 'COOLjavascript:page' => false, 106 + '/javascript:alert(1)' => false, 107 + ); 108 + 109 + foreach (array(true, false) as $use_uri) { 110 + foreach ($hrefs as $href => $expect) { 111 + if (is_array($expect)) { 112 + $expect = ($use_uri ? $expect[1] : $expect[0]); 113 + } 114 + 115 + if ($use_uri) { 116 + $href_value = new PhutilURI($href); 117 + } else { 118 + $href_value = $href; 119 + } 120 + 121 + $caught = null; 122 + try { 123 + phutil_tag('a', array('href' => $href_value), 'click for candy'); 124 + } catch (Exception $ex) { 125 + $caught = $ex; 126 + } 127 + 128 + $desc = pht( 129 + 'Unexpected result for "%s". <uri = %s, expect exception = %s>', 130 + $href, 131 + $use_uri ? pht('Yes') : pht('No'), 132 + $expect ? pht('Yes') : pht('No')); 133 + 134 + $this->assertEqual( 135 + $expect, 136 + $caught instanceof Exception, 137 + $desc); 138 + } 139 + } 140 + } 141 + 142 + public function testURIEscape() { 143 + $this->assertEqual( 144 + '%2B/%20%3F%23%26%3A%21xyz%25', 145 + phutil_escape_uri('+/ ?#&:!xyz%')); 146 + } 147 + 148 + public function testURIPathComponentEscape() { 149 + $this->assertEqual( 150 + 'a%252Fb', 151 + phutil_escape_uri_path_component('a/b')); 152 + 153 + $str = ''; 154 + for ($ii = 0; $ii <= 255; $ii++) { 155 + $str .= chr($ii); 156 + } 157 + 158 + $this->assertEqual( 159 + $str, 160 + phutil_unescape_uri_path_component( 161 + rawurldecode( // Simulates webserver. 162 + phutil_escape_uri_path_component($str)))); 163 + } 164 + 165 + public function testHsprintf() { 166 + $this->assertEqual( 167 + '<div>&lt;3</div>', 168 + (string)hsprintf('<div>%s</div>', '<3')); 169 + } 170 + 171 + public function testAppendHTML() { 172 + $html = phutil_tag('hr'); 173 + $html->appendHTML(phutil_tag('br'), '<evil>'); 174 + $this->assertEqual('<hr /><br />&lt;evil&gt;', $html->getHTMLContent()); 175 + } 176 + 177 + public function testArrayEscaping() { 178 + $this->assertEqual( 179 + '<div>&lt;div&gt;</div>', 180 + phutil_escape_html( 181 + array( 182 + hsprintf('<div>'), 183 + array( 184 + array( 185 + '<', 186 + array( 187 + 'd', 188 + array( 189 + array( 190 + hsprintf('i'), 191 + ), 192 + 'v', 193 + ), 194 + ), 195 + array( 196 + array( 197 + '>', 198 + ), 199 + ), 200 + ), 201 + ), 202 + hsprintf('</div>'), 203 + ))); 204 + 205 + $this->assertEqual( 206 + '<div><br /><hr /><wbr /></div>', 207 + phutil_tag( 208 + 'div', 209 + array(), 210 + array( 211 + array( 212 + array( 213 + phutil_tag('br'), 214 + array( 215 + phutil_tag('hr'), 216 + ), 217 + phutil_tag('wbr'), 218 + ), 219 + ), 220 + ))->getHTMLContent()); 221 + } 222 + 223 + }
+19
src/infrastructure/markup/__tests__/PhutilSafeHTMLTestCase.php
··· 1 + <?php 2 + 3 + final class PhutilSafeHTMLTestCase extends PhutilTestCase { 4 + 5 + public function testOperator() { 6 + if (!extension_loaded('operator')) { 7 + $this->assertSkipped(pht('Operator extension not available.')); 8 + } 9 + 10 + $a = phutil_tag('a'); 11 + $ab = $a.phutil_tag('b'); 12 + $this->assertEqual('<a></a><b></b>', $ab->getHTMLContent()); 13 + $this->assertEqual('<a></a>', $a->getHTMLContent()); 14 + 15 + $a .= phutil_tag('a'); 16 + $this->assertEqual('<a></a><a></a>', $a->getHTMLContent()); 17 + } 18 + 19 + }
+68
src/infrastructure/markup/__tests__/PhutilTranslatedHTMLTestCase.php
··· 1 + <?php 2 + 3 + final class PhutilTranslatedHTMLTestCase extends PhutilTestCase { 4 + 5 + public function testHTMLTranslations() { 6 + $string = '%s awoke <strong>suddenly</strong> at %s.'; 7 + $when = '<4 AM>'; 8 + 9 + $translator = $this->newTranslator('en_US'); 10 + 11 + // When no components are HTML, everything is treated as a string. 12 + $who = '<span>Abraham</span>'; 13 + $translation = $translator->translate( 14 + $string, 15 + $who, 16 + $when); 17 + $this->assertEqual( 18 + 'string', 19 + gettype($translation)); 20 + $this->assertEqual( 21 + '<span>Abraham</span> awoke <strong>suddenly</strong> at <4 AM>.', 22 + $translation); 23 + 24 + // When at least one component is HTML, everything is treated as HTML. 25 + $who = phutil_tag('span', array(), 'Abraham'); 26 + $translation = $translator->translate( 27 + $string, 28 + $who, 29 + $when); 30 + $this->assertTrue($translation instanceof PhutilSafeHTML); 31 + $this->assertEqual( 32 + '<span>Abraham</span> awoke <strong>suddenly</strong> at &lt;4 AM&gt;.', 33 + $translation->getHTMLContent()); 34 + 35 + $translation = $translator->translate( 36 + $string, 37 + $who, 38 + new PhutilNumber(1383930802)); 39 + $this->assertEqual( 40 + '<span>Abraham</span> awoke <strong>suddenly</strong> at 1,383,930,802.', 41 + $translation->getHTMLContent()); 42 + 43 + // In this translation, we have no alternatives for the first conversion. 44 + $translator->setTranslations( 45 + array( 46 + 'Run the command %s %d time(s).' => array( 47 + array( 48 + 'Run the command %s once.', 49 + 'Run the command %s %d times.', 50 + ), 51 + ), 52 + )); 53 + 54 + $this->assertEqual( 55 + 'Run the command <tt>ls</tt> 123 times.', 56 + (string)$translator->translate( 57 + 'Run the command %s %d time(s).', 58 + hsprintf('<tt>%s</tt>', 'ls'), 59 + 123)); 60 + } 61 + 62 + private function newTranslator($locale_code) { 63 + $locale = PhutilLocale::loadLocale($locale_code); 64 + return id(new PhutilTranslator()) 65 + ->setLocale($locale); 66 + } 67 + 68 + }
+183
src/infrastructure/markup/render.php
··· 1 + <?php 2 + 3 + /** 4 + * Render an HTML tag in a way that treats user content as unsafe by default. 5 + * 6 + * Tag rendering has some special logic which implements security features: 7 + * 8 + * - When rendering `<a>` tags, if the `rel` attribute is not specified, it 9 + * is interpreted as `rel="noreferrer"`. 10 + * - When rendering `<a>` tags, the `href` attribute may not begin with 11 + * `javascript:`. 12 + * 13 + * These special cases can not be disabled. 14 + * 15 + * IMPORTANT: The `$tag` attribute and the keys of the `$attributes` array are 16 + * trusted blindly, and not escaped. You should not pass user data in these 17 + * parameters. 18 + * 19 + * @param string The name of the tag, like `a` or `div`. 20 + * @param map<string, string> A map of tag attributes. 21 + * @param wild Content to put in the tag. 22 + * @return PhutilSafeHTML Tag object. 23 + */ 24 + function phutil_tag($tag, array $attributes = array(), $content = null) { 25 + // If the `href` attribute is present, make sure it is not a "javascript:" 26 + // URI. We never permit these. 27 + if (!empty($attributes['href'])) { 28 + // This might be a URI object, so cast it to a string. 29 + $href = (string)$attributes['href']; 30 + 31 + if (isset($href[0])) { 32 + // Block 'javascript:' hrefs at the tag level: no well-designed 33 + // application should ever use them, and they are a potent attack vector. 34 + 35 + // This function is deep in the core and performance sensitive, so we're 36 + // doing a cheap version of this test first to avoid calling preg_match() 37 + // on URIs which begin with '/' or `#`. These cover essentially all URIs 38 + // in Phabricator. 39 + if (($href[0] !== '/') && ($href[0] !== '#')) { 40 + // Chrome 33 and IE 11 both interpret "javascript\n:" as a Javascript 41 + // URI, and all browsers interpret " javascript:" as a Javascript URI, 42 + // so be aggressive about looking for "javascript:" in the initial 43 + // section of the string. 44 + 45 + $normalized_href = preg_replace('([^a-z0-9/:]+)i', '', $href); 46 + if (preg_match('/^javascript:/i', $normalized_href)) { 47 + throw new Exception( 48 + pht( 49 + "Attempting to render a tag with an '%s' attribute that begins ". 50 + "with '%s'. This is either a serious security concern or a ". 51 + "serious architecture concern. Seek urgent remedy.", 52 + 'href', 53 + 'javascript:')); 54 + } 55 + } 56 + } 57 + } 58 + 59 + // For tags which can't self-close, treat null as the empty string -- for 60 + // example, always render `<div></div>`, never `<div />`. 61 + static $self_closing_tags = array( 62 + 'area' => true, 63 + 'base' => true, 64 + 'br' => true, 65 + 'col' => true, 66 + 'command' => true, 67 + 'embed' => true, 68 + 'frame' => true, 69 + 'hr' => true, 70 + 'img' => true, 71 + 'input' => true, 72 + 'keygen' => true, 73 + 'link' => true, 74 + 'meta' => true, 75 + 'param' => true, 76 + 'source' => true, 77 + 'track' => true, 78 + 'wbr' => true, 79 + ); 80 + 81 + $attr_string = ''; 82 + foreach ($attributes as $k => $v) { 83 + if ($v === null) { 84 + continue; 85 + } 86 + $v = phutil_escape_html($v); 87 + $attr_string .= ' '.$k.'="'.$v.'"'; 88 + } 89 + 90 + if ($content === null) { 91 + if (isset($self_closing_tags[$tag])) { 92 + return new PhutilSafeHTML('<'.$tag.$attr_string.' />'); 93 + } else { 94 + $content = ''; 95 + } 96 + } else { 97 + $content = phutil_escape_html($content); 98 + } 99 + 100 + return new PhutilSafeHTML('<'.$tag.$attr_string.'>'.$content.'</'.$tag.'>'); 101 + } 102 + 103 + function phutil_tag_div($class, $content = null) { 104 + return phutil_tag('div', array('class' => $class), $content); 105 + } 106 + 107 + function phutil_escape_html($string) { 108 + if ($string instanceof PhutilSafeHTML) { 109 + return $string; 110 + } else if ($string instanceof PhutilSafeHTMLProducerInterface) { 111 + $result = $string->producePhutilSafeHTML(); 112 + if ($result instanceof PhutilSafeHTML) { 113 + return phutil_escape_html($result); 114 + } else if (is_array($result)) { 115 + return phutil_escape_html($result); 116 + } else if ($result instanceof PhutilSafeHTMLProducerInterface) { 117 + return phutil_escape_html($result); 118 + } else { 119 + try { 120 + assert_stringlike($result); 121 + return phutil_escape_html((string)$result); 122 + } catch (Exception $ex) { 123 + throw new Exception( 124 + pht( 125 + "Object (of class '%s') implements %s but did not return anything ". 126 + "renderable from %s.", 127 + get_class($string), 128 + 'PhutilSafeHTMLProducerInterface', 129 + 'producePhutilSafeHTML()')); 130 + } 131 + } 132 + } else if (is_array($string)) { 133 + $result = ''; 134 + foreach ($string as $item) { 135 + $result .= phutil_escape_html($item); 136 + } 137 + return $result; 138 + } 139 + 140 + return htmlspecialchars($string, ENT_QUOTES, 'UTF-8'); 141 + } 142 + 143 + function phutil_escape_html_newlines($string) { 144 + return PhutilSafeHTML::applyFunction('nl2br', $string); 145 + } 146 + 147 + /** 148 + * Mark string as safe for use in HTML. 149 + */ 150 + function phutil_safe_html($string) { 151 + if ($string == '') { 152 + return $string; 153 + } else if ($string instanceof PhutilSafeHTML) { 154 + return $string; 155 + } else { 156 + return new PhutilSafeHTML($string); 157 + } 158 + } 159 + 160 + /** 161 + * HTML safe version of `implode()`. 162 + */ 163 + function phutil_implode_html($glue, array $pieces) { 164 + $glue = phutil_escape_html($glue); 165 + 166 + foreach ($pieces as $k => $piece) { 167 + $pieces[$k] = phutil_escape_html($piece); 168 + } 169 + 170 + return phutil_safe_html(implode($glue, $pieces)); 171 + } 172 + 173 + /** 174 + * Format a HTML code. This function behaves like `sprintf()`, except that all 175 + * the normal conversions (like %s) will be properly escaped. 176 + */ 177 + function hsprintf($html /* , ... */) { 178 + $args = func_get_args(); 179 + array_shift($args); 180 + return new PhutilSafeHTML( 181 + vsprintf($html, array_map('phutil_escape_html', $args))); 182 + } 183 +
+115
src/infrastructure/markup/syntax/engine/PhutilDefaultSyntaxHighlighterEngine.php
··· 1 + <?php 2 + 3 + final class PhutilDefaultSyntaxHighlighterEngine 4 + extends PhutilSyntaxHighlighterEngine { 5 + 6 + private $config = array(); 7 + 8 + public function setConfig($key, $value) { 9 + $this->config[$key] = $value; 10 + return $this; 11 + } 12 + 13 + public function getLanguageFromFilename($filename) { 14 + static $default_map = array( 15 + // All files which have file extensions that we haven't already matched 16 + // map to their extensions. 17 + '@\\.([^./]+)$@' => 1, 18 + ); 19 + 20 + $maps = array(); 21 + if (!empty($this->config['filename.map'])) { 22 + $maps[] = $this->config['filename.map']; 23 + } 24 + $maps[] = $default_map; 25 + 26 + foreach ($maps as $map) { 27 + foreach ($map as $regexp => $lang) { 28 + $matches = null; 29 + if (preg_match($regexp, $filename, $matches)) { 30 + if (is_numeric($lang)) { 31 + return idx($matches, $lang); 32 + } else { 33 + return $lang; 34 + } 35 + } 36 + } 37 + } 38 + 39 + return null; 40 + } 41 + 42 + public function getHighlightFuture($language, $source) { 43 + if ($language === null) { 44 + $language = PhutilLanguageGuesser::guessLanguage($source); 45 + } 46 + 47 + $have_pygments = !empty($this->config['pygments.enabled']); 48 + 49 + if ($language == 'php' && PhutilXHPASTBinary::isAvailable()) { 50 + return id(new PhutilXHPASTSyntaxHighlighter()) 51 + ->getHighlightFuture($source); 52 + } 53 + 54 + if ($language == 'console') { 55 + return id(new PhutilConsoleSyntaxHighlighter()) 56 + ->getHighlightFuture($source); 57 + } 58 + 59 + if ($language == 'diviner' || $language == 'remarkup') { 60 + return id(new PhutilDivinerSyntaxHighlighter()) 61 + ->getHighlightFuture($source); 62 + } 63 + 64 + if ($language == 'rainbow') { 65 + return id(new PhutilRainbowSyntaxHighlighter()) 66 + ->getHighlightFuture($source); 67 + } 68 + 69 + if ($language == 'php') { 70 + return id(new PhutilLexerSyntaxHighlighter()) 71 + ->setConfig('lexer', new PhutilPHPFragmentLexer()) 72 + ->setConfig('language', 'php') 73 + ->getHighlightFuture($source); 74 + } 75 + 76 + if ($language == 'py' || $language == 'python') { 77 + return id(new PhutilLexerSyntaxHighlighter()) 78 + ->setConfig('lexer', new PhutilPythonFragmentLexer()) 79 + ->setConfig('language', 'py') 80 + ->getHighlightFuture($source); 81 + } 82 + 83 + if ($language == 'java') { 84 + return id(new PhutilLexerSyntaxHighlighter()) 85 + ->setConfig('lexer', new PhutilJavaFragmentLexer()) 86 + ->setConfig('language', 'java') 87 + ->getHighlightFuture($source); 88 + } 89 + 90 + if ($language == 'json') { 91 + return id(new PhutilLexerSyntaxHighlighter()) 92 + ->setConfig('lexer', new PhutilJSONFragmentLexer()) 93 + ->getHighlightFuture($source); 94 + } 95 + 96 + if ($language == 'invisible') { 97 + return id(new PhutilInvisibleSyntaxHighlighter()) 98 + ->getHighlightFuture($source); 99 + } 100 + 101 + // Don't invoke Pygments for plain text, since it's expensive and has 102 + // no effect. 103 + if ($language !== 'text' && $language !== 'txt') { 104 + if ($have_pygments) { 105 + return id(new PhutilPygmentsSyntaxHighlighter()) 106 + ->setConfig('language', $language) 107 + ->getHighlightFuture($source); 108 + } 109 + } 110 + 111 + return id(new PhutilDefaultSyntaxHighlighter()) 112 + ->getHighlightFuture($source); 113 + } 114 + 115 + }
+19
src/infrastructure/markup/syntax/engine/PhutilSyntaxHighlighterEngine.php
··· 1 + <?php 2 + 3 + abstract class PhutilSyntaxHighlighterEngine extends Phobject { 4 + 5 + abstract public function setConfig($key, $value); 6 + abstract public function getHighlightFuture($language, $source); 7 + abstract public function getLanguageFromFilename($filename); 8 + 9 + final public function highlightSource($language, $source) { 10 + try { 11 + return $this->getHighlightFuture($language, $source)->resolve(); 12 + } catch (PhutilSyntaxHighlighterException $ex) { 13 + return id(new PhutilDefaultSyntaxHighlighter()) 14 + ->getHighlightFuture($source) 15 + ->resolve(); 16 + } 17 + } 18 + 19 + }
+28
src/infrastructure/markup/syntax/engine/__tests__/PhutilDefaultSyntaxHighlighterEngineTestCase.php
··· 1 + <?php 2 + 3 + /** 4 + * Test cases for @{class:PhutilDefaultSyntaxHighlighterEngine}. 5 + */ 6 + final class PhutilDefaultSyntaxHighlighterEngineTestCase 7 + extends PhutilTestCase { 8 + 9 + public function testFilenameGreediness() { 10 + $names = array( 11 + 'x.php' => 'php', 12 + '/x.php' => 'php', 13 + 'x.y.php' => 'php', 14 + '/x.y/z.php' => 'php', 15 + '/x.php/' => null, 16 + ); 17 + 18 + $engine = new PhutilDefaultSyntaxHighlighterEngine(); 19 + foreach ($names as $path => $language) { 20 + $detect = $engine->getLanguageFromFilename($path); 21 + $this->assertEqual( 22 + $language, 23 + $detect, 24 + pht('Language detect for %s', $path)); 25 + } 26 + } 27 + 28 + }
+51
src/infrastructure/markup/syntax/highlighter/PhutilConsoleSyntaxHighlighter.php
··· 1 + <?php 2 + 3 + /** 4 + * Simple syntax highlighter for console output. We just try to highlight the 5 + * commands so it's easier to follow transcripts. 6 + */ 7 + final class PhutilConsoleSyntaxHighlighter extends Phobject { 8 + 9 + private $config = array(); 10 + 11 + public function setConfig($key, $value) { 12 + $this->config[$key] = $value; 13 + return $this; 14 + } 15 + 16 + public function getHighlightFuture($source) { 17 + $in_command = false; 18 + $lines = explode("\n", $source); 19 + foreach ($lines as $key => $line) { 20 + $matches = null; 21 + 22 + // Parse commands like this: 23 + // 24 + // some/path/ $ ./bin/example # Do things 25 + // 26 + // ...into path, command, and comment components. 27 + 28 + $pattern = 29 + '@'. 30 + ($in_command ? '()(.*?)' : '^(\S+[\\\\/] )?([$] .*?)'). 31 + '(#.*|\\\\)?$@'; 32 + 33 + if (preg_match($pattern, $line, $matches)) { 34 + $lines[$key] = hsprintf( 35 + '%s<span class="gp">%s</span>%s', 36 + $matches[1], 37 + $matches[2], 38 + (!empty($matches[3]) 39 + ? hsprintf('<span class="k">%s</span>', $matches[3]) 40 + : '')); 41 + $in_command = (idx($matches, 3) == '\\'); 42 + } else { 43 + $lines[$key] = hsprintf('<span class="go">%s</span>', $line); 44 + } 45 + } 46 + $lines = phutil_implode_html("\n", $lines); 47 + 48 + return new ImmediateFuture($lines); 49 + } 50 + 51 + }
+14
src/infrastructure/markup/syntax/highlighter/PhutilDefaultSyntaxHighlighter.php
··· 1 + <?php 2 + 3 + final class PhutilDefaultSyntaxHighlighter extends Phobject { 4 + 5 + public function setConfig($key, $value) { 6 + return $this; 7 + } 8 + 9 + public function getHighlightFuture($source) { 10 + $result = hsprintf('%s', $source); 11 + return new ImmediateFuture($result); 12 + } 13 + 14 + }
+81
src/infrastructure/markup/syntax/highlighter/PhutilDivinerSyntaxHighlighter.php
··· 1 + <?php 2 + 3 + /** 4 + * Simple syntax highlighter for the ".diviner" format, which is just Remarkup 5 + * with a specific ruleset. This should also work alright for Remarkup. 6 + */ 7 + final class PhutilDivinerSyntaxHighlighter extends Phobject { 8 + 9 + private $config = array(); 10 + private $replaceClass; 11 + 12 + public function setConfig($key, $value) { 13 + $this->config[$key] = $value; 14 + return $this; 15 + } 16 + 17 + public function getHighlightFuture($source) { 18 + $source = phutil_escape_html($source); 19 + 20 + // This highlighter isn't perfect but tries to do an okay job at getting 21 + // some of the basics at least. There's lots of room for improvement. 22 + 23 + $blocks = explode("\n\n", $source); 24 + foreach ($blocks as $key => $block) { 25 + if (preg_match('/^[^ ](?! )/m', $block)) { 26 + $blocks[$key] = $this->highlightBlock($block); 27 + } 28 + } 29 + $source = implode("\n\n", $blocks); 30 + 31 + $source = phutil_safe_html($source); 32 + return new ImmediateFuture($source); 33 + } 34 + 35 + private function highlightBlock($source) { 36 + // Highlight "@{class:...}" links to other documentation pages. 37 + $source = $this->highlightPattern('/@{([\w@]+?):([^}]+?)}/', $source, 'nc'); 38 + 39 + // Highlight "@title", "@group", etc. 40 + $source = $this->highlightPattern('/^@(\w+)/m', $source, 'k'); 41 + 42 + // Highlight bold, italic and monospace. 43 + $source = $this->highlightPattern('@\\*\\*(.+?)\\*\\*@s', $source, 's'); 44 + $source = $this->highlightPattern('@(?<!:)//(.+?)//@s', $source, 's'); 45 + $source = $this->highlightPattern( 46 + '@##([\s\S]+?)##|\B`(.+?)`\B@', 47 + $source, 48 + 's'); 49 + 50 + // Highlight stuff that looks like headers. 51 + $source = $this->highlightPattern('/^=(.*)$/m', $source, 'nv'); 52 + 53 + return $source; 54 + } 55 + 56 + private function highlightPattern($regexp, $source, $class) { 57 + $this->replaceClass = $class; 58 + $source = preg_replace_callback( 59 + $regexp, 60 + array($this, 'replacePattern'), 61 + $source); 62 + 63 + return $source; 64 + } 65 + 66 + public function replacePattern($matches) { 67 + 68 + // NOTE: The goal here is to make sure a <span> never crosses a newline. 69 + 70 + $content = $matches[0]; 71 + $content = explode("\n", $content); 72 + foreach ($content as $key => $line) { 73 + $content[$key] = 74 + '<span class="'.$this->replaceClass.'">'. 75 + $line. 76 + '</span>'; 77 + } 78 + return implode("\n", $content); 79 + } 80 + 81 + }
+43
src/infrastructure/markup/syntax/highlighter/PhutilInvisibleSyntaxHighlighter.php
··· 1 + <?php 2 + 3 + final class PhutilInvisibleSyntaxHighlighter extends Phobject { 4 + 5 + private $config = array(); 6 + 7 + public function setConfig($key, $value) { 8 + $this->config[$key] = $value; 9 + return $this; 10 + } 11 + 12 + public function getHighlightFuture($source) { 13 + $keys = array_map('chr', range(0x0, 0x1F)); 14 + $vals = array_map( 15 + array($this, 'decimalToHtmlEntityDecoded'), range(0x2400, 0x241F)); 16 + 17 + $invisible = array_combine($keys, $vals); 18 + 19 + $result = array(); 20 + foreach (str_split($source) as $character) { 21 + if (isset($invisible[$character])) { 22 + $result[] = phutil_tag( 23 + 'span', 24 + array('class' => 'invisible'), 25 + $invisible[$character]); 26 + 27 + if ($character === "\n") { 28 + $result[] = $character; 29 + } 30 + } else { 31 + $result[] = $character; 32 + } 33 + } 34 + 35 + $result = phutil_implode_html('', $result); 36 + return new ImmediateFuture($result); 37 + } 38 + 39 + private function decimalToHtmlEntityDecoded($dec) { 40 + return html_entity_decode("&#{$dec};"); 41 + } 42 + 43 + }
+72
src/infrastructure/markup/syntax/highlighter/PhutilLexerSyntaxHighlighter.php
··· 1 + <?php 2 + 3 + final class PhutilLexerSyntaxHighlighter extends PhutilSyntaxHighlighter { 4 + 5 + private $config = array(); 6 + 7 + public function setConfig($key, $value) { 8 + $this->config[$key] = $value; 9 + return $this; 10 + } 11 + 12 + public function getHighlightFuture($source) { 13 + $strip = false; 14 + $state = 'start'; 15 + $lang = idx($this->config, 'language'); 16 + 17 + if ($lang == 'php') { 18 + if (strpos($source, '<?') === false) { 19 + $state = 'php'; 20 + } 21 + } 22 + 23 + $lexer = idx($this->config, 'lexer'); 24 + $tokens = $lexer->getTokens($source, $state); 25 + $tokens = $lexer->mergeTokens($tokens); 26 + 27 + $result = array(); 28 + foreach ($tokens as $token) { 29 + list($type, $value, $context) = $token; 30 + 31 + $data_name = null; 32 + switch ($type) { 33 + case 'nc': 34 + case 'nf': 35 + case 'na': 36 + $data_name = $value; 37 + break; 38 + } 39 + 40 + if (strpos($value, "\n") !== false) { 41 + $value = explode("\n", $value); 42 + } else { 43 + $value = array($value); 44 + } 45 + foreach ($value as $part) { 46 + if (strlen($part)) { 47 + if ($type) { 48 + $result[] = phutil_tag( 49 + 'span', 50 + array( 51 + 'class' => $type, 52 + 'data-symbol-context' => $context, 53 + 'data-symbol-name' => $data_name, 54 + ), 55 + $part); 56 + } else { 57 + $result[] = $part; 58 + } 59 + } 60 + $result[] = "\n"; 61 + } 62 + 63 + // Throw away the last "\n". 64 + array_pop($result); 65 + } 66 + 67 + $result = phutil_implode_html('', $result); 68 + 69 + return new ImmediateFuture($result); 70 + } 71 + 72 + }
+229
src/infrastructure/markup/syntax/highlighter/PhutilPygmentsSyntaxHighlighter.php
··· 1 + <?php 2 + 3 + final class PhutilPygmentsSyntaxHighlighter extends Phobject { 4 + 5 + private $config = array(); 6 + 7 + public function setConfig($key, $value) { 8 + $this->config[$key] = $value; 9 + return $this; 10 + } 11 + 12 + public function getHighlightFuture($source) { 13 + $language = idx($this->config, 'language'); 14 + 15 + if (preg_match('/\r(?!\n)/', $source)) { 16 + // TODO: Pygments converts "\r" newlines into "\n" newlines, so we can't 17 + // use it on files with "\r" newlines. If we have "\r" not followed by 18 + // "\n" in the file, skip highlighting. 19 + $language = null; 20 + } 21 + 22 + if ($language) { 23 + $language = $this->getPygmentsLexerNameFromLanguageName($language); 24 + 25 + // See T13224. Under Ubuntu, avoid leaving an intermedite "dash" shell 26 + // process so we hit "pygmentize" directly if we have to SIGKILL this 27 + // because it explodes. 28 + 29 + $future = new ExecFuture( 30 + 'exec pygmentize -O encoding=utf-8 -O stripnl=False -f html -l %s', 31 + $language); 32 + 33 + $scrub = false; 34 + if ($language == 'php' && strpos($source, '<?') === false) { 35 + $source = "<?php\n".$source; 36 + $scrub = true; 37 + } 38 + 39 + // See T13224. In some cases, "pygmentize" has explosive runtime on small 40 + // inputs. Put a hard cap on how long it is allowed to run for to limit 41 + // the amount of damage it can do. 42 + $future->setTimeout(15); 43 + 44 + $future->write($source); 45 + 46 + return new PhutilDefaultSyntaxHighlighterEnginePygmentsFuture( 47 + $future, 48 + $source, 49 + $scrub); 50 + } 51 + 52 + return id(new PhutilDefaultSyntaxHighlighter()) 53 + ->getHighlightFuture($source); 54 + } 55 + 56 + private function getPygmentsLexerNameFromLanguageName($language) { 57 + static $map = array( 58 + 'adb' => 'ada', 59 + 'ads' => 'ada', 60 + 'ahkl' => 'ahk', 61 + 'as' => 'as3', 62 + 'asax' => 'aspx-vb', 63 + 'ascx' => 'aspx-vb', 64 + 'ashx' => 'aspx-vb', 65 + 'ASM' => 'nasm', 66 + 'asm' => 'nasm', 67 + 'asmx' => 'aspx-vb', 68 + 'aspx' => 'aspx-vb', 69 + 'autodelegate' => 'myghty', 70 + 'autohandler' => 'mason', 71 + 'aux' => 'tex', 72 + 'axd' => 'aspx-vb', 73 + 'b' => 'brainfuck', 74 + 'bas' => 'vb.net', 75 + 'bf' => 'brainfuck', 76 + 'bmx' => 'blitzmax', 77 + 'c++' => 'cpp', 78 + 'c++-objdump' => 'cpp-objdump', 79 + 'cc' => 'cpp', 80 + 'cfc' => 'cfm', 81 + 'cfg' => 'ini', 82 + 'cfml' => 'cfm', 83 + 'cl' => 'common-lisp', 84 + 'clj' => 'clojure', 85 + 'cmd' => 'bat', 86 + 'coffee' => 'coffee-script', 87 + 'cs' => 'csharp', 88 + 'csh' => 'tcsh', 89 + 'cw' => 'redcode', 90 + 'cxx' => 'cpp', 91 + 'cxx-objdump' => 'cpp-objdump', 92 + 'darcspatch' => 'dpatch', 93 + 'def' => 'modula2', 94 + 'dhandler' => 'mason', 95 + 'di' => 'd', 96 + 'duby' => 'rb', 97 + 'dyl' => 'dylan', 98 + 'ebuild' => 'bash', 99 + 'eclass' => 'bash', 100 + 'el' => 'common-lisp', 101 + 'eps' => 'postscript', 102 + 'erl' => 'erlang', 103 + 'erl-sh' => 'erl', 104 + 'f' => 'fortran', 105 + 'f90' => 'fortran', 106 + 'feature' => 'Cucumber', 107 + 'fhtml' => 'velocity', 108 + 'flx' => 'felix', 109 + 'flxh' => 'felix', 110 + 'frag' => 'glsl', 111 + 'g' => 'antlr-ruby', 112 + 'G' => 'antlr-ruby', 113 + 'gdc' => 'gooddata-cl', 114 + 'gemspec' => 'rb', 115 + 'geo' => 'glsl', 116 + 'GNUmakefile' => 'make', 117 + 'h' => 'c', 118 + 'h++' => 'cpp', 119 + 'hh' => 'cpp', 120 + 'hpp' => 'cpp', 121 + 'hql' => 'sql', 122 + 'hrl' => 'erlang', 123 + 'hs' => 'haskell', 124 + 'htaccess' => 'apacheconf', 125 + 'htm' => 'html', 126 + 'html' => 'html+evoque', 127 + 'hxx' => 'cpp', 128 + 'hy' => 'hybris', 129 + 'hyb' => 'hybris', 130 + 'ik' => 'ioke', 131 + 'inc' => 'pov', 132 + 'j' => 'objective-j', 133 + 'jbst' => 'duel', 134 + 'kid' => 'genshi', 135 + 'ksh' => 'bash', 136 + 'less' => 'css', 137 + 'lgt' => 'logtalk', 138 + 'lisp' => 'common-lisp', 139 + 'll' => 'llvm', 140 + 'm' => 'objective-c', 141 + 'mak' => 'make', 142 + 'Makefile' => 'make', 143 + 'makefile' => 'make', 144 + 'man' => 'groff', 145 + 'mao' => 'mako', 146 + 'mc' => 'mason', 147 + 'md' => 'minid', 148 + 'mhtml' => 'mason', 149 + 'mi' => 'mason', 150 + 'ml' => 'ocaml', 151 + 'mli' => 'ocaml', 152 + 'mll' => 'ocaml', 153 + 'mly' => 'ocaml', 154 + 'mm' => 'objective-c', 155 + 'mo' => 'modelica', 156 + 'mod' => 'modula2', 157 + 'moo' => 'moocode', 158 + 'mu' => 'mupad', 159 + 'myt' => 'myghty', 160 + 'ns2' => 'newspeak', 161 + 'pas' => 'delphi', 162 + 'patch' => 'diff', 163 + 'phtml' => 'html+php', 164 + 'pl' => 'prolog', 165 + 'plot' => 'gnuplot', 166 + 'plt' => 'gnuplot', 167 + 'pm' => 'perl', 168 + 'po' => 'pot', 169 + 'pp' => 'puppet', 170 + 'pro' => 'prolog', 171 + 'proto' => 'protobuf', 172 + 'ps' => 'postscript', 173 + 'pxd' => 'cython', 174 + 'pxi' => 'cython', 175 + 'py' => 'python', 176 + 'pyw' => 'python', 177 + 'pyx' => 'cython', 178 + 'R' => 'splus', 179 + 'r' => 'rebol', 180 + 'r3' => 'rebol', 181 + 'rake' => 'rb', 182 + 'Rakefile' => 'rb', 183 + 'rbw' => 'rb', 184 + 'rbx' => 'rb', 185 + 'rest' => 'rst', 186 + 'rl' => 'ragel-em', 187 + 'robot' => 'robotframework', 188 + 'Rout' => 'rconsole', 189 + 'rss' => 'xml', 190 + 's' => 'gas', 191 + 'S' => 'splus', 192 + 'sc' => 'python', 193 + 'scm' => 'scheme', 194 + 'SConscript' => 'python', 195 + 'SConstruct' => 'python', 196 + 'scss' => 'css', 197 + 'sh' => 'bash', 198 + 'sh-session' => 'console', 199 + 'spt' => 'cheetah', 200 + 'sqlite3-console' => 'sqlite3', 201 + 'st' => 'smalltalk', 202 + 'sv' => 'v', 203 + 'tac' => 'python', 204 + 'tmpl' => 'cheetah', 205 + 'toc' => 'tex', 206 + 'tpl' => 'smarty', 207 + 'txt' => 'text', 208 + 'vapi' => 'vala', 209 + 'vb' => 'vb.net', 210 + 'vert' => 'glsl', 211 + 'vhd' => 'vhdl', 212 + 'vimrc' => 'vim', 213 + 'vm' => 'velocity', 214 + 'weechatlog' => 'irc', 215 + 'wlua' => 'lua', 216 + 'wsdl' => 'xml', 217 + 'xhtml' => 'html', 218 + 'xml' => 'xml+evoque', 219 + 'xqy' => 'xquery', 220 + 'xsd' => 'xml', 221 + 'xsl' => 'xslt', 222 + 'xslt' => 'xml', 223 + 'yml' => 'yaml', 224 + ); 225 + 226 + return idx($map, $language, $language); 227 + } 228 + 229 + }
+46
src/infrastructure/markup/syntax/highlighter/PhutilRainbowSyntaxHighlighter.php
··· 1 + <?php 2 + 3 + /** 4 + * Highlights source code with a rainbow of colors, regardless of the language. 5 + * This highlighter is useless, absurd, and extremely slow. 6 + */ 7 + final class PhutilRainbowSyntaxHighlighter extends Phobject { 8 + 9 + private $config = array(); 10 + 11 + public function setConfig($key, $value) { 12 + $this->config[$key] = $value; 13 + return $this; 14 + } 15 + 16 + public function getHighlightFuture($source) { 17 + 18 + $color = 0; 19 + $colors = array( 20 + 'rbw_r', 21 + 'rbw_o', 22 + 'rbw_y', 23 + 'rbw_g', 24 + 'rbw_b', 25 + 'rbw_i', 26 + 'rbw_v', 27 + ); 28 + 29 + $result = array(); 30 + foreach (phutil_utf8v($source) as $character) { 31 + if ($character == ' ' || $character == "\n") { 32 + $result[] = $character; 33 + continue; 34 + } 35 + $result[] = phutil_tag( 36 + 'span', 37 + array('class' => $colors[$color]), 38 + $character); 39 + $color = ($color + 1) % count($colors); 40 + } 41 + 42 + $result = phutil_implode_html('', $result); 43 + return new ImmediateFuture($result); 44 + } 45 + 46 + }
+6
src/infrastructure/markup/syntax/highlighter/PhutilSyntaxHighlighter.php
··· 1 + <?php 2 + 3 + abstract class PhutilSyntaxHighlighter extends Phobject { 4 + abstract public function setConfig($key, $value); 5 + abstract public function getHighlightFuture($source); 6 + }
+3
src/infrastructure/markup/syntax/highlighter/PhutilSyntaxHighlighterException.php
··· 1 + <?php 2 + 3 + final class PhutilSyntaxHighlighterException extends Exception {}
+18
src/infrastructure/markup/syntax/highlighter/PhutilXHPASTSyntaxHighlighter.php
··· 1 + <?php 2 + 3 + final class PhutilXHPASTSyntaxHighlighter extends Phobject { 4 + 5 + public function getHighlightFuture($source) { 6 + $scrub = false; 7 + if (strpos($source, '<?') === false) { 8 + $source = "<?php\n".$source; 9 + $scrub = true; 10 + } 11 + 12 + return new PhutilXHPASTSyntaxHighlighterFuture( 13 + PhutilXHPASTBinary::getParserFuture($source), 14 + $source, 15 + $scrub); 16 + } 17 + 18 + }
+24
src/infrastructure/markup/syntax/highlighter/__tests__/PhutilJSONFragmentLexerHighlighterTestCase.php
··· 1 + <?php 2 + 3 + final class PhutilJSONFragmentLexerHighlighterTestCase extends PhutilTestCase { 4 + 5 + public function testLexer() { 6 + $highlighter = id(new PhutilLexerSyntaxHighlighter()) 7 + ->setConfig('language', 'json') 8 + ->setConfig('lexer', new PhutilJSONFragmentLexer()); 9 + 10 + $path = dirname(__FILE__).'/data/jsonfragment/'; 11 + foreach (Filesystem::listDirectory($path, $include_hidden = false) as $f) { 12 + if (preg_match('/.test$/', $f)) { 13 + $expect = preg_replace('/.test$/', '.expect', $f); 14 + $source = Filesystem::readFile($path.'/'.$f); 15 + 16 + $this->assertEqual( 17 + Filesystem::readFile($path.'/'.$expect), 18 + (string)$highlighter->getHighlightFuture($source)->resolve(), 19 + $f); 20 + } 21 + } 22 + } 23 + 24 + }
+25
src/infrastructure/markup/syntax/highlighter/__tests__/PhutilPHPFragmentLexerHighlighterTestCase.php
··· 1 + <?php 2 + 3 + final class PhutilPHPFragmentLexerHighlighterTestCase extends PhutilTestCase { 4 + 5 + public function testLexer() { 6 + $highlighter = new PhutilLexerSyntaxHighlighter(); 7 + $highlighter->setConfig('language', 'php'); 8 + $highlighter->setConfig('lexer', new PhutilPHPFragmentLexer()); 9 + 10 + 11 + $path = dirname(__FILE__).'/phpfragment/'; 12 + foreach (Filesystem::listDirectory($path, $include_hidden = false) as $f) { 13 + if (preg_match('/.test$/', $f)) { 14 + $expect = preg_replace('/.test$/', '.expect', $f); 15 + $source = Filesystem::readFile($path.'/'.$f); 16 + 17 + $this->assertEqual( 18 + Filesystem::readFile($path.'/'.$expect), 19 + (string)$highlighter->getHighlightFuture($source)->resolve(), 20 + $f); 21 + } 22 + } 23 + } 24 + 25 + }
+39
src/infrastructure/markup/syntax/highlighter/__tests__/PhutilXHPASTSyntaxHighlighterTestCase.php
··· 1 + <?php 2 + 3 + final class PhutilXHPASTSyntaxHighlighterTestCase extends PhutilTestCase { 4 + 5 + private function highlight($source) { 6 + $highlighter = new PhutilXHPASTSyntaxHighlighter(); 7 + $future = $highlighter->getHighlightFuture($source); 8 + return $future->resolve(); 9 + } 10 + 11 + private function read($file) { 12 + $path = dirname(__FILE__).'/xhpast/'.$file; 13 + return Filesystem::readFile($path); 14 + } 15 + 16 + public function testBuiltinClassnames() { 17 + $this->assertEqual( 18 + $this->read('builtin-classname.expect'), 19 + (string)$this->highlight($this->read('builtin-classname.source')), 20 + pht('Builtin classnames should not be marked as linkable symbols.')); 21 + $this->assertEqual( 22 + rtrim($this->read('trailing-comment.expect')), 23 + (string)$this->highlight($this->read('trailing-comment.source')), 24 + pht('Trailing comments should not be dropped.')); 25 + $this->assertEqual( 26 + $this->read('multiline-token.expect'), 27 + (string)$this->highlight($this->read('multiline-token.source')), 28 + pht('Multi-line tokens should be split across lines.')); 29 + $this->assertEqual( 30 + $this->read('leading-whitespace.expect'), 31 + (string)$this->highlight($this->read('leading-whitespace.source')), 32 + pht('Snippets with leading whitespace should be preserved.')); 33 + $this->assertEqual( 34 + $this->read('no-leading-whitespace.expect'), 35 + (string)$this->highlight($this->read('no-leading-whitespace.source')), 36 + pht('Snippets with no leading whitespace should be preserved.')); 37 + } 38 + 39 + }
+12
src/infrastructure/markup/syntax/highlighter/__tests__/data/jsonfragment/basics.expect
··· 1 + <span class="o">{</span> 2 + <span class="s">&quot;key&quot;</span><span class="o">:</span> <span class="mf">3.5</span><span class="o">,</span> 3 + <span class="s">&quot;true&quot;</span><span class="o">:</span> <span class="k">true</span><span class="o">,</span> 4 + <span class="s">&quot;false&quot;</span><span class="o">:</span> <span class="k">false</span><span class="o">,</span> 5 + <span class="s">&quot;null&quot;</span><span class="o">:</span> <span class="k">null</span><span class="o">,</span> 6 + <span class="s">&quot;list&quot;</span><span class="o">:</span> <span class="o">[</span><span class="mf">1</span><span class="o">,</span> <span class="mf">2</span><span class="o">,</span> <span class="mf">3</span><span class="o">],</span> 7 + <span class="s">&quot;object&quot;</span><span class="o">:</span> <span class="o">{</span> 8 + <span class="s">&quot;k1&quot;</span><span class="o">:</span> <span class="s">&quot;v1&quot;</span> 9 + <span class="o">},</span> 10 + <span class="s">&quot;numbers&quot;</span><span class="o">:</span> <span class="o">[</span><span class="mf">0</span>e<span class="mf">1</span><span class="o">,</span> <span class="mf">1</span>e<span class="mf">-1</span><span class="o">,</span> <span class="mf">-1</span>e<span class="mf">-1</span><span class="o">,</span> <span class="mf">-1</span>e+<span class="mf">1</span><span class="o">],</span> 11 + <span class="s">&quot;</span><span class="k">\&quot;\u1234</span><span class="s">&#039;abc[]{}...&quot;</span> 12 + <span class="o">}</span>
+12
src/infrastructure/markup/syntax/highlighter/__tests__/data/jsonfragment/basics.test
··· 1 + { 2 + "key": 3.5, 3 + "true": true, 4 + "false": false, 5 + "null": null, 6 + "list": [1, 2, 3], 7 + "object": { 8 + "k1": "v1" 9 + }, 10 + "numbers": [0e1, 1e-1, -1e-1, -1e+1], 11 + "\"\u1234'abc[]{}..." 12 + }
+16
src/infrastructure/markup/syntax/highlighter/__tests__/phpfragment/abuse.expect
··· 1 + <span class="cp">&lt;?</span> 2 + 3 + <span class="c">// comment? comment! </span><span class="cp">?&gt;</span> 4 + 5 + data 6 + 7 + <span class="cp">&lt;?php</span> 8 + 9 + <span class="cp">__halt_compiler</span> <span class="cm">/* ! */</span> <span class="o">(</span> <span class="c">// )</span> 10 + <span class="o">)</span> <span class="cm">/* ;;;; */</span> 11 + 12 + <span class="o">;</span> 13 + 14 + data data 15 + &lt;?php 16 + data
+16
src/infrastructure/markup/syntax/highlighter/__tests__/phpfragment/abuse.test
··· 1 + <? 2 + 3 + // comment? comment! ?> 4 + 5 + data 6 + 7 + <?php 8 + 9 + __halt_compiler /* ! */ ( // ) 10 + ) /* ;;;; */ 11 + 12 + ; 13 + 14 + data data 15 + <?php 16 + data
+5
src/infrastructure/markup/syntax/highlighter/__tests__/phpfragment/basics.expect
··· 1 + <span class="k">public</span> <span class="k">function</span> <span class="no">f</span><span class="o">()</span> <span class="o">{</span> 2 + <span class="nc" data-symbol-name="ExampleClass">ExampleClass</span><span class="o">::</span><span class="na" data-symbol-context="ExampleClass" data-symbol-name="EXAMPLE_CONSTANT">EXAMPLE_CONSTANT</span><span class="o">;</span> 3 + <span class="nc" data-symbol-name="ExampleClass">ExampleClass</span><span class="o">::</span><span class="nf" data-symbol-context="ExampleClass" data-symbol-name="exampleMethod">exampleMethod</span><span class="o">();</span> 4 + <span class="nf" data-symbol-name="example_function">example_function</span><span class="o">();</span> 5 + <span class="o">}</span>
+5
src/infrastructure/markup/syntax/highlighter/__tests__/phpfragment/basics.test
··· 1 + public function f() { 2 + ExampleClass::EXAMPLE_CONSTANT; 3 + ExampleClass::exampleMethod(); 4 + example_function(); 5 + }
+3
src/infrastructure/markup/syntax/highlighter/__tests__/phpfragment/leading-whitespace.expect
··· 1 + <span class="k">foreach</span> <span class="o">(</span><span class="nv">$x</span> <span class="k">as</span> <span class="nv">$y</span><span class="o">)</span> <span class="o">{</span> 2 + <span class="nf" data-symbol-name="z">z</span><span class="o">();</span> 3 + <span class="o">}</span>
+3
src/infrastructure/markup/syntax/highlighter/__tests__/phpfragment/leading-whitespace.test
··· 1 + foreach ($x as $y) { 2 + z(); 3 + }
+3
src/infrastructure/markup/syntax/highlighter/__tests__/phpfragment/no-leading-whitespace.expect
··· 1 + <span class="k">foreach</span> <span class="o">(</span><span class="nv">$x</span> <span class="k">as</span> <span class="nv">$y</span><span class="o">)</span> <span class="o">{</span> 2 + <span class="nf" data-symbol-name="z">z</span><span class="o">();</span> 3 + <span class="o">}</span>
+3
src/infrastructure/markup/syntax/highlighter/__tests__/phpfragment/no-leading-whitespace.test
··· 1 + foreach ($x as $y) { 2 + z(); 3 + }
+10
src/infrastructure/markup/syntax/highlighter/__tests__/xhpast/builtin-classname.expect
··· 1 + <span class="o">&lt;?php</span> 2 + 3 + <span class="k">class</span> <span data-symbol-name="C" class="nc">C</span> <span class="k">{</span> 4 + <span class="k">public</span> <span class="k">function</span> <span class="nx">f</span><span class="k">(</span><span class="k">)</span> <span class="k">{</span> 5 + <span data-symbol-name="D" class="nc">D</span><span class="k">::</span><span data-symbol-context="D" data-symbol-name="X" class="na">X</span><span class="k">;</span> 6 + <span class="nx">self</span><span class="k">::</span><span data-symbol-name="X" class="na">X</span><span class="k">;</span> 7 + <span class="nx">parent</span><span class="k">::</span><span data-symbol-name="X" class="na">X</span><span class="k">;</span> 8 + <span class="k">static</span><span class="k">::</span><span data-symbol-name="X" class="na">X</span><span class="k">;</span> 9 + <span class="k">}</span> 10 + <span class="k">}</span>
+10
src/infrastructure/markup/syntax/highlighter/__tests__/xhpast/builtin-classname.source
··· 1 + <?php 2 + 3 + class C { 4 + public function f() { 5 + D::X; 6 + self::X; 7 + parent::X; 8 + static::X; 9 + } 10 + }
+3
src/infrastructure/markup/syntax/highlighter/__tests__/xhpast/leading-whitespace.expect
··· 1 + <span class="k">foreach</span> <span class="k">(</span><span class="nv">$x</span> <span class="k">as</span> <span class="nv">$y</span><span class="k">)</span> <span class="k">{</span> 2 + <span data-symbol-name="z" class="nf">z</span><span class="k">(</span><span class="k">)</span><span class="k">;</span> 3 + <span class="k">}</span>
+3
src/infrastructure/markup/syntax/highlighter/__tests__/xhpast/leading-whitespace.source
··· 1 + foreach ($x as $y) { 2 + z(); 3 + }
+5
src/infrastructure/markup/syntax/highlighter/__tests__/xhpast/multiline-token.expect
··· 1 + <span class="o">&lt;?php</span> 2 + 3 + <span class="c">/* this comment 4 + </span><span class="c">extends across 5 + </span><span class="c">multiple lines */</span>
+5
src/infrastructure/markup/syntax/highlighter/__tests__/xhpast/multiline-token.source
··· 1 + <?php 2 + 3 + /* this comment 4 + extends across 5 + multiple lines */
+3
src/infrastructure/markup/syntax/highlighter/__tests__/xhpast/no-leading-whitespace.expect
··· 1 + <span class="k">foreach</span> <span class="k">(</span><span class="nv">$x</span> <span class="k">as</span> <span class="nv">$y</span><span class="k">)</span> <span class="k">{</span> 2 + <span data-symbol-name="z" class="nf">z</span><span class="k">(</span><span class="k">)</span><span class="k">;</span> 3 + <span class="k">}</span>
+3
src/infrastructure/markup/syntax/highlighter/__tests__/xhpast/no-leading-whitespace.source
··· 1 + foreach ($x as $y) { 2 + z(); 3 + }
+3
src/infrastructure/markup/syntax/highlighter/__tests__/xhpast/trailing-comment.expect
··· 1 + <span class="o">&lt;?php</span> 2 + <span class="c">// xyz 3 + </span>
+2
src/infrastructure/markup/syntax/highlighter/__tests__/xhpast/trailing-comment.source
··· 1 + <?php 2 + // xyz
+33
src/infrastructure/markup/syntax/highlighter/pygments/PhutilDefaultSyntaxHighlighterEnginePygmentsFuture.php
··· 1 + <?php 2 + 3 + final class PhutilDefaultSyntaxHighlighterEnginePygmentsFuture 4 + extends FutureProxy { 5 + 6 + private $source; 7 + private $scrub; 8 + 9 + public function __construct(Future $proxied, $source, $scrub = false) { 10 + parent::__construct($proxied); 11 + $this->source = $source; 12 + $this->scrub = $scrub; 13 + } 14 + 15 + protected function didReceiveResult($result) { 16 + list($err, $stdout, $stderr) = $result; 17 + 18 + if (!$err && strlen($stdout)) { 19 + // Strip off fluff Pygments adds. 20 + $stdout = preg_replace( 21 + '@^<div class="highlight"><pre>(.*)</pre></div>\s*$@s', 22 + '\1', 23 + $stdout); 24 + if ($this->scrub) { 25 + $stdout = preg_replace('/^.*\n/', '', $stdout); 26 + } 27 + return phutil_safe_html($stdout); 28 + } 29 + 30 + throw new PhutilSyntaxHighlighterException($stderr, $err); 31 + } 32 + 33 + }
+262
src/infrastructure/markup/syntax/highlighter/xhpast/PhutilXHPASTSyntaxHighlighterFuture.php
··· 1 + <?php 2 + 3 + final class PhutilXHPASTSyntaxHighlighterFuture extends FutureProxy { 4 + 5 + private $source; 6 + private $scrub; 7 + 8 + public function __construct(Future $proxied, $source, $scrub = false) { 9 + parent::__construct($proxied); 10 + $this->source = $source; 11 + $this->scrub = $scrub; 12 + } 13 + 14 + protected function didReceiveResult($result) { 15 + try { 16 + return $this->applyXHPHighlight($result); 17 + } catch (Exception $ex) { 18 + // XHP can't highlight source that isn't syntactically valid. Fall back 19 + // to the fragment lexer. 20 + $source = ($this->scrub 21 + ? preg_replace('/^.*\n/', '', $this->source) 22 + : $this->source); 23 + return id(new PhutilLexerSyntaxHighlighter()) 24 + ->setConfig('lexer', new PhutilPHPFragmentLexer()) 25 + ->setConfig('language', 'php') 26 + ->getHighlightFuture($source) 27 + ->resolve(); 28 + } 29 + } 30 + 31 + private function applyXHPHighlight($result) { 32 + 33 + // We perform two passes here: one using the AST to find symbols we care 34 + // about -- particularly, class names and function names. These are used 35 + // in the crossreference stuff to link into Diffusion. After we've done our 36 + // AST pass, we do a followup pass on the token stream to catch all the 37 + // simple stuff like strings and comments. 38 + 39 + $tree = XHPASTTree::newFromDataAndResolvedExecFuture( 40 + $this->source, 41 + $result); 42 + 43 + $root = $tree->getRootNode(); 44 + 45 + $tokens = $root->getTokens(); 46 + $interesting_symbols = $this->findInterestingSymbols($root); 47 + 48 + 49 + if ($this->scrub) { 50 + // If we're scrubbing, we prepended "<?php\n" to the text to force the 51 + // highlighter to treat it as PHP source. Now, we need to remove that. 52 + 53 + $ok = false; 54 + if (count($tokens) >= 2) { 55 + if ($tokens[0]->getTypeName() === 'T_OPEN_TAG') { 56 + if ($tokens[1]->getTypeName() === 'T_WHITESPACE') { 57 + $ok = true; 58 + } 59 + } 60 + } 61 + 62 + if (!$ok) { 63 + throw new Exception( 64 + pht( 65 + 'Expected T_OPEN_TAG, T_WHITESPACE tokens at head of results '. 66 + 'for highlighting parse of PHP snippet.')); 67 + } 68 + 69 + // Remove the "<?php". 70 + unset($tokens[0]); 71 + 72 + $value = $tokens[1]->getValue(); 73 + if ((strlen($value) < 1) || ($value[0] != "\n")) { 74 + throw new Exception( 75 + pht( 76 + 'Expected "\\n" at beginning of T_WHITESPACE token at head of '. 77 + 'tokens for highlighting parse of PHP snippet.')); 78 + } 79 + 80 + $value = substr($value, 1); 81 + $tokens[1]->overwriteValue($value); 82 + } 83 + 84 + $out = array(); 85 + foreach ($tokens as $key => $token) { 86 + $value = $token->getValue(); 87 + $class = null; 88 + $multi = false; 89 + $attrs = array(); 90 + if (isset($interesting_symbols[$key])) { 91 + $sym = $interesting_symbols[$key]; 92 + $class = $sym[0]; 93 + $attrs['data-symbol-context'] = idx($sym, 'context'); 94 + $attrs['data-symbol-name'] = idx($sym, 'symbol'); 95 + } else { 96 + switch ($token->getTypeName()) { 97 + case 'T_WHITESPACE': 98 + break; 99 + case 'T_DOC_COMMENT': 100 + $class = 'dc'; 101 + $multi = true; 102 + break; 103 + case 'T_COMMENT': 104 + $class = 'c'; 105 + $multi = true; 106 + break; 107 + case 'T_CONSTANT_ENCAPSED_STRING': 108 + case 'T_ENCAPSED_AND_WHITESPACE': 109 + case 'T_INLINE_HTML': 110 + $class = 's'; 111 + $multi = true; 112 + break; 113 + case 'T_VARIABLE': 114 + $class = 'nv'; 115 + break; 116 + case 'T_OPEN_TAG': 117 + case 'T_OPEN_TAG_WITH_ECHO': 118 + case 'T_CLOSE_TAG': 119 + $class = 'o'; 120 + break; 121 + case 'T_LNUMBER': 122 + case 'T_DNUMBER': 123 + $class = 'm'; 124 + break; 125 + case 'T_STRING': 126 + static $magic = array( 127 + 'true' => true, 128 + 'false' => true, 129 + 'null' => true, 130 + ); 131 + if (isset($magic[strtolower($value)])) { 132 + $class = 'k'; 133 + break; 134 + } 135 + $class = 'nx'; 136 + break; 137 + default: 138 + $class = 'k'; 139 + break; 140 + } 141 + } 142 + 143 + if ($class) { 144 + $attrs['class'] = $class; 145 + if ($multi) { 146 + // If the token may have multiple lines in it, make sure each 147 + // <span> crosses no more than one line so the lines can be put 148 + // in a table, etc., later. 149 + $value = phutil_split_lines($value, $retain_endings = true); 150 + } else { 151 + $value = array($value); 152 + } 153 + foreach ($value as $val) { 154 + $out[] = phutil_tag('span', $attrs, $val); 155 + } 156 + } else { 157 + $out[] = $value; 158 + } 159 + } 160 + 161 + return phutil_implode_html('', $out); 162 + } 163 + 164 + private function findInterestingSymbols(XHPASTNode $root) { 165 + // Class name symbols appear in: 166 + // class X extends X implements X, X { ... } 167 + // new X(); 168 + // $x instanceof X 169 + // catch (X $x) 170 + // function f(X $x) 171 + // X::f(); 172 + // X::$m; 173 + // X::CONST; 174 + 175 + // These are PHP builtin tokens which can appear in a classname context. 176 + // Don't link them since they don't go anywhere useful. 177 + static $builtin_class_tokens = array( 178 + 'self' => true, 179 + 'parent' => true, 180 + 'static' => true, 181 + ); 182 + 183 + // Fortunately XHPAST puts all of these in a special node type so it's 184 + // easy to find them. 185 + $result_map = array(); 186 + $class_names = $root->selectDescendantsOfType('n_CLASS_NAME'); 187 + foreach ($class_names as $class_name) { 188 + foreach ($class_name->getTokens() as $key => $token) { 189 + if (isset($builtin_class_tokens[$token->getValue()])) { 190 + // This is something like "self::method()". 191 + continue; 192 + } 193 + $result_map[$key] = array( 194 + 'nc', // "Name, Class" 195 + 'symbol' => $class_name->getConcreteString(), 196 + ); 197 + } 198 + } 199 + 200 + // Function name symbols appear in: 201 + // f() 202 + 203 + $function_calls = $root->selectDescendantsOfType('n_FUNCTION_CALL'); 204 + foreach ($function_calls as $call) { 205 + $call = $call->getChildByIndex(0); 206 + if ($call->getTypeName() == 'n_SYMBOL_NAME') { 207 + // This is a normal function call, not some $f() shenanigans. 208 + foreach ($call->getTokens() as $key => $token) { 209 + $result_map[$key] = array( 210 + 'nf', // "Name, Function" 211 + 'symbol' => $call->getConcreteString(), 212 + ); 213 + } 214 + } 215 + } 216 + 217 + // Upon encountering $x->y, link y without context, since $x is unknown. 218 + 219 + $prop_access = $root->selectDescendantsOfType('n_OBJECT_PROPERTY_ACCESS'); 220 + foreach ($prop_access as $access) { 221 + $right = $access->getChildByIndex(1); 222 + if ($right->getTypeName() == 'n_INDEX_ACCESS') { 223 + // otherwise $x->y[0] doesn't get highlighted 224 + $right = $right->getChildByIndex(0); 225 + } 226 + if ($right->getTypeName() == 'n_STRING') { 227 + foreach ($right->getTokens() as $key => $token) { 228 + $result_map[$key] = array( 229 + 'na', // "Name, Attribute" 230 + 'symbol' => $right->getConcreteString(), 231 + ); 232 + } 233 + } 234 + } 235 + 236 + // Upon encountering x::y, try to link y with context x. 237 + 238 + $static_access = $root->selectDescendantsOfType('n_CLASS_STATIC_ACCESS'); 239 + foreach ($static_access as $access) { 240 + $class = $access->getChildByIndex(0); 241 + $right = $access->getChildByIndex(1); 242 + if ($class->getTypeName() == 'n_CLASS_NAME' && 243 + ($right->getTypeName() == 'n_STRING' || 244 + $right->getTypeName() == 'n_VARIABLE')) { 245 + $classname = head($class->getTokens())->getValue(); 246 + $result = array( 247 + 'na', 248 + 'symbol' => ltrim($right->getConcreteString(), '$'), 249 + ); 250 + if (!isset($builtin_class_tokens[$classname])) { 251 + $result['context'] = $classname; 252 + } 253 + foreach ($right->getTokens() as $key => $token) { 254 + $result_map[$key] = $result; 255 + } 256 + } 257 + } 258 + 259 + return $result_map; 260 + } 261 + 262 + }
+83
src/infrastructure/parser/PhutilPygmentizeParser.php
··· 1 + <?php 2 + 3 + /** 4 + * Parser that converts `pygmetize` output or similar HTML blocks from "class" 5 + * attributes to "style" attributes. 6 + */ 7 + final class PhutilPygmentizeParser extends Phobject { 8 + 9 + private $map = array(); 10 + 11 + public function setMap(array $map) { 12 + $this->map = $map; 13 + return $this; 14 + } 15 + 16 + public function getMap() { 17 + return $this->map; 18 + } 19 + 20 + public function parse($block) { 21 + $class_look = 'class="'; 22 + $class_len = strlen($class_look); 23 + 24 + $class_start = null; 25 + 26 + $map = $this->map; 27 + 28 + $len = strlen($block); 29 + $out = ''; 30 + $mode = 'text'; 31 + for ($ii = 0; $ii < $len; $ii++) { 32 + $c = $block[$ii]; 33 + switch ($mode) { 34 + case 'text': 35 + // We're in general text between tags, and just passing characers 36 + // through unmodified. 37 + if ($c == '<') { 38 + $mode = 'tag'; 39 + } 40 + $out .= $c; 41 + break; 42 + case 'tag': 43 + // We're inside a tag, and looking for `class="` so we can rewrite 44 + // it. 45 + if ($c == '>') { 46 + $mode = 'text'; 47 + } 48 + if ($c == 'c') { 49 + if (!substr_compare($block, $class_look, $ii, $class_len)) { 50 + $mode = 'class'; 51 + $ii += $class_len; 52 + $class_start = $ii; 53 + } 54 + } 55 + 56 + if ($mode != 'class') { 57 + $out .= $c; 58 + } 59 + break; 60 + case 'class': 61 + // We're inside a `class="..."` tag, and looking for the ending quote 62 + // so we can replace it. 63 + if ($c == '"') { 64 + $class = substr($block, $class_start, $ii - $class_start); 65 + 66 + // If this class is present in the map, rewrite it into an inline 67 + // style attribute. 68 + if (isset($map[$class])) { 69 + $out .= 'style="'.phutil_escape_html($map[$class]).'"'; 70 + } else { 71 + $out .= 'class="'.$class.'"'; 72 + } 73 + 74 + $mode = 'tag'; 75 + } 76 + break; 77 + } 78 + } 79 + 80 + return $out; 81 + } 82 + 83 + }
+43
src/infrastructure/parser/__tests__/PhutilPygmentizeParserTestCase.php
··· 1 + <?php 2 + 3 + final class PhutilPygmentizeParserTestCase extends PhutilTestCase { 4 + 5 + public function testPygmentizeParser() { 6 + $this->tryParser( 7 + '', 8 + '', 9 + array(), 10 + pht('Empty')); 11 + 12 + $this->tryParser( 13 + '<span class="mi">1</span>', 14 + '<span style="color: #ff0000">1</span>', 15 + array( 16 + 'mi' => 'color: #ff0000', 17 + ), 18 + pht('Simple')); 19 + 20 + $this->tryParser( 21 + '<span class="mi">1</span>', 22 + '<span class="mi">1</span>', 23 + array(), 24 + pht('Missing Class')); 25 + 26 + $this->tryParser( 27 + '<span data-symbol-name="X" class="nc">X</span>', 28 + '<span data-symbol-name="X" style="color: #ff0000">X</span>', 29 + array( 30 + 'nc' => 'color: #ff0000', 31 + ), 32 + pht('Extra Attribute')); 33 + } 34 + 35 + private function tryParser($input, $expect, array $map, $label) { 36 + $actual = id(new PhutilPygmentizeParser()) 37 + ->setMap($map) 38 + ->parse($input); 39 + 40 + $this->assertEqual($expect, $actual, pht('Pygmentize Parser: %s', $label)); 41 + } 42 + 43 + }