├── tests ├── FileValidator │ ├── fixtures │ │ ├── source │ │ │ ├── keep.txt │ │ │ ├── email │ │ │ │ ├── invalid_sig.txt │ │ │ │ ├── crlf_sig.txt │ │ │ │ └── email.txt │ │ │ ├── help │ │ │ │ ├── valid.php │ │ │ │ └── invalid_help.php │ │ │ ├── css │ │ │ │ ├── invalid.css │ │ │ │ ├── invalid2.css │ │ │ │ └── valid.css │ │ │ └── language │ │ │ │ ├── lang_output.php │ │ │ │ ├── lang.php │ │ │ │ └── lang2.php │ │ └── origin │ │ │ ├── index │ │ │ ├── empty_index.htm │ │ │ ├── default_index.htm │ │ │ └── invalid_index.htm │ │ │ ├── iso │ │ │ ├── fewer_iso.txt │ │ │ ├── valid_iso.txt │ │ │ └── more_iso.txt │ │ │ ├── line_endings │ │ │ ├── invalid.php │ │ │ └── valid.php │ │ │ ├── help │ │ │ ├── invalid_help_var.php │ │ │ ├── no_help.php │ │ │ ├── invalid_help.php │ │ │ ├── valid.php │ │ │ └── additional_variable.php │ │ │ ├── license │ │ │ ├── invalid1.txt │ │ │ └── valid_gnu_gplv2.txt │ │ │ ├── in_phpbb │ │ │ ├── valid.php │ │ │ └── invalid.php │ │ │ ├── email │ │ │ ├── crlf_sig.txt │ │ │ ├── invalid_sig.txt │ │ │ └── email.txt │ │ │ ├── language │ │ │ ├── lang2.php │ │ │ ├── lang_output.php │ │ │ └── lang.php │ │ │ ├── nophpclosingtag │ │ │ ├── shortarraysyntax.php │ │ │ ├── withoutnewline.php │ │ │ ├── withcrlf.php │ │ │ ├── withouttag.php │ │ │ └── withtag.php │ │ │ ├── css │ │ │ ├── invalid.css │ │ │ ├── invalid2.css │ │ │ └── valid.css │ │ │ └── utf8withoutbom │ │ │ ├── with.php │ │ │ └── without.php │ ├── ValidateNoPhpClosingTagTest.php │ ├── ValidateLineEndingsTest.php │ ├── ValidateDefinedInPhpbbTest.php │ ├── ValidateIndexTest.php │ ├── ValidateLicenseTest.php │ ├── ValidateUtf8withoutbomTest.php │ ├── TestBase.php │ ├── ValidateCSSFileTest.php │ ├── ValidateEmailTest.php │ └── ValidateLangTest.php ├── FileListValidator │ ├── fixtures │ │ ├── 4.0 │ │ │ ├── origin │ │ │ │ ├── file.php │ │ │ │ ├── additional.php │ │ │ │ ├── additional.txt │ │ │ │ ├── subdir │ │ │ │ │ ├── file.php │ │ │ │ │ └── additional.php │ │ │ │ └── language │ │ │ │ │ └── origin │ │ │ │ │ ├── AUTHORS │ │ │ │ │ ├── README │ │ │ │ │ ├── VERSION │ │ │ │ │ ├── AUTHORS.md │ │ │ │ │ ├── CHANGELOG │ │ │ │ │ ├── CHANGELOG.md │ │ │ │ │ ├── README.md │ │ │ │ │ ├── VERSION.md │ │ │ │ │ ├── index.htm │ │ │ │ │ └── composer.json │ │ │ └── source │ │ │ │ ├── file.php │ │ │ │ ├── missing.php │ │ │ │ ├── missing.txt │ │ │ │ ├── subdir │ │ │ │ ├── file.php │ │ │ │ └── missing.php │ │ │ │ └── language │ │ │ │ └── source │ │ │ │ └── composer.json │ │ └── origin │ │ │ └── language │ │ │ └── origin │ │ │ └── common.php │ └── FileListTest.php ├── bootstrap.php ├── TestBase.php ├── LangKeyValidator │ ├── TestBase.php │ ├── ValidateAclTest.php │ ├── ValidateTest.php │ ├── ValidateDateformatsTest.php │ ├── ValidateArrayKeyTest.php │ ├── ValidatePluralKeysTest.php │ ├── ValidateHtmlTest.php │ └── ValidateStringTest.php └── Mock │ └── Output.php ├── .gitignore ├── phpunit.xml ├── translation.php ├── src └── Phpbb │ └── TranslationValidator │ ├── Cli.php │ ├── Output │ ├── OutputFormatter.php │ ├── OutputInterface.php │ ├── Message.php │ └── Output.php │ ├── Command │ ├── DownloadCommand.php │ └── ValidateCommand.php │ └── Validator │ ├── FileListValidator.php │ ├── ValidatorRunner.php │ ├── LangKeyValidator.php │ └── FileValidator.php ├── .github └── workflows │ └── phpunit.yaml ├── composer.json ├── README.md └── license.txt /tests/FileValidator/fixtures/source/keep.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/FileListValidator/fixtures/4.0/origin/file.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/FileListValidator/fixtures/4.0/source/file.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | /.idea/ 3 | /bin/ 4 | /4.0/ 5 | -------------------------------------------------------------------------------- /tests/FileListValidator/fixtures/4.0/origin/additional.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/FileListValidator/fixtures/4.0/origin/additional.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/FileListValidator/fixtures/4.0/source/missing.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/FileListValidator/fixtures/4.0/source/missing.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/FileValidator/fixtures/origin/index/empty_index.htm: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/FileListValidator/fixtures/4.0/origin/subdir/file.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/FileListValidator/fixtures/4.0/source/subdir/file.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/FileListValidator/fixtures/4.0/source/subdir/missing.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/FileListValidator/fixtures/4.0/origin/language/origin/AUTHORS: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/FileListValidator/fixtures/4.0/origin/language/origin/README: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/FileListValidator/fixtures/4.0/origin/language/origin/VERSION: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/FileListValidator/fixtures/4.0/origin/subdir/additional.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/FileListValidator/fixtures/4.0/origin/language/origin/AUTHORS.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/FileListValidator/fixtures/4.0/origin/language/origin/CHANGELOG: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/FileListValidator/fixtures/4.0/origin/language/origin/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/FileListValidator/fixtures/4.0/origin/language/origin/README.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/FileListValidator/fixtures/4.0/origin/language/origin/VERSION.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/FileListValidator/fixtures/4.0/origin/language/origin/index.htm: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/FileValidator/fixtures/origin/iso/fewer_iso.txt: -------------------------------------------------------------------------------- 1 | English 2 | Origin missing author -------------------------------------------------------------------------------- /tests/FileValidator/fixtures/origin/iso/valid_iso.txt: -------------------------------------------------------------------------------- 1 | English 2 | Origin 3 | Copyright owners -------------------------------------------------------------------------------- /tests/FileValidator/fixtures/origin/line_endings/invalid.php: -------------------------------------------------------------------------------- 1 | 'Kakao', 5 | ]; 6 | -------------------------------------------------------------------------------- /tests/FileValidator/fixtures/origin/help/no_help.php: -------------------------------------------------------------------------------- 1 | '--', 6 | 1 => 'foo' 7 | ), 8 | ); 9 | -------------------------------------------------------------------------------- /tests/FileValidator/fixtures/origin/nophpclosingtag/withoutnewline.php: -------------------------------------------------------------------------------- 1 | '1 day', 5 | )); -------------------------------------------------------------------------------- /tests/FileValidator/fixtures/origin/nophpclosingtag/withcrlf.php: -------------------------------------------------------------------------------- 1 | '1 day', 5 | )); 6 | -------------------------------------------------------------------------------- /tests/FileValidator/fixtures/origin/nophpclosingtag/withouttag.php: -------------------------------------------------------------------------------- 1 | '1 day', 5 | )); 6 | -------------------------------------------------------------------------------- /tests/FileValidator/fixtures/origin/nophpclosingtag/withtag.php: -------------------------------------------------------------------------------- 1 | '1 day', 5 | )); 6 | 7 | ?> -------------------------------------------------------------------------------- /tests/FileValidator/fixtures/origin/css/invalid.css: -------------------------------------------------------------------------------- 1 | /* Icon images */ 2 | .invalid-inline { invalid: in{line; } 3 | .invalid-inline2 invalid: inline2; } 4 | -------------------------------------------------------------------------------- /tests/FileValidator/fixtures/origin/email/invalid_sig.txt: -------------------------------------------------------------------------------- 1 | Original does not contain sig 2 | 3 | also this file is UTF8 WITH BOM 4 | {YEHAA} 5 | {EMAIL_SIG} 6 | -------------------------------------------------------------------------------- /tests/FileValidator/fixtures/source/css/invalid.css: -------------------------------------------------------------------------------- 1 | /* Icon images */ 2 | .invalid-inline { invalid: inline; } 3 | .invalid-inline2 { invalid: inline2; } 4 | -------------------------------------------------------------------------------- /tests/FileValidator/fixtures/source/css/invalid2.css: -------------------------------------------------------------------------------- 1 | /* Icon images */ 2 | .missing-rule { missing: rule; } 3 | 4 | /* EN Language Pack */ 5 | .additional-block { 6 | padding-top: 20px; 7 | } 8 | -------------------------------------------------------------------------------- /tests/FileValidator/fixtures/origin/css/invalid2.css: -------------------------------------------------------------------------------- 1 | /* Icon images */ 2 | .additional-rule { additional: rule; } 3 | 4 | /* EN Language Pack */ 5 | .additional-block { 6 | padding-top: 20px; 7 | } 8 | 9 | Output after rules 10 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /tests/FileValidator/fixtures/origin/index/invalid_index.htm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Hack it? 9 | 10 | 11 | -------------------------------------------------------------------------------- /tests/FileValidator/fixtures/source/language/lang_output.php: -------------------------------------------------------------------------------- 1 | '1 day', 15 | )); 16 | -------------------------------------------------------------------------------- /tests/FileValidator/fixtures/origin/language/lang_output.php: -------------------------------------------------------------------------------- 1 | '1 day', 15 | )); 16 | 17 | ?> 18 | 19 | 20 | -------------------------------------------------------------------------------- /tests/FileValidator/fixtures/origin/help/invalid_help.php: -------------------------------------------------------------------------------- 1 | '--', 6 | ), 7 | // This block will switch the FAQ-Questions to the second template column 8 | array( 9 | 0 => '--', 10 | 2 => '--' 11 | ), 12 | array( 13 | 'lol' => 'bar' 14 | ), 15 | 'foo', 16 | ); 17 | -------------------------------------------------------------------------------- /tests/FileValidator/fixtures/origin/help/valid.php: -------------------------------------------------------------------------------- 1 | '--', 6 | 1 => 'foo' 7 | ), 8 | // This block will switch the FAQ-Questions to the second template column 9 | array( 10 | 0 => '--', 11 | 1 => '--' 12 | ), 13 | array( 14 | 0 => 'foo', 15 | 1 => 'bar' 16 | ), 17 | ); -------------------------------------------------------------------------------- /tests/FileValidator/fixtures/origin/help/additional_variable.php: -------------------------------------------------------------------------------- 1 | '--', 10 | 1 => 'foo' 11 | ), 12 | // This block will switch the FAQ-Questions to the second template column 13 | array( 14 | 0 => '--', 15 | 1 => '--' 16 | ), 17 | array( 18 | 0 => 'foo', 19 | 1 => 'bar' 20 | ), 21 | ); 22 | -------------------------------------------------------------------------------- /tests/FileValidator/fixtures/source/email/email.txt: -------------------------------------------------------------------------------- 1 | Subject: Activate user account 2 | 3 | Hello, 4 | 5 | The account owned by "{USERNAME}" has been deactivated or newly created, you should check the details of this user (if required) and handle it appropriately. 6 | 7 | Use this link to view the user's profile: 8 | {U_USER_DETAILS} 9 | 10 | Use this link to activate the account: 11 | {U_ACTIVATE} 12 | 13 | 14 | {EMAIL_SIG} -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 11 | ./tests/ 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /tests/FileValidator/fixtures/source/language/lang.php: -------------------------------------------------------------------------------- 1 | '1 day', 15 | '1_MONTH' => '1 month', 16 | '1_YEAR' => '1 year', 17 | '2_WEEKS' => '2 weeks', 18 | '3_MONTHS' => '3 months', 19 | '6_MONTHS' => '6 months', 20 | '7_DAYS' => '7 days', 21 | )); 22 | -------------------------------------------------------------------------------- /tests/FileValidator/fixtures/source/language/lang2.php: -------------------------------------------------------------------------------- 1 | '1 day', 15 | '1_MONTH' => '1 month', 16 | '1_YEAR' => '1 year', 17 | '2_WEEKS' => '2 weeks', 18 | '3_MONTHS' => '3 months', 19 | '6_MONTHS' => '6 months', 20 | '7_DAYS' => '7 days', 21 | )); 22 | -------------------------------------------------------------------------------- /tests/FileValidator/fixtures/origin/css/valid.css: -------------------------------------------------------------------------------- 1 | /* Icon images */ 2 | .pm-icon, .pm-icon a { background-image: url("./icon_contact_pm.gif"); } 3 | .quote-icon, .quote-icon a { background-image: url("./icon_post_quote.gif"); } 4 | .edit-icon, .edit-icon a { background-image: url("./icon_post_edit.gif"); } 5 | 6 | /* EN Language Pack */ 7 | .imageset.icon_contact_pm { 8 | background-image: url("./icon_contact_pm.gif"); 9 | padding-left: 28px; 10 | padding-top: 20px; 11 | } 12 | -------------------------------------------------------------------------------- /tests/FileValidator/fixtures/source/css/valid.css: -------------------------------------------------------------------------------- 1 | /* Icon images */ 2 | .pm-icon, .pm-icon a { background-image: url("./icon_contact_pm.gif"); } 3 | .quote-icon, .quote-icon a { background-image: url("./icon_post_quote.gif"); } 4 | .edit-icon, .edit-icon a { background-image: url("./icon_post_edit.gif"); } 5 | 6 | /* EN Language Pack */ 7 | .imageset.icon_contact_pm { 8 | background-image: url("./icon_contact_pm.gif"); 9 | padding-left: 28px; 10 | padding-top: 20px; 11 | } 12 | -------------------------------------------------------------------------------- /translation.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | run(); 20 | -------------------------------------------------------------------------------- /tests/FileValidator/fixtures/origin/language/lang.php: -------------------------------------------------------------------------------- 1 | '1 day', 17 | '1_MONTH' => '1 month', 18 | '1_YEAR' => '1 year', 19 | '2_WEEKS' => '2 weeks', 20 | '3_MONTHS' => '3 months', 21 | '6_MONTHS' => '6 months', 22 | // Missing language key '7_DAYS' => '7 days', 23 | '8_DAYS' => 'Additional language key', 24 | )); 25 | -------------------------------------------------------------------------------- /tests/FileValidator/fixtures/origin/utf8withoutbom/with.php: -------------------------------------------------------------------------------- 1 | '1 day', 17 | '1_MONTH' => '1 month', 18 | '1_YEAR' => '1 year', 19 | '2_WEEKS' => '2 weeks', 20 | '3_MONTHS' => '3 months', 21 | '6_MONTHS' => '6 months', 22 | // Missing language key '7_DAYS' => '7 days', 23 | '8_DAYS' => 'Additional language key', 24 | )); 25 | 26 | ?> -------------------------------------------------------------------------------- /tests/FileValidator/fixtures/origin/utf8withoutbom/without.php: -------------------------------------------------------------------------------- 1 | '1 day', 17 | '1_MONTH' => '1 month', 18 | '1_YEAR' => '1 year', 19 | '2_WEEKS' => '2 weeks', 20 | '3_MONTHS' => '3 months', 21 | '6_MONTHS' => '6 months', 22 | // Missing language key '7_DAYS' => '7 days', 23 | '8_DAYS' => 'Additional language key', 24 | )); 25 | 26 | ?> -------------------------------------------------------------------------------- /tests/FileValidator/fixtures/origin/email/email.txt: -------------------------------------------------------------------------------- 1 | *Missing subject in line 1* 2 | 3 | Hello *{TEMPLATE_VAR_DOES_NOT_EXIST}*, 4 | 5 | The account owned by "{USERNAME}" has been deactivated or newly created, you should check the details of this user (if required) and handle it appropriately. 6 | 7 | Use this link to view the user's profile: 8 | {U_USER_DETAILS} 9 | 10 | Allow Template Syntax 11 | 12 | Use this link to activate the account: 13 | {U_ACTIVATE*NOT_USING_NORMAL_VAR*} 14 | 15 | Haxxor translator adds a URL and HTML to the Email 16 | 17 | {EMAIL_SIG} 18 | 19 | *Email Sig not at the end, and no new Line at End* -------------------------------------------------------------------------------- /src/Phpbb/TranslationValidator/Cli.php: -------------------------------------------------------------------------------- 1 | output = new \Phpbb\TranslationValidator\Tests\Mock\Output(); 21 | } 22 | 23 | public function assertOutputMessages($expected) 24 | { 25 | sort($expected); 26 | $this->assertEquals($expected, $this->output->getMessages()); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/FileValidator/ValidateNoPhpClosingTagTest.php: -------------------------------------------------------------------------------- 1 | validator->setPhpbbVersion($phpbbVersion); 28 | $this->validator->validateNoPhpClosingTag($file); 29 | $this->assertOutputMessages($expected); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/FileValidator/ValidateLineEndingsTest.php: -------------------------------------------------------------------------------- 1 | validator->validateLineEndings($file); 31 | $this->assertOutputMessages($expected); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/FileValidator/ValidateDefinedInPhpbbTest.php: -------------------------------------------------------------------------------- 1 | validator->validateDefinedInPhpbb($file); 31 | $this->assertOutputMessages($expected); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/FileValidator/ValidateIndexTest.php: -------------------------------------------------------------------------------- 1 | validator->validateIndexFile($file); 32 | $this->assertOutputMessages($expected); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/FileValidator/ValidateLicenseTest.php: -------------------------------------------------------------------------------- 1 | validator->validateLicenseFile($file); 31 | $this->assertOutputMessages($expected); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /.github/workflows/phpunit.yaml: -------------------------------------------------------------------------------- 1 | # .github/workflows/phpunit.yaml 2 | name: phpunit 3 | 4 | on: [push, pull_request] 5 | 6 | jobs: 7 | tests: 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | php: ['8.1'] 12 | 13 | name: PHP ${{ matrix.php }} tests 14 | steps: 15 | - run: echo "This job for ${{ github.ref }} was automatically triggered by a ${{ github.event_name }} event on ${{ runner.os }}." 16 | 17 | # Basically git clone 18 | - uses: actions/checkout@v2 19 | 20 | # Use PHP of specific version 21 | - uses: shivammathur/setup-php@v2 22 | with: 23 | php-version: ${{ matrix.php }} 24 | coverage: none # disable xdebug, pcov 25 | 26 | # If we use two steps like this, we can better see if composer or the test failed 27 | - run: composer install --dev --no-interaction --prefer-source 28 | - run: vendor/phpunit/phpunit/phpunit 29 | - run: echo "This job's status is ${{ job.status }}." 30 | -------------------------------------------------------------------------------- /src/Phpbb/TranslationValidator/Output/OutputFormatter.php: -------------------------------------------------------------------------------- 1 | new OutputFormatterStyle('black', 'green'), 19 | 'notice' => new OutputFormatterStyle('cyan'), 20 | 'noticebg' => new OutputFormatterStyle('black', 'cyan'), 21 | 'warning' => new OutputFormatterStyle('yellow'), 22 | 'error' => new OutputFormatterStyle('red'), 23 | 'fatal' => new OutputFormatterStyle('white', 'red'), 24 | ))); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/FileValidator/ValidateUtf8withoutbomTest.php: -------------------------------------------------------------------------------- 1 | validator->validateUtf8withoutbom($file); 31 | $this->assertOutputMessages($expected); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/LangKeyValidator/TestBase.php: -------------------------------------------------------------------------------- 1 | validator = new \Phpbb\TranslationValidator\Validator\LangKeyValidator($this->getMockBuilder('Symfony\Component\Console\Input\InputInterface')->getMock(), $this->output); 21 | $this->validator->setOrigin('origin', dirname(__FILE__) . '/fixtures/origin', 'language/origin/') 22 | ->setSource('source', dirname(__FILE__) . '/fixtures/source', 'language/source/') 23 | ->setPhpbbVersion('4.0') 24 | ->setPluralRule(1); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/FileValidator/TestBase.php: -------------------------------------------------------------------------------- 1 | validator = new \Phpbb\TranslationValidator\Validator\FileValidator($this->getMockBuilder('Symfony\Component\Console\Input\InputInterface')->getMock(), $this->output); 27 | $this->validator->setOrigin('origin', dirname(__FILE__) . '/fixtures/origin', 'language/origin/') 28 | ->setSource('source', dirname(__FILE__) . '/fixtures/source', 'language/source/') 29 | ->setPhpbbVersion('3.2'); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/FileListValidator/fixtures/origin/language/origin/common.php: -------------------------------------------------------------------------------- 1 | 7 | * @license GNU General Public License, version 2 (GPL-2.0) 8 | * 9 | * For full copyright and license information, please see 10 | * the docs/CREDITS.txt file. 11 | * 12 | */ 13 | 14 | /** 15 | * DO NOT CHANGE 16 | */ 17 | if (!defined('IN_PHPBB')) 18 | { 19 | exit; 20 | } 21 | 22 | if (empty($lang) || !is_array($lang)) 23 | { 24 | $lang = array(); 25 | } 26 | 27 | // DEVELOPERS PLEASE NOTE 28 | // 29 | // All language files should use UTF-8 as their encoding and the files must not contain a BOM. 30 | // 31 | // Placeholders can now contain order information, e.g. instead of 32 | // 'Page %s of %s' you can (and should) write 'Page %1$s of %2$s', this allows 33 | // translators to re-order the output of data while ensuring it remains correct 34 | // 35 | // You do not need this where single placeholders are used, e.g. 'Message %d' is fine 36 | // equally where a string contains only two placeholders which are used to wrap text 37 | // in a url you again do not need to specify an order e.g., 'Click %sHERE%s' is fine 38 | // 39 | // Some characters you may want to copy&paste: 40 | // ’ » “ ” … 41 | // 42 | 43 | $lang = array_merge($lang, array( 44 | 'DIRECTION' => 'ltr', 45 | )); 46 | -------------------------------------------------------------------------------- /tests/LangKeyValidator/ValidateAclTest.php: -------------------------------------------------------------------------------- 1 | 'foo', 'cat' => 'bar'), array('lang' => 'foo'), array( 19 | Output::FATAL . '-Permission is missing the cat-key--MissingCat', 20 | )), 21 | array('MissingLang', array('lang' => 'foo', 'cat' => 'bar'), array('cat' => 'bar'), array( 22 | Output::FATAL . '-Permission is missing the lang-key--MissingLang', 23 | )), 24 | array('InvalidCat', array('lang' => 'foo', 'cat' => 'bar'), array('lang' => 'foo', 'cat' => 'notBar'), array( 25 | Output::FATAL . '-Permission should have cat bar but has notBar--InvalidCat', 26 | )), 27 | ); 28 | } 29 | 30 | /** 31 | * @dataProvider validateAclData 32 | */ 33 | public function testValidateAcl($key, $against_language, $validate_language, $expected) 34 | { 35 | $this->validator->validateAcl('', $key, $against_language, $validate_language); 36 | $this->assertOutputMessages($expected); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "phpbb/translation-validator", 3 | "description": "A language package validator for phpBB language packs. Language packs are required to pass the validator when submitted to the language pack database.", 4 | "homepage": "https://github.com/phpbb/phpbb-translation-validator", 5 | "version": "2.0.0-dev", 6 | "license": "GPL-2.0-only", 7 | "authors": [ 8 | { 9 | "name": "Joas Schilling", 10 | "email": "nickvergessen@phpbb.com", 11 | "homepage": "https://www.phpbb.com/", 12 | "role": "Former Developer" 13 | }, 14 | { 15 | "name": "Battye", 16 | "email": "battye@phpbb.com", 17 | "homepage": "https://www.phpbb.com", 18 | "role": "Developer" 19 | }, 20 | { 21 | "name": "Christian Schnegelberger", 22 | "email": "crizzo@phpbb.com", 23 | "homepage": "https://www.phpbb.com", 24 | "role": "Developer" 25 | } 26 | ], 27 | "minimum-stability": "stable", 28 | "require": { 29 | "php": ">=8.1", 30 | "symfony/yaml": "~6.3", 31 | "symfony/console": "~6.3", 32 | "symfony/finder": "~6.3", 33 | "battye/php-array-parser": "~1.0" 34 | }, 35 | "require-dev": { 36 | "phpunit/phpunit": "~10.0" 37 | }, 38 | "bin": [ 39 | "translation.php" 40 | ], 41 | "config": { 42 | "platform": { 43 | "php": "8.1" 44 | }, 45 | "bin-dir": "bin" 46 | }, 47 | "autoload": { 48 | "classmap": [ 49 | "src/", 50 | "tests/" 51 | ] 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /tests/FileValidator/ValidateCSSFileTest.php: -------------------------------------------------------------------------------- 1 | validator->validateCSSFile($file, $file); 38 | $this->assertOutputMessages($expected); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Phpbb/TranslationValidator/Output/OutputInterface.php: -------------------------------------------------------------------------------- 1 | type = $type; 27 | $this->message = $message; 28 | $this->file = $file; 29 | $this->file_details = $file_details; 30 | } 31 | 32 | public function getType() 33 | { 34 | return $this->type; 35 | } 36 | 37 | public function __toString() 38 | { 39 | $file = ''; 40 | 41 | if ($this->file !== null) 42 | { 43 | $file = ' in ' . $this->file; 44 | 45 | if ($this->file_details !== null) 46 | { 47 | $file .= ':' . $this->file_details; 48 | } 49 | } 50 | 51 | switch ($this->type) 52 | { 53 | case Output::NOTICE: 54 | return " Notice{$file}:\n$this->message"; 55 | case Output::WARNING: 56 | return " Warning{$file}:\n$this->message"; 57 | case Output::ERROR: 58 | return " Error{$file}:\n$this->message"; 59 | case Output::FATAL: 60 | return " Fatal{$file}:\n$this->message"; 61 | default: 62 | return ''; 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /tests/LangKeyValidator/ValidateTest.php: -------------------------------------------------------------------------------- 1 | validator->validate('', $key, $against_language, $validate_language); 45 | $this->assertOutputMessages($expected); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tests/LangKeyValidator/ValidateDateformatsTest.php: -------------------------------------------------------------------------------- 1 | array()), array( 22 | Output::FATAL . '-Should be type string but is type array--InvalidArray.Array', 23 | )), 24 | array('InvalidInteger', array('Integer' => 0), array( 25 | Output::FATAL . '-Should be type string but is type integer--InvalidInteger.Integer', 26 | )), 27 | array('ValidString', array('String' => 'foobar'), array()), 28 | array('UsingHTML', array('String' => 'foobar'), array( 29 | Output::NOTICE . '-String is using additional html: --UsingHTML.String', 30 | )), 31 | array('UsingHTMLKey', array('String' => 'foo'), array( 32 | Output::NOTICE . '-String is using additional html: --UsingHTMLKey.String', 33 | )), 34 | ); 35 | } 36 | 37 | /** 38 | * @dataProvider validateDateformatsData 39 | */ 40 | public function testValidateDateformats($key, $validate_language, $expected) 41 | { 42 | $this->validator->validateDateformats('', $key, $validate_language); 43 | $this->assertOutputMessages($expected); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tests/Mock/Output.php: -------------------------------------------------------------------------------- 1 | fatals[] = $type . '-' . $message . '-'. $file . '-'. $file_details; 37 | break; 38 | case Output::ERROR: 39 | $this->errors[] = $type . '-' . $message . '-'. $file . '-'. $file_details; 40 | break; 41 | case Output::WARNING: 42 | $this->warnings[] = $type . '-' . $message . '-'. $file . '-'. $file_details; 43 | break; 44 | case Output::NOTICE: 45 | $this->notices[] = $type . '-' . $message . '-'. $file . '-'. $file_details; 46 | break; 47 | default: 48 | // TODO: Decide on this? 49 | } 50 | } 51 | 52 | /** 53 | * Get all messages saved into the message queue. 54 | * @return array Array with messages 55 | */ 56 | public function getMessages() 57 | { 58 | $messages = parent::getMessages(); 59 | sort($messages); 60 | return $messages; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /tests/FileValidator/ValidateEmailTest.php: -------------------------------------------------------------------------------- 1 | validator->validateEmail($file, $file); 42 | $this->assertOutputMessages($expected); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tests/LangKeyValidator/ValidateArrayKeyTest.php: -------------------------------------------------------------------------------- 1 | 'bar'), array('bar' => 'bar2'), array( 40 | Output::FATAL . '-Array is missing key: foo--MissingKey', 41 | Output::FATAL . '-Array has invalid key: bar--MissingKey', 42 | Output::ERROR . '-Key was not validated: bar--MissingKey.bar', 43 | )), 44 | array('MissingIntKey', array(1 => 'bar', 2 => 'bars'), array(1 => 'bar/s'), array( 45 | )), 46 | ); 47 | } 48 | 49 | /** 50 | * @dataProvider validateArrayKeyData 51 | */ 52 | public function testValidateArrayKey($key, $against_language, $validate_language, $expected) 53 | { 54 | $this->validator->validateArrayKey('', $key, $against_language, $validate_language); 55 | $this->assertOutputMessages($expected); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tests/LangKeyValidator/ValidatePluralKeysTest.php: -------------------------------------------------------------------------------- 1 | 'None'), array( 22 | Output::FATAL . '-Plural array must not be empty--EmptyZeroArray', 23 | )), 24 | array('FullArray', 0, array( 25 | 1 => 'Default', 26 | ), array()), 27 | array('FullZeroArray', 0, array( 28 | 0 => 'Zero', 29 | 1 => 'Default', 30 | ), array()), 31 | array('MissingArray', 1, array( 32 | 1 => 'Default', 33 | //2 => 'Default2 Missing', 34 | ), array( 35 | Output::WARNING . '-Plural array is missing case: 2--MissingArray', 36 | )), 37 | array('MissingZeroArray', 1, array( 38 | 0 => 'Zero', 39 | 1 => 'Default1', 40 | //2 => 'Default2 Missing', 41 | ), array( 42 | Output::WARNING . '-Plural array is missing case: 2--MissingZeroArray', 43 | )), 44 | array('AdditionalArray', 0, array( 45 | 1 => 'Default', 46 | 2 => 'Additional', 47 | ), array( 48 | Output::FATAL . '-Plural array has additional case: 2--AdditionalArray', 49 | )), 50 | array('AdditionalZeroArray', 0, array( 51 | 0 => 'Zero', 52 | 1 => 'Default', 53 | 2 => 'Additional', 54 | ), array( 55 | Output::FATAL . '-Plural array has additional case: 2--AdditionalZeroArray', 56 | )), 57 | ); 58 | } 59 | 60 | /** 61 | * @dataProvider validatePluralKeysData 62 | */ 63 | public function testValidatePluralKeys($key, $plural_rule, $validate_language, $expected) 64 | { 65 | $this->validator->setPluralRule($plural_rule); 66 | 67 | $this->validator->validatePluralKeys('', $key, array(), $validate_language); 68 | $this->assertOutputMessages($expected); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /tests/FileValidator/ValidateLangTest.php: -------------------------------------------------------------------------------- 1 | validator->validateLangFile($file, $file); 38 | $this->assertOutputMessages($expected); 39 | } 40 | 41 | /** 42 | * Test the reCaptcha checks 43 | */ 44 | public function testValidateLangCaptchas() 45 | { 46 | // Failure - as we supply a key that isn't valid 47 | $reCaptchaLanguage = 'incorrect'; 48 | $this->validator->validateCaptchaValues('', $reCaptchaLanguage); 49 | 50 | $output = $this->output->getMessages(); 51 | $expected = Output::ERROR . '-reCaptcha must match a language/country code on https://developers.google.com/recaptcha/docs/language - if no code exists for your language you can use "en".--'; 52 | 53 | $this->assertEquals($this->output->getMessageCount(Output::ERROR), 1); 54 | $this->assertEquals($output[0], $expected); 55 | 56 | // Pass - as 'en' is valid 57 | $reCaptchaLanguage = 'en'; 58 | $this->validator->validateCaptchaValues('', $reCaptchaLanguage); 59 | 60 | $this->assertEquals($this->output->getMessageCount(Output::ERROR), 1); // Shouldn't change in size as no error added 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /tests/FileListValidator/FileListTest.php: -------------------------------------------------------------------------------- 1 | validator = new \Phpbb\TranslationValidator\Validator\FileListValidator($this->getMockBuilder('Symfony\Component\Console\Input\InputInterface')->getMock(), $this->output); 23 | } 24 | 25 | public function validateFileListData() 26 | { 27 | return array( 28 | array( 29 | '4.0', 30 | array( 31 | Output::FATAL . '-Missing required file-missing.php-', 32 | Output::FATAL . '-Missing required file-missing.txt-', 33 | Output::FATAL . '-Missing required file-subdir/missing.php-', 34 | Output::FATAL . '-Missing required file-language/origin/LICENSE-', 35 | Output::FATAL . '-Found additional file-additional.php-', 36 | Output::FATAL . '-Found additional file-subdir/additional.php-', 37 | Output::FATAL . '-Found additional file-additional.txt-', 38 | Output::FATAL . '-Found additional file-language/origin/AUTHORS-', 39 | Output::FATAL . '-Found additional file-language/origin/CHANGELOG-', 40 | Output::FATAL . '-Found additional file-language/origin/README-', 41 | Output::FATAL . '-Found additional file-language/origin/VERSION-', 42 | 43 | Output::NOTICE . '-Found additional file-language/origin/AUTHORS.md-', 44 | Output::NOTICE . '-Found additional file-language/origin/CHANGELOG.md-', 45 | Output::NOTICE . '-Found additional file-language/origin/README.md-', 46 | Output::NOTICE . '-Found additional file-language/origin/VERSION.md-', 47 | Output::NOTICE . '-Found additional file-language/origin/index.htm-', 48 | ), 49 | ), 50 | ); 51 | } 52 | 53 | /** 54 | * @dataProvider validateFileListData 55 | */ 56 | public function testValidateFileList($phpbbVersion, $expected) 57 | { 58 | $this->validator->setOrigin('origin', dirname(__FILE__) . '/fixtures/'. $phpbbVersion . '/origin', 'language/origin/') 59 | ->setSource('source', dirname(__FILE__) . '/fixtures/'. $phpbbVersion . '/source', 'language/source/'); 60 | 61 | $this->validator 62 | ->setPhpbbVersion($phpbbVersion) 63 | ->validate(); 64 | $this->assertOutputMessages($expected); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /tests/LangKeyValidator/ValidateHtmlTest.php: -------------------------------------------------------------------------------- 1 | foobar', 'foobar', array()), 20 | array('Different html', 'foobar', 'foobar', array( 21 | Output::NOTICE . '-String is using additional html: --Different html', 22 | )), 23 | array('Additional html', 'foobar', 'foobar foobar foobar foobar', array( 24 | Output::NOTICE . '-String is using additional html: --Additional html', 25 | Output::ERROR . '-String is using additional html: --Additional html', 26 | Output::FATAL . '-String is using additional html: --Additional html', 27 | )), 28 | array('Additional unclosed html', 'foobar', 'foobar foobar', array( 29 | Output::NOTICE . '-String is using additional html: --Additional unclosed html', 30 | Output::FATAL . '-String is missing closing tag for html: strong--Additional unclosed html', 31 | )), 32 | array('Invalid html', 'foobar', 'foobarfoobar', array( 33 | Output::FATAL . '-String is using invalid html: --Invalid html', 34 | Output::FATAL . '-String is missing closing tag for html: em--Invalid html', 35 | )), 36 | array('Unclosed html', 'foobar', 'foofoobarbar', array( 37 | Output::FATAL . '-String is missing closing tag for html: em--Unclosed html', 38 | )), 39 | 40 | array( 41 | 'http:// vs https://', 42 | 'foobar', 'bar foo', 43 | array( 44 | Output::NOTICE . '-String is using additional html: --http:// vs https://', 45 | ), 46 | ), 47 | array( 48 | 'Different link', 49 | 'foobar', 'bar foo', 50 | array( 51 | Output::WARNING . '-String is using additional html: --Different link', 52 | ), 53 | ), 54 | array( 55 | 'TRANSLATION_INFO', 56 | 'Additional link in translator credits', 'bar foo', 57 | array( 58 | Output::WARNING . '-String is using additional html: -language/origin/common.php-TRANSLATION_INFO', 59 | ), 60 | 'language/origin/common.php', 61 | ), 62 | array( 63 | 'Additional link in help page', 64 | 'foobar', 'bar foo', 65 | array( 66 | Output::ERROR . '-String is using additional html: -language/origin/help_faq.php-Additional link in help page', 67 | ), 68 | 'language/origin/help_faq.php', 69 | ), 70 | array( 71 | 'Additional link', 72 | 'foobar', 'bar foo', 73 | array( 74 | Output::ERROR . '-String is using additional html: --Additional link', 75 | ), 76 | ), 77 | ); 78 | } 79 | 80 | /** 81 | * @dataProvider validateHtmlData 82 | */ 83 | public function testValidateHtml($key, $against_language, $validate_language, $expected, $file = '') 84 | { 85 | $this->validator->validateHtml($file, $key, $against_language, $validate_language); 86 | $this->assertOutputMessages($expected); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /tests/LangKeyValidator/ValidateStringTest.php: -------------------------------------------------------------------------------- 1 | validator->validateString('', $key, $against_language, $validate_language); 53 | $this->assertOutputMessages($expected); 54 | } 55 | 56 | public function validateStringPluralsData() 57 | { 58 | return array( 59 | array('Integer', 'foobar %d', 'foo %d bar', array()), 60 | array('MissingInt', 'foobar %d', 'foo bar', array()), 61 | array('2Integers', 'foobar %d %d', 'foo %d %d bar', array()), 62 | array('2IntegersMissingInt', 'foobar %d %d', 'foo %d bar', array( 63 | Output::FATAL . '-Should have 2 integer arguments, but has 1--2IntegersMissingInt', 64 | )), 65 | array('2IntegersNum', 'foobar %1$d %2$d', 'foo %1$d %2$d bar', array()), 66 | array('2IntegersNumMissingInt1', 'foobar %1$d %2$d', 'foo %2$d bar', array( 67 | Output::FATAL . '-Should have 2 integer arguments, but has 1--2IntegersNumMissingInt1', 68 | )), 69 | array('2IntegersNumMissingInt2', 'foobar %1$d %2$d', 'foo %1$d bar', array()), 70 | array('2IntegersNumMAdditionalInt1', 'foobar %2$d', 'foo %1$d %2$d bar', array()), 71 | array('2IntegersNumMAdditionalInt2', 'foobar %1$d', 'foo %1$d %2$d bar', array( 72 | Output::FATAL . '-Should have 1 integer arguments, but has 2--2IntegersNumMAdditionalInt2', 73 | )), 74 | ); 75 | } 76 | 77 | /** 78 | * @dataProvider validateStringPluralsData 79 | */ 80 | public function testValidateStringPlurals($key, $against_language, $validate_language, $expected) 81 | { 82 | $this->validator->validateString('', $key, $against_language, $validate_language, true); 83 | $this->assertOutputMessages($expected); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # phpBB Translation Validator 2 | 3 | With the help of this command line application you are able 4 | to validate [phpBB](https://www.phpbb.com) language packs. 5 | This application runs on your local machine and can be integrated 6 | into a [GitHub](https://www.github.com) repository. 7 | 8 | ## 📋Requirements 9 | 10 | This tool requires PHP 8.1 or above. In addition it needs several 11 | symfony and other packages, which need to be downloaded and installed with [Composer](https://getcomposer.org). 12 | 13 | 14 | ## 🏗️ Installation 15 | 16 | Clone this repository: 17 | 18 | git clone https://github.com/phpbb/phpbb-translation-validator.git 19 | 20 | Install the dependencies with Composer: 21 | 22 | composer.phar install 23 | 24 | Create a directory called `4.0` in the root of the Translation Validator. Afterwards download 25 | the [British English language pack](http://www.phpbb.com/customise/db/translation/british_english/) 26 | and put its content into ``4.0/en/``. Do the same with the languages you wish to test. Which leads e.g. to: 27 | 28 | phpbb-translation-validator/4.0/en/ 29 | phpbb-translation-validator/4.0/de/ 30 | phpbb-translation-validator/4.0/fr/ 31 | phpbb-translation-validator/translation.php 32 | 33 | ## ⚗️ Validate language packs 34 | 35 | The simplest way to validate this language packages, 36 | is to open a command line tool in the validator directory. 37 | Then run this command (the final argument is the language you wish to test and that has already been stored to the `4.0` directory; e.g. `fr` for French): 38 | 39 | php translation.php validate fr 40 | 41 | There are more arguments that can be supplied. For example, suppose you wanted to have your `4.x` directory in a different location, you wanted to explicitly specify phpBB version 4.x (default validation is against 4.0), you wanted to run in safe mode and you wanted to see all notices displayed - you would run this command: 42 | 43 | php translation.php validate fr 44 | --package-dir=/path/to/your/4.0 45 | --phpbb-version=4.0 46 | --safe-mode 47 | --display-notices 48 | 49 | The `--safe-mode` flag indicates that you want to parse files instead of directly including them. 50 | This is useful if you want to run validations on a web server. 51 | 52 | If you are missing the English language files for the official Viglink extension, 53 | they can be easily donwloaded using this command: 54 | 55 | php translation.php download --files=phpbb-extensions/viglink --phpbb-version=4.0 56 | 57 | ## 🛠️ Integration to your Repository 58 | 59 | In your project you can add phpBB Translation Validator as a dependency: 60 | 61 | { 62 | "require-dev": { 63 | "phpbb/translation-validator": "2.0.*" 64 | } 65 | } 66 | 67 | Then add a `php vendor/bin/translation.php` call to your workflow. 68 | 69 | We use GitHub Actions as a continuous integration server and phpunit for our unit testing. 70 | 71 | ### 🏠 Local phpunit execution 72 | 73 | To run the unit tests locally, use this command: 74 | 75 | php vendor/phpunit/phpunit/phpunit tests/ 76 | 77 | ## 🤖 Tests 78 | 79 | ![GitHub Actions CI](https://github.com/phpbb/phpbb-translation-validator/actions/workflows/phpunit.yaml/badge.svg?branch=master) 80 | 81 | ## 🧑‍💻 Contributing 82 | 83 | If you notice any problems with this application, please raise an issue at the [Github-Repository](https://github.com/phpbb/phpbb-translation-validator/issues). 84 | 85 | To submit your own code contributions, please fork the project and submit a pull request at [Github-Repository](https://github.com/phpbb/phpbb-translation-validator/pulls). 86 | 87 | When a new version is released, the version number will be updated in `composer.json` and `translation.php`. A new tag will be created and the package will become available at [Packagist](https://packagist.org/packages/phpbb/translation-validator). 88 | 89 | ## 📜 License 90 | 91 | [GNU General Public License v2](license.txt) 92 | -------------------------------------------------------------------------------- /src/Phpbb/TranslationValidator/Command/DownloadCommand.php: -------------------------------------------------------------------------------- 1 | setName('download') 32 | ->setDescription('If you are missing important files, this tool can automatically download them for you.') 33 | ->addOption('files', null, InputOption::VALUE_REQUIRED, 'Which files do you want to download?', 'phpbb-extensions/viglink') 34 | ->addOption('phpbb-version', null, InputOption::VALUE_OPTIONAL, 'The phpBB version you use to validate against', '4.0'); 35 | } 36 | protected function execute(InputInterface $input, OutputInterface $output) 37 | { 38 | $files = $input->getOption('files'); 39 | $phpbbVersion = $input->getOption('phpbb-version'); 40 | 41 | if (!in_array($files, [self::VIGLINK_EXTENSION])) 42 | { 43 | throw new \RuntimeException($files . ' is not supported for automatic download.'); 44 | } 45 | 46 | $output = new Output($output, false); 47 | $output->setFormatter(new OutputFormatter($output->isDecorated())); 48 | 49 | $output->writeln('Downloading ' . $files); 50 | 51 | if ($files === self::VIGLINK_EXTENSION) 52 | { 53 | // Download Viglink files if they are missing 54 | $this->downloadViglinkExtensionLanguagesFiles($output, $phpbbVersion); 55 | } 56 | 57 | $output->writeln('Script complete.'); 58 | } 59 | 60 | /** 61 | * Download missing Viglink files and store them in phpbb-translation-validator/3.x/en/ext/phpbb/viglink/language/en 62 | * @param $output 63 | * @param $phpbbVersion 64 | */ 65 | private function downloadViglinkExtensionLanguagesFiles($output, $phpbbVersion) 66 | { 67 | $files = $this->readGitHubApiUrl(sprintf(self::GITHUB_API_URL, self::VIGLINK_EXTENSION)); 68 | 69 | // Create Viglink folder structure if it doesn't exist 70 | $directory = __DIR__ . '/../../../../' . $phpbbVersion . '/en/' . self::VIGLINK_PATH; 71 | 72 | if (!file_exists($directory)) 73 | { 74 | $output->writeln('Viglink directory does not exist, creating now at ' . $directory . self::GITHUB_LANGUAGE_EXTRACT); 75 | mkdir($directory . self::GITHUB_LANGUAGE_EXTRACT, 0777, true); 76 | } 77 | 78 | foreach ($files['tree'] as $file) 79 | { 80 | if (strpos($file['path'], self::GITHUB_LANGUAGE_EXTRACT) !== false) 81 | { 82 | $fileToCreate = $directory . '/' . $file['path']; 83 | 84 | if (!file_exists($fileToCreate)) 85 | { 86 | // This is a file we want 87 | $languageFile = $this->readGitHubApiUrl($file['url']); 88 | $languageFileContents = base64_decode($languageFile['content']); 89 | 90 | // Save the file 91 | $output->writeln('Creating missing file now at ' . $fileToCreate); 92 | file_put_contents($fileToCreate, $languageFileContents); 93 | } 94 | } 95 | } 96 | } 97 | 98 | /** 99 | * Return JSON from GitHub API 100 | * Must supply a user agent otherwise GitHub will reject the request 101 | * @param $file 102 | * @return mixed 103 | */ 104 | private function readGitHubApiUrl($file) 105 | { 106 | $context = stream_context_create([ 107 | 'http' => [ 108 | 'method' => 'GET', 109 | 'header' => [ 110 | 'User-Agent: phpbb-translation-validator' 111 | ] 112 | ] 113 | ]); 114 | 115 | // An unauthenticated request is fine as long is this isn't run over 60 times within an hour 116 | // More information at: https://docs.github.com/en/rest/guides/getting-started-with-the-rest-api 117 | $content = json_decode( 118 | file_get_contents($file, false, $context), 119 | true 120 | ); 121 | 122 | return $content; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/Phpbb/TranslationValidator/Command/ValidateCommand.php: -------------------------------------------------------------------------------- 1 | setName('validate') 26 | ->setDescription('Run the validator on your language pack.') 27 | ->addArgument('origin-iso', InputArgument::REQUIRED, 'The ISO of the language to validate') 28 | ->addOption('phpbb-version', null, InputOption::VALUE_OPTIONAL, 'The phpBB Version to validate against', '4.0') 29 | ->addOption('source-iso', null, InputOption::VALUE_OPTIONAL, 'The ISO of the language to validate against', 'en') 30 | ->addOption('package-dir', null, InputOption::VALUE_OPTIONAL, 'The path to the directory with the language packages', null) 31 | ->addOption('language-dir', null, InputOption::VALUE_OPTIONAL, 'The path to the directory with the language folders', null) 32 | ->addOption('debug', null, InputOption::VALUE_NONE, 'Run in debug') 33 | ->addOption('display-notices', null, InputOption::VALUE_NONE, 'Display notices in report') 34 | ->addOption('safe-mode', 's', InputOption::VALUE_NONE, 'Run in web safe mode to parse files instead of including them') 35 | ; 36 | } 37 | 38 | protected function execute(InputInterface $input, OutputInterface $output) 39 | { 40 | if (!defined('IN_PHPBB')) 41 | { 42 | // Need to set this, otherwise we can not load the language files 43 | define('IN_PHPBB', true); 44 | } 45 | 46 | $originIso = $input->getArgument('origin-iso'); 47 | $sourceIso = $input->getOption('source-iso'); 48 | $phpbbVersion = $input->getOption('phpbb-version'); 49 | $packageDir = $input->getOption('package-dir'); 50 | $languageDir = $input->getOption('language-dir'); 51 | $debug = $input->getOption('debug'); 52 | $displayNotices = $input->getOption('display-notices'); 53 | $safeMode = $input->getOption('safe-mode'); 54 | 55 | if ($phpbbVersion != '4.0') 56 | { 57 | throw new \RuntimeException('Invalid phpbb-version, allowed versions: 4.0'); 58 | } 59 | 60 | $output = new Output($output, $debug); 61 | $output->setFormatter(new OutputFormatter($output->isDecorated())); 62 | 63 | $output->writeln("Running Language Pack Validator on language $originIso."); 64 | 65 | // If it's safe mode, just put a note so the person running knows it is not as thorough as running it manually 66 | if ($safeMode) 67 | { 68 | $output->writeln('[Safe Mode] Running in web safe mode; it is recommended to still run the script manually for completeness.'); 69 | } 70 | 71 | $output->writeln(''); 72 | $runner = new ValidatorRunner($input, $output); 73 | $runner->setPhpbbVersion($phpbbVersion) 74 | ->setDebug($debug) 75 | ->setSafeMode($safeMode); 76 | 77 | if ($packageDir !== null) 78 | { 79 | $runner->setSource($sourceIso, $packageDir . '/' . $sourceIso, 'language/' . $sourceIso . '/') 80 | ->setOrigin($originIso, $packageDir . '/' . $originIso, 'language/' . $originIso . '/'); 81 | } 82 | else if ($languageDir !== null) 83 | { 84 | $runner->setSource($sourceIso, $languageDir . '/' . $sourceIso, '') 85 | ->setOrigin($originIso, $languageDir . '/' . $originIso, ''); 86 | } 87 | else 88 | { 89 | $runner->setSource($sourceIso, $phpbbVersion . '/' . $sourceIso, 'language/' . $sourceIso . '/') 90 | ->setOrigin($originIso, $phpbbVersion . '/' . $originIso, 'language/' . $originIso . '/'); 91 | } 92 | 93 | $output->writelnIfDebug("Setup ValidatorRunner"); 94 | 95 | $runner->runValidators(); 96 | $output->writeln(''); 97 | $output->writeln("Test results for language pack:"); 98 | $output->writeln(''); 99 | 100 | $found_msg = ''; 101 | $found_msg .= 'Fatal: ' . $output->getMessageCount(Output::FATAL); 102 | $found_msg .= ', Error: ' . $output->getMessageCount(Output::ERROR); 103 | $found_msg .= ', Warning: ' . $output->getMessageCount(Output::WARNING); 104 | $found_msg .= ', Notice: ' . $output->getMessageCount(Output::NOTICE); 105 | 106 | if ($output->getMessageCount(Output::FATAL)) 107 | { 108 | $output->writeln('' . str_repeat(' ', strlen($found_msg)) . ''); 109 | $output->writeln('Validation: FAILED' . str_repeat(' ', strlen($found_msg) - 18) . ''); 110 | $output->writeln('' . $found_msg . ''); 111 | $output->writeln(''); 112 | $output->writeln(''); 113 | } 114 | else 115 | { 116 | $output->writeln('PASSED: ' . $found_msg . ''); 117 | } 118 | 119 | foreach ($output->getMessages() as $msg) 120 | { 121 | /** @var \Phpbb\TranslationValidator\Output\Message $msg */ 122 | if ($msg->getType() === Output::NOTICE && !$debug && !$displayNotices) 123 | { 124 | continue; 125 | } 126 | 127 | $output->writeln((string) $msg); 128 | $output->writeln(''); 129 | } 130 | $output->writeln(''); 131 | 132 | if ($output->getMessageCount(Output::FATAL)) 133 | { 134 | $output->writeln('' . str_repeat(' ', strlen($found_msg)) . ''); 135 | $output->writeln('Validation: FAILED' . str_repeat(' ', strlen($found_msg) - 18) . ''); 136 | $output->writeln('' . $found_msg . ''); 137 | } 138 | else 139 | { 140 | $output->writeln('PASSED: ' . $found_msg . ''); 141 | } 142 | 143 | 144 | return ($output->getMessageCount(Output::FATAL) > 0) ? 1 : 0; 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/Phpbb/TranslationValidator/Output/Output.php: -------------------------------------------------------------------------------- 1 | output = $output; 37 | $this->debug = $debug; 38 | } 39 | 40 | /** 41 | * Writes a message to the output. 42 | * 43 | * @param string|array $messages The message as an array of lines or a single string 44 | * @param Boolean $newline Whether to add a newline 45 | * @param integer $type The type of output (one of the OUTPUT constants) 46 | * 47 | * @throws \InvalidArgumentException When unknown output type is given 48 | * 49 | * @api 50 | */ 51 | public function write($messages, $newline = false, $type = self::OUTPUT_NORMAL) 52 | { 53 | return $this->output->write($messages, $newline, $type); 54 | } 55 | 56 | /** 57 | * Writes a message to the output and adds a newline at the end. 58 | * 59 | * @param string|array $messages The message as an array of lines of a single string 60 | * @param integer $type The type of output (one of the OUTPUT constants) 61 | * 62 | * @throws \InvalidArgumentException When unknown output type is given 63 | * 64 | * @api 65 | */ 66 | public function writeln($messages, $type = self::OUTPUT_NORMAL) 67 | { 68 | return $this->output->writeln($messages, $type); 69 | } 70 | 71 | /** 72 | * Sets the verbosity of the output. 73 | * 74 | * @param integer $level The level of verbosity (one of the VERBOSITY constants) 75 | * 76 | * @api 77 | */ 78 | public function setVerbosity($level) 79 | { 80 | return $this->output->setVerbosity($level); 81 | } 82 | 83 | /** 84 | * Gets the current verbosity of the output. 85 | * 86 | * @return integer The current level of verbosity (one of the VERBOSITY constants) 87 | * 88 | * @api 89 | */ 90 | public function getVerbosity(): int 91 | { 92 | return $this->output->getVerbosity(); 93 | } 94 | 95 | /** 96 | * Sets the decorated flag. 97 | * 98 | * @param Boolean $decorated Whether to decorate the messages 99 | * 100 | * @api 101 | */ 102 | public function setDecorated($decorated) 103 | { 104 | return $this->output->setDecorated($decorated); 105 | } 106 | 107 | /** 108 | * Gets the decorated flag. 109 | * 110 | * @return Boolean true if the output will decorate messages, false otherwise 111 | * 112 | * @api 113 | */ 114 | public function isDecorated(): bool 115 | { 116 | return $this->output->isDecorated(); 117 | } 118 | 119 | /** 120 | * Sets output formatter. 121 | * 122 | * @param OutputFormatterInterface $formatter 123 | * 124 | * @api 125 | */ 126 | public function setFormatter(OutputFormatterInterface $formatter) 127 | { 128 | return $this->output->setFormatter($formatter); 129 | } 130 | 131 | /** 132 | * Returns current output formatter instance. 133 | * 134 | * @return OutputFormatterInterface 135 | * 136 | * @api 137 | */ 138 | public function getFormatter(): \Symfony\Component\Console\Formatter\OutputFormatterInterface 139 | { 140 | return $this->output->getFormatter(); 141 | } 142 | 143 | /** 144 | * Write a message to the output, but only if Debug is enabled. 145 | * 146 | * @param $message string|array $messages The message as an array of lines of a single string 147 | * 148 | * @throws \InvalidArgumentException When unknown output type is given 149 | */ 150 | public function writelnIfDebug($message) 151 | { 152 | if ($this->debug) 153 | { 154 | $this->writeln($message); 155 | } 156 | } 157 | 158 | /** 159 | * Add a new message to the output of the validator. 160 | * 161 | * @param int $type Type message 162 | * @param string $message Message 163 | * @param string $file 164 | * @param string $file_details 165 | */ 166 | public function addMessage($type, $message, $file = null, $file_details = null) 167 | { 168 | switch ($type) 169 | { 170 | case Output::FATAL: 171 | $this->fatals[] = new Message($type, $message, $file, $file_details); 172 | break; 173 | case Output::ERROR: 174 | $this->errors[] = new Message($type, $message, $file, $file_details); 175 | break; 176 | case Output::WARNING: 177 | $this->warnings[] = new Message($type, $message, $file, $file_details); 178 | break; 179 | case Output::NOTICE: 180 | $this->notices[] = new Message($type, $message, $file, $file_details); 181 | break; 182 | default: 183 | // TODO: Decide on this? 184 | } 185 | } 186 | 187 | /** 188 | * Get all messages saved into the message queue. 189 | * @return array Array with messages 190 | */ 191 | public function getMessages() 192 | { 193 | return array_merge($this->fatals, $this->errors, $this->warnings, $this->notices); 194 | } 195 | 196 | /** 197 | * Get the amount of messages that were fatal. 198 | * @param int $type 199 | * @return int 200 | */ 201 | public function getMessageCount($type) 202 | { 203 | switch ($type) 204 | { 205 | case Output::FATAL: 206 | return sizeof($this->fatals); 207 | case Output::ERROR: 208 | return sizeof($this->errors); 209 | case Output::WARNING: 210 | return sizeof($this->warnings); 211 | case Output::NOTICE: 212 | return sizeof($this->notices); 213 | } 214 | return 0; 215 | } 216 | 217 | /** 218 | * Returns whether verbosity is quiet (-q). 219 | * 220 | * @return bool true if verbosity is set to VERBOSITY_QUIET, false otherwise 221 | */ 222 | public function isQuiet(): bool 223 | { 224 | // TODO: Implement isQuiet() method. 225 | } 226 | 227 | /** 228 | * Returns whether verbosity is verbose (-v). 229 | * 230 | * @return bool true if verbosity is set to VERBOSITY_VERBOSE, false otherwise 231 | */ 232 | public function isVerbose(): bool 233 | { 234 | // TODO: Implement isVerbose() method. 235 | } 236 | 237 | /** 238 | * Returns whether verbosity is very verbose (-vv). 239 | * 240 | * @return bool true if verbosity is set to VERBOSITY_VERY_VERBOSE, false otherwise 241 | */ 242 | public function isVeryVerbose(): bool 243 | { 244 | // TODO: Implement isVeryVerbose() method. 245 | } 246 | 247 | /** 248 | * Returns whether verbosity is debug (-vvv). 249 | * 250 | * @return bool true if verbosity is set to VERBOSITY_DEBUG, false otherwise 251 | */ 252 | public function isDebug(): bool 253 | { 254 | // TODO: Implement isDebug() method. 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /src/Phpbb/TranslationValidator/Validator/FileListValidator.php: -------------------------------------------------------------------------------- 1 | input = $input; 53 | $this->output = $output; 54 | } 55 | 56 | /** 57 | * Set phpBB Version 58 | * 59 | * @param string $originIso The ISO of the language to validate 60 | * @param string $originPath Path to the origin directory 61 | * @param string $originLanguagePath Relative path to the origin language/ directory 62 | * @return $this 63 | */ 64 | public function setOrigin($originIso, $originPath, $originLanguagePath) 65 | { 66 | $this->originIso = $originIso; 67 | $this->originPath = $originPath; 68 | $this->originLanguagePath = $originLanguagePath; 69 | return $this; 70 | } 71 | 72 | /** 73 | * Set phpBB Version 74 | * 75 | * @param string $sourceIso The ISO of the language to validate against 76 | * @param string $sourcePath Path to the source directory 77 | * @param string $sourceLanguagePath Relative path to the source language/ directory 78 | * @return $this 79 | */ 80 | public function setSource($sourceIso, $sourcePath, $sourceLanguagePath) 81 | { 82 | $this->sourceIso = $sourceIso; 83 | $this->sourcePath = $sourcePath; 84 | $this->sourceLanguagePath = $sourceLanguagePath; 85 | return $this; 86 | } 87 | 88 | /** 89 | * Set phpBB Version 90 | * 91 | * @param string $phpbbVersion The phpBB Version to validate against 92 | * @return $this 93 | */ 94 | public function setPhpbbVersion($phpbbVersion) 95 | { 96 | $this->phpbbVersion = $phpbbVersion; 97 | return $this; 98 | } 99 | 100 | /** 101 | * Set debug mode 102 | * 103 | * @param bool $debug Debug mode 104 | * @return $this 105 | */ 106 | public function setDebug($debug) 107 | { 108 | $this->debug = $debug; 109 | return $this; 110 | } 111 | 112 | /** 113 | * Set safe mode 114 | * 115 | * @param $safeMode 116 | * @return $this 117 | */ 118 | public function setSafeMode($safeMode) 119 | { 120 | $this->safeMode = $safeMode; 121 | return $this; 122 | } 123 | 124 | /** 125 | * Validates the directories 126 | * 127 | * Directories should not miss any files. 128 | * Directories must not contain additional php files. 129 | * Directories should not contain additional files. 130 | * 131 | * @return array List of files we can continue to validate. 132 | */ 133 | public function validate() 134 | { 135 | $sourceFiles = $this->getFileList($this->sourcePath); 136 | // License file is required but missing from en/, so we add it here 137 | $sourceFiles[] = $this->sourceLanguagePath . 'LICENSE'; 138 | $sourceFiles = array_unique($sourceFiles); 139 | 140 | // Get extra->direction from composer.json to allow additional rtl-files for rtl-translations 141 | $filePath = $this->originPath . '/' . $this->originLanguagePath . 'composer.json'; 142 | 143 | // Safe mode for safe execution on a server 144 | if ($this->safeMode) 145 | { 146 | $jsonContent = self::langParser($filePath); 147 | } 148 | else 149 | { 150 | $fileContents = (string) file_get_contents($filePath); 151 | $jsonContent = json_decode($fileContents, true); 152 | } 153 | 154 | $this->direction = $jsonContent['extra']['direction']; 155 | // Throw error, if invalid direction is used 156 | if (!in_array($this->direction, array('rtl', 'ltr'))) 157 | { 158 | $this->output->addMessage(Output::FATAL, 'DIRECTION needs to be rtl or ltr'); 159 | } 160 | 161 | $originFiles = $this->getFileList($this->originPath); 162 | 163 | $validFiles = array(); 164 | foreach ($sourceFiles as $sourceFile) 165 | { 166 | $testOriginFile = str_replace('/' . $this->sourceIso . '/', '/' . $this->originIso . '/', $sourceFile); 167 | if (!in_array($testOriginFile, $originFiles)) 168 | { 169 | $this->output->addMessage(Output::FATAL, 'Missing required file', $testOriginFile); 170 | } 171 | else if (substr($sourceFile, -4) != '.gif' && substr($sourceFile, -12) != 'imageset.cfg') 172 | { 173 | $validFiles[$sourceFile] = $testOriginFile; 174 | } 175 | } 176 | 177 | foreach ($originFiles as $origin_file) 178 | { 179 | $testSourceFile = str_replace('/' . $this->originIso . '/', '/' . $this->sourceIso . '/', $origin_file); 180 | if (!in_array($testSourceFile, $sourceFiles)) 181 | { 182 | if (in_array($origin_file, array( 183 | $this->originLanguagePath . 'AUTHORS.md', 184 | $this->originLanguagePath . 'CHANGELOG.md', 185 | $this->originLanguagePath . 'README.md', 186 | $this->originLanguagePath . 'VERSION.md', 187 | ))) 188 | { 189 | $this->output->addMessage(Output::NOTICE, 'Found additional file', $origin_file); 190 | } 191 | else 192 | { 193 | if (substr($origin_file, -10) === '/index.htm' || $origin_file === 'index.htm') 194 | { 195 | $level = Output::NOTICE; 196 | } 197 | else if (in_array(substr($origin_file, -4), array('.gif', '.png')) && $this->direction === 'rtl') 198 | { 199 | $level = Output::WARNING; 200 | } 201 | else if (substr($origin_file, -3) !== '.md') 202 | { 203 | $level = Output::FATAL; 204 | } 205 | else 206 | { 207 | $level = Output::ERROR; 208 | } 209 | 210 | // Treat some official extensions differently 211 | if (strpos($origin_file, DownloadCommand::VIGLINK_PATH) !== false) 212 | { 213 | $this->output->addMessage($level, 'No source file for the official extension found - to download, run: php translation.php download --files=phpbb-extensions/viglink', $origin_file); 214 | } 215 | 216 | else 217 | { 218 | $this->output->addMessage($level, 'Found additional file', $origin_file); 219 | } 220 | } 221 | 222 | if (substr($origin_file, -14) === '/site_logo.gif') 223 | { 224 | $this->output->addMessage(Output::FATAL, 'Found additional file', $origin_file); 225 | } 226 | } 227 | } 228 | 229 | return $validFiles; 230 | } 231 | 232 | /** 233 | * Returns a list of files in the directory 234 | * 235 | * @param string $dir Directory to go through 236 | * @return array List of files 237 | */ 238 | protected function getFileList($dir) 239 | { 240 | $finder = new Finder(); 241 | $iterator = $finder 242 | ->ignoreDotFiles(false) 243 | ->files() 244 | ->sortByName() 245 | ->in($dir) 246 | ; 247 | 248 | $files = array(); 249 | foreach ($iterator as $file) 250 | { 251 | /** @var \SplFileInfo $file */ 252 | $files[] = str_replace(DIRECTORY_SEPARATOR, '/', substr($file->getPathname(), strlen($dir) + 1)); 253 | } 254 | 255 | return $files; 256 | } 257 | 258 | /** 259 | * Get the language direction - we want to be more lenient for rtl languages 260 | * @return string 261 | */ 262 | public function getDirection() 263 | { 264 | return $this->direction; 265 | } 266 | } 267 | -------------------------------------------------------------------------------- /src/Phpbb/TranslationValidator/Validator/ValidatorRunner.php: -------------------------------------------------------------------------------- 1 | input = $input; 65 | $this->output = $output; 66 | } 67 | 68 | /** 69 | * Set phpBB Version 70 | * 71 | * @param string $originIso The ISO of the language to validate 72 | * @param string $originPath Path to the origin directory 73 | * @param string $originLanguagePath Relative path to the origin language/ directory 74 | * @return $this 75 | */ 76 | public function setOrigin($originIso, $originPath, $originLanguagePath) 77 | { 78 | $this->originIso = $originIso; 79 | $this->originPath = $originPath; 80 | $this->originLanguagePath = $originLanguagePath; 81 | return $this; 82 | } 83 | 84 | /** 85 | * Set phpBB Version 86 | * 87 | * @param string $sourceIso The ISO of the language to validate against 88 | * @param string $sourcePath Path to the source directory 89 | * @param string $sourceLanguagePath Relative path to the source language/ directory 90 | * @return $this 91 | */ 92 | public function setSource($sourceIso, $sourcePath, $sourceLanguagePath) 93 | { 94 | $this->sourceIso = $sourceIso; 95 | $this->sourcePath = $sourcePath; 96 | $this->sourceLanguagePath = $sourceLanguagePath; 97 | return $this; 98 | } 99 | 100 | /** 101 | * Set safe mode (if true, don't include any PHP files) 102 | * 103 | * @param $safeMode 104 | * @return $this 105 | */ 106 | public function setSafeMode($safeMode) 107 | { 108 | $this->safeMode = $safeMode; 109 | return $this; 110 | } 111 | 112 | /** 113 | * Set phpBB Version 114 | * 115 | * @param string $phpbbVersion The phpBB Version to validate against 116 | * @return $this 117 | */ 118 | public function setPhpbbVersion($phpbbVersion) 119 | { 120 | $this->phpbbVersion = $phpbbVersion; 121 | return $this; 122 | } 123 | 124 | /** 125 | * Set debug mode 126 | * 127 | * @param bool $debug Debug mode 128 | * @return $this 129 | */ 130 | public function setDebug($debug) 131 | { 132 | $this->debug = $debug; 133 | return $this; 134 | } 135 | 136 | /** 137 | * Run the actual test suite. 138 | */ 139 | public function runValidators() 140 | { 141 | $fileListValidator = new FileListValidator($this->input, $this->output); 142 | 143 | $validateFiles = $fileListValidator->setSource($this->sourceIso, $this->sourcePath, $this->sourceLanguagePath) 144 | ->setOrigin($this->originIso, $this->originPath, $this->originLanguagePath) 145 | ->setPhpbbVersion($this->phpbbVersion) 146 | ->setDebug($this->debug) 147 | ->setSafeMode($this->safeMode) 148 | ->validate(); 149 | 150 | if (empty($validateFiles)) 151 | { 152 | $this->output->writelnIfDebug(''); 153 | $this->output->writelnIfDebug("No files found for validation."); 154 | return; 155 | } 156 | 157 | $pluralRule = $this->guessPluralRule(); 158 | $this->output->writelnIfDebug("Using plural rule #$pluralRule for validation."); 159 | $this->output->writelnIfDebug(''); 160 | 161 | $this->output->writelnIfDebug("Validating file list:"); 162 | $this->printErrorLevel($this->output); 163 | 164 | $this->maxProgress = sizeof($validateFiles) + 1; 165 | $this->progressLength = 11 + strlen($this->maxProgress) * 2; 166 | 167 | $fileValidator = new FileValidator($this->input, $this->output); 168 | $fileValidator->setSource($this->sourceIso, $this->sourcePath, $this->sourceLanguagePath) 169 | ->setOrigin($this->originIso, $this->originPath, $this->originLanguagePath) 170 | ->setDirection($fileListValidator->getDirection()) 171 | ->setPhpbbVersion($this->phpbbVersion) 172 | ->setPluralRule($pluralRule) 173 | ->setDebug($this->debug) 174 | ->setSafeMode($this->safeMode); 175 | 176 | foreach ($validateFiles as $sourceFile => $originFile) 177 | { 178 | $this->output->writelnIfDebug(''); 179 | $this->output->writelnIfDebug("Validating file: $originFile"); 180 | $fileValidator->validate($sourceFile, $originFile); 181 | $this->printErrorLevel($this->output); 182 | 183 | usleep(31250);//125000); 184 | } 185 | 186 | $this->output->writeln('.'); 187 | } 188 | 189 | protected function printErrorLevel(Output $output) 190 | { 191 | $fatals = $output->getMessageCount(Output::FATAL); 192 | $errors = $output->getMessageCount(Output::ERROR); 193 | $warnings = $output->getMessageCount(Output::WARNING); 194 | $notices = $output->getMessageCount(Output::NOTICE); 195 | if ($fatals > $this->numFatal) 196 | { 197 | $this->output->write("F"); 198 | } 199 | else if ($errors > $this->numError) 200 | { 201 | $this->output->write("E"); 202 | } 203 | else if ($warnings > $this->numWarning) 204 | { 205 | $this->output->write("W"); 206 | } 207 | else if ($notices > $this->numNotice) 208 | { 209 | $this->output->write("N"); 210 | } 211 | else 212 | { 213 | $this->output->write("."); 214 | } 215 | $this->progress++; 216 | 217 | if ($this->progress % (79 - $this->progressLength) == 0) 218 | { 219 | $this->output->write(' ' . sprintf('%' . strlen($this->maxProgress) . 's', $this->progress)); 220 | $this->output->write(' / ' . $this->maxProgress); 221 | $this->output->writeln(' (' . sprintf('%3s', floor(100 * ($this->progress / $this->maxProgress))) . '%)'); 222 | } 223 | 224 | $this->numFatal = $fatals; 225 | $this->numError = $errors; 226 | $this->numWarning = $warnings; 227 | $this->numNotice = $notices; 228 | } 229 | 230 | /** 231 | * Try to find the plural rule for the language in composer.json 232 | * @return int 233 | */ 234 | protected function guessPluralRule(): int 235 | { 236 | $filePath = $this->originPath . '/' . $this->originLanguagePath . 'composer.json'; 237 | 238 | if (file_exists($filePath)) 239 | { 240 | // Safe mode for safe execution on a server 241 | if ($this->safeMode) 242 | { 243 | $jsonContent = self::langParser($filePath); 244 | } 245 | else 246 | { 247 | $fileContents = (string) file_get_contents($filePath); 248 | $jsonContent = json_decode($fileContents, true); 249 | } 250 | 251 | if (!isset($jsonContent['extra']['plural-rule'])) 252 | { 253 | $this->output->writelnIfDebug("No plural rule set, falling back to plural rule #1"); 254 | } 255 | } 256 | else 257 | { 258 | $this->output->writelnIfDebug("Could not find composer.json, falling back to plural rule #1"); 259 | } 260 | 261 | return $jsonContent['extra']['plural-rule'] ?? 1; 262 | } 263 | 264 | /** 265 | * Parse language files for lang arrays 266 | * @param $file 267 | * @return array|null 268 | */ 269 | public static function arrayParser($file) 270 | { 271 | // Parse language files that use new or old array formats 272 | $regex = '/\$lang\s*=\s*array_merge\s*\(\$lang,\s*(?|array\s*\((.*?)\)|\[(.*?)\])\);/s'; 273 | return parser::parse_regex($regex, $file); 274 | } 275 | 276 | /** 277 | * Merge parsed language entries into a single array 278 | * @param $filePath 279 | * @param string $relativePath 280 | * @return array 281 | */ 282 | public static function langParser($filePath, string $relativePath = '') 283 | { 284 | $lang = []; 285 | $parsed = self::arrayParser($relativePath . $filePath); 286 | 287 | foreach ($parsed as $parse) 288 | { 289 | $lang = array_merge($lang, $parse); 290 | } 291 | 292 | return $lang; 293 | } 294 | } 295 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc. 5 | 675 Mass Ave, Cambridge, MA 02139, USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Library General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | -------------------------------------------------------------------------------- /tests/FileValidator/fixtures/origin/license/valid_gnu_gplv2.txt: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc. 5 | 675 Mass Ave, Cambridge, MA 02139, USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Library General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | -------------------------------------------------------------------------------- /src/Phpbb/TranslationValidator/Validator/LangKeyValidator.php: -------------------------------------------------------------------------------- 1 | key -> html-tag 51 | * 52 | * @var array 53 | */ 54 | protected $additionalHtmlFound = array(); 55 | 56 | /** 57 | * @param InputInterface $input 58 | * @param OutputInterface $output 59 | */ 60 | public function __construct(InputInterface $input, OutputInterface $output) 61 | { 62 | $this->input = $input; 63 | $this->output = $output; 64 | } 65 | 66 | /** 67 | * Set the language direction 68 | * @param $direction 69 | * @return $this 70 | */ 71 | public function setDirection($direction) 72 | { 73 | $this->direction = $direction; 74 | return $this; 75 | } 76 | 77 | /** 78 | * Set phpBB Version 79 | * 80 | * @param string $originIso The ISO of the language to validate 81 | * @param string $originPath Path to the origin directory 82 | * @param string $originLanguagePath Relative path to the origin language/ directory 83 | * @return $this 84 | */ 85 | public function setOrigin($originIso, $originPath, $originLanguagePath) 86 | { 87 | $this->originIso = $originIso; 88 | $this->originPath = $originPath; 89 | $this->originLanguagePath = $originLanguagePath; 90 | return $this; 91 | } 92 | 93 | /** 94 | * Set phpBB Version 95 | * 96 | * @param string $sourceIso The ISO of the language to validate against 97 | * @param string $sourcePath Path to the source directory 98 | * @param string $sourceLanguagePath Relative path to the source language/ directory 99 | * @return $this 100 | */ 101 | public function setSource($sourceIso, $sourcePath, $sourceLanguagePath) 102 | { 103 | $this->sourceIso = $sourceIso; 104 | $this->sourcePath = $sourcePath; 105 | $this->sourceLanguagePath = $sourceLanguagePath; 106 | return $this; 107 | } 108 | 109 | /** 110 | * Set phpBB Version 111 | * 112 | * @param string $phpbbVersion The phpBB Version to validate against 113 | * @return $this 114 | */ 115 | public function setPhpbbVersion($phpbbVersion) 116 | { 117 | $this->phpbbVersion = $phpbbVersion; 118 | return $this; 119 | } 120 | 121 | /** 122 | * Set plural rule 123 | * 124 | * @param int $pluralRule 125 | * @return $this 126 | */ 127 | public function setPluralRule($pluralRule) 128 | { 129 | $this->pluralRule = $pluralRule; 130 | return $this; 131 | } 132 | 133 | /** 134 | * Set debug mode 135 | * 136 | * @param bool $debug Debug mode 137 | * @return $this 138 | */ 139 | public function setDebug($debug) 140 | { 141 | $this->debug = $debug; 142 | return $this; 143 | } 144 | 145 | /** 146 | * Validates type of the language and decides on further validation 147 | * 148 | * @param string $file File to validate 149 | * @param string $key Key to validate 150 | * @param mixed $against_language Original language 151 | * @param mixed $validate_language Translated language 152 | * @return null 153 | */ 154 | public function validate($file, $key, $against_language, $validate_language) 155 | { 156 | if (gettype($against_language) !== gettype($validate_language)) 157 | { 158 | $this->output->addMessage(Output::FATAL, sprintf('Key should be type %s but is type %s', gettype($against_language), gettype($validate_language)), $file, $key); 159 | return; 160 | } 161 | 162 | if ($key === 'PLURAL_RULE') 163 | { 164 | if ($validate_language < 0 || $validate_language > 15) 165 | { 166 | $this->output->addMessage(Output::FATAL, sprintf('The plural rule %d, which you are trying to use, does not exist. For more information see https://wiki.phpbb.com/Plural_Rules.', $validate_language), $file, $key); 167 | return; 168 | } 169 | } 170 | else if ($key === 'DIRECTION') 171 | { 172 | if (!in_array($validate_language, array('ltr', 'rtl'))) 173 | { 174 | $this->output->addMessage(Output::FATAL, sprintf('The text direction %s, which you are trying to use, does not exist. Currently only ltr (left-to-right) and rtl (right-to-left) are allowed.', $validate_language), $file, $key); 175 | return; 176 | } 177 | } 178 | else if ($key === 'USER_LANG') 179 | { 180 | if (str_replace('_', '-', $this->originIso) !== $validate_language && strpos($validate_language, $this->originIso . '-') !== 0) 181 | { 182 | $this->output->addMessage(Output::FATAL, sprintf('The user language %s, which you are trying to use, does not match the language.', $validate_language), $file, $key); 183 | return; 184 | } 185 | } 186 | else if (gettype($against_language) === 'string') 187 | { 188 | $this->validateString($file, $key, $against_language, $validate_language); 189 | } 190 | else 191 | { 192 | $this->validateArray($file, $key, $against_language, $validate_language); 193 | } 194 | } 195 | 196 | /** 197 | * Decides which array validation function should be used, based on the key 198 | * 199 | * Supports: 200 | * - Dateformats 201 | * - Datetime 202 | * - Timezones 203 | * - BBCode Tokens 204 | * - Report Reasons 205 | * - Plurals 206 | * 207 | * @param string $file File to validate 208 | * @param string $key Key to validate 209 | * @param array $against_language Original language 210 | * @param array $validate_language Translated language 211 | * @return null 212 | */ 213 | public function validateArray($file, $key, $against_language, $validate_language) 214 | { 215 | //var_dump($key, $against_language); echo '

