diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..f5f7003 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,240 @@ +[*] +charset = utf-8 +end_of_line = lf +indent_size = 2 +indent_style = space +insert_final_newline = true +max_line_length = 120 +tab_width = 2 +ij_continuation_indent_size = 2 +ij_visual_guides = 80,120 + +[{*.php}] +ij_php_align_assignments = false +ij_php_align_class_constants = false +ij_php_align_enum_cases = false +ij_php_align_group_field_declarations = false +ij_php_align_inline_comments = false +ij_php_align_key_value_pairs = false +ij_php_align_match_arm_bodies = false +ij_php_align_multiline_array_initializer_expression = false +ij_php_align_multiline_binary_operation = false +ij_php_align_multiline_chained_methods = false +ij_php_align_multiline_extends_list = false +ij_php_align_multiline_for = true +ij_php_align_multiline_parameters = false +ij_php_align_multiline_parameters_in_calls = false +ij_php_align_multiline_ternary_operation = false +ij_php_align_named_arguments = false +ij_php_align_phpdoc_comments = false +ij_php_align_phpdoc_param_names = false +ij_php_anonymous_brace_style = end_of_line +ij_php_api_weight = 28 +ij_php_array_initializer_new_line_after_left_brace = true +ij_php_array_initializer_right_brace_on_new_line = true +ij_php_array_initializer_wrap = on_every_item +ij_php_assignment_wrap = normal +ij_php_attributes_wrap = normal +ij_php_author_weight = 28 +ij_php_binary_operation_sign_on_next_line = false +ij_php_binary_operation_wrap = normal +ij_php_blank_lines_after_class_header = 1 +ij_php_blank_lines_after_function = 1 +ij_php_blank_lines_after_imports = 1 +ij_php_blank_lines_after_opening_tag = 0 +ij_php_blank_lines_after_package = 1 +ij_php_blank_lines_around_class = 1 +ij_php_blank_lines_around_constants = 1 +ij_php_blank_lines_around_enum_cases = 0 +ij_php_blank_lines_around_field = 1 +ij_php_blank_lines_around_method = 1 +ij_php_blank_lines_before_class_end = 1 +ij_php_blank_lines_before_imports = 1 +ij_php_blank_lines_before_method_body = 0 +ij_php_blank_lines_before_package = 1 +ij_php_blank_lines_before_return_statement = 1 +ij_php_blank_lines_between_imports = 0 +ij_php_block_brace_style = end_of_line +ij_php_call_parameters_new_line_after_left_paren = true +ij_php_call_parameters_right_paren_on_new_line = true +ij_php_call_parameters_wrap = on_every_item +ij_php_catch_on_new_line = true +ij_php_category_weight = 28 +ij_php_class_brace_style = end_of_line +ij_php_comma_after_last_argument = false +ij_php_comma_after_last_array_element = true +ij_php_comma_after_last_closure_use_var = false +ij_php_comma_after_last_match_arm = false +ij_php_comma_after_last_parameter = false +ij_php_concat_spaces = true +ij_php_copyright_weight = 28 +ij_php_deprecated_weight = 4 +ij_php_do_while_brace_force = always +ij_php_else_if_style = as_is +ij_php_else_on_new_line = true +ij_php_example_weight = 28 +ij_php_extends_keyword_wrap = off +ij_php_extends_list_wrap = off +ij_php_fields_default_visibility = private +ij_php_filesource_weight = 28 +ij_php_finally_on_new_line = true +ij_php_for_brace_force = always +ij_php_for_statement_new_line_after_left_paren = false +ij_php_for_statement_right_paren_on_new_line = false +ij_php_for_statement_wrap = off +ij_php_force_empty_methods_in_one_line = false +ij_php_force_short_declaration_array_style = true +ij_php_getters_setters_naming_style = camel_case +ij_php_getters_setters_order_style = getters_first +ij_php_global_weight = 28 +ij_php_group_use_wrap = on_every_item +ij_php_if_brace_force = always +ij_php_if_lparen_on_next_line = false +ij_php_if_rparen_on_next_line = false +ij_php_ignore_weight = 28 +ij_php_import_sorting = alphabetic +ij_php_indent_break_from_case = true +ij_php_indent_case_from_switch = true +ij_php_indent_code_in_php_tags = false +ij_php_internal_weight = 28 +ij_php_keep_blank_lines_after_lbrace = 1 +ij_php_keep_blank_lines_before_right_brace = 1 +ij_php_keep_blank_lines_in_code = 1 +ij_php_keep_blank_lines_in_declarations = 1 +ij_php_keep_control_statement_in_one_line = false +ij_php_keep_first_column_comment = false +ij_php_keep_indents_on_empty_lines = false +ij_php_keep_line_breaks = false +ij_php_keep_rparen_and_lbrace_on_one_line = true +ij_php_keep_simple_classes_in_one_line = false +ij_php_keep_simple_methods_in_one_line = false +ij_php_lambda_brace_style = end_of_line +ij_php_license_weight = 28 +ij_php_line_comment_add_space = false +ij_php_line_comment_at_first_column = true +ij_php_link_weight = 28 +ij_php_lower_case_boolean_const = false +ij_php_lower_case_keywords = true +ij_php_lower_case_null_const = false +ij_php_method_brace_style = end_of_line +ij_php_method_call_chain_wrap = on_every_item +ij_php_method_parameters_new_line_after_left_paren = true +ij_php_method_parameters_right_paren_on_new_line = true +ij_php_method_parameters_wrap = on_every_item +ij_php_method_weight = 28 +ij_php_modifier_list_wrap = false +ij_php_multiline_chained_calls_semicolon_on_new_line = true +ij_php_namespace_brace_style = 1 +ij_php_new_line_after_php_opening_tag = true +ij_php_null_type_position = in_the_end +ij_php_package_weight = 28 +ij_php_param_weight = 1 +ij_php_parameters_attributes_wrap = normal +ij_php_parentheses_expression_new_line_after_left_paren = false +ij_php_parentheses_expression_right_paren_on_new_line = false +ij_php_phpdoc_blank_line_before_tags = true +ij_php_phpdoc_blank_lines_around_parameters = true +ij_php_phpdoc_keep_blank_lines = true +ij_php_phpdoc_param_spaces_between_name_and_description = 1 +ij_php_phpdoc_param_spaces_between_tag_and_type = 1 +ij_php_phpdoc_param_spaces_between_type_and_name = 1 +ij_php_phpdoc_use_fqcn = true +ij_php_phpdoc_wrap_long_lines = true +ij_php_place_assignment_sign_on_next_line = false +ij_php_place_parens_for_constructor = 1 +ij_php_property_read_weight = 28 +ij_php_property_weight = 28 +ij_php_property_write_weight = 28 +ij_php_return_type_on_new_line = false +ij_php_return_weight = 2 +ij_php_see_weight = 5 +ij_php_since_weight = 28 +ij_php_sort_phpdoc_elements = true +ij_php_space_after_colon = true +ij_php_space_after_colon_in_enum_backed_type = true +ij_php_space_after_colon_in_named_argument = true +ij_php_space_after_colon_in_return_type = true +ij_php_space_after_comma = true +ij_php_space_after_for_semicolon = true +ij_php_space_after_quest = true +ij_php_space_after_type_cast = true +ij_php_space_after_unary_not = false +ij_php_space_before_array_initializer_left_brace = false +ij_php_space_before_catch_keyword = true +ij_php_space_before_catch_left_brace = true +ij_php_space_before_catch_parentheses = true +ij_php_space_before_class_left_brace = true +ij_php_space_before_closure_left_parenthesis = true +ij_php_space_before_colon = true +ij_php_space_before_colon_in_enum_backed_type = false +ij_php_space_before_colon_in_named_argument = false +ij_php_space_before_colon_in_return_type = false +ij_php_space_before_comma = false +ij_php_space_before_do_left_brace = true +ij_php_space_before_else_keyword = true +ij_php_space_before_else_left_brace = true +ij_php_space_before_finally_keyword = true +ij_php_space_before_finally_left_brace = true +ij_php_space_before_for_left_brace = true +ij_php_space_before_for_parentheses = true +ij_php_space_before_for_semicolon = false +ij_php_space_before_if_left_brace = true +ij_php_space_before_if_parentheses = true +ij_php_space_before_method_call_parentheses = false +ij_php_space_before_method_left_brace = true +ij_php_space_before_method_parentheses = false +ij_php_space_before_quest = true +ij_php_space_before_short_closure_left_parenthesis = false +ij_php_space_before_switch_left_brace = true +ij_php_space_before_switch_parentheses = true +ij_php_space_before_try_left_brace = true +ij_php_space_before_unary_not = false +ij_php_space_before_while_keyword = true +ij_php_space_before_while_left_brace = true +ij_php_space_before_while_parentheses = true +ij_php_space_between_ternary_quest_and_colon = false +ij_php_spaces_around_additive_operators = true +ij_php_spaces_around_arrow = false +ij_php_spaces_around_assignment_in_declare = true +ij_php_spaces_around_assignment_operators = true +ij_php_spaces_around_bitwise_operators = true +ij_php_spaces_around_equality_operators = true +ij_php_spaces_around_logical_operators = true +ij_php_spaces_around_multiplicative_operators = true +ij_php_spaces_around_null_coalesce_operator = true +ij_php_spaces_around_pipe_in_union_type = false +ij_php_spaces_around_relational_operators = true +ij_php_spaces_around_shift_operators = true +ij_php_spaces_around_unary_operator = false +ij_php_spaces_around_var_within_brackets = false +ij_php_spaces_within_array_initializer_braces = false +ij_php_spaces_within_brackets = false +ij_php_spaces_within_catch_parentheses = false +ij_php_spaces_within_for_parentheses = false +ij_php_spaces_within_if_parentheses = false +ij_php_spaces_within_method_call_parentheses = false +ij_php_spaces_within_method_parentheses = false +ij_php_spaces_within_parentheses = false +ij_php_spaces_within_short_echo_tags = true +ij_php_spaces_within_switch_parentheses = false +ij_php_spaces_within_while_parentheses = false +ij_php_special_else_if_treatment = false +ij_php_subpackage_weight = 28 +ij_php_ternary_operation_signs_on_next_line = true +ij_php_ternary_operation_wrap = on_every_item +ij_php_throws_weight = 3 +ij_php_todo_weight = 6 +ij_php_treat_multiline_arrays_and_lambdas_multiline = false +ij_php_unknown_tag_weight = 28 +ij_php_upper_case_boolean_const = true +ij_php_upper_case_null_const = true +ij_php_uses_weight = 28 +ij_php_var_weight = 0 +ij_php_variable_naming_style = camel_case +ij_php_version_weight = 28 +ij_php_while_brace_force = always +ij_php_while_on_new_line = false + +[{*.neon,*.neon.dist,*neon.template}] +indent_style = tab +tab_width = 4 diff --git a/.github/workflows/phpcs.yml b/.github/workflows/phpcs.yml new file mode 100644 index 0000000..a0290ee --- /dev/null +++ b/.github/workflows/phpcs.yml @@ -0,0 +1,43 @@ +name: PHP_CodeSniffer + +on: + pull_request: + paths: + - '**.php' + - tools/phpcs/composer.json + - phpcs.xml.dist + - .github/workflows/phpcs.yml + +jobs: + phpcs: + runs-on: ubuntu-latest + name: PHP_CodeSniffer + + steps: + - uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 8.3 + coverage: none + tools: cs2pr + env: + fail-fast: true + + - name: Get composer cache directory + id: composer-cache + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + + - name: Cache dependencies + uses: actions/cache@v4 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('tools/phpcs/composer.json') }} + restore-keys: ${{ runner.os }}-composer- + + - name: Install dependencies + run: composer composer-phpcs -- update --no-progress --prefer-dist + + - name: Run PHP_CodeSniffer + run: composer phpcs -- -q --report=checkstyle | cs2pr diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml new file mode 100644 index 0000000..7d62b82 --- /dev/null +++ b/.github/workflows/phpstan.yml @@ -0,0 +1,55 @@ +name: PHPStan + +on: + pull_request: + paths: + - '**.php' + - composer.json + - tools/phpstan/composer.json + - ci/composer.json + - phpstan.ci.neon + - phpstan.neon.dist + - .github/workflows/phpstan.yml + +jobs: + phpstan: + runs-on: ubuntu-latest + strategy: + matrix: + php-versions: ['8.1', '8.4'] + prefer: ['prefer-stable', 'prefer-lowest'] + name: PHPStan with PHP ${{ matrix.php-versions }} ${{ matrix.prefer }} + + steps: + - uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-versions }} + coverage: none + env: + fail-fast: true + + - name: Get composer cache directory + id: composer-cache + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + + - name: Cache dependencies + uses: actions/cache@v4 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ matrix.prefer }}-${{ hashFiles('**/composer.json') }} + restore-keys: ${{ runner.os }}-composer-${{ matrix.prefer }}- + + - name: Install dependencies + run: | + composer update --no-progress --prefer-dist --${{ matrix.prefer }} --optimize-autoloader && + composer composer-phpunit -- update --no-progress --prefer-dist && + composer composer-phpstan -- update --no-progress --prefer-dist --optimize-autoloader && + composer --working-dir=ci update --no-progress --prefer-dist --${{ matrix.prefer }} --ignore-platform-req=ext-gd && + git clone --depth=1 https://lab.civicrm.org/extensions/action-provider.git ../action-provider && + git clone --depth=1 https://github.com/Project60/org.project60.sepa.git ../org.project60.sepa + + - name: Run PHPStan + run: composer phpstan -- analyse -c phpstan.ci.neon diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml new file mode 100644 index 0000000..c356341 --- /dev/null +++ b/.github/workflows/phpunit.yml @@ -0,0 +1,38 @@ +name: PHPUnit + +on: + pull_request: + paths: + - '**.php' + - composer.json + - tools/phpunit/composer.json + - phpunit.xml.dist + - tests/docker-prepare.sh + - .github/workflows/phpunit.yml + +env: + # On github CI machine creating the "/vendor" volume fails otherwise with: read-only file system: unknown + BIND_VOLUME_PERMISSIONS: rw + +jobs: + phpunit: + runs-on: ubuntu-latest + strategy: + matrix: + civicrm-image-tags: [ 'drupal', '5.54-drupal-php8.1' ] + name: PHPUnit with Docker image michaelmcandrew/civicrm:${{ matrix.civicrm-image-tags }} + env: + CIVICRM_IMAGE_TAG: ${{ matrix.civicrm-image-tags }} + + steps: + - uses: actions/checkout@v4 + - name: Pull images + run: docker compose -f tests/docker-compose.yml pull --quiet + - name: Start containers + run: docker compose -f tests/docker-compose.yml up -d + - name: Prepare environment + run: docker compose -f tests/docker-compose.yml exec civicrm sites/default/files/civicrm/ext/org.project60.bic/tests/docker-prepare.sh + - name: Run PHPUnit + run: docker compose -f tests/docker-compose.yml exec civicrm sites/default/files/civicrm/ext/org.project60.bic/tests/docker-phpunit.sh + - name: Remove containers + run: docker compose -f tests/docker-compose.yml down -v diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..62dffec --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +/.phpcs.cache +/.phpunit.result.cache +/.phpstan/ +/ci/composer.lock +/ci/vendor/ +/composer.lock +/phpstan.neon +/tools/*/vendor/ +/tools/*/composer.lock +/vendor/ diff --git a/CRM/Bic/Page/BicList.php b/CRM/Bic/Page/BicList.php index 2c7d63a..cb9effa 100644 --- a/CRM/Bic/Page/BicList.php +++ b/CRM/Bic/Page/BicList.php @@ -1,33 +1,34 @@ $count) { + foreach ($stats['values'] as $country => $count) { $countries[] = $country; } - + // Get country names - $country_names = null; - if($countries) { + $country_names = NULL; + if ($countries) { $config = CRM_Core_Config::singleton(); - $country_names = array(); + $country_names = []; $id2code = CRM_Core_PseudoConstant::countryIsoCode(); $default_country = $id2code[$config->defaultContactCountry] ?? ''; $code2id = array_flip($id2code); - $id2country = CRM_Core_PseudoConstant::country(false, false); + $id2country = CRM_Core_PseudoConstant::country(FALSE, FALSE); foreach ($countries as $code) { $country_id = $code2id[$code]; $country_name = $id2country[$country_id]; @@ -39,8 +40,9 @@ function run() { $this->assign('countries', $countries); $this->assign('country_names', $country_names); $this->assign('default_country', $default_country); - $this->assign('show_message', true); - + $this->assign('show_message', TRUE); + parent::run(); } + } diff --git a/CRM/Bic/Page/Config.php b/CRM/Bic/Page/Config.php index 1796ef5..ed8193a 100644 --- a/CRM/Bic/Page/Config.php +++ b/CRM/Bic/Page/Config.php @@ -14,11 +14,13 @@ | written permission from the original author(s). | +--------------------------------------------------------*/ -require_once 'CRM/Core/Page.php'; +declare(strict_types = 1); + use CRM_Bic_ExtensionUtil as E; class CRM_Bic_Page_Config extends CRM_Core_Page { - function run() { + + public function run() { CRM_Utils_System::setTitle(E::ts('Available Banks')); @@ -28,13 +30,14 @@ function run() { foreach ($countries as $country) { if (isset($stats['values'][$country])) { $total_count += $stats['values'][$country]; - } else { + } + else { $stats['values'][$country] = 0; } } // gather the names - $country_names = array(); + $country_names = []; $config = CRM_Core_Config::singleton(); @@ -56,4 +59,5 @@ function run() { parent::run(); } + } diff --git a/CRM/Bic/Parser/AT.php b/CRM/Bic/Parser/AT.php index 71e63e7..054bf9d 100644 --- a/CRM/Bic/Parser/AT.php +++ b/CRM/Bic/Parser/AT.php @@ -14,16 +14,16 @@ | written permission from the original author(s). | +--------------------------------------------------------*/ -use CRM_Bic_ExtensionUtil as E; +declare(strict_types = 1); -require_once 'CRM/Bic/Parser/Parser.php'; +use CRM_Bic_ExtensionUtil as E; /** * Abstract class defining the basis for national bank info parsers */ class CRM_Bic_Parser_AT extends CRM_Bic_Parser_Parser { - static $page_url = 'https://www.oenb.at/docroot/downloads_observ/sepa-zv-vz_gesamt.csv'; + public static $page_url = 'https://www.oenb.at/docroot/downloads_observ/sepa-zv-vz_gesamt.csv'; public function update() { // first, download the page @@ -34,7 +34,7 @@ public function update() { } $banks = []; - $headers = null; + $headers = NULL; // iterate CSV records foreach ($data as $line) { @@ -43,16 +43,20 @@ public function update() { $fields = str_getcsv($line, ';'); // skip some stuff: preamble - if (count($fields) < 20) continue; + if (count($fields) < 20) { + continue; + } if ($headers === NULL) { // first 'real' line should be header $headers = $fields; - if ( !in_array('Identnummer', $headers) + if (!in_array('Identnummer', $headers) || !in_array('Bankleitzahl', $headers) || !in_array('SWIFT-Code', $headers)) { - return $this->createParserOutdatedError(E::ts("Source file doesn't contain Identnummer/Bankleitzahl/SWIFT-Code")); + return $this->createParserOutdatedError( + E::ts("Source file doesn't contain Identnummer/Bankleitzahl/SWIFT-Code") + ); } } else { @@ -60,18 +64,20 @@ public function update() { $data_set = array_combine($headers, $fields); // we will only process banks with a BIC/SWIFT Code - if ( empty($data_set['Identnummer']) + if (empty($data_set['Identnummer']) || empty($data_set['Bankleitzahl']) || empty($data_set['SWIFT-Code']) - ) continue; + ) { + continue; + } // compile data set - $banks[$data_set['Bankleitzahl']] = array( - 'value' => $data_set['Bankleitzahl'], - 'name' => $data_set['SWIFT-Code'], - 'label' => $data_set['Bankenname'] ?? 'Unknown', - 'description' => $this->getDescription($data_set) - ); + $banks[$data_set['Bankleitzahl']] = [ + 'value' => $data_set['Bankleitzahl'], + 'name' => $data_set['SWIFT-Code'], + 'label' => $data_set['Bankenname'] ?? 'Unknown', + 'description' => $this->getDescription($data_set), + ]; } } @@ -88,8 +94,7 @@ public function update() { * @return string * the description */ - protected function getDescription($data_set) - { + protected function getDescription($data_set) { $description = ''; if (!empty($data_set['Straße'])) { $description .= $data_set['Straße'] . ','; @@ -103,12 +108,15 @@ protected function getDescription($data_set) return $description; } - /* - * Extracts the National Bank Identifier from an Austrian IBAN. - */ + /** + * + * Extracts the National Bank Identifier from an Austrian IBAN. + * + */ public function extractNBIDfromIBAN($iban) { - return array( - substr($iban, 4, 5), - ); + return [ + substr($iban, 4, 5), + ]; } + } diff --git a/CRM/Bic/Parser/BE.php b/CRM/Bic/Parser/BE.php index a54be91..e874e96 100644 --- a/CRM/Bic/Parser/BE.php +++ b/CRM/Bic/Parser/BE.php @@ -14,18 +14,15 @@ | written permission from the original author(s). | +--------------------------------------------------------*/ -// Include Composer's autoloader file. -require_once __DIR__ . '/../../../vendor/autoload.php'; - -require_once 'CRM/Bic/Parser/Parser.php'; +declare(strict_types = 1); /** * Abstract class defining the basis for national bank info parsers */ class CRM_Bic_Parser_BE extends CRM_Bic_Parser_Parser { - static $page_url = 'https://www.nbb.be/doc/be/be/protocol/r_fulllist_of_codes_current.xls'; - static $country_code = 'BE'; + public static $page_url = 'https://www.nbb.be/doc/be/be/protocol/r_fulllist_of_codes_current.xls'; + public static $country_code = 'BE'; public function update() { // First, download the file @@ -43,8 +40,8 @@ public function update() { $excel_reader = \PhpOffice\PhpSpreadsheet\IOFactory::createReaderForFile($file_name); // Set reader options - $excel_reader->setReadDataOnly(true); - $excel_reader->setLoadSheetsOnly(["Q_FULL_LIST_XLS_REPORT"]); + $excel_reader->setReadDataOnly(TRUE); + $excel_reader->setLoadSheetsOnly(['Q_FULL_LIST_XLS_REPORT']); // Read Excel file $excel_object = $excel_reader->load($file_name); @@ -52,33 +49,37 @@ public function update() { // Process Excel data $skip_lines = 2; - $banks[] = array(); - foreach($excel_rows as $excel_row) { + $banks[] = []; + foreach ($excel_rows as $excel_row) { $skip_lines -= 1; - if ($skip_lines >= 0) continue; + if ($skip_lines >= 0) { + continue; + } // Process row $bic = str_replace(' ', '', $excel_row[1]); - if (in_array(strtolower($bic), array('nav', 'nap', '-')) || substr($bic, 0, 4)=='VRIJ') { + if (in_array(strtolower($bic), ['nav', 'nap', '-']) || substr($bic, 0, 4) == 'VRIJ') { // these are actually dummy entries continue; } // compile bank name $bank_name = ''; - for ($i=2; $i<5; $i++) { + for ($i = 2; $i < 5; $i++) { $localized_name = trim($excel_row[$i]); if (!empty($localized_name)) { - if (!empty($bank_name)) $bank_name .= ' / '; + if (!empty($bank_name)) { + $bank_name .= ' / '; + } $bank_name .= $localized_name; } } - $bank = array( + $bank = [ 'value' => $excel_row[0], 'name' => $bic, 'label' => $bank_name, 'description' => '', - ); + ]; $banks[] = $bank; } @@ -92,13 +93,15 @@ public function update() { return $this->updateEntries(CRM_Bic_Parser_BE::$country_code, $banks); } - /* + /** + * * Extracts the National Bank Identifier from an Belgium IBAN. + * */ public function extractNBIDfromIBAN($iban) { - return array( + return [ substr($iban, 4, 3), - ); + ]; } } diff --git a/CRM/Bic/Parser/CH.php b/CRM/Bic/Parser/CH.php index 7bca142..99182be 100644 --- a/CRM/Bic/Parser/CH.php +++ b/CRM/Bic/Parser/CH.php @@ -14,18 +14,18 @@ | written permission from the original author(s). | +--------------------------------------------------------*/ -require_once 'CRM/Bic/Parser/Parser.php'; +declare(strict_types = 1); /** * Abstract class defining the basis for national bank info parsers */ class CRM_Bic_Parser_CH extends CRM_Bic_Parser_Parser { - static $page_url = 'https://api.six-group.com/api/epcd/bankmaster/v2/public/downloads/bcbankenstamm'; + public static $page_url = 'https://api.six-group.com/api/epcd/bankmaster/v2/public/downloads/bcbankenstamm'; public function update() { // first, download the page - $banks = array(); + $banks = []; $data = $this->downloadFile(CRM_Bic_Parser_CH::$page_url); if (empty($data)) { return $this->createParserOutdatedError(ts("Couldn't download data set")); @@ -40,30 +40,35 @@ public function update() { $address = substr($line, 184, 4) . ' ' . trim(substr($line, 194, 35)); // we only want branches with BICs - if (empty($bic)) continue; + if (empty($bic)) { + continue; + } // encode names - $name = mb_convert_encoding($name, 'UTF-8', 'ISO-8859-1'); + $name = mb_convert_encoding($name, 'UTF-8', 'ISO-8859-1'); $address = mb_convert_encoding($address, 'UTF-8', 'ISO-8859-1'); - - $banks[$bc_code] = array( + + $banks[$bc_code] = [ 'value' => $bc_code, 'name' => $bic, 'label' => $name, 'description' => $address, - ); + ]; } // // finally, update DB return $this->updateEntries('CH', $banks); } - /* + /** + * * Extracts the National Bank Identifier from an Austrian IBAN. + * */ public function extractNBIDfromIBAN($iban) { - return array( + return [ substr($iban, 4, 5), - ); - } + ]; + } + } diff --git a/CRM/Bic/Parser/DE.php b/CRM/Bic/Parser/DE.php index ea0f6c1..04b89b9 100644 --- a/CRM/Bic/Parser/DE.php +++ b/CRM/Bic/Parser/DE.php @@ -15,75 +15,77 @@ | written permission from the original author(s). | +--------------------------------------------------------*/ -// Include Composer's autoloader file. -require_once __DIR__ . '/../../../vendor/autoload.php'; - -require_once 'CRM/Bic/Parser/Parser.php'; +declare(strict_types = 1); /** * Abstract class defining the basis for national bank info parsers */ class CRM_Bic_Parser_DE extends CRM_Bic_Parser_Parser { - static $page_url = 'https://www.bundesbank.de/resource/blob/926192/07561adcd024c5752c46afa317d554af/mL/blz-aktuell-csv-data.csv'; - - static $country_code = 'DE'; - - public function update() - { - // First, download the file - $file_name = sys_get_temp_dir() . '/DE-banks.csv'; - $downloaded_file = $this->downloadFile(CRM_Bic_Parser_DE::$page_url); - - if (empty($downloaded_file)) { - return $this->createParserOutdatedError(ts("Couldn't download data file")); - } - - // store file - file_put_contents($file_name, $downloaded_file); - unset($downloaded_file); + public static $page_url = + 'https://www.bundesbank.de/resource/blob/926192/07561adcd024c5752c46afa317d554af/mL/blz-aktuell-csv-data.csv'; - // Open and read CSV file - if (($handle = fopen($file_name, "r")) === false) { - return $this->createParserOutdatedError(ts("Couldn't open data file")); - } + public static $country_code = 'DE'; - // Skip header row - fgetcsv($handle, 1000, ';'); + public function update() { + // First, download the file + $file_name = sys_get_temp_dir() . '/DE-banks.csv'; + $downloaded_file = $this->downloadFile(CRM_Bic_Parser_DE::$page_url); - $banks = []; - while (($data = fgetcsv($handle, 1000, ';')) !== false) { - // skip entries with no bic - if (empty($data[7])) continue; - if ($data[1] != 1) continue; - - // Process row - $bank = array( - 'value' => $data[0], - 'name' => mb_convert_encoding($data[7], 'UTF-8', 'ISO-8859-1'), - 'label' => mb_convert_encoding($data[5], 'UTF-8', 'ISO-8859-1'), - 'description' => mb_convert_encoding($data[3], 'UTF-8', 'ISO-8859-1') . ' ' - . mb_convert_encoding($data[4], 'UTF-8', 'ISO-8859-1'), - ); - - $banks[] = $bank; - } + if (empty($downloaded_file)) { + return $this->createParserOutdatedError(ts("Couldn't download data file")); + } - fclose($handle); - unlink($file_name); + // store file + file_put_contents($file_name, $downloaded_file); + unset($downloaded_file); - // Finally, update DB - return $this->updateEntries(CRM_Bic_Parser_DE::$country_code, $banks); + // Open and read CSV file + if (($handle = fopen($file_name, 'r')) === FALSE) { + return $this->createParserOutdatedError(ts("Couldn't open data file")); } - /* - * Extracts the National Bank Identifier from an IBAN. - */ - public function extractNBIDfromIBAN($iban) - { - return array( - substr($iban, 4, 8), - ); + // Skip header row + fgetcsv($handle, 1000, ';'); + + $banks = []; + while (($data = fgetcsv($handle, 1000, ';')) !== FALSE) { + // skip entries with no bic + if (empty($data[7])) { + continue; + } + if ($data[1] != 1) { + continue; + } + + // Process row + $bank = [ + 'value' => $data[0], + 'name' => mb_convert_encoding($data[7], 'UTF-8', 'ISO-8859-1'), + 'label' => mb_convert_encoding($data[5], 'UTF-8', 'ISO-8859-1'), + 'description' => mb_convert_encoding($data[3], 'UTF-8', 'ISO-8859-1') . ' ' + . mb_convert_encoding($data[4], 'UTF-8', 'ISO-8859-1'), + ]; + + $banks[] = $bank; } + fclose($handle); + unlink($file_name); + + // Finally, update DB + return $this->updateEntries(CRM_Bic_Parser_DE::$country_code, $banks); + } + + /** + * + * Extracts the National Bank Identifier from an IBAN. + * + */ + public function extractNBIDfromIBAN($iban) { + return [ + substr($iban, 4, 8), + ]; + } + } diff --git a/CRM/Bic/Parser/ES.php b/CRM/Bic/Parser/ES.php index 6cf58c4..b4a305d 100644 --- a/CRM/Bic/Parser/ES.php +++ b/CRM/Bic/Parser/ES.php @@ -14,12 +14,14 @@ | written permission from the original author(s). | +--------------------------------------------------------*/ +declare(strict_types = 1); + /** * Implements CRM_Bic_Parser_Parser for the Spanish case. */ class CRM_Bic_Parser_ES extends CRM_Bic_Parser_Parser { - static $page_url = 'https://raw.githubusercontent.com/ixiam/sepa_bic/main/ES/bic.csv'; + public static $page_url = 'https://raw.githubusercontent.com/ixiam/sepa_bic/main/ES/bic.csv'; public function update() { // first, download the page @@ -29,7 +31,7 @@ public function update() { return $this->createParserOutdatedError(ts("Couldn't download basic page")); } $header = NULL; - $banks = array(); + $banks = []; // iterate CSV records foreach ($data as $line) { @@ -49,12 +51,12 @@ public function update() { continue; } - $banks[$code] = array( + $banks[$code] = [ 'value' => $code, 'name' => $bic, 'label' => $name, 'description' => $name, - ); + ]; } } @@ -66,19 +68,19 @@ public function update() { * Extracts the National Bank Identifier from an Spanish IBAN. */ public function extractNBIDfromIBAN($iban) { - return array( + return [ substr($iban, 4, 4), substr($iban, 4, 8), - ); + ]; } /** * Returns csv line as array */ private function csvLineToArray($str) { - $expr = "/,(?=(?:[^\"]*\"[^\"]*\")*(?![^\"]*\"))/"; + $expr = '/,(?=(?:[^"]*"[^"]*")*(?![^"]*"))/'; $results = preg_split($expr, trim($str)); - return preg_replace("/^\"(.*)\"$/", "$1", $results); + return preg_replace('/^"(.*)"$/', '$1', $results); } } diff --git a/CRM/Bic/Parser/LU.php b/CRM/Bic/Parser/LU.php index 2a86107..13c68bd 100644 --- a/CRM/Bic/Parser/LU.php +++ b/CRM/Bic/Parser/LU.php @@ -14,18 +14,18 @@ | written permission from the original author(s). | +--------------------------------------------------------*/ -// Include Composer's autoloader file. -require_once __DIR__ . '/../../../vendor/autoload.php'; - -require_once 'CRM/Bic/Parser/Parser.php'; +declare(strict_types = 1); /** * Abstract class defining the basis for national bank info parsers */ class CRM_Bic_Parser_LU extends CRM_Bic_Parser_Parser { - static $page_url = 'https://www.abbl.lu/media/file/global/dynamic/c0f7906fec3b33783c9f73c1f6109afdbbc9a66c/Luxembourg%20Register%20of%20IBAN-BIC%20Codes-01.12.2022.xlsx'; - static $country_code = 'LU'; + // phpcs:disable Generic.Files.LineLength.TooLong + public static $page_url = + 'https://www.abbl.lu/media/file/global/dynamic/c0f7906fec3b33783c9f73c1f6109afdbbc9a66c/Luxembourg%20Register%20of%20IBAN-BIC%20Codes-01.12.2022.xlsx'; + // phpcs:enable + public static $country_code = 'LU'; public function update() { // First, download the file @@ -43,8 +43,8 @@ public function update() { $excel_reader = \PhpOffice\PhpSpreadsheet\IOFactory::createReaderForFile($file_name); // Set reader options - $excel_reader->setReadDataOnly(true); - $excel_reader->setLoadSheetsOnly(["Organizations"]); + $excel_reader->setReadDataOnly(TRUE); + $excel_reader->setLoadSheetsOnly(['Organizations']); // Read Excel file $excel_object = $excel_reader->load($file_name); @@ -52,18 +52,20 @@ public function update() { // Process Excel data $skip_lines = 2; - $banks[] = array(); - foreach($excel_rows as $excel_row) { + $banks[] = []; + foreach ($excel_rows as $excel_row) { $skip_lines -= 1; - if ($skip_lines >= 0) continue; + if ($skip_lines >= 0) { + continue; + } // Process row - $bank = array( + $bank = [ 'value' => $excel_row[1], 'name' => str_replace(' ', '', $excel_row[2]), 'label' => $excel_row[0], - 'description' => '' - ); + 'description' => '', + ]; $banks[] = $bank; } @@ -77,13 +79,15 @@ public function update() { return $this->updateEntries(CRM_Bic_Parser_LU::$country_code, $banks); } - /* + /** + * * Extracts the National Bank Identifier from an IBAN. + * */ public function extractNBIDfromIBAN($iban) { - return array( - substr($iban, 4, 3) - ); + return [ + substr($iban, 4, 3), + ]; } } diff --git a/CRM/Bic/Parser/NL.php b/CRM/Bic/Parser/NL.php index c45289c..84b3c80 100644 --- a/CRM/Bic/Parser/NL.php +++ b/CRM/Bic/Parser/NL.php @@ -14,19 +14,16 @@ | written permission from the original author(s). | +--------------------------------------------------------*/ -// Include Composer's autoloader file. -require_once __DIR__ . '/../../../vendor/autoload.php'; - -require_once 'CRM/Bic/Parser/Parser.php'; +declare(strict_types = 1); /** * Abstract class defining the basis for national bank info parsers */ class CRM_Bic_Parser_NL extends CRM_Bic_Parser_Parser { - static $page_url = 'https://www.betaalvereniging.nl/wp-content/uploads/BIC-lijst-NL.xlsx'; + public static $page_url = 'https://www.betaalvereniging.nl/wp-content/uploads/BIC-lijst-NL.xlsx'; - static $country_code = 'NL'; + public static $country_code = 'NL'; public function update() { // First, download the file @@ -44,8 +41,8 @@ public function update() { $excel_reader = \PhpOffice\PhpSpreadsheet\IOFactory::createReaderForFile($file_name); // Set reader options - $excel_reader->setReadDataOnly(true); - $excel_reader->setLoadSheetsOnly(["BIC-lijst"]); + $excel_reader->setReadDataOnly(TRUE); + $excel_reader->setLoadSheetsOnly(['BIC-lijst']); // Read Excel file $excel_object = $excel_reader->load($file_name); @@ -53,18 +50,20 @@ public function update() { // Process Excel data $skip_lines = 2; - $banks[] = array(); - foreach($excel_rows as $excel_row) { + $banks[] = []; + foreach ($excel_rows as $excel_row) { $skip_lines -= 1; - if ($skip_lines >= 0) continue; + if ($skip_lines >= 0) { + continue; + } // Process row - $bank = array( + $bank = [ 'value' => $excel_row[1], 'name' => $excel_row[0], 'label' => $excel_row[2], - 'description' => '' - ); + 'description' => '', + ]; $banks[] = $bank; } @@ -78,13 +77,15 @@ public function update() { return $this->updateEntries(CRM_Bic_Parser_NL::$country_code, $banks); } - /* + /** + * * Extracts the National Bank Identifier from an IBAN. + * */ public function extractNBIDfromIBAN($iban) { - return array( + return [ substr($iban, 4, 4), - ); + ]; } } diff --git a/CRM/Bic/Parser/PL.php b/CRM/Bic/Parser/PL.php index 2d43a6f..5089286 100644 --- a/CRM/Bic/Parser/PL.php +++ b/CRM/Bic/Parser/PL.php @@ -15,7 +15,7 @@ | written permission from the original author(s). | +--------------------------------------------------------*/ -require_once 'CRM/Bic/Parser/Parser.php'; +declare(strict_types = 1); /** * Implementation of abstract class defining the basis for national bank info parsers, Polish banks @@ -23,7 +23,7 @@ class CRM_Bic_Parser_PL extends CRM_Bic_Parser_Parser { // official source of Polish National Bank - static $page_url = 'https://ewib.nbp.pl/plewibnra?dokNazwa=plewibnra.txt'; + public static $page_url = 'https://ewib.nbp.pl/plewibnra?dokNazwa=plewibnra.txt'; public function update() { // first, download the page, it's a CSV file, so more convenient not to use built in method @@ -32,12 +32,12 @@ public function update() { return $this->createParserOutdatedError(ts("Couldn't download CSV file")); } - $data = array(); + $data = []; $count = 0; - foreach( $lines as $line ) { + foreach ($lines as $line) { // convert to UTF8 - original encoding is IBM Latin 2 (CP852) - $line = iconv('CP852', 'UTF-8', $line ); + $line = iconv('CP852', 'UTF-8', $line); // it's tab delimited file with a lot of extra whitespace - splitting, trimming $data[] = array_map('trim', str_getcsv($line, "\t")); $count++; @@ -50,14 +50,14 @@ public function update() { } // process lines - for ($i=0; $i<$count; $i++) { + for ($i = 0; $i < $count; $i++) { $key = $data[$i][4]; - $banks[$key] = array( + $banks[$key] = [ 'value' => $key, 'name' => $data[$i][20], 'label' => $data[$i][1] . ', ' . $data[$i][5], 'description' => '', - ); + ]; } unset($data); @@ -66,13 +66,16 @@ public function update() { return $this->updateEntries('PL', $banks); } - /* + /** + * * Extracts the National Bank Identifier from an IBAN. * In Poland it's 8 digits after checksum + * */ public function extractNBIDfromIBAN($iban) { - return array( - substr($iban, 4, 8) - ); + return [ + substr($iban, 4, 8), + ]; } + } diff --git a/CRM/Bic/Parser/Parser.php b/CRM/Bic/Parser/Parser.php index b577d47..7b86547 100644 --- a/CRM/Bic/Parser/Parser.php +++ b/CRM/Bic/Parser/Parser.php @@ -14,16 +14,20 @@ | written permission from the original author(s). | +--------------------------------------------------------*/ +declare(strict_types = 1); + use CRM_Bic_ExtensionUtil as E; /** * Abstract class defining the basis for national bank info parsers */ +// phpcs:disable Generic.NamingConventions.AbstractClassNamePrefix.Missing abstract class CRM_Bic_Parser_Parser { +// phpcs:enable /** * static function to instatiate the country parser - * + * * @return a parser object */ public static function getParser($country_code) { @@ -32,7 +36,8 @@ public static function getParser($country_code) { if (file_exists($parser_implementation)) { $parser_class = 'CRM_Bic_Parser_' . $country_code; return new $parser_class(); - } else { + } + else { return NULL; } } @@ -49,11 +54,11 @@ public static function getParserList() { // Iterates through the CRM/Bic/Parser folder looking for country files foreach ($iterator as $fileinfo) { $file_name = $fileinfo->getFilename(); - $file_name_parts = explode(".", $file_name); + $file_name_parts = explode('.', $file_name); - if ((end($file_name_parts) == "php") && + if ((end($file_name_parts) == 'php') && (strlen(reset($file_name_parts)) == 2) && - (reset($file_name_parts) != "Parser")) { + (reset($file_name_parts) != 'Parser')) { $countries[] = reset($file_name_parts); } } @@ -61,16 +66,14 @@ public static function getParserList() { return $countries; } - /** * Triggers the parser instance to prepare a full update * * @return array( 'count' => nr of banks found - * 'error' => in case of an error + * 'error' => in case of an error * */ - public abstract function update(); - + abstract public function update(); /** * Extracts the national bank ID from a given IBAN @@ -87,42 +90,48 @@ public function extractNBIDfromIBAN($iban) { * * @link http://en.wikipedia.org/wiki/International_Bank_Account_Number */ - + return FALSE; } - /** * Will update all entries for a given country - * + * * @param string $country * ISO country code - * @param array $entries + * @param array $entries * a set of ['value'=>national_code, 'label'=>bank_name, 'name'=>BIC, 'description'=>optional data]; */ protected function updateEntries($country, $entries) { // if there are no entries given, something probably went wrong. // As a result, all existing entries would be deleted. To prevent this, we'll throw an exception if (empty($entries)) { - $error_message = E::ts("No bank entries could be extracted from the source for country %1. It is possible that the source is outdated. If you can rule out network issues, please report this here: https://github.com/Project60/org.project60.bic/issues.", [1 => $country]); - CRM_Core_Session::setStatus($error_message, E::ts("Problem with %1 Data Source", [1 => $country]), 'error'); + // phpcs:disable Generic.Files.LineLength.TooLong + $error_message = E::ts( + 'No bank entries could be extracted from the source for country %1. It is possible that the source is outdated. If you can rule out network issues, please report this here: https://github.com/Project60/org.project60.bic/issues.', + [1 => $country] + ); + // phpcs:enable + CRM_Core_Session::setStatus($error_message, E::ts('Problem with %1 Data Source', [1 => $country]), 'error'); throw new CRM_Core_Exception($error_message); } try { - $option_group = civicrm_api3('OptionGroup', 'getsingle', array('name' => 'bank_list')); + $option_group = civicrm_api3('OptionGroup', 'getsingle', ['name' => 'bank_list']); $option_group_id = (int) $option_group['id']; - } catch (Exception $e) { - return $this->createError("OptionGroup not found. Reinstall extension!"); + } + catch (Exception $e) { + return $this->createError('OptionGroup not found. Reinstall extension!'); } // init stats - $stats = array( + $stats = [ 'count_processed' => 0, 'count_added' => 0, 'count_deleted' => 0, 'count_ignored' => 0, - 'count_updated' => 0); + 'count_updated' => 0, + ]; // get all data sets $current_data_query = " @@ -134,16 +143,16 @@ protected function updateEntries($country, $entries) { value LIKE '$country%' AND option_group_id = $option_group_id;"; - $current_data = array(); + $current_data = []; $query = CRM_Core_DAO::executeQuery($current_data_query); while ($query->fetch()) { - $current_data[$query->value] = array( + $current_data[$query->value] = [ 'id' => $query->id, 'value' => $query->value, 'name' => $query->name, 'label' => $query->label, - 'description' => $query->description - ); + 'description' => $query->description, + ]; } unset($query); @@ -154,7 +163,8 @@ protected function updateEntries($country, $entries) { if (empty($trimmed_value) || empty($trimmed_name)) { $stats['count_ignored'] += 1; continue; - } else { + } + else { $stats['count_processed'] += 1; } @@ -165,19 +175,20 @@ protected function updateEntries($country, $entries) { if (isset($current_data[$bank['value']])) { $oldbank = $current_data[$bank['value']]; // it already exists -> update? - if ( $bank['value'] != $oldbank['value'] - || $bank['name'] != $oldbank['name'] - || $bank['label'] != $oldbank['label'] + if ($bank['value'] != $oldbank['value'] + || $bank['name'] != $oldbank['name'] + || $bank['label'] != $oldbank['label'] || $bank['description'] != $oldbank['description']) { - + // this has changed... UPDATE $bank['id'] = $oldbank['id']; $bank['option_group_id'] = $option_group_id; civicrm_api3('OptionValue', 'create', $bank); $stats['count_updated'] += 1; - } + } unset($current_data[$bank['value']]); - } else { + } + else { // this is new: add new option value $bank['option_group_id'] = $option_group_id; civicrm_api3('OptionValue', 'create', $bank); @@ -196,11 +207,11 @@ protected function updateEntries($country, $entries) { /** * Download a file and return as a string - * + * * @return file content or NULL on error */ protected function downloadFile($url) { - if (substr($url, 0, 1)=='/') { + if (substr($url, 0, 1) == '/') { // local files we just read return file_get_contents($url); } @@ -208,20 +219,25 @@ protected function downloadFile($url) { // on other sources we use CURL $ch = curl_init(); $timeout = 10; - curl_setopt($ch, CURLOPT_USERAGENT, "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36"); + curl_setopt( + $ch, + CURLOPT_USERAGENT, + 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36' + ); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $timeout); - curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, TRUE); $data = curl_exec($ch); $curl_errno = curl_errno($ch); curl_close($ch); - + if (!$curl_errno) { - return $data; - } else { + return $data; + } + else { return NULL; } } @@ -230,22 +246,31 @@ protected function downloadFile($url) { * generate a compliant error reply for the updateEntries method */ protected function createError($message) { - return array( + return [ 'count' => 0, - 'error' => $message - ); + 'error' => $message, + ]; } /** * generate a compliant error reply for the updateEntries method */ protected function createParserOutdatedError($error_message) { - $message = ts("

An error occurred while updating the bank information:

%1

", array(1=>$error_message)); - $message .= ts("

Please make sure your server is connected to the internet and try again.

"); - $message .= "
"; - $message .= ts("

If the problem persists, the source file (provided by the bank) might have been changed. In this case the 'Little BIC Extension' needs to be updated. Please check here for a newer release.

"); - $message .= "
"; - $message .= ts("

If you are already running the newest version, feel free to contact us via email or on GitHub. The sooner we know about this problem, the sooner it'll be fixed.

"); + $message = ts('

An error occurred while updating the bank information:

%1

', [1 => $error_message]); + $message .= ts('

Please make sure your server is connected to the internet and try again.

'); + $message .= '
'; + // phpcs:disable Generic.Files.LineLength.TooLong + $message .= ts( + "

If the problem persists, the source file (provided by the bank) might have been changed. In this case the 'Little BIC Extension' needs to be updated. Please check here for a newer release.

" + ); + // phpcs:enable + $message .= '
'; + // phpcs:disable Generic.Files.LineLength.TooLong + $message .= ts( + "

If you are already running the newest version, feel free to contact us via email or on GitHub. The sooner we know about this problem, the sooner it'll be fixed.

" + ); + // phpcs:enable return $this->createError($message); } + } diff --git a/CRM/Bic/Upgrader.php b/CRM/Bic/Upgrader.php index 2d3d0e2..6645d9f 100644 --- a/CRM/Bic/Upgrader.php +++ b/CRM/Bic/Upgrader.php @@ -14,35 +14,38 @@ | written permission from the original author(s). | +--------------------------------------------------------*/ +declare(strict_types = 1); + use CRM_Bic_ExtensionUtil as E; /** * Collection of upgrade steps. */ -class CRM_Bic_Upgrader extends CRM_Extension_Upgrader_Base -{ +class CRM_Bic_Upgrader extends CRM_Extension_Upgrader_Base { - /** - * Make sure the option group is there - */ - public function enable() - { - // check if option group is there, and create if it isn't - try { - $option_group = civicrm_api3('OptionGroup', 'getsingle', array('name' => 'bank_list')); - } catch (Exception $e) { - // group's not there yet, create: - try { - $option_group = civicrm_api3('OptionGroup', 'create', array( - 'name' => 'bank_list', - 'title' => ts('List of banks'), - 'is_reserved' => 0, - 'is_active' => 1, - )); - } catch (Exception $create_ex) { - // TODO: more info? - Civi::log()->warning("Couldn't create 'bank_list' OptionGroup."); - } - } + /** + * Make sure the option group is there + */ + public function enable() { + // check if option group is there, and create if it isn't + try { + $option_group = civicrm_api3('OptionGroup', 'getsingle', ['name' => 'bank_list']); + } + catch (Exception $e) { + // group's not there yet, create: + try { + $option_group = civicrm_api3('OptionGroup', 'create', [ + 'name' => 'bank_list', + 'title' => ts('List of banks'), + 'is_reserved' => 0, + 'is_active' => 1, + ]); + } + catch (Exception $create_ex) { + // TODO: more info? + Civi::log()->warning("Couldn't create 'bank_list' OptionGroup."); + } } + } + } diff --git a/Civi/Bic/ActionProvider/Action/LookupBic.php b/Civi/Bic/ActionProvider/Action/LookupBic.php index 488327b..68d84a6 100644 --- a/Civi/Bic/ActionProvider/Action/LookupBic.php +++ b/Civi/Bic/ActionProvider/Action/LookupBic.php @@ -14,105 +14,102 @@ | written permission from the original author(s). | +--------------------------------------------------------*/ +declare(strict_types = 1); + namespace Civi\Bic\ActionProvider\Action; +use CRM_Bic_ExtensionUtil as E; use Civi\ActionProvider\Action\AbstractAction; -use \Civi\ActionProvider\Parameter\ParameterBagInterface; -use \Civi\ActionProvider\Parameter\Specification; -use \Civi\ActionProvider\Parameter\SpecificationBag; +use Civi\ActionProvider\Parameter\ParameterBagInterface; +use Civi\ActionProvider\Parameter\Specification; +use Civi\ActionProvider\Parameter\SpecificationBag; -use CRM_Sepa_ExtensionUtil as E; +class LookupBic extends AbstractAction { -class LookupBic extends AbstractAction -{ + /** + * Returns the specification of the configuration options for the actual action. + * + * @return \Civi\ActionProvider\Parameter\SpecificationBag specs + */ + public function getConfigurationSpecification() { + $normalise_options = [ + '0' => E::ts('No'), + '1' => E::ts('Yes'), + ]; - /** - * Returns the specification of the configuration options for the actual action. - * - * @return SpecificationBag specs - */ - public function getConfigurationSpecification() - { - $normalise_options = [ - '0' => E::ts("No"), - '1' => E::ts("Yes"), - ]; + return new SpecificationBag([ + new Specification('normalise', 'Integer', E::ts('Normalise IBAN'), FALSE, NULL, NULL, $normalise_options, FALSE), + ]); + } - return new SpecificationBag([ - new Specification('normalise', 'Integer', E::ts('Normalise IBAN'), false, null, null, $normalise_options, false), - ]); - } + /** + * Returns the specification of the parameters of the actual action. + * + * @return \Civi\ActionProvider\Parameter\SpecificationBag specs + */ + public function getParameterSpecification() { + return new SpecificationBag([ + // required fields + new Specification('iban', 'String', E::ts('IBAN'), TRUE), + new Specification('bic', 'String', E::ts('BIC (pass-trough if not found)'), FALSE), + ]); + } - /** - * Returns the specification of the parameters of the actual action. - * - * @return SpecificationBag specs - */ - public function getParameterSpecification() - { - return new SpecificationBag([ - // required fields - new Specification('iban', 'String', E::ts('IBAN'), true), - new Specification('bic', 'String', E::ts('BIC (pass-trough if not found)'), false), - ]); - } + /** + * Returns the specification of the output parameters of this action. + * + * @return \Civi\ActionProvider\Parameter\SpecificationBag specs + */ + public function getOutputSpecification() { + return new SpecificationBag([ + new Specification('iban', 'String', E::ts('IBAN'), TRUE, NULL, NULL, NULL, FALSE), + new Specification('bic', 'String', E::ts('BIC'), FALSE, NULL, NULL, NULL, FALSE), + new Specification('title', 'String', E::ts('Title'), FALSE, NULL, NULL, NULL, FALSE), + new Specification('error', 'String', E::ts('Error'), FALSE, NULL, NULL, NULL, FALSE), + ]); + } - /** - * Returns the specification of the output parameters of this action. - * - * @return SpecificationBag specs - */ - public function getOutputSpecification() - { - return new SpecificationBag([ - new Specification('iban', 'String', E::ts('IBAN'), true, null, null, null, false), - new Specification('bic', 'String', E::ts('BIC'), false, null, null, null, false), - new Specification('title', 'String', E::ts('Title'), false, null, null, null, false), - new Specification('error', 'String', E::ts('Error'), false, null, null, null, false), - ]); - } + /** + * Run the action + * + * @param \Civi\ActionProvider\Parameter\ParameterBagInterface $parameters + * The parameters to this action. + * @param \Civi\ActionProvider\Parameter\ParameterBagInterface $output + * The parameters this action can send back + * + * @return void + */ + protected function doAction(ParameterBagInterface $parameters, ParameterBagInterface $output) { + // get BIC + $iban = $parameters->getParameter('iban'); - /** - * Run the action - * - * @param ParameterBagInterface $parameters - * The parameters to this action. - * @param ParameterBagInterface $output - * The parameters this action can send back - * - * @return void - */ - protected function doAction(ParameterBagInterface $parameters, ParameterBagInterface $output) - { - // get BIC - $iban = $parameters->getParameter('iban'); - - // normalise - $normalise = $this->configuration->getParameter('normalise'); - if ($normalise) { - $iban = strtoupper($iban); - $iban = preg_replace('/\W/', '', $iban); - } - $output->setParameter('iban', $iban); + // normalise + $normalise = $this->configuration->getParameter('normalise'); + if ($normalise) { + $iban = strtoupper($iban); + $iban = preg_replace('/\W/', '', $iban); + } + $output->setParameter('iban', $iban); - // verify - $valid = (bool) preg_match("/^[A-Z0-9]+$/", $iban); - // FIXME: additional validation? + // verify + $valid = (bool) preg_match('/^[A-Z0-9]+$/', $iban); + // FIXME: additional validation? - // look up BIC - $lookup = \civicrm_api3('Bic', 'findbyiban', ['iban' => $iban]); + // look up BIC + $lookup = \civicrm_api3('Bic', 'findbyiban', ['iban' => $iban]); - // act - if (empty($lookup['bic'])) { - // not found: pass through input BIC - $output->setParameter('bic', $parameters->getParameter('bic')); - $output->setParameter('error', E::ts("BIC not found/verified")); - } else { - // BIC found - $output->setParameter('bic', $lookup['bic']); - $output->setParameter('title', $lookup['title']); - $output->setParameter('error', ''); - } + // act + if (empty($lookup['bic'])) { + // not found: pass through input BIC + $output->setParameter('bic', $parameters->getParameter('bic')); + $output->setParameter('error', E::ts('BIC not found/verified')); + } + else { + // BIC found + $output->setParameter('bic', $lookup['bic']); + $output->setParameter('title', $lookup['title']); + $output->setParameter('error', ''); } + } } diff --git a/Civi/Bic/ActionProvider/Action/VerifyBic.php b/Civi/Bic/ActionProvider/Action/VerifyBic.php index 0cf3a61..5ebab4a 100644 --- a/Civi/Bic/ActionProvider/Action/VerifyBic.php +++ b/Civi/Bic/ActionProvider/Action/VerifyBic.php @@ -14,110 +14,108 @@ | written permission from the original author(s). | +--------------------------------------------------------*/ +declare(strict_types = 1); + namespace Civi\Bic\ActionProvider\Action; +use CRM_Bic_ExtensionUtil as E; use Civi\ActionProvider\Action\AbstractAction; -use \Civi\ActionProvider\Parameter\ParameterBagInterface; -use \Civi\ActionProvider\Parameter\Specification; -use \Civi\ActionProvider\Parameter\SpecificationBag; - -use CRM_Sepa_ExtensionUtil as E; - -class VerifyBIC extends AbstractAction -{ - - /** - * Returns the specification of the configuration options for the actual action. - * - * @return SpecificationBag specs - */ - public function getConfigurationSpecification() - { - $normalise_options = [ - '0' => E::ts("leave as is"), - '1' => E::ts("normalise to upper case, no spaces"), - ]; - - $invalid_bic_options = [ - 'pass_trough' => E::ts("pass through as is"), - 'erase' => E::ts("set to empty"), - ]; - - return new SpecificationBag([ - new Specification('normalise', 'Integer', E::ts('Normalisation'), false, null, null, $normalise_options, false), - new Specification('invalid', 'String', E::ts('If BIC invalid'), false, null, null, $invalid_bic_options, false), - ]); - } +use Civi\ActionProvider\Parameter\ParameterBagInterface; +use Civi\ActionProvider\Parameter\Specification; +use Civi\ActionProvider\Parameter\SpecificationBag; + +class VerifyBIC extends AbstractAction { + + /** + * Returns the specification of the configuration options for the actual action. + * + * @return \Civi\ActionProvider\Parameter\SpecificationBag specs + */ + public function getConfigurationSpecification() { + $normalise_options = [ + '0' => E::ts('leave as is'), + '1' => E::ts('normalise to upper case, no spaces'), + ]; + + $invalid_bic_options = [ + 'pass_trough' => E::ts('pass through as is'), + 'erase' => E::ts('set to empty'), + ]; + + return new SpecificationBag([ + new Specification('normalise', 'Integer', E::ts('Normalisation'), FALSE, NULL, NULL, $normalise_options, FALSE), + new Specification('invalid', 'String', E::ts('If BIC invalid'), FALSE, NULL, NULL, $invalid_bic_options, FALSE), + ]); + } - /** - * Returns the specification of the parameters of the actual action. - * - * @return SpecificationBag specs - */ - public function getParameterSpecification() - { - return new SpecificationBag([ - // required fields - new Specification('bic', 'String', E::ts('BIC (to check)'), true), - ]); + /** + * Returns the specification of the parameters of the actual action. + * + * @return \Civi\ActionProvider\Parameter\SpecificationBag specs + */ + public function getParameterSpecification() { + return new SpecificationBag([ + // required fields + new Specification('bic', 'String', E::ts('BIC (to check)'), TRUE), + ]); + } + + /** + * Returns the specification of the output parameters of this action. + * + * @return \Civi\ActionProvider\Parameter\SpecificationBag specs + */ + public function getOutputSpecification() { + return new SpecificationBag([ + new Specification('bic', 'String', E::ts('BIC'), FALSE, NULL, NULL, NULL, FALSE), + new Specification('error', 'String', E::ts('Error'), FALSE, NULL, NULL, NULL, FALSE), + ]); + } + + /** + * Run the action + * + * @param \Civi\ActionProvider\Parameter\ParameterBagInterface $parameters + * The parameters to this action. + * @param \Civi\ActionProvider\Parameter\ParameterBagInterface $output + * The parameters this action can send back + * + * @return void + */ + protected function doAction(ParameterBagInterface $parameters, ParameterBagInterface $output) { + // get BIC + $bic = $parameters->getParameter('bic'); + + // normalise + $normalise = $this->configuration->getParameter('normalise'); + if ($normalise) { + $bic = strtoupper($bic); + $bic = preg_replace('/\s/', '', $bic); } - /** - * Returns the specification of the output parameters of this action. - * - * @return SpecificationBag specs - */ - public function getOutputSpecification() - { - return new SpecificationBag([ - new Specification('bic', 'String', E::ts('BIC'), false, null, null, null, false), - new Specification('error', 'String', E::ts('Error'), false, null, null, null, false), - ]); + // verify + $valid = (bool) preg_match('/^[A-Z]{6}[A-Z2-9][A-NP-Z0-9]([A-Z0-9]{3})?$/', $bic); + + // apply + if ($valid) { + $output->setParameter('bic', $bic); + $output->setParameter('error', ''); + } + else { + $invalid = $this->configuration->getParameter('invalid'); + switch ($invalid) { + case 'pass_trough': + $output->setParameter('bic', $bic); + break; - /** - * Run the action - * - * @param ParameterBagInterface $parameters - * The parameters to this action. - * @param ParameterBagInterface $output - * The parameters this action can send back - * - * @return void - */ - protected function doAction(ParameterBagInterface $parameters, ParameterBagInterface $output) - { - // get BIC - $bic = $parameters->getParameter('bic'); - - // normalise - $normalise = $this->configuration->getParameter('normalise'); - if ($normalise) { - $bic = strtoupper($bic); - $bic = preg_replace('/\s/', '', $bic); - } - - // verify - $valid = (bool) preg_match("/^[A-Z]{6}[A-Z2-9][A-NP-Z0-9]([A-Z0-9]{3})?$/", $bic); - - // apply - if ($valid) { - $output->setParameter('bic', $bic); - $output->setParameter('error', ''); - - } else { - $invalid = $this->configuration->getParameter('invalid'); - switch ($invalid) { - case 'pass_trough': - $output->setParameter('bic', $bic); - break; - - default: - case 'erase': - $output->setParameter('bic', ''); - break; - } - $output->setParameter('error', E::ts("BIC '%1' invalid", [1 => $bic])); - } + default: + case 'erase': + $output->setParameter('bic', ''); + break; + } + $output->setParameter('error', E::ts("BIC '%1' invalid", [1 => $bic])); } -} \ No newline at end of file + } + +} diff --git a/Civi/Bic/ContainerSpecs.php b/Civi/Bic/ContainerSpecs.php index 268118d..9a336ed 100644 --- a/Civi/Bic/ContainerSpecs.php +++ b/Civi/Bic/ContainerSpecs.php @@ -14,6 +14,7 @@ | written permission from the original author(s). | +--------------------------------------------------------*/ +declare(strict_types = 1); namespace Civi\Bic; @@ -22,23 +23,34 @@ use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; -class ContainerSpecs implements CompilerPassInterface -{ +class ContainerSpecs implements CompilerPassInterface { - /** - * Register BIC Actions - */ - public function process(ContainerBuilder $container) - { - if (!$container->hasDefinition('action_provider')) { - return; - } - $typeFactoryDefinition = $container->getDefinition('action_provider'); - $typeFactoryDefinition->addMethodCall('addAction', ['LookupBIC', 'Civi\Bic\ActionProvider\Action\LookupBic', E::ts('Look up BIC for IBAN'), [ - \Civi\ActionProvider\Action\AbstractAction::DATA_RETRIEVAL_TAG, - ]]); - $typeFactoryDefinition->addMethodCall('addAction', ['VerifyBIC', 'Civi\Bic\ActionProvider\Action\VerifyBic', E::ts('Verify BIC'), [ - \Civi\ActionProvider\Action\AbstractAction::DATA_RETRIEVAL_TAG, - ]]); + /** + * {@inheritDoc} + */ + public function process(ContainerBuilder $container) { + if (!$container->hasDefinition('action_provider')) { + return; } -} \ No newline at end of file + $typeFactoryDefinition = $container->getDefinition('action_provider'); + $typeFactoryDefinition->addMethodCall( + 'addAction', + [ + 'LookupBIC', + 'Civi\Bic\ActionProvider\Action\LookupBic', + E::ts('Look up BIC for IBAN'), + [\Civi\ActionProvider\Action\AbstractAction::DATA_RETRIEVAL_TAG], + ] + ); + $typeFactoryDefinition->addMethodCall( + 'addAction', + [ + 'VerifyBIC', + 'Civi\Bic\ActionProvider\Action\VerifyBic', + E::ts('Verify BIC'), + [\Civi\ActionProvider\Action\AbstractAction::DATA_RETRIEVAL_TAG], + ] + ); + } + +} diff --git a/ComposerHelper.php b/ComposerHelper.php new file mode 100644 index 0000000..ac7d95d --- /dev/null +++ b/ComposerHelper.php @@ -0,0 +1,29 @@ +getComposer()->getRepositoryManager(); + $package = $event->getComposer()->getPackage(); + $package->setRequires( + array_filter( + $package->getRequires(), + fn (Link $require, string $name) => + 'civicrm/civicrm-core' !== $name && + 'civicrm-ext' !== $repositoryManager->findPackage($name, $require->getConstraint())?->getType(), + ARRAY_FILTER_USE_BOTH + ) + ); + } + +} diff --git a/api/v3/Bic.php b/api/v3/Bic.php index 3a5bbd0..66033e6 100644 --- a/api/v3/Bic.php +++ b/api/v3/Bic.php @@ -14,10 +14,12 @@ | written permission from the original author(s). | +--------------------------------------------------------*/ +declare(strict_types = 1); + /** * API call to look up BIC codes for a given IBAN - * - * @param 'iban' an IBAN number + * + * @phpstan-param array{iban: string} $params */ function civicrm_api3_bic_getfromiban($params) { if (empty($params['iban'])) { @@ -31,7 +33,7 @@ function civicrm_api3_bic_getfromiban($params) { } $nbids = $parser->extractNBIDfromIBAN($params['iban']); - if ($nbids==FALSE) { + if ($nbids == FALSE) { return civicrm_api3_create_error("IBAN parsing not supported for country '$country'!"); } @@ -43,13 +45,15 @@ function civicrm_api3_bic_getfromiban($params) { foreach ($search['values'] as $entity) { if ($candidate == NULL) { $candidate = $entity; - } else { + } + else { if ($candidate != $entity) { // two different matches found! Civi::log()->debug("LittleBIC: contradicting bank records detected: {$country}{$nbid}"); return civicrm_api3_create_error("Contradicting bank records detected: {$country}{$nbid}"); - } else { + } + else { // identical records! Civi::log()->debug("LittleBIC: duplicate bank records detected: {$country}{$nbid}"); } @@ -60,106 +64,113 @@ function civicrm_api3_bic_getfromiban($params) { return $candidate; } - } catch (Exception $e) { + } + catch (Exception $e) { // not found? no problem, just keep looking } } - return civicrm_api3_create_error("BIC for the given IBAN not found."); + return civicrm_api3_create_error('BIC for the given IBAN not found.'); } /** * API call to search BIC codes for a given IBAN * * other than getfromiban, this method won't return errors, if nothing was found - * - * @param 'iban' an IBAN number + * + * @phpstan-param array{iban: string} $params */ function civicrm_api3_bic_findbyiban($params) { if (empty($params['iban']) || strlen($params['iban']) < 7) { - return civicrm_api3_create_success(array(), $params); + return civicrm_api3_create_success([], $params); } $result = civicrm_api3_bic_getfromiban($params); if (empty($result['is_error'])) { return $result; - } else { - return civicrm_api3_create_success(array(), $params); + } + else { + return civicrm_api3_create_success([], $params); } } - /** * API call to look up BIC codes or national IDs - * + * * You can either provide the BIC in the 'bic' parameter, * or you would have to give the ISO country code in 'country' * along with the national bank ID in 'nbid' * */ function civicrm_api3_bic_get($params) { - $query = array(); + $query = []; if (!empty($params['bic'])) { $query['name'] = $params['bic']; - } elseif (!empty($params['country']) && !empty($params['nbid'])) { + } + elseif (!empty($params['country']) && !empty($params['nbid'])) { $query['value'] = $params['country'] . $params['nbid']; - } else { + } + else { return civicrm_api3_create_error("You have to either provide 'bic' or 'country'+'nbid' parameters."); } try { - $option_group = civicrm_api3('OptionGroup', 'getsingle', array('name' => 'bank_list')); + $option_group = civicrm_api3('OptionGroup', 'getsingle', ['name' => 'bank_list']); $query['option_group_id'] = $option_group['id']; - } catch (Exception $e) { - return civicrm_api3_create_error("OptionGroup not found. Reinstall extension!"); + } + catch (Exception $e) { + return civicrm_api3_create_error('OptionGroup not found. Reinstall extension!'); } try { - $data = array(); + $data = []; $option_values = civicrm_api3('OptionValue', 'get', $query); foreach ($option_values['values'] as $key => $value) { - $data[$key] = array( + $data[$key] = [ 'bic' => $value['name'], 'country' => substr($value['value'], 0, 2), 'nbid' => substr($value['value'], 2), 'description' => CRM_Utils_Array::value('description', $value), - 'title' => CRM_Utils_Array::value('label', $value) - ); + 'title' => CRM_Utils_Array::value('label', $value), + ]; } - } catch (Exception $e) { - return civicrm_api3_create_error("Entity does not exist."); } - + catch (Exception $e) { + return civicrm_api3_create_error('Entity does not exist.'); + } + return civicrm_api3_create_success($data, $params); } - /** * API call to update the stored bank data * - * @param 'country' country code to update or 'all' + * @phpstan-param array{ + * country: string, # country code to update or 'all' + * } $params */ function civicrm_api3_bic_update($params) { if (empty($params['country'])) { - return civicrm_api3_create_error("No country given"); + return civicrm_api3_create_error('No country given'); } - $countries = array(); - if ($params['country']=='all') { + $countries = []; + if ($params['country'] == 'all') { $countries = CRM_Bic_Parser_Parser::getParserList(); - } else { + } + else { $countries[] = $params['country']; } // now, loop through the given countries - $result = array(); - $total_count =0; + $result = []; + $total_count = 0; foreach ($countries as $country) { $parser = CRM_Bic_Parser_Parser::getParser($country); if (empty($parser)) { return civicrm_api3_create_error("Parser for '$country' not found!"); } - + // and execute update for each // TODO: process errors $result[$country] = $parser->update(); @@ -167,10 +178,9 @@ function civicrm_api3_bic_update($params) { } $null = NULL; - return civicrm_api3_create_success($result, $params, $null, $null, $null, array('total_count' => $total_count)); + return civicrm_api3_create_success($result, $params, $null, $null, $null, ['total_count' => $total_count]); } - /** * API call get stats about the stored banks * @@ -178,14 +188,15 @@ function civicrm_api3_bic_update($params) { */ function civicrm_api3_bic_stats($params) { try { - $option_group = civicrm_api3('OptionGroup', 'getsingle', array('name' => 'bank_list')); - } catch (Exception $e) { - return civicrm_api3_create_error("OptionGroup not found. Reinstall extension!"); + $option_group = civicrm_api3('OptionGroup', 'getsingle', ['name' => 'bank_list']); + } + catch (Exception $e) { + return civicrm_api3_create_error('OptionGroup not found. Reinstall extension!'); } $option_group_id = (int) $option_group['id']; if (empty($option_group_id)) { - return civicrm_api3_create_error("OptionGroup not found. Reinstall extension!"); + return civicrm_api3_create_error('OptionGroup not found. Reinstall extension!'); } $query = " @@ -194,15 +205,15 @@ function civicrm_api3_bic_stats($params) { COUNT(value) AS count FROM civicrm_option_value - WHERE + WHERE option_group_id = $option_group_id GROUP BY country_code; "; - $result = array(); + $result = []; $query_result = CRM_Core_DAO::executeQuery($query); while ($query_result->fetch()) { $result[$query_result->country_code] = (int) $query_result->count; } - + return civicrm_api3_create_success($result, $params); } diff --git a/bic.php b/bic.php index 97b0a6e..cc5cf30 100644 --- a/bic.php +++ b/bic.php @@ -1,49 +1,65 @@ addCompilerPass(new \Civi\Bic\ContainerSpecs()); - } + _bic_composer_autoload(); + + if (class_exists('\Civi\Bic\ContainerSpecs')) { + $container->addCompilerPass(new \Civi\Bic\ContainerSpecs()); + } } /** - * Implementation of hook_civicrm_config + * Implements hook_civicrm_config(). */ function bic_civicrm_config(&$config) { + _bic_composer_autoload(); _bic_civix_civicrm_config($config); } /** - * Implementation of hook_civicrm_install + * Implements hook_civicrm_install(). */ function bic_civicrm_install() { - return _bic_civix_civicrm_install(); + _bic_civix_civicrm_install(); } /** - * Implementation of hook_civicrm_enable + * Implements hook_civicrm_enable(). */ function bic_civicrm_enable() { - return _bic_civix_civicrm_enable(); + _bic_civix_civicrm_enable(); } /** + * Implements hook_civicrm_alterAPIPersmissions(). + * * Set permissions for runner/engine API call */ function bic_civicrm_alterAPIPermissions($entity, $action, &$params, &$permissions) { // TODO: adjust to correct permission - $permissions['bic']['getfromiban'] = array('access CiviCRM'); - $permissions['bic']['findbyiban'] = array('access AJAX API'); - $permissions['bic']['get'] = array('access CiviCRM'); + $permissions['bic']['getfromiban'] = ['access CiviCRM']; + $permissions['bic']['findbyiban'] = ['access AJAX API']; + $permissions['bic']['get'] = ['access CiviCRM']; } /** @@ -53,15 +69,15 @@ function bic_civicrm_alterAPIPermissions($entity, $action, &$params, &$permissio * */ function bic_civicrm_navigationMenu(&$menu) { - _bic_civix_insert_navigation_menu($menu, 'Search', array( - 'label' => ts('Find Banks', array('domain' => 'org.project60.bic')), + _bic_civix_insert_navigation_menu($menu, 'Search', [ + 'label' => E::ts('Find Banks', ['domain' => 'org.project60.bic']), 'name' => 'BankLists', 'url' => 'civicrm/bicList', 'permission' => 'access CiviContribute', 'operator' => NULL, 'separator' => 2, 'active' => 1, - )); + ]); _bic_civix_navigationMenu($menu); } diff --git a/ci/composer.json b/ci/composer.json new file mode 100644 index 0000000..0a404d9 --- /dev/null +++ b/ci/composer.json @@ -0,0 +1,15 @@ +{ + "minimum-stability": "dev", + "prefer-stable": true, + "config": { + "allow-plugins": { + "civicrm/composer-compile-plugin": false, + "civicrm/composer-downloads-plugin": true, + "cweagans/composer-patches": true + }, + "sort-packages": true + }, + "require": { + "civicrm/civicrm-core": ">=5.54" + } +} diff --git a/composer.json b/composer.json index d92b94b..ee75e7d 100644 --- a/composer.json +++ b/composer.json @@ -1,17 +1,65 @@ { - "name": "project60/org.project60.bic", - "description": "Imports and maintains a list of banks", - "version": "1.8.0-alpha1", - "type": "civicrm-module", - "authors": [ - { - "name": "B. Endres", - "email": "endres@systopia.de" + "name": "project60/org.project60.bic", + "description": "Generates and maintains a list of banks", + "type": "civicrm-ext", + "license": "AGPL-3.0-or-later", + "authors": [ + { + "name": "SYSTOPIA GmbH", + "email": "info@systopia.de", + "homepage": "https://www.systopia.de" + } + ], + "minimum-stability": "dev", + "prefer-stable": true, + "config": { + "prepend-autoloader": false, + "sort-packages": true, + "platform": { + } + }, + "require": { + "php": "^8.1", + "civicrm/civicrm-core": ">=5.54", + "phpoffice/phpspreadsheet": "^1.30" + }, + "autoload": { + "classmap": ["ComposerHelper.php"] + }, + "scripts": { + "pre-update-cmd": [ + "Bic\\ComposerHelper::preUpdate" + ], + "composer-phpcs": [ + "@composer --working-dir=tools/phpcs" + ], + "composer-phpstan": [ + "@composer --working-dir=tools/phpstan" + ], + "composer-phpunit": [ + "@composer --working-dir=tools/phpunit" + ], + "composer-tools": [ + "@composer-phpcs", + "@composer-phpstan", + "@composer-phpunit" + ], + "phpcs": [ + "@php tools/phpcs/vendor/bin/phpcs" + ], + "phpcbf": [ + "@php tools/phpcs/vendor/bin/phpcbf" + ], + "phpstan": [ + "@php tools/phpstan/vendor/bin/phpstan -v" + ], + "phpunit": [ + "@php tools/phpunit/vendor/bin/simple-phpunit --coverage-text" + ], + "test": [ + "@phpcs", + "@phpstan", + "@phpunit" + ] } - ], - "license": "AGPL-3.0", - "require": { - "php": "^8.0", - "phpoffice/phpspreadsheet": "^1.30" - } } diff --git a/docs/index.md b/docs/index.md new file mode 120000 index 0000000..32d46ee --- /dev/null +++ b/docs/index.md @@ -0,0 +1 @@ +../README.md \ No newline at end of file diff --git a/info.xml b/info.xml index 6af214e..6634025 100644 --- a/info.xml +++ b/info.xml @@ -17,7 +17,7 @@ https://github.com/Project60/org.project60.bic/issues - 5.45 + 5.54 8.0 diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..70803d3 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,27 @@ +site_name: Little BIC extension +site_description: Generates and maintains a list of banks +site_author: B. Endres +site_url: https://github.com/Project60/org.project60.bic + +theme: + name: material + +nav: + - Introduction: index.md + +markdown_extensions: + - attr_list + - admonition + - def_list + - codehilite + - toc: + permalink: True + - pymdownx.superfences + - pymdownx.inlinehilite + - pymdownx.tilde + - pymdownx.betterem + - pymdownx.mark + +plugins: + - search: + lang: en diff --git a/phpcs.xml.dist b/phpcs.xml.dist new file mode 100644 index 0000000..dc7d9cf --- /dev/null +++ b/phpcs.xml.dist @@ -0,0 +1,83 @@ + + + CiviCRM coding standard with some additional changes + + api + Civi + CRM + tests + bic.php + + /CRM/Bic/DAO/.*\.php$ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 0000000..2e810f7 --- /dev/null +++ b/phpstan-baseline.neon @@ -0,0 +1,944 @@ +parameters: + ignoreErrors: + - + message: '#^Cannot access offset ''values'' on array\|int\.$#' + identifier: offsetAccess.nonOffsetAccessible + count: 1 + path: CRM/Bic/Page/BicList.php + + - + message: '#^Method CRM_Bic_Page_BicList\:\:run\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: CRM/Bic/Page/BicList.php + + - + message: '#^Offset \(int\|string\) might not exist on array\|null\.$#' + identifier: offsetAccess.notFound + count: 1 + path: CRM/Bic/Page/BicList.php + + - + message: '#^Only booleans are allowed in an if condition, list\|null given\.$#' + identifier: if.condNotBoolean + count: 1 + path: CRM/Bic/Page/BicList.php + + - + message: '#^Variable \$default_country might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: CRM/Bic/Page/BicList.php + + - + message: '#^Cannot access offset ''values'' on array\|int\.$#' + identifier: offsetAccess.nonOffsetAccessible + count: 2 + path: CRM/Bic/Page/Config.php + + - + message: '#^Iterating over an object of an unknown class an\.$#' + identifier: class.notFound + count: 2 + path: CRM/Bic/Page/Config.php + + - + message: '#^Method CRM_Bic_Page_Config\:\:run\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: CRM/Bic/Page/Config.php + + - + message: '#^Offset \(int\|string\) might not exist on array\|null\.$#' + identifier: offsetAccess.notFound + count: 1 + path: CRM/Bic/Page/Config.php + + - + message: '#^Call to function in_array\(\) requires parameter \#3 to be set\.$#' + identifier: function.strict + count: 3 + path: CRM/Bic/Parser/AT.php + + - + message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' + identifier: empty.notAllowed + count: 7 + path: CRM/Bic/Parser/AT.php + + - + message: '#^Method CRM_Bic_Parser_AT\:\:extractNBIDfromIBAN\(\) has parameter \$iban with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: CRM/Bic/Parser/AT.php + + - + message: '#^Method CRM_Bic_Parser_AT\:\:extractNBIDfromIBAN\(\) return type has no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: CRM/Bic/Parser/AT.php + + - + message: '#^Method CRM_Bic_Parser_AT\:\:getDescription\(\) has parameter \$data_set with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: CRM/Bic/Parser/AT.php + + - + message: '#^Method CRM_Bic_Parser_AT\:\:update\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: CRM/Bic/Parser/AT.php + + - + message: '#^Parameter \#1 \$keys of function array_combine expects array\, list\ given\.$#' + identifier: argument.type + count: 1 + path: CRM/Bic/Parser/AT.php + + - + message: '#^Parameter \#1 \$string of function str_getcsv expects string, string\|false given\.$#' + identifier: argument.type + count: 1 + path: CRM/Bic/Parser/AT.php + + - + message: '#^Parameter \#2 \$subject of function preg_split expects string, file given\.$#' + identifier: argument.type + count: 1 + path: CRM/Bic/Parser/AT.php + + - + message: '#^Property CRM_Bic_Parser_AT\:\:\$page_url has no type specified\.$#' + identifier: missingType.property + count: 1 + path: CRM/Bic/Parser/AT.php + + - + message: '#^Call to function in_array\(\) requires parameter \#3 to be set\.$#' + identifier: function.strict + count: 1 + path: CRM/Bic/Parser/BE.php + + - + message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' + identifier: empty.notAllowed + count: 3 + path: CRM/Bic/Parser/BE.php + + - + message: '#^Implicit array creation is not allowed \- variable \$banks does not exist\.$#' + identifier: variable.implicitArray + count: 1 + path: CRM/Bic/Parser/BE.php + + - + message: '#^Loose comparison via "\=\=" is not allowed\.$#' + identifier: equal.notAllowed + count: 1 + path: CRM/Bic/Parser/BE.php + + - + message: '#^Method CRM_Bic_Parser_BE\:\:extractNBIDfromIBAN\(\) has parameter \$iban with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: CRM/Bic/Parser/BE.php + + - + message: '#^Method CRM_Bic_Parser_BE\:\:extractNBIDfromIBAN\(\) return type has no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: CRM/Bic/Parser/BE.php + + - + message: '#^Method CRM_Bic_Parser_BE\:\:update\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: CRM/Bic/Parser/BE.php + + - + message: '#^Property CRM_Bic_Parser_BE\:\:\$country_code has no type specified\.$#' + identifier: missingType.property + count: 1 + path: CRM/Bic/Parser/BE.php + + - + message: '#^Property CRM_Bic_Parser_BE\:\:\$page_url has no type specified\.$#' + identifier: missingType.property + count: 1 + path: CRM/Bic/Parser/BE.php + + - + message: '#^Argument of an invalid type list\\|false supplied for foreach, only iterables are supported\.$#' + identifier: foreach.nonIterable + count: 1 + path: CRM/Bic/Parser/CH.php + + - + message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' + identifier: empty.notAllowed + count: 2 + path: CRM/Bic/Parser/CH.php + + - + message: '#^Method CRM_Bic_Parser_CH\:\:extractNBIDfromIBAN\(\) has parameter \$iban with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: CRM/Bic/Parser/CH.php + + - + message: '#^Method CRM_Bic_Parser_CH\:\:extractNBIDfromIBAN\(\) return type has no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: CRM/Bic/Parser/CH.php + + - + message: '#^Method CRM_Bic_Parser_CH\:\:update\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: CRM/Bic/Parser/CH.php + + - + message: '#^Parameter \#2 \$subject of function preg_split expects string, file given\.$#' + identifier: argument.type + count: 1 + path: CRM/Bic/Parser/CH.php + + - + message: '#^Property CRM_Bic_Parser_CH\:\:\$page_url has no type specified\.$#' + identifier: missingType.property + count: 1 + path: CRM/Bic/Parser/CH.php + + - + message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' + identifier: empty.notAllowed + count: 2 + path: CRM/Bic/Parser/DE.php + + - + message: '#^Loose comparison via "\!\=" is not allowed\.$#' + identifier: notEqual.notAllowed + count: 1 + path: CRM/Bic/Parser/DE.php + + - + message: '#^Method CRM_Bic_Parser_DE\:\:extractNBIDfromIBAN\(\) has parameter \$iban with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: CRM/Bic/Parser/DE.php + + - + message: '#^Method CRM_Bic_Parser_DE\:\:extractNBIDfromIBAN\(\) return type has no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: CRM/Bic/Parser/DE.php + + - + message: '#^Method CRM_Bic_Parser_DE\:\:update\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: CRM/Bic/Parser/DE.php + + - + message: '#^NotEqual\: Condition between 1 and string\|null are falsy, please do not mix types\.$#' + identifier: voku.NotEqual + count: 1 + path: CRM/Bic/Parser/DE.php + + - + message: '#^Parameter \#1 \$string of function mb_convert_encoding expects array\|string, string\|null given\.$#' + identifier: argument.type + count: 3 + path: CRM/Bic/Parser/DE.php + + - + message: '#^Property CRM_Bic_Parser_DE\:\:\$country_code has no type specified\.$#' + identifier: missingType.property + count: 1 + path: CRM/Bic/Parser/DE.php + + - + message: '#^Property CRM_Bic_Parser_DE\:\:\$page_url has no type specified\.$#' + identifier: missingType.property + count: 1 + path: CRM/Bic/Parser/DE.php + + - + message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' + identifier: empty.notAllowed + count: 3 + path: CRM/Bic/Parser/ES.php + + - + message: '#^Method CRM_Bic_Parser_ES\:\:csvLineToArray\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: CRM/Bic/Parser/ES.php + + - + message: '#^Method CRM_Bic_Parser_ES\:\:csvLineToArray\(\) has parameter \$str with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: CRM/Bic/Parser/ES.php + + - + message: '#^Method CRM_Bic_Parser_ES\:\:extractNBIDfromIBAN\(\) has parameter \$iban with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: CRM/Bic/Parser/ES.php + + - + message: '#^Method CRM_Bic_Parser_ES\:\:extractNBIDfromIBAN\(\) return type has no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: CRM/Bic/Parser/ES.php + + - + message: '#^Method CRM_Bic_Parser_ES\:\:update\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: CRM/Bic/Parser/ES.php + + - + message: '#^Parameter \#2 \$subject of function preg_split expects string, file given\.$#' + identifier: argument.type + count: 1 + path: CRM/Bic/Parser/ES.php + + - + message: '#^Parameter \#3 \$subject of function preg_replace expects array\\|string, list\\|false given\.$#' + identifier: argument.type + count: 1 + path: CRM/Bic/Parser/ES.php + + - + message: '#^Property CRM_Bic_Parser_ES\:\:\$page_url has no type specified\.$#' + identifier: missingType.property + count: 1 + path: CRM/Bic/Parser/ES.php + + - + message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' + identifier: empty.notAllowed + count: 1 + path: CRM/Bic/Parser/LU.php + + - + message: '#^Implicit array creation is not allowed \- variable \$banks does not exist\.$#' + identifier: variable.implicitArray + count: 1 + path: CRM/Bic/Parser/LU.php + + - + message: '#^Method CRM_Bic_Parser_LU\:\:extractNBIDfromIBAN\(\) has parameter \$iban with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: CRM/Bic/Parser/LU.php + + - + message: '#^Method CRM_Bic_Parser_LU\:\:extractNBIDfromIBAN\(\) return type has no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: CRM/Bic/Parser/LU.php + + - + message: '#^Method CRM_Bic_Parser_LU\:\:update\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: CRM/Bic/Parser/LU.php + + - + message: '#^Property CRM_Bic_Parser_LU\:\:\$country_code has no type specified\.$#' + identifier: missingType.property + count: 1 + path: CRM/Bic/Parser/LU.php + + - + message: '#^Property CRM_Bic_Parser_LU\:\:\$page_url has no type specified\.$#' + identifier: missingType.property + count: 1 + path: CRM/Bic/Parser/LU.php + + - + message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' + identifier: empty.notAllowed + count: 1 + path: CRM/Bic/Parser/NL.php + + - + message: '#^Implicit array creation is not allowed \- variable \$banks does not exist\.$#' + identifier: variable.implicitArray + count: 1 + path: CRM/Bic/Parser/NL.php + + - + message: '#^Method CRM_Bic_Parser_NL\:\:extractNBIDfromIBAN\(\) has parameter \$iban with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: CRM/Bic/Parser/NL.php + + - + message: '#^Method CRM_Bic_Parser_NL\:\:extractNBIDfromIBAN\(\) return type has no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: CRM/Bic/Parser/NL.php + + - + message: '#^Method CRM_Bic_Parser_NL\:\:update\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: CRM/Bic/Parser/NL.php + + - + message: '#^Property CRM_Bic_Parser_NL\:\:\$country_code has no type specified\.$#' + identifier: missingType.property + count: 1 + path: CRM/Bic/Parser/NL.php + + - + message: '#^Property CRM_Bic_Parser_NL\:\:\$page_url has no type specified\.$#' + identifier: missingType.property + count: 1 + path: CRM/Bic/Parser/NL.php + + - + message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' + identifier: empty.notAllowed + count: 2 + path: CRM/Bic/Parser/PL.php + + - + message: '#^Implicit array creation is not allowed \- variable \$banks might not exist\.$#' + identifier: variable.implicitArray + count: 1 + path: CRM/Bic/Parser/PL.php + + - + message: '#^Method CRM_Bic_Parser_PL\:\:extractNBIDfromIBAN\(\) has parameter \$iban with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: CRM/Bic/Parser/PL.php + + - + message: '#^Method CRM_Bic_Parser_PL\:\:extractNBIDfromIBAN\(\) return type has no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: CRM/Bic/Parser/PL.php + + - + message: '#^Method CRM_Bic_Parser_PL\:\:update\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: CRM/Bic/Parser/PL.php + + - + message: '#^Parameter \#1 \$callback of function array_map expects \(callable\(string\|null\)\: mixed\)\|null, ''trim'' given\.$#' + identifier: argument.type + count: 1 + path: CRM/Bic/Parser/PL.php + + - + message: '#^Parameter \#1 \$string of function str_getcsv expects string, string\|false given\.$#' + identifier: argument.type + count: 1 + path: CRM/Bic/Parser/PL.php + + - + message: '#^Property CRM_Bic_Parser_PL\:\:\$page_url has no type specified\.$#' + identifier: missingType.property + count: 1 + path: CRM/Bic/Parser/PL.php + + - + message: '#^Variable \$banks might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: CRM/Bic/Parser/PL.php + + - + message: '#^Variable \$count in empty\(\) always exists and is not falsy\.$#' + identifier: empty.variable + count: 1 + path: CRM/Bic/Parser/PL.php + + - + message: '#^Access to an undefined property object\:\:\$description\.$#' + identifier: property.notFound + count: 1 + path: CRM/Bic/Parser/Parser.php + + - + message: '#^Access to an undefined property object\:\:\$id\.$#' + identifier: property.notFound + count: 1 + path: CRM/Bic/Parser/Parser.php + + - + message: '#^Access to an undefined property object\:\:\$label\.$#' + identifier: property.notFound + count: 1 + path: CRM/Bic/Parser/Parser.php + + - + message: '#^Access to an undefined property object\:\:\$name\.$#' + identifier: property.notFound + count: 1 + path: CRM/Bic/Parser/Parser.php + + - + message: '#^Access to an undefined property object\:\:\$value\.$#' + identifier: property.notFound + count: 2 + path: CRM/Bic/Parser/Parser.php + + - + message: '#^Call to an undefined method object\:\:fetch\(\)\.$#' + identifier: method.notFound + count: 1 + path: CRM/Bic/Parser/Parser.php + + - + message: '#^Cannot access offset ''id'' on array\|int\.$#' + identifier: offsetAccess.nonOffsetAccessible + count: 1 + path: CRM/Bic/Parser/Parser.php + + - + message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' + identifier: empty.notAllowed + count: 3 + path: CRM/Bic/Parser/Parser.php + + - + message: '#^Implicit array creation is not allowed \- variable \$countries might not exist\.$#' + identifier: variable.implicitArray + count: 1 + path: CRM/Bic/Parser/Parser.php + + - + message: '#^In method "CRM_Bic_Parser_Parser\:\:updateEntries", caught "Exception" must be rethrown\. Either catch a more specific exception or add a "throw" clause in the "catch" block to propagate the exception\. More info\: http\://bit\.ly/failloud$#' + count: 1 + path: CRM/Bic/Parser/Parser.php + + - + message: '#^Loose comparison via "\!\=" is not allowed\.$#' + identifier: notEqual.notAllowed + count: 5 + path: CRM/Bic/Parser/Parser.php + + - + message: '#^Loose comparison via "\=\=" is not allowed\.$#' + identifier: equal.notAllowed + count: 3 + path: CRM/Bic/Parser/Parser.php + + - + message: '#^Method CRM_Bic_Parser_Parser\:\:createError\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: CRM/Bic/Parser/Parser.php + + - + message: '#^Method CRM_Bic_Parser_Parser\:\:createError\(\) has parameter \$message with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: CRM/Bic/Parser/Parser.php + + - + message: '#^Method CRM_Bic_Parser_Parser\:\:createParserOutdatedError\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: CRM/Bic/Parser/Parser.php + + - + message: '#^Method CRM_Bic_Parser_Parser\:\:createParserOutdatedError\(\) has parameter \$error_message with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: CRM/Bic/Parser/Parser.php + + - + message: '#^Method CRM_Bic_Parser_Parser\:\:downloadFile\(\) has invalid return type file\.$#' + identifier: class.notFound + count: 1 + path: CRM/Bic/Parser/Parser.php + + - + message: '#^Method CRM_Bic_Parser_Parser\:\:downloadFile\(\) has parameter \$url with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: CRM/Bic/Parser/Parser.php + + - + message: '#^Method CRM_Bic_Parser_Parser\:\:downloadFile\(\) should return file but returns bool\|string\.$#' + identifier: return.type + count: 1 + path: CRM/Bic/Parser/Parser.php + + - + message: '#^Method CRM_Bic_Parser_Parser\:\:downloadFile\(\) should return file but returns null\.$#' + identifier: return.type + count: 1 + path: CRM/Bic/Parser/Parser.php + + - + message: '#^Method CRM_Bic_Parser_Parser\:\:downloadFile\(\) should return file but returns string\|false\.$#' + identifier: return.type + count: 1 + path: CRM/Bic/Parser/Parser.php + + - + message: '#^Method CRM_Bic_Parser_Parser\:\:extractNBIDfromIBAN\(\) has parameter \$iban with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: CRM/Bic/Parser/Parser.php + + - + message: '#^Method CRM_Bic_Parser_Parser\:\:extractNBIDfromIBAN\(\) return type has no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: CRM/Bic/Parser/Parser.php + + - + message: '#^Method CRM_Bic_Parser_Parser\:\:extractNBIDfromIBAN\(\) should return array but returns false\.$#' + identifier: return.type + count: 1 + path: CRM/Bic/Parser/Parser.php + + - + message: '#^Method CRM_Bic_Parser_Parser\:\:getParser\(\) has invalid return type a\.$#' + identifier: class.notFound + count: 1 + path: CRM/Bic/Parser/Parser.php + + - + message: '#^Method CRM_Bic_Parser_Parser\:\:getParser\(\) has parameter \$country_code with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: CRM/Bic/Parser/Parser.php + + - + message: '#^Method CRM_Bic_Parser_Parser\:\:getParser\(\) should return a but returns null\.$#' + identifier: return.type + count: 1 + path: CRM/Bic/Parser/Parser.php + + - + message: '#^Method CRM_Bic_Parser_Parser\:\:getParser\(\) should return a but returns object\.$#' + identifier: return.type + count: 1 + path: CRM/Bic/Parser/Parser.php + + - + message: '#^Method CRM_Bic_Parser_Parser\:\:getParserList\(\) has invalid return type an\.$#' + identifier: class.notFound + count: 1 + path: CRM/Bic/Parser/Parser.php + + - + message: '#^Method CRM_Bic_Parser_Parser\:\:getParserList\(\) should return an but returns list\\.$#' + identifier: return.type + count: 1 + path: CRM/Bic/Parser/Parser.php + + - + message: '#^Method CRM_Bic_Parser_Parser\:\:update\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: CRM/Bic/Parser/Parser.php + + - + message: '#^Method CRM_Bic_Parser_Parser\:\:updateEntries\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: CRM/Bic/Parser/Parser.php + + - + message: '#^Method CRM_Bic_Parser_Parser\:\:updateEntries\(\) has parameter \$entries with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: CRM/Bic/Parser/Parser.php + + - + message: '#^Only booleans are allowed in a negated boolean, int given\.$#' + identifier: booleanNot.exprNotBoolean + count: 1 + path: CRM/Bic/Parser/Parser.php + + - + message: ''' + #^PHPDoc tag @return has invalid value \(array\( 'count' \=\> nr of banks found + 'error' \=\> in case of an error\)\: Unexpected token "\(", expected TOKEN_HORIZONTAL_WS at offset 86 on line 4$# + ''' + identifier: phpDoc.parseError + count: 1 + path: CRM/Bic/Parser/Parser.php + + - + message: '#^Parameter \#3 \$value of function curl_setopt expects bool, int given\.$#' + identifier: argument.type + count: 1 + path: CRM/Bic/Parser/Parser.php + + - + message: '#^Variable \$countries might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: CRM/Bic/Parser/Parser.php + + - + message: '#^In method "CRM_Bic_Upgrader\:\:enable", caught "Exception" must be rethrown\. Either catch a more specific exception or add a "throw" clause in the "catch" block to propagate the exception\. More info\: http\://bit\.ly/failloud$#' + count: 2 + path: CRM/Bic/Upgrader.php + + - + message: '#^Method CRM_Bic_Upgrader\:\:enable\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: CRM/Bic/Upgrader.php + + - + message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' + identifier: empty.notAllowed + count: 1 + path: Civi/Bic/ActionProvider/Action/LookupBic.php + + - + message: '#^Method Civi\\Bic\\ContainerSpecs\:\:process\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: Civi/Bic/ContainerSpecs.php + + - + message: '#^Access to an undefined property object\:\:\$count\.$#' + identifier: property.notFound + count: 1 + path: api/v3/Bic.php + + - + message: '#^Access to an undefined property object\:\:\$country_code\.$#' + identifier: property.notFound + count: 1 + path: api/v3/Bic.php + + - + message: '#^Argument of an invalid type an\|list\ supplied for foreach, only iterables are supported\.$#' + identifier: foreach.nonIterable + count: 1 + path: api/v3/Bic.php + + - + message: '#^Call to an undefined method object\:\:fetch\(\)\.$#' + identifier: method.notFound + count: 1 + path: api/v3/Bic.php + + - + message: ''' + #^Call to deprecated method value\(\) of class CRM_Utils_Array\: + In most cases this can be replaced with + \$list\[\$key\] \?\? \$default + with the minor difference that when \$list\[\$key\] exists and is NULL, this function will always + return NULL\.$# + ''' + identifier: staticMethod.deprecated + count: 2 + path: api/v3/Bic.php + + - + message: '#^Call to method extractNBIDfromIBAN\(\) on an unknown class a\.$#' + identifier: class.notFound + count: 1 + path: api/v3/Bic.php + + - + message: '#^Call to method update\(\) on an unknown class a\.$#' + identifier: class.notFound + count: 1 + path: api/v3/Bic.php + + - + message: '#^Cannot access offset ''id'' on array\|int\.$#' + identifier: offsetAccess.nonOffsetAccessible + count: 2 + path: api/v3/Bic.php + + - + message: '#^Cannot access offset ''values'' on array\|int\.$#' + identifier: offsetAccess.nonOffsetAccessible + count: 2 + path: api/v3/Bic.php + + - + message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' + identifier: empty.notAllowed + count: 10 + path: api/v3/Bic.php + + - + message: '#^Empty catch block\.$#' + count: 1 + path: api/v3/Bic.php + + - + message: '#^Function civicrm_api3_bic_findbyiban\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: api/v3/Bic.php + + - + message: '#^Function civicrm_api3_bic_get\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: api/v3/Bic.php + + - + message: '#^Function civicrm_api3_bic_get\(\) has parameter \$params with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: api/v3/Bic.php + + - + message: '#^Function civicrm_api3_bic_getfromiban\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: api/v3/Bic.php + + - + message: '#^Function civicrm_api3_bic_stats\(\) has invalid return type a\.$#' + identifier: class.notFound + count: 1 + path: api/v3/Bic.php + + - + message: '#^Function civicrm_api3_bic_stats\(\) has parameter \$params with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: api/v3/Bic.php + + - + message: '#^Function civicrm_api3_bic_stats\(\) should return a but returns array\.$#' + identifier: return.type + count: 3 + path: api/v3/Bic.php + + - + message: '#^Function civicrm_api3_bic_update\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: api/v3/Bic.php + + - + message: '#^Function civicrm_api3_bic_update\(\) has parameter \$params with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: api/v3/Bic.php + + - + message: '#^In function "civicrm_api3_bic_get", caught "Exception" must be rethrown\. Either catch a more specific exception or add a "throw" clause in the "catch" block to propagate the exception\. More info\: http\://bit\.ly/failloud$#' + count: 2 + path: api/v3/Bic.php + + - + message: '#^In function "civicrm_api3_bic_getfromiban", caught "Exception" must be rethrown\. Either catch a more specific exception or add a "throw" clause in the "catch" block to propagate the exception\. More info\: http\://bit\.ly/failloud$#' + count: 1 + path: api/v3/Bic.php + + - + message: '#^In function "civicrm_api3_bic_stats", caught "Exception" must be rethrown\. Either catch a more specific exception or add a "throw" clause in the "catch" block to propagate the exception\. More info\: http\://bit\.ly/failloud$#' + count: 1 + path: api/v3/Bic.php + + - + message: '#^Loose comparison via "\!\=" is not allowed\.$#' + identifier: notEqual.notAllowed + count: 1 + path: api/v3/Bic.php + + - + message: '#^Loose comparison via "\=\=" is not allowed\.$#' + identifier: equal.notAllowed + count: 3 + path: api/v3/Bic.php + + - + message: ''' + #^PHPDoc tag @phpstan\-param has invalid value \(array\{ + country\: string, \# country code to update or 'all' + \} \$params\)\: Unexpected token "\#", expected type at offset 97 on line 5$# + ''' + identifier: phpDoc.parseError + count: 1 + path: api/v3/Bic.php + + - + message: '#^Function bic_civicrm_alterAPIPermissions\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: bic.php + + - + message: '#^Function bic_civicrm_alterAPIPermissions\(\) has parameter \$action with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: bic.php + + - + message: '#^Function bic_civicrm_alterAPIPermissions\(\) has parameter \$entity with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: bic.php + + - + message: '#^Function bic_civicrm_alterAPIPermissions\(\) has parameter \$params with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: bic.php + + - + message: '#^Function bic_civicrm_alterAPIPermissions\(\) has parameter \$permissions with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: bic.php + + - + message: '#^Function bic_civicrm_config\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: bic.php + + - + message: '#^Function bic_civicrm_config\(\) has parameter \$config with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: bic.php + + - + message: '#^Function bic_civicrm_container\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: bic.php + + - + message: '#^Function bic_civicrm_enable\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: bic.php + + - + message: '#^Function bic_civicrm_install\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: bic.php + + - + message: '#^Function bic_civicrm_navigationMenu\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: bic.php + + - + message: '#^Function bic_civicrm_navigationMenu\(\) has parameter \$menu with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: bic.php diff --git a/phpstan.ci.neon b/phpstan.ci.neon new file mode 100644 index 0000000..bd8e612 --- /dev/null +++ b/phpstan.ci.neon @@ -0,0 +1,14 @@ +includes: + - phpstan.neon.dist + +parameters: + scanDirectories: + - ci/vendor/civicrm/civicrm-core/api/ + - ci/vendor/civicrm/civicrm-core/CRM/ + bootstrapFiles: + - ci/vendor/autoload.php + # Because we test with different versions in CI we have unmatched errors + reportUnmatchedIgnoredErrors: false + ignoreErrors: + # Errors we get when using "prefer-lowest" + - '#::getSubscribedEvents\(\) return type has no value type specified in iterable type array.$#' diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 0000000..e5ab3c7 --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,47 @@ +includes: + - phpstan-baseline.neon + +parameters: + paths: + - api + - Civi + - CRM + - tests + - bic.php + excludePaths: + analyse: + - CRM/Bic/DAO/* + - tests/phpunit/bootstrap.php + scanFiles: + - bic.civix.php + - tools/phpunit/vendor/bin/.phpunit/phpunit/src/Framework/TestCase.php + scanDirectories: + - tools/phpunit/vendor/bin/.phpunit/phpunit/src/Framework + - ../action-provider/Civi/ + - ../org.project60.sepa/CRM/ + bootstrapFiles: + - tools/phpunit/vendor/bin/.phpunit/phpunit/vendor/autoload.php + - phpstanBootstrap.php + level: 9 + universalObjectCratesClasses: + - Civi\Core\Event\GenericHookEvent + - CRM_Core_Config + - CRM_Core_DAO + earlyTerminatingMethodCalls: + CRM_Queue_Runner: + - runAllViaWeb + checkTooWideReturnTypesInProtectedAndPublicMethods: true + checkUninitializedProperties: true + checkMissingCallableSignature: true + treatPhpDocTypesAsCertain: false + exceptions: + check: + missingCheckedExceptionInThrows: true + tooWideThrowType: true + checkedExceptionClasses: + - \Webmozart\Assert\InvalidArgumentException + implicitThrows: false + ignoreErrors: + # Note paths are prefixed with "*/" to work with inspections in PHPStorm because of: + # https://youtrack.jetbrains.com/issue/WI-63891/PHPStan-ignoreErrors-configuration-isnt-working-with-inspections + tmpDir: .phpstan diff --git a/phpstan.neon.template b/phpstan.neon.template new file mode 100644 index 0000000..ccdc8d4 --- /dev/null +++ b/phpstan.neon.template @@ -0,0 +1,12 @@ +# Copy this file to phpstan.neon and replace {VENDOR_DIR} with the appropriate +# path. + +includes: + - phpstan.neon.dist + +parameters: + scanDirectories: + - {VENDOR_DIR}/civicrm/civicrm-core/api/ + - {VENDOR_DIR}/civicrm/civicrm-core/CRM/ + bootstrapFiles: + - {VENDOR_DIR}/autoload.php diff --git a/phpstanBootstrap.php b/phpstanBootstrap.php new file mode 100644 index 0000000..67ad97d --- /dev/null +++ b/phpstanBootstrap.php @@ -0,0 +1,47 @@ +. + */ + +declare(strict_types = 1); + +// phpcs:disable Drupal.Commenting.DocComment.ContentAfterOpen +/** @var \PHPStan\DependencyInjection\Container $container */ +/** @phpstan-var array $bootstrapFiles */ +$bootstrapFiles = $container->getParameter('bootstrapFiles'); +foreach ($bootstrapFiles as $bootstrapFile) { + if (str_ends_with($bootstrapFile, 'vendor/autoload.php')) { + $vendorDir = dirname($bootstrapFile); + $civiCrmVendorDir = $vendorDir . '/civicrm'; + $civiCrmCoreDir = $civiCrmVendorDir . '/civicrm-core'; + if (file_exists($civiCrmCoreDir)) { + set_include_path(get_include_path() + . PATH_SEPARATOR . $civiCrmCoreDir + . PATH_SEPARATOR . $civiCrmVendorDir . '/civicrm-packages' + ); + // $bootstrapFile might not be included, yet. It is required for the + // following require_once, though. + require_once $bootstrapFile; + // Prevent error "Class 'CRM_Core_Exception' not found in file". + require_once $civiCrmCoreDir . '/CRM/Core/Exception.php'; + + break; + } + } +} + +if (file_exists(__DIR__ . '/vendor/autoload.php')) { + require_once __DIR__ . '/vendor/autoload.php'; +} diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..4877d3a --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,38 @@ + + + + + + + + + + + ./tests/phpunit + + + + + + api + CRM + Civi + + + CRM/Bic/DAO + + + + + + + + + + diff --git a/tests/docker-compose.yml b/tests/docker-compose.yml new file mode 100644 index 0000000..30bc030 --- /dev/null +++ b/tests/docker-compose.yml @@ -0,0 +1,35 @@ +services: + civicrm: + image: michaelmcandrew/civicrm:${CIVICRM_IMAGE_TAG:-drupal} + environment: + - PROJECT_NAME=test + - BASE_URL=http://localhost + - CIVICRM_DB_NAME=test + - CIVICRM_DB_USER=root + - CIVICRM_DB_PASS=secret + - CIVICRM_DB_HOST=mysql + - CIVICRM_DB_PORT=3306 + - CIVICRM_CRED_KEYS=aes-cbc::test + - CIVICRM_SIGN_KEYS=jwt-hs256::test + - CIVICRM_SITE_KEY=TEST_KEY + - DRUPAL_DB_NAME=test + - DRUPAL_DB_USER=root + - DRUPAL_DB_PASS=secret + - DRUPAL_DB_HOST=mysql + - DRUPAL_DB_PORT=3306 + - PHP_DATE_TIMEZONE=UTC + - DEBUG=ON + - SMTP_HOST=localhost + - SMTP_MAILDOMAIN=example.org + volumes: + - ../:/var/www/html/sites/default/files/civicrm/ext/org.project60.bic:${BIND_VOLUME_PERMISSIONS:-ro} + - /var/www/html/sites/default/files/civicrm/ext/org.project60.bic/vendor + - /var/www/html/sites/default/files/civicrm/ext/org.project60.bic/tools/phpunit/vendor + # Don't start Apache HTTP Server, but keep container running + command: ["tail", "-f", "/dev/null"] + stop_signal: SIGKILL + mysql: + image: mariadb + environment: + MARIADB_ROOT_PASSWORD: secret + MARIADB_DATABASE: test diff --git a/tests/docker-phpunit.sh b/tests/docker-phpunit.sh new file mode 100755 index 0000000..9271976 --- /dev/null +++ b/tests/docker-phpunit.sh @@ -0,0 +1,29 @@ +#!/bin/bash +set -eu -o pipefail + +SCRIPT_DIR=$(realpath "$(dirname "$0")") +EXT_DIR=$(dirname "$SCRIPT_DIR") + +cd "$EXT_DIR" +if [ ! -e tools/phpunit/vendor/bin ]; then + "$SCRIPT_DIR/docker-prepare.sh" +fi + +# CIVICRM_SMARTY_AUTOLOAD_PATH is not set in the container's civicrm.settings.php so we have to do it here. +# Otherwise this results in this error: +# Fatal error: Cannot declare class Smarty, because the name is already in use in /var/www/html/sites/all/modules/civicrm/packages/smarty5/Smarty.php on line 4 +smarty=$(printf '%s\n' /var/www/html/sites/all/modules/civicrm/packages/smarty* | sort -r | head -n1) +if [ -e "$smarty/Smarty.php" ]; then + export CIVICRM_SMARTY_AUTOLOAD_PATH="$smarty/Smarty.php" +elif [ -e "$smarty/vendor/autoload.php" ]; then + export CIVICRM_SMARTY_AUTOLOAD_PATH="$smarty/vendor/autoload.php" +fi + +export XDEBUG_MODE=coverage +# TODO: Remove when not needed, anymore. +# In Docker container with CiviCRM 5.5? all deprecations are reported as direct +# deprecations so "disabling" check of deprecation count is necessary for the +# tests to pass (if baselineFile does not contain all deprecations). +export SYMFONY_DEPRECATIONS_HELPER="max[total]=99999&baselineFile=./tests/ignored-deprecations.json" + +composer phpunit -- --cache-result-file=/tmp/.phpunit.result.cache "$@" diff --git a/tests/docker-prepare.sh b/tests/docker-prepare.sh new file mode 100755 index 0000000..e0a5119 --- /dev/null +++ b/tests/docker-prepare.sh @@ -0,0 +1,59 @@ +#!/bin/bash +set -eu -o pipefail + +EXT_DIR=$(dirname "$(dirname "$(realpath "$0")")") +EXT_NAME=$(basename "$EXT_DIR") + +if ! type git >/dev/null 2>&1; then + apt -y update + apt -y install git +fi + +# Prevent this git error: The repository does not have the correct ownership and git refuses to use it +git config --global --add safe.directory "/var/www/html/sites/default/files/civicrm/ext/$EXT_NAME" + +i=0 +while ! mysql -h "$CIVICRM_DB_HOST" -P "$CIVICRM_DB_PORT" -u "$CIVICRM_DB_USER" --password="$CIVICRM_DB_PASS" -e 'SELECT 1;' >/dev/null 2>&1; do + i=$((i+1)) + if [ $i -gt 10 ]; then + echo "Failed to connect to database" >&2 + exit 1 + fi + + echo -n . + sleep 1 +done + +echo + +export XDEBUG_MODE=off +if mysql -h "$CIVICRM_DB_HOST" -P "$CIVICRM_DB_PORT" -u "$CIVICRM_DB_USER" --password="$CIVICRM_DB_PASS" "$CIVICRM_DB_NAME" -e 'SELECT 1 FROM civicrm_setting LIMIT 1;' >/dev/null 2>&1; then + cv flush +else + # For headless tests it is required that CIVICRM_UF is defined using the corresponding env variable. + sed -E "s/define\('CIVICRM_UF', '([^']+)'\);/define('CIVICRM_UF', getenv('CIVICRM_UF') ?: '\1');/g" \ + -i /var/www/html/sites/default/civicrm.settings.php + civicrm-docker-install + + # Avoid this error: + # The autoloader expected class "Civi\ActionSchedule\Mapping" to be defined in + # file "[...]/Civi/ActionSchedule/Mapping.php". The file was found but the + # class was not in it, the class name or namespace probably has a typo. + # + # Necessary for CiviCRM 5.66.0 - 5.74.x. + # https://github.com/civicrm/civicrm-core/blob/5.66.0/Civi/ActionSchedule/Mapping.php + if [ -e /var/www/html/sites/all/modules/civicrm/Civi/ActionSchedule/Mapping.php ] \ + && grep -q '// Empty file' /var/www/html/sites/all/modules/civicrm/Civi/ActionSchedule/Mapping.php; then + rm /var/www/html/sites/all/modules/civicrm/Civi/ActionSchedule/Mapping.php + fi + + # For headless tests these files need to exist. + touch /var/www/html/sites/all/modules/civicrm/sql/test_data.mysql + touch /var/www/html/sites/all/modules/civicrm/sql/test_data_second_domain.mysql + + cv ext:enable "$EXT_NAME" +fi + +cd "$EXT_DIR" +composer update --no-progress --prefer-dist --optimize-autoloader +composer composer-phpunit -- update --no-progress --prefer-dist diff --git a/tests/ignored-deprecations.json b/tests/ignored-deprecations.json new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/tests/ignored-deprecations.json @@ -0,0 +1 @@ +[] diff --git a/tests/phpunit/bootstrap.php b/tests/phpunit/bootstrap.php new file mode 100644 index 0000000..210c17a --- /dev/null +++ b/tests/phpunit/bootstrap.php @@ -0,0 +1,122 @@ +add('CRM_', [$extensionDir]); + $loader->addPsr4('Civi\\', [$extensionDir . '/Civi']); + $loader->add('api_', [$extensionDir]); + $loader->addPsr4('api\\', [$extensionDir . '/api']); + $loader->register(); + + if (file_exists($extensionDir . '/autoload.php')) { + require_once $extensionDir . '/autoload.php'; + } +} + +/** + * Call the "cv" command. + * + * @param string $cmd + * The rest of the command to send. + * @param string $decode + * Ex: 'json' or 'phpcode'. + * @return mixed + * Response output (if the command executed normally). + * For 'raw' or 'phpcode', this will be a string. For 'json', it could be any JSON value. + * @throws \RuntimeException + * If the command terminates abnormally. + */ +function cv(string $cmd, string $decode = 'json') { + $cmd = 'cv ' . $cmd; + $descriptorSpec = [0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => STDERR]; + $oldOutput = getenv('CV_OUTPUT'); + putenv('CV_OUTPUT=json'); + + // Execute `cv` in the original folder. This is a work-around for + // phpunit/codeception, which seem to manipulate PWD. + $cmd = sprintf('cd %s; %s', escapeshellarg(getenv('PWD')), $cmd); + + $process = proc_open($cmd, $descriptorSpec, $pipes, __DIR__); + putenv("CV_OUTPUT=$oldOutput"); + fclose($pipes[0]); + $result = stream_get_contents($pipes[1]); + fclose($pipes[1]); + if (proc_close($process) !== 0) { + throw new \RuntimeException("Command failed ($cmd):\n$result"); + } + switch ($decode) { + case 'raw': + return $result; + + case 'phpcode': + // If the last output is /*PHPCODE*/, then we managed to complete execution. + if (substr(trim($result), 0, 12) !== '/*BEGINPHP*/' || substr(trim($result), -10) !== '/*ENDPHP*/') { + throw new \RuntimeException("Command failed ($cmd):\n$result"); + } + return $result; + + case 'json': + return json_decode($result, TRUE); + + default: + throw new \RuntimeException("Bad decoder format ($decode)"); + } +} diff --git a/tools/create-release.sh b/tools/create-release.sh new file mode 100755 index 0000000..29cbc35 --- /dev/null +++ b/tools/create-release.sh @@ -0,0 +1,399 @@ +#!/bin/bash +set -euo pipefail + +# shellcheck disable=SC2155 +readonly PHP=$(which "${PHP:-php}") +# shellcheck disable=SC2155 +readonly COMPOSER=$(which "${COMPOSER:-composer}") +# shellcheck disable=SC2155 +readonly JQ=$(which "${JQ:-jq}") + +if [ -z "$PHP" ]; then + echo "php not found" >&2 + exit 1 +fi + +if [ -z "$COMPOSER" ]; then + echo "composer not found" >&2 + exit 1 +fi + +if [ -z "$JQ" ]; then + echo "jq not found" >&2 + exit 1 +fi + +# shellcheck disable=SC2155 +readonly SCRIPT_NAME=$(basename "$0") + +usage() { + cat <.*' info.xml | sed 's#[[:space:]]*\(.*\)[[:space:]]*#\1#') + if [ -z "$version" ]; then + echo "Version not found in info.xml" >&2 + exit 1 + fi + + if ! [[ "$version" =~ ^([0-9]+\.[0-9]+\.[0-9]+)-dev$ ]]; then + echo "The version number $version doesn't match the form a.b.c-dev" >&2 + exit 1 + fi + + echo "${BASH_REMATCH[1]}" +} + +detectDevelStage() { + local -r version=$1 + if [[ "$version" = *alpha* ]]; then + echo alpha + elif [[ "$version" = *beta* ]]; then + echo beta + elif [[ "$version" = 0.* ]]; then + echo dev + else + echo stable + fi +} + +detectNextVersion() { + local -r version=$1 + [[ "$version" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+) ]] + + local -r major=${BASH_REMATCH[1]} + local minor=${BASH_REMATCH[2]} + local patch=${BASH_REMATCH[3]} + + if [[ "$version" = *-* ]]; then + # $version has pre-release + true + elif [ "$major" = 0 ]; then + patch=$((patch+1)) + elif [ "$patch" = 0 ]; then + minor=$((minor+1)) + else + patch=$((patch+1)) + fi + + echo "$major.$minor.$patch-dev" +} + +validateVersion() { + local -r version=$1 + if ! [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-(alpha|beta)[1-9][0-9]*)?$ ]]; then + echo "The version number $version is not a valid release version" >&2 + exit 1 + fi + + if [ -n "$(git tag -l "$version")" ]; then + echo "git tag $version already exists" >&2 + exit 1 + fi +} + +validateDevelopmentStage() { + local -r develStage=$1 + if ! [[ "$develStage" =~ ^(stable|alpha|beta|dev)$ ]]; then + echo "$develStage is not a valid development stage" >&2 + exit 1 + fi +} + +validateNextVersion() { + local -r nextVersion=$1 + if ! [[ "$nextVersion" =~ ^[0-9]+\.[0-9]+\.[0-9]+-dev$ ]]; then + echo "The version number $nextVersion is not a valid next version" >&2 + exit 1 + fi +} + +validateInfoXml() { + if [ ! -f info.xml ]; then + echo "info.xml not found in working directory" >&2 + exit 1 + fi + + # Ensure info.xml contains elements so that they can be replaced in updateInfoXml. + if ! grep -q ".*" info.xml; then + echo "version element not found in info.xml" >&2 + exit 1 + fi + + if ! grep -q ".*" info.xml; then + echo "develStage element not found in info.xml" >&2 + exit 1 + fi + + if ! grep -q -e "" -e ".*" info.xml; then + echo "releaseDate element not found in info.xml" >&2 + exit 1 + fi +} + +updateInfoXml() { + local -r version=$1 + local -r develStage=$2 + local -r releaseDate=${3:-} + + if [ -z "$releaseDate" ]; then + local -r releaseDateXml="" + else + local -r releaseDateXml="$releaseDate" + fi + + sed -i -e "s#.*#$version#g" \ + -e "s#.*#$develStage#g" \ + -e "s#.*#$releaseDateXml#g" \ + -e "s##$releaseDateXml#g" \ + info.xml +} + +hasComposerRequires() { + if [ ! -f composer.json ]; then + return 1 + fi + + # All requires that are not "php" or "ext-*". + requires=$("$JQ" -r '.require|keys|.[]' composer.json 2>/dev/null | sed -e '/^php$/d' -e '/ext-.*/d') + [ "$requires" != "" ] +} + +isVersionLesser() { + "$PHP" -r "if (version_compare('$1', '$2', '>=')) exit(1);" +} + +getMinPhpVersion() { + local phpVersion="" + local -r phpConstraint=$("$JQ" --raw-output --monochrome-output .require.php composer.json) + if [ "$phpConstraint" = "null" ]; then + echo "PHP version constraint not found in composer.json. Please consider adding it" >&2 + echo -n "Minimal supported PHP version: " >&2 + read -r phpVersion + else + local -r oldIfs=$IFS + IFS=' |' + local constraint + for constraint in $phpConstraint; do + if [[ "$constraint" =~ ^(\^|~|>=)([0-9]+(\.[0-9]+(\.[0-9]+)?)?)$ ]]; then + if [ -z "$phpVersion" ] || isVersionLesser "${BASH_REMATCH[2]}" "$phpVersion"; then + phpVersion=${BASH_REMATCH[2]} + fi + fi + done + IFS=$oldIfs + + if [ -n "$phpVersion" ]; then + echo -n "Minimal supported PHP version [$phpVersion]: " >&2 + read -r input + if [ "$input" != "" ]; then + phpVersion=$input + fi + else + echo "Minimal supported PHP version could not be detected from composer version constraint. (Supported operators: ^, ~, >=, |)" >&2 + echo -n "Minimal supported PHP version: " >&2 + read -r phpVersion + fi + fi + + echo "$phpVersion" +} + +validateMinPhpVersion() { + if ! [[ "$1" =~ ^[0-9]+(\.[0-9]+(\.[0-9]+)?)?$ ]]; then + echo "$1 is not a supported minimal PHP version" >&2 + exit 1 + fi +} + +updatePot() { + local -r potFiles=(l10n/*.pot) + if [ ${#potFiles[@]} -ge 1 ] && [ -e "${potFiles[0]}" ] && [ -x tools/update-pot.sh ]; then + echo "Update .pot file" + tools/update-pot.sh + if ! git diff --no-patch --exit-code "${potFiles[*]}"; then + echo ".pot file has changed. Please update the translation and push changes to the repository." >&2 + exit 1 + fi + fi +} + +main() { + DRY_RUN=0 + local noComposer=0 + local noPotUpdate=0 + + while [ $# -gt 0 ]; do + case $1 in + -h|--help) + usage + exit 0 + ;; + + --dry-run) + DRY_RUN=1 + shift + ;; + + --no-composer) + noComposer=1 + shift + ;; + + --no-pot-update) + noPotUpdate=1 + shift + ;; + + *) + break + ;; + esac + done + + if [ $# -gt 3 ]; then + usage >&2 + exit 1 + fi + + validateInfoXml + + local version + local nextVersion + local develStage + + if [ $# -ge 1 ]; then + version=$1 + else + version=$(detectVersion) + echo -n "Version [$version]: " + read -r input + if [ -n "$input" ]; then + version=$input + fi + fi + validateVersion "$version" + + if [ $# -ge 2 ]; then + develStage=$2 + else + develStage=$(detectDevelStage "$version") + echo -n "Development stage [$develStage]: " + read -r input + if [ -n "$input" ]; then + develStage=$input + fi + fi + validateDevelopmentStage "$develStage" + + if [ $# -ge 3 ]; then + nextVersion=$3 + else + nextVersion=$(detectNextVersion "$version") + echo -n "Next version [$nextVersion]: " + read -r input + if [ -n "$input" ]; then + nextVersion=$input + fi + fi + validateNextVersion "$nextVersion" + + if [ $noComposer -eq 0 ] && ! hasComposerRequires; then + noComposer=1 + fi + + if [ $noComposer -eq 0 ]; then + local -r minPhpVersion=$(getMinPhpVersion) + validateMinPhpVersion "$minPhpVersion" + fi + + if [ $noPotUpdate -eq 0 ]; then + updatePot + fi + + local -r releaseDate=$(date +%Y-%m-%d) + run updateInfoXml "$version" "$develStage" "$releaseDate" + run git add info.xml + + if [ $noComposer -eq 0 ]; then + local -r previousPlatformPhp=$(composer config platform.php 2>/dev/null ||:) + run composer config platform.php "$minPhpVersion" + run composer update --no-dev --optimize-autoloader + if [ -n "$previousPlatformPhp" ]; then + run composer config platform.php "$previousPlatformPhp" + else + run composer config --unset platform.php + fi + run git add -f composer.lock vendor + fi + + run git commit -m "Set version to $version" + run git tag "$version" + + run updateInfoXml "$nextVersion" "dev" + run git add info.xml + + if [ $noComposer -eq 0 ]; then + run git rm -r composer.lock vendor + fi + + if [ -f composer.json ]; then + local -r branch=$(git branch --show-current) + if [ "$branch" = "main" ] || [ "$branch" = "master" ]; then + [[ "$nextVersion" =~ ^([0-9]+\.[0-9]+)\.[0-9]+ ]] + local -r alias=${BASH_REMATCH[1]}.x-dev + run composer config "extra.branch-alias.dev-$branch" "$alias" + run git add composer.json + fi + fi + + run git commit -m "Set version to $nextVersion" + + echo "" + echo "Push changes with: git push && git push --tags" +} + +main "$@" diff --git a/tools/git/hooks-wrapper.sh b/tools/git/hooks-wrapper.sh new file mode 100755 index 0000000..f32978c --- /dev/null +++ b/tools/git/hooks-wrapper.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +# Inspired by https://github.com/sjungwirth/githooks/blob/949d55e84e92dfd84e9a73a8b7624bb2e4dbc872/bin/git/hooks-wrapper + +# This script needs to be symlinked to .git/hooks/. + +set -e + +HOOKNAME=$(basename "$0") +NATIVE_HOOKS_DIR=$(dirname "$0") +CUSTOM_HOOKS_DIR=$(dirname "$(realpath "$0")")/hooks + +# Runs all executables in $CUSTOM_HOOKS_DIR/hooks/$HOOKNAME.d and +# $NATIVE_HOOKS_DIR/$HOOKNAME.local if existent. + +exitcode= +for hook in "$CUSTOM_HOOKS_DIR/$HOOKNAME.d/"*; do + if [ -x "$hook" ]; then + "$hook" "$@" || exitcode=${exitcode:-$?} + fi +done + +if [ -x "$NATIVE_HOOKS_DIR/$HOOKNAME.local" ]; then + "$NATIVE_HOOKS_DIR/$HOOKNAME.local" "$@" || exitcode=${exitcode:-$?} +fi + +# shellcheck disable=SC2086 +exit $exitcode diff --git a/tools/git/hooks/pre-commit.d/remember-update-pot.sh b/tools/git/hooks/pre-commit.d/remember-update-pot.sh new file mode 100755 index 0000000..827242c --- /dev/null +++ b/tools/git/hooks/pre-commit.d/remember-update-pot.sh @@ -0,0 +1,4 @@ +#!/bin/sh + +echo "Please remember to update the .pot file (tools/update-pot.sh) and the translation." + diff --git a/tools/git/hooks/pre-merge-commit.d/run-pre-commit-hook.sh b/tools/git/hooks/pre-merge-commit.d/run-pre-commit-hook.sh new file mode 100755 index 0000000..b3c4912 --- /dev/null +++ b/tools/git/hooks/pre-merge-commit.d/run-pre-commit-hook.sh @@ -0,0 +1,6 @@ +#!/bin/sh + +. git-sh-setup +if [ -x "$GIT_DIR/hooks/pre-commit" ]; then + exec "$GIT_DIR/hooks/pre-commit" +fi diff --git a/tools/git/init-hooks.sh b/tools/git/init-hooks.sh new file mode 100755 index 0000000..b635da0 --- /dev/null +++ b/tools/git/init-hooks.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +# Inspired by https://github.com/sjungwirth/githooks/blob/949d55e84e92dfd84e9a73a8b7624bb2e4dbc872/bin/git/init-hooks + +set -e + +SCRIPT_DIR=$(dirname "$0") +NATIVE_HOOKS_DIR=$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel)/.git/hooks +CUSTOM_HOOKS_DIR=$SCRIPT_DIR/hooks +HOOKS_WRAPPER=$(realpath -s --relative-to="$NATIVE_HOOKS_DIR" "$SCRIPT_DIR")/hooks-wrapper.sh + +cd "$CUSTOM_HOOKS_DIR" +HOOK_DIRS=(*.d) + +for hook_dir in "${HOOK_DIRS[@]}"; do + hookname=${hook_dir:0:-2} + if [ ! -L "$NATIVE_HOOKS_DIR/$hookname" ]; then + if [ -f "$NATIVE_HOOKS_DIR/$hookname" ]; then + mv "$NATIVE_HOOKS_DIR/$hookname" "$NATIVE_HOOKS_DIR/$hookname.local" + fi + ln -s "$HOOKS_WRAPPER" "$NATIVE_HOOKS_DIR/$hookname" + fi +done diff --git a/tools/phpcs/composer.json b/tools/phpcs/composer.json new file mode 100644 index 0000000..980e4b9 --- /dev/null +++ b/tools/phpcs/composer.json @@ -0,0 +1,11 @@ +{ + "repositories": [ + { + "type": "git", + "url": "https://github.com/civicrm/coder.git" + } + ], + "require": { + "drupal/coder": "dev-8.x-2.x-civi" + } +} diff --git a/tools/phpstan/composer.json b/tools/phpstan/composer.json new file mode 100644 index 0000000..ce2fee2 --- /dev/null +++ b/tools/phpstan/composer.json @@ -0,0 +1,19 @@ +{ + "require": { + "kcs/phpstan-strict-rules": "^2", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2", + "phpstan/phpstan-beberlei-assert": "^2", + "phpstan/phpstan-deprecation-rules": "^2", + "phpstan/phpstan-phpunit": "^2", + "phpstan/phpstan-strict-rules": "^2", + "phpstan/phpstan-webmozart-assert": "^2", + "voku/phpstan-rules": "^3.6" + }, + "config": { + "allow-plugins": { + "phpstan/extension-installer": true + }, + "sort-packages": true + } +} diff --git a/tools/phpunit/composer.json b/tools/phpunit/composer.json new file mode 100644 index 0000000..ab64ce0 --- /dev/null +++ b/tools/phpunit/composer.json @@ -0,0 +1,13 @@ +{ + "require": { + "symfony/phpunit-bridge": "^7" + }, + "scripts": { + "post-install-cmd": [ + "@php vendor/bin/simple-phpunit install --configuration ../../phpunit.xml.dist" + ], + "post-update-cmd": [ + "@php vendor/bin/simple-phpunit install --configuration ../../phpunit.xml.dist" + ] + } +} diff --git a/tools/update-pot.sh b/tools/update-pot.sh new file mode 100755 index 0000000..485335f --- /dev/null +++ b/tools/update-pot.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +set -euo pipefail + +readonly SCRIPT_PATH="$0" +SCRIPT_NAME=$(basename "$SCRIPT_PATH") +readonly SCRIPT_NAME +SCRIPT_DIR=$(dirname "$SCRIPT_PATH") +readonly SCRIPT_DIR + +usage() { + cat <