Commit cad7a9ac authored by Thomas Löffler's avatar Thomas Löffler
Browse files

Merge branch 'task/typo3-v11-support' into 'main'

TYPO3 v11 support

See merge request !23
parents 01d665a4 1563bd3f
#!/bin/bash
# We need to install dependencies only for Docker
[[ ! -e /.dockerenv ]] && exit 0
set -xe
# Install git (the php image doesn't have it) which is required by composer, and zip
apt-get update -yqq
apt-get install git unzip zlib1g-dev libzip-dev -yqq
docker-php-ext-install zip
# Install xdebug
pecl install xdebug
docker-php-ext-enable xdebug
# Install composer
curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
<?php
/*
* This file is part of the be_secure_pw project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
/**
* This file represents the configuration for Code Sniffing PSR-2-related
* automatic checks of coding guidelines
* Install @fabpot's great php-cs-fixer tool via
*
* $ composer global require friendsofphp/php-cs-fixer
*
* And then simply run
*
* $ ./bin/php-cs-fixer fix --config ./Build/php-cs-fixer.php
*
* inside the TYPO3 directory. Warning: This may take up to 10 minutes.
*
* For more information read:
* https://www.php-fig.org/psr/psr-2/
* https://cs.sensiolabs.org
*/
if (PHP_SAPI !== 'cli') {
die('This script supports command line usage only. Please check your command.');
}
// Define in which folders to search and which folders to exclude
// Exclude all files and directories from .gitignore
$finder = (new PhpCsFixer\Finder())
->ignoreVCSIgnored(true)
->in(realpath(__DIR__ . '/../../'));
// Return a Code Sniffing configuration using
// all sniffers needed for PSR-2
// and additionally:
// - Remove leading slashes in use clauses.
// - PHP single-line arrays should not have trailing comma.
// - Single-line whitespace before closing semicolon are prohibited.
// - Remove unused use statements in the PHP source code
// - Ensure Concatenation to have at least one whitespace around
// - Remove trailing whitespace at the end of blank lines.
return (new \PhpCsFixer\Config())
->setRiskyAllowed(true)
->setRules([
'@DoctrineAnnotation' => true,
'@PSR2' => true,
'array_syntax' => ['syntax' => 'short'],
'blank_line_after_opening_tag' => true,
'braces' => ['allow_single_line_closure' => true],
'cast_spaces' => ['space' => 'none'],
'compact_nullable_typehint' => true,
'concat_space' => ['spacing' => 'one'],
'declare_equal_normalize' => ['space' => 'none'],
'dir_constant' => true,
'function_typehint_space' => true,
'lowercase_cast' => true,
'method_argument_space' => ['on_multiline' => 'ensure_fully_multiline'],
'modernize_types_casting' => true,
'native_function_casing' => true,
'new_with_braces' => true,
'no_alias_functions' => true,
'no_blank_lines_after_phpdoc' => true,
'no_empty_phpdoc' => true,
'no_empty_statement' => true,
'no_extra_blank_lines' => true,
'no_leading_import_slash' => true,
'no_leading_namespace_whitespace' => true,
'no_null_property_initialization' => true,
'no_short_bool_cast' => true,
'no_singleline_whitespace_before_semicolons' => true,
'no_superfluous_elseif' => true,
'no_trailing_comma_in_singleline_array' => true,
'no_unneeded_control_parentheses' => true,
'no_unused_imports' => true,
'no_useless_else' => true,
'no_whitespace_in_blank_line' => true,
'ordered_imports' => true,
'php_unit_construct' => ['assertions' => ['assertEquals', 'assertSame', 'assertNotEquals', 'assertNotSame']],
'php_unit_mock_short_will_return' => true,
'php_unit_test_case_static_method_calls' => ['call_type' => 'self'],
'phpdoc_no_access' => true,
'phpdoc_no_empty_return' => true,
'phpdoc_no_package' => true,
'phpdoc_scalar' => true,
'phpdoc_trim' => true,
'phpdoc_types' => true,
'phpdoc_types_order' => ['null_adjustment' => 'always_last', 'sort_algorithm' => 'none'],
'return_type_declaration' => ['space_before' => 'none'],
'single_quote' => true,
'single_line_comment_style' => ['comment_types' => ['hash']],
'single_trait_insert_per_statement' => true,
'trailing_comma_in_multiline' => ['elements' => ['arrays']],
'whitespace_after_comma_in_array' => true,
])
->setFinder($finder);
#!/bin/sh
error=false
while test $# -gt 0; do
current=$1
shift
if [ ! -d $current ] && [ ! -f $current ] ; then
echo "Invalid directory or file: $current"
error=true
continue
fi
for file in `find $current -type f -name "*.php"` ; do
RESULTS=`php -l $file`
if [ "$RESULTS" != "No syntax errors detected in $file" ] ; then
echo $RESULTS
error=true
fi
done
done
if [ "$error" = true ] ; then
exit 1
else
exit 0
fi
\ No newline at end of file
<?xml version="1.0"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" backupGlobals="true" backupStaticAttributes="false" bootstrap="../vendor/nimut/testing-framework/res/Configuration/UnitTestsBootstrap.php" colors="false" convertErrorsToExceptions="false" convertWarningsToExceptions="true" forceCoversAnnotation="false" processIsolation="false" stopOnError="false" stopOnFailure="false" stopOnIncomplete="false" stopOnSkipped="false" verbose="false" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd">
<coverage processUncoveredFiles="true">
<include>
<directory suffix=".php">../../Classes/</directory>
</include>
<report>
<text outputFile="php://stdout" showUncoveredFiles="false"/>
</report>
</coverage>
<testsuites>
<testsuite name="Base tests">
<directory>../../Tests/Unit</directory>
</testsuite>
</testsuites>
<logging>
<junit outputFile="../unittest-report.xml"/>
</logging>
</phpunit>
<?php
declare(strict_types=1);
use Rector\Core\Configuration\Option;
use Rector\Core\ValueObject\PhpVersion;
use Rector\Php55\Rector\String_\StringClassNameToClassConstantRector;
use Rector\PostRector\Rector\NameImportingPostRector;
use Ssch\TYPO3Rector\Configuration\Typo3Option;
use Ssch\TYPO3Rector\FileProcessor\Composer\Rector\ExtensionComposerRector;
use Ssch\TYPO3Rector\FileProcessor\TypoScript\Rector\FileIncludeToImportStatementTypoScriptRector;
use Ssch\TYPO3Rector\Rector\v9\v0\InjectAnnotationRector;
use Ssch\TYPO3Rector\Rector\General\ConvertImplicitVariablesToExplicitGlobalsRector;
use Ssch\TYPO3Rector\Rector\General\ExtEmConfRector;
use Ssch\TYPO3Rector\Set\Typo3LevelSetList;
use Ssch\TYPO3Rector\FileProcessor\TypoScript\Rector\ExtbasePersistenceTypoScriptRector;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
return static function (ContainerConfigurator $containerConfigurator): void {
$parameters = $containerConfigurator->parameters();
$containerConfigurator->import(Typo3LevelSetList::UP_TO_TYPO3_11);
// In order to have a better analysis from phpstan we teach it here some more things
$parameters->set(Option::PHPSTAN_FOR_RECTOR_PATH, Typo3Option::PHPSTAN_FOR_RECTOR_PATH);
// FQN classes are not imported by default. If you don't do it manually after every Rector run, enable it by:
$parameters->set(Option::AUTO_IMPORT_NAMES, true);
// this will not import root namespace classes, like \DateTime or \Exception
$parameters->set(Option::IMPORT_SHORT_CLASSES, false);
// this will not import classes used in PHP DocBlocks, like in /** @var \Some\Class */
$parameters->set(Option::IMPORT_DOC_BLOCKS, false);
// Define your target version which you want to support
$parameters->set(Option::PHP_VERSION_FEATURES, PhpVersion::PHP_72);
// If you have an editorconfig and changed files should keep their format enable it here
// $parameters->set(Option::ENABLE_EDITORCONFIG, true);
// If you only want to process one/some TYPO3 extension(s), you can specify its path(s) here.
// If you use the option --config change __DIR__ to getcwd()
$parameters->set(Option::PATHS, [
getcwd() . '/Classes',
getcwd() . '/Configuration',
getcwd() . '/Tests',
]);
// If you use the option --config change __DIR__ to getcwd()
$parameters->set(Option::SKIP, [
// @see https://github.com/sabbelasichon/typo3-rector/issues/2536
getcwd() . '/**/Configuration/ExtensionBuilder/*',
// We skip those directories on purpose as there might be node_modules or similar
// that include typescript which would result in false positive processing
getcwd() . '/**/Resources/**/node_modules/*',
getcwd() . '/**/Resources/**/NodeModules/*',
getcwd() . '/**/Resources/**/BowerComponents/*',
getcwd() . '/**/Resources/**/bower_components/*',
getcwd() . '/**/Resources/**/build/*',
]);
// This is used by the class \Ssch\TYPO3Rector\Rector\PostRector\FullQualifiedNamePostRector to force FQN in this paths and files
$parameters->set(Typo3Option::PATHS_FULL_QUALIFIED_NAMESPACES, [
# If you are targeting TYPO3 Version 11 use can now use Short namespace
# @see namespace https://docs.typo3.org/m/typo3/reference-coreapi/master/en-us/ExtensionArchitecture/ConfigurationFiles/Index.html
'ext_localconf.php',
'ext_tables.php',
'ClassAliasMap.php',
getcwd() . '/**/Configuration/*.php',
getcwd() . '/**/Configuration/**/*.php',
]);
// If you have trouble that rector cannot run because some TYPO3 constants are not defined add an additional constants file
// @see https://github.com/sabbelasichon/typo3-rector/blob/master/typo3.constants.php
// @see https://github.com/rectorphp/rector/blob/main/docs/static_reflection_and_autoload.md#include-files
$parameters->set(Option::BOOTSTRAP_FILES, [
getcwd() . '/.Build/vendor/autoload.php',
]);
// get services (needed for register a single rule)
$services = $containerConfigurator->services();
// register a single rule
// $services->set(InjectAnnotationRector::class);
/**
* Useful rule from RectorPHP itself to transform i.e. GeneralUtility::makeInstance('TYPO3\CMS\Core\Log\LogManager')
* to GeneralUtility::makeInstance(\TYPO3\CMS\Core\Log\LogManager::class) calls.
* But be warned, sometimes it produces false positives (edge cases), so watch out
*/
// $services->set(StringClassNameToClassConstantRector::class);
// Optional non-php file functionalities:
// @see https://github.com/sabbelasichon/typo3-rector/blob/main/docs/beyond_php_file_processors.md
// Adapt your composer.json dependencies to the latest available version for the defined SetList
// $containerConfigurator->import(Typo3SetList::COMPOSER_PACKAGES_104_CORE);
// $containerConfigurator->import(Typo3SetList::COMPOSER_PACKAGES_104_EXTENSIONS);
// Rewrite your extbase persistence class mapping from typoscript into php according to official docs.
// This processor will create a summarized file with all of the typoscript rewrites combined into a single file.
// The filename can be passed as argument, "Configuration_Extbase_Persistence_Classes.php" is default.
// $services->set(ExtbasePersistenceTypoScriptRector::class);
// Add some general TYPO3 rules
$services->set(ConvertImplicitVariablesToExplicitGlobalsRector::class);
$services->set(ExtEmConfRector::class);
// $services->set(ExtensionComposerRector::class);
// Do you want to modernize your TypoScript include statements for files and move from <INCLUDE /> to @import use the FileIncludeToImportStatementVisitor
// $services->set(FileIncludeToImportStatementTypoScriptRector::class);
};
cache:
key: "$CI_COMMIT_REF_NAME-$CI_JOB_STAGE"
paths:
- /cache/composer
before_script:
- apk add git --update
stages:
- test
- release
.test: &testing
stage: test
image: php:$DOCKER_TAG
only:
- branches
variables:
TYPO3_PATH_WEB: "$PWD/.Build/Web"
TYPO3_PATH_ROOT: "$PWD/.Build/Web"
COMPOSER_CACHE_DIR: "/cache/composer"
before_script:
- apk add git --update
- php -r "copy('https://getcomposer.org/composer.phar', 'composer.phar');"
- php composer.phar config minimum-stability ${COMPOSER_STABILITY}
- php composer.phar config cache-dir /cache/composer
- php composer.phar remove --dev nimut/testing-framework
- php composer.phar remove typo3/cms-core
- rm composer.lock
- rm -Rf .Build/vendor/
- php composer.phar require ${TYPO3_VERSION} --update-with-dependencies
- php composer.phar require --dev nimut/testing-framework:${NIMUT_TESTFRAMEWORK_VERSION} --update-with-dependencies
- php composer.phar require --dev squizlabs/php_codesniffer
script:
- .Build/bin/phpunit -c Configuration/.Build/Tests/UnitTests.xml
- .Build/bin/phpcs --standard=PSR2 --extensions=php *
allow_failure: true
# Build in PHP 7.2 and TYPO3 (9.x)
test:php72:typo3_9:
<<: *testing
variables:
DOCKER_TAG: 7.2-alpine3.7
TYPO3_VERSION: typo3/cms-core:^9
NIMUT_TESTFRAMEWORK_VERSION: ^4
COMPOSER_STABILITY: stable
# Build in PHP 7.2 and TYPO3 (current master)
test:php72:typo3_master:
<<: *testing
variables:
DOCKER_TAG: 7.2-alpine3.7
TYPO3_VERSION: typo3/minimal:dev-master
NIMUT_TESTFRAMEWORK_VERSION: ^4
COMPOSER_STABILITY: dev
# TER Upload when tagging in master branch
# The variables T3O_USERNAME and T3O_PASSWORD should be set in GitLab
ter-upload:
image: webdevops/php
stage: release
only:
- tags
before_script:
- php -r "copy('https://getcomposer.org/composer.phar', 'composer.phar');"
- php composer.phar config cache-dir /cache/composer
script:
- php composer.phar require namelesscoder/typo3-repository-client
- >
if [ -n "$CI_COMMIT_TAG" ] && [ -n "$T3O_USERNAME" ] && [ -n "$T3O_PASSWORD" ]; then
echo -e "Preparing upload of release ${CI_COMMIT_TAG} to TER\n"
# Cleanup before we upload
git reset --hard HEAD && git clean -fx
# Upload
TAG_MESSAGE=`git tag -n10 -l $CI_COMMIT_TAG | sed 's/^[0-9.]*[ ]*//g'`
echo "Uploading release ${CI_COMMIT_TAG} to TER"
.Build/bin/upload . "$T3O_USERNAME" "$T3O_PASSWORD" "$TAG_MESSAGE"
fi;
.Build/ export-ignore
cache:
key: "$CI_COMMIT_REF_NAME-$CI_JOB_STAGE"
paths:
- /cache/composer
stages:
- test
- release
.unit_tests: &unit_tests
stage: test
image: php:$PHP_VERSION
before_script:
- apt-get update && apt-get install -y bash
- bash ./.Build/Tests/docker_install.sh > /dev/null
- composer install --ignore-platform-reqs
- composer require typo3/minimal:$TYPO3_VERSION typo3/cms-lowlevel:$TYPO3_VERSION nimut/testing-framework:$TESTFW_VERSION -W
script:
- composer test:unit
artifacts:
reports:
junit: build/*-report.xml
"Unit Tests with PHP 7.4":
<<: *unit_tests
variables:
PHP_VERSION: '7.4'
TYPO3_VERSION: '^11.5'
TESTFW_VERSION: '^6.0'
"Unit Tests with PHP 8.0":
<<: *unit_tests
variables:
PHP_VERSION: '8.0'
TYPO3_VERSION: '^11.5'
TESTFW_VERSION: '^6.0'
"PHP CS Fixer":
stage: test
image: ekreative/php-cs-fixer:3
script:
- php-cs-fixer fix --dry-run --config=.Build/Tests/php-cs-fixer.php --diff --format junit > .Build/php-cs-fixer.xml
artifacts:
reports:
junit:
- .Build/php-cs-fixer.xml
"PHP Rector":
stage: test
image: composer:2
before_script:
- composer install --ignore-platform-reqs
script:
- composer test:rector
"Publish new version to TER":
stage: release
image: composer:2
variables:
TYPO3_EXTENSION_KEY: "be_secure_pw"
only:
- tags
before_script:
- composer global require typo3/tailor
script:
- >
if [ -n "$CI_COMMIT_TAG" ] && [ -n "$TYPO3_API_TOKEN" ] && [ -n "$TYPO3_EXTENSION_KEY" ]; then
echo -e "Preparing upload of release ${CI_COMMIT_TAG} to TER\n"
# Cleanup before we upload
git reset --hard HEAD && git clean -fx
# Upload
TAG_MESSAGE=`git tag -n10 -l $CI_COMMIT_TAG | sed 's/^[0-9.]*[ ]*//g'`
echo "Uploading release ${CI_COMMIT_TAG} to TER"
/tmp/vendor/bin/tailor ter:publish --comment "$TAG_MESSAGE" "$CI_COMMIT_TAG" "$TYPO3_EXTENSION_KEY"
fi;
<?php
namespace SpoonerWeb\BeSecurePw\Configuration;
/*
* This file is part of a TYPO3 extension.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
/**
* Class ExtensionConfiguration
*
* @author Thomas Löffler <loeffler@spooner-web.de>
*/
class ExtensionConfiguration
{
public static function getExtensionConfig(): ?array
{
return \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Core\Configuration\ExtensionConfiguration::class)
->get('be_secure_pw');
}
}
\ No newline at end of file
<?php
declare(strict_types=1);
namespace SpoonerWeb\BeSecurePw\Evaluation;
/**
* This file is part of the TYPO3 CMS project.
* This file is part of the TYPO3 CMS extension "be_secure_pw".
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
......@@ -14,25 +17,36 @@ namespace SpoonerWeb\BeSecurePw\Evaluation;
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Utility;
use SpoonerWeb\BeSecurePw\Service\PawnedPasswordService;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;
use TYPO3\CMS\Core\Localization\LanguageServiceFactory;
use TYPO3\CMS\Core\Log\LogManager;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Messaging\FlashMessageQueue;
use TYPO3\CMS\Core\Messaging\FlashMessage;
use TYPO3\CMS\Saltedpasswords\Utility\SaltedPasswordsUtility;
use TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashFactory;
use TYPO3\CMS\Core\Messaging\FlashMessageService;
use TYPO3\CMS\Core\SingletonInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Class PasswordEvaluator
*
* @author Thomas Loeffler <loeffler@spooner-web.de>
*/
class PasswordEvaluator
class PasswordEvaluator implements SingletonInterface
{
const PATTERN_LOWER_CHAR = '/[a-z]/';
const PATTERN_CAPITAL_CHAR = '/[A-Z]/';
const PATTERN_DIGIT = '/[0-9]/';
const PATTERN_SPECIAL_CHAR = '/[^0-9a-z]/i';
public const PATTERN_LOWER_CHAR = '/[a-z]/';
public const PATTERN_CAPITAL_CHAR = '/[A-Z]/';
public const PATTERN_DIGIT = '/[0-9]/';
public const PATTERN_SPECIAL_CHAR = '/[^0-9a-z]/i';
protected LanguageServiceFactory $languageServiceFactory;
protected BackendUserAuthentication $backendUser;
public function __construct(LanguageServiceFactory $languageServiceFactory, BackendUserAuthentication $backendUser)
{
$this->languageServiceFactory = $languageServiceFactory;
$this->backendUser = $backendUser;
}
/**
* This function just return the field value as it is. No transforming,
......@@ -50,49 +64,39 @@ class PasswordEvaluator
*
* @param string $value The value that has to be checked.
* @param string $is_in Is-In String
* @param integer $set Determines if the field can be set (value correct) or not
* @param boolean $storeFlashMessageInSession Used only for phpunit issues
* @param int $set Determines if the field can be set (value correct) or not
* @param bool $storeFlashMessageInSession Used only for phpunit issues
* @return string The new value of the field
* @throws \TYPO3\CMS\Core\Crypto\PasswordHashing\InvalidPasswordHashException
* @throws \TYPO3\CMS\Core\Configuration\Exception\ExtensionConfigurationExtensionNotConfiguredException
* @throws \TYPO3\CMS\Core\Configuration\Exception\ExtensionConfigurationPathDoesNotExistException
*/
public function evaluateFieldValue(
string $value,
$is_in,
string $is_in,
int &$set,
bool $storeFlashMessageInSession = true
): string {
$extConf = GeneralUtility::makeInstance(ExtensionConfiguration::class)->get('be_secure_pw');
$extConf = \SpoonerWeb\BeSecurePw\Configuration\ExtensionConfiguration::getExtensionConfig();
/** @var \TYPO3\CMS\Core\Log\Logger $logger */
$logger = GeneralUtility::makeInstance(LogManager::class)->getLogger(__CLASS__);
/** @var \TYPO3\CMS\Core\DataHandling\DataHandler $tce */
$tce = Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Core\DataHandling\DataHandler::class);
$tce->BE_USER = $GLOBALS['BE_USER'];
$languageService = $this->languageServiceFactory->create($this->backendUser->uc['lang'] ?? 'default');
$languageService->includeLLFile('EXT:be_secure_pw/Resources/Private/Language/locallang.xlf');
/** @var \TYPO3\CMS\Core\Log\Logger $logger */
$logger = Utility\GeneralUtility::makeInstance(LogManager::class)->getLogger(__CLASS__);
// get the languages from ext
/** @var \TYPO3\CMS\Lang\LanguageService $languageService */
$languageService = Utility\GeneralUtility::makeInstance(LanguageService::class);
$languageService->init($tce->BE_USER->uc['lang']);
$languageService->includeLLFile('EXT:be_secure_pw/Resources/Private/Language/locallang.xml');
/** @var \TYPO3\CMS\Core\Messaging\FlashMessageQueue $flashMessageQueue */
$flashMessageQueue = Utility\GeneralUtility::makeInstance(
FlashMessageQueue::class,
'core.template.flashMessages'
);
$set = true;
$set = 1;
$messages = [];
// check for password length
$passwordLength = (int)$extConf['passwordLength'];
if ($extConf['passwordLength'] && $passwordLength && strlen($value) < $extConf['passwordLength']) {
$passwordLength = $extConf['passwordLength'] ?? 0;
$passwordLengthValue = (int)$passwordLength;
if ($passwordLengthValue && strlen($value) < $passwordLengthValue) {
/* password too short */
$set = false;
$logger->error(
sprintf($languageService->getLL('shortPassword'), $passwordLength)
);
$messages[] = sprintf($languageService->getLL('shortPassword'), $passwordLength);
$set = 0;
$passwordToShortString = $languageService->getLL('shortPassword') ?? '';
$logger->error(sprintf($passwordToShortString, $passwordLengthValue));
$messages[] = sprintf($passwordToShortString, $passwordLengthValue);
}
$counter = 0;
......@@ -106,7 +110,8 @@ class PasswordEvaluator
];
foreach ($checks as $index => $pattern) {
if ($extConf[$index]) {
$checkActive = $extConf[$index] ?? false;
if ($checkActive) {
if (preg_match($pattern, $value) > 0) {
$counter++;
} else {
......@@ -115,18 +120,21 @@ class PasswordEvaluator
}
}
if ($counter < $extConf['patterns']) {
$patterns = $extConf['patterns'] ?? 0;