'; 216 | if ($key === 'dateformats') 217 | { 218 | $this->validateDateformats($file, $key, $validate_language); 219 | } 220 | else if ( 221 | $key === 'datetime' || 222 | $key === 'timezones' || 223 | $key === 'tokens' || 224 | $key === 'report_reasons' || 225 | $key === 'PM_ACTION' || 226 | $key === 'PM_CHECK' || 227 | $key === 'PM_RULE' 228 | ) 229 | { 230 | $this->validateArrayKey($file, $key, $against_language, $validate_language); 231 | } 232 | else 233 | { 234 | $against_keys = array_keys($against_language); 235 | $key_types = array(); 236 | foreach ($against_keys as $against_key) 237 | { 238 | $type = gettype($against_key); 239 | if (!isset($key_types[$type])) 240 | { 241 | $key_types[$type] = 0; 242 | } 243 | $key_types[$type]++; 244 | } 245 | 246 | if (sizeof($key_types) == 1) 247 | { 248 | if (isset($key_types['string'])) 249 | { 250 | $this->validateArrayKey($file, $key, $against_language, $validate_language); 251 | } 252 | else if (isset($key_types['integer'])) 253 | { 254 | $this->validatePluralKeys($file, $key, $against_language, $validate_language); 255 | } 256 | else 257 | { 258 | $this->output->addMessage(Output::NOTICE, 'Array has mixed types: ' . implode(', ', array_keys($key_types)), $file, $key); 259 | } 260 | } 261 | else 262 | { 263 | $this->output->addMessage(Output::NOTICE, 'Array has mixed types: ' . implode(', ', array_keys($key_types)), $file, $key); 264 | } 265 | } 266 | } 267 | 268 | /** 269 | * Validates the plural keys 270 | * 271 | * The set of plural cases should not be empty 272 | * There might be an additional case for 0 items 273 | * There must not be an additional case 274 | * There might be less cases then possible 275 | * 276 | * @param string $file File to validate 277 | * @param string $key Key to validate 278 | * @param array $against_language Original language 279 | * @param array $validate_language Translated language 280 | * @return null 281 | * @throws \Exception 282 | */ 283 | public function validatePluralKeys($file, $key, $against_language, $validate_language) 284 | { 285 | $origin_cases = array_keys($validate_language); 286 | 287 | if (empty($origin_cases)) 288 | { 289 | $this->output->addMessage(Output::FATAL, 'Plural array must not be empty', $file, $key); 290 | return; 291 | } 292 | 293 | $valid_cases = $this->getPluralKeys($this->pluralRule); 294 | 295 | $intersect_cases = array_intersect($origin_cases, $valid_cases); 296 | $missing_cases = array_diff($valid_cases, $origin_cases); 297 | $additional_cases = array_diff($origin_cases, $valid_cases, array(0)); 298 | 299 | if (!empty($additional_cases)) 300 | { 301 | $this->output->addMessage(Output::FATAL, 'Plural array has additional case: ' . implode(', ', $additional_cases), $file, $key); 302 | } 303 | 304 | if (empty($intersect_cases)) 305 | { 306 | // No intersection means there are no entries apart from the 0 307 | $this->output->addMessage(Output::FATAL, 'Plural array must not be empty', $file, $key); 308 | return; 309 | } 310 | 311 | if (!empty($missing_cases)) 312 | { 313 | // Do we want to allow this? Lazy translators... 314 | $this->output->addMessage(Output::WARNING, 'Plural array is missing case: ' . implode(', ', $missing_cases), $file, $key); 315 | } 316 | 317 | if (!empty($intersect_cases)) 318 | { 319 | $compare_against = ''; 320 | if ($against_language) 321 | { 322 | $compare_against = end($against_language); 323 | } 324 | 325 | foreach ($intersect_cases as $case) 326 | { 327 | $this->validateString($file, $key . '.' . $case, $compare_against, $validate_language[$case], true); 328 | } 329 | } 330 | } 331 | 332 | /** 333 | * Returns an array with the valid cases for the given plural rule 334 | * 335 | * @param int $pluralRule 336 | * @return array 337 | * @throws \Exception 338 | */ 339 | protected function getPluralKeys($pluralRule) 340 | { 341 | switch ($pluralRule) 342 | { 343 | case 0: 344 | return array(1); 345 | case 1: 346 | case 2: 347 | case 15: 348 | return array(1, 2); 349 | case 3: 350 | case 5: 351 | case 6: 352 | case 7: 353 | case 8: 354 | case 9: 355 | case 14: 356 | return array(1, 2, 3); 357 | case 4: 358 | case 10: 359 | case 13: 360 | return array(1, 2, 3, 4); 361 | case 11: 362 | return array(1, 2, 3, 4, 5); 363 | case 12: 364 | return array(1, 2, 3, 4, 5, 6); 365 | } 366 | 367 | throw new \Exception('Unsupported plural rule'); 368 | } 369 | 370 | /** 371 | * Validates the dateformats 372 | * 373 | * Should not be empty 374 | * Keys and Descriptions should not contain HTML 375 | * 376 | * @param string $file File to validate 377 | * @param string $key Key to validate 378 | * @param array $validate_language Translated language 379 | * @return null 380 | */ 381 | public function validateDateformats($file, $key, $validate_language) 382 | { 383 | if (empty($validate_language)) 384 | { 385 | $this->output->addMessage(Output::FATAL, 'Array must not be empty', $file, $key); 386 | return; 387 | } 388 | 389 | foreach ($validate_language as $dateformat => $example_time) 390 | { 391 | $this->validateString($file, $key . '.' . $dateformat, '', $dateformat); 392 | $this->validateString($file, $key . '.' . $dateformat, '', $example_time); 393 | } 394 | } 395 | 396 | /** 397 | * Validates a permission entry 398 | * 399 | * Should have a cat and lang key 400 | * cat should be the same as in origin language 401 | * lang should compare like a string to origin lang 402 | * 403 | * @param string $file File to validate 404 | * @param string $key Key to validate 405 | * @param array $against_language Original language 406 | * @param array $validate_language Translated language 407 | * @return null 408 | */ 409 | public function validateAcl($file, $key, $against_language, $validate_language) 410 | { 411 | if (!isset($validate_language['cat'])) 412 | { 413 | $this->output->addMessage(Output::FATAL, 'Permission is missing the cat-key', $file, $key); 414 | } 415 | else if ($validate_language['cat'] !== $against_language['cat']) 416 | { 417 | $this->output->addMessage(Output::FATAL, sprintf('Permission should have cat %1$s but has %2$s', $against_language['cat'], $validate_language['cat']), $file, $key); 418 | } 419 | 420 | if (!isset($validate_language['lang'])) 421 | { 422 | $this->output->addMessage(Output::FATAL, 'Permission is missing the lang-key', $file, $key); 423 | } 424 | else 425 | { 426 | $this->validateString($file, $key, $against_language['lang'], $validate_language['lang']); 427 | } 428 | } 429 | 430 | /** 431 | * Validates an array entry 432 | * 433 | * Arrays that have strings as key, must have the same keys in the foreign language 434 | * Arrays that have integers as keys, might have different ones (plurals) 435 | * Additional keys can not be further validated 436 | * 437 | * Function works recursive 438 | * 439 | * @param string $file File to validate 440 | * @param string $key Key to validate 441 | * @param array $against_language Original language 442 | * @param array $validate_language Translated language 443 | * @return null 444 | */ 445 | public function validateArrayKey($file, $key, $against_language, $validate_language) 446 | { 447 | if (gettype($against_language) !== gettype($validate_language)) 448 | { 449 | $this->output->addMessage(Output::FATAL, sprintf('Should be type %1$s but is type %2$s', gettype($against_language), gettype($validate_language)), $file, $key); 450 | return; 451 | } 452 | 453 | $cat_validate_keys = array_keys($validate_language); 454 | $cat_against_keys = array_keys($against_language); 455 | $invalid_keys = array_diff($cat_validate_keys, $cat_against_keys); 456 | 457 | foreach ($against_language as $array_key => $lang) 458 | { 459 | // Only error for string keys, plurals might force different keys 460 | if (!isset($validate_language[$array_key])) 461 | { 462 | if (gettype($array_key) == 'string') 463 | { 464 | $this->output->addMessage(Output::FATAL, 'Array is missing key: ' . $array_key, $file, $key); 465 | } 466 | continue; 467 | } 468 | 469 | if (is_string($lang)) 470 | { 471 | $this->validateString($file, $key . '.' . $array_key, $lang, $validate_language[$array_key]); 472 | } 473 | else 474 | { 475 | $this->validateArray($file, $key . '.' . $array_key, $lang, $validate_language[$array_key]); 476 | } 477 | } 478 | 479 | if (!empty($invalid_keys)) 480 | { 481 | foreach ($invalid_keys as $array_key) 482 | { 483 | if (gettype($array_key) == 'string') 484 | { 485 | $this->output->addMessage(Output::FATAL, 'Array has invalid key: ' . $array_key, $file, $key); 486 | } 487 | else 488 | { 489 | // Strangly used plural? 490 | $this->output->addMessage(Output::FATAL, 'Array has invalid key: ' . $array_key, $file, $key); 491 | } 492 | $this->output->addMessage(Output::ERROR, 'Key was not validated: ' . $array_key, $file, $key . '.' . $array_key); 493 | } 494 | } 495 | } 496 | 497 | /** 498 | * Validates a string 499 | * 500 | * Checks whether replacements %d and %s are used correctly 501 | * Checks for HTML 502 | * 503 | * @param string $file File to validate 504 | * @param string $key Key to validate 505 | * @param string $against_language Original language string 506 | * @param string $validate_language Translated language string 507 | * @param bool $is_plural String is part of a plural (we don't complain, if the first integer is missing) 508 | * @return null 509 | */ 510 | public function validateString($file, $key, $against_language, $validate_language, $is_plural = false) 511 | { 512 | if (gettype($against_language) !== gettype($validate_language)) 513 | { 514 | $this->output->addMessage(Output::FATAL, sprintf('Should be type %1$s but is type %2$s', gettype($against_language), gettype($validate_language)), $file, $key); 515 | return; 516 | } 517 | 518 | $against_strings = $against_strings_nonumber = substr_count($against_language, '%s'); 519 | $against_integers = $against_integers_nonumber = substr_count($against_language, '%d'); 520 | $validate_strings = $validate_strings_nonumber = substr_count($validate_language, '%s'); 521 | $validate_integers = $validate_integers_nonumber = substr_count($validate_language, '%d'); 522 | 523 | $against_integers_ary = $validate_integers_ary = array(); 524 | for ($i = 1; $i < 10; $i++) 525 | { 526 | if ($looping_count = substr_count($against_language, '%' . $i . '$s')) 527 | { 528 | $against_strings++; 529 | } 530 | if ($looping_count = substr_count($against_language, '%' . $i . '$d')) 531 | { 532 | $against_integers++; 533 | $against_integers_ary[] = $i; 534 | } 535 | if ($looping_count = substr_count($validate_language, '%' . $i . '$s')) 536 | { 537 | $validate_strings++; 538 | } 539 | if ($looping_count = substr_count($validate_language, '%' . $i . '$d')) 540 | { 541 | $validate_integers++; 542 | $validate_integers_ary[] = $i; 543 | } 544 | } 545 | 546 | if ($against_strings - $validate_strings !== 0) 547 | { 548 | $level = Output::FATAL; 549 | if ($this->originLanguagePath . 'ucp.php' === $file && in_array($key, array('TERMS_OF_USE_CONTENT', 'PRIVACY_POLICY'))) 550 | { 551 | $level = Output::ERROR; 552 | } 553 | 554 | $this->output->addMessage($level, sprintf('Should have %1$s string arguments, but has %2$s', $against_strings, $validate_strings), $file, $key); 555 | } 556 | 557 | if ($against_integers - $validate_integers !== 0) 558 | { 559 | if (!$is_plural || ($is_plural && abs($against_integers - $validate_integers) !== 1)) 560 | { 561 | $this->output->addMessage(Output::FATAL, sprintf('Should have %1$s integer arguments, but has %2$s', $against_integers, $validate_integers), $file, $key); 562 | } 563 | else if ($is_plural) 564 | { 565 | // If there are more then 1 integer parameters and they have no numbers 566 | // the number of integers must match! 567 | if ($against_integers_nonumber > 1) 568 | { 569 | $this->output->addMessage(Output::FATAL, sprintf('Should have %1$s integer arguments, but has %2$s', $against_integers, $validate_integers), $file, $key); 570 | } 571 | else if (!$against_integers_nonumber) 572 | { 573 | if (sizeof($against_integers_ary) > sizeof($validate_integers_ary)) 574 | { 575 | // If the integers have numbers, only the first integer is allowed to be missing. 576 | array_pop($against_integers_ary); 577 | $diff = array_diff($against_integers_ary, $validate_integers_ary); 578 | if (!empty($diff)) 579 | { 580 | $this->output->addMessage(Output::FATAL, sprintf('Should have %1$s integer arguments, but has %2$s', $against_integers, $validate_integers), $file, $key); 581 | } 582 | } 583 | else 584 | { 585 | // But this could also happen to the against language. So when the first integer 586 | // of the validate language is prior to against, that is okay aswell. 587 | array_pop($validate_integers_ary); 588 | $diff = array_diff($validate_integers_ary, $against_integers_ary); 589 | if (empty($diff)) 590 | { 591 | $this->output->addMessage(Output::FATAL, sprintf('Should have %1$s integer arguments, but has %2$s', $against_integers, $validate_integers), $file, $key); 592 | } 593 | } 594 | } 595 | 596 | } 597 | } 598 | 599 | $this->validateHtml($file, $key, $against_language, $validate_language); 600 | } 601 | 602 | /** 603 | * Validates the html usage in a string 604 | * 605 | * Checks whether the used HTML tags are also used in the original language. 606 | * Omitting tags is okay, as long as both (start and end) are omitted. 607 | * 608 | * @param string $file File to validate 609 | * @param string $key Key to validate 610 | * @param string $sourceString Language string to validate against 611 | * @param string $originString Language string to validate 612 | * @return null 613 | */ 614 | public function validateHtml($file, $key, $sourceString, $originString) 615 | { 616 | if ($this->originLanguagePath . 'install.php' === $file && in_array($key, array( 617 | 'INSTALL_CONGRATS_EXPLAIN', 618 | 'INSTALL_INTRO_BODY', 619 | 'SUPPORT_BODY', 620 | 'UPDATE_INSTALLATION_EXPLAIN', 621 | 'OVERVIEW_BODY', 622 | ))) 623 | { 624 | $sourceString = '

' . $sourceString . '

'; 625 | $originString = '

' . $originString . '

'; 626 | } 627 | 628 | $sourceHtml = $originHtml = $openTags = array(); 629 | preg_match_all('/\<.+?\>/', $sourceString, $sourceHtml); 630 | preg_match_all('/\<.+?\>/', $originString, $originHtml); 631 | 632 | if (empty($originHtml) || empty($originHtml[0])) 633 | { 634 | // Return when we have no HTML 635 | return; 636 | } 637 | $sourceHtml = isset($sourceHtml[0]) ? $sourceHtml[0] : array(); 638 | 639 | $failedUnclosed = false; 640 | foreach ($originHtml[0] as $possibleHtml) 641 | { 642 | $openingTag = $possibleHtml[1] !== '/'; 643 | $ignoreAdditional = false; 644 | 645 | // The closing tag contains a space 646 | if (!$openingTag && strpos($possibleHtml, ' ') !== false) 647 | { 648 | $this->output->addMessage(Output::FATAL, 'String is using invalid html: ' . $possibleHtml, $file, $key); 649 | $ignoreAdditional = true; 650 | } 651 | 652 | // missing closingTag allowed in:
,
, 653 | $allowedMissingClosingTag = array( 654 | 'br', 655 | 'hr', 656 | 'img', 657 | ); 658 | 659 | $tag = (strpos($possibleHtml, ' ') !== false) ? substr($possibleHtml, 1, strpos($possibleHtml, ' ') - 1) : substr($possibleHtml, 1, strpos($possibleHtml, '>') - 1); 660 | $tag = ($openingTag) ? $tag : substr($tag, 1); 661 | 662 | if ($openingTag) 663 | { 664 | if (in_array($tag, $openTags)) 665 | { 666 | if (!$failedUnclosed) 667 | { 668 | $this->output->addMessage(Output::FATAL, 'String is missing closing tag for html: ' . $tag, $file, $key); 669 | } 670 | $failedUnclosed = true; 671 | } 672 | else if (substr($possibleHtml, -3) !== ' />' && !in_array($tag, $allowedMissingClosingTag)) 673 | { 674 | $openTags[] = $tag; 675 | } 676 | } 677 | else if (empty($openTags)) 678 | { 679 | if (!$failedUnclosed) 680 | { 681 | $this->output->addMessage(Output::FATAL, sprintf('String is closing tag for html “%s” which was not opened before', $tag), $file, $key); 682 | } 683 | $failedUnclosed = true; 684 | } 685 | else if (end($openTags) != $tag) 686 | { 687 | if (!$failedUnclosed) 688 | { 689 | $this->output->addMessage(Output::FATAL, 'String is missing closing tag for html: ' . end($openTags), $file, $key); 690 | } 691 | $failedUnclosed = true; 692 | } 693 | else 694 | { 695 | array_pop($openTags); 696 | } 697 | 698 | // HTML tag is not used in original language 699 | if (!$ignoreAdditional && !in_array($possibleHtml, $sourceHtml) && !isset($this->additionalHtmlFound[$file][$key][$possibleHtml])) 700 | { 701 | $this->additionalHtmlFound[$file][$key][$possibleHtml] = true; 702 | if (strpos($possibleHtml, 'getErrorLevelForAdditionalHtml($possibleHtml); 709 | if (strpos($possibleHtml, '
', $sourceHtml) || 717 | $this->originLanguagePath . 'common.php' === $file && $key === 'TRANSLATION_INFO' || 718 | $this->originLanguagePath . 'help/faq.php' === $file || 719 | $this->originLanguagePath . 'help/bbcode.php' === $file 720 | ) 721 | { 722 | // Source contains a link aswell, mostly IST changed the link to better match the language 723 | // It's the translation info with the credit links of the translators 724 | // Or the help pages (faq and bbcode help), where links are not as bad as in other places. 725 | $level = Output::WARNING; 726 | } 727 | } 728 | 729 | $this->output->addMessage($level, 'String is using additional html: ' . $possibleHtml, $file, $key); 730 | } 731 | } 732 | 733 | if (!empty($openTags) && !$failedUnclosed) 734 | { 735 | $this->output->addMessage(Output::FATAL, 'String is missing closing tag for html: ' . $openTags[0], $file, $key); 736 | } 737 | } 738 | 739 | /** 740 | * Returns the error level for a given HTML 741 | * 742 | * Error: 743 | * - should be 744 | * - should be 745 | * Notice: 746 | * - 747 | * - 748 | * - 749 | * - 750 | * -
751 | * -
752 | * -
753 | * Fatal: 754 | * - others 755 | * 756 | * @param string $html 757 | * @return int Error level 758 | */ 759 | protected function getErrorLevelForAdditionalHtml($html) 760 | { 761 | if (preg_match('#^<(i|b|ul|ol|li|h3)( style="[a-zA-Z0-9_\:\ \;\-]+")?>$#', $html)) 762 | { 763 | return Output::ERROR; 764 | } 765 | 766 | if (in_array($html, array( 767 | '', 768 | '', 769 | '', 770 | '', 771 | '
', 772 | '
', 773 | '
' 774 | ))) 775 | { 776 | return Output::NOTICE; 777 | } 778 | 779 | if (preg_match('#^
$#', $html) || 780 | preg_match('#^$#', $html)) 781 | { 782 | return Output::ERROR; 783 | } 784 | 785 | if ($this->direction == 'rtl' && (strpos($html, 'ltr') !== false || strpos($html, 'rtl') !== false)) 786 | { 787 | return Output::WARNING; // be more lenient for RTL 788 | } 789 | 790 | return Output::FATAL; 791 | } 792 | } 793 | -------------------------------------------------------------------------------- /src/Phpbb/TranslationValidator/Validator/FileValidator.php: -------------------------------------------------------------------------------- 1 | input = $input; 170 | $this->output = $output; 171 | $this->langKeyValidator = new LangKeyValidator($input, $output); 172 | } 173 | 174 | /** 175 | * Set the language direction 176 | * @param $direction 177 | * @return $this 178 | */ 179 | public function setDirection($direction) 180 | { 181 | $this->direction = $direction; 182 | $this->langKeyValidator->setDirection($direction); 183 | return $this; 184 | } 185 | 186 | /** 187 | * Set phpBB Version 188 | * 189 | * @param string $originIso The ISO of the language to validate 190 | * @param string $originPath Path to the origin directory 191 | * @param string $originLanguagePath Relative path to the origin language/ directory 192 | * @return $this 193 | */ 194 | public function setOrigin($originIso, $originPath, $originLanguagePath) 195 | { 196 | $this->originIso = $originIso; 197 | $this->originPath = $originPath; 198 | $this->originLanguagePath = $originLanguagePath; 199 | $this->langKeyValidator->setOrigin($originIso, $originPath, $originLanguagePath); 200 | return $this; 201 | } 202 | 203 | /** 204 | * Set phpBB Version 205 | * 206 | * @param string $sourceIso The ISO of the language to validate against 207 | * @param string $sourcePath Path to the source directory 208 | * @param string $sourceLanguagePath Relative path to the source language/ directory 209 | * @return $this 210 | */ 211 | public function setSource($sourceIso, $sourcePath, $sourceLanguagePath) 212 | { 213 | $this->sourceIso = $sourceIso; 214 | $this->sourcePath = $sourcePath; 215 | $this->sourceLanguagePath = $sourceLanguagePath; 216 | $this->langKeyValidator->setSource($sourceIso, $sourcePath, $sourceLanguagePath); 217 | return $this; 218 | } 219 | 220 | /** 221 | * Set phpBB Version 222 | * 223 | * @param string $phpbbVersion The phpBB Version to validate against 224 | * @return $this 225 | */ 226 | public function setPhpbbVersion($phpbbVersion) 227 | { 228 | $this->phpbbVersion = $phpbbVersion; 229 | $this->langKeyValidator->setPhpbbVersion($phpbbVersion); 230 | return $this; 231 | } 232 | 233 | /** 234 | * Set plural rule 235 | * 236 | * @param int $pluralRule 237 | * @return $this 238 | */ 239 | public function setPluralRule($pluralRule) 240 | { 241 | $this->pluralRule = $pluralRule; 242 | $this->langKeyValidator->setPluralRule($pluralRule); 243 | return $this; 244 | } 245 | 246 | /** 247 | * Set debug mode 248 | * 249 | * @param bool $debug Debug mode 250 | * @return $this 251 | */ 252 | public function setDebug($debug) 253 | { 254 | $this->debug = $debug; 255 | $this->langKeyValidator->setDebug($debug); 256 | return $this; 257 | } 258 | 259 | /** 260 | * Set safe mode 261 | * 262 | * @param $safeMode 263 | * @return $this 264 | */ 265 | public function setSafeMode($safeMode) 266 | { 267 | $this->safeMode = $safeMode; 268 | return $this; 269 | } 270 | 271 | /** 272 | * Open the composer.json of the language pack and 273 | * save it to an array, accessible for the following functions 274 | */ 275 | public function openComposerJson($originFile) 276 | { 277 | $fileContents = (string) file_get_contents($this->originPath . '/' . $originFile); 278 | 279 | return json_decode($fileContents, true); 280 | } 281 | 282 | /** 283 | * Decides which validation function to use 284 | * 285 | * @param string $sourceFile Source file for comparison 286 | * @param string $originFile File to validate 287 | * @return null 288 | */ 289 | public function validate($sourceFile, $originFile) 290 | { 291 | $this->validateLineEndings($originFile); 292 | if (substr($originFile, -4) === '.php') 293 | { 294 | $this->validateDefinedInPhpbb($originFile); 295 | $this->validateUtf8withoutbom($originFile); 296 | $this->validateNoPhpClosingTag($originFile); 297 | } 298 | 299 | if (strpos($originFile, $this->originLanguagePath . 'email/') === 0 && substr($originFile, -4) === '.txt') 300 | { 301 | $this->validateEmail($sourceFile, $originFile); 302 | } 303 | else if (substr($originFile, -4) === '.php') 304 | { 305 | $this->validateLangFile($sourceFile, $originFile); 306 | } 307 | else if (substr($originFile, -9) === 'index.htm') 308 | { 309 | $this->validateIndexFile($originFile); 310 | } 311 | else if ($originFile === $this->originLanguagePath . 'LICENSE') 312 | { 313 | $this->validateLicenseFile($originFile); 314 | } 315 | else if ($originFile === $this->originLanguagePath . 'composer.json') 316 | { 317 | $this->validateJsonFile($originFile); 318 | $this->validateCaptchaValues($originFile); 319 | } 320 | else if (substr($originFile, -4) === '.css') 321 | { 322 | $this->validateUtf8withoutbom($originFile); 323 | $this->validateCSSFile($sourceFile, $originFile); 324 | } 325 | else 326 | { 327 | $this->output->addMessage(Output::NOTICE, 'File is not validated', $originFile); 328 | } 329 | } 330 | 331 | /** 332 | * Validates a normal language file 333 | * 334 | * Files should not produce any output. 335 | * Files should only define the $lang variable. 336 | * Files must have all language keys defined in the source file. 337 | * Files should not have additional language keys. 338 | * 339 | * @param string $sourceFile Source file for comparison 340 | * @param string $originFile File to validate 341 | * @return null 342 | */ 343 | public function validateLangFile($sourceFile, $originFile) 344 | { 345 | $originFilePath = $this->originPath . '/' . $originFile; 346 | $sourceFilePath = $this->sourcePath . '/' . $sourceFile; 347 | 348 | if (!$this->safeMode) 349 | { 350 | ob_start(); 351 | 352 | /** @var $lang */ 353 | include($originFilePath); 354 | 355 | $defined_variables = get_defined_vars(); 356 | if (sizeof($defined_variables) != 5 || !isset($defined_variables['lang']) || gettype($defined_variables['lang']) != 'array') 357 | { 358 | $this->output->addMessage(Output::FATAL, 'Must only contain the lang-array', $originFile); 359 | if (!isset($defined_variables['lang']) || gettype($defined_variables['lang']) != 'array') 360 | { 361 | return; 362 | } 363 | } 364 | 365 | $output = ob_get_contents(); 366 | ob_end_clean(); 367 | 368 | if ($output !== '') 369 | { 370 | $this->output->addMessage(Output::FATAL, 'Must not produces output: ' . htmlspecialchars($output), $originFile); 371 | } 372 | } 373 | 374 | else 375 | { 376 | /** @var $lang */ 377 | $lang = ValidatorRunner::langParser($originFilePath); 378 | $this->output->addMessage(Output::NOTICE, '[Safe Mode] Manually run the translation validator to check for disallowed output.', $originFile); 379 | } 380 | 381 | $validate = $lang; 382 | unset($lang); 383 | 384 | if (!$this->safeMode) 385 | { 386 | /** @var $lang */ 387 | include($sourceFilePath); 388 | } 389 | 390 | else 391 | { 392 | /** @var $lang */ 393 | $lang = ValidatorRunner::langParser($sourceFilePath); 394 | } 395 | 396 | $against = $lang; 397 | unset($lang); 398 | 399 | foreach ($against as $againstLangKey => $againstLanguage) 400 | { 401 | if (!isset($validate[$againstLangKey])) 402 | { 403 | $this->output->addMessage(Output::FATAL, 'Must contain key: ' . $againstLangKey, $originFile); 404 | continue; 405 | } 406 | 407 | $this->langKeyValidator->validate($originFile, $againstLangKey, $againstLanguage, $validate[$againstLangKey]); 408 | } 409 | 410 | foreach ($validate as $validateLangKey => $validateLanguage) 411 | { 412 | if (!isset($against[$validateLangKey])) 413 | { 414 | $this->output->addMessage(Output::FATAL, 'Must not contain key: ' . $validateLangKey, $originFile); 415 | } 416 | } 417 | } 418 | 419 | /** 420 | * Validates an email .txt file 421 | * 422 | * Emails must have a subject when the source file has one, otherwise must not have one. 423 | * Emails must have a signature when the source file has one, otherwise must not have one. 424 | * Emails should use template vars, used by the source file. 425 | * Emails should not use additional template vars. 426 | * Emails should not use any HTML. 427 | * Emails should contain a newline at their end. 428 | * 429 | * @param string $sourceFile Source file for comparison 430 | * @param string $originFile File to validate 431 | * @return null 432 | */ 433 | public function validateEmail($sourceFile, $originFile) 434 | { 435 | $sourceContent = (string) file_get_contents($this->sourcePath . '/' . $sourceFile); 436 | $originContent = (string) file_get_contents($this->originPath . '/' . $originFile); 437 | $originContent = str_replace("\r\n", "\n", $originContent); 438 | $originContent = str_replace("\r", "\n", $originContent); 439 | 440 | $sourceContent = explode("\n", $sourceContent); 441 | $originContent = explode("\n", $originContent); 442 | 443 | // Is the file saved as UTF8 with BOM? 444 | if (substr($originContent[0], 0, 3) === "\xEF\xBB\xBF") 445 | { 446 | $this->output->addMessage(Output::FATAL, 'File must be encoded using UTF8 without BOM', $originFile); 447 | $originContent[0] = substr($originContent[0], 3); 448 | } 449 | 450 | // One language contains a subject, the other one does not 451 | if (strpos($sourceContent[0], 'Subject: ') === 0 && strpos($originContent[0], 'Subject: ') !== 0) 452 | { 453 | $this->output->addMessage(Output::FATAL, 'Must have a "Subject: "-line', $originFile); 454 | } 455 | else if (strpos($sourceContent[0], 'Subject: ') !== 0 && strpos($originContent[0], 'Subject: ') === 0) 456 | { 457 | $this->output->addMessage(Output::FATAL, 'Must not have a "Subject: "-line', $originFile); 458 | } 459 | 460 | // One language contains the signature, the other one does not 461 | if ((end($sourceContent) === '{EMAIL_SIG}' || prev($sourceContent) === '{EMAIL_SIG}') 462 | && end($originContent) !== '{EMAIL_SIG}' && prev($originContent) !== '{EMAIL_SIG}') 463 | { 464 | $this->output->addMessage(Output::FATAL, 'Must have the signature appended', $originFile); 465 | } 466 | else if ((end($originContent) === '{EMAIL_SIG}' || prev($originContent) === '{EMAIL_SIG}') 467 | && end($sourceContent) !== '{EMAIL_SIG}' && prev($sourceContent) !== '{EMAIL_SIG}') 468 | { 469 | $this->output->addMessage(Output::FATAL, 'Must not have the signature appended', $originFile); 470 | } 471 | 472 | $originTemplateVars = $sourceTemplateVars = array(); 473 | preg_match_all('/{.+?}/', implode("\n", $originContent), $originTemplateVars); 474 | preg_match_all('/{.+?}/', implode("\n", $sourceContent), $sourceTemplateVars); 475 | 476 | 477 | $additionalOrigin = array_diff($sourceTemplateVars[0], $originTemplateVars[0]); 478 | $additionalSource = array_diff($originTemplateVars[0], array_merge(array( 479 | '{U_BOARD}', 480 | '{EMAIL_SIG}', 481 | '{SITENAME}', 482 | ), $sourceTemplateVars[0])); 483 | 484 | // Check the used template variables 485 | if (!empty($additionalSource)) 486 | { 487 | $this->output->addMessage(Output::FATAL, 'Is using additional variables: ' . implode(', ', $additionalSource), $originFile); 488 | } 489 | 490 | if (!empty($additionalOrigin)) 491 | { 492 | $this->output->addMessage(Output::ERROR, 'Is not using variables: ' . implode(', ', $additionalOrigin), $originFile); 493 | } 494 | 495 | $validateHtml = array(); 496 | preg_match_all('/\<.+?\>/', implode("\n", $originContent), $validateHtml); 497 | if (!empty($validateHtml) && !empty($validateHtml[0])) 498 | { 499 | foreach ($validateHtml[0] as $possibleHtml) 500 | { 501 | if ((substr($possibleHtml, 0, 5) !== '') 502 | && (substr($possibleHtml, 0, 2) !== '<{' || substr($possibleHtml, -2) !== '}>') 503 | ) 504 | { 505 | $this->output->addMessage(Output::FATAL, 'Using additional HTML: ' . htmlspecialchars($possibleHtml), $originFile); 506 | } 507 | } 508 | } 509 | 510 | // Check for new liens at the end of the file 511 | if (end($originContent) !== '') 512 | { 513 | $this->output->addMessage(Output::FATAL, 'Missing new line at the end of the file', $originFile); 514 | } 515 | } 516 | 517 | /** 518 | * Validates the LICENSE file 519 | * 520 | * Only "GNU GENERAL PUBLIC LICENSE Version 2" is allowed 521 | * 522 | * @param string $originFile File to validate 523 | * @return null 524 | */ 525 | public function validateLicenseFile($originFile) 526 | { 527 | $fileContents = (string) file_get_contents($this->originPath . '/' . $originFile); 528 | 529 | if (md5($fileContents) != 'e060338598cd2cd6b8503733fdd40a11') 530 | { 531 | $this->output->addMessage(Output::FATAL, 'License must be: GNU GENERAL PUBLIC LICENSE Version 2, June 1991', $originFile); 532 | } 533 | } 534 | 535 | /** 536 | * Validates a index.htm file 537 | * 538 | * Only empty index.htm or the default htm file are allowed 539 | * 540 | * @param string $originFile File to validate 541 | * @return null 542 | */ 543 | public function validateIndexFile($originFile) 544 | { 545 | $fileContents = (string) file_get_contents($this->originPath . '/' . $originFile); 546 | 547 | // Empty index.htm file or one that displayes an empty white page 548 | if ($fileContents !== '' && md5($fileContents) != '16703867d439efbd7c373dc2269e25a7') 549 | { 550 | $this->output->addMessage(Output::FATAL, 'File must be empty', $originFile); 551 | } 552 | } 553 | 554 | /** 555 | * Validates the composer.json file 556 | * 557 | * Should be valid and contain the necessary information: 558 | * Mandatory: 559 | * name, description, type, version, homepage, license 560 | * Authors: name (optional: email and homepage) 561 | * Extra: language-iso, english-name, local-name, 562 | * phpbb-version, direction, user-lang, plural-rule 563 | * Optional: 564 | * Support: urls to: forum, wiki, issues etc 565 | * 566 | * @param string $originFile File to validate 567 | * @return null 568 | */ 569 | public function validateJsonFile($originFile) 570 | { 571 | $jsonContent = $this->openComposerJson($originFile); 572 | 573 | if (!str_starts_with($jsonContent['name'], 'phpbb/phpbb-language-')) 574 | { 575 | $this->output->addMessage(Output::FATAL, 'Name should start with phpbb/phpbb-language- followed by the language iso code', $originFile); 576 | } 577 | // Check for an existing description 578 | if (!array_key_exists('description', $jsonContent) || $jsonContent['description'] == '') 579 | { 580 | $this->output->addMessage(Output::FATAL, 'Description is missing', $originFile); 581 | } 582 | // Check if the description contains only words and punctuation, not URLs. 583 | elseif (preg_match('/\b(?:www|https)\b|(?:\.[a-z]{2,})/i', $jsonContent['description'])) 584 | { 585 | $this->output->addMessage(Output::ERROR, 'The description should only contain words - no URLs.', $originFile); 586 | } 587 | // Check if the type is correctly defined 588 | if ($jsonContent['type'] != 'phpbb-language') 589 | { 590 | $this->output->addMessage(Output::FATAL, 'Type must be exactly: "phpbb-language"', $originFile); 591 | } 592 | // Check if there is a valid version definition 593 | if (!array_key_exists('version', $jsonContent)) 594 | { 595 | $this->output->addMessage(Output::FATAL, 'Language pack needs a version definition.', $originFile); 596 | } 597 | elseif ($jsonContent['version'] == '') 598 | { 599 | $this->output->addMessage(Output::FATAL, 'The defined version should not be empty.', $originFile); 600 | } 601 | elseif (!preg_match('/^(\d+\.)?(\d+\.)?(\*|\d+)$/', $jsonContent['version'])) 602 | { 603 | $this->output->addMessage(Output::ERROR, 'The defined version is in the wrong format.', $originFile); 604 | } 605 | // Homepage should be at least an empty string 606 | if (!preg_match('/(?:https?:\/\/|www\.)[^\s]+|(?:\b[a-z0-9-]+\.(?:com|net|org|info|io|co|biz|me|xyz|ai|app|dev|tech|tv|us|uk|de|fr|ru|jp|cn|in)\b)/i', $jsonContent['homepage']) && $jsonContent['homepage'] != '') 607 | { 608 | $this->output->addMessage(Output::ERROR, 'The homepage value allows only URLs or can be left empty.', $originFile); 609 | } 610 | // Check for the correct license value 611 | if ($jsonContent['license'] != 'GPL-2.0-only') 612 | { 613 | $this->output->addMessage(Output::FATAL, 'The license value has to be "GPL-2.0-only"', $originFile); 614 | } 615 | // Check for the authors 616 | if (!array_key_exists('authors', $jsonContent)) 617 | { 618 | $this->output->addMessage(Output::ERROR, 'The authors value is missing.', $originFile); 619 | } 620 | // Check for support, authors should at least give one contact option! 621 | if (!array_key_exists('support', $jsonContent)) 622 | { 623 | $this->output->addMessage(Output::ERROR, 'The support value is missing.', $originFile); 624 | } 625 | elseif (count ($jsonContent['support']) < 1) 626 | { 627 | $this->output->addMessage(Output::ERROR, 'The support category has no values. Please provide at least one contact option e.g. forum or email.', $originFile); 628 | } 629 | // Check for the extra-section 630 | if (!array_key_exists('extra', $jsonContent)) 631 | { 632 | $this->output->addMessage(Output::FATAL, 'The extra section is missing.', $originFile); 633 | } 634 | // language-iso must be valid 635 | if (!preg_match('/^(?:[a-z]*_?){0,2}[a-z]*$/', $jsonContent['extra']['language-iso'])) 636 | { 637 | $this->output->addMessage(Output::FATAL, 'The language-iso should only contain small letters from a to z and maximum two underscores.', $originFile); 638 | } 639 | elseif ($jsonContent['extra']['language-iso'] != $this->originIso) 640 | { 641 | $this->output->addMessage(Output::FATAL, 'Language iso is not valid', $originFile); 642 | } 643 | // Check for english name 644 | if ($jsonContent['extra']['english-name'] == '' || !preg_match('/^[a-zA-Z\s]+$/', $jsonContent['extra']['english-name'])) 645 | { 646 | $this->output->addMessage(Output::ERROR, 'The english-name value should only contain letters aA-zZ and spaces.', $originFile); 647 | } 648 | // Check for local name 649 | if ($jsonContent['extra']['local-name'] == '') 650 | { 651 | $this->output->addMessage(Output::ERROR, 'The local-name value should not be empty.', $originFile); 652 | } 653 | // Check for valid phpBB-Version, we accept: 4.0.0, 4.0.0-a1 or 4.0.0-b1 or 4.0.0-RC1 654 | if (!preg_match('/^\d+\.\d+\.\d+(-(?:a|b|RC)\d+)?$/', $jsonContent['extra']['phpbb-version']) || $jsonContent['extra']['phpbb-version'] == '' ) 655 | { 656 | $this->output->addMessage(Output::FATAL, 'The phpbb-version value should not be empty and contain a valid version number.', $originFile); 657 | } 658 | // Check for valid direction 659 | $textDirection = $jsonContent['extra']['direction']; 660 | if (!in_array($textDirection, array('ltr', 'rtl'))) 661 | { 662 | $this->output->addMessage(Output::FATAL, 'The direction can only be rtl or ltr.', $originFile); 663 | } 664 | // Check for user-lang: en-gb 665 | if (!isset($jsonContent['extra']['user-lang']) || $jsonContent['extra']['user-lang'] == '') 666 | { 667 | $this->output->addMessage(Output::FATAL, 'The user-lang must be defined.', $originFile); 668 | } 669 | // Check for plural-rule 670 | if (!preg_match('/^(?:[0-9]|1[0-5])$/', $jsonContent['extra']['plural-rule'])) 671 | { 672 | $this->output->addMessage(Output::FATAL, 'Plural rules does not have a valid value.', $originFile); 673 | } 674 | } 675 | 676 | /** 677 | * Check that the reCaptcha and Turnstile key provided is allowed 678 | * @param $originFile 679 | */ 680 | public function validateCaptchaValues($originFile, $optParams = '') 681 | { 682 | $jsonContent = $this->openComposerJson($originFile); 683 | 684 | if ($optParams != '') 685 | { 686 | $jsonContent['extra']['recaptcha-lang'] = $optParams; 687 | } 688 | // The key 'RECAPTCHA_LANG' must match the list provided by Google, or be left empty 689 | // Check for valid recaptcha-lang: en-GB 690 | if (!in_array($jsonContent['extra']['recaptcha-lang'], $this->reCaptchaLanguages)) 691 | { 692 | $this->output->addMessage(Output::ERROR, 'reCaptcha must match a language/country code on https://developers.google.com/recaptcha/docs/language - if no code exists for your language you can use "en".', $originFile); 693 | } 694 | // Check for valid turnstile-lang: en 695 | // (should be in: https://developers.cloudflare.com/turnstile/reference/supported-languages/ ) 696 | if (!in_array($jsonContent['extra']['turnstile-lang'], $this->reTurnstilesLanguages)) 697 | { 698 | $this->output->addMessage(Output::ERROR, 'Turnstile must match a 2-digit-language code from https://developers.cloudflare.com/turnstile/reference/supported-languages/ - if no code exists for your language you can use "en".', $originFile); 699 | } 700 | } 701 | 702 | /** 703 | * Validates whether a file checks for the IN_PHPBB constant 704 | * 705 | * @param string $originFile File to validate 706 | * @return null 707 | */ 708 | public function validateDefinedInPhpbb($originFile) 709 | { 710 | $fileContents = (string) file_get_contents($this->originPath . '/' . $originFile); 711 | 712 | // Regex copied from MPV 713 | if (!preg_match("#defined([ ]+){0,1}\\(([ ]+){0,1}'IN_PHPBB'#", $fileContents)) 714 | { 715 | $this->output->addMessage(Output::FATAL, 'Must check whether IN_PHPBB is defined', $originFile); 716 | } 717 | } 718 | 719 | /** 720 | * Validates whether a file checks for the IN_PHPBB constant 721 | * 722 | * @param string $originFile File to validate 723 | * @return null 724 | */ 725 | public function validateUtf8withoutbom($originFile) 726 | { 727 | $fileContents = (string) file_get_contents($this->originPath . '/' . $originFile); 728 | $fileContents = explode("\n", $fileContents); 729 | $fileContents = $fileContents[0]; 730 | 731 | // Is the file saved as UTF8 with BOM? 732 | if (substr($fileContents, 0, 3) === "\xEF\xBB\xBF") 733 | { 734 | $this->output->addMessage(Output::FATAL, 'File must be encoded using UTF8 without BOM', $originFile); 735 | } 736 | } 737 | 738 | /** 739 | * Validates whether a file does not contain a closing php tag 740 | * 741 | * @param string $originFile File to validate 742 | * @return null 743 | */ 744 | public function validateNoPhpClosingTag($originFile) 745 | { 746 | $fileContents = (string) file_get_contents($this->originPath . '/' . $originFile); 747 | $fileContents = str_replace("\r\n", "\n", $fileContents); 748 | $fileContents = str_replace("\r", "\n", $fileContents); 749 | 750 | // Does the file contain anything after the last ");" 751 | if (substr($fileContents, -3) !== ");\n") 752 | { 753 | if (substr($fileContents, -3) !== "];\n") 754 | { 755 | $this->output->addMessage(Output::FATAL, 'File must not contain a PHP closing tag, but end with one new line', $originFile); 756 | } 757 | } 758 | } 759 | 760 | /** 761 | * Validates whether a file checks whether the file uses Linux line endings 762 | * 763 | * @param string $originFile File to validate 764 | * @return null 765 | */ 766 | public function validateLineEndings($originFile) 767 | { 768 | $fileContents = (string) file_get_contents($this->originPath . '/' . $originFile); 769 | 770 | if (strpos($fileContents, "\r") !== false) 771 | { 772 | $this->output->addMessage(Output::FATAL, 'Not using Linux line endings (LF)', $originFile); 773 | } 774 | } 775 | 776 | /** 777 | * Validates whether a file checks whether the file uses Linux line endings 778 | * 779 | * @param string $sourceFile Source file for comparison 780 | * @param string $originFile File to validate 781 | * @return null 782 | */ 783 | public function validateCSSFile($sourceFile, $originFile) 784 | { 785 | $sourceFileContents = (string) file_get_contents($this->sourcePath . '/' . $sourceFile); 786 | $originFileContents = (string) file_get_contents($this->originPath . '/' . $originFile); 787 | 788 | $sourceRules = $this->getCSSRules($sourceFile, $sourceFileContents); 789 | $originRules = $this->getCSSRules($originFile, $originFileContents); 790 | 791 | $missingRules = array_diff(array_keys($sourceRules), array_keys($originRules)); 792 | $additionalRules = array_diff(array_keys($originRules), array_keys($sourceRules)); 793 | if (!empty($missingRules)) 794 | { 795 | $this->output->addMessage(Output::FATAL, 'Stylesheet file is missing CSS rules: ' . implode(', ', $missingRules), $originFile); 796 | } 797 | if (!empty($additionalRules)) 798 | { 799 | $additionalRulesLevel = ($this->direction == 'rtl') ? Output::WARNING : Output::FATAL; // be more lenient for RTL 800 | $this->output->addMessage($additionalRulesLevel, 'Stylesheet file has additional CSS rules: ' . implode(', ', $additionalRules), $originFile); 801 | } 802 | } 803 | 804 | protected function getCSSRules($fileName, $fileContent) 805 | { 806 | // Remove comments 807 | $fileContent = preg_replace('#/\*(?:.(?!/)|[^\*](?=/)|(?output->addMessage(Output::FATAL, 'Stylesheet file structure is invalid (Output after last rule)', $fileName); 815 | } 816 | 817 | $cssRules = array(); 818 | foreach ($content as $section) 819 | { 820 | if (strpos($section, '{') === false) 821 | { 822 | $this->output->addMessage(Output::FATAL, 'Stylesheet file structure is invalid: ' . trim($section), $fileName); 823 | continue; 824 | } 825 | 826 | list($rule, $ruleContent) = explode(' {', $section, 2); 827 | $rule = trim($rule); 828 | $ruleContent = trim($ruleContent); 829 | 830 | if (strpos($ruleContent, '{') !== false) 831 | { 832 | $this->output->addMessage(Output::FATAL, 'CSS rule is invalid: ' . $rule, $fileName); 833 | continue; 834 | } 835 | 836 | if (!isset($cssRules[$rule])) 837 | { 838 | $cssRules[$rule] = ''; 839 | } 840 | 841 | $cssRules[$rule] .= $ruleContent; 842 | } 843 | 844 | return $cssRules; 845 | } 846 | } 847 | --------------------------------------------------------------------------------