From 43bd0534502afbfa857c648d8cc9cf32b2022abe Mon Sep 17 00:00:00 2001 From: Simon Cossar Date: Tue, 11 May 2021 15:31:24 -0700 Subject: [PATCH] Bump version to 2.2.4 (#404) * Add base log classes * Return maxFiles to normal level * Use protected class variables for folder names in folder-manager * Add unit tests for logger classes && various logger improvements * Add log viewer * Fix initialization sequence in LogViewer * Add wp-discourse settings to plugin meta * Remove metafile comments * Add partial coverage and annotate LogViewer * Add code coverage reporting and a tests readme * Tests readme xdebug section formatting * Add logging and tests to discourse-publish This abstracts remote post components to make it possible to add consistent error and log handling. Also adds basic tests coverage for discourse-publish. * Add successful publication test * Add working tests for publish_after_create and publish_after_update * Always remove test files and database upon install * Cleanup copy and assertions for existing tests * Final cleanup && verbose setting * Improve structure of publish test * Final tests, linting, security and cleanup * PHP 7.0 Compatibility * PHP 5.6 Compatibility * JSHint fixes * Update file-handler.php * Update log viewer title * Use older monolog and update file_handler function signatures * Add nonce to other view_log action * Namespace production composer packages and define build process * Update COMPOSER.md * Update FORMATTING.md * Log viewer style, naming and log-refresh improvements * Filter out all return type declarations during scoping * JsHint: Don't use default params * Update COMPOSER.md * Copy fix * Update scoper patchers notes * Address syntax issues - Remove >php7 syntax from non-required files - Add phpcs pattern exclusions to phpcs.xml - update formatting docs * discourse-publish: address all phpcs notices and add more tests Note: also added dealerdirect/phpcodesniffer-composer-installer to handle local requiring of codesniffer * Handle all phpcs warnings in lib/logs * Add todo: review phpcs exclusions to discourse-publish * Monolog cleanup - Remove unused monolog handlers, processors and formatters - Add vendor_namespaced to excluded phpcs patterns * Update CI versions to those used in composer * Switch to using composer directly in CI actions * Composer is packaged in shivammathur/setup-php * Setup PHPCS via shivammathur/setup-php * Incorrect tools key * Use vendor/bin version of phpcs * Install composer dependencies via ramsey/composer-install * Update composer.lock to composer 2 and --ignore-platform-reqs * Install lowest version of dependencies * Move dependency-versions key * Move composer-options key * Exclude vendor directory from syntax checker * Add vendor to jshintignore * Update phpcs.xml to properly exclude js css and config files * Address phpcs issues in log-viewer * Fix remaining whitespace issues created in this PR * Remove out of date sniffs and exclude specific code where necessary * Final cleanup * Properly escape html in log viewer * Remove unnecessary verbiage from documentation * Bump plugin's version to 2.2.4 Co-authored-by: Angus McLeod --- .github/workflows/ci.yml | 17 +- .jshintignore | 3 +- admin/admin-menu.php | 19 + admin/admin.php | 7 +- admin/css/admin-styles.css | 63 + admin/js/admin.js | 104 ++ admin/log-viewer.php | 405 +++++ admin/options-page.php | 14 +- admin/publish-settings.php | 29 + admin/settings-validator.php | 1 + bin/install-wp-tests.sh | 161 ++ composer.json | 26 +- composer.lock | 1406 ++++++++--------- docs/COMPOSER.md | 85 + docs/FORMATTING.md | 54 + docs/TESTS.md | 116 ++ lib/discourse-publish.php | 526 ++++-- lib/logs/formatters/line-formatter.php | 25 + lib/logs/handlers/file-handler.php | 359 +++++ lib/logs/handlers/null-handler.php | 13 + lib/logs/logger.php | 59 + lib/logs/managers/file-manager.php | 162 ++ lib/sso-client/nonce.php | 3 + lib/sync-discourse-topic.php | 3 + lib/utilities.php | 2 +- phpcs.xml | 23 +- phpunit.xml | 22 + readme.txt | 8 +- scoper.inc.php | 132 ++ tests/fixtures/response_body/post_create.json | 66 + tests/fixtures/response_body/post_update.json | 68 + tests/phpunit/bootstrap.php | 33 + tests/phpunit/multisite.xml | 23 + .../test-discourse-publish-multisite.php | 85 + tests/phpunit/test-discourse-publish.php | 764 +++++++++ tests/phpunit/test-file-handler.php | 233 +++ tests/phpunit/test-file-manager.php | 143 ++ tests/phpunit/test-log-viewer.php | 65 + tests/phpunit/test-logger.php | 67 + vendor_namespaced/monolog/monolog/LICENSE | 19 + .../Monolog/Formatter/FormatterInterface.php | 34 + .../src/Monolog/Formatter/LineFormatter.php | 150 ++ .../Monolog/Formatter/NormalizerFormatter.php | 146 ++ .../src/Monolog/Handler/AbstractHandler.php | 172 ++ .../Handler/AbstractProcessingHandler.php | 59 + .../src/Monolog/Handler/HandlerInterface.php | 82 + .../src/Monolog/Handler/NullHandler.php | 41 + .../src/Monolog/Handler/StreamHandler.php | 160 ++ .../monolog/monolog/src/Monolog/Logger.php | 692 ++++++++ .../src/Monolog/ResettableInterface.php | 30 + .../monolog/monolog/src/Monolog/Utils.php | 162 ++ vendor_namespaced/psr/log/LICENSE | 19 + .../psr/log/Psr/Log/AbstractLogger.php | 121 ++ .../log/Psr/Log/InvalidArgumentException.php | 7 + .../psr/log/Psr/Log/LogLevel.php | 18 + .../psr/log/Psr/Log/LoggerAwareInterface.php | 18 + .../psr/log/Psr/Log/LoggerAwareTrait.php | 25 + .../psr/log/Psr/Log/LoggerInterface.php | 117 ++ .../psr/log/Psr/Log/LoggerTrait.php | 134 ++ .../psr/log/Psr/Log/NullLogger.php | 30 + wp-discourse.php | 6 +- 61 files changed, 6741 insertions(+), 895 deletions(-) create mode 100644 admin/log-viewer.php create mode 100755 bin/install-wp-tests.sh create mode 100644 docs/COMPOSER.md create mode 100644 docs/FORMATTING.md create mode 100644 docs/TESTS.md create mode 100644 lib/logs/formatters/line-formatter.php create mode 100644 lib/logs/handlers/file-handler.php create mode 100644 lib/logs/handlers/null-handler.php create mode 100644 lib/logs/logger.php create mode 100644 lib/logs/managers/file-manager.php create mode 100644 phpunit.xml create mode 100644 scoper.inc.php create mode 100644 tests/fixtures/response_body/post_create.json create mode 100644 tests/fixtures/response_body/post_update.json create mode 100644 tests/phpunit/bootstrap.php create mode 100644 tests/phpunit/multisite.xml create mode 100644 tests/phpunit/multisite/test-discourse-publish-multisite.php create mode 100644 tests/phpunit/test-discourse-publish.php create mode 100644 tests/phpunit/test-file-handler.php create mode 100644 tests/phpunit/test-file-manager.php create mode 100644 tests/phpunit/test-log-viewer.php create mode 100644 tests/phpunit/test-logger.php create mode 100644 vendor_namespaced/monolog/monolog/LICENSE create mode 100644 vendor_namespaced/monolog/monolog/src/Monolog/Formatter/FormatterInterface.php create mode 100644 vendor_namespaced/monolog/monolog/src/Monolog/Formatter/LineFormatter.php create mode 100644 vendor_namespaced/monolog/monolog/src/Monolog/Formatter/NormalizerFormatter.php create mode 100644 vendor_namespaced/monolog/monolog/src/Monolog/Handler/AbstractHandler.php create mode 100644 vendor_namespaced/monolog/monolog/src/Monolog/Handler/AbstractProcessingHandler.php create mode 100644 vendor_namespaced/monolog/monolog/src/Monolog/Handler/HandlerInterface.php create mode 100644 vendor_namespaced/monolog/monolog/src/Monolog/Handler/NullHandler.php create mode 100644 vendor_namespaced/monolog/monolog/src/Monolog/Handler/StreamHandler.php create mode 100644 vendor_namespaced/monolog/monolog/src/Monolog/Logger.php create mode 100644 vendor_namespaced/monolog/monolog/src/Monolog/ResettableInterface.php create mode 100644 vendor_namespaced/monolog/monolog/src/Monolog/Utils.php create mode 100644 vendor_namespaced/psr/log/LICENSE create mode 100644 vendor_namespaced/psr/log/Psr/Log/AbstractLogger.php create mode 100644 vendor_namespaced/psr/log/Psr/Log/InvalidArgumentException.php create mode 100644 vendor_namespaced/psr/log/Psr/Log/LogLevel.php create mode 100644 vendor_namespaced/psr/log/Psr/Log/LoggerAwareInterface.php create mode 100644 vendor_namespaced/psr/log/Psr/Log/LoggerAwareTrait.php create mode 100644 vendor_namespaced/psr/log/Psr/Log/LoggerInterface.php create mode 100644 vendor_namespaced/psr/log/Psr/Log/LoggerTrait.php create mode 100644 vendor_namespaced/psr/log/Psr/Log/NullLogger.php diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e5dde89..cf0f322 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,9 +10,6 @@ jobs: build: runs-on: ubuntu-latest name: PHP ${{ matrix.php }} - env: - PHPCS_DIR: "/tmp/phpcs" - SNIFFS_DIR: "/tmp/sniffs" strategy: matrix: include: @@ -28,21 +25,23 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} + - name: Install Composer + if: "matrix.sniff" + uses: ramsey/composer-install@v1 + with: + dependency-versions: "lowest" + composer-options: "--ignore-platform-reqs" - name: Install Tools if: "matrix.sniff" run: | git clone --depth 1 git://github.com/phpenv/phpenv.git ~/.phpenv export PATH="$HOME/.phpenv/bin:$PATH" - git clone -b 2.9.1 --depth 1 https://github.com/squizlabs/PHP_CodeSniffer.git $PHPCS_DIR - git clone -b 0.11.0 --depth 1 https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards.git $SNIFFS_DIR - git clone -b 7.1.5 --depth 1 https://github.com/wimg/PHPCompatibility.git $SNIFFS_DIR/PHPCompatibility - $PHPCS_DIR/scripts/phpcs --config-set installed_paths $SNIFFS_DIR phpenv rehash sudo npm install -g jshint wget https://develop.svn.wordpress.org/trunk/.jshintrc - name: Syntax Check run: | - find -L . -name '*.php' -print0 | xargs -0 -n 1 -P 4 php -l + find -L . -name '*.php' -not -path "./vendor/*" -print0 | xargs -0 -n 1 -P 4 php -l - name: JSHint if: "matrix.sniff" run: | @@ -50,4 +49,4 @@ jobs: - name: WordPress Coding Standards if: "matrix.sniff" run: | - $PHPCS_DIR/scripts/phpcs -p -s -v -n . --ignore=*/tests/*,*/tests/lib/*,*/lib/wp-new-user-notification.php*,*/vendor/*,*/admin/discourse-sidebar/build/index.asset.php* --extensions=php + vendor/bin/phpcs -p -s -v -n . diff --git a/.jshintignore b/.jshintignore index 01a27f1..3c0becf 100644 --- a/.jshintignore +++ b/.jshintignore @@ -1,2 +1,3 @@ admin/discourse-sidebar/src/* -admin/discourse-sidebar/build/* \ No newline at end of file +admin/discourse-sidebar/build/* +vendor/* \ No newline at end of file diff --git a/admin/admin-menu.php b/admin/admin-menu.php index 791b2e9..b7059f7 100644 --- a/admin/admin-menu.php +++ b/admin/admin-menu.php @@ -123,6 +123,16 @@ class AdminMenu { array( $this, 'sso_options_tab' ) ); add_action( 'load-' . $sso_settings, array( $this->form_helper, 'connection_status_notice' ) ); + + $log_viewer = add_submenu_page( + 'wp_discourse_options', + __( 'Logs', 'wp-discourse' ), + __( 'Logs', 'wp-discourse' ), + 'manage_options', + 'log_viewer', + array( $this, 'log_viewer_tab' ) + ); + add_action( 'load-' . $log_viewer, array( $this->form_helper, 'connection_status_notice' ) ); } /** @@ -178,4 +188,13 @@ class AdminMenu { $this->options_page->display( 'sso_options' ); } } + + /** + * Called to display the 'log_viewer' tab. + */ + public function log_viewer_tab() { + if ( current_user_can( 'manage_options' ) ) { + $this->options_page->display( 'log_viewer' ); + } + } } diff --git a/admin/admin.php b/admin/admin.php index 511d5f2..65501ce 100644 --- a/admin/admin.php +++ b/admin/admin.php @@ -4,6 +4,7 @@ * * @link https://github.com/discourse/wp-discourse/blob/master/lib/admin.php * @package WPDiscourse + * @todo Review phpcs exclusions */ namespace WPDiscourse\Admin; @@ -23,6 +24,7 @@ if ( is_admin() ) { require_once __DIR__ . '/admin-notice.php'; require_once __DIR__ . '/meta-box.php'; require_once __DIR__ . '/user-profile.php'; + require_once __DIR__ . '/log-viewer.php'; $form_helper = FormHelper::get_instance(); $options_page = OptionsPage::get_instance(); @@ -40,6 +42,7 @@ if ( is_admin() ) { new AdminNotice(); new MetaBox(); new UserProfile(); + new LogViewer(); add_action( 'admin_enqueue_scripts', __NAMESPACE__ . '\\enqueue_admin_scripts' ); if ( is_multisite() ) { @@ -63,6 +66,8 @@ function enqueue_admin_scripts() { $max_tags = ! isset( $commenting_options['max-tags'] ) ? 5 : $commenting_options['max-tags']; $data = array( 'maxTags' => $max_tags, + 'ajax' => admin_url( 'admin-ajax.php' ), + 'nonce' => wp_create_nonce( 'admin-ajax-nonce' ), ); wp_localize_script( 'admin_js', 'wpdc', $data ); } @@ -77,6 +82,6 @@ function enqueue_network_styles() { return; } - wp_register_style( 'wp_discourse_network_admin', WPDISCOURSE_URL . '/admin/css/network-admin-styles.css' ); + wp_register_style( 'wp_discourse_network_admin', WPDISCOURSE_URL . '/admin/css/network-admin-styles.css' ); // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion wp_enqueue_style( 'wp_discourse_network_admin' ); } diff --git a/admin/css/admin-styles.css b/admin/css/admin-styles.css index 361968e..043f550 100644 --- a/admin/css/admin-styles.css +++ b/admin/css/admin-styles.css @@ -205,3 +205,66 @@ button.wpdc-remove-tag:hover .wpdc-remove-tag-icon:before { .discourse-comment-type { padding-left: 23px; } + +#wpdc-log-viewer-controls { + display: flex; + align-items: center; + justify-content: space-between; +} + +#wpdc-log-viewer-controls .name { + display: flex; + align-items: center; +} + +#wpdc-log-viewer-controls .name h3 { + font-size: 1.2em; + margin-right: 1em; +} + +#wpdc-log-viewer-controls .load-log { + cursor: pointer; +} + +#wpdc-log-viewer-controls .load-log .return-to { + display: none; +} + +#wpdc-log-viewer-controls.meta .load-log .return-to { + display: block; +} + +#wpdc-log-viewer-controls.meta .load-log .refresh { + display: none; +} + +#wpdc-log-viewer { + height: 700px; + background: white; + display: inline-block; + width: 100%; + position: relative; + overflow: scroll; +} + +#wpdc-log-viewer .spinner { + visibility: hidden; + float: none; + position: absolute; + margin: 0; + top: 50%; + left: 50%; + transform: translate(-50%); +} + +#wpdc-log-viewer.loading .spinner { + visibility: visible; +} + +#wpdc-log-viewer pre { + visibility: visible; +} + +#wpdc-log-viewer.loading pre { + visibility: hidden; +} diff --git a/admin/js/admin.js b/admin/js/admin.js index 21cbb44..9e49c8a 100644 --- a/admin/js/admin.js +++ b/admin/js/admin.js @@ -125,4 +125,108 @@ $( '.discourse-comment-type' ).toggleClass( 'hidden' ); } ); + + var $logControls = $('#wpdc-log-viewer-controls'); + var $logViewer = $('#wpdc-log-viewer'); + + function handleLogResponse(response, logKey, meta) { + if (response && response.data) { + var title = (meta ? '' : 'Log for ') + response.data.name; + $logControls.find('h3').html(title); + $logViewer.find('pre').html(response.data.contents); + } + + if (logKey) { + $logViewer.data('log-key', logKey); + } + + $logControls.toggleClass('meta', meta); + $logViewer.removeClass('loading'); + } + + $logControls.find('select').on('change', function() { + var logKey = $logControls.find('select').val(); + + if (logKey) { + $logViewer.addClass('loading'); + + $.ajax({ + url: wpdc.ajax, + type: 'post', + data: { + action: 'wpdc_view_log', + nonce: wpdc.nonce, + key: logKey + }, + success: function(response) { + if (response.success) { + handleLogResponse(response, logKey, false); + } + } + }); + } + }); + + $logControls.find('.load-log').on('click', function() { + var logKey = $logViewer.data('log-key'); + + if (logKey) { + $logViewer.addClass('loading'); + + $.ajax({ + url: wpdc.ajax, + type: 'post', + data: { + action: 'wpdc_view_log', + nonce: wpdc.nonce, + key: logKey + }, + success: function(response) { + if (response.success) { + handleLogResponse(response, logKey, false); + } + } + }); + } + }); + + $logControls.find('.button.view-meta').on('click', function() { + $logViewer.addClass('loading'); + $.ajax({ + url: wpdc.ajax, + type: 'post', + data: { + action: 'wpdc_view_logs_metafile' + }, + success: function(response) { + if (response.success) { + handleLogResponse(response, null, true); + } + } + }); + }); + + $logControls.find('.button.download-logs').on('click', function() { + var xhr = new XMLHttpRequest(); + xhr.open('POST', wpdc.ajax + '?action=wpdc_download_logs', true); + xhr.onload = function() { + if (xhr.readyState === 4 && xhr.status === 200) { + var blob = new Blob([ xhr.response ], { type: 'application/zip' }); + var url = window.URL.createObjectURL(blob); + var a = document.createElement('a'); + + document.body.appendChild(a); + a.style = 'display:none'; + a.href = url; + a.download = xhr.getResponseHeader('Content-Disposition').split('filename=')[1]; + a.click(); + a.remove(); + setTimeout(function() { + window.URL.revokeObjectURL(url); + }); + } + }; + xhr.responseType = 'arraybuffer'; + xhr.send(); + }); })( jQuery ); diff --git a/admin/log-viewer.php b/admin/log-viewer.php new file mode 100644 index 0000000..eebdfd4 --- /dev/null +++ b/admin/log-viewer.php @@ -0,0 +1,405 @@ +metafile_name = 'logs-metafile'; + add_action( 'admin_init', array( $this, 'setup_log_viewer' ) ); + } + + /** + * Run LogViewer setup tasks. + * + * @param object $file_handler Instance of \WPDiscourse\Logs\FileHandler. + */ + public function setup_log_viewer( $file_handler = null ) { + if ( $file_handler ) { + $this->file_handler = $file_handler; + } else { + $this->file_handler = new FileHandler( new FileManager() ); + } + + $this->enabled = $this->file_handler->enabled(); + + if ( $this->enabled ) { + $this->setup_logs(); + $this->update_meta_file(); + + add_action( 'wp_ajax_wpdc_view_log', array( $this, 'log_file_contents' ) ); + add_action( 'wp_ajax_wpdc_view_logs_metafile', array( $this, 'meta_file_contents' ) ); + add_action( 'wp_ajax_wpdc_download_logs', array( $this, 'download_logs' ) ); + } + + $this->register_log_viewer(); + } + + /** + * Add settings section and register the setting. + */ + public function register_log_viewer() { + add_settings_section( + 'discourse_log_viewer', + __( 'Logs', 'wp-discourse' ), + array( + $this, + 'log_viewer_markup', + ), + 'discourse_logs' + ); + register_setting( 'discourse_logs', 'discourse_logs' ); + } + + /** + * Setup logs + */ + public function setup_logs() { + $this->retrieve_logs(); + + if ( ! empty( $this->logs ) && empty( $this->selected_log ) ) { + $this->selected_log = reset( $this->logs ); + } + } + + /** + * Outputs the markup for the log viewer. + */ + public function log_viewer_markup() { + $selected_log_key = null; + + if ( ! empty( $this->selected_log ) ) { + $selected_log_key = $this->build_log_key( $this->selected_log ); + } + + ?> + enabled ) : ?> + +

%s', esc_url( 'https://meta.discourse.org/t/50752' ), esc_html__( 'WP Discourse Setup', 'text-domain' ) ) ); ?>

+ logs ) ) : ?> +
+
+

+ + file_name( $this->selected_log ) ); ?> +

+ + + + +
+
+ +
+
+
+
+
+ +
selected_log['file'] ) ); ?>
+
+ +

+ + +

+ + file_handler; + $log_files = $file_handler->list_files(); + + $this->logs = array_reduce( + $log_files, + function ( $result, $log_file ) use ( $file_handler ) { + $date = $file_handler->get_date_from_url( $log_file ); + $number = $file_handler->get_number_from_url( $log_file ); + $log = array( + 'date' => $date, + 'number' => $number, + 'file' => $log_file, + ); + $result[ $this->build_log_key( $log ) ] = $log; + return $result; + }, + array() + ); + } + + /** + * Return log file contents for selected key. + */ + public function log_file_contents() { + // See further https://github.com/WordPress/WordPress-Coding-Standards/issues/869. + if ( ! isset( $_REQUEST['nonce'] ) || ! wp_verify_nonce( sanitize_key( $_REQUEST['nonce'] ), 'admin-ajax-nonce' ) || ! isset( $_POST['key'] ) ) { + wp_send_json_error(); + return; + } + + $log_key = sanitize_text_field( wp_unslash( $_POST ['key'] ) ); + $log = $this->logs[ $log_key ]; + + if ( $log ) { + $this->selected_log = $log; + + $response = array( + 'contents' => file_get_contents( $this->selected_log['file'] ), + 'name' => $this->file_name( $this->selected_log ), + ); + + wp_send_json_success( $response ); + } else { + wp_send_json_error(); + } + } + + /** + * Return log meta file contents. + */ + public function meta_file_contents() { + $response = array( + 'contents' => file_get_contents( $this->get_metafile_path() ), + 'name' => 'Log Meta File', + ); + wp_send_json_success( $response ); + } + + /** + * Download bundled log files. + */ + public function download_logs() { + $log_files = $this->file_handler->list_files(); + $date_range = $this->build_date_range( $log_files ); + + $plugin_data = get_plugin_data( WPDISCOURSE_PATH . 'wp-discourse.php' ); + $plugin_name = $plugin_data['TextDomain']; + $site_title = str_replace( ' ', '-', strtolower( get_bloginfo( 'name' ) ) ); + + $filename = "{$site_title}-{$plugin_name}-logs-$date_range.zip"; + $file = tempnam( 'tmp', $filename ); + $zip = new \ZipArchive(); + $zip->open( $file, \ZipArchive::OVERWRITE ); + + foreach ( $log_files as $log_file ) { + $name = $this->file_handler->get_filename( $log_file ); + $zip->addFile( $log_file, "$name.log" ); + } + + $metafile_name = $this->metafile_name; + $metafile_path = $this->get_metafile_path(); + $metafile_filename = "{$plugin_name}-{$metafile_name}-{$date_range}.txt"; + + $zip->addFile( $metafile_path, $metafile_filename ); + $zip->close(); + + header( 'Content-type: application/zip' ); + header( 'Content-Length: ' . filesize( $file ) ); + header( "Content-Disposition: attachment; filename=$filename" ); + readfile( $file ); + unlink( $file ); + + wp_die(); + } + + /** + * Update meta file. + */ + public function update_meta_file() { + $filename = $this->get_metafile_path(); + $contents = $this->build_metafile_contents(); + file_put_contents( $filename, $contents ); + } + + /** + * Retrieve logs. + */ + public function get_logs() { + return $this->logs; + } + + /** + * Retrieve enabled state. + */ + public function is_enabled() { + return $this->enabled; + } + + /** + * Generate file name. + * + * @param array $log_info Log info array. + */ + protected function file_name( $log_info ) { + $date = gmdate( get_option( 'date_format' ), strtotime( $log_info['date'] ) ); + $number = $log_info['number']; + $name = esc_html( $date ); + if ( $number > 1 ) { + $name .= ' (' . esc_html( $number ) . ')'; + } + return $name; + } + + /** + * Build log key from log in logs list. + * + * @param object $item Log object. + */ + protected function build_log_key( $item ) { + $date = $item['date']; + $number = $item['number']; + return "$date-$number"; + } + + /** + * Generate server statistics file. + */ + protected function build_metafile_contents() { + $contents = "### This file is included in log downloads ###\n\n"; + + global $wpdb; + global $wp_version; + + if ( method_exists( $wpdb, 'db_version' ) ) { + $mysql = preg_replace( '/[^0-9.].*/', '', $wpdb->db_version() ); + } else { + $mysql = 'N/A'; + } + $wp = $wp_version; + $php = phpversion(); + $multisite = is_multisite(); + + $contents .= "### Server ###\n\n"; + $contents .= "WordPress - $wp\n"; + $contents .= "PHP - $php\n"; + $contents .= "MySQL - $mysql\n\n"; + + $active_plugins = get_option( 'active_plugins' ); + $all_plugins = get_plugins(); + $plugins = array(); + + $contents .= "### Active Plugins ###\n\n"; + + foreach ( $all_plugins as $plugin_folder => $plugin_data ) { + if ( in_array( $plugin_folder, $active_plugins, true ) ) { + $contents .= "{$plugin_data["Name"]} - {$plugin_data["Version"]}\n"; + } + } + + $contents .= "\n### WP Discourse Settings (Secrets Excluded) ###\n\n"; + $excluded_keys = array( + 'url', + 'key', + 'secret', + 'text', + 'publish-username', + 'publish-category', + 'publish-failure-email', + 'login-path', + 'existing-comments-heading', + 'sso-client-login-form-redirect', + ); + + foreach ( $this->get_options() as $key => $value ) { + $exclude = false; + + foreach ( $excluded_keys as $excluded_key ) { + if ( strpos( $key, $excluded_key ) !== false ) { + $exclude = true; + } + } + + if ( ! $exclude ) { + if ( is_array( $value ) ) { + $value = implode( ',', $value ); + } + $contents .= "$key - $value\n"; + } + } + + return $contents; + } + + /** + * Get metafile name. + */ + protected function get_metafile_path() { + $metafile_dir = $this->file_handler->file_manager->upload_dir; + return "$metafile_dir/{$this->metafile_name}.txt"; + } + + /** + * Build date range. + * + * @param array $log_files List of log files. + */ + protected function build_date_range( $log_files ) { + $log_values = array_values( $log_files ); + $newest_file = reset( $log_files ); + $oldest_file = end( $log_values ); + $date_end = $this->file_handler->get_date_from_url( $newest_file ); + $date_start = $this->file_handler->get_date_from_url( $oldest_file ); + return "$date_start-$date_end"; + } +} diff --git a/admin/options-page.php b/admin/options-page.php index 538bfe8..5b1091f 100644 --- a/admin/options-page.php +++ b/admin/options-page.php @@ -96,6 +96,10 @@ class OptionsPage { + + + form_helper->checkbox_input( + 'verbose-publication-logs', + 'discourse_publish', + __( + 'Enable verbose logs for publication.', + 'wp-discourse' + ), + __( + 'Will log successful publications as well as errors.', + 'wp-discourse' + ) + ); + } + /** * Details for the 'publishing_options' tab. */ diff --git a/admin/settings-validator.php b/admin/settings-validator.php index 6c4d04e..11de0c4 100644 --- a/admin/settings-validator.php +++ b/admin/settings-validator.php @@ -105,6 +105,7 @@ class SettingsValidator { add_filter( 'wpdc_validate_hide_discourse_name_field', array( $this, 'validate_checkbox' ) ); add_filter( 'wpdc_validate_discourse_username_editable', array( $this, 'validate_checkbox' ) ); add_filter( 'wpdc_validate_discourse_direct_db_publication_flags', array( $this, 'validate_checkbox' ) ); + add_filter( 'wpdc_validate_discourse_verbose_publication_logs', array( $this, 'validate_checkbox' ) ); add_filter( 'wpdc_validate_enable_discourse_comments', array( $this, 'validate_checkbox' ) ); add_filter( 'wpdc_validate_comment_type', array( $this, 'validate_radio_string_value' ) ); diff --git a/bin/install-wp-tests.sh b/bin/install-wp-tests.sh new file mode 100755 index 0000000..413a583 --- /dev/null +++ b/bin/install-wp-tests.sh @@ -0,0 +1,161 @@ +#!/usr/bin/env bash + +if [ $# -lt 3 ]; then + echo "usage: $0 [db-host] [wp-version] [skip-database-creation]" + exit 1 +fi + +DB_NAME=$1 +DB_USER=$2 +DB_PASS=$3 +DB_HOST=${4-localhost} +WP_VERSION=${5-latest} +SKIP_DB_CREATE=${6-false} + +TMPDIR=${TMPDIR-/tmp} +TMPDIR=$(echo $TMPDIR | sed -e "s/\/$//") +WP_TESTS_DIR=${WP_TESTS_DIR-$TMPDIR/wordpress-tests-lib} +WP_CORE_DIR=${WP_CORE_DIR-$TMPDIR/wordpress/} + +rm -rf $WP_TESTS_DIR +rm -rf $WP_CORE_DIR + +download() { + if [ `which curl` ]; then + curl -s "$1" > "$2"; + elif [ `which wget` ]; then + wget -nv -O "$2" "$1" + fi +} + +if [[ $WP_VERSION =~ ^[0-9]+\.[0-9]+\-(beta|RC)[0-9]+$ ]]; then + WP_BRANCH=${WP_VERSION%\-*} + WP_TESTS_TAG="branches/$WP_BRANCH" + +elif [[ $WP_VERSION =~ ^[0-9]+\.[0-9]+$ ]]; then + WP_TESTS_TAG="branches/$WP_VERSION" +elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0-9]+ ]]; then + if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then + # version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x + WP_TESTS_TAG="tags/${WP_VERSION%??}" + else + WP_TESTS_TAG="tags/$WP_VERSION" + fi +elif [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then + WP_TESTS_TAG="trunk" +else + # http serves a single offer, whereas https serves multiple. we only want one + download http://api.wordpress.org/core/version-check/1.7/ /tmp/wp-latest.json + grep '[0-9]+\.[0-9]+(\.[0-9]+)?' /tmp/wp-latest.json + LATEST_VERSION=$(grep -o '"version":"[^"]*' /tmp/wp-latest.json | sed 's/"version":"//') + if [[ -z "$LATEST_VERSION" ]]; then + echo "Latest WordPress version could not be found" + exit 1 + fi + WP_TESTS_TAG="tags/$LATEST_VERSION" +fi +set -ex + +install_wp() { + + if [ -d $WP_CORE_DIR ]; then + return; + fi + + mkdir -p $WP_CORE_DIR + + if [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then + mkdir -p $TMPDIR/wordpress-nightly + download https://wordpress.org/nightly-builds/wordpress-latest.zip $TMPDIR/wordpress-nightly/wordpress-nightly.zip + unzip -q $TMPDIR/wordpress-nightly/wordpress-nightly.zip -d $TMPDIR/wordpress-nightly/ + mv $TMPDIR/wordpress-nightly/wordpress/* $WP_CORE_DIR + else + if [ $WP_VERSION == 'latest' ]; then + local ARCHIVE_NAME='latest' + elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+ ]]; then + # https serves multiple offers, whereas http serves single. + download https://api.wordpress.org/core/version-check/1.7/ $TMPDIR/wp-latest.json + if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then + # version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x + LATEST_VERSION=${WP_VERSION%??} + else + # otherwise, scan the releases and get the most up to date minor version of the major release + local VERSION_ESCAPED=`echo $WP_VERSION | sed 's/\./\\\\./g'` + LATEST_VERSION=$(grep -o '"version":"'$VERSION_ESCAPED'[^"]*' $TMPDIR/wp-latest.json | sed 's/"version":"//' | head -1) + fi + if [[ -z "$LATEST_VERSION" ]]; then + local ARCHIVE_NAME="wordpress-$WP_VERSION" + else + local ARCHIVE_NAME="wordpress-$LATEST_VERSION" + fi + else + local ARCHIVE_NAME="wordpress-$WP_VERSION" + fi + download https://wordpress.org/${ARCHIVE_NAME}.tar.gz $TMPDIR/wordpress.tar.gz + tar --strip-components=1 -zxmf $TMPDIR/wordpress.tar.gz -C $WP_CORE_DIR + fi + + download https://raw.github.com/markoheijnen/wp-mysqli/master/db.php $WP_CORE_DIR/wp-content/db.php +} + +install_test_suite() { + # portable in-place argument for both GNU sed and Mac OSX sed + if [[ $(uname -s) == 'Darwin' ]]; then + local ioption='-i.bak' + else + local ioption='-i' + fi + + # set up testing suite if it doesn't yet exist + if [ ! -d $WP_TESTS_DIR ]; then + # set up testing suite + mkdir -p $WP_TESTS_DIR + svn co --quiet https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/includes/ $WP_TESTS_DIR/includes + svn co --quiet https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/data/ $WP_TESTS_DIR/data + fi + + if [ ! -f wp-tests-config.php ]; then + download https://develop.svn.wordpress.org/${WP_TESTS_TAG}/wp-tests-config-sample.php "$WP_TESTS_DIR"/wp-tests-config.php + # remove all forward slashes in the end + WP_CORE_DIR=$(echo $WP_CORE_DIR | sed "s:/\+$::") + sed $ioption "s:dirname( __FILE__ ) . '/src/':'$WP_CORE_DIR/':" "$WP_TESTS_DIR"/wp-tests-config.php + sed $ioption "s/youremptytestdbnamehere/$DB_NAME/" "$WP_TESTS_DIR"/wp-tests-config.php + sed $ioption "s/yourusernamehere/$DB_USER/" "$WP_TESTS_DIR"/wp-tests-config.php + sed $ioption "s/yourpasswordhere/$DB_PASS/" "$WP_TESTS_DIR"/wp-tests-config.php + sed $ioption "s|localhost|${DB_HOST}|" "$WP_TESTS_DIR"/wp-tests-config.php + fi + +} + +install_db() { + + if [ ${SKIP_DB_CREATE} = "true" ]; then + return 0 + fi + + # parse DB_HOST for port or socket references + local PARTS=(${DB_HOST//\:/ }) + local DB_HOSTNAME=${PARTS[0]}; + local DB_SOCK_OR_PORT=${PARTS[1]}; + local EXTRA="" + + if ! [ -z $DB_HOSTNAME ] ; then + if [ $(echo $DB_SOCK_OR_PORT | grep -e '^[0-9]\{1,\}$') ]; then + EXTRA=" --host=$DB_HOSTNAME --port=$DB_SOCK_OR_PORT --protocol=tcp" + elif ! [ -z $DB_SOCK_OR_PORT ] ; then + EXTRA=" --socket=$DB_SOCK_OR_PORT" + elif ! [ -z $DB_HOSTNAME ] ; then + EXTRA=" --host=$DB_HOSTNAME --protocol=tcp" + fi + fi + + local exists=$(mysql -s -N -e "SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME='$DB_NAME'"); + if [ -n "$exists" ]; then + yes | mysqladmin drop $DB_NAME --user="$DB_USER" --password="$DB_PASS"$EXTRA + fi + mysqladmin create $DB_NAME --user="$DB_USER" --password="$DB_PASS"$EXTRA +} + +install_wp +install_test_suite +install_db diff --git a/composer.json b/composer.json index 1dbdab9..877b81e 100644 --- a/composer.json +++ b/composer.json @@ -14,7 +14,10 @@ "homepage": "https://github.com/eviltrout" } ], - "keywords": ["wordpress", "discourse"], + "keywords": [ + "wordpress", + "discourse" + ], "support": { "issues": "https://github.com/discourse/wp-discourse/issues" }, @@ -24,8 +27,25 @@ }, "require-dev": { "squizlabs/php_codesniffer": "3.*", + "dealerdirect/phpcodesniffer-composer-installer": "^0.7.1", "10up/wp_mock": "dev-master", + "phpunit/phpunit": "7.5.20", + "phpunit/php-code-coverage": "^6.1.4", "phpcompatibility/php-compatibility": "^9.3.5", - "wp-coding-standards/wpcs": "^2.3.0" + "wp-coding-standards/wpcs": "^2.3.0", + "monolog/monolog": "^1.25" + }, + "autoload": { + "classmap": [ + "vendor_namespaced/" + ] + }, + "autoload-dev": { + "classmap": [ + "lib/", + "admin/", + "tests/", + "vendor_namespaced/" + ] } -} +} \ No newline at end of file diff --git a/composer.lock b/composer.lock index f69cbad..635c6b9 100644 --- a/composer.lock +++ b/composer.lock @@ -4,20 +4,20 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "f207e60a2b2cf3ce7b6965a2f66c50fa", + "content-hash": "4d221414dbd6eb314ee1c83b37e87f27", "packages": [ { "name": "composer/installers", - "version": "v1.9.0", + "version": "v1.10.0", "source": { "type": "git", "url": "https://github.com/composer/installers.git", - "reference": "b93bcf0fa1fccb0b7d176b0967d969691cd74cca" + "reference": "1a0357fccad9d1cc1ea0c9a05b8847fbccccb78d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/installers/zipball/b93bcf0fa1fccb0b7d176b0967d969691cd74cca", - "reference": "b93bcf0fa1fccb0b7d176b0967d969691cd74cca", + "url": "https://api.github.com/repos/composer/installers/zipball/1a0357fccad9d1cc1ea0c9a05b8847fbccccb78d", + "reference": "1a0357fccad9d1cc1ea0c9a05b8847fbccccb78d", "shasum": "" }, "require": { @@ -28,17 +28,18 @@ "shama/baton": "*" }, "require-dev": { - "composer/composer": "1.6.* || 2.0.*@dev", - "composer/semver": "1.0.* || 2.0.*@dev", - "phpunit/phpunit": "^4.8.36", - "sebastian/comparator": "^1.2.4", + "composer/composer": "1.6.* || ^2.0", + "composer/semver": "^1 || ^3", + "phpstan/phpstan": "^0.12.55", + "phpstan/phpstan-phpunit": "^0.12.16", + "symfony/phpunit-bridge": "^4.2 || ^5", "symfony/process": "^2.3" }, "type": "composer-plugin", "extra": { "class": "Composer\\Installers\\Plugin", "branch-alias": { - "dev-master": "1.0-dev" + "dev-main": "1.x-dev" } }, "autoload": { @@ -76,6 +77,7 @@ "Porto", "RadPHP", "SMF", + "Starbug", "Thelia", "Whmcs", "WolfCMS", @@ -116,6 +118,7 @@ "phpbb", "piwik", "ppi", + "processwire", "puppet", "pxcms", "reindex", @@ -131,17 +134,25 @@ "zend", "zikula" ], + "support": { + "issues": "https://github.com/composer/installers/issues", + "source": "https://github.com/composer/installers/tree/v1.10.0" + }, "funding": [ { "url": "https://packagist.com", "type": "custom" }, + { + "url": "https://github.com/composer", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/composer/composer", "type": "tidelift" } ], - "time": "2020-04-07T06:57:05+00:00" + "time": "2021-01-14T11:07:16+00:00" } ], "packages-dev": [ @@ -170,6 +181,7 @@ "php-coveralls/php-coveralls": "^2.1", "sebastian/comparator": ">=1.2.3" }, + "default-branch": true, "type": "library", "autoload": { "psr-4": { @@ -184,6 +196,10 @@ "GPL-2.0-or-later" ], "description": "A mocking library to take the pain out of unit testing for WordPress", + "support": { + "issues": "https://github.com/10up/wp_mock/issues", + "source": "https://github.com/10up/wp_mock/tree/master" + }, "time": "2020-07-15T03:36:07+00:00" }, { @@ -228,8 +244,82 @@ "runkit", "testing" ], + "support": { + "issues": "https://github.com/antecedent/patchwork/issues", + "source": "https://github.com/antecedent/patchwork/tree/2.1.12" + }, "time": "2019-12-22T17:52:09+00:00" }, + { + "name": "dealerdirect/phpcodesniffer-composer-installer", + "version": "v0.7.1", + "source": { + "type": "git", + "url": "https://github.com/Dealerdirect/phpcodesniffer-composer-installer.git", + "reference": "fe390591e0241955f22eb9ba327d137e501c771c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Dealerdirect/phpcodesniffer-composer-installer/zipball/fe390591e0241955f22eb9ba327d137e501c771c", + "reference": "fe390591e0241955f22eb9ba327d137e501c771c", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0 || ^2.0", + "php": ">=5.3", + "squizlabs/php_codesniffer": "^2.0 || ^3.0 || ^4.0" + }, + "require-dev": { + "composer/composer": "*", + "phpcompatibility/php-compatibility": "^9.0", + "sensiolabs/security-checker": "^4.1.0" + }, + "type": "composer-plugin", + "extra": { + "class": "Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin" + }, + "autoload": { + "psr-4": { + "Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Franck Nijhof", + "email": "franck.nijhof@dealerdirect.com", + "homepage": "http://www.frenck.nl", + "role": "Developer / IT Manager" + } + ], + "description": "PHP_CodeSniffer Standards Composer Installer Plugin", + "homepage": "http://www.dealerdirect.com", + "keywords": [ + "PHPCodeSniffer", + "PHP_CodeSniffer", + "code quality", + "codesniffer", + "composer", + "installer", + "phpcs", + "plugin", + "qa", + "quality", + "standard", + "standards", + "style guide", + "stylecheck", + "tests" + ], + "support": { + "issues": "https://github.com/dealerdirect/phpcodesniffer-composer-installer/issues", + "source": "https://github.com/dealerdirect/phpcodesniffer-composer-installer" + }, + "time": "2020-12-07T18:04:37+00:00" + }, { "name": "doctrine/instantiator", "version": "1.4.0", @@ -279,6 +369,10 @@ "constructor", "instantiate" ], + "support": { + "issues": "https://github.com/doctrine/instantiator/issues", + "source": "https://github.com/doctrine/instantiator/tree/1.4.0" + }, "funding": [ { "url": "https://www.doctrine-project.org/sponsorship.html", @@ -340,37 +434,38 @@ "keywords": [ "test" ], + "support": { + "issues": "https://github.com/hamcrest/hamcrest-php/issues", + "source": "https://github.com/hamcrest/hamcrest-php/tree/v2.0.1" + }, "time": "2020-07-09T08:09:16+00:00" }, { "name": "mockery/mockery", - "version": "1.4.2", + "version": "1.3.4", "source": { "type": "git", "url": "https://github.com/mockery/mockery.git", - "reference": "20cab678faed06fac225193be281ea0fddb43b93" + "reference": "31467aeb3ca3188158613322d66df81cedd86626" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/mockery/mockery/zipball/20cab678faed06fac225193be281ea0fddb43b93", - "reference": "20cab678faed06fac225193be281ea0fddb43b93", + "url": "https://api.github.com/repos/mockery/mockery/zipball/31467aeb3ca3188158613322d66df81cedd86626", + "reference": "31467aeb3ca3188158613322d66df81cedd86626", "shasum": "" }, "require": { "hamcrest/hamcrest-php": "^2.0.1", "lib-pcre": ">=7.0", - "php": "^7.3 || ^8.0" - }, - "conflict": { - "phpunit/phpunit": "<8.0" + "php": ">=5.6.0" }, "require-dev": { - "phpunit/phpunit": "^8.5 || ^9.3" + "phpunit/phpunit": "^5.7.10|^6.5|^7.5|^8.5|^9.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.4.x-dev" + "dev-master": "1.3.x-dev" } }, "autoload": { @@ -408,7 +503,97 @@ "test double", "testing" ], - "time": "2020-08-11T18:10:13+00:00" + "support": { + "issues": "https://github.com/mockery/mockery/issues", + "source": "https://github.com/mockery/mockery/tree/1.3.4" + }, + "time": "2021-02-24T09:51:00+00:00" + }, + { + "name": "monolog/monolog", + "version": "1.26.0", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/monolog.git", + "reference": "2209ddd84e7ef1256b7af205d0717fb62cfc9c33" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/2209ddd84e7ef1256b7af205d0717fb62cfc9c33", + "reference": "2209ddd84e7ef1256b7af205d0717fb62cfc9c33", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "psr/log": "~1.0" + }, + "provide": { + "psr/log-implementation": "1.0.0" + }, + "require-dev": { + "aws/aws-sdk-php": "^2.4.9 || ^3.0", + "doctrine/couchdb": "~1.0@dev", + "graylog2/gelf-php": "~1.0", + "php-amqplib/php-amqplib": "~2.4", + "php-console/php-console": "^3.1.3", + "phpstan/phpstan": "^0.12.59", + "phpunit/phpunit": "~4.5", + "ruflin/elastica": ">=0.90 <3.0", + "sentry/sentry": "^0.13", + "swiftmailer/swiftmailer": "^5.3|^6.0" + }, + "suggest": { + "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", + "doctrine/couchdb": "Allow sending log messages to a CouchDB server", + "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", + "ext-mongo": "Allow sending log messages to a MongoDB server", + "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", + "mongodb/mongodb": "Allow sending log messages to a MongoDB server via PHP Driver", + "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", + "php-console/php-console": "Allow sending log messages to Google Chrome", + "rollbar/rollbar": "Allow sending log messages to Rollbar", + "ruflin/elastica": "Allow sending log messages to an Elastic Search server", + "sentry/sentry": "Allow sending log messages to a Sentry server" + }, + "type": "library", + "autoload": { + "psr-4": { + "Monolog\\": "src/Monolog" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "Sends your logs to files, sockets, inboxes, databases and various web services", + "homepage": "http://github.com/Seldaek/monolog", + "keywords": [ + "log", + "logging", + "psr-3" + ], + "support": { + "issues": "https://github.com/Seldaek/monolog/issues", + "source": "https://github.com/Seldaek/monolog/tree/1.26.0" + }, + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", + "type": "tidelift" + } + ], + "time": "2020-12-14T12:56:38+00:00" }, { "name": "myclabs/deep-copy", @@ -456,6 +641,10 @@ "object", "object graph" ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.10.2" + }, "funding": [ { "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", @@ -464,83 +653,30 @@ ], "time": "2020-11-13T09:40:50+00:00" }, - { - "name": "nikic/php-parser", - "version": "v4.10.3", - "source": { - "type": "git", - "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "dbe56d23de8fcb157bbc0cfb3ad7c7de0cfb0984" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dbe56d23de8fcb157bbc0cfb3ad7c7de0cfb0984", - "reference": "dbe56d23de8fcb157bbc0cfb3ad7c7de0cfb0984", - "shasum": "" - }, - "require": { - "ext-tokenizer": "*", - "php": ">=7.0" - }, - "require-dev": { - "ircmaxell/php-yacc": "^0.0.7", - "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0" - }, - "bin": [ - "bin/php-parse" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.9-dev" - } - }, - "autoload": { - "psr-4": { - "PhpParser\\": "lib/PhpParser" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Nikita Popov" - } - ], - "description": "A PHP parser written in PHP", - "keywords": [ - "parser", - "php" - ], - "time": "2020-12-03T17:45:45+00:00" - }, { "name": "phar-io/manifest", - "version": "2.0.1", + "version": "1.0.3", "source": { "type": "git", "url": "https://github.com/phar-io/manifest.git", - "reference": "85265efd3af7ba3ca4b2a2c34dbfc5788dd29133" + "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phar-io/manifest/zipball/85265efd3af7ba3ca4b2a2c34dbfc5788dd29133", - "reference": "85265efd3af7ba3ca4b2a2c34dbfc5788dd29133", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/7761fcacf03b4d4f16e7ccb606d4879ca431fcf4", + "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4", "shasum": "" }, "require": { "ext-dom": "*", "ext-phar": "*", - "ext-xmlwriter": "*", - "phar-io/version": "^3.0.1", - "php": "^7.2 || ^8.0" + "phar-io/version": "^2.0", + "php": "^5.6 || ^7.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0.x-dev" + "dev-master": "1.0.x-dev" } }, "autoload": { @@ -570,24 +706,28 @@ } ], "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", - "time": "2020-06-27T14:33:11+00:00" + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/master" + }, + "time": "2018-07-08T19:23:20+00:00" }, { "name": "phar-io/version", - "version": "3.0.3", + "version": "2.0.1", "source": { "type": "git", "url": "https://github.com/phar-io/version.git", - "reference": "726c026815142e4f8677b7cb7f2249c9ffb7ecae" + "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phar-io/version/zipball/726c026815142e4f8677b7cb7f2249c9ffb7ecae", - "reference": "726c026815142e4f8677b7cb7f2249c9ffb7ecae", + "url": "https://api.github.com/repos/phar-io/version/zipball/45a2ec53a73c70ce41d55cedef9063630abaf1b6", + "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6", "shasum": "" }, "require": { - "php": "^7.2 || ^8.0" + "php": "^5.6 || ^7.0" }, "type": "library", "autoload": { @@ -617,7 +757,11 @@ } ], "description": "Library for handling version information and constraints", - "time": "2020-11-30T09:21:21+00:00" + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/master" + }, + "time": "2018-07-08T19:19:57+00:00" }, { "name": "phpcompatibility/php-compatibility", @@ -675,6 +819,10 @@ "phpcs", "standards" ], + "support": { + "issues": "https://github.com/PHPCompatibility/PHPCompatibility/issues", + "source": "https://github.com/PHPCompatibility/PHPCompatibility" + }, "time": "2019-12-27T09:44:58+00:00" }, { @@ -724,6 +872,10 @@ "reflection", "static analysis" ], + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues", + "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x" + }, "time": "2020-06-27T09:03:43+00:00" }, { @@ -776,6 +928,10 @@ } ], "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/master" + }, "time": "2020-09-03T19:13:55+00:00" }, { @@ -821,20 +977,24 @@ } ], "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", + "support": { + "issues": "https://github.com/phpDocumentor/TypeResolver/issues", + "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.4.0" + }, "time": "2020-09-17T18:55:26+00:00" }, { "name": "phpspec/prophecy", - "version": "1.12.1", + "version": "1.13.0", "source": { "type": "git", "url": "https://github.com/phpspec/prophecy.git", - "reference": "8ce87516be71aae9b956f81906aaf0338e0d8a2d" + "reference": "be1996ed8adc35c3fd795488a653f4b518be70ea" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/8ce87516be71aae9b956f81906aaf0338e0d8a2d", - "reference": "8ce87516be71aae9b956f81906aaf0338e0d8a2d", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/be1996ed8adc35c3fd795488a653f4b518be70ea", + "reference": "be1996ed8adc35c3fd795488a653f4b518be70ea", "shasum": "" }, "require": { @@ -846,7 +1006,7 @@ }, "require-dev": { "phpspec/phpspec": "^6.0", - "phpunit/phpunit": "^8.0 || ^9.0 <9.3" + "phpunit/phpunit": "^8.0 || ^9.0" }, "type": "library", "extra": { @@ -884,48 +1044,48 @@ "spy", "stub" ], - "time": "2020-09-29T09:10:42+00:00" + "support": { + "issues": "https://github.com/phpspec/prophecy/issues", + "source": "https://github.com/phpspec/prophecy/tree/1.13.0" + }, + "time": "2021-03-17T13:42:18+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "9.2.5", + "version": "6.1.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "f3e026641cc91909d421802dd3ac7827ebfd97e1" + "reference": "807e6013b00af69b6c5d9ceb4282d0393dbb9d8d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/f3e026641cc91909d421802dd3ac7827ebfd97e1", - "reference": "f3e026641cc91909d421802dd3ac7827ebfd97e1", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/807e6013b00af69b6c5d9ceb4282d0393dbb9d8d", + "reference": "807e6013b00af69b6c5d9ceb4282d0393dbb9d8d", "shasum": "" }, "require": { "ext-dom": "*", - "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^4.10.2", - "php": ">=7.3", - "phpunit/php-file-iterator": "^3.0.3", - "phpunit/php-text-template": "^2.0.2", - "sebastian/code-unit-reverse-lookup": "^2.0.2", - "sebastian/complexity": "^2.0", - "sebastian/environment": "^5.1.2", - "sebastian/lines-of-code": "^1.0.3", - "sebastian/version": "^3.0.1", - "theseer/tokenizer": "^1.2.0" + "php": "^7.1", + "phpunit/php-file-iterator": "^2.0", + "phpunit/php-text-template": "^1.2.1", + "phpunit/php-token-stream": "^3.0", + "sebastian/code-unit-reverse-lookup": "^1.0.1", + "sebastian/environment": "^3.1 || ^4.0", + "sebastian/version": "^2.0.1", + "theseer/tokenizer": "^1.1" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^7.0" }, "suggest": { - "ext-pcov": "*", - "ext-xdebug": "*" + "ext-xdebug": "^2.6.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "9.2-dev" + "dev-master": "6.1-dev" } }, "autoload": { @@ -951,38 +1111,36 @@ "testing", "xunit" ], - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-11-28T06:44:49+00:00" + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/master" + }, + "time": "2018-10-31T16:06:48+00:00" }, { "name": "phpunit/php-file-iterator", - "version": "3.0.5", + "version": "2.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "aa4be8575f26070b100fccb67faabb28f21f66f8" + "reference": "4b49fb70f067272b659ef0174ff9ca40fdaa6357" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/aa4be8575f26070b100fccb67faabb28f21f66f8", - "reference": "aa4be8575f26070b100fccb67faabb28f21f66f8", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/4b49fb70f067272b659ef0174ff9ca40fdaa6357", + "reference": "4b49fb70f067272b659ef0174ff9ca40fdaa6357", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=7.1" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^8.5" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-master": "2.0.x-dev" } }, "autoload": { @@ -1007,99 +1165,36 @@ "filesystem", "iterator" ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/2.0.3" + }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" } ], - "time": "2020-09-28T05:57:25+00:00" - }, - { - "name": "phpunit/php-invoker", - "version": "3.1.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-invoker.git", - "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67", - "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "require-dev": { - "ext-pcntl": "*", - "phpunit/phpunit": "^9.3" - }, - "suggest": { - "ext-pcntl": "*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.1-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Invoke callables with a timeout", - "homepage": "https://github.com/sebastianbergmann/php-invoker/", - "keywords": [ - "process" - ], - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-09-28T05:58:55+00:00" + "time": "2020-11-30T08:25:21+00:00" }, { "name": "phpunit/php-text-template", - "version": "2.0.4", + "version": "1.2.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-text-template.git", - "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28" + "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", - "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686", + "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686", "shasum": "" }, "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" + "php": ">=5.3.3" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0-dev" - } - }, "autoload": { "classmap": [ "src/" @@ -1121,38 +1216,36 @@ "keywords": [ "template" ], - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-10-26T05:33:50+00:00" + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/1.2.1" + }, + "time": "2015-06-21T13:50:34+00:00" }, { "name": "phpunit/php-timer", - "version": "5.0.3", + "version": "2.1.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2" + "reference": "2454ae1765516d20c4ffe103d85a58a9a3bd5662" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", - "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/2454ae1765516d20c4ffe103d85a58a9a3bd5662", + "reference": "2454ae1765516d20c4ffe103d85a58a9a3bd5662", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=7.1" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^8.5" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-master": "2.1-dev" } }, "autoload": { @@ -1176,65 +1269,127 @@ "keywords": [ "timer" ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "source": "https://github.com/sebastianbergmann/php-timer/tree/2.1.3" + }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" } ], - "time": "2020-10-26T13:16:10+00:00" + "time": "2020-11-30T08:20:02+00:00" }, { - "name": "phpunit/phpunit", - "version": "9.5.0", + "name": "phpunit/php-token-stream", + "version": "3.1.2", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "8e16c225d57c3d6808014df6b1dd7598d0a5bbbe" + "url": "https://github.com/sebastianbergmann/php-token-stream.git", + "reference": "472b687829041c24b25f475e14c2f38a09edf1c2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/8e16c225d57c3d6808014df6b1dd7598d0a5bbbe", - "reference": "8e16c225d57c3d6808014df6b1dd7598d0a5bbbe", + "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/472b687829041c24b25f475e14c2f38a09edf1c2", + "reference": "472b687829041c24b25f475e14c2f38a09edf1c2", "shasum": "" }, "require": { - "doctrine/instantiator": "^1.3.1", + "ext-tokenizer": "*", + "php": ">=7.1" + }, + "require-dev": { + "phpunit/phpunit": "^7.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Wrapper around PHP's tokenizer extension.", + "homepage": "https://github.com/sebastianbergmann/php-token-stream/", + "keywords": [ + "tokenizer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-token-stream/issues", + "source": "https://github.com/sebastianbergmann/php-token-stream/tree/3.1.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "abandoned": true, + "time": "2020-11-30T08:38:46+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "7.5.20", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "9467db479d1b0487c99733bb1e7944d32deded2c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/9467db479d1b0487c99733bb1e7944d32deded2c", + "reference": "9467db479d1b0487c99733bb1e7944d32deded2c", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.1", "ext-dom": "*", "ext-json": "*", "ext-libxml": "*", "ext-mbstring": "*", "ext-xml": "*", - "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.10.1", - "phar-io/manifest": "^2.0.1", - "phar-io/version": "^3.0.2", - "php": ">=7.3", - "phpspec/prophecy": "^1.12.1", - "phpunit/php-code-coverage": "^9.2.3", - "phpunit/php-file-iterator": "^3.0.5", - "phpunit/php-invoker": "^3.1.1", - "phpunit/php-text-template": "^2.0.3", - "phpunit/php-timer": "^5.0.2", - "sebastian/cli-parser": "^1.0.1", - "sebastian/code-unit": "^1.0.6", - "sebastian/comparator": "^4.0.5", - "sebastian/diff": "^4.0.3", - "sebastian/environment": "^5.1.3", - "sebastian/exporter": "^4.0.3", - "sebastian/global-state": "^5.0.1", - "sebastian/object-enumerator": "^4.0.3", - "sebastian/resource-operations": "^3.0.3", - "sebastian/type": "^2.3", - "sebastian/version": "^3.0.2" + "myclabs/deep-copy": "^1.7", + "phar-io/manifest": "^1.0.2", + "phar-io/version": "^2.0", + "php": "^7.1", + "phpspec/prophecy": "^1.7", + "phpunit/php-code-coverage": "^6.0.7", + "phpunit/php-file-iterator": "^2.0.1", + "phpunit/php-text-template": "^1.2.1", + "phpunit/php-timer": "^2.1", + "sebastian/comparator": "^3.0", + "sebastian/diff": "^3.0", + "sebastian/environment": "^4.0", + "sebastian/exporter": "^3.1", + "sebastian/global-state": "^2.0", + "sebastian/object-enumerator": "^3.0.3", + "sebastian/resource-operations": "^2.0", + "sebastian/version": "^2.0.1" + }, + "conflict": { + "phpunit/phpunit-mock-objects": "*" }, "require-dev": { - "ext-pdo": "*", - "phpspec/prophecy-phpunit": "^2.0.1" + "ext-pdo": "*" }, "suggest": { "ext-soap": "*", - "ext-xdebug": "*" + "ext-xdebug": "*", + "phpunit/php-invoker": "^2.0" }, "bin": [ "phpunit" @@ -1242,15 +1397,12 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "9.5-dev" + "dev-master": "7.5-dev" } }, "autoload": { "classmap": [ "src/" - ], - "files": [ - "src/Framework/Assert/Functions.php" ] }, "notification-url": "https://packagist.org/downloads/", @@ -1271,146 +1423,86 @@ "testing", "xunit" ], - "funding": [ - { - "url": "https://phpunit.de/donate.html", - "type": "custom" - }, - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-12-04T05:05:53+00:00" + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "source": "https://github.com/sebastianbergmann/phpunit/tree/7.5.20" + }, + "time": "2020-01-08T08:45:45+00:00" }, { - "name": "sebastian/cli-parser", - "version": "1.0.1", + "name": "psr/log", + "version": "1.1.3", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/cli-parser.git", - "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2" + "url": "https://github.com/php-fig/log.git", + "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/442e7c7e687e42adc03470c7b668bc4b2402c0b2", - "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2", + "url": "https://api.github.com/repos/php-fig/log/zipball/0f73288fd15629204f9d42b7055f72dacbe811fc", + "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc", "shasum": "" }, "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" + "php": ">=5.3.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0-dev" + "dev-master": "1.1.x-dev" } }, "autoload": { - "classmap": [ - "src/" - ] + "psr-4": { + "Psr\\Log\\": "Psr/Log/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" } ], - "description": "Library for parsing CLI options", - "homepage": "https://github.com/sebastianbergmann/cli-parser", - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" ], - "time": "2020-09-28T06:08:49+00:00" - }, - { - "name": "sebastian/code-unit", - "version": "1.0.8", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/code-unit.git", - "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120" + "support": { + "source": "https://github.com/php-fig/log/tree/1.1.3" }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120", - "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Collection of value objects that represent the PHP code units", - "homepage": "https://github.com/sebastianbergmann/code-unit", - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-10-26T13:08:54+00:00" + "time": "2020-03-23T09:12:05+00:00" }, { "name": "sebastian/code-unit-reverse-lookup", - "version": "2.0.3", + "version": "1.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", - "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5" + "reference": "1de8cd5c010cb153fcd68b8d0f64606f523f7619" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", - "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/1de8cd5c010cb153fcd68b8d0f64606f523f7619", + "reference": "1de8cd5c010cb153fcd68b8d0f64606f523f7619", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=5.6" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^8.5" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-master": "1.0.x-dev" } }, "autoload": { @@ -1430,40 +1522,44 @@ ], "description": "Looks up which function or method a line of code belongs to", "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/1.0.2" + }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" } ], - "time": "2020-09-28T05:30:19+00:00" + "time": "2020-11-30T08:15:22+00:00" }, { "name": "sebastian/comparator", - "version": "4.0.6", + "version": "3.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "55f4261989e546dc112258c7a75935a81a7ce382" + "reference": "1071dfcef776a57013124ff35e1fc41ccd294758" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/55f4261989e546dc112258c7a75935a81a7ce382", - "reference": "55f4261989e546dc112258c7a75935a81a7ce382", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/1071dfcef776a57013124ff35e1fc41ccd294758", + "reference": "1071dfcef776a57013124ff35e1fc41ccd294758", "shasum": "" }, "require": { - "php": ">=7.3", - "sebastian/diff": "^4.0", - "sebastian/exporter": "^4.0" + "php": ">=7.1", + "sebastian/diff": "^3.0", + "sebastian/exporter": "^3.1" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^8.5" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-master": "3.0-dev" } }, "autoload": { @@ -1500,92 +1596,43 @@ "compare", "equality" ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "source": "https://github.com/sebastianbergmann/comparator/tree/3.0.3" + }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" } ], - "time": "2020-10-26T15:49:45+00:00" - }, - { - "name": "sebastian/complexity", - "version": "2.0.2", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/complexity.git", - "reference": "739b35e53379900cc9ac327b2147867b8b6efd88" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/739b35e53379900cc9ac327b2147867b8b6efd88", - "reference": "739b35e53379900cc9ac327b2147867b8b6efd88", - "shasum": "" - }, - "require": { - "nikic/php-parser": "^4.7", - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Library for calculating the complexity of PHP code units", - "homepage": "https://github.com/sebastianbergmann/complexity", - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-10-26T15:52:27+00:00" + "time": "2020-11-30T08:04:30+00:00" }, { "name": "sebastian/diff", - "version": "4.0.4", + "version": "3.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d" + "reference": "14f72dd46eaf2f2293cbe79c93cc0bc43161a211" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/3461e3fccc7cfdfc2720be910d3bd73c69be590d", - "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/14f72dd46eaf2f2293cbe79c93cc0bc43161a211", + "reference": "14f72dd46eaf2f2293cbe79c93cc0bc43161a211", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=7.1" }, "require-dev": { - "phpunit/phpunit": "^9.3", - "symfony/process": "^4.2 || ^5" + "phpunit/phpunit": "^7.5 || ^8.0", + "symfony/process": "^2 || ^3.3 || ^4" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-master": "3.0-dev" } }, "autoload": { @@ -1615,33 +1662,37 @@ "unidiff", "unified diff" ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "source": "https://github.com/sebastianbergmann/diff/tree/3.0.3" + }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" } ], - "time": "2020-10-26T13:10:38+00:00" + "time": "2020-11-30T07:59:04+00:00" }, { "name": "sebastian/environment", - "version": "5.1.3", + "version": "4.2.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "388b6ced16caa751030f6a69e588299fa09200ac" + "reference": "d47bbbad83711771f167c72d4e3f25f7fcc1f8b0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/388b6ced16caa751030f6a69e588299fa09200ac", - "reference": "388b6ced16caa751030f6a69e588299fa09200ac", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/d47bbbad83711771f167c72d4e3f25f7fcc1f8b0", + "reference": "d47bbbad83711771f167c72d4e3f25f7fcc1f8b0", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=7.1" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^7.5" }, "suggest": { "ext-posix": "*" @@ -1649,7 +1700,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "5.1-dev" + "dev-master": "4.2-dev" } }, "autoload": { @@ -1674,40 +1725,44 @@ "environment", "hhvm" ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "source": "https://github.com/sebastianbergmann/environment/tree/4.2.4" + }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" } ], - "time": "2020-09-28T05:52:38+00:00" + "time": "2020-11-30T07:53:42+00:00" }, { "name": "sebastian/exporter", - "version": "4.0.3", + "version": "3.1.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "d89cc98761b8cb5a1a235a6b703ae50d34080e65" + "reference": "6b853149eab67d4da22291d36f5b0631c0fd856e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/d89cc98761b8cb5a1a235a6b703ae50d34080e65", - "reference": "d89cc98761b8cb5a1a235a6b703ae50d34080e65", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/6b853149eab67d4da22291d36f5b0631c0fd856e", + "reference": "6b853149eab67d4da22291d36f5b0631c0fd856e", "shasum": "" }, "require": { - "php": ">=7.3", - "sebastian/recursion-context": "^4.0" + "php": ">=7.0", + "sebastian/recursion-context": "^3.0" }, "require-dev": { "ext-mbstring": "*", - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^6.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-master": "3.1.x-dev" } }, "autoload": { @@ -1747,201 +1802,42 @@ "export", "exporter" ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "source": "https://github.com/sebastianbergmann/exporter/tree/3.1.3" + }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" } ], - "time": "2020-09-28T05:24:23+00:00" + "time": "2020-11-30T07:47:53+00:00" }, { "name": "sebastian/global-state", - "version": "5.0.2", + "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "a90ccbddffa067b51f574dea6eb25d5680839455" + "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/a90ccbddffa067b51f574dea6eb25d5680839455", - "reference": "a90ccbddffa067b51f574dea6eb25d5680839455", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4", + "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4", "shasum": "" }, "require": { - "php": ">=7.3", - "sebastian/object-reflector": "^2.0", - "sebastian/recursion-context": "^4.0" + "php": "^7.0" }, "require-dev": { - "ext-dom": "*", - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^6.0" }, "suggest": { "ext-uopz": "*" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Snapshotting of global state", - "homepage": "http://www.github.com/sebastianbergmann/global-state", - "keywords": [ - "global state" - ], - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-10-26T15:55:19+00:00" - }, - { - "name": "sebastian/lines-of-code", - "version": "1.0.3", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/lines-of-code.git", - "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/c1c2e997aa3146983ed888ad08b15470a2e22ecc", - "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc", - "shasum": "" - }, - "require": { - "nikic/php-parser": "^4.6", - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Library for counting the lines of code in PHP source code", - "homepage": "https://github.com/sebastianbergmann/lines-of-code", - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-11-28T06:42:11+00:00" - }, - { - "name": "sebastian/object-enumerator", - "version": "4.0.4", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/object-enumerator.git", - "reference": "5c9eeac41b290a3712d88851518825ad78f45c71" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71", - "reference": "5c9eeac41b290a3712d88851518825ad78f45c71", - "shasum": "" - }, - "require": { - "php": ">=7.3", - "sebastian/object-reflector": "^2.0", - "sebastian/recursion-context": "^4.0" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Traverses array structures and object graphs to enumerate all referenced objects", - "homepage": "https://github.com/sebastianbergmann/object-enumerator/", - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-10-26T13:12:34+00:00" - }, - { - "name": "sebastian/object-reflector", - "version": "2.0.4", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/object-reflector.git", - "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", - "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "type": "library", "extra": { "branch-alias": { "dev-master": "2.0-dev" @@ -1962,40 +1858,153 @@ "email": "sebastian@phpunit.de" } ], - "description": "Allows reflection of object attributes, including inherited and non-public ones", - "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "description": "Snapshotting of global state", + "homepage": "http://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "source": "https://github.com/sebastianbergmann/global-state/tree/2.0.0" + }, + "time": "2017-04-27T15:39:26+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "3.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "e67f6d32ebd0c749cf9d1dbd9f226c727043cdf2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/e67f6d32ebd0c749cf9d1dbd9f226c727043cdf2", + "reference": "e67f6d32ebd0c749cf9d1dbd9f226c727043cdf2", + "shasum": "" + }, + "require": { + "php": ">=7.0", + "sebastian/object-reflector": "^1.1.1", + "sebastian/recursion-context": "^3.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/3.0.4" + }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" } ], - "time": "2020-10-26T13:14:26+00:00" + "time": "2020-11-30T07:40:27+00:00" }, { - "name": "sebastian/recursion-context", - "version": "4.0.4", + "name": "sebastian/object-reflector", + "version": "1.1.2", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "cd9d8cf3c5804de4341c283ed787f099f5506172" + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "9b8772b9cbd456ab45d4a598d2dd1a1bced6363d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/cd9d8cf3c5804de4341c283ed787f099f5506172", - "reference": "cd9d8cf3c5804de4341c283ed787f099f5506172", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/9b8772b9cbd456ab45d4a598d2dd1a1bced6363d", + "reference": "9b8772b9cbd456ab45d4a598d2dd1a1bced6363d", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=7.0" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^6.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-master": "1.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/1.1.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-11-30T07:37:18+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "367dcba38d6e1977be014dc4b22f47a484dac7fb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/367dcba38d6e1977be014dc4b22f47a484dac7fb", + "reference": "367dcba38d6e1977be014dc4b22f47a484dac7fb", + "shasum": "" + }, + "require": { + "php": ">=7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" } }, "autoload": { @@ -2023,38 +2032,39 @@ ], "description": "Provides functionality to recursively process PHP variables", "homepage": "http://www.github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/3.0.1" + }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" } ], - "time": "2020-10-26T13:17:30+00:00" + "time": "2020-11-30T07:34:24+00:00" }, { "name": "sebastian/resource-operations", - "version": "3.0.3", + "version": "2.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/resource-operations.git", - "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8" + "reference": "31d35ca87926450c44eae7e2611d45a7a65ea8b3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", - "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/31d35ca87926450c44eae7e2611d45a7a65ea8b3", + "reference": "31d35ca87926450c44eae7e2611d45a7a65ea8b3", "shasum": "" }, "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.0" + "php": ">=7.1" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-master": "2.0-dev" } }, "autoload": { @@ -2074,87 +2084,39 @@ ], "description": "Provides a list of PHP built-in functions that operate on resources", "homepage": "https://www.github.com/sebastianbergmann/resource-operations", + "support": { + "issues": "https://github.com/sebastianbergmann/resource-operations/issues", + "source": "https://github.com/sebastianbergmann/resource-operations/tree/2.0.2" + }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" } ], - "time": "2020-09-28T06:45:17+00:00" - }, - { - "name": "sebastian/type", - "version": "2.3.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/type.git", - "reference": "81cd61ab7bbf2de744aba0ea61fae32f721df3d2" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/81cd61ab7bbf2de744aba0ea61fae32f721df3d2", - "reference": "81cd61ab7bbf2de744aba0ea61fae32f721df3d2", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.3-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Collection of value objects that represent the types of the PHP type system", - "homepage": "https://github.com/sebastianbergmann/type", - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-10-26T13:18:59+00:00" + "time": "2020-11-30T07:30:19+00:00" }, { "name": "sebastian/version", - "version": "3.0.2", + "version": "2.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/version.git", - "reference": "c6c1022351a901512170118436c764e473f6de8c" + "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c", - "reference": "c6c1022351a901512170118436c764e473f6de8c", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/99732be0ddb3361e16ad77b68ba41efc8e979019", + "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=5.6" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-master": "2.0.x-dev" } }, "autoload": { @@ -2175,13 +2137,11 @@ ], "description": "Library that helps with managing the version number of Git-hosted PHP projects", "homepage": "https://github.com/sebastianbergmann/version", - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-09-28T06:39:44+00:00" + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "source": "https://github.com/sebastianbergmann/version/tree/master" + }, + "time": "2016-10-03T07:35:21+00:00" }, { "name": "squizlabs/php_codesniffer", @@ -2232,20 +2192,25 @@ "phpcs", "standards" ], + "support": { + "issues": "https://github.com/squizlabs/PHP_CodeSniffer/issues", + "source": "https://github.com/squizlabs/PHP_CodeSniffer", + "wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki" + }, "time": "2020-10-23T02:01:07+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.20.0", + "version": "v1.22.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "f4ba089a5b6366e453971d3aad5fe8e897b37f41" + "reference": "c6c942b1ac76c82448322025e084cadc56048b4e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/f4ba089a5b6366e453971d3aad5fe8e897b37f41", - "reference": "f4ba089a5b6366e453971d3aad5fe8e897b37f41", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/c6c942b1ac76c82448322025e084cadc56048b4e", + "reference": "c6c942b1ac76c82448322025e084cadc56048b4e", "shasum": "" }, "require": { @@ -2257,7 +2222,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.20-dev" + "dev-main": "1.22-dev" }, "thanks": { "name": "symfony/polyfill", @@ -2294,6 +2259,9 @@ "polyfill", "portable" ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.22.1" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -2308,7 +2276,7 @@ "type": "tidelift" } ], - "time": "2020-10-23T14:02:19+00:00" + "time": "2021-01-07T16:49:33+00:00" }, { "name": "theseer/tokenizer", @@ -2348,6 +2316,10 @@ } ], "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/master" + }, "funding": [ { "url": "https://github.com/theseer", @@ -2358,30 +2330,35 @@ }, { "name": "webmozart/assert", - "version": "1.9.1", + "version": "1.10.0", "source": { "type": "git", - "url": "https://github.com/webmozart/assert.git", - "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389" + "url": "https://github.com/webmozarts/assert.git", + "reference": "6964c76c7804814a842473e0c8fd15bab0f18e25" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webmozart/assert/zipball/bafc69caeb4d49c39fd0779086c03a3738cbb389", - "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/6964c76c7804814a842473e0c8fd15bab0f18e25", + "reference": "6964c76c7804814a842473e0c8fd15bab0f18e25", "shasum": "" }, "require": { - "php": "^5.3.3 || ^7.0 || ^8.0", + "php": "^7.2 || ^8.0", "symfony/polyfill-ctype": "^1.8" }, "conflict": { "phpstan/phpstan": "<0.12.20", - "vimeo/psalm": "<3.9.1" + "vimeo/psalm": "<4.6.1 || 4.6.2" }, "require-dev": { - "phpunit/phpunit": "^4.8.36 || ^7.5.13" + "phpunit/phpunit": "^8.5.13" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.10-dev" + } + }, "autoload": { "psr-4": { "Webmozart\\Assert\\": "src/" @@ -2403,7 +2380,11 @@ "check", "validate" ], - "time": "2020-07-08T17:02:28+00:00" + "support": { + "issues": "https://github.com/webmozarts/assert/issues", + "source": "https://github.com/webmozarts/assert/tree/1.10.0" + }, + "time": "2021-03-09T10:59:23+00:00" }, { "name": "wp-coding-standards/wpcs", @@ -2449,6 +2430,11 @@ "standards", "wordpress" ], + "support": { + "issues": "https://github.com/WordPress/WordPress-Coding-Standards/issues", + "source": "https://github.com/WordPress/WordPress-Coding-Standards", + "wiki": "https://github.com/WordPress/WordPress-Coding-Standards/wiki" + }, "time": "2020-05-13T23:57:56+00:00" } ], @@ -2463,5 +2449,5 @@ "php": ">=5.4" }, "platform-dev": [], - "plugin-api-version": "1.1.0" + "plugin-api-version": "2.0.0" } diff --git a/docs/COMPOSER.md b/docs/COMPOSER.md new file mode 100644 index 0000000..7c97a84 --- /dev/null +++ b/docs/COMPOSER.md @@ -0,0 +1,85 @@ +### WP Discourse Composer Development Usage + +WP Discourse uses [Composer](https://getcomposer.org) in the standard fashion, but there are a few things that are worth pointing out about using composer packaages in production. + +### Using Composer Packages in Production + +This approach is inspired by approaches used by [Yoast](https://developer.yoast.com/blog/safely-using-php-dependencies-in-the-wordpress-ecosystem/) and [Delicious Brains](https://deliciousbrains.com/php-scoper-namespace-composer-depencies/), both of which use namespacing with a custom build process. We use a simplified version of that approach, which we are scripting over time. In short, we: + +1. isolate and namespace (``WPDiscourse``) vendor packages used in production; and +2. define a build and autoload process to ensure ``1`` works with both wordpress.org and composer installation. + +The goal here is simplicity and selectivity. We do not want to auto-namespace the entire plugin, or every development dependency. + +#### Step 1. Add package as a development dependency + +Add whatever package you want to use in production as a development dependency, for example + +``` +"require-dev": { + ... + "monolog/monolog": "^1.25" +} +``` + +Then run ``composer install`` to install your package in ``vendor``. + +#### Step 2. Build a distribution version of the package + +##### 2.1 Setup + +First, install [``humbug/php-scoper``](https://github.com/humbug/php-scoper) globally on your machine. A local global install for development is cleaner than a project install via ``bamarni/composer-bin-plugin`` for our purposes. + +Then, create a finder in the 'finders' array in ``scoper.inc.php`` to include your package(s) in the scoping. See the [documentation on Symfony Finders](https://symfony.com/doc/current/components/finder.html) for details on usage. + +You may also need to perform modifications on the package files in order to achieve compatibility. For example, monolog requires various function signature parsing and type declaration removals. This can be performed in the 'patchers' callback. + +##### 2.2 Running + +Now run ``add-prefix`` as follows + +``` +php-scoper add-prefix --output-dir=./vendor_namespaced/ --force +``` + +This will populate ``vendor_namespaced`` with namespaced versions of the packages matching the listed paths, with any patchers applied. + +#### Step 3. Use the namespaced package in your code + +When using the package in the plugin code, use the version in ``vendor_namespaced``, which is namespaced with ``WPDiscourse``. For example, use + +``` +\WPDiscourse\Monolog\Logger +``` +not + +``` +\Monolog\Logger +``` + +The non-namespaced version will still be present in development (in your ``vendor`` folder), but shouldn't be used in the plugin code, and won't be bundled in the production build. + +#### Step 4. Build for production + +When building for production, use composer as you normally would when preparing a production build of a Wordpress plugin, i.e. by installing optimized non-development packages + +``` +composer install --prefer-dist --optimize-autoloader --no-dev +``` + +This will also add the namespaced packages in ``vendor_namespaced`` to the autoload due to the autoload classmap: + +``` +"autoload": { + "classmap": [ + "vendor_namespaced/" + ] +} +``` +You can see a full list of autoloaded classes in ``vendor/composer/autoload_classmap.php``. + +Once the production dependencies are installed, the plugin should then be bundled for submission. + + + + diff --git a/docs/FORMATTING.md b/docs/FORMATTING.md new file mode 100644 index 0000000..6732028 --- /dev/null +++ b/docs/FORMATTING.md @@ -0,0 +1,54 @@ +### WP Discourse Code Formatting + +WP Discourse uses +- [PHP_CodeSniffer](https://github.com/squizlabs/PHP_CodeSniffer) to handle code formatting (``phpcs``); +- the native PHP syntax checker; and +- jshint for javascript. + +These formatters will be applied on each pull request in Github Actions (via ``.github/workflows/ci.yml``), so make sure you run them locally before making your PR. + +#### PHPCS + +The ``phpcs`` configuration is handled in the ``.phpcs.xml`` file, a type of [Annotated Ruleset](https://github.com/squizlabs/PHP_CodeSniffer/wiki/Annotated-Ruleset). Install the development composer packages by running ``composer install`` prior to using ``phpcs``, and run it using ``vendor/bin/phpcs``, for example + +``` +vendor/bin/phpcs lib/discourse-publish.php +``` + +All errors must be addressed, and all warnings should be addressed as far as possible. You can attempt to fix any issues automatically using ``phpcbf``. If using ``phpcbf`` + +1. Make sure your working tree is clean as you may wish to revert the results (in some cases it can create more issues than it solves) + +2. Only use ``phpcbf`` on a file by file basis. + +#### Native Syntax Check + +The ``.github/workflows/ci.yml`` applies a syntax check for each supported version of PHP by searching for all ``.php`` files in the repository, running the relevant version of the PHP interpreter, and catching syntax errors via ``xargs``: + +``` +find -L . -name '*.php' -not -path "./vendor/*" -print0 | xargs -0 -n 1 -P 4 php -l +``` + +To perform this locally: + +1. First, check the PHP version "matrix" in ``.github/workflows/ci.yml`` to see which versions we are currently testing. + +2. Then you'll need a local build of each PHP version. You can install different PHP versions using ``phpbrew`` or ``phpenv`` (we may include a ``phpenv`` setup in the project in the future). + +3. Finally run the above command using each PHP version in the matrix. + +All errors must be fixed, even if they are not in a file required in production, and all warnings must be addressed as far as possible. + +#### JSHint + +First, install ``jshint`` globally on your machine + +``` +npm install -g jshint +``` + +Then run it in the ``wp-discourse`` directory + +``` +jshint . +``` \ No newline at end of file diff --git a/docs/TESTS.md b/docs/TESTS.md new file mode 100644 index 0000000..c1538a2 --- /dev/null +++ b/docs/TESTS.md @@ -0,0 +1,116 @@ +### WP Discourse Tests Guide + +For general guides on PHPUnit tests, and their application in Wordpress, see + +- The [WP Handbook on Plugin Unit Tests](https://make.wordpress.org/cli/handbook/misc/plugin-unit-tests/) +- The [PHPUnit Documentation](https://phpunit.readthedocs.io) + +Please make sure you check the version of the PHPUnit Documentation you're viewing against the version this plugin is using in ``./composer.json``. You can change to any version of the documentation by updating the docs url. + +### Setup + +#### Files and Database + +First, set up your tests database by running the following command in the root plugin directory. Make sure you substitute your local root mysql password. + +``` +cd wp-discourse +bash bin/install-wp-tests.sh wordpress_test root 'password' localhost latest +``` + +If this command returns an error, clean up anything it created before running it again, i.e. + +``` +## Remove the tmp files +rm -rf /path/to/tmp/wordpress +rm -rf /path/to/tmp/wordpress-tests-lib + +## Drop the test database +mysql +DROP DATABASE wordpress_test; +FLUSH PRIVILEGES; +exit +``` + +Make sure that command completes successfully, with no errors, before continuing. + +#### Environment and Dependencies + +If you haven't already, run ``composer install`` in the root plugin directory to pull in the main tests dependencies. + +##### Xdebug + +One additional dependency you need in your environment is the php extension Xdebug. The installation of Xdebug is environment specific, however we would recommend you use the [Xdebug Installation Wizard](https://xdebug.org/wizard) to ensure your installation is correct. + +For testing purposes Xdebug should be run in ``coverage`` mode, which means that you should have a lines in your ``php.ini`` that look like this + +``` +xdebug.mode = coverage +``` + +### Configuration + +The tests suite is configured by the ``phpunit.xml`` file. Read more about the elements in the file, and their usage in the [PHPUnit Documentation](https://phpunit.readthedocs.io). + +#### Coverage + +The tests coverage whitelist in the config file limits the coverage reports to the classes in the plugin that have tests. + +``` + + lib/logs + lib/discourse-publish.php + +``` + +This scope should be expanded over time as more classes have tests written for them. To see the overall progress of tests coverage, remove the coverage whitelist elements from the config file. + +### Usage + +Once you've completed the ``Setup`` section, run the tests suite using + +``` +vendor/bin/phpunit +``` + +To run a specific file add the path to the file as an argument + +``` +vendor/bin/phpunit tests/phpunit/test-discourse-publish.php +``` + +To run a specific test in a suite use the ``--filter`` option + +``` +vendor/bin/phpunit tests/phpunit/test-discourse-publish.php --filter=test_sync_to_discourse_when_creating_with_embed_error +``` + +To add a coverage report to the output add ``--coverage-text``, which will send the report to stdout. + +``` +vendor/bin/phpunit --coverage-text +``` + +Run ``phpunit --help`` to see other report formats that can be generated (e.g. html or crap4j). + +### Multisite + +Multisite tests are written and run separately. The multisite tests are in ``tests/phpunit/multisite`` and the mulitsite config is in ``tests/phpunit/multisite.xml``. + +#### Writing Multisite Tests + +Multisite tests extend the single-site tests of the same class. This allows for any tests and helper methods for that class to be used in the multisite test. For example see ``tests/phpunit/multisite/test-discourse-publish-multisite.php``. + +``` +class DiscoursePublishMultisiteTest extends DiscoursePublishTest +``` + +When the multisite test is run the single-site tests in the parent test class will also be run in the multisite environment. + +#### Running Multisite Tests + +To run a multisite test, you need to use the multisite config + +``` +vendor/bin/phpunit -c tests/phpunit/multisite.xml +``` diff --git a/lib/discourse-publish.php b/lib/discourse-publish.php index 13522fe..7a2757b 100644 --- a/lib/discourse-publish.php +++ b/lib/discourse-publish.php @@ -2,7 +2,8 @@ /** * Publishes a post to Discourse. * - * @package WPDicourse + * @package WPDiscourse + * @todo Periodically review phpcs exclusions. */ namespace WPDiscourse\DiscoursePublish; @@ -10,6 +11,7 @@ namespace WPDiscourse\DiscoursePublish; use WPDiscourse\Templates\HTMLTemplates as Templates; use WPDiscourse\Shared\PluginUtilities; use WPDiscourse\Shared\TemplateFunctions; +use WPDiscourse\Logs\Logger; /** * Class DiscoursePublish @@ -34,25 +36,62 @@ class DiscoursePublish { */ protected $email_notifier; + /** + * Instance of Logger + * + * @access protected + * @var \WPDiscourse\Logs\Logger + */ + protected $logger; + + /** + * Instance store for log args + * + * @access protected + * @var mixed|void + */ + protected $log_args; + /** * DiscoursePublish constructor. * * @param object $email_notifier An object for sending an email verification notice. + * @param bool $register_actions Flag determines whether to register publish actions. */ - public function __construct( $email_notifier ) { + public function __construct( $email_notifier, $register_actions = true ) { $this->email_notifier = $email_notifier; add_action( 'init', array( $this, 'setup_options' ) ); - // Priority is set to 13 so that 'publish_post_after_save' is called after the meta-box is saved. - add_action( 'save_post', array( $this, 'publish_post_after_save' ), 13, 2 ); - add_action( 'xmlrpc_publish_post', array( $this, 'xmlrpc_publish_post_to_discourse' ) ); + add_action( 'init', array( $this, 'setup_logger' ) ); + + // Registration is conditional to make testing easier. + if ( $register_actions ) { + // Priority is set to 13 so that 'publish_post_after_save' is called after the meta-box is saved. + add_action( 'save_post', array( $this, 'publish_post_after_save' ), 13, 2 ); + add_action( 'xmlrpc_publish_post', array( $this, 'xmlrpc_publish_post_to_discourse' ) ); + } } /** * Setup options. + * + * @param object $extra_options Extra options used for testing. */ - public function setup_options() { + public function setup_options( $extra_options = null ) { $this->options = $this->get_options(); + + if ( ! empty( $extra_options ) ) { + foreach ( $extra_options as $key => $value ) { + $this->options[ $key ] = $value; + } + } + } + + /** + * Setup Logger for the pubish context. + */ + public function setup_logger() { + $this->logger = Logger::create( 'publish' ); } /** @@ -68,10 +107,10 @@ class DiscoursePublish { $publish_status_not_set = 'publish' !== get_post_status( $post_id ); $publish_private = apply_filters( 'wpdc_publish_private_post', false, $post_id ); if ( wp_is_post_revision( $post_id ) - || ( $publish_status_not_set && ! $publish_private ) - || $plugin_unconfigured - || empty( $post->post_title ) - || ! $this->is_valid_sync_post_type( $post_id ) + || ( $publish_status_not_set && ! $publish_private ) + || $plugin_unconfigured + || empty( $post->post_title ) + || ! $this->is_valid_sync_post_type( $post_id ) ) { return null; @@ -158,13 +197,25 @@ class DiscoursePublish { global $wpdb; // this avoids a double sync, just 1 is allowed to go through at a time. - $got_lock = $wpdb->get_row( "SELECT GET_LOCK('discourse_sync_lock', 0) got_it" ); + $got_lock = $wpdb->get_row( "SELECT GET_LOCK('discourse_sync_lock', 0) got_it" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery if ( 1 === intval( $got_lock->got_it ) ) { $this->sync_to_discourse_work( $post_id, $title, $raw ); - $wpdb->get_results( "SELECT RELEASE_LOCK('discourse_sync_lock')" ); + $wpdb->get_results( "SELECT RELEASE_LOCK('discourse_sync_lock')" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery } } + /** + * Calls `sync_to_discourse_work` without a lock. Only used for testing. + * Should not be used elsewhere in plugin. + * + * @param int $post_id The post id. + * @param string $title The title. + * @param string $raw The raw content of the post. + */ + public function sync_to_discourse_without_lock( $post_id, $title, $raw ) { + return $this->sync_to_discourse_work( $post_id, $title, $raw ); + } + /** * Syncs a post to Discourse. * @@ -177,13 +228,20 @@ class DiscoursePublish { protected function sync_to_discourse_work( $post_id, $title, $raw ) { $options = $this->options; $discourse_id = $this->dc_get_post_meta( $post_id, 'discourse_post_id', true ); - $current_post = get_post( $post_id ); - $author_id = $current_post->post_author; + $post = get_post( $post_id ); + $author_id = $post->post_author; $use_full_post = ! empty( $options['full-post-content'] ); $use_multisite_configuration = is_multisite() && ! empty( $options['multisite-configuration-enabled'] ); $add_featured_link = ! empty( $options['add-featured-link'] ); $permalink = get_permalink( $post_id ); + $this->log_args = array( + 'wp_title' => $title, + 'wp_author_id' => $author_id, + 'wp_post_id' => $post_id, + 'discourse_post_id' => $discourse_id, + ); + if ( $use_full_post ) { $blocks = parse_blocks( $raw ); $parsed = ''; @@ -203,7 +261,7 @@ class DiscoursePublish { $excerpt = apply_filters( 'wp_discourse_excerpt', $parsed, $options['custom-excerpt-length'], $use_full_post ); } else { if ( has_excerpt( $post_id ) ) { - $wp_excerpt = apply_filters( 'get_the_excerpt', $current_post->post_excerpt ); + $wp_excerpt = apply_filters( 'get_the_excerpt', $post->post_excerpt ); $excerpt = apply_filters( 'wp_discourse_excerpt', $wp_excerpt, $options['custom-excerpt-length'], $use_full_post ); } @@ -228,7 +286,7 @@ class DiscoursePublish { if ( ! empty( $featured ) ) { $baked = str_replace( '{featuredimage}', '![image](' . $featured['0'] . ')', $baked ); } - $username = apply_filters( 'wpdc_discourse_username', get_the_author_meta( 'discourse_username', $current_post->post_author ), $author_id ); + $username = apply_filters( 'wpdc_discourse_username', get_the_author_meta( 'discourse_username', $post->post_author ), $author_id ); if ( ! $username || strlen( $username ) < 2 ) { $username = $options['publish-username']; } @@ -250,17 +308,19 @@ class DiscoursePublish { $tags_param = ''; } + $remote_post_type = ''; + // The post hasn't been published to Discourse yet. if ( ! $discourse_id > 0 ) { // Unlisted has been moved from post metadata to a site option. This is awkward for now. $unlisted_post = get_post_meta( $post_id, 'wpdc_unlisted_topic', true ); $unlisted_option = $this->options['publish-as-unlisted']; - $unlisted = apply_filters( 'wpdc_publish_unlisted', ! empty( $unlisted_post ) || ! empty( $unlisted_option ), $current_post, $post_id ); + $unlisted = apply_filters( 'wpdc_publish_unlisted', ! empty( $unlisted_post ) || ! empty( $unlisted_option ), $post, $post_id ); if ( $unlisted ) { update_post_meta( $post_id, 'wpdc_unlisted_topic', 1 ); } - $data = array( + $data = array( 'embed_url' => $permalink, 'featured_link' => $add_featured_link ? $permalink : null, 'title' => $title, @@ -270,8 +330,8 @@ class DiscoursePublish { 'auto_track' => ( ! empty( $options['auto-track'] ) ? 'true' : 'false' ), 'visible' => $unlisted ? 'false' : 'true', ); - $url = $options['url'] . '/posts'; - $post_options = array( + $url = $options['url'] . '/posts'; + $remote_post_options = array( 'timeout' => 30, 'method' => 'POST', 'headers' => array( @@ -280,16 +340,16 @@ class DiscoursePublish { ), 'body' => http_build_query( $data ) . $tags_param, ); - + $remote_post_type = 'create_post'; } else { // The post has already been published. - $data = array( + $data = array( 'title' => $title, 'post[raw]' => $baked, 'skip_validations' => 'true', ); - $url = $options['url'] . '/posts/' . $discourse_id; - $post_options = array( + $url = $options['url'] . '/posts/' . $discourse_id; + $remote_post_options = array( 'timeout' => 30, 'method' => 'PUT', 'headers' => array( @@ -298,51 +358,22 @@ class DiscoursePublish { ), 'body' => http_build_query( $data ), ); - }// End if(). - - $result = wp_remote_post( esc_url_raw( $url ), $post_options ); - - if ( ! $this->validate( $result ) ) { - if ( is_wp_error( $result ) ) { - $error_message = $result->get_error_message(); - $error_code = null; - update_post_meta( $post_id, 'wpdc_publishing_error', $error_message ); - } else { - $result_body = json_decode( wp_remote_retrieve_body( $result ) ); - if ( ! empty( $result_body ) && ! empty( $result_body->errors ) && ! empty( $result_body->errors[0] ) ) { - $error_message = $result_body->errors[0]; - $error_code = null; - } else { - $error_message = wp_remote_retrieve_response_message( $result ); - $error_code = intval( wp_remote_retrieve_response_code( $result ) ); - } - // This is a fix for a bug that was introduced by not setting the wpdc_auto_publish_overridden post_metadata - // when posts are unlined from Discourse. That metadata is now being set. This fix is for dealing with - // previously unlinked posts. - if ( 'Embed url has already been taken' === $error_message ) { - update_post_meta( $post_id, 'wpdc_auto_publish_overridden', 1 ); - } - update_post_meta( $post_id, 'wpdc_publishing_error', sanitize_text_field( $error_message ) ); - } - - // Delete to avoid attempts to republish posts that are returning errors. - delete_post_meta( $post_id, 'publish_to_discourse' ); - - $this->create_bad_response_notifications( $current_post, $post_id, $error_message, $error_code ); - - return new \WP_Error( 'discourse_publishing_response_error', 'An invalid response was returned from Discourse after attempting to publish a post.' ); + $remote_post_type = 'update_post'; } - $body = json_decode( wp_remote_retrieve_body( $result ) ); - // Check for queued posts. We have already determined that a status code of `200` was returned. A post queued by Discourse will have an empty body. - if ( empty( $body ) ) { - update_post_meta( $post_id, 'wpdc_publishing_error', 'queued_topic' ); + $response = $this->remote_post( $url, $remote_post_options, $remote_post_type, $post_id ); - return new \WP_Error( 'discourse_publishing_response_error', 'The published post has been added to the Discourse approval queue.' ); + if ( is_wp_error( $response ) ) { + return $response; } - // The response when a topic is first created. - if ( ! empty( $body->id ) && ! empty( $body->topic_slug ) && ! empty( $body->topic_id ) ) { + $body = $this->validate_response_body( $response, $remote_post_type, $post_id ); + + if ( is_wp_error( $body ) ) { + return $body; + } + + if ( 'create_post' === $remote_post_type ) { $discourse_id = intval( $body->id ); $topic_slug = sanitize_text_field( $body->topic_slug ); $topic_id = intval( $body->topic_id ); @@ -352,6 +383,8 @@ class DiscoursePublish { $this->dc_add_post_meta( $post_id, 'discourse_permalink', esc_url_raw( $options['url'] . '/t/' . $topic_slug . '/' . $topic_id ), true ); update_post_meta( $post_id, 'publish_post_category', $category ); + $this->log_args['discourse_post_id'] = $discourse_id; + // Used for resetting the error notification, if one was being displayed. update_post_meta( $post_id, 'wpdc_publishing_response', 'success' ); if ( $use_multisite_configuration ) { @@ -363,76 +396,62 @@ class DiscoursePublish { if ( ! empty( $pin_until ) ) { $pin_response = $this->pin_discourse_topic( $post_id, $topic_id, $pin_until ); - return $pin_response; + if ( is_wp_error( $pin_response ) ) { + return $pin_response; + } } // The topic has been created and its associated post's metadata has been updated. return null; + } - } elseif ( ! empty( $body->post ) ) { + if ( 'update_post' === $remote_post_type ) { + $discourse_post = $body->post; + $topic_slug = sanitize_text_field( $discourse_post->topic_slug ); + $topic_id = intval( $discourse_post->topic_id ); + $discourse_topic_url = esc_url_raw( $options['url'] . '/t/' . $topic_slug . '/' . $topic_id ); - $discourse_post = $body->post; - $topic_slug = ! empty( $discourse_post->topic_slug ) ? sanitize_text_field( $discourse_post->topic_slug ) : null; - $topic_id = ! empty( $discourse_post->topic_id ) ? intval( $discourse_post->topic_id ) : null; + update_post_meta( $post_id, 'discourse_permalink', $discourse_topic_url ); + update_post_meta( $post_id, 'discourse_topic_id', $topic_id ); + update_post_meta( $post_id, 'wpdc_publishing_response', 'success' ); + // Allows the publish_post_category to be set by clicking the "Update Discourse Topic" button. + update_post_meta( $post_id, 'publish_post_category', $category ); - // Handles deleted topics for recent versions of Discourse. - if ( ! empty( $discourse_post->deleted_at ) ) { - update_post_meta( $post_id, 'wpdc_publishing_error', 'deleted_topic' ); - - return new \WP_Error( 'discourse_publishing_response_error', 'The Discourse topic associated with this post has been deleted.' ); + if ( $use_multisite_configuration ) { + // Used when use_multisite_configuration is enabled, if an existing post is not yet associated with a topic_id/blog_id. + if ( ! $this->topic_blog_id_exists( $topic_id ) ) { + $blog_id = get_current_blog_id(); + $this->save_topic_blog_id( $topic_id, $blog_id ); + } } - if ( $topic_slug && $topic_id ) { - $discourse_topic_url = esc_url_raw( $options['url'] . '/t/' . $topic_slug . '/' . $topic_id ); - update_post_meta( $post_id, 'discourse_permalink', $discourse_topic_url ); - update_post_meta( $post_id, 'discourse_topic_id', $topic_id ); - update_post_meta( $post_id, 'wpdc_publishing_response', 'success' ); - // Allows the publish_post_category to be set by clicking the "Update Discourse Topic" button. - update_post_meta( $post_id, 'publish_post_category', $category ); + // Update the topic's featured_link property. + if ( ! empty( $options['add-featured-link'] ) ) { + $data = array( + 'featured_link' => $permalink, + ); + $remote_post_options = array( + 'timeout' => 30, + 'method' => 'PUT', + 'headers' => array( + 'Api-Key' => sanitize_key( $options['api-key'] ), + 'Api-Username' => sanitize_text_field( $username ), + ), + 'body' => http_build_query( $data ), + ); - if ( $use_multisite_configuration ) { - // Used when use_multisite_configuration is enabled, if an existing post is not yet associated with a topic_id/blog_id. - if ( ! $this->topic_blog_id_exists( $topic_id ) ) { - $blog_id = get_current_blog_id(); - $this->save_topic_blog_id( $topic_id, $blog_id ); - } + $featured_response = $this->remote_post( $discourse_topic_url, $remote_post_options, 'featured_link', $post_id ); + + if ( is_wp_error( $featured_response ) ) { + return $featured_response; } + } - // Update the topic's featured_link property. - if ( ! empty( $options['add-featured-link'] ) ) { - $data = array( - 'featured_link' => $permalink, - ); - $post_options = array( - 'timeout' => 30, - 'method' => 'PUT', - 'headers' => array( - 'Api-Key' => sanitize_key( $options['api-key'] ), - 'Api-Username' => sanitize_text_field( $username ), - ), - 'body' => http_build_query( $data ), - ); + // The topic has been updated, and its associated post's metadata has been updated. + return null; + } - $result = wp_remote_post( esc_url_raw( $discourse_topic_url ), $post_options ); - if ( ! $this->validate( $result ) ) { - - return new \WP_Error( 'discourse_publishing_response_error', 'An error was returned when attempting to update the Discourse featured link.' ); - } - } - - // The topic has been updated, and its associated post's metadata has been updated. - return null; - } else { - $this->create_bad_response_notifications( $current_post, $post_id ); - - return new \WP_Error( 'discourse_publishing_response_error', 'An invalid response was returned from Discourse after attempting to publish a post.' ); - }// End if(). - }// End if(). - - // Neither the 'id' or the 'post' property existed on the response body. - $this->create_bad_response_notifications( $current_post, $post_id ); - - return new \WP_Error( 'discourse_publishing_response_error', 'An invalid response was returned from Discourse after attempting to publish a post.' ); + return $this->handle_error( 'unknown', $response, $post_id ); } /** @@ -468,7 +487,7 @@ class DiscoursePublish { * @return null|\WP_Error */ protected function pin_discourse_topic( $post_id, $topic_id, $pin_until ) { - $status_url = esc_url_raw( $this->options['url'] . "/t/$topic_id/status" ); + $status_url = $this->options['url'] . "/t/$topic_id/status"; $data = array( 'status' => 'pinned', 'enabled' => 'true', @@ -484,42 +503,235 @@ class DiscoursePublish { 'body' => http_build_query( $data ), ); - $response = wp_remote_post( $status_url, $post_options ); - - if ( ! $this->validate( $response ) ) { - - return new \WP_Error( 'discourse_publishing_response_error', 'The topic could not be pinned on Discourse.' ); - } + $response = $this->remote_post( $status_url, $post_options, 'pin_topic', $post_id ); delete_post_meta( $post_id, 'wpdc_pin_until' ); - return null; + return $response; } /** * Creates an admin_notice and calls the publish_failure_notification method after a bad response is returned from Discourse. * - * @param \WP_Post $current_post The post for which the notifications are being created. - * @param int $post_id The current post id. - * @param string $error_message The error message returned from the request. - * @param int $error_code The error code returned from the request. + * @param object $error The error returned from the request. + * @param int $post_id The post for which the notifications are being created. */ - protected function create_bad_response_notifications( $current_post, $post_id, $error_message = '', $error_code = null ) { - update_post_meta( $post_id, 'wpdc_publishing_response', 'error' ); + protected function create_bad_response_notifications( $error, $post_id ) { + $post = get_post( $post_id ); + + if ( empty( $post ) ) { + return; + } + $this->email_notifier->publish_failure_notification( - $current_post, + $post, array( 'location' => 'after_bad_response', - 'error_message' => $error_message, - 'error_code' => $error_code, + 'error_message' => $error->message, + 'error_code' => $error->code, ) ); } + /** + * Wrapper of wp_remote_post to handle validation, logging and error handling. + * + * @param string $url Url of the remote post. + * @param object $remote_options Options to pass to remote post. + * @param string $remote_type Remote post type. + * @param int $post_id ID of post being sent. + */ + public function remote_post( $url, $remote_options, $remote_type, $post_id ) { + $response = wp_remote_post( esc_url_raw( $url ), $remote_options ); + + if ( ! $this->validate( $response ) ) { + $response = $this->handle_error( $remote_type, $response, $post_id ); + } elseif ( ! empty( $this->options['verbose-publication-logs'] ) ) { + $this->logger->info( "$remote_type.post_success", $this->log_args ); + } + + return $response; + } + + /** + * Validation for post response body. + * + * @param object $response Response to be validated. + * @param string $remote_type Remote post type. + * @param int $post_id ID of post being sent. + */ + protected function validate_response_body( $response, $remote_type, $post_id ) { + $body = json_decode( wp_remote_retrieve_body( $response ) ); + $error_type = 'body_validation'; + + if ( $this->post_is_enqueued( $body ) ) { + return $this->handle_notice( $remote_type, $response, $post_id, 'queued_topic' ); + } + + if ( 'create_post' === $remote_type && ! $this->validate_create_post_body( $body ) ) { + return $this->handle_error( $remote_type, $response, $post_id, $error_type ); + } + + if ( 'update_post' === $remote_type ) { + if ( ! $this->validate_update_post_body( $body ) ) { + return $this->handle_error( $remote_type, $response, $post_id, $error_type ); + } + + if ( $this->post_is_deleted( $body ) ) { + return $this->handle_notice( $remote_type, $response, $post_id, 'deleted_topic' ); + } + } + + if ( ! empty( $this->options['verbose-publication-logs'] ) ) { + $this->logger->info( "$remote_type.body_valid", $this->log_args ); + } + + return $body; + } + + /** + * Validate the body of a response when creating a post. + * + * @param object $body Body to be validated. + */ + protected function validate_create_post_body( $body ) { + return ! empty( $body->id ) && ! empty( $body->topic_slug ) && ! empty( $body->topic_id ); + } + + /** + * Validate the body of a response when updating a post. + * + * @param object $body Body to be validated. + */ + protected function validate_update_post_body( $body ) { + return ! empty( $body->post ) && ! empty( $body->post->topic_slug ) && ! empty( $body->post->topic_id ); + } + + /** + * Test for whether post was enqueued. + * + * @param object $body Body to be validated. + */ + protected function post_is_enqueued( $body ) { + return empty( $body ); + } + + /** + * Test for whether topic has been deleted. + * + * @param object $body Body to be validated. + */ + protected function post_is_deleted( $body ) { + return ! empty( $body->post->deleted_at ); + } + + /** + * Handle publication errors.s + * + * @param string $remote_type Remote post type. + * @param object $response Remote post response. + * @param string $post_id ID of post sent. + * @param bool $error_type Error type. + */ + protected function handle_error( $remote_type, $response, $post_id, $error_type = 'post' ) { + $atts = $this->get_response_attributes( $response ); + + if ( 'create_post' === $remote_type ) { + // This is a fix for a bug that was introduced by not setting the wpdc_auto_publish_overridden post_metadata + // when posts are unlinked from Discourse. That metadata is now being set. This fix is for dealing with + // previously unlinked posts. + if ( 'Embed url has already been taken' === $atts->message ) { + update_post_meta( $post_id, 'wpdc_auto_publish_overridden', 1 ); + } + + update_post_meta( $post_id, 'wpdc_publishing_error', sanitize_text_field( $atts->message ) ); + delete_post_meta( $post_id, 'publish_to_discourse' ); + + if ( 'body_validation' === $error_type ) { + update_post_meta( $post_id, 'wpdc_publishing_response', 'error' ); + } + } + + $this->create_bad_response_notifications( $atts, $post_id ); + + if ( 'body_validation' === $error_type ) { + $message = __( 'An invalid response was returned from Discourse', 'wp-discourse' ); + } else { + $message = __( 'An error occurred when communicating with Discourse', 'wp-discourse' ); + } + + $this->logger->error( "{$remote_type}.{$error_type}_error", $this->log_args ); + + return new \WP_Error( 'discourse_publishing_response_error', $message ); + } + + /** + * Handle publication notices. + * + * @param string $remote_type Type of remote post. + * @param object $response Remote post response. + * @param string $post_id ID of post sent. + * @param string $notice_type Type of notice. + */ + protected function handle_notice( $remote_type, $response, $post_id, $notice_type ) { + // The presence of notice types 'queued_topic' and 'deleted_topic' in + // wpdc_publising_error are currently used for determining whether a + // post can be published in discourse-sidebar/src/index.js. + update_post_meta( $post_id, 'wpdc_publishing_error', $notice_type ); + + $this->get_response_attributes( $response ); + $this->logger->warn( "{$remote_type}.{$notice_type}_notice", $this->log_args ); + + $notice_messages = array( + 'queued_topic' => __( 'The published post has been added to the Discourse approval queue', 'wp-discourse' ), + 'deleted_topic' => __( 'The Discourse topic associated with this post has been deleted', 'wp-discourse' ), + ); + $message = $notice_messages[ $notice_type ]; + + return new \WP_Error( 'discourse_publishing_response_notice', $message ); + } + + /** + * Retrieve the message and code from a response. + * + * @param object $response Remote post response. + */ + protected function get_response_attributes( $response ) { + $atts = (object) array( + 'message' => null, + 'code' => null, + ); + + if ( is_wp_error( $response ) ) { + $atts->message = $response->get_error_message(); + } else { + $body = json_decode( wp_remote_retrieve_body( $response ) ); + + if ( ! empty( $body ) && ! empty( $body->errors ) && ! empty( $body->errors[0] ) ) { + $atts->message = $body->errors[0]; + } else { + $atts->message = wp_remote_retrieve_response_message( $response ); + } + } + + if ( ! empty( $atts->message ) ) { + $this->log_args['response_message'] = $atts->message; + } + + $raw_code = wp_remote_retrieve_response_code( $response ); + + if ( ! empty( $raw_code ) ) { + $atts->code = intval( $raw_code ); + $this->log_args['http_code'] = $atts->code; + } + + return $atts; + } + /** * Checks if a post_type can be synced. * - * @param null| $post_id The ID of the post in question. + * @param null $post_id The ID of the post in question. * * @return bool */ @@ -579,7 +791,7 @@ class DiscoursePublish { '%d', '%d', ) - ); + ); // db call whitelist. } /** @@ -591,18 +803,22 @@ class DiscoursePublish { * * @return bool */ - protected function topic_blog_id_exists( $topic_id ) { + public function topic_blog_id_exists( $topic_id ) { global $wpdb; - $table_name = $wpdb->base_prefix . 'wpdc_topic_blog'; - $query = "SELECT * FROM $table_name WHERE topic_id = %d"; - $row = $wpdb->get_row( $wpdb->prepare( $query, $topic_id ) ); + // phpcs:disable WordPress.DB.DirectDatabaseQuery + $row = $wpdb->get_row( + $wpdb->prepare( + "SELECT * FROM {$wpdb->base_prefix}wpdc_topic_blog WHERE topic_id = %d", + $topic_id + ) + ); + // phpcs:enable WordPress.DB.DirectDatabaseQuery return $row ? true : false; } /** - * Gets post metadata via wp method, or directly from db, - * depending on the direct-db-publication-flags option. + * Gets post metadata via wp method, or directly from db, depending on the direct-db-publication-flags option. * * @param int $post_id Post ID. * @param string $key The meta key to retrieve. @@ -616,21 +832,23 @@ class DiscoursePublish { } global $wpdb; - + $limit = $single ? 'LIMIT 1' : ''; + // phpcs:disable WordPress.DB.DirectDatabaseQuery $value = $wpdb->get_var( $wpdb->prepare( - "SELECT meta_value FROM {$wpdb->postmeta} WHERE post_id = %d AND meta_key= %s" . ( $single ? ' LIMIT 1' : '' ) . ';', + "SELECT meta_value FROM {$wpdb->postmeta} WHERE post_id = %d AND meta_key = %s %1s;", // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.UnquotedComplexPlaceholder $post_id, - $key + $key, + $limit ) ); + // phpcs:enable WordPress.DB.DirectDatabaseQuery return $value; } /** - * Adds post metadata via wp method, or directly to db, - * depending on the direct-db-publication-flags option. + * Adds post metadata via wp method, or directly to db, depending on the direct-db-publication-flags option. * * @param int $post_id Post ID. * @param string $key The meta key. @@ -650,12 +868,13 @@ class DiscoursePublish { return false; } + // phpcs:disable WordPress.DB.DirectDatabaseQuery $result = $wpdb->insert( $wpdb->postmeta, array( 'post_id' => $post_id, - 'meta_key' => $key, - 'meta_value' => $value, + 'meta_key' => $key, // phpcs:ignore WordPress.DB.SlowDBQuery + 'meta_value' => $value, // phpcs:ignore WordPress.DB.SlowDBQuery ), array( '%d', @@ -663,6 +882,7 @@ class DiscoursePublish { '%s', ) ); + // phpcs:disable WordPress.DB.DirectDatabaseQuery return $result ? true : false; } diff --git a/lib/logs/formatters/line-formatter.php b/lib/logs/formatters/line-formatter.php new file mode 100644 index 0000000..8a79687 --- /dev/null +++ b/lib/logs/formatters/line-formatter.php @@ -0,0 +1,25 @@ +validate(); + $this->enabled = $file_manager->ready(); + + if ( ! $this->enabled ) { + return; + } + + $this->file_manager = $file_manager; + $this->max_files = $max_files; + $this->file_size_limit = $file_size_limit; + $this->file_number = $this->current_file_number(); + $this->datetime = $datetime; + + // Arguments for StreamHandler. + $current_url = $this->current_file_url(); + $url = $current_url ? $current_url : $this->build_new_file_url(); + $level = Logger::DEBUG; // we want this handler for all levels. + $bubble = false; // we currently have only one handler, so no bubbling. + $file_permission = null; // we handle permissions in the file manager. + $use_locking = true; // we want a log file lock if possible. + + StreamHandler::__construct( $url, $level, $bubble, $file_permission, $use_locking ); + } + + /** + * Public method to determine whether file handler is enabled + */ + public function enabled() { + return $this->enabled; + } + + /** + * {@inheritdoc} + */ + public function close() { + parent::close(); + + if ( true === $this->must_rotate ) { + $this->rotate(); + } + } + + /** + * {@inheritdoc} + */ + public function reset() { + parent::reset(); + + if ( true === $this->must_rotate ) { + $this->rotate(); + } + } + + /** + * {@inheritdoc} + * + * @param array $record Log record being written. + */ + protected function write( array $record ) { + if ( null === $this->must_rotate ) { + $this->must_rotate = ! file_exists( $this->url ); + } + + // Ensure we're writing to today's log file. + $date = $record['datetime']->format( static::DATE_FORMAT ); + if ( ! preg_match( '/' . $date . '/', $this->url ) ) { + $files_for_date = $this->list_files( "*$date*" ); + + if ( count( $files_for_date ) > 0 ) { + $this->url = $files_for_date[0]; + } else { + $this->must_rotate = true; + $this->close(); + } + } + + // Ensure the log file is not too large. + if ( file_exists( $this->url ) && ! $this->validate_size() ) { + $this->file_number++; + $this->must_rotate = true; + $this->close(); + } + + StreamHandler::write( $record ); + } + + /** + * Returns the log file size limit + */ + public function get_file_size_limit() { + return $this->file_size_limit; + } + + /** + * Lists log files in descending order by date and number + * + * @param string $filter optional. Regex pattern for filename. + */ + public function list_files( $filter = '*' ) { + $files = glob( $this->file_manager->logs_dir . "/$filter.log" ); + + usort( + $files, + function ( $a, $b ) { + $a_date = $this->get_date_from_url( $a ); + $b_date = $this->get_date_from_url( $b ); + + if ( $a_date > $b_date ) { + return -1; + } + + if ( $a_date < $b_date ) { + return 1; + } + + $a_number = $this->get_number_from_url( $a ); + $b_number = $this->get_number_from_url( $b ); + + if ( $a_number > $b_number ) { + return -1; + } + + if ( $a_number < $b_number ) { + return 1; + } + + return strcmp( $a, $b ); + } + ); + + return $files; + } + + /** + * Returns the url of the current log file + */ + public function current_file_url() { + $date = $this->get_date(); + $files = $this->list_files( "*$date*" ); + + if ( count( $files ) > 0 ) { + return reset( $files ); + } else { + return false; + } + } + + /** + * Returns the current log file number + */ + public function current_file_number() { + $file_url = $this->current_file_url(); + + if ( $file_url ) { + return $this->get_number_from_url( $file_url ); + } else { + return 1; + } + } + + /** + * Handles log rotation + */ + protected function rotate() { + $this->url = $this->build_new_file_url(); + $files = $this->list_files(); + + if ( count( $files ) >= ( $this->max_files - 1 ) ) { + foreach ( array_slice( $files, ( $this->max_files - 1 ) ) as $file ) { + if ( is_writable( $file ) ) { + // Note from monolog/monolog: + // "suppress errors here as unlink() might fail if two processes + // are cleaning up/rotating at the same time.". + // phpcs:disable WordPress.PHP.DevelopmentFunctions + set_error_handler( + function () { + return false; + } + ); + unlink( $file ); + restore_error_handler(); + // phpcs:enabled WordPress.PHP.DevelopmentFunctions + } + } + } + + $this->must_rotate = false; + } + + /** + * Builds a new log file url + */ + protected function build_new_file_url() { + $dir_path = $this->file_manager->logs_dir; + $name = $this->file_name(); + $hash = wp_hash( $name, 'nonce' ); + $extension = 'log'; + return "$dir_path/$name-$hash.$extension"; + } + + /** + * Returns date used by file handler + */ + public function get_date() { + if ( isset( $this->datetime ) ) { + return $this->datetime->format( static::DATE_FORMAT ); + } else { + return gmdate( static::DATE_FORMAT ); + } + } + + /** + * Validates size of current log file against size limit + */ + protected function validate_size() { + // Note https://github.com/WordPress/WordPress-Coding-Standards/pull/1265#issuecomment-405143028. + // Note https://github.com/woocommerce/woocommerce/issues/6091. + $handle = fopen( $this->url, 'r+' ); // phpcs:ignore WordPress.WP.AlternativeFunctions + $stat = fstat( $handle ); + $last_line_byte_buffer = 100; + return $stat['size'] <= ( $this->file_size_limit - $last_line_byte_buffer ); + } + + /** + * Builds current log file name + */ + protected function file_name() { + $date = $this->get_date(); + $number = $this->file_number; + return $this->build_filename( $date, $number ); + } + + /** + * Build file name + * + * @param string $date Log date. + * @param string $number Log number. + */ + protected function build_filename( $date, $number ) { + $namespace = static::FILE_NAMESPACE; + return "$namespace-$date-$number"; + } + + /** + * Retrieves file number from file url + * + * @param string $file_url URL of log file. + */ + public function get_number_from_url( $file_url ) { + $parts = explode( '-', $file_url ); + end( $parts ); + return (int) prev( $parts ); + } + + /** + * Retrieves file date from file url + * + * @param string $file_url URL of log file. + */ + public function get_date_from_url( $file_url ) { + $parts = explode( '-', $file_url ); + $date_parts = array_slice( array_slice( $parts, -5 ), 0, 3 ); + return implode( '-', $date_parts ); + } + + /** + * Retrieves file name from file url + * + * @param string $file_url URL of log file. + */ + public function get_filename( $file_url ) { + $date = $this->get_date_from_url( $file_url ); + $number = $this->get_number_from_url( $file_url ); + return $this->build_filename( $date, $number ); + } +} diff --git a/lib/logs/handlers/null-handler.php b/lib/logs/handlers/null-handler.php new file mode 100644 index 0000000..62e2973 --- /dev/null +++ b/lib/logs/handlers/null-handler.php @@ -0,0 +1,13 @@ +enabled() ) { + if ( $formatter ) { + $handler->setFormatter( $formatter ); + } + } else { + $handler = new NullHandler(); + } + + $logger->pushHandler( $handler ); + + return $logger; + } +}; diff --git a/lib/logs/managers/file-manager.php b/lib/logs/managers/file-manager.php new file mode 100644 index 0000000..112ec7d --- /dev/null +++ b/lib/logs/managers/file-manager.php @@ -0,0 +1,162 @@ +ready = false; + $this->upload_dir = wp_upload_dir()['basedir'] . '/' . $this->upload_folder; + $this->logs_dir = $this->upload_dir . '/' . $this->logs_folder; + } + + /** + * Validates that all necessary files are ready. + * Sets $ready to true if validation passes. + */ + public function validate() { + if ( ! is_writable( wp_upload_dir()['basedir'] ) ) { + return false; + } + + $files = array( + array( + 'base' => $this->upload_dir, + 'file' => 'index.html', + 'content' => '', + ), + array( + 'base' => $this->upload_dir, + 'file' => '.htaccess', + 'content' => $this->htaccess_content(), + ), + array( + 'base' => $this->logs_dir, + 'file' => 'index.html', + 'content' => '', + ), + array( + 'base' => $this->logs_dir, + 'file' => '.htaccess', + 'content' => $this->htaccess_content(), + ), + ); + + $this->create_files( $files ); + + $ready = $this->files_are_ready( $files ); + $this->ready = $ready; + + return $ready; + } + + /** + * Public method to determine whether file manager is ready + */ + public function ready() { + return $this->ready; + } + + /** + * Creates files if they don't exist + * + * @access protected + * @param string $files List of files. + */ + protected function create_files( $files ) { + foreach ( $files as $file ) { + $file_path = trailingslashit( $file['base'] ) . $file['file']; + $dir_exists = wp_mkdir_p( $file['base'] ); + $dir_writable = is_writable( $file['base'] ); + + // Note https://github.com/WordPress/WordPress-Coding-Standards/pull/1265#issuecomment-405143028. + // Note https://github.com/woocommerce/woocommerce/issues/6091. + // phpcs:disable WordPress.WP.AlternativeFunctions + if ( $dir_exists && $dir_writable && ! file_exists( $file_path ) ) { + $file_handle = fopen( $file_path, 'wb' ); + + if ( $file_handle ) { + fwrite( $file_handle, $file['content'] ); + fclose( $file_handle ); + } + } + // phpcs:enable Wordpress.WP.AlternativeFunctions + } + } + + /** + * Checks if all files exist and are writable + * + * @access protected + * @param string $files List of files. + */ + protected function files_are_ready( $files ) { + foreach ( $files as $file ) { + $directory_path = trailingslashit( $file['base'] ); + $file_path = $directory_path . $file['file']; + + if ( ! is_writable( $directory_path ) || ! file_exists( $file_path ) ) { + return false; + } + } + + return true; + } + + /** + * Returns content of htaccess files + * + * @access protected + */ + protected function htaccess_content() { + return 'deny from all'; + } +} diff --git a/lib/sso-client/nonce.php b/lib/sso-client/nonce.php index bda1b21..9b7d8f2 100644 --- a/lib/sso-client/nonce.php +++ b/lib/sso-client/nonce.php @@ -3,8 +3,11 @@ * Nonce generator & validator. * * @package WPDiscourse + * @todo Review phpcs disablement. */ +// phpcs:disable WordPress.DB.PreparedSQL + namespace WPDiscourse\SSOClient; /** diff --git a/lib/sync-discourse-topic.php b/lib/sync-discourse-topic.php index 4b5ae3e..c10f6e4 100644 --- a/lib/sync-discourse-topic.php +++ b/lib/sync-discourse-topic.php @@ -3,8 +3,11 @@ * Uses a Discourse webhook to sync topics with their associated WordPress posts. * * @package WPDiscourse\DiscourseWebhookRefresh + * @todo Review phpcs disablement. */ +// phpcs:disable WordPress.DB.PreparedSQL + namespace WPDiscourse\SyncDiscourseTopic; use WPDiscourse\Webhook\Webhook; diff --git a/lib/utilities.php b/lib/utilities.php index f125bd1..cef6af0 100644 --- a/lib/utilities.php +++ b/lib/utilities.php @@ -661,8 +661,8 @@ class Utilities { break; case 'html': $result->{$key} = $value; + break; default: - ; break; } } else { diff --git a/phpcs.xml b/phpcs.xml index 5b37cbb..72e4041 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -6,22 +6,23 @@ + - - - + - - - - - - - - + + php + */tests/* + */lib/wp-new-user-notification.php* + */vendor/* + */admin/discourse-sidebar/build/index.asset.php** + */vendor_namespaced/* + *.js + *.css + scoper.inc.php \ No newline at end of file diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..e1ff9e0 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,22 @@ + + + + + tests/phpunit/ + tests/phpunit/multisite/ + + + + + lib/logs + lib/discourse-publish.php + + + diff --git a/readme.txt b/readme.txt index 299c316..fad8428 100644 --- a/readme.txt +++ b/readme.txt @@ -2,9 +2,9 @@ Contributors: scossar, cdck, angusmcleod, samsaffron, techapj Tags: discourse, forum, comments, sso Requires at least: 4.7 -Tested up to: 5.7 +Tested up to: 5.8 Requires PHP: 5.6.0 -Stable tag: 2.2.3 +Stable tag: 2.2.4 License: GPLv2 or later License URI: http://www.gnu.org/licenses/gpl-2.0.html @@ -123,6 +123,10 @@ To create a coherent top menu, see our tutorial on how to make a [Custom nav hea == Changelog == +#### 2.2.4 05/11/2021 + +- Add a logging system. This update adds a Logs tab to the plugin's options menu. + #### 2.2.3 04/05/2021 - Add `wpdc_comments_count` filter to allow comments count for posts that have not been published to Discourse to be filtered diff --git a/scoper.inc.php b/scoper.inc.php new file mode 100644 index 0000000..30c876d --- /dev/null +++ b/scoper.inc.php @@ -0,0 +1,132 @@ + 'WPDiscourse', + + // By default when running php-scoper add-prefix, it will prefix all relevant code found in the current working + // directory. You can however define which files should be scoped by defining a collection of Finders in the + // following configuration key. + // + // For more see: https://github.com/humbug/php-scoper#finders-and-paths + 'finders' => [ + Finder::create() + ->files() + ->ignoreVCS(true) + ->in('vendor') + ->path([ + 'Monolog/Formatter/FormatterInterface.php', + 'Monolog/Formatter/NormalizerFormatter.php', + 'Monolog/Formatter/LineFormatter.php', + 'Monolog/Handler/AbstractHandler.php', + 'Monolog/Handler/AbstractProcessingHandler.php', + 'Monolog/Handler/HandlerInterface.php', + 'Monolog/Handler/NullHandler.php', + 'Monolog/Handler/StreamHandler.php', + 'Monolog/ResettableInterface.php', + 'Monolog/Logger.php', + 'Monolog/Utils.php', + 'monolog/LICENSE' + ]), + Finder::create() + ->files() + ->ignoreVCS(true) + ->notName('/.*\\.md|.*\\.dist|Makefile|composer\\.json|composer\\.lock/') + ->exclude([ + 'doc', + 'test', + 'test_old', + 'Test', + 'tests', + 'Tests', + 'vendor-bin', + ]) + ->in('vendor') + ->path('/^psr/') + ], + + // Whitelists a list of files. Unlike the other whitelist related features, this one is about completely leaving + // a file untouched. + // Paths are relative to the configuration file unless if they are already absolute + 'files-whitelist' => [], + + // When scoping PHP files, there will be scenarios where some of the code being scoped indirectly references the + // original namespace. These will include, for example, strings or string manipulations. PHP-Scoper has limited + // support for prefixing such strings. To circumvent that, you can define patchers to manipulate the file to your + // heart contents. + // + // For more see: https://github.com/humbug/php-scoper#patchers + 'patchers' => [ + function (string $filePath, string $prefix, string $contents) { + $lines = explode( "\n", $contents ); + + foreach ( $lines as $index => $line ) { + + // Ensure functions do not have return type declarations, which are not supported in PHP 5.*. + // See further https://github.com/Seldaek/monolog/issues/1537. + if ( preg_match( '/\h(function)\h/', $line ) && ltrim($line)[0] !== '*' ) { + $last_bracket_index = strripos( $line, ')' ); + + if ( $last_bracket_index ) { + $new_line = substr( $line, 0, $last_bracket_index ); + $new_line .= ")"; + + $line_without_args = preg_replace( '/\(([^()]*+|(?R))*\)/', '', $line ); + + if ( strpos( $line_without_args, "{" ) !== false ) { + $new_line .= " {"; + } + + if ( substr( $line, -1 ) == ";" ) { + $new_line .= ";"; + } + + $lines[ $index ] = $new_line; + } + } + + // Remove strict_type declarations + if ( preg_match( '/strict_types/', $line ) && ltrim($line)[0] !== '*' ) { + unset( $lines[ $index ] ); + } + + } + + return implode( "\n", $lines ); + }, + ], + + // PHP-Scoper's goal is to make sure that all code for a project lies in a distinct PHP namespace. However, you + // may want to share a common API between the bundled code of your PHAR and the consumer code. For example if + // you have a PHPUnit PHAR with isolated code, you still want the PHAR to be able to understand the + // PHPUnit\Framework\TestCase class. + // + // A way to achieve this is by specifying a list of classes to not prefix with the following configuration key. Note + // that this does not work with functions or constants neither with classes belonging to the global namespace. + // + // Fore more see https://github.com/humbug/php-scoper#whitelist + 'whitelist' => [ + // 'PHPUnit\Framework\TestCase', // A specific class + // 'PHPUnit\Framework\*', // The whole namespace + // '*', // Everything + ], + + // If `true` then the user defined constants belonging to the global namespace will not be prefixed. + // + // For more see https://github.com/humbug/php-scoper#constants--constants--functions-from-the-global-namespace + 'whitelist-global-constants' => true, + + // If `true` then the user defined classes belonging to the global namespace will not be prefixed. + // + // For more see https://github.com/humbug/php-scoper#constants--constants--functions-from-the-global-namespace + 'whitelist-global-classes' => true, + + // If `true` then the user defined functions belonging to the global namespace will not be prefixed. + // + // For more see https://github.com/humbug/php-scoper#constants--constants--functions-from-the-global-namespace + 'whitelist-global-functions' => true, +]; diff --git a/tests/fixtures/response_body/post_create.json b/tests/fixtures/response_body/post_create.json new file mode 100644 index 0000000..c9136f5 --- /dev/null +++ b/tests/fixtures/response_body/post_create.json @@ -0,0 +1,66 @@ +{ + "id": 23, + "name": "Angus McLeod", + "username": "angus", + "avatar_template": "/user_avatar/localhost/angus/{size}/3_2.png", + "created_at": "2021-02-04T05:21:59.570Z", + "cooked": "

Originally published at:\t\t\thttp://wordpress/blog/2021/02/04/this-is-a-new-post-5/
\n

This is the content

", + "post_number": 1, + "post_type": 1, + "updated_at": "2021-02-04T05:21:59.570Z", + "reply_count": 0, + "reply_to_post_number": null, + "quote_count": 0, + "incoming_link_count": 0, + "reads": 0, + "readers_count": 0, + "score": 0, + "yours": true, + "topic_id": 20, + "topic_slug": "this-is-a-new-post", + "display_username": "Angus McLeod", + "primary_group_name": null, + "primary_group_flair_url": null, + "primary_group_flair_bg_color": null, + "primary_group_flair_color": null, + "version": 1, + "can_edit": true, + "can_delete": false, + "can_recover": false, + "can_wiki": true, + "user_title": "", + "bookmarked": false, + "actions_summary": [ + { + "id": 3, + "can_act": true + }, + { + "id": 4, + "can_act": true + }, + { + "id": 8, + "can_act": true + }, + { + "id": 7, + "can_act": true + } + ], + "moderator": false, + "admin": true, + "staff": true, + "user_id": 1, + "draft_sequence": 0, + "hidden": false, + "trust_level": 1, + "deleted_at": null, + "user_deleted": false, + "edit_reason": null, + "can_view_edit_history": true, + "wiki": false, + "reviewable_id": null, + "reviewable_score_count": 0, + "reviewable_score_pending_count": 0 +} \ No newline at end of file diff --git a/tests/fixtures/response_body/post_update.json b/tests/fixtures/response_body/post_update.json new file mode 100644 index 0000000..146cfcc --- /dev/null +++ b/tests/fixtures/response_body/post_update.json @@ -0,0 +1,68 @@ +{ + "post": { + "id": 23, + "name": "Angus McLeod", + "username": "angus", + "avatar_template": "/user_avatar/localhost/angus/{size}/3_2.png", + "created_at": "2021-02-04T05:21:59.570Z", + "cooked": "

Originally published at:\t\t\thttp://wordpress/blog/2021/02/04/this-is-a-new-post-5/
\n

This is the updated content

", + "post_number": 1, + "post_type": 1, + "updated_at": "2021-02-10T23:06:05.328Z", + "reply_count": 0, + "reply_to_post_number": null, + "quote_count": 0, + "incoming_link_count": 0, + "reads": 0, + "readers_count": 0, + "score": 0, + "yours": true, + "topic_id": 20, + "topic_slug": "this-is-a-new-post", + "display_username": "Angus McLeod", + "primary_group_name": null, + "primary_group_flair_url": null, + "primary_group_flair_bg_color": null, + "primary_group_flair_color": null, + "version": 1, + "can_edit": true, + "can_delete": false, + "can_recover": false, + "can_wiki": true, + "user_title": "", + "bookmarked": false, + "actions_summary": [ + { + "id": 3, + "can_act": true + }, + { + "id": 4, + "can_act": true + }, + { + "id": 8, + "can_act": true + }, + { + "id": 7, + "can_act": true + } + ], + "moderator": false, + "admin": true, + "staff": true, + "user_id": 1, + "draft_sequence": 0, + "hidden": false, + "trust_level": 1, + "deleted_at": null, + "user_deleted": false, + "edit_reason": null, + "can_view_edit_history": true, + "wiki": false, + "reviewable_id": null, + "reviewable_score_count": 0, + "reviewable_score_pending_count": 0 + } +} \ No newline at end of file diff --git a/tests/phpunit/bootstrap.php b/tests/phpunit/bootstrap.php new file mode 100644 index 0000000..9707413 --- /dev/null +++ b/tests/phpunit/bootstrap.php @@ -0,0 +1,33 @@ + + + + + + + + multisite/ + + + + + lib/discourse-publish.php + + + diff --git a/tests/phpunit/multisite/test-discourse-publish-multisite.php b/tests/phpunit/multisite/test-discourse-publish-multisite.php new file mode 100644 index 0000000..82db733 --- /dev/null +++ b/tests/phpunit/multisite/test-discourse-publish-multisite.php @@ -0,0 +1,85 @@ +create_topic_blog_table(); + } + + + /** + * Teardown multisite tests + */ + public function tearDown() { + parent::tearDown(); + $this->clear_topic_blog_table(); + } + + /** + * Sync_to_discourse handles new posts correctly in multisite + */ + public function test_sync_to_discourse_when_creating_in_multisite() { + // Set as multisite. + self::$plugin_options['multisite-configuration-enabled'] = 1; + $this->publish->setup_options( self::$plugin_options ); + + // Set up a response body for creating a new post. + $body = $this->mock_remote_post_success( 'post_create' ); + $discourse_topic_id = $body->topic_id; + + // Add the post. + $post_id = wp_insert_post( self::$post_atts, false, false ); + + // Run the publication. + $post = get_post( $post_id ); + $this->publish->sync_to_discourse_without_lock( $post_id, $post->title, $post->post_content ); + + // Ensure the topic blog id is created properly. + $this->assertTrue( $this->publish->topic_blog_id_exists( $body->topic_id ) ); + + // cleanup. + wp_delete_post( $post_id ); + } + + /** + * Create topic_blog_table if it doesn't exist. + */ + protected function create_topic_blog_table() { + global $wpdb; + + $table = $wpdb->base_prefix . 'wpdc_topic_blog'; + $sql = sprintf( + 'CREATE TABLE %s ( + topic_id mediumint(9) NOT NULL, + blog_id mediumint(9) NOT NULL, + PRIMARY KEY (topic_id) + ) %s;', + $table, + $wpdb->get_charset_collate() + ); + + require_once ABSPATH . 'wp-admin/includes/upgrade.php'; + maybe_create_table( $table, $sql ); + } + + /** + * Clear topic_blog_table + */ + protected function clear_topic_blog_table() { + global $wpdb; + $table = $wpdb->base_prefix . 'wpdc_topic_blog'; + $result = $wpdb->query( "TRUNCATE TABLE $table" ); + } +} diff --git a/tests/phpunit/test-discourse-publish.php b/tests/phpunit/test-discourse-publish.php new file mode 100644 index 0000000..44dba46 --- /dev/null +++ b/tests/phpunit/test-discourse-publish.php @@ -0,0 +1,764 @@ +publish = new DiscoursePublish( new EmailNotification(), $register_actions ); + $this->publish->setup_logger(); + $this->publish->setup_options( self::$plugin_options ); + } + + /** + * Teardown each test. + */ + public function tearDown() { + $this->clear_logs(); + remove_all_filters( 'pre_http_request' ); + } + + /** + * Sync_to_discourse handles new posts correctly. + */ + public function test_sync_to_discourse_when_creating() { + // Set up a response body for creating a new post. + $body = $this->mock_remote_post_success( 'post_create' ); + $discourse_post_id = $body->id; + $discourse_topic_id = $body->topic_id; + $discourse_permalink = self::$discourse_url . '/t/' . $body->topic_slug . '/' . $body->topic_id; + $discourse_category = self::$post_atts['meta_input']['publish_post_category']; + + // Add the post. + $post_id = wp_insert_post( self::$post_atts, false, false ); + + // Run the publication. + $post = get_post( $post_id ); + $this->publish->sync_to_discourse_without_lock( $post_id, $post->title, $post->post_content ); + + // Ensure the right post meta is created. + $this->assertEquals( get_post_meta( $post_id, 'discourse_post_id', true ), $discourse_post_id ); + $this->assertEquals( get_post_meta( $post_id, 'discourse_topic_id', true ), $discourse_topic_id ); + $this->assertEquals( get_post_meta( $post_id, 'discourse_permalink', true ), $discourse_permalink ); + $this->assertEquals( get_post_meta( $post_id, 'publish_post_category', true ), $discourse_category ); + $this->assertEquals( get_post_meta( $post_id, 'wpdc_publishing_response', true ), 'success' ); + + // Cleanup. + wp_delete_post( $post_id ); + } + + /** + * Sync_to_discourse when creating a new post with embed error response. + */ + public function test_sync_to_discourse_when_creating_with_embed_error() { + // Set up the error responses. + $raw_response = $this->build_response( 'unprocessable', 'embed' ); + $error_message = json_decode( $raw_response['body'] )->errors[0]; + $this->mock_remote_post( $raw_response ); + + // Add the post. + $post_id = wp_insert_post( self::$post_atts, false, false ); + + // Run the publication. + $post = get_post( $post_id ); + $response = $this->publish->sync_to_discourse_without_lock( + $post_id, + $post->title, + $post->post_content + ); + + // Ensure the right error is returned. + $this->assertEquals( $response, $this->build_post_error() ); + + // Ensure the post meta is updated correctly. + $this->assertEquals( get_post_meta( $post_id, 'wpdc_auto_publish_overridden', true ), 1 ); + $this->assertEquals( get_post_meta( $post_id, 'publish_to_discourse', true ), '' ); + $this->assertEquals( get_post_meta( $post_id, 'wpdc_publishing_error', true ), $error_message ); + + // Ensure the right log is created. + $log = $this->get_last_log(); + $this->assertRegExp( '/publish.ERROR: create_post.post_error/', $log ); + $this->assertRegExp( '/"http_code":' . $raw_response['response']['code'] . '/', $log ); + $this->assertRegExp( '/"response_message":"' . $error_message . '"/', $log ); + + // Cleanup. + wp_delete_post( $post_id ); + } + + /** + * Sync_to_discourse when creating a new post with category error response. + */ + public function test_sync_to_discourse_when_creating_with_category_error() { + // Set up the error responses. + $raw_response = $this->build_response( 'invalid_parameters', 'category' ); + $error_message = json_decode( $raw_response['body'] )->errors[0]; + $this->mock_remote_post( $raw_response ); + + // Add the post. + $post_id = wp_insert_post( self::$post_atts, false, false ); + + // Run the publication. + $post = get_post( $post_id ); + $response = $this->publish->sync_to_discourse_without_lock( + $post_id, + $post->title, + $post->post_content + ); + + // Ensure the right error is returned. + $this->assertEquals( $response, $this->build_post_error() ); + + // Ensure the post meta is updated correctly. + $this->assertEquals( get_post_meta( $post_id, 'publish_to_discourse', true ), '' ); + $this->assertEquals( get_post_meta( $post_id, 'wpdc_publishing_error', true ), $error_message ); + + // Ensure the right log is created. + $log = $this->get_last_log(); + $this->assertRegExp( '/publish.ERROR: create_post.post_error/', $log ); + $this->assertRegExp( '/"http_code":' . $raw_response['response']['code'] . '/', $log ); + $this->assertRegExp( '/"response_message":"' . $error_message . '"/', $log ); + + // cleanup. + wp_delete_post( $post_id ); + } + + /** + * Sync_to_discourse when creating a new post with invalid body in response. + */ + public function test_sync_to_discourse_when_creating_with_response_body_error() { + // Setup the invalid respond body. + $response = $this->build_response( 'success' ); + $response['body'] = '{ "invalid_body" : true }'; + $this->mock_remote_post( $response ); + + // Add the post. + $post_id = wp_insert_post( self::$post_atts, false, false ); + + // Run the publication. + $post = get_post( $post_id ); + $response = $this->publish->sync_to_discourse_without_lock( + $post_id, + $post->title, + $post->post_content + ); + + // Ensure the right error is returned. + $this->assertEquals( $response, $this->build_body_error() ); + + // Ensure the post meta is updated correctly. + $this->assertEquals( get_post_meta( $post_id, 'publish_to_discourse', true ), '' ); + $this->assertEquals( get_post_meta( $post_id, 'wpdc_publishing_error', true ), 'OK' ); + $this->assertEquals( get_post_meta( $post_id, 'wpdc_publishing_response', true ), 'error' ); + + // Ensure the right log is created. + $log = $this->get_last_log(); + $this->assertRegExp( '/publish.ERROR: create_post.body_validation_error/', $log ); + + // cleanup. + wp_delete_post( $post_id ); + } + + /** + * Sync_to_discourse when creating a new post and post is enqueued. + */ + public function test_sync_to_discourse_when_creating_with_enqueued_post() { + // Setup the enqueued response body. + $response = $this->build_response( 'success' ); + $response['body'] = ''; + $this->mock_remote_post( $response ); + + // Add the post. + $post_id = wp_insert_post( self::$post_atts, false, false ); + + // Run the publication. + $post = get_post( $post_id ); + $response = $this->publish->sync_to_discourse_without_lock( + $post_id, + $post->title, + $post->post_content + ); + + // Ensure the right error is returned. + $message = __( 'The published post has been added to the Discourse approval queue', 'wp-discourse' ); + $this->assertEquals( $response, $this->build_notice( $message ) ); + + // Ensure the post meta is updated correctly. + $this->assertEquals( get_post_meta( $post_id, 'publish_to_discourse', true ), 1 ); + $this->assertEquals( get_post_meta( $post_id, 'discourse_post_id', true ), '' ); + $this->assertEquals( get_post_meta( $post_id, 'wpdc_publishing_error', true ), 'queued_topic' ); + + // Ensure the right log is created. + $log = $this->get_last_log(); + $this->assertRegExp( '/publish.WARNING: create_post.queued_topic_notice/', $log ); + + // cleanup. + wp_delete_post( $post_id ); + } + + /** + * Sync_to_discourse when creating a new post with direct-db-publication-flags. + */ + public function test_sync_to_discourse_when_creating_with_direct_db_publication_flags() { + // Enable direct db pubilcation flags option. + self::$plugin_options['direct-db-publication-flags'] = 1; + $this->publish->setup_options( self::$plugin_options ); + + // Set up a response body for creating a new post. + $body = $this->mock_remote_post_success( 'post_create' ); + $discourse_post_id = $body->id; + $discourse_topic_id = $body->topic_id; + $discourse_permalink = self::$discourse_url . '/t/' . $body->topic_slug . '/' . $body->topic_id; + $discourse_category = self::$post_atts['meta_input']['publish_post_category']; + + // Add the post. + $post_id = wp_insert_post( self::$post_atts, false, false ); + + // Run the publication. + $post = get_post( $post_id ); + $this->publish->sync_to_discourse_without_lock( $post_id, $post->title, $post->post_content ); + + // Ensure the right post meta is created. + $this->assertEquals( get_post_meta( $post_id, 'discourse_post_id', true ), $discourse_post_id ); + $this->assertEquals( get_post_meta( $post_id, 'discourse_topic_id', true ), $discourse_topic_id ); + $this->assertEquals( get_post_meta( $post_id, 'discourse_permalink', true ), $discourse_permalink ); + $this->assertEquals( get_post_meta( $post_id, 'publish_post_category', true ), $discourse_category ); + $this->assertEquals( get_post_meta( $post_id, 'wpdc_publishing_response', true ), 'success' ); + + // Cleanup. + wp_delete_post( $post_id ); + } + + /** + * Sync_to_discourse when creating a new post and pinning topics. + */ + public function test_sync_to_discourse_pin_topic() { + // Set up a response body for creating a new post, with subsequent pin request. + $pin_until = '2021-02-17'; + $pin_until_body = http_build_query( + array( + 'status' => 'pinned', + 'enabled' => 'true', + 'until' => $pin_until, + ) + ); + $second_request = array( + 'body' => $pin_until_body, + 'response' => $this->build_response( 'success' ), + ); + $body = $this->mock_remote_post_success( 'post_create', $second_request ); + + // Add a post that will be pinned. + $post_atts = self::$post_atts; + $post_atts['meta_input']['wpdc_pin_until'] = $pin_until; + $post_id = wp_insert_post( $post_atts, false, false ); + + // Run the publication. + $post = get_post( $post_id ); + $response = $this->publish->sync_to_discourse_without_lock( + $post_id, + $post->title, + $post->post_content + ); + + // Ensure the right result. + $this->assertFalse( is_wp_error( $response ) ); + $this->assertTrue( empty( get_post_meta( $post_id, 'wpdc_pin_until', true ) ) ); + + // Cleanup. + wp_delete_post( $post_id ); + } + + /** + * Sync_to_discourse when updating a post. + */ + public function test_sync_to_discourse_when_updating() { + // Set up a response body for updating an existing post. + $body = $this->mock_remote_post_success( 'post_update' ); + $post = $body->post; + + $discourse_post_id = $post->id; + $discourse_topic_id = $post->topic_id; + $discourse_permalink = self::$discourse_url . '/t/' . $post->topic_slug . '/' . $post->topic_id; + $discourse_category = self::$post_atts['meta_input']['publish_post_category']; + + // Add a post that's already been published to Discourse. + $post_atts = self::$post_atts; + $post_atts['meta_input']['discourse_post_id'] = $discourse_post_id; + $post_id = wp_insert_post( $post_atts, false, false ); + + // Run the update. + update_post_meta( $post_id, 'update_discourse_topic', 1 ); + $post = get_post( $post_id ); + $this->publish->sync_to_discourse_without_lock( $post_id, $post->title, $post->post_content ); + + // Ensure the right post meta still exists. + $this->assertEquals( get_post_meta( $post_id, 'discourse_post_id', true ), $discourse_post_id ); + $this->assertEquals( get_post_meta( $post_id, 'discourse_topic_id', true ), $discourse_topic_id ); + $this->assertEquals( get_post_meta( $post_id, 'discourse_permalink', true ), $discourse_permalink ); + $this->assertEquals( get_post_meta( $post_id, 'publish_post_category', true ), $discourse_category ); + $this->assertEquals( get_post_meta( $post_id, 'wpdc_publishing_response', true ), 'success' ); + + // Cleanup. + wp_delete_post( $post_id ); + } + + /** + * Sync_to_discourse when updating a post and post is deleted. + */ + public function test_sync_to_discourse_when_updating_with_deleted_topic() { + // Setup the response body for an existing post that's been deleted. + $response = $this->build_response( 'success' ); + $raw_body = $this->response_body_json( 'post_update' ); + $body = json_decode( $raw_body ); + $body->post->deleted_at = '2021-03-10T23:06:05.328Z'; + $response['body'] = json_encode( $body ); + $this->mock_remote_post( $response ); + + // Add a post that's already been published to Discourse. + $discourse_post_id = $body->post->id; + $post_atts = self::$post_atts; + $post_atts['meta_input']['discourse_post_id'] = $discourse_post_id; + $post_id = wp_insert_post( $post_atts, false, false ); + + // Run the update. + update_post_meta( $post_id, 'update_discourse_topic', 1 ); + $post = get_post( $post_id ); + $response = $this->publish->sync_to_discourse_without_lock( + $post_id, + $post->title, + $post->post_content + ); + + // Ensure the right error is returned. + $message = __( 'The Discourse topic associated with this post has been deleted', 'wp-discourse' ); + $this->assertEquals( $response, $this->build_notice( $message ) ); + + // Ensure the post meta is updated correctly. + $this->assertEquals( get_post_meta( $post_id, 'publish_to_discourse', true ), 1 ); + $this->assertEquals( get_post_meta( $post_id, 'discourse_post_id', true ), $discourse_post_id ); + $this->assertEquals( get_post_meta( $post_id, 'wpdc_publishing_error', true ), 'deleted_topic' ); + + // Ensure the right log is created. + $log = $this->get_last_log(); + $this->assertRegExp( '/publish.WARNING: update_post.deleted_topic_notice/', $log ); + + // cleanup. + wp_delete_post( $post_id ); + } + + /** + * Sync_to_discourse when updating a post and adding featured link. + */ + public function test_sync_to_discourse_when_updating_with_featured_link() { + // Enable featured link option. + self::$plugin_options['add-featured-link'] = 1; + $this->publish->setup_options( self::$plugin_options ); + + // Add a post that's already been published to Discourse. + $body = json_decode( $this->response_body_json( 'post_update' ) ); + $discourse_post = $body->post; + $post_atts = self::$post_atts; + $post_atts['meta_input']['discourse_post_id'] = $discourse_post->id; + $post_id = wp_insert_post( $post_atts, false, false ); + + // Set up a response body for updating an existing post, and the featured link in the second request. + $featured_link_body = http_build_query( + array( + 'featured_link' => get_permalink( $post_id ), + ) + ); + $second_request = array( + 'body' => $featured_link_body, + 'response' => $this->build_response( 'success' ), + ); + $body = $this->mock_remote_post_success( 'post_update', $second_request ); + $post = $body->post; + + // Run the update. + update_post_meta( $post_id, 'update_discourse_topic', 1 ); + $post = get_post( $post_id ); + $response = $this->publish->sync_to_discourse_without_lock( + $post_id, + $post->title, + $post->post_content + ); + + // Ensure the right result. + $this->assertFalse( is_wp_error( $response ) ); + + // Cleanup. + wp_delete_post( $post_id ); + } + + /** + * Sync_to_discourse when updating a post with direct-db-publication-flags. + */ + public function test_sync_to_discourse_when_updating_with_direct_db_publication_flags() { + // Enable direct db pubilcation flags option. + self::$plugin_options['direct-db-publication-flags'] = 1; + $this->publish->setup_options( self::$plugin_options ); + + // Set up a response body for updating an existing post. + $body = $this->mock_remote_post_success( 'post_update' ); + $post = $body->post; + + $discourse_post_id = $post->id; + $discourse_topic_id = $post->topic_id; + $discourse_permalink = self::$discourse_url . '/t/' . $post->topic_slug . '/' . $post->topic_id; + $discourse_category = self::$post_atts['meta_input']['publish_post_category']; + + // Add a post that's already been published to Discourse. + $post_atts = self::$post_atts; + $post_atts['meta_input']['discourse_post_id'] = $discourse_post_id; + $post_id = wp_insert_post( $post_atts, false, false ); + + // Run the update. + update_post_meta( $post_id, 'update_discourse_topic', 1 ); + $post = get_post( $post_id ); + $result = $this->publish->sync_to_discourse_without_lock( $post_id, $post->title, $post->post_content ); + + // Ensure the right post meta still exists. + $this->assertEquals( get_post_meta( $post_id, 'discourse_post_id', true ), $discourse_post_id ); + $this->assertEquals( get_post_meta( $post_id, 'discourse_topic_id', true ), $discourse_topic_id ); + $this->assertEquals( get_post_meta( $post_id, 'discourse_permalink', true ), $discourse_permalink ); + $this->assertEquals( get_post_meta( $post_id, 'publish_post_category', true ), $discourse_category ); + $this->assertEquals( get_post_meta( $post_id, 'wpdc_publishing_response', true ), 'success' ); + + // Cleanup. + wp_delete_post( $post_id ); + } + + /** + * Successful remote_post request returns original response. + */ + public function test_remote_post_success() { + $success_response = $this->build_response( 'success' ); + $this->mock_remote_post( $success_response ); + $response = $this->publish->remote_post( ...self::$remote_post_params ); + $this->assertEquals( $response, $success_response ); + } + + /** + * Forbidden remote_post request returns standardised WP_Error and creates correct log. + */ + public function test_remote_post_forbidden() { + $raw_response = $this->build_response( 'forbidden' ); + $this->mock_remote_post( $raw_response ); + + $response = $this->publish->remote_post( ...self::$remote_post_params ); + $this->assertEquals( $response, $this->build_post_error() ); + + $log = $this->get_last_log(); + $this->assertRegExp( '/publish.ERROR: create_post.post_error/', $log ); + $this->assertRegExp( '/"http_code":' . $raw_response['response']['code'] . '/', $log ); + } + + /** + * Unprocessable remote_post request returns standardised WP_Error and creates correct log. + */ + public function test_remote_post_unprocessable() { + $raw_response = $this->build_response( 'unprocessable', 'title' ); + $this->mock_remote_post( $raw_response ); + + $response = $this->publish->remote_post( ...self::$remote_post_params ); + $this->assertEquals( $response, $this->build_post_error() ); + + $log = $this->get_last_log(); + $this->assertRegExp( '/publish.ERROR: create_post.post_error/', $log ); + $this->assertRegExp( '/"http_code":' . $raw_response['response']['code'] . '/', $log ); + } + + /** + * Forbidden remote_post request returns standardised WP_Error and creates correct log. + */ + public function test_remote_post_failed_to_connect() { + $this->mock_remote_post( + new WP_Error( + 'http_request_failed', + 'cURL error 7: Failed to connect to localhost port 3000: Connection refused' + ) + ); + + $response = $this->publish->remote_post( ...self::$remote_post_params ); + $this->assertEquals( $response, $this->build_post_error() ); + + $log = $this->get_last_log(); + $this->assertRegExp( '/publish.ERROR: create_post.post_error/', $log ); + } + + /** + * Mock remote post response. + * + * @param object $response Remote post response object. + * @param object $second_request Second request response of second request in tested method. + */ + protected function mock_remote_post( $response, $second_request = null ) { + add_filter( + 'pre_http_request', + function( $prempt, $args, $url ) use ( $response, $second_request ) { + if ( ! empty( $second_request ) && ( $second_request['body'] === $args['body'] ) ) { + return $second_request['response']; + } else { + return $response; + } + }, + 10, + 3 + ); + } + + /** + * Mock remote post success. + * + * @param string $type Type of response. + * @param object $second_request Second request response of second request in tested method. + */ + protected function mock_remote_post_success( $type, $second_request = null ) { + $raw_body = $this->response_body_json( $type ); + $response = $this->build_response( 'success' ); + $response['body'] = $raw_body; + $this->mock_remote_post( $response, $second_request ); + return json_decode( $raw_body ); + } + + /** + * Build error returned by discourse-publish when post request fails. + */ + protected function build_post_error() { + $message = __( 'An error occurred when communicating with Discourse', 'wp-discourse' ); + return new WP_Error( 'discourse_publishing_response_error', $message ); + } + + /** + * Build error returned by discourse-publish when response body is invalid. + */ + protected function build_body_error() { + $message = __( 'An invalid response was returned from Discourse', 'wp-discourse' ); + return new WP_Error( 'discourse_publishing_response_error', $message ); + } + + /** + * Build an error notice returned by discourse-publish when post queued or topic deleted. + */ + protected function build_notice( $message ) { + return new WP_Error( 'discourse_publishing_response_notice', $message ); + } + + /** + * Get last line in latest log file. + */ + protected function get_last_log() { + $manager = new FileManager(); + $log_files = glob( $manager->logs_dir . '/*.log' ); + $log_file = $log_files[0]; + return shell_exec( "tail -n 1 $log_file" ); + } + + /** + * Clear all logs. + */ + protected function clear_logs() { + $manager = new FileManager(); + $log_files = glob( $manager->logs_dir . '/*.log' ); + + foreach ( $log_files as $file ) { + if ( is_file( $file ) ) { + unlink( $file ); + } + } + } + + /** + * Get fixture with response body. + * + * @param string $file Name of response body file. + */ + protected function response_body_file( $file ) { + return file_get_contents( __DIR__ . "/../fixtures/response_body/$file.json" ); + } + + /** + * Build JSON of response body. + * + * @param string $type Type of response. + * @param string $sub_type Sub-type of response. + * @param string $action_type Action type of test. + */ + protected function response_body_json( $type, $sub_type = null, $action_type = 'create_post' ) { + if ( in_array( $type, array( 'post_create', 'post_update' ), true ) ) { + return $this->response_body_file( $type ); + } + if ( 'unprocessable' === $type ) { + $messages = array( + 'title' => 'Title seems unclear, most of the words contain the same letters over and over?', + 'embed' => 'Embed url has already been taken', + ); + $message_type = $sub_type; + } else { + $messages = array( + 'invalid_parameters' => "You supplied invalid parameters to the request: $sub_type", + 'forbidden' => 'You are not permitted to view the requested resource. The API username or key is invalid.', + ); + $message_type = $type; + } + return wp_json_encode( + array( + 'action' => $action_type, + 'errors' => array( $messages[ $message_type ] ), + 'error_type' => $type, + ) + ); + } + + /** + * Build remote post response. + * + * @param string $type Type of response. + * @param string $sub_type Sub-type of response. + */ + protected function build_response( $type, $sub_type = null ) { + $codes = array( + 'success' => 200, + 'invalid_parameters' => 400, + 'forbidden' => 403, + 'unprocessable' => 422, + ); + $messages = array( + 'success' => 'OK', + 'invalid_parameters' => 'Bad Request', + 'forbidden' => 'Forbidden', + 'unprocessable' => 'Unprocessable Entity', + ); + if ( in_array( $type, array( 'invalid_parameters', 'unprocessable' ), true ) ) { + $body = $this->response_body_json( $type, $sub_type ); + } else { + $body = array( + 'success' => '{}', + 'forbidden' => 'You are not permitted to view the requested resource. The API username or key is invalid.', + )[ $type ]; + } + return array( + 'headers' => array(), + 'body' => $body, + 'response' => array( + 'code' => $codes[ $type ], + 'message' => $messages[ $type ], + ), + ); + } + + /** + * Initialize static variables used by test class. + */ + public static function initialize_static_variables() { + self::$discourse_url = 'http://meta.discourse.org'; + + self::$remote_post_params = array( + self::$discourse_url, + array( + 'timeout' => 30, + 'method' => 'POST', + 'headers' => array( + 'Api-Key' => '1234', + 'Api-Username' => 'angus', + ), + 'body' => http_build_query( + array( + 'embed_url' => 'https://wordpress.org/post.php', + 'featured_link' => null, + 'title' => 'New Topic Title', + 'raw' => 'Post content', + 'category' => 3, + 'skip_validations' => 'true', + 'auto_track' => 'false', + 'visible' => 'true', + ) + ), + ), + 'create_post', + 1, + ); + + self::$post_atts = array( + 'post_author' => 0, + 'post_content' => 'This is a new post', + 'post_title' => 'This is the post title', + 'meta_input' => array( + 'wpdc_auto_publish_overridden' => 0, + 'publish_to_discourse' => 1, + 'publish_post_category' => 1, + ), + 'post_status' => 'publish', + ); + + self::$plugin_options = array( + 'url' => self::$discourse_url, + 'api-key' => '1235567', + 'publish-username' => 'angus', + 'allowed_post_types' => array( 'post' ), + ); + } +} + diff --git a/tests/phpunit/test-file-handler.php b/tests/phpunit/test-file-handler.php new file mode 100644 index 0000000..81a5f3d --- /dev/null +++ b/tests/phpunit/test-file-handler.php @@ -0,0 +1,233 @@ +assertInstanceOf( FileHandler::class, $file_handler ); + } + + /** + * It is enabled if the File Manager is ready + */ + public function test_enabled() { + $file_handler = new FileHandler( new FileManager() ); + $this->assertTrue( $file_handler->enabled() ); + } + + /** + * It is not enabled if the File Manager is not ready + */ + public function test_not_enabled() { + $file_manager_double = \Mockery::mock( FileManager::class )->makePartial(); + $file_manager_double->shouldReceive( 'ready' )->andReturn( false ); + $file_handler = new FileHandler( $file_manager_double ); + $this->assertFalse( $file_handler->enabled() ); + } + + /** + * It creates log files to write logs to + */ + public function test_log_file_create() { + $file_handler = new FileHandler( new FileManager() ); + $logger = Logger::create( 'test', $file_handler ); + $logger->info( 'New Log' ); + + $manager = new FileManager(); + $log_files = glob( $manager->logs_dir . '/*.log' ); + $this->assertCount( 1, $log_files ); + + $log_file = $log_files[0]; + $this->assertFileExists( $log_file ); + } + + /** + * It writes logs to a file it has created + */ + public function test_log_file_write() { + $file_handler = new FileHandler( new FileManager() ); + $logger = Logger::create( 'test', $file_handler ); + $logger->info( 'New Log' ); + + $manager = new FileManager(); + $log_files = glob( $manager->logs_dir . '/*.log' ); + $this->assertCount( 1, $log_files ); + + $log_file = $log_files[0]; + $last_entry = shell_exec( "tail -n 1 $log_file" ); + $this->assertRegExp( '/New Log/', $last_entry ); + } + + /** + * It writes multiple logs to the same file + */ + public function test_log_file_multiple() { + $file_manager = new FileManager(); + $file_handler = new FileHandler( $file_manager ); + + $logger = Logger::create( 'test', $file_handler ); + for ( $i = 1; $i <= 10; $i++ ) { + $logger->warning( "Multi Log $i" ); + } + + $log_files = glob( $file_manager->logs_dir . '/*.log' ); + $this->assertCount( 1, $log_files ); + + $matching_line_count = 0; + $handle = fopen( $log_files[0], 'r' ); + while ( ! feof( $handle ) ) { + $line = fgets( $handle ); + + if ( strpos( $line, 'Multi Log' ) !== false ) { + $matching_line_count++; + } + } + fclose( $handle ); + + $this->assertEquals( 10, $matching_line_count ); + } + + /** + * It rotates log files every day. + */ + public function test_log_file_date_rotation() { + $file_manager = new FileManager(); + $file_handler = new FileHandler( $file_manager ); + + $logger = Logger::create( 'test', $file_handler ); + $logger->warning( "Today's Log" ); + + $todays_datetime = new \DateTimeImmutable( 'now' ); + $tomorrows_datetime = new \DateTimeImmutable( 'tomorrow' ); + + // Make file handler think it's tomorrow. + $tomorrows_file_handler = new FileHandler( $file_manager, null, null, $tomorrows_datetime ); + + // Make logger think it's tomorrow. + $tomorrows_logger = Logger::create( 'test', $tomorrows_file_handler ); + $tomorrows_logger->pushProcessor( + function ( $record ) use ( $tomorrows_datetime ) { + $record['datetime'] = $tomorrows_datetime; + return $record; + } + ); + + $tomorrows_logger->warning( "Tomorrow's Log" ); + + $tomorrows_date = $tomorrows_datetime->format( FileHandler::DATE_FORMAT ); + $todays_date = $todays_datetime->format( FileHandler::DATE_FORMAT ); + + $files = $file_handler->list_files(); + $this->assertRegExp( '/' . $tomorrows_date . '/', $files[0] ); + $this->assertRegExp( '/' . $todays_date . '/', $files[1] ); + } + + /** + * It rotates logs when size limit is reached. + */ + public function test_log_file_size_limit_rotation() { + $file_manager = new FileManager(); + $file_handler = new FileHandler( $file_manager ); + + $logger = Logger::create( 'high-volume', $file_handler ); + $logger->warning( 'High volume log' ); + + // It's inefficient to create a large file via individual logs, so we're + // stuffing the log file with filler data so it's almost up to the limit + // then taking it over the limit with normal logs. + + $handle = fopen( $file_handler->getUrl(), 'wb' ); + $limit = $file_handler->get_file_size_limit(); + + while ( fstat( $handle )['size'] < ( $limit - ( 1024 * 30 * 1 ) ) ) { + fwrite( $handle, str_repeat( "filler line taking up 30 bts\n", 1024 ) ); + } + + for ( $i = 1; $i <= 300; $i++ ) { + $logger->warning( 'High volume log' ); + } + + $this->assertLessThanOrEqual( $limit, fstat( $handle )['size'] ); + $this->assertCount( 2, $file_handler->list_files() ); + } + + /** + * It increments file numbers on each rotation. + */ + public function test_log_file_number() { + $file_manager = new FileManager(); + + // Size limit to restrict each file to a single line. + $low_limit_file_handler = new FileHandler( $file_manager, 200 ); + + $logger = Logger::create( 'one-log-per-file', $low_limit_file_handler ); + + for ( $i = 1; $i <= 7; $i++ ) { + $logger->warning( 'A line long enough to take it over 100 bytes with log metadata' ); + } + + $this->assertCount( 7, $low_limit_file_handler->list_files() ); + $this->assertEquals( 7, $low_limit_file_handler->current_file_number() ); + } + + /** + * It respects the max_files limit. + */ + public function test_log_max_files() { + $file_manager = new FileManager(); + + // Size limit to restrict each file to a single line. + $handler = new FileHandler( $file_manager, 200 ); + $logger = Logger::create( 'one-log-per-file', $handler ); + + for ( $i = 1; $i <= 15; $i++ ) { + $logger->warning( 'A line long enough to take it over 100 bytes with log metadata' ); + } + + $files = $handler->list_files(); + + $this->assertCount( 10, $files ); + + // Ensure the right files have been removed. + $this->assertEquals( 15, $handler->get_number_from_url( $files[0] ) ); + $this->assertEquals( 6, $handler->get_number_from_url( end( $files ) ) ); + } + + /** + * Teardown class. + */ + public function tearDown() { + $this->clear_logs(); + \Mockery::close(); + } + + /** + * Clear logs. + */ + private function clear_logs() { + $manager = new FileManager(); + $log_files = glob( $manager->logs_dir . '/*.log' ); + + foreach ( $log_files as $file ) { + if ( is_file( $file ) ) { + unlink( $file ); + } + } + } +} diff --git a/tests/phpunit/test-file-manager.php b/tests/phpunit/test-file-manager.php new file mode 100644 index 0000000..6574d99 --- /dev/null +++ b/tests/phpunit/test-file-manager.php @@ -0,0 +1,143 @@ +recursive_rmdir( $file_manager->upload_dir ); + $this->assertDirectoryNotExists( $file_manager->upload_dir ); + + $file_manager->validate(); + + $this->assertDirectoryExists( $file_manager->upload_dir ); + $this->assertFileExists( $file_manager->upload_dir . '/.htaccess' ); + } + + /** + * Validation creates discourse logs folder and .htaccess file if they don't exist. + */ + public function test_validation_logs_creation() { + $file_manager = new FileManager(); + + $this->recursive_rmdir( $file_manager->logs_dir ); + $this->assertDirectoryNotExists( $file_manager->logs_dir ); + + $file_manager->validate(); + + $this->assertDirectoryExists( $file_manager->logs_dir ); + $this->assertFileExists( $file_manager->logs_dir . '/.htaccess' ); + } + + /** + * It is ready if validation passes. + */ + public function test_validation_ready() { + $file_manager = new FileManager(); + $this->assertTrue( $file_manager->validate() ); + $this->assertTrue( $file_manager->ready() ); + } + + /** + * It is not ready if validation is not run + */ + public function test_validation_not_ready() { + $file_manager = new FileManager(); + $this->assertFalse( $file_manager->ready() ); + } + + /** + * Validation will not pass if wp uploads directory is not writable + */ + public function test_validation_when_wp_uploads_not_writable() { + $file_manager = new FileManager(); + + chmod( wp_upload_dir()['basedir'], 0444 ); + + $this->assertFalse( $file_manager->validate() ); + $this->assertFalse( $file_manager->ready() ); + } + + /** + * Validation will not pass if all necessary folders and files are not present and writable. + */ + public function test_validation_when_folders_partially_restricted() { + $file_manager = new FileManager(); + + $this->assertTrue( $file_manager->validate() ); + $this->assertTrue( $file_manager->ready() ); + + chmod( $file_manager->logs_dir, 0444 ); + + $this->assertFalse( $file_manager->validate() ); + $this->assertFalse( $file_manager->ready() ); + } + + /** + * Reset directory permissions. + */ + protected function reset_permissions() { + $file_manager = new FileManager(); + + chmod( wp_upload_dir()['basedir'], 0744 ); + + if ( is_dir( $file_manager->upload_dir ) ) { + chmod( $file_manager->upload_dir, 0744 ); + } + + if ( is_dir( $file_manager->logs_dir ) ) { + chmod( $file_manager->logs_dir, 0744 ); + } + } + + /** + * Recursively remove directory. + * + * @param string $dir Path of directory to remove. + */ + protected function recursive_rmdir( $dir ) { + if ( is_dir( $dir ) ) { + $objects = scandir( $dir ); + + foreach ( $objects as $object ) { + if ( '.' !== $object && '..' !== $object ) { + if ( is_dir( $dir . DIRECTORY_SEPARATOR . $object ) ) { + $this->recursive_rmdir( $dir . DIRECTORY_SEPARATOR . $object ); + } else { + unlink( $dir . DIRECTORY_SEPARATOR . $object ); + } + } + } + + rmdir( $dir ); + } + } +} diff --git a/tests/phpunit/test-log-viewer.php b/tests/phpunit/test-log-viewer.php new file mode 100644 index 0000000..d7a9263 --- /dev/null +++ b/tests/phpunit/test-log-viewer.php @@ -0,0 +1,65 @@ +makePartial(); + $handler_double->shouldReceive( 'enabled' )->andReturn( false ); + + $viewer = new LogViewer(); + $viewer->setup_log_viewer( $handler_double ); + + $this->assertFalse( $viewer->is_enabled() ); + + ob_start(); + $viewer->log_viewer_markup(); + $markup = ob_get_contents(); + ob_end_clean(); + + $this->assertXmlStringEqualsXmlString( $markup, '

Logs are disabled.

' ); + } + + /** + * It should retrieve logs and map them to date, number and file + */ + public function test_log_retrieval() { + $handler = new FileHandler( new FileManager() ); + $logger = Logger::create( 'test', $handler ); + $logger->info( 'New Log' ); + + $viewer = new LogViewer(); + $viewer->setup_log_viewer(); + + $date = $handler->get_date(); + $number = $handler->current_file_number(); + $file = $handler->current_file_url(); + + $this->assertArraySubset( + array( + "$date-$number" => array( + 'date' => $date, + 'number' => $number, + 'file' => $file, + ), + ), + $viewer->get_logs() + ); + } +} diff --git a/tests/phpunit/test-logger.php b/tests/phpunit/test-logger.php new file mode 100644 index 0000000..7eb8eab --- /dev/null +++ b/tests/phpunit/test-logger.php @@ -0,0 +1,67 @@ +assertInstanceOf( Logger::class, $logger ); + + return $logger; + } + + /** + * It attaches FileHandler as the default handler + * + * @param object $logger Instance of \WPDiscourse\Logs\Logger. + * @depends test_create + */ + public function test_create_handler( $logger ) { + $handlers = $logger->getHandlers(); + $this->assertCount( 1, $handlers ); + + $file_handler = reset( $handlers ); + $this->assertInstanceOf( FileHandler::class, $file_handler ); + + return $file_handler; + } + + /** + * It attaches LineFormatter as the default formatter + * + * @param object $file_handler Instance of \WPDiscourse\Logs\FileHandler. + * @depends test_create_handler + */ + public function test_create_handler_formatter( $file_handler ) { + $this->assertInstanceOf( LineFormatter::class, $file_handler->getFormatter() ); + } + + /** + * It attaches NullHandler if FileHandler is not enabled + */ + public function test_create_file_handler_not_enabled() { + $file_handler_double = \Mockery::mock( FileHandler::class )->makePartial(); + $file_handler_double->shouldReceive( 'enabled' )->andReturn( false ); + + $logger = Logger::create( 'test', $file_handler_double ); + $handlers = $logger->getHandlers(); + + $this->assertCount( 1, $handlers ); + $this->assertContainsOnlyInstancesOf( NullHandler::class, $handlers ); + } +} diff --git a/vendor_namespaced/monolog/monolog/LICENSE b/vendor_namespaced/monolog/monolog/LICENSE new file mode 100644 index 0000000..1647321 --- /dev/null +++ b/vendor_namespaced/monolog/monolog/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2011-2016 Jordi Boggiano + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor_namespaced/monolog/monolog/src/Monolog/Formatter/FormatterInterface.php b/vendor_namespaced/monolog/monolog/src/Monolog/Formatter/FormatterInterface.php new file mode 100644 index 0000000..734fc61 --- /dev/null +++ b/vendor_namespaced/monolog/monolog/src/Monolog/Formatter/FormatterInterface.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace WPDiscourse\Monolog\Formatter; + +/** + * Interface for formatters + * + * @author Jordi Boggiano + */ +interface FormatterInterface +{ + /** + * Formats a log record. + * + * @param array $record A record to format + * @return mixed The formatted record + */ + public function format(array $record); + /** + * Formats a set of log records. + * + * @param array $records A set of records to format + * @return mixed The formatted set of records + */ + public function formatBatch(array $records); +} diff --git a/vendor_namespaced/monolog/monolog/src/Monolog/Formatter/LineFormatter.php b/vendor_namespaced/monolog/monolog/src/Monolog/Formatter/LineFormatter.php new file mode 100644 index 0000000..e742c35 --- /dev/null +++ b/vendor_namespaced/monolog/monolog/src/Monolog/Formatter/LineFormatter.php @@ -0,0 +1,150 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace WPDiscourse\Monolog\Formatter; + +use WPDiscourse\Monolog\Utils; +/** + * Formats incoming records into a one-line string + * + * This is especially useful for logging to files + * + * @author Jordi Boggiano + * @author Christophe Coevoet + */ +class LineFormatter extends \WPDiscourse\Monolog\Formatter\NormalizerFormatter +{ + const SIMPLE_FORMAT = "[%datetime%] %channel%.%level_name%: %message% %context% %extra%\n"; + protected $format; + protected $allowInlineLineBreaks; + protected $ignoreEmptyContextAndExtra; + protected $includeStacktraces; + /** + * @param string $format The format of the message + * @param string $dateFormat The format of the timestamp: one supported by DateTime::format + * @param bool $allowInlineLineBreaks Whether to allow inline line breaks in log entries + * @param bool $ignoreEmptyContextAndExtra + */ + public function __construct($format = null, $dateFormat = null, $allowInlineLineBreaks = \false, $ignoreEmptyContextAndExtra = \false) + { + $this->format = $format ?: static::SIMPLE_FORMAT; + $this->allowInlineLineBreaks = $allowInlineLineBreaks; + $this->ignoreEmptyContextAndExtra = $ignoreEmptyContextAndExtra; + parent::__construct($dateFormat); + } + public function includeStacktraces($include = \true) + { + $this->includeStacktraces = $include; + if ($this->includeStacktraces) { + $this->allowInlineLineBreaks = \true; + } + } + public function allowInlineLineBreaks($allow = \true) + { + $this->allowInlineLineBreaks = $allow; + } + public function ignoreEmptyContextAndExtra($ignore = \true) + { + $this->ignoreEmptyContextAndExtra = $ignore; + } + /** + * {@inheritdoc} + */ + public function format(array $record) + { + $vars = parent::format($record); + $output = $this->format; + foreach ($vars['extra'] as $var => $val) { + if (\false !== \strpos($output, '%extra.' . $var . '%')) { + $output = \str_replace('%extra.' . $var . '%', $this->stringify($val), $output); + unset($vars['extra'][$var]); + } + } + foreach ($vars['context'] as $var => $val) { + if (\false !== \strpos($output, '%context.' . $var . '%')) { + $output = \str_replace('%context.' . $var . '%', $this->stringify($val), $output); + unset($vars['context'][$var]); + } + } + if ($this->ignoreEmptyContextAndExtra) { + if (empty($vars['context'])) { + unset($vars['context']); + $output = \str_replace('%context%', '', $output); + } + if (empty($vars['extra'])) { + unset($vars['extra']); + $output = \str_replace('%extra%', '', $output); + } + } + foreach ($vars as $var => $val) { + if (\false !== \strpos($output, '%' . $var . '%')) { + $output = \str_replace('%' . $var . '%', $this->stringify($val), $output); + } + } + // remove leftover %extra.xxx% and %context.xxx% if any + if (\false !== \strpos($output, '%')) { + $output = \preg_replace('/%(?:extra|context)\\..+?%/', '', $output); + } + return $output; + } + public function formatBatch(array $records) + { + $message = ''; + foreach ($records as $record) { + $message .= $this->format($record); + } + return $message; + } + public function stringify($value) + { + return $this->replaceNewlines($this->convertToString($value)); + } + protected function normalizeException($e) + { + // TODO 2.0 only check for Throwable + if (!$e instanceof \Exception && !$e instanceof \Throwable) { + throw new \InvalidArgumentException('Exception/Throwable expected, got ' . \gettype($e) . ' / ' . \WPDiscourse\Monolog\Utils::getClass($e)); + } + $previousText = ''; + if ($previous = $e->getPrevious()) { + do { + $previousText .= ', ' . \WPDiscourse\Monolog\Utils::getClass($previous) . '(code: ' . $previous->getCode() . '): ' . $previous->getMessage() . ' at ' . $previous->getFile() . ':' . $previous->getLine(); + } while ($previous = $previous->getPrevious()); + } + $str = '[object] (' . \WPDiscourse\Monolog\Utils::getClass($e) . '(code: ' . $e->getCode() . '): ' . $e->getMessage() . ' at ' . $e->getFile() . ':' . $e->getLine() . $previousText . ')'; + if ($this->includeStacktraces) { + $str .= "\n[stacktrace]\n" . $e->getTraceAsString() . "\n"; + } + return $str; + } + protected function convertToString($data) + { + if (null === $data || \is_bool($data)) { + return \var_export($data, \true); + } + if (\is_scalar($data)) { + return (string) $data; + } + if (\version_compare(\PHP_VERSION, '5.4.0', '>=')) { + return $this->toJson($data, \true); + } + return \str_replace('\\/', '/', $this->toJson($data, \true)); + } + protected function replaceNewlines($str) + { + if ($this->allowInlineLineBreaks) { + if (0 === \strpos($str, '{')) { + return \str_replace(array('\\r', '\\n'), array("\r", "\n"), $str); + } + return $str; + } + return \str_replace(array("\r\n", "\r", "\n"), ' ', $str); + } +} diff --git a/vendor_namespaced/monolog/monolog/src/Monolog/Formatter/NormalizerFormatter.php b/vendor_namespaced/monolog/monolog/src/Monolog/Formatter/NormalizerFormatter.php new file mode 100644 index 0000000..ed173ac --- /dev/null +++ b/vendor_namespaced/monolog/monolog/src/Monolog/Formatter/NormalizerFormatter.php @@ -0,0 +1,146 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace WPDiscourse\Monolog\Formatter; + +use Exception; +use WPDiscourse\Monolog\Utils; +/** + * Normalizes incoming records to remove objects/resources so it's easier to dump to various targets + * + * @author Jordi Boggiano + */ +class NormalizerFormatter implements \WPDiscourse\Monolog\Formatter\FormatterInterface +{ + const SIMPLE_DATE = "Y-m-d H:i:s"; + protected $dateFormat; + /** + * @param string $dateFormat The format of the timestamp: one supported by DateTime::format + */ + public function __construct($dateFormat = null) + { + $this->dateFormat = $dateFormat ?: static::SIMPLE_DATE; + if (!\function_exists('json_encode')) { + throw new \RuntimeException('PHP\'s json extension is required to use Monolog\'s NormalizerFormatter'); + } + } + /** + * {@inheritdoc} + */ + public function format(array $record) + { + return $this->normalize($record); + } + /** + * {@inheritdoc} + */ + public function formatBatch(array $records) + { + foreach ($records as $key => $record) { + $records[$key] = $this->format($record); + } + return $records; + } + protected function normalize($data, $depth = 0) + { + if ($depth > 9) { + return 'Over 9 levels deep, aborting normalization'; + } + if (null === $data || \is_scalar($data)) { + if (\is_float($data)) { + if (\is_infinite($data)) { + return ($data > 0 ? '' : '-') . 'INF'; + } + if (\is_nan($data)) { + return 'NaN'; + } + } + return $data; + } + if (\is_array($data)) { + $normalized = array(); + $count = 1; + foreach ($data as $key => $value) { + if ($count++ > 1000) { + $normalized['...'] = 'Over 1000 items (' . \count($data) . ' total), aborting normalization'; + break; + } + $normalized[$key] = $this->normalize($value, $depth + 1); + } + return $normalized; + } + if ($data instanceof \DateTime) { + return $data->format($this->dateFormat); + } + if (\is_object($data)) { + // TODO 2.0 only check for Throwable + if ($data instanceof \Exception || \PHP_VERSION_ID > 70000 && $data instanceof \Throwable) { + return $this->normalizeException($data); + } + // non-serializable objects that implement __toString stringified + if (\method_exists($data, '__toString') && !$data instanceof \JsonSerializable) { + $value = $data->__toString(); + } else { + // the rest is json-serialized in some way + $value = $this->toJson($data, \true); + } + return \sprintf("[object] (%s: %s)", \WPDiscourse\Monolog\Utils::getClass($data), $value); + } + if (\is_resource($data)) { + return \sprintf('[resource] (%s)', \get_resource_type($data)); + } + return '[unknown(' . \gettype($data) . ')]'; + } + protected function normalizeException($e) + { + // TODO 2.0 only check for Throwable + if (!$e instanceof \Exception && !$e instanceof \Throwable) { + throw new \InvalidArgumentException('Exception/Throwable expected, got ' . \gettype($e) . ' / ' . \WPDiscourse\Monolog\Utils::getClass($e)); + } + $data = array('class' => \WPDiscourse\Monolog\Utils::getClass($e), 'message' => $e->getMessage(), 'code' => (int) $e->getCode(), 'file' => $e->getFile() . ':' . $e->getLine()); + if ($e instanceof \SoapFault) { + if (isset($e->faultcode)) { + $data['faultcode'] = $e->faultcode; + } + if (isset($e->faultactor)) { + $data['faultactor'] = $e->faultactor; + } + if (isset($e->detail)) { + if (\is_string($e->detail)) { + $data['detail'] = $e->detail; + } elseif (\is_object($e->detail) || \is_array($e->detail)) { + $data['detail'] = $this->toJson($e->detail, \true); + } + } + } + $trace = $e->getTrace(); + foreach ($trace as $frame) { + if (isset($frame['file'])) { + $data['trace'][] = $frame['file'] . ':' . $frame['line']; + } + } + if ($previous = $e->getPrevious()) { + $data['previous'] = $this->normalizeException($previous); + } + return $data; + } + /** + * Return the JSON representation of a value + * + * @param mixed $data + * @param bool $ignoreErrors + * @throws \RuntimeException if encoding fails and errors are not ignored + * @return string + */ + protected function toJson($data, $ignoreErrors = \false) + { + return \WPDiscourse\Monolog\Utils::jsonEncode($data, null, $ignoreErrors); + } +} diff --git a/vendor_namespaced/monolog/monolog/src/Monolog/Handler/AbstractHandler.php b/vendor_namespaced/monolog/monolog/src/Monolog/Handler/AbstractHandler.php new file mode 100644 index 0000000..8f08e99 --- /dev/null +++ b/vendor_namespaced/monolog/monolog/src/Monolog/Handler/AbstractHandler.php @@ -0,0 +1,172 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace WPDiscourse\Monolog\Handler; + +use WPDiscourse\Monolog\Formatter\FormatterInterface; +use WPDiscourse\Monolog\Formatter\LineFormatter; +use WPDiscourse\Monolog\Logger; +use WPDiscourse\Monolog\ResettableInterface; +/** + * Base Handler class providing the Handler structure + * + * @author Jordi Boggiano + */ +abstract class AbstractHandler implements \WPDiscourse\Monolog\Handler\HandlerInterface, \WPDiscourse\Monolog\ResettableInterface +{ + protected $level = \WPDiscourse\Monolog\Logger::DEBUG; + protected $bubble = \true; + /** + * @var FormatterInterface + */ + protected $formatter; + protected $processors = array(); + /** + * @param int|string $level The minimum logging level at which this handler will be triggered + * @param bool $bubble Whether the messages that are handled can bubble up the stack or not + */ + public function __construct($level = \WPDiscourse\Monolog\Logger::DEBUG, $bubble = \true) + { + $this->setLevel($level); + $this->bubble = $bubble; + } + /** + * {@inheritdoc} + */ + public function isHandling(array $record) + { + return $record['level'] >= $this->level; + } + /** + * {@inheritdoc} + */ + public function handleBatch(array $records) + { + foreach ($records as $record) { + $this->handle($record); + } + } + /** + * Closes the handler. + * + * This will be called automatically when the object is destroyed + */ + public function close() + { + } + /** + * {@inheritdoc} + */ + public function pushProcessor($callback) + { + if (!\is_callable($callback)) { + throw new \InvalidArgumentException('Processors must be valid callables (callback or object with an __invoke method), ' . \var_export($callback, \true) . ' given'); + } + \array_unshift($this->processors, $callback); + return $this; + } + /** + * {@inheritdoc} + */ + public function popProcessor() + { + if (!$this->processors) { + throw new \LogicException('You tried to pop from an empty processor stack.'); + } + return \array_shift($this->processors); + } + /** + * {@inheritdoc} + */ + public function setFormatter(\WPDiscourse\Monolog\Formatter\FormatterInterface $formatter) + { + $this->formatter = $formatter; + return $this; + } + /** + * {@inheritdoc} + */ + public function getFormatter() + { + if (!$this->formatter) { + $this->formatter = $this->getDefaultFormatter(); + } + return $this->formatter; + } + /** + * Sets minimum logging level at which this handler will be triggered. + * + * @param int|string $level Level or level name + * @return self + */ + public function setLevel($level) + { + $this->level = \WPDiscourse\Monolog\Logger::toMonologLevel($level); + return $this; + } + /** + * Gets minimum logging level at which this handler will be triggered. + * + * @return int + */ + public function getLevel() + { + return $this->level; + } + /** + * Sets the bubbling behavior. + * + * @param bool $bubble true means that this handler allows bubbling. + * false means that bubbling is not permitted. + * @return self + */ + public function setBubble($bubble) + { + $this->bubble = $bubble; + return $this; + } + /** + * Gets the bubbling behavior. + * + * @return bool true means that this handler allows bubbling. + * false means that bubbling is not permitted. + */ + public function getBubble() + { + return $this->bubble; + } + public function __destruct() + { + try { + $this->close(); + } catch (\Exception $e) { + // do nothing + } catch (\Throwable $e) { + // do nothing + } + } + public function reset() + { + foreach ($this->processors as $processor) { + if ($processor instanceof \WPDiscourse\Monolog\ResettableInterface) { + $processor->reset(); + } + } + } + /** + * Gets the default formatter. + * + * @return FormatterInterface + */ + protected function getDefaultFormatter() + { + return new \WPDiscourse\Monolog\Formatter\LineFormatter(); + } +} diff --git a/vendor_namespaced/monolog/monolog/src/Monolog/Handler/AbstractProcessingHandler.php b/vendor_namespaced/monolog/monolog/src/Monolog/Handler/AbstractProcessingHandler.php new file mode 100644 index 0000000..737bd6b --- /dev/null +++ b/vendor_namespaced/monolog/monolog/src/Monolog/Handler/AbstractProcessingHandler.php @@ -0,0 +1,59 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace WPDiscourse\Monolog\Handler; + +use WPDiscourse\Monolog\ResettableInterface; +/** + * Base Handler class providing the Handler structure + * + * Classes extending it should (in most cases) only implement write($record) + * + * @author Jordi Boggiano + * @author Christophe Coevoet + */ +abstract class AbstractProcessingHandler extends \WPDiscourse\Monolog\Handler\AbstractHandler +{ + /** + * {@inheritdoc} + */ + public function handle(array $record) + { + if (!$this->isHandling($record)) { + return \false; + } + $record = $this->processRecord($record); + $record['formatted'] = $this->getFormatter()->format($record); + $this->write($record); + return \false === $this->bubble; + } + /** + * Writes the record down to the log of the implementing handler + * + * @param array $record + * @return void + */ + protected abstract function write(array $record); + /** + * Processes a record. + * + * @param array $record + * @return array + */ + protected function processRecord(array $record) + { + if ($this->processors) { + foreach ($this->processors as $processor) { + $record = \call_user_func($processor, $record); + } + } + return $record; + } +} diff --git a/vendor_namespaced/monolog/monolog/src/Monolog/Handler/HandlerInterface.php b/vendor_namespaced/monolog/monolog/src/Monolog/Handler/HandlerInterface.php new file mode 100644 index 0000000..c5723ce --- /dev/null +++ b/vendor_namespaced/monolog/monolog/src/Monolog/Handler/HandlerInterface.php @@ -0,0 +1,82 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace WPDiscourse\Monolog\Handler; + +use WPDiscourse\Monolog\Formatter\FormatterInterface; +/** + * Interface that all Monolog Handlers must implement + * + * @author Jordi Boggiano + */ +interface HandlerInterface +{ + /** + * Checks whether the given record will be handled by this handler. + * + * This is mostly done for performance reasons, to avoid calling processors for nothing. + * + * Handlers should still check the record levels within handle(), returning false in isHandling() + * is no guarantee that handle() will not be called, and isHandling() might not be called + * for a given record. + * + * @param array $record Partial log record containing only a level key + * + * @return bool + */ + public function isHandling(array $record); + /** + * Handles a record. + * + * All records may be passed to this method, and the handler should discard + * those that it does not want to handle. + * + * The return value of this function controls the bubbling process of the handler stack. + * Unless the bubbling is interrupted (by returning true), the Logger class will keep on + * calling further handlers in the stack with a given log record. + * + * @param array $record The record to handle + * @return bool true means that this handler handled the record, and that bubbling is not permitted. + * false means the record was either not processed or that this handler allows bubbling. + */ + public function handle(array $record); + /** + * Handles a set of records at once. + * + * @param array $records The records to handle (an array of record arrays) + */ + public function handleBatch(array $records); + /** + * Adds a processor in the stack. + * + * @param callable $callback + * @return self + */ + public function pushProcessor($callback); + /** + * Removes the processor on top of the stack and returns it. + * + * @return callable + */ + public function popProcessor(); + /** + * Sets the formatter. + * + * @param FormatterInterface $formatter + * @return self + */ + public function setFormatter(\WPDiscourse\Monolog\Formatter\FormatterInterface $formatter); + /** + * Gets the formatter. + * + * @return FormatterInterface + */ + public function getFormatter(); +} diff --git a/vendor_namespaced/monolog/monolog/src/Monolog/Handler/NullHandler.php b/vendor_namespaced/monolog/monolog/src/Monolog/Handler/NullHandler.php new file mode 100644 index 0000000..3d394ac --- /dev/null +++ b/vendor_namespaced/monolog/monolog/src/Monolog/Handler/NullHandler.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace WPDiscourse\Monolog\Handler; + +use WPDiscourse\Monolog\Logger; +/** + * Blackhole + * + * Any record it can handle will be thrown away. This can be used + * to put on top of an existing stack to override it temporarily. + * + * @author Jordi Boggiano + */ +class NullHandler extends \WPDiscourse\Monolog\Handler\AbstractHandler +{ + /** + * @param int $level The minimum logging level at which this handler will be triggered + */ + public function __construct($level = \WPDiscourse\Monolog\Logger::DEBUG) + { + parent::__construct($level, \false); + } + /** + * {@inheritdoc} + */ + public function handle(array $record) + { + if ($record['level'] < $this->level) { + return \false; + } + return \true; + } +} diff --git a/vendor_namespaced/monolog/monolog/src/Monolog/Handler/StreamHandler.php b/vendor_namespaced/monolog/monolog/src/Monolog/Handler/StreamHandler.php new file mode 100644 index 0000000..33362e0 --- /dev/null +++ b/vendor_namespaced/monolog/monolog/src/Monolog/Handler/StreamHandler.php @@ -0,0 +1,160 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace WPDiscourse\Monolog\Handler; + +use WPDiscourse\Monolog\Logger; +use WPDiscourse\Monolog\Utils; +/** + * Stores to any stream resource + * + * Can be used to store into php://stderr, remote and local files, etc. + * + * @author Jordi Boggiano + */ +class StreamHandler extends \WPDiscourse\Monolog\Handler\AbstractProcessingHandler +{ + protected $stream; + protected $url; + private $errorMessage; + protected $filePermission; + protected $useLocking; + private $dirCreated; + /** + * @param resource|string $stream + * @param int $level The minimum logging level at which this handler will be triggered + * @param bool $bubble Whether the messages that are handled can bubble up the stack or not + * @param int|null $filePermission Optional file permissions (default (0644) are only for owner read/write) + * @param bool $useLocking Try to lock log file before doing any writes + * + * @throws \Exception If a missing directory is not buildable + * @throws \InvalidArgumentException If stream is not a resource or string + */ + public function __construct($stream, $level = \WPDiscourse\Monolog\Logger::DEBUG, $bubble = \true, $filePermission = null, $useLocking = \false) + { + parent::__construct($level, $bubble); + if (\is_resource($stream)) { + $this->stream = $stream; + } elseif (\is_string($stream)) { + $this->url = \WPDiscourse\Monolog\Utils::canonicalizePath($stream); + } else { + throw new \InvalidArgumentException('A stream must either be a resource or a string.'); + } + $this->filePermission = $filePermission; + $this->useLocking = $useLocking; + } + /** + * {@inheritdoc} + */ + public function close() + { + if ($this->url && \is_resource($this->stream)) { + \fclose($this->stream); + } + $this->stream = null; + $this->dirCreated = null; + } + /** + * Return the currently active stream if it is open + * + * @return resource|null + */ + public function getStream() + { + return $this->stream; + } + /** + * Return the stream URL if it was configured with a URL and not an active resource + * + * @return string|null + */ + public function getUrl() + { + return $this->url; + } + /** + * {@inheritdoc} + */ + protected function write(array $record) + { + if (!\is_resource($this->stream)) { + if (null === $this->url || '' === $this->url) { + throw new \LogicException('Missing stream url, the stream can not be opened. This may be caused by a premature call to close().'); + } + $this->createDir(); + $this->errorMessage = null; + \set_error_handler(array($this, 'customErrorHandler')); + $this->stream = \fopen($this->url, 'a'); + if ($this->filePermission !== null) { + @\chmod($this->url, $this->filePermission); + } + \restore_error_handler(); + if (!\is_resource($this->stream)) { + $this->stream = null; + throw new \UnexpectedValueException(\sprintf('The stream or file "%s" could not be opened in append mode: ' . $this->errorMessage, $this->url)); + } + } + if ($this->useLocking) { + // ignoring errors here, there's not much we can do about them + \flock($this->stream, \LOCK_EX); + } + $this->streamWrite($this->stream, $record); + if ($this->useLocking) { + \flock($this->stream, \LOCK_UN); + } + } + /** + * Write to stream + * @param resource $stream + * @param array $record + */ + protected function streamWrite($stream, array $record) + { + \fwrite($stream, (string) $record['formatted']); + } + private function customErrorHandler($code, $msg) + { + $this->errorMessage = \preg_replace('{^(fopen|mkdir)\\(.*?\\): }', '', $msg); + } + /** + * @param string $stream + * + * @return null|string + */ + private function getDirFromStream($stream) + { + $pos = \strpos($stream, '://'); + if ($pos === \false) { + return \dirname($stream); + } + if ('file://' === \substr($stream, 0, 7)) { + return \dirname(\substr($stream, 7)); + } + return null; + } + private function createDir() + { + // Do not try to create dir if it has already been tried. + if ($this->dirCreated) { + return; + } + $dir = $this->getDirFromStream($this->url); + if (null !== $dir && !\is_dir($dir)) { + $this->errorMessage = null; + \set_error_handler(array($this, 'customErrorHandler')); + $status = \mkdir($dir, 0777, \true); + \restore_error_handler(); + if (\false === $status && !\is_dir($dir)) { + throw new \UnexpectedValueException(\sprintf('There is no existing directory at "%s" and its not buildable: ' . $this->errorMessage, $dir)); + } + } + $this->dirCreated = \true; + } +} diff --git a/vendor_namespaced/monolog/monolog/src/Monolog/Logger.php b/vendor_namespaced/monolog/monolog/src/Monolog/Logger.php new file mode 100644 index 0000000..4cdcf4f --- /dev/null +++ b/vendor_namespaced/monolog/monolog/src/Monolog/Logger.php @@ -0,0 +1,692 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace WPDiscourse\Monolog; + +use WPDiscourse\Monolog\Handler\HandlerInterface; +use WPDiscourse\Monolog\Handler\StreamHandler; +use WPDiscourse\Psr\Log\LoggerInterface; +use WPDiscourse\Psr\Log\InvalidArgumentException; +use Exception; +/** + * Monolog log channel + * + * It contains a stack of Handlers and a stack of Processors, + * and uses them to store records that are added to it. + * + * @author Jordi Boggiano + */ +class Logger implements \WPDiscourse\Psr\Log\LoggerInterface, \WPDiscourse\Monolog\ResettableInterface +{ + /** + * Detailed debug information + */ + const DEBUG = 100; + /** + * Interesting events + * + * Examples: User logs in, SQL logs. + */ + const INFO = 200; + /** + * Uncommon events + */ + const NOTICE = 250; + /** + * Exceptional occurrences that are not errors + * + * Examples: Use of deprecated APIs, poor use of an API, + * undesirable things that are not necessarily wrong. + */ + const WARNING = 300; + /** + * Runtime errors + */ + const ERROR = 400; + /** + * Critical conditions + * + * Example: Application component unavailable, unexpected exception. + */ + const CRITICAL = 500; + /** + * Action must be taken immediately + * + * Example: Entire website down, database unavailable, etc. + * This should trigger the SMS alerts and wake you up. + */ + const ALERT = 550; + /** + * Urgent alert. + */ + const EMERGENCY = 600; + /** + * Monolog API version + * + * This is only bumped when API breaks are done and should + * follow the major version of the library + * + * @var int + */ + const API = 1; + /** + * Logging levels from syslog protocol defined in RFC 5424 + * + * @var array $levels Logging levels + */ + protected static $levels = array(self::DEBUG => 'DEBUG', self::INFO => 'INFO', self::NOTICE => 'NOTICE', self::WARNING => 'WARNING', self::ERROR => 'ERROR', self::CRITICAL => 'CRITICAL', self::ALERT => 'ALERT', self::EMERGENCY => 'EMERGENCY'); + /** + * @var \DateTimeZone + */ + protected static $timezone; + /** + * @var string + */ + protected $name; + /** + * The handler stack + * + * @var HandlerInterface[] + */ + protected $handlers; + /** + * Processors that will process all log records + * + * To process records of a single handler instead, add the processor on that specific handler + * + * @var callable[] + */ + protected $processors; + /** + * @var bool + */ + protected $microsecondTimestamps = \true; + /** + * @var callable + */ + protected $exceptionHandler; + /** + * @param string $name The logging channel + * @param HandlerInterface[] $handlers Optional stack of handlers, the first one in the array is called first, etc. + * @param callable[] $processors Optional array of processors + */ + public function __construct($name, array $handlers = array(), array $processors = array()) + { + $this->name = $name; + $this->setHandlers($handlers); + $this->processors = $processors; + } + /** + * @return string + */ + public function getName() + { + return $this->name; + } + /** + * Return a new cloned instance with the name changed + * + * @return static + */ + public function withName($name) + { + $new = clone $this; + $new->name = $name; + return $new; + } + /** + * Pushes a handler on to the stack. + * + * @param HandlerInterface $handler + * @return $this + */ + public function pushHandler(\WPDiscourse\Monolog\Handler\HandlerInterface $handler) + { + \array_unshift($this->handlers, $handler); + return $this; + } + /** + * Pops a handler from the stack + * + * @return HandlerInterface + */ + public function popHandler() + { + if (!$this->handlers) { + throw new \LogicException('You tried to pop from an empty handler stack.'); + } + return \array_shift($this->handlers); + } + /** + * Set handlers, replacing all existing ones. + * + * If a map is passed, keys will be ignored. + * + * @param HandlerInterface[] $handlers + * @return $this + */ + public function setHandlers(array $handlers) + { + $this->handlers = array(); + foreach (\array_reverse($handlers) as $handler) { + $this->pushHandler($handler); + } + return $this; + } + /** + * @return HandlerInterface[] + */ + public function getHandlers() + { + return $this->handlers; + } + /** + * Adds a processor on to the stack. + * + * @param callable $callback + * @return $this + */ + public function pushProcessor($callback) + { + if (!\is_callable($callback)) { + throw new \InvalidArgumentException('Processors must be valid callables (callback or object with an __invoke method), ' . \var_export($callback, \true) . ' given'); + } + \array_unshift($this->processors, $callback); + return $this; + } + /** + * Removes the processor on top of the stack and returns it. + * + * @return callable + */ + public function popProcessor() + { + if (!$this->processors) { + throw new \LogicException('You tried to pop from an empty processor stack.'); + } + return \array_shift($this->processors); + } + /** + * @return callable[] + */ + public function getProcessors() + { + return $this->processors; + } + /** + * Control the use of microsecond resolution timestamps in the 'datetime' + * member of new records. + * + * Generating microsecond resolution timestamps by calling + * microtime(true), formatting the result via sprintf() and then parsing + * the resulting string via \DateTime::createFromFormat() can incur + * a measurable runtime overhead vs simple usage of DateTime to capture + * a second resolution timestamp in systems which generate a large number + * of log events. + * + * @param bool $micro True to use microtime() to create timestamps + */ + public function useMicrosecondTimestamps($micro) + { + $this->microsecondTimestamps = (bool) $micro; + } + /** + * Adds a log record. + * + * @param int $level The logging level + * @param string $message The log message + * @param array $context The log context + * @return bool Whether the record has been processed + */ + public function addRecord($level, $message, array $context = array()) + { + if (!$this->handlers) { + $this->pushHandler(new \WPDiscourse\Monolog\Handler\StreamHandler('php://stderr', static::DEBUG)); + } + $levelName = static::getLevelName($level); + // check if any handler will handle this message so we can return early and save cycles + $handlerKey = null; + \reset($this->handlers); + while ($handler = \current($this->handlers)) { + if ($handler->isHandling(array('level' => $level))) { + $handlerKey = \key($this->handlers); + break; + } + \next($this->handlers); + } + if (null === $handlerKey) { + return \false; + } + if (!static::$timezone) { + static::$timezone = new \DateTimeZone(\date_default_timezone_get() ?: 'UTC'); + } + // php7.1+ always has microseconds enabled, so we do not need this hack + if ($this->microsecondTimestamps && \PHP_VERSION_ID < 70100) { + $ts = \DateTime::createFromFormat('U.u', \sprintf('%.6F', \microtime(\true)), static::$timezone); + } else { + $ts = new \DateTime(null, static::$timezone); + } + $ts->setTimezone(static::$timezone); + $record = array('message' => (string) $message, 'context' => $context, 'level' => $level, 'level_name' => $levelName, 'channel' => $this->name, 'datetime' => $ts, 'extra' => array()); + try { + foreach ($this->processors as $processor) { + $record = \call_user_func($processor, $record); + } + while ($handler = \current($this->handlers)) { + if (\true === $handler->handle($record)) { + break; + } + \next($this->handlers); + } + } catch (\Exception $e) { + $this->handleException($e, $record); + } + return \true; + } + /** + * Ends a log cycle and frees all resources used by handlers. + * + * Closing a Handler means flushing all buffers and freeing any open resources/handles. + * Handlers that have been closed should be able to accept log records again and re-open + * themselves on demand, but this may not always be possible depending on implementation. + * + * This is useful at the end of a request and will be called automatically on every handler + * when they get destructed. + */ + public function close() + { + foreach ($this->handlers as $handler) { + if (\method_exists($handler, 'close')) { + $handler->close(); + } + } + } + /** + * Ends a log cycle and resets all handlers and processors to their initial state. + * + * Resetting a Handler or a Processor means flushing/cleaning all buffers, resetting internal + * state, and getting it back to a state in which it can receive log records again. + * + * This is useful in case you want to avoid logs leaking between two requests or jobs when you + * have a long running process like a worker or an application server serving multiple requests + * in one process. + */ + public function reset() + { + foreach ($this->handlers as $handler) { + if ($handler instanceof \WPDiscourse\Monolog\ResettableInterface) { + $handler->reset(); + } + } + foreach ($this->processors as $processor) { + if ($processor instanceof \WPDiscourse\Monolog\ResettableInterface) { + $processor->reset(); + } + } + } + /** + * Adds a log record at the DEBUG level. + * + * @param string $message The log message + * @param array $context The log context + * @return bool Whether the record has been processed + */ + public function addDebug($message, array $context = array()) + { + return $this->addRecord(static::DEBUG, $message, $context); + } + /** + * Adds a log record at the INFO level. + * + * @param string $message The log message + * @param array $context The log context + * @return bool Whether the record has been processed + */ + public function addInfo($message, array $context = array()) + { + return $this->addRecord(static::INFO, $message, $context); + } + /** + * Adds a log record at the NOTICE level. + * + * @param string $message The log message + * @param array $context The log context + * @return bool Whether the record has been processed + */ + public function addNotice($message, array $context = array()) + { + return $this->addRecord(static::NOTICE, $message, $context); + } + /** + * Adds a log record at the WARNING level. + * + * @param string $message The log message + * @param array $context The log context + * @return bool Whether the record has been processed + */ + public function addWarning($message, array $context = array()) + { + return $this->addRecord(static::WARNING, $message, $context); + } + /** + * Adds a log record at the ERROR level. + * + * @param string $message The log message + * @param array $context The log context + * @return bool Whether the record has been processed + */ + public function addError($message, array $context = array()) + { + return $this->addRecord(static::ERROR, $message, $context); + } + /** + * Adds a log record at the CRITICAL level. + * + * @param string $message The log message + * @param array $context The log context + * @return bool Whether the record has been processed + */ + public function addCritical($message, array $context = array()) + { + return $this->addRecord(static::CRITICAL, $message, $context); + } + /** + * Adds a log record at the ALERT level. + * + * @param string $message The log message + * @param array $context The log context + * @return bool Whether the record has been processed + */ + public function addAlert($message, array $context = array()) + { + return $this->addRecord(static::ALERT, $message, $context); + } + /** + * Adds a log record at the EMERGENCY level. + * + * @param string $message The log message + * @param array $context The log context + * @return bool Whether the record has been processed + */ + public function addEmergency($message, array $context = array()) + { + return $this->addRecord(static::EMERGENCY, $message, $context); + } + /** + * Gets all supported logging levels. + * + * @return array Assoc array with human-readable level names => level codes. + */ + public static function getLevels() + { + return \array_flip(static::$levels); + } + /** + * Gets the name of the logging level. + * + * @param int $level + * @return string + */ + public static function getLevelName($level) + { + if (!isset(static::$levels[$level])) { + throw new \WPDiscourse\Psr\Log\InvalidArgumentException('Level "' . $level . '" is not defined, use one of: ' . \implode(', ', \array_keys(static::$levels))); + } + return static::$levels[$level]; + } + /** + * Converts PSR-3 levels to Monolog ones if necessary + * + * @param string|int $level Level number (monolog) or name (PSR-3) + * @return int + */ + public static function toMonologLevel($level) + { + if (\is_string($level)) { + // Contains chars of all log levels and avoids using strtoupper() which may have + // strange results depending on locale (for example, "i" will become "İ") + $upper = \strtr($level, 'abcdefgilmnortuwy', 'ABCDEFGILMNORTUWY'); + if (\defined(__CLASS__ . '::' . $upper)) { + return \constant(__CLASS__ . '::' . $upper); + } + } + return $level; + } + /** + * Checks whether the Logger has a handler that listens on the given level + * + * @param int $level + * @return bool + */ + public function isHandling($level) + { + $record = array('level' => $level); + foreach ($this->handlers as $handler) { + if ($handler->isHandling($record)) { + return \true; + } + } + return \false; + } + /** + * Set a custom exception handler + * + * @param callable $callback + * @return $this + */ + public function setExceptionHandler($callback) + { + if (!\is_callable($callback)) { + throw new \InvalidArgumentException('Exception handler must be valid callable (callback or object with an __invoke method), ' . \var_export($callback, \true) . ' given'); + } + $this->exceptionHandler = $callback; + return $this; + } + /** + * @return callable + */ + public function getExceptionHandler() + { + return $this->exceptionHandler; + } + /** + * Delegates exception management to the custom exception handler, + * or throws the exception if no custom handler is set. + */ + protected function handleException(\Exception $e, array $record) + { + if (!$this->exceptionHandler) { + throw $e; + } + \call_user_func($this->exceptionHandler, $e, $record); + } + /** + * Adds a log record at an arbitrary level. + * + * This method allows for compatibility with common interfaces. + * + * @param mixed $level The log level + * @param string $message The log message + * @param array $context The log context + * @return bool Whether the record has been processed + */ + public function log($level, $message, array $context = array()) + { + $level = static::toMonologLevel($level); + return $this->addRecord($level, $message, $context); + } + /** + * Adds a log record at the DEBUG level. + * + * This method allows for compatibility with common interfaces. + * + * @param string $message The log message + * @param array $context The log context + * @return bool Whether the record has been processed + */ + public function debug($message, array $context = array()) + { + return $this->addRecord(static::DEBUG, $message, $context); + } + /** + * Adds a log record at the INFO level. + * + * This method allows for compatibility with common interfaces. + * + * @param string $message The log message + * @param array $context The log context + * @return bool Whether the record has been processed + */ + public function info($message, array $context = array()) + { + return $this->addRecord(static::INFO, $message, $context); + } + /** + * Adds a log record at the NOTICE level. + * + * This method allows for compatibility with common interfaces. + * + * @param string $message The log message + * @param array $context The log context + * @return bool Whether the record has been processed + */ + public function notice($message, array $context = array()) + { + return $this->addRecord(static::NOTICE, $message, $context); + } + /** + * Adds a log record at the WARNING level. + * + * This method allows for compatibility with common interfaces. + * + * @param string $message The log message + * @param array $context The log context + * @return bool Whether the record has been processed + */ + public function warn($message, array $context = array()) + { + return $this->addRecord(static::WARNING, $message, $context); + } + /** + * Adds a log record at the WARNING level. + * + * This method allows for compatibility with common interfaces. + * + * @param string $message The log message + * @param array $context The log context + * @return bool Whether the record has been processed + */ + public function warning($message, array $context = array()) + { + return $this->addRecord(static::WARNING, $message, $context); + } + /** + * Adds a log record at the ERROR level. + * + * This method allows for compatibility with common interfaces. + * + * @param string $message The log message + * @param array $context The log context + * @return bool Whether the record has been processed + */ + public function err($message, array $context = array()) + { + return $this->addRecord(static::ERROR, $message, $context); + } + /** + * Adds a log record at the ERROR level. + * + * This method allows for compatibility with common interfaces. + * + * @param string $message The log message + * @param array $context The log context + * @return bool Whether the record has been processed + */ + public function error($message, array $context = array()) + { + return $this->addRecord(static::ERROR, $message, $context); + } + /** + * Adds a log record at the CRITICAL level. + * + * This method allows for compatibility with common interfaces. + * + * @param string $message The log message + * @param array $context The log context + * @return bool Whether the record has been processed + */ + public function crit($message, array $context = array()) + { + return $this->addRecord(static::CRITICAL, $message, $context); + } + /** + * Adds a log record at the CRITICAL level. + * + * This method allows for compatibility with common interfaces. + * + * @param string $message The log message + * @param array $context The log context + * @return bool Whether the record has been processed + */ + public function critical($message, array $context = array()) + { + return $this->addRecord(static::CRITICAL, $message, $context); + } + /** + * Adds a log record at the ALERT level. + * + * This method allows for compatibility with common interfaces. + * + * @param string $message The log message + * @param array $context The log context + * @return bool Whether the record has been processed + */ + public function alert($message, array $context = array()) + { + return $this->addRecord(static::ALERT, $message, $context); + } + /** + * Adds a log record at the EMERGENCY level. + * + * This method allows for compatibility with common interfaces. + * + * @param string $message The log message + * @param array $context The log context + * @return bool Whether the record has been processed + */ + public function emerg($message, array $context = array()) + { + return $this->addRecord(static::EMERGENCY, $message, $context); + } + /** + * Adds a log record at the EMERGENCY level. + * + * This method allows for compatibility with common interfaces. + * + * @param string $message The log message + * @param array $context The log context + * @return bool Whether the record has been processed + */ + public function emergency($message, array $context = array()) + { + return $this->addRecord(static::EMERGENCY, $message, $context); + } + /** + * Set the timezone to be used for the timestamp of log records. + * + * This is stored globally for all Logger instances + * + * @param \DateTimeZone $tz Timezone object + */ + public static function setTimezone(\DateTimeZone $tz) + { + self::$timezone = $tz; + } +} diff --git a/vendor_namespaced/monolog/monolog/src/Monolog/ResettableInterface.php b/vendor_namespaced/monolog/monolog/src/Monolog/ResettableInterface.php new file mode 100644 index 0000000..590edda --- /dev/null +++ b/vendor_namespaced/monolog/monolog/src/Monolog/ResettableInterface.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace WPDiscourse\Monolog; + +/** + * Handler or Processor implementing this interface will be reset when Logger::reset() is called. + * + * Resetting ends a log cycle gets them back to their initial state. + * + * Resetting a Handler or a Processor means flushing/cleaning all buffers, resetting internal + * state, and getting it back to a state in which it can receive log records again. + * + * This is useful in case you want to avoid logs leaking between two requests or jobs when you + * have a long running process like a worker or an application server serving multiple requests + * in one process. + * + * @author Grégoire Pineau + */ +interface ResettableInterface +{ + public function reset(); +} diff --git a/vendor_namespaced/monolog/monolog/src/Monolog/Utils.php b/vendor_namespaced/monolog/monolog/src/Monolog/Utils.php new file mode 100644 index 0000000..15fe677 --- /dev/null +++ b/vendor_namespaced/monolog/monolog/src/Monolog/Utils.php @@ -0,0 +1,162 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace WPDiscourse\Monolog; + +class Utils +{ + /** + * @internal + */ + public static function getClass($object) + { + $class = \get_class($object); + return 'c' === $class[0] && 0 === \strpos($class, "class@anonymous\0") ? \get_parent_class($class) . '@anonymous' : $class; + } + /** + * Makes sure if a relative path is passed in it is turned into an absolute path + * + * @param string $streamUrl stream URL or path without protocol + * + * @return string + */ + public static function canonicalizePath($streamUrl) + { + $prefix = ''; + if ('file://' === \substr($streamUrl, 0, 7)) { + $streamUrl = \substr($streamUrl, 7); + $prefix = 'file://'; + } + // other type of stream, not supported + if (\false !== \strpos($streamUrl, '://')) { + return $streamUrl; + } + // already absolute + if (\substr($streamUrl, 0, 1) === '/' || \substr($streamUrl, 1, 1) === ':' || \substr($streamUrl, 0, 2) === '\\\\') { + return $prefix . $streamUrl; + } + $streamUrl = \getcwd() . '/' . $streamUrl; + return $prefix . $streamUrl; + } + /** + * Return the JSON representation of a value + * + * @param mixed $data + * @param int $encodeFlags flags to pass to json encode, defaults to JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE + * @param bool $ignoreErrors whether to ignore encoding errors or to throw on error, when ignored and the encoding fails, "null" is returned which is valid json for null + * @throws \RuntimeException if encoding fails and errors are not ignored + * @return string + */ + public static function jsonEncode($data, $encodeFlags = null, $ignoreErrors = \false) + { + if (null === $encodeFlags && \version_compare(\PHP_VERSION, '5.4.0', '>=')) { + $encodeFlags = \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE; + } + if ($ignoreErrors) { + $json = @\json_encode($data, $encodeFlags); + if (\false === $json) { + return 'null'; + } + return $json; + } + $json = \json_encode($data, $encodeFlags); + if (\false === $json) { + $json = self::handleJsonError(\json_last_error(), $data); + } + return $json; + } + /** + * Handle a json_encode failure. + * + * If the failure is due to invalid string encoding, try to clean the + * input and encode again. If the second encoding attempt fails, the + * inital error is not encoding related or the input can't be cleaned then + * raise a descriptive exception. + * + * @param int $code return code of json_last_error function + * @param mixed $data data that was meant to be encoded + * @param int $encodeFlags flags to pass to json encode, defaults to JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE + * @throws \RuntimeException if failure can't be corrected + * @return string JSON encoded data after error correction + */ + public static function handleJsonError($code, $data, $encodeFlags = null) + { + if ($code !== \JSON_ERROR_UTF8) { + self::throwEncodeError($code, $data); + } + if (\is_string($data)) { + self::detectAndCleanUtf8($data); + } elseif (\is_array($data)) { + \array_walk_recursive($data, array('Monolog\\Utils', 'detectAndCleanUtf8')); + } else { + self::throwEncodeError($code, $data); + } + if (null === $encodeFlags && \version_compare(\PHP_VERSION, '5.4.0', '>=')) { + $encodeFlags = \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE; + } + $json = \json_encode($data, $encodeFlags); + if ($json === \false) { + self::throwEncodeError(\json_last_error(), $data); + } + return $json; + } + /** + * Throws an exception according to a given code with a customized message + * + * @param int $code return code of json_last_error function + * @param mixed $data data that was meant to be encoded + * @throws \RuntimeException + */ + private static function throwEncodeError($code, $data) + { + switch ($code) { + case \JSON_ERROR_DEPTH: + $msg = 'Maximum stack depth exceeded'; + break; + case \JSON_ERROR_STATE_MISMATCH: + $msg = 'Underflow or the modes mismatch'; + break; + case \JSON_ERROR_CTRL_CHAR: + $msg = 'Unexpected control character found'; + break; + case \JSON_ERROR_UTF8: + $msg = 'Malformed UTF-8 characters, possibly incorrectly encoded'; + break; + default: + $msg = 'Unknown error'; + } + throw new \RuntimeException('JSON encoding failed: ' . $msg . '. Encoding: ' . \var_export($data, \true)); + } + /** + * Detect invalid UTF-8 string characters and convert to valid UTF-8. + * + * Valid UTF-8 input will be left unmodified, but strings containing + * invalid UTF-8 codepoints will be reencoded as UTF-8 with an assumed + * original encoding of ISO-8859-15. This conversion may result in + * incorrect output if the actual encoding was not ISO-8859-15, but it + * will be clean UTF-8 output and will not rely on expensive and fragile + * detection algorithms. + * + * Function converts the input in place in the passed variable so that it + * can be used as a callback for array_walk_recursive. + * + * @param mixed $data Input to check and convert if needed, passed by ref + * @private + */ + public static function detectAndCleanUtf8(&$data) + { + if (\is_string($data) && !\preg_match('//u', $data)) { + $data = \preg_replace_callback('/[\\x80-\\xFF]+/', function ($m) { + return \utf8_encode($m[0]); + }, $data); + $data = \str_replace(array('¤', '¦', '¨', '´', '¸', '¼', '½', '¾'), array('€', 'Š', 'š', 'Ž', 'ž', 'Œ', 'œ', 'Ÿ'), $data); + } + } +} diff --git a/vendor_namespaced/psr/log/LICENSE b/vendor_namespaced/psr/log/LICENSE new file mode 100644 index 0000000..474c952 --- /dev/null +++ b/vendor_namespaced/psr/log/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2012 PHP Framework Interoperability Group + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor_namespaced/psr/log/Psr/Log/AbstractLogger.php b/vendor_namespaced/psr/log/Psr/Log/AbstractLogger.php new file mode 100644 index 0000000..f7f0b6b --- /dev/null +++ b/vendor_namespaced/psr/log/Psr/Log/AbstractLogger.php @@ -0,0 +1,121 @@ +log(\WPDiscourse\Psr\Log\LogLevel::EMERGENCY, $message, $context); + } + /** + * Action must be taken immediately. + * + * Example: Entire website down, database unavailable, etc. This should + * trigger the SMS alerts and wake you up. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function alert($message, array $context = array()) + { + $this->log(\WPDiscourse\Psr\Log\LogLevel::ALERT, $message, $context); + } + /** + * Critical conditions. + * + * Example: Application component unavailable, unexpected exception. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function critical($message, array $context = array()) + { + $this->log(\WPDiscourse\Psr\Log\LogLevel::CRITICAL, $message, $context); + } + /** + * Runtime errors that do not require immediate action but should typically + * be logged and monitored. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function error($message, array $context = array()) + { + $this->log(\WPDiscourse\Psr\Log\LogLevel::ERROR, $message, $context); + } + /** + * Exceptional occurrences that are not errors. + * + * Example: Use of deprecated APIs, poor use of an API, undesirable things + * that are not necessarily wrong. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function warning($message, array $context = array()) + { + $this->log(\WPDiscourse\Psr\Log\LogLevel::WARNING, $message, $context); + } + /** + * Normal but significant events. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function notice($message, array $context = array()) + { + $this->log(\WPDiscourse\Psr\Log\LogLevel::NOTICE, $message, $context); + } + /** + * Interesting events. + * + * Example: User logs in, SQL logs. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function info($message, array $context = array()) + { + $this->log(\WPDiscourse\Psr\Log\LogLevel::INFO, $message, $context); + } + /** + * Detailed debug information. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function debug($message, array $context = array()) + { + $this->log(\WPDiscourse\Psr\Log\LogLevel::DEBUG, $message, $context); + } +} diff --git a/vendor_namespaced/psr/log/Psr/Log/InvalidArgumentException.php b/vendor_namespaced/psr/log/Psr/Log/InvalidArgumentException.php new file mode 100644 index 0000000..4fb9142 --- /dev/null +++ b/vendor_namespaced/psr/log/Psr/Log/InvalidArgumentException.php @@ -0,0 +1,7 @@ +logger = $logger; + } +} diff --git a/vendor_namespaced/psr/log/Psr/Log/LoggerInterface.php b/vendor_namespaced/psr/log/Psr/Log/LoggerInterface.php new file mode 100644 index 0000000..0d5ad97 --- /dev/null +++ b/vendor_namespaced/psr/log/Psr/Log/LoggerInterface.php @@ -0,0 +1,117 @@ +log(\WPDiscourse\Psr\Log\LogLevel::EMERGENCY, $message, $context); + } + /** + * Action must be taken immediately. + * + * Example: Entire website down, database unavailable, etc. This should + * trigger the SMS alerts and wake you up. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function alert($message, array $context = array()) + { + $this->log(\WPDiscourse\Psr\Log\LogLevel::ALERT, $message, $context); + } + /** + * Critical conditions. + * + * Example: Application component unavailable, unexpected exception. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function critical($message, array $context = array()) + { + $this->log(\WPDiscourse\Psr\Log\LogLevel::CRITICAL, $message, $context); + } + /** + * Runtime errors that do not require immediate action but should typically + * be logged and monitored. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function error($message, array $context = array()) + { + $this->log(\WPDiscourse\Psr\Log\LogLevel::ERROR, $message, $context); + } + /** + * Exceptional occurrences that are not errors. + * + * Example: Use of deprecated APIs, poor use of an API, undesirable things + * that are not necessarily wrong. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function warning($message, array $context = array()) + { + $this->log(\WPDiscourse\Psr\Log\LogLevel::WARNING, $message, $context); + } + /** + * Normal but significant events. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function notice($message, array $context = array()) + { + $this->log(\WPDiscourse\Psr\Log\LogLevel::NOTICE, $message, $context); + } + /** + * Interesting events. + * + * Example: User logs in, SQL logs. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function info($message, array $context = array()) + { + $this->log(\WPDiscourse\Psr\Log\LogLevel::INFO, $message, $context); + } + /** + * Detailed debug information. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function debug($message, array $context = array()) + { + $this->log(\WPDiscourse\Psr\Log\LogLevel::DEBUG, $message, $context); + } + /** + * Logs with an arbitrary level. + * + * @param mixed $level + * @param string $message + * @param array $context + * + * @return void + * + * @throws \Psr\Log\InvalidArgumentException + */ + public abstract function log($level, $message, array $context = array()); +} diff --git a/vendor_namespaced/psr/log/Psr/Log/NullLogger.php b/vendor_namespaced/psr/log/Psr/Log/NullLogger.php new file mode 100644 index 0000000..5d18323 --- /dev/null +++ b/vendor_namespaced/psr/log/Psr/Log/NullLogger.php @@ -0,0 +1,30 @@ +logger) { }` + * blocks. + */ +class NullLogger extends \WPDiscourse\Psr\Log\AbstractLogger +{ + /** + * Logs with an arbitrary level. + * + * @param mixed $level + * @param string $message + * @param array $context + * + * @return void + * + * @throws \Psr\Log\InvalidArgumentException + */ + public function log($level, $message, array $context = array()) + { + // noop + } +} diff --git a/wp-discourse.php b/wp-discourse.php index eaa4537..97abd58 100644 --- a/wp-discourse.php +++ b/wp-discourse.php @@ -2,7 +2,7 @@ /** * Plugin Name: WP-Discourse * Description: Use Discourse as a community engine for your WordPress blog - * Version: 2.2.3 + * Version: 2.2.4 * Author: Discourse * Text Domain: wp-discourse * Domain Path: /languages @@ -34,7 +34,7 @@ define( 'WPDISCOURSE_PATH', plugin_dir_path( __FILE__ ) ); define( 'WPDISCOURSE_URL', plugins_url( '', __FILE__ ) ); define( 'MIN_WP_VERSION', '4.7' ); define( 'MIN_PHP_VERSION', '5.6.0' ); -define( 'WPDISCOURSE_VERSION', '2.2.3' ); +define( 'WPDISCOURSE_VERSION', '2.2.4' ); require_once WPDISCOURSE_PATH . 'lib/plugin-utilities.php'; require_once WPDISCOURSE_PATH . 'lib/template-functions.php'; @@ -57,6 +57,8 @@ require_once WPDISCOURSE_PATH . 'lib/sso-client/query-redirect.php'; require_once WPDISCOURSE_PATH . 'lib/shortcodes/sso-client.php'; require_once WPDISCOURSE_PATH . 'templates/html-templates.php'; require_once WPDISCOURSE_PATH . 'admin/discourse-sidebar/discourse-sidebar.php'; +require_once WPDISCOURSE_PATH . 'vendor/autoload.php'; +require_once WPDISCOURSE_PATH . 'lib/logs/logger.php'; require_once WPDISCOURSE_PATH . 'admin/admin.php'; new WPDiscourse\Discourse\Discourse